Commit ff96fbf5 by ichuang

Merge pull request #146 from MITx/cpennington/cms-xml-processing

These changes make xml import into the cms use XModuleDescriptors
parents 81a1dd89 3c60d1a9
### ###
### One-off script for importing courseware form XML format ### One-off script for importing courseware form XML format
### ###
from django.core.management.base import BaseCommand, CommandError
#import mitxmako.middleware from keystore.django import keystore
#from courseware import content_parser from raw_module import RawDescriptor
#from django.contrib.auth.models import User from lxml import etree
import os.path from fs.osfs import OSFS
from StringIO import StringIO
from mako.template import Template
from mako.lookup import TemplateLookup from mako.lookup import TemplateLookup
from collections import defaultdict
from django.core.management.base import BaseCommand from path import path
from keystore.django import keystore from x_module import XModuleDescriptor, XMLParsingSystem
unnamed_modules = 0
etree.set_default_parser(etree.XMLParser(dtd_validation=False, load_dtd=False,
remove_comments=True))
from lxml import etree
class Command(BaseCommand): class Command(BaseCommand):
help = \ help = \
''' Run FTP server.''' '''Import the specified data directory into the default keystore'''
def handle(self, *args, **options): def handle(self, *args, **options):
print args if len(args) != 3:
data_dir = args[0] raise CommandError("import requires 3 arguments: <org> <course> <data directory>")
parser = etree.XMLParser(remove_comments = True) org, course, data_dir = args
data_dir = path(data_dir)
class ImportSystem(XMLParsingSystem):
def __init__(self):
self.load_item = keystore().get_item
self.fs = OSFS(data_dir)
def process_xml(self, xml):
try:
xml_data = etree.fromstring(xml)
except:
raise CommandError("Unable to parse xml: " + xml)
if not xml_data.get('name'):
global unnamed_modules
unnamed_modules += 1
xml_data.set('name', '{tag}_{count}'.format(tag=xml_data.tag, count=unnamed_modules))
module = XModuleDescriptor.load_from_xml(etree.tostring(xml_data), self, org, course, RawDescriptor)
keystore().create_item(module.url)
if 'data' in module.definition:
keystore().update_item(module.url, module.definition['data'])
if 'children' in module.definition:
keystore().update_children(module.url, module.definition['children'])
return module
lookup = TemplateLookup(directories=[data_dir]) lookup = TemplateLookup(directories=[data_dir])
template = lookup.get_template("course.xml") template = lookup.get_template("course.xml")
course_string = template.render(groups=[]) course_string = template.render(groups=[])
course = etree.parse(StringIO(course_string), parser=parser) ImportSystem().process_xml(course_string)
elements = list(course.iter())
tag_to_category = {
# Custom tags
'videodev': 'Custom',
'slides': 'Custom',
'book': 'Custom',
'image': 'Custom',
'discuss': 'Custom',
# Simple lists
'chapter': 'Week',
'course': 'Course',
'section': defaultdict(lambda: 'Section', {
'Lab': 'Lab',
'Lecture Sequence': 'LectureSequence',
'Homework': 'Homework',
'Tutorial Index': 'TutorialIndex',
'Video': 'VideoSegment',
'Midterm': 'Exam',
'Final': 'Exam',
'Problems': 'ProblemSet',
}),
'videosequence': 'VideoSequence',
'problemset': 'ProblemSet',
'vertical': 'Section',
'sequential': 'Section',
'tab': 'Section',
# True types
'video': 'VideoSegment',
'html': 'HTML',
'problem': 'Problem',
}
name_index = 0
for e in elements:
name = e.attrib.get('name', None)
for f in elements:
if f != e and f.attrib.get('name', None) == name:
name = None
if not name:
name = "{tag}_{index}".format(tag=e.tag, index=name_index)
name_index = name_index + 1
if e.tag in tag_to_category:
category = tag_to_category[e.tag]
if isinstance(category, dict):
category = category[e.get('format')]
category = category.replace('/', '-')
name = name.replace('/', '-')
e.set('url', 'i4x://mit.edu/6002xs12/{category}/{name}'.format(
category=category,
name=name))
else:
print "Skipping element with tag", e.tag
def handle_skip(e):
print "Skipping ", e
results = {}
def handle_custom(e):
data = {'type':'i4x://mit.edu/6002xs12/tag/{tag}'.format(tag=e.tag),
'attrib':dict(e.attrib)}
results[e.attrib['url']] = {'data':data}
def handle_list(e):
if e.attrib.get("class", None) == "tutorials":
return
children = [le.attrib['url'] for le in e.getchildren()]
results[e.attrib['url']] = {'children':children}
def handle_video(e):
url = e.attrib['url']
clip_url = url.replace('VideoSegment', 'VideoClip')
# Take: 0.75:izygArpw-Qo,1.0:p2Q6BrNhdh8,1.25:1EeWXzPdhSA,1.50:rABDYkeK0x8
# Make: [(0.75, 'izygArpw-Qo'), (1.0, 'p2Q6BrNhdh8'), (1.25, '1EeWXzPdhSA'), (1.5, 'rABDYkeK0x8')]
youtube_str = e.attrib['youtube']
youtube_list = [(float(x), y) for x,y in map(lambda x:x.split(':'), youtube_str.split(','))]
clip_infos = [{ "status": "ready",
"format": "youtube",
"sane": True,
"location": "youtube",
"speed": speed,
"id": youtube_id,
"size": None} \
for (speed, youtube_id) \
in youtube_list]
results[clip_url] = {'data':{'clip_infos':clip_infos}}
results[url] = {'children' : [{'url':clip_url}]}
def handle_html(e):
if 'src' in e.attrib:
text = open(data_dir+'html/'+e.attrib['src']).read()
else:
textlist=[e.text]+[etree.tostring(elem) for elem in e]+[e.tail]
textlist=[i for i in textlist if type(i)==str]
text = "".join(textlist)
results[e.attrib['url']] = {'data':{'text':text}}
def handle_problem(e):
data = open(os.path.join(data_dir, 'problems', e.attrib['filename']+'.xml')).read()
results[e.attrib['url']] = {'data':{'statement':data}}
element_actions = {# Inside HTML ==> Skip these
'a': handle_skip,
'h1': handle_skip,
'h2': handle_skip,
'hr': handle_skip,
'strong': handle_skip,
'ul': handle_skip,
'li': handle_skip,
'p': handle_skip,
# Custom tags
'videodev': handle_custom,
'slides': handle_custom,
'book': handle_custom,
'image': handle_custom,
'discuss': handle_custom,
# Simple lists
'chapter': handle_list,
'course': handle_list,
'sequential': handle_list,
'vertical': handle_list,
'section': handle_list,
'videosequence': handle_list,
'problemset': handle_list,
'tab': handle_list,
# True types
'video': handle_video,
'html': handle_html,
'problem': handle_problem,
}
for e in elements:
element_actions[e.tag](e)
for k in results:
keystore().create_item(k, 'Piotr Mitros')
if 'data' in results[k]:
keystore().update_item(k, results[k]['data'])
if 'children' in results[k]:
keystore().update_children(k, results[k]['children'])
...@@ -11,7 +11,7 @@ def index(request): ...@@ -11,7 +11,7 @@ def index(request):
org = 'mit.edu' org = 'mit.edu'
course = '6002xs12' course = '6002xs12'
name = '6.002 Spring 2012' name = '6.002 Spring 2012'
course = keystore().get_item(['i4x', org, course, 'Course', name]) course = keystore().get_item(['i4x', org, course, 'course', name])
weeks = course.get_children() weeks = course.get_children()
return render_to_response('index.html', {'weeks': weeks}) return render_to_response('index.html', {'weeks': weeks})
...@@ -21,6 +21,7 @@ def edit_item(request): ...@@ -21,6 +21,7 @@ def edit_item(request):
item = keystore().get_item(item_id) item = keystore().get_item(item_id)
return render_to_response('unit.html', { return render_to_response('unit.html', {
'contents': item.get_html(), 'contents': item.get_html(),
'js_module': item.js_module_name(),
'type': item.type, 'type': item.type,
'name': item.name, 'name': item.name,
}) })
......
...@@ -158,6 +158,7 @@ PIPELINE_CSS = { ...@@ -158,6 +158,7 @@ PIPELINE_CSS = {
PIPELINE_ALWAYS_RECOMPILE = ['sass/base-style.scss'] PIPELINE_ALWAYS_RECOMPILE = ['sass/base-style.scss']
from xmodule.x_module import XModuleDescriptor from xmodule.x_module import XModuleDescriptor
from xmodule.raw_module import RawDescriptor
js_file_dir = PROJECT_ROOT / "static" / "coffee" / "module" js_file_dir = PROJECT_ROOT / "static" / "coffee" / "module"
try: try:
os.makedirs(js_file_dir) os.makedirs(js_file_dir)
...@@ -168,7 +169,7 @@ except OSError as exc: ...@@ -168,7 +169,7 @@ except OSError as exc:
raise raise
module_js_sources = [] module_js_sources = []
for xmodule in XModuleDescriptor.load_classes(): for xmodule in XModuleDescriptor.load_classes() + [RawDescriptor]:
js = xmodule.get_javascript() js = xmodule.get_javascript()
for filetype in ('coffee', 'js'): for filetype in ('coffee', 'js'):
for idx, fragment in enumerate(js.get(filetype, [])): for idx, fragment in enumerate(js.get(filetype, [])):
......
class @CMS class @CMS
@setHeight = =>
windowHeight = $(this).height()
@contentHeight = windowHeight - 29
@bind = => @bind = =>
$('a.module-edit').click -> $('a.module-edit').click ->
CMS.edit_item($(this).attr('id')) CMS.edit_item($(this).attr('id'))
return false return false
$(window).bind('resize', CMS.setHeight)
@edit_item = (id) => @edit_item = (id) =>
$.get('/edit_item', {id: id}, (data) => $.get('/edit_item', {id: id}, (data) =>
$('#module-html').empty().append(data) $('#module-html').empty().append(data)
CMS.bind() CMS.bind()
$('section.edit-pane').show() $('body.content .cal').css('height', @contentHeight)
$('body').addClass('content') $('body').addClass('content')
$('section.edit-pane').show()
new Unit('unit-wrapper', id) new Unit('unit-wrapper', id)
) )
...@@ -78,6 +84,7 @@ $ -> ...@@ -78,6 +84,7 @@ $ ->
$('.problem-new a').click -> $('.problem-new a').click ->
$('section.edit-pane').show() $('section.edit-pane').show()
return false return false
CMS.setHeight()
CMS.bind() CMS.bind()
<section id="unit-wrapper" class="${type}"> <section id="unit-wrapper" class="${js_module}">
<header> <header>
<section> <section>
<h1 class="editable">${name}</h1> <h1 class="editable">${name}</h1>
......
...@@ -38,7 +38,7 @@ ...@@ -38,7 +38,7 @@
% for week in weeks: % for week in weeks:
<li> <li>
<header> <header>
<h1><a href="#" class="week-edit" id="${week.url}">${week.name}</a></h1> <h1><a href="#" class="module-edit" id="${week.url}">${week.name}</a></h1>
<ul> <ul>
% if week.goals: % if week.goals:
% for goal in week.goals: % for goal in week.goals:
......
<section class="raw-edit">
<section class="meta wip">
<section class="status-settings">
<ul>
<li><a href="#" class="current">Scrap</a></li>
<li><a href="#">Draft</a></li>
<li><a href="#">Proofed</a></li>
<li><a href="#">Published</a></li>
</ul>
<a href="#" class="settings">Settings</a>
</section>
<section class="author">
<dl>
<dt>Last modified:</dt>
<dd>mm/dd/yy</dd>
<dt>By</dt>
<dd>Anant Agarwal</dd>
</dl>
</section>
<section class="tags">
<div>
<h2>Tags:</h2>
<p class="editable">Click to edit</p>
</div>
<div>
<h2>Goal</h2>
<p class="editable">Click to edit</p>
</div>
</section>
</section>
<textarea name="" class="edit-box" rows="8" cols="40">${data}</textarea>
<pre class="preview">${data | h}</pre>
<div class="actions wip">
<a href="" class="save-update">Save &amp; Update</a>
<a href="#" class="cancel">Cancel</a>
</div>
<%include file="notes.html"/>
</section>
...@@ -125,7 +125,7 @@ class ModuleStore(object): ...@@ -125,7 +125,7 @@ class ModuleStore(object):
""" """
An abstract interface for a database backend that stores XModuleDescriptor instances An abstract interface for a database backend that stores XModuleDescriptor instances
""" """
def get_item(self, location): def get_item(self, location, default_class=None):
""" """
Returns an XModuleDescriptor instance for the item at location. Returns an XModuleDescriptor instance for the item at location.
If location.revision is None, returns the item with the most If location.revision is None, returns the item with the most
...@@ -136,6 +136,8 @@ class ModuleStore(object): ...@@ -136,6 +136,8 @@ class ModuleStore(object):
If no object is found at that location, raises keystore.exceptions.ItemNotFoundError If no object is found at that location, raises keystore.exceptions.ItemNotFoundError
location: Something that can be passed to Location location: Something that can be passed to Location
default_class: An XModuleDescriptor subclass to use if no plugin matching the
location is found
""" """
raise NotImplementedError raise NotImplementedError
......
...@@ -8,6 +8,7 @@ from __future__ import absolute_import ...@@ -8,6 +8,7 @@ from __future__ import absolute_import
from django.conf import settings from django.conf import settings
from .mongo import MongoModuleStore from .mongo import MongoModuleStore
from raw_module import RawDescriptor
_KEYSTORES = {} _KEYSTORES = {}
...@@ -16,6 +17,9 @@ def keystore(name='default'): ...@@ -16,6 +17,9 @@ def keystore(name='default'):
global _KEYSTORES global _KEYSTORES
if name not in _KEYSTORES: if name not in _KEYSTORES:
_KEYSTORES[name] = MongoModuleStore(**settings.KEYSTORE[name]) # TODO (cpennington): Load the default class from a string
_KEYSTORES[name] = MongoModuleStore(
default_class=RawDescriptor,
**settings.KEYSTORE[name])
return _KEYSTORES[name] return _KEYSTORES[name]
...@@ -8,7 +8,7 @@ class MongoModuleStore(ModuleStore): ...@@ -8,7 +8,7 @@ class MongoModuleStore(ModuleStore):
""" """
A Mongodb backed ModuleStore A Mongodb backed ModuleStore
""" """
def __init__(self, host, db, collection, port=27017): def __init__(self, host, db, collection, port=27017, default_class=None):
self.collection = pymongo.connection.Connection( self.collection = pymongo.connection.Connection(
host=host, host=host,
port=port port=port
...@@ -16,6 +16,7 @@ class MongoModuleStore(ModuleStore): ...@@ -16,6 +16,7 @@ class MongoModuleStore(ModuleStore):
# Force mongo to report errors, at the expense of performance # Force mongo to report errors, at the expense of performance
self.collection.safe = True self.collection.safe = True
self.default_class = default_class
def get_item(self, location): def get_item(self, location):
""" """
...@@ -28,6 +29,8 @@ class MongoModuleStore(ModuleStore): ...@@ -28,6 +29,8 @@ class MongoModuleStore(ModuleStore):
If no object is found at that location, raises keystore.exceptions.ItemNotFoundError If no object is found at that location, raises keystore.exceptions.ItemNotFoundError
location: Something that can be passed to Location location: Something that can be passed to Location
default_class: An XModuleDescriptor subclass to use if no plugin matching the
location is found
""" """
query = {} query = {}
...@@ -45,9 +48,10 @@ class MongoModuleStore(ModuleStore): ...@@ -45,9 +48,10 @@ class MongoModuleStore(ModuleStore):
if item is None: if item is None:
raise ItemNotFoundError(location) raise ItemNotFoundError(location)
return XModuleDescriptor.load_from_json(item, DescriptorSystem(self.get_item)) return XModuleDescriptor.load_from_json(
item, DescriptorSystem(self.get_item), self.default_class)
def create_item(self, location, editor): def create_item(self, location):
""" """
Create an empty item at the specified location with the supplied editor Create an empty item at the specified location with the supplied editor
...@@ -55,7 +59,6 @@ class MongoModuleStore(ModuleStore): ...@@ -55,7 +59,6 @@ class MongoModuleStore(ModuleStore):
""" """
self.collection.insert({ self.collection.insert({
'location': Location(location).dict(), 'location': Location(location).dict(),
'editor': editor
}) })
def update_item(self, location, data): def update_item(self, location, data):
......
...@@ -16,9 +16,31 @@ class HtmlModuleDescriptor(MakoModuleDescriptor): ...@@ -16,9 +16,31 @@ class HtmlModuleDescriptor(MakoModuleDescriptor):
""" """
mako_template = "widgets/html-edit.html" mako_template = "widgets/html-edit.html"
# TODO (cpennington): Make this into a proper module
js = {'coffee': [resource_string(__name__, 'js/module/html.coffee')]} js = {'coffee': [resource_string(__name__, 'js/module/html.coffee')]}
js_module = 'HTML'
@classmethod
def from_xml(cls, xml_data, system, org=None, course=None):
"""
Creates an instance of this descriptor from the supplied xml_data.
This may be overridden by subclasses
xml_data: A string of xml that will be translated into data and children for
this module
system: An XModuleSystem for interacting with external resources
org and course are optional strings that will be used in the generated modules
url identifiers
"""
xml_object = etree.fromstring(xml_data)
return cls(
system,
definition={'data': {'text': xml_data}},
location=['i4x',
org,
course,
xml_object.tag,
xml_object.get('name')]
)
class Module(XModule): class Module(XModule):
id_attribute = 'filename' id_attribute = 'filename'
......
class @Raw
constructor: (@id) ->
@edit_box = $("##{@id} .edit-box")
@preview = $("##{@id} .preview")
@edit_box.on('input', =>
@preview.empty().text(@edit_box.val())
)
save: -> @edit_box.val()
...@@ -12,7 +12,11 @@ class MakoModuleDescriptor(XModuleDescriptor): ...@@ -12,7 +12,11 @@ class MakoModuleDescriptor(XModuleDescriptor):
the descriptor as the `module` parameter to that template the descriptor as the `module` parameter to that template
""" """
def get_context(self):
"""
Return the context to render the mako template with
"""
return {'module': self}
def get_html(self): def get_html(self):
return render_to_string(self.mako_template, { return render_to_string(self.mako_template, self.get_context())
'module': self
})
from pkg_resources import resource_string
from mako_module import MakoModuleDescriptor
from lxml import etree
class RawDescriptor(MakoModuleDescriptor):
"""
Module that provides a raw editing view of it's data and children
"""
mako_template = "widgets/raw-edit.html"
js = {'coffee': [resource_string(__name__, 'js/module/raw.coffee')]}
js_module = 'Raw'
def get_context(self):
return {
'module': self,
'data': self.definition['data'],
}
@classmethod
def from_xml(cls, xml_data, system, org=None, course=None):
"""
Creates an instance of this descriptor from the supplied xml_data.
This may be overridden by subclasses
xml_data: A string of xml that will be translated into data and children for
this module
system: An XModuleSystem for interacting with external resources
org and course are optional strings that will be used in the generated modules
url identifiers
"""
xml_object = etree.fromstring(xml_data)
return cls(
system,
definition={'data': xml_data},
location=['i4x',
org,
course,
xml_object.tag,
xml_object.get('name')]
)
...@@ -115,5 +115,23 @@ class Module(XModule): ...@@ -115,5 +115,23 @@ class Module(XModule):
self.rendered = False self.rendered = False
class SectionDescriptor(MakoModuleDescriptor): class SequenceDescriptor(MakoModuleDescriptor):
mako_template = 'widgets/sequence-edit.html' mako_template = 'widgets/sequence-edit.html'
@classmethod
def from_xml(cls, xml_data, system, org=None, course=None):
xml_object = etree.fromstring(xml_data)
children = [
system.process_xml(etree.tostring(child_module)).url
for child_module in xml_object
]
return cls(
system, {'children': children},
location=['i4x',
org,
course,
xml_object.tag,
xml_object.get('name')]
)
...@@ -13,18 +13,14 @@ setup( ...@@ -13,18 +13,14 @@ setup(
# for a description of entry_points # for a description of entry_points
entry_points={ entry_points={
'xmodule.v1': [ 'xmodule.v1': [
"Course = seq_module:SectionDescriptor", "chapter = seq_module:SequenceDescriptor",
"Week = seq_module:SectionDescriptor", "course = seq_module:SequenceDescriptor",
"Section = seq_module:SectionDescriptor", "html = html_module:HtmlModuleDescriptor",
"LectureSequence = seq_module:SectionDescriptor", "section = translation_module:SemanticSectionDescriptor",
"Lab = seq_module:SectionDescriptor", "sequential = seq_module:SequenceDescriptor",
"Homework = seq_module:SectionDescriptor", "vertical = seq_module:SequenceDescriptor",
"TutorialIndex = seq_module:SectionDescriptor", "problemset = seq_module:SequenceDescriptor",
"Exam = seq_module:SectionDescriptor", "videosequence = seq_module:SequenceDescriptor",
"VideoSegment = video_module:VideoSegmentDescriptor",
"ProblemSet = seq_module:SectionDescriptor",
"Problem = capa_module:CapaModuleDescriptor",
"HTML = html_module:HtmlModuleDescriptor",
] ]
} }
) )
"""
These modules exist to translate old format XML into newer, semantic forms
"""
from x_module import XModuleDescriptor
from lxml import etree
from functools import wraps
import logging
log = logging.getLogger(__name__)
def process_includes(fn):
"""
Wraps a XModuleDescriptor.from_xml method, and modifies xml_data to replace
any immediate child <include> items with the contents of the file that they are
supposed to include
"""
@wraps(fn)
def from_xml(cls, xml_data, system, org=None, course=None):
xml_object = etree.fromstring(xml_data)
next_include = xml_object.find('include')
while next_include is not None:
file = next_include.get('file')
if file is not None:
try:
ifp = system.fs.open(file)
except Exception:
log.exception('Error in problem xml include: %s' % (etree.tostring(next_include, pretty_print=True)))
log.exception('Cannot find file %s in %s' % (file, dir))
raise
try:
# read in and convert to XML
incxml = etree.XML(ifp.read())
except Exception:
log.exception('Error in problem xml include: %s' % (etree.tostring(next_include, pretty_print=True)))
log.exception('Cannot parse XML in %s' % (file))
raise
# insert new XML into tree in place of inlcude
parent = next_include.getparent()
parent.insert(parent.index(next_include), incxml)
parent.remove(next_include)
next_include = xml_object.find('include')
return fn(cls, etree.tostring(xml_object), system, org, course)
return from_xml
class SemanticSectionDescriptor(XModuleDescriptor):
@classmethod
@process_includes
def from_xml(cls, xml_data, system, org=None, course=None):
"""
Removes sections single child elements in favor of just embedding the child element
"""
xml_object = etree.fromstring(xml_data)
if len(xml_object) == 1:
for (key, val) in xml_object.items():
if key == 'format':
continue
xml_object[0].set(key, val)
return system.process_xml(etree.tostring(xml_object[0]))
else:
xml_object.tag = 'sequence'
return system.process_xml(etree.tostring(xml_object))
...@@ -15,8 +15,24 @@ class ModuleMissingError(Exception): ...@@ -15,8 +15,24 @@ class ModuleMissingError(Exception):
class Plugin(object): class Plugin(object):
"""
Base class for a system that uses entry_points to load plugins.
Implementing classes are expected to have the following attributes:
entry_point: The name of the entry point to load plugins from
"""
@classmethod @classmethod
def load_class(cls, identifier): def load_class(cls, identifier, default=None):
"""
Loads a single class intance specified by identifier. If identifier
specifies more than a single class, then logs a warning and returns the first
class identified.
If default is not None, will return default if no entry_point matching identifier
is found. Otherwise, will raise a ModuleMissingError
"""
identifier = identifier.lower()
classes = list(pkg_resources.iter_entry_points(cls.entry_point, name=identifier)) classes = list(pkg_resources.iter_entry_points(cls.entry_point, name=identifier))
if len(classes) > 1: if len(classes) > 1:
log.warning("Found multiple classes for {entry_point} with identifier {id}: {classes}. Returning the first one.".format( log.warning("Found multiple classes for {entry_point} with identifier {id}: {classes}. Returning the first one.".format(
...@@ -25,6 +41,8 @@ class Plugin(object): ...@@ -25,6 +41,8 @@ class Plugin(object):
classes=", ".join(class_.module_name for class_ in classes))) classes=", ".join(class_.module_name for class_ in classes)))
if len(classes) == 0: if len(classes) == 0:
if default is not None:
return default
raise ModuleMissingError(identifier) raise ModuleMissingError(identifier)
return classes[0].load() return classes[0].load()
...@@ -160,9 +178,10 @@ class XModuleDescriptor(Plugin): ...@@ -160,9 +178,10 @@ class XModuleDescriptor(Plugin):
""" """
entry_point = "xmodule.v1" entry_point = "xmodule.v1"
js = {} js = {}
js_module = None
@staticmethod @staticmethod
def load_from_json(json_data, system): def load_from_json(json_data, system, default_class=None):
""" """
This method instantiates the correct subclass of XModuleDescriptor based This method instantiates the correct subclass of XModuleDescriptor based
on the contents of json_data. on the contents of json_data.
...@@ -170,7 +189,10 @@ class XModuleDescriptor(Plugin): ...@@ -170,7 +189,10 @@ class XModuleDescriptor(Plugin):
json_data must contain a 'location' element, and must be suitable to be json_data must contain a 'location' element, and must be suitable to be
passed into the subclasses `from_json` method. passed into the subclasses `from_json` method.
""" """
class_ = XModuleDescriptor.load_class(json_data['location']['category']) class_ = XModuleDescriptor.load_class(
json_data['location']['category'],
default_class
)
return class_.from_json(json_data, system) return class_.from_json(json_data, system)
@classmethod @classmethod
...@@ -184,6 +206,37 @@ class XModuleDescriptor(Plugin): ...@@ -184,6 +206,37 @@ class XModuleDescriptor(Plugin):
""" """
return cls(system=system, **json_data) return cls(system=system, **json_data)
@staticmethod
def load_from_xml(xml_data, system, org=None, course=None, default_class=None):
"""
This method instantiates the correct subclass of XModuleDescriptor based
on the contents of xml_data.
xml_data must be a string containing valid xml
system is an XMLParsingSystem
org and course are optional strings that will be used in the generated modules
url identifiers
"""
class_ = XModuleDescriptor.load_class(
etree.fromstring(xml_data).tag,
default_class
)
return class_.from_xml(xml_data, system, org, course)
@classmethod
def from_xml(cls, xml_data, system, org=None, course=None):
"""
Creates an instance of this descriptor from the supplied xml_data.
This may be overridden by subclasses
xml_data: A string of xml that will be translated into data and children for
this module
system is an XMLParsingSystem
org and course are optional strings that will be used in the generated modules
url identifiers
"""
raise NotImplementedError('Modules must implement from_xml to be parsable from xml')
@classmethod @classmethod
def get_javascript(cls): def get_javascript(cls):
""" """
...@@ -196,6 +249,12 @@ class XModuleDescriptor(Plugin): ...@@ -196,6 +249,12 @@ class XModuleDescriptor(Plugin):
""" """
return cls.js return cls.js
def js_module_name(self):
"""
Return the name of the javascript class to instantiate when
this module descriptor is loaded for editing
"""
return self.js_module
def __init__(self, def __init__(self,
system, system,
...@@ -230,15 +289,12 @@ class XModuleDescriptor(Plugin): ...@@ -230,15 +289,12 @@ class XModuleDescriptor(Plugin):
self._child_instances = None self._child_instances = None
def get_children(self, categories=None): def get_children(self):
"""Returns a list of XModuleDescriptor instances for the children of this module""" """Returns a list of XModuleDescriptor instances for the children of this module"""
if self._child_instances is None: if self._child_instances is None:
self._child_instances = [self.system.load_item(child) for child in self.definition['children']] self._child_instances = [self.system.load_item(child) for child in self.definition.get('children', [])]
if categories is None: return self._child_instances
return self._child_instances
else:
return [child for child in self._child_instances if child.type in categories]
def get_html(self): def get_html(self):
""" """
...@@ -277,7 +333,18 @@ class XModuleDescriptor(Plugin): ...@@ -277,7 +333,18 @@ class XModuleDescriptor(Plugin):
class DescriptorSystem(object): class DescriptorSystem(object):
def __init__(self, load_item): def __init__(self, load_item):
""" """
load_item: Takes a Location and returns and XModuleDescriptor load_item: Takes a Location and returns an XModuleDescriptor
""" """
self.load_item = load_item self.load_item = load_item
class XMLParsingSystem(DescriptorSystem):
def __init__(self, load_item, process_xml, fs):
"""
process_xml: Takes an xml string, and returns the the XModuleDescriptor created from that xml
fs: A Filesystem object that contains all of the xml resources needed to parse
the course
"""
self.process_xml = process_xml
self.fs = fs
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment