Cleanup intertwined descriptor and keystore code
This commit is contained in:
committed by
Matthew Mongeau
parent
05dcd76bf1
commit
b0b728c711
@@ -7,9 +7,11 @@ DEBUG = True
|
||||
TEMPLATE_DEBUG = DEBUG
|
||||
|
||||
KEYSTORE = {
|
||||
'host': 'localhost',
|
||||
'db': 'mongo_base',
|
||||
'collection': 'key_store',
|
||||
'default': {
|
||||
'host': 'localhost',
|
||||
'db': 'mongo_base',
|
||||
'collection': 'key_store',
|
||||
}
|
||||
}
|
||||
|
||||
DATABASES = {
|
||||
|
||||
@@ -40,14 +40,18 @@
|
||||
<header>
|
||||
<h1><a href="#">${week.name}</a></h1>
|
||||
<ul>
|
||||
% for goal in week.get_goals():
|
||||
<li class="goal editable"><strong>${goal.name}:</strong>${goal.data}</li>
|
||||
% endfor
|
||||
% if week.goals:
|
||||
% for goal in week.goals:
|
||||
<li class="goal editable">${goal}</li>
|
||||
% endfor
|
||||
% else:
|
||||
<li class="goal editable">Please create a learning goal for this week</li>
|
||||
% endif
|
||||
</ul>
|
||||
</header>
|
||||
|
||||
<ul>
|
||||
% for module in week.get_non_goals():
|
||||
% for module in week.get_children():
|
||||
<li class="${module.type}">
|
||||
<a href="#" class="${module.type}-edit">${module.name}</a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -44,10 +44,27 @@ You should be familiar with the following. If you're not, go read some docs...
|
||||
|
||||
### Common libraries
|
||||
|
||||
- x_modules -- generic learning modules. *x* can be sequence, video, template, html, vertical, capa, etc. These are the things that one puts inside sections in the course structure. Modules know how to render themselves to html, how to score themselves, and handle ajax calls from the front end.
|
||||
- x_modules take a 'system context' parameter, which helps isolate xmodules from any particular application, so they can be used in many places. The modules should make no references to Django (though there are still a few left). The system context knows how to render things, track events, complain about 404s, etc.
|
||||
- TODO: document the system context interface--it's different in `x_module.XModule.__init__` and in `x_module tests.py` (do this in the code, not here)
|
||||
- in `common/lib/xmodule`
|
||||
- xmodule: generic learning modules. *x* can be sequence, video, template, html,
|
||||
vertical, capa, etc. These are the things that one puts inside sections
|
||||
in the course structure.
|
||||
|
||||
- XModuleDescriptor: This defines the problem and all data and UI needed to edit
|
||||
that problem. It is unaware of any student data, but can be used to retrieve
|
||||
an XModule, which is aware of that student state.
|
||||
|
||||
- XModule: The XModule is a problem instance that is particular to a student. It knows
|
||||
how to render itself to html to display the problem, how to score itself,
|
||||
and how to handle ajax calls from the front end.
|
||||
|
||||
- Both XModule and XModuleDescriptor take system context parameters. These are named
|
||||
ModuleSystem and DescriptorSystem respectively. These help isolate the XModules
|
||||
from any interactions with external resources that they require.
|
||||
|
||||
For instance, the DescriptorSystem has a function to load an XModuleDescriptor
|
||||
from a Location object, and the ModuleSystem knows how to render things,
|
||||
track events, and complain about 404s
|
||||
- TODO: document the system context interface--it's different in `x_module.XModule.__init__` and in `x_module tests.py` (do this in the code, not here)
|
||||
- in `common/lib/xmodule`
|
||||
|
||||
- capa modules -- defines `LoncapaProblem` and many related things.
|
||||
- in `common/lib/capa`
|
||||
@@ -76,7 +93,14 @@ The LMS is a django site, with root in `lms/`. It runs in many different enviro
|
||||
|
||||
- See `lms/urls.py` for the wirings of urls to views.
|
||||
|
||||
- Tracking: there is support for basic tracking of client-side events in `lms/djangoapps/track`.
|
||||
- Tracking: there is support for basic tracking of client-side events in `lms/djangoapps/track`.
|
||||
|
||||
### CMS
|
||||
|
||||
The CMS is a django site, with root in `cms`. It can run in a number of different
|
||||
environments, defined in `cms/envs`.
|
||||
|
||||
- Core rendering path: Still TBD
|
||||
|
||||
### Other modules
|
||||
|
||||
|
||||
Reference in New Issue
Block a user