Cleanup intertwined descriptor and keystore code
This commit is contained in:
committed by
Matthew Mongeau
parent
05dcd76bf1
commit
b0b728c711
@@ -1,13 +1,6 @@
|
||||
"""
|
||||
This module provides an abstraction for working objects that conceptually have
|
||||
the following attributes:
|
||||
|
||||
location: An identifier for an item, of which there might be many revisions
|
||||
children: A list of urls for other items required to fully define this object
|
||||
data: A set of nested data needed to define this object
|
||||
editor: The editor/owner of the object
|
||||
parents: Url pointers for objects that this object was derived from
|
||||
revision: What revision of the item this is
|
||||
This module provides an abstraction for working with XModuleDescriptors
|
||||
that are stored in a database an accessible using their Location as an identifier
|
||||
"""
|
||||
|
||||
import re
|
||||
@@ -123,27 +116,26 @@ class Location(object):
|
||||
'revision': self.revision}
|
||||
|
||||
|
||||
class KeyStore(object):
|
||||
class ModuleStore(object):
|
||||
"""
|
||||
An abstract interface for a database backend that stores XModuleDescriptor instances
|
||||
"""
|
||||
def get_item(self, location):
|
||||
"""
|
||||
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 most item with the most
|
||||
recent revision
|
||||
|
||||
If any segment of the location is None except revision, raises
|
||||
keystore.exceptions.InsufficientSpecificationError
|
||||
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
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
# TODO (cpennington): Replace with clone_item
|
||||
def create_item(self, location, editor):
|
||||
"""
|
||||
Create an empty item at the specified location with the supplied editor
|
||||
|
||||
location: Something that can be passed to Location
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def update_item(self, location, data):
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
"""
|
||||
Module that provides a connection to the keystore specified in the django settings.
|
||||
|
||||
Passes settings.KEYSTORE as kwargs to MongoKeyStore
|
||||
Passes settings.KEYSTORE as kwargs to MongoModuleStore
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
from django.conf import settings
|
||||
from .mongo import MongoKeyStore
|
||||
from .mongo import MongoModuleStore
|
||||
|
||||
_KEYSTORE = None
|
||||
_KEYSTORES = {}
|
||||
|
||||
|
||||
def keystore():
|
||||
global _KEYSTORE
|
||||
def keystore(name='default'):
|
||||
global _KEYSTORES
|
||||
|
||||
if _KEYSTORE is None:
|
||||
_KEYSTORE = MongoKeyStore(**settings.KEYSTORE)
|
||||
if name not in _KEYSTORES:
|
||||
_KEYSTORES[name] = MongoModuleStore(**settings.KEYSTORE[name])
|
||||
|
||||
return _KEYSTORE
|
||||
return _KEYSTORES[name]
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import pymongo
|
||||
from . import KeyStore, Location
|
||||
from . import ModuleStore, Location
|
||||
from .exceptions import ItemNotFoundError, InsufficientSpecificationError
|
||||
from xmodule.x_module import XModuleDescriptor
|
||||
from xmodule.x_module import XModuleDescriptor, XModuleSystem
|
||||
|
||||
|
||||
class MongoKeyStore(KeyStore):
|
||||
class MongoModuleStore(ModuleStore):
|
||||
"""
|
||||
A Mongodb backed KeyStore
|
||||
A Mongodb backed ModuleStore
|
||||
"""
|
||||
def __init__(self, host, db, collection, port=27017):
|
||||
self.collection = pymongo.connection.Connection(
|
||||
@@ -19,34 +19,33 @@ class MongoKeyStore(KeyStore):
|
||||
|
||||
def get_item(self, location):
|
||||
"""
|
||||
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 most item with the most
|
||||
recent revision
|
||||
|
||||
If any segment of the location is None except revision, raises
|
||||
keystore.exceptions.InsufficientSpecificationError
|
||||
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)
|
||||
in Location(location).dict().items()
|
||||
if val is not None
|
||||
)
|
||||
items = self.collection.find(
|
||||
|
||||
query = {}
|
||||
for key, val in Location(location).dict().iteritems():
|
||||
if key != 'revision' and val is None:
|
||||
raise InsufficientSpecificationError(location)
|
||||
|
||||
if val is not None:
|
||||
query['location.{key}'.format(key=key)] = val
|
||||
|
||||
item = self.collection.find_one(
|
||||
query,
|
||||
sort=[('revision', pymongo.ASCENDING)],
|
||||
limit=1,
|
||||
)
|
||||
if items.count() > 1:
|
||||
raise InsufficientSpecificationError(location)
|
||||
|
||||
if items.count() == 0:
|
||||
if item is None:
|
||||
raise ItemNotFoundError(location)
|
||||
|
||||
return XModuleDescriptor.load_from_json(items[0], self.get_item)
|
||||
return XModuleDescriptor.load_from_json(item, XModuleSystem(self.get_item))
|
||||
|
||||
def create_item(self, location, editor):
|
||||
"""
|
||||
@@ -72,7 +71,7 @@ class MongoKeyStore(KeyStore):
|
||||
# atomic update syntax
|
||||
self.collection.update(
|
||||
{'location': Location(location).dict()},
|
||||
{'$set': {'data': data}}
|
||||
{'$set': {'definition.data': data}}
|
||||
)
|
||||
|
||||
def update_children(self, location, children):
|
||||
@@ -88,5 +87,5 @@ class MongoKeyStore(KeyStore):
|
||||
# atomic update syntax
|
||||
self.collection.update(
|
||||
{'location': Location(location).dict()},
|
||||
{'$set': {'children': children}}
|
||||
{'$set': {'definition.children': children}}
|
||||
)
|
||||
|
||||
@@ -97,22 +97,5 @@ class Module(XModule):
|
||||
self.rendered = False
|
||||
|
||||
|
||||
class WeekDescriptor(XModuleDescriptor):
|
||||
|
||||
def get_goals(self):
|
||||
"""
|
||||
Return a list of Goal XModuleDescriptors that are children
|
||||
of this Week
|
||||
"""
|
||||
return [child for child in self.get_children() if child.type == 'Goal']
|
||||
|
||||
def get_non_goals(self):
|
||||
"""
|
||||
Return a list of non-Goal XModuleDescriptors that are children of
|
||||
this Week
|
||||
"""
|
||||
return [child for child in self.get_children() if child.type != 'Goal']
|
||||
|
||||
|
||||
class SectionDescriptor(XModuleDescriptor):
|
||||
pass
|
||||
|
||||
@@ -5,10 +5,13 @@ setup(
|
||||
version="0.1",
|
||||
packages=find_packages(),
|
||||
install_requires=['distribute'],
|
||||
|
||||
# See http://guide.python-distribute.org/creation.html#entry-points
|
||||
# for a description of entry_points
|
||||
entry_points={
|
||||
'xmodule.v1': [
|
||||
"Course = seq_module:SectionDescriptor",
|
||||
"Week = seq_module:WeekDescriptor",
|
||||
"Week = seq_module:SectionDescriptor",
|
||||
"Section = seq_module:SectionDescriptor",
|
||||
"LectureSequence = seq_module:SectionDescriptor",
|
||||
"Lab = seq_module:SectionDescriptor",
|
||||
|
||||
@@ -21,7 +21,7 @@ class Plugin(object):
|
||||
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])))
|
||||
classes=", ".join(class_.module_name for class_ in classes)))
|
||||
|
||||
if len(classes) == 0:
|
||||
raise ModuleMissingError(identifier)
|
||||
@@ -125,47 +125,82 @@ class XModule(object):
|
||||
|
||||
|
||||
class XModuleDescriptor(Plugin):
|
||||
"""
|
||||
An XModuleDescriptor is a specification for an element of a course. This could
|
||||
be a problem, an organizational element (a group of content), or a segment of video,
|
||||
for example.
|
||||
|
||||
XModuleDescriptors are independent and agnostic to the current student state on a
|
||||
problem. They handle the editing interface used by instructors to create a problem,
|
||||
and can generate XModules (which do know about student state).
|
||||
"""
|
||||
entry_point = "xmodule.v1"
|
||||
|
||||
@staticmethod
|
||||
def load_from_json(json_data, load_item):
|
||||
def load_from_json(json_data, system):
|
||||
"""
|
||||
This method instantiates the correct subclass of XModuleDescriptor based
|
||||
on the contents of json_data.
|
||||
|
||||
json_data must contain a 'location' element, and must be suitable to be
|
||||
passed into the subclasses `from_json` method.
|
||||
"""
|
||||
class_ = XModuleDescriptor.load_class(json_data['location']['category'])
|
||||
return class_.from_json(json_data, load_item)
|
||||
return class_.from_json(json_data, system)
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json_data, load_item):
|
||||
def from_json(cls, json_data, system):
|
||||
"""
|
||||
Creates an instance of this descriptor from the supplied json_data.
|
||||
This may be overridden by subclasses
|
||||
|
||||
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
|
||||
system: An XModuleSystem for interacting with external resources
|
||||
"""
|
||||
return cls(load_item=load_item, **json_data)
|
||||
return cls(system=system, **json_data)
|
||||
|
||||
def __init__(self,
|
||||
load_item,
|
||||
data=None,
|
||||
children=None,
|
||||
system,
|
||||
definition=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 []
|
||||
"""
|
||||
Construct a new XModuleDescriptor. The only required arguments are the
|
||||
system, used for interaction with external resources, and the definition,
|
||||
which specifies all the data needed to edit and display the problem (but none
|
||||
of the associated metadata that handles recordkeeping around the problem).
|
||||
|
||||
This allows for maximal flexibility to add to the interface while preserving
|
||||
backwards compatibility.
|
||||
|
||||
system: An XModuleSystem for interacting with external resources
|
||||
definition: A dict containing `data` and `children` representing the problem definition
|
||||
|
||||
Current arguments passed in kwargs:
|
||||
location: A keystore.Location object indicating the name and ownership of this problem
|
||||
goals: A list of strings of learning goals associated with this module
|
||||
"""
|
||||
self.system = system
|
||||
self.definition = definition if definition is not None else {}
|
||||
self.name = Location(kwargs.get('location')).name
|
||||
self.type = Location(kwargs.get('location')).category
|
||||
|
||||
# For now, we represent goals as a list of strings, but this
|
||||
# is one of the things that we are going to be iterating on heavily
|
||||
# to find the best teaching method
|
||||
self.goals = kwargs.get('goals', [])
|
||||
|
||||
self._child_instances = None
|
||||
|
||||
def get_children(self, categories=None):
|
||||
"""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]
|
||||
self._child_instances = [self.system.load_item(child) for child in self.definition['children']]
|
||||
|
||||
if categories is None:
|
||||
return self._child_instances
|
||||
else:
|
||||
return [child for child in self._child_instances if child.type in categories]
|
||||
|
||||
|
||||
def get_xml(self):
|
||||
''' For conversions between JSON and legacy XML representations.
|
||||
'''
|
||||
@@ -192,3 +227,12 @@ class XModuleDescriptor(Plugin):
|
||||
# Full ==> what we edit
|
||||
# '''
|
||||
# raise NotImplementedError
|
||||
|
||||
|
||||
class DescriptorSystem(object):
|
||||
def __init__(self, load_item):
|
||||
"""
|
||||
load_item: Takes a Location and returns and XModuleDescriptor
|
||||
"""
|
||||
|
||||
self.load_item = load_item
|
||||
|
||||
Reference in New Issue
Block a user