Commit 93bb1f58 by Braden MacDonald Committed by E. Kolpakov

Basic UI for managing library users with limited editing functionality (shared with course code)

parent c477bab6
......@@ -26,12 +26,12 @@ from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from .component import get_component_templates, CONTAINER_TEMPATES
from student.auth import has_studio_write_access, has_studio_read_access
from student.roles import CourseCreatorRole
from student.auth import STUDIO_VIEW_USERS, STUDIO_EDIT_ROLES, get_user_permissions, has_studio_read_access, has_studio_write_access
from student.roles import CourseCreatorRole, CourseInstructorRole, CourseStaffRole, LibraryUserRole
from student import auth
from util.json_request import expect_json, JsonResponse, JsonResponseBadRequest
__all__ = ['library_handler']
__all__ = ['library_handler', 'manage_library_users']
log = logging.getLogger(__name__)
......@@ -191,3 +191,37 @@ def library_blocks_view(library, user, response_format):
'templates': CONTAINER_TEMPATES,
'lib_users_url': reverse_library_url('manage_library_users', unicode(library.location.library_key)),
})
def manage_library_users(request, library_key_string):
"""
Studio UI for editing the users within a library.
Uses the /course_team/:library_key/:user_email/ REST API to make changes.
"""
library_key = CourseKey.from_string(library_key_string)
if not isinstance(library_key, LibraryLocator):
raise Http404 # This is not a library
user_perms = get_user_permissions(request.user, library_key)
if not (user_perms & STUDIO_VIEW_USERS):
raise PermissionDenied()
library = modulestore().get_library(library_key)
if library is None:
raise Http404
# Segment all the users explicitly associated with this library, ensuring each user only has one role listed:
instructors = set(CourseInstructorRole(library_key).users_with_role())
staff = set(CourseStaffRole(library_key).users_with_role()) - instructors
users = set(LibraryUserRole(library_key).users_with_role()) - instructors - staff
all_users = instructors | staff | users
return render_to_response('manage_users_lib.html', {
'context_library': library,
'staff': staff,
'instructors': instructors,
'users': users,
'all_users': all_users,
'allow_actions': bool(user_perms & STUDIO_EDIT_ROLES),
'library_key': unicode(library_key),
'lib_users_url': reverse_library_url('manage_library_users', library_key_string),
})
......@@ -4,12 +4,14 @@ Unit tests for contentstore.views.library
More important high-level tests are in contentstore/tests/test_libraries.py
"""
from contentstore.tests.utils import AjaxEnabledTestClient, parse_json
from contentstore.utils import reverse_course_url, reverse_library_url
from contentstore.views.component import get_component_templates
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import LibraryFactory
from mock import patch
from opaque_keys.edx.locator import CourseKey, LibraryLocator
import ddt
from student.roles import LibraryUserRole
LIBRARY_REST_URL = '/library/' # URL for GET/POST requests involving libraries
......@@ -197,3 +199,27 @@ class UnitTestLibraries(ModuleStoreTestCase):
self.assertIn('problem', templates)
self.assertNotIn('discussion', templates)
self.assertNotIn('advanced', templates)
def test_manage_library_users(self):
"""
Simple test that the Library "User Access" view works.
Also tests that we can use the REST API to assign a user to a library.
"""
library = LibraryFactory.create()
extra_user, _ = self.create_non_staff_user()
manage_users_url = reverse_library_url('manage_library_users', unicode(library.location.library_key))
response = self.client.get(manage_users_url)
self.assertEqual(response.status_code, 200)
# extra_user has not been assigned to the library so should not show up in the list:
self.assertNotIn(extra_user.username, response.content)
# Now add extra_user to the library:
user_details_url = reverse_course_url('course_team_handler', library.location.library_key, kwargs={'email': extra_user.email})
edit_response = self.client.ajax_post(user_details_url, {"role": LibraryUserRole.ROLE})
self.assertIn(edit_response.status_code, (200, 204))
# Now extra_user should apear in the list:
response = self.client.get(manage_users_url)
self.assertEqual(response.status_code, 200)
self.assertIn(extra_user.username, response.content)
/*
Code for editing users and assigning roles within a library context.
*/
define(['jquery', 'underscore', 'gettext', 'js/views/feedback_prompt', 'js/views/utils/view_utils'],
function($, _, gettext, PromptView, ViewUtils) {
'use strict';
return function (libraryName, allUserEmails, tplUserURL) {
var unknownErrorMessage = gettext('Unknown'),
$createUserForm = $('#create-user-form'),
$createUserFormWrapper = $createUserForm.closest('.wrapper-create-user'),
$cancelButton;
// Our helper method that calls the RESTful API to add/remove/change user roles:
var changeRole = function(email, newRole, opts) {
var url = tplUserURL.replace('@@EMAIL@@', email);
var errMessage = opts.errMessage || gettext("There was an error changing the user's role");
var onSuccess = opts.onSuccess || function(data){ ViewUtils.reload(); };
var onError = opts.onError || function(){};
$.ajax({
url: url,
type: newRole ? 'POST' : 'DELETE',
dataType: 'json',
contentType: 'application/json',
notifyOnError: false,
data: JSON.stringify({role: newRole}),
success: onSuccess,
error: function(jqXHR, textStatus, errorThrown) {
var message, prompt;
try {
message = JSON.parse(jqXHR.responseText).error || unknownErrorMessage;
} catch (e) {
message = unknownErrorMessage;
}
prompt = new PromptView.Error({
title: errMessage,
message: message,
actions: {
primary: { text: gettext('OK'), click: function(view) { view.hide(); onError(); } }
}
});
prompt.show();
}
});
};
$createUserForm.bind('submit', function(event) {
event.preventDefault();
var email = $('#user-email-input').val().trim();
var msg;
if(!email) {
msg = new PromptView.Error({
title: gettext('A valid email address is required'),
message: gettext('You must enter a valid email address in order to add an instructor'),
actions: {
primary: {
text: gettext('Return and add email address'),
click: function(view) { view.hide(); $('#user-email-input').focus(); }
}
}
});
msg.show();
return;
}
if(_.contains(allUserEmails, email)) {
msg = new PromptView.Warning({
title: gettext('Already a library team member'),
message: _.template(
gettext("{email} is already on the “{course}” team. If you're trying to add a new member, please double-check the email address you provided."), {
email: email,
course: libraryName
}, {interpolate: /\{(.+?)\}/g}
),
actions: {
primary: {
text: gettext('Return to team listing'),
click: function(view) { view.hide(); $('#user-email-input').focus(); }
}
}
});
msg.show();
return;
}
// Use the REST API to create the user, giving them a role of "library_user" for now:
changeRole(
$('#user-email-input').val().trim(),
'library_user',
{
errMessage: gettext('Error adding user'),
onError: function() { $('#user-email-input').focus(); }
}
);
});
$cancelButton = $createUserForm.find('.action-cancel');
$cancelButton.on('click', function(event) {
event.preventDefault();
$('.create-user-button').toggleClass('is-disabled');
$createUserFormWrapper.toggleClass('is-shown');
$('#user-email-input').val('');
});
$('.create-user-button').on('click', function(event) {
event.preventDefault();
$('.create-user-button').toggleClass('is-disabled');
$createUserFormWrapper.toggleClass('is-shown');
$createUserForm.find('#user-email-input').focus();
});
$('body').on('keyup', function(event) {
if(event.which == jQuery.ui.keyCode.ESCAPE && $createUserFormWrapper.is('.is-shown')) {
$cancelButton.click();
}
});
$('.remove-user').click(function() {
var email = $(this).closest('li[data-email]').data('email'),
msg = new PromptView.Warning({
title: gettext('Are you sure?'),
message: _.template(gettext('Are you sure you want to delete {email} from the library “{library}”?'), {email: email, library: libraryName}, {interpolate: /\{(.+?)\}/g}),
actions: {
primary: {
text: gettext('Delete'),
click: function(view) {
// User the REST API to delete the user:
changeRole(email, null, { errMessage: gettext('Error removing user') });
}
},
secondary: {
text: gettext('Cancel'),
click: function(view) { view.hide(); }
}
}
});
msg.show();
});
$('.user-actions .make-instructor').click(function(event) {
event.preventDefault();
var email = $(this).closest('li[data-email]').data('email');
changeRole(email, 'instructor', {});
});
$('.user-actions .make-staff').click(function(event) {
event.preventDefault();
var email = $(this).closest('li[data-email]').data('email');
changeRole(email, 'staff', {});
});
$('.user-actions .make-user').click(function(event) {
event.preventDefault();
var email = $(this).closest('li[data-email]').data('email');
changeRole(email, 'library_user', {});
});
};
});
......@@ -116,11 +116,16 @@
&.flag-role-admin {
background: $pink;
}
&.flag-role-user {
background: $yellow-d1;
.msg-you { color: $yellow-l1; }
}
}
// ELEM: item - metadata
.item-metadata {
width: flex-grid(5, 9);
width: flex-grid(4, 9);
@include margin-right(flex-gutter());
.user-username, .user-email {
......@@ -143,7 +148,7 @@
// ELEM: item - actions
.item-actions {
width: flex-grid(4, 9);
width: flex-grid(5, 9);
position: static; // nasty reset needed due to base.scss
text-align: right;
......@@ -153,12 +158,34 @@
}
.action-role {
width: flex-grid(3, 4);
width: flex-grid(7, 8);
margin-right: flex-gutter();
.add-admin-role {
@include blue-button;
@include transition(all .15s);
@extend %t-action2;
@extend %t-strong;
display: inline-block;
padding: ($baseline/5) $baseline;
}
.remove-admin-role {
@include grey-button;
@include transition(all .15s);
@extend %t-action2;
@extend %t-strong;
display: inline-block;
padding: ($baseline/5) $baseline;
}
.notoggleforyou {
@extend %t-copy-sub1;
color: $gray-l2;
}
}
.action-delete {
width: flex-grid(1, 4);
width: flex-grid(1, 8);
// STATE: disabled
&.is-disabled {
......@@ -178,33 +205,6 @@
float: none;
color: inherit;
}
// ELEM: admin role controls
.toggle-admin-role {
&.add-admin-role {
@include blue-button;
@include transition(all .15s);
@extend %t-action2;
@extend %t-strong;
display: inline-block;
padding: ($baseline/5) $baseline;
}
&.remove-admin-role {
@include grey-button;
@include transition(all .15s);
@extend %t-action2;
@extend %t-strong;
display: inline-block;
padding: ($baseline/5) $baseline;
}
}
.notoggleforyou {
@extend %t-copy-sub1;
color: $gray-l2;
}
}
// STATE: hover
......
<%! import json %>
<%! from django.utils.translation import ugettext as _ %>
<%! from django.core.urlresolvers import reverse %>
<%inherit file="base.html" />
<%def name="online_help_token()"><% return "team" %></%def>
<%block name="title">${_("Library User Access")}</%block>
<%block name="bodyclass">is-signedin course users view-team</%block>
<%block name="content">
<div class="wrapper-mast wrapper">
<header class="mast has-actions has-subtitle">
<h1 class="page-header">
<small class="subtitle">${_("Settings")}</small>
<span class="sr">&gt; </span>${_("User Access")}
</h1>
<nav class="nav-actions">
<h3 class="sr">${_("Page Actions")}</h3>
<ul>
%if allow_actions:
<li class="nav-item">
<a href="#" class="button new-button create-user-button"><i class="icon-plus"></i> ${_("New Team Member")}</a>
</li>
%endif
</ul>
</nav>
</header>
</div>
<div class="wrapper-content wrapper">
<section class="content">
<article class="content-primary" role="main">
%if allow_actions:
<div class="wrapper-create-element animate wrapper-create-user">
<form class="form-create create-user" id="create-user-form" name="create-user-form">
<div class="wrapper-form">
<h3 class="title">${_("Grant Access to This Library")}</h3>
<fieldset class="form-fields">
<legend class="sr">${_("New Team Member Information")}</legend>
<ol class="list-input">
<li class="field text required create-user-email">
<label for="user-email-input">${_("User's Email Address")}</label>
<input id="user-email-input" class="user-email-input" name="user-email" type="text" placeholder="${_('example: username@domain.com')}" value="">
<span class="tip tip-stacked">${_("Please provide the email address of the user you'd like to add")}</span>
</li>
</ol>
</fieldset>
</div>
<div class="actions">
<button class="action action-primary" type="submit">${_("Add User")}</button>
<button class="action action-secondary action-cancel">${_("Cancel")}</button>
</div>
</form>
</div>
%endif
<ol class="user-list">
% for user in all_users:
<%
is_instructor = user in instructors
is_staff = user in staff
role_id = 'admin' if is_instructor else ('staff' if is_staff else 'user')
role_desc = _("Admin") if is_instructor else (_("Staff") if is_staff else _("User"))
%>
<li class="user-item" data-email="${user.email}">
<span class="wrapper-ui-badge">
<span class="flag flag-role flag-role-${role_id} is-hanging">
<span class="label sr">${_("Current Role:")}</span>
<span class="value">
${role_desc}
% if request.user.id == user.id:
<span class="msg-you">${_("You!")}</span>
% endif
</span>
</span>
</span>
<div class="item-metadata">
<h3 class="user-name">
<span class="user-username">${user.username}</span>
<span class="user-email">
<a class="action action-email" href="mailto:${user.email}" title="${_("send an email message to {email}").format(email=user.email)}">${user.email}</a>
</span>
</h3>
</div>
% if allow_actions:
<ul class="item-actions user-actions">
% if is_instructor:
% if len(instructors) > 1:
<li class="action action-role">
<a href="#" class="make-staff admin-role remove-admin-role">${_("Remove Admin Access")}</span></a>
</li>
% else:
<li class="action action-role">
<span class="admin-role notoggleforyou">${_("Promote another member to Admin to remove admin rights")}</span>
</li>
% endif
% elif is_staff:
<li class="action action-role">
<a href="#" class="make-instructor admin-role add-admin-role">${_("Add Admin Access")}</span></a>
<a href="#" class="make-user admin-role remove-admin-role">${_("Remove Staff Access")}</span></a>
</li>
% else:
<li class="action action-role">
<a href="#" class="make-staff admin-role add-admin-role">${_("Add Staff Access")}</span></a>
</li>
% endif
<li class="action action-delete ${"is-disabled" if request.user.id == user.id and is_instructor and len(instructors) == 1 else ""}">
<a href="#" class="delete remove-user action-icon" data-tooltip="${_("Remove this user")}"><i class="icon-trash"></i><span class="sr">${_("Delete the user, {username}").format(username=user.username)}</span></a>
</li>
</ul>
% elif request.user.id == user.id:
<ul class="item-actions user-actions">
<li class="action action-delete">
<a href="#" class="delete remove-user action-icon" data-tooltip="${_("Remove me")}"><i class="icon-trash"></i><span class="sr">${_("Remove me from this library")}</span></a>
</li>
</ul>
% endif
</li>
% endfor
</ol>
% if allow_actions and len(all_users) == 1:
<div class="notice notice-incontext notice-create has-actions">
<div class="msg">
<h3 class="title">${_('Add More Users to This Library')}</h3>
<div class="copy">
<p>${_('Adding team members makes content authoring collaborative. Users must be signed up for Studio and have an active account. ')}</p>
</div>
</div>
<ul class="list-actions">
<li class="action-item">
<a href="#" class="action action-primary button new-button create-user-button"><i class="icon-plus icon-inline"></i> ${_('Add a New User')}</a>
</li>
</ul>
</div>
%endif
</article>
<aside class="content-supplementary" role="complimentary">
<div class="bit">
<h3 class="title-3">${_("Library Access Roles")}</h3>
<p>${_("Library staff are content co-authors. They have full writing and editing privileges on all content in the library.")}</p>
<p>${_("Admins are library team members who can also add and remove other team members.")}</p>
<p>${_("Library users cannot edit content in the library, but can view the content and are able to reference or use library elements in their own courses.")}</p>
</div>
<div class="bit">
<h3 class="title-3">${_("Organization-Wide Access")}</h3>
<p>${_("Users who have been granted admin, staff, or user access at the organization level may not be listed here but will still have access to this library.")}</p>
</div>
% if allow_actions:
<div class="bit">
<h3 class="title-3">${_("Transferring Ownership")}</h3>
<p>${_("Every library must have an Admin. If you're the Admin and you want transfer ownership of the library, click Add admin access to make another user the Admin, then ask that user to remove you from the library admin list.")}</p>
</div>
% endif
</aside>
</section>
</div>
</%block>
<%block name="requirejs">
require(["js/factories/manage_users_lib"], function(ManageUsersFactory) {
ManageUsersFactory(
"${context_library.display_name_with_default | h}",
${json.dumps([user.email for user in all_users])},
"${reverse('contentstore.views.course_team_handler', kwargs={'course_key_string': library_key, 'email': '@@EMAIL@@'})}"
);
});
</%block>
......@@ -45,7 +45,7 @@
</h2>
<nav class="nav-course nav-dd ui-left">
<h2 class="sr">${_("{course_name}'s Navigation:").format(course_name=context_course.display_name_with_default)}</h2>
<h2 class="sr">${_("Navigation for {course_name}").format(course_name=context_course.display_name_with_default)}</h2>
<ol>
<li class="nav-item nav-course-courseware">
<h3 class="title"><span class="label"><span class="label-prefix sr">${_("Course")} </span>${_("Content")}</span> <i class="icon fa fa-caret-down ui-toggle-dd"></i></h3>
......@@ -132,6 +132,38 @@
</li>
</ol>
</nav>
% elif context_library:
<%
library_key = context_library.location.course_key
index_url = reverse('contentstore.views.library_handler', kwargs={'library_key_string': unicode(library_key)})
%>
<h2 class="info-course">
<span class="sr">${_("Current Library:")}</span>
<a class="course-link" href="${index_url}">
<span class="course-org">${context_library.display_org_with_default | h}</span><span class="course-number">${context_library.display_number_with_default | h}</span>
<span class="course-title" title="${context_library.display_name_with_default}">${context_library.display_name_with_default}</span>
</a>
</h2>
<nav class="nav-course nav-dd ui-left">
<h2 class="sr">${_("Navigation for {course_name}").format(course_name=context_library.display_name_with_default)}</h2>
<ol>
<li class="nav-item nav-library-settings">
<h3 class="title"><span class="label"><span class="label-prefix sr">${_("Library")} </span>${_("Settings")}</span> <i class="icon-caret-down ui-toggle-dd"></i></h3>
<div class="wrapper wrapper-nav-sub">
<div class="nav-sub">
<ul>
<li class="nav-item nav-library-settings-team">
<a href="${lib_users_url}">${_("User Access")}</a>
</li>
</ul>
</div>
</div>
</li>
</ol>
</nav>
% endif
</div>
......
......@@ -121,6 +121,8 @@ if settings.FEATURES.get('ENABLE_CONTENT_LIBRARIES'):
urlpatterns += (
url(r'^library/{}?$'.format(LIBRARY_KEY_PATTERN),
'contentstore.views.library_handler', name='library_handler'),
url(r'^library/{}/team/$'.format(LIBRARY_KEY_PATTERN),
'contentstore.views.manage_library_users', name='manage_library_users'),
)
if settings.FEATURES.get('ENABLE_EXPORT_GIT'):
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment