diff --git a/cms/djangoapps/contentstore/courseware_index.py b/cms/djangoapps/contentstore/courseware_index.py index 5f4f48dcb6..78cb90352f 100644 --- a/cms/djangoapps/contentstore/courseware_index.py +++ b/cms/djangoapps/contentstore/courseware_index.py @@ -153,6 +153,12 @@ class SearchIndexerBase(object): # list - those are ready to be destroyed indexed_items = set() + def get_item_location(item): + """ + Gets the version agnostic item location + """ + return item.location.version_agnostic().replace(branch=None) + def index_item(item, skip_index=False, groups_usage_info=None): """ Add this item to the search index and indexed_items list @@ -175,8 +181,25 @@ class SearchIndexerBase(object): return item_content_groups = None + + if item.category == "split_test": + split_partition = item.get_selected_partition() + for split_test_child in item.get_children(): + if split_partition: + for group in split_partition.groups: + group_id = unicode(group.id) + child_location = item.group_id_to_child.get(group_id, None) + if child_location == split_test_child.location: + groups_usage_info.update({ + unicode(get_item_location(split_test_child)): [group_id], + }) + for component in split_test_child.get_children(): + groups_usage_info.update({ + unicode(get_item_location(component)): [group_id] + }) + if groups_usage_info: - item_location = item.location.version_agnostic().replace(branch=None) + item_location = get_item_location(item) item_content_groups = groups_usage_info.get(unicode(item_location), None) item_id = unicode(cls._id_modifier(item.scope_ids.usage_id)) diff --git a/cms/djangoapps/contentstore/tests/test_courseware_index.py b/cms/djangoapps/contentstore/tests/test_courseware_index.py index c45ba256db..520a01a4f9 100644 --- a/cms/djangoapps/contentstore/tests/test_courseware_index.py +++ b/cms/djangoapps/contentstore/tests/test_courseware_index.py @@ -952,10 +952,20 @@ class GroupConfigurationSearchMongo(CourseTestCase, MixedWithOptionsTestCase): Tests indexing of content groups on course modules using mongo modulestore. """ MODULESTORE = TEST_DATA_MONGO_MODULESTORE + INDEX_NAME = CoursewareSearchIndexer.INDEX_NAME def setUp(self): super(GroupConfigurationSearchMongo, self).setUp() + self._setup_course_with_content() + self._setup_split_test_module() + self._setup_content_groups() + self.reload_course() + + def _setup_course_with_content(self): + """ + Set up course with html content in it. + """ self.chapter = ItemFactory.create( parent_location=self.course.location, category='chapter', @@ -964,6 +974,7 @@ class GroupConfigurationSearchMongo(CourseTestCase, MixedWithOptionsTestCase): publish_item=True, start=datetime(2015, 3, 1, tzinfo=UTC), ) + self.sequential = ItemFactory.create( parent_location=self.chapter.location, category='sequential', @@ -972,6 +983,16 @@ class GroupConfigurationSearchMongo(CourseTestCase, MixedWithOptionsTestCase): publish_item=True, start=datetime(2015, 3, 1, tzinfo=UTC), ) + + self.sequential2 = ItemFactory.create( + parent_location=self.chapter.location, + category='sequential', + display_name="Lesson 2", + modulestore=self.store, + publish_item=True, + start=datetime(2015, 3, 1, tzinfo=UTC), + ) + self.vertical = ItemFactory.create( parent_location=self.sequential.location, category='vertical', @@ -990,6 +1011,15 @@ class GroupConfigurationSearchMongo(CourseTestCase, MixedWithOptionsTestCase): start=datetime(2015, 4, 1, tzinfo=UTC), ) + self.vertical3 = ItemFactory.create( + parent_location=self.sequential2.location, + category='vertical', + display_name='Subsection 3', + modulestore=self.store, + publish_item=True, + start=datetime(2015, 4, 1, tzinfo=UTC), + ) + # unspecified start - should inherit from container self.html_unit1 = ItemFactory.create( parent_location=self.vertical.location, @@ -1018,7 +1048,75 @@ class GroupConfigurationSearchMongo(CourseTestCase, MixedWithOptionsTestCase): ) self.html_unit3.parent = self.vertical2 - groups_list = { + def _setup_split_test_module(self): + """ + Set up split test module. + """ + c0_url = self.course.id.make_usage_key("vertical", "condition_0_vertical") + c1_url = self.course.id.make_usage_key("vertical", "condition_1_vertical") + c2_url = self.course.id.make_usage_key("vertical", "condition_2_vertical") + + self.split_test_unit = ItemFactory.create( + parent_location=self.vertical3.location, + category='split_test', + user_partition_id=0, + display_name="Test Content Experiment 1", + group_id_to_child={"2": c0_url, "3": c1_url, "4": c2_url} + ) + + self.condition_0_vertical = ItemFactory.create( + parent_location=self.split_test_unit.location, + category="vertical", + display_name="Group ID 2", + location=c0_url, + ) + self.condition_0_vertical.parent = self.vertical3 + + self.condition_1_vertical = ItemFactory.create( + parent_location=self.split_test_unit.location, + category="vertical", + display_name="Group ID 3", + location=c1_url, + ) + self.condition_1_vertical.parent = self.vertical3 + + self.condition_2_vertical = ItemFactory.create( + parent_location=self.split_test_unit.location, + category="vertical", + display_name="Group ID 4", + location=c2_url, + ) + self.condition_2_vertical.parent = self.vertical3 + + self.html_unit4 = ItemFactory.create( + parent_location=self.condition_0_vertical.location, + category="html", + display_name="Split A", + publish_item=True, + ) + self.html_unit4.parent = self.condition_0_vertical + + self.html_unit5 = ItemFactory.create( + parent_location=self.condition_1_vertical.location, + category="html", + display_name="Split B", + publish_item=True, + ) + self.html_unit5.parent = self.condition_1_vertical + + self.html_unit6 = ItemFactory.create( + parent_location=self.condition_2_vertical.location, + category="html", + display_name="Split C", + publish_item=True, + ) + self.html_unit6.parent = self.condition_2_vertical + + def _setup_content_groups(self): + """ + Set up cohort and experiment content groups. + """ + cohort_groups_list = { u'id': 666, u'name': u'Test name', u'scheme': u'cohort', @@ -1029,18 +1127,33 @@ class GroupConfigurationSearchMongo(CourseTestCase, MixedWithOptionsTestCase): {u'id': 1, u'name': u'Group B', u'version': 1, u'usage': []}, ], } + experiment_groups_list = { + u'id': 0, + u'name': u'Experiment aware partition', + u'scheme': u'random', + u'description': u'Experiment aware description', + u'version': UserPartition.VERSION, + u'groups': [ + {u'id': 2, u'name': u'Group A', u'version': 1, u'usage': []}, + {u'id': 3, u'name': u'Group B', u'version': 1, u'usage': []}, + {u'id': 4, u'name': u'Group C', u'version': 1, u'usage': []} + ], + } self.client.put( self._group_conf_url(cid=666), - data=json.dumps(groups_list), + data=json.dumps(cohort_groups_list), + content_type="application/json", + HTTP_ACCEPT="application/json", + HTTP_X_REQUESTED_WITH="XMLHttpRequest", + ) + self.client.put( + self._group_conf_url(cid=0), + data=json.dumps(experiment_groups_list), content_type="application/json", HTTP_ACCEPT="application/json", HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) - - self.reload_course() - - INDEX_NAME = CoursewareSearchIndexer.INDEX_NAME def _group_conf_url(self, cid=-1): """ @@ -1075,6 +1188,52 @@ class GroupConfigurationSearchMongo(CourseTestCase, MixedWithOptionsTestCase): } ) + def _html_experiment_group_result(self, html_unit, content_groups): + """ + Return call object with arguments and content group for html_unit. + """ + return call( + 'courseware_content', + { + 'course_name': unicode(self.course.display_name), + 'id': unicode(html_unit.location), + 'content': {'html_content': '', 'display_name': unicode(html_unit.display_name)}, + 'course': unicode(self.course.id), + 'location': [ + unicode(self.chapter.display_name), + unicode(self.sequential2.display_name), + unicode(self.vertical3.display_name) + ], + 'content_type': 'Text', + 'org': self.course.org, + 'content_groups': content_groups, + 'start_date': datetime(2015, 4, 1, 0, 0, tzinfo=tzutc()) + } + ) + + def _vertical_experiment_group_result(self, vertical, content_groups): + """ + Return call object with arguments and content group for split_test vertical. + """ + return call( + 'courseware_content', + { + 'start_date': datetime(2015, 4, 1, 0, 0, tzinfo=tzutc()), + 'content': {'display_name': unicode(vertical.display_name)}, + 'course': unicode(self.course.id), + 'location': [ + unicode(self.chapter.display_name), + unicode(self.sequential2.display_name), + unicode(vertical.parent.display_name) + ], + 'content_type': 'Sequence', + 'content_groups': content_groups, + 'id': unicode(vertical.location), + 'course_name': unicode(self.course.display_name), + 'org': self.course.org + } + ) + def _html_nogroup_result(self, html_unit): """ Return call object with arguments and content group set to empty array for html_unit. @@ -1107,9 +1266,9 @@ class GroupConfigurationSearchMongo(CourseTestCase, MixedWithOptionsTestCase): # Only published modules should be in the index added_to_index = self.reindex_course(self.store) - self.assertEqual(added_to_index, 7) + self.assertEqual(added_to_index, 16) response = self.searcher.search(field_dictionary={"course": unicode(self.course.id)}) - self.assertEqual(response["total"], 8) + self.assertEqual(response["total"], 23) group_access_content = {'group_access': {666: [1]}} @@ -1119,11 +1278,52 @@ class GroupConfigurationSearchMongo(CourseTestCase, MixedWithOptionsTestCase): ) self.publish_item(self.store, self.html_unit1.location) + self.publish_item(self.store, self.split_test_unit.location) with patch(settings.SEARCH_ENGINE + '.index') as mock_index: self.reindex_course(self.store) self.assertTrue(mock_index.called) self.assertIn(self._html_group_result(self.html_unit1, [1]), mock_index.mock_calls) + self.assertIn(self._html_experiment_group_result(self.html_unit4, [unicode(2)]), mock_index.mock_calls) + self.assertIn(self._html_experiment_group_result(self.html_unit5, [unicode(3)]), mock_index.mock_calls) + self.assertIn(self._html_experiment_group_result(self.html_unit6, [unicode(4)]), mock_index.mock_calls) + self.assertNotIn(self._html_experiment_group_result(self.html_unit6, [unicode(5)]), mock_index.mock_calls) + self.assertIn( + self._vertical_experiment_group_result(self.condition_0_vertical, [unicode(2)]), + mock_index.mock_calls + ) + self.assertNotIn( + self._vertical_experiment_group_result(self.condition_1_vertical, [unicode(2)]), + mock_index.mock_calls + ) + self.assertNotIn( + self._vertical_experiment_group_result(self.condition_2_vertical, [unicode(2)]), + mock_index.mock_calls + ) + self.assertNotIn( + self._vertical_experiment_group_result(self.condition_0_vertical, [unicode(3)]), + mock_index.mock_calls + ) + self.assertIn( + self._vertical_experiment_group_result(self.condition_1_vertical, [unicode(3)]), + mock_index.mock_calls + ) + self.assertNotIn( + self._vertical_experiment_group_result(self.condition_2_vertical, [unicode(3)]), + mock_index.mock_calls + ) + self.assertNotIn( + self._vertical_experiment_group_result(self.condition_0_vertical, [unicode(4)]), + mock_index.mock_calls + ) + self.assertNotIn( + self._vertical_experiment_group_result(self.condition_1_vertical, [unicode(4)]), + mock_index.mock_calls + ) + self.assertIn( + self._vertical_experiment_group_result(self.condition_2_vertical, [unicode(4)]), + mock_index.mock_calls + ) mock_index.reset_mock() def test_content_group_not_assigned(self): diff --git a/common/test/acceptance/tests/lms/test_lms_split_test_courseware_search.py b/common/test/acceptance/tests/lms/test_lms_split_test_courseware_search.py new file mode 100644 index 0000000000..57bfac76e5 --- /dev/null +++ b/common/test/acceptance/tests/lms/test_lms_split_test_courseware_search.py @@ -0,0 +1,170 @@ +""" +Test courseware search +""" +import os +import json + +from ...pages.common.logout import LogoutPage +from ...pages.studio.overview import CourseOutlinePage +from ...pages.lms.courseware_search import CoursewareSearchPage +from ...pages.lms.course_nav import CourseNavPage +from ...fixtures.course import XBlockFixtureDesc +from ..helpers import create_user_partition_json + +from xmodule.partitions.partitions import Group + +from nose.plugins.attrib import attr + +from ..studio.base_studio_test import ContainerBase + +from ...pages.studio.auto_auth import AutoAuthPage as StudioAutoAuthPage + + +@attr('shard_1') +class SplitTestCoursewareSearchTest(ContainerBase): + """ + Test courseware search on Split Test Module. + """ + USERNAME = 'STUDENT_TESTER' + EMAIL = 'student101@example.com' + + TEST_INDEX_FILENAME = "test_root/index_file.dat" + + def setUp(self, is_staff=True): + """ + Create search page and course content to search + """ + # create test file in which index for this test will live + with open(self.TEST_INDEX_FILENAME, "w+") as index_file: + json.dump({}, index_file) + + super(SplitTestCoursewareSearchTest, self).setUp(is_staff=is_staff) + self.staff_user = self.user + + self.courseware_search_page = CoursewareSearchPage(self.browser, self.course_id) + self.course_navigation_page = CourseNavPage(self.browser) + self.course_outline = CourseOutlinePage( + self.browser, + self.course_info['org'], + self.course_info['number'], + self.course_info['run'] + ) + + self._add_and_configure_split_test() + self._studio_reindex() + + def tearDown(self): + super(SplitTestCoursewareSearchTest, self).tearDown() + os.remove(self.TEST_INDEX_FILENAME) + + def _auto_auth(self, username, email, staff): + """ + Logout and login with given credentials. + """ + LogoutPage(self.browser).visit() + StudioAutoAuthPage(self.browser, username=username, email=email, + course_id=self.course_id, staff=staff).visit() + + def _studio_reindex(self): + """ + Reindex course content on studio course page + """ + self._auto_auth(self.staff_user["username"], self.staff_user["email"], True) + self.course_outline.visit() + self.course_outline.start_reindex() + self.course_outline.wait_for_ajax() + + def _add_and_configure_split_test(self): + """ + Add a split test and a configuration to a test course fixture + """ + # Create a new group configurations + # pylint: disable=W0212 + self.course_fixture._update_xblock(self.course_fixture._course_location, { + "metadata": { + u"user_partitions": [ + create_user_partition_json( + 0, + "Name", + "Description.", + [Group("0", "Group A"), Group("1", "Group B")] + ), + create_user_partition_json( + 456, + "Name 2", + "Description 2.", + [Group("2", "Group C"), Group("3", "Group D")] + ), + ], + }, + }) + + # Add a split test module to the 'Test Unit' vertical in the course tree + split_test_1 = XBlockFixtureDesc('split_test', 'Test Content Experiment 1', metadata={'user_partition_id': 0}) + split_test_1_parent_vertical = self.course_fixture.get_nested_xblocks(category="vertical")[1] + self.course_fixture.create_xblock(split_test_1_parent_vertical.locator, split_test_1) + + # Add a split test module to the 'Test 2 Unit' vertical in the course tree + split_test_2 = XBlockFixtureDesc('split_test', 'Test Content Experiment 2', metadata={'user_partition_id': 456}) + split_test_2_parent_vertical = self.course_fixture.get_nested_xblocks(category="vertical")[2] + self.course_fixture.create_xblock(split_test_2_parent_vertical.locator, split_test_2) + + def populate_course_fixture(self, course_fixture): + """ + Populate the children of the test course fixture. + """ + course_fixture.add_advanced_settings({ + u"advanced_modules": {"value": ["split_test"]}, + }) + + course_fixture.add_children( + XBlockFixtureDesc('chapter', 'Content Section').add_children( + XBlockFixtureDesc('sequential', 'Content Subsection').add_children( + XBlockFixtureDesc('vertical', 'Content Unit').add_children( + XBlockFixtureDesc('html', 'VISIBLETOALLCONTENT', data='VISIBLETOALLCONTENT') + ) + ) + ), + XBlockFixtureDesc('chapter', 'Test Section').add_children( + XBlockFixtureDesc('sequential', 'Test Subsection').add_children( + XBlockFixtureDesc('vertical', 'Test Unit') + ) + ), + XBlockFixtureDesc('chapter', 'X Section').add_children( + XBlockFixtureDesc('sequential', 'X Subsection').add_children( + XBlockFixtureDesc('vertical', 'X Unit') + ) + ), + ) + + self.test_1_breadcrumb = "Test Section \xe2\x96\xb8 Test Subsection \xe2\x96\xb8 Test Unit".decode("utf-8") + self.test_2_breadcrumb = "X Section \xe2\x96\xb8 X Subsection \xe2\x96\xb8 X Unit".decode("utf-8") + + def test_page_existence(self): + """ + Make sure that the page is accessible. + """ + self._auto_auth(self.USERNAME, self.EMAIL, False) + self.courseware_search_page.visit() + + def test_search_for_experiment_content_user_not_assigned(self): + """ + Test user can't search for experiment content if not assigned to a group. + """ + self._auto_auth(self.USERNAME, self.EMAIL, False) + self.courseware_search_page.visit() + self.courseware_search_page.search_for_term("Group") + assert "Sorry, no results were found." in self.courseware_search_page.search_results.html[0] + + def test_search_for_experiment_content_user_assigned_to_one_group(self): + """ + Test user can search for experiment content restricted to his group + when assigned to just one experiment group + """ + self._auto_auth(self.USERNAME, self.EMAIL, False) + self.courseware_search_page.visit() + self.course_navigation_page.go_to_section("Test Section", "Test Subsection") + self.courseware_search_page.search_for_term("Group") + assert "1 result" in self.courseware_search_page.search_results.html[0] + assert self.test_1_breadcrumb in self.courseware_search_page.search_results.html[0] + assert self.test_2_breadcrumb not in self.courseware_search_page.search_results.html[0] diff --git a/lms/lib/courseware_search/lms_filter_generator.py b/lms/lib/courseware_search/lms_filter_generator.py index 648c37015d..43db88b6b8 100644 --- a/lms/lib/courseware_search/lms_filter_generator.py +++ b/lms/lib/courseware_search/lms_filter_generator.py @@ -8,37 +8,88 @@ from student.models import CourseEnrollment from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.locations import SlashSeparatedCourseKey +from xmodule.modulestore.django import modulestore from search.filter_generator import SearchFilterGenerator -from openedx.core.djangoapps.course_groups.partition_scheme import get_cohorted_user_partition +from openedx.core.djangoapps.user_api.partition_schemes import RandomUserPartitionScheme +from openedx.core.djangoapps.course_groups.partition_scheme import CohortPartitionScheme from courseware.access import get_user_role +INCLUDE_SCHEMES = [CohortPartitionScheme, RandomUserPartitionScheme, ] +SCHEME_SUPPORTS_ASSIGNMENT = [RandomUserPartitionScheme, ] + + class LmsSearchFilterGenerator(SearchFilterGenerator): """ SearchFilterGenerator for LMS Search """ + _user_enrollments = {} + + def _enrollments_for_user(self, user): + """ Return the specified user's course enrollments """ + if user not in self._user_enrollments: + self._user_enrollments[user] = CourseEnrollment.enrollments_for_user(user) + return self._user_enrollments[user] + def filter_dictionary(self, **kwargs): - """ base implementation which filters via start_date """ - filter_dictionary = super(LmsSearchFilterGenerator, self).filter_dictionary(**kwargs) - if 'user' in kwargs and 'course_id' in kwargs and kwargs['course_id']: - user = kwargs['user'] - try: - course_key = CourseKey.from_string(kwargs['course_id']) - except InvalidKeyError: - course_key = SlashSeparatedCourseKey.from_deprecated_string(kwargs['course_id']) + """ LMS implementation, adds filtering by user partition, course id and user """ - # Staff user looking at course as staff user - if get_user_role(user, course_key) == 'staff': - return filter_dictionary - - cohorted_user_partition = get_cohorted_user_partition(course_key) - if cohorted_user_partition: - partition_group = cohorted_user_partition.scheme.get_group_for_user( + def get_group_for_user_partition(user_partition, course_key, user): + """ Returns the specified user's group for user partition """ + if user_partition.scheme in SCHEME_SUPPORTS_ASSIGNMENT: + return user_partition.scheme.get_group_for_user( course_key, user, - cohorted_user_partition, + user_partition, + assign=False, ) - filter_dictionary['content_groups'] = unicode(partition_group.id) if partition_group else None + else: + return user_partition.scheme.get_group_for_user( + course_key, + user, + user_partition, + ) + + def get_group_ids_for_user(course, user): + """ Collect user partition group ids for user for this course """ + partition_groups = [] + for user_partition in course.user_partitions: + if user_partition.scheme in INCLUDE_SCHEMES: + group = get_group_for_user_partition(user_partition, course.id, user) + if group: + partition_groups.append(group) + partition_group_ids = [unicode(partition_group.id) for partition_group in partition_groups] + return partition_group_ids if partition_group_ids else None + + filter_dictionary = super(LmsSearchFilterGenerator, self).filter_dictionary(**kwargs) + if 'user' in kwargs: + user = kwargs['user'] + + if 'course_id' in kwargs and kwargs['course_id']: + try: + course_key = CourseKey.from_string(kwargs['course_id']) + except InvalidKeyError: + course_key = SlashSeparatedCourseKey.from_deprecated_string(kwargs['course_id']) + + # Staff user looking at course as staff user + if get_user_role(user, course_key) == 'staff': + return filter_dictionary + # Need to check course exist (if course gets deleted enrollments don't get cleaned up) + course = modulestore().get_course(course_key) + if course: + filter_dictionary['content_groups'] = get_group_ids_for_user(course, user) + else: + user_enrollments = self._enrollments_for_user(user) + content_groups = [] + for enrollment in user_enrollments: + course = modulestore().get_course(enrollment.course_id) + if course: + enrollment_group_ids = get_group_ids_for_user(course, user) + if enrollment_group_ids: + content_groups.extend(enrollment_group_ids) + + filter_dictionary['content_groups'] = content_groups if content_groups else None + return filter_dictionary def field_dictionary(self, **kwargs): @@ -47,7 +98,7 @@ class LmsSearchFilterGenerator(SearchFilterGenerator): if not kwargs.get('user'): field_dictionary['course'] = [] elif not kwargs.get('course_id'): - user_enrollments = CourseEnrollment.enrollments_for_user(kwargs['user']) + user_enrollments = self._enrollments_for_user(kwargs['user']) field_dictionary['course'] = [unicode(enrollment.course_id) for enrollment in user_enrollments] # if we have an org filter, only include results for this org filter @@ -62,8 +113,9 @@ class LmsSearchFilterGenerator(SearchFilterGenerator): exclude_dictionary = super(LmsSearchFilterGenerator, self).exclude_dictionary(**kwargs) course_org_filter = microsite.get_value('course_org_filter') # If we have a course filter we are ensuring that we only get those courses above - org_filter_out_set = microsite.get_all_orgs() - if not course_org_filter and org_filter_out_set: - exclude_dictionary['org'] = list(org_filter_out_set) + if not course_org_filter: + org_filter_out_set = microsite.get_all_orgs() + if org_filter_out_set: + exclude_dictionary['org'] = list(org_filter_out_set) return exclude_dictionary diff --git a/lms/lib/courseware_search/lms_result_processor.py b/lms/lib/courseware_search/lms_result_processor.py index 78625661e1..accb8d44a4 100644 --- a/lms/lib/courseware_search/lms_result_processor.py +++ b/lms/lib/courseware_search/lms_result_processor.py @@ -62,9 +62,10 @@ class LmsSearchResultProcessor(SearchResultProcessor): def should_remove(self, user): """ Test to see if this result should be removed due to access restriction """ - return not has_access( + user_has_access = has_access( user, "load", self.get_item(self.get_usage_key()), self.get_course_key() ) + return not user_has_access diff --git a/lms/lib/courseware_search/test/test_lms_filter_generator.py b/lms/lib/courseware_search/test/test_lms_filter_generator.py index 2d7a5dde4c..3e18bf5ac4 100644 --- a/lms/lib/courseware_search/test/test_lms_filter_generator.py +++ b/lms/lib/courseware_search/test/test_lms_filter_generator.py @@ -11,10 +11,12 @@ from student.models import CourseEnrollment from xmodule.partitions.partitions import Group, UserPartition from openedx.core.djangoapps.course_groups.partition_scheme import CohortPartitionScheme +from openedx.core.djangoapps.user_api.partition_schemes import RandomUserPartitionScheme from openedx.core.djangoapps.course_groups.tests.helpers import CohortFactory, config_course_cohorts from openedx.core.djangoapps.course_groups.cohorts import add_user_to_cohort from openedx.core.djangoapps.course_groups.views import link_cohort_to_partition_group from opaque_keys import InvalidKeyError +from opaque_keys.edx.keys import CourseKey from lms.lib.courseware_search.lms_filter_generator import LmsSearchFilterGenerator @@ -49,6 +51,13 @@ class LmsSearchFilterGeneratorTestCase(ModuleStoreTestCase): publish_item=True, ) + self.chapter2 = ItemFactory.create( + parent_location=self.courses[1].location, + category='chapter', + display_name="Week 1", + publish_item=True, + ) + self.groups = [Group(1, 'Group 1'), Group(2, 'Group 2')] self.content_groups = [1, 2] @@ -56,64 +65,11 @@ class LmsSearchFilterGeneratorTestCase(ModuleStoreTestCase): def setUp(self): super(LmsSearchFilterGeneratorTestCase, self).setUp() self.build_courses() - self.user_partition = None - self.first_cohort = None - self.second_cohort = None self.user = UserFactory.create(username="jack", email="jack@fake.edx.org", password='test') for course in self.courses: CourseEnrollment.enroll(self.user, course.location.course_key) - def add_seq_with_content_groups(self, groups=None): - """ - Adds sequential and two content groups to first course in courses list. - """ - config_course_cohorts(self.courses[0], is_cohorted=True) - - if groups is None: - groups = self.groups - - self.user_partition = UserPartition( - id=0, - name='Partition 1', - description='This is partition 1', - groups=groups, - scheme=CohortPartitionScheme - ) - - self.user_partition.scheme.name = "cohort" - - ItemFactory.create( - parent_location=self.chapter.location, - category='sequential', - display_name="Lesson 1", - publish_item=True, - metadata={u"user_partitions": [self.user_partition.to_json()]} - ) - - self.first_cohort, self.second_cohort = [ - CohortFactory(course_id=self.courses[0].id) for _ in range(2) - ] - - self.courses[0].user_partitions = [self.user_partition] - self.courses[0].save() - modulestore().update_item(self.courses[0], self.user.id) - - def add_user_to_cohort_group(self): - """ - adds user to cohort and links cohort to content group - """ - add_user_to_cohort(self.first_cohort, self.user.username) - - link_cohort_to_partition_group( - self.first_cohort, - self.user_partition.id, - self.groups[0].id, - ) - - self.courses[0].save() - modulestore().update_item(self.courses[0], self.user.id) - def test_course_id_not_provided(self): """ Tests that we get the list of IDs of courses the user is enrolled in when the course ID is null or not provided @@ -191,6 +147,152 @@ class LmsSearchFilterGeneratorTestCase(ModuleStoreTestCase): self.assertIn('org', field_dictionary) self.assertEqual('TestMicrosite3', field_dictionary['org']) + +class LmsSearchFilterGeneratorGroupsTestCase(LmsSearchFilterGeneratorTestCase): + """ + Test case class to test search result processor + with content and user groups present within the course + """ + + def setUp(self): + super(LmsSearchFilterGeneratorGroupsTestCase, self).setUp() + self.user_partition = None + self.split_test_user_partition = None + self.first_cohort = None + self.second_cohort = None + + def add_seq_with_content_groups(self, groups=None): + """ + Adds sequential and two content groups to first course in courses list. + """ + config_course_cohorts(self.courses[0], is_cohorted=True) + + if groups is None: + groups = self.groups + + self.user_partition = UserPartition( + id=0, + name='Partition 1', + description='This is partition 1', + groups=groups, + scheme=CohortPartitionScheme + ) + + self.user_partition.scheme.name = "cohort" + + ItemFactory.create( + parent_location=self.chapter.location, + category='sequential', + display_name="Lesson 1", + publish_item=True, + metadata={u"user_partitions": [self.user_partition.to_json()]} + ) + + self.first_cohort, self.second_cohort = [ + CohortFactory(course_id=self.courses[0].id) for _ in range(2) + ] + + self.courses[0].user_partitions = [self.user_partition] + self.courses[0].save() + modulestore().update_item(self.courses[0], self.user.id) + + def add_user_to_cohort_group(self): + """ + adds user to cohort and links cohort to content group + """ + add_user_to_cohort(self.first_cohort, self.user.username) + + link_cohort_to_partition_group( + self.first_cohort, + self.user_partition.id, + self.groups[0].id, + ) + + self.courses[0].save() + modulestore().update_item(self.courses[0], self.user.id) + + def add_split_test(self, groups=None): + """ + Adds split test and two content groups to second course in courses list. + """ + if groups is None: + groups = self.groups + + self.split_test_user_partition = UserPartition( + id=0, + name='Partition 2', + description='This is partition 2', + groups=groups, + scheme=RandomUserPartitionScheme + ) + + self.split_test_user_partition.scheme.name = "random" + + sequential = ItemFactory.create( + parent_location=self.chapter.location, + category='sequential', + display_name="Lesson 2", + publish_item=True, + ) + + vertical = ItemFactory.create( + parent_location=sequential.location, + category='vertical', + display_name='Subsection 3', + publish_item=True, + ) + + split_test_unit = ItemFactory.create( + parent_location=vertical.location, + category='split_test', + user_partition_id=0, + display_name="Test Content Experiment 1", + ) + + condition_1_vertical = ItemFactory.create( + parent_location=split_test_unit.location, + category="vertical", + display_name="Group ID 1", + ) + + condition_2_vertical = ItemFactory.create( + parent_location=split_test_unit.location, + category="vertical", + display_name="Group ID 2", + ) + + ItemFactory.create( + parent_location=condition_1_vertical.location, + category="html", + display_name="Group A", + publish_item=True, + ) + + ItemFactory.create( + parent_location=condition_2_vertical.location, + category="html", + display_name="Group B", + publish_item=True, + ) + + self.courses[1].user_partitions = [self.split_test_user_partition] + self.courses[1].save() + modulestore().update_item(self.courses[1], self.user.id) + + def add_user_to_splittest_group(self): + """ + adds user to a random split test group + """ + self.split_test_user_partition.scheme.get_group_for_user( + CourseKey.from_string(unicode(self.courses[1].id)), + self.user, + self.split_test_user_partition, + assign=True, + ) + + self.courses[1].save() + modulestore().update_item(self.courses[1], self.user.id) + def test_content_group_id_provided(self): """ Tests that we get the content group ID when course is assigned to cohort @@ -205,7 +307,7 @@ class LmsSearchFilterGeneratorTestCase(ModuleStoreTestCase): self.assertTrue('start_date' in filter_dictionary) self.assertEqual(unicode(self.courses[0].id), field_dictionary['course']) - self.assertEqual(unicode(self.content_groups[0]), filter_dictionary['content_groups']) + self.assertEqual([unicode(self.content_groups[0])], filter_dictionary['content_groups']) def test_content_multiple_groups_id_provided(self): """ @@ -233,7 +335,7 @@ class LmsSearchFilterGeneratorTestCase(ModuleStoreTestCase): self.assertTrue('start_date' in filter_dictionary) self.assertEqual(unicode(self.courses[0].id), field_dictionary['course']) # returns only first group, relevant to current user - self.assertEqual(unicode(self.content_groups[0]), filter_dictionary['content_groups']) + self.assertEqual([unicode(self.content_groups[0])], filter_dictionary['content_groups']) def test_content_group_id_not_provided(self): """ @@ -266,6 +368,44 @@ class LmsSearchFilterGeneratorTestCase(ModuleStoreTestCase): self.assertEqual(unicode(self.courses[0].id), field_dictionary['course']) self.assertEqual(None, filter_dictionary['content_groups']) + def test_split_test_with_user_groups_user_not_assigned(self): + """ + Tests that we don't get user group ID when user is not assigned to a split test group + """ + self.add_split_test() + + field_dictionary, filter_dictionary, _ = LmsSearchFilterGenerator.generate_field_filters( + user=self.user, + course_id=unicode(self.courses[1].id) + ) + + self.assertTrue('start_date' in filter_dictionary) + self.assertEqual(unicode(self.courses[1].id), field_dictionary['course']) + self.assertEqual(None, filter_dictionary['content_groups']) + + def test_split_test_with_user_groups_user_assigned(self): + """ + Tests that we get user group ID when user is assigned to a split test group + """ + self.add_split_test() + self.add_user_to_splittest_group() + + field_dictionary, filter_dictionary, _ = LmsSearchFilterGenerator.generate_field_filters( + user=self.user, + course_id=unicode(self.courses[1].id) + ) + + partition_group = self.split_test_user_partition.scheme.get_group_for_user( + CourseKey.from_string(unicode(self.courses[1].id)), + self.user, + self.split_test_user_partition, + assign=False, + ) + + self.assertTrue('start_date' in filter_dictionary) + self.assertEqual(unicode(self.courses[1].id), field_dictionary['course']) + self.assertEqual([unicode(partition_group.id)], filter_dictionary['content_groups']) + def test_invalid_course_key(self): """ Test system raises an error if no course found. diff --git a/lms/lib/courseware_search/test/test_lms_search_initializer.py b/lms/lib/courseware_search/test/test_lms_search_initializer.py index c1508b37fd..fac3e08172 100644 --- a/lms/lib/courseware_search/test/test_lms_search_initializer.py +++ b/lms/lib/courseware_search/test/test_lms_search_initializer.py @@ -97,7 +97,7 @@ class LmsSearchInitializerTestCase(StaffMasqueradeTestCase): user=self.global_staff, course_id=unicode(self.course.id) ) - self.assertEqual(filter_directory['content_groups'], unicode(1)) + self.assertEqual(filter_directory['content_groups'], [unicode(1)]) def test_staff_masquerading_as_a_staff_user(self): """