Commit 6427dd67 by Calen Pennington

WIP: Get the cms running. Component previews work

parent 85e109da
......@@ -122,7 +122,7 @@ def compute_unit_state(unit):
'private' content is editabled and not visible in the LMS
"""
if unit.metadata.get('is_draft', False):
if unit.cms.is_draft:
try:
modulestore('direct').get_item(unit.location)
return UnitState.draft
......@@ -142,4 +142,4 @@ def update_item(location, value):
if value is None:
get_modulestore(location).delete_item(location)
else:
get_modulestore(location).update_item(location, value)
\ No newline at end of file
get_modulestore(location).update_item(location, value)
......@@ -29,11 +29,14 @@ from django.conf import settings
from xmodule.modulestore import Location
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
from xmodule.modulestore.inheritance import own_metadata
from xmodule.model import Scope
from xmodule.runtime import KeyValueStore, DbModel, InvalidScopeError
from xmodule.x_module import ModuleSystem
from xmodule.error_module import ErrorDescriptor
from xmodule.errortracker import exc_info_to_str
from static_replace import replace_urls
from external_auth.views import ssl_login_shortcut
from xmodule.modulestore.mongo import MongoUsage
from mitxmako.shortcuts import render_to_response, render_to_string
from xmodule.modulestore.django import modulestore
......@@ -214,8 +217,13 @@ def edit_subsection(request, location):
# remove all metadata from the generic dictionary that is presented in a more normalized UI
policy_metadata = dict((key,value) for key, value in item.metadata.iteritems()
if key not in ['display_name', 'start', 'due', 'format'] and key not in item.system_metadata_fields)
policy_metadata = dict(
(key,value)
for field
in item.fields
if field.name not in ['display_name', 'start', 'due', 'format'] and
field.scope == Scope.settings
)
can_view_live = False
subsection_units = item.get_children()
......@@ -312,11 +320,6 @@ def edit_unit(request, location):
unit_state = compute_unit_state(item)
try:
published_date = time.strftime('%B %d, %Y', item.metadata.get('published_date'))
except TypeError:
published_date = None
return render_to_response('unit.html', {
'context_course': course,
'active_tab': 'courseware',
......@@ -327,11 +330,11 @@ def edit_unit(request, location):
'draft_preview_link': preview_lms_link,
'published_preview_link': lms_link,
'subsection': containing_subsection,
'release_date': get_date_display(datetime.fromtimestamp(time.mktime(containing_subsection.start))) if containing_subsection.start is not None else None,
'release_date': get_date_display(datetime.fromtimestamp(time.mktime(containing_subsection.lms.start))) if containing_subsection.lms.start is not None else None,
'section': containing_section,
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
'unit_state': unit_state,
'published_date': published_date,
'published_date': item.cms.published_date.strftime('%B %d, %Y') if item.cms.published_date is not None else None,
})
......@@ -395,9 +398,8 @@ def preview_dispatch(request, preview_id, location, dispatch=None):
dispatch: The action to execute
"""
instance_state, shared_state = load_preview_state(request, preview_id, location)
descriptor = modulestore().get_item(location)
instance = load_preview_module(request, preview_id, descriptor, instance_state, shared_state)
instance = load_preview_module(request, preview_id, descriptor)
# Let the module handle the AJAX
try:
ajax_return = instance.handle_ajax(dispatch, request.POST)
......@@ -408,47 +410,40 @@ def preview_dispatch(request, preview_id, location, dispatch=None):
log.exception("error processing ajax call")
raise
save_preview_state(request, preview_id, location, instance.get_instance_state(), instance.get_shared_state())
print request.session.items()
return HttpResponse(ajax_return)
def load_preview_state(request, preview_id, location):
def render_from_lms(template_name, dictionary, context=None, namespace='main'):
"""
Load the state of a preview module from the request
preview_id (str): An identifier specifying which preview this module is used for
location: The Location of the module to dispatch to
Render a template using the LMS MAKO_TEMPLATES
"""
if 'preview_states' not in request.session:
request.session['preview_states'] = defaultdict(dict)
instance_state = request.session['preview_states'][preview_id, location].get('instance')
shared_state = request.session['preview_states'][preview_id, location].get('shared')
return instance_state, shared_state
return render_to_string(template_name, dictionary, context, namespace="lms." + namespace)
def save_preview_state(request, preview_id, location, instance_state, shared_state):
"""
Save the state of a preview module to the request
preview_id (str): An identifier specifying which preview this module is used for
location: The Location of the module to dispatch to
instance_state: The instance state to save
shared_state: The shared state to save
"""
if 'preview_states' not in request.session:
request.session['preview_states'] = defaultdict(dict)
class SessionKeyValueStore(KeyValueStore):
def __init__(self, request, model_data):
self._model_data = model_data
self._session = request.session
request.session['preview_states'][preview_id, location]['instance'] = instance_state
request.session['preview_states'][preview_id, location]['shared'] = shared_state
def get(self, key):
try:
return self._model_data[key.field_name]
except (KeyError, InvalidScopeError):
return self._session[tuple(key)]
def set(self, key, value):
try:
self._model_data[key.field_name] = value
except (KeyError, InvalidScopeError):
self._session[tuple(key)] = value
def render_from_lms(template_name, dictionary, context=None, namespace='main'):
"""
Render a template using the LMS MAKO_TEMPLATES
"""
return render_to_string(template_name, dictionary, context, namespace="lms." + namespace)
def delete(self, key):
try:
del self._model_data[key.field_name]
except (KeyError, InvalidScopeError):
del self._session[tuple(key)]
def preview_module_system(request, preview_id, descriptor):
......@@ -461,6 +456,14 @@ def preview_module_system(request, preview_id, descriptor):
descriptor: An XModuleDescriptor
"""
def preview_model_data(model_data):
return DbModel(
SessionKeyValueStore(request, model_data),
descriptor.module_class,
preview_id,
MongoUsage(preview_id, descriptor.location.url()),
)
return ModuleSystem(
ajax_url=reverse('preview_dispatch', args=[preview_id, descriptor.location.url(), '']).rstrip('/'),
# TODO (cpennington): Do we want to track how instructors are using the preview problems?
......@@ -471,6 +474,7 @@ def preview_module_system(request, preview_id, descriptor):
debug=True,
replace_urls=replace_urls,
user=request.user,
xmodule_model_data=preview_model_data,
)
......@@ -484,11 +488,10 @@ def get_preview_module(request, preview_id, location):
location: A Location
"""
descriptor = modulestore().get_item(location)
instance_state, shared_state = descriptor.get_sample_state()[0]
return load_preview_module(request, preview_id, descriptor, instance_state, shared_state)
return load_preview_module(request, preview_id, descriptor)
def load_preview_module(request, preview_id, descriptor, instance_state, shared_state):
def load_preview_module(request, preview_id, descriptor):
"""
Return a preview XModule instantiated from the supplied descriptor, instance_state, and shared_state
......@@ -502,10 +505,11 @@ def load_preview_module(request, preview_id, descriptor, instance_state, shared_
try:
module = descriptor.xmodule(system)
except:
log.debug("Unable to load preview module", exc_info=True)
module = ErrorDescriptor.from_descriptor(
descriptor,
error_msg=exc_info_to_str(sys.exc_info())
).xmodule_constructor(system)(None, None)
).xmodule(system)
# cdodge: Special case
if module.location.category == 'static_tab':
......@@ -523,11 +527,9 @@ def load_preview_module(request, preview_id, descriptor, instance_state, shared_
module.get_html = replace_static_urls(
module.get_html,
module.metadata.get('data_dir', module.location.course),
getattr(module, 'data_dir', module.location.course),
course_namespace = Location([module.location.tag, module.location.org, module.location.course, None, None])
)
save_preview_state(request, preview_id, descriptor.location.url(),
module.get_instance_state(), module.get_shared_state())
return module
......@@ -541,7 +543,7 @@ def get_module_previews(request, descriptor):
"""
preview_html = []
for idx, (instance_state, shared_state) in enumerate(descriptor.get_sample_state()):
module = load_preview_module(request, str(idx), descriptor, instance_state, shared_state)
module = load_preview_module(request, str(idx), descriptor)
preview_html.append(module.get_html())
return preview_html
......@@ -625,23 +627,26 @@ def save_item(request):
# update existing metadata with submitted metadata (which can be partial)
# IMPORTANT NOTE: if the client passed pack 'null' (None) for a piece of metadata that means 'remove it'
for metadata_key in posted_metadata.keys():
for metadata_key, value in posted_metadata.items():
# let's strip out any metadata fields from the postback which have been identified as system metadata
# and therefore should not be user-editable, so we should accept them back from the client
if metadata_key in existing_item.system_metadata_fields:
del posted_metadata[metadata_key]
elif posted_metadata[metadata_key] is None:
print "DELETING", metadata_key, value
print metadata_key in existing_item._model_data
# remove both from passed in collection as well as the collection read in from the modulestore
if metadata_key in existing_item.metadata:
del existing_item.metadata[metadata_key]
if metadata_key in existing_item._model_data:
del existing_item._model_data[metadata_key]
del posted_metadata[metadata_key]
# overlay the new metadata over the modulestore sourced collection to support partial updates
existing_item.metadata.update(posted_metadata)
else:
existing_item._model_data[metadata_key] = value
# commit to datastore
store.update_metadata(item_location, existing_item.metadata)
# TODO (cpennington): This really shouldn't have to do this much reaching in to get the metadata
print existing_item._model_data._kvs._metadata
store.update_metadata(item_location, existing_item._model_data._kvs._metadata)
return HttpResponse()
......
......@@ -237,15 +237,15 @@ class CourseGradingModel:
# 5 hours 59 minutes 59 seconds => converted to iso format
rawgrace = descriptor.lms.graceperiod
if rawgrace:
hours_from_day = rawgrace.days*24
hours_from_days = rawgrace.days*24
seconds = rawgrace.seconds
hours_from_seconds = int(seconds / 3600)
seconds -= hours_from_seconds * 3600
minutes = int(seconds / 60)
seconds -= minutes * 60
return {
'hours': hourse_from_days + hours_from_seconds,
'minutes': minutes_from_seconds,
'hours': hours_from_days + hours_from_seconds,
'minutes': minutes,
'seconds': seconds,
}
else:
......
......@@ -25,7 +25,7 @@
</div>
<div>
<label>Format:</label>
<input type="text" value="${subsection.metadata['format'] if 'format' in subsection.metadata else ''}" class="unit-subtitle" data-metadata-name="format"/>
<input type="text" value="${subsection.lms.format}" class="unit-subtitle" data-metadata-name="format"/>
</div>
<div class="sortable-unit-list">
<label>Units:</label>
......@@ -54,13 +54,13 @@
<label>Release date:<!-- <span class="description">Determines when this subsection and the units within it will be released publicly.</span>--></label>
<div class="datepair" data-language="javascript">
<%
start_date = datetime.fromtimestamp(mktime(subsection.start)) if subsection.start is not None else None
parent_start_date = datetime.fromtimestamp(mktime(parent_item.start)) if parent_item.start is not None else None
start_date = datetime.fromtimestamp(mktime(subsection.lms.start)) if subsection.lms.start is not None else None
parent_start_date = datetime.fromtimestamp(mktime(parent_item.lms.start)) if parent_item.lms.start is not None else None
%>
<input type="text" id="start_date" name="start_date" value="${start_date.strftime('%m/%d/%Y') if start_date is not None else ''}" placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
<input type="text" id="start_time" name="start_time" value="${start_date.strftime('%H:%M') if start_date is not None else ''}" placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
</div>
% if subsection.start != parent_item.start and subsection.start:
% if subsection.lms.start != parent_item.lms.start and subsection.lms.start:
% if parent_start_date is None:
<p class="notice">The date above differs from the release date of ${parent_item.lms.display_name}, which is unset.
% else:
......@@ -83,7 +83,7 @@
<p class="date-description">
<%
# due date uses it own formatting for stringifying the date. As with capa_module.py, there's a utility module available for us to use
due_date = dateutil.parser.parse(subsection.metadata.get('due')) if 'due' in subsection.metadata else None
due_date = dateutil.parser.parse(subsection.lms.due) if subsection.lms.due is not None else None
%>
<input type="text" id="due_date" name="due_date" value="${due_date.strftime('%m/%d/%Y') if due_date is not None else ''}" placeholder="MM/DD/YYYY" class="date" size='15' autocomplete="off"/>
<input type="text" id="due_time" name="due_time" value="${due_date.strftime('%H:%M') if due_date is not None else ''}" placeholder="HH:MM" class="time" size='10' autocomplete="off"/>
......
......@@ -141,7 +141,7 @@
</h3>
<div class="section-published-date">
<%
start_date = datetime.fromtimestamp(mktime(section.start)) if section.start is not None else None
start_date = datetime.fromtimestamp(mktime(section.lms.start)) if section.lms.start is not None else None
start_date_str = start_date.strftime('%m/%d/%Y') if start_date is not None else ''
start_time_str = start_date.strftime('%H:%M') if start_date is not None else ''
%>
......@@ -178,7 +178,7 @@
</a>
</div>
<div class="gradable-status" data-initial-status="${subsection.metadata.get('format', 'Not Graded')}">
<div class="gradable-status" data-initial-status="${subsection.lms.format if section.lms.format is not None else 'Not Graded'}">
</div>
<div class="item-actions">
......
import datetime
from xmodule.model import Namespace, Boolean, Scope, ModelType, String
class DateTuple(ModelType):
"""
ModelType that stores datetime objects as time tuples
"""
def from_json(self, value):
return datetime.datetime(*value)
def to_json(self, value):
return list(value.timetuple())
class CmsNamespace(Namespace):
is_draft = Boolean(help="Whether this module is a draft", default=False, scope=Scope.settings)
published_date = DateTuple(help="Date when the module was published", scope=Scope.settings)
published_by = String(help="Id of the user who published this module", scope=Scope.settings)
from pkg_resources import resource_string
from xmodule.mako_module import MakoModuleDescriptor
from xmodule.model import Scope, String
import logging
log = logging.getLogger(__name__)
......@@ -14,13 +15,15 @@ class EditingDescriptor(MakoModuleDescriptor):
"""
mako_template = "widgets/raw-edit.html"
data = String(scope=Scope.content, default='')
# cdodge: a little refactoring here, since we're basically doing the same thing
# here as with our parent class, let's call into it to get the basic fields
# set and then add our additional fields. Trying to keep it DRY.
def get_context(self):
_context = MakoModuleDescriptor.get_context(self)
# Add our specific template information (the raw data body)
_context.update({'data': self.definition.get('data', '')})
_context.update({'data': self.data})
return _context
......
......@@ -106,7 +106,7 @@ class ErrorDescriptor(JSONEditingDescriptor):
def from_descriptor(cls, descriptor, error_msg='Error not available'):
return cls._construct(
descriptor.system,
json.dumps(descriptor._model_data, indent=4),
descriptor._model_data,
error_msg,
location=descriptor.location,
)
......
......@@ -32,9 +32,7 @@ class MakoModuleDescriptor(XModuleDescriptor):
"""
Return the context to render the mako template with
"""
return {'module': self,
'editable_metadata_fields': self.editable_fields
}
return {'module': self}
def get_html(self):
return self.system.render_template(
......
......@@ -15,11 +15,11 @@ def as_draft(location):
def wrap_draft(item):
"""
Sets `item.metadata['is_draft']` to `True` if the item is a
draft, and false otherwise. Sets the item's location to the
Sets `item.cms.is_draft` to `True` if the item is a
draft, and `False` otherwise. Sets the item's location to the
non-draft location in either case
"""
item.metadata['is_draft'] = item.location.revision == DRAFT
item.cms.is_draft = item.location.revision == DRAFT
item.location = item.location._replace(revision=None)
return item
......@@ -112,7 +112,7 @@ class DraftModuleStore(ModuleStoreBase):
"""
draft_loc = as_draft(location)
draft_item = self.get_item(location)
if not draft_item.metadata['is_draft']:
if not draft_item.cms.is_draft:
self.clone_item(location, draft_loc)
return super(DraftModuleStore, self).update_item(draft_loc, data)
......@@ -127,7 +127,7 @@ class DraftModuleStore(ModuleStoreBase):
"""
draft_loc = as_draft(location)
draft_item = self.get_item(location)
if not draft_item.metadata['is_draft']:
if not draft_item.cms.is_draft:
self.clone_item(location, draft_loc)
return super(DraftModuleStore, self).update_children(draft_loc, children)
......@@ -143,7 +143,7 @@ class DraftModuleStore(ModuleStoreBase):
draft_loc = as_draft(location)
draft_item = self.get_item(location)
if not draft_item.metadata['is_draft']:
if not draft_item.cms.is_draft:
self.clone_item(location, draft_loc)
if 'is_draft' in metadata:
......@@ -175,8 +175,8 @@ class DraftModuleStore(ModuleStoreBase):
draft = self.get_item(location)
metadata = {}
metadata.update(draft.metadata)
metadata['published_date'] = tuple(datetime.utcnow().timetuple())
metadata['published_by'] = published_by_id
metadata.cms.published_date = datetime.utcnow()
metadata.cms.published_by = published_by_id
super(DraftModuleStore, self).update_item(location, draft.definition.get('data', {}))
super(DraftModuleStore, self).update_children(location, draft.definition.get('children', []))
super(DraftModuleStore, self).update_metadata(location, metadata)
......
......@@ -52,6 +52,11 @@ def own_metadata(module):
field.name not in inherited_metadata and
field.name in module._model_data):
metadata[field.name] = module._model_data[field.name]
try:
metadata[field.name] = module._model_data[field.name]
except KeyError:
# Ignore any missing keys in _model_data
pass
return metadata
import pymongo
import sys
import logging
from bson.son import SON
from collections import namedtuple
from fs.osfs import OSFS
from itertools import repeat
from path import path
......@@ -11,17 +13,79 @@ from xmodule.errortracker import null_error_tracker, exc_info_to_str
from xmodule.mako_module import MakoDescriptorSystem
from xmodule.x_module import XModuleDescriptor
from xmodule.error_module import ErrorDescriptor
from xmodule.runtime import DbModel, KeyValueStore, InvalidScopeError
from xmodule.model import Scope
from . import ModuleStoreBase, Location
from .draft import DraftModuleStore
from .exceptions import (ItemNotFoundError,
DuplicateItemError)
log = logging.getLogger(__name__)
# TODO (cpennington): This code currently operates under the assumption that
# there is only one revision for each item. Once we start versioning inside the CMS,
# that assumption will have to change
class MongoKeyValueStore(KeyValueStore):
"""
A KeyValueStore that maps keyed data access to one of the 3 data areas
known to the MongoModuleStore (data, children, and metadata)
"""
def __init__(self, data, children, metadata):
self._data = data
self._children = children
self._metadata = metadata
def get(self, key):
print "GET", key
if key.field_name == 'children':
return self._children
elif key.scope == Scope.settings:
return self._metadata[key.field_name]
elif key.scope == Scope.content:
if key.field_name == 'data' and not isinstance(self._data, dict):
return self._data
else:
return self._data[key.field_name]
else:
raise InvalidScopeError(key.scope)
def set(self, key, value):
print "SET", key, value
if key.field_name == 'children':
self._children = value
elif key.scope == Scope.settings:
self._metadata[key.field_name] = value
elif key.scope == Scope.content:
if key.field_name == 'data' and not isinstance(self._data, dict):
self._data = value
else:
self._data[key.field_name] = value
else:
raise InvalidScopeError(key.scope)
def delete(self, key):
print "DELETE", key
if key.field_name == 'children':
self._children = []
elif key.scope == Scope.settings:
if key.field_name in self._metadata:
del self._metadata[key.field_name]
elif key.scope == Scope.content:
if key.field_name == 'data' and not isinstance(self._data, dict):
self._data = None
else:
del self._data[key.field_name]
else:
raise InvalidScopeError(key.scope)
MongoUsage = namedtuple('MongoUsage', 'id, def_id')
class CachingDescriptorSystem(MakoDescriptorSystem):
"""
A system that has a cache of module json that it will use to load modules
......@@ -64,8 +128,21 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
# always load an entire course. We're punting on this until after launch, and then
# will build a proper course policy framework.
try:
return XModuleDescriptor.load_from_json(json_data, self, self.default_class)
class_ = XModuleDescriptor.load_class(
json_data['location']['category'],
self.default_class
)
definition = json_data.get('definition', {})
kvs = MongoKeyValueStore(
definition.get('data', {}),
definition.get('children', []),
json_data.get('metadata', {}),
)
model_data = DbModel(kvs, class_, None, MongoUsage(self.course_id, location))
return class_(self, location, model_data)
except:
log.debug("Failed to load descriptor", exc_info=True)
return ErrorDescriptor.from_json(
json_data,
self,
......
......@@ -3,6 +3,13 @@ from collections import MutableMapping, namedtuple
from .model import ModuleScope, ModelType
class InvalidScopeError(Exception):
"""
Raised to indicated that operating on the supplied scope isn't allowed by a KeyValueStore
"""
pass
class KeyValueStore(object):
"""The abstract interface for Key Value Stores."""
......@@ -102,8 +109,12 @@ class DbModel(MutableMapping):
def __len__(self):
return len(self.keys())
def __contains__(self, item):
return item in self.keys()
def keys(self):
fields = [field.name for field in self._module_cls.fields]
for namespace_name in self._module_cls.namespaces:
fields.extend(field.name for field in getattr(self._module_cls, namespace_name).fields)
print fields
return fields
......@@ -28,13 +28,41 @@ class VideoModule(XModule):
css = {'scss': [resource_string(__name__, 'css/video/display.scss')]}
js_module_name = "Video"
youtube = String(help="Youtube ids for each speed, in the format <speed>:<id>[,<speed>:<id> ...]", scope=Scope.content)
show_captions = String(help="Whether to display captions with this video", scope=Scope.content)
source = String(help="External source for this video", scope=Scope.content)
track = String(help="Subtitle file", scope=Scope.content)
position = Int(help="Current position in the video", scope=Scope.student_state, default=0)
data = String(help="XML data for the problem", scope=Scope.content)
position = Int(help="Current position in the video", scope=Scope.student_state)
display_name = String(help="Display name for this module", scope=Scope.settings)
def __init__(self, *args, **kwargs):
XModule.__init__(self, *args, **kwargs)
xmltree = etree.fromstring(self.data)
self.youtube = xmltree.get('youtube')
self.position = 0
self.show_captions = xmltree.get('show_captions', 'true')
self.source = self._get_source(xmltree)
self.track = self._get_track(xmltree)
def _get_source(self, xmltree):
# find the first valid source
return self._get_first_external(xmltree, 'source')
def _get_track(self, xmltree):
# find the first valid track
return self._get_first_external(xmltree, 'track')
def _get_first_external(self, xmltree, tag):
"""
Will return the first valid element
of the given tag.
'valid' means has a non-empty 'src' attribute
"""
result = None
for element in xmltree.findall(tag):
src = element.get('src')
if src:
result = src
break
return result
def handle_ajax(self, dispatch, get):
'''
......@@ -87,50 +115,7 @@ class VideoModule(XModule):
})
class VideoDescriptor(RawDescriptor):
module_class = VideoModule
stores_state = True
template_dir_name = "video"
youtube = String(help="Youtube ids for each speed, in the format <speed>:<id>[,<speed>:<id> ...]", scope=Scope.content)
show_captions = String(help="Whether to display captions with this video", scope=Scope.content)
source = String(help="External source for this video", scope=Scope.content)
track = String(help="Subtitle file", scope=Scope.content)
@classmethod
def definition_from_xml(cls, xml_object, system):
return {
'youtube': xml_object.get('youtube'),
'show_captions': xml_object.get('show_captions', 'true'),
'source': _get_first_external(xml_object, 'source'),
'track': _get_first_external(xml_object, 'track'),
}, []
def definition_to_xml(self, resource_fs):
xml_object = etree.Element('video', {
'youtube': self.youtube,
'show_captions': self.show_captions,
})
if self.source is not None:
SubElement(xml_object, 'source', {'src': self.source})
if self.track is not None:
SubElement(xml_object, 'track', {'src': self.track})
return xml_object
def _get_first_external(xmltree, tag):
"""
Will return the first valid element
of the given tag.
'valid' means has a non-empty 'src' attribute
"""
result = None
for element in xmltree.findall(tag):
src = element.get('src')
if src:
result = src
break
return result
......@@ -8,13 +8,10 @@ from .models import (
XModuleStudentInfoField
)
from xmodule.runtime import DbModel, KeyValueStore
from xmodule.runtime import KeyValueStore, InvalidScopeError
from xmodule.model import Scope
class InvalidScopeError(Exception):
pass
class InvalidWriteError(Exception):
pass
......
......@@ -12,7 +12,8 @@ setup(
# for a description of entry_points
entry_points={
'xmodule.namespace': [
'lms = lms.xmodule_namespace:LmsNamespace'
'lms = lms.xmodule_namespace:LmsNamespace',
'cms = cms.xmodule_namespace:CmsNamespace',
],
}
)
\ No newline at end of file
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