From 3877201dd2096d842a31d139ab80ab0d726da33d Mon Sep 17 00:00:00 2001 From: Sagirov Eugeniy Date: Mon, 26 Dec 2022 14:39:50 +0200 Subject: [PATCH] test: update test_contentstore.py for split modulestore --- .../contentstore/tests/test_contentstore.py | 259 +++++------------- cms/djangoapps/contentstore/tests/utils.py | 156 +---------- 2 files changed, 70 insertions(+), 345 deletions(-) diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index ca5ea3e954..94194502be 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -2,6 +2,7 @@ import copy +import re import shutil from datetime import timedelta from functools import wraps @@ -34,6 +35,8 @@ from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore.django import modulestore from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.inheritance import own_metadata +from xmodule.modulestore.split_mongo import BlockKey +from xmodule.modulestore.tests.django_utils import TEST_DATA_SPLIT_MODULESTORE from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, check_mongo_calls from xmodule.modulestore.xml_exporter import export_course_to_xml from xmodule.modulestore.xml_importer import import_course_from_xml, perform_xlint @@ -83,6 +86,7 @@ class ContentStoreTestCase(CourseTestCase): """ Base class for Content Store Test Cases """ + MODULESTORE = TEST_DATA_SPLIT_MODULESTORE class ImportRequiredTestCases(ContentStoreTestCase): @@ -212,7 +216,7 @@ class ImportRequiredTestCases(ContentStoreTestCase): all_thumbnails = content_store.get_all_content_thumbnails_for_course(course.id) self.assertGreater(len(all_thumbnails), 0) - location = AssetKey.from_string('/c4x/edX/toy/asset/just_a_test.jpg') + location = AssetKey.from_string('asset-v1:edX+toy+2012_Fall+type@asset+block@just_a_test.jpg') content = content_store.find(location) self.assertIsNotNone(content) @@ -282,7 +286,7 @@ class ImportRequiredTestCases(ContentStoreTestCase): ) # first check a static asset link - course_key = self.store.make_course_key('edX', 'toy', 'run') + course_key = self.store.make_course_key('edX', 'toy', '2012_Fall') html_block_location = course_key.make_usage_key('html', 'nonportable') html_block = self.store.get_item(html_block_location) self.assertIn('/static/foo.jpg', html_block.data) @@ -302,101 +306,6 @@ class ImportRequiredTestCases(ContentStoreTestCase): filesystem = OSFS(root_dir / ('test_export/' + dirname)) self.assertTrue(filesystem.exists(item.location.block_id + filename_suffix)) - @mock.patch('xmodule.course_block.requests.get') - def test_export_course_roundtrip(self, mock_get): - mock_get.return_value.text = dedent(""" - - - - """).strip() - - content_store = contentstore() - course_id = self.import_and_populate_course() - - root_dir = path(mkdtemp_clean()) - print(f'Exporting to tempdir = {root_dir}') - - # export out to a tempdir - export_course_to_xml(self.store, content_store, course_id, root_dir, 'test_export') - - # check for static tabs - self.verify_content_existence(self.store, root_dir, course_id, 'tabs', 'static_tab', '.html') - - # check for about content - self.verify_content_existence(self.store, root_dir, course_id, 'about', 'about', '.html') - - # assert that there is an html and video directory in drafts: - draft_dir = OSFS(root_dir / 'test_export/drafts') - self.assertTrue(draft_dir.exists('html')) - self.assertTrue(draft_dir.exists('video')) - # and assert that they contain the created modules - self.assertIn(self.DRAFT_HTML + ".xml", draft_dir.listdir('html')) - self.assertIn(self.DRAFT_VIDEO + ".xml", draft_dir.listdir('video')) - # and assert the child of the orphaned draft wasn't exported - self.assertNotIn(self.ORPHAN_DRAFT_HTML + ".xml", draft_dir.listdir('html')) - - # check for grading_policy.json - filesystem = OSFS(root_dir / 'test_export/policies/2012_Fall') - self.assertTrue(filesystem.exists('grading_policy.json')) - - course = self.store.get_course(course_id) - # compare what's on disk compared to what we have in our course - with filesystem.open('grading_policy.json', 'r') as grading_policy: - on_disk = loads(grading_policy.read()) - self.assertEqual(on_disk, course.grading_policy) - - # check for policy.json - self.assertTrue(filesystem.exists('policy.json')) - - # compare what's on disk to what we have in the course block - with filesystem.open('policy.json', 'r') as course_policy: - on_disk = loads(course_policy.read()) - self.assertIn('course/2012_Fall', on_disk) - self.assertEqual(on_disk['course/2012_Fall'], own_metadata(course)) - - # remove old course - self.store.delete_course(course_id, self.user.id) - - # reimport over old course - self.check_import(root_dir, content_store, course_id) - - # import to different course id - new_course_id = self.store.make_course_key('anotherX', 'anotherToy', 'Someday') - self.check_import(root_dir, content_store, new_course_id) - self.assertCoursesEqual(course_id, new_course_id) - - shutil.rmtree(root_dir) - - def check_import(self, root_dir, content_store, course_id): - """Imports the course in root_dir into the given course_id and verifies its content""" - # reimport - import_course_from_xml( - self.store, - self.user.id, - root_dir, - ['test_export'], - static_content_store=content_store, - target_id=course_id, - ) - - # verify content of the course - self.check_populated_course(course_id) - - # verify additional export attributes - def verify_export_attrs_removed(attributes): - """Verifies all temporary attributes added during export are removed""" - self.assertNotIn('index_in_children_list', attributes) - self.assertNotIn('parent_sequential_url', attributes) - self.assertNotIn('parent_url', attributes) - - vertical = self.store.get_item(course_id.make_usage_key('vertical', self.TEST_VERTICAL)) - verify_export_attrs_removed(vertical.xml_attributes) - - for child in vertical.get_children(): - verify_export_attrs_removed(child.xml_attributes) - if hasattr(child, 'data'): - verify_export_attrs_removed(child.data) - def test_export_course_with_metadata_only_video(self): content_store = contentstore() @@ -552,7 +461,8 @@ class ImportRequiredTestCases(ContentStoreTestCase): import_course_from_xml( self.store, self.user.id, root_dir, ['test_export_no_content_store'], static_content_store=None, - target_id=course_id + target_id=course_id, + create_if_not_present=True ) # Verify reimported course @@ -560,7 +470,6 @@ class ImportRequiredTestCases(ContentStoreTestCase): items = self.store.get_items( course_id, qualifiers={ - 'category': 'sequential', 'name': 'vertical_sequential', } ) @@ -734,7 +643,7 @@ class MiscCourseTests(ContentStoreTestCase): information but the draft child xblock has parent information. """ # Make an existing unit a draft - self.store.convert_to_draft(self.problem.location, self.user.id) + self.problem = self.store.unpublish(self.problem.location, self.user.id) root_dir = path(mkdtemp_clean()) export_course_to_xml(self.store, None, self.course.id, root_dir, 'test_export') @@ -786,19 +695,12 @@ class MiscCourseTests(ContentStoreTestCase): def test_advanced_components_require_two_clicks(self): self.check_components_on_page(['word_cloud'], ['Word cloud']) - def test_malformed_edit_unit_request(self): - # just pick one vertical - usage_key = self.course.id.make_usage_key('vertical', None) - - resp = self.client.get_html(get_url('container_handler', usage_key)) - self.assertEqual(resp.status_code, 400) - def test_edit_unit(self): """Verifies rendering the editor in all the verticals in the given test course""" self._check_verticals([self.vert_loc]) def _get_draft_counts(self, item): # lint-amnesty, pylint: disable=missing-function-docstring - cnt = 1 if getattr(item, 'is_draft', False) else 0 + cnt = 1 if not self.store.has_published_version(item) else 0 for child in item.get_children(): cnt = cnt + self._get_draft_counts(child) @@ -810,15 +712,14 @@ class MiscCourseTests(ContentStoreTestCase): Unfortunately, None = published for the revision field, so get_items() would return both draft and non-draft copies. """ - self.store.convert_to_draft(self.problem.location, self.user.id) + self.problem = self.store.unpublish(self.problem.location, self.user.id) # Query get_items() and find the html item. This should just return back a single item (not 2). direct_store_items = self.store.get_items( self.course.id, revision=ModuleStoreEnum.RevisionOption.published_only ) items_from_direct_store = [item for item in direct_store_items if item.location == self.problem.location] - self.assertEqual(len(items_from_direct_store), 1) - self.assertFalse(getattr(items_from_direct_store[0], 'is_draft', False)) + self.assertEqual(len(items_from_direct_store), 0) # Fetch from the draft store. draft_store_items = self.store.get_items( @@ -826,8 +727,6 @@ class MiscCourseTests(ContentStoreTestCase): ) items_from_draft_store = [item for item in draft_store_items if item.location == self.problem.location] self.assertEqual(len(items_from_draft_store), 1) - # TODO the below won't work for split mongo - self.assertTrue(getattr(items_from_draft_store[0], 'is_draft', False)) def test_draft_metadata(self): """ @@ -899,11 +798,11 @@ class MiscCourseTests(ContentStoreTestCase): self.assertEqual(num_drafts, 0) # put into draft - self.store.convert_to_draft(self.problem.location, self.user.id) + self.problem = self.store.unpublish(self.problem.location, self.user.id) # make sure we can query that item and verify that it is a draft draft_problem = self.store.get_item(self.problem.location) - self.assertTrue(getattr(draft_problem, 'is_draft', False)) + self.assertEqual(self.store.has_published_version(draft_problem), False) # now requery with depth course = self.store.get_course(self.course.id, depth=None) @@ -1025,15 +924,9 @@ class MiscCourseTests(ContentStoreTestCase): self.assertEqual(count, 0) def test_illegal_draft_crud_ops(self): - # this test presumes old mongo and split_draft not full split - with self.assertRaises(InvalidVersionError): - self.store.convert_to_draft(self.chapter_loc, self.user.id) - chapter = self.store.get_item(self.chapter_loc) chapter.data = 'chapter data' self.store.update_item(chapter, self.user.id) - newobject = self.store.get_item(self.chapter_loc) - self.assertFalse(getattr(newobject, 'is_draft', False)) with self.assertRaises(InvalidVersionError): self.store.unpublish(self.chapter_loc, self.user.id) @@ -1043,17 +936,11 @@ class MiscCourseTests(ContentStoreTestCase): Test that user get proper responses for urls with invalid url or asset/course key """ - resp = self.client.get_html('/c4x/CDX/123123/asset/&invalid.png') - self.assertEqual(resp.status_code, 400) - - resp = self.client.get_html('/c4x/CDX/123123/asset/invalid.png') + resp = self.client.get_html('asset-v1:CDX+123123+2012_Fall+type@asset+block@&invalid.png') self.assertEqual(resp.status_code, 404) - # Now test that 404 response is returned when user tries to access - # asset of some invalid course from split ModuleStore - with self.store.default_store(ModuleStoreEnum.Type.split): - resp = self.client.get_html('/c4x/InvalidOrg/InvalidCourse/asset/invalid.png') - self.assertEqual(resp.status_code, 404) + resp = self.client.get_html('asset-v1:CDX+123123+2012_Fall+type@asset+block@invalid.png') + self.assertEqual(resp.status_code, 404) @override_waffle_switch(waffle.ENABLE_ACCESSIBILITY_POLICY_PAGE, active=False) def test_disabled_accessibility_page(self): @@ -1078,20 +965,21 @@ class MiscCourseTests(ContentStoreTestCase): self.assertGreater(len(assets), 0) self.assertGreater(count, 0) - self.store.convert_to_draft(self.vert_loc, self.user.id) + self.store.unpublish(self.vert_loc, self.user.id) # delete the course self.store.delete_course(self.course.id, self.user.id) # assert that there's absolutely no non-draft modules in the course # this should also include all draft items - items = self.store.get_items(self.course.id) - self.assertEqual(len(items), 0) + with self.assertRaises(ItemNotFoundError): + self.store.get_items(self.course.id) - # assert that all content in the asset library is also deleted + # assert that all content in the asset library is keeped + # in case the course is later restored. assets, count = contentstore().get_all_content_for_course(self.course.id) - self.assertEqual(len(assets), 0) - self.assertEqual(count, 0) + self.assertGreater(len(assets), 0) + self.assertGreater(count, 0) def test_course_handouts_rewrites(self): """ @@ -1111,7 +999,8 @@ class MiscCourseTests(ContentStoreTestCase): # check that /static/ has been converted to the full path # note, we know the link it should be because that's what in the 'toy' course in the test data asset_key = self.course.id.make_asset_key('asset', 'handouts_sample_handout.txt') - self.assertContains(resp, str(asset_key)) + asset_url = '/'.join(str(asset_key).rsplit('@', 1)) + self.assertContains(resp, asset_url) def test_prefetch_children(self): # make sure we haven't done too many round trips to DB: @@ -1121,19 +1010,19 @@ class MiscCourseTests(ContentStoreTestCase): # so we don't need to make an extra query to compute it. # set the branch to 'publish' in order to prevent extra lookups of draft versions with self.store.branch_setting(ModuleStoreEnum.Branch.published_only, self.course.id): - with check_mongo_calls(4): - course = self.store.get_course(self.course.id, depth=2) + with check_mongo_calls(2): + course = self.store.get_course(self.course.id, depth=2, lazy=False) # make sure we pre-fetched a known sequential which should be at depth=2 - self.assertIn(self.seq_loc, course.system.module_data) + self.assertIn(BlockKey.from_usage_key(self.seq_loc), course.system.module_data) # make sure we don't have a specific vertical which should be at depth=3 - self.assertNotIn(self.vert_loc, course.system.module_data) + self.assertNotIn(BlockKey.from_usage_key(self.vert_loc), course.system.module_data) # Now, test with the branch set to draft. No extra round trips b/c it doesn't go deep enough to get # beyond direct only categories with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred, self.course.id): - with check_mongo_calls(4): + with check_mongo_calls(2): self.store.get_course(self.course.id, depth=2) def _check_verticals(self, locations): @@ -1205,21 +1094,19 @@ class ContentStoreTest(ContentStoreTestCase): self.course_data['run'] = 'run.name' self.assert_created_course() - @ddt.data(ModuleStoreEnum.Type.split, ModuleStoreEnum.Type.mongo) - def test_course_with_different_cases(self, default_store): + def test_course_with_different_cases(self): """ Tests that course can not be created with different case using an AJAX request to course handler. """ course_number = '99x' - with self.store.default_store(default_store): - # Verify create a course passes with lower case. - self.course_data['number'] = course_number.lower() - self.assert_created_course() + # Verify create a course passes with lower case. + self.course_data['number'] = course_number.lower() + self.assert_created_course() - # Verify create a course fail when same course number is provided with different case. - self.course_data['number'] = course_number.upper() - self.assert_course_creation_failed(self.duplicate_course_error) + # Verify create a course fail when same course number is provided with different case. + self.course_data['number'] = course_number.upper() + self.assert_course_creation_failed(self.duplicate_course_error) def test_create_course_check_forum_seeding(self): """Test new course creation and verify forum seeding """ @@ -1356,38 +1243,35 @@ class ContentStoreTest(ContentStoreTestCase): self.course_data['display_name'] = 'Robot Super Course Two' self.course_data['run'] = '2013_Summer' - self.assert_course_creation_failed(self.duplicate_course_error) + self.assert_created_course() - @ddt.data(ModuleStoreEnum.Type.split, ModuleStoreEnum.Type.mongo) - def test_create_course_case_change(self, default_store): + def test_create_course_case_change(self): """Test new course creation - error path due to case insensitive name equality""" self.course_data['number'] = '99x' - with self.store.default_store(default_store): + # Verify that the course was created properly. + self.assert_created_course() - # Verify that the course was created properly. - self.assert_created_course() + # Keep the copy of original org + cache_current = self.course_data['org'] - # Keep the copy of original org - cache_current = self.course_data['org'] + # Change `org` to lower case and verify that course did not get created + self.course_data['org'] = self.course_data['org'].lower() + self.assert_course_creation_failed(self.duplicate_course_error) - # Change `org` to lower case and verify that course did not get created - self.course_data['org'] = self.course_data['org'].lower() - self.assert_course_creation_failed(self.duplicate_course_error) + # Replace the org with its actual value, and keep the copy of course number. + self.course_data['org'] = cache_current + cache_current = self.course_data['number'] - # Replace the org with its actual value, and keep the copy of course number. - self.course_data['org'] = cache_current - cache_current = self.course_data['number'] + self.course_data['number'] = self.course_data['number'].upper() + self.assert_course_creation_failed(self.duplicate_course_error) - self.course_data['number'] = self.course_data['number'].upper() - self.assert_course_creation_failed(self.duplicate_course_error) + # Replace the org with its actual value, and keep the copy of course number. + self.course_data['number'] = cache_current + __ = self.course_data['run'] - # Replace the org with its actual value, and keep the copy of course number. - self.course_data['number'] = cache_current - __ = self.course_data['run'] - - self.course_data['run'] = self.course_data['run'].upper() - self.assert_course_creation_failed(self.duplicate_course_error) + self.course_data['run'] = self.course_data['run'].upper() + self.assert_course_creation_failed(self.duplicate_course_error) def test_course_substring(self): """ @@ -1515,7 +1399,9 @@ class ContentStoreTest(ContentStoreTestCase): self.assertEqual(resp.status_code, 200) data = parse_json(resp) - retarget = str(course.id.make_usage_key('chapter', 'REPLACE')).replace('REPLACE', r'([0-9]|[a-f]){3,}') + retarget = re.escape( + str(course.id.make_usage_key('chapter', 'REPLACE')) + ).replace('REPLACE', r'([0-9]|[a-f]){3,}') self.assertRegex(data['locator'], retarget) def test_capa_block(self): @@ -1625,7 +1511,7 @@ class ContentStoreTest(ContentStoreTestCase): self.assertEqual(course_block.pdf_textbooks[0]["chapters"][1]["url"], '/static/Chapter2.pdf') def test_import_into_new_course_id_wiki_slug_renamespacing(self): - # If reimporting into the same course do not change the wiki_slug. + # If reimporting into the same course change the wiki_slug. target_id = self.store.make_course_key('edX', 'toy', '2012_Fall') course_data = { 'org': target_id.org, @@ -1641,7 +1527,7 @@ class ContentStoreTest(ContentStoreTestCase): # Import a course with wiki_slug == location.course import_course_from_xml(self.store, self.user.id, TEST_DATA_DIR, ['toy'], target_id=target_id) course_block = self.store.get_course(target_id) - self.assertEqual(course_block.wiki_slug, 'toy') + self.assertEqual(course_block.wiki_slug, 'edX.toy.2012_Fall') # But change the wiki_slug if it is a different course. target_id = self.store.make_course_key('MITx', '111', '2013_Spring') @@ -1667,7 +1553,7 @@ class ContentStoreTest(ContentStoreTestCase): import_course_from_xml(self.store, self.user.id, TEST_DATA_DIR, ['simple'], create_if_not_present=True) did_load_item = False try: - course_key = self.store.make_course_key('edX', 'simple', 'problem') + course_key = self.store.make_course_key('edX', 'simple', '2012_Fall') usage_key = course_key.make_usage_key('problem', 'ps01-simple') self.store.get_item(usage_key) did_load_item = True @@ -1677,13 +1563,12 @@ class ContentStoreTest(ContentStoreTestCase): # make sure we found the item (e.g. it didn't error while loading) self.assertTrue(did_load_item) - @ddt.data(ModuleStoreEnum.Type.split, ModuleStoreEnum.Type.mongo) - def test_forum_id_generation(self, default_store): + def test_forum_id_generation(self): """ Test that a discussion item, even if it doesn't set its discussion_id, consistently generates the same one """ - course = CourseFactory.create(default_store=default_store) + course = CourseFactory.create() # create a discussion item discussion_item = self.store.create_item(self.user.id, course.id, 'discussion', 'new_component') @@ -1825,9 +1710,11 @@ class MetadataSaveTestCase(ContentStoreTestCase): """ + video_data = VideoBlock.parse_video_xml(video_sample_xml) + video_data.pop('source') self.video_descriptor = ItemFactory.create( parent_location=course.location, category='video', - **VideoBlock.parse_video_xml(video_sample_xml) + **video_data ) def test_metadata_not_persistence(self): @@ -1891,10 +1778,6 @@ class RerunCourseTest(ContentStoreTestCase): rerun_course_data.update(destination_course_data) destination_course_key = _get_course_id(self.store, destination_course_data) - # course_handler raise 404 for old mongo course - if destination_course_key.deprecated: - raise SkipTest('OldMongo Deprecation') - # post the request course_url = get_url('course_handler', destination_course_key, 'course_key_string') response = self.client.ajax_post(course_url, rerun_course_data) @@ -2154,7 +2037,7 @@ class ContentLicenseTest(ContentStoreTestCase): self.course.license = "creative-commons: BY SA" self.store.update_item(self.course, None) export_course_to_xml(self.store, content_store, self.course.id, root_dir, 'test_license') - fname = f"{self.course.scope_ids.usage_id.block_id}.xml" + fname = f"{self.course.id.run}.xml" run_file_path = root_dir / "test_license" / "course" / fname with run_file_path.open() as f: run_xml = etree.parse(f) @@ -2221,10 +2104,6 @@ def _create_course(test, course_key, course_data): """ Creates a course via an AJAX request and verifies the URL returned in the response. """ - # course_handler raise 404 for old mongo course - if course_key.deprecated: - raise SkipTest('OldMongo Deprecation') - course_url = get_url('course_handler', course_key, 'course_key_string') response = test.client.ajax_post(course_url, course_data) test.assertEqual(response.status_code, 200) diff --git a/cms/djangoapps/contentstore/tests/utils.py b/cms/djangoapps/contentstore/tests/utils.py index cae265f5ab..b58ec17502 100644 --- a/cms/djangoapps/contentstore/tests/utils.py +++ b/cms/djangoapps/contentstore/tests/utils.py @@ -8,15 +8,13 @@ import json from django.conf import settings from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user from django.test.client import Client -from opaque_keys.edx.keys import AssetKey, CourseKey +from opaque_keys.edx.keys import AssetKey from xmodule.contentstore.django import contentstore -from xmodule.modulestore import ModuleStoreEnum 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.factories import CourseFactory from xmodule.modulestore.tests.utils import ProceduralCourseTestMixin -from xmodule.modulestore.xml_importer import import_course_from_xml from xmodule.tests.test_transcripts_utils import YoutubeVideoHTMLResponse from cms.djangoapps.contentstore.utils import reverse_url @@ -125,158 +123,6 @@ class CourseTestCase(ProceduralCourseTestMixin, ModuleStoreTestCase): DRAFT_VIDEO = 'draft_video' LOCKED_ASSET_KEY = AssetKey.from_string('/c4x/edX/toy/asset/sample_static.html') - def import_and_populate_course(self): - """ - Imports the test toy course and populates it with additional test data - """ - content_store = contentstore() - import_course_from_xml(self.store, self.user.id, TEST_DATA_DIR, ['toy'], static_content_store=content_store) - course_id = CourseKey.from_string('/'.join(['edX', 'toy', '2012_Fall'])) - - # create an Orphan - # We had a bug where orphaned draft nodes caused export to fail. This is here to cover that case. - vertical = self.store.get_item(course_id.make_usage_key('vertical', self.TEST_VERTICAL), depth=1) - vertical.location = vertical.location.replace(name='no_references') - self.store.update_item(vertical, self.user.id, allow_not_found=True) - orphan_vertical = self.store.get_item(vertical.location) - self.assertEqual(orphan_vertical.location.block_id, 'no_references') - self.assertEqual(len(orphan_vertical.children), len(vertical.children)) - - # create an orphan vertical and html; we already don't try to import - # the orphaned vertical, but we should make sure we don't import - # the orphaned vertical's child html, too - orphan_draft_vertical = self.store.create_item( - self.user.id, course_id, 'vertical', self.ORPHAN_DRAFT_VERTICAL - ) - orphan_draft_html = self.store.create_item( - self.user.id, course_id, 'html', self.ORPHAN_DRAFT_HTML - ) - orphan_draft_vertical.children.append(orphan_draft_html.location) - self.store.update_item(orphan_draft_vertical, self.user.id) - - # create a Draft vertical - vertical = self.store.get_item(course_id.make_usage_key('vertical', self.TEST_VERTICAL), depth=1) - draft_vertical = self.store.convert_to_draft(vertical.location, self.user.id) - self.assertTrue(self.store.has_published_version(draft_vertical)) - - # create a Private (draft only) vertical - private_vertical = self.store.create_item(self.user.id, course_id, 'vertical', self.PRIVATE_VERTICAL) - self.assertFalse(self.store.has_published_version(private_vertical)) - - # create a Published (no draft) vertical - public_vertical = self.store.create_item(self.user.id, course_id, 'vertical', self.PUBLISHED_VERTICAL) - public_vertical = self.store.publish(public_vertical.location, self.user.id) - self.assertTrue(self.store.has_published_version(public_vertical)) - - # add the new private and new public as children of the sequential - sequential = self.store.get_item(course_id.make_usage_key('sequential', self.SEQUENTIAL)) - sequential.children.append(private_vertical.location) - sequential.children.append(public_vertical.location) - self.store.update_item(sequential, self.user.id) - - # create an html and video component to make drafts: - draft_html = self.store.create_item(self.user.id, course_id, 'html', self.DRAFT_HTML) - draft_video = self.store.create_item(self.user.id, course_id, 'video', self.DRAFT_VIDEO) - - # add them as children to the public_vertical - public_vertical.children.append(draft_html.location) - public_vertical.children.append(draft_video.location) - self.store.update_item(public_vertical, self.user.id) - # publish changes to vertical - self.store.publish(public_vertical.location, self.user.id) - # convert html/video to draft - self.store.convert_to_draft(draft_html.location, self.user.id) - self.store.convert_to_draft(draft_video.location, self.user.id) - - # lock an asset - content_store.set_attr(self.LOCKED_ASSET_KEY, 'locked', True) - - # create a non-portable link - should be rewritten in new courses - html_block = self.store.get_item(course_id.make_usage_key('html', 'nonportable')) - new_data = html_block.data = html_block.data.replace( - '/static/', - f'/c4x/{course_id.org}/{course_id.course}/asset/' - ) - self.store.update_item(html_block, self.user.id) - - html_block = self.store.get_item(html_block.location) - self.assertEqual(new_data, html_block.data) - - return course_id - - def check_populated_course(self, course_id): - """ - Verifies the content of the given course, per data that was populated in import_and_populate_course - """ - items = self.store.get_items( - course_id, - qualifiers={'category': 'vertical'}, - revision=ModuleStoreEnum.RevisionOption.published_only - ) - self.check_verticals(items) - - def verify_item_publish_state(item, publish_state): - """Verifies the publish state of the item is as expected.""" - self.assertEqual(self.store.has_published_version(item), publish_state) - - def get_and_verify_publish_state(item_type, item_name, publish_state): - """ - Gets the given item from the store and verifies the publish state - of the item is as expected. - """ - item = self.store.get_item(course_id.make_usage_key(item_type, item_name)) - verify_item_publish_state(item, publish_state) - return item - - # verify draft vertical has a published version with published children - vertical = get_and_verify_publish_state('vertical', self.TEST_VERTICAL, True) - for child in vertical.get_children(): - verify_item_publish_state(child, True) - - # verify that it has a draft too - self.assertTrue(getattr(vertical, "is_draft", False)) - - # make sure that we don't have a sequential that is in draft mode - sequential = get_and_verify_publish_state('sequential', self.SEQUENTIAL, True) - self.assertFalse(getattr(sequential, "is_draft", False)) - - # verify that we have the private vertical - private_vertical = get_and_verify_publish_state('vertical', self.PRIVATE_VERTICAL, False) - - # verify that we have the public vertical - public_vertical = get_and_verify_publish_state('vertical', self.PUBLISHED_VERTICAL, True) - - # verify that we have the draft html - draft_html = self.store.get_item(course_id.make_usage_key('html', self.DRAFT_HTML)) - self.assertTrue(getattr(draft_html, 'is_draft', False)) - - # verify that we have the draft video - draft_video = self.store.get_item(course_id.make_usage_key('video', self.DRAFT_VIDEO)) - self.assertTrue(getattr(draft_video, 'is_draft', False)) - - # verify verticals are children of sequential - for vert in [vertical, private_vertical, public_vertical]: - self.assertIn(vert.location, sequential.children) - - # verify draft html is the child of the public vertical - self.assertIn(draft_html.location, public_vertical.children) - - # verify draft video is the child of the public vertical - self.assertIn(draft_video.location, public_vertical.children) - - # verify textbook exists - course = self.store.get_course(course_id) - self.assertGreater(len(course.textbooks), 0) - - # verify asset attributes of locked asset key - self.assertAssetsEqual(self.LOCKED_ASSET_KEY, self.LOCKED_ASSET_KEY.course_key, course_id) - - # verify non-portable links are rewritten - html_block = self.store.get_item(course_id.make_usage_key('html', 'nonportable')) - self.assertIn('/static/foo.jpg', html_block.data) - - return course - def assertCoursesEqual(self, course1_id, course2_id): """ Verifies the content of the two given courses are equal