diff --git a/cms/djangoapps/contentstore/management/commands/import.py b/cms/djangoapps/contentstore/management/commands/import.py index 690e3dbea0..3e0ccdd5e8 100644 --- a/cms/djangoapps/contentstore/management/commands/import.py +++ b/cms/djangoapps/contentstore/management/commands/import.py @@ -82,7 +82,7 @@ class Command(BaseCommand): def handle_list(e): if e.attrib.get("class", None) == "tutorials": return - children = [{'url':le.attrib['url']} for le in e.getchildren()] + children = [le.attrib['url'] for le in e.getchildren()] results[e.attrib['url']] = {'children':children} def handle_video(e): diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 429fb6c26b..64bde14869 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -3,11 +3,10 @@ from keystore.django import keystore from django.contrib.auth.decorators import login_required -@login_required -def calendar(request, org, course): - weeks = keystore.get_children_for_item(['i4x', org, course, 'Course', None]) - return render_to_response('calendar.html', {'weeks': weeks}) - - def index(request): - return render_to_response('index.html', {}) + # FIXME (cpennington): These need to be read in from the active user + org = 'mit.edu' + course = '6002xs12' + course = keystore.get_item(['i4x', org, course, 'Course', None]) + weeks = course.get_children() + return render_to_response('index.html', {'weeks': weeks}) diff --git a/cms/lib/keystore/__init__.py b/cms/lib/keystore/__init__.py index d0a24be797..61c797241d 100644 --- a/cms/lib/keystore/__init__.py +++ b/cms/lib/keystore/__init__.py @@ -37,7 +37,7 @@ class Location(object): self.update(location.list()) def url(self): - return "i4x://{org}/{course}/{category}/{name}".format(**self.dict()) + return "{tag}://{org}/{course}/{category}/{name}".format(**self.dict()) def list(self): return [self.tag, self.org, self.course, self.category, self.name] @@ -54,10 +54,9 @@ class Location(object): class KeyStore(object): - def get_children_for_item(self, location): + def get_item(self, location): """ - Returns the children for the most recent revision of the object - with the specified location. + Returns an XModuleDescriptor instance for the item at location If no object is found at that location, raises keystore.exceptions.ItemNotFoundError diff --git a/cms/lib/keystore/mongo.py b/cms/lib/keystore/mongo.py index d29afb4bd2..d9760909c9 100644 --- a/cms/lib/keystore/mongo.py +++ b/cms/lib/keystore/mongo.py @@ -1,6 +1,7 @@ import pymongo from . import KeyStore, Location from .exceptions import ItemNotFoundError, InsufficientSpecificationError +from xmodule.x_module import XModuleDescriptor class MongoKeyStore(KeyStore): @@ -12,11 +13,22 @@ class MongoKeyStore(KeyStore): host=host, port=port )[db][collection] - + # Force mongo to report errors, at the expense of performance self.collection.safe = True - def get_children_for_item(self, location): + def get_item(self, location): + """ + Returns an XModuleDescriptor instance for the item at location + + If no object is found at that location, raises keystore.exceptions.ItemNotFoundError + + Searches for all matches of a partially specifed location, but raises an + keystore.exceptions.InsufficientSpecificationError if more + than a single object matches the query. + + location: Something that can be passed to Location + """ query = dict( ('location.{key}'.format(key=key), val) for (key, val) @@ -25,7 +37,6 @@ class MongoKeyStore(KeyStore): ) items = self.collection.find( query, - fields={'children': True}, sort=[('revision', pymongo.ASCENDING)], limit=1, ) @@ -35,7 +46,7 @@ class MongoKeyStore(KeyStore): if items.count() == 0: raise ItemNotFoundError(location) - return items[0]['children'] + return XModuleDescriptor.load_from_json(items[0], self.get_item) def create_item(self, location, editor): """ diff --git a/cms/templates/widgets/navigation.html b/cms/templates/widgets/navigation.html index 28b92979d9..75d581dd73 100644 --- a/cms/templates/widgets/navigation.html +++ b/cms/templates/widgets/navigation.html @@ -35,9 +35,10 @@
    + % for week in weeks:
  1. -

    Week 1

    +

    ${week.name}

    • Goal title: This is a goal that will be in the header of the week
    • Goal title two: This is another goal for this week so that students have two things to learn
    • @@ -64,125 +65,7 @@ <%include file="module-dropdown.html"/>
  2. -
  3. -
    -

    Week 2

    -
      -
    • Another title This is the goal for the week
    • -
    -
    - - -
  4. -
  5. -
    -

    Week 3

    -
      -
    • Another title This is the goal for the week
    • -
    -
    - - -
  6. -
  7. -
    -

    Week 4

    -
      -
    • Another title This is the goal for the week
    • -
    • Goal title two: This is another fgoal for this week so that students have two things to learn
    • -
    -
    - - -
  8. - -
  9. -
    -

    Week 5

    -
      -
    • Please create a learning goal for this week
    • -
    -
    - - -
  10. -
  11. -
    -

    Week 6

    -
      -
    • Please create a learning goal for this week
    • -
    -
    - - -
  12. -
  13. -
    -

    Week 7

    -
      -
    • Please create a learning goal for this week
    • -
    -
    - - -
  14. + %endfor
  15. Course Scratch Pad

    diff --git a/common/lib/xmodule/seq_module.py b/common/lib/xmodule/seq_module.py index f643eaab4a..4ae4fb3813 100644 --- a/common/lib/xmodule/seq_module.py +++ b/common/lib/xmodule/seq_module.py @@ -95,3 +95,11 @@ class Module(XModule): self.position = int(system.get('position')) self.rendered = False + + +class CourseModuleDescriptor(XModuleDescriptor): + pass + + +class ChapterModuleDescriptor(XModuleDescriptor): + pass diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py new file mode 100644 index 0000000000..6a659b6852 --- /dev/null +++ b/common/lib/xmodule/setup.py @@ -0,0 +1,14 @@ +from setuptools import setup, find_packages + +setup( + name="XModule", + version="0.1", + packages=find_packages(), + install_requires=['distribute'], + entry_points={ + 'xmodule.v1': [ + "Course = seq_module:CourseModuleDescriptor", + "Chapter = seq_module:ChapterModuleDescriptor", + ] + } +) diff --git a/common/lib/xmodule/x_module.py b/common/lib/xmodule/x_module.py index d783694fee..23025df50b 100644 --- a/common/lib/xmodule/x_module.py +++ b/common/lib/xmodule/x_module.py @@ -1,8 +1,34 @@ from lxml import etree +import pkg_resources +import logging +from keystore import Location + +log = logging.getLogger('mitx.' + __name__) def dummy_track(event_type, event): pass + +class ModuleMissingError(Exception): + pass + + +class Plugin(object): + @classmethod + def load_class(cls, identifier): + classes = list(pkg_resources.iter_entry_points(cls.entry_point, name=identifier)) + if len(classes) > 1: + log.warning("Found multiple classes for {entry_point} with identifier {id}: {classes}. Returning the first one.".format( + entry_point=cls.entry_point, + id=identifier, + classes=", ".join([class_.module_name for class_ in classes]))) + + if len(classes) == 0: + raise ModuleMissingError(identifier) + + return classes[0].load() + + class XModule(object): ''' Implements a generic learning module. Initialized on access with __init__, first time with state=None, and @@ -24,8 +50,8 @@ class XModule(object): or a CAPA input type ''' return ['xmodule'] - def get_name(): - name = self.__xmltree.get(name) + def get_name(self): + name = self.__xmltree.get('name') if name: return name else: @@ -98,15 +124,42 @@ class XModule(object): return "" -class XModuleDescriptor(object): - def __init__(self, xml = None, json = None): - if not xml and not json: - raise "XModuleDescriptor must be initalized with XML or JSON" - if not xml: - raise NotImplementedError("Code does not have support for JSON yet") - - self.xml = xml - self.json = json +class XModuleDescriptor(Plugin): + + entry_point = "xmodule.v1" + + @staticmethod + def load_from_json(json_data, load_item): + class_ = XModuleDescriptor.load_class(json_data['location']['category']) + return class_.from_json(json_data, load_item) + + @classmethod + def from_json(cls, json_data, load_item): + """ + Creates an instance of this descriptor from the supplied json_data. + + json_data: Json data specifying the data, children, and metadata for the descriptor + load_item: A function that takes an i4x url and returns a module descriptor + """ + return cls(load_item=load_item, **json_data) + + def __init__(self, + load_item, + data=None, + children=None, + **kwargs): + self.load_item = load_item + self.data = data if data is not None else {} + self.children = children if children is not None else [] + self.name = Location(kwargs.get('location')).name + self._child_instances = None + + def get_children(self): + """Returns a list of XModuleDescriptor instances for the children of this module""" + if self._child_instances is None: + self._child_instances = [self.load_item(child) for child in self.children] + return self._child_instances + def get_xml(self): ''' For conversions between JSON and legacy XML representations. diff --git a/requirements.txt b/requirements.txt index 122276ea39..37a30b6cef 100644 --- a/requirements.txt +++ b/requirements.txt @@ -27,3 +27,4 @@ pymongo django_nose nosexcover rednose +-e common/lib/xmodule