diff --git a/common/lib/xmodule/xmodule/modulestore/xml.py b/common/lib/xmodule/xmodule/modulestore/xml.py index 8c4c373d4f..0c0d750882 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml.py +++ b/common/lib/xmodule/xmodule/modulestore/xml.py @@ -1,3 +1,4 @@ +import json import logging import os import re @@ -149,7 +150,7 @@ class XMLModuleStore(ModuleStoreBase): for course_dir in course_dirs: self.try_load_course(course_dir) - def try_load_course(self,course_dir): + def try_load_course(self, course_dir): ''' Load a course, keeping track of errors as we go along. ''' @@ -170,7 +171,27 @@ class XMLModuleStore(ModuleStoreBase): ''' String representation - for debugging ''' - return 'data_dir=%s, %d courses, %d modules' % (self.data_dir,len(self.courses),len(self.modules)) + return 'data_dir=%s, %d courses, %d modules' % ( + self.data_dir, len(self.courses), len(self.modules)) + + def load_policy(self, policy_path, tracker): + """ + Attempt to read a course policy from policy_path. If the file + exists, but is invalid, log an error and return {}. + + If the policy loads correctly, returns the deserialized version. + """ + if not os.path.exists(policy_path): + return {} + try: + with open(policy_path) as f: + return json.load(f) + except (IOError, ValueError) as err: + msg = "Error loading course policy from {}".format(policy_path) + tracker(msg) + log.warning(msg + " " + str(err)) + return {} + def load_course(self, course_dir, tracker): """ @@ -214,6 +235,11 @@ class XMLModuleStore(ModuleStoreBase): system = ImportSystem(self, org, course, course_dir, tracker) course_descriptor = system.process_xml(etree.tostring(course_data)) + policy_path = self.data_dir / course_dir / 'policy.json' + + policy = self.load_policy(policy_path, tracker) + XModuleDescriptor.apply_policy(course_descriptor, policy) + # NOTE: The descriptors end up loading somewhat bottom up, which # breaks metadata inheritance via get_children(). Instead # (actually, in addition to, for now), we do a final inheritance pass diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py index 06449dc37f..af3f04e366 100644 --- a/common/lib/xmodule/xmodule/x_module.py +++ b/common/lib/xmodule/xmodule/x_module.py @@ -298,6 +298,14 @@ class XModule(HTMLSnippet): return "" +def policy_key(location): + """ + Get the key for a location in a policy file. (Since the policy file is + specific to a course, it doesn't need the full location url). + """ + return '{cat}/{name}'.format(cat=location.category, name=location.name) + + class XModuleDescriptor(Plugin, HTMLSnippet): """ An XModuleDescriptor is a specification for an element of a course. This @@ -416,6 +424,24 @@ class XModuleDescriptor(Plugin, HTMLSnippet): return dict((k,v) for k,v in self.metadata.items() if k not in self._inherited_metadata) + + @staticmethod + def apply_policy(node, policy): + """ + Given a descriptor, traverse all its descendants and update its metadata + with the policy. + + Notes: + - this does not propagate inherited metadata. The caller should + call compute_inherited_metadata after applying the policy. + - metadata specified in the policy overrides metadata in the xml + """ + k = policy_key(node.location) + if k in policy: + node.metadata.update(policy[k]) + for c in node.get_children(): + XModuleDescriptor.apply_policy(c, policy) + @staticmethod def compute_inherited_metadata(node): """Given a descriptor, traverse all of its descendants and do metadata diff --git a/lms/djangoapps/courseware/management/commands/metadata_to_json.py b/lms/djangoapps/courseware/management/commands/metadata_to_json.py new file mode 100644 index 0000000000..0f48e93319 --- /dev/null +++ b/lms/djangoapps/courseware/management/commands/metadata_to_json.py @@ -0,0 +1,98 @@ +""" +A script to walk a course xml tree, generate a dictionary of all the metadata, +and print it out as a json dict. +""" +import os +import sys +import json + +from collections import OrderedDict +from path import path + +from django.core.management.base import BaseCommand + +from xmodule.modulestore.xml import XMLModuleStore +from xmodule.x_module import policy_key + +def import_course(course_dir, verbose=True): + course_dir = path(course_dir) + data_dir = course_dir.dirname() + course_dirs = [course_dir.basename()] + + # No default class--want to complain if it doesn't find plugins for any + # module. + modulestore = XMLModuleStore(data_dir, + default_class=None, + eager=True, + course_dirs=course_dirs) + + def str_of_err(tpl): + (msg, exc_str) = tpl + return '{msg}\n{exc}'.format(msg=msg, exc=exc_str) + + courses = modulestore.get_courses() + + n = len(courses) + if n != 1: + sys.stderr.write('ERROR: Expect exactly 1 course. Loaded {n}: {lst}\n'.format( + n=n, lst=courses)) + return None + + course = courses[0] + errors = modulestore.get_item_errors(course.location) + if len(errors) != 0: + sys.stderr.write('ERRORs during import: {}\n'.format('\n'.join(map(str_of_err, errors)))) + + return course + +def node_metadata(node): + # make a copy + to_export = ('format', 'display_name', + 'graceperiod', 'showanswer', 'rerandomize', + 'start', 'due', 'graded', 'hide_from_toc', + 'ispublic', 'xqa_key') + + orig = node.own_metadata + d = {k: orig[k] for k in to_export if k in orig} + return d + +def get_metadata(course): + d = OrderedDict({}) + queue = [course] + while len(queue) > 0: + node = queue.pop() + d[policy_key(node.location)] = node_metadata(node) + # want to print first children first, so put them at the end + # (we're popping from the end) + queue.extend(reversed(node.get_children())) + return d + + +def print_metadata(course_dir, output): + course = import_course(course_dir) + if course: + meta = get_metadata(course) + result = json.dumps(meta, indent=4) + if output: + with file(output, 'w') as f: + f.write(result) + else: + print result + + +class Command(BaseCommand): + help = """Imports specified course.xml and prints its +metadata as a json dict. + +Usage: metadata_to_json PATH-TO-COURSE-DIR OUTPUT-PATH + +if OUTPUT-PATH isn't given, print to stdout. +""" + def handle(self, *args, **options): + n = len(args) + if n < 1 or n > 2: + print Command.help + return + + output_path = args[1] if n > 1 else None + print_metadata(args[0], output_path)