Commit cbfc7b20 by Calen Pennington

WIP more changes to model definitions. Next Up: actually wiring model data into the rdbms

parent 8ba41635
......@@ -115,7 +115,7 @@ def index(request):
return render_to_response('index.html', {
'new_course_template' : Location('i4x', 'edx', 'templates', 'course', 'Empty'),
'courses': [(course.metadata.get('display_name'),
'courses': [(course.title,
reverse('course_index', args=[
course.location.org,
course.location.course,
......@@ -269,7 +269,7 @@ def edit_unit(request, location):
for template in templates:
if template.location.category in COMPONENT_TYPES:
component_templates[template.location.category].append((
template.display_name,
template.lms.display_name,
template.location.url(),
))
......
......@@ -21,7 +21,7 @@
<article class="subsection-body window" data-id="${subsection.location}">
<div class="subsection-name-input">
<label>Display Name:</label>
<input type="text" value="${subsection.metadata['display_name']}" class="subsection-display-name-input" data-metadata-name="display_name"/>
<input type="text" value="${subsection.lms.display_name}" class="subsection-display-name-input" data-metadata-name="display_name"/>
</div>
<div>
<label>Format:</label>
......@@ -62,11 +62,11 @@
</div>
% if subsection.start != parent_item.start and subsection.start:
% if parent_start_date is None:
<p class="notice">The date above differs from the release date of ${parent_item.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:
<p class="notice">The date above differs from the release date of ${parent_item.display_name} – ${parent_start_date.strftime('%m/%d/%Y')} at ${parent_start_date.strftime('%H:%M')}.
<p class="notice">The date above differs from the release date of ${parent_item.lms.display_name} – ${parent_start_date.strftime('%m/%d/%Y')} at ${parent_start_date.strftime('%H:%M')}.
% endif
<a href="#" class="sync-date no-spinner">Sync to ${parent_item.display_name}.</a></p>
<a href="#" class="sync-date no-spinner">Sync to ${parent_item.lms.display_name}.</a></p>
% endif
</div>
......
......@@ -8,7 +8,7 @@
<div>${module_type}</div>
<div>
% for template in module_templates:
<a class="save" data-template-id="${template.location.url()}">${template.display_name}</a>
<a class="save" data-template-id="${template.location.url()}">${template.lms.display_name}</a>
% endfor
</div>
</div>
......
......@@ -132,9 +132,9 @@
<div class="item-details" data-id="${section.location}">
<h3 class="section-name">
<span data-tooltip="Edit this section's name" class="section-name-span">${section.display_name}</span>
<span data-tooltip="Edit this section's name" class="section-name-span">${section.lms.display_name}</span>
<form class="section-name-edit" style="display:none">
<input type="text" value="${section.display_name}" class="edit-section-name" autocomplete="off"/>
<input type="text" value="${section.lms.display_name}" class="edit-section-name" autocomplete="off"/>
<input type="submit" class="save-button edit-section-name-save" value="Save" />
<input type="button" class="cancel-button edit-section-name-cancel" value="Cancel" />
</form>
......@@ -174,7 +174,7 @@
<a href="#" data-tooltip="Expand/collapse this subsection" class="expand-collapse-icon expand"></a>
<a href="${reverse('edit_subsection', args=[subsection.location])}">
<span class="folder-icon"></span>
<span class="subsection-name"><span class="subsection-name-value">${subsection.display_name}</span></span>
<span class="subsection-name"><span class="subsection-name-value">${subsection.lms.display_name}</span></span>
</a>
</div>
......
......@@ -27,7 +27,7 @@
</div>
<div class="main-column">
<article class="unit-body window">
<p class="unit-name-input"><label>Display Name:</label><input type="text" value="${unit.display_name}" class="unit-display-name-input" /></p>
<p class="unit-name-input"><label>Display Name:</label><input type="text" value="${unit.lms.display_name}" class="unit-display-name-input" /></p>
<ol class="components">
% for id in components:
<li class="component" data-id="${id}"/>
......@@ -85,7 +85,7 @@
% if release_date is not None:
on <strong>${release_date}</strong>
% endif
with the subsection <a href="${reverse('edit_subsection', kwargs={'location': subsection.location})}">"${subsection.display_name}"</a></p>
with the subsection <a href="${reverse('edit_subsection', kwargs={'location': subsection.location})}">"${subsection.lms.display_name}"</a></p>
</div>
<div class="row unit-actions">
<a href="#" class="delete-draft delete-button">Delete Draft</a>
......@@ -100,12 +100,12 @@
<div><input type="text" class="url" value="/courseware/${section.url_name}/${subsection.url_name}" disabled /></div>
<ol>
<li>
<a href="#" class="section-item">${section.display_name}</a>
<a href="#" class="section-item">${section.lms.display_name}</a>
<ol>
<li>
<a href="${reverse('edit_subsection', args=[subsection.location])}" class="section-item">
<span class="folder-icon"></span>
<span class="subsection-name"><span class="subsection-name-value">${subsection.display_name}</span></span>
<span class="subsection-name"><span class="subsection-name-value">${subsection.lms.display_name}</span></span>
</a>
${units.enum_units(subsection, actions=False, selected=unit.location)}
</li>
......
......@@ -8,7 +8,7 @@
% if context_course:
<% ctx_loc = context_course.location %>
<a href="/" class="home"><span class="small-home-icon"></span></a>
<a href="${reverse('course_index', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}" class="class-name">${context_course.display_name}</a>
<a href="${reverse('course_index', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}" class="class-name">${context_course.lms.display_name}</a>
% endif
</div>
......
......@@ -60,7 +60,7 @@
data-type="${module.js_module_name}"
data-preview-type="${module.module_class.js_module_name}">
<a href="#" class="module-edit">${module.display_name}</a>
<a href="#" class="module-edit">${module.lms.display_name}</a>
</li>
% endfor
<%include file="module-dropdown.html"/>
......
......@@ -40,7 +40,7 @@
<a href="#" class="module-edit"
data-id="${child.location.url()}"
data-type="${child.js_module_name}"
data-preview-type="${child.module_class.js_module_name}">${child.display_name}</a>
data-preview-type="${child.module_class.js_module_name}">${child.lms.display_name}</a>
<a href="#" class="draggable">handle</a>
</li>
%endfor
......
......@@ -22,7 +22,7 @@ This def will enumerate through a passed in subsection and list all of the units
<div class="section-item ${selected_class}">
<a href="${reverse('edit_unit', args=[unit.location])}" class="${unit_state}-item">
<span class="${unit.category}-icon"></span>
<span class="unit-name">${unit.display_name}</span>
<span class="unit-name">${unit.lms.display_name}</span>
</a>
% if actions:
<div class="item-actions">
......
......@@ -231,7 +231,7 @@ def change_enrollment(request):
if not has_access(user, course, 'enroll'):
return {'success': False,
'error': 'enrollment in {} not allowed at this time'
.format(course.display_name)}
.format(course.lms.display_name)}
org, course_num, run=course_id.split("/")
statsd.increment("common.student.enrollment",
......
......@@ -32,7 +32,7 @@ def wrap_xmodule(get_html, module, template, context=None):
def _get_html():
context.update({
'content': get_html(),
'display_name' : module.metadata.get('display_name') if module.metadata is not None else None,
'display_name': module.lms.display_name,
'class_': module.__class__.__name__,
'module_name': module.js_module_name
})
......
......@@ -40,6 +40,6 @@ setup(
"static_tab = xmodule.html_module:StaticTabDescriptor",
"custom_tag_template = xmodule.raw_module:RawDescriptor",
"about = xmodule.html_module:AboutDescriptor"
]
],
}
)
......@@ -21,8 +21,6 @@ from xmodule.raw_module import RawDescriptor
from xmodule.exceptions import NotFoundError
from .model import Int, Scope, ModuleScope, ModelType, String, Boolean, Object, Float
Date = Timedelta = ModelType
log = logging.getLogger("mitx.courseware")
#-----------------------------------------------------------------------------
......@@ -45,25 +43,34 @@ def only_one(lst, default="", process=lambda x: x):
raise Exception('Malformed XML: expected at most one element in list.')
def parse_timedelta(time_str):
"""
time_str: A string with the following components:
<D> day[s] (optional)
<H> hour[s] (optional)
<M> minute[s] (optional)
<S> second[s] (optional)
class Timedelta(ModelType):
def from_json(self, time_str):
"""
time_str: A string with the following components:
<D> day[s] (optional)
<H> hour[s] (optional)
<M> minute[s] (optional)
<S> second[s] (optional)
Returns a datetime.timedelta parsed from the string
"""
parts = TIMEDELTA_REGEX.match(time_str)
if not parts:
return
parts = parts.groupdict()
time_params = {}
for (name, param) in parts.iteritems():
if param:
time_params[name] = int(param)
return timedelta(**time_params)
Returns a datetime.timedelta parsed from the string
"""
parts = TIMEDELTA_REGEX.match(time_str)
if not parts:
return
parts = parts.groupdict()
time_params = {}
for (name, param) in parts.iteritems():
if param:
time_params[name] = int(param)
return timedelta(**time_params)
def to_json(self, value):
values = []
for attr in ('days', 'hours', 'minutes', 'seconds'):
cur_value = getattr(value, attr, 0)
if cur_value > 0:
values.append("%d %s" % (cur_value, attr))
return ' '.join(values)
class ComplexEncoder(json.JSONEncoder):
......@@ -82,7 +89,7 @@ class CapaModule(XModule):
attempts = Int(help="Number of attempts taken by the student on this problem", default=0, scope=Scope.student_state)
max_attempts = Int(help="Maximum number of attempts that a student is allowed", scope=Scope.settings)
due = Date(help="Date that this problem is due by", scope=Scope.settings)
due = String(help="Date that this problem is due by", scope=Scope.settings)
graceperiod = Timedelta(help="Amount of time after the due date that submissions will be accepted", scope=Scope.settings)
show_answer = String(help="When to show the problem answer to the student", scope=Scope.settings, default="closed")
force_save_button = Boolean(help="Whether to force the save button to appear on the page", scope=Scope.settings)
......@@ -90,6 +97,7 @@ class CapaModule(XModule):
data = String(help="XML data for the problem", scope=Scope.content)
correct_map = Object(help="Dictionary with the correctness of current student answers", scope=Scope.student_state)
done = Boolean(help="Whether the student has answered the problem", scope=Scope.student_state)
display_name = String(help="Display name for this module", scope=Scope.settings)
js = {'coffee': [resource_string(__name__, 'js/src/capa/display.coffee'),
resource_string(__name__, 'js/src/collapsible.coffee'),
......@@ -104,8 +112,13 @@ class CapaModule(XModule):
def __init__(self, system, location, descriptor, model_data):
XModule.__init__(self, system, location, descriptor, model_data)
if self.graceperiod is not None and self.due:
self.close_date = self.due + self.graceperiod
if self.due:
due_date = dateutil.parser.parse(self.due)
else:
due_date = None
if self.graceperiod is not None and due_date:
self.close_date = due_date + self.graceperiod
#log.debug("Then parsed " + grace_period_string +
# " to closing date" + str(self.close_date))
else:
......
......@@ -13,8 +13,7 @@ import time
import copy
from .model import Scope, ModelType, List, String, Object, Boolean
Date = ModelType
from .x_module import Date
log = logging.getLogger(__name__)
......@@ -31,6 +30,10 @@ class CourseDescriptor(SequenceDescriptor):
end = Date(help="Date that this class ends", scope=Scope.settings)
advertised_start = Date(help="Date that this course is advertised to start", scope=Scope.settings)
grading_policy = Object(help="Grading policy definition for this class", scope=Scope.content)
show_calculator = Boolean(help="Whether to show the calculator in this course", default=False, scope=Scope.settings)
start = Date(help="Start time when this module is visible", scope=Scope.settings)
display_name = String(help="Display name for this module", scope=Scope.settings)
has_children = True
info_sidebar_name = String(scope=Scope.settings, default='Course Handouts')
......@@ -342,10 +345,6 @@ class CourseDescriptor(SequenceDescriptor):
def tabs(self, value):
self.metadata['tabs'] = value
@property
def show_calculator(self):
return self.metadata.get("show_calculator", None) == "Yes"
@lazyproperty
def grading_context(self):
"""
......@@ -433,6 +432,7 @@ class CourseDescriptor(SequenceDescriptor):
@property
def start_date_text(self):
print self.advertised_start, self.start
return time.strftime("%b %d, %Y", self.advertised_start or self.start)
@property
......
......@@ -3,8 +3,7 @@ from pkg_resources import resource_string, resource_listdir
from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
import json
from .model import String, Scope
class DiscussionModule(XModule):
js = {'coffee':
......@@ -12,18 +11,19 @@ class DiscussionModule(XModule):
resource_string(__name__, 'js/src/discussion/display.coffee')]
}
js_module_name = "InlineDiscussion"
data = String(help="XML definition of inline discussion", scope=Scope.content)
def get_html(self):
context = {
'discussion_id': self.discussion_id,
}
return self.system.render_template('discussion/_discussion_module.html', context)
def __init__(self, system, location, definition, descriptor, instance_state=None, shared_state=None, **kwargs):
XModule.__init__(self, system, location, definition, descriptor, instance_state, shared_state, **kwargs)
def __init__(self, *args, **kwargs):
XModule.__init__(self, *args, **kwargs)
if isinstance(instance_state, str):
instance_state = json.loads(instance_state)
xml_data = etree.fromstring(definition['data'])
xml_data = etree.fromstring(self.data)
self.discussion_id = xml_data.attrib['id']
self.title = xml_data.attrib['for']
self.discussion_category = xml_data.attrib['discussion_category']
......
......@@ -15,6 +15,7 @@ from .html_checker import check_html
from xmodule.modulestore import Location
from xmodule.contentstore.content import XASSET_SRCREF_PREFIX, StaticContent
from .model import Scope, String
log = logging.getLogger("mitx.courseware")
......@@ -26,15 +27,11 @@ class HtmlModule(XModule):
]
}
js_module_name = "HTMLModule"
data = String(help="Html contents to display for this module", scope=Scope.content)
def get_html(self):
return self.html
def __init__(self, system, location, definition, descriptor,
instance_state=None, shared_state=None, **kwargs):
XModule.__init__(self, system, location, definition, descriptor,
instance_state, shared_state, **kwargs)
self.html = self.definition['data']
return self.data
......
......@@ -2,7 +2,6 @@ class @Video
constructor: (element) ->
@el = $(element).find('.video')
@id = @el.attr('id').replace(/video_/, '')
@caption_data_dir = @el.data('caption-data-dir')
@caption_asset_path = @el.data('caption-asset-path')
@show_captions = @el.data('show-captions') == "true"
window.player = null
......
from collections import namedtuple
from .plugin import Plugin
class ModuleScope(object):
USAGE, DEFINITION, TYPE, ALL = xrange(4)
......@@ -15,6 +17,13 @@ Scope.student_info = Scope(student=True, module=ModuleScope.ALL)
class ModelType(object):
"""
A field class that can be used as a class attribute to define what data the class will want
to refer to.
When the class is instantiated, it will be available as an instance attribute of the same
name, by proxying through to self._model_data on the containing object.
"""
sequence = 0
def __init__(self, help=None, default=None, scope=Scope.content):
......@@ -33,10 +42,13 @@ class ModelType(object):
if instance is None:
return self
return instance._model_data.get(self.name, self.default)
if self.name not in instance._model_data:
return self.default
return self.from_json(instance._model_data[self.name])
def __set__(self, instance, value):
instance._model_data[self.name] = value
instance._model_data[self.name] = self.to_json(value)
def __delete__(self, instance):
del instance._model_data[self.name]
......@@ -47,27 +59,27 @@ class ModelType(object):
def __lt__(self, other):
return self._seq < other._seq
def to_json(self, value):
return value
def from_json(self, value):
return value
Int = Float = Boolean = Object = List = String = Any = ModelType
class ModelMetaclass(type):
def __new__(cls, name, bases, attrs):
# Find registered methods
reg_methods = {}
for value in attrs.itervalues():
for reg_type, names in getattr(value, "_method_registrations", {}).iteritems():
for n in names:
reg_methods[reg_type + n] = value
attrs['registered_methods'] = reg_methods
if attrs.get('has_children', False):
attrs['children'] = ModelType(help='The children of this XModule', default=[], scope=None)
"""
A metaclass to be used for classes that want to use ModelTypes as class attributes
to define data access.
@property
def child_map(self):
return dict((child.name, child) for child in self.children)
attrs['child_map'] = child_map
All class attributes that are ModelTypes will be added to the 'fields' attribute on
the instance.
Additionally, any namespaces registered in the `xmodule.namespace` will be added to
the instance
"""
def __new__(cls, name, bases, attrs):
fields = []
for n, v in attrs.items():
if isinstance(v, ModelType):
......@@ -77,3 +89,61 @@ class ModelMetaclass(type):
attrs['fields'] = fields
return super(ModelMetaclass, cls).__new__(cls, name, bases, attrs)
class NamespacesMetaclass(type):
"""
A metaclass to be used for classes that want to include namespaced fields in their
instances.
Any namespaces registered in the `xmodule.namespace` will be added to
the instance
"""
def __new__(cls, name, bases, attrs):
for ns_name, namespace in Namespace.load_classes():
if issubclass(namespace, Namespace):
attrs[ns_name] = NamespaceDescriptor(namespace)
return super(NamespacesMetaclass, cls).__new__(cls, name, bases, attrs)
class ParentModelMetaclass(type):
"""
A ModelMetaclass that transforms the attribute `has_children = True`
into a List field with an empty scope.
"""
def __new__(cls, name, bases, attrs):
if attrs.get('has_children', False):
attrs['children'] = List(help='The children of this XModule', default=[], scope=None)
else:
attrs['has_children'] = False
return super(ParentModelMetaclass, cls).__new__(cls, name, bases, attrs)
class NamespaceDescriptor(object):
def __init__(self, namespace):
self._namespace = namespace
def __get__(self, instance, owner):
if owner is None:
return self
return self._namespace(instance)
class Namespace(Plugin):
"""
A baseclass that sets up machinery for ModelType fields that proxies the contained fields
requests for _model_data to self._container._model_data.
"""
__metaclass__ = ModelMetaclass
__slots__ = ['container']
entry_point = 'xmodule.namespace'
def __init__(self, container):
self._container = container
@property
def _model_data(self):
return self._container._model_data
import pymongo
import sys
import logging
from bson.son import SON
from fs.osfs import OSFS
......@@ -9,8 +8,8 @@ from path import path
from importlib import import_module
from xmodule.errortracker import null_error_tracker, exc_info_to_str
from xmodule.x_module import XModuleDescriptor
from xmodule.mako_module import MakoDescriptorSystem
from xmodule.x_module import XModuleDescriptor
from xmodule.error_module import ErrorDescriptor
from . import ModuleStoreBase, Location
......
......@@ -192,7 +192,7 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
xmlstore.modules[course_id][descriptor.location] = descriptor
if hasattr(descriptor, 'children'):
for child in descriptor.children:
for child in descriptor.get_children():
parent_tracker.add_parent(child.location, descriptor.location)
return descriptor
......@@ -318,8 +318,6 @@ class XMLModuleStore(ModuleStoreBase):
# Didn't load course. Instead, save the errors elsewhere.
self.errored_courses[course_dir] = errorlog
def __unicode__(self):
'''
String representation - for debugging
......@@ -345,8 +343,6 @@ class XMLModuleStore(ModuleStoreBase):
log.warning(msg + " " + str(err))
return {}
def load_course(self, course_dir, tracker):
"""
Load a course into this module store
......@@ -363,7 +359,7 @@ class XMLModuleStore(ModuleStoreBase):
# been imported into the cms from xml
course_file = StringIO(clean_out_mako_templating(course_file.read()))
course_data = etree.parse(course_file,parser=edx_xml_parser).getroot()
course_data = etree.parse(course_file, parser=edx_xml_parser).getroot()
org = course_data.get('org')
......
import pkg_resources
import logging
log = logging.getLogger(__name__)
class PluginNotFoundError(Exception):
pass
class Plugin(object):
"""
Base class for a system that uses entry_points to load plugins.
Implementing classes are expected to have the following attributes:
entry_point: The name of the entry point to load plugins from
"""
_plugin_cache = None
@classmethod
def load_class(cls, identifier, default=None):
"""
Loads a single class instance specified by identifier. If identifier
specifies more than a single class, then logs a warning and returns the
first class identified.
If default is not None, will return default if no entry_point matching
identifier is found. Otherwise, will raise a ModuleMissingError
"""
if cls._plugin_cache is None:
cls._plugin_cache = {}
if identifier not in cls._plugin_cache:
identifier = identifier.lower()
classes = list(pkg_resources.iter_entry_points(
cls.entry_point, name=identifier))
if len(classes) > 1:
log.warning("Found multiple classes for {entry_point} with "
"identifier {id}: {classes}. "
"Returning the first one.".format(
entry_point=cls.entry_point,
id=identifier,
classes=", ".join(
class_.module_name for class_ in classes)))
if len(classes) == 0:
if default is not None:
return default
raise PluginNotFoundError(identifier)
cls._plugin_cache[identifier] = classes[0].load()
return cls._plugin_cache[identifier]
@classmethod
def load_classes(cls):
"""
Returns a list of containing the identifiers and their corresponding classes for all
of the available instances of this plugin
"""
return [(class_.name, class_.load())
for class_
in pkg_resources.iter_entry_points(cls.entry_point)]
......@@ -8,6 +8,7 @@ from xmodule.xml_module import XmlDescriptor
from xmodule.x_module import XModule
from xmodule.progress import Progress
from xmodule.exceptions import NotFoundError
from .model import Int, Scope
from pkg_resources import resource_string
log = logging.getLogger("mitx.common.lib.seq_module")
......@@ -16,6 +17,12 @@ log = logging.getLogger("mitx.common.lib.seq_module")
# OBSOLETE: This obsoletes 'type'
class_priority = ['video', 'problem']
def display_name(module):
if hasattr(module, 'display_name'):
return module.display_name
if hasattr(module, 'lms'):
return module.lms.display_name
class SequenceModule(XModule):
''' Layout module which lays out content in a temporal sequence
......@@ -26,22 +33,18 @@ class SequenceModule(XModule):
css = {'scss': [resource_string(__name__, 'css/sequence/display.scss')]}
js_module_name = "Sequence"
def __init__(self, system, location, definition, descriptor, instance_state=None,
shared_state=None, **kwargs):
XModule.__init__(self, system, location, definition, descriptor,
instance_state, shared_state, **kwargs)
# NOTE: Position is 1-indexed. This is silly, but there are now student
# positions saved on prod, so it's not easy to fix.
self.position = 1
has_children = True
# NOTE: Position is 1-indexed. This is silly, but there are now student
# positions saved on prod, so it's not easy to fix.
position = Int(help="Last tab viewed in this sequence", default=1, scope=Scope.student_state)
if instance_state is not None:
state = json.loads(instance_state)
if 'position' in state:
self.position = int(state['position'])
def __init__(self, *args, **kwargs):
XModule.__init__(self, *args, **kwargs)
# if position is specified in system, then use that instead
if system.get('position'):
self.position = int(system.get('position'))
if self.system.get('position'):
self.position = int(self.system.get('position'))
self.rendered = False
......@@ -79,9 +82,9 @@ class SequenceModule(XModule):
childinfo = {
'content': child.get_html(),
'title': "\n".join(
grand_child.display_name.strip()
display_name(grand_child)
for grand_child in child.get_children()
if 'display_name' in grand_child.metadata
if display_name(grand_child) is not None
),
'progress_status': Progress.to_js_status_str(progress),
'progress_detail': Progress.to_js_detail_str(progress),
......@@ -89,7 +92,7 @@ class SequenceModule(XModule):
'id': child.id,
}
if childinfo['title']=='':
childinfo['title'] = child.metadata.get('display_name','')
childinfo['title'] = display_name(child)
contents.append(childinfo)
params = {'items': contents,
......@@ -116,7 +119,8 @@ class SequenceDescriptor(MakoModuleDescriptor, XmlDescriptor):
mako_template = 'widgets/sequence-edit.html'
module_class = SequenceModule
stores_state = True # For remembering where in the sequence the student is
has_children = True
stores_state = True # For remembering where in the sequence the student is
js = {'coffee': [resource_string(__name__, 'js/src/sequence/edit.coffee')]}
js_module_name = "SequenceDescriptor"
......
......@@ -11,8 +11,10 @@ class_priority = ['video', 'problem']
class VerticalModule(XModule):
''' Layout module for laying out submodules vertically.'''
def __init__(self, system, location, definition, descriptor, instance_state=None, shared_state=None, **kwargs):
XModule.__init__(self, system, location, definition, descriptor, instance_state, shared_state, **kwargs)
has_children = True
def __init__(self, *args, **kwargs):
XModule.__init__(self, *args, **kwargs)
self.contents = None
def get_html(self):
......@@ -45,6 +47,8 @@ class VerticalModule(XModule):
class VerticalDescriptor(SequenceDescriptor):
module_class = VerticalModule
has_children = True
js = {'coffee': [resource_string(__name__, 'js/src/vertical/edit.coffee')]}
js_module_name = "VerticalDescriptor"
......@@ -9,6 +9,7 @@ from xmodule.raw_module import RawDescriptor
from xmodule.modulestore.mongo import MongoModuleStore
from xmodule.modulestore.django import modulestore
from xmodule.contentstore.content import StaticContent
from .model import Int, Scope, String
log = logging.getLogger(__name__)
......@@ -27,22 +28,20 @@ class VideoModule(XModule):
css = {'scss': [resource_string(__name__, 'css/video/display.scss')]}
js_module_name = "Video"
def __init__(self, system, location, definition, descriptor,
instance_state=None, shared_state=None, **kwargs):
XModule.__init__(self, system, location, definition, descriptor,
instance_state, shared_state, **kwargs)
xmltree = etree.fromstring(self.definition['data'])
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)
if instance_state is not None:
state = json.loads(instance_state)
if 'position' in state:
self.position = int(float(state['position']))
def _get_source(self, xmltree):
# find the first valid source
return self._get_first_external(xmltree, 'source')
......@@ -102,7 +101,7 @@ class VideoModule(XModule):
else:
# VS[compat]
# cdodge: filesystem static content support.
caption_asset_path = "/static/{0}/subs/".format(self.metadata['data_dir'])
caption_asset_path = "/static/{0}/subs/".format(self.descriptor.data_dir)
return self.system.render_template('video.html', {
'streams': self.video_list(),
......@@ -111,8 +110,6 @@ class VideoModule(XModule):
'source': self.source,
'track' : self.track,
'display_name': self.display_name,
# TODO (cpennington): This won't work when we move to data that isn't on the filesystem
'data_dir': self.metadata['data_dir'],
'caption_asset_path': caption_asset_path,
'show_captions': self.show_captions
})
......
import logging
import pkg_resources
import yaml
import os
import time
......@@ -10,9 +9,9 @@ from collections import namedtuple
from pkg_resources import resource_listdir, resource_string, resource_isdir
from xmodule.modulestore import Location
from .model import ModelMetaclass, String, Scope, ModuleScope, ModelType
Date = ModelType
from .model import ModelMetaclass, ParentModelMetaclass, NamespacesMetaclass, ModelType
from .plugin import Plugin
class Date(ModelType):
......@@ -38,72 +37,15 @@ class Date(ModelType):
"""
return time.strftime(self.time_format, value)
log = logging.getLogger('mitx.' + __name__)
def dummy_track(event_type, event):
pass
class ModuleMissingError(Exception):
class XModuleMetaclass(ParentModelMetaclass, NamespacesMetaclass, ModelMetaclass):
pass
log = logging.getLogger('mitx.' + __name__)
class Plugin(object):
"""
Base class for a system that uses entry_points to load plugins.
Implementing classes are expected to have the following attributes:
entry_point: The name of the entry point to load plugins from
"""
_plugin_cache = None
@classmethod
def load_class(cls, identifier, default=None):
"""
Loads a single class instance specified by identifier. If identifier
specifies more than a single class, then logs a warning and returns the
first class identified.
If default is not None, will return default if no entry_point matching
identifier is found. Otherwise, will raise a ModuleMissingError
"""
if cls._plugin_cache is None:
cls._plugin_cache = {}
if identifier not in cls._plugin_cache:
identifier = identifier.lower()
classes = list(pkg_resources.iter_entry_points(
cls.entry_point, name=identifier))
if len(classes) > 1:
log.warning("Found multiple classes for {entry_point} with "
"identifier {id}: {classes}. "
"Returning the first one.".format(
entry_point=cls.entry_point,
id=identifier,
classes=", ".join(
class_.module_name for class_ in classes)))
if len(classes) == 0:
if default is not None:
return default
raise ModuleMissingError(identifier)
cls._plugin_cache[identifier] = classes[0].load()
return cls._plugin_cache[identifier]
@classmethod
def load_classes(cls):
"""
Returns a list of containing the identifiers and their corresponding classes for all
of the available instances of this plugin
"""
return [(class_.name, class_.load())
for class_
in pkg_resources.iter_entry_points(cls.entry_point)]
def dummy_track(event_type, event):
pass
class HTMLSnippet(object):
......@@ -179,9 +121,7 @@ class XModule(HTMLSnippet):
See the HTML module for a simple example.
'''
__metaclass__ = ModelMetaclass
display_name = String(help="Display name for this module", scope=Scope(student=False, module=ModuleScope.USAGE))
__metaclass__ = XModuleMetaclass
# The default implementation of get_icon_class returns the icon_class
# attribute of the class
......@@ -244,9 +184,22 @@ class XModule(HTMLSnippet):
self.url_name = self.location.name
self.category = self.location.category
self._model_data = model_data
self._loaded_children = None
if self.display_name is None:
self.display_name = self.url_name.replace('_', ' ')
def get_children(self):
'''
Return module instances for all the children of this module.
'''
if not self.has_children:
return []
if self._loaded_children is None:
children = [self.system.get_module(loc) for loc in self.children]
# get_module returns None if the current user doesn't have access
# to the location.
self._loaded_children = [c for c in children if c is not None]
return self._loaded_children
def __unicode__(self):
return '<x_module(id={0})>'.format(self.id)
......@@ -257,7 +210,7 @@ class XModule(HTMLSnippet):
immediately inside this module.
'''
items = []
for child in self.children():
for child in self.get_children():
items.extend(child.displayable_items())
return items
......@@ -366,10 +319,8 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
"""
entry_point = "xmodule.v1"
module_class = XModule
__metaclass__ = ModelMetaclass
__metaclass__ = XModuleMetaclass
display_name = String(help="Display name for this module", scope=Scope(student=False, module=ModuleScope.USAGE))
start = Date(help="Start time when this module is visible", scope=Scope(student=False, module=ModuleScope.USAGE))
# Attributes for inspection of the descriptor
stores_state = False # Indicates whether the xmodule state should be
# stored in a database (independent of shared state)
......@@ -430,8 +381,6 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
metadata: A dictionary containing the following optional keys:
goals: A list of strings of learning goals associated with this
module
display_name: The name to use for displaying this module to the
user
url_name: The name to use for this module in urls and other places
where a unique name is needed.
format: The format of this module ('Homework', 'Lab', etc)
......@@ -452,12 +401,36 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
self._child_instances = None
self._inherited_metadata = set()
self._child_instances = None
def get_children(self):
"""Returns a list of XModuleDescriptor instances for the children of
this module"""
if not self.has_children:
return []
if self._child_instances is None:
self._child_instances = []
for child_loc in self.children:
try:
child = self.system.load_item(child_loc)
except ItemNotFoundError:
log.exception('Unable to load item {loc}, skipping'.format(loc=child_loc))
continue
# TODO (vshnayder): this should go away once we have
# proper inheritance support in mongo. The xml
# datastore does all inheritance on course load.
#child.inherit_metadata(self.metadata)
self._child_instances.append(child)
return self._child_instances
def get_child_by_url_name(self, url_name):
"""
Return a child XModuleDescriptor with the specified url_name, if it exists, and None otherwise.
"""
for c in self.children:
for c in self.get_children():
if c.url_name == url_name:
return c
return None
......@@ -472,7 +445,7 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
system,
self.location,
self,
system.xmodule_model_data(self.model_data),
system.xmodule_model_data(self._model_data),
)
def has_dynamic_children(self):
......@@ -686,6 +659,7 @@ class ModuleSystem(object):
get_module,
render_template,
replace_urls,
xmodule_model_data,
user=None,
filestore=None,
debug=False,
......@@ -739,6 +713,7 @@ class ModuleSystem(object):
self.node_path = node_path
self.anonymous_student_id = anonymous_student_id
self.user_is_staff = user is not None and user.is_staff
self.xmodule_model_data = xmodule_model_data
def get(self, attr):
''' provide uniform access to attributes (like etree).'''
......@@ -748,9 +723,6 @@ class ModuleSystem(object):
'''provide uniform access to attributes (like etree)'''
self.__dict__[attr] = val
def xmodule_module_data(self, module_data):
return module_data
def __repr__(self):
return repr(self.__dict__)
......
......@@ -148,7 +148,7 @@ def grade(student, request, course, student_module_cache=None, keep_raw_scores=F
format_scores = []
for section in sections:
section_descriptor = section['section_descriptor']
section_name = section_descriptor.metadata.get('display_name')
section_name = section_descriptor.display_name
should_grade_section = False
# If we haven't seen a single problem in the section, we don't have to grade it at all! We can assume 0%
......@@ -276,14 +276,14 @@ def progress_summary(student, request, course, student_module_cache):
# Don't include chapters that aren't displayable (e.g. due to error)
for chapter_module in course_module.get_display_items():
# Skip if the chapter is hidden
hidden = chapter_module.metadata.get('hide_from_toc','false')
hidden = chapter_module._model_data.get('hide_from_toc','false')
if hidden.lower() == 'true':
continue
sections = []
for section_module in chapter_module.get_display_items():
# Skip if the section is hidden
hidden = section_module.metadata.get('hide_from_toc','false')
hidden = section_module._model_data.get('hide_from_toc','false')
if hidden.lower() == 'true':
continue
......
......@@ -88,8 +88,7 @@ def toc_for_course(user, request, course, active_chapter, active_section):
chapters = list()
for chapter in course_module.get_display_items():
hide_from_toc = chapter.metadata.get('hide_from_toc','false').lower() == 'true'
if hide_from_toc:
if chapter.lms.hide_from_toc:
continue
sections = list()
......@@ -97,18 +96,17 @@ def toc_for_course(user, request, course, active_chapter, active_section):
active = (chapter.url_name == active_chapter and
section.url_name == active_section)
hide_from_toc = section.metadata.get('hide_from_toc', 'false').lower() == 'true'
if not hide_from_toc:
sections.append({'display_name': section.display_name,
if not section.lms.hide_from_toc:
sections.append({'display_name': section.lms.display_name,
'url_name': section.url_name,
'format': section.metadata.get('format', ''),
'due': section.metadata.get('due', ''),
'format': section.lms.format,
'due': section.lms.due,
'active': active,
'graded': section.metadata.get('graded', False),
'graded': section.lms.graded,
})
chapters.append({'display_name': chapter.display_name,
chapters.append({'display_name': chapter.lms.display_name,
'url_name': chapter.url_name,
'sections': sections,
'active': chapter.url_name == active_chapter})
......@@ -146,7 +144,8 @@ def get_module(user, request, location, student_module_cache, course_id, positio
log.exception("Error in get_module")
return None
def _get_module(user, request, location, student_module_cache, course_id, position=None, wrap_xmodule_display = True):
def _get_module(user, request, location, student_module_cache, course_id, position=None, wrap_xmodule_display=True):
"""
Actually implement get_module. See docstring there for details.
"""
......@@ -268,7 +267,7 @@ def _get_module(user, request, location, student_module_cache, course_id, positi
module.get_html = replace_static_urls(
_get_html,
module.metadata['data_dir'] if 'data_dir' in module.metadata else '',
getattr(module, 'data_dir', ''),
course_namespace = module.location._replace(category=None, name=None))
# Allow URLs of the form '/course/' refer to the root of multicourse directory
......
from setuptools import setup, find_packages
setup(
name="edX LMS",
version="0.1",
install_requires=['distribute'],
requires=[
'xmodule',
],
# See http://guide.python-distribute.org/creation.html#entry-points
# for a description of entry_points
entry_points={
'xmodule.namespace': [
'lms = lms.xmodule_namespace:LmsNamespace'
],
}
)
\ No newline at end of file
<h2>${chapter_module.display_name}</h2>
<h2>${chapter_module.lms.display_name}</h2>
<p>You were most recently in <a href="${prev_section_url}">${prev_section.display_name}</a>. If you're done with that, choose another section on the left.</p>
<p>You were most recently in <a href="${prev_section_url}">${prev_section.lms.display_name}</a>. If you're done with that, choose another section on the left.</p>
......@@ -2,7 +2,7 @@
<h2> ${display_name} </h2>
% endif
<div id="video_${id}" class="video" data-streams="${streams}" data-caption-data-dir="${data_dir}" data-caption-asset-path="${caption_asset_path}" data-show-captions="${show_captions}">
<div id="video_${id}" class="video" data-streams="${streams}" data-caption-asset-path="${caption_asset_path}" data-show-captions="${show_captions}">
<div class="tc-wrapper">
<article class="video-wrapper">
<section class="video-player">
......
from xmodule.model import Namespace, Boolean, Scope, String
from xmodule.x_module import Date
class LmsNamespace(Namespace):
hide_from_toc = Boolean(
help="Whether to display this module in the table of contents",
default=False,
scope=Scope.settings
)
graded = Boolean(
help="Whether this module contributes to the final course grade",
default=False,
scope=Scope.settings
)
format = String(
help="What format this module is in (used for deciding which "
"grader to apply, and what to show in the TOC)",
scope=Scope.settings
)
display_name = String(help="Display name for this module", scope=Scope.settings)
start = Date(help="Start time when this module is visible", scope=Scope.settings)
due = String(help="Date that this problem is due by", scope=Scope.settings, default='')
# Python libraries to install that are local to the mitx repo
-e common/lib/capa
-e common/lib/xmodule
-e lms
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