diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py
index f5f75485c8..9fc8cd6987 100644
--- a/common/djangoapps/student/views.py
+++ b/common/djangoapps/student/views.py
@@ -1746,6 +1746,7 @@ def auto_auth(request):
* `staff`: Set to "true" to make the user global staff.
* `course_id`: Enroll the student in the course with `course_id`
* `roles`: Comma-separated list of roles to grant the student in the course with `course_id`
+ * `no_login`: Define this to create the user but not login
If username, email, or password are not provided, use
randomly generated credentials.
@@ -1765,6 +1766,7 @@ def auto_auth(request):
if course_id:
course_key = CourseLocator.from_string(course_id)
role_names = [v.strip() for v in request.GET.get('roles', '').split(',') if v.strip()]
+ login_when_done = 'no_login' not in request.GET
# Get or create the user object
post_data = {
@@ -1808,14 +1810,16 @@ def auto_auth(request):
user.roles.add(role)
# Log in as the user
- user = authenticate(username=username, password=password)
- login(request, user)
+ if login_when_done:
+ user = authenticate(username=username, password=password)
+ login(request, user)
create_comments_service_user(user)
# Provide the user with a valid CSRF token
# then return a 200 response
- success_msg = u"Logged in user {0} ({1}) with password {2} and user_id {3}".format(
+ success_msg = u"{} user {} ({}) with password {} and user_id {}".format(
+ u"Logged in" if login_when_done else "Created",
username, email, password, user.id
)
response = HttpResponse(success_msg)
diff --git a/common/test/acceptance/pages/studio/auto_auth.py b/common/test/acceptance/pages/studio/auto_auth.py
index e8beeaca5b..2e2cffd677 100644
--- a/common/test/acceptance/pages/studio/auto_auth.py
+++ b/common/test/acceptance/pages/studio/auto_auth.py
@@ -15,7 +15,7 @@ class AutoAuthPage(PageObject):
this url will create a user and log them in.
"""
- def __init__(self, browser, username=None, email=None, password=None, staff=None, course_id=None, roles=None):
+ def __init__(self, browser, username=None, email=None, password=None, staff=None, course_id=None, roles=None, no_login=None):
"""
Auto-auth is an end-point for HTTP GET requests.
By default, it will create accounts with random user credentials,
@@ -51,6 +51,9 @@ class AutoAuthPage(PageObject):
if roles is not None:
self._params['roles'] = roles
+ if no_login:
+ self._params['no_login'] = True
+
@property
def url(self):
"""
@@ -66,7 +69,7 @@ class AutoAuthPage(PageObject):
def is_browser_on_page(self):
message = self.q(css='BODY').text[0]
- match = re.search(r'Logged in user ([^$]+) with password ([^$]+) and user_id ([^$]+)$', message)
+ match = re.search(r'(Logged in|Created) user ([^$]+) with password ([^$]+) and user_id ([^$]+)$', message)
return True if match else False
def get_user_id(self):
diff --git a/common/test/acceptance/pages/studio/users.py b/common/test/acceptance/pages/studio/users.py
new file mode 100644
index 0000000000..c1a2427d56
--- /dev/null
+++ b/common/test/acceptance/pages/studio/users.py
@@ -0,0 +1,189 @@
+"""
+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 . import BASE_URL
+
+
+def wait_for_ajax_or_reload(browser):
+ """
+ Wait for all ajax requests to finish, OR for the page to reload.
+ Normal wait_for_ajax() chokes on occasion if the pages reloads,
+ giving "WebDriverException: Message: u'jQuery is not defined'"
+ """
+ def _is_ajax_finished():
+ """ Wait for jQuery to finish all AJAX calls, if it is present. """
+ return browser.execute_script("return typeof(jQuery) == 'undefined' || jQuery.active == 0")
+
+ 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
+
+ @property
+ def url(self):
+ """
+ URL to this page - override in subclass
+ """
+ raise NotImplementedError
+
+ def is_browser_on_page(self):
+ """
+ Returns True iff the browser has loaded the page.
+ """
+ return self.q(css='body.view-team').present
+
+ @property
+ def users(self):
+ """
+ Return a list of users listed on this page.
+ """
+ return self.q(css='.user-list .user-item').map(lambda el: UserWrapper(self.browser, el.get_attribute('data-email'))).results
+
+ @property
+ def has_add_button(self):
+ """
+ Is the "New Team Member" button present?
+ """
+ return self.q(css='.create-user-button').present
+
+ def click_add_button(self):
+ """
+ Click on the "New Team Member" button
+ """
+ self.q(css='.create-user-button').click()
+
+ @property
+ def new_user_form_visible(self):
+ """ Is the new user form visible? """
+ return self.q(css='.form-create.create-user .user-email-input').visible
+
+ def set_new_user_email(self, email):
+ """ Set the value of the "New User Email Address" field. """
+ self.q(css='.form-create.create-user .user-email-input').fill(email)
+
+ def click_submit_new_user_form(self):
+ """ Submit the "New User" form """
+ self.q(css='.form-create.create-user .action-primary').click()
+ wait_for_ajax_or_reload(self.browser)
+
+
+class LibraryUsersPage(UsersPage):
+ """
+ Library Team page in Studio
+ """
+
+ @property
+ def url(self):
+ """
+ URL to the "User Access" page for the given library.
+ """
+ return "{}/library/{}/team/".format(BASE_URL, unicode(self.locator))
+
+
+class UserWrapper(PageObject):
+ """
+ A PageObject representing a wrapper around a user listed on the course/library team page.
+ """
+ url = None
+ COMPONENT_BUTTONS = {
+ 'basic_tab': '.editor-tabs li.inner_tab_wrap:nth-child(1) > a',
+ 'advanced_tab': '.editor-tabs li.inner_tab_wrap:nth-child(2) > a',
+ 'save_settings': '.action-save',
+ }
+
+ def __init__(self, browser, email):
+ super(UserWrapper, self).__init__(browser)
+ self.email = email
+ self.selector = '.user-list .user-item[data-email="{}"]'.format(self.email)
+
+ def is_browser_on_page(self):
+ """
+ Sanity check that our wrapper element is on the page.
+ """
+ return self.q(css=self.selector).present
+
+ def _bounded_selector(self, selector):
+ """
+ Return `selector`, but limited to this particular user entry's context
+ """
+ return '{} {}'.format(self.selector, selector)
+
+ @property
+ def name(self):
+ """ Get this user's username, as displayed. """
+ return self.q(css=self._bounded_selector('.user-username')).text[0]
+
+ @property
+ def role_label(self):
+ """ Get this user's role, as displayed. """
+ return self.q(css=self._bounded_selector('.flag-role .value')).text[0]
+
+ @property
+ def is_current_user(self):
+ """ Does the UI indicate that this is the current user? """
+ return self.q(css=self._bounded_selector('.flag-role .msg-you')).present
+
+ @property
+ def can_promote(self):
+ """ Can this user be promoted to a more powerful role? """
+ return self.q(css=self._bounded_selector('.add-admin-role')).present
+
+ @property
+ def promote_button_text(self):
+ """ What does the promote user button say? """
+ return self.q(css=self._bounded_selector('.add-admin-role')).text[0]
+
+ def click_promote(self):
+ """ Click on the button to promote this user to the more powerful role """
+ self.q(css=self._bounded_selector('.add-admin-role')).click()
+ wait_for_ajax_or_reload(self.browser)
+
+ @property
+ def can_demote(self):
+ """ Can this user be demoted to a less powerful role? """
+ return self.q(css=self._bounded_selector('.remove-admin-role')).present
+
+ @property
+ def demote_button_text(self):
+ """ What does the demote user button say? """
+ return self.q(css=self._bounded_selector('.remove-admin-role')).text[0]
+
+ def click_demote(self):
+ """ Click on the button to demote this user to the less powerful role """
+ self.q(css=self._bounded_selector('.remove-admin-role')).click()
+ wait_for_ajax_or_reload(self.browser)
+
+ @property
+ def can_delete(self):
+ """ Can this user be deleted? """
+ return self.q(css=self._bounded_selector('.action-delete:not(.is-disabled) .remove-user')).present
+
+ def click_delete(self):
+ """ Click the button to delete this user. """
+ disable_animations(self)
+ self.q(css=self._bounded_selector('.remove-user')).click()
+ # We can't use confirm_prompt because its wait_for_ajax is flaky when the page is expected to reload.
+ self.wait_for_element_visibility('.prompt', 'Prompt is visible')
+ self.wait_for_element_visibility('.prompt .action-primary', 'Confirmation button is visible')
+ self.q(css='.prompt .action-primary').click()
+ wait_for_ajax_or_reload(self.browser)
+
+ @property
+ def has_no_change_warning(self):
+ """ Does this have a warning in place of the promote/demote buttons? """
+ return self.q(css=self._bounded_selector('.notoggleforyou')).present
+
+ @property
+ def no_change_warning_text(self):
+ """ Text of the warning seen in place of the promote/demote buttons. """
+ return self.q(css=self._bounded_selector('.notoggleforyou')).text[0]
diff --git a/common/test/acceptance/tests/studio/test_studio_library.py b/common/test/acceptance/tests/studio/test_studio_library.py
index b0d6cffb1a..afd5e8eb36 100644
--- a/common/test/acceptance/tests/studio/test_studio_library.py
+++ b/common/test/acceptance/tests/studio/test_studio_library.py
@@ -5,8 +5,10 @@ from ddt import ddt, data
from .base_studio_test import StudioLibraryTest
from ...fixtures.course import XBlockFixtureDesc
+from ...pages.studio.auto_auth import AutoAuthPage
from ...pages.studio.utils import add_component
from ...pages.studio.library import LibraryPage
+from ...pages.studio.users import LibraryUsersPage
@ddt
@@ -306,3 +308,138 @@ class LibraryNavigationTest(StudioLibraryTest):
self.assertEqual(self.lib_page.xblocks[0].name, '1')
self.assertEqual(self.lib_page.xblocks[-1].name, '11')
self.assertEqual(self.lib_page.get_page_number(), '1')
+
+
+class LibraryUsersPageTest(StudioLibraryTest):
+ """
+ Test the functionality of the library "Instructor Access" page.
+ """
+ def setUp(self):
+ super(LibraryUsersPageTest, self).setUp()
+
+ # Create a second user for use in these tests:
+ AutoAuthPage(self.browser, username="second", email="second@example.com", no_login=True).visit()
+
+ self.page = LibraryUsersPage(self.browser, self.library_key)
+ self.page.visit()
+
+ def _expect_refresh(self):
+ """
+ Wait for the page to reload.
+ """
+ self.page = LibraryUsersPage(self.browser, self.library_key).wait_for_page()
+
+ def test_user_management(self):
+ """
+ Scenario: Ensure that we can edit the permissions of users.
+ Given I have a library in Studio where I am the only admin
+ assigned (which is the default for a newly-created library)
+ And I navigate to Library "Instructor Access" Page in Studio
+ Then there should be one user listed (myself), and I must
+ not be able to remove myself or my instructor privilege.
+
+ When I click Add Intructor
+ Then I see a form to complete
+ When I complete the form and submit it
+ Then I can see the new user is listed as a "User" of the library
+
+ When I click to Add Staff permissions to the new user
+ Then I can see the new user has staff permissions and that I am now
+ able to promote them to an Admin or remove their staff permissions.
+
+ When I click to Add Admin permissions to the new user
+ Then I can see the new user has admin permissions and that I can now
+ remove Admin permissions from either user.
+ """
+ def check_is_only_admin(user):
+ """
+ Ensure user is an admin user and cannot be removed.
+ (There must always be at least one admin user.)
+ """
+ self.assertIn("admin", user.role_label.lower())
+ self.assertFalse(user.can_promote)
+ self.assertFalse(user.can_demote)
+ self.assertFalse(user.can_delete)
+ self.assertTrue(user.has_no_change_warning)
+ self.assertIn("Promote another member to Admin to remove admin rights", user.no_change_warning_text)
+
+ self.assertEqual(len(self.page.users), 1)
+ user = self.page.users[0]
+ self.assertTrue(user.is_current_user)
+ check_is_only_admin(user)
+
+ # Add a new user:
+
+ self.assertTrue(self.page.has_add_button)
+ self.assertFalse(self.page.new_user_form_visible)
+ self.page.click_add_button()
+ self.assertTrue(self.page.new_user_form_visible)
+ self.page.set_new_user_email('second@example.com')
+ self.page.click_submit_new_user_form()
+
+ # Check the new user's listing:
+
+ def get_two_users():
+ """
+ Expect two users to be listed, one being me, and another user.
+ Returns me, them
+ """
+ users = self.page.users
+ self.assertEqual(len(users), 2)
+ self.assertEqual(len([u for u in users if u.is_current_user]), 1)
+ if users[0].is_current_user:
+ return users[0], users[1]
+ else:
+ return users[1], users[0]
+
+ self._expect_refresh()
+ user_me, them = get_two_users()
+ check_is_only_admin(user_me)
+
+ self.assertIn("user", them.role_label.lower())
+ self.assertTrue(them.can_promote)
+ self.assertIn("Add Staff Access", them.promote_button_text)
+ self.assertFalse(them.can_demote)
+ self.assertTrue(them.can_delete)
+ self.assertFalse(them.has_no_change_warning)
+
+ # Add Staff permissions to the new user:
+
+ them.click_promote()
+ self._expect_refresh()
+ user_me, them = get_two_users()
+ check_is_only_admin(user_me)
+
+ self.assertIn("staff", them.role_label.lower())
+ self.assertTrue(them.can_promote)
+ self.assertIn("Add Admin Access", them.promote_button_text)
+ self.assertTrue(them.can_demote)
+ self.assertIn("Remove Staff Access", them.demote_button_text)
+ self.assertTrue(them.can_delete)
+ self.assertFalse(them.has_no_change_warning)
+
+ # Add Admin permissions to the new user:
+
+ them.click_promote()
+ self._expect_refresh()
+ user_me, them = get_two_users()
+ self.assertIn("admin", user_me.role_label.lower())
+ self.assertFalse(user_me.can_promote)
+ self.assertTrue(user_me.can_demote)
+ self.assertTrue(user_me.can_delete)
+ self.assertFalse(user_me.has_no_change_warning)
+
+ self.assertIn("admin", them.role_label.lower())
+ self.assertFalse(them.can_promote)
+ self.assertTrue(them.can_demote)
+ self.assertIn("Remove Admin Access", them.demote_button_text)
+ self.assertTrue(them.can_delete)
+ self.assertFalse(them.has_no_change_warning)
+
+ # Delete the new user:
+
+ them.click_delete()
+ self._expect_refresh()
+ self.assertEqual(len(self.page.users), 1)
+ user = self.page.users[0]
+ self.assertTrue(user.is_current_user)