Commit 6427dd67 by Calen Pennington

WIP: Get the cms running. Component previews work

parent 85e109da
...@@ -122,7 +122,7 @@ def compute_unit_state(unit): ...@@ -122,7 +122,7 @@ def compute_unit_state(unit):
'private' content is editabled and not visible in the LMS 'private' content is editabled and not visible in the LMS
""" """
if unit.metadata.get('is_draft', False): if unit.cms.is_draft:
try: try:
modulestore('direct').get_item(unit.location) modulestore('direct').get_item(unit.location)
return UnitState.draft return UnitState.draft
......
...@@ -29,11 +29,14 @@ from django.conf import settings ...@@ -29,11 +29,14 @@ from django.conf import settings
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
from xmodule.modulestore.inheritance import own_metadata 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.x_module import ModuleSystem
from xmodule.error_module import ErrorDescriptor from xmodule.error_module import ErrorDescriptor
from xmodule.errortracker import exc_info_to_str from xmodule.errortracker import exc_info_to_str
from static_replace import replace_urls from static_replace import replace_urls
from external_auth.views import ssl_login_shortcut 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 mitxmako.shortcuts import render_to_response, render_to_string
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
...@@ -214,8 +217,13 @@ def edit_subsection(request, location): ...@@ -214,8 +217,13 @@ def edit_subsection(request, location):
# remove all metadata from the generic dictionary that is presented in a more normalized UI # 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() policy_metadata = dict(
if key not in ['display_name', 'start', 'due', 'format'] and key not in item.system_metadata_fields) (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 can_view_live = False
subsection_units = item.get_children() subsection_units = item.get_children()
...@@ -312,11 +320,6 @@ def edit_unit(request, location): ...@@ -312,11 +320,6 @@ def edit_unit(request, location):
unit_state = compute_unit_state(item) 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', { return render_to_response('unit.html', {
'context_course': course, 'context_course': course,
'active_tab': 'courseware', 'active_tab': 'courseware',
...@@ -327,11 +330,11 @@ def edit_unit(request, location): ...@@ -327,11 +330,11 @@ def edit_unit(request, location):
'draft_preview_link': preview_lms_link, 'draft_preview_link': preview_lms_link,
'published_preview_link': lms_link, 'published_preview_link': lms_link,
'subsection': containing_subsection, '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, 'section': containing_section,
'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'), 'create_new_unit_template': Location('i4x', 'edx', 'templates', 'vertical', 'Empty'),
'unit_state': unit_state, '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): ...@@ -395,9 +398,8 @@ def preview_dispatch(request, preview_id, location, dispatch=None):
dispatch: The action to execute dispatch: The action to execute
""" """
instance_state, shared_state = load_preview_state(request, preview_id, location)
descriptor = modulestore().get_item(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 # Let the module handle the AJAX
try: try:
ajax_return = instance.handle_ajax(dispatch, request.POST) ajax_return = instance.handle_ajax(dispatch, request.POST)
...@@ -408,47 +410,40 @@ def preview_dispatch(request, preview_id, location, dispatch=None): ...@@ -408,47 +410,40 @@ def preview_dispatch(request, preview_id, location, dispatch=None):
log.exception("error processing ajax call") log.exception("error processing ajax call")
raise raise
save_preview_state(request, preview_id, location, instance.get_instance_state(), instance.get_shared_state()) print request.session.items()
return HttpResponse(ajax_return) 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 Render a template using the LMS MAKO_TEMPLATES
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: return render_to_string(template_name, dictionary, context, namespace="lms." + namespace)
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
class SessionKeyValueStore(KeyValueStore):
def __init__(self, request, model_data):
self._model_data = model_data
self._session = request.session
def save_preview_state(request, preview_id, location, instance_state, shared_state): def get(self, key):
""" try:
Save the state of a preview module to the request return self._model_data[key.field_name]
except (KeyError, InvalidScopeError):
preview_id (str): An identifier specifying which preview this module is used for return self._session[tuple(key)]
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 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'): def delete(self, key):
""" try:
Render a template using the LMS MAKO_TEMPLATES del self._model_data[key.field_name]
""" except (KeyError, InvalidScopeError):
return render_to_string(template_name, dictionary, context, namespace="lms." + namespace) del self._session[tuple(key)]
def preview_module_system(request, preview_id, descriptor): def preview_module_system(request, preview_id, descriptor):
...@@ -461,6 +456,14 @@ def preview_module_system(request, preview_id, descriptor): ...@@ -461,6 +456,14 @@ def preview_module_system(request, preview_id, descriptor):
descriptor: An XModuleDescriptor 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( return ModuleSystem(
ajax_url=reverse('preview_dispatch', args=[preview_id, descriptor.location.url(), '']).rstrip('/'), 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? # 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): ...@@ -471,6 +474,7 @@ def preview_module_system(request, preview_id, descriptor):
debug=True, debug=True,
replace_urls=replace_urls, replace_urls=replace_urls,
user=request.user, user=request.user,
xmodule_model_data=preview_model_data,
) )
...@@ -484,11 +488,10 @@ def get_preview_module(request, preview_id, location): ...@@ -484,11 +488,10 @@ def get_preview_module(request, preview_id, location):
location: A Location location: A Location
""" """
descriptor = modulestore().get_item(location) descriptor = modulestore().get_item(location)
instance_state, shared_state = descriptor.get_sample_state()[0] return load_preview_module(request, preview_id, descriptor)
return load_preview_module(request, preview_id, descriptor, instance_state, shared_state)
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 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_ ...@@ -502,10 +505,11 @@ def load_preview_module(request, preview_id, descriptor, instance_state, shared_
try: try:
module = descriptor.xmodule(system) module = descriptor.xmodule(system)
except: except:
log.debug("Unable to load preview module", exc_info=True)
module = ErrorDescriptor.from_descriptor( module = ErrorDescriptor.from_descriptor(
descriptor, descriptor,
error_msg=exc_info_to_str(sys.exc_info()) error_msg=exc_info_to_str(sys.exc_info())
).xmodule_constructor(system)(None, None) ).xmodule(system)
# cdodge: Special case # cdodge: Special case
if module.location.category == 'static_tab': if module.location.category == 'static_tab':
...@@ -523,11 +527,9 @@ def load_preview_module(request, preview_id, descriptor, instance_state, shared_ ...@@ -523,11 +527,9 @@ def load_preview_module(request, preview_id, descriptor, instance_state, shared_
module.get_html = replace_static_urls( module.get_html = replace_static_urls(
module.get_html, 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]) 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 return module
...@@ -541,7 +543,7 @@ def get_module_previews(request, descriptor): ...@@ -541,7 +543,7 @@ def get_module_previews(request, descriptor):
""" """
preview_html = [] preview_html = []
for idx, (instance_state, shared_state) in enumerate(descriptor.get_sample_state()): 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()) preview_html.append(module.get_html())
return preview_html return preview_html
...@@ -625,23 +627,26 @@ def save_item(request): ...@@ -625,23 +627,26 @@ def save_item(request):
# update existing metadata with submitted metadata (which can be partial) # 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' # 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 # 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 # 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: if metadata_key in existing_item.system_metadata_fields:
del posted_metadata[metadata_key] del posted_metadata[metadata_key]
elif posted_metadata[metadata_key] is None: 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 # remove both from passed in collection as well as the collection read in from the modulestore
if metadata_key in existing_item.metadata: if metadata_key in existing_item._model_data:
del existing_item.metadata[metadata_key] del existing_item._model_data[metadata_key]
del posted_metadata[metadata_key] del posted_metadata[metadata_key]
else:
# overlay the new metadata over the modulestore sourced collection to support partial updates existing_item._model_data[metadata_key] = value
existing_item.metadata.update(posted_metadata)
# commit to datastore # 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() return HttpResponse()
......
...@@ -237,15 +237,15 @@ class CourseGradingModel: ...@@ -237,15 +237,15 @@ class CourseGradingModel:
# 5 hours 59 minutes 59 seconds => converted to iso format # 5 hours 59 minutes 59 seconds => converted to iso format
rawgrace = descriptor.lms.graceperiod rawgrace = descriptor.lms.graceperiod
if rawgrace: if rawgrace:
hours_from_day = rawgrace.days*24 hours_from_days = rawgrace.days*24
seconds = rawgrace.seconds seconds = rawgrace.seconds
hours_from_seconds = int(seconds / 3600) hours_from_seconds = int(seconds / 3600)
seconds -= hours_from_seconds * 3600 seconds -= hours_from_seconds * 3600
minutes = int(seconds / 60) minutes = int(seconds / 60)
seconds -= minutes * 60 seconds -= minutes * 60
return { return {
'hours': hourse_from_days + hours_from_seconds, 'hours': hours_from_days + hours_from_seconds,
'minutes': minutes_from_seconds, 'minutes': minutes,
'seconds': seconds, 'seconds': seconds,
} }
else: else:
......
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
</div> </div>
<div> <div>
<label>Format:</label> <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>
<div class="sortable-unit-list"> <div class="sortable-unit-list">
<label>Units:</label> <label>Units:</label>
...@@ -54,13 +54,13 @@ ...@@ -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> <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"> <div class="datepair" data-language="javascript">
<% <%
start_date = datetime.fromtimestamp(mktime(subsection.start)) if subsection.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.start)) if parent_item.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_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"/> <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> </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: % 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. <p class="notice">The date above differs from the release date of ${parent_item.lms.display_name}, which is unset.
% else: % else:
...@@ -83,7 +83,7 @@ ...@@ -83,7 +83,7 @@
<p class="date-description"> <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 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_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"/> <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 @@ ...@@ -141,7 +141,7 @@
</h3> </h3>
<div class="section-published-date"> <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_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 '' start_time_str = start_date.strftime('%H:%M') if start_date is not None else ''
%> %>
...@@ -178,7 +178,7 @@ ...@@ -178,7 +178,7 @@
</a> </a>
</div> </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>
<div class="item-actions"> <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 pkg_resources import resource_string
from xmodule.mako_module import MakoModuleDescriptor from xmodule.mako_module import MakoModuleDescriptor
from xmodule.model import Scope, String
import logging import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -14,13 +15,15 @@ class EditingDescriptor(MakoModuleDescriptor): ...@@ -14,13 +15,15 @@ class EditingDescriptor(MakoModuleDescriptor):
""" """
mako_template = "widgets/raw-edit.html" 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 # 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 # 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. # set and then add our additional fields. Trying to keep it DRY.
def get_context(self): def get_context(self):
_context = MakoModuleDescriptor.get_context(self) _context = MakoModuleDescriptor.get_context(self)
# Add our specific template information (the raw data body) # Add our specific template information (the raw data body)
_context.update({'data': self.definition.get('data', '')}) _context.update({'data': self.data})
return _context return _context
......
...@@ -106,7 +106,7 @@ class ErrorDescriptor(JSONEditingDescriptor): ...@@ -106,7 +106,7 @@ class ErrorDescriptor(JSONEditingDescriptor):
def from_descriptor(cls, descriptor, error_msg='Error not available'): def from_descriptor(cls, descriptor, error_msg='Error not available'):
return cls._construct( return cls._construct(
descriptor.system, descriptor.system,
json.dumps(descriptor._model_data, indent=4), descriptor._model_data,
error_msg, error_msg,
location=descriptor.location, location=descriptor.location,
) )
......
...@@ -32,9 +32,7 @@ class MakoModuleDescriptor(XModuleDescriptor): ...@@ -32,9 +32,7 @@ class MakoModuleDescriptor(XModuleDescriptor):
""" """
Return the context to render the mako template with Return the context to render the mako template with
""" """
return {'module': self, return {'module': self}
'editable_metadata_fields': self.editable_fields
}
def get_html(self): def get_html(self):
return self.system.render_template( return self.system.render_template(
......
...@@ -15,11 +15,11 @@ def as_draft(location): ...@@ -15,11 +15,11 @@ def as_draft(location):
def wrap_draft(item): def wrap_draft(item):
""" """
Sets `item.metadata['is_draft']` to `True` if the item is a Sets `item.cms.is_draft` to `True` if the item is a
draft, and false otherwise. Sets the item's location to the draft, and `False` otherwise. Sets the item's location to the
non-draft location in either case 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) item.location = item.location._replace(revision=None)
return item return item
...@@ -112,7 +112,7 @@ class DraftModuleStore(ModuleStoreBase): ...@@ -112,7 +112,7 @@ class DraftModuleStore(ModuleStoreBase):
""" """
draft_loc = as_draft(location) draft_loc = as_draft(location)
draft_item = self.get_item(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) self.clone_item(location, draft_loc)
return super(DraftModuleStore, self).update_item(draft_loc, data) return super(DraftModuleStore, self).update_item(draft_loc, data)
...@@ -127,7 +127,7 @@ class DraftModuleStore(ModuleStoreBase): ...@@ -127,7 +127,7 @@ class DraftModuleStore(ModuleStoreBase):
""" """
draft_loc = as_draft(location) draft_loc = as_draft(location)
draft_item = self.get_item(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) self.clone_item(location, draft_loc)
return super(DraftModuleStore, self).update_children(draft_loc, children) return super(DraftModuleStore, self).update_children(draft_loc, children)
...@@ -143,7 +143,7 @@ class DraftModuleStore(ModuleStoreBase): ...@@ -143,7 +143,7 @@ class DraftModuleStore(ModuleStoreBase):
draft_loc = as_draft(location) draft_loc = as_draft(location)
draft_item = self.get_item(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) self.clone_item(location, draft_loc)
if 'is_draft' in metadata: if 'is_draft' in metadata:
...@@ -175,8 +175,8 @@ class DraftModuleStore(ModuleStoreBase): ...@@ -175,8 +175,8 @@ class DraftModuleStore(ModuleStoreBase):
draft = self.get_item(location) draft = self.get_item(location)
metadata = {} metadata = {}
metadata.update(draft.metadata) metadata.update(draft.metadata)
metadata['published_date'] = tuple(datetime.utcnow().timetuple()) metadata.cms.published_date = datetime.utcnow()
metadata['published_by'] = published_by_id metadata.cms.published_by = published_by_id
super(DraftModuleStore, self).update_item(location, draft.definition.get('data', {})) super(DraftModuleStore, self).update_item(location, draft.definition.get('data', {}))
super(DraftModuleStore, self).update_children(location, draft.definition.get('children', [])) super(DraftModuleStore, self).update_children(location, draft.definition.get('children', []))
super(DraftModuleStore, self).update_metadata(location, metadata) super(DraftModuleStore, self).update_metadata(location, metadata)
......
...@@ -52,6 +52,11 @@ def own_metadata(module): ...@@ -52,6 +52,11 @@ def own_metadata(module):
field.name not in inherited_metadata and field.name not in inherited_metadata and
field.name in module._model_data): field.name in module._model_data):
try:
metadata[field.name] = module._model_data[field.name] metadata[field.name] = module._model_data[field.name]
except KeyError:
# Ignore any missing keys in _model_data
pass
return metadata return metadata
import pymongo import pymongo
import sys import sys
import logging
from bson.son import SON from bson.son import SON
from collections import namedtuple
from fs.osfs import OSFS from fs.osfs import OSFS
from itertools import repeat from itertools import repeat
from path import path from path import path
...@@ -11,17 +13,79 @@ from xmodule.errortracker import null_error_tracker, exc_info_to_str ...@@ -11,17 +13,79 @@ from xmodule.errortracker import null_error_tracker, exc_info_to_str
from xmodule.mako_module import MakoDescriptorSystem from xmodule.mako_module import MakoDescriptorSystem
from xmodule.x_module import XModuleDescriptor from xmodule.x_module import XModuleDescriptor
from xmodule.error_module import ErrorDescriptor from xmodule.error_module import ErrorDescriptor
from xmodule.runtime import DbModel, KeyValueStore, InvalidScopeError
from xmodule.model import Scope
from . import ModuleStoreBase, Location from . import ModuleStoreBase, Location
from .draft import DraftModuleStore from .draft import DraftModuleStore
from .exceptions import (ItemNotFoundError, from .exceptions import (ItemNotFoundError,
DuplicateItemError) DuplicateItemError)
log = logging.getLogger(__name__)
# TODO (cpennington): This code currently operates under the assumption that # 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, # there is only one revision for each item. Once we start versioning inside the CMS,
# that assumption will have to change # 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): class CachingDescriptorSystem(MakoDescriptorSystem):
""" """
A system that has a cache of module json that it will use to load modules A system that has a cache of module json that it will use to load modules
...@@ -64,8 +128,21 @@ class CachingDescriptorSystem(MakoDescriptorSystem): ...@@ -64,8 +128,21 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
# always load an entire course. We're punting on this until after launch, and then # always load an entire course. We're punting on this until after launch, and then
# will build a proper course policy framework. # will build a proper course policy framework.
try: 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: except:
log.debug("Failed to load descriptor", exc_info=True)
return ErrorDescriptor.from_json( return ErrorDescriptor.from_json(
json_data, json_data,
self, self,
......
...@@ -3,6 +3,13 @@ from collections import MutableMapping, namedtuple ...@@ -3,6 +3,13 @@ from collections import MutableMapping, namedtuple
from .model import ModuleScope, ModelType 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): class KeyValueStore(object):
"""The abstract interface for Key Value Stores.""" """The abstract interface for Key Value Stores."""
...@@ -102,8 +109,12 @@ class DbModel(MutableMapping): ...@@ -102,8 +109,12 @@ class DbModel(MutableMapping):
def __len__(self): def __len__(self):
return len(self.keys()) return len(self.keys())
def __contains__(self, item):
return item in self.keys()
def keys(self): def keys(self):
fields = [field.name for field in self._module_cls.fields] fields = [field.name for field in self._module_cls.fields]
for namespace_name in self._module_cls.namespaces: for namespace_name in self._module_cls.namespaces:
fields.extend(field.name for field in getattr(self._module_cls, namespace_name).fields) fields.extend(field.name for field in getattr(self._module_cls, namespace_name).fields)
print fields
return fields return fields
...@@ -28,13 +28,41 @@ class VideoModule(XModule): ...@@ -28,13 +28,41 @@ class VideoModule(XModule):
css = {'scss': [resource_string(__name__, 'css/video/display.scss')]} css = {'scss': [resource_string(__name__, 'css/video/display.scss')]}
js_module_name = "Video" js_module_name = "Video"
youtube = String(help="Youtube ids for each speed, in the format <speed>:<id>[,<speed>:<id> ...]", scope=Scope.content) data = String(help="XML data for the problem", scope=Scope.content)
show_captions = String(help="Whether to display captions with this video", scope=Scope.content) position = Int(help="Current position in the video", scope=Scope.student_state)
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)
display_name = String(help="Display name for this module", scope=Scope.settings) 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): def handle_ajax(self, dispatch, get):
''' '''
...@@ -87,50 +115,7 @@ class VideoModule(XModule): ...@@ -87,50 +115,7 @@ class VideoModule(XModule):
}) })
class VideoDescriptor(RawDescriptor): class VideoDescriptor(RawDescriptor):
module_class = VideoModule module_class = VideoModule
stores_state = True stores_state = True
template_dir_name = "video" 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 ( ...@@ -8,13 +8,10 @@ from .models import (
XModuleStudentInfoField XModuleStudentInfoField
) )
from xmodule.runtime import DbModel, KeyValueStore from xmodule.runtime import KeyValueStore, InvalidScopeError
from xmodule.model import Scope from xmodule.model import Scope
class InvalidScopeError(Exception):
pass
class InvalidWriteError(Exception): class InvalidWriteError(Exception):
pass pass
......
...@@ -12,7 +12,8 @@ setup( ...@@ -12,7 +12,8 @@ setup(
# for a description of entry_points # for a description of entry_points
entry_points={ entry_points={
'xmodule.namespace': [ '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