Add support metadata in policy.json
* if there is a policy.json in the course dir, read it
* file format is a dict with keys {category}/{url_name}, and values metadata dictionaries
* apply the policy, overwriting keys that are in the xml
* then do metadata inheritance, inheriting any overwritten keys.
* also a management cmd to generate a policy.json from a course dir.
This commit is contained in:
@@ -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 '<XMLModuleStore>data_dir=%s, %d courses, %d modules' % (self.data_dir,len(self.courses),len(self.modules))
|
||||
return '<XMLModuleStore>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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user