SOL-536 Experiment-Aware content search
This commit is contained in:
committed by
Dino Cikatic
parent
0d975674c2
commit
d3aa9dfd87
@@ -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))
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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='<html>VISIBLETOALLCONTENT</html>')
|
||||
)
|
||||
)
|
||||
),
|
||||
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]
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user