diff --git a/common/test/acceptance/pages/lms/courseware.py b/common/test/acceptance/pages/lms/courseware.py
index 510c19b9be..b5c835a398 100644
--- a/common/test/acceptance/pages/lms/courseware.py
+++ b/common/test/acceptance/pages/lms/courseware.py
@@ -33,12 +33,19 @@ class CoursewarePage(CoursePage):
"""
return len(self.q(css=self.subsection_selector))
+ @property
+ def xblock_components(self):
+ """
+ Return the xblock components within the unit on the page.
+ """
+ return self.q(css=self.xblock_component_selector)
+
@property
def num_xblock_components(self):
"""
Return the number of rendered xblocks within the unit on the page
"""
- return len(self.q(css=self.xblock_component_selector))
+ return len(self.xblock_components)
def xblock_component_type(self, index=0):
"""
diff --git a/common/test/acceptance/pages/lms/instructor_dashboard.py b/common/test/acceptance/pages/lms/instructor_dashboard.py
index b120ba4305..5543359f53 100644
--- a/common/test/acceptance/pages/lms/instructor_dashboard.py
+++ b/common/test/acceptance/pages/lms/instructor_dashboard.py
@@ -133,6 +133,10 @@ class MembershipPageCohortManagementSection(PageObject):
"""
Returns the name of the selected cohort.
"""
+ EmptyPromise(
+ lambda: len(self._get_cohort_options().results) > 0,
+ "Waiting for cohort selector to populate"
+ ).fulfill()
return self._cohort_name(
self._get_cohort_options().filter(lambda el: el.is_selected()).first.text[0]
)
@@ -163,7 +167,10 @@ class MembershipPageCohortManagementSection(PageObject):
Adds a new manual cohort with the specified name.
If a content group should also be associated, the name of the content group should be specified.
"""
- self.q(css=self._bounded_selector("div.cohort-management-nav .action-create")).first.click()
+ create_buttons = self.q(css=self._bounded_selector(".action-create"))
+ # There are 2 create buttons on the page. The second one is only present when no cohort yet exists
+ # (in which case the first is not visible). Click on the last present create button.
+ create_buttons.results[len(create_buttons.results) - 1].click()
textinput = self.q(css=self._bounded_selector("#cohort-name")).results[0]
textinput.send_keys(cohort_name)
if content_group:
diff --git a/common/test/acceptance/tests/lms/test_lms_user_preview.py b/common/test/acceptance/tests/lms/test_lms_user_preview.py
index b17e74250c..ff23534ee3 100644
--- a/common/test/acceptance/tests/lms/test_lms_user_preview.py
+++ b/common/test/acceptance/tests/lms/test_lms_user_preview.py
@@ -266,28 +266,26 @@ class CourseWithContentGroupsTest(StaffViewTest):
""")
+ self.alpha_text = "VISIBLE TO ALPHA"
+ self.beta_text = "VISIBLE TO BETA"
+ self.everyone_text = "VISIBLE TO EVERYONE"
+
course_fixture.add_children(
XBlockFixtureDesc('chapter', 'Test Section').add_children(
XBlockFixtureDesc('sequential', 'Test Subsection').add_children(
- XBlockFixtureDesc(
- 'problem', 'Visible to alpha', data=problem_data, metadata={"group_access": {0: [0]}}
- ),
- XBlockFixtureDesc(
- 'problem', 'Visible to beta', data=problem_data, metadata={"group_access": {0: [1]}}
- ),
- XBlockFixtureDesc('problem', 'Visible to everyone', data=problem_data)
+ XBlockFixtureDesc('vertical', 'Test Unit').add_children(
+ XBlockFixtureDesc(
+ 'problem', self.alpha_text, data=problem_data, metadata={"group_access": {0: [0]}}
+ ),
+ XBlockFixtureDesc(
+ 'problem', self.beta_text, data=problem_data, metadata={"group_access": {0: [1]}}
+ ),
+ XBlockFixtureDesc('problem', self.everyone_text, data=problem_data)
+ )
)
)
)
- def _verify_visible_problems(self, expected_items):
- """
- Verify that the expected problems are visible.
- """
- course_nav = CourseNavPage(self.browser)
- actual_items = course_nav.sequence_items
- self.assertItemsEqual(expected_items, actual_items)
-
def test_staff_sees_all_problems(self):
"""
Scenario: Staff see all problems
@@ -296,8 +294,8 @@ class CourseWithContentGroupsTest(StaffViewTest):
When I view the courseware in the LMS with staff access
Then I see all the problems, regardless of their group_access property
"""
- self._goto_staff_page()
- self._verify_visible_problems(['Visible to alpha', 'Visible to beta', 'Visible to everyone'])
+ course_page = self._goto_staff_page()
+ verify_expected_problem_visibility(self, course_page, [self.alpha_text, self.beta_text, self.everyone_text])
def test_student_not_in_content_group(self):
"""
@@ -310,7 +308,7 @@ class CourseWithContentGroupsTest(StaffViewTest):
"""
course_page = self._goto_staff_page()
course_page.set_staff_view_mode('Student')
- self._verify_visible_problems(['Visible to everyone'])
+ verify_expected_problem_visibility(self, course_page, [self.everyone_text])
def test_as_student_in_alpha(self):
"""
@@ -323,7 +321,7 @@ class CourseWithContentGroupsTest(StaffViewTest):
"""
course_page = self._goto_staff_page()
course_page.set_staff_view_mode('Student in alpha')
- self._verify_visible_problems(['Visible to alpha', 'Visible to everyone'])
+ verify_expected_problem_visibility(self, course_page, [self.alpha_text, self.everyone_text])
def test_as_student_in_beta(self):
"""
@@ -336,4 +334,15 @@ class CourseWithContentGroupsTest(StaffViewTest):
"""
course_page = self._goto_staff_page()
course_page.set_staff_view_mode('Student in beta')
- self._verify_visible_problems(['Visible to beta', 'Visible to everyone'])
+ verify_expected_problem_visibility(self, course_page, [self.beta_text, self.everyone_text])
+
+
+def verify_expected_problem_visibility(test, courseware_page, expected_problems):
+ """
+ Helper method that checks that the expected problems are visible on the current page.
+ """
+ test.assertEqual(
+ len(expected_problems), courseware_page.num_xblock_components, "Incorrect number of visible problems"
+ )
+ for index, expected_problem in enumerate(expected_problems):
+ test.assertIn(expected_problem, courseware_page.xblock_components[index].text)
diff --git a/common/test/acceptance/tests/studio/base_studio_test.py b/common/test/acceptance/tests/studio/base_studio_test.py
index 02fdcbe998..066117a303 100644
--- a/common/test/acceptance/tests/studio/base_studio_test.py
+++ b/common/test/acceptance/tests/studio/base_studio_test.py
@@ -58,12 +58,12 @@ class ContainerBase(StudioCourseTest):
Base class for tests that do operations on the container page.
"""
- def setUp(self):
+ def setUp(self, is_staff=False):
"""
Create a unique identifier for the course used in this test.
"""
# Ensure that the superclass sets up
- super(ContainerBase, self).setUp()
+ super(ContainerBase, self).setUp(is_staff=is_staff)
self.outline = CourseOutlinePage(
self.browser,
diff --git a/common/test/acceptance/tests/studio/test_studio_split_test.py b/common/test/acceptance/tests/studio/test_studio_split_test.py
index 395cb1f48b..3d8ef20bae 100644
--- a/common/test/acceptance/tests/studio/test_studio_split_test.py
+++ b/common/test/acceptance/tests/studio/test_studio_split_test.py
@@ -14,7 +14,6 @@ from bok_choy.promise import Promise, EmptyPromise
from ...fixtures.course import XBlockFixtureDesc
from ...pages.studio.component_editor import ComponentEditorView
from ...pages.studio.overview import CourseOutlinePage, CourseOutlineUnit
-from ...pages.studio.settings_advanced import AdvancedSettingsPage
from ...pages.studio.container import ContainerPage
from ...pages.studio.settings_group_configurations import GroupConfigurationsPage
from ...pages.studio.utils import add_advanced_component
diff --git a/common/test/acceptance/tests/test_cohorted_courseware.py b/common/test/acceptance/tests/test_cohorted_courseware.py
new file mode 100644
index 0000000000..b702352a6d
--- /dev/null
+++ b/common/test/acceptance/tests/test_cohorted_courseware.py
@@ -0,0 +1,227 @@
+"""
+End-to-end test for cohorted courseware. This uses both Studio and LMS.
+"""
+
+from nose.plugins.attrib import attr
+import json
+
+from studio.base_studio_test import ContainerBase
+
+from ..pages.studio.settings_group_configurations import GroupConfigurationsPage
+from ..pages.studio.settings_advanced import AdvancedSettingsPage
+from ..pages.studio.auto_auth import AutoAuthPage as StudioAutoAuthPage
+from ..fixtures.course import XBlockFixtureDesc
+from ..pages.studio.component_editor import ComponentVisibilityEditorView
+from ..pages.lms.instructor_dashboard import InstructorDashboardPage
+from ..pages.lms.course_nav import CourseNavPage
+from ..pages.lms.courseware import CoursewarePage
+from ..pages.lms.auto_auth import AutoAuthPage as LmsAutoAuthPage
+from ..tests.lms.test_lms_user_preview import verify_expected_problem_visibility
+
+from bok_choy.promise import EmptyPromise
+
+
+@attr('shard_1')
+class EndToEndCohortedCoursewareTest(ContainerBase):
+
+ def setUp(self, is_staff=True):
+
+ super(EndToEndCohortedCoursewareTest, self).setUp(is_staff=is_staff)
+ self.staff_user = self.user
+
+ self.content_group_a = "Content Group A"
+ self.content_group_b = "Content Group B"
+
+ # Create a student who will be in "Cohort A"
+ self.cohort_a_student_username = "cohort_a_student"
+ self.cohort_a_student_email = "cohort_a_student@example.com"
+ StudioAutoAuthPage(
+ self.browser, username=self.cohort_a_student_username, email=self.cohort_a_student_email, no_login=True
+ ).visit()
+
+ # Create a student who will be in "Cohort B"
+ self.cohort_b_student_username = "cohort_b_student"
+ self.cohort_b_student_email = "cohort_b_student@example.com"
+ StudioAutoAuthPage(
+ self.browser, username=self.cohort_b_student_username, email=self.cohort_b_student_email, no_login=True
+ ).visit()
+
+ # Create a student who will end up in the default cohort group
+ self.cohort_default_student_username = "cohort default student"
+ self.cohort_default_student_email = "cohort_default_student@example.com"
+ StudioAutoAuthPage(
+ self.browser, username=self.cohort_default_student_username,
+ email=self.cohort_default_student_email, no_login=True
+ ).visit()
+
+ # Start logged in as the staff user.
+ StudioAutoAuthPage(
+ self.browser, username=self.staff_user["username"], email=self.staff_user["email"]
+ ).visit()
+
+ def populate_course_fixture(self, course_fixture):
+ """
+ Populate the children of the test course fixture.
+ """
+ self.group_a_problem = 'GROUP A CONTENT'
+ self.group_b_problem = 'GROUP B CONTENT'
+ self.group_a_and_b_problem = 'GROUP A AND B CONTENT'
+ self.visible_to_all_problem = 'VISIBLE TO ALL CONTENT'
+ course_fixture.add_children(
+ XBlockFixtureDesc('chapter', 'Test Section').add_children(
+ XBlockFixtureDesc('sequential', 'Test Subsection').add_children(
+ XBlockFixtureDesc('vertical', 'Test Unit').add_children(
+ XBlockFixtureDesc('problem', self.group_a_problem, data=''),
+ XBlockFixtureDesc('problem', self.group_b_problem, data=''),
+ XBlockFixtureDesc('problem', self.group_a_and_b_problem, data=''),
+ XBlockFixtureDesc('problem', self.visible_to_all_problem, data='')
+ )
+ )
+ )
+ )
+
+ def enable_cohorts_in_course(self):
+ """
+ This turns on cohorts for the course. Currently this is still done through Advanced
+ Settings. Eventually it will be done in the LMS Instructor Dashboard.
+ """
+ advanced_settings = AdvancedSettingsPage(
+ self.browser,
+ self.course_info['org'],
+ self.course_info['number'],
+ self.course_info['run']
+ )
+
+ advanced_settings.visit()
+ cohort_config = '{"cohorted": true}'
+ advanced_settings.set('Cohort Configuration', cohort_config)
+ advanced_settings.refresh_and_wait_for_load()
+
+ self.assertEquals(
+ json.loads(cohort_config),
+ json.loads(advanced_settings.get('Cohort Configuration')),
+ 'Wrong input for Cohort Configuration'
+ )
+
+ def create_content_groups(self):
+ """
+ Creates two content groups in Studio Group Configurations Settings.
+ """
+ group_configurations_page = GroupConfigurationsPage(
+ self.browser,
+ self.course_info['org'],
+ self.course_info['number'],
+ self.course_info['run']
+ )
+ group_configurations_page.visit()
+
+ group_configurations_page.create_first_content_group()
+ config = group_configurations_page.content_groups[0]
+ config.name = self.content_group_a
+ config.save()
+
+ group_configurations_page.add_content_group()
+ config = group_configurations_page.content_groups[1]
+ config.name = self.content_group_b
+ config.save()
+
+ def link_problems_to_content_groups_and_publish(self):
+ """
+ Updates 3 of the 4 existing problems to limit their visibility by content group.
+ Publishes the modified units.
+ """
+ container_page = self.go_to_unit_page()
+
+ def set_visibility(problem_index, content_group, second_content_group=None):
+ problem = container_page.xblocks[problem_index]
+ problem.edit_visibility()
+ if second_content_group:
+ ComponentVisibilityEditorView(self.browser, problem.locator).select_option(
+ second_content_group, save=False
+ )
+ ComponentVisibilityEditorView(self.browser, problem.locator).select_option(content_group)
+
+ set_visibility(1, self.content_group_a)
+ set_visibility(2, self.content_group_b)
+ set_visibility(3, self.content_group_a, self.content_group_b)
+
+ container_page.publish_action.click()
+
+ def create_cohorts_and_assign_students(self):
+ """
+ Adds 2 manual cohorts, linked to content groups, to the course.
+ Each cohort is assigned one student.
+ """
+ instructor_dashboard_page = InstructorDashboardPage(self.browser, self.course_id)
+ instructor_dashboard_page.visit()
+ membership_page = instructor_dashboard_page.select_membership()
+ cohort_management_page = membership_page.select_cohort_management_section()
+
+ def add_cohort_with_student(cohort_name, content_group, student):
+ cohort_management_page.add_cohort(cohort_name, content_group=content_group)
+ # After adding the cohort, it should automatically be selected
+ EmptyPromise(
+ lambda: cohort_name == cohort_management_page.get_selected_cohort(), "Waiting for new cohort"
+ ).fulfill()
+ cohort_management_page.add_students_to_selected_cohort([student])
+
+ add_cohort_with_student("Cohort A", self.content_group_a, self.cohort_a_student_username)
+ add_cohort_with_student("Cohort B", self.content_group_b, self.cohort_b_student_username)
+
+ def view_cohorted_content_as_different_users(self):
+ """
+ View content as staff, student in Cohort A, student in Cohort B, and student in Default Cohort.
+ """
+ courseware_page = CoursewarePage(self.browser, self.course_id)
+
+ def login_and_verify_visible_problems(username, email, expected_problems):
+ LmsAutoAuthPage(
+ self.browser, username=username, email=email, course_id=self.course_id
+ ).visit()
+ courseware_page.visit()
+ verify_expected_problem_visibility(self, courseware_page, expected_problems)
+
+ login_and_verify_visible_problems(
+ self.staff_user["username"], self.staff_user["email"],
+ [self.group_a_problem, self.group_b_problem, self.group_a_and_b_problem, self.visible_to_all_problem]
+ )
+
+ login_and_verify_visible_problems(
+ self.cohort_a_student_username, self.cohort_a_student_email,
+ [self.group_a_problem, self.group_a_and_b_problem, self.visible_to_all_problem]
+ )
+
+ login_and_verify_visible_problems(
+ self.cohort_b_student_username, self.cohort_b_student_email,
+ [self.group_b_problem, self.group_a_and_b_problem, self.visible_to_all_problem]
+ )
+
+ login_and_verify_visible_problems(
+ self.cohort_default_student_username, self.cohort_default_student_email,
+ [self.visible_to_all_problem]
+ )
+
+ def test_cohorted_courseware(self):
+ """
+ Scenario: Can create content that is only visible to students in particular cohorts
+ Given that I have course with 4 problems, 1 staff member, and 3 students
+ When I enable cohorts in the course
+ And I create two content groups, Content Group A, and Content Group B, in the course
+ And I link one problem to Content Group A
+ And I link one problem to Content Group B
+ And I link one problem to both Content Group A and Content Group B
+ And one problem remains unlinked to any Content Group
+ And I create two manual cohorts, Cohort A and Cohort B,
+ linked to Content Group A and Content Group B, respectively
+ And I assign one student to each manual cohort
+ And one student remains in the default cohort
+ Then the staff member can see all 4 problems
+ And the student in Cohort A can see all the problems except the one linked to Content Group B
+ And the student in Cohort B can see all the problems except the one linked to Content Group A
+ And the student in the default cohort can ony see the problem that is unlinked to any Content Group
+ """
+ self.enable_cohorts_in_course()
+ self.create_content_groups()
+ self.link_problems_to_content_groups_and_publish()
+ self.create_cohorts_and_assign_students()
+ self.view_cohorted_content_as_different_users()