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:
Sagirov Evgeniy
2023-09-06 17:01:31 +03:00
committed by GitHub
parent a1d840fd09
commit c5d1807c81
40 changed files with 663 additions and 3146 deletions

View File

@@ -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.

View File

@@ -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.')

View File

@@ -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.

View File

@@ -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)

View File

@@ -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):
"""

View File

@@ -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))

View File

@@ -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)

View File

@@ -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)

View File

@@ -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):
"""

View File

@@ -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))

View File

@@ -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(

View File

@@ -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):

View File

@@ -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):
"""

View File

@@ -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")

View File

@@ -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

View File

@@ -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

View File

@@ -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):

View File

@@ -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))

View File

@@ -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/')

View File

@@ -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

View File

@@ -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

View File

@@ -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):

View File

@@ -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),

View File

@@ -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

View File

@@ -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(

View File

@@ -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'

View File

@@ -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
}
]
}
}

View File

@@ -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()}

View File

@@ -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):

View File

@@ -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)

View File

@@ -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

View File

@@ -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):
"""

View File

@@ -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'

View File

@@ -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,

View File

@@ -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'])

View File

@@ -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

View File

@@ -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

View File

@@ -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):

View File

@@ -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

View File

@@ -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,