Commit 9a56b800 by E. Kolpakov

Unified Course Team and Library Users page. Converted both to Backbone.

Converted lettuce Course Team page tests to bok choy
parent ec6cc04b
@shard_2
Feature: CMS.Course Team
As a course author, I want to be able to add others to my team
Scenario: Admins can add other users
Given I have opened a new course in Studio
And the user "alice" exists
And I am viewing the course team settings
When I add "alice" to the course team
And "alice" logs in
Then she does see the course on her page
Scenario: Added admins cannot delete or add other users
Given I have opened a new course in Studio
And the user "bob" exists
And I am viewing the course team settings
When I add "bob" to the course team
And "bob" logs in
And he selects the new course
And he views the course team settings
Then he cannot delete users
And he cannot add users
Scenario: Admins can delete other users
Given I have opened a new course in Studio
And the user "carol" exists
And I am viewing the course team settings
When I add "carol" to the course team
And I delete "carol" from the course team
And "carol" logs in
Then she does not see the course on her page
Scenario: Admins cannot add users that do not exist
Given I have opened a new course in Studio
And I am viewing the course team settings
When I add "dennis" to the course team
Then I should see "Could not find user by email address" somewhere on the page
Scenario: Admins should be able to make other people into admins
Given I have opened a new course in Studio
And the user "emily" exists
And I am viewing the course team settings
And I add "emily" to the course team
When I make "emily" a course team admin
And "emily" logs in
And she selects the new course
And she views the course team settings
Then "emily" should be marked as an admin
And she can add users
And she can delete users
Scenario: Admins should be able to remove other admins
Given I have opened a new course in Studio
And the user "frank" exists as a course admin
And I am viewing the course team settings
When I remove admin rights from "frank"
And "frank" logs in
And he selects the new course
And he views the course team settings
Then "frank" should not be marked as an admin
And he cannot add users
And he cannot delete users
Scenario: Admins should be able to give course ownership to someone else
Given I have opened a new course in Studio
And the user "gina" exists
And I am viewing the course team settings
When I add "gina" to the course team
And I make "gina" a course team admin
And I remove admin rights from myself
And "gina" logs in
And she selects the new course
And she views the course team settings
And she deletes me from the course team
And I am logged into studio
Then I do not see the course on my page
Scenario: Admins should be able to remove their own admin rights
Given I have opened a new course in Studio
And the user "harry" exists as a course admin
And I am viewing the course team settings
Then I should be marked as an admin
And I can add users
And I can delete users
When I remove admin rights from myself
Then I should not be marked as an admin
And I cannot add users
And I cannot delete users
And I cannot make myself a course team admin
# pylint: disable=missing-docstring
# pylint: disable=redefined-outer-name
from lettuce import world, step
from nose.tools import assert_in # pylint: disable=no-name-in-module
@step(u'(I am viewing|s?he views) the course team settings$')
def view_grading_settings(_step, whom):
world.click_course_settings()
link_css = 'li.nav-course-settings-team a'
world.css_click(link_css)
@step(u'I add "([^"]*)" to the course team$')
def add_other_user(_step, name):
new_user_css = 'a.create-user-button'
world.css_click(new_user_css)
# Wait for the css animation to apply the is-shown class
shown_css = 'div.wrapper-create-user.is-shown'
world.wait_for_present(shown_css)
email_css = 'input#user-email-input'
world.css_fill(email_css, name + '@edx.org')
if world.is_firefox():
world.trigger_event(email_css)
confirm_css = 'form.create-user button.action-primary'
world.css_click(confirm_css)
@step(u'I delete "([^"]*)" from the course team$')
def delete_other_user(_step, name):
to_delete_css = '.user-item .item-actions a.remove-user[data-id="{email}"]'.format(
email="{0}{1}".format(name, '@edx.org'))
world.css_click(to_delete_css)
world.confirm_studio_prompt()
@step(u's?he deletes me from the course team$')
def other_delete_self(_step):
to_delete_css = '.user-item .item-actions a.remove-user[data-id="{email}"]'.format(
email="robot+studio@edx.org")
world.css_click(to_delete_css)
world.confirm_studio_prompt()
@step(u'I make "([^"]*)" a course team admin$')
def make_course_team_admin(_step, name):
admin_btn_css = '.user-item[data-email="{name}@edx.org"] .user-actions .add-admin-role'.format(
name=name)
world.css_click(admin_btn_css)
@step(u'I remove admin rights from ("([^"]*)"|myself)$')
def remove_course_team_admin(_step, outer_capture, name):
if outer_capture == "myself":
email = world.scenario_dict["USER"].email
else:
email = name + '@edx.org'
admin_btn_css = '.user-item[data-email="{email}"] .user-actions .remove-admin-role'.format(
email=email)
world.css_click(admin_btn_css)
@step(u'I( do not)? see the course on my page$')
@step(u's?he does( not)? see the course on (his|her) page$')
def see_course(_step, do_not_see, gender='self'):
class_css = 'h3.course-title'
if do_not_see:
assert world.is_css_not_present(class_css)
else:
all_courses = world.css_find(class_css)
all_names = [item.html for item in all_courses]
assert_in(world.scenario_dict['COURSE'].display_name, all_names)
@step(u'"([^"]*)" should( not)? be marked as an admin$')
def marked_as_admin(_step, name, not_marked_admin):
flag_css = '.user-item[data-email="{name}@edx.org"] .flag-role.flag-role-admin'.format(
name=name)
if not_marked_admin:
assert world.is_css_not_present(flag_css)
else:
assert world.is_css_present(flag_css)
@step(u'I should( not)? be marked as an admin$')
def self_marked_as_admin(_step, not_marked_admin):
return marked_as_admin(_step, "robot+studio", not_marked_admin)
@step(u'I can(not)? delete users$')
@step(u's?he can(not)? delete users$')
def can_delete_users(_step, can_not_delete):
to_delete_css = 'a.remove-user'
if can_not_delete:
assert world.is_css_not_present(to_delete_css)
else:
assert world.is_css_present(to_delete_css)
@step(u'I can(not)? add users$')
@step(u's?he can(not)? add users$')
def can_add_users(_step, can_not_add):
add_css = 'a.create-user-button'
if can_not_add:
assert world.is_css_not_present(add_css)
else:
assert world.is_css_present(add_css)
@step(u'I can(not)? make ("([^"]*)"|myself) a course team admin$')
@step(u's?he can(not)? make ("([^"]*)"|me) a course team admin$')
def can_make_course_admin(_step, can_not_make_admin, outer_capture, name):
if outer_capture == "myself":
email = world.scenario_dict["USER"].email
else:
email = name + '@edx.org'
add_button_css = '.user-item[data-email="{email}"] .add-admin-role'.format(email=email)
if can_not_make_admin:
assert world.is_css_not_present(add_button_css)
else:
assert world.is_css_present(add_button_css)
......@@ -24,6 +24,7 @@ from opaque_keys.edx.locator import LibraryLocator, LibraryUsageLocator
from xmodule.modulestore.exceptions import DuplicateCourseError
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from .user import user_with_role
from .component import get_component_templates, CONTAINER_TEMPATES
from student.auth import (
......@@ -220,14 +221,18 @@ def manage_library_users(request, library_key_string):
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
formatted_users = []
for user in instructors:
formatted_users.append(user_with_role(user, 'instructor'))
for user in staff:
formatted_users.append(user_with_role(user, 'staff'))
for user in users:
formatted_users.append(user_with_role(user, 'library_user'))
return render_to_response('manage_users_lib.html', {
'context_library': library,
'staff': staff,
'instructors': instructors,
'users': users,
'all_users': all_users,
'users': formatted_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),
......
......@@ -136,7 +136,7 @@ class ChecklistTestCase(CourseTestCase):
# Verify no side effect in the original list.
self.assertEqual(get_action_url(checklist, index), stored)
test_expansion(self.course.checklists[0], 0, 'ManageUsers', '/course_team/mitX/333/Checklists_Course/')
test_expansion(self.course.checklists[0], 0, 'ManageUsers', '/course_team/mitX/333/Checklists_Course')
test_expansion(self.course.checklists[1], 1, 'CourseOutline', '/course/mitX/333/Checklists_Course')
test_expansion(self.course.checklists[2], 0, 'http://help.edge.edx.org/', 'http://help.edge.edx.org/')
......
......@@ -61,6 +61,16 @@ def course_team_handler(request, course_key_string=None, email=None):
return HttpResponseNotFound()
def user_with_role(user, role):
""" Build user representation with attached role """
return {
'id': user.id,
'username': user.username,
'email': user.email,
'role': role
}
def _manage_users(request, course_key):
"""
This view will return all CMS users who are editors for the specified course
......@@ -71,14 +81,20 @@ def _manage_users(request, course_key):
raise PermissionDenied()
course_module = modulestore().get_course(course_key)
instructors = CourseInstructorRole(course_key).users_with_role()
instructors = set(CourseInstructorRole(course_key).users_with_role())
# the page only lists staff and assumes they're a superset of instructors. Do a union to ensure.
staff = set(CourseStaffRole(course_key).users_with_role()).union(instructors)
formatted_users = []
for user in instructors:
formatted_users.append(user_with_role(user, 'instructor'))
for user in staff - instructors:
formatted_users.append(user_with_role(user, 'staff'))
return render_to_response('manage_users.html', {
'context_course': course_module,
'staff': staff,
'instructors': instructors,
'show_transfer_ownership_hint': request.user in instructors and len(instructors) == 1,
'users': formatted_users,
'allow_actions': bool(user_perms & STUDIO_EDIT_ROLES),
})
......
/*
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) {
define(['underscore', 'gettext', 'js/views/manage_users_and_roles'],
function(_, gettext, ManageUsersAndRoles) {
'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. Recheck the email address if you want to add a new member."), {
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(); }
}
return function (containerName, users, tplUserURL, current_user_id, allow_actions) {
function updateMessages(messages) {
var local_messages = _.extend({}, messages);
local_messages.alreadyMember.title = gettext('Already a library team member');
local_messages.deleteUser.messageTpl = gettext(
'Are you sure you want to delete {email} from the library “{container}”?'
);
});
$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', {});
});
return local_messages;
}
// Roles order are important: first role is considered initial role (the role added to user when (s)he's added
// Last role is considered an admin role (unrestricted access + ability to manage other users' permissions)
// Changing roles is performed in promote-demote fashion, so moves only to adjacent roles is allowed
var roles = [
{key:'library_user', name:gettext('Library User')},
{key:'staff', name:gettext('Staff')},
{key:'instructor', 'name': gettext("Admin")}
];
var options = {
el: $("#content"),
containerName: containerName,
tplUserURL: tplUserURL,
roles: roles,
users: users,
messages_modifier: updateMessages,
current_user_id: current_user_id,
allow_actions: allow_actions
};
var view = new ManageUsersAndRoles(options);
view.render();
};
});
......@@ -113,11 +113,13 @@
background: $pink-u3;
}
&.flag-role-admin {
&.flag-role-admin,
&.flag-role-instructor {
background: $pink;
}
&.flag-role-user {
&.flag-role-user,
&.flag-role-library_user {
background: $yellow-d1;
.msg-you { color: $yellow-l1; }
}
......
<div id='page-alert'></div>
<div id='page-prompt'></div>
<div id="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>Instructor Access
</h1>
<nav class="nav-actions">
<h3 class="sr">Page Actions</h3>
<ul></ul>
</nav>
</header>
</div>
<div class="wrapper-content wrapper">
<section class="content">
<article class="content-primary" role="main">
<ol id="user-list" class="user-list">
<div class="ui-loading">
<p><span class="spin"><i class="icon fa fa-refresh"></i></span> <span class="copy">Loading</span></p>
</div>
</ol>
</article>
</section>
</div>
</div>
<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>Instructor Access
</h1>
<nav class="nav-actions">
<h3 class="sr">Page Actions</h3>
<ul>
<li class="nav-item">
<a href="#" class="button new-button create-user-button"><i class="icon fa fa-plus"></i> Add Instructor</a>
</li>
</ul>
</nav>
</header>
</div>
<div class="wrapper-content wrapper">
<section class="content">
<article class="content-primary" role="main">
<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 Instructor Access to This Library</h3>
<fieldset class="form-fields">
<legend class="sr">New Instructor 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 instructor 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>
<ol class="user-list">
<li class="user-item" data-email="honor@example.com">
<span class="wrapper-ui-badge">
<span class="flag flag-role flag-role-staff is-hanging">
<span class="label sr">Current Role:</span>
<span class="value">
Staff
</span>
</span>
</span>
<div class="item-metadata">
<h3 class="user-name">
<span class="user-username">honor</span>
<span class="user-email">
<a class="action action-email" href="mailto:honor@example.com" title="send an email message to honor@example.com">honor@example.com</a>
</span>
</h3>
</div>
<ul class="item-actions user-actions">
<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>
<li class="action action-delete ">
<a href="#" class="delete remove-user action-icon" data-tooltip="Remove this user"><i class="icon fa fa-trash-o"></i><span class="sr">Delete the user, honor</span></a>
<div id='page-alert'></div>
<div id='page-prompt'></div>
<div id="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>Instructor Access
</h1>
<nav class="nav-actions">
<h3 class="sr">Page Actions</h3>
<ul>
<li class="nav-item">
<a href="#" class="button new-button create-user-button"><i class="icon fa fa-plus"></i> Add Instructor</a>
</li>
</ul>
</li>
<li class="user-item" data-email="audit@example.com">
<span class="wrapper-ui-badge">
<span class="flag flag-role flag-role-admin is-hanging">
<span class="label sr">Current Role:</span>
<span class="value">
Admin
</span>
</span>
</span>
<div class="item-metadata">
<h3 class="user-name">
<span class="user-username">audit</span>
<span class="user-email">
<a class="action action-email" href="mailto:audit@example.com" title="send an email message to audit@example.com">audit@example.com</a>
</span>
</h3>
</div>
<ul class="item-actions user-actions">
<li class="action action-role">
<a href="#" class="make-staff admin-role remove-admin-role">Remove Admin Access</span></a>
</li>
<li class="action action-delete ">
<a href="#" class="delete remove-user action-icon" data-tooltip="Remove this user"><i class="icon fa fa-trash-o"></i><span class="sr">Delete the user, audit</span></a>
</li>
</ul>
</li>
<li class="user-item" data-email="staff@example.com">
<span class="wrapper-ui-badge">
<span class="flag flag-role flag-role-user is-hanging">
<span class="label sr">Current Role:</span>
<span class="value">
User
</span>
</span>
</span>
<div class="item-metadata">
<h3 class="user-name">
<span class="user-username">staff</span>
<span class="user-email">
<a class="action action-email" href="mailto:staff@example.com" title="send an email message to staff@example.com">staff@example.com</a>
</span>
</h3>
</nav>
</header>
</div>
<div class="wrapper-content wrapper">
<section class="content">
<article class="content-primary" role="main">
<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 Instructor Access to This Library</h3>
<fieldset class="form-fields">
<legend class="sr">New Instructor 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 instructor 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>
<ul class="item-actions user-actions">
<li class="action action-role">
<a href="#" class="make-staff admin-role add-admin-role">Add Staff Access</span></a>
</li>
<li class="action action-delete ">
<a href="#" class="delete remove-user action-icon" data-tooltip="Remove this user"><i class="icon fa fa-trash-o"></i><span class="sr">Delete the user, staff</span></a>
</li>
</ul>
</li>
</ol>
<ol id="user-list" class="user-list">
<div class="ui-loading">
<p><span class="spin"><i class="icon fa fa-refresh"></i></span> <span class="copy">Loading</span></p>
</div>
</ol>
</article>
</section>
</article>
</section>
</div>
</div>
<li class="user-item" data-email="<%= user.email %>">
<span class="wrapper-ui-badge">
<span class="flag flag-role flag-role-<%= user.role %> is-hanging">
<span class="label sr"><%= gettext("Current Role:") %></span>
<span class="value">
<%= roles[user.role] %>
<% if (is_current_user) { %>
<span class="msg-you"><%= gettext("You!") %></span>
<% } %>
</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="<%= viewHelpers.format(gettext("send an email message to {email}"), {email: user.email})%>">
<%= user.email %>
</a>
</span>
</h3>
</div>
<% if (allow_actions) { %>
<ul class="item-actions user-actions">
<li class="action action-role">
<% for (var i=0; i < actions.length; i++) { %>
<% var action = actions[i]; %>
<% if (action.notoggle) { %>
<span class="admin-role notoggleforyou"><%= gettext("Promote another member to Admin to remove your admin rights") %></span>
<% } else { %>
<a href="#" class="make-<%= action.to_role %> admin-role <%= action.direction %>-admin-role">
<% var template = (action.direction === 'add') ? gettext("Add {role} Access") : gettext("Remove {role} Access"); %>
<%= viewHelpers.format(template, {role: action.label}) %></span>
</a>
<% } %>
<% } %>
</li>
<li class="action action-delete <%=!allow_delete ? "is-disabled" : "" %> aria-disabled="<%=!allow_delete%>">
<a href="#" class="delete remove-user action-icon" data-id="<%= user.email %>">
<i class="icon fa fa-trash-o"></i>
<span class="sr"><%= viewHelpers.format(gettext("Delete the user, {username}"), {username:user.username}) %></span>
</a>
</li>
</ul>
<% } %>
</li>
<%! import json %>
<%! from django.utils.translation import ugettext as _ %>
<%! from django.core.urlresolvers import reverse %>
<%! from student.roles import CourseInstructorRole %>
<%inherit file="base.html" />
<%def name="online_help_token()"><% return "team_course" %></%def>
<%block name="title">${_("Course Team Settings")}</%block>
<%block name="bodyclass">is-signedin course users view-team</%block>
<%namespace name='static' file='static_content.html'/>
<%block name="header_extras">
<script type="text/template" id="team-member-tpl">
<%static:include path="js/team-member.underscore" />
</script>
</%block>
<%block name="content">
......@@ -44,7 +50,7 @@
<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" name="user-email" type="text" placeholder="${_('example: username@domain.com')}" value="">
<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 course staff member you'd like to add")}</span>
</li>
</ol>
......@@ -59,73 +65,13 @@
</div>
%endif
<ol class="user-list">
% for user in staff:
<% course_team_url = reverse(
'contentstore.views.course_team_handler',
kwargs={'course_key_string': unicode(context_course.id), 'email': user.email}
)
%>
<li class="user-item" data-email="${user.email}" data-url="${course_team_url}">
<% is_instuctor = CourseInstructorRole(context_course.id).has_user(user) %>
% if is_instuctor:
<span class="wrapper-ui-badge">
<span class="flag flag-role flag-role-admin is-hanging">
<span class="label sr">${_("Current Role:")}</span>
<span class="value">
${_("Admin")}
% if request.user.id == user.id:
<span class="msg-you">${_("You!")}</span>
% endif
</span>
</span>
</span>
% else:
<span class="wrapper-ui-badge">
<span class="flag flag-role flag-role-staff is-hanging">
<span class="label sr">${_("Current Role:")}</span>
<span class="value">
${_("Staff")}
% if request.user.id == user.id:
<span class="msg-you">${_("You!")}</span>
% endif
</span>
</span>
</span>
% endif
<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">
<li class="action action-role">
% if is_instuctor and len(instructors) == 1:
<span class="admin-role notoggleforyou">${_("Promote another member to Admin to remove your admin rights")}</span>
% else:
<a href="#" class="admin-role toggle-admin-role ${'remove' if is_instuctor else 'add'}-admin-role">${_("Remove Admin Access") if is_instuctor else _("Add Admin Access")}</a>
% endif
</li>
<li class="action action-delete ${"is-disabled" if request.user.id == user.id else ""}" aria-disabled="${'true' if request.user.id == user.id else 'false'}">
<a href="#" class="delete remove-user action-icon" data-id="${user.email}"><i class="icon fa fa-trash-o"></i><span class="sr">${_("Delete the user, {username}").format(username=user.username)}</span></a>
</li>
</ul>
% endif
</li>
% endfor
<ol class="user-list" id="user-list">
<div class="ui-loading">
<p><span class="spin"><i class="icon fa fa-refresh"></i></span> <span class="copy">Loading</span></p>
</div>
</ol>
<% user_is_instuctor = CourseInstructorRole(context_course.id).has_user(request.user) %>
% if user_is_instuctor and len(staff) == 1:
% if allow_actions and len(users) == 1:
<div class="notice notice-incontext notice-create has-actions">
<div class="msg">
<h3 class="title">${_('Add Team Members to This Course')}</h3>
......@@ -150,7 +96,7 @@
<p>${_("Admins are course team members who can add and remove other course team members.")}</p>
</div>
% if user_is_instuctor and len(instructors) == 1:
% if show_transfer_ownership_hint:
<div class="bit">
<h3 class="title-3">${_("Transferring Ownership")}</h3>
<p>${_("Every course must have an Admin. If you're the Admin and you want transfer ownership of the course, click Add admin access to make another user the Admin, then ask that user to remove you from the Course Team list.")}</p>
......@@ -162,7 +108,13 @@
</%block>
<%block name="requirejs">
require(["js/factories/manage_users"], function(ManageUsersFactory) {
ManageUsersFactory(${json.dumps([user.email for user in staff])}, "${reverse('contentstore.views.course_team_handler', kwargs={'course_key_string': unicode(context_course.id), 'email': '@@EMAIL@@'})}");
require(["js/factories/manage_users"], function(ManageCourseUsersFactory) {
ManageCourseUsersFactory(
"${context_course.display_name | h}",
${json.dumps(users)},
"${reverse('contentstore.views.course_team_handler', kwargs={'course_key_string': unicode(context_course.id), 'email': '@@EMAIL@@'})}",
${ request.user.id },
${str(allow_actions).lower()}
);
});
</%block>
......@@ -5,6 +5,13 @@
<%def name="online_help_token()"><% return "team_library" %></%def>
<%block name="title">${_("Library User Access")}</%block>
<%block name="bodyclass">is-signedin course users view-team</%block>
<%namespace name='static' file='static_content.html'/>
<%block name="header_extras">
<script type="text/template" id="team-member-tpl">
<%static:include path="js/team-member.underscore" />
</script>
</%block>
<%block name="content">
......@@ -58,77 +65,13 @@
</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 your 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 fa fa-trash-o"></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 fa fa-trash-o"></i><span class="sr">${_("Remove me from this library")}</span></a>
</li>
</ul>
% endif
</li>
% endfor
<ol class="user-list" id="user-list">
<div class="ui-loading">
<p><span class="spin"><i class="icon fa fa-refresh"></i></span> <span class="copy">Loading</span></p>
</div>
</ol>
% if allow_actions and len(all_users) == 1:
% if allow_actions and len(users) == 1:
<div class="notice notice-incontext notice-create has-actions">
<div class="msg">
<h3 class="title">${_('Add More Users to This Library')}</h3>
......@@ -146,7 +89,7 @@
%endif
</article>
<aside class="content-supplementary" role="complimentary">
<aside class="content-supplementary" role="complementary">
<div class="bit">
<h3 class="title-3">${_("Library Access Roles")}</h3>
<p>${_("There are three access roles for libraries: User, Staff, and Admin.")}</p>
......@@ -160,11 +103,13 @@
</%block>
<%block name="requirejs">
require(["js/factories/manage_users_lib"], function(ManageUsersFactory) {
ManageUsersFactory(
require(["js/factories/manage_users_lib"], function(ManageLibraryUsersFactory) {
ManageLibraryUsersFactory(
"${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@@'})}"
${json.dumps(users)},
"${reverse('contentstore.views.course_team_handler', kwargs={'course_key_string': library_key, 'email': '@@EMAIL@@'})}",
${ request.user.id },
${str(allow_actions).lower()}
);
});
</%block>
......@@ -75,13 +75,13 @@ urlpatterns += patterns(
url(r'^signin$', 'login_page', name='login'),
url(r'^request_course_creator$', 'request_course_creator'),
url(r'^course_team/{}/(?P<email>.+)?$'.format(COURSELIKE_KEY_PATTERN), 'course_team_handler'),
url(r'^course_team/{}(?:/(?P<email>.+))?$'.format(COURSELIKE_KEY_PATTERN), 'course_team_handler'),
url(r'^course_info/{}$'.format(settings.COURSE_KEY_PATTERN), 'course_info_handler'),
url(
r'^course_info_update/{}/(?P<provided_id>\d+)?$'.format(settings.COURSE_KEY_PATTERN),
'course_info_update_handler'
),
url(r'^home/$', 'course_listing', name='home'),
url(r'^home/?$', 'course_listing', name='home'),
url(
r'^course/{}/search_reindex?$'.format(settings.COURSE_KEY_PATTERN),
'course_search_index_handler',
......
......@@ -233,3 +233,11 @@ def run_ipdb(_step):
import ipdb
ipdb.set_trace()
assert True
@step(u'(I am viewing|s?he views) the course team settings$')
def view_course_team_settings(_step, whom):
""" navigates to course team settings page """
world.click_course_settings()
link_css = 'li.nav-course-settings-team a'
world.css_click(link_css)
......@@ -84,6 +84,23 @@ class DashboardPage(PageObject):
"""
self.q(css='.wrapper-create-library .new-library-save').click()
def list_courses(self):
"""
List all the courses found on the page's list of libraries.
"""
# Workaround Selenium/Firefox bug: `.text` property is broken on invisible elements
course_tab_link = self.q(css='#course-index-tabs .courses-tab a')
if course_tab_link:
course_tab_link.click()
div2info = lambda element: {
'name': element.find_element_by_css_selector('.course-title').text,
'org': element.find_element_by_css_selector('.course-org .value').text,
'number': element.find_element_by_css_selector('.course-num .value').text,
'run': element.find_element_by_css_selector('.course-run .value').text,
'url': element.find_element_by_css_selector('a.course-link').get_attribute('href'),
}
return self.q(css='.courses li.course-item').map(div2info).results
def list_libraries(self):
"""
Click the tab to display the available libraries, and return detail of them.
......
......@@ -7,8 +7,8 @@ from bok_choy.promise import EmptyPromise
from selenium.webdriver.support.select import Select
from .component_editor import ComponentEditorView
from .container import XBlockWrapper
from ...pages.studio.users import UsersPageMixin
from ...pages.studio.pagination import PaginatedMixin
from ...tests.helpers import disable_animations
from .utils import confirm_prompt, wait_for_notification
from . import BASE_URL
......@@ -35,7 +35,7 @@ class LibraryPage(PageObject):
return self.q(css='body.view-library').present
class LibraryEditPage(LibraryPage, PaginatedMixin):
class LibraryEditPage(LibraryPage, PaginatedMixin, UsersPageMixin):
"""
Library edit page in Studio
"""
......@@ -56,11 +56,7 @@ class LibraryEditPage(LibraryPage, PaginatedMixin):
for improved test reliability.
"""
self.wait_for_ajax()
self.wait_for_element_invisibility(
'.ui-loading',
'Wait for the page to complete its initial loading of XBlocks via AJAX'
)
disable_animations(self)
super(LibraryEditPage, self).wait_until_ready()
@property
def xblocks(self):
......@@ -146,6 +142,7 @@ class StudioLibraryContentEditor(ComponentEditorView):
@property
def library_name(self):
""" Gets name of library """
return self.get_selected_option_text(self.LIBRARY_LABEL)
@library_name.setter
......
"""
Course Team page in Studio.
"""
from .course_page import CoursePage
class CourseTeamPage(CoursePage):
"""
Course Team page in Studio.
"""
url_path = "course_team"
def is_browser_on_page(self):
return self.q(css='body.view-team').present
......@@ -4,6 +4,7 @@ Page classes to test either the Course Team page or the Library Team page.
from bok_choy.promise import EmptyPromise
from bok_choy.page_object import PageObject
from ...tests.helpers import disable_animations
from .course_page import CoursePage
from . import BASE_URL
......@@ -20,16 +21,10 @@ def wait_for_ajax_or_reload(browser):
EmptyPromise(_is_ajax_finished, "Finished waiting for ajax requests.").fulfill()
class UsersPage(PageObject):
"""
Base class for either the Course Team page or the Library Team page
"""
def __init__(self, browser, locator):
super(UsersPage, self).__init__(browser)
self.locator = locator
class UsersPageMixin(PageObject):
""" Common functionality for course/library team pages """
new_user_form_selector = '.form-create.create-user .user-email-input'
@property
def url(self):
"""
URL to this page - override in subclass
......@@ -52,6 +47,13 @@ class UsersPage(PageObject):
).results
@property
def usernames(self):
"""
Returns a list of user names for users listed on this page
"""
return [user.name for user in self.users]
@property
def has_add_button(self):
"""
Is the "New Team Member" button present?
......@@ -78,11 +80,55 @@ class UsersPage(PageObject):
self.q(css='.form-create.create-user .action-primary').click()
wait_for_ajax_or_reload(self.browser)
def get_user(self, email):
""" Gets user wrapper by email """
target_users = [user for user in self.users if user.email == email]
assert len(target_users) == 1
return target_users[0]
def add_user_to_course(self, email):
""" Adds user to a course/library """
self.click_add_button()
self.wait_for(lambda: self.new_user_form_visible, "Add user form is visible")
self.set_new_user_email(email)
self.click_submit_new_user_form()
def delete_user_from_course(self, email):
""" Deletes user from course/library """
target_user = self.get_user(email)
target_user.click_delete()
def modal_dialog_visible(self, dialog_type):
""" Checks if modal dialog of specified class is displayed """
return self.q(css='.prompt.{dialog_type}'.format(dialog_type=dialog_type)).visible
def modal_dialog_text(self, dialog_type):
""" Gets modal dialog text """
return self.q(css='.prompt.{dialog_type} .message'.format(dialog_type=dialog_type)).text[0]
def wait_until_ready(self):
"""
When the page first loads, there is a loading indicator and most
functionality is not yet available. This waits for that loading to
finish.
Always call this before using the page. It also disables animations
for improved test reliability.
"""
self.wait_for_element_invisibility(
'.ui-loading',
'Wait for the page to complete its initial loading and rendering via Backbone'
)
disable_animations(self)
class LibraryUsersPage(UsersPage):
class LibraryUsersPage(UsersPageMixin):
"""
Library Team page in Studio
"""
def __init__(self, browser, locator):
super(LibraryUsersPage, self).__init__(browser)
self.locator = locator
@property
def url(self):
......@@ -92,6 +138,14 @@ class LibraryUsersPage(UsersPage):
return "{}/library/{}/team/".format(BASE_URL, unicode(self.locator))
class CourseTeamPage(CoursePage, UsersPageMixin):
"""
Course Team page in Studio.
"""
url_path = "course_team"
class UserWrapper(PageObject):
"""
A PageObject representing a wrapper around a user listed on the course/library team page.
......
......@@ -42,6 +42,10 @@ class StudioCourseTest(UniqueCourseTest):
Log in as the user that created the course. The user will be given instructor access
to the course and enrolled in it. By default the user will not have staff access unless
is_staff is passed as True.
Args:
user(dict): dictionary containing user data: {'username': ..., 'email': ..., 'password': ...}
is_staff(bool): register this user as staff
"""
self.auth_page = AutoAuthPage(
self.browser,
......
......@@ -14,7 +14,7 @@ from ...pages.studio.import_export import ExportCoursePage, ImportCoursePage
from ...pages.studio.howitworks import HowitworksPage
from ...pages.studio.index import DashboardPage
from ...pages.studio.login import LoginPage
from ...pages.studio.manage_users import CourseTeamPage
from ...pages.studio.users import CourseTeamPage
from ...pages.studio.overview import CourseOutlinePage
from ...pages.studio.settings import SettingsPage
from ...pages.studio.settings_advanced import AdvancedSettingsPage
......
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