Commit b0b728c7 by Calen Pennington Committed by Matthew Mongeau

Cleanup intertwined descriptor and keystore code

parent 05dcd76b
......@@ -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>
......
"""
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):
......
"""
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]
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
......
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