Commit c19f7dd5 by Calen Pennington

Merge pull request #122 from MITx/cpennington/cms-review-cleanup

Cleanup from https://github.com/MITx/mitx/pull/120
parents 6d1db3ba a099aaa9
...@@ -4,9 +4,10 @@ from django.contrib.auth.decorators import login_required ...@@ -4,9 +4,10 @@ from django.contrib.auth.decorators import login_required
def index(request): def index(request):
# FIXME (cpennington): These need to be read in from the active user # TODO (cpennington): These need to be read in from the active user
org = 'mit.edu' org = 'mit.edu'
course = '6002xs12' course = '6002xs12'
course = keystore().get_item(['i4x', org, course, 'Course', None]) name = '6.002 Spring 2012'
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})
...@@ -27,15 +27,12 @@ from path import path ...@@ -27,15 +27,12 @@ from path import path
PROJECT_ROOT = path(__file__).abspath().dirname().dirname() # /mitx/cms PROJECT_ROOT = path(__file__).abspath().dirname().dirname() # /mitx/cms
COMMON_ROOT = PROJECT_ROOT.dirname() / "common" COMMON_ROOT = PROJECT_ROOT.dirname() / "common"
ENV_ROOT = PROJECT_ROOT.dirname().dirname() # virtualenv dir /mitx is in ENV_ROOT = PROJECT_ROOT.dirname().dirname() # virtualenv dir /mitx is in
ASKBOT_ROOT = ENV_ROOT / "askbot-devel"
COURSES_ROOT = ENV_ROOT / "data" COURSES_ROOT = ENV_ROOT / "data"
# FIXME: To support multiple courses, we should walk the courses dir at startup # FIXME: To support multiple courses, we should walk the courses dir at startup
DATA_DIR = COURSES_ROOT DATA_DIR = COURSES_ROOT
sys.path.append(ENV_ROOT) sys.path.append(ENV_ROOT)
sys.path.append(ASKBOT_ROOT)
sys.path.append(ASKBOT_ROOT / "askbot" / "deps")
sys.path.append(PROJECT_ROOT / 'djangoapps') sys.path.append(PROJECT_ROOT / 'djangoapps')
sys.path.append(PROJECT_ROOT / 'lib') sys.path.append(PROJECT_ROOT / 'lib')
sys.path.append(COMMON_ROOT / 'djangoapps') sys.path.append(COMMON_ROOT / 'djangoapps')
...@@ -145,5 +142,4 @@ INSTALLED_APPS = ( ...@@ -145,5 +142,4 @@ INSTALLED_APPS = (
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'contentstore', 'contentstore',
'instructor',
) )
...@@ -7,9 +7,11 @@ DEBUG = True ...@@ -7,9 +7,11 @@ DEBUG = True
TEMPLATE_DEBUG = DEBUG TEMPLATE_DEBUG = DEBUG
KEYSTORE = { KEYSTORE = {
'host': 'localhost', 'default': {
'db': 'mongo_base', 'host': 'localhost',
'collection': 'key_store', 'db': 'mongo_base',
'collection': 'key_store',
}
} }
DATABASES = { DATABASES = {
......
...@@ -5,7 +5,9 @@ try: ...@@ -5,7 +5,9 @@ try:
imp.find_module('settings') # Assumed to be in the same directory. imp.find_module('settings') # Assumed to be in the same directory.
except ImportError: except ImportError:
import sys import sys
sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n" % __file__) sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. "
"It appears you've customized things.\nYou'll have to run django-admin.py, "
"passing it your settings module.\n" % __file__)
sys.exit(1) sys.exit(1)
import settings import settings
......
...@@ -40,14 +40,18 @@ ...@@ -40,14 +40,18 @@
<header> <header>
<h1><a href="#">${week.name}</a></h1> <h1><a href="#">${week.name}</a></h1>
<ul> <ul>
% for goal in week.get_goals(): % if week.goals:
<li class="goal editable"><strong>${goal.name}:</strong>${goal.data}</li> % for goal in week.goals:
% endfor <li class="goal editable">${goal}</li>
% endfor
% else:
<li class="goal editable">Please create a learning goal for this week</li>
% endif
</ul> </ul>
</header> </header>
<ul> <ul>
% for module in week.get_non_goals(): % for module in week.get_children():
<li class="${module.type}"> <li class="${module.type}">
<a href="#" class="${module.type}-edit">${module.name}</a> <a href="#" class="${module.type}-edit">${module.name}</a>
<a href="#" class="draggable">handle</a> <a href="#" class="draggable">handle</a>
......
...@@ -5,7 +5,5 @@ from django.conf.urls.defaults import patterns, url ...@@ -5,7 +5,5 @@ from django.conf.urls.defaults import patterns, url
# admin.autodiscover() # admin.autodiscover()
urlpatterns = patterns('', urlpatterns = patterns('',
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/calendar/', 'contentstore.views.calendar', name='calendar'),
url(r'^accounts/login/', 'instructor.views.do_login', name='login'),
url(r'^$', 'contentstore.views.index', name='index'), url(r'^$', 'contentstore.views.index', name='index'),
) )
""" """
This module provides an abstraction for working objects that conceptually have This module provides an abstraction for working with XModuleDescriptors
the following attributes: that are stored in a database an accessible using their Location as an identifier
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
""" """
import re
from .exceptions import InvalidLocationError
URL_RE = re.compile("""
(?P<tag>[^:]+)://
(?P<org>[^/]+)/
(?P<course>[^/]+)/
(?P<category>[^/]+)/
(?P<name>[^/]+)
(/(?P<revision>[^/]+))?
""", re.VERBOSE)
class Location(object): class Location(object):
''' Encodes a location. '''
Can be: Encodes a location.
* String (url)
* Tuple Locations representations of URLs of the
* Dictionary form {tag}://{org}/{course}/{category}/{name}[/{revision}]
However, they can also be represented a dictionaries (specifying each component),
tuples or list (specified in order), or as strings of the url
''' '''
def __init__(self, location): def __init__(self, location):
"""
Create a new location that is a clone of the specifed one.
location - Can be any of the following types:
string: should be of the form {tag}://{org}/{course}/{category}/{name}[/{revision}]
list: should be of the form [tag, org, course, category, name, revision]
dict: should be of the form {
'tag': tag,
'org': org,
'course': course,
'category': category,
'name': name,
'revision': revision,
}
Location: another Location object
In both the dict and list forms, the revision is optional, and can be ommitted.
None of the components of a location may contain the '/' character
Components may be set to None, which may be interpreted by some contexts to mean
wildcard selection
"""
self.update(location) self.update(location)
def update(self, location): def update(self, location):
"""
Update this instance with data from another Location object.
location: can take the same forms as specified by `__init__`
"""
self.tag = self.org = self.course = self.category = self.name = self.revision = None
if isinstance(location, basestring): if isinstance(location, basestring):
self.tag = location.split('/')[0][:-1] match = URL_RE.match(location)
(self.org, self.course, self.category, self.name) = location.split('/')[2:] if match is None:
raise InvalidLocationError(location)
else:
self.update(match.groupdict())
elif isinstance(location, list): elif isinstance(location, list):
(self.tag, self.org, self.course, self.category, self.name) = location if len(location) not in (5, 6):
raise InvalidLocationError(location)
(self.tag, self.org, self.course, self.category, self.name) = location[0:5]
self.revision = location[5] if len(location) == 6 else None
elif isinstance(location, dict): elif isinstance(location, dict):
self.tag = location['tag'] try:
self.org = location['org'] self.tag = location['tag']
self.course = location['course'] self.org = location['org']
self.category = location['category'] self.course = location['course']
self.name = location['name'] self.category = location['category']
self.name = location['name']
except KeyError:
raise InvalidLocationError(location)
self.revision = location.get('revision')
elif isinstance(location, Location): elif isinstance(location, Location):
self.update(location.list()) self.update(location.list())
else:
raise InvalidLocationError(location)
for val in self.list():
if val is not None and '/' in val:
raise InvalidLocationError(location)
def __str__(self):
return self.url()
def url(self): def url(self):
return "{tag}://{org}/{course}/{category}/{name}".format(**self.dict()) """
Return a string containing the URL for this location
"""
url = "{tag}://{org}/{course}/{category}/{name}".format(**self.dict())
if self.revision:
url += "/" + self.revision
return url
def list(self): def list(self):
return [self.tag, self.org, self.course, self.category, self.name] """
Return a list representing this location
"""
return [self.tag, self.org, self.course, self.category, self.name, self.revision]
def dict(self): def dict(self):
"""
Return a dictionary representing this location
"""
return {'tag': self.tag, return {'tag': self.tag,
'org': self.org, 'org': self.org,
'course': self.course, 'course': self.course,
'category': self.category, 'category': self.category,
'name': self.name} 'name': self.name,
'revision': self.revision}
def to_json(self):
return self.dict()
class KeyStore(object): class ModuleStore(object):
"""
An abstract interface for a database backend that stores XModuleDescriptor instances
"""
def get_item(self, location): 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 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 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 location: Something that can be passed to Location
""" """
raise NotImplementedError raise NotImplementedError
# TODO (cpennington): Replace with clone_item
def create_item(self, location, editor): 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 raise NotImplementedError
def update_item(self, location, data): def update_item(self, location, data):
...@@ -95,19 +162,3 @@ class KeyStore(object): ...@@ -95,19 +162,3 @@ class KeyStore(object):
children: A list of child item identifiers children: A list of child item identifiers
""" """
raise NotImplementedError raise NotImplementedError
class KeyStoreItem(object):
"""
An object from a KeyStore, which can be saved back to that keystore
"""
def __init__(self, location, children, data, editor, parents, revision):
self.location = location
self.children = children
self.data = data
self.editor = editor
self.parents = parents
self.revision = revision
def save(self):
raise NotImplementedError
""" """
Module that provides a connection to the keystore specified in the django settings. 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 __future__ import absolute_import
from django.conf import settings from django.conf import settings
from .mongo import MongoKeyStore from .mongo import MongoModuleStore
_KEYSTORE = None _KEYSTORES = {}
def keystore(): def keystore(name='default'):
global _KEYSTORE global _KEYSTORES
if _KEYSTORE is None: if name not in _KEYSTORES:
_KEYSTORE = MongoKeyStore(**settings.KEYSTORE) _KEYSTORES[name] = MongoModuleStore(**settings.KEYSTORE[name])
return _KEYSTORE return _KEYSTORES[name]
...@@ -9,3 +9,6 @@ class ItemNotFoundError(Exception): ...@@ -9,3 +9,6 @@ class ItemNotFoundError(Exception):
class InsufficientSpecificationError(Exception): class InsufficientSpecificationError(Exception):
pass pass
class InvalidLocationError(Exception):
pass
import pymongo import pymongo
from . import KeyStore, Location from . import ModuleStore, Location
from .exceptions import ItemNotFoundError, InsufficientSpecificationError from .exceptions import ItemNotFoundError, InsufficientSpecificationError
from xmodule.x_module import XModuleDescriptor from xmodule.x_module import XModuleDescriptor, DescriptorSystem
class MongoKeyStore(KeyStore): class MongoModuleStore(ModuleStore):
""" """
A Mongodb backed KeyStore A Mongodb backed ModuleStore
""" """
def __init__(self, host, db, collection, port=27017): def __init__(self, host, db, collection, port=27017):
self.collection = pymongo.connection.Connection( self.collection = pymongo.connection.Connection(
...@@ -19,34 +19,33 @@ class MongoKeyStore(KeyStore): ...@@ -19,34 +19,33 @@ class MongoKeyStore(KeyStore):
def get_item(self, location): 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 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 location: Something that can be passed to Location
""" """
query = dict(
('location.{key}'.format(key=key), val) query = {}
for (key, val) for key, val in Location(location).dict().iteritems():
in Location(location).dict().items() if key != 'revision' and val is None:
if val is not None raise InsufficientSpecificationError(location)
)
items = self.collection.find( if val is not None:
query['location.{key}'.format(key=key)] = val
item = self.collection.find_one(
query, query,
sort=[('revision', pymongo.ASCENDING)], sort=[('revision', pymongo.ASCENDING)],
limit=1,
) )
if items.count() > 1: if item is None:
raise InsufficientSpecificationError(location)
if items.count() == 0:
raise ItemNotFoundError(location) raise ItemNotFoundError(location)
return XModuleDescriptor.load_from_json(items[0], self.get_item) return XModuleDescriptor.load_from_json(item, DescriptorSystem(self.get_item))
def create_item(self, location, editor): def create_item(self, location, editor):
""" """
...@@ -67,9 +66,12 @@ class MongoKeyStore(KeyStore): ...@@ -67,9 +66,12 @@ class MongoKeyStore(KeyStore):
location: Something that can be passed to Location location: Something that can be passed to Location
data: A nested dictionary of problem data data: A nested dictionary of problem data
""" """
# See http://www.mongodb.org/display/DOCS/Updating for
# atomic update syntax
self.collection.update( self.collection.update(
{'location': Location(location).dict()}, {'location': Location(location).dict()},
{'$set': {'data': data}} {'$set': {'definition.data': data}}
) )
def update_children(self, location, children): def update_children(self, location, children):
...@@ -80,7 +82,10 @@ class MongoKeyStore(KeyStore): ...@@ -80,7 +82,10 @@ class MongoKeyStore(KeyStore):
location: Something that can be passed to Location location: Something that can be passed to Location
children: A list of child item identifiers children: A list of child item identifiers
""" """
# See http://www.mongodb.org/display/DOCS/Updating for
# atomic update syntax
self.collection.update( self.collection.update(
{'location': Location(location).dict()}, {'location': Location(location).dict()},
{'$set': {'children': children}} {'$set': {'definition.children': children}}
) )
from nose.tools import assert_equals, assert_raises
from keystore import Location
from keystore.exceptions import InvalidLocationError
def check_string_roundtrip(url):
assert_equals(url, Location(url).url())
assert_equals(url, str(Location(url)))
def test_string_roundtrip():
check_string_roundtrip("tag://org/course/category/name")
check_string_roundtrip("tag://org/course/category/name/revision")
check_string_roundtrip("tag://org/course/category/name with spaces/revision")
def test_dict():
input_dict = {
'tag': 'tag',
'course': 'course',
'category': 'category',
'name': 'name',
'org': 'org'
}
assert_equals("tag://org/course/category/name", Location(input_dict).url())
assert_equals(dict(revision=None, **input_dict), Location(input_dict).dict())
input_dict['revision'] = 'revision'
assert_equals("tag://org/course/category/name/revision", Location(input_dict).url())
assert_equals(input_dict, Location(input_dict).dict())
def test_list():
input_list = ['tag', 'org', 'course', 'category', 'name']
assert_equals("tag://org/course/category/name", Location(input_list).url())
assert_equals(input_list + [None], Location(input_list).list())
input_list.append('revision')
assert_equals("tag://org/course/category/name/revision", Location(input_list).url())
assert_equals(input_list, Location(input_list).list())
def test_location():
input_list = ['tag', 'org', 'course', 'category', 'name']
assert_equals("tag://org/course/category/name", Location(Location(input_list)).url())
def test_invalid_locations():
assert_raises(InvalidLocationError, Location, "foo")
assert_raises(InvalidLocationError, Location, ["foo", "bar"])
assert_raises(InvalidLocationError, Location, ["foo", "bar", "baz", "blat", "foo/bar"])
assert_raises(InvalidLocationError, Location, None)
...@@ -97,22 +97,5 @@ class Module(XModule): ...@@ -97,22 +97,5 @@ class Module(XModule):
self.rendered = False 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): class SectionDescriptor(XModuleDescriptor):
pass pass
...@@ -5,10 +5,13 @@ setup( ...@@ -5,10 +5,13 @@ setup(
version="0.1", version="0.1",
packages=find_packages(), packages=find_packages(),
install_requires=['distribute'], install_requires=['distribute'],
# See http://guide.python-distribute.org/creation.html#entry-points
# for a description of entry_points
entry_points={ entry_points={
'xmodule.v1': [ 'xmodule.v1': [
"Course = seq_module:SectionDescriptor", "Course = seq_module:SectionDescriptor",
"Week = seq_module:WeekDescriptor", "Week = seq_module:SectionDescriptor",
"Section = seq_module:SectionDescriptor", "Section = seq_module:SectionDescriptor",
"LectureSequence = seq_module:SectionDescriptor", "LectureSequence = seq_module:SectionDescriptor",
"Lab = seq_module:SectionDescriptor", "Lab = seq_module:SectionDescriptor",
......
...@@ -21,7 +21,7 @@ class Plugin(object): ...@@ -21,7 +21,7 @@ class Plugin(object):
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(
entry_point=cls.entry_point, entry_point=cls.entry_point,
id=identifier, id=identifier,
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:
raise ModuleMissingError(identifier) raise ModuleMissingError(identifier)
...@@ -125,47 +125,82 @@ class XModule(object): ...@@ -125,47 +125,82 @@ class XModule(object):
class XModuleDescriptor(Plugin): 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" entry_point = "xmodule.v1"
@staticmethod @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']) class_ = XModuleDescriptor.load_class(json_data['location']['category'])
return class_.from_json(json_data, load_item) return class_.from_json(json_data, system)
@classmethod @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. 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 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, def __init__(self,
load_item, system,
data=None, definition=None,
children=None,
**kwargs): **kwargs):
self.load_item = load_item """
self.data = data if data is not None else {} Construct a new XModuleDescriptor. The only required arguments are the
self.children = children if children is not None else [] 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.name = Location(kwargs.get('location')).name
self.type = Location(kwargs.get('location')).category 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 self._child_instances = None
def get_children(self, categories=None): def get_children(self, categories=None):
"""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.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: if categories is None:
return self._child_instances return self._child_instances
else: else:
return [child for child in self._child_instances if child.type in categories] return [child for child in self._child_instances if child.type in categories]
def get_xml(self): def get_xml(self):
''' For conversions between JSON and legacy XML representations. ''' For conversions between JSON and legacy XML representations.
''' '''
...@@ -192,3 +227,12 @@ class XModuleDescriptor(Plugin): ...@@ -192,3 +227,12 @@ class XModuleDescriptor(Plugin):
# Full ==> what we edit # Full ==> what we edit
# ''' # '''
# raise NotImplementedError # raise NotImplementedError
class DescriptorSystem(object):
def __init__(self, load_item):
"""
load_item: Takes a Location and returns and XModuleDescriptor
"""
self.load_item = load_item
...@@ -107,3 +107,6 @@ If code qualifies: ...@@ -107,3 +107,6 @@ If code qualifies:
* When impossible, it should live in the github repo. * When impossible, it should live in the github repo.
* Discussion should live on github, Basecamp or Pivotal, depending on * Discussion should live on github, Basecamp or Pivotal, depending on
context. context.
* Notes for later fixes should in general be put into Pivotal as stories.
If they are left in the code, they should be prefixed by
# TODO (<name>)
...@@ -44,10 +44,27 @@ You should be familiar with the following. If you're not, go read some docs... ...@@ -44,10 +44,27 @@ You should be familiar with the following. If you're not, go read some docs...
### Common libraries ### 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. - xmodule: generic learning modules. *x* can be sequence, video, template, html,
- 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. vertical, capa, etc. These are the things that one puts inside sections
- 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 the course structure.
- in `common/lib/xmodule`
- 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. - capa modules -- defines `LoncapaProblem` and many related things.
- in `common/lib/capa` - in `common/lib/capa`
...@@ -76,7 +93,14 @@ The LMS is a django site, with root in `lms/`. It runs in many different enviro ...@@ -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. - 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 ### 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