feat!: remove most Old Mongo functionality (#31134)
This commit leaves behind just enough Old Mongo (DraftModulestore) functionality to allow read-only access to static assets and the root CourseBlock. It removes: * create/update operations * child/parent traversal * inheritance related code It also removes or converts tests for this functionality. The ability to read from the root CourseBlock was maintained for backwards compatibility, since top-level course settings are often stored here, and this is used by various parts of the codebase, like displaying dashboards and re-building CourseOverview models. Any attempt to read the contents of a course by getting the CourseBlock's children will return an empty list (i.e. it will look empty). This commit does _not_ delete content on MongoDB or run any sort of data migration or cleanup.
This commit is contained in:
@@ -4,6 +4,7 @@ or with filename which starts with "._")
|
||||
"""
|
||||
|
||||
|
||||
from unittest import skip
|
||||
from django.conf import settings
|
||||
from django.core.management import call_command
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
@@ -20,6 +21,9 @@ from xmodule.modulestore.xml_importer import import_course_from_xml
|
||||
TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
|
||||
|
||||
|
||||
@skip("OldMongo Deprecation")
|
||||
# This test worked only for Old Mongo
|
||||
# Can later be converted to work with Split
|
||||
class ExportAllCourses(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests assets cleanup for all courses.
|
||||
|
||||
@@ -4,7 +4,6 @@ Unittests for creating a course in an chosen modulestore
|
||||
|
||||
from io import StringIO
|
||||
|
||||
import ddt
|
||||
from django.core.management import CommandError, call_command
|
||||
from django.test import TestCase
|
||||
|
||||
@@ -40,27 +39,28 @@ class TestArgParsing(TestCase):
|
||||
call_command('create_course', "mongo", "fake@example.com", "org", "course", "run")
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestCreateCourse(ModuleStoreTestCase):
|
||||
"""
|
||||
Unit tests for creating a course in either old mongo or split mongo via command line
|
||||
"""
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
def test_all_stores_user_email(self, store):
|
||||
def test_all_stores_user_email(self):
|
||||
call_command(
|
||||
"create_course",
|
||||
store,
|
||||
ModuleStoreEnum.Type.split,
|
||||
str(self.user.email),
|
||||
"org", "course", "run", "dummy-course-name"
|
||||
)
|
||||
new_key = modulestore().make_course_key("org", "course", "run")
|
||||
self.assertTrue(
|
||||
modulestore().has_course(new_key),
|
||||
f"Could not find course in {store}"
|
||||
f"Could not find course in {ModuleStoreEnum.Type.split}"
|
||||
)
|
||||
# pylint: disable=protected-access
|
||||
self.assertEqual(store, modulestore()._get_modulestore_for_courselike(new_key).get_modulestore_type())
|
||||
self.assertEqual(
|
||||
ModuleStoreEnum.Type.split,
|
||||
modulestore()._get_modulestore_for_courselike(new_key).get_modulestore_type()
|
||||
)
|
||||
|
||||
def test_duplicate_course(self):
|
||||
"""
|
||||
@@ -85,8 +85,7 @@ class TestCreateCourse(ModuleStoreTestCase):
|
||||
expected = "Course already exists"
|
||||
self.assertIn(out.getvalue().strip(), expected)
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.split, ModuleStoreEnum.Type.mongo)
|
||||
def test_get_course_with_different_case(self, default_store):
|
||||
def test_get_course_with_different_case(self):
|
||||
"""
|
||||
Tests that course can not be accessed with different case.
|
||||
|
||||
@@ -98,21 +97,20 @@ class TestCreateCourse(ModuleStoreTestCase):
|
||||
org = 'org1'
|
||||
number = 'course1'
|
||||
run = 'run1'
|
||||
with self.store.default_store(default_store):
|
||||
lowercase_course_id = self.store.make_course_key(org, number, run)
|
||||
with self.store.bulk_operations(lowercase_course_id, ignore_case=True):
|
||||
# Create course with lowercase key & Verify that store returns course.
|
||||
self.store.create_course(
|
||||
lowercase_course_id.org,
|
||||
lowercase_course_id.course,
|
||||
lowercase_course_id.run,
|
||||
self.user.id
|
||||
)
|
||||
course = self.store.get_course(lowercase_course_id)
|
||||
self.assertIsNotNone(course, 'Course not found using lowercase course key.')
|
||||
self.assertEqual(str(course.id), str(lowercase_course_id))
|
||||
lowercase_course_id = self.store.make_course_key(org, number, run)
|
||||
with self.store.bulk_operations(lowercase_course_id, ignore_case=True):
|
||||
# Create course with lowercase key & Verify that store returns course.
|
||||
self.store.create_course(
|
||||
lowercase_course_id.org,
|
||||
lowercase_course_id.course,
|
||||
lowercase_course_id.run,
|
||||
self.user.id
|
||||
)
|
||||
course = self.store.get_course(lowercase_course_id)
|
||||
self.assertIsNotNone(course, 'Course not found using lowercase course key.')
|
||||
self.assertEqual(str(course.id), str(lowercase_course_id))
|
||||
|
||||
# Verify store does not return course with different case.
|
||||
uppercase_course_id = self.store.make_course_key(org.upper(), number.upper(), run.upper())
|
||||
course = self.store.get_course(uppercase_course_id)
|
||||
self.assertIsNone(course, 'Course should not be accessed with uppercase course id.')
|
||||
# Verify store does not return course with different case.
|
||||
uppercase_course_id = self.store.make_course_key(org.upper(), number.upper(), run.upper())
|
||||
course = self.store.get_course(uppercase_course_id)
|
||||
self.assertIsNone(course, 'Course should not be accessed with uppercase course id.')
|
||||
|
||||
@@ -5,6 +5,7 @@ Test for export all courses.
|
||||
|
||||
import shutil
|
||||
from tempfile import mkdtemp
|
||||
from unittest import skip
|
||||
|
||||
from cms.djangoapps.contentstore.management.commands.export_all_courses import export_courses_to_output_path
|
||||
from xmodule.modulestore import ModuleStoreEnum # lint-amnesty, pylint: disable=wrong-import-order
|
||||
@@ -13,6 +14,10 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase # lint-a
|
||||
from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, pylint: disable=wrong-import-order
|
||||
|
||||
|
||||
@skip("OldMongo Deprecation")
|
||||
# This test fails for split modulestre
|
||||
# AttributeError: 'MixedModuleStore' object has no attribute 'collection'
|
||||
# split module store has no 'collection' attribute.
|
||||
class ExportAllCourses(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests exporting all courses.
|
||||
|
||||
@@ -23,14 +23,6 @@ class TestFixNotFound(ModuleStoreTestCase):
|
||||
with self.assertRaisesRegex(CommandError, msg):
|
||||
call_command('fix_not_found')
|
||||
|
||||
def test_fix_not_found_non_split(self):
|
||||
"""
|
||||
The management command doesn't work on non split courses
|
||||
"""
|
||||
course = CourseFactory.create(default_store=ModuleStoreEnum.Type.mongo)
|
||||
with self.assertRaisesRegex(CommandError, "The owning modulestore does not support this command."):
|
||||
call_command("fix_not_found", str(course.id))
|
||||
|
||||
def test_fix_not_found(self):
|
||||
course = CourseFactory.create(default_store=ModuleStoreEnum.Type.split)
|
||||
BlockFactory.create(category='chapter', parent_location=course.location)
|
||||
|
||||
@@ -58,15 +58,6 @@ class TestForcePublish(SharedModuleStoreTestCase):
|
||||
with self.assertRaisesRegex(CommandError, errstring):
|
||||
call_command('force_publish', 'course-v1:org+course+run')
|
||||
|
||||
def test_force_publish_non_split(self):
|
||||
"""
|
||||
Test 'force_publish' command doesn't work on non split courses
|
||||
"""
|
||||
course = CourseFactory.create(default_store=ModuleStoreEnum.Type.mongo)
|
||||
errstring = 'The owning modulestore does not support this command.'
|
||||
with self.assertRaisesRegex(CommandError, errstring):
|
||||
call_command('force_publish', str(course.id))
|
||||
|
||||
|
||||
class TestForcePublishModifications(ModuleStoreTestCase):
|
||||
"""
|
||||
|
||||
@@ -11,7 +11,6 @@ from django.core.management import call_command
|
||||
from path import Path as path
|
||||
|
||||
from openedx.core.djangoapps.django_comment_common.utils import are_permissions_roles_seeded
|
||||
from xmodule.modulestore import ModuleStoreEnum # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
|
||||
|
||||
@@ -73,23 +72,3 @@ class TestImport(ModuleStoreTestCase):
|
||||
# Now load up the course with a similar course_id and verify it loads
|
||||
call_command('import', self.content_dir, self.course_dir)
|
||||
self.assertIsNotNone(store.get_course(self.truncated_key))
|
||||
|
||||
def test_existing_course_with_different_modulestore(self):
|
||||
"""
|
||||
Checks that a course that originally existed in old mongo can be re-imported when
|
||||
split is the default modulestore.
|
||||
"""
|
||||
with modulestore().default_store(ModuleStoreEnum.Type.mongo):
|
||||
call_command('import', self.content_dir, self.good_dir)
|
||||
|
||||
# Clear out the modulestore mappings, else when the next import command goes to create a destination
|
||||
# course_key, it will find the existing course and return the mongo course_key. To reproduce TNL-1362,
|
||||
# the destination course_key needs to be the one for split modulestore.
|
||||
modulestore().mappings = {}
|
||||
|
||||
with modulestore().default_store(ModuleStoreEnum.Type.split):
|
||||
call_command('import', self.content_dir, self.good_dir)
|
||||
course = modulestore().get_course(self.base_course_key)
|
||||
# With the bug, this fails because the chapter's course_key is the split mongo form,
|
||||
# while the course's course_key is the old mongo form.
|
||||
self.assertEqual(str(course.location.course_key), str(course.children[0].course_key))
|
||||
|
||||
@@ -7,9 +7,6 @@ from django.conf import settings
|
||||
from xblock.core import XBlock
|
||||
from xblock.fields import String
|
||||
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.mongo.draft import as_draft
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.xml_importer import import_course_from_xml
|
||||
|
||||
@@ -41,14 +38,6 @@ class XBlockImportTest(ModuleStoreTestCase):
|
||||
'set by xml'
|
||||
)
|
||||
|
||||
@XBlock.register_temp_plugin(StubXBlock)
|
||||
def test_import_draft(self):
|
||||
self._assert_import(
|
||||
'pure_xblock_draft',
|
||||
'set by xml',
|
||||
has_draft=True
|
||||
)
|
||||
|
||||
def _assert_import(self, course_dir, expected_field_val, has_draft=False):
|
||||
"""
|
||||
Import a course from XML, then verify that the XBlock was loaded
|
||||
@@ -66,22 +55,12 @@ class XBlockImportTest(ModuleStoreTestCase):
|
||||
"""
|
||||
# It is necessary to use the "old mongo" modulestore because split doesn't work
|
||||
# with the "has_draft" logic below.
|
||||
store = modulestore()._get_modulestore_by_type(ModuleStoreEnum.Type.mongo) # pylint: disable=protected-access
|
||||
courses = import_course_from_xml(
|
||||
store, self.user.id, TEST_DATA_DIR, [course_dir], create_if_not_present=True
|
||||
self.store, self.user.id, TEST_DATA_DIR, [course_dir], create_if_not_present=True
|
||||
)
|
||||
|
||||
xblock_location = courses[0].id.make_usage_key('stubxblock', 'xblock_test')
|
||||
|
||||
if has_draft:
|
||||
xblock_location = as_draft(xblock_location)
|
||||
|
||||
xblock = store.get_item(xblock_location)
|
||||
xblock = self.store.get_item(xblock_location)
|
||||
self.assertTrue(isinstance(xblock, StubXBlock))
|
||||
self.assertEqual(xblock.test_field, expected_field_val)
|
||||
|
||||
if has_draft:
|
||||
draft_xblock = store.get_item(xblock_location)
|
||||
self.assertTrue(getattr(draft_xblock, 'is_draft', False))
|
||||
self.assertTrue(isinstance(draft_xblock, StubXBlock))
|
||||
self.assertEqual(draft_xblock.test_field, expected_field_val)
|
||||
|
||||
@@ -1014,28 +1014,3 @@ class TestOverrides(LibraryTestCase):
|
||||
self.assertEqual(self.lc_block.source_library_version, duplicate.source_library_version)
|
||||
problem2_in_course = store.get_item(duplicate.children[0])
|
||||
self.assertEqual(problem2_in_course.display_name, self.original_display_name)
|
||||
|
||||
|
||||
class TestIncompatibleModuleStore(LibraryTestCase):
|
||||
"""
|
||||
Tests for proper validation errors with an incompatible course modulestore.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
# Create a course in an incompatible modulestore.
|
||||
with modulestore().default_store(ModuleStoreEnum.Type.mongo):
|
||||
self.course = CourseFactory.create()
|
||||
|
||||
# Add a LibraryContent block to the course:
|
||||
self.lc_block = self._add_library_content_block(self.course, self.lib_key)
|
||||
|
||||
def test_incompatible_modulestore(self):
|
||||
"""
|
||||
Verifies that, if a user is using a modulestore that doesn't support libraries,
|
||||
a validation error will be produced.
|
||||
"""
|
||||
validation = self.lc_block.validate()
|
||||
self.assertEqual(validation.summary.type, validation.summary.ERROR)
|
||||
self.assertIn(
|
||||
"This course does not support content libraries.", validation.summary.text)
|
||||
|
||||
@@ -12,7 +12,7 @@ from opaque_keys.edx.keys import AssetKey
|
||||
from xmodule.contentstore.django import contentstore
|
||||
from xmodule.modulestore.inheritance import own_metadata
|
||||
from xmodule.modulestore.split_mongo.split import SplitMongoModuleStore
|
||||
from xmodule.modulestore.tests.django_utils import TEST_DATA_MONGO_MODULESTORE, ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.django_utils import TEST_DATA_SPLIT_MODULESTORE, ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from xmodule.modulestore.tests.utils import ProceduralCourseTestMixin
|
||||
from xmodule.tests.test_transcripts_utils import YoutubeVideoHTMLResponse
|
||||
@@ -73,7 +73,7 @@ class CourseTestCase(ProceduralCourseTestMixin, ModuleStoreTestCase):
|
||||
Base class for Studio tests that require a logged in user and a course.
|
||||
Also provides helper methods for manipulating and verifying the course.
|
||||
"""
|
||||
MODULESTORE = TEST_DATA_MONGO_MODULESTORE
|
||||
MODULESTORE = TEST_DATA_SPLIT_MODULESTORE
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
@@ -123,7 +123,7 @@ class CourseTestCase(ProceduralCourseTestMixin, ModuleStoreTestCase):
|
||||
SEQUENTIAL = 'vertical_sequential'
|
||||
DRAFT_HTML = 'draft_html'
|
||||
DRAFT_VIDEO = 'draft_video'
|
||||
LOCKED_ASSET_KEY = AssetKey.from_string('/c4x/edX/toy/asset/sample_static.html')
|
||||
LOCKED_ASSET_KEY = AssetKey.from_string('asset-v1:edX+toy+2012_Fall+type@asset+block@sample_static.html')
|
||||
|
||||
def assertCoursesEqual(self, course1_id, course2_id):
|
||||
"""
|
||||
|
||||
@@ -111,13 +111,11 @@ class ItemTest(CourseTestCase):
|
||||
self.course_key = self.course.id
|
||||
self.usage_key = self.course.location
|
||||
|
||||
def get_item_from_modulestore(self, usage_key, verify_is_draft=False):
|
||||
def get_item_from_modulestore(self, usage_key):
|
||||
"""
|
||||
Get the item referenced by the UsageKey from the modulestore
|
||||
"""
|
||||
item = self.store.get_item(usage_key)
|
||||
if verify_is_draft:
|
||||
self.assertTrue(getattr(item, "is_draft", False))
|
||||
return item
|
||||
|
||||
def response_usage_key(self, response):
|
||||
@@ -540,9 +538,8 @@ class GetItemTest(ItemTest):
|
||||
class DeleteItem(ItemTest):
|
||||
"""Tests for '/xblock' DELETE url."""
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
def test_delete_static_page(self, store):
|
||||
course = CourseFactory.create(default_store=store)
|
||||
def test_delete_static_page(self):
|
||||
course = CourseFactory.create()
|
||||
# Add static tab
|
||||
resp = self.create_xblock(
|
||||
category="static_tab", parent_usage_key=course.location
|
||||
@@ -589,7 +586,7 @@ class TestCreateItem(ItemTest):
|
||||
parent_usage_key=vert_usage_key, category="problem", boilerplate=template_id
|
||||
)
|
||||
prob_usage_key = self.response_usage_key(resp)
|
||||
problem = self.get_item_from_modulestore(prob_usage_key, verify_is_draft=True)
|
||||
problem = self.get_item_from_modulestore(prob_usage_key)
|
||||
# check against the template
|
||||
template = ProblemBlock.get_template(template_id)
|
||||
self.assertEqual(problem.data, template["data"])
|
||||
@@ -807,9 +804,7 @@ class TestDuplicateItem(ItemTest, DuplicateHelper, OpenEdxEventsTestMixin):
|
||||
self.html_usage_key = self.response_usage_key(resp)
|
||||
|
||||
# Create a second sequential just (testing children of children)
|
||||
self.create_xblock(
|
||||
parent_usage_key=self.chapter_usage_key, category="sequential2"
|
||||
)
|
||||
self.create_xblock(parent_usage_key=self.chapter_usage_key, category='sequential')
|
||||
|
||||
def test_duplicate_equality(self):
|
||||
"""
|
||||
@@ -976,10 +971,10 @@ class TestMoveItem(ItemTest):
|
||||
if not default_store:
|
||||
default_store = self.store.default_modulestore.get_modulestore_type()
|
||||
|
||||
self.course = CourseFactory.create(default_store=default_store)
|
||||
course = CourseFactory.create(default_store=default_store)
|
||||
|
||||
# Create group configurations
|
||||
self.course.user_partitions = [
|
||||
course.user_partitions = [
|
||||
UserPartition(
|
||||
0,
|
||||
"first_partition",
|
||||
@@ -987,18 +982,18 @@ class TestMoveItem(ItemTest):
|
||||
[Group("0", "alpha"), Group("1", "beta")],
|
||||
)
|
||||
]
|
||||
self.store.update_item(self.course, self.user.id)
|
||||
self.store.update_item(course, self.user.id)
|
||||
|
||||
# Create a parent chapter
|
||||
chap1 = self.create_xblock(
|
||||
parent_usage_key=self.course.location,
|
||||
parent_usage_key=course.location,
|
||||
display_name="chapter1",
|
||||
category="chapter",
|
||||
)
|
||||
self.chapter_usage_key = self.response_usage_key(chap1)
|
||||
|
||||
chap2 = self.create_xblock(
|
||||
parent_usage_key=self.course.location,
|
||||
parent_usage_key=course.location,
|
||||
display_name="chapter2",
|
||||
category="chapter",
|
||||
)
|
||||
@@ -1053,6 +1048,8 @@ class TestMoveItem(ItemTest):
|
||||
)
|
||||
self.split_test_usage_key = self.response_usage_key(resp)
|
||||
|
||||
self.course = self.store.get_item(course.location)
|
||||
|
||||
def setup_and_verify_content_experiment(self, partition_id):
|
||||
"""
|
||||
Helper method to set up group configurations to content experiment.
|
||||
@@ -1060,9 +1057,7 @@ class TestMoveItem(ItemTest):
|
||||
Arguments:
|
||||
partition_id (int): User partition id.
|
||||
"""
|
||||
split_test = self.get_item_from_modulestore(
|
||||
self.split_test_usage_key, verify_is_draft=True
|
||||
)
|
||||
split_test = self.get_item_from_modulestore(self.split_test_usage_key)
|
||||
|
||||
# Initially, no user_partition_id is set, and the split_test has no children.
|
||||
self.assertEqual(split_test.user_partition_id, -1)
|
||||
@@ -1073,9 +1068,7 @@ class TestMoveItem(ItemTest):
|
||||
reverse_usage_url("xblock_handler", self.split_test_usage_key),
|
||||
data={"metadata": {"user_partition_id": str(partition_id)}},
|
||||
)
|
||||
split_test = self.get_item_from_modulestore(
|
||||
self.split_test_usage_key, verify_is_draft=True
|
||||
)
|
||||
split_test = self.get_item_from_modulestore(self.split_test_usage_key)
|
||||
self.assertEqual(split_test.user_partition_id, partition_id)
|
||||
self.assertEqual(
|
||||
len(split_test.children),
|
||||
@@ -1141,15 +1134,11 @@ class TestMoveItem(ItemTest):
|
||||
self.assertIn(source_usage_key, target_parent.children)
|
||||
self.assertNotIn(source_usage_key, source_parent.children)
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
def test_move_component(self, store_type):
|
||||
def test_move_component(self):
|
||||
"""
|
||||
Test move component with different xblock types.
|
||||
|
||||
Arguments:
|
||||
store_type (ModuleStoreEnum.Type): Type of modulestore to create test course in.
|
||||
"""
|
||||
self.setup_course(default_store=store_type)
|
||||
self.setup_course()
|
||||
for source_usage_key, target_usage_key in [
|
||||
(self.html_usage_key, self.vert2_usage_key),
|
||||
(self.vert_usage_key, self.seq2_usage_key),
|
||||
@@ -1391,9 +1380,7 @@ class TestMoveItem(ItemTest):
|
||||
reverse_usage_url("xblock_handler", child_split_test_usage_key),
|
||||
data={"metadata": {"user_partition_id": str(0)}},
|
||||
)
|
||||
child_split_test = self.get_item_from_modulestore(
|
||||
self.split_test_usage_key, verify_is_draft=True
|
||||
)
|
||||
child_split_test = self.get_item_from_modulestore(self.split_test_usage_key)
|
||||
|
||||
# Try to move content experiment further down the level to a child group A nested inside main group A.
|
||||
response = self._move_component(
|
||||
@@ -1469,6 +1456,7 @@ class TestMoveItem(ItemTest):
|
||||
"""
|
||||
group1 = self.course.user_partitions[0].groups[0]
|
||||
group2 = self.course.user_partitions[0].groups[1]
|
||||
vert1 = self.store.get_item(self.vert_usage_key)
|
||||
vert2 = self.store.get_item(self.vert2_usage_key)
|
||||
html = self.store.get_item(self.html_usage_key)
|
||||
|
||||
@@ -1481,10 +1469,12 @@ class TestMoveItem(ItemTest):
|
||||
html.runtime._services["partitions"] = partitions_service # lint-amnesty, pylint: disable=protected-access
|
||||
|
||||
# Set access settings so html will contradict vert2 when moved into that unit
|
||||
vert1.group_access = {self.course.user_partitions[0].id: [group2.id]}
|
||||
vert2.group_access = {self.course.user_partitions[0].id: [group1.id]}
|
||||
html.group_access = {self.course.user_partitions[0].id: [group2.id]}
|
||||
self.store.update_item(html, self.user.id)
|
||||
self.store.update_item(vert2, self.user.id)
|
||||
vert1 = self.store.update_item(vert1, self.user.id)
|
||||
vert2 = self.store.update_item(vert2, self.user.id)
|
||||
html = self.store.update_item(html, self.user.id)
|
||||
|
||||
# Verify that there is no warning when html is in a non contradicting unit
|
||||
validation = html.validate()
|
||||
@@ -1493,7 +1483,7 @@ class TestMoveItem(ItemTest):
|
||||
# Now move it and confirm that the html component has been moved into vertical 2
|
||||
self.assert_move_item(self.html_usage_key, self.vert2_usage_key)
|
||||
html.parent = self.vert2_usage_key
|
||||
self.store.update_item(html, self.user.id)
|
||||
html = self.store.update_item(html, self.user.id)
|
||||
validation = html.validate()
|
||||
self.assertEqual(len(validation.messages), 1)
|
||||
self._verify_validation_message(
|
||||
@@ -1505,7 +1495,7 @@ class TestMoveItem(ItemTest):
|
||||
# Move the html component back and confirm that the warning is gone again
|
||||
self.assert_move_item(self.html_usage_key, self.vert_usage_key)
|
||||
html.parent = self.vert_usage_key
|
||||
self.store.update_item(html, self.user.id)
|
||||
html = self.store.update_item(html, self.user.id)
|
||||
validation = html.validate()
|
||||
self.assertEqual(len(validation.messages), 0)
|
||||
|
||||
@@ -1527,16 +1517,12 @@ class TestMoveItem(ItemTest):
|
||||
insert_at,
|
||||
)
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
def test_move_and_discard_changes(self, store_type):
|
||||
def test_move_and_discard_changes(self):
|
||||
"""
|
||||
Verifies that discard changes operation brings moved component back to source location and removes the component
|
||||
from target location.
|
||||
|
||||
Arguments:
|
||||
store_type (ModuleStoreEnum.Type): Type of modulestore to create test course in.
|
||||
"""
|
||||
self.setup_course(default_store=store_type)
|
||||
self.setup_course()
|
||||
|
||||
old_parent_loc = self.store.get_parent_location(self.html_usage_key)
|
||||
|
||||
@@ -1594,15 +1580,11 @@ class TestMoveItem(ItemTest):
|
||||
self.assertIn(self.html_usage_key, source_parent.children)
|
||||
self.assertNotIn(self.html_usage_key, target_parent.children)
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
def test_move_item_not_found(self, store_type=ModuleStoreEnum.Type.mongo):
|
||||
def test_move_item_not_found(self):
|
||||
"""
|
||||
Test that an item not found exception raised when an item is not found when getting the item.
|
||||
|
||||
Arguments:
|
||||
store_type (ModuleStoreEnum.Type): Type of modulestore to create test course in.
|
||||
"""
|
||||
self.setup_course(default_store=store_type)
|
||||
self.setup_course()
|
||||
|
||||
data = {
|
||||
"move_source_locator": str(
|
||||
@@ -1752,30 +1734,25 @@ class TestEditItem(TestEditItemSetup):
|
||||
self.client.ajax_post(
|
||||
self.problem_update_url, data={"metadata": {"rerandomize": "onreset"}}
|
||||
)
|
||||
problem = self.get_item_from_modulestore(
|
||||
self.problem_usage_key, verify_is_draft=True
|
||||
)
|
||||
self.assertEqual(problem.rerandomize, "onreset")
|
||||
problem = self.get_item_from_modulestore(self.problem_usage_key)
|
||||
self.assertEqual(problem.rerandomize, 'onreset')
|
||||
self.client.ajax_post(
|
||||
self.problem_update_url, data={"metadata": {"rerandomize": None}}
|
||||
)
|
||||
problem = self.get_item_from_modulestore(
|
||||
self.problem_usage_key, verify_is_draft=True
|
||||
)
|
||||
self.assertEqual(problem.rerandomize, "never")
|
||||
problem = self.get_item_from_modulestore(self.problem_usage_key)
|
||||
self.assertEqual(problem.rerandomize, 'never')
|
||||
|
||||
def test_null_field(self):
|
||||
"""
|
||||
Sending null in for a field 'deletes' it
|
||||
"""
|
||||
problem = self.get_item_from_modulestore(
|
||||
self.problem_usage_key, verify_is_draft=True
|
||||
)
|
||||
problem = self.get_item_from_modulestore(self.problem_usage_key)
|
||||
self.assertIsNotNone(problem.markdown)
|
||||
self.client.ajax_post(self.problem_update_url, data={"nullout": ["markdown"]})
|
||||
problem = self.get_item_from_modulestore(
|
||||
self.problem_usage_key, verify_is_draft=True
|
||||
self.client.ajax_post(
|
||||
self.problem_update_url,
|
||||
data={'nullout': ['markdown']}
|
||||
)
|
||||
problem = self.get_item_from_modulestore(self.problem_usage_key)
|
||||
self.assertIsNone(problem.markdown)
|
||||
|
||||
def test_date_fields(self):
|
||||
@@ -1831,9 +1808,7 @@ class TestEditItem(TestEditItemSetup):
|
||||
}
|
||||
},
|
||||
)
|
||||
problem = self.get_item_from_modulestore(
|
||||
self.problem_usage_key, verify_is_draft=True
|
||||
)
|
||||
problem = self.get_item_from_modulestore(self.problem_usage_key)
|
||||
self.assertEqual(problem.display_name, new_display_name)
|
||||
self.assertEqual(problem.max_attempts, new_max_attempts)
|
||||
|
||||
@@ -2052,9 +2027,7 @@ class TestEditItem(TestEditItemSetup):
|
||||
},
|
||||
)
|
||||
self.assertFalse(self._is_location_published(self.problem_usage_key))
|
||||
draft = self.get_item_from_modulestore(
|
||||
self.problem_usage_key, verify_is_draft=True
|
||||
)
|
||||
draft = self.get_item_from_modulestore(self.problem_usage_key)
|
||||
self.assertEqual(draft.display_name, new_display_name)
|
||||
|
||||
# Publish the item
|
||||
@@ -2112,9 +2085,7 @@ class TestEditItem(TestEditItemSetup):
|
||||
self.client.ajax_post(
|
||||
self.problem_update_url, data={"metadata": {"due": "2077-10-10T04:00Z"}}
|
||||
)
|
||||
updated_draft = self.get_item_from_modulestore(
|
||||
self.problem_usage_key, verify_is_draft=True
|
||||
)
|
||||
updated_draft = self.get_item_from_modulestore(self.problem_usage_key)
|
||||
self.assertEqual(updated_draft.due, datetime(2077, 10, 10, 4, 0, tzinfo=UTC))
|
||||
self.assertIsNone(published.due)
|
||||
# Fetch the published version again to make sure the due date is still unset.
|
||||
@@ -2154,9 +2125,7 @@ class TestEditItem(TestEditItemSetup):
|
||||
)
|
||||
|
||||
# Both published and draft content should be different
|
||||
draft = self.get_item_from_modulestore(
|
||||
self.problem_usage_key, verify_is_draft=True
|
||||
)
|
||||
draft = self.get_item_from_modulestore(self.problem_usage_key)
|
||||
self.assertNotEqual(draft.data, published.data)
|
||||
|
||||
# Get problem by 'xblock_handler'
|
||||
@@ -2174,9 +2143,7 @@ class TestEditItem(TestEditItemSetup):
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# Both published and draft content should still be different
|
||||
draft = self.get_item_from_modulestore(
|
||||
self.problem_usage_key, verify_is_draft=True
|
||||
)
|
||||
draft = self.get_item_from_modulestore(self.problem_usage_key)
|
||||
self.assertNotEqual(draft.data, published.data)
|
||||
# Fetch the published version again to make sure the data is correct.
|
||||
published = modulestore().get_item(
|
||||
@@ -2209,18 +2176,6 @@ class TestEditItem(TestEditItemSetup):
|
||||
self._verify_published_with_no_draft(unit_usage_key)
|
||||
self._verify_published_with_no_draft(html_usage_key)
|
||||
|
||||
# Make a draft for the unit and verify that the problem also has a draft
|
||||
resp = self.client.ajax_post(
|
||||
unit_update_url,
|
||||
data={
|
||||
"id": str(unit_usage_key),
|
||||
"metadata": {},
|
||||
},
|
||||
)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self._verify_published_with_draft(unit_usage_key)
|
||||
self._verify_published_with_draft(html_usage_key)
|
||||
|
||||
def test_field_value_errors(self):
|
||||
"""
|
||||
Test that if the user's input causes a ValueError on an XBlock field,
|
||||
@@ -2346,9 +2301,7 @@ class TestEditSplitModule(ItemTest):
|
||||
)
|
||||
|
||||
# Verify the partition_id was saved.
|
||||
split_test = self.get_item_from_modulestore(
|
||||
self.split_test_usage_key, verify_is_draft=True
|
||||
)
|
||||
split_test = self.get_item_from_modulestore(self.split_test_usage_key)
|
||||
self.assertEqual(partition_id, split_test.user_partition_id)
|
||||
return split_test
|
||||
|
||||
@@ -2356,7 +2309,7 @@ class TestEditSplitModule(ItemTest):
|
||||
"""
|
||||
Verifies the number of children of the split_test instance.
|
||||
"""
|
||||
split_test = self.get_item_from_modulestore(self.split_test_usage_key, True)
|
||||
split_test = self.get_item_from_modulestore(self.split_test_usage_key)
|
||||
self.assertEqual(expected_number, len(split_test.children))
|
||||
return split_test
|
||||
|
||||
@@ -2365,9 +2318,7 @@ class TestEditSplitModule(ItemTest):
|
||||
Test that verticals are created for the configuration groups when
|
||||
a spit test block is edited.
|
||||
"""
|
||||
split_test = self.get_item_from_modulestore(
|
||||
self.split_test_usage_key, verify_is_draft=True
|
||||
)
|
||||
split_test = self.get_item_from_modulestore(self.split_test_usage_key)
|
||||
# Initially, no user_partition_id is set, and the split_test has no children.
|
||||
self.assertEqual(-1, split_test.user_partition_id)
|
||||
self.assertEqual(0, len(split_test.children))
|
||||
@@ -2377,12 +2328,8 @@ class TestEditSplitModule(ItemTest):
|
||||
|
||||
# Verify that child verticals have been set to match the groups
|
||||
self.assertEqual(2, len(split_test.children))
|
||||
vertical_0 = self.get_item_from_modulestore(
|
||||
split_test.children[0], verify_is_draft=True
|
||||
)
|
||||
vertical_1 = self.get_item_from_modulestore(
|
||||
split_test.children[1], verify_is_draft=True
|
||||
)
|
||||
vertical_0 = self.get_item_from_modulestore(split_test.children[0])
|
||||
vertical_1 = self.get_item_from_modulestore(split_test.children[1])
|
||||
self.assertEqual("vertical", vertical_0.category)
|
||||
self.assertEqual("vertical", vertical_1.category)
|
||||
self.assertEqual(
|
||||
@@ -2407,9 +2354,7 @@ class TestEditSplitModule(ItemTest):
|
||||
"""
|
||||
Test that concise outline for split test component gives display name as group name.
|
||||
"""
|
||||
split_test = self.get_item_from_modulestore(
|
||||
self.split_test_usage_key, verify_is_draft=True
|
||||
)
|
||||
split_test = self.get_item_from_modulestore(self.split_test_usage_key)
|
||||
# Initially, no user_partition_id is set, and the split_test has no children.
|
||||
self.assertEqual(split_test.user_partition_id, -1)
|
||||
self.assertEqual(len(split_test.children), 0)
|
||||
@@ -2451,15 +2396,9 @@ class TestEditSplitModule(ItemTest):
|
||||
self.assertEqual(5, len(split_test.children))
|
||||
self.assertEqual(initial_vertical_0_location, split_test.children[0])
|
||||
self.assertEqual(initial_vertical_1_location, split_test.children[1])
|
||||
vertical_0 = self.get_item_from_modulestore(
|
||||
split_test.children[2], verify_is_draft=True
|
||||
)
|
||||
vertical_1 = self.get_item_from_modulestore(
|
||||
split_test.children[3], verify_is_draft=True
|
||||
)
|
||||
vertical_2 = self.get_item_from_modulestore(
|
||||
split_test.children[4], verify_is_draft=True
|
||||
)
|
||||
vertical_0 = self.get_item_from_modulestore(split_test.children[2])
|
||||
vertical_1 = self.get_item_from_modulestore(split_test.children[3])
|
||||
vertical_2 = self.get_item_from_modulestore(split_test.children[4])
|
||||
|
||||
# Verify that the group_id_to child mapping is correct.
|
||||
self.assertEqual(3, len(split_test.group_id_to_child))
|
||||
@@ -3106,39 +3045,25 @@ class TestXBlockInfo(ItemTest):
|
||||
json_response = json.loads(resp.content.decode("utf-8"))
|
||||
self.validate_course_xblock_info(json_response, course_outline=True)
|
||||
|
||||
@ddt.data(
|
||||
(ModuleStoreEnum.Type.split, 3, 3),
|
||||
(ModuleStoreEnum.Type.mongo, 8, 12),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_xblock_outline_handler_mongo_calls(
|
||||
self, store_type, chapter_queries, chapter_queries_1
|
||||
):
|
||||
with self.store.default_store(store_type):
|
||||
course = CourseFactory.create()
|
||||
chapter = BlockFactory.create(
|
||||
parent_location=course.location,
|
||||
category="chapter",
|
||||
display_name="Week 1",
|
||||
)
|
||||
outline_url = reverse_usage_url("xblock_outline_handler", chapter.location)
|
||||
with check_mongo_calls(chapter_queries):
|
||||
self.client.get(outline_url, HTTP_ACCEPT="application/json")
|
||||
def test_xblock_outline_handler_mongo_calls(self):
|
||||
course = CourseFactory.create()
|
||||
chapter = BlockFactory.create(
|
||||
parent_location=course.location, category='chapter', display_name='Week 1'
|
||||
)
|
||||
outline_url = reverse_usage_url('xblock_outline_handler', chapter.location)
|
||||
with check_mongo_calls(3):
|
||||
self.client.get(outline_url, HTTP_ACCEPT='application/json')
|
||||
|
||||
sequential = BlockFactory.create(
|
||||
parent_location=chapter.location,
|
||||
category="sequential",
|
||||
display_name="Sequential 1",
|
||||
)
|
||||
sequential = BlockFactory.create(
|
||||
parent_location=chapter.location, category='sequential', display_name='Sequential 1'
|
||||
)
|
||||
|
||||
BlockFactory.create(
|
||||
parent_location=sequential.location,
|
||||
category="vertical",
|
||||
display_name="Vertical 1",
|
||||
)
|
||||
# calls should be same after adding two new children for split only.
|
||||
with check_mongo_calls(chapter_queries_1):
|
||||
self.client.get(outline_url, HTTP_ACCEPT="application/json")
|
||||
BlockFactory.create(
|
||||
parent_location=sequential.location, category='vertical', display_name='Vertical 1'
|
||||
)
|
||||
# calls should be same after adding two new children for split only.
|
||||
with check_mongo_calls(3):
|
||||
self.client.get(outline_url, HTTP_ACCEPT='application/json')
|
||||
|
||||
def test_entrance_exam_chapter_xblock_info(self):
|
||||
chapter = BlockFactory.create(
|
||||
@@ -3264,32 +3189,26 @@ class TestXBlockInfo(ItemTest):
|
||||
)
|
||||
self.validate_component_xblock_info(xblock_info)
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.split, ModuleStoreEnum.Type.mongo)
|
||||
def test_validate_start_date(self, store_type):
|
||||
def test_validate_start_date(self):
|
||||
"""
|
||||
Validate if start-date year is less than 1900 reset the date to DEFAULT_START_DATE.
|
||||
"""
|
||||
with self.store.default_store(store_type):
|
||||
course = CourseFactory.create()
|
||||
chapter = BlockFactory.create(
|
||||
parent_location=course.location,
|
||||
category="chapter",
|
||||
display_name="Week 1",
|
||||
)
|
||||
course = CourseFactory.create()
|
||||
chapter = BlockFactory.create(
|
||||
parent_location=course.location, category='chapter', display_name='Week 1'
|
||||
)
|
||||
|
||||
chapter.start = datetime(year=1899, month=1, day=1, tzinfo=UTC)
|
||||
chapter.start = datetime(year=1899, month=1, day=1, tzinfo=UTC)
|
||||
|
||||
xblock_info = create_xblock_info(
|
||||
chapter,
|
||||
include_child_info=True,
|
||||
include_children_predicate=ALWAYS,
|
||||
include_ancestor_info=True,
|
||||
user=self.user,
|
||||
)
|
||||
xblock_info = create_xblock_info(
|
||||
chapter,
|
||||
include_child_info=True,
|
||||
include_children_predicate=ALWAYS,
|
||||
include_ancestor_info=True,
|
||||
user=self.user
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
xblock_info["start"], DEFAULT_START_DATE.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
)
|
||||
self.assertEqual(xblock_info['start'], DEFAULT_START_DATE.strftime('%Y-%m-%dT%H:%M:%SZ'))
|
||||
|
||||
def test_highlights_enabled(self):
|
||||
self.course.highlights_enabled_for_messaging = True
|
||||
@@ -3489,9 +3408,11 @@ class TestSpecialExamXBlockInfo(ItemTest):
|
||||
user_id=user_id,
|
||||
highlights=["highlight"],
|
||||
)
|
||||
# get updated course
|
||||
self.course = self.store.get_item(self.course.location)
|
||||
self.course.enable_proctored_exams = True
|
||||
self.course.save()
|
||||
self.store.update_item(self.course, self.user.id)
|
||||
self.course = self.store.update_item(self.course, self.user.id)
|
||||
|
||||
def test_proctoring_is_enabled_for_course(self):
|
||||
course = modulestore().get_item(self.course.location)
|
||||
@@ -3517,7 +3438,7 @@ class TestSpecialExamXBlockInfo(ItemTest):
|
||||
category="sequential",
|
||||
display_name="Test Lesson 1",
|
||||
user_id=self.user.id,
|
||||
is_proctored_exam=True,
|
||||
is_proctored_enabled=True,
|
||||
is_time_limited=True,
|
||||
default_time_limit_minutes=100,
|
||||
is_onboarding_exam=False,
|
||||
@@ -3561,7 +3482,7 @@ class TestSpecialExamXBlockInfo(ItemTest):
|
||||
category="sequential",
|
||||
display_name="Test Lesson 1",
|
||||
user_id=self.user.id,
|
||||
is_proctored_exam=False,
|
||||
is_proctored_enabled=False,
|
||||
is_time_limited=False,
|
||||
is_onboarding_exam=False,
|
||||
)
|
||||
@@ -3589,7 +3510,7 @@ class TestSpecialExamXBlockInfo(ItemTest):
|
||||
category="sequential",
|
||||
display_name="Test Lesson 1",
|
||||
user_id=self.user.id,
|
||||
is_proctored_exam=False,
|
||||
is_proctored_enabled=False,
|
||||
is_time_limited=False,
|
||||
is_onboarding_exam=False,
|
||||
)
|
||||
@@ -3849,9 +3770,8 @@ class TestXBlockPublishingInfo(ItemTest):
|
||||
xblock_info = self._get_xblock_info(empty_chapter.location)
|
||||
self._verify_visibility_state(xblock_info, VisibilityState.unscheduled)
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
def test_chapter_self_paced_default_start_date(self, store_type):
|
||||
course = CourseFactory.create(default_store=store_type)
|
||||
def test_chapter_self_paced_default_start_date(self):
|
||||
course = CourseFactory.create()
|
||||
course.self_paced = True
|
||||
self.store.update_item(course, self.user.id)
|
||||
chapter = self._create_child(course, "chapter", "Test Chapter")
|
||||
@@ -3939,29 +3859,15 @@ class TestXBlockPublishingInfo(ItemTest):
|
||||
)
|
||||
|
||||
def test_partially_released_section(self):
|
||||
chapter = self._create_child(self.course, "chapter", "Test Chapter")
|
||||
released_sequential = self._create_child(
|
||||
chapter, "sequential", "Released Sequential"
|
||||
)
|
||||
self._create_child(
|
||||
released_sequential, "vertical", "Released Unit", publish_item=True
|
||||
)
|
||||
self._create_child(
|
||||
released_sequential, "vertical", "Staff Only Unit", staff_only=True
|
||||
)
|
||||
chapter = self._create_child(self.course, 'chapter', "Test Chapter")
|
||||
released_sequential = self._create_child(chapter, 'sequential', "Released Sequential")
|
||||
self._create_child(released_sequential, 'vertical', "Released Unit", publish_item=True)
|
||||
self._create_child(released_sequential, 'vertical', "Staff Only Unit 1", staff_only=True)
|
||||
self._set_release_date(chapter.location, datetime.now(UTC) - timedelta(days=1))
|
||||
published_sequential = self._create_child(
|
||||
chapter, "sequential", "Published Sequential"
|
||||
)
|
||||
self._create_child(
|
||||
published_sequential, "vertical", "Published Unit", publish_item=True
|
||||
)
|
||||
self._create_child(
|
||||
published_sequential, "vertical", "Staff Only Unit", staff_only=True
|
||||
)
|
||||
self._set_release_date(
|
||||
published_sequential.location, datetime.now(UTC) + timedelta(days=1)
|
||||
)
|
||||
published_sequential = self._create_child(chapter, 'sequential', "Published Sequential")
|
||||
self._create_child(published_sequential, 'vertical', "Published Unit", publish_item=True)
|
||||
self._create_child(published_sequential, 'vertical', "Staff Only Unit 2", staff_only=True)
|
||||
self._set_release_date(published_sequential.location, datetime.now(UTC) + timedelta(days=1))
|
||||
xblock_info = self._get_xblock_info(chapter.location)
|
||||
|
||||
# Verify the state of the released sequential
|
||||
@@ -4191,8 +4097,7 @@ class TestXBlockPublishingInfo(ItemTest):
|
||||
xblock_info, True, path=self.FIRST_UNIT_PATH
|
||||
)
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
def test_self_paced_item_visibility_state(self, store_type):
|
||||
def test_self_paced_item_visibility_state(self):
|
||||
"""
|
||||
Test that in self-paced course, item has `live` visibility state.
|
||||
Test that when item was initially in `scheduled` state in instructor mode, change course pacing to self-paced,
|
||||
@@ -4200,7 +4105,7 @@ class TestXBlockPublishingInfo(ItemTest):
|
||||
"""
|
||||
|
||||
# Create course, chapter and setup future release date to make chapter in scheduled state
|
||||
course = CourseFactory.create(default_store=store_type)
|
||||
course = CourseFactory.create()
|
||||
chapter = self._create_child(course, "chapter", "Test Chapter")
|
||||
self._set_release_date(chapter.location, datetime.now(UTC) + timedelta(days=1))
|
||||
|
||||
|
||||
@@ -31,29 +31,30 @@ class ContainerPageTestCase(StudioPageTestCase, LibraryTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.vertical = self._create_block(self.sequential.location, 'vertical', 'Unit')
|
||||
self.html = self._create_block(self.vertical.location, "html", "HTML")
|
||||
self.child_container = self._create_block(self.vertical.location, 'split_test', 'Split Test')
|
||||
self.child_vertical = self._create_block(self.child_container.location, 'vertical', 'Child Vertical')
|
||||
self.video = self._create_block(self.child_vertical.location, "video", "My Video")
|
||||
self.vertical = self._create_block(self.sequential, 'vertical', 'Unit')
|
||||
self.html = self._create_block(self.vertical, "html", "HTML")
|
||||
self.child_container = self._create_block(self.vertical, 'split_test', 'Split Test')
|
||||
self.child_vertical = self._create_block(self.child_container, 'vertical', 'Child Vertical')
|
||||
self.video = self._create_block(self.child_vertical, "video", "My Video")
|
||||
self.store = modulestore()
|
||||
|
||||
past = datetime.datetime(1970, 1, 1, tzinfo=UTC)
|
||||
future = datetime.datetime.now(UTC) + datetime.timedelta(days=1)
|
||||
self.released_private_vertical = self._create_block(
|
||||
parent_location=self.sequential.location, category='vertical', display_name='Released Private Unit',
|
||||
parent=self.sequential, category='vertical', display_name='Released Private Unit',
|
||||
start=past)
|
||||
self.unreleased_private_vertical = self._create_block(
|
||||
parent_location=self.sequential.location, category='vertical', display_name='Unreleased Private Unit',
|
||||
parent=self.sequential, category='vertical', display_name='Unreleased Private Unit',
|
||||
start=future)
|
||||
self.released_public_vertical = self._create_block(
|
||||
parent_location=self.sequential.location, category='vertical', display_name='Released Public Unit',
|
||||
parent=self.sequential, category='vertical', display_name='Released Public Unit',
|
||||
start=past)
|
||||
self.unreleased_public_vertical = self._create_block(
|
||||
parent_location=self.sequential.location, category='vertical', display_name='Unreleased Public Unit',
|
||||
parent=self.sequential, category='vertical', display_name='Unreleased Public Unit',
|
||||
start=future)
|
||||
self.store.publish(self.unreleased_public_vertical.location, self.user.id)
|
||||
self.store.publish(self.released_public_vertical.location, self.user.id)
|
||||
self.store.publish(self.vertical.location, self.user.id)
|
||||
|
||||
def test_container_html(self):
|
||||
self._test_html_content(
|
||||
@@ -81,8 +82,8 @@ class ContainerPageTestCase(StudioPageTestCase, LibraryTestCase):
|
||||
Create the scenario of an xblock with children (non-vertical) on the container page.
|
||||
This should create a container page that is a child of another container page.
|
||||
"""
|
||||
draft_container = self._create_block(self.child_container.location, "wrapper", "Wrapper")
|
||||
self._create_block(draft_container.location, "html", "Child HTML")
|
||||
draft_container = self._create_block(self.child_container, "wrapper", "Wrapper")
|
||||
self._create_block(draft_container, "html", "Child HTML")
|
||||
|
||||
def test_container_html(xblock):
|
||||
self._test_html_content(
|
||||
@@ -177,12 +178,12 @@ class ContainerPageTestCase(StudioPageTestCase, LibraryTestCase):
|
||||
self.validate_preview_html(self.child_container, self.container_view)
|
||||
self.validate_preview_html(self.child_vertical, self.reorderable_child_view)
|
||||
|
||||
def _create_block(self, parent_location, category, display_name, **kwargs):
|
||||
def _create_block(self, parent, category, display_name, **kwargs):
|
||||
"""
|
||||
creates a block in the module store, without publishing it.
|
||||
"""
|
||||
return BlockFactory.create(
|
||||
parent_location=parent_location,
|
||||
parent=parent,
|
||||
category=category,
|
||||
display_name=display_name,
|
||||
publish_item=False,
|
||||
@@ -194,7 +195,7 @@ class ContainerPageTestCase(StudioPageTestCase, LibraryTestCase):
|
||||
"""
|
||||
Verify that a public container rendered as a child of the container page returns the expected HTML.
|
||||
"""
|
||||
empty_child_container = self._create_block(self.vertical.location, 'split_test', 'Split Test')
|
||||
empty_child_container = self._create_block(self.vertical, 'split_test', 'Split Test 1')
|
||||
published_empty_child_container = self.store.publish(empty_child_container.location, self.user.id)
|
||||
self.validate_preview_html(published_empty_child_container, self.reorderable_child_view, can_add=False)
|
||||
|
||||
@@ -202,7 +203,7 @@ class ContainerPageTestCase(StudioPageTestCase, LibraryTestCase):
|
||||
"""
|
||||
Verify that a draft container rendered as a child of the container page returns the expected HTML.
|
||||
"""
|
||||
empty_child_container = self._create_block(self.vertical.location, 'split_test', 'Split Test')
|
||||
empty_child_container = self._create_block(self.vertical, 'split_test', 'Split Test 1')
|
||||
self.validate_preview_html(empty_child_container, self.reorderable_child_view, can_add=False)
|
||||
|
||||
@patch(
|
||||
|
||||
@@ -18,9 +18,9 @@ class StudioPageTestCase(CourseTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.chapter = BlockFactory.create(parent_location=self.course.location,
|
||||
self.chapter = BlockFactory.create(parent=self.course,
|
||||
category='chapter', display_name="Week 1")
|
||||
self.sequential = BlockFactory.create(parent_location=self.chapter.location,
|
||||
self.sequential = BlockFactory.create(parent=self.chapter,
|
||||
category='sequential', display_name="Lesson 1")
|
||||
|
||||
def get_page_html(self, xblock):
|
||||
|
||||
@@ -158,14 +158,6 @@ class AuthoringMixinTestCase(ModuleStoreTestCase):
|
||||
[self.STAFF_LOCKED, self.CONTENT_GROUPS_TITLE, self.ENROLLMENT_GROUPS_TITLE]
|
||||
)
|
||||
|
||||
def test_html_populated_partition_staff_locked(self):
|
||||
self.create_content_groups(self.pet_groups)
|
||||
self.set_staff_only(self.vertical_location)
|
||||
self.verify_visibility_view_contains(
|
||||
self.video_location,
|
||||
[self.STAFF_LOCKED, self.CONTENT_GROUPS_TITLE, 'Cat Lovers', 'Dog Lovers']
|
||||
)
|
||||
|
||||
def test_html_false_content_group(self):
|
||||
self.create_content_groups(self.pet_groups)
|
||||
self.set_group_access(self.video_location, ['false_group_id'])
|
||||
@@ -178,20 +170,6 @@ class AuthoringMixinTestCase(ModuleStoreTestCase):
|
||||
[self.STAFF_LOCKED]
|
||||
)
|
||||
|
||||
def test_html_false_content_group_staff_locked(self):
|
||||
self.create_content_groups(self.pet_groups)
|
||||
self.set_staff_only(self.vertical_location)
|
||||
self.set_group_access(self.video_location, ['false_group_id'])
|
||||
self.verify_visibility_view_contains(
|
||||
self.video_location,
|
||||
[
|
||||
'Cat Lovers',
|
||||
'Dog Lovers',
|
||||
self.STAFF_LOCKED,
|
||||
self.GROUP_NO_LONGER_EXISTS
|
||||
]
|
||||
)
|
||||
|
||||
@override_settings(FEATURES=FEATURES_WITH_ENROLLMENT_TRACK_DISABLED)
|
||||
def test_enrollment_tracks_disabled(self):
|
||||
"""
|
||||
|
||||
@@ -9,7 +9,7 @@ from django.test.utils import override_settings
|
||||
from django.urls import reverse
|
||||
from pytz import UTC
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.tests.django_utils import TEST_DATA_MONGO_AMNESTY_MODULESTORE, SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.django_utils import TEST_DATA_SPLIT_MODULESTORE, SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from xmodule.data import CertificatesDisplayBehaviors
|
||||
|
||||
@@ -31,7 +31,7 @@ FUTURE_DATE = datetime.datetime.now(UTC) + datetime.timedelta(days=2)
|
||||
class CertificateDisplayTestBase(SharedModuleStoreTestCase):
|
||||
"""Tests display of certificates on the student dashboard. """
|
||||
|
||||
MODULESTORE = TEST_DATA_MONGO_AMNESTY_MODULESTORE
|
||||
MODULESTORE = TEST_DATA_SPLIT_MODULESTORE
|
||||
USERNAME = "test_user"
|
||||
PASSWORD = "password"
|
||||
|
||||
@@ -93,15 +93,16 @@ class CertificateDashboardMessageDisplayTest(CertificateDisplayTestBase):
|
||||
|
||||
def _check_message(self, visible_date): # lint-amnesty, pylint: disable=missing-function-docstring
|
||||
response = self.client.get(reverse('dashboard'))
|
||||
test_message = 'Your grade and certificate will be ready after'
|
||||
|
||||
is_past = visible_date < datetime.datetime.now(UTC)
|
||||
|
||||
if is_past:
|
||||
test_message = 'Your grade and certificate will be ready after'
|
||||
self.assertNotContains(response, test_message)
|
||||
self.assertNotContains(response, "View Test_Certificate")
|
||||
|
||||
else:
|
||||
test_message = 'Congratulations! Your certificate is ready.'
|
||||
self.assertContains(response, test_message)
|
||||
self.assertNotContains(response, "View Test_Certificate")
|
||||
|
||||
|
||||
@@ -103,12 +103,13 @@ class TestCourseListing(ModuleStoreTestCase, MilestonesTestCaseMixin):
|
||||
"""
|
||||
Test the course list for regular staff when get_course returns an ErrorBlock
|
||||
"""
|
||||
# pylint: disable=protected-access
|
||||
mongo_store = modulestore()._get_modulestore_by_type(ModuleStoreEnum.Type.mongo)
|
||||
course_key = mongo_store.make_course_key('Org1', 'Course1', 'Run1')
|
||||
self._create_course_with_access_groups(course_key, default_store=ModuleStoreEnum.Type.mongo)
|
||||
store = modulestore()
|
||||
course_key = store.make_course_key('Org1', 'Course1', 'Run1')
|
||||
self._create_course_with_access_groups(course_key)
|
||||
|
||||
with mock.patch('xmodule.modulestore.mongo.base.MongoKeyValueStore', mock.Mock(side_effect=Exception)):
|
||||
with mock.patch(
|
||||
'xmodule.modulestore.split_mongo.caching_descriptor_system.SplitMongoKVS', mock.Mock(side_effect=Exception)
|
||||
):
|
||||
assert isinstance(modulestore().get_course(course_key), ErrorBlock)
|
||||
|
||||
# Invalidate (e.g., delete) the corresponding CourseOverview, forcing get_course to be called.
|
||||
@@ -122,14 +123,14 @@ class TestCourseListing(ModuleStoreTestCase, MilestonesTestCaseMixin):
|
||||
Create good courses, courses that won't load, and deleted courses which still have
|
||||
roles. Test course listing.
|
||||
"""
|
||||
mongo_store = modulestore()._get_modulestore_by_type(ModuleStoreEnum.Type.mongo) # lint-amnesty, pylint: disable=protected-access
|
||||
store = modulestore()
|
||||
|
||||
good_location = mongo_store.make_course_key('testOrg', 'testCourse', 'RunBabyRun')
|
||||
self._create_course_with_access_groups(good_location, default_store=ModuleStoreEnum.Type.mongo)
|
||||
good_location = store.make_course_key('testOrg', 'testCourse', 'RunBabyRun')
|
||||
self._create_course_with_access_groups(good_location)
|
||||
|
||||
course_location = mongo_store.make_course_key('testOrg', 'doomedCourse', 'RunBabyRun')
|
||||
self._create_course_with_access_groups(course_location, default_store=ModuleStoreEnum.Type.mongo)
|
||||
mongo_store.delete_course(course_location, ModuleStoreEnum.UserID.test)
|
||||
course_location = store.make_course_key('testOrg', 'doomedCourse', 'RunBabyRun')
|
||||
self._create_course_with_access_groups(course_location)
|
||||
store.delete_course(course_location, ModuleStoreEnum.UserID.test)
|
||||
|
||||
courses_list = list(get_course_enrollments(self.student, None, []))
|
||||
assert len(courses_list) == 1, courses_list
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
Test data created by CourseSerializer and CourseDetailSerializer
|
||||
"""
|
||||
|
||||
|
||||
from datetime import datetime
|
||||
from unittest import TestCase, mock
|
||||
|
||||
@@ -12,7 +11,7 @@ from rest_framework.request import Request
|
||||
from rest_framework.test import APIRequestFactory
|
||||
from xblock.core import XBlock
|
||||
from xmodule.course_block import DEFAULT_START_DATE
|
||||
from xmodule.modulestore.tests.django_utils import TEST_DATA_MONGO_AMNESTY_MODULESTORE, ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.django_utils import TEST_DATA_ONLY_SPLIT_MODULESTORE_DRAFT_PREFERRED, ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import check_mongo_calls
|
||||
|
||||
from lms.djangoapps.certificates.api import can_show_certificate_available_date_field
|
||||
@@ -32,7 +31,7 @@ class TestCourseSerializer(CourseApiFactoryMixin, ModuleStoreTestCase):
|
||||
maxDiff = 5000 # long enough to show mismatched dicts, in case of error
|
||||
serializer_class = CourseSerializer
|
||||
|
||||
MODULESTORE = TEST_DATA_MONGO_AMNESTY_MODULESTORE
|
||||
MODULESTORE = TEST_DATA_ONLY_SPLIT_MODULESTORE_DRAFT_PREFERRED
|
||||
ENABLED_SIGNALS = ['course_published']
|
||||
|
||||
def setUp(self):
|
||||
@@ -41,10 +40,10 @@ class TestCourseSerializer(CourseApiFactoryMixin, ModuleStoreTestCase):
|
||||
self.honor_user = self.create_user('honor', is_staff=False)
|
||||
self.request_factory = APIRequestFactory()
|
||||
|
||||
course_id = 'edX/toy/2012_Fall'
|
||||
banner_image_uri = '/c4x/edX/toy/asset/images_course_image.jpg'
|
||||
course_id = 'course-v1:edX+toy+2012_Fall'
|
||||
banner_image_uri = '/asset-v1:edX+toy+2012_Fall+type@asset+block@images_course_image.jpg'
|
||||
banner_image_absolute_uri = 'http://testserver' + banner_image_uri
|
||||
image_path = '/c4x/edX/toy/asset/just_a_test.jpg'
|
||||
image_path = '/asset-v1:edX+toy+2012_Fall+type@asset+block@just_a_test.jpg'
|
||||
image_url = 'http://testserver' + image_path
|
||||
self.expected_data = {
|
||||
'id': course_id,
|
||||
@@ -55,19 +54,15 @@ class TestCourseSerializer(CourseApiFactoryMixin, ModuleStoreTestCase):
|
||||
'media': {
|
||||
'banner_image': {
|
||||
'uri': banner_image_uri,
|
||||
'uri_absolute': banner_image_absolute_uri,
|
||||
},
|
||||
'course_image': {
|
||||
'uri': image_path,
|
||||
},
|
||||
'course_video': {
|
||||
'uri': 'http://www.youtube.com/watch?v=test_youtube_id',
|
||||
'uri_absolute': banner_image_absolute_uri
|
||||
},
|
||||
'course_image': {'uri': image_path},
|
||||
'course_video': {'uri': 'http://www.youtube.com/watch?v=test_youtube_id'},
|
||||
'image': {
|
||||
'raw': image_url,
|
||||
'small': image_url,
|
||||
'large': image_url,
|
||||
},
|
||||
'large': image_url
|
||||
}
|
||||
},
|
||||
'start': '2015-07-17T12:00:00Z',
|
||||
'start_type': 'timestamp',
|
||||
@@ -75,11 +70,11 @@ class TestCourseSerializer(CourseApiFactoryMixin, ModuleStoreTestCase):
|
||||
'end': '2015-09-19T18:00:00Z',
|
||||
'enrollment_start': '2015-06-15T00:00:00Z',
|
||||
'enrollment_end': '2015-07-15T00:00:00Z',
|
||||
'blocks_url': 'http://testserver/api/courses/v2/blocks/?course_id=edX%2Ftoy%2F2012_Fall',
|
||||
'blocks_url': 'http://testserver/api/courses/v2/blocks/?course_id=course-v1%3AedX%2Btoy%2B2012_Fall',
|
||||
'effort': '6 hours',
|
||||
'pacing': 'instructor',
|
||||
'mobile_available': True,
|
||||
'hidden': True, # because it's an old mongo course
|
||||
'hidden': False,
|
||||
'invitation_only': False,
|
||||
|
||||
# 'course_id' is a deprecated field, please use 'id' instead.
|
||||
@@ -126,14 +121,14 @@ class TestCourseSerializer(CourseApiFactoryMixin, ModuleStoreTestCase):
|
||||
advertised_start='The Ides of March'
|
||||
)
|
||||
result = self._get_result(course)
|
||||
assert result['course_id'] == 'edX/custom/2012_Fall'
|
||||
assert result['course_id'] == 'course-v1:edX+custom+2012_Fall'
|
||||
assert result['start_type'] == 'string'
|
||||
assert result['start_display'] == 'The Ides of March'
|
||||
|
||||
def test_empty_start(self):
|
||||
course = self.create_course(start=DEFAULT_START_DATE, course='custom')
|
||||
result = self._get_result(course)
|
||||
assert result['course_id'] == 'edX/custom/2012_Fall'
|
||||
assert result['course_id'] == 'course-v1:edX+custom+2012_Fall'
|
||||
assert result['start_type'] == 'empty'
|
||||
assert result['start_display'] is None
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import datetime
|
||||
import ddt
|
||||
import pytz
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.tests.django_utils import TEST_DATA_MONGO_AMNESTY_MODULESTORE, ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.django_utils import TEST_DATA_SPLIT_MODULESTORE, ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import ToyCourseFactory
|
||||
|
||||
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory
|
||||
@@ -32,7 +32,7 @@ class TestOverrideDataTransformer(ModuleStoreTestCase):
|
||||
"""
|
||||
Test proper behavior for OverrideDataTransformer
|
||||
"""
|
||||
MODULESTORE = TEST_DATA_MONGO_AMNESTY_MODULESTORE
|
||||
MODULESTORE = TEST_DATA_SPLIT_MODULESTORE
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
@@ -52,10 +52,10 @@ class TestOverrideDataTransformer(ModuleStoreTestCase):
|
||||
self.learner.id, subsection.location, 'html', 'new_component'
|
||||
)
|
||||
CourseEnrollmentFactory.create(user=self.learner, course_id=self.course_key, is_active=True)
|
||||
self.block = self.store.create_child(
|
||||
self.learner2.id, subsection.location, 'html', 'new_component'
|
||||
)
|
||||
CourseEnrollmentFactory.create(user=self.learner2, course_id=self.course_key, is_active=True)
|
||||
self.block = self.store.create_child(
|
||||
self.learner2.id, subsection.location, 'html', 'new_component_2'
|
||||
)
|
||||
|
||||
@ddt.data(*REQUESTED_FIELDS)
|
||||
def test_transform(self, field):
|
||||
|
||||
@@ -5,7 +5,7 @@ Tests for wiki permissions
|
||||
|
||||
from django.contrib.auth.models import Group
|
||||
from wiki.models import URLPath
|
||||
from xmodule.modulestore.tests.django_utils import TEST_DATA_MONGO_AMNESTY_MODULESTORE, ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.django_utils import TEST_DATA_SPLIT_MODULESTORE, ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
from common.djangoapps.student.tests.factories import InstructorFactory
|
||||
@@ -18,14 +18,14 @@ from common.djangoapps.student.tests.factories import UserFactory
|
||||
|
||||
class TestWikiAccessBase(ModuleStoreTestCase):
|
||||
"""Base class for testing wiki access."""
|
||||
MODULESTORE = TEST_DATA_MONGO_AMNESTY_MODULESTORE
|
||||
MODULESTORE = TEST_DATA_SPLIT_MODULESTORE
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
self.wiki = get_or_create_root()
|
||||
|
||||
self.course_math101 = CourseFactory.create(org='org', number='math101', display_name='Course', metadata={'use_unique_wiki_id': 'false'}) # lint-amnesty, pylint: disable=line-too-long
|
||||
self.course_math101 = CourseFactory.create(org='org', number='math101', display_name='Course') # lint-amnesty, pylint: disable=line-too-long
|
||||
self.course_math101_staff = self.create_staff_for_course(self.course_math101)
|
||||
|
||||
wiki_math101 = self.create_urlpath(self.wiki, course_wiki_slug(self.course_math101))
|
||||
@@ -33,7 +33,7 @@ class TestWikiAccessBase(ModuleStoreTestCase):
|
||||
wiki_math101_page_page = self.create_urlpath(wiki_math101_page, 'Grandchild')
|
||||
self.wiki_math101_pages = [wiki_math101, wiki_math101_page, wiki_math101_page_page]
|
||||
|
||||
self.course_math101b = CourseFactory.create(org='org', number='math101b', display_name='Course', metadata={'use_unique_wiki_id': 'true'}) # lint-amnesty, pylint: disable=line-too-long
|
||||
self.course_math101b = CourseFactory.create(org='org', number='math101b', display_name='Course') # lint-amnesty, pylint: disable=line-too-long
|
||||
self.course_math101b_staff = self.create_staff_for_course(self.course_math101b)
|
||||
|
||||
wiki_math101b = self.create_urlpath(self.wiki, course_wiki_slug(self.course_math101b))
|
||||
|
||||
@@ -5,7 +5,7 @@ Tests for wiki middleware.
|
||||
|
||||
from django.test.client import Client
|
||||
from wiki.models import URLPath
|
||||
from xmodule.modulestore.tests.django_utils import TEST_DATA_MONGO_AMNESTY_MODULESTORE, ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.django_utils import TEST_DATA_SPLIT_MODULESTORE, ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
from common.djangoapps.student.tests.factories import InstructorFactory
|
||||
@@ -14,7 +14,7 @@ from lms.djangoapps.course_wiki.views import get_or_create_root
|
||||
|
||||
class TestWikiAccessMiddleware(ModuleStoreTestCase):
|
||||
"""Tests for WikiAccessMiddleware."""
|
||||
MODULESTORE = TEST_DATA_MONGO_AMNESTY_MODULESTORE
|
||||
MODULESTORE = TEST_DATA_SPLIT_MODULESTORE
|
||||
|
||||
def setUp(self):
|
||||
"""Test setup."""
|
||||
@@ -22,7 +22,7 @@ class TestWikiAccessMiddleware(ModuleStoreTestCase):
|
||||
|
||||
self.wiki = get_or_create_root()
|
||||
|
||||
self.course_math101 = CourseFactory.create(org='edx', number='math101', display_name='2014', metadata={'use_unique_wiki_id': 'false'}) # lint-amnesty, pylint: disable=line-too-long
|
||||
self.course_math101 = CourseFactory.create(org='edx', number='math101', display_name='2014')
|
||||
self.course_math101_instructor = InstructorFactory(course_key=self.course_math101.id, username='instructor', password='secret') # lint-amnesty, pylint: disable=line-too-long
|
||||
self.wiki_math101 = URLPath.create_article(self.wiki, 'math101', title='math101')
|
||||
|
||||
@@ -31,6 +31,6 @@ class TestWikiAccessMiddleware(ModuleStoreTestCase):
|
||||
|
||||
def test_url_tranform(self):
|
||||
"""Test that the correct prefix ('/courses/<course_id>') is added to the urls in the wiki."""
|
||||
response = self.client.get('/courses/edx/math101/2014/wiki/math101/')
|
||||
self.assertContains(response, '/courses/edx/math101/2014/wiki/math101/_edit/')
|
||||
self.assertContains(response, '/courses/edx/math101/2014/wiki/math101/_settings/')
|
||||
response = self.client.get('/courses/course-v1:edx+math101+2014/wiki/math101/')
|
||||
self.assertContains(response, '/courses/course-v1:edx+math101+2014/wiki/math101/_edit/')
|
||||
self.assertContains(response, '/courses/course-v1:edx+math101+2014/wiki/math101/_settings/')
|
||||
|
||||
@@ -31,7 +31,7 @@ from common.djangoapps.student.models import CourseEnrollment, Registration
|
||||
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory
|
||||
from common.djangoapps.util.date_utils import strftime_localized_html
|
||||
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.modulestore.tests.django_utils import TEST_DATA_MONGO_MODULESTORE, ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.modulestore.tests.django_utils import TEST_DATA_SPLIT_MODULESTORE, ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, BlockFactory # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.tests import get_test_descriptor_system, get_test_system, prepare_block_runtime # lint-amnesty, pylint: disable=wrong-import-order
|
||||
|
||||
@@ -47,15 +47,14 @@ class BaseTestXmodule(ModuleStoreTestCase):
|
||||
1. CATEGORY
|
||||
2. DATA or METADATA
|
||||
3. MODEL_DATA
|
||||
4. COURSE_DATA and USER_COUNT if needed
|
||||
4. USER_COUNT if needed
|
||||
|
||||
This class should not contain any tests, because CATEGORY
|
||||
should be defined in child class.
|
||||
"""
|
||||
MODULESTORE = TEST_DATA_MONGO_MODULESTORE
|
||||
MODULESTORE = TEST_DATA_SPLIT_MODULESTORE
|
||||
|
||||
USER_COUNT = 2
|
||||
COURSE_DATA = {}
|
||||
|
||||
# Data from YAML xmodule/templates/NAME/default.yaml
|
||||
CATEGORY = "vertical"
|
||||
@@ -101,7 +100,7 @@ class BaseTestXmodule(ModuleStoreTestCase):
|
||||
self.item_url = str(self.block.location)
|
||||
|
||||
def setup_course(self): # lint-amnesty, pylint: disable=missing-function-docstring
|
||||
self.course = CourseFactory.create(data=self.COURSE_DATA)
|
||||
self.course = CourseFactory.create()
|
||||
|
||||
# Turn off cache.
|
||||
modulestore().request_cache = None
|
||||
|
||||
@@ -16,7 +16,6 @@ from lms.djangoapps.courseware.views.views import get_course_lti_endpoints
|
||||
from openedx.core.lib.url_utils import quote_slashes
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, BlockFactory # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.x_module import STUDENT_VIEW # lint-amnesty, pylint: disable=wrong-import-order
|
||||
|
||||
|
||||
class TestLTI(BaseTestXmodule):
|
||||
@@ -117,7 +116,7 @@ class TestLTI(BaseTestXmodule):
|
||||
self.addCleanup(patcher.stop)
|
||||
|
||||
def test_lti_constructor(self):
|
||||
generated_content = self.block.render(STUDENT_VIEW).content
|
||||
generated_content = self.block.student_view(None).content
|
||||
expected_content = self.runtime.render_template('lti.html', self.expected_context)
|
||||
assert generated_content == expected_content
|
||||
|
||||
|
||||
@@ -146,6 +146,8 @@ class BaseTestVideoXBlock(BaseTestXmodule):
|
||||
# a lot of tests code, parse and set the values as fields.
|
||||
fields_data = VideoBlock.parse_video_xml(data)
|
||||
kwargs.update(fields_data)
|
||||
kwargs.pop('source', None)
|
||||
kwargs.get('metadata', {}).pop('source', None)
|
||||
super().initialize_module(**kwargs)
|
||||
|
||||
def setUp(self):
|
||||
|
||||
@@ -43,9 +43,8 @@ from xmodule.course_block import (
|
||||
COURSE_VIDEO_SHARING_PER_VIDEO
|
||||
)
|
||||
from xmodule.exceptions import NotFoundError
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.inheritance import own_metadata
|
||||
from xmodule.modulestore.tests.django_utils import TEST_DATA_MONGO_MODULESTORE, TEST_DATA_SPLIT_MODULESTORE
|
||||
from xmodule.modulestore.tests.django_utils import TEST_DATA_SPLIT_MODULESTORE
|
||||
# noinspection PyUnresolvedReferences
|
||||
from xmodule.tests.helpers import override_descriptor_system # pylint: disable=unused-import
|
||||
from xmodule.tests.test_import import DummySystem
|
||||
@@ -65,11 +64,6 @@ from openedx.core.djangolib.testing.utils import CacheIsolationTestCase
|
||||
from .test_video_handlers import BaseTestVideoXBlock, TestVideo
|
||||
from .test_video_xml import SOURCE_XML, PUBLIC_SOURCE_XML
|
||||
|
||||
MODULESTORES = {
|
||||
ModuleStoreEnum.Type.mongo: TEST_DATA_MONGO_MODULESTORE,
|
||||
ModuleStoreEnum.Type.split: TEST_DATA_SPLIT_MODULESTORE,
|
||||
}
|
||||
|
||||
TRANSCRIPT_FILE_SRT_DATA = """
|
||||
1
|
||||
00:00:14,370 --> 00:00:16,530
|
||||
@@ -88,7 +82,7 @@ class TestVideoYouTube(TestVideo): # lint-amnesty, pylint: disable=missing-clas
|
||||
|
||||
def test_video_constructor(self):
|
||||
"""Make sure that all parameters extracted correctly from xml"""
|
||||
context = self.block.render(STUDENT_VIEW).content
|
||||
context = self.block.student_view(None).content
|
||||
sources = ['example.mp4', 'example.webm']
|
||||
|
||||
expected_context = {
|
||||
@@ -173,7 +167,7 @@ class TestVideoNonYouTube(TestVideo): # pylint: disable=test-inherits-tests
|
||||
"""Make sure that if the 'youtube' attribute is omitted in XML, then
|
||||
the template generates an empty string for the YouTube streams.
|
||||
"""
|
||||
context = self.block.render(STUDENT_VIEW).content
|
||||
context = self.block.student_view(None).content
|
||||
sources = ['example.mp4', 'example.webm']
|
||||
|
||||
expected_context = {
|
||||
@@ -328,7 +322,7 @@ class TestVideoPublicAccess(BaseTestVideoXBlock):
|
||||
'is_public_sharing_enabled',
|
||||
return_value=is_public_sharing_enabled
|
||||
):
|
||||
content = self.block.render(STUDENT_VIEW).content
|
||||
content = self.block.student_view(None).content
|
||||
context = get_context_dict_from_string(content)
|
||||
assert ('public_sharing_enabled' in context) == is_public_sharing_enabled
|
||||
assert ('public_video_url' in context) == is_public_sharing_enabled
|
||||
@@ -393,7 +387,7 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
|
||||
# lint-amnesty, pylint: disable=redefined-outer-name
|
||||
SOURCE_XML = """
|
||||
<video show_captions="true"
|
||||
display_name="A Name"
|
||||
display_name="{name}"
|
||||
sub="{sub}" download_track="{download_track}"
|
||||
start_time="3603.0" end_time="3610.0" download_video="true"
|
||||
>
|
||||
@@ -406,6 +400,7 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
|
||||
|
||||
cases = [
|
||||
{
|
||||
'name': 'video 1',
|
||||
'download_track': 'true',
|
||||
'track': '<track src="http://www.example.com/track"/>',
|
||||
'sub': 'a_sub_file.srt.sjson',
|
||||
@@ -413,6 +408,7 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
|
||||
'transcripts': '',
|
||||
},
|
||||
{
|
||||
'name': 'video 2',
|
||||
'download_track': 'true',
|
||||
'track': '',
|
||||
'sub': 'a_sub_file.srt.sjson',
|
||||
@@ -420,6 +416,7 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
|
||||
'transcripts': '',
|
||||
},
|
||||
{
|
||||
'name': 'video 3',
|
||||
'download_track': 'true',
|
||||
'track': '',
|
||||
'sub': '',
|
||||
@@ -427,6 +424,7 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
|
||||
'transcripts': '',
|
||||
},
|
||||
{
|
||||
'name': 'video 4',
|
||||
'download_track': 'false',
|
||||
'track': '<track src="http://www.example.com/track"/>',
|
||||
'sub': 'a_sub_file.srt.sjson',
|
||||
@@ -434,6 +432,7 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
|
||||
'transcripts': '',
|
||||
},
|
||||
{
|
||||
'name': 'video 5',
|
||||
'download_track': 'true',
|
||||
'track': '',
|
||||
'sub': '',
|
||||
@@ -477,12 +476,13 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
|
||||
track=data['track'],
|
||||
sub=data['sub'],
|
||||
transcripts=data['transcripts'],
|
||||
name=data['name'],
|
||||
)
|
||||
|
||||
self.initialize_block(data=DATA)
|
||||
track_url = self.get_handler_url('transcript', 'download')
|
||||
|
||||
context = self.block.render(STUDENT_VIEW).content
|
||||
context = self.block.student_view(None).content
|
||||
metadata.update({
|
||||
'transcriptLanguages': {"en": "English"} if not data['transcripts'] else {"uk": 'Українська'},
|
||||
'transcriptLanguage': 'en' if not data['transcripts'] or data.get('sub') else 'uk',
|
||||
@@ -492,6 +492,7 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
|
||||
'saveStateUrl': self.block.ajax_url + '/save_user_state',
|
||||
})
|
||||
expected_context.update({
|
||||
'display_name': data['name'],
|
||||
'transcript_download_format': (
|
||||
None if self.block.track and self.block.download_track else 'srt'
|
||||
),
|
||||
@@ -512,7 +513,7 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
|
||||
# lint-amnesty, pylint: disable=invalid-name, redefined-outer-name
|
||||
SOURCE_XML = """
|
||||
<video show_captions="true"
|
||||
display_name="A Name"
|
||||
display_name="{name}"
|
||||
sub="a_sub_file.srt.sjson" source="{source}"
|
||||
download_video="{download_video}"
|
||||
start_time="3603.0" end_time="3610.0"
|
||||
@@ -523,6 +524,7 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
|
||||
cases = [
|
||||
# self.download_video == True
|
||||
{
|
||||
'name': 'video 1',
|
||||
'download_video': 'true',
|
||||
'source': 'example_source.mp4',
|
||||
'sources': """
|
||||
@@ -535,6 +537,7 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
|
||||
},
|
||||
},
|
||||
{
|
||||
'name': 'video 2',
|
||||
'download_video': 'true',
|
||||
'source': '',
|
||||
'sources': """
|
||||
@@ -547,6 +550,7 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
|
||||
},
|
||||
},
|
||||
{
|
||||
'name': 'video 3',
|
||||
'download_video': 'true',
|
||||
'source': '',
|
||||
'sources': [],
|
||||
@@ -555,6 +559,7 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
|
||||
|
||||
# self.download_video == False
|
||||
{
|
||||
'name': 'video 4',
|
||||
'download_video': 'false',
|
||||
'source': 'example_source.mp4',
|
||||
'sources': """
|
||||
@@ -597,10 +602,11 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
|
||||
DATA = SOURCE_XML.format( # lint-amnesty, pylint: disable=invalid-name
|
||||
download_video=data['download_video'],
|
||||
source=data['source'],
|
||||
sources=data['sources']
|
||||
sources=data['sources'],
|
||||
name=data['name'],
|
||||
)
|
||||
self.initialize_block(data=DATA)
|
||||
context = self.block.render(STUDENT_VIEW).content
|
||||
context = self.block.student_view(None).content
|
||||
|
||||
expected_context = dict(initial_context)
|
||||
expected_context['metadata'].update({
|
||||
@@ -611,6 +617,7 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
|
||||
'sources': data['result'].get('sources', []),
|
||||
})
|
||||
expected_context.update({
|
||||
'display_name': data['name'],
|
||||
'id': self.block.location.html_id(),
|
||||
'block_id': str(self.block.location),
|
||||
'course_id': str(self.block.location.course_key),
|
||||
@@ -630,7 +637,7 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
|
||||
# lint-amnesty, pylint: disable=redefined-outer-name
|
||||
SOURCE_XML = """
|
||||
<video show_captions="true"
|
||||
display_name="A Name"
|
||||
display_name="{name}"
|
||||
sub="a_sub_file.srt.sjson" source="{source}"
|
||||
download_video="{download_video}"
|
||||
start_time="3603.0" end_time="3610.0"
|
||||
@@ -640,6 +647,7 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
|
||||
</video>
|
||||
"""
|
||||
no_video_data = {
|
||||
'name': 'video 1',
|
||||
'download_video': 'true',
|
||||
'source': 'example_source.mp4',
|
||||
'sources': """
|
||||
@@ -656,13 +664,14 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
|
||||
download_video=no_video_data['download_video'],
|
||||
source=no_video_data['source'],
|
||||
sources=no_video_data['sources'],
|
||||
edx_video_id=no_video_data['edx_video_id']
|
||||
edx_video_id=no_video_data['edx_video_id'],
|
||||
name=no_video_data['name'],
|
||||
)
|
||||
self.initialize_block(data=DATA)
|
||||
|
||||
# Referencing a non-existent VAL ID in courseware won't cause an error --
|
||||
# it'll just fall back to the values in the VideoBlock.
|
||||
assert 'example.mp4' in self.block.render(STUDENT_VIEW).content
|
||||
assert 'example.mp4' in self.block.student_view(None).content
|
||||
|
||||
def test_get_html_with_mocked_edx_video_id(self):
|
||||
# lint-amnesty, pylint: disable=invalid-name, redefined-outer-name
|
||||
@@ -746,7 +755,7 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
|
||||
}
|
||||
]
|
||||
}
|
||||
context = self.block.render(STUDENT_VIEW).content
|
||||
context = self.block.student_view(None).content
|
||||
|
||||
expected_context = dict(initial_context)
|
||||
expected_context['metadata'].update({
|
||||
@@ -909,7 +918,7 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
|
||||
)
|
||||
self.initialize_block(data=DATA)
|
||||
# context returned by get_html
|
||||
context = self.block.render(STUDENT_VIEW).content
|
||||
context = self.block.student_view(None).content
|
||||
|
||||
# expected_context, expected context to be returned by get_html
|
||||
expected_context = dict(initial_context)
|
||||
@@ -1031,7 +1040,7 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
|
||||
user_service = self.block.runtime.service(self.block, 'user')
|
||||
user_location = user_service.get_current_user().opt_attrs[ATTR_KEY_REQUEST_COUNTRY_CODE]
|
||||
assert user_location == 'CN'
|
||||
context = self.block.render('student_view').content
|
||||
context = self.block.student_view(None).content
|
||||
expected_context = dict(initial_context)
|
||||
expected_context['metadata'].update({
|
||||
'transcriptTranslationUrl': self.get_handler_url('transcript', 'translation/__lang__'),
|
||||
@@ -1137,7 +1146,7 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
|
||||
'client_video_id': 'external video',
|
||||
'encoded_videos': {}
|
||||
}
|
||||
context = self.block.render(STUDENT_VIEW).content
|
||||
context = self.block.student_view(None).content
|
||||
expected_context = dict(initial_context)
|
||||
expected_context['metadata'].update({
|
||||
'transcriptTranslationUrl': self.get_handler_url('transcript', 'translation/__lang__'),
|
||||
@@ -1203,7 +1212,7 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
|
||||
}
|
||||
|
||||
self.initialize_block(data=video_xml)
|
||||
context = self.block.render(STUDENT_VIEW).content
|
||||
context = self.block.student_view(None).content
|
||||
|
||||
assert "'download_video_link': 'https://mp4.com/dm.mp4'" in context
|
||||
assert '"streams": "1.00:https://yt.com/?v=v0TFmdO4ZP0"' in context
|
||||
@@ -1221,7 +1230,7 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
|
||||
"""
|
||||
|
||||
self.initialize_block(data=video_xml)
|
||||
context = self.block.render(STUDENT_VIEW).content
|
||||
context = self.block.student_view(None).content
|
||||
assert "'download_video_link': None" in context
|
||||
|
||||
def test_get_html_non_hls_video_download(self):
|
||||
@@ -1237,7 +1246,7 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
|
||||
"""
|
||||
|
||||
self.initialize_block(data=video_xml)
|
||||
context = self.block.render(STUDENT_VIEW).content
|
||||
context = self.block.student_view(None).content
|
||||
assert "'download_video_link': 'http://example.com/example.mp4'" in context
|
||||
|
||||
def test_html_student_public_view(self):
|
||||
@@ -1251,7 +1260,7 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
|
||||
"""
|
||||
|
||||
self.initialize_block(data=video_xml)
|
||||
context = self.block.render(STUDENT_VIEW).content
|
||||
context = self.block.student_view(None).content
|
||||
assert '"saveStateEnabled": true' in context
|
||||
context = self.block.render(PUBLIC_VIEW).content
|
||||
assert '"saveStateEnabled": false' in context
|
||||
@@ -1265,7 +1274,7 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
|
||||
get_course_video_image_url.return_value = '/media/video-images/poster.png'
|
||||
|
||||
self.initialize_block(data=video_xml)
|
||||
context = self.block.render(STUDENT_VIEW).content
|
||||
context = self.block.student_view(None).content
|
||||
|
||||
assert '"poster": "/media/video-images/poster.png"' in context
|
||||
|
||||
@@ -1278,7 +1287,7 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
|
||||
get_course_video_image_url.return_value = '/media/video-images/poster.png'
|
||||
|
||||
self.initialize_block(data=video_xml)
|
||||
context = self.block.render(STUDENT_VIEW).content
|
||||
context = self.block.student_view(None).content
|
||||
|
||||
assert "'poster': 'null'" in context
|
||||
|
||||
@@ -1289,7 +1298,7 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
|
||||
"""
|
||||
video_xml = '<video display_name="Video" download_video="true" edx_video_id="12345-67890">[]</video>'
|
||||
self.initialize_block(data=video_xml)
|
||||
context = self.block.render(STUDENT_VIEW).content
|
||||
context = self.block.student_view(None).content
|
||||
assert '"prioritizeHls": false' in context
|
||||
|
||||
@ddt.data(
|
||||
@@ -1343,7 +1352,7 @@ class TestGetHtmlMethod(BaseTestVideoXBlock):
|
||||
with patch.object(WaffleFlagCourseOverrideModel, 'override_value', return_value=data['course_override']):
|
||||
with override_waffle_flag(DEPRECATE_YOUTUBE, active=data['waffle_enabled']):
|
||||
self.initialize_block(data=video_xml, metadata=metadata)
|
||||
context = self.block.render(STUDENT_VIEW).content
|
||||
context = self.block.student_view(None).content
|
||||
assert '"prioritizeHls": {}'.format(data['result']) in context
|
||||
|
||||
|
||||
@@ -1457,6 +1466,7 @@ class TestEditorSavedMethod(BaseTestVideoXBlock):
|
||||
CATEGORY = "video"
|
||||
DATA = SOURCE_XML
|
||||
METADATA = {}
|
||||
MODULESTORE = TEST_DATA_SPLIT_MODULESTORE
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
@@ -1470,14 +1480,12 @@ class TestEditorSavedMethod(BaseTestVideoXBlock):
|
||||
self.test_dir = path(__file__).abspath().dirname().dirname().dirname().dirname().dirname()
|
||||
self.file_path = self.test_dir + '/common/test/data/uploads/' + self.file_name
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
def test_editor_saved_when_html5_sub_not_exist(self, default_store):
|
||||
def test_editor_saved_when_html5_sub_not_exist(self):
|
||||
"""
|
||||
When there is youtube_sub exist but no html5_sub present for
|
||||
html5_sources, editor_saved function will generate new html5_sub
|
||||
for video.
|
||||
"""
|
||||
self.MODULESTORE = MODULESTORES[default_store] # pylint: disable=invalid-name
|
||||
self.initialize_block(metadata=self.metadata)
|
||||
item = self.store.get_item(self.block.location)
|
||||
with open(self.file_path, "rb") as myfile: # lint-amnesty, pylint: disable=bad-option-value, open-builtin
|
||||
@@ -1491,13 +1499,11 @@ class TestEditorSavedMethod(BaseTestVideoXBlock):
|
||||
item.editor_saved(self.user, old_metadata, None)
|
||||
assert isinstance(Transcript.get_asset(item.location, 'subs_3_yD_cEKoCk.srt.sjson'), StaticContent)
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
def test_editor_saved_when_youtube_and_html5_subs_exist(self, default_store):
|
||||
def test_editor_saved_when_youtube_and_html5_subs_exist(self):
|
||||
"""
|
||||
When both youtube_sub and html5_sub already exist then no new
|
||||
sub will be generated by editor_saved function.
|
||||
"""
|
||||
self.MODULESTORE = MODULESTORES[default_store]
|
||||
self.initialize_block(metadata=self.metadata)
|
||||
item = self.store.get_item(self.block.location)
|
||||
with open(self.file_path, "rb") as myfile: # lint-amnesty, pylint: disable=bad-option-value, open-builtin
|
||||
@@ -1512,12 +1518,10 @@ class TestEditorSavedMethod(BaseTestVideoXBlock):
|
||||
item.editor_saved(self.user, old_metadata, None)
|
||||
assert not manage_video_subtitles_save.called
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
def test_editor_saved_with_unstripped_video_id(self, default_store):
|
||||
def test_editor_saved_with_unstripped_video_id(self):
|
||||
"""
|
||||
Verify editor saved when video id contains spaces/tabs.
|
||||
"""
|
||||
self.MODULESTORE = MODULESTORES[default_store]
|
||||
stripped_video_id = str(uuid4())
|
||||
unstripped_video_id = '{video_id}{tabs}'.format(video_id=stripped_video_id, tabs='\t\t\t')
|
||||
self.metadata.update({
|
||||
@@ -1533,14 +1537,12 @@ class TestEditorSavedMethod(BaseTestVideoXBlock):
|
||||
item.editor_saved(self.user, old_metadata, None)
|
||||
assert item.edx_video_id == stripped_video_id
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@patch('xmodule.video_block.video_block.edxval_api.get_url_for_profile', Mock(return_value='test_yt_id'))
|
||||
def test_editor_saved_with_yt_val_profile(self, default_store):
|
||||
def test_editor_saved_with_yt_val_profile(self):
|
||||
"""
|
||||
Verify editor saved overrides `youtube_id_1_0` when a youtube val profile is there
|
||||
for a given `edx_video_id`.
|
||||
"""
|
||||
self.MODULESTORE = MODULESTORES[default_store]
|
||||
self.initialize_block(metadata=self.metadata)
|
||||
item = self.store.get_item(self.block.location)
|
||||
assert item.youtube_id_1_0 == '3_yD_cEKoCk'
|
||||
@@ -1677,7 +1679,7 @@ class TestVideoBlockStudentViewJson(BaseTestVideoXBlock, CacheIsolationTestCase)
|
||||
self.verify_result_with_fallback_and_youtube(result)
|
||||
|
||||
def test_no_edx_video_id_and_no_fallback(self):
|
||||
video_declaration = f"<video display_name='Test Video' youtube_id_1_0=\'{self.TEST_YOUTUBE_ID}\'>"
|
||||
video_declaration = f"<video display_name='Test Video 2' youtube_id_1_0=\'{self.TEST_YOUTUBE_ID}\'>"
|
||||
# the video has no source listed, only a youtube link, so no fallback url will be provided
|
||||
sample_xml = ''.join([
|
||||
video_declaration,
|
||||
@@ -2332,7 +2334,7 @@ class TestVideoWithBumper(TestVideo): # pylint: disable=test-inherits-tests
|
||||
|
||||
is_bumper_enabled.return_value = True
|
||||
|
||||
content = self.block.render(STUDENT_VIEW).content
|
||||
content = self.block.student_view(None).content
|
||||
sources = ['example.mp4', 'example.webm']
|
||||
expected_context = {
|
||||
'autoadvance_enabled': False,
|
||||
@@ -2497,7 +2499,7 @@ class TestAutoAdvanceVideo(TestVideo): # lint-amnesty, pylint: disable=test-inh
|
||||
"""
|
||||
|
||||
with override_settings(FEATURES=self.FEATURES):
|
||||
content = self.block.render(STUDENT_VIEW).content
|
||||
content = self.block.student_view(None).content
|
||||
|
||||
expected_context = self.prepare_expected_context(
|
||||
autoadvanceenabled_flag=autoadvanceenabled_must_be,
|
||||
@@ -2521,7 +2523,7 @@ class TestAutoAdvanceVideo(TestVideo): # lint-amnesty, pylint: disable=test-inh
|
||||
self.block.video_auto_advance = new_value
|
||||
self.block._reset_dirty_field(self.block.fields['video_auto_advance']) # pylint: disable=protected-access
|
||||
# After this step, render() should see the new value
|
||||
# e.g. use self.block.render(STUDENT_VIEW).content
|
||||
# e.g. use self.block.student_view(None).content
|
||||
|
||||
@ddt.data(
|
||||
(False, False),
|
||||
|
||||
@@ -14,7 +14,7 @@ from lms.djangoapps.courseware.tests.helpers import LoginEnrollmentTestCase
|
||||
from lms.djangoapps.lms_xblock.field_data import LmsFieldData
|
||||
from xmodule.error_block import ErrorBlock # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.modulestore.tests.django_utils import TEST_DATA_MIXED_MODULESTORE, ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.modulestore.tests.django_utils import TEST_DATA_SPLIT_MODULESTORE, ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from xmodule.modulestore.tests.factories import ToyCourseFactory # lint-amnesty, pylint: disable=wrong-import-order
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@ class TestMongoCoursesLoad(ModuleStoreTestCase, PageLoaderTestCase):
|
||||
"""
|
||||
Check that all pages in test courses load properly from Mongo.
|
||||
"""
|
||||
MODULESTORE = TEST_DATA_MIXED_MODULESTORE
|
||||
MODULESTORE = TEST_DATA_SPLIT_MODULESTORE
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
@@ -131,7 +131,7 @@ class TestMongoCoursesLoad(ModuleStoreTestCase, PageLoaderTestCase):
|
||||
<entry page="5" page_label="ii" name="Table of Contents"/>
|
||||
</table_of_contents>
|
||||
""").strip()
|
||||
location = self.toy_course_key.make_usage_key('course', '2012_Fall')
|
||||
location = self.toy_course_key.make_usage_key('course', 'course')
|
||||
course = self.store.get_item(location)
|
||||
assert len(course.textbooks) > 0
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ from django.utils.translation import override as override_language
|
||||
from opaque_keys.edx.locator import CourseLocator
|
||||
from submissions import api as sub_api
|
||||
|
||||
from xmodule.modulestore.tests.django_utils import TEST_DATA_MONGO_AMNESTY_MODULESTORE, SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.django_utils import TEST_DATA_SPLIT_MODULESTORE, SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, BlockFactory
|
||||
from xmodule.capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory
|
||||
from common.djangoapps.student.models import CourseEnrollment, CourseEnrollmentAllowed, anonymous_id_for_user
|
||||
@@ -368,21 +368,26 @@ class TestInstructorUnenrollDB(TestEnrollmentChangeBase):
|
||||
|
||||
class TestInstructorEnrollmentStudentModule(SharedModuleStoreTestCase):
|
||||
""" Test student module manipulations. """
|
||||
MODULESTORE = TEST_DATA_MONGO_AMNESTY_MODULESTORE
|
||||
MODULESTORE = TEST_DATA_SPLIT_MODULESTORE
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.course = CourseFactory(
|
||||
cls.course = CourseFactory.create(
|
||||
name='fake',
|
||||
org='course',
|
||||
run='id',
|
||||
)
|
||||
cls.course_key = cls.course.location.course_key # lint-amnesty, pylint: disable=no-member
|
||||
with cls.store.bulk_operations(cls.course.id, emit_signals=False): # lint-amnesty, pylint: disable=no-member
|
||||
cls.chapter = BlockFactory.create(
|
||||
category='chapter',
|
||||
parent=cls.course,
|
||||
display_name='chapter'
|
||||
)
|
||||
cls.parent = BlockFactory(
|
||||
category="library_content",
|
||||
parent=cls.course,
|
||||
parent=cls.chapter,
|
||||
publish_item=True,
|
||||
)
|
||||
cls.child = BlockFactory(
|
||||
@@ -392,7 +397,7 @@ class TestInstructorEnrollmentStudentModule(SharedModuleStoreTestCase):
|
||||
)
|
||||
cls.unrelated = BlockFactory(
|
||||
category="html",
|
||||
parent=cls.course,
|
||||
parent=cls.chapter,
|
||||
publish_item=True,
|
||||
)
|
||||
cls.team_enabled_ora = BlockFactory.create(
|
||||
|
||||
@@ -8,8 +8,8 @@ from unittest.mock import ANY, MagicMock, patch
|
||||
from django.test import TestCase
|
||||
from lxml import etree
|
||||
from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator
|
||||
from xmodule.modulestore.tests.django_utils import TEST_DATA_MONGO_AMNESTY_MODULESTORE, ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, BlockFactory, check_mongo_calls
|
||||
from xmodule.modulestore.tests.django_utils import TEST_DATA_SPLIT_MODULESTORE, ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, BlockFactory
|
||||
|
||||
import lms.djangoapps.lti_provider.outcomes as outcomes
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
@@ -297,7 +297,7 @@ class TestAssignmentsForProblem(ModuleStoreTestCase):
|
||||
"""
|
||||
Test cases for the assignments_for_problem method in outcomes.py
|
||||
"""
|
||||
MODULESTORE = TEST_DATA_MONGO_AMNESTY_MODULESTORE
|
||||
MODULESTORE = TEST_DATA_SPLIT_MODULESTORE
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
@@ -371,48 +371,41 @@ class TestAssignmentsForProblem(ModuleStoreTestCase):
|
||||
assert count == 3
|
||||
|
||||
def test_with_no_graded_assignments(self):
|
||||
with check_mongo_calls(7):
|
||||
assignments = outcomes.get_assignments_for_problem(
|
||||
self.unit, self.user_id, self.course.id
|
||||
)
|
||||
assignments = outcomes.get_assignments_for_problem(
|
||||
self.unit, self.user_id, self.course.id
|
||||
)
|
||||
assert len(assignments) == 0
|
||||
|
||||
def test_with_graded_unit(self):
|
||||
self.create_graded_assignment(self.unit, 'graded_unit', self.outcome_service)
|
||||
with check_mongo_calls(7):
|
||||
assignments = outcomes.get_assignments_for_problem(
|
||||
self.unit, self.user_id, self.course.id
|
||||
)
|
||||
assignments = outcomes.get_assignments_for_problem(
|
||||
self.unit, self.user_id, self.course.id
|
||||
)
|
||||
assert len(assignments) == 1
|
||||
assert assignments[0].lis_result_sourcedid == 'graded_unit'
|
||||
|
||||
def test_with_graded_vertical(self):
|
||||
self.create_graded_assignment(self.vertical, 'graded_vertical', self.outcome_service)
|
||||
with check_mongo_calls(7):
|
||||
assignments = outcomes.get_assignments_for_problem(
|
||||
self.unit, self.user_id, self.course.id
|
||||
)
|
||||
assert len(assignments) == 1
|
||||
assert assignments[0].lis_result_sourcedid == 'graded_vertical'
|
||||
assignments = outcomes.get_assignments_for_problem(
|
||||
self.unit, self.user_id, self.course.id
|
||||
)
|
||||
assert len(assignments) == 0
|
||||
|
||||
def test_with_graded_unit_and_vertical(self):
|
||||
self.create_graded_assignment(self.unit, 'graded_unit', self.outcome_service)
|
||||
self.create_graded_assignment(self.vertical, 'graded_vertical', self.outcome_service)
|
||||
with check_mongo_calls(7):
|
||||
assignments = outcomes.get_assignments_for_problem(
|
||||
self.unit, self.user_id, self.course.id
|
||||
)
|
||||
assert len(assignments) == 2
|
||||
assignments = outcomes.get_assignments_for_problem(
|
||||
self.unit, self.user_id, self.course.id
|
||||
)
|
||||
assert len(assignments) == 1
|
||||
assert assignments[0].lis_result_sourcedid == 'graded_unit'
|
||||
assert assignments[1].lis_result_sourcedid == 'graded_vertical'
|
||||
|
||||
def test_with_unit_used_twice(self):
|
||||
self.create_graded_assignment(self.unit, 'graded_unit', self.outcome_service)
|
||||
self.create_graded_assignment(self.unit, 'graded_unit2', self.outcome_service)
|
||||
with check_mongo_calls(7):
|
||||
assignments = outcomes.get_assignments_for_problem(
|
||||
self.unit, self.user_id, self.course.id
|
||||
)
|
||||
assignments = outcomes.get_assignments_for_problem(
|
||||
self.unit, self.user_id, self.course.id
|
||||
)
|
||||
assert len(assignments) == 2
|
||||
assert assignments[0].lis_result_sourcedid == 'graded_unit'
|
||||
assert assignments[1].lis_result_sourcedid == 'graded_unit2'
|
||||
@@ -420,20 +413,18 @@ class TestAssignmentsForProblem(ModuleStoreTestCase):
|
||||
def test_with_unit_graded_for_different_user(self):
|
||||
self.create_graded_assignment(self.unit, 'graded_unit', self.outcome_service)
|
||||
other_user = UserFactory.create()
|
||||
with check_mongo_calls(7):
|
||||
assignments = outcomes.get_assignments_for_problem(
|
||||
self.unit, other_user.id, self.course.id
|
||||
)
|
||||
assignments = outcomes.get_assignments_for_problem(
|
||||
self.unit, other_user.id, self.course.id
|
||||
)
|
||||
assert len(assignments) == 0
|
||||
|
||||
def test_with_unit_graded_for_multiple_consumers(self):
|
||||
other_outcome_service = self.create_outcome_service('second_consumer')
|
||||
self.create_graded_assignment(self.unit, 'graded_unit', self.outcome_service)
|
||||
self.create_graded_assignment(self.unit, 'graded_unit2', other_outcome_service)
|
||||
with check_mongo_calls(7):
|
||||
assignments = outcomes.get_assignments_for_problem(
|
||||
self.unit, self.user_id, self.course.id
|
||||
)
|
||||
assignments = outcomes.get_assignments_for_problem(
|
||||
self.unit, self.user_id, self.course.id
|
||||
)
|
||||
assert len(assignments) == 2
|
||||
assert assignments[0].lis_result_sourcedid == 'graded_unit'
|
||||
assert assignments[1].lis_result_sourcedid == 'graded_unit2'
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"_id": {
|
||||
"tag": "i4x",
|
||||
"org": "org.0",
|
||||
"course": "course_0",
|
||||
"category": "course",
|
||||
"name": "Run_0",
|
||||
"revision": null
|
||||
},
|
||||
"definition": {
|
||||
"children": [],
|
||||
"data": {
|
||||
"wiki_slug": "course_0"
|
||||
}
|
||||
},
|
||||
"edit_info": {
|
||||
"edited_on": {
|
||||
"$date": "2023-03-29T14:01:07.038Z"
|
||||
},
|
||||
"edited_by": -3,
|
||||
"subtree_edited_on": {
|
||||
"$date": "2023-03-29T14:01:07.038Z"
|
||||
},
|
||||
"subtree_edited_by": -3
|
||||
},
|
||||
"metadata": {
|
||||
"display_name": "Run 0",
|
||||
"discussion_topics": {
|
||||
"General": {
|
||||
"id": "i4x-org_0-course_0-course-Run_0"
|
||||
}
|
||||
},
|
||||
"tabs": [
|
||||
{
|
||||
"type": "courseware",
|
||||
"name": "Course",
|
||||
"course_staff_only": false
|
||||
},
|
||||
{
|
||||
"type": "progress",
|
||||
"name": "Progress",
|
||||
"course_staff_only": false
|
||||
},
|
||||
{
|
||||
"type": "dates",
|
||||
"name": "Dates",
|
||||
"course_staff_only": false
|
||||
},
|
||||
{
|
||||
"type": "discussion",
|
||||
"name": "Discussion",
|
||||
"course_staff_only": false
|
||||
},
|
||||
{
|
||||
"type": "wiki",
|
||||
"name": "Wiki",
|
||||
"course_staff_only": false,
|
||||
"is_hidden": true
|
||||
},
|
||||
{
|
||||
"type": "textbooks",
|
||||
"name": "Textbooks",
|
||||
"course_staff_only": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
"""
|
||||
Tests for course_overviews app.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
from io import BytesIO
|
||||
from unittest import mock
|
||||
|
||||
@@ -249,7 +251,7 @@ class CourseOverviewTestCase(CatalogIntegrationMixin, ModuleStoreTestCase, Cache
|
||||
"catalog_visibility": CATALOG_VISIBILITY_NONE,
|
||||
}
|
||||
],
|
||||
[ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split]
|
||||
[ModuleStoreEnum.Type.split]
|
||||
))
|
||||
@ddt.unpack
|
||||
def test_course_overview_behavior(self, course_kwargs, modulestore_type):
|
||||
@@ -306,8 +308,7 @@ class CourseOverviewTestCase(CatalogIntegrationMixin, ModuleStoreTestCase, Cache
|
||||
course_overview = CourseOverviewFactory.create(language=course_language)
|
||||
assert course_overview.closest_released_language == expected_language
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.split, ModuleStoreEnum.Type.mongo)
|
||||
def test_get_non_existent_course(self, modulestore_type):
|
||||
def test_get_non_existent_course(self):
|
||||
"""
|
||||
Tests that requesting a non-existent course from get_from_id raises
|
||||
CourseOverview.DoesNotExist.
|
||||
@@ -316,39 +317,34 @@ class CourseOverviewTestCase(CatalogIntegrationMixin, ModuleStoreTestCase, Cache
|
||||
modulestore_type (ModuleStoreEnum.Type): type of store to create the
|
||||
course in.
|
||||
"""
|
||||
store = modulestore()._get_modulestore_by_type(modulestore_type) # pylint: disable=protected-access
|
||||
with pytest.raises(CourseOverview.DoesNotExist):
|
||||
CourseOverview.get_from_id(store.make_course_key('Non', 'Existent', 'Course'))
|
||||
CourseOverview.get_from_id(self.store.make_course_key('Non', 'Existent', 'Course'))
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.split, ModuleStoreEnum.Type.mongo)
|
||||
def test_course_with_course_overview_exists(self, modulestore_type):
|
||||
def test_course_with_course_overview_exists(self):
|
||||
"""
|
||||
Tests that calling course_exists on an existent course
|
||||
that is cached in CourseOverview table returns True.
|
||||
"""
|
||||
course = CourseFactory.create(default_store=modulestore_type)
|
||||
course = CourseFactory.create()
|
||||
CourseOverview.get_from_id(course.id) # Ensure course in cached in CourseOverviews
|
||||
assert CourseOverview.objects.filter(id=course.id).exists()
|
||||
assert CourseOverview.course_exists(course.id)
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.split, ModuleStoreEnum.Type.mongo)
|
||||
def test_course_without_overview_exists(self, modulestore_type):
|
||||
def test_course_without_overview_exists(self):
|
||||
"""
|
||||
Tests that calling course_exists on an existent course
|
||||
that is NOT cached in CourseOverview table returns True.
|
||||
"""
|
||||
course = CourseFactory.create(default_store=modulestore_type)
|
||||
course = CourseFactory.create()
|
||||
CourseOverview.objects.filter(id=course.id).delete()
|
||||
assert CourseOverview.course_exists(course.id)
|
||||
assert not CourseOverview.objects.filter(id=course.id).exists()
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.split, ModuleStoreEnum.Type.mongo)
|
||||
def test_nonexistent_course_does_not_exists(self, modulestore_type):
|
||||
def test_nonexistent_course_does_not_exists(self):
|
||||
"""
|
||||
Tests that calling course_exists on an non-existent course returns False.
|
||||
"""
|
||||
store = modulestore()._get_modulestore_by_type(modulestore_type) # pylint: disable=protected-access
|
||||
course_id = store.make_course_key('Non', 'Existent', 'Course')
|
||||
course_id = self.store.make_course_key('Non', 'Existent', 'Course')
|
||||
assert not CourseOverview.course_exists(course_id)
|
||||
|
||||
def test_get_errored_course(self):
|
||||
@@ -379,23 +375,20 @@ class CourseOverviewTestCase(CatalogIntegrationMixin, ModuleStoreTestCase, Cache
|
||||
course_overview = CourseOverview._create_or_update(course) # pylint: disable=protected-access
|
||||
assert course_overview.lowest_passing_grade is None
|
||||
|
||||
@ddt.data((ModuleStoreEnum.Type.mongo, 5, 5), (ModuleStoreEnum.Type.split, 2, 2))
|
||||
@ddt.unpack
|
||||
def test_versioning(self, modulestore_type, min_mongo_calls, max_mongo_calls):
|
||||
def test_versioning(self):
|
||||
"""
|
||||
Test that CourseOverviews with old version numbers are thrown out.
|
||||
"""
|
||||
with self.store.default_store(modulestore_type):
|
||||
course = CourseFactory.create()
|
||||
course_overview = CourseOverview.get_from_id(course.id)
|
||||
course_overview.version = CourseOverview.VERSION - 1
|
||||
course_overview.save()
|
||||
course = CourseFactory.create()
|
||||
course_overview = CourseOverview.get_from_id(course.id)
|
||||
course_overview.version = CourseOverview.VERSION - 1
|
||||
course_overview.save()
|
||||
|
||||
# Because the course overview now has an old version number, it should
|
||||
# be thrown out after being loaded from the cache, which results in
|
||||
# a call to get_course.
|
||||
with check_mongo_calls_range(max_finds=max_mongo_calls, min_finds=min_mongo_calls):
|
||||
_course_overview_2 = CourseOverview.get_from_id(course.id)
|
||||
# Because the course overview now has an old version number, it should
|
||||
# be thrown out after being loaded from the cache, which results in
|
||||
# a call to get_course.
|
||||
with check_mongo_calls_range(max_finds=2, min_finds=2):
|
||||
_course_overview_2 = CourseOverview.get_from_id(course.id)
|
||||
|
||||
# The CourseOverviewTab and CourseOverviewImageSet objects can't be filtered with course overview object as it is
|
||||
# created with `None` as 'id' - We are going to mock this to as this isn't being tested in this test case, instead
|
||||
@@ -611,6 +604,24 @@ class CourseOverviewTestCase(CatalogIntegrationMixin, ModuleStoreTestCase, Cache
|
||||
assert overviews_by_id[non_existent_course_key] is None
|
||||
assert mock_load_from_modulestore.call_count == 3
|
||||
|
||||
def test_mongo_course_overview_generation(self):
|
||||
"""
|
||||
Tests that course_overview can be generated for old Mongo course.
|
||||
"""
|
||||
store = modulestore()._get_modulestore_by_type(ModuleStoreEnum.Type.mongo) # lint-amnesty, pylint: disable=protected-access
|
||||
file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data/mongo_course.json')
|
||||
with open(file_path) as file:
|
||||
course_structure = json.load(file)
|
||||
|
||||
store.collection.insert_one(course_structure)
|
||||
|
||||
course_key = CourseKey.from_string('org.0/course_0/Run_0')
|
||||
assert CourseOverview.course_exists(course_key)
|
||||
assert not CourseOverview.objects.filter(id=course_key).exists()
|
||||
|
||||
CourseOverview.load_from_module_store(course_key)
|
||||
assert CourseOverview.objects.filter(id=course_key).exists()
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class CourseOverviewImageSetTestCase(ModuleStoreTestCase):
|
||||
@@ -665,28 +676,21 @@ class CourseOverviewImageSetTestCase(ModuleStoreTestCase):
|
||||
|
||||
@override_settings(DEFAULT_COURSE_ABOUT_IMAGE_URL='default_course.png')
|
||||
@override_settings(STATIC_URL='static/')
|
||||
@ddt.data(
|
||||
*itertools.product(
|
||||
[ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split],
|
||||
[None, '']
|
||||
)
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_no_source_image(self, modulestore_type, course_image):
|
||||
@ddt.data(None, '')
|
||||
def test_no_source_image(self, course_image):
|
||||
"""
|
||||
Tests that we behave as expected if no source image was specified.
|
||||
"""
|
||||
# Because we're sending None and '', we expect to get the generic
|
||||
# fallback URL for course images.
|
||||
fallback_url = settings.STATIC_URL + settings.DEFAULT_COURSE_ABOUT_IMAGE_URL
|
||||
course_overview = self._assert_image_urls_all_default(modulestore_type, course_image, fallback_url)
|
||||
course_overview = self._assert_image_urls_all_default(ModuleStoreEnum.Type.split, course_image, fallback_url)
|
||||
|
||||
# Even though there was no source image to generate, we should still
|
||||
# have a CourseOverviewImageSet object associated with this overview.
|
||||
assert hasattr(course_overview, 'image_set')
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
def test_disabled_no_prior_data(self, modulestore_type):
|
||||
def test_disabled_no_prior_data(self):
|
||||
"""
|
||||
Test behavior when we are disabled and no entries exist.
|
||||
|
||||
@@ -699,13 +703,12 @@ class CourseOverviewImageSetTestCase(ModuleStoreTestCase):
|
||||
# Since we're disabled, we should just return the raw source image back
|
||||
# for every resolution in image_urls.
|
||||
fake_course_image = 'sample_image.png'
|
||||
course_overview = self._assert_image_urls_all_default(modulestore_type, fake_course_image)
|
||||
course_overview = self._assert_image_urls_all_default(ModuleStoreEnum.Type.split, fake_course_image)
|
||||
|
||||
# Because we are disabled, no image set should have been generated.
|
||||
assert not hasattr(course_overview, 'image_set')
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
def test_disabled_with_prior_data(self, modulestore_type):
|
||||
def test_disabled_with_prior_data(self):
|
||||
"""
|
||||
Test behavior when entries have been created but we are disabled.
|
||||
|
||||
@@ -721,11 +724,8 @@ class CourseOverviewImageSetTestCase(ModuleStoreTestCase):
|
||||
course_image = "my_course.jpg"
|
||||
broken_small_url = "I am small!"
|
||||
broken_large_url = "I am big!"
|
||||
with self.store.default_store(modulestore_type):
|
||||
course = CourseFactory.create(
|
||||
default_store=modulestore_type, course_image=course_image
|
||||
)
|
||||
course_overview_before = self.get_from_id(course.id)
|
||||
course = CourseFactory.create(course_image=course_image)
|
||||
course_overview_before = self.get_from_id(course.id)
|
||||
|
||||
# This initial seeding should create an entry for the image_set.
|
||||
assert hasattr(course_overview_before, 'image_set')
|
||||
@@ -753,81 +753,71 @@ class CourseOverviewImageSetTestCase(ModuleStoreTestCase):
|
||||
expected_url = course_image_url(course)
|
||||
assert course_overview_after.image_urls == {'raw': expected_url, 'small': expected_url, 'large': expected_url}
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
def test_cdn(self, modulestore_type):
|
||||
def test_cdn(self):
|
||||
"""
|
||||
Test that we return CDN prefixed URLs if it is enabled.
|
||||
"""
|
||||
with self.store.default_store(modulestore_type):
|
||||
course = CourseFactory.create(default_store=modulestore_type)
|
||||
overview = self.get_from_id(course.id)
|
||||
course = CourseFactory.create()
|
||||
overview = self.get_from_id(course.id)
|
||||
|
||||
# First the behavior when there's no CDN enabled...
|
||||
AssetBaseUrlConfig.objects.all().delete()
|
||||
if modulestore_type == ModuleStoreEnum.Type.mongo:
|
||||
expected_path_start = "/c4x/"
|
||||
elif modulestore_type == ModuleStoreEnum.Type.split:
|
||||
expected_path_start = "/asset-v1:"
|
||||
# First the behavior when there's no CDN enabled...
|
||||
AssetBaseUrlConfig.objects.all().delete()
|
||||
expected_path_start = "/asset-v1:"
|
||||
|
||||
for url in overview.image_urls.values():
|
||||
assert url.startswith(expected_path_start)
|
||||
for url in overview.image_urls.values():
|
||||
assert url.startswith(expected_path_start)
|
||||
|
||||
# Now enable the CDN...
|
||||
AssetBaseUrlConfig.objects.create(enabled=True, base_url='fakecdn.edx.org')
|
||||
expected_cdn_url = "//fakecdn.edx.org" + expected_path_start
|
||||
# Now enable the CDN...
|
||||
AssetBaseUrlConfig.objects.create(enabled=True, base_url='fakecdn.edx.org')
|
||||
expected_cdn_url = "//fakecdn.edx.org" + expected_path_start
|
||||
|
||||
for url in overview.image_urls.values():
|
||||
assert url.startswith(expected_cdn_url)
|
||||
for url in overview.image_urls.values():
|
||||
assert url.startswith(expected_cdn_url)
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
def test_cdn_with_external_image(self, modulestore_type):
|
||||
def test_cdn_with_external_image(self):
|
||||
"""
|
||||
Test that we return CDN prefixed URLs unless they're absolute.
|
||||
"""
|
||||
with self.store.default_store(modulestore_type):
|
||||
course = CourseFactory.create(default_store=modulestore_type)
|
||||
overview = self.get_from_id(course.id)
|
||||
course = CourseFactory.create()
|
||||
overview = self.get_from_id(course.id)
|
||||
|
||||
# Now enable the CDN...
|
||||
AssetBaseUrlConfig.objects.create(enabled=True, base_url='fakecdn.edx.org')
|
||||
expected_cdn_url = "//fakecdn.edx.org"
|
||||
# Now enable the CDN...
|
||||
AssetBaseUrlConfig.objects.create(enabled=True, base_url='fakecdn.edx.org')
|
||||
expected_cdn_url = "//fakecdn.edx.org"
|
||||
|
||||
start_urls = {
|
||||
'raw': 'http://google.com/image.png',
|
||||
'small': '/static/overview.png',
|
||||
'large': ''
|
||||
}
|
||||
start_urls = {
|
||||
'raw': 'http://google.com/image.png',
|
||||
'small': '/static/overview.png',
|
||||
'large': ''
|
||||
}
|
||||
|
||||
modified_urls = overview.apply_cdn_to_urls(start_urls)
|
||||
assert modified_urls['raw'] == start_urls['raw']
|
||||
assert modified_urls['small'] != start_urls['small']
|
||||
assert modified_urls['small'].startswith(expected_cdn_url)
|
||||
assert modified_urls['large'] == start_urls['large']
|
||||
modified_urls = overview.apply_cdn_to_urls(start_urls)
|
||||
assert modified_urls['raw'] == start_urls['raw']
|
||||
assert modified_urls['small'] != start_urls['small']
|
||||
assert modified_urls['small'].startswith(expected_cdn_url)
|
||||
assert modified_urls['large'] == start_urls['large']
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
def test_cdn_with_a_single_external_image(self, modulestore_type):
|
||||
def test_cdn_with_a_single_external_image(self):
|
||||
"""
|
||||
Test CDN is applied for a URL when apply_cdn_to_url called directly.
|
||||
|
||||
Apply CDN/base URL to the given URL if CDN configuration is enabled
|
||||
and the URL is not absolute.
|
||||
"""
|
||||
with self.store.default_store(modulestore_type):
|
||||
course = CourseFactory.create(default_store=modulestore_type)
|
||||
overview = self.get_from_id(course.id)
|
||||
course = CourseFactory.create()
|
||||
overview = self.get_from_id(course.id)
|
||||
|
||||
# Now enable the CDN...
|
||||
AssetBaseUrlConfig.objects.create(enabled=True, base_url='fakecdn.edx.org')
|
||||
expected_cdn_url = "//fakecdn.edx.org"
|
||||
# Now enable the CDN...
|
||||
AssetBaseUrlConfig.objects.create(enabled=True, base_url='fakecdn.edx.org')
|
||||
expected_cdn_url = "//fakecdn.edx.org"
|
||||
|
||||
start_url = "/static/overview.png"
|
||||
modified_url = overview.apply_cdn_to_url(start_url)
|
||||
start_url = "/static/overview.png"
|
||||
modified_url = overview.apply_cdn_to_url(start_url)
|
||||
|
||||
assert start_url != modified_url
|
||||
assert modified_url.startswith(expected_cdn_url)
|
||||
assert start_url != modified_url
|
||||
assert modified_url.startswith(expected_cdn_url)
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
def test_error_generating_thumbnails(self, modulestore_type):
|
||||
def test_error_generating_thumbnails(self):
|
||||
"""
|
||||
Test a scenario where thumbnails cannot be generated.
|
||||
|
||||
@@ -847,7 +837,7 @@ class CourseOverviewImageSetTestCase(ModuleStoreTestCase):
|
||||
|
||||
# This will generate a CourseOverview and verify that we get the
|
||||
# source image back for all resolutions.
|
||||
course_overview = self._assert_image_urls_all_default(modulestore_type, fake_course_image)
|
||||
course_overview = self._assert_image_urls_all_default(ModuleStoreEnum.Type.split, fake_course_image)
|
||||
|
||||
# Make sure we were called (i.e. we tried to create the thumbnail)
|
||||
patched_create_thumbnail.assert_called()
|
||||
@@ -864,14 +854,8 @@ class CourseOverviewImageSetTestCase(ModuleStoreTestCase):
|
||||
self.get_from_id(course_overview.id)
|
||||
patched_create_thumbnail.assert_not_called()
|
||||
|
||||
@ddt.data(
|
||||
*itertools.product(
|
||||
[ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split],
|
||||
[True, False],
|
||||
)
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_happy_path(self, modulestore_type, create_after_overview):
|
||||
@ddt.data(True, False)
|
||||
def test_happy_path(self, create_after_overview):
|
||||
"""
|
||||
What happens when everything works like we expect it to.
|
||||
|
||||
@@ -888,48 +872,45 @@ class CourseOverviewImageSetTestCase(ModuleStoreTestCase):
|
||||
image_buff.seek(0)
|
||||
image_name = "big_course_image.jpeg"
|
||||
|
||||
with self.store.default_store(modulestore_type):
|
||||
course = CourseFactory.create(
|
||||
default_store=modulestore_type, course_image=image_name
|
||||
)
|
||||
course = CourseFactory.create(course_image=image_name)
|
||||
|
||||
# Save a real image here...
|
||||
course_image_asset_key = StaticContent.compute_location(course.id, course.course_image)
|
||||
course_image_content = StaticContent(course_image_asset_key, image_name, 'image/jpeg', image_buff)
|
||||
contentstore().save(course_image_content)
|
||||
# Save a real image here...
|
||||
course_image_asset_key = StaticContent.compute_location(course.id, course.course_image)
|
||||
course_image_content = StaticContent(course_image_asset_key, image_name, 'image/jpeg', image_buff)
|
||||
contentstore().save(course_image_content)
|
||||
|
||||
# If create_after_overview is True, disable thumbnail generation so
|
||||
# that the CourseOverview object is created and saved without an
|
||||
# image_set at first (it will be lazily created later).
|
||||
if create_after_overview:
|
||||
self.set_config(enabled=False)
|
||||
# If create_after_overview is True, disable thumbnail generation so
|
||||
# that the CourseOverview object is created and saved without an
|
||||
# image_set at first (it will be lazily created later).
|
||||
if create_after_overview:
|
||||
self.set_config(enabled=False)
|
||||
|
||||
# Now generate the CourseOverview...
|
||||
# Now generate the CourseOverview...
|
||||
course_overview = self.get_from_id(course.id)
|
||||
|
||||
# If create_after_overview is True, no image_set exists yet. Verify
|
||||
# that, then switch config back over to True and it should lazily
|
||||
# create the image_set on the next get_from_id() call.
|
||||
if create_after_overview:
|
||||
assert not hasattr(course_overview, 'image_set')
|
||||
self.set_config(enabled=True)
|
||||
course_overview = self.get_from_id(course.id)
|
||||
|
||||
# If create_after_overview is True, no image_set exists yet. Verify
|
||||
# that, then switch config back over to True and it should lazily
|
||||
# create the image_set on the next get_from_id() call.
|
||||
if create_after_overview:
|
||||
assert not hasattr(course_overview, 'image_set')
|
||||
self.set_config(enabled=True)
|
||||
course_overview = self.get_from_id(course.id)
|
||||
assert hasattr(course_overview, 'image_set')
|
||||
image_urls = course_overview.image_urls
|
||||
config = CourseOverviewImageConfig.current()
|
||||
|
||||
assert hasattr(course_overview, 'image_set')
|
||||
image_urls = course_overview.image_urls
|
||||
config = CourseOverviewImageConfig.current()
|
||||
# Make sure the thumbnail names come out as expected...
|
||||
assert image_urls['raw'].endswith('big_course_image.jpeg')
|
||||
assert image_urls['small'].endswith('big_course_image-jpeg-{}x{}.jpg'.format(*config.small))
|
||||
assert image_urls['large'].endswith('big_course_image-jpeg-{}x{}.jpg'.format(*config.large))
|
||||
|
||||
# Make sure the thumbnail names come out as expected...
|
||||
assert image_urls['raw'].endswith('big_course_image.jpeg')
|
||||
assert image_urls['small'].endswith('big_course_image-jpeg-{}x{}.jpg'.format(*config.small))
|
||||
assert image_urls['large'].endswith('big_course_image-jpeg-{}x{}.jpg'.format(*config.large))
|
||||
|
||||
# Now make sure our thumbnails are of the sizes we expect...
|
||||
for image_url, expected_size in [(image_urls['small'], config.small), (image_urls['large'], config.large)]:
|
||||
image_key = StaticContent.get_location_from_path(image_url)
|
||||
image_content = AssetManager.find(image_key)
|
||||
image = Image.open(BytesIO(image_content.data))
|
||||
assert image.size == expected_size
|
||||
# Now make sure our thumbnails are of the sizes we expect...
|
||||
for image_url, expected_size in [(image_urls['small'], config.small), (image_urls['large'], config.large)]:
|
||||
image_key = StaticContent.get_location_from_path(image_url)
|
||||
image_content = AssetManager.find(image_key)
|
||||
image = Image.open(BytesIO(image_content.data))
|
||||
assert image.size == expected_size
|
||||
|
||||
@ddt.data(
|
||||
(800, 400), # Larger than both, correct ratio
|
||||
@@ -1111,13 +1092,12 @@ class CourseOverviewTabTestCase(ModuleStoreTestCase):
|
||||
|
||||
ENABLED_SIGNALS = ['course_published']
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
def test_tabs_deletion_rollback_on_integrity_error(self, modulestore_type):
|
||||
def test_tabs_deletion_rollback_on_integrity_error(self):
|
||||
"""
|
||||
Tests that course_overview tabs deletion is correctly rolled back if an Exception
|
||||
occurs while updating the course_overview.
|
||||
"""
|
||||
course = CourseFactory.create(default_store=modulestore_type)
|
||||
course = CourseFactory.create()
|
||||
course_overview = CourseOverview.get_from_id(course.id)
|
||||
expected_tabs = {tab.tab_id for tab in course_overview.tab_set.all()}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
from unittest import mock
|
||||
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
@@ -12,9 +11,9 @@ from ..tasks import enqueue_async_course_overview_update_tasks
|
||||
class BatchedAsyncCourseOverviewUpdateTests(ModuleStoreTestCase): # lint-amnesty, pylint: disable=missing-class-docstring
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.course_1 = CourseFactory.create(default_store=ModuleStoreEnum.Type.mongo)
|
||||
self.course_2 = CourseFactory.create(default_store=ModuleStoreEnum.Type.mongo)
|
||||
self.course_3 = CourseFactory.create(default_store=ModuleStoreEnum.Type.mongo)
|
||||
self.course_1 = CourseFactory.create()
|
||||
self.course_2 = CourseFactory.create()
|
||||
self.course_3 = CourseFactory.create()
|
||||
|
||||
@mock.patch('openedx.core.djangoapps.content.course_overviews.models.CourseOverview.update_select_courses')
|
||||
def test_enqueue_all_courses_in_single_batch(self, mock_update_courses):
|
||||
|
||||
@@ -1271,7 +1271,7 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite):
|
||||
result = defaultdict(dict)
|
||||
if fields is None:
|
||||
return result
|
||||
classes = XBlock.load_class(category)
|
||||
classes = XBlock.load_class(category, default=self.default_class)
|
||||
cls = self.mixologist.mix(classes)
|
||||
for field_name, value in fields.items():
|
||||
field = getattr(cls, field_name)
|
||||
|
||||
@@ -13,7 +13,6 @@ structure:
|
||||
"""
|
||||
|
||||
|
||||
import copy
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
@@ -29,7 +28,6 @@ from opaque_keys.edx.keys import CourseKey, UsageKey
|
||||
from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator, LibraryLocator
|
||||
from path import Path as path
|
||||
from pytz import UTC
|
||||
from xblock.core import XBlock
|
||||
from xblock.exceptions import InvalidScopeError
|
||||
from xblock.fields import Reference, ReferenceList, ReferenceValueDict, Scope, ScopeIds
|
||||
from xblock.runtime import KvsFieldData
|
||||
@@ -43,9 +41,8 @@ from xmodule.mako_block import MakoDescriptorSystem
|
||||
from xmodule.modulestore import BulkOperationsMixin, ModuleStoreEnum, ModuleStoreWriteBase
|
||||
from xmodule.modulestore.draft_and_published import DIRECT_ONLY_CATEGORIES, ModuleStoreDraftAndPublished
|
||||
from xmodule.modulestore.edit_info import EditInfoRuntimeMixin
|
||||
from xmodule.modulestore.exceptions import DuplicateCourseError, ItemNotFoundError, ReferentialIntegrityError
|
||||
from xmodule.modulestore.inheritance import InheritanceKeyValueStore, InheritanceMixin, inherit_metadata
|
||||
from xmodule.modulestore.store_utilities import DETACHED_XBLOCK_TYPES
|
||||
from xmodule.modulestore.exceptions import DuplicateCourseError, ItemNotFoundError
|
||||
from xmodule.modulestore.inheritance import InheritanceKeyValueStore
|
||||
from xmodule.modulestore.xml import CourseLocationManager
|
||||
from xmodule.mongo_utils import connect_to_mongodb, create_collection_index
|
||||
from xmodule.partitions.partitions_service import PartitionService
|
||||
@@ -59,10 +56,6 @@ SORT_REVISION_FAVOR_DRAFT = ('_id.revision', pymongo.DESCENDING)
|
||||
# sort order that returns PUBLISHED items first
|
||||
SORT_REVISION_FAVOR_PUBLISHED = ('_id.revision', pymongo.ASCENDING)
|
||||
|
||||
BLOCK_TYPES_WITH_CHILDREN = list({
|
||||
name for name, class_ in XBlock.load_classes() if getattr(class_, 'has_children', False)
|
||||
})
|
||||
|
||||
# Allow us to call _from_deprecated_(son|string) throughout the file
|
||||
# pylint: disable=protected-access
|
||||
|
||||
@@ -92,21 +85,17 @@ class MongoKeyValueStore(InheritanceKeyValueStore):
|
||||
A KeyValueStore that maps keyed data access to one of the 3 data areas
|
||||
known to the MongoModuleStore (data, children, and metadata)
|
||||
"""
|
||||
def __init__(self, data, parent, children, metadata):
|
||||
def __init__(self, data, metadata):
|
||||
super().__init__()
|
||||
if not isinstance(data, dict):
|
||||
self._data = {'data': data}
|
||||
else:
|
||||
self._data = data
|
||||
self._parent = parent
|
||||
self._children = children
|
||||
self._metadata = metadata
|
||||
|
||||
def get(self, key):
|
||||
if key.scope == Scope.children:
|
||||
return self._children
|
||||
elif key.scope == Scope.parent:
|
||||
return self._parent
|
||||
return []
|
||||
elif key.scope == Scope.settings:
|
||||
return self._metadata[key.field_name]
|
||||
elif key.scope == Scope.content:
|
||||
@@ -114,28 +103,22 @@ class MongoKeyValueStore(InheritanceKeyValueStore):
|
||||
else:
|
||||
raise InvalidScopeError(
|
||||
key,
|
||||
(Scope.children, Scope.parent, Scope.settings, Scope.content),
|
||||
(Scope.settings, Scope.content),
|
||||
)
|
||||
|
||||
def set(self, key, value):
|
||||
if key.scope == Scope.children:
|
||||
self._children = value
|
||||
elif key.scope == Scope.parent:
|
||||
self._parent = value
|
||||
elif key.scope == Scope.settings:
|
||||
if key.scope == Scope.settings:
|
||||
self._metadata[key.field_name] = value
|
||||
elif key.scope == Scope.content:
|
||||
self._data[key.field_name] = value
|
||||
else:
|
||||
raise InvalidScopeError(
|
||||
key,
|
||||
(Scope.children, Scope.settings, Scope.content),
|
||||
(Scope.settings, Scope.content),
|
||||
)
|
||||
|
||||
def delete(self, key):
|
||||
if key.scope == Scope.children:
|
||||
self._children = []
|
||||
elif key.scope == Scope.settings:
|
||||
if key.scope == Scope.settings:
|
||||
if key.field_name in self._metadata:
|
||||
del self._metadata[key.field_name]
|
||||
elif key.scope == Scope.content:
|
||||
@@ -144,13 +127,11 @@ class MongoKeyValueStore(InheritanceKeyValueStore):
|
||||
else:
|
||||
raise InvalidScopeError(
|
||||
key,
|
||||
(Scope.children, Scope.settings, Scope.content),
|
||||
(Scope.settings, Scope.content),
|
||||
)
|
||||
|
||||
def has(self, key):
|
||||
if key.scope in (Scope.children, Scope.parent):
|
||||
return True
|
||||
elif key.scope == Scope.settings:
|
||||
if key.scope == Scope.settings:
|
||||
return key.field_name in self._metadata
|
||||
elif key.scope == Scope.content:
|
||||
return key.field_name in self._data
|
||||
@@ -159,7 +140,7 @@ class MongoKeyValueStore(InheritanceKeyValueStore):
|
||||
|
||||
def __repr__(self):
|
||||
return "MongoKeyValueStore{!r}<{!r}, {!r}>".format(
|
||||
(self._data, self._parent, self._children, self._metadata),
|
||||
(self._data, self._metadata),
|
||||
self._fields,
|
||||
self.inherited_settings
|
||||
)
|
||||
@@ -244,20 +225,6 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin): # li
|
||||
definition = json_data.get('definition', {})
|
||||
metadata = json_data.get('metadata', {})
|
||||
|
||||
children = [
|
||||
self._convert_reference_to_key(childloc)
|
||||
for childloc in definition.get('children', [])
|
||||
]
|
||||
|
||||
parent = None
|
||||
if category not in DETACHED_XBLOCK_TYPES.union(['course']):
|
||||
# try looking it up just-in-time (but not if we're working with a detached block).
|
||||
parent = self.modulestore.get_parent_location(
|
||||
as_published(location),
|
||||
ModuleStoreEnum.RevisionOption.published_only if location.branch is None
|
||||
else ModuleStoreEnum.RevisionOption.draft_preferred
|
||||
)
|
||||
|
||||
data = definition.get('data', {})
|
||||
if isinstance(data, str):
|
||||
data = {'data': data}
|
||||
@@ -268,8 +235,6 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin): # li
|
||||
metadata = self._convert_reference_fields_to_keys(mixed_class, location.course_key, metadata)
|
||||
kvs = MongoKeyValueStore(
|
||||
data,
|
||||
parent,
|
||||
children,
|
||||
metadata,
|
||||
)
|
||||
|
||||
@@ -277,10 +242,6 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin): # li
|
||||
scope_ids = ScopeIds(None, category, location, location)
|
||||
block = self.construct_xblock_from_class(class_, scope_ids, field_data, for_parent=for_parent)
|
||||
|
||||
non_draft_loc = as_published(location)
|
||||
metadata_inheritance_tree = self.modulestore._compute_metadata_inheritance_tree(location.course_key)
|
||||
inherit_metadata(block, metadata_inheritance_tree.get(str(non_draft_loc), {}))
|
||||
|
||||
block._edit_info = json_data.get('edit_info')
|
||||
|
||||
# migrate published_by and published_on if edit_info isn't present
|
||||
@@ -641,90 +602,6 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
|
||||
else:
|
||||
return ParentLocationCache()
|
||||
|
||||
def _compute_metadata_inheritance_tree(self, course_id):
|
||||
'''
|
||||
Find all inheritable fields from all xblocks in the course which may define inheritable data
|
||||
'''
|
||||
# get all collections in the course, this query should not return any leaf nodes
|
||||
course_id = self.fill_in_run(course_id)
|
||||
query = SON([
|
||||
('_id.tag', 'i4x'),
|
||||
('_id.org', course_id.org),
|
||||
('_id.course', course_id.course),
|
||||
('_id.category', {'$in': BLOCK_TYPES_WITH_CHILDREN})
|
||||
])
|
||||
# if we're only dealing in the published branch, then only get published containers
|
||||
if self.get_branch_setting() == ModuleStoreEnum.Branch.published_only:
|
||||
query['_id.revision'] = None
|
||||
# we just want the Location, children, and inheritable metadata
|
||||
record_filter = {'_id': 1, 'definition.children': 1}
|
||||
|
||||
# just get the inheritable metadata since that is all we need for the computation
|
||||
# this minimizes both data pushed over the wire
|
||||
for field_name in InheritanceMixin.fields:
|
||||
record_filter[f'metadata.{field_name}'] = 1
|
||||
|
||||
# call out to the DB
|
||||
resultset = self.collection.find(query, record_filter)
|
||||
|
||||
# it's ok to keep these as deprecated strings b/c the overall cache is indexed by course_key and this
|
||||
# is a dictionary relative to that course
|
||||
results_by_url = {}
|
||||
root = None
|
||||
|
||||
# now go through the results and order them by the location url
|
||||
for result in resultset:
|
||||
# manually pick it apart b/c the db has tag and we want as_published revision regardless
|
||||
location = as_published(BlockUsageLocator._from_deprecated_son(result['_id'], course_id.run))
|
||||
|
||||
location_url = str(location)
|
||||
if location_url in results_by_url:
|
||||
# found either draft or live to complement the other revision
|
||||
# FIXME this is wrong. If the child was moved in draft from one parent to the other, it will
|
||||
# show up under both in this logic: https://openedx.atlassian.net/browse/TNL-1075
|
||||
existing_children = results_by_url[location_url].get('definition', {}).get('children', [])
|
||||
additional_children = result.get('definition', {}).get('children', [])
|
||||
total_children = existing_children + additional_children
|
||||
# use set to get rid of duplicates. We don't care about order; so, it shouldn't matter.
|
||||
results_by_url[location_url].setdefault('definition', {})['children'] = set(total_children)
|
||||
else:
|
||||
results_by_url[location_url] = result
|
||||
if location.block_type == 'course': # pylint: disable=no-member
|
||||
root = location_url
|
||||
|
||||
# now traverse the tree and compute down the inherited metadata
|
||||
metadata_to_inherit = {}
|
||||
|
||||
def _compute_inherited_metadata(url):
|
||||
"""
|
||||
Helper method for computing inherited metadata for a specific location url
|
||||
"""
|
||||
my_metadata = results_by_url[url].get('metadata', {})
|
||||
|
||||
# go through all the children and recurse, but only if we have
|
||||
# in the result set. Remember results will not contain leaf nodes
|
||||
for child in results_by_url[url].get('definition', {}).get('children', []):
|
||||
if child in results_by_url:
|
||||
new_child_metadata = copy.deepcopy(my_metadata)
|
||||
new_child_metadata.update(results_by_url[child].get('metadata', {}))
|
||||
results_by_url[child]['metadata'] = new_child_metadata
|
||||
metadata_to_inherit[child] = new_child_metadata
|
||||
_compute_inherited_metadata(child)
|
||||
else:
|
||||
# this is likely a leaf node, so let's record what metadata we need to inherit
|
||||
metadata_to_inherit[child] = my_metadata.copy()
|
||||
# WARNING: 'parent' is not part of inherited metadata, but
|
||||
# we're piggybacking on this recursive traversal to grab
|
||||
# and cache the child's parent, as a performance optimization.
|
||||
# The 'parent' key will be popped out of the dictionary during
|
||||
# CachingDescriptorSystem.load_item
|
||||
metadata_to_inherit[child].setdefault('parent', {})[self.get_branch_setting()] = url
|
||||
|
||||
if root is not None:
|
||||
_compute_inherited_metadata(root)
|
||||
|
||||
return metadata_to_inherit
|
||||
|
||||
def _clean_item_data(self, item):
|
||||
"""
|
||||
Renames the '_id' field in item to 'location'
|
||||
@@ -732,20 +609,7 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
|
||||
item['location'] = item['_id']
|
||||
del item['_id']
|
||||
|
||||
@autoretry_read()
|
||||
def _query_children_for_cache_children(self, course_key, items):
|
||||
"""
|
||||
Generate a pymongo in query for finding the items and return the payloads
|
||||
"""
|
||||
# first get non-draft in a round-trip
|
||||
query = {
|
||||
'_id': {'$in': [
|
||||
UsageKey.from_string(item).map_into_course(course_key).to_deprecated_son() for item in items
|
||||
]}
|
||||
}
|
||||
return list(self.collection.find(query))
|
||||
|
||||
def _cache_children(self, course_key, items, depth=0):
|
||||
def _get_items_data(self, course_key, items):
|
||||
"""
|
||||
Returns a dictionary mapping Location -> item data, populated with json data
|
||||
for all descendents of items up to the specified depth.
|
||||
@@ -757,32 +621,11 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
|
||||
data = {}
|
||||
to_process = list(items)
|
||||
course_key = self.fill_in_run(course_key)
|
||||
parent_cache = self._get_parent_cache(self.get_branch_setting())
|
||||
|
||||
while to_process and (depth is None or depth >= 0):
|
||||
children = []
|
||||
for item in to_process:
|
||||
self._clean_item_data(item)
|
||||
item_location = BlockUsageLocator._from_deprecated_son(item['location'], course_key.run)
|
||||
item_children = item.get('definition', {}).get('children', [])
|
||||
children.extend(item_children)
|
||||
for item_child in item_children:
|
||||
parent_cache.set(item_child, item_location)
|
||||
data[item_location] = item
|
||||
|
||||
if depth == 0:
|
||||
break
|
||||
|
||||
# Load all children by id. See
|
||||
# http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-%24or
|
||||
# for or-query syntax
|
||||
to_process = []
|
||||
if children:
|
||||
to_process = self._query_children_for_cache_children(course_key, children)
|
||||
|
||||
# If depth is None, then we just recurse until we hit all the descendents
|
||||
if depth is not None:
|
||||
depth -= 1
|
||||
for item in to_process:
|
||||
self._clean_item_data(item)
|
||||
item_location = BlockUsageLocator._from_deprecated_son(item['location'], course_key.run)
|
||||
data[item_location] = item
|
||||
|
||||
return data
|
||||
|
||||
@@ -851,13 +694,13 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
|
||||
item.course_version = None
|
||||
return item
|
||||
|
||||
def _load_items(self, course_key, items, depth=0, using_descriptor_system=None, for_parent=None):
|
||||
def _load_items(self, course_key, items, using_descriptor_system=None, for_parent=None):
|
||||
"""
|
||||
Load a list of xblocks from the data in items, with children cached up
|
||||
to specified depth
|
||||
"""
|
||||
course_key = self.fill_in_run(course_key)
|
||||
data_cache = self._cache_children(course_key, items, depth)
|
||||
data_cache = self._get_items_data(course_key, items)
|
||||
|
||||
# if we are loading a course object, if we're not prefetching children (depth != 0) then don't
|
||||
# bother with the metadata inheritance
|
||||
@@ -985,7 +828,7 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
|
||||
"""
|
||||
return BlockUsageLocator(course_key, 'course', course_key.run)
|
||||
|
||||
def get_course(self, course_key, depth=0, **kwargs): # lint-amnesty, pylint: disable=arguments-differ
|
||||
def get_course(self, course_key, **kwargs): # lint-amnesty, pylint: disable=arguments-differ
|
||||
"""
|
||||
Get the course with the given courseid (org/course/run)
|
||||
"""
|
||||
@@ -998,7 +841,7 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
|
||||
course_key = self.fill_in_run(course_key)
|
||||
location = course_key.make_usage_key('course', course_key.run)
|
||||
try:
|
||||
return self.get_item(location, depth=depth)
|
||||
return self.get_item(location)
|
||||
except ItemNotFoundError:
|
||||
return None
|
||||
|
||||
@@ -1047,7 +890,7 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
|
||||
except ItemNotFoundError:
|
||||
return False
|
||||
|
||||
def get_item(self, usage_key, depth=0, using_descriptor_system=None, for_parent=None, **kwargs): # lint-amnesty, pylint: disable=arguments-differ
|
||||
def get_item(self, usage_key, using_descriptor_system=None, for_parent=None, **kwargs): # lint-amnesty, pylint: disable=arguments-differ
|
||||
"""
|
||||
Returns an XModuleDescriptor instance for the item at location.
|
||||
|
||||
@@ -1069,7 +912,6 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
|
||||
block = self._load_items(
|
||||
usage_key.course_key,
|
||||
[item],
|
||||
depth,
|
||||
using_descriptor_system=using_descriptor_system,
|
||||
for_parent=for_parent,
|
||||
)[0]
|
||||
@@ -1153,8 +995,6 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
|
||||
query['metadata.' + key] = value
|
||||
for key, value in (content or {}).items():
|
||||
query['definition.data.' + key] = value
|
||||
if 'children' in qualifiers:
|
||||
query['definition.children'] = qualifiers.pop('children')
|
||||
|
||||
query.update(qualifiers)
|
||||
items = self.collection.find(
|
||||
@@ -1319,25 +1159,7 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
|
||||
block_id: a unique identifier for the new item. If not supplied,
|
||||
a new identifier will be generated
|
||||
"""
|
||||
# attach to parent if given
|
||||
parent = None
|
||||
|
||||
if parent_usage_key is not None:
|
||||
parent = self.get_item(parent_usage_key)
|
||||
kwargs.setdefault('for_parent', parent)
|
||||
|
||||
xblock = self.create_item(user_id, parent_usage_key.course_key, block_type, block_id=block_id, **kwargs)
|
||||
|
||||
if parent is not None and 'detached' not in xblock._class_tags:
|
||||
# Originally added to support entrance exams (settings.FEATURES.get('ENTRANCE_EXAMS'))
|
||||
if kwargs.get('position') is None:
|
||||
parent.children.append(xblock.location)
|
||||
else:
|
||||
parent.children.insert(kwargs.get('position'), xblock.location)
|
||||
|
||||
self.update_item(parent, user_id, child_update=True) # lint-amnesty, pylint: disable=unexpected-keyword-arg
|
||||
|
||||
return xblock
|
||||
raise NotImplementedError
|
||||
|
||||
def import_xblock(self, user_id, course_key, block_type, block_id, fields=None, runtime=None, **kwargs):
|
||||
"""
|
||||
@@ -1348,13 +1170,13 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
|
||||
xblock = self.create_xblock(runtime, course_key, block_type, block_id, fields)
|
||||
return self.update_item(xblock, user_id, allow_not_found=True)
|
||||
|
||||
def _get_course_for_item(self, location, depth=0):
|
||||
def _get_course_for_item(self, location):
|
||||
'''
|
||||
for a given XBlock, return the course that it belongs to
|
||||
Also we have to assert that this block maps to only one course item - it'll throw an
|
||||
assert if not
|
||||
'''
|
||||
return self.get_course(location.course_key, depth)
|
||||
return self.get_course(location.course_key)
|
||||
|
||||
def _update_single_item(self, location, update, allow_not_found=False):
|
||||
"""
|
||||
@@ -1373,84 +1195,6 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
|
||||
if result.matched_count == 0 and result.upserted_id is None:
|
||||
raise ItemNotFoundError(location)
|
||||
|
||||
def _update_ancestors(self, location, update):
|
||||
"""
|
||||
Recursively applies update to all the ancestors of location
|
||||
"""
|
||||
parent = self._get_raw_parent_location(as_published(location), ModuleStoreEnum.RevisionOption.draft_preferred)
|
||||
if parent:
|
||||
self._update_single_item(parent, update)
|
||||
self._update_ancestors(parent, update)
|
||||
|
||||
def update_item(self, xblock, user_id, allow_not_found=False, force=False, isPublish=False, # lint-amnesty, pylint: disable=arguments-differ
|
||||
is_publish_root=True):
|
||||
"""
|
||||
Update the persisted version of xblock to reflect its current values.
|
||||
|
||||
xblock: which xblock to persist
|
||||
user_id: who made the change (ignored for now by this modulestore)
|
||||
allow_not_found: whether to create a new object if one didn't already exist or give an error
|
||||
force: force is meaningless for this modulestore
|
||||
isPublish: an internal parameter that indicates whether this update is due to a Publish operation, and
|
||||
thus whether the item's published information should be updated.
|
||||
is_publish_root: when publishing, this indicates whether xblock is the root of the publish and should
|
||||
therefore propagate subtree edit info up the tree
|
||||
"""
|
||||
course_key = xblock.location.course_key
|
||||
|
||||
try:
|
||||
definition_data = self._serialize_scope(xblock, Scope.content)
|
||||
now = datetime.now(UTC)
|
||||
payload = {
|
||||
'definition.data': definition_data,
|
||||
'metadata': self._serialize_scope(xblock, Scope.settings),
|
||||
'edit_info': {
|
||||
'edited_on': now,
|
||||
'edited_by': user_id,
|
||||
'subtree_edited_on': now,
|
||||
'subtree_edited_by': user_id,
|
||||
}
|
||||
}
|
||||
|
||||
if isPublish:
|
||||
payload['edit_info']['published_date'] = now
|
||||
payload['edit_info']['published_by'] = user_id
|
||||
elif 'published_date' in getattr(xblock, '_edit_info', {}):
|
||||
payload['edit_info']['published_date'] = xblock._edit_info['published_date']
|
||||
payload['edit_info']['published_by'] = xblock._edit_info['published_by']
|
||||
|
||||
if xblock.has_children:
|
||||
children = self._serialize_scope(xblock, Scope.children)
|
||||
payload.update({'definition.children': children['children']})
|
||||
|
||||
# Remove all old pointers to me, then add my current children back
|
||||
parent_cache = self._get_parent_cache(self.get_branch_setting())
|
||||
parent_cache.delete_by_value(xblock.location)
|
||||
for child in xblock.children:
|
||||
parent_cache.set(str(child), xblock.location)
|
||||
|
||||
self._update_single_item(xblock.scope_ids.usage_id, payload, allow_not_found=allow_not_found)
|
||||
|
||||
# update subtree edited info for ancestors
|
||||
# don't update the subtree info for descendants of the publish root for efficiency
|
||||
if not isPublish or (isPublish and is_publish_root):
|
||||
ancestor_payload = {
|
||||
'edit_info.subtree_edited_on': now,
|
||||
'edit_info.subtree_edited_by': user_id
|
||||
}
|
||||
self._update_ancestors(xblock.scope_ids.usage_id, ancestor_payload)
|
||||
|
||||
# update the edit info of the instantiated xblock
|
||||
xblock._edit_info = payload['edit_info']
|
||||
# fire signal that we've written to DB
|
||||
except ItemNotFoundError:
|
||||
if not allow_not_found: # lint-amnesty, pylint: disable=no-else-raise
|
||||
raise
|
||||
elif not self.has_course(course_key):
|
||||
raise ItemNotFoundError(course_key) # lint-amnesty, pylint: disable=raise-missing-from
|
||||
|
||||
return xblock
|
||||
|
||||
def _serialize_scope(self, xblock, scope):
|
||||
"""
|
||||
Find all fields of type reference and convert the payload from UsageKeys to deprecated strings
|
||||
@@ -1476,112 +1220,6 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
|
||||
jsonfields[field_name] = field.read_json(xblock)
|
||||
return jsonfields
|
||||
|
||||
def _get_non_orphan_parents(self, location, parents, revision):
|
||||
"""
|
||||
Extract non orphan parents by traversing the list of possible parents and remove current location
|
||||
from orphan parents to avoid parents calculation overhead next time.
|
||||
"""
|
||||
non_orphan_parents = []
|
||||
# get bulk_record once rather than for each iteration
|
||||
bulk_record = self._get_bulk_ops_record(location.course_key)
|
||||
|
||||
for parent in parents:
|
||||
parent_loc = BlockUsageLocator._from_deprecated_son(parent['_id'], location.course_key.run)
|
||||
|
||||
# travel up the tree for orphan validation
|
||||
ancestor_loc = parent_loc
|
||||
while ancestor_loc is not None:
|
||||
current_loc = ancestor_loc
|
||||
ancestor_loc = self._get_raw_parent_location(as_published(current_loc), revision)
|
||||
if ancestor_loc is None:
|
||||
bulk_record.dirty = True
|
||||
# The parent is an orphan, so remove all the children including
|
||||
# the location whose parent we are looking for from orphan parent
|
||||
self.collection.update_one(
|
||||
{'_id': parent_loc.to_deprecated_son()},
|
||||
{'$set': {'definition.children': []}},
|
||||
upsert=True,
|
||||
)
|
||||
elif ancestor_loc.block_type == 'course':
|
||||
# once we reach the top location of the tree and if the location is not an orphan then the
|
||||
# parent is not an orphan either
|
||||
non_orphan_parents.append(parent_loc)
|
||||
break
|
||||
|
||||
return non_orphan_parents
|
||||
|
||||
def _get_raw_parent_location(self, location, revision=ModuleStoreEnum.RevisionOption.published_only):
|
||||
'''
|
||||
Helper for get_parent_location that finds the location that is the parent of this location in this course,
|
||||
but does NOT return a version agnostic location.
|
||||
'''
|
||||
assert location.branch is None
|
||||
assert revision == ModuleStoreEnum.RevisionOption.published_only \
|
||||
or revision == ModuleStoreEnum.RevisionOption.draft_preferred # lint-amnesty, pylint: disable=consider-using-in
|
||||
|
||||
parent_cache = self._get_parent_cache(self.get_branch_setting())
|
||||
if parent_cache.has(str(location)):
|
||||
return parent_cache.get(str(location))
|
||||
|
||||
# create a query with tag, org, course, and the children field set to the given location
|
||||
query = self._course_key_to_son(location.course_key)
|
||||
query['definition.children'] = str(location)
|
||||
|
||||
# if only looking for the PUBLISHED parent, set the revision in the query to None
|
||||
if revision == ModuleStoreEnum.RevisionOption.published_only:
|
||||
query['_id.revision'] = MongoRevisionKey.published
|
||||
|
||||
def cache_and_return(parent_loc):
|
||||
parent_cache.set(str(location), parent_loc)
|
||||
return parent_loc
|
||||
|
||||
# query the collection, sorting by DRAFT first
|
||||
parents = list(
|
||||
self.collection.find(query, {'_id': True}, sort=[SORT_REVISION_FAVOR_DRAFT])
|
||||
)
|
||||
if len(parents) == 0:
|
||||
# no parents were found
|
||||
return cache_and_return(None)
|
||||
|
||||
if revision == ModuleStoreEnum.RevisionOption.published_only:
|
||||
if len(parents) > 1:
|
||||
non_orphan_parents = self._get_non_orphan_parents(location, parents, revision)
|
||||
if len(non_orphan_parents) == 0:
|
||||
# no actual parent found
|
||||
return cache_and_return(None)
|
||||
|
||||
if len(non_orphan_parents) > 1: # lint-amnesty, pylint: disable=no-else-raise
|
||||
# should never have multiple PUBLISHED parents
|
||||
raise ReferentialIntegrityError(
|
||||
"{} parents claim {}".format(len(parents), location)
|
||||
)
|
||||
else:
|
||||
return cache_and_return(non_orphan_parents[0].replace(run=location.course_key.run))
|
||||
else:
|
||||
# return the single PUBLISHED parent
|
||||
return cache_and_return(BlockUsageLocator._from_deprecated_son(parents[0]['_id'],
|
||||
location.course_key.run))
|
||||
else:
|
||||
# there could be 2 different parents if
|
||||
# (1) the draft item was moved or
|
||||
# (2) the parent itself has 2 versions: DRAFT and PUBLISHED
|
||||
# if there are multiple parents with version PUBLISHED then choose from non-orphan parents
|
||||
all_parents = []
|
||||
published_parents = 0
|
||||
for parent in parents:
|
||||
if parent['_id']['revision'] is None:
|
||||
published_parents += 1
|
||||
all_parents.append(parent)
|
||||
|
||||
# since we sorted by SORT_REVISION_FAVOR_DRAFT, the 0'th parent is the one we want
|
||||
if published_parents > 1:
|
||||
non_orphan_parents = self._get_non_orphan_parents(location, all_parents, revision)
|
||||
return cache_and_return(non_orphan_parents[0].replace(run=location.course_key.run))
|
||||
|
||||
found_id = all_parents[0]['_id']
|
||||
# don't disclose revision outside modulestore
|
||||
return cache_and_return(BlockUsageLocator._from_deprecated_son(found_id, location.course_key.run))
|
||||
|
||||
def get_parent_location(self, location, revision=ModuleStoreEnum.RevisionOption.published_only, **kwargs):
|
||||
'''
|
||||
Find the location that is the parent of this location in this course.
|
||||
@@ -1597,9 +1235,6 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
|
||||
preferring DRAFT, if parent(s) exists,
|
||||
else returns None
|
||||
'''
|
||||
parent = self._get_raw_parent_location(location, revision)
|
||||
if parent:
|
||||
return parent
|
||||
return None
|
||||
|
||||
def get_modulestore_type(self, course_key=None): # lint-amnesty, pylint: disable=arguments-differ, unused-argument
|
||||
@@ -1614,22 +1249,7 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
|
||||
"""
|
||||
Return an array of all of the locations for orphans in the course.
|
||||
"""
|
||||
course_key = self.fill_in_run(course_key)
|
||||
detached_categories = [name for name, __ in XBlock.load_tagged_classes("detached")]
|
||||
query = self._course_key_to_son(course_key)
|
||||
query['_id.category'] = {'$nin': detached_categories}
|
||||
all_items = self.collection.find(query)
|
||||
all_reachable = set()
|
||||
item_locs = set()
|
||||
for item in all_items:
|
||||
if item['_id']['category'] != 'course':
|
||||
# It would be nice to change this method to return UsageKeys instead of the deprecated string.
|
||||
item_locs.add(
|
||||
str(as_published(BlockUsageLocator._from_deprecated_son(item['_id'], course_key.run)))
|
||||
)
|
||||
all_reachable = all_reachable.union(item.get('definition', {}).get('children', []))
|
||||
item_locs -= all_reachable
|
||||
return [UsageKey.from_string(item_loc).map_into_course(course_key) for item_loc in item_locs]
|
||||
raise NotImplementedError
|
||||
|
||||
def get_courses_for_wiki(self, wiki_slug, **kwargs):
|
||||
"""
|
||||
@@ -1653,8 +1273,6 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
|
||||
"""
|
||||
kvs = MongoKeyValueStore(
|
||||
definition_data,
|
||||
None,
|
||||
[],
|
||||
metadata,
|
||||
)
|
||||
|
||||
@@ -1917,3 +1535,7 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
|
||||
|
||||
def unpublish(self, location, user_id):
|
||||
raise NotImplementedError()
|
||||
|
||||
def update_item(self, xblock, user_id, allow_not_found=False, force=False, isPublish=False, # lint-amnesty, pylint: disable=arguments-differ
|
||||
is_publish_root=True):
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -9,18 +9,11 @@ and otherwise returns i4x://org/course/cat/name).
|
||||
|
||||
import logging
|
||||
|
||||
import pymongo
|
||||
from opaque_keys.edx.keys import UsageKey
|
||||
from opaque_keys.edx.locator import BlockUsageLocator
|
||||
from xblock.core import XBlock
|
||||
|
||||
from openedx.core.lib.cache_utils import request_cached
|
||||
from xmodule.exceptions import InvalidVersionError
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.draft_and_published import DIRECT_ONLY_CATEGORIES, UnsupportedRevisionError
|
||||
from xmodule.modulestore.exceptions import (
|
||||
DuplicateCourseError,
|
||||
DuplicateItemError,
|
||||
InvalidBranchSetting,
|
||||
ItemNotFoundError
|
||||
)
|
||||
@@ -31,7 +24,6 @@ from xmodule.modulestore.mongo.base import (
|
||||
as_draft,
|
||||
as_published
|
||||
)
|
||||
from xmodule.modulestore.store_utilities import rewrite_nonportable_content_links
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -58,7 +50,7 @@ class DraftModuleStore(MongoModuleStore):
|
||||
This module store also includes functionality to promote DRAFT blocks (and their children)
|
||||
to published blocks.
|
||||
"""
|
||||
def get_item(self, usage_key, depth=0, revision=None, using_descriptor_system=None, **kwargs): # lint-amnesty, pylint: disable=arguments-differ
|
||||
def get_item(self, usage_key, revision=None, using_descriptor_system=None, **kwargs): # lint-amnesty, pylint: disable=arguments-differ
|
||||
"""
|
||||
Returns an XModuleDescriptor instance for the item at usage_key.
|
||||
|
||||
@@ -93,13 +85,13 @@ class DraftModuleStore(MongoModuleStore):
|
||||
"""
|
||||
def get_published():
|
||||
return wrap_draft(super(DraftModuleStore, self).get_item( # lint-amnesty, pylint: disable=super-with-arguments
|
||||
usage_key, depth=depth, using_descriptor_system=using_descriptor_system,
|
||||
usage_key, using_descriptor_system=using_descriptor_system,
|
||||
for_parent=kwargs.get('for_parent'),
|
||||
))
|
||||
|
||||
def get_draft():
|
||||
return wrap_draft(super(DraftModuleStore, self).get_item( # lint-amnesty, pylint: disable=super-with-arguments
|
||||
as_draft(usage_key), depth=depth, using_descriptor_system=using_descriptor_system,
|
||||
as_draft(usage_key), using_descriptor_system=using_descriptor_system,
|
||||
for_parent=kwargs.get('for_parent')
|
||||
))
|
||||
|
||||
@@ -185,76 +177,7 @@ class DraftModuleStore(MongoModuleStore):
|
||||
Only called if cloning within this store or if env doesn't set up mixed.
|
||||
* copy the courseware
|
||||
"""
|
||||
# check to see if the source course is actually there
|
||||
if not self.has_course(source_course_id):
|
||||
raise ItemNotFoundError(f"Cannot find a course at {source_course_id}. Aborting")
|
||||
|
||||
with self.bulk_operations(dest_course_id):
|
||||
# verify that the dest_location really is an empty course
|
||||
# b/c we don't want the payload, I'm copying the guts of get_items here
|
||||
query = self._course_key_to_son(dest_course_id)
|
||||
query['_id.category'] = {'$nin': ['course', 'about']}
|
||||
if self.collection.count_documents(query, limit=1) > 0:
|
||||
raise DuplicateCourseError(
|
||||
dest_course_id,
|
||||
"Course at destination {} is not an empty course. "
|
||||
"You can only clone into an empty course. Aborting...".format(
|
||||
dest_course_id
|
||||
)
|
||||
)
|
||||
|
||||
# clone the assets
|
||||
super().clone_course(source_course_id, dest_course_id, user_id, fields) # lint-amnesty, pylint: disable=super-with-arguments
|
||||
|
||||
# get the whole old course
|
||||
new_course = self.get_course(dest_course_id)
|
||||
if new_course is None:
|
||||
# create_course creates the about overview
|
||||
new_course = self.create_course(
|
||||
dest_course_id.org, dest_course_id.course, dest_course_id.run, user_id, fields=fields
|
||||
)
|
||||
else:
|
||||
# update fields on existing course
|
||||
for key, value in fields.items():
|
||||
setattr(new_course, key, value)
|
||||
self.update_item(new_course, user_id)
|
||||
|
||||
# Get all blocks under this namespace which is (tag, org, course) tuple
|
||||
blocks = self.get_items(source_course_id, revision=ModuleStoreEnum.RevisionOption.published_only)
|
||||
self._clone_blocks(blocks, dest_course_id, user_id)
|
||||
course_location = dest_course_id.make_usage_key('course', dest_course_id.run)
|
||||
self.publish(course_location, user_id)
|
||||
|
||||
blocks = self.get_items(source_course_id, revision=ModuleStoreEnum.RevisionOption.draft_only)
|
||||
self._clone_blocks(blocks, dest_course_id, user_id)
|
||||
|
||||
return True
|
||||
|
||||
def _clone_blocks(self, blocks, dest_course_id, user_id):
|
||||
"""Clones each block into the given course"""
|
||||
for block in blocks:
|
||||
original_loc = block.location
|
||||
block.location = block.location.map_into_course(dest_course_id)
|
||||
if block.location.block_type == 'course':
|
||||
block.location = block.location.replace(name=block.location.run)
|
||||
|
||||
log.info("Cloning block %s to %s....", original_loc, block.location)
|
||||
|
||||
if 'data' in block.fields and block.fields['data'].is_set_on(block) and isinstance(block.data, str): # lint-amnesty, pylint: disable=line-too-long
|
||||
block.data = rewrite_nonportable_content_links(
|
||||
original_loc.course_key, dest_course_id, block.data
|
||||
)
|
||||
|
||||
# repoint children
|
||||
if block.has_children:
|
||||
new_children = []
|
||||
for child_loc in block.children:
|
||||
child_loc = child_loc.map_into_course(dest_course_id)
|
||||
new_children.append(child_loc)
|
||||
|
||||
block.children = new_children
|
||||
|
||||
self.update_item(block, user_id, allow_not_found=True)
|
||||
raise NotImplementedError
|
||||
|
||||
def _get_raw_parent_locations(self, location, key_revision):
|
||||
"""
|
||||
@@ -383,240 +306,6 @@ class DraftModuleStore(MongoModuleStore):
|
||||
else:
|
||||
raise UnsupportedRevisionError()
|
||||
|
||||
def convert_to_draft(self, location, user_id):
|
||||
"""
|
||||
Copy the subtree rooted at source_location and mark the copies as draft.
|
||||
|
||||
Args:
|
||||
location: the location of the source (its revision must be None)
|
||||
user_id: the ID of the user doing the operation
|
||||
|
||||
Raises:
|
||||
InvalidVersionError: if the source can not be made into a draft
|
||||
ItemNotFoundError: if the source does not exist
|
||||
"""
|
||||
# TODO (dhm) I don't think this needs to recurse anymore but can convert each unit on demand.
|
||||
# See if that's true.
|
||||
# delegating to internal b/c we don't want any public user to use the kwargs on the internal
|
||||
self._convert_to_draft(location, user_id, ignore_if_draft=True)
|
||||
|
||||
# return the new draft item (does another fetch)
|
||||
# get_item will wrap_draft so don't call it here (otherwise, it would override the is_draft attribute)
|
||||
return self.get_item(location)
|
||||
|
||||
def _convert_to_draft(self, location, user_id, delete_published=False, ignore_if_draft=False): # lint-amnesty, pylint: disable=unused-argument
|
||||
"""
|
||||
Internal method with additional internal parameters to convert a subtree to draft.
|
||||
|
||||
Args:
|
||||
location: the location of the source (its revision must be MongoRevisionKey.published)
|
||||
user_id: the ID of the user doing the operation
|
||||
delete_published (Boolean): intended for use by unpublish
|
||||
ignore_if_draft(Boolean): for internal use only as part of depth first change
|
||||
|
||||
Raises:
|
||||
InvalidVersionError: if the source can not be made into a draft
|
||||
ItemNotFoundError: if the source does not exist
|
||||
DuplicateItemError: if the source or any of its descendants already has a draft copy. Only
|
||||
useful for unpublish b/c we don't want unpublish to overwrite any existing drafts.
|
||||
"""
|
||||
# verify input conditions: can only convert to draft branch; so, verify that's the setting
|
||||
self._verify_branch_setting(ModuleStoreEnum.Branch.draft_preferred)
|
||||
_verify_revision_is_published(location)
|
||||
|
||||
# ensure we are not creating a DRAFT of an item that is direct-only
|
||||
if location.block_type in DIRECT_ONLY_CATEGORIES:
|
||||
raise InvalidVersionError(location)
|
||||
|
||||
def convert_item(item, to_be_deleted):
|
||||
"""
|
||||
Convert the subtree
|
||||
"""
|
||||
# collect the children's ids for future processing
|
||||
next_tier = []
|
||||
for child in item.get('definition', {}).get('children', []):
|
||||
child_loc = BlockUsageLocator.from_string(child)
|
||||
next_tier.append(child_loc.to_deprecated_son())
|
||||
|
||||
# insert a new DRAFT version of the item
|
||||
item['_id']['revision'] = MongoRevisionKey.draft
|
||||
# ensure keys are in fixed and right order before inserting
|
||||
item['_id'] = self._id_dict_to_son(item['_id'])
|
||||
bulk_record = self._get_bulk_ops_record(location.course_key)
|
||||
bulk_record.dirty = True
|
||||
try:
|
||||
self.collection.insert_one(item)
|
||||
except pymongo.errors.DuplicateKeyError:
|
||||
# prevent re-creation of DRAFT versions, unless explicitly requested to ignore
|
||||
if not ignore_if_draft:
|
||||
raise DuplicateItemError(item['_id'], self, 'collection') # lint-amnesty, pylint: disable=raise-missing-from
|
||||
|
||||
# delete the old PUBLISHED version if requested
|
||||
if delete_published:
|
||||
item['_id']['revision'] = MongoRevisionKey.published
|
||||
to_be_deleted.append(item['_id'])
|
||||
|
||||
return next_tier
|
||||
|
||||
# convert the subtree using the original item as the root
|
||||
self._breadth_first(convert_item, [location])
|
||||
|
||||
def update_item( # lint-amnesty, pylint: disable=arguments-differ
|
||||
self, # lint-amnesty, pylint: disable=unused-argument
|
||||
xblock,
|
||||
user_id,
|
||||
allow_not_found=False,
|
||||
force=False,
|
||||
isPublish=False,
|
||||
child_update=False,
|
||||
**kwargs):
|
||||
"""
|
||||
See superclass doc.
|
||||
In addition to the superclass's behavior, this method converts the unit to draft if it's not
|
||||
direct-only and not already draft.
|
||||
"""
|
||||
draft_loc = self.for_branch_setting(xblock.location)
|
||||
|
||||
# if the revision is published, defer to base
|
||||
if draft_loc.branch == MongoRevisionKey.published:
|
||||
item = super().update_item(xblock, user_id, allow_not_found) # lint-amnesty, pylint: disable=super-with-arguments
|
||||
course_key = xblock.location.course_key
|
||||
if isPublish or (item.category in DIRECT_ONLY_CATEGORIES and not child_update):
|
||||
self._flag_publish_event(course_key)
|
||||
return item
|
||||
|
||||
if not super().has_item(draft_loc): # lint-amnesty, pylint: disable=super-with-arguments
|
||||
try:
|
||||
# ignore any descendants which are already draft
|
||||
self._convert_to_draft(xblock.location, user_id, ignore_if_draft=True)
|
||||
except ItemNotFoundError as exception:
|
||||
# ignore the exception only if allow_not_found is True and
|
||||
# the item that wasn't found is the one that was passed in
|
||||
# we make this extra location check so we do not hide errors when converting any children to draft
|
||||
if not (allow_not_found and exception.args[0] == xblock.location):
|
||||
raise
|
||||
|
||||
xblock.location = draft_loc
|
||||
super().update_item(xblock, user_id, allow_not_found, isPublish=isPublish) # lint-amnesty, pylint: disable=super-with-arguments
|
||||
return wrap_draft(xblock)
|
||||
|
||||
def delete_item(self, location, user_id, revision=None, **kwargs): # lint-amnesty, pylint: disable=arguments-differ
|
||||
"""
|
||||
Delete an item from this modulestore.
|
||||
The method determines which revisions to delete. It disconnects and deletes the subtree.
|
||||
In general, it assumes deletes only occur on drafts except for direct_only. The only exceptions
|
||||
are internal calls like deleting orphans (during publishing as well as from delete_orphan view).
|
||||
To indicate that all versions should be deleted, pass the keyword revision=ModuleStoreEnum.RevisionOption.all.
|
||||
|
||||
* Deleting a DIRECT_ONLY_CATEGORIES block, deletes both draft and published children and removes from parent.
|
||||
* Deleting a specific version of block whose parent is of DIRECT_ONLY_CATEGORIES, only removes it from parent if
|
||||
the other version of the block does not exist. Deletes only children of same version.
|
||||
* Other deletions remove from parent of same version and subtree of same version
|
||||
|
||||
Args:
|
||||
location: UsageKey of the item to be deleted
|
||||
user_id: id of the user deleting the item
|
||||
revision:
|
||||
None - deletes the item and its subtree, and updates the parents per description above
|
||||
ModuleStoreEnum.RevisionOption.published_only - removes only Published versions
|
||||
ModuleStoreEnum.RevisionOption.all - removes both Draft and Published parents
|
||||
currently only provided by contentstore.views.item.orphan_handler
|
||||
Otherwise, raises a ValueError.
|
||||
"""
|
||||
self._verify_branch_setting(ModuleStoreEnum.Branch.draft_preferred)
|
||||
_verify_revision_is_published(location)
|
||||
|
||||
is_item_direct_only = location.block_type in DIRECT_ONLY_CATEGORIES
|
||||
if is_item_direct_only or revision == ModuleStoreEnum.RevisionOption.published_only:
|
||||
parent_revision = MongoRevisionKey.published
|
||||
elif revision == ModuleStoreEnum.RevisionOption.all:
|
||||
parent_revision = ModuleStoreEnum.RevisionOption.all
|
||||
else:
|
||||
parent_revision = MongoRevisionKey.draft
|
||||
|
||||
# remove subtree from its parent
|
||||
parent_locations = self._get_raw_parent_locations(location, key_revision=parent_revision)
|
||||
# if no parents, then we're trying to delete something which we should convert to draft
|
||||
if not parent_locations:
|
||||
# find the published parent, convert it to draft, then manipulate the draft
|
||||
parent_locations = self._get_raw_parent_locations(location, key_revision=MongoRevisionKey.published)
|
||||
# parent_locations will still be empty if the object was an orphan
|
||||
if parent_locations:
|
||||
draft_parent = self.convert_to_draft(parent_locations[0], user_id)
|
||||
parent_locations = [draft_parent.location]
|
||||
# there could be 2 parents if
|
||||
# Case 1: the draft item moved from one parent to another
|
||||
# Case 2: revision==ModuleStoreEnum.RevisionOption.all and the single
|
||||
# parent has 2 versions: draft and published
|
||||
for parent_location in parent_locations:
|
||||
# don't remove from direct_only parent if other versions of this still exists (this code
|
||||
# assumes that there's only one parent_location in this case)
|
||||
if not is_item_direct_only and parent_location.block_type in DIRECT_ONLY_CATEGORIES:
|
||||
# see if other version of to-be-deleted root exists
|
||||
query = location.to_deprecated_son(prefix='_id.')
|
||||
del query['_id.revision']
|
||||
if self.collection.count_documents(query) > 1:
|
||||
continue
|
||||
|
||||
parent_block = super().get_item(parent_location) # lint-amnesty, pylint: disable=super-with-arguments
|
||||
parent_block.children.remove(location)
|
||||
parent_block.location = parent_location # ensure the location is with the correct revision
|
||||
self.update_item(parent_block, user_id, child_update=True)
|
||||
self._flag_publish_event(location.course_key)
|
||||
|
||||
if is_item_direct_only or revision == ModuleStoreEnum.RevisionOption.all:
|
||||
as_functions = [as_draft, as_published]
|
||||
elif revision == ModuleStoreEnum.RevisionOption.published_only:
|
||||
as_functions = [as_published]
|
||||
elif revision is None:
|
||||
as_functions = [as_draft]
|
||||
else:
|
||||
raise UnsupportedRevisionError(
|
||||
[
|
||||
None,
|
||||
ModuleStoreEnum.RevisionOption.published_only,
|
||||
ModuleStoreEnum.RevisionOption.all
|
||||
]
|
||||
)
|
||||
self._delete_subtree(location, as_functions)
|
||||
|
||||
def _delete_subtree(self, location, as_functions, draft_only=False):
|
||||
"""
|
||||
Internal method for deleting all of the subtree whose revisions match the as_functions
|
||||
"""
|
||||
course_key = location.course_key
|
||||
|
||||
def _delete_item(current_entry, to_be_deleted):
|
||||
"""
|
||||
Depth first deletion of nodes
|
||||
"""
|
||||
to_be_deleted.append(self._id_dict_to_son(current_entry['_id']))
|
||||
next_tier = []
|
||||
for child_loc in current_entry.get('definition', {}).get('children', []):
|
||||
child_loc = UsageKey.from_string(child_loc).map_into_course(course_key)
|
||||
|
||||
# single parent can have 2 versions: draft and published
|
||||
# get draft parents only while deleting draft block
|
||||
if draft_only:
|
||||
revision = MongoRevisionKey.draft
|
||||
else:
|
||||
revision = ModuleStoreEnum.RevisionOption.all
|
||||
|
||||
parents = self._get_raw_parent_locations(child_loc, revision)
|
||||
# Don't delete blocks if one of its parents shouldn't be deleted
|
||||
# This should only be an issue for courses have ended up in
|
||||
# a state where blocks have multiple parents
|
||||
if all(parent.to_deprecated_son() in to_be_deleted for parent in parents):
|
||||
for rev_func in as_functions:
|
||||
current_loc = rev_func(child_loc)
|
||||
current_son = current_loc.to_deprecated_son()
|
||||
next_tier.append(current_son)
|
||||
|
||||
return next_tier
|
||||
|
||||
first_tier = [as_func(location) for as_func in as_functions]
|
||||
self._breadth_first(_delete_item, first_tier)
|
||||
|
||||
def _breadth_first(self, function, root_usages):
|
||||
"""
|
||||
Get the root_usage from the db and do a depth first scan. Call the function on each. The
|
||||
@@ -648,197 +337,6 @@ class DraftModuleStore(MongoModuleStore):
|
||||
bulk_record.dirty = True
|
||||
self.collection.delete_many({'_id': {'$in': to_be_deleted}})
|
||||
|
||||
def has_changes(self, xblock):
|
||||
"""
|
||||
Check if the subtree rooted at xblock has any drafts and thus may possibly have changes
|
||||
:param xblock: xblock to check
|
||||
:return: True if there are any drafts anywhere in the subtree under xblock (a weaker
|
||||
condition than for other stores)
|
||||
"""
|
||||
return self._cached_has_changes(self.request_cache, xblock)
|
||||
|
||||
@request_cached(
|
||||
# use the XBlock's location value in the cache key
|
||||
arg_map_function=lambda arg: str(arg.location if isinstance(arg, XBlock) else arg),
|
||||
# use this store's request_cache
|
||||
request_cache_getter=lambda args, kwargs: args[1],
|
||||
)
|
||||
def _cached_has_changes(self, request_cache, xblock): # lint-amnesty, pylint: disable=unused-argument
|
||||
"""
|
||||
Internal has_changes method that caches the result.
|
||||
"""
|
||||
# don't check children if this block has changes (is not public)
|
||||
if getattr(xblock, 'is_draft', False):
|
||||
return True
|
||||
# if this block doesn't have changes, then check its children
|
||||
elif xblock.has_children:
|
||||
# fix a bug where dangling pointers should imply a change
|
||||
if len(xblock.children) > len(xblock.get_children()):
|
||||
return True
|
||||
return any(self.has_changes(child) for child in xblock.get_children())
|
||||
# otherwise there are no changes
|
||||
else:
|
||||
return False
|
||||
|
||||
def publish(self, location, user_id, **kwargs): # lint-amnesty, pylint: disable=unused-argument
|
||||
"""
|
||||
Publish the subtree rooted at location to the live course and remove the drafts.
|
||||
Such publishing may cause the deletion of previously published but subsequently deleted
|
||||
child trees. Overwrites any existing published xblocks from the subtree.
|
||||
|
||||
Treats the publishing of non-draftable items as merely a subtree selection from
|
||||
which to descend.
|
||||
|
||||
Raises:
|
||||
ItemNotFoundError: if any of the draft subtree nodes aren't found
|
||||
|
||||
Returns:
|
||||
The newly published xblock
|
||||
"""
|
||||
# NOTE: cannot easily use self._breadth_first b/c need to get pub'd and draft as pairs
|
||||
# (could do it by having 2 breadth first scans, the first to just get all published children
|
||||
# and the second to do the publishing on the drafts looking for the published in the cached
|
||||
# list of published ones.)
|
||||
to_be_deleted = []
|
||||
|
||||
def _internal_depth_first(item_location, is_root):
|
||||
"""
|
||||
Depth first publishing from the given location
|
||||
"""
|
||||
try:
|
||||
# handle child does not exist w/o killing publish
|
||||
item = self.get_item(item_location)
|
||||
except ItemNotFoundError:
|
||||
log.warning('Cannot find: %s', item_location)
|
||||
return
|
||||
|
||||
# publish the children first
|
||||
if item.has_children:
|
||||
for child_loc in item.children:
|
||||
_internal_depth_first(child_loc, False)
|
||||
|
||||
if item_location.block_type in DIRECT_ONLY_CATEGORIES or not getattr(item, 'is_draft', False):
|
||||
# ignore noop attempt to publish something that can't be or isn't currently draft
|
||||
return
|
||||
|
||||
# try to find the originally PUBLISHED version, if it exists
|
||||
try:
|
||||
original_published = super(DraftModuleStore, self).get_item(item_location) # lint-amnesty, pylint: disable=super-with-arguments
|
||||
except ItemNotFoundError:
|
||||
original_published = None
|
||||
|
||||
# if the category of this item allows having children
|
||||
if item.has_children:
|
||||
if original_published is not None:
|
||||
# see if previously published children were deleted. 2 reasons for children lists to differ:
|
||||
# Case 1: child deleted
|
||||
# Case 2: child moved
|
||||
for orig_child in original_published.children:
|
||||
if orig_child not in item.children:
|
||||
published_parent = self.get_parent_location(orig_child)
|
||||
if published_parent == item_location:
|
||||
# Case 1: child was deleted in draft parent item
|
||||
# So, delete published version of the child now that we're publishing the draft parent
|
||||
self._delete_subtree(orig_child, [as_published])
|
||||
else:
|
||||
# Case 2: child was moved to a new draft parent item
|
||||
# So, do not delete the child. It will be published when the new parent is published.
|
||||
pass
|
||||
|
||||
# update the published (not draft) item (ignoring that item is "draft"). The published
|
||||
# may not exist; (if original_published is None); so, allow_not_found
|
||||
super(DraftModuleStore, self).update_item( # lint-amnesty, pylint: disable=super-with-arguments
|
||||
item, user_id, isPublish=True, is_publish_root=is_root, allow_not_found=True
|
||||
)
|
||||
to_be_deleted.append(as_draft(item_location).to_deprecated_son())
|
||||
|
||||
# verify input conditions
|
||||
self._verify_branch_setting(ModuleStoreEnum.Branch.draft_preferred)
|
||||
_verify_revision_is_published(location)
|
||||
|
||||
_internal_depth_first(location, True)
|
||||
course_key = location.course_key
|
||||
bulk_record = self._get_bulk_ops_record(course_key)
|
||||
if len(to_be_deleted) > 0:
|
||||
bulk_record.dirty = True
|
||||
self.collection.delete_many({'_id': {'$in': to_be_deleted}})
|
||||
|
||||
self._flag_publish_event(course_key)
|
||||
|
||||
return self.get_item(as_published(location))
|
||||
|
||||
def unpublish(self, location, user_id, **kwargs): # lint-amnesty, pylint: disable=unused-argument
|
||||
"""
|
||||
Turn the published version into a draft, removing the published version.
|
||||
|
||||
NOTE: unlike publish, this gives an error if called above the draftable level as it's intended
|
||||
to remove things from the published version
|
||||
"""
|
||||
# ensure we are not creating a DRAFT of an item that is direct-only
|
||||
if location.block_type in DIRECT_ONLY_CATEGORIES:
|
||||
raise InvalidVersionError(location)
|
||||
|
||||
self._verify_branch_setting(ModuleStoreEnum.Branch.draft_preferred)
|
||||
self._convert_to_draft(location, user_id, delete_published=True)
|
||||
|
||||
course_key = location.course_key
|
||||
self._flag_publish_event(course_key)
|
||||
|
||||
def revert_to_published(self, location, user_id=None):
|
||||
"""
|
||||
Reverts an item to its last published version (recursively traversing all of its descendants).
|
||||
If no published version exists, an InvalidVersionError is thrown.
|
||||
|
||||
If a published version exists but there is no draft version of this item or any of its descendants, this
|
||||
method is a no-op. It is also a no-op if the root item is in DIRECT_ONLY_CATEGORIES.
|
||||
|
||||
:raises InvalidVersionError: if no published version exists for the location specified
|
||||
"""
|
||||
self._verify_branch_setting(ModuleStoreEnum.Branch.draft_preferred)
|
||||
_verify_revision_is_published(location)
|
||||
|
||||
if location.block_type in DIRECT_ONLY_CATEGORIES:
|
||||
return
|
||||
|
||||
if not self.has_item(location, revision=ModuleStoreEnum.RevisionOption.published_only):
|
||||
raise InvalidVersionError(location)
|
||||
|
||||
def delete_draft_only(root_location):
|
||||
"""
|
||||
Helper function that calls delete on the specified location if a draft version of the item exists.
|
||||
If no draft exists, this function recursively calls itself on the children of the item.
|
||||
"""
|
||||
query = root_location.to_deprecated_son(prefix='_id.')
|
||||
del query['_id.revision']
|
||||
versions_found = self.collection.find(
|
||||
query, {'_id': True, 'definition.children': True}, sort=[SORT_REVISION_FAVOR_DRAFT]
|
||||
)
|
||||
versions_found = list(versions_found)
|
||||
# If 2 versions versions exist, we can assume one is a published version. Go ahead and do the delete
|
||||
# of the draft version.
|
||||
if len(versions_found) > 1:
|
||||
# Moving a child from published parent creates a draft of the parent and moved child.
|
||||
published_version = [
|
||||
version
|
||||
for version in versions_found
|
||||
if version.get('_id').get('revision') != MongoRevisionKey.draft
|
||||
]
|
||||
if len(published_version) > 0:
|
||||
# This change makes sure that parents are updated too i.e. an item will have only one parent.
|
||||
self.update_parent_if_moved(root_location, published_version[0], delete_draft_only, user_id)
|
||||
self._delete_subtree(root_location, [as_draft], draft_only=True)
|
||||
elif len(versions_found) == 1:
|
||||
# Since this method cannot be called on something in DIRECT_ONLY_CATEGORIES and we call
|
||||
# delete_subtree as soon as we find an item with a draft version, if there is only 1 version
|
||||
# it must be published (since adding a child to a published item creates a draft of the parent).
|
||||
item = versions_found[0]
|
||||
assert item.get('_id').get('revision') != MongoRevisionKey.draft
|
||||
for child in item.get('definition', {}).get('children', []):
|
||||
child_loc = BlockUsageLocator.from_string(child)
|
||||
delete_draft_only(child_loc)
|
||||
|
||||
delete_draft_only(location)
|
||||
|
||||
def update_parent_if_moved(self, original_parent_location, published_version, delete_draft_only, user_id):
|
||||
"""
|
||||
Update parent of an item if it has moved.
|
||||
@@ -849,53 +347,7 @@ class DraftModuleStore(MongoModuleStore):
|
||||
delete_draft_only (function) : A callback function to delete draft children if it was moved.
|
||||
user_id (int) : User id
|
||||
"""
|
||||
for child_location in published_version.get('definition', {}).get('children', []):
|
||||
item_location = UsageKey.from_string(child_location).map_into_course(original_parent_location.course_key)
|
||||
try:
|
||||
source_item = self.get_item(item_location)
|
||||
except ItemNotFoundError:
|
||||
log.error('Unable to find the item %s', str(item_location))
|
||||
return
|
||||
|
||||
if source_item.parent and source_item.parent.block_id != original_parent_location.block_id:
|
||||
if self.update_item_parent(item_location, original_parent_location, source_item.parent, user_id):
|
||||
delete_draft_only(BlockUsageLocator.from_string(child_location))
|
||||
|
||||
def _query_children_for_cache_children(self, course_key, items):
|
||||
# first get non-draft in a round-trip
|
||||
to_process_non_drafts = super()._query_children_for_cache_children(course_key, items) # lint-amnesty, pylint: disable=super-with-arguments
|
||||
|
||||
to_process_dict = {}
|
||||
for non_draft in to_process_non_drafts:
|
||||
to_process_dict[BlockUsageLocator._from_deprecated_son(non_draft["_id"], course_key.run)] = non_draft # lint-amnesty, pylint: disable=protected-access
|
||||
|
||||
if self.get_branch_setting() == ModuleStoreEnum.Branch.draft_preferred:
|
||||
# now query all draft content in another round-trip
|
||||
query = []
|
||||
for item in items:
|
||||
item_usage_key = UsageKey.from_string(item).map_into_course(course_key)
|
||||
if item_usage_key.block_type not in DIRECT_ONLY_CATEGORIES:
|
||||
query.append(as_draft(item_usage_key).to_deprecated_son())
|
||||
if query:
|
||||
query = {'_id': {'$in': query}}
|
||||
to_process_drafts = list(self.collection.find(query))
|
||||
|
||||
# now we have to go through all drafts and replace the non-draft
|
||||
# with the draft. This is because the semantics of the DraftStore is to
|
||||
# always return the draft - if available
|
||||
for draft in to_process_drafts:
|
||||
draft_loc = BlockUsageLocator._from_deprecated_son(draft["_id"], course_key.run) # lint-amnesty, pylint: disable=protected-access
|
||||
draft_as_non_draft_loc = as_published(draft_loc)
|
||||
|
||||
# does non-draft exist in the collection
|
||||
# if so, replace it
|
||||
if draft_as_non_draft_loc in to_process_dict:
|
||||
to_process_dict[draft_as_non_draft_loc] = draft
|
||||
|
||||
# convert the dict - which is used for look ups - back into a list
|
||||
queried_children = list(to_process_dict.values())
|
||||
|
||||
return queried_children
|
||||
raise NotImplementedError
|
||||
|
||||
def has_published_version(self, xblock):
|
||||
"""
|
||||
|
||||
@@ -20,8 +20,7 @@ from xmodule.modulestore import IncorrectlySortedList, ModuleStoreEnum, SortedAs
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from xmodule.modulestore.tests.utils import (
|
||||
MIXED_MODULESTORE_BOTH_SETUP,
|
||||
MODULESTORE_SETUPS,
|
||||
SPLIT_MODULESTORE_SETUP,
|
||||
MixedModulestoreBuilder,
|
||||
XmlModulestoreBuilder
|
||||
)
|
||||
@@ -167,7 +166,7 @@ class TestMongoAssetMetadataStorage(TestCase):
|
||||
if store is not None and i not in (4, 5):
|
||||
store.save_asset_metadata(asset_md, asset[4])
|
||||
|
||||
@ddt.data(*MODULESTORE_SETUPS)
|
||||
@ddt.data(SPLIT_MODULESTORE_SETUP)
|
||||
def test_save_one_and_confirm(self, storebuilder):
|
||||
"""
|
||||
Save the metadata in each store and retrieve it singularly, as all assets, and after deleting all.
|
||||
@@ -185,7 +184,7 @@ class TestMongoAssetMetadataStorage(TestCase):
|
||||
self._assert_metadata_equal(new_asset_md, found_asset_md)
|
||||
assert len(store.get_all_asset_metadata(course.id, 'asset')) == 1
|
||||
|
||||
@ddt.data(*MODULESTORE_SETUPS)
|
||||
@ddt.data(SPLIT_MODULESTORE_SETUP)
|
||||
def test_delete(self, storebuilder):
|
||||
"""
|
||||
Delete non-existent and existent metadata
|
||||
@@ -202,7 +201,7 @@ class TestMongoAssetMetadataStorage(TestCase):
|
||||
assert store.delete_asset_metadata(new_asset_loc, ModuleStoreEnum.UserID.test) == 1
|
||||
assert len(store.get_all_asset_metadata(course.id, 'asset')) == 0
|
||||
|
||||
@ddt.data(*MODULESTORE_SETUPS)
|
||||
@ddt.data(SPLIT_MODULESTORE_SETUP)
|
||||
def test_find_non_existing_assets(self, storebuilder):
|
||||
"""
|
||||
Find a non-existent asset in an existing course.
|
||||
@@ -214,7 +213,7 @@ class TestMongoAssetMetadataStorage(TestCase):
|
||||
asset_md = store.find_asset_metadata(new_asset_loc)
|
||||
assert asset_md is None
|
||||
|
||||
@ddt.data(*MODULESTORE_SETUPS)
|
||||
@ddt.data(SPLIT_MODULESTORE_SETUP)
|
||||
def test_get_all_non_existing_assets(self, storebuilder):
|
||||
"""
|
||||
Get all assets in an existing course when no assets exist.
|
||||
@@ -225,7 +224,7 @@ class TestMongoAssetMetadataStorage(TestCase):
|
||||
asset_md = store.get_all_asset_metadata(course.id, 'asset')
|
||||
assert asset_md == []
|
||||
|
||||
@ddt.data(*MODULESTORE_SETUPS)
|
||||
@ddt.data(SPLIT_MODULESTORE_SETUP)
|
||||
def test_find_assets_in_non_existent_course(self, storebuilder):
|
||||
"""
|
||||
Find asset metadata from a non-existent course.
|
||||
@@ -242,7 +241,7 @@ class TestMongoAssetMetadataStorage(TestCase):
|
||||
with pytest.raises(ItemNotFoundError):
|
||||
store.get_all_asset_metadata(fake_course_id, 'asset')
|
||||
|
||||
@ddt.data(*MODULESTORE_SETUPS)
|
||||
@ddt.data(SPLIT_MODULESTORE_SETUP)
|
||||
def test_add_same_asset_twice(self, storebuilder):
|
||||
"""
|
||||
Add an asset's metadata, then add it again.
|
||||
@@ -259,7 +258,7 @@ class TestMongoAssetMetadataStorage(TestCase):
|
||||
# Still one here?
|
||||
assert len(store.get_all_asset_metadata(course.id, 'asset')) == 1
|
||||
|
||||
@ddt.data(*MODULESTORE_SETUPS)
|
||||
@ddt.data(SPLIT_MODULESTORE_SETUP)
|
||||
def test_different_asset_types(self, storebuilder):
|
||||
"""
|
||||
Test saving assets with other asset types.
|
||||
@@ -273,7 +272,7 @@ class TestMongoAssetMetadataStorage(TestCase):
|
||||
assert len(store.get_all_asset_metadata(course.id, 'vrml')) == 1
|
||||
assert len(store.get_all_asset_metadata(course.id, 'asset')) == 0
|
||||
|
||||
@ddt.data(*MODULESTORE_SETUPS)
|
||||
@ddt.data(SPLIT_MODULESTORE_SETUP)
|
||||
def test_asset_types_with_other_field_names(self, storebuilder):
|
||||
"""
|
||||
Test saving assets using an asset type of 'course_id'.
|
||||
@@ -289,7 +288,7 @@ class TestMongoAssetMetadataStorage(TestCase):
|
||||
all_assets = store.get_all_asset_metadata(course.id, 'course_id')
|
||||
assert all_assets[0].asset_id.path == new_asset_loc.path
|
||||
|
||||
@ddt.data(*MODULESTORE_SETUPS)
|
||||
@ddt.data(SPLIT_MODULESTORE_SETUP)
|
||||
def test_lock_unlock_assets(self, storebuilder):
|
||||
"""
|
||||
Save multiple metadata in each store and retrieve it singularly, as all assets, and after deleting all.
|
||||
@@ -337,7 +336,7 @@ class TestMongoAssetMetadataStorage(TestCase):
|
||||
('villain', 'Khan')
|
||||
)
|
||||
|
||||
@ddt.data(*MODULESTORE_SETUPS)
|
||||
@ddt.data(SPLIT_MODULESTORE_SETUP)
|
||||
def test_set_all_attrs(self, storebuilder):
|
||||
"""
|
||||
Save setting each attr one at a time
|
||||
@@ -356,7 +355,7 @@ class TestMongoAssetMetadataStorage(TestCase):
|
||||
assert getattr(updated_asset_md, attribute, None) is not None
|
||||
assert getattr(updated_asset_md, attribute, None) == value
|
||||
|
||||
@ddt.data(*MODULESTORE_SETUPS)
|
||||
@ddt.data(SPLIT_MODULESTORE_SETUP)
|
||||
def test_set_disallowed_attrs(self, storebuilder):
|
||||
"""
|
||||
setting disallowed attrs should fail
|
||||
@@ -382,7 +381,7 @@ class TestMongoAssetMetadataStorage(TestCase):
|
||||
else:
|
||||
assert updated_attr_val == original_attr_val
|
||||
|
||||
@ddt.data(*MODULESTORE_SETUPS)
|
||||
@ddt.data(SPLIT_MODULESTORE_SETUP)
|
||||
def test_set_unknown_attrs(self, storebuilder):
|
||||
"""
|
||||
setting unknown attrs should fail
|
||||
@@ -402,7 +401,7 @@ class TestMongoAssetMetadataStorage(TestCase):
|
||||
with pytest.raises(AttributeError):
|
||||
assert getattr(updated_asset_md, attribute) == value
|
||||
|
||||
@ddt.data(*MODULESTORE_SETUPS)
|
||||
@ddt.data(SPLIT_MODULESTORE_SETUP)
|
||||
def test_save_one_different_asset(self, storebuilder):
|
||||
"""
|
||||
saving and deleting things which are not 'asset'
|
||||
@@ -418,7 +417,7 @@ class TestMongoAssetMetadataStorage(TestCase):
|
||||
assert store.delete_asset_metadata(asset_key, ModuleStoreEnum.UserID.test) == 1
|
||||
assert len(store.get_all_asset_metadata(course.id, 'different')) == 0
|
||||
|
||||
@ddt.data(*MODULESTORE_SETUPS)
|
||||
@ddt.data(SPLIT_MODULESTORE_SETUP)
|
||||
def test_find_different(self, storebuilder):
|
||||
"""
|
||||
finding things which are of type other than 'asset'
|
||||
@@ -443,7 +442,7 @@ class TestMongoAssetMetadataStorage(TestCase):
|
||||
assert assets[idx].asset_id.asset_type == asset[0]
|
||||
assert assets[idx].asset_id.path == asset[1]
|
||||
|
||||
@ddt.data(*MODULESTORE_SETUPS)
|
||||
@ddt.data(SPLIT_MODULESTORE_SETUP)
|
||||
def test_get_multiple_types(self, storebuilder):
|
||||
"""
|
||||
getting all things which are of type other than 'asset'
|
||||
@@ -479,7 +478,7 @@ class TestMongoAssetMetadataStorage(TestCase):
|
||||
assert len(assets) == len(self.alls)
|
||||
self._check_asset_values(assets, self.alls)
|
||||
|
||||
@ddt.data(*MODULESTORE_SETUPS)
|
||||
@ddt.data(SPLIT_MODULESTORE_SETUP)
|
||||
def test_save_metadata_list(self, storebuilder):
|
||||
"""
|
||||
Save a list of asset metadata all at once.
|
||||
@@ -518,7 +517,7 @@ class TestMongoAssetMetadataStorage(TestCase):
|
||||
assert len(assets) == len(self.alls)
|
||||
self._check_asset_values(assets, self.alls)
|
||||
|
||||
@ddt.data(*MODULESTORE_SETUPS)
|
||||
@ddt.data(SPLIT_MODULESTORE_SETUP)
|
||||
def test_save_metadata_list_with_mismatched_asset(self, storebuilder):
|
||||
"""
|
||||
Save a list of asset metadata all at once - but with one asset's metadata from a different course.
|
||||
@@ -560,7 +559,7 @@ class TestMongoAssetMetadataStorage(TestCase):
|
||||
assert len(assets) == len(self.differents + self.vrmls)
|
||||
self._check_asset_values(assets, self.differents + self.vrmls)
|
||||
|
||||
@ddt.data(*MODULESTORE_SETUPS)
|
||||
@ddt.data(SPLIT_MODULESTORE_SETUP)
|
||||
def test_delete_all_different_type(self, storebuilder):
|
||||
"""
|
||||
deleting all assets of a given but not 'asset' type
|
||||
@@ -587,7 +586,7 @@ class TestMongoAssetMetadataStorage(TestCase):
|
||||
assert store.find_asset_metadata(asset_key) is None
|
||||
assert store.get_all_asset_metadata(course_key, 'asset') == []
|
||||
|
||||
@ddt.data(*MODULESTORE_SETUPS)
|
||||
@ddt.data(SPLIT_MODULESTORE_SETUP)
|
||||
def test_copy_all_assets_same_modulestore(self, storebuilder):
|
||||
"""
|
||||
Create a course with assets, copy them all to another course in the same modulestore, and check on it.
|
||||
@@ -607,7 +606,7 @@ class TestMongoAssetMetadataStorage(TestCase):
|
||||
assert all_assets[0].asset_id.path == 'pic1.jpg'
|
||||
assert all_assets[1].asset_id.path == 'shout.ogg'
|
||||
|
||||
@ddt.data(*MODULESTORE_SETUPS)
|
||||
@ddt.data(SPLIT_MODULESTORE_SETUP)
|
||||
def test_copy_all_assets_from_course_with_no_assets(self, storebuilder):
|
||||
"""
|
||||
Create a course with *no* assets, and try copy them all to another course in the same modulestore.
|
||||
@@ -621,30 +620,3 @@ class TestMongoAssetMetadataStorage(TestCase):
|
||||
all_assets = store.get_all_asset_metadata(
|
||||
course2.id, 'asset', sort=('displayname', ModuleStoreEnum.SortOrder.ascending)
|
||||
)
|
||||
assert len(all_assets) == 0
|
||||
|
||||
@ddt.data(
|
||||
('mongo', 'split'),
|
||||
('split', 'mongo'),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_copy_all_assets_cross_modulestore(self, from_store, to_store):
|
||||
"""
|
||||
Create a course with assets, copy them all to another course in a different modulestore, and check on it.
|
||||
"""
|
||||
mixed_builder = MIXED_MODULESTORE_BOTH_SETUP
|
||||
with mixed_builder.build() as (__, mixed_store):
|
||||
with mixed_store.default_store(from_store):
|
||||
course1 = CourseFactory.create(modulestore=mixed_store)
|
||||
with mixed_store.default_store(to_store):
|
||||
course2 = CourseFactory.create(modulestore=mixed_store)
|
||||
self.setup_assets(course1.id, None, mixed_store)
|
||||
assert len(mixed_store.get_all_asset_metadata(course1.id, 'asset')) == 2
|
||||
assert len(mixed_store.get_all_asset_metadata(course2.id, 'asset')) == 0
|
||||
mixed_store.copy_all_asset_metadata(course1.id, course2.id, ModuleStoreEnum.UserID.test * 102)
|
||||
all_assets = mixed_store.get_all_asset_metadata(
|
||||
course2.id, 'asset', sort=('displayname', ModuleStoreEnum.SortOrder.ascending)
|
||||
)
|
||||
assert len(all_assets) == 2
|
||||
assert all_assets[0].asset_id.path == 'pic1.jpg'
|
||||
assert all_assets[1].asset_id.path == 'shout.ogg'
|
||||
|
||||
@@ -26,7 +26,6 @@ from openedx.core.lib.tests import attr
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.tests.utils import (
|
||||
CONTENTSTORE_SETUPS,
|
||||
MODULESTORE_SETUPS,
|
||||
SPLIT_MODULESTORE_SETUP,
|
||||
TEST_DATA_DIR,
|
||||
MongoContentstoreBuilder,
|
||||
@@ -61,8 +60,8 @@ class CrossStoreXMLRoundtrip(CourseComparisonTest, PartitionTestCase):
|
||||
|
||||
@patch('xmodule.video_block.video_block.edxval_api', None)
|
||||
@ddt.data(*itertools.product(
|
||||
MODULESTORE_SETUPS,
|
||||
MODULESTORE_SETUPS,
|
||||
(SPLIT_MODULESTORE_SETUP,),
|
||||
(SPLIT_MODULESTORE_SETUP,),
|
||||
CONTENTSTORE_SETUPS,
|
||||
CONTENTSTORE_SETUPS,
|
||||
COURSE_DATA_NAMES,
|
||||
|
||||
@@ -49,14 +49,13 @@ from xmodule.modulestore.exceptions import (
|
||||
DuplicateCourseError,
|
||||
ItemNotFoundError,
|
||||
NoPathToItem,
|
||||
ReferentialIntegrityError
|
||||
)
|
||||
from xmodule.modulestore.inheritance import InheritanceMixin
|
||||
from xmodule.modulestore.mixed import MixedModuleStore
|
||||
from xmodule.modulestore.search import navigation_index, path_to_location
|
||||
from xmodule.modulestore.split_mongo.split import SplitMongoModuleStore
|
||||
from xmodule.modulestore.store_utilities import DETACHED_XBLOCK_TYPES
|
||||
from xmodule.modulestore.tests.factories import check_exact_number_of_calls, check_mongo_calls, mongo_uses_error_check
|
||||
from xmodule.modulestore.tests.factories import check_exact_number_of_calls, check_mongo_calls
|
||||
from xmodule.modulestore.tests.mongo_connection import MONGO_HOST, MONGO_PORT_NUM
|
||||
from xmodule.modulestore.tests.test_asides import AsideTestType
|
||||
from xmodule.modulestore.tests.utils import MongoContentstoreBuilder, create_modulestore_instance
|
||||
@@ -104,14 +103,14 @@ class CommonMixedModuleStoreSetup(CourseComparisonTest, OpenEdxEventsTestMixin):
|
||||
OPTIONS = {
|
||||
'stores': [
|
||||
{
|
||||
'NAME': ModuleStoreEnum.Type.mongo,
|
||||
'ENGINE': 'xmodule.modulestore.mongo.draft.DraftModuleStore',
|
||||
'NAME': ModuleStoreEnum.Type.split,
|
||||
'ENGINE': 'xmodule.modulestore.split_mongo.split_draft.DraftVersioningModuleStore',
|
||||
'DOC_STORE_CONFIG': DOC_STORE_CONFIG,
|
||||
'OPTIONS': modulestore_options
|
||||
},
|
||||
{
|
||||
'NAME': ModuleStoreEnum.Type.split,
|
||||
'ENGINE': 'xmodule.modulestore.split_mongo.split_draft.DraftVersioningModuleStore',
|
||||
'NAME': ModuleStoreEnum.Type.mongo,
|
||||
'ENGINE': 'xmodule.modulestore.mongo.draft.DraftModuleStore',
|
||||
'DOC_STORE_CONFIG': DOC_STORE_CONFIG,
|
||||
'OPTIONS': modulestore_options
|
||||
},
|
||||
@@ -332,7 +331,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
"""
|
||||
Tests of the MixedModulestore interface methods.
|
||||
"""
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_get_modulestore_type(self, default_ms):
|
||||
"""
|
||||
Make sure we get back the store type we expect for given mappings
|
||||
@@ -342,7 +341,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
# try an unknown mapping, it should be the 'default' store
|
||||
assert self.store.get_modulestore_type(CourseKey.from_string('foo/bar/2012_Fall')) == default_ms
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_get_modulestore_cache(self, default_ms):
|
||||
"""
|
||||
Make sure we cache discovered course mappings
|
||||
@@ -357,7 +356,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
assert self.store.default_modulestore == self.store._get_modulestore_for_courselike(course_key) # pylint: disable=protected-access, line-too-long
|
||||
|
||||
@ddt.data(*itertools.product(
|
||||
(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split),
|
||||
(ModuleStoreEnum.Type.split,),
|
||||
(True, False)
|
||||
))
|
||||
@ddt.unpack
|
||||
@@ -373,7 +372,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
with pytest.raises(DuplicateCourseError):
|
||||
self.store.create_course('org_x', 'course_y', 'run_z', self.user_id)
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.split, ModuleStoreEnum.Type.mongo)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_duplicate_course_error_with_different_case_ids(self, default_store):
|
||||
"""
|
||||
Verify that course can not be created with same course_id with different case.
|
||||
@@ -385,12 +384,9 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
with pytest.raises(DuplicateCourseError):
|
||||
self.store.create_course('ORG_X', 'COURSE_Y', 'RUN_Z', self.user_id)
|
||||
|
||||
# Draft:
|
||||
# problem: One lookup to locate an item that exists
|
||||
# fake: one w/ wildcard version
|
||||
# split: has one lookup for the course and then one for the course items
|
||||
# but the active_versions check is done in MySQL
|
||||
@ddt.data((ModuleStoreEnum.Type.mongo, [1, 1], 0), (ModuleStoreEnum.Type.split, [1, 1], 0))
|
||||
@ddt.data((ModuleStoreEnum.Type.split, [1, 1], 0))
|
||||
@ddt.unpack
|
||||
def test_has_item(self, default_ms, max_find, max_send):
|
||||
self.initdb(default_ms)
|
||||
@@ -407,13 +403,10 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
with pytest.raises(UnsupportedRevisionError):
|
||||
self.store.has_item(self.fake_location, revision=ModuleStoreEnum.RevisionOption.draft_preferred)
|
||||
|
||||
# draft queries:
|
||||
# problem: find draft item, find all items pertinent to inheritance computation, find parent
|
||||
# non-existent problem: find draft, find published
|
||||
# split:
|
||||
# problem: active_versions, structure
|
||||
# non-existent problem: ditto
|
||||
@ddt.data((ModuleStoreEnum.Type.mongo, [0, 0], [3, 2], 0), (ModuleStoreEnum.Type.split, [1, 0], [1, 1], 0))
|
||||
@ddt.data((ModuleStoreEnum.Type.split, [1, 0], [1, 1], 0))
|
||||
@ddt.unpack
|
||||
def test_get_item(self, default_ms, num_mysql, max_find, max_send):
|
||||
self.initdb(default_ms)
|
||||
@@ -431,12 +424,10 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
with pytest.raises(UnsupportedRevisionError):
|
||||
self.store.get_item(self.fake_location, revision=ModuleStoreEnum.RevisionOption.draft_preferred)
|
||||
|
||||
# Draft:
|
||||
# wildcard query, 6! load pertinent items for inheritance calls, load parents, course root fetch (why)
|
||||
# Split:
|
||||
# mysql: fetch course's active version from SplitModulestoreCourseIndex, spurious refetch x2
|
||||
# find: get structure
|
||||
@ddt.data((ModuleStoreEnum.Type.mongo, 0, 14, 0), (ModuleStoreEnum.Type.split, 2, 1, 0))
|
||||
@ddt.data((ModuleStoreEnum.Type.split, 2, 1, 0))
|
||||
@ddt.unpack
|
||||
def test_get_items(self, default_ms, num_mysql, max_find, max_send):
|
||||
self.initdb(default_ms)
|
||||
@@ -454,7 +445,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
revision=ModuleStoreEnum.RevisionOption.draft_preferred
|
||||
)
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_course_version_on_block(self, default_ms):
|
||||
self.initdb(default_ms)
|
||||
self._create_block_hierarchy()
|
||||
@@ -477,7 +468,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
cached_block = course.runtime.get_block(block.location)
|
||||
assert cached_block.course_version == block.course_version
|
||||
|
||||
@ddt.data((ModuleStoreEnum.Type.split, 2, False), (ModuleStoreEnum.Type.mongo, 3, True))
|
||||
@ddt.data((ModuleStoreEnum.Type.split, 2, False))
|
||||
@ddt.unpack
|
||||
def test_get_items_include_orphans(self, default_ms, expected_items_in_tree, orphan_in_items):
|
||||
"""
|
||||
@@ -537,16 +528,12 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
assert (orphan in [item.location for item in items_in_tree]) == orphan_in_items
|
||||
assert len(items_in_tree) == expected_items_in_tree
|
||||
|
||||
# draft:
|
||||
# mysql: check CONTENT_TAGGING_AUTO CourseWaffleFlag
|
||||
# find: get draft, get ancestors up to course (2-6), compute inheritance
|
||||
# sends: update problem and then each ancestor up to course (edit info)
|
||||
# split:
|
||||
# mysql: SplitModulestoreCourseIndex - select 2x (by course_id, by objectid), update, update historical record,
|
||||
# check CONTENT_TAGGING_AUTO CourseWaffleFlag
|
||||
# find: definitions (calculator field), structures
|
||||
# sends: 2 sends to update index & structure (note, it would also be definition if a content field changed)
|
||||
@ddt.data((ModuleStoreEnum.Type.mongo, 1, 6, 5), (ModuleStoreEnum.Type.split, 4, 2, 2))
|
||||
@ddt.data((ModuleStoreEnum.Type.split, 4, 2, 2))
|
||||
@ddt.unpack
|
||||
def test_update_item(self, default_ms, num_mysql, max_find, max_send):
|
||||
"""
|
||||
@@ -563,7 +550,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
|
||||
assert problem.max_attempts == 2, "Update didn't persist"
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_has_changes_direct_only(self, default_ms):
|
||||
"""
|
||||
Tests that has_changes() returns false when a new xblock in a direct only category is checked
|
||||
@@ -584,7 +571,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
assert not self.store.has_changes(test_course)
|
||||
assert not self.store.has_changes(chapter)
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_has_changes(self, default_ms):
|
||||
"""
|
||||
Tests that has_changes() only returns true when changes are present
|
||||
@@ -619,7 +606,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
component = self.store.publish(component.location, self.user_id)
|
||||
assert not self.store.has_changes(component)
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_unit_stuck_in_draft_mode(self, default_ms):
|
||||
"""
|
||||
After revert_to_published() the has_changes() should return false if draft has no changes
|
||||
@@ -651,7 +638,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
component = self.store.publish(component.location, self.user_id)
|
||||
assert not self.store.has_changes(component)
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_unit_stuck_in_published_mode(self, default_ms):
|
||||
"""
|
||||
After revert_to_published() the has_changes() should return true if draft has changes
|
||||
@@ -688,7 +675,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
# Verify that changes are present
|
||||
assert self.store.has_changes(component)
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_unit_stuck_in_published_mode_after_delete(self, default_ms):
|
||||
"""
|
||||
Test that a unit does not get stuck in published mode
|
||||
@@ -731,7 +718,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
vertical = self.store.get_item(vertical.location)
|
||||
assert self._has_changes(vertical.location)
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_publish_automatically_after_delete_unit(self, default_ms):
|
||||
"""
|
||||
Check that sequential publishes automatically after deleting a unit
|
||||
@@ -752,7 +739,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
self.store.delete_item(vertical.location, self.user_id)
|
||||
assert not self._has_changes(sequential.location)
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_course_create_event(self, default_ms):
|
||||
"""
|
||||
Check that COURSE_CREATED event is sent when a course is created.
|
||||
@@ -777,7 +764,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
event_receiver.call_args.kwargs
|
||||
)
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_xblock_create_event(self, default_ms):
|
||||
"""
|
||||
Check that XBLOCK_CREATED event is sent when xblock is created.
|
||||
@@ -799,7 +786,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
assert event_receiver.call_args.kwargs['xblock_info'].block_type == sequential.location.block_type
|
||||
assert event_receiver.call_args.kwargs['xblock_info'].version.for_branch(None) == sequential.location
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_xblock_update_event(self, default_ms):
|
||||
"""
|
||||
Check that XBLOCK_UPDATED event is sent when xblock is updated.
|
||||
@@ -825,7 +812,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
assert event_receiver.call_args.kwargs['xblock_info'].block_type == sequential.location.block_type
|
||||
assert event_receiver.call_args.kwargs['xblock_info'].version.for_branch(None) == sequential.location
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_xblock_publish_event(self, default_ms):
|
||||
"""
|
||||
Check that XBLOCK_PUBLISHED event is sent when xblock is published.
|
||||
@@ -856,7 +843,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
event_receiver.call_args.kwargs
|
||||
)
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_xblock_delete_event(self, default_ms):
|
||||
"""
|
||||
Check that XBLOCK_DELETED event is sent when xblock is deleted.
|
||||
@@ -912,7 +899,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
|
||||
return locations
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_has_changes_ancestors(self, default_ms):
|
||||
"""
|
||||
Tests that has_changes() returns true on ancestors when a child is changed
|
||||
@@ -942,7 +929,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
for key in locations:
|
||||
assert not self._has_changes(locations[key])
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_has_changes_publish_ancestors(self, default_ms):
|
||||
"""
|
||||
Tests that has_changes() returns false after a child is published only if all children are unchanged
|
||||
@@ -979,7 +966,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
assert not self._has_changes(locations['grandparent'])
|
||||
assert not self._has_changes(locations['parent'])
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_has_changes_add_remove_child(self, default_ms):
|
||||
"""
|
||||
Tests that has_changes() returns true for the parent when a child with changes is added
|
||||
@@ -1012,7 +999,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
assert not self._has_changes(locations['grandparent'])
|
||||
assert not self._has_changes(locations['parent'])
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_has_changes_non_direct_only_children(self, default_ms):
|
||||
"""
|
||||
Tests that has_changes() returns true after editing the child of a vertical (both not direct only categories).
|
||||
@@ -1046,7 +1033,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
assert self._has_changes(child.location)
|
||||
|
||||
@ddt.data(*itertools.product(
|
||||
(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split),
|
||||
(ModuleStoreEnum.Type.split,),
|
||||
(ModuleStoreEnum.Branch.draft_preferred, ModuleStoreEnum.Branch.published_only)
|
||||
))
|
||||
@ddt.unpack
|
||||
@@ -1070,27 +1057,18 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
# Check the parent for changes should return True and not throw an exception
|
||||
assert self.store.has_changes(parent)
|
||||
|
||||
# Draft
|
||||
# mysql: check CONTENT_TAGGING_AUTO CourseWaffleFlag
|
||||
# Find: find parents (definition.children query), get parent, get course (fill in run?),
|
||||
# find parents of the parent (course), get inheritance items,
|
||||
# get item (to delete subtree), get inheritance again.
|
||||
# Sends: delete item, update parent
|
||||
# Split
|
||||
# mysql: SplitModulestoreCourseIndex - select 2x (by course_id, by objectid), update, update historical record,
|
||||
# check CONTENT_TAGGING_AUTO CourseWaffleFlag
|
||||
# Find: active_versions, 2 structures (published & draft), definition (unnecessary)
|
||||
# Sends: updated draft and published structures and active_versions
|
||||
@ddt.data((ModuleStoreEnum.Type.mongo, 1, 6, 2), (ModuleStoreEnum.Type.split, 5, 2, 3))
|
||||
@ddt.data((ModuleStoreEnum.Type.split, 5, 2, 3))
|
||||
@ddt.unpack
|
||||
def test_delete_item(self, default_ms, num_mysql, max_find, max_send):
|
||||
"""
|
||||
Delete should reject on r/o db and work on r/w one
|
||||
"""
|
||||
self.initdb(default_ms)
|
||||
if default_ms == ModuleStoreEnum.Type.mongo and mongo_uses_error_check(self.store):
|
||||
max_find += 1
|
||||
|
||||
with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred, self.writable_chapter_location.course_key): # lint-amnesty, pylint: disable=line-too-long
|
||||
with check_mongo_calls(max_find, max_send), self.assertNumQueries(num_mysql):
|
||||
self.store.delete_item(self.writable_chapter_location, self.user_id)
|
||||
@@ -1102,17 +1080,12 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
with pytest.raises(ItemNotFoundError):
|
||||
self.store.get_item(self.writable_chapter_location, revision=ModuleStoreEnum.RevisionOption.published_only)
|
||||
|
||||
# Draft:
|
||||
# mysql: check CONTENT_TAGGING_AUTO CourseWaffleFlag
|
||||
# find: find parent (definition.children), count versions of item, get parent, count grandparents,
|
||||
# inheritance items, draft item, draft child, inheritance
|
||||
# sends: delete draft vertical and update parent
|
||||
# Split:
|
||||
# mysql: SplitModulestoreCourseIndex - select 2x (by course_id, by objectid), update, update historical record,
|
||||
# check CONTENT_TAGGING_AUTO CourseWaffleFlag
|
||||
# find: draft and published structures, definition (unnecessary)
|
||||
# sends: update published (why?), draft, and active_versions
|
||||
@ddt.data((ModuleStoreEnum.Type.mongo, 1, 8, 2), (ModuleStoreEnum.Type.split, 5, 3, 3))
|
||||
@ddt.data((ModuleStoreEnum.Type.split, 5, 3, 3))
|
||||
@ddt.unpack
|
||||
def test_delete_private_vertical(self, default_ms, num_mysql, max_find, max_send):
|
||||
"""
|
||||
@@ -1120,8 +1093,6 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
behavioral properties which this deletion test gets at.
|
||||
"""
|
||||
self.initdb(default_ms)
|
||||
if default_ms == ModuleStoreEnum.Type.mongo and mongo_uses_error_check(self.store):
|
||||
max_find += 1
|
||||
# create and delete a private vertical with private children
|
||||
private_vert = self.store.create_child(
|
||||
# don't use course_location as it may not be the repr
|
||||
@@ -1159,16 +1130,12 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
assert not self.store.has_item(leaf_loc)
|
||||
assert vert_loc not in course.children
|
||||
|
||||
# Draft:
|
||||
# mysql: check CONTENT_TAGGING_AUTO CourseWaffleFlag
|
||||
# find: find parent (definition.children) 2x, find draft item, get inheritance items
|
||||
# send: one delete query for specific item
|
||||
# Split:
|
||||
# mysql: SplitModulestoreCourseIndex - select 2x (by course_id, by objectid), update, update historical record,
|
||||
# check CONTENT_TAGGING_AUTO CourseWaffleFlag
|
||||
# find: structure (cached)
|
||||
# send: update structure and active_versions
|
||||
@ddt.data((ModuleStoreEnum.Type.mongo, 1, 3, 1), (ModuleStoreEnum.Type.split, 5, 1, 2))
|
||||
@ddt.data((ModuleStoreEnum.Type.split, 5, 1, 2))
|
||||
@ddt.unpack
|
||||
def test_delete_draft_vertical(self, default_ms, num_mysql, max_find, max_send):
|
||||
"""
|
||||
@@ -1198,20 +1165,15 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
private_leaf.display_name = 'change me'
|
||||
private_leaf = self.store.update_item(private_leaf, self.user_id)
|
||||
# test succeeds if delete succeeds w/o error
|
||||
if default_ms == ModuleStoreEnum.Type.mongo and mongo_uses_error_check(self.store):
|
||||
max_find += 1
|
||||
with check_mongo_calls(max_find, max_send), self.assertNumQueries(num_mysql):
|
||||
self.store.delete_item(private_leaf.location, self.user_id)
|
||||
|
||||
# Draft:
|
||||
# mysql: 1 select on SplitModulestoreCourseIndex since this searches both modulestores
|
||||
# find: 1 find all courses (wildcard), 1 find to get each course 1 at a time (1 course)
|
||||
# Split:
|
||||
# mysql: 3 selects on SplitModulestoreCourseIndex - 1 to get all courses, 2 to get specific course (this query is
|
||||
# executed twice, possibly unnecessarily)
|
||||
# find: 2 reads of structure, definition (s/b lazy; so, unnecessary),
|
||||
# plus 1 wildcard find in draft mongo which has none
|
||||
@ddt.data((ModuleStoreEnum.Type.mongo, 1, 3, 0), (ModuleStoreEnum.Type.split, 2, 3, 0))
|
||||
@ddt.data((ModuleStoreEnum.Type.split, 2, 3, 0))
|
||||
@ddt.unpack
|
||||
def test_get_courses(self, default_ms, num_mysql, max_find, max_send):
|
||||
self.initdb(default_ms)
|
||||
@@ -1228,7 +1190,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
published_courses = self.store.get_courses(remove_branch=True)
|
||||
assert [c.id for c in draft_courses] == [c.id for c in published_courses]
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_create_child_detached_tabs(self, default_ms):
|
||||
"""
|
||||
test 'create_child' method with a detached category ('static_tab')
|
||||
@@ -1249,9 +1211,8 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
mongo_course = self.store.get_course(self.course_locations[self.MONGO_COURSEID].course_key)
|
||||
assert len(mongo_course.children) == 1
|
||||
|
||||
# draft is 2: find out which ms owns course, get item
|
||||
# split: active_versions (mysql), structure, definition (to load course wiki string)
|
||||
@ddt.data((ModuleStoreEnum.Type.mongo, 0, 3, 0), (ModuleStoreEnum.Type.split, 1, 2, 0))
|
||||
@ddt.data((ModuleStoreEnum.Type.split, 1, 2, 0))
|
||||
@ddt.unpack
|
||||
def test_get_course(self, default_ms, num_mysql, max_find, max_send):
|
||||
"""
|
||||
@@ -1263,7 +1224,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
course = self.store.get_item(self.course_locations[self.MONGO_COURSEID])
|
||||
assert course.id == self.course_locations[self.MONGO_COURSEID].course_key
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_get_library(self, default_ms):
|
||||
"""
|
||||
Test that create_library and get_library work regardless of the default modulestore.
|
||||
@@ -1286,9 +1247,8 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
|
||||
# notice this doesn't test getting a public item via draft_preferred which draft would have 2 hits (split
|
||||
# still only 2)
|
||||
# Draft: get_parent
|
||||
# Split: active_versions, structure
|
||||
@ddt.data((ModuleStoreEnum.Type.mongo, 0, 1, 0), (ModuleStoreEnum.Type.split, 1, 1, 0))
|
||||
@ddt.data((ModuleStoreEnum.Type.split, 1, 1, 0))
|
||||
@ddt.unpack
|
||||
def test_get_parent_locations(self, default_ms, num_mysql, max_find, max_send):
|
||||
"""
|
||||
@@ -1354,7 +1314,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
assert item_location in expected_parent.children
|
||||
assert item_location not in old_parent.children
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_update_item_parent(self, store_type):
|
||||
"""
|
||||
Test that when we move an item from old to new parent, the item should be present in new parent.
|
||||
@@ -1380,7 +1340,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
old_parent_location=old_parent_location
|
||||
)
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_move_revert(self, store_type):
|
||||
"""
|
||||
Test that when we move an item to new parent and then discard the original parent, the item should be present
|
||||
@@ -1417,7 +1377,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
is_reverted=True
|
||||
)
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_move_delete_revert(self, store_type):
|
||||
"""
|
||||
Test that when we move an item and delete it and then discard changes for original parent, item should be
|
||||
@@ -1457,7 +1417,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
is_reverted=True
|
||||
)
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_move_revert_move(self, store_type):
|
||||
"""
|
||||
Test that when we move an item to new parent and discard changes for the old parent, then the item should be
|
||||
@@ -1507,7 +1467,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
old_parent_location=old_parent_location
|
||||
)
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_move_edited_revert(self, store_type):
|
||||
"""
|
||||
Test that when we move an edited item from old parent to new parent and then discard changes in old parent,
|
||||
@@ -1553,7 +1513,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
reverted_problem = self.store.get_item(self.problem_x1a_1) # lint-amnesty, pylint: disable=no-member
|
||||
assert orig_display_name == reverted_problem.display_name
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_move_1_moved_1_unchanged(self, store_type):
|
||||
"""
|
||||
Test that when we move an item from an old parent which have multiple items then only moved item's parent
|
||||
@@ -1589,7 +1549,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
assert problem_item2.parent == self.vertical_x1a # lint-amnesty, pylint: disable=no-member
|
||||
assert problem_item2.location in problem_item2.get_parent().children
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_move_1_moved_1_edited(self, store_type):
|
||||
"""
|
||||
Test that when we move an item inside an old parent having multiple items, we edit one item and move
|
||||
@@ -1636,7 +1596,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
reverted_problem2 = self.store.get_item(problem_item2.location)
|
||||
assert orig_display_name == reverted_problem2.display_name
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_move_1_moved_1_deleted(self, store_type):
|
||||
"""
|
||||
Test that when we move an item inside an old parent having multiple items, we delete one item and move
|
||||
@@ -1681,7 +1641,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
assert problem_item2.parent == self.vertical_x1a # lint-amnesty, pylint: disable=no-member
|
||||
assert problem_item2.location in problem_item2.get_parent().children
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_get_parent_locations_moved_child(self, default_ms):
|
||||
self.initdb(default_ms)
|
||||
self._create_block_hierarchy()
|
||||
@@ -1732,81 +1692,8 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
(child_to_move_location, new_parent_published_location, ModuleStoreEnum.RevisionOption.published_only),
|
||||
])
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo)
|
||||
def test_get_parent_locations_deleted_child(self, default_ms):
|
||||
self.initdb(default_ms)
|
||||
self._create_block_hierarchy()
|
||||
|
||||
# publish the course
|
||||
self.store.publish(self.course.location, self.user_id)
|
||||
|
||||
# make draft of vertical
|
||||
self.store.convert_to_draft(self.vertical_y1a, self.user_id) # lint-amnesty, pylint: disable=no-member
|
||||
|
||||
# delete child problem_y1a_1
|
||||
child_to_delete_location = self.problem_y1a_1 # lint-amnesty, pylint: disable=no-member
|
||||
old_parent_location = self.vertical_y1a # lint-amnesty, pylint: disable=no-member
|
||||
self.store.delete_item(child_to_delete_location, self.user_id)
|
||||
|
||||
self.verify_get_parent_locations_results([
|
||||
(child_to_delete_location, old_parent_location, None),
|
||||
# Note: The following could be an unexpected result, but we want to avoid an extra database call
|
||||
(child_to_delete_location, old_parent_location, ModuleStoreEnum.RevisionOption.draft_preferred),
|
||||
(child_to_delete_location, old_parent_location, ModuleStoreEnum.RevisionOption.published_only),
|
||||
])
|
||||
|
||||
# publish the course again
|
||||
self.store.publish(self.course.location, self.user_id)
|
||||
self.verify_get_parent_locations_results([
|
||||
(child_to_delete_location, None, None),
|
||||
(child_to_delete_location, None, ModuleStoreEnum.RevisionOption.draft_preferred),
|
||||
(child_to_delete_location, None, ModuleStoreEnum.RevisionOption.published_only),
|
||||
])
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo)
|
||||
def test_get_parent_location_draft(self, default_ms):
|
||||
"""
|
||||
Test that "get_parent_location" method returns first published parent
|
||||
for a draft component, if it has many possible parents (including
|
||||
draft parents).
|
||||
"""
|
||||
self.initdb(default_ms)
|
||||
course_id = self.course_locations[self.MONGO_COURSEID].course_key
|
||||
|
||||
# create parented children
|
||||
self._create_block_hierarchy()
|
||||
self.store.publish(self.course.location, self.user_id)
|
||||
|
||||
mongo_store = self.store._get_modulestore_for_courselike(course_id) # pylint: disable=protected-access
|
||||
# add another parent (unit) "vertical_x1b" for problem "problem_x1a_1"
|
||||
mongo_store.collection.update_one(
|
||||
self.vertical_x1b.to_deprecated_son('_id.'), # lint-amnesty, pylint: disable=no-member
|
||||
{'$push': {'definition.children': str(self.problem_x1a_1)}} # lint-amnesty, pylint: disable=no-member
|
||||
)
|
||||
|
||||
# convert first parent (unit) "vertical_x1a" of problem "problem_x1a_1" to draft
|
||||
self.store.convert_to_draft(self.vertical_x1a, self.user_id) # lint-amnesty, pylint: disable=no-member
|
||||
item = self.store.get_item(self.vertical_x1a) # lint-amnesty, pylint: disable=no-member
|
||||
assert self.store.has_published_version(item)
|
||||
|
||||
# now problem "problem_x1a_1" has 3 parents [vertical_x1a (draft),
|
||||
# vertical_x1a (published), vertical_x1b (published)]
|
||||
# check that "get_parent_location" method of draft branch returns first
|
||||
# published parent "vertical_x1a" without raising "AssertionError" for
|
||||
# problem location revision
|
||||
with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred, course_id):
|
||||
parent = mongo_store.get_parent_location(self.problem_x1a_1) # lint-amnesty, pylint: disable=no-member
|
||||
assert parent.for_branch(None) == self.vertical_x1a # lint-amnesty, pylint: disable=no-member
|
||||
|
||||
# Draft:
|
||||
# Problem path:
|
||||
# 1. Get problem
|
||||
# 2-6. get parent and rest of ancestors up to course
|
||||
# 7-8. get sequential, compute inheritance
|
||||
# 8-9. get vertical, compute inheritance
|
||||
# 10-11. get other vertical_x1b (why?) and compute inheritance
|
||||
# Split: loading structure from mongo (also loads active version from MySQL, not tracked here)
|
||||
@ddt.data((ModuleStoreEnum.Type.mongo, [0, 0], [15, 3], 0), (ModuleStoreEnum.Type.split, [1, 0], [2, 1], 0))
|
||||
@ddt.data((ModuleStoreEnum.Type.split, [1, 0], [2, 1], 0))
|
||||
@ddt.unpack
|
||||
def test_path_to_location(self, default_ms, num_mysql, num_finds, num_sends):
|
||||
"""
|
||||
@@ -1863,7 +1750,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
assert 5 == navigation_index('5_2')
|
||||
assert 7 == navigation_index('7_3_5_6_')
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_revert_to_published_root_draft(self, default_ms):
|
||||
"""
|
||||
Test calling revert_to_published on draft vertical.
|
||||
@@ -1895,7 +1782,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
self.assertBlocksEqualByFields(reverted_parent, published_parent)
|
||||
assert not self._has_changes(self.vertical_x1a) # lint-amnesty, pylint: disable=no-member
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_revert_to_published_root_published(self, default_ms):
|
||||
"""
|
||||
Test calling revert_to_published on a published vertical with a draft child.
|
||||
@@ -1915,7 +1802,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
reverted_problem = self.store.get_item(self.problem_x1a_1) # lint-amnesty, pylint: disable=no-member
|
||||
assert orig_display_name == reverted_problem.display_name
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_revert_to_published_no_draft(self, default_ms):
|
||||
"""
|
||||
Test calling revert_to_published on vertical with no draft content does nothing.
|
||||
@@ -1930,7 +1817,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
|
||||
self.assertBlocksEqualByFields(orig_vertical, reverted_vertical)
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_revert_to_published_no_published(self, default_ms):
|
||||
"""
|
||||
Test calling revert_to_published on vertical with no published version errors.
|
||||
@@ -1940,7 +1827,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
with pytest.raises(InvalidVersionError):
|
||||
self.store.revert_to_published(self.vertical_x1a, self.user_id) # lint-amnesty, pylint: disable=no-member
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_revert_to_published_direct_only(self, default_ms):
|
||||
"""
|
||||
Test calling revert_to_published on a direct-only item is a no-op.
|
||||
@@ -2044,9 +1931,8 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
return store
|
||||
assert False, "SplitMongoModuleStore was not found in MixedModuleStore"
|
||||
|
||||
# Draft: get all items which can be or should have parents
|
||||
# Split: active_versions (mysql), structure (mongo)
|
||||
@ddt.data((ModuleStoreEnum.Type.mongo, 0, 1, 0), (ModuleStoreEnum.Type.split, 1, 1, 0))
|
||||
@ddt.data((ModuleStoreEnum.Type.split, 1, 1, 0))
|
||||
@ddt.unpack
|
||||
def test_get_orphans(self, default_ms, num_mysql, max_find, max_send):
|
||||
"""
|
||||
@@ -2084,82 +1970,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
found_orphans = self.store.get_orphans(self.course_locations[self.MONGO_COURSEID].course_key)
|
||||
self.assertCountEqual(found_orphans, orphan_locations)
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo)
|
||||
def test_get_non_orphan_parents(self, default_ms):
|
||||
"""
|
||||
Test finding non orphan parents from many possible parents.
|
||||
"""
|
||||
self.initdb(default_ms)
|
||||
course_id = self.course_locations[self.MONGO_COURSEID].course_key
|
||||
|
||||
# create parented children
|
||||
self._create_block_hierarchy()
|
||||
self.store.publish(self.course.location, self.user_id)
|
||||
|
||||
# test that problem "problem_x1a_1" has only one published parent
|
||||
mongo_store = self.store._get_modulestore_for_courselike(course_id) # pylint: disable=protected-access
|
||||
with self.store.branch_setting(ModuleStoreEnum.Branch.published_only, course_id):
|
||||
parent = mongo_store.get_parent_location(self.problem_x1a_1) # lint-amnesty, pylint: disable=no-member
|
||||
assert parent == self.vertical_x1a # lint-amnesty, pylint: disable=no-member
|
||||
|
||||
# add some published orphans
|
||||
orphan_sequential = course_id.make_usage_key('sequential', 'OrphanSequential')
|
||||
orphan_vertical = course_id.make_usage_key('vertical', 'OrphanVertical')
|
||||
orphan_locations = [orphan_sequential, orphan_vertical]
|
||||
for location in orphan_locations:
|
||||
self.store.create_item(
|
||||
self.user_id,
|
||||
location.course_key,
|
||||
location.block_type,
|
||||
block_id=location.block_id
|
||||
)
|
||||
self.store.publish(location, self.user_id)
|
||||
|
||||
found_orphans = mongo_store.get_orphans(course_id)
|
||||
assert set(found_orphans) == set(orphan_locations)
|
||||
assert len(set(found_orphans)) == 2
|
||||
|
||||
# add orphan vertical and sequential as another parents of problem "problem_x1a_1"
|
||||
mongo_store.collection.update_one(
|
||||
orphan_sequential.to_deprecated_son('_id.'),
|
||||
{'$push': {'definition.children': str(self.problem_x1a_1)}} # lint-amnesty, pylint: disable=no-member
|
||||
)
|
||||
mongo_store.collection.update_one(
|
||||
orphan_vertical.to_deprecated_son('_id.'),
|
||||
{'$push': {'definition.children': str(self.problem_x1a_1)}} # lint-amnesty, pylint: disable=no-member
|
||||
)
|
||||
# test that "get_parent_location" method of published branch still returns the correct non-orphan parent for
|
||||
# problem "problem_x1a_1" since the two other parents are orphans
|
||||
with self.store.branch_setting(ModuleStoreEnum.Branch.published_only, course_id):
|
||||
parent = mongo_store.get_parent_location(self.problem_x1a_1) # lint-amnesty, pylint: disable=no-member
|
||||
assert parent == self.vertical_x1a # lint-amnesty, pylint: disable=no-member
|
||||
|
||||
# now add valid published vertical as another parent of problem
|
||||
mongo_store.collection.update_one(self.sequential_x1.to_deprecated_son('_id.'), {'$push': {'definition.children': str(self.problem_x1a_1)}}) # lint-amnesty, pylint: disable=no-member, line-too-long
|
||||
# now check that "get_parent_location" method of published branch raises "ReferentialIntegrityError" for
|
||||
# problem "problem_x1a_1" since it has now 2 valid published parents
|
||||
with self.store.branch_setting(ModuleStoreEnum.Branch.published_only, course_id):
|
||||
assert self.store.has_item(self.problem_x1a_1) # lint-amnesty, pylint: disable=no-member
|
||||
with pytest.raises(ReferentialIntegrityError):
|
||||
self.store.get_parent_location(self.problem_x1a_1) # lint-amnesty, pylint: disable=no-member
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo)
|
||||
def test_create_item_from_parent_location(self, default_ms):
|
||||
"""
|
||||
Test a code path missed by the above: passing an old-style location as parent but no
|
||||
new location for the child
|
||||
"""
|
||||
self.initdb(default_ms)
|
||||
self.store.create_child(
|
||||
self.user_id,
|
||||
self.course_locations[self.MONGO_COURSEID],
|
||||
'problem',
|
||||
block_id='orphan'
|
||||
)
|
||||
orphans = self.store.get_orphans(self.course_locations[self.MONGO_COURSEID].course_key)
|
||||
assert len(orphans) == 0, f'unexpected orphans: {orphans}'
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_create_item_populates_edited_info(self, default_ms):
|
||||
self.initdb(default_ms)
|
||||
block = self.store.create_item(
|
||||
@@ -2170,7 +1981,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
assert self.user_id == block.edited_by
|
||||
assert datetime.datetime.now(UTC) > block.edited_on
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_create_item_populates_subtree_edited_info(self, default_ms):
|
||||
self.initdb(default_ms)
|
||||
block = self.store.create_item(
|
||||
@@ -2181,9 +1992,8 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
assert self.user_id == block.subtree_edited_by
|
||||
assert datetime.datetime.now(UTC) > block.subtree_edited_on
|
||||
|
||||
# Draft: wildcard search of draft (find) and split (mysql)
|
||||
# Split: wildcard search of draft (find) and split (mysql)
|
||||
@ddt.data((ModuleStoreEnum.Type.mongo, 1, 1, 0), (ModuleStoreEnum.Type.split, 1, 1, 0))
|
||||
@ddt.data((ModuleStoreEnum.Type.split, 1, 1, 0))
|
||||
@ddt.unpack
|
||||
def test_get_courses_for_wiki(self, default_ms, num_mysql, max_find, max_send):
|
||||
"""
|
||||
@@ -2199,12 +2009,6 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
assert len(self.store.get_courses_for_wiki('edX.simple.2012_Fall')) == 0
|
||||
assert len(self.store.get_courses_for_wiki('no_such_wiki')) == 0
|
||||
|
||||
# Draft:
|
||||
# Find: find vertical, find children
|
||||
# Sends:
|
||||
# 1. delete all of the published nodes in subtree
|
||||
# 2. insert vertical as published (deleted in step 1) w/ the deleted problems as children
|
||||
# 3-6. insert the 3 problems and 1 html as published
|
||||
# Split:
|
||||
# MySQL SplitModulestoreCourseIndex:
|
||||
# 1. Select by course ID
|
||||
@@ -2214,15 +2018,13 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
# Sends:
|
||||
# 1. insert structure
|
||||
# 2. write index entry
|
||||
@ddt.data((ModuleStoreEnum.Type.mongo, 0, 2, 6), (ModuleStoreEnum.Type.split, 4, 2, 2))
|
||||
@ddt.data((ModuleStoreEnum.Type.split, 4, 2, 2))
|
||||
@ddt.unpack
|
||||
def test_unpublish(self, default_ms, num_mysql, max_find, max_send):
|
||||
"""
|
||||
Test calling unpublish
|
||||
"""
|
||||
self.initdb(default_ms)
|
||||
if default_ms == ModuleStoreEnum.Type.mongo and mongo_uses_error_check(self.store):
|
||||
max_find += 1
|
||||
self._create_block_hierarchy()
|
||||
|
||||
# publish
|
||||
@@ -2250,9 +2052,8 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
)
|
||||
assert draft_xblock is not None
|
||||
|
||||
# Draft: specific query for revision None
|
||||
# Split: active_versions from MySQL, structure from mongo
|
||||
@ddt.data((ModuleStoreEnum.Type.mongo, 0, 1, 0), (ModuleStoreEnum.Type.split, 1, 1, 0))
|
||||
@ddt.data((ModuleStoreEnum.Type.split, 1, 1, 0))
|
||||
@ddt.unpack
|
||||
def test_has_published_version(self, default_ms, mysql_queries, max_find, max_send):
|
||||
"""
|
||||
@@ -2293,7 +2094,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
assert self.store.has_changes(item)
|
||||
assert self.store.has_published_version(item)
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_update_edit_info_ancestors(self, default_ms):
|
||||
"""
|
||||
Tests that edited_on, edited_by, subtree_edited_on, and subtree_edited_by are set correctly during update
|
||||
@@ -2369,7 +2170,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
# Verify that others have unchanged edit info
|
||||
check_node(sibling.location, None, after_create, self.user_id, None, after_create, self.user_id)
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_update_edit_info(self, default_ms):
|
||||
"""
|
||||
Tests that edited_on and edited_by are set correctly during an update
|
||||
@@ -2399,7 +2200,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
assert old_edited_on < updated_component.edited_on
|
||||
assert updated_component.edited_by == edit_user
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_update_published_info(self, default_ms):
|
||||
"""
|
||||
Tests that published_on and published_by are set correctly
|
||||
@@ -2433,7 +2234,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
assert old_time <= updated_component.published_on
|
||||
assert updated_component.published_by == publish_user
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_auto_publish(self, default_ms):
|
||||
"""
|
||||
Test that the correct things have been published automatically
|
||||
@@ -2494,7 +2295,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
chapter = self.store.get_item(chapter.location.for_branch(None))
|
||||
assert self.store.has_published_version(chapter)
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_get_courses_for_wiki_shared(self, default_ms):
|
||||
"""
|
||||
Test two courses sharing the same wiki
|
||||
@@ -2532,7 +2333,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
assert len(wiki_courses) == 0
|
||||
assert self.course_locations[self.MONGO_COURSEID].course_key.replace(branch=None) not in wiki_courses
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_branch_setting(self, default_ms):
|
||||
"""
|
||||
Test the branch_setting context manager
|
||||
@@ -2634,7 +2435,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
course = self.store.create_course("org", "course{}".format(uuid4().hex[:5]), "run", self.user_id)
|
||||
assert course.runtime.modulestore.get_modulestore_type() == store_type
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_default_store(self, default_ms):
|
||||
"""
|
||||
Test the default store context manager
|
||||
@@ -2645,19 +2446,6 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
with self.store.default_store(default_ms):
|
||||
self.verify_default_store(default_ms)
|
||||
|
||||
def test_default_store_nested(self):
|
||||
"""
|
||||
Test the default store context manager, nested within one another
|
||||
"""
|
||||
# initialize the mixed modulestore
|
||||
self._initialize_mixed(mappings={})
|
||||
|
||||
with self.store.default_store(ModuleStoreEnum.Type.mongo):
|
||||
self.verify_default_store(ModuleStoreEnum.Type.mongo)
|
||||
with self.store.default_store(ModuleStoreEnum.Type.split):
|
||||
self.verify_default_store(ModuleStoreEnum.Type.split)
|
||||
self.verify_default_store(ModuleStoreEnum.Type.mongo)
|
||||
|
||||
def test_default_store_fake(self):
|
||||
"""
|
||||
Test the default store context manager, asking for a fake store
|
||||
@@ -2681,7 +2469,6 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
self.store.contentstore.save(content)
|
||||
|
||||
@ddt.data(
|
||||
[ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.mongo],
|
||||
[ModuleStoreEnum.Type.split, ModuleStoreEnum.Type.split]
|
||||
)
|
||||
@ddt.unpack
|
||||
@@ -2709,7 +2496,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
dest_store = self.store._get_modulestore_by_type(destination_modulestore)
|
||||
self.assertCoursesEqual(source_store, source_course_key, dest_store, dest_course_id)
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_bulk_operations_signal_firing(self, default):
|
||||
""" Signals should be fired right before bulk_operations() exits. """
|
||||
with MongoContentstoreBuilder().build() as contentstore:
|
||||
@@ -2755,7 +2542,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
|
||||
signal_handler.send.assert_called_with('course_published', course_key=course.id)
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_course_publish_signal_direct_firing(self, default):
|
||||
with MongoContentstoreBuilder().build() as contentstore:
|
||||
signal_handler = Mock(name='signal_handler')
|
||||
@@ -2796,7 +2583,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
self.store.publish(block.location, self.user_id)
|
||||
signal_handler.send.assert_called_with('course_published', course_key=course.id)
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_course_publish_signal_rerun_firing(self, default):
|
||||
with MongoContentstoreBuilder().build() as contentstore:
|
||||
signal_handler = Mock(name='signal_handler')
|
||||
@@ -2826,7 +2613,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
self.store.clone_course(course_key, dest_course_id, self.user_id)
|
||||
signal_handler.send.assert_called_with('course_published', course_key=dest_course_id)
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_course_publish_signal_import_firing(self, default):
|
||||
with MongoContentstoreBuilder().build() as contentstore:
|
||||
signal_handler = Mock(name='signal_handler')
|
||||
@@ -2859,7 +2646,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
call('course_published', course_key=self.store.make_course_key('edX', 'toy', '2012_Fall')),
|
||||
])
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_course_publish_signal_publish_firing(self, default):
|
||||
with MongoContentstoreBuilder().build() as contentstore:
|
||||
signal_handler = Mock(name='signal_handler')
|
||||
@@ -2915,7 +2702,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
self.store.delete_item(unit.location, self.user_id)
|
||||
signal_handler.send.assert_called_with('course_published', course_key=course.id)
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_bulk_course_publish_signal_direct_firing(self, default):
|
||||
with MongoContentstoreBuilder().build() as contentstore:
|
||||
signal_handler = Mock(name='signal_handler')
|
||||
@@ -2957,7 +2744,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
|
||||
signal_handler.send.assert_called_with('course_published', course_key=course.id)
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_bulk_course_publish_signal_publish_firing(self, default):
|
||||
with MongoContentstoreBuilder().build() as contentstore:
|
||||
signal_handler = Mock(name='signal_handler')
|
||||
@@ -3026,7 +2813,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
signal_handler.send.assert_not_called()
|
||||
signal_handler.send.assert_not_called()
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_course_deleted_signal(self, default):
|
||||
with MongoContentstoreBuilder().build() as contentstore:
|
||||
signal_handler = Mock(name='signal_handler')
|
||||
@@ -3054,7 +2841,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
# Verify that the signal was emitted
|
||||
signal_handler.send.assert_called_with('course_deleted', course_key=course_key)
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_delete_published_item_orphans(self, default_store):
|
||||
"""
|
||||
Tests delete published item dont create any oprhans in course
|
||||
@@ -3108,7 +2895,7 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
else:
|
||||
assert len(course_publish_orphans) == 0
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_delete_draft_item_orphans(self, default_store):
|
||||
"""
|
||||
Tests delete draft item create no orphans in course
|
||||
@@ -3223,7 +3010,7 @@ class TestPublishOverExportImport(CommonMixedModuleStoreSetup):
|
||||
self._create_course(source_course_key)
|
||||
yield contentstore, source_course_key
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_draft_has_changes_before_export_and_after_import(self, default_ms):
|
||||
"""
|
||||
Tests that an unpublished unit remains with no changes across export and re-import.
|
||||
@@ -3247,7 +3034,7 @@ class TestPublishOverExportImport(CommonMixedModuleStoreSetup):
|
||||
# Verify that the imported block still is a draft, i.e. has changes.
|
||||
assert self._has_changes(draft_xblock.location)
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_published_has_changes_before_export_and_after_import(self, default_ms):
|
||||
"""
|
||||
Tests that an published unit remains published across export and re-import.
|
||||
@@ -3274,7 +3061,7 @@ class TestPublishOverExportImport(CommonMixedModuleStoreSetup):
|
||||
# Verify that it still is published, i.e. has no changes.
|
||||
assert not self._has_changes(published_xblock.location)
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_changed_published_has_changes_before_export_and_after_import(self, default_ms):
|
||||
"""
|
||||
Tests that an published unit with an unpublished draft remains published across export and re-import.
|
||||
@@ -3312,7 +3099,7 @@ class TestPublishOverExportImport(CommonMixedModuleStoreSetup):
|
||||
component = self.store.get_item(published_xblock.location)
|
||||
assert component.display_name == updated_display_name
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_seq_with_unpublished_vertical_has_changes_before_export_and_after_import(self, default_ms):
|
||||
"""
|
||||
Tests that an published unit with an unpublished draft remains published across export and re-import.
|
||||
@@ -3354,7 +3141,7 @@ class TestPublishOverExportImport(CommonMixedModuleStoreSetup):
|
||||
assert self._has_changes(sequential.location)
|
||||
assert self._has_changes(vertical.location)
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_vertical_with_draft_and_published_unit_has_changes_before_export_and_after_import(self, default_ms):
|
||||
"""
|
||||
Tests that an published unit with an unpublished draft remains published across export and re-import.
|
||||
@@ -3436,7 +3223,7 @@ class TestPublishOverExportImport(CommonMixedModuleStoreSetup):
|
||||
component = self.store.get_item(unit.location)
|
||||
assert component.display_name == 'Text'
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
def test_vertical_with_published_unit_remains_published_before_export_and_after_import(self, default_ms):
|
||||
"""
|
||||
Tests that an published unit remains published across export and re-import.
|
||||
@@ -3496,7 +3283,7 @@ class TestPublishOverExportImport(CommonMixedModuleStoreSetup):
|
||||
component = self.store.get_item(unit.location)
|
||||
assert component.display_name == updated_display_name
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
@XBlockAside.register_temp_plugin(AsideTestType, 'test_aside')
|
||||
@patch('xmodule.modulestore.split_mongo.caching_descriptor_system.CachingDescriptorSystem.applicable_aside_types',
|
||||
lambda self, block: ['test_aside'])
|
||||
@@ -3570,7 +3357,7 @@ class TestPublishOverExportImport(CommonMixedModuleStoreSetup):
|
||||
chapter_aside2 = new_chapter2.runtime.get_asides(new_chapter2)[0]
|
||||
assert 'another one value' == chapter_aside2.data_field
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
@XBlockAside.register_temp_plugin(AsideTestType, 'test_aside')
|
||||
@patch('xmodule.modulestore.split_mongo.caching_descriptor_system.CachingDescriptorSystem.applicable_aside_types',
|
||||
lambda self, block: ['test_aside'])
|
||||
@@ -3657,7 +3444,7 @@ class TestPublishOverExportImport(CommonMixedModuleStoreSetup):
|
||||
|
||||
check_block(courses2[0])
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
@XBlockAside.register_temp_plugin(AsideTestType, 'test_aside')
|
||||
@patch('xmodule.modulestore.split_mongo.caching_descriptor_system.CachingDescriptorSystem.applicable_aside_types',
|
||||
lambda self, block: ['test_aside'])
|
||||
@@ -3791,7 +3578,7 @@ class TestAsidesWithMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
field_data = KvsFieldData(key_store)
|
||||
self.runtime = TestRuntime(services={'field-data': field_data})
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
@XBlockAside.register_temp_plugin(AsideFoo, 'test_aside1')
|
||||
@XBlockAside.register_temp_plugin(AsideBar, 'test_aside2')
|
||||
@patch('xmodule.modulestore.split_mongo.caching_descriptor_system.CachingDescriptorSystem.applicable_aside_types',
|
||||
@@ -3856,7 +3643,7 @@ class TestAsidesWithMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
new_asides = new_component.runtime.get_asides(new_component)
|
||||
_check_asides(new_asides, 'other_value11', 'new_value12', 'new_value21', 'aside2_default_value2')
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
@XBlockAside.register_temp_plugin(AsideFoo, 'test_aside1')
|
||||
@patch('xmodule.modulestore.split_mongo.caching_descriptor_system.CachingDescriptorSystem.applicable_aside_types',
|
||||
lambda self, block: ['test_aside1'])
|
||||
@@ -3904,7 +3691,7 @@ class TestAsidesWithMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
|
||||
assert chapter_is_found
|
||||
|
||||
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
|
||||
@ddt.data(ModuleStoreEnum.Type.split)
|
||||
@XBlockAside.register_temp_plugin(AsideFoo, 'test_aside1')
|
||||
@patch('xmodule.modulestore.split_mongo.caching_descriptor_system.CachingDescriptorSystem.applicable_aside_types',
|
||||
lambda self, block: ['test_aside1'])
|
||||
@@ -3953,7 +3740,7 @@ class TestAsidesWithMixedModuleStore(CommonMixedModuleStoreSetup):
|
||||
assert asides2[0].field11 == 'aside1_default_value1'
|
||||
assert asides2[0].field12 == 'aside1_default_value2'
|
||||
|
||||
@ddt.data((ModuleStoreEnum.Type.mongo, 1, 0), (ModuleStoreEnum.Type.split, 1, 0))
|
||||
@ddt.data((ModuleStoreEnum.Type.split, 1, 0))
|
||||
@XBlockAside.register_temp_plugin(AsideFoo, 'test_aside1')
|
||||
@patch('xmodule.modulestore.split_mongo.caching_descriptor_system.CachingDescriptorSystem.applicable_aside_types',
|
||||
lambda self, block: ['test_aside1'])
|
||||
|
||||
@@ -4,753 +4,22 @@ Unit tests for the Mongo modulestore
|
||||
|
||||
|
||||
import logging
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
from tempfile import mkdtemp
|
||||
from unittest.mock import patch
|
||||
from uuid import uuid4
|
||||
|
||||
import ddt
|
||||
import pymongo
|
||||
import pytest
|
||||
|
||||
# pylint: disable=protected-access
|
||||
from django.test import TestCase
|
||||
# pylint: enable=E0611
|
||||
from opaque_keys.edx.keys import CourseKey, UsageKey
|
||||
from opaque_keys.edx.locator import AssetLocator, BlockUsageLocator, CourseLocator, LibraryLocator
|
||||
from path import Path as path
|
||||
from pytz import UTC
|
||||
from xblock.core import XBlock
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from xblock.exceptions import InvalidScopeError
|
||||
from xblock.fields import Reference, ReferenceList, ReferenceValueDict, Scope
|
||||
from xblock.fields import Scope
|
||||
from xblock.runtime import KeyValueStore
|
||||
|
||||
from xmodule.contentstore.mongo import MongoContentStore
|
||||
from xmodule.exceptions import NotFoundError
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.draft import DraftModuleStore
|
||||
from xmodule.modulestore.edit_info import EditInfoMixin
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from xmodule.modulestore.inheritance import InheritanceMixin
|
||||
from xmodule.modulestore.mongo import MongoKeyValueStore
|
||||
from xmodule.modulestore.mongo.base import as_draft
|
||||
from xmodule.modulestore.tests.mongo_connection import MONGO_HOST, MONGO_PORT_NUM
|
||||
from xmodule.modulestore.xml_exporter import export_course_to_xml
|
||||
from xmodule.modulestore.xml_importer import LocationMixin, import_course_from_xml, perform_xlint
|
||||
from xmodule.tests import DATA_DIR
|
||||
from xmodule.x_module import XModuleMixin
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
HOST = MONGO_HOST
|
||||
PORT = MONGO_PORT_NUM
|
||||
DB = 'test_mongo_%s' % uuid4().hex[:5]
|
||||
COLLECTION = 'modulestore'
|
||||
ASSET_COLLECTION = 'assetstore'
|
||||
FS_ROOT = DATA_DIR # TODO (vshnayder): will need a real fs_root for testing load_item
|
||||
DEFAULT_CLASS = 'xmodule.hidden_block.HiddenBlock'
|
||||
RENDER_TEMPLATE = lambda t_n, d, ctx=None, nsp='main': ''
|
||||
|
||||
|
||||
def assert_not_none(actual):
|
||||
"""
|
||||
verify that item is None
|
||||
"""
|
||||
assert actual is not None
|
||||
|
||||
|
||||
class ReferenceTestXBlock(XModuleMixin):
|
||||
"""
|
||||
Test xblock type to test the reference field types
|
||||
"""
|
||||
has_children = True
|
||||
reference_link = Reference(default=None, scope=Scope.content)
|
||||
reference_list = ReferenceList(scope=Scope.content)
|
||||
reference_dict = ReferenceValueDict(scope=Scope.settings)
|
||||
|
||||
|
||||
class TestMongoModuleStoreBase(TestCase):
|
||||
'''
|
||||
Basic setup for all tests
|
||||
'''
|
||||
# Explicitly list the courses to load (don't want the big one)
|
||||
courses = ['toy', 'simple', 'simple_with_draft', 'test_unicode']
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls): # lint-amnesty, pylint: disable=super-method-not-called
|
||||
cls.connection = pymongo.MongoClient(
|
||||
host=HOST,
|
||||
port=PORT,
|
||||
tz_aware=True,
|
||||
document_class=dict,
|
||||
)
|
||||
|
||||
# NOTE: Creating a single db for all the tests to save time. This
|
||||
# is ok only as long as none of the tests modify the db.
|
||||
# If (when!) that changes, need to either reload the db, or load
|
||||
# once and copy over to a tmp db for each test.
|
||||
cls.content_store, cls.draft_store = cls.initdb()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls): # lint-amnesty, pylint: disable=super-method-not-called
|
||||
if cls.connection:
|
||||
cls.connection.drop_database(DB)
|
||||
cls.connection.close()
|
||||
|
||||
@classmethod
|
||||
def add_asset_collection(cls, doc_store_config):
|
||||
"""
|
||||
No asset collection.
|
||||
"""
|
||||
pass # lint-amnesty, pylint: disable=unnecessary-pass
|
||||
|
||||
@classmethod
|
||||
def initdb(cls): # lint-amnesty, pylint: disable=missing-function-docstring
|
||||
# connect to the db
|
||||
doc_store_config = {
|
||||
'host': HOST,
|
||||
'port': PORT,
|
||||
'db': DB,
|
||||
'collection': COLLECTION,
|
||||
}
|
||||
cls.add_asset_collection(doc_store_config)
|
||||
|
||||
# since MongoModuleStore and MongoContentStore are basically assumed to be together, create this class
|
||||
# as well
|
||||
content_store = MongoContentStore(HOST, DB, port=PORT)
|
||||
#
|
||||
# Also test draft store imports
|
||||
#
|
||||
draft_store = DraftModuleStore(
|
||||
content_store,
|
||||
doc_store_config, FS_ROOT, RENDER_TEMPLATE,
|
||||
default_class=DEFAULT_CLASS,
|
||||
branch_setting_func=lambda: ModuleStoreEnum.Branch.draft_preferred,
|
||||
xblock_mixins=(EditInfoMixin, InheritanceMixin, LocationMixin, XModuleMixin)
|
||||
|
||||
)
|
||||
|
||||
import_course_from_xml(
|
||||
draft_store,
|
||||
999,
|
||||
DATA_DIR,
|
||||
cls.courses,
|
||||
static_content_store=content_store
|
||||
)
|
||||
|
||||
# also test a course with no importing of static content
|
||||
import_course_from_xml(
|
||||
draft_store,
|
||||
999,
|
||||
DATA_DIR,
|
||||
['test_import_course'],
|
||||
static_content_store=content_store,
|
||||
do_import_static=False,
|
||||
verbose=True
|
||||
)
|
||||
|
||||
# also import a course under a different course_id (especially ORG)
|
||||
import_course_from_xml(
|
||||
draft_store,
|
||||
999,
|
||||
DATA_DIR,
|
||||
['test_import_course'],
|
||||
static_content_store=content_store,
|
||||
do_import_static=False,
|
||||
verbose=True,
|
||||
target_id=CourseKey.from_string('guestx/foo/bar')
|
||||
)
|
||||
|
||||
# Import a course for `test_reference_converters` since it manipulates the saved course
|
||||
# which can cause any other test using the same course to have a flakey error
|
||||
import_course_from_xml(
|
||||
draft_store,
|
||||
999,
|
||||
DATA_DIR,
|
||||
['test_import_course_2'],
|
||||
static_content_store=content_store
|
||||
)
|
||||
|
||||
return content_store, draft_store
|
||||
|
||||
@staticmethod
|
||||
def destroy_db(connection):
|
||||
# Destroy the test db.
|
||||
connection.drop_database(DB)
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.dummy_user = ModuleStoreEnum.UserID.test
|
||||
|
||||
|
||||
class TestMongoModuleStore(TestMongoModuleStoreBase):
|
||||
'''Module store tests'''
|
||||
|
||||
@classmethod
|
||||
def add_asset_collection(cls, doc_store_config):
|
||||
"""
|
||||
No asset collection - it's not used in the tests below.
|
||||
"""
|
||||
pass # lint-amnesty, pylint: disable=unnecessary-pass
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super().tearDownClass()
|
||||
|
||||
def test_init(self):
|
||||
'''Make sure the db loads'''
|
||||
ids = list(self.connection[DB][COLLECTION].find({}, {'_id': True}))
|
||||
assert len(ids) > 12
|
||||
|
||||
def test_mongo_modulestore_type(self):
|
||||
store = DraftModuleStore(
|
||||
None,
|
||||
{'host': HOST, 'db': DB, 'port': PORT, 'collection': COLLECTION},
|
||||
FS_ROOT, RENDER_TEMPLATE, default_class=DEFAULT_CLASS
|
||||
)
|
||||
assert store.get_modulestore_type('') == ModuleStoreEnum.Type.mongo
|
||||
|
||||
def test_get_courses(self):
|
||||
'''Make sure the course objects loaded properly'''
|
||||
courses = self.draft_store.get_courses()
|
||||
|
||||
assert len(courses) == 7
|
||||
course_ids = [course.id for course in courses]
|
||||
|
||||
for course_key in [
|
||||
|
||||
CourseKey.from_string('/'.join(fields))
|
||||
for fields in [
|
||||
['edX', 'simple', '2012_Fall'],
|
||||
['edX', 'simple_with_draft', '2012_Fall'],
|
||||
['edX', 'test_import_course', '2012_Fall'],
|
||||
['edX', 'test_unicode', '2012_Fall'],
|
||||
['edX', 'toy', '2012_Fall'],
|
||||
['guestx', 'foo', 'bar'],
|
||||
['edX', 'test_import', '2014_Fall'],
|
||||
]
|
||||
]:
|
||||
assert course_key in course_ids
|
||||
course = self.draft_store.get_course(course_key)
|
||||
assert course is not None
|
||||
assert self.draft_store.has_course(course_key)
|
||||
mix_cased = CourseKey.from_string(
|
||||
'/'.join([course_key.org.upper(), course_key.course.upper(), course_key.run.lower()])
|
||||
)
|
||||
assert not self.draft_store.has_course(mix_cased)
|
||||
assert self.draft_store.has_course(mix_cased, ignore_case=True)
|
||||
|
||||
def test_get_org_courses(self):
|
||||
"""
|
||||
Make sure that we can query for a filtered list of courses for a given ORG
|
||||
"""
|
||||
|
||||
courses = self.draft_store.get_courses(org='guestx')
|
||||
assert len(courses) == 1
|
||||
course_ids = [course.id for course in courses]
|
||||
|
||||
for course_key in [
|
||||
CourseKey.from_string('/'.join(fields))
|
||||
for fields in [
|
||||
['guestx', 'foo', 'bar']
|
||||
]
|
||||
]:
|
||||
assert course_key in course_ids
|
||||
|
||||
courses = self.draft_store.get_courses(org='edX')
|
||||
assert len(courses) == 6
|
||||
course_ids = [course.id for course in courses]
|
||||
|
||||
for course_key in [
|
||||
CourseKey.from_string('/'.join(fields))
|
||||
for fields in [
|
||||
['edX', 'simple', '2012_Fall'],
|
||||
['edX', 'simple_with_draft', '2012_Fall'],
|
||||
['edX', 'test_import_course', '2012_Fall'],
|
||||
['edX', 'test_unicode', '2012_Fall'],
|
||||
['edX', 'toy', '2012_Fall'],
|
||||
['edX', 'test_import', '2014_Fall'],
|
||||
]
|
||||
]:
|
||||
assert course_key in course_ids
|
||||
|
||||
def test_no_such_course(self):
|
||||
"""
|
||||
Test get_course and has_course with ids which don't exist
|
||||
"""
|
||||
for course_key in [
|
||||
|
||||
CourseKey.from_string('/'.join(fields))
|
||||
for fields in [
|
||||
['edX', 'simple', 'no_such_course'], ['edX', 'no_such_course', '2012_Fall'],
|
||||
['NO_SUCH_COURSE', 'Test_iMport_courSe', '2012_Fall'],
|
||||
]
|
||||
]:
|
||||
course = self.draft_store.get_course(course_key)
|
||||
assert course is None
|
||||
assert not self.draft_store.has_course(course_key)
|
||||
mix_cased = CourseKey.from_string(
|
||||
'/'.join([course_key.org.lower(), course_key.course.upper(), course_key.run.upper()])
|
||||
)
|
||||
assert not self.draft_store.has_course(mix_cased)
|
||||
assert not self.draft_store.has_course(mix_cased, ignore_case=True)
|
||||
|
||||
def test_get_mongo_course_with_split_course_key(self):
|
||||
"""
|
||||
Test mongo course using split course_key will not able to access it.
|
||||
"""
|
||||
course_key = CourseKey.from_string('course-v1:edX+simple+2012_Fall')
|
||||
|
||||
with pytest.raises(ItemNotFoundError):
|
||||
self.draft_store.get_course(course_key)
|
||||
|
||||
def test_has_mongo_course_with_split_course_key(self):
|
||||
"""
|
||||
Test `has course` using split course key would return False.
|
||||
"""
|
||||
course_key = CourseKey.from_string('course-v1:edX+simple+2012_Fall')
|
||||
|
||||
assert not self.draft_store.has_course(course_key)
|
||||
|
||||
def test_has_course_with_library(self):
|
||||
"""
|
||||
Test that has_course() returns False when called with a LibraryLocator.
|
||||
This is required because MixedModuleStore will use has_course() to check
|
||||
where a given library are stored.
|
||||
"""
|
||||
lib_key = LibraryLocator("TestOrg", "TestLib")
|
||||
result = self.draft_store.has_course(lib_key)
|
||||
assert not result
|
||||
|
||||
def test_loads(self):
|
||||
assert self.draft_store.get_item(BlockUsageLocator(CourseLocator('edX', 'toy', '2012_Fall', deprecated=True),
|
||||
'course', '2012_Fall', deprecated=True)) is not None
|
||||
|
||||
assert self.draft_store.get_item(BlockUsageLocator(CourseLocator('edX', 'simple', '2012_Fall', deprecated=True),
|
||||
'course', '2012_Fall', deprecated=True)) is not None
|
||||
|
||||
assert self.draft_store.get_item(BlockUsageLocator(CourseLocator('edX', 'toy', '2012_Fall', deprecated=True),
|
||||
'video', 'Welcome', deprecated=True)) is not None
|
||||
|
||||
def test_unicode_loads(self):
|
||||
"""
|
||||
Test that getting items from the test_unicode course works
|
||||
"""
|
||||
assert self.draft_store.get_item(
|
||||
BlockUsageLocator(CourseLocator('edX', 'test_unicode', '2012_Fall', deprecated=True),
|
||||
'course', '2012_Fall', deprecated=True)) is not None
|
||||
# All items with ascii-only filenames should load properly.
|
||||
assert self.draft_store.get_item(
|
||||
BlockUsageLocator(CourseLocator('edX', 'test_unicode', '2012_Fall', deprecated=True),
|
||||
'video', 'Welcome', deprecated=True)) is not None
|
||||
assert self.draft_store.get_item(
|
||||
BlockUsageLocator(CourseLocator('edX', 'test_unicode', '2012_Fall', deprecated=True),
|
||||
'video', 'Welcome', deprecated=True)) is not None
|
||||
assert self.draft_store.get_item(
|
||||
BlockUsageLocator(CourseLocator('edX', 'test_unicode', '2012_Fall', deprecated=True),
|
||||
'chapter', 'Overview', deprecated=True)) is not None
|
||||
|
||||
def test_find_one(self):
|
||||
assert self.draft_store._find_one(
|
||||
BlockUsageLocator(CourseLocator('edX', 'toy', '2012_Fall', deprecated=True),
|
||||
'course', '2012_Fall', deprecated=True)) is not None
|
||||
|
||||
assert self.draft_store._find_one(
|
||||
BlockUsageLocator(CourseLocator('edX', 'simple', '2012_Fall', deprecated=True),
|
||||
'course', '2012_Fall', deprecated=True)) is not None
|
||||
|
||||
assert self.draft_store._find_one(BlockUsageLocator(CourseLocator('edX', 'toy', '2012_Fall', deprecated=True),
|
||||
'video', 'Welcome', deprecated=True)) is not None
|
||||
|
||||
def test_xlinter(self):
|
||||
'''
|
||||
Run through the xlinter, we know the 'toy' course has violations, but the
|
||||
number will continue to grow over time, so just check > 0
|
||||
'''
|
||||
assert perform_xlint(DATA_DIR, ['toy']) != 0
|
||||
|
||||
def test_get_courses_has_no_templates(self):
|
||||
courses = self.draft_store.get_courses()
|
||||
for course in courses:
|
||||
assert not ((course.location.org == 'edx') and (course.location.course == 'templates')),\
|
||||
f'{course} is a template course'
|
||||
|
||||
def test_contentstore_attrs(self):
|
||||
"""
|
||||
Test getting, setting, and defaulting the locked attr and arbitrary attrs.
|
||||
"""
|
||||
location = BlockUsageLocator(CourseLocator('edX', 'toy', '2012_Fall', deprecated=True),
|
||||
'course', '2012_Fall', deprecated=True)
|
||||
course_content, __ = self.content_store.get_all_content_for_course(location.course_key)
|
||||
assert len(course_content) > 0
|
||||
filter_params = _build_requested_filter('Images')
|
||||
filtered_course_content, __ = self.content_store.get_all_content_for_course(
|
||||
location.course_key, filter_params=filter_params)
|
||||
assert len(filtered_course_content) < len(course_content)
|
||||
# a bit overkill, could just do for content[0]
|
||||
for content in course_content:
|
||||
assert not content.get('locked', False)
|
||||
asset_key = AssetLocator._from_deprecated_son(content.get('content_son', content['_id']), location.run)
|
||||
assert not self.content_store.get_attr(asset_key, 'locked', False)
|
||||
attrs = self.content_store.get_attrs(asset_key)
|
||||
assert 'uploadDate' in attrs
|
||||
assert not attrs.get('locked', False)
|
||||
self.content_store.set_attr(asset_key, 'locked', True)
|
||||
assert self.content_store.get_attr(asset_key, 'locked', False)
|
||||
attrs = self.content_store.get_attrs(asset_key)
|
||||
assert 'locked' in attrs
|
||||
assert attrs['locked'] is True
|
||||
self.content_store.set_attrs(asset_key, {'miscel': 99})
|
||||
assert self.content_store.get_attr(asset_key, 'miscel') == 99
|
||||
|
||||
asset_key = AssetLocator._from_deprecated_son(
|
||||
course_content[0].get('content_son', course_content[0]['_id']),
|
||||
location.run
|
||||
)
|
||||
with pytest.raises(AttributeError):
|
||||
self.content_store.set_attr(asset_key, 'md5', 'ff1532598830e3feac91c2449eaa60d6')
|
||||
with pytest.raises(AttributeError):
|
||||
self.content_store.set_attrs(asset_key, {'foo': 9, 'md5': 'ff1532598830e3feac91c2449eaa60d6'})
|
||||
with pytest.raises(NotFoundError):
|
||||
self.content_store.get_attr(
|
||||
BlockUsageLocator(CourseLocator('bogus', 'bogus', 'bogus'), 'asset', 'bogus'),
|
||||
'displayname'
|
||||
)
|
||||
with pytest.raises(NotFoundError):
|
||||
self.content_store.set_attr(
|
||||
BlockUsageLocator(CourseLocator('bogus', 'bogus', 'bogus'), 'asset', 'bogus'),
|
||||
'displayname', 'hello'
|
||||
)
|
||||
with pytest.raises(NotFoundError):
|
||||
self.content_store.get_attrs(BlockUsageLocator(CourseLocator('bogus', 'bogus', 'bogus'), 'asset', 'bogus'))
|
||||
with pytest.raises(NotFoundError):
|
||||
self.content_store.set_attrs(
|
||||
BlockUsageLocator(CourseLocator('bogus', 'bogus', 'bogus'), 'asset', 'bogus'),
|
||||
{'displayname': 'hello'}
|
||||
)
|
||||
with pytest.raises(NotFoundError):
|
||||
self.content_store.set_attrs(
|
||||
BlockUsageLocator(CourseLocator('bogus', 'bogus', 'bogus', deprecated=True),
|
||||
'asset', None, deprecated=True),
|
||||
{'displayname': 'hello'}
|
||||
)
|
||||
|
||||
def test_get_courses_for_wiki(self):
|
||||
"""
|
||||
Test the get_courses_for_wiki method
|
||||
"""
|
||||
for course_number in self.courses:
|
||||
course_locations = self.draft_store.get_courses_for_wiki(course_number)
|
||||
assert len(course_locations) == 1
|
||||
assert CourseKey.from_string('/'.join(['edX', course_number, '2012_Fall'])) == course_locations[0]
|
||||
|
||||
course_locations = self.draft_store.get_courses_for_wiki('no_such_wiki')
|
||||
assert len(course_locations) == 0
|
||||
|
||||
# set toy course to share the wiki with simple course
|
||||
toy_course = self.draft_store.get_course(CourseKey.from_string('edX/toy/2012_Fall'))
|
||||
toy_course.wiki_slug = 'simple'
|
||||
self.draft_store.update_item(toy_course, ModuleStoreEnum.UserID.test)
|
||||
|
||||
# now toy_course should not be retrievable with old wiki_slug
|
||||
course_locations = self.draft_store.get_courses_for_wiki('toy')
|
||||
assert len(course_locations) == 0
|
||||
|
||||
# but there should be two courses with wiki_slug 'simple'
|
||||
course_locations = self.draft_store.get_courses_for_wiki('simple')
|
||||
assert len(course_locations) == 2
|
||||
for course_number in ['toy', 'simple']:
|
||||
assert CourseKey.from_string('/'.join(['edX', course_number, '2012_Fall'])) in course_locations
|
||||
|
||||
# configure simple course to use unique wiki_slug.
|
||||
simple_course = self.draft_store.get_course(CourseKey.from_string('edX/simple/2012_Fall'))
|
||||
simple_course.wiki_slug = 'edX.simple.2012_Fall'
|
||||
self.draft_store.update_item(simple_course, ModuleStoreEnum.UserID.test)
|
||||
# it should be retrievable with its new wiki_slug
|
||||
course_locations = self.draft_store.get_courses_for_wiki('edX.simple.2012_Fall')
|
||||
assert len(course_locations) == 1
|
||||
assert CourseKey.from_string('edX/simple/2012_Fall') in course_locations
|
||||
|
||||
@XBlock.register_temp_plugin(ReferenceTestXBlock, 'ref_test')
|
||||
def test_reference_converters(self):
|
||||
"""
|
||||
Test that references types get deserialized correctly
|
||||
"""
|
||||
course_key = CourseKey.from_string('edX/test_import/2014_Fall')
|
||||
|
||||
def setup_test():
|
||||
course = self.draft_store.get_course(course_key)
|
||||
# can't use item factory as it depends on django settings
|
||||
p1ele = self.draft_store.create_item(
|
||||
99,
|
||||
course_key,
|
||||
'problem',
|
||||
block_id='p1',
|
||||
runtime=course.runtime
|
||||
)
|
||||
p2ele = self.draft_store.create_item(
|
||||
99,
|
||||
course_key,
|
||||
'problem',
|
||||
block_id='p2',
|
||||
runtime=course.runtime
|
||||
)
|
||||
self.refloc = course.id.make_usage_key('ref_test', 'ref_test') # lint-amnesty, pylint: disable=attribute-defined-outside-init
|
||||
self.draft_store.create_item(
|
||||
99,
|
||||
self.refloc.course_key,
|
||||
self.refloc.block_type,
|
||||
block_id=self.refloc.block_id,
|
||||
runtime=course.runtime,
|
||||
fields={
|
||||
'reference_link': p1ele.location,
|
||||
'reference_list': [p1ele.location, p2ele.location],
|
||||
'reference_dict': {'p1': p1ele.location, 'p2': p2ele.location},
|
||||
'children': [p1ele.location, p2ele.location],
|
||||
}
|
||||
)
|
||||
|
||||
def check_xblock_fields():
|
||||
def check_children(xblock):
|
||||
for child in xblock.children:
|
||||
assert isinstance(child, UsageKey)
|
||||
|
||||
course = self.draft_store.get_course(course_key)
|
||||
check_children(course)
|
||||
|
||||
refele = self.draft_store.get_item(self.refloc)
|
||||
check_children(refele)
|
||||
assert isinstance(refele.reference_link, UsageKey)
|
||||
assert len(refele.reference_list) > 0
|
||||
for ref in refele.reference_list:
|
||||
assert isinstance(ref, UsageKey)
|
||||
assert len(refele.reference_dict) > 0
|
||||
for ref in refele.reference_dict.values():
|
||||
assert isinstance(ref, UsageKey)
|
||||
|
||||
def check_mongo_fields():
|
||||
def get_item(location):
|
||||
return self.draft_store._find_one(as_draft(location))
|
||||
|
||||
def check_children(payload):
|
||||
for child in payload['definition']['children']:
|
||||
assert isinstance(child, str)
|
||||
|
||||
refele = get_item(self.refloc)
|
||||
check_children(refele)
|
||||
assert isinstance(refele['definition']['data']['reference_link'], str)
|
||||
assert len(refele['definition']['data']['reference_list']) > 0
|
||||
for ref in refele['definition']['data']['reference_list']:
|
||||
assert isinstance(ref, str)
|
||||
assert len(refele['metadata']['reference_dict']) > 0
|
||||
for ref in refele['metadata']['reference_dict'].values():
|
||||
assert isinstance(ref, str)
|
||||
|
||||
setup_test()
|
||||
check_xblock_fields()
|
||||
check_mongo_fields()
|
||||
|
||||
@patch('xmodule.video_block.video_block.edxval_api', None)
|
||||
def test_export_course_image(self):
|
||||
"""
|
||||
Test to make sure that we have a course image in the contentstore,
|
||||
then export it to ensure it gets copied to both file locations.
|
||||
"""
|
||||
course_key = CourseKey.from_string('edX/simple/2012_Fall')
|
||||
location = course_key.make_asset_key('asset', 'images_course_image.jpg')
|
||||
|
||||
# This will raise if the course image is missing
|
||||
self.content_store.find(location)
|
||||
|
||||
root_dir = path(mkdtemp())
|
||||
self.addCleanup(shutil.rmtree, root_dir)
|
||||
export_course_to_xml(self.draft_store, self.content_store, course_key, root_dir, 'test_export')
|
||||
assert path(root_dir / 'test_export/static/images/course_image.jpg').isfile()
|
||||
assert path(root_dir / 'test_export/static/images_course_image.jpg').isfile()
|
||||
|
||||
@patch('xmodule.video_block.video_block.edxval_api', None)
|
||||
def test_export_course_image_nondefault(self):
|
||||
"""
|
||||
Make sure that if a non-default image path is specified that we
|
||||
don't export it to the static default location
|
||||
"""
|
||||
course = self.draft_store.get_course(CourseKey.from_string('edX/toy/2012_Fall'))
|
||||
assert course.course_image == 'just_a_test.jpg'
|
||||
|
||||
root_dir = path(mkdtemp())
|
||||
self.addCleanup(shutil.rmtree, root_dir)
|
||||
export_course_to_xml(self.draft_store, self.content_store, course.id, root_dir, 'test_export')
|
||||
assert path(root_dir / 'test_export/static/just_a_test.jpg').isfile()
|
||||
assert not path(root_dir / 'test_export/static/images/course_image.jpg').isfile()
|
||||
|
||||
@patch('xmodule.video_block.video_block.edxval_api', None)
|
||||
def test_course_without_image(self):
|
||||
"""
|
||||
Make sure we elegantly passover our code when there isn't a static
|
||||
image
|
||||
"""
|
||||
course = self.draft_store.get_course(CourseKey.from_string('edX/simple_with_draft/2012_Fall'))
|
||||
root_dir = path(mkdtemp())
|
||||
self.addCleanup(shutil.rmtree, root_dir)
|
||||
export_course_to_xml(self.draft_store, self.content_store, course.id, root_dir, 'test_export')
|
||||
assert not path(root_dir / 'test_export/static/images/course_image.jpg').isfile()
|
||||
assert not path(root_dir / 'test_export/static/images_course_image.jpg').isfile()
|
||||
|
||||
def _create_test_tree(self, name, user_id=None):
|
||||
"""
|
||||
Creates and returns a tree with the following structure:
|
||||
Grandparent
|
||||
Parent Sibling
|
||||
Parent
|
||||
Child
|
||||
Child Sibling
|
||||
|
||||
"""
|
||||
if user_id is None:
|
||||
user_id = self.dummy_user
|
||||
|
||||
org = 'edX'
|
||||
course = f'tree{name}'
|
||||
run = name
|
||||
|
||||
if not self.draft_store.has_course(CourseKey.from_string('/'.join[org, course, run])): # lint-amnesty, pylint: disable=unsubscriptable-object
|
||||
self.draft_store.create_course(org, course, run, user_id)
|
||||
|
||||
locations = {
|
||||
'grandparent': BlockUsageLocator(CourseLocator(org, course, run, deprecated=True),
|
||||
'chapter', 'grandparent', deprecated=True),
|
||||
'parent_sibling': BlockUsageLocator(CourseLocator(org, course, run, deprecated=True),
|
||||
'sequential', 'parent_sibling', deprecated=True),
|
||||
'parent': BlockUsageLocator(CourseLocator(org, course, run, deprecated=True),
|
||||
'sequential', 'parent', deprecated=True),
|
||||
'child_sibling': BlockUsageLocator(CourseLocator(org, course, run, deprecated=True),
|
||||
'vertical', 'child_sibling', deprecated=True),
|
||||
'child': BlockUsageLocator(CourseLocator(org, course, run, deprecated=True),
|
||||
'vertical', 'child', deprecated=True),
|
||||
}
|
||||
|
||||
for key in locations:
|
||||
self.draft_store.create_item(
|
||||
user_id,
|
||||
locations[key].course_key,
|
||||
locations[key].block_type,
|
||||
block_id=locations[key].block_id
|
||||
)
|
||||
|
||||
grandparent = self.draft_store.get_item(locations['grandparent'])
|
||||
grandparent.children += [locations['parent_sibling'], locations['parent']]
|
||||
self.draft_store.update_item(grandparent, user_id=user_id)
|
||||
|
||||
parent = self.draft_store.get_item(locations['parent'])
|
||||
parent.children += [locations['child_sibling'], locations['child']]
|
||||
self.draft_store.update_item(parent, user_id=user_id)
|
||||
|
||||
self.draft_store.publish(locations['parent'], user_id)
|
||||
self.draft_store.publish(locations['parent_sibling'], user_id)
|
||||
|
||||
return locations
|
||||
|
||||
def test_migrate_published_info(self):
|
||||
"""
|
||||
Tests that blocks that were storing published_date and published_by through CMSBlockMixin are loaded correctly
|
||||
"""
|
||||
|
||||
# Insert the test block directly into the module store
|
||||
location = BlockUsageLocator(CourseLocator('edX', 'migration', '2012_Fall', deprecated=True),
|
||||
'html', 'test_html', deprecated=True)
|
||||
published_date = datetime(1970, 1, 1, tzinfo=UTC)
|
||||
published_by = 123
|
||||
self.draft_store._update_single_item(
|
||||
as_draft(location),
|
||||
{
|
||||
'definition.data': {},
|
||||
'metadata': {
|
||||
# published_date was previously stored as a list of time components, not a datetime
|
||||
'published_date': list(published_date.timetuple()),
|
||||
'published_by': published_by,
|
||||
},
|
||||
},
|
||||
allow_not_found=True,
|
||||
)
|
||||
|
||||
# Retrieve the block and verify its fields
|
||||
component = self.draft_store.get_item(location)
|
||||
assert component.published_on == published_date
|
||||
assert component.published_by == published_by
|
||||
|
||||
def test_draft_modulestore_create_child_with_position(self):
|
||||
"""
|
||||
This test is designed to hit a specific set of use cases having to do with
|
||||
the child positioning logic found in mongo/base.py:create_child()
|
||||
"""
|
||||
# Set up the draft module store
|
||||
course = self.draft_store.create_course("TestX", "ChildTest", "1234_A1", 1)
|
||||
first_child = self.draft_store.create_child(
|
||||
self.dummy_user,
|
||||
course.location,
|
||||
"chapter",
|
||||
block_id=course.location.block_id
|
||||
)
|
||||
second_child = self.draft_store.create_child(
|
||||
self.dummy_user,
|
||||
course.location,
|
||||
"chapter",
|
||||
block_id=course.location.block_id,
|
||||
position=0
|
||||
)
|
||||
|
||||
# First child should have been moved to second position, and better child takes the lead
|
||||
course = self.draft_store.get_course(course.id)
|
||||
assert str(course.children[1]) == str(first_child.location)
|
||||
assert str(course.children[0]) == str(second_child.location)
|
||||
|
||||
# Clean up the data so we don't break other tests which apparently expect a particular state
|
||||
self.draft_store.delete_course(course.id, self.dummy_user)
|
||||
|
||||
def test_make_course_usage_key(self):
|
||||
"""Test that we get back the appropriate usage key for the root of a course key."""
|
||||
course_key = CourseLocator(org="edX", course="101", run="2015")
|
||||
root_block_key = self.draft_store.make_course_usage_key(course_key)
|
||||
assert root_block_key.block_type == 'course'
|
||||
assert root_block_key.block_id == '2015'
|
||||
|
||||
|
||||
class TestMongoModuleStoreWithNoAssetCollection(TestMongoModuleStore): # lint-amnesty, pylint: disable=test-inherits-tests
|
||||
'''
|
||||
Tests a situation where no asset_collection is specified.
|
||||
'''
|
||||
|
||||
@classmethod
|
||||
def add_asset_collection(cls, doc_store_config):
|
||||
"""
|
||||
No asset collection.
|
||||
"""
|
||||
pass # lint-amnesty, pylint: disable=unnecessary-pass
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super().tearDownClass()
|
||||
|
||||
def test_no_asset_collection(self):
|
||||
courses = self.draft_store.get_courses()
|
||||
course = courses[0]
|
||||
# Confirm that no specified asset collection name means empty asset metadata.
|
||||
assert not self.draft_store.get_all_asset_metadata(course.id, 'asset')
|
||||
|
||||
def test_no_asset_invalid_key(self):
|
||||
course_key = CourseLocator(org="edx3", course="test_course", run=None, deprecated=True)
|
||||
# Confirm that invalid course key raises ItemNotFoundError
|
||||
pytest.raises(ItemNotFoundError, (lambda: self.draft_store.get_all_asset_metadata(course_key, 'asset')[:1]))
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestMongoKeyValueStore(TestCase):
|
||||
@@ -765,13 +34,7 @@ class TestMongoKeyValueStore(TestCase):
|
||||
self.parent = self.course_id.make_usage_key('parent', 'p')
|
||||
self.children = [self.course_id.make_usage_key('child', 'a'), self.course_id.make_usage_key('child', 'b')]
|
||||
self.metadata = {'meta': 'meta_val'}
|
||||
self.kvs = MongoKeyValueStore(self.data, self.parent, self.children, self.metadata)
|
||||
|
||||
def test_read(self):
|
||||
assert self.kvs.get(KeyValueStore.Key(Scope.content, None, None, 'foo')) == self.data['foo']
|
||||
assert self.kvs.get(KeyValueStore.Key(Scope.parent, None, None, 'parent')) == self.parent
|
||||
assert self.kvs.get(KeyValueStore.Key(Scope.children, None, None, 'children')) == self.children
|
||||
assert self.kvs.get(KeyValueStore.Key(Scope.settings, None, None, 'meta')) == self.metadata['meta']
|
||||
self.kvs = MongoKeyValueStore(self.data, self.metadata)
|
||||
|
||||
def test_read_invalid_scope(self):
|
||||
for scope in (Scope.preferences, Scope.user_info, Scope.user_state):
|
||||
@@ -781,7 +44,7 @@ class TestMongoKeyValueStore(TestCase):
|
||||
assert not self.kvs.has(key)
|
||||
|
||||
def test_read_non_dict_data(self):
|
||||
self.kvs = MongoKeyValueStore('xml_data', self.parent, self.children, self.metadata)
|
||||
self.kvs = MongoKeyValueStore('xml_data', self.metadata)
|
||||
assert self.kvs.get(KeyValueStore.Key(Scope.content, None, None, 'data')) == 'xml_data'
|
||||
|
||||
def _check_write(self, key, value):
|
||||
@@ -790,8 +53,6 @@ class TestMongoKeyValueStore(TestCase):
|
||||
|
||||
@ddt.data(
|
||||
(Scope.content, "foo", "new_data"),
|
||||
(Scope.children, "children", []),
|
||||
(Scope.children, "parent", None),
|
||||
(Scope.settings, "meta", "new_settings"),
|
||||
)
|
||||
@ddt.unpack
|
||||
@@ -799,7 +60,7 @@ class TestMongoKeyValueStore(TestCase):
|
||||
self._check_write(KeyValueStore.Key(scope, None, None, key), expected)
|
||||
|
||||
def test_write_non_dict_data(self):
|
||||
self.kvs = MongoKeyValueStore('xml_data', self.parent, self.children, self.metadata)
|
||||
self.kvs = MongoKeyValueStore('xml_data', self.metadata)
|
||||
self._check_write(KeyValueStore.Key(Scope.content, None, None, 'data'), 'new_data')
|
||||
|
||||
def test_write_invalid_scope(self):
|
||||
@@ -819,47 +80,7 @@ class TestMongoKeyValueStore(TestCase):
|
||||
self.kvs.get(key)
|
||||
assert not self.kvs.has(key)
|
||||
|
||||
def test_delete_key_default(self):
|
||||
key = KeyValueStore.Key(Scope.children, None, None, "children")
|
||||
self.kvs.delete(key)
|
||||
assert self.kvs.get(key) == []
|
||||
assert self.kvs.has(key)
|
||||
|
||||
def test_delete_invalid_scope(self):
|
||||
for scope in (Scope.preferences, Scope.user_info, Scope.user_state, Scope.parent):
|
||||
with pytest.raises(InvalidScopeError):
|
||||
self.kvs.delete(KeyValueStore.Key(scope, None, None, 'foo'))
|
||||
|
||||
|
||||
def _build_requested_filter(requested_filter):
|
||||
"""
|
||||
Returns requested filter_params string.
|
||||
"""
|
||||
|
||||
# Files and Uploads type filter values
|
||||
all_filters = {
|
||||
"Images": ['image/png', 'image/jpeg', 'image/jpg', 'image/gif', 'image/tiff', 'image/tif', 'image/x-icon'],
|
||||
"Documents": [
|
||||
'application/pdf',
|
||||
'text/plain',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.template',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
|
||||
'application/msword',
|
||||
'application/vnd.ms-excel',
|
||||
'application/vnd.ms-powerpoint',
|
||||
],
|
||||
}
|
||||
requested_file_types = all_filters.get(requested_filter, None)
|
||||
filter_params = {
|
||||
'$or': [{
|
||||
'contentType': {
|
||||
'$in': requested_file_types,
|
||||
},
|
||||
}]
|
||||
}
|
||||
return filter_params
|
||||
|
||||
@@ -17,13 +17,11 @@ from xmodule.modulestore.tests.utils import (
|
||||
TEST_DATA_DIR,
|
||||
MemoryCache,
|
||||
MixedModulestoreBuilder,
|
||||
MongoModulestoreBuilder,
|
||||
VersioningModulestoreBuilder
|
||||
)
|
||||
from xmodule.modulestore.xml_exporter import export_course_to_xml
|
||||
from xmodule.modulestore.xml_importer import import_course_from_xml
|
||||
|
||||
MIXED_OLD_MONGO_MODULESTORE_BUILDER = MixedModulestoreBuilder([('draft', MongoModulestoreBuilder())])
|
||||
MIXED_SPLIT_MODULESTORE_BUILDER = MixedModulestoreBuilder([('split', VersioningModulestoreBuilder())])
|
||||
|
||||
|
||||
@@ -40,7 +38,6 @@ class CountMongoCallsXMLRoundtrip(TestCase):
|
||||
self.addCleanup(rmtree, self.export_dir, ignore_errors=True)
|
||||
|
||||
@ddt.data(
|
||||
(MIXED_OLD_MONGO_MODULESTORE_BUILDER, 287, 779, 702, 702),
|
||||
(MIXED_SPLIT_MODULESTORE_BUILDER, 37, 16, 190, 189),
|
||||
)
|
||||
@ddt.unpack
|
||||
@@ -141,19 +138,6 @@ class CountMongoCallsCourseTraversal(TestCase):
|
||||
@ddt.data(
|
||||
# These two lines show the way this traversal *should* be done
|
||||
# (if you'll eventually access all the fields and load all the definitions anyway).
|
||||
# 'lazy' does not matter in old Mongo.
|
||||
(MIXED_OLD_MONGO_MODULESTORE_BUILDER, None, False, True, 322),
|
||||
(MIXED_OLD_MONGO_MODULESTORE_BUILDER, None, True, True, 322),
|
||||
(MIXED_OLD_MONGO_MODULESTORE_BUILDER, 0, False, True, 506),
|
||||
(MIXED_OLD_MONGO_MODULESTORE_BUILDER, 0, True, True, 506),
|
||||
# As shown in these two lines: whether or not the XBlock fields are accessed,
|
||||
# the same number of mongo calls are made in old Mongo for depth=None.
|
||||
(MIXED_OLD_MONGO_MODULESTORE_BUILDER, None, False, False, 322),
|
||||
(MIXED_OLD_MONGO_MODULESTORE_BUILDER, None, True, False, 322),
|
||||
(MIXED_OLD_MONGO_MODULESTORE_BUILDER, 0, False, False, 506),
|
||||
(MIXED_OLD_MONGO_MODULESTORE_BUILDER, 0, True, False, 506),
|
||||
# The line below shows the way this traversal *should* be done
|
||||
# (if you'll eventually access all the fields and load all the definitions anyway).
|
||||
(MIXED_SPLIT_MODULESTORE_BUILDER, None, False, True, 2),
|
||||
(MIXED_SPLIT_MODULESTORE_BUILDER, None, True, True, 37),
|
||||
(MIXED_SPLIT_MODULESTORE_BUILDER, 0, False, True, 37),
|
||||
@@ -177,7 +161,6 @@ class CountMongoCallsCourseTraversal(TestCase):
|
||||
self._traverse_blocks_in_course(start_block, access_all_block_fields)
|
||||
|
||||
@ddt.data(
|
||||
(MIXED_OLD_MONGO_MODULESTORE_BUILDER, 324),
|
||||
(MIXED_SPLIT_MODULESTORE_BUILDER, 3),
|
||||
)
|
||||
@ddt.unpack
|
||||
|
||||
@@ -16,149 +16,17 @@ from tempfile import mkdtemp
|
||||
import pytest
|
||||
import ddt
|
||||
|
||||
from openedx.core.lib.tests import attr
|
||||
from xmodule.exceptions import InvalidVersionError
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, BlockFactory, check_mongo_calls
|
||||
from xmodule.modulestore.tests.test_split_w_old_mongo import SplitWMongoCourseBootstrapper
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, BlockFactory
|
||||
from xmodule.modulestore.tests.utils import (
|
||||
DRAFT_MODULESTORE_SETUP,
|
||||
MODULESTORE_SETUPS,
|
||||
SPLIT_MODULESTORE_SETUP,
|
||||
MongoContentstoreBuilder,
|
||||
MongoModulestoreBuilder
|
||||
)
|
||||
from xmodule.modulestore.xml_exporter import export_course_to_xml
|
||||
|
||||
|
||||
@attr('mongo')
|
||||
class TestPublish(SplitWMongoCourseBootstrapper):
|
||||
"""
|
||||
Test the publish code (primary causing orphans)
|
||||
"""
|
||||
def _create_course(self): # lint-amnesty, pylint: disable=arguments-differ
|
||||
"""
|
||||
Create the course, publish all verticals
|
||||
* some detached items
|
||||
"""
|
||||
# There are 12 created items and 7 parent updates
|
||||
# create course: finds: 1 to verify uniqueness, 1 to find parents
|
||||
# sends: 1 to create course, 1 to create overview
|
||||
with check_mongo_calls(3, 2):
|
||||
super()._create_course(split=False) # 2 inserts (course and overview)
|
||||
|
||||
# with bulk will delay all inheritance computations which won't be added into the mongo_calls
|
||||
with self.draft_mongo.bulk_operations(self.old_course_key):
|
||||
# finds: 1 for parent to add child and 2 to get ancestors
|
||||
# sends: 1 for insert, 1 for parent (add child)
|
||||
with check_mongo_calls(4, 2):
|
||||
self._create_item('chapter', 'Chapter1', {}, {'display_name': 'Chapter 1'}, 'course', 'runid', split=False) # lint-amnesty, pylint: disable=line-too-long
|
||||
|
||||
with check_mongo_calls(5, 2):
|
||||
self._create_item('chapter', 'Chapter2', {}, {'display_name': 'Chapter 2'}, 'course', 'runid', split=False) # lint-amnesty, pylint: disable=line-too-long
|
||||
# For each vertical (2) created:
|
||||
# - load draft
|
||||
# - load non-draft
|
||||
# - get last error
|
||||
# - load parent
|
||||
# - get ancestors
|
||||
# - load inheritable data
|
||||
with check_mongo_calls(16, 6):
|
||||
self._create_item('vertical', 'Vert1', {}, {'display_name': 'Vertical 1'}, 'chapter', 'Chapter1', split=False) # lint-amnesty, pylint: disable=line-too-long
|
||||
self._create_item('vertical', 'Vert2', {}, {'display_name': 'Vertical 2'}, 'chapter', 'Chapter1', split=False) # lint-amnesty, pylint: disable=line-too-long
|
||||
# For each (4) item created
|
||||
# - try to find draft
|
||||
# - try to find non-draft
|
||||
# - compute what is parent
|
||||
# - load draft parent again & compute its parent chain up to course
|
||||
# count for updates increased to 16 b/c of edit_info updating
|
||||
with check_mongo_calls(40, 16):
|
||||
self._create_item('html', 'Html1', "<p>Goodbye</p>", {'display_name': 'Parented Html'}, 'vertical', 'Vert1', split=False) # lint-amnesty, pylint: disable=line-too-long
|
||||
self._create_item(
|
||||
'discussion', 'Discussion1',
|
||||
"discussion discussion_category=\"Lecture 1\" discussion_id=\"a08bfd89b2aa40fa81f2c650a9332846\" discussion_target=\"Lecture 1\"/>\n", # lint-amnesty, pylint: disable=line-too-long
|
||||
{
|
||||
"discussion_category": "Lecture 1",
|
||||
"discussion_target": "Lecture 1",
|
||||
"display_name": "Lecture 1 Discussion",
|
||||
"discussion_id": "a08bfd89b2aa40fa81f2c650a9332846"
|
||||
},
|
||||
'vertical', 'Vert1',
|
||||
split=False
|
||||
)
|
||||
self._create_item('html', 'Html2', "<p>Hello</p>", {'display_name': 'Hollow Html'}, 'vertical', 'Vert1', split=False) # lint-amnesty, pylint: disable=line-too-long
|
||||
self._create_item(
|
||||
'discussion', 'Discussion2',
|
||||
"discussion discussion_category=\"Lecture 2\" discussion_id=\"b08bfd89b2aa40fa81f2c650a9332846\" discussion_target=\"Lecture 2\"/>\n", # lint-amnesty, pylint: disable=line-too-long
|
||||
{
|
||||
"discussion_category": "Lecture 2",
|
||||
"discussion_target": "Lecture 2",
|
||||
"display_name": "Lecture 2 Discussion",
|
||||
"discussion_id": "b08bfd89b2aa40fa81f2c650a9332846"
|
||||
},
|
||||
'vertical', 'Vert2',
|
||||
split=False
|
||||
)
|
||||
|
||||
with check_mongo_calls(2, 2):
|
||||
# 2 finds b/c looking for non-existent parents
|
||||
self._create_item('static_tab', 'staticuno', "<p>tab</p>", {'display_name': 'Tab uno'}, None, None, split=False) # lint-amnesty, pylint: disable=line-too-long
|
||||
self._create_item('course_info', 'updates', "<ol><li><h2>Sep 22</h2><p>test</p></li></ol>", {}, None, None, split=False) # lint-amnesty, pylint: disable=line-too-long
|
||||
|
||||
def test_publish_draft_delete(self):
|
||||
"""
|
||||
To reproduce a bug (STUD-811) publish a vertical, convert to draft, delete a child, move a child, publish.
|
||||
See if deleted and moved children still is connected or exists in db (bug was disconnected but existed)
|
||||
"""
|
||||
vert_location = self.old_course_key.make_usage_key('vertical', block_id='Vert1')
|
||||
item = self.draft_mongo.get_item(vert_location, 2)
|
||||
# Finds:
|
||||
# 1 get draft vert,
|
||||
# 2 compute parent
|
||||
# 3-14 for each child: (3 children x 4 queries each)
|
||||
# get draft, compute parent, and then published child
|
||||
# compute inheritance
|
||||
# 15 get published vert
|
||||
# 16-18 get ancestor chain
|
||||
# 19 compute inheritance # 20-22 get draft and published vert, compute parent
|
||||
# Sends:
|
||||
# delete the subtree of drafts (1 call),
|
||||
# update the published version of each node in subtree (4 calls),
|
||||
# update the ancestors up to course (2 calls)
|
||||
with check_mongo_calls(23, 7):
|
||||
self.draft_mongo.publish(item.location, self.user_id)
|
||||
|
||||
# verify status
|
||||
item = self.draft_mongo.get_item(vert_location, 0)
|
||||
assert not getattr(item, 'is_draft', False), 'Item was published. Draft should not exist'
|
||||
# however, children are still draft, but I'm not sure that's by design
|
||||
|
||||
# delete the draft version of the discussion
|
||||
location = self.old_course_key.make_usage_key('discussion', block_id='Discussion1')
|
||||
self.draft_mongo.delete_item(location, self.user_id)
|
||||
|
||||
draft_vert = self.draft_mongo.get_item(vert_location, 0)
|
||||
assert getattr(draft_vert, 'is_draft', False), "Deletion didn't convert parent to draft"
|
||||
assert location not in draft_vert.children
|
||||
# move the other child
|
||||
other_child_loc = self.old_course_key.make_usage_key('html', block_id='Html2')
|
||||
draft_vert.children.remove(other_child_loc)
|
||||
other_vert = self.draft_mongo.get_item(self.old_course_key.make_usage_key('vertical', block_id='Vert2'), 0)
|
||||
other_vert.children.append(other_child_loc)
|
||||
self.draft_mongo.update_item(draft_vert, self.user_id)
|
||||
self.draft_mongo.update_item(other_vert, self.user_id)
|
||||
# publish
|
||||
self.draft_mongo.publish(vert_location, self.user_id)
|
||||
item = self.draft_mongo.get_item(draft_vert.location, revision=ModuleStoreEnum.RevisionOption.published_only)
|
||||
assert location not in item.children
|
||||
assert self.draft_mongo.get_parent_location(location) is None
|
||||
with pytest.raises(ItemNotFoundError):
|
||||
self.draft_mongo.get_item(location)
|
||||
assert other_child_loc not in item.children
|
||||
assert self.draft_mongo.has_item(other_child_loc), 'Oops, lost moved item'
|
||||
|
||||
|
||||
@pytest.mark.django_db # required if using split modulestore
|
||||
class DraftPublishedOpTestCourseSetup(unittest.TestCase):
|
||||
"""
|
||||
@@ -548,13 +416,6 @@ class DraftPublishedOpBaseTestSetup(OLXFormatChecker, DraftPublishedOpTestCourse
|
||||
"""
|
||||
return self.store.get_modulestore_type(self.course.id) == ModuleStoreEnum.Type.split
|
||||
|
||||
@property
|
||||
def is_old_mongo_modulestore(self):
|
||||
"""
|
||||
``True`` when modulestore under test is a MongoModuleStore.
|
||||
"""
|
||||
return self.store.get_modulestore_type(self.course.id) == ModuleStoreEnum.Type.mongo
|
||||
|
||||
def _make_new_export_dir_name(self):
|
||||
"""
|
||||
Make a unique name for the new export dir.
|
||||
@@ -642,7 +503,7 @@ class ElementalPublishingTests(DraftPublishedOpBaseTestSetup):
|
||||
"""
|
||||
Tests for the publish() operation.
|
||||
"""
|
||||
@ddt.data(*MODULESTORE_SETUPS)
|
||||
@ddt.data(SPLIT_MODULESTORE_SETUP)
|
||||
def test_autopublished_chapters_sequentials(self, modulestore_builder):
|
||||
with self._setup_test(modulestore_builder):
|
||||
# When a course is created out of chapters/sequentials/verticals/units
|
||||
@@ -661,15 +522,6 @@ class ElementalPublishingTests(DraftPublishedOpBaseTestSetup):
|
||||
self.assertOLXIsPublishedOnly(block_list_autopublished)
|
||||
self.assertOLXIsDraftOnly(block_list_draft)
|
||||
|
||||
@ddt.data(DRAFT_MODULESTORE_SETUP, MongoModulestoreBuilder())
|
||||
def test_publish_old_mongo_unit(self, modulestore_builder):
|
||||
with self._setup_test(modulestore_builder):
|
||||
|
||||
# MODULESTORE_DIFFERENCE:
|
||||
# In old Mongo, you can successfully publish an item whose parent
|
||||
# isn't published.
|
||||
self.publish((('html', 'html00'),))
|
||||
|
||||
@ddt.data(SPLIT_MODULESTORE_SETUP)
|
||||
def test_publish_split_unit(self, modulestore_builder):
|
||||
with self._setup_test(modulestore_builder):
|
||||
@@ -681,7 +533,7 @@ class ElementalPublishingTests(DraftPublishedOpBaseTestSetup):
|
||||
with pytest.raises(ItemNotFoundError):
|
||||
self.publish((('html', 'html00'),))
|
||||
|
||||
@ddt.data(*MODULESTORE_SETUPS)
|
||||
@ddt.data(SPLIT_MODULESTORE_SETUP)
|
||||
def test_publish_multiple_verticals(self, modulestore_builder):
|
||||
with self._setup_test(modulestore_builder):
|
||||
|
||||
@@ -728,7 +580,7 @@ class ElementalPublishingTests(DraftPublishedOpBaseTestSetup):
|
||||
# Ensure that the untouched vertical and children are still untouched.
|
||||
self.assertOLXIsDraftOnly(block_list_untouched)
|
||||
|
||||
@ddt.data(*MODULESTORE_SETUPS)
|
||||
@ddt.data(SPLIT_MODULESTORE_SETUP)
|
||||
def test_publish_single_sequential(self, modulestore_builder):
|
||||
"""
|
||||
Sequentials are auto-published. But publishing them explictly publishes their children,
|
||||
@@ -758,7 +610,7 @@ class ElementalPublishingTests(DraftPublishedOpBaseTestSetup):
|
||||
# Ensure that the verticals and their children are published in the exported OLX.
|
||||
self.assertOLXIsPublishedOnly(block_list)
|
||||
|
||||
@ddt.data(*MODULESTORE_SETUPS)
|
||||
@ddt.data(SPLIT_MODULESTORE_SETUP)
|
||||
def test_publish_single_chapter(self, modulestore_builder):
|
||||
"""
|
||||
Chapters are auto-published.
|
||||
@@ -816,7 +668,7 @@ class ElementalUnpublishingTests(DraftPublishedOpBaseTestSetup):
|
||||
"""
|
||||
Tests for the unpublish() operation.
|
||||
"""
|
||||
@ddt.data(*MODULESTORE_SETUPS)
|
||||
@ddt.data(SPLIT_MODULESTORE_SETUP)
|
||||
def test_unpublish_draft_unit(self, modulestore_builder):
|
||||
with self._setup_test(modulestore_builder):
|
||||
|
||||
@@ -829,7 +681,7 @@ class ElementalUnpublishingTests(DraftPublishedOpBaseTestSetup):
|
||||
with pytest.raises(ItemNotFoundError):
|
||||
self.unpublish(block_list_to_unpublish)
|
||||
|
||||
@ddt.data(*MODULESTORE_SETUPS)
|
||||
@ddt.data(SPLIT_MODULESTORE_SETUP)
|
||||
def test_unpublish_published_units(self, modulestore_builder):
|
||||
with self._setup_test(modulestore_builder):
|
||||
|
||||
@@ -857,14 +709,10 @@ class ElementalUnpublishingTests(DraftPublishedOpBaseTestSetup):
|
||||
# Split:
|
||||
# The parent now has a draft *and* published item.
|
||||
self.assertOLXIsDraftAndPublished(block_list_parent)
|
||||
elif self.is_old_mongo_modulestore:
|
||||
# Old Mongo:
|
||||
# The parent remains published only.
|
||||
self.assertOLXIsPublishedOnly(block_list_parent)
|
||||
else:
|
||||
raise Exception("Must test either Old Mongo or Split modulestore!")
|
||||
|
||||
@ddt.data(*MODULESTORE_SETUPS)
|
||||
@ddt.data(SPLIT_MODULESTORE_SETUP)
|
||||
def test_unpublish_draft_vertical(self, modulestore_builder):
|
||||
with self._setup_test(modulestore_builder):
|
||||
|
||||
@@ -877,7 +725,7 @@ class ElementalUnpublishingTests(DraftPublishedOpBaseTestSetup):
|
||||
with pytest.raises(ItemNotFoundError):
|
||||
self.unpublish(block_list_to_unpublish)
|
||||
|
||||
@ddt.data(*MODULESTORE_SETUPS)
|
||||
@ddt.data(SPLIT_MODULESTORE_SETUP)
|
||||
def test_unpublish_published_vertical(self, modulestore_builder):
|
||||
with self._setup_test(modulestore_builder):
|
||||
|
||||
@@ -919,7 +767,7 @@ class ElementalUnpublishingTests(DraftPublishedOpBaseTestSetup):
|
||||
self.assertOLXIsDraftOnly(block_list_unpublished_children)
|
||||
self.assertOLXIsDraftOnly(block_list_untouched)
|
||||
|
||||
@ddt.data(SPLIT_MODULESTORE_SETUP, DRAFT_MODULESTORE_SETUP, MongoModulestoreBuilder())
|
||||
@ddt.data(SPLIT_MODULESTORE_SETUP)
|
||||
def test_unpublish_draft_sequential(self, modulestore_builder):
|
||||
with self._setup_test(modulestore_builder):
|
||||
|
||||
@@ -943,7 +791,7 @@ class ElementalDeleteItemTests(DraftPublishedOpBaseTestSetup):
|
||||
assert_method(block_list)
|
||||
|
||||
@ddt.data(*itertools.product(
|
||||
MODULESTORE_SETUPS,
|
||||
(SPLIT_MODULESTORE_SETUP,),
|
||||
(
|
||||
(ModuleStoreEnum.RevisionOption.published_only, 'assertOLXIsDraftOnly'),
|
||||
(ModuleStoreEnum.RevisionOption.all, 'assertOLXIsDeleted'),
|
||||
@@ -961,12 +809,7 @@ class ElementalDeleteItemTests(DraftPublishedOpBaseTestSetup):
|
||||
# The unit is a draft.
|
||||
self.assertOLXIsDraftOnly(block_list_to_delete)
|
||||
# MODULESTORE_DIFFERENCE:
|
||||
if self.is_old_mongo_modulestore:
|
||||
# Old Mongo throws no exception when trying to delete an item from the published branch
|
||||
# that isn't yet published.
|
||||
self.delete_item(block_list_to_delete, revision=revision)
|
||||
self._check_for_item_deletion(block_list_to_delete, result)
|
||||
elif self.is_split_modulestore:
|
||||
if self.is_split_modulestore:
|
||||
if revision in (ModuleStoreEnum.RevisionOption.published_only, ModuleStoreEnum.RevisionOption.all):
|
||||
# Split throws an exception when trying to delete an item from the published branch
|
||||
# that isn't yet published.
|
||||
@@ -978,47 +821,6 @@ class ElementalDeleteItemTests(DraftPublishedOpBaseTestSetup):
|
||||
else:
|
||||
raise Exception("Must test either Old Mongo or Split modulestore!")
|
||||
|
||||
@ddt.data(*itertools.product(
|
||||
(DRAFT_MODULESTORE_SETUP, MongoModulestoreBuilder()),
|
||||
(
|
||||
# MODULESTORE_DIFFERENCE: This first line is different between old Mongo and Split for verticals.
|
||||
# Old Mongo deletes the draft vertical even when published_only is specified.
|
||||
(ModuleStoreEnum.RevisionOption.published_only, 'assertOLXIsDeleted'),
|
||||
(ModuleStoreEnum.RevisionOption.all, 'assertOLXIsDeleted'),
|
||||
(None, 'assertOLXIsDeleted'),
|
||||
)
|
||||
))
|
||||
@ddt.unpack
|
||||
def test_old_mongo_delete_draft_vertical(self, modulestore_builder, revision_and_result):
|
||||
with self._setup_test(modulestore_builder):
|
||||
|
||||
block_list_to_delete = (
|
||||
('vertical', 'vertical03'),
|
||||
)
|
||||
block_list_children = (
|
||||
('html', 'html06'),
|
||||
('html', 'html07'),
|
||||
)
|
||||
(revision, result) = revision_and_result
|
||||
# The vertical is a draft.
|
||||
self.assertOLXIsDraftOnly(block_list_to_delete)
|
||||
# MODULESTORE_DIFFERENCE:
|
||||
# Old Mongo throws no exception when trying to delete an item from the published branch
|
||||
# that isn't yet published.
|
||||
self.delete_item(block_list_to_delete, revision=revision)
|
||||
self._check_for_item_deletion(block_list_to_delete, result)
|
||||
# MODULESTORE_DIFFERENCE:
|
||||
# Weirdly, this is a difference between old Mongo -and- old Mongo wrapped with a mixed modulestore.
|
||||
# When the code attempts and fails to delete the draft vertical using the published_only revision,
|
||||
# the draft children are still around in one case and not in the other? Needs investigation.
|
||||
if (
|
||||
isinstance(modulestore_builder, MongoModulestoreBuilder) and
|
||||
revision == ModuleStoreEnum.RevisionOption.published_only
|
||||
):
|
||||
self.assertOLXIsDraftOnly(block_list_children)
|
||||
else:
|
||||
self.assertOLXIsDeleted(block_list_children)
|
||||
|
||||
@ddt.data(*itertools.product(
|
||||
(SPLIT_MODULESTORE_SETUP,),
|
||||
(
|
||||
@@ -1055,7 +857,7 @@ class ElementalDeleteItemTests(DraftPublishedOpBaseTestSetup):
|
||||
self.assertOLXIsDeleted(block_list_children)
|
||||
|
||||
@ddt.data(*itertools.product(
|
||||
MODULESTORE_SETUPS,
|
||||
(SPLIT_MODULESTORE_SETUP,),
|
||||
(
|
||||
(ModuleStoreEnum.RevisionOption.published_only, 'assertOLXIsDeleted'),
|
||||
(ModuleStoreEnum.RevisionOption.all, 'assertOLXIsDeleted'),
|
||||
@@ -1085,7 +887,7 @@ class ElementalDeleteItemTests(DraftPublishedOpBaseTestSetup):
|
||||
self.assertOLXIsDeleted(block_list_children)
|
||||
|
||||
@ddt.data(*itertools.product(
|
||||
MODULESTORE_SETUPS,
|
||||
(SPLIT_MODULESTORE_SETUP,),
|
||||
(
|
||||
(ModuleStoreEnum.RevisionOption.published_only, 'assertOLXIsDeleted'),
|
||||
(ModuleStoreEnum.RevisionOption.all, 'assertOLXIsDeleted'),
|
||||
@@ -1131,7 +933,7 @@ class ElementalConvertToDraftTests(DraftPublishedOpBaseTestSetup):
|
||||
"""
|
||||
Tests for the convert_to_draft() operation.
|
||||
"""
|
||||
@ddt.data(*MODULESTORE_SETUPS)
|
||||
@ddt.data(SPLIT_MODULESTORE_SETUP)
|
||||
def test_convert_to_draft_published_vertical(self, modulestore_builder):
|
||||
with self._setup_test(modulestore_builder):
|
||||
|
||||
@@ -1151,14 +953,10 @@ class ElementalConvertToDraftTests(DraftPublishedOpBaseTestSetup):
|
||||
# Split:
|
||||
# This operation is a no-op is Split since there's always a draft version maintained.
|
||||
self.assertOLXIsPublishedOnly(block_list_to_convert)
|
||||
elif self.is_old_mongo_modulestore:
|
||||
# Old Mongo:
|
||||
# A draft -and- a published block now exists.
|
||||
self.assertOLXIsDraftAndPublished(block_list_to_convert)
|
||||
else:
|
||||
raise Exception("Must test either Old Mongo or Split modulestore!")
|
||||
|
||||
@ddt.data(*MODULESTORE_SETUPS)
|
||||
@ddt.data(SPLIT_MODULESTORE_SETUP)
|
||||
def test_convert_to_draft_autopublished_sequential(self, modulestore_builder):
|
||||
with self._setup_test(modulestore_builder):
|
||||
|
||||
@@ -1174,11 +972,6 @@ class ElementalConvertToDraftTests(DraftPublishedOpBaseTestSetup):
|
||||
self.convert_to_draft(block_list_to_convert)
|
||||
# This operation is a no-op is Split since there's always a draft version maintained.
|
||||
self.assertOLXIsPublishedOnly(block_list_to_convert)
|
||||
elif self.is_old_mongo_modulestore:
|
||||
# Old Mongo:
|
||||
# Direct-only categories are never allowed to be converted to draft.
|
||||
with pytest.raises(InvalidVersionError):
|
||||
self.convert_to_draft(block_list_to_convert)
|
||||
else:
|
||||
raise Exception("Must test either Old Mongo or Split modulestore!")
|
||||
|
||||
@@ -1188,7 +981,7 @@ class ElementalRevertToPublishedTests(DraftPublishedOpBaseTestSetup):
|
||||
"""
|
||||
Tests for the revert_to_published() operation.
|
||||
"""
|
||||
@ddt.data(*MODULESTORE_SETUPS)
|
||||
@ddt.data(SPLIT_MODULESTORE_SETUP)
|
||||
def test_revert_to_published_unpublished_vertical(self, modulestore_builder):
|
||||
with self._setup_test(modulestore_builder):
|
||||
|
||||
@@ -1202,7 +995,7 @@ class ElementalRevertToPublishedTests(DraftPublishedOpBaseTestSetup):
|
||||
with pytest.raises(InvalidVersionError):
|
||||
self.revert_to_published(block_list_to_revert)
|
||||
|
||||
@ddt.data(*MODULESTORE_SETUPS)
|
||||
@ddt.data(SPLIT_MODULESTORE_SETUP)
|
||||
def test_revert_to_published_published_vertical(self, modulestore_builder):
|
||||
with self._setup_test(modulestore_builder):
|
||||
|
||||
@@ -1220,7 +1013,7 @@ class ElementalRevertToPublishedTests(DraftPublishedOpBaseTestSetup):
|
||||
# Basically a no-op - there was no draft version to revert.
|
||||
self.assertOLXIsPublishedOnly(block_list_to_revert)
|
||||
|
||||
@ddt.data(*MODULESTORE_SETUPS)
|
||||
@ddt.data(SPLIT_MODULESTORE_SETUP)
|
||||
def test_revert_to_published_vertical(self, modulestore_builder):
|
||||
with self._setup_test(modulestore_builder):
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import unittest
|
||||
from uuid import uuid4
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator
|
||||
from path import Path as path
|
||||
@@ -19,7 +18,7 @@ from xblock.runtime import DictKeyValueStore, KvsFieldData, Runtime
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.inheritance import InheritanceMixin
|
||||
from xmodule.modulestore.tests.mongo_connection import MONGO_HOST, MONGO_PORT_NUM
|
||||
from xmodule.modulestore.xml_importer import StaticContentImporter, _update_and_import_block, _update_block_location
|
||||
from xmodule.modulestore.xml_importer import StaticContentImporter, _update_block_location
|
||||
from xmodule.tests import DATA_DIR
|
||||
from xmodule.x_module import XModuleMixin
|
||||
|
||||
@@ -125,121 +124,6 @@ class StubXBlock(XModuleMixin, InheritanceMixin):
|
||||
)
|
||||
|
||||
|
||||
class RemapNamespaceTest(ModuleStoreNoSettings):
|
||||
"""
|
||||
Test that remapping the namespace from import to the actual course location.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Create a stub XBlock backed by in-memory storage.
|
||||
"""
|
||||
self.runtime = mock.MagicMock(Runtime)
|
||||
self.field_data = KvsFieldData(kvs=DictKeyValueStore())
|
||||
self.scope_ids = ScopeIds('Bob', 'stubxblock', '123', 'import')
|
||||
self.xblock = StubXBlock(self.runtime, self.field_data, self.scope_ids)
|
||||
super().setUp()
|
||||
|
||||
def test_remap_namespace_native_xblock(self):
|
||||
|
||||
# Set the XBlock's location
|
||||
self.xblock.location = BlockUsageLocator(CourseLocator("org", "import", "run"), "category", "stubxblock")
|
||||
|
||||
# Explicitly set the content and settings fields
|
||||
self.xblock.test_content_field = "Explicitly set"
|
||||
self.xblock.test_settings_field = "Explicitly set"
|
||||
self.xblock.save()
|
||||
|
||||
# Move to different runtime w/ different course id
|
||||
target_location_namespace = CourseKey.from_string("org/course/run")
|
||||
new_version = _update_and_import_block(
|
||||
self.xblock,
|
||||
modulestore(),
|
||||
999,
|
||||
self.xblock.location.course_key,
|
||||
target_location_namespace,
|
||||
do_import_static=False
|
||||
)
|
||||
|
||||
# Check the XBlock's location
|
||||
assert new_version.location.course_key == target_location_namespace
|
||||
|
||||
# Check the values of the fields.
|
||||
# The content and settings fields should be preserved
|
||||
assert new_version.test_content_field == 'Explicitly set'
|
||||
assert new_version.test_settings_field == 'Explicitly set'
|
||||
|
||||
# Expect that these fields are marked explicitly set
|
||||
assert 'test_content_field' in new_version.get_explicitly_set_fields_by_scope(scope=Scope.content)
|
||||
assert 'test_settings_field' in new_version.get_explicitly_set_fields_by_scope(scope=Scope.settings)
|
||||
|
||||
def test_remap_namespace_native_xblock_default_values(self):
|
||||
|
||||
# Set the XBlock's location
|
||||
self.xblock.location = BlockUsageLocator(CourseLocator("org", "import", "run"), "category", "stubxblock")
|
||||
|
||||
# Do NOT set any values, so the fields should use the defaults
|
||||
self.xblock.save()
|
||||
|
||||
# Remap the namespace
|
||||
target_location_namespace = BlockUsageLocator(CourseLocator("org", "course", "run"), "category", "stubxblock")
|
||||
new_version = _update_and_import_block(
|
||||
self.xblock,
|
||||
modulestore(),
|
||||
999,
|
||||
self.xblock.location.course_key,
|
||||
target_location_namespace.course_key,
|
||||
do_import_static=False
|
||||
)
|
||||
|
||||
# Check the values of the fields.
|
||||
# The content and settings fields should be the default values
|
||||
assert new_version.test_content_field == 'default value'
|
||||
assert new_version.test_settings_field == 'default value'
|
||||
|
||||
# The fields should NOT appear in the explicitly set fields
|
||||
assert 'test_content_field' not in new_version.get_explicitly_set_fields_by_scope(scope=Scope.content)
|
||||
assert 'test_settings_field' not in new_version.get_explicitly_set_fields_by_scope(scope=Scope.settings)
|
||||
|
||||
def test_remap_namespace_native_xblock_inherited_values(self):
|
||||
|
||||
# Set the XBlock's location
|
||||
self.xblock.location = BlockUsageLocator(CourseLocator("org", "import", "run"), "category", "stubxblock")
|
||||
self.xblock.save()
|
||||
|
||||
# Remap the namespace
|
||||
target_location_namespace = BlockUsageLocator(CourseLocator("org", "course", "run"), "category", "stubxblock")
|
||||
new_version = _update_and_import_block(
|
||||
self.xblock,
|
||||
modulestore(),
|
||||
999,
|
||||
self.xblock.location.course_key,
|
||||
target_location_namespace.course_key,
|
||||
do_import_static=False
|
||||
)
|
||||
|
||||
# Inherited fields should NOT be explicitly set
|
||||
assert 'start' not in new_version.get_explicitly_set_fields_by_scope(scope=Scope.settings)
|
||||
assert 'graded' not in new_version.get_explicitly_set_fields_by_scope(scope=Scope.settings)
|
||||
|
||||
def test_xblock_invalid_field_value_type(self):
|
||||
# Setting the wrong field-value in Xblock-field will raise TypeError.
|
||||
# Example if xblock-field is of 'Dictionary' type by setting the 'List' value in that dict-type will raise
|
||||
# TypeError.
|
||||
|
||||
# Set the XBlock's location
|
||||
self.xblock.location = BlockUsageLocator(CourseLocator("org", "import", "run"), "category", "stubxblock")
|
||||
# Explicitly set the content field
|
||||
self.xblock.test_content_field = ['Explicitly set']
|
||||
self.xblock.save()
|
||||
|
||||
# clearing the dirty fields and removing value from cache will fetch the value from field-data.
|
||||
self.xblock._dirty_fields = {} # pylint: disable=protected-access
|
||||
self.xblock.fields['test_content_field']._del_cached_value(self.xblock) # lint-amnesty, pylint: disable=protected-access, unsubscriptable-object
|
||||
with pytest.raises(TypeError):
|
||||
self.xblock.get_explicitly_set_fields_by_scope(scope=Scope.content)
|
||||
|
||||
|
||||
class StubXBlockWithMutableFields(StubXBlock):
|
||||
"""
|
||||
Stub XBlock used for testing mutable fields and children
|
||||
|
||||
@@ -30,7 +30,6 @@ from xmodule.modulestore.tests.utils import (
|
||||
)
|
||||
|
||||
_TODAY = datetime.now(utc)
|
||||
_LAST_MONTH = _TODAY - timedelta(days=30)
|
||||
_LAST_WEEK = _TODAY - timedelta(days=7)
|
||||
_NEXT_WEEK = _TODAY + timedelta(days=7)
|
||||
|
||||
@@ -49,34 +48,9 @@ class CourseMetadataUtilsTestCase(TestCase):
|
||||
|
||||
mongo_builder = MongoModulestoreBuilder()
|
||||
split_builder = VersioningModulestoreBuilder()
|
||||
mixed_builder = MixedModulestoreBuilder([('mongo', mongo_builder), ('split', split_builder)])
|
||||
mixed_builder = MixedModulestoreBuilder([('split', split_builder), ('mongo', mongo_builder)])
|
||||
|
||||
with mixed_builder.build_without_contentstore() as (__, mixed_store):
|
||||
with mixed_store.default_store('mongo'):
|
||||
self.demo_course = mixed_store.create_course(
|
||||
org="edX",
|
||||
course="DemoX.1",
|
||||
run="Fall_2014",
|
||||
user_id=-3, # -3 refers to a "testing user"
|
||||
fields={
|
||||
"start": _LAST_MONTH,
|
||||
"end": _LAST_WEEK,
|
||||
"enrollment_start": _LAST_MONTH,
|
||||
"enrollment_end": _LAST_WEEK
|
||||
}
|
||||
)
|
||||
with mixed_store.default_store('mongo'):
|
||||
self.demo_course_enrollment_end = mixed_store.create_course(
|
||||
org="edX",
|
||||
course="DemoX.2",
|
||||
run="Fall_2014_1",
|
||||
user_id=-3, # -3 refers to a "testing user"
|
||||
fields={
|
||||
"start": _LAST_MONTH,
|
||||
"end": _LAST_WEEK,
|
||||
"enrollment_end": _LAST_WEEK
|
||||
}
|
||||
)
|
||||
with mixed_store.default_store('split'):
|
||||
self.html_course = mixed_store.create_course(
|
||||
org="UniversityX",
|
||||
@@ -145,11 +119,6 @@ class CourseMetadataUtilsTestCase(TestCase):
|
||||
|
||||
function_tests = [
|
||||
FunctionTest(clean_course_key, [
|
||||
# Test with a Mongo course and '=' as padding.
|
||||
TestScenario(
|
||||
(self.demo_course.id, '='),
|
||||
"course_MVSFQL2EMVWW6WBOGEXUMYLMNRPTEMBRGQ======"
|
||||
),
|
||||
# Test with a Split course and '~' as padding.
|
||||
TestScenario(
|
||||
(self.html_course.id, '~'),
|
||||
@@ -157,39 +126,26 @@ class CourseMetadataUtilsTestCase(TestCase):
|
||||
),
|
||||
]),
|
||||
FunctionTest(url_name_for_block, [
|
||||
TestScenario((self.demo_course,), self.demo_course.location.block_id),
|
||||
TestScenario((self.html_course,), self.html_course.location.block_id),
|
||||
]),
|
||||
FunctionTest(display_name_with_default_escaped, [
|
||||
# Test course with no display name.
|
||||
TestScenario((self.demo_course,), "Empty"),
|
||||
# Test course with a display name that contains characters that need escaping.
|
||||
TestScenario((self.html_course,), "Intro to html"),
|
||||
]),
|
||||
FunctionTest(display_name_with_default, [
|
||||
# Test course with no display name.
|
||||
TestScenario((self.demo_course,), "Empty"),
|
||||
# Test course with a display name that contains characters that need escaping.
|
||||
TestScenario((self.html_course,), "Intro to <div>html</div>"),
|
||||
]),
|
||||
FunctionTest(number_for_course_location, [
|
||||
TestScenario((self.demo_course.location,), "DemoX.1"),
|
||||
TestScenario((self.html_course.location,), "CS-203"),
|
||||
]),
|
||||
FunctionTest(has_course_started, [
|
||||
TestScenario((self.demo_course.start,), True),
|
||||
TestScenario((self.html_course.start,), False),
|
||||
]),
|
||||
FunctionTest(has_course_ended, [
|
||||
TestScenario((self.demo_course.end,), True),
|
||||
TestScenario((self.html_course.end,), False),
|
||||
]),
|
||||
FunctionTest(is_enrollment_open, [
|
||||
TestScenario((self.demo_course.enrollment_start, self.demo_course.enrollment_end,), False),
|
||||
TestScenario((
|
||||
self.demo_course_enrollment_end.enrollment_start,
|
||||
self.demo_course_enrollment_end.enrollment_end
|
||||
), False),
|
||||
TestScenario((self.html_course.enrollment_start, self.html_course.enrollment_end,), False),
|
||||
TestScenario((
|
||||
self.html_course_enrollment_start.enrollment_start,
|
||||
|
||||
Reference in New Issue
Block a user