WIP: Get the cms running. Component previews work
This commit is contained in:
@@ -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)
|
||||
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,42 +410,11 @@ 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):
|
||||
"""
|
||||
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
|
||||
"""
|
||||
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
|
||||
|
||||
|
||||
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)
|
||||
|
||||
request.session['preview_states'][preview_id, location]['instance'] = instance_state
|
||||
request.session['preview_states'][preview_id, location]['shared'] = shared_state
|
||||
|
||||
|
||||
def render_from_lms(template_name, dictionary, context=None, namespace='main'):
|
||||
"""
|
||||
Render a template using the LMS MAKO_TEMPLATES
|
||||
@@ -451,6 +422,30 @@ def render_from_lms(template_name, dictionary, context=None, namespace='main'):
|
||||
return render_to_string(template_name, dictionary, context, namespace="lms." + namespace)
|
||||
|
||||
|
||||
class SessionKeyValueStore(KeyValueStore):
|
||||
def __init__(self, request, model_data):
|
||||
self._model_data = model_data
|
||||
self._session = request.session
|
||||
|
||||
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 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):
|
||||
"""
|
||||
Returns a ModuleSystem for the specified descriptor that is specialized for
|
||||
@@ -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">
|
||||
|
||||
20
cms/xmodule_namespace.py
Normal file
20
cms/xmodule_namespace.py
Normal file
@@ -0,0 +1,20 @@
|
||||
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)
|
||||
@@ -1,5 +1,6 @@
|
||||
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
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user