diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index d6dcc3811da445b96f256486d0a7a6232072214d..cb42c5a1fc435580aa561d103264cae60f0e45ad 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -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) diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py index 809e43dea305ee3500c2c0fcfa1ca0bf62182b7a..a6957e61070f4727d1ce0816ccdb6066f8cf72b0 100644 --- a/cms/djangoapps/contentstore/views.py +++ b/cms/djangoapps/contentstore/views.py @@ -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() diff --git a/cms/djangoapps/models/settings/course_grading.py b/cms/djangoapps/models/settings/course_grading.py index 9ddbe87727bf5a7b1b5945a57c0183b948e08348..ce229dd196449f552d9230151726be3c9b7314bd 100644 --- a/cms/djangoapps/models/settings/course_grading.py +++ b/cms/djangoapps/models/settings/course_grading.py @@ -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: diff --git a/cms/templates/edit_subsection.html b/cms/templates/edit_subsection.html index 53567c73e192381b39738b48b6f25d8c9e083e4c..e98ad526ed562ea9ae5b48ae51ca540ff8b18b58 100644 --- a/cms/templates/edit_subsection.html +++ b/cms/templates/edit_subsection.html @@ -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"/> diff --git a/cms/templates/overview.html b/cms/templates/overview.html index fad28dbf07e1784a02f722e6f0b48a419a43a958..099cfb1a5bff985ee069fb59fbdb623cf5769dcb 100644 --- a/cms/templates/overview.html +++ b/cms/templates/overview.html @@ -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"> diff --git a/cms/xmodule_namespace.py b/cms/xmodule_namespace.py new file mode 100644 index 0000000000000000000000000000000000000000..0ea729e27ba83b7f4ad5166f2644654fadafd192 --- /dev/null +++ b/cms/xmodule_namespace.py @@ -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) diff --git a/common/lib/xmodule/xmodule/editing_module.py b/common/lib/xmodule/xmodule/editing_module.py index e025179b63e2fc916a56a84b6dd44ed2670241bc..531fd7d8b9f1068a4ea73ffd1af3c7e32506f397 100644 --- a/common/lib/xmodule/xmodule/editing_module.py +++ b/common/lib/xmodule/xmodule/editing_module.py @@ -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 diff --git a/common/lib/xmodule/xmodule/error_module.py b/common/lib/xmodule/xmodule/error_module.py index 37e98b5b7797b05e89760c9310415ac499af8f16..6a06b3ad3afe27d08a30f2d718b87de11a3ce087 100644 --- a/common/lib/xmodule/xmodule/error_module.py +++ b/common/lib/xmodule/xmodule/error_module.py @@ -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, ) diff --git a/common/lib/xmodule/xmodule/mako_module.py b/common/lib/xmodule/xmodule/mako_module.py index bdf3cb474900d2e0fbebf106b5ddba7454d84dfa..8ae68051a0c5a68240b0feaa2973e5af08eae7ec 100644 --- a/common/lib/xmodule/xmodule/mako_module.py +++ b/common/lib/xmodule/xmodule/mako_module.py @@ -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( diff --git a/common/lib/xmodule/xmodule/modulestore/draft.py b/common/lib/xmodule/xmodule/modulestore/draft.py index 5fbf05ed9bc738b0565ee3a0f58ef4159cc0f6b2..100bdb1dc6c605eb911a756a6b20a67db8c93183 100644 --- a/common/lib/xmodule/xmodule/modulestore/draft.py +++ b/common/lib/xmodule/xmodule/modulestore/draft.py @@ -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) diff --git a/common/lib/xmodule/xmodule/modulestore/inheritance.py b/common/lib/xmodule/xmodule/modulestore/inheritance.py index 38c592564d6c37cc640597343c88e66d8d169716..18cf0b33515a74925f5d9ac88ec3af82b5808b83 100644 --- a/common/lib/xmodule/xmodule/modulestore/inheritance.py +++ b/common/lib/xmodule/xmodule/modulestore/inheritance.py @@ -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 diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py index d145523d16cedd51120b941f7ce90345300e3d16..5c55c4d5076e11184bc4e739d4156fb5fb84912b 100644 --- a/common/lib/xmodule/xmodule/modulestore/mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/mongo.py @@ -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, diff --git a/common/lib/xmodule/xmodule/runtime.py b/common/lib/xmodule/xmodule/runtime.py index 6dee5b0b008149296badc302186c4aaa5ed0d811..5a5533133b5ee977e7a278a09386c9fccfecf305 100644 --- a/common/lib/xmodule/xmodule/runtime.py +++ b/common/lib/xmodule/xmodule/runtime.py @@ -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 diff --git a/common/lib/xmodule/xmodule/video_module.py b/common/lib/xmodule/xmodule/video_module.py index ecc7d7fdad3dcb483447995faf9355183c5eaaaa..34ce353afdb07b32a6e744bd4499babe7ac3ef56 100644 --- a/common/lib/xmodule/xmodule/video_module.py +++ b/common/lib/xmodule/xmodule/video_module.py @@ -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 diff --git a/lms/djangoapps/courseware/model_data.py b/lms/djangoapps/courseware/model_data.py index e8dfb025edf29017a7b054018579e14996469130..b2f2d3ef4893f6541e2a82200c5a9b33769389ba 100644 --- a/lms/djangoapps/courseware/model_data.py +++ b/lms/djangoapps/courseware/model_data.py @@ -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 diff --git a/setup.py b/setup.py index 84f242cf3dac3dab3d938ddad62ae75f3cdccfcd..a07f836413353a1ed5ce7e78b60352ec63de09f7 100644 --- a/setup.py +++ b/setup.py @@ -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