From 4569ee9daa643804fd2476d46d156a3ecad6fa99 Mon Sep 17 00:00:00 2001 From: Brian Wilson Date: Thu, 19 Dec 2013 18:53:08 -0500 Subject: [PATCH 1/2] Add options to dump_course_structure to output inherited metadata. --- .../commands/dump_course_structure.py | 44 ++++++++++++++++--- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/lms/djangoapps/courseware/management/commands/dump_course_structure.py b/lms/djangoapps/courseware/management/commands/dump_course_structure.py index 3debfa1af15a..1a0f3b437835 100644 --- a/lms/djangoapps/courseware/management/commands/dump_course_structure.py +++ b/lms/djangoapps/courseware/management/commands/dump_course_structure.py @@ -23,10 +23,11 @@ from django.core.management.base import BaseCommand, CommandError from xmodule.modulestore.django import modulestore -from xmodule.modulestore.inheritance import own_metadata - +from xmodule.modulestore.inheritance import own_metadata, compute_inherited_metadata +from xblock.fields import Scope FILTER_LIST = ['xml_attributes', 'checklists'] +INHERITED_FILTER_LIST = ['children', 'xml_attributes', 'checklists'] class Command(BaseCommand): @@ -41,6 +42,14 @@ class Command(BaseCommand): action='store', default='default', help='Name of the modulestore'), + make_option('--inherited', + action='store_true', + default=False, + help='Whether to include inherited metadata'), + make_option('--inherited_defaults', + action='store_true', + default=False, + help='Whether to include default values of inherited metadata'), ) def handle(self, *args, **options): @@ -62,14 +71,18 @@ def handle(self, *args, **options): if course is None: raise CommandError("Invalid course_id") + # precompute inherited metadata at the course level, if needed: + if options['inherited']: + compute_inherited_metadata(course) + # Convert course data to dictionary and dump it as JSON to stdout - info = dump_module(course) + info = dump_module(course, inherited=options['inherited'], defaults=options['inherited_defaults']) return json.dumps(info, indent=2, sort_keys=True) -def dump_module(module, destination=None): +def dump_module(module, destination=None, inherited=False, defaults=False): """ Add the module and all its children to the destination dictionary in as a flat structure. @@ -83,10 +96,29 @@ def dump_module(module, destination=None): destination[module.location.url()] = { 'category': module.location.category, 'children': module.children if hasattr(module, 'children') else [], - 'metadata': filtered_metadata + 'metadata': filtered_metadata, } + if inherited: + # when calculating inherited metadata, don't include existing + # locally-defined metadata + inherited_metadata_filter_list = list(filtered_metadata.keys()) + inherited_metadata_filter_list.extend(INHERITED_FILTER_LIST) + + def is_inherited(field): + if field.name in inherited_metadata_filter_list: + return False + elif field.scope != Scope.settings: + return False + elif defaults == True: + return True + else: + return field.values != field.default + + inherited_metadata = {field.name: field.read_json(module) for field in module.fields.values() if is_inherited(field)} + destination[module.location.url()]['inherited_metadata'] = inherited_metadata + for child in module.get_children(): - dump_module(child, destination) + dump_module(child, destination, inherited, defaults) return destination From d0fea4290241a2d93b5591fc58e146813bd401ac Mon Sep 17 00:00:00 2001 From: Brian Wilson Date: Mon, 30 Dec 2013 13:32:57 -0500 Subject: [PATCH 2/2] Add tests for inherited metadata to test_dump_course, and move to commands/tests dir. --- .../tests/test_clean_history.py | 0 .../commands/tests/test_dump_course.py} | 48 ++++++++++++++++++- 2 files changed, 46 insertions(+), 2 deletions(-) rename lms/djangoapps/courseware/management/{ => commands}/tests/test_clean_history.py (100%) rename lms/djangoapps/courseware/{tests/test_commands.py => management/commands/tests/test_dump_course.py} (67%) diff --git a/lms/djangoapps/courseware/management/tests/test_clean_history.py b/lms/djangoapps/courseware/management/commands/tests/test_clean_history.py similarity index 100% rename from lms/djangoapps/courseware/management/tests/test_clean_history.py rename to lms/djangoapps/courseware/management/commands/tests/test_clean_history.py diff --git a/lms/djangoapps/courseware/tests/test_commands.py b/lms/djangoapps/courseware/management/commands/tests/test_dump_course.py similarity index 67% rename from lms/djangoapps/courseware/tests/test_commands.py rename to lms/djangoapps/courseware/management/commands/tests/test_dump_course.py index e73aea1426db..e45ed8b23543 100644 --- a/lms/djangoapps/courseware/tests/test_commands.py +++ b/lms/djangoapps/courseware/management/commands/tests/test_dump_course.py @@ -10,6 +10,7 @@ from django.core.management import call_command from django.test.utils import override_settings +from django.test.testcases import TestCase from courseware.tests.modulestore_config import TEST_DATA_XML_MODULESTORE from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE @@ -24,7 +25,7 @@ TEST_COURSE_ID = 'edX/simple/2012_Fall' -class CommandsTestBase(object): +class CommandsTestBase(TestCase): """ Base class for testing different django commands. @@ -66,6 +67,15 @@ def test_dump_course_structure(self): dump = json.loads(output) + # check that all elements in the course structure have metadata, + # but not inherited metadata: + for element_name in dump: + element = dump[element_name] + self.assertIn('metadata', element) + self.assertIn('children', element) + self.assertIn('category', element) + self.assertNotIn('inherited_metadata', element) + # Check a few elements in the course dump parent_id = 'i4x://edX/simple/chapter/Overview' @@ -81,10 +91,44 @@ def test_dump_course_structure(self): self.assertEqual(len(dump[video_id]['metadata']), 4) self.assertIn('youtube_id_1_0', dump[video_id]['metadata']) - # Check if there is the right number of elements + # Check if there are the right number of elements self.assertEqual(len(dump), 16) + def test_dump_inherited_course_structure(self): + args = [TEST_COURSE_ID] + kwargs = {'modulestore': 'default', 'inherited': True} + output = self.call_command('dump_course_structure', *args, **kwargs) + dump = json.loads(output) + # check that all elements in the course structure have inherited metadata, + # and that it contains a particular value as well: + for element_name in dump: + element = dump[element_name] + self.assertIn('metadata', element) + self.assertIn('children', element) + self.assertIn('category', element) + self.assertIn('inherited_metadata', element) + self.assertIsNone(element['inherited_metadata']['ispublic']) + # ... but does not contain inherited metadata containing a default value: + self.assertNotIn('due', element['inherited_metadata']) + + def test_dump_inherited_course_structure_with_defaults(self): + args = [TEST_COURSE_ID] + kwargs = {'modulestore': 'default', 'inherited': True, 'inherited_defaults': True} + output = self.call_command('dump_course_structure', *args, **kwargs) + dump = json.loads(output) + # check that all elements in the course structure have inherited metadata, + # and that it contains a particular value as well: + for element_name in dump: + element = dump[element_name] + self.assertIn('metadata', element) + self.assertIn('children', element) + self.assertIn('category', element) + self.assertIn('inherited_metadata', element) + self.assertIsNone(element['inherited_metadata']['ispublic']) + # ... and contains inherited metadata containing a default value: + self.assertIsNone(element['inherited_metadata']['due']) + def test_export_course(self): tmp_dir = path(mkdtemp()) filename = tmp_dir / 'test.tar.gz'