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): ...@@ -115,7 +115,7 @@ def index(request):
return render_to_response('index.html', { return render_to_response('index.html', {
'new_course_template' : Location('i4x', 'edx', 'templates', 'course', 'Empty'), 'new_course_template' : Location('i4x', 'edx', 'templates', 'course', 'Empty'),
'courses': [(course.metadata.get('display_name'), 'courses': [(course.title,
reverse('course_index', args=[ reverse('course_index', args=[
course.location.org, course.location.org,
course.location.course, course.location.course,
...@@ -269,7 +269,7 @@ def edit_unit(request, location): ...@@ -269,7 +269,7 @@ def edit_unit(request, location):
for template in templates: for template in templates:
if template.location.category in COMPONENT_TYPES: if template.location.category in COMPONENT_TYPES:
component_templates[template.location.category].append(( component_templates[template.location.category].append((
template.display_name, template.lms.display_name,
template.location.url(), template.location.url(),
)) ))
......
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
<article class="subsection-body window" data-id="${subsection.location}"> <article class="subsection-body window" data-id="${subsection.location}">
<div class="subsection-name-input"> <div class="subsection-name-input">
<label>Display Name:</label> <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>
<div> <div>
<label>Format:</label> <label>Format:</label>
...@@ -62,11 +62,11 @@ ...@@ -62,11 +62,11 @@
</div> </div>
% if subsection.start != parent_item.start and subsection.start: % if subsection.start != parent_item.start and subsection.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.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:
<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 % 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 % endif
</div> </div>
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
<div>${module_type}</div> <div>${module_type}</div>
<div> <div>
% for template in module_templates: % 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 % endfor
</div> </div>
</div> </div>
......
...@@ -132,9 +132,9 @@ ...@@ -132,9 +132,9 @@
<div class="item-details" data-id="${section.location}"> <div class="item-details" data-id="${section.location}">
<h3 class="section-name"> <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"> <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="submit" class="save-button edit-section-name-save" value="Save" />
<input type="button" class="cancel-button edit-section-name-cancel" value="Cancel" /> <input type="button" class="cancel-button edit-section-name-cancel" value="Cancel" />
</form> </form>
...@@ -174,7 +174,7 @@ ...@@ -174,7 +174,7 @@
<a href="#" data-tooltip="Expand/collapse this subsection" class="expand-collapse-icon expand"></a> <a href="#" data-tooltip="Expand/collapse this subsection" class="expand-collapse-icon expand"></a>
<a href="${reverse('edit_subsection', args=[subsection.location])}"> <a href="${reverse('edit_subsection', args=[subsection.location])}">
<span class="folder-icon"></span> <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> </a>
</div> </div>
......
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
</div> </div>
<div class="main-column"> <div class="main-column">
<article class="unit-body window"> <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"> <ol class="components">
% for id in components: % for id in components:
<li class="component" data-id="${id}"/> <li class="component" data-id="${id}"/>
...@@ -85,7 +85,7 @@ ...@@ -85,7 +85,7 @@
% if release_date is not None: % if release_date is not None:
on <strong>${release_date}</strong> on <strong>${release_date}</strong>
% endif % 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>
<div class="row unit-actions"> <div class="row unit-actions">
<a href="#" class="delete-draft delete-button">Delete Draft</a> <a href="#" class="delete-draft delete-button">Delete Draft</a>
...@@ -100,12 +100,12 @@ ...@@ -100,12 +100,12 @@
<div><input type="text" class="url" value="/courseware/${section.url_name}/${subsection.url_name}" disabled /></div> <div><input type="text" class="url" value="/courseware/${section.url_name}/${subsection.url_name}" disabled /></div>
<ol> <ol>
<li> <li>
<a href="#" class="section-item">${section.display_name}</a> <a href="#" class="section-item">${section.lms.display_name}</a>
<ol> <ol>
<li> <li>
<a href="${reverse('edit_subsection', args=[subsection.location])}" class="section-item"> <a href="${reverse('edit_subsection', args=[subsection.location])}" class="section-item">
<span class="folder-icon"></span> <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> </a>
${units.enum_units(subsection, actions=False, selected=unit.location)} ${units.enum_units(subsection, actions=False, selected=unit.location)}
</li> </li>
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
% if context_course: % if context_course:
<% ctx_loc = context_course.location %> <% ctx_loc = context_course.location %>
<a href="/" class="home"><span class="small-home-icon"></span></a> <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 % endif
</div> </div>
......
...@@ -60,7 +60,7 @@ ...@@ -60,7 +60,7 @@
data-type="${module.js_module_name}" data-type="${module.js_module_name}"
data-preview-type="${module.module_class.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> </li>
% endfor % endfor
<%include file="module-dropdown.html"/> <%include file="module-dropdown.html"/>
......
...@@ -40,7 +40,7 @@ ...@@ -40,7 +40,7 @@
<a href="#" class="module-edit" <a href="#" class="module-edit"
data-id="${child.location.url()}" data-id="${child.location.url()}"
data-type="${child.js_module_name}" 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> <a href="#" class="draggable">handle</a>
</li> </li>
%endfor %endfor
......
...@@ -22,7 +22,7 @@ This def will enumerate through a passed in subsection and list all of the units ...@@ -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}"> <div class="section-item ${selected_class}">
<a href="${reverse('edit_unit', args=[unit.location])}" class="${unit_state}-item"> <a href="${reverse('edit_unit', args=[unit.location])}" class="${unit_state}-item">
<span class="${unit.category}-icon"></span> <span class="${unit.category}-icon"></span>
<span class="unit-name">${unit.display_name}</span> <span class="unit-name">${unit.lms.display_name}</span>
</a> </a>
% if actions: % if actions:
<div class="item-actions"> <div class="item-actions">
......
...@@ -231,7 +231,7 @@ def change_enrollment(request): ...@@ -231,7 +231,7 @@ def change_enrollment(request):
if not has_access(user, course, 'enroll'): if not has_access(user, course, 'enroll'):
return {'success': False, return {'success': False,
'error': 'enrollment in {} not allowed at this time' 'error': 'enrollment in {} not allowed at this time'
.format(course.display_name)} .format(course.lms.display_name)}
org, course_num, run=course_id.split("/") org, course_num, run=course_id.split("/")
statsd.increment("common.student.enrollment", statsd.increment("common.student.enrollment",
......
...@@ -32,7 +32,7 @@ def wrap_xmodule(get_html, module, template, context=None): ...@@ -32,7 +32,7 @@ def wrap_xmodule(get_html, module, template, context=None):
def _get_html(): def _get_html():
context.update({ context.update({
'content': get_html(), '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__, 'class_': module.__class__.__name__,
'module_name': module.js_module_name 'module_name': module.js_module_name
}) })
......
...@@ -40,6 +40,6 @@ setup( ...@@ -40,6 +40,6 @@ setup(
"static_tab = xmodule.html_module:StaticTabDescriptor", "static_tab = xmodule.html_module:StaticTabDescriptor",
"custom_tag_template = xmodule.raw_module:RawDescriptor", "custom_tag_template = xmodule.raw_module:RawDescriptor",
"about = xmodule.html_module:AboutDescriptor" "about = xmodule.html_module:AboutDescriptor"
] ],
} }
) )
...@@ -21,8 +21,6 @@ from xmodule.raw_module import RawDescriptor ...@@ -21,8 +21,6 @@ from xmodule.raw_module import RawDescriptor
from xmodule.exceptions import NotFoundError from xmodule.exceptions import NotFoundError
from .model import Int, Scope, ModuleScope, ModelType, String, Boolean, Object, Float from .model import Int, Scope, ModuleScope, ModelType, String, Boolean, Object, Float
Date = Timedelta = ModelType
log = logging.getLogger("mitx.courseware") log = logging.getLogger("mitx.courseware")
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
...@@ -45,25 +43,34 @@ def only_one(lst, default="", process=lambda x: x): ...@@ -45,25 +43,34 @@ def only_one(lst, default="", process=lambda x: x):
raise Exception('Malformed XML: expected at most one element in list.') raise Exception('Malformed XML: expected at most one element in list.')
def parse_timedelta(time_str): class Timedelta(ModelType):
""" def from_json(self, time_str):
time_str: A string with the following components: """
<D> day[s] (optional) time_str: A string with the following components:
<H> hour[s] (optional) <D> day[s] (optional)
<M> minute[s] (optional) <H> hour[s] (optional)
<S> second[s] (optional) <M> minute[s] (optional)
<S> second[s] (optional)
Returns a datetime.timedelta parsed from the string Returns a datetime.timedelta parsed from the string
""" """
parts = TIMEDELTA_REGEX.match(time_str) parts = TIMEDELTA_REGEX.match(time_str)
if not parts: if not parts:
return return
parts = parts.groupdict() parts = parts.groupdict()
time_params = {} time_params = {}
for (name, param) in parts.iteritems(): for (name, param) in parts.iteritems():
if param: if param:
time_params[name] = int(param) time_params[name] = int(param)
return timedelta(**time_params) 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): class ComplexEncoder(json.JSONEncoder):
...@@ -82,7 +89,7 @@ class CapaModule(XModule): ...@@ -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) 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) 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) 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") 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) 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): ...@@ -90,6 +97,7 @@ class CapaModule(XModule):
data = String(help="XML data for the problem", scope=Scope.content) 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) 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) 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'), js = {'coffee': [resource_string(__name__, 'js/src/capa/display.coffee'),
resource_string(__name__, 'js/src/collapsible.coffee'), resource_string(__name__, 'js/src/collapsible.coffee'),
...@@ -104,8 +112,13 @@ class CapaModule(XModule): ...@@ -104,8 +112,13 @@ class CapaModule(XModule):
def __init__(self, system, location, descriptor, model_data): def __init__(self, system, location, descriptor, model_data):
XModule.__init__(self, system, location, descriptor, model_data) XModule.__init__(self, system, location, descriptor, model_data)
if self.graceperiod is not None and self.due: if self.due:
self.close_date = self.due + self.graceperiod 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 + #log.debug("Then parsed " + grace_period_string +
# " to closing date" + str(self.close_date)) # " to closing date" + str(self.close_date))
else: else:
......
...@@ -13,8 +13,7 @@ import time ...@@ -13,8 +13,7 @@ import time
import copy import copy
from .model import Scope, ModelType, List, String, Object, Boolean from .model import Scope, ModelType, List, String, Object, Boolean
from .x_module import Date
Date = ModelType
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -31,6 +30,10 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -31,6 +30,10 @@ class CourseDescriptor(SequenceDescriptor):
end = Date(help="Date that this class ends", scope=Scope.settings) 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) 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) 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') info_sidebar_name = String(scope=Scope.settings, default='Course Handouts')
...@@ -342,10 +345,6 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -342,10 +345,6 @@ class CourseDescriptor(SequenceDescriptor):
def tabs(self, value): def tabs(self, value):
self.metadata['tabs'] = value self.metadata['tabs'] = value
@property
def show_calculator(self):
return self.metadata.get("show_calculator", None) == "Yes"
@lazyproperty @lazyproperty
def grading_context(self): def grading_context(self):
""" """
...@@ -433,6 +432,7 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -433,6 +432,7 @@ class CourseDescriptor(SequenceDescriptor):
@property @property
def start_date_text(self): def start_date_text(self):
print self.advertised_start, self.start
return time.strftime("%b %d, %Y", self.advertised_start or self.start) return time.strftime("%b %d, %Y", self.advertised_start or self.start)
@property @property
......
...@@ -3,8 +3,7 @@ from pkg_resources import resource_string, resource_listdir ...@@ -3,8 +3,7 @@ from pkg_resources import resource_string, resource_listdir
from xmodule.x_module import XModule from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor from xmodule.raw_module import RawDescriptor
from .model import String, Scope
import json
class DiscussionModule(XModule): class DiscussionModule(XModule):
js = {'coffee': js = {'coffee':
...@@ -12,18 +11,19 @@ class DiscussionModule(XModule): ...@@ -12,18 +11,19 @@ class DiscussionModule(XModule):
resource_string(__name__, 'js/src/discussion/display.coffee')] resource_string(__name__, 'js/src/discussion/display.coffee')]
} }
js_module_name = "InlineDiscussion" js_module_name = "InlineDiscussion"
data = String(help="XML definition of inline discussion", scope=Scope.content)
def get_html(self): def get_html(self):
context = { context = {
'discussion_id': self.discussion_id, 'discussion_id': self.discussion_id,
} }
return self.system.render_template('discussion/_discussion_module.html', context) return self.system.render_template('discussion/_discussion_module.html', context)
def __init__(self, system, location, definition, descriptor, instance_state=None, shared_state=None, **kwargs): def __init__(self, *args, **kwargs):
XModule.__init__(self, system, location, definition, descriptor, instance_state, shared_state, **kwargs) XModule.__init__(self, *args, **kwargs)
if isinstance(instance_state, str): xml_data = etree.fromstring(self.data)
instance_state = json.loads(instance_state)
xml_data = etree.fromstring(definition['data'])
self.discussion_id = xml_data.attrib['id'] self.discussion_id = xml_data.attrib['id']
self.title = xml_data.attrib['for'] self.title = xml_data.attrib['for']
self.discussion_category = xml_data.attrib['discussion_category'] self.discussion_category = xml_data.attrib['discussion_category']
......
...@@ -15,6 +15,7 @@ from .html_checker import check_html ...@@ -15,6 +15,7 @@ from .html_checker import check_html
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.contentstore.content import XASSET_SRCREF_PREFIX, StaticContent from xmodule.contentstore.content import XASSET_SRCREF_PREFIX, StaticContent
from .model import Scope, String
log = logging.getLogger("mitx.courseware") log = logging.getLogger("mitx.courseware")
...@@ -26,15 +27,11 @@ class HtmlModule(XModule): ...@@ -26,15 +27,11 @@ class HtmlModule(XModule):
] ]
} }
js_module_name = "HTMLModule" js_module_name = "HTMLModule"
data = String(help="Html contents to display for this module", scope=Scope.content)
def get_html(self): def get_html(self):
return self.html return self.data
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']
......
...@@ -2,7 +2,6 @@ class @Video ...@@ -2,7 +2,6 @@ class @Video
constructor: (element) -> constructor: (element) ->
@el = $(element).find('.video') @el = $(element).find('.video')
@id = @el.attr('id').replace(/video_/, '') @id = @el.attr('id').replace(/video_/, '')
@caption_data_dir = @el.data('caption-data-dir')
@caption_asset_path = @el.data('caption-asset-path') @caption_asset_path = @el.data('caption-asset-path')
@show_captions = @el.data('show-captions') == "true" @show_captions = @el.data('show-captions') == "true"
window.player = null window.player = null
......
from collections import namedtuple from collections import namedtuple
from .plugin import Plugin
class ModuleScope(object): class ModuleScope(object):
USAGE, DEFINITION, TYPE, ALL = xrange(4) USAGE, DEFINITION, TYPE, ALL = xrange(4)
...@@ -15,6 +17,13 @@ Scope.student_info = Scope(student=True, module=ModuleScope.ALL) ...@@ -15,6 +17,13 @@ Scope.student_info = Scope(student=True, module=ModuleScope.ALL)
class ModelType(object): 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 sequence = 0
def __init__(self, help=None, default=None, scope=Scope.content): def __init__(self, help=None, default=None, scope=Scope.content):
...@@ -33,10 +42,13 @@ class ModelType(object): ...@@ -33,10 +42,13 @@ class ModelType(object):
if instance is None: if instance is None:
return self 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): def __set__(self, instance, value):
instance._model_data[self.name] = value instance._model_data[self.name] = self.to_json(value)
def __delete__(self, instance): def __delete__(self, instance):
del instance._model_data[self.name] del instance._model_data[self.name]
...@@ -47,27 +59,27 @@ class ModelType(object): ...@@ -47,27 +59,27 @@ class ModelType(object):
def __lt__(self, other): def __lt__(self, other):
return self._seq < other._seq 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 Int = Float = Boolean = Object = List = String = Any = ModelType
class ModelMetaclass(type): class ModelMetaclass(type):
def __new__(cls, name, bases, attrs): """
# Find registered methods A metaclass to be used for classes that want to use ModelTypes as class attributes
reg_methods = {} to define data access.
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)
@property All class attributes that are ModelTypes will be added to the 'fields' attribute on
def child_map(self): the instance.
return dict((child.name, child) for child in self.children)
attrs['child_map'] = child_map
Additionally, any namespaces registered in the `xmodule.namespace` will be added to
the instance
"""
def __new__(cls, name, bases, attrs):
fields = [] fields = []
for n, v in attrs.items(): for n, v in attrs.items():
if isinstance(v, ModelType): if isinstance(v, ModelType):
...@@ -77,3 +89,61 @@ class ModelMetaclass(type): ...@@ -77,3 +89,61 @@ class ModelMetaclass(type):
attrs['fields'] = fields attrs['fields'] = fields
return super(ModelMetaclass, cls).__new__(cls, name, bases, attrs) 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 pymongo
import sys import sys
import logging
from bson.son import SON from bson.son import SON
from fs.osfs import OSFS from fs.osfs import OSFS
...@@ -9,8 +8,8 @@ from path import path ...@@ -9,8 +8,8 @@ from path import path
from importlib import import_module from importlib import import_module
from xmodule.errortracker import null_error_tracker, exc_info_to_str 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.mako_module import MakoDescriptorSystem
from xmodule.x_module import XModuleDescriptor
from xmodule.error_module import ErrorDescriptor from xmodule.error_module import ErrorDescriptor
from . import ModuleStoreBase, Location from . import ModuleStoreBase, Location
......
...@@ -192,7 +192,7 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): ...@@ -192,7 +192,7 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
xmlstore.modules[course_id][descriptor.location] = descriptor xmlstore.modules[course_id][descriptor.location] = descriptor
if hasattr(descriptor, 'children'): if hasattr(descriptor, 'children'):
for child in descriptor.children: for child in descriptor.get_children():
parent_tracker.add_parent(child.location, descriptor.location) parent_tracker.add_parent(child.location, descriptor.location)
return descriptor return descriptor
...@@ -318,8 +318,6 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -318,8 +318,6 @@ class XMLModuleStore(ModuleStoreBase):
# Didn't load course. Instead, save the errors elsewhere. # Didn't load course. Instead, save the errors elsewhere.
self.errored_courses[course_dir] = errorlog self.errored_courses[course_dir] = errorlog
def __unicode__(self): def __unicode__(self):
''' '''
String representation - for debugging String representation - for debugging
...@@ -345,8 +343,6 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -345,8 +343,6 @@ class XMLModuleStore(ModuleStoreBase):
log.warning(msg + " " + str(err)) log.warning(msg + " " + str(err))
return {} return {}
def load_course(self, course_dir, tracker): def load_course(self, course_dir, tracker):
""" """
Load a course into this module store Load a course into this module store
...@@ -363,7 +359,7 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -363,7 +359,7 @@ class XMLModuleStore(ModuleStoreBase):
# been imported into the cms from xml # been imported into the cms from xml
course_file = StringIO(clean_out_mako_templating(course_file.read())) 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') 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 ...@@ -8,6 +8,7 @@ from xmodule.xml_module import XmlDescriptor
from xmodule.x_module import XModule from xmodule.x_module import XModule
from xmodule.progress import Progress from xmodule.progress import Progress
from xmodule.exceptions import NotFoundError from xmodule.exceptions import NotFoundError
from .model import Int, Scope
from pkg_resources import resource_string from pkg_resources import resource_string
log = logging.getLogger("mitx.common.lib.seq_module") log = logging.getLogger("mitx.common.lib.seq_module")
...@@ -16,6 +17,12 @@ log = logging.getLogger("mitx.common.lib.seq_module") ...@@ -16,6 +17,12 @@ log = logging.getLogger("mitx.common.lib.seq_module")
# OBSOLETE: This obsoletes 'type' # OBSOLETE: This obsoletes 'type'
class_priority = ['video', 'problem'] 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): class SequenceModule(XModule):
''' Layout module which lays out content in a temporal sequence ''' Layout module which lays out content in a temporal sequence
...@@ -26,22 +33,18 @@ class SequenceModule(XModule): ...@@ -26,22 +33,18 @@ class SequenceModule(XModule):
css = {'scss': [resource_string(__name__, 'css/sequence/display.scss')]} css = {'scss': [resource_string(__name__, 'css/sequence/display.scss')]}
js_module_name = "Sequence" js_module_name = "Sequence"
def __init__(self, system, location, definition, descriptor, instance_state=None, has_children = True
shared_state=None, **kwargs):
XModule.__init__(self, system, location, definition, descriptor, # NOTE: Position is 1-indexed. This is silly, but there are now student
instance_state, shared_state, **kwargs) # positions saved on prod, so it's not easy to fix.
# NOTE: Position is 1-indexed. This is silly, but there are now student position = Int(help="Last tab viewed in this sequence", default=1, scope=Scope.student_state)
# positions saved on prod, so it's not easy to fix.
self.position = 1
if instance_state is not None: def __init__(self, *args, **kwargs):
state = json.loads(instance_state) XModule.__init__(self, *args, **kwargs)
if 'position' in state:
self.position = int(state['position'])
# if position is specified in system, then use that instead # if position is specified in system, then use that instead
if system.get('position'): if self.system.get('position'):
self.position = int(system.get('position')) self.position = int(self.system.get('position'))
self.rendered = False self.rendered = False
...@@ -79,9 +82,9 @@ class SequenceModule(XModule): ...@@ -79,9 +82,9 @@ class SequenceModule(XModule):
childinfo = { childinfo = {
'content': child.get_html(), 'content': child.get_html(),
'title': "\n".join( 'title': "\n".join(
grand_child.display_name.strip() display_name(grand_child)
for grand_child in child.get_children() 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_status': Progress.to_js_status_str(progress),
'progress_detail': Progress.to_js_detail_str(progress), 'progress_detail': Progress.to_js_detail_str(progress),
...@@ -89,7 +92,7 @@ class SequenceModule(XModule): ...@@ -89,7 +92,7 @@ class SequenceModule(XModule):
'id': child.id, 'id': child.id,
} }
if childinfo['title']=='': if childinfo['title']=='':
childinfo['title'] = child.metadata.get('display_name','') childinfo['title'] = display_name(child)
contents.append(childinfo) contents.append(childinfo)
params = {'items': contents, params = {'items': contents,
...@@ -116,7 +119,8 @@ class SequenceDescriptor(MakoModuleDescriptor, XmlDescriptor): ...@@ -116,7 +119,8 @@ class SequenceDescriptor(MakoModuleDescriptor, XmlDescriptor):
mako_template = 'widgets/sequence-edit.html' mako_template = 'widgets/sequence-edit.html'
module_class = SequenceModule 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 = {'coffee': [resource_string(__name__, 'js/src/sequence/edit.coffee')]}
js_module_name = "SequenceDescriptor" js_module_name = "SequenceDescriptor"
......
...@@ -11,8 +11,10 @@ class_priority = ['video', 'problem'] ...@@ -11,8 +11,10 @@ class_priority = ['video', 'problem']
class VerticalModule(XModule): class VerticalModule(XModule):
''' Layout module for laying out submodules vertically.''' ''' Layout module for laying out submodules vertically.'''
def __init__(self, system, location, definition, descriptor, instance_state=None, shared_state=None, **kwargs): has_children = True
XModule.__init__(self, system, location, definition, descriptor, instance_state, shared_state, **kwargs)
def __init__(self, *args, **kwargs):
XModule.__init__(self, *args, **kwargs)
self.contents = None self.contents = None
def get_html(self): def get_html(self):
...@@ -45,6 +47,8 @@ class VerticalModule(XModule): ...@@ -45,6 +47,8 @@ class VerticalModule(XModule):
class VerticalDescriptor(SequenceDescriptor): class VerticalDescriptor(SequenceDescriptor):
module_class = VerticalModule module_class = VerticalModule
has_children = True
js = {'coffee': [resource_string(__name__, 'js/src/vertical/edit.coffee')]} js = {'coffee': [resource_string(__name__, 'js/src/vertical/edit.coffee')]}
js_module_name = "VerticalDescriptor" js_module_name = "VerticalDescriptor"
...@@ -9,6 +9,7 @@ from xmodule.raw_module import RawDescriptor ...@@ -9,6 +9,7 @@ from xmodule.raw_module import RawDescriptor
from xmodule.modulestore.mongo import MongoModuleStore from xmodule.modulestore.mongo import MongoModuleStore
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.contentstore.content import StaticContent from xmodule.contentstore.content import StaticContent
from .model import Int, Scope, String
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -27,22 +28,20 @@ class VideoModule(XModule): ...@@ -27,22 +28,20 @@ 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"
def __init__(self, system, location, definition, descriptor, data = String(help="XML data for the problem", scope=Scope.content)
instance_state=None, shared_state=None, **kwargs): position = Int(help="Current position in the video", scope=Scope.student_state)
XModule.__init__(self, system, location, definition, descriptor, display_name = String(help="Display name for this module", scope=Scope.settings)
instance_state, shared_state, **kwargs)
xmltree = etree.fromstring(self.definition['data']) def __init__(self, *args, **kwargs):
XModule.__init__(self, *args, **kwargs)
xmltree = etree.fromstring(self.data)
self.youtube = xmltree.get('youtube') self.youtube = xmltree.get('youtube')
self.position = 0 self.position = 0
self.show_captions = xmltree.get('show_captions', 'true') self.show_captions = xmltree.get('show_captions', 'true')
self.source = self._get_source(xmltree) self.source = self._get_source(xmltree)
self.track = self._get_track(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): def _get_source(self, xmltree):
# find the first valid source # find the first valid source
return self._get_first_external(xmltree, 'source') return self._get_first_external(xmltree, 'source')
...@@ -102,7 +101,7 @@ class VideoModule(XModule): ...@@ -102,7 +101,7 @@ class VideoModule(XModule):
else: else:
# VS[compat] # VS[compat]
# cdodge: filesystem static content support. # 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', { return self.system.render_template('video.html', {
'streams': self.video_list(), 'streams': self.video_list(),
...@@ -111,8 +110,6 @@ class VideoModule(XModule): ...@@ -111,8 +110,6 @@ class VideoModule(XModule):
'source': self.source, 'source': self.source,
'track' : self.track, 'track' : self.track,
'display_name': self.display_name, '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, 'caption_asset_path': caption_asset_path,
'show_captions': self.show_captions 'show_captions': self.show_captions
}) })
......
import logging import logging
import pkg_resources
import yaml import yaml
import os import os
import time import time
...@@ -10,9 +9,9 @@ from collections import namedtuple ...@@ -10,9 +9,9 @@ from collections import namedtuple
from pkg_resources import resource_listdir, resource_string, resource_isdir from pkg_resources import resource_listdir, resource_string, resource_isdir
from xmodule.modulestore import Location 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): class Date(ModelType):
...@@ -38,72 +37,15 @@ class Date(ModelType): ...@@ -38,72 +37,15 @@ class Date(ModelType):
""" """
return time.strftime(self.time_format, value) return time.strftime(self.time_format, value)
log = logging.getLogger('mitx.' + __name__)
def dummy_track(event_type, event):
pass
class XModuleMetaclass(ParentModelMetaclass, NamespacesMetaclass, ModelMetaclass):
class ModuleMissingError(Exception):
pass 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 dummy_track(event_type, event):
def load_classes(cls): pass
"""
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)]
class HTMLSnippet(object): class HTMLSnippet(object):
...@@ -179,9 +121,7 @@ class XModule(HTMLSnippet): ...@@ -179,9 +121,7 @@ class XModule(HTMLSnippet):
See the HTML module for a simple example. See the HTML module for a simple example.
''' '''
__metaclass__ = ModelMetaclass __metaclass__ = XModuleMetaclass
display_name = String(help="Display name for this module", scope=Scope(student=False, module=ModuleScope.USAGE))
# The default implementation of get_icon_class returns the icon_class # The default implementation of get_icon_class returns the icon_class
# attribute of the class # attribute of the class
...@@ -244,9 +184,22 @@ class XModule(HTMLSnippet): ...@@ -244,9 +184,22 @@ class XModule(HTMLSnippet):
self.url_name = self.location.name self.url_name = self.location.name
self.category = self.location.category self.category = self.location.category
self._model_data = model_data self._model_data = model_data
self._loaded_children = None
if self.display_name is None: def get_children(self):
self.display_name = self.url_name.replace('_', ' ') '''
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): def __unicode__(self):
return '<x_module(id={0})>'.format(self.id) return '<x_module(id={0})>'.format(self.id)
...@@ -257,7 +210,7 @@ class XModule(HTMLSnippet): ...@@ -257,7 +210,7 @@ class XModule(HTMLSnippet):
immediately inside this module. immediately inside this module.
''' '''
items = [] items = []
for child in self.children(): for child in self.get_children():
items.extend(child.displayable_items()) items.extend(child.displayable_items())
return items return items
...@@ -366,10 +319,8 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates): ...@@ -366,10 +319,8 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
""" """
entry_point = "xmodule.v1" entry_point = "xmodule.v1"
module_class = XModule 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 # Attributes for inspection of the descriptor
stores_state = False # Indicates whether the xmodule state should be stores_state = False # Indicates whether the xmodule state should be
# stored in a database (independent of shared state) # stored in a database (independent of shared state)
...@@ -430,8 +381,6 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates): ...@@ -430,8 +381,6 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
metadata: A dictionary containing the following optional keys: metadata: A dictionary containing the following optional keys:
goals: A list of strings of learning goals associated with this goals: A list of strings of learning goals associated with this
module 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 url_name: The name to use for this module in urls and other places
where a unique name is needed. where a unique name is needed.
format: The format of this module ('Homework', 'Lab', etc) format: The format of this module ('Homework', 'Lab', etc)
...@@ -452,12 +401,36 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates): ...@@ -452,12 +401,36 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
self._child_instances = None self._child_instances = None
self._inherited_metadata = set() 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): def get_child_by_url_name(self, url_name):
""" """
Return a child XModuleDescriptor with the specified url_name, if it exists, and None otherwise. 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: if c.url_name == url_name:
return c return c
return None return None
...@@ -472,7 +445,7 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates): ...@@ -472,7 +445,7 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
system, system,
self.location, self.location,
self, self,
system.xmodule_model_data(self.model_data), system.xmodule_model_data(self._model_data),
) )
def has_dynamic_children(self): def has_dynamic_children(self):
...@@ -686,6 +659,7 @@ class ModuleSystem(object): ...@@ -686,6 +659,7 @@ class ModuleSystem(object):
get_module, get_module,
render_template, render_template,
replace_urls, replace_urls,
xmodule_model_data,
user=None, user=None,
filestore=None, filestore=None,
debug=False, debug=False,
...@@ -739,6 +713,7 @@ class ModuleSystem(object): ...@@ -739,6 +713,7 @@ class ModuleSystem(object):
self.node_path = node_path self.node_path = node_path
self.anonymous_student_id = anonymous_student_id self.anonymous_student_id = anonymous_student_id
self.user_is_staff = user is not None and user.is_staff self.user_is_staff = user is not None and user.is_staff
self.xmodule_model_data = xmodule_model_data
def get(self, attr): def get(self, attr):
''' provide uniform access to attributes (like etree).''' ''' provide uniform access to attributes (like etree).'''
...@@ -748,9 +723,6 @@ class ModuleSystem(object): ...@@ -748,9 +723,6 @@ class ModuleSystem(object):
'''provide uniform access to attributes (like etree)''' '''provide uniform access to attributes (like etree)'''
self.__dict__[attr] = val self.__dict__[attr] = val
def xmodule_module_data(self, module_data):
return module_data
def __repr__(self): def __repr__(self):
return repr(self.__dict__) return repr(self.__dict__)
......
...@@ -148,7 +148,7 @@ def grade(student, request, course, student_module_cache=None, keep_raw_scores=F ...@@ -148,7 +148,7 @@ def grade(student, request, course, student_module_cache=None, keep_raw_scores=F
format_scores = [] format_scores = []
for section in sections: for section in sections:
section_descriptor = section['section_descriptor'] section_descriptor = section['section_descriptor']
section_name = section_descriptor.metadata.get('display_name') section_name = section_descriptor.display_name
should_grade_section = False 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% # 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): ...@@ -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) # Don't include chapters that aren't displayable (e.g. due to error)
for chapter_module in course_module.get_display_items(): for chapter_module in course_module.get_display_items():
# Skip if the chapter is hidden # 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': if hidden.lower() == 'true':
continue continue
sections = [] sections = []
for section_module in chapter_module.get_display_items(): for section_module in chapter_module.get_display_items():
# Skip if the section is hidden # 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': if hidden.lower() == 'true':
continue continue
......
...@@ -88,8 +88,7 @@ def toc_for_course(user, request, course, active_chapter, active_section): ...@@ -88,8 +88,7 @@ def toc_for_course(user, request, course, active_chapter, active_section):
chapters = list() chapters = list()
for chapter in course_module.get_display_items(): for chapter in course_module.get_display_items():
hide_from_toc = chapter.metadata.get('hide_from_toc','false').lower() == 'true' if chapter.lms.hide_from_toc:
if hide_from_toc:
continue continue
sections = list() sections = list()
...@@ -97,18 +96,17 @@ def toc_for_course(user, request, course, active_chapter, active_section): ...@@ -97,18 +96,17 @@ def toc_for_course(user, request, course, active_chapter, active_section):
active = (chapter.url_name == active_chapter and active = (chapter.url_name == active_chapter and
section.url_name == active_section) section.url_name == active_section)
hide_from_toc = section.metadata.get('hide_from_toc', 'false').lower() == 'true'
if not hide_from_toc: if not section.lms.hide_from_toc:
sections.append({'display_name': section.display_name, sections.append({'display_name': section.lms.display_name,
'url_name': section.url_name, 'url_name': section.url_name,
'format': section.metadata.get('format', ''), 'format': section.lms.format,
'due': section.metadata.get('due', ''), 'due': section.lms.due,
'active': active, '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, 'url_name': chapter.url_name,
'sections': sections, 'sections': sections,
'active': chapter.url_name == active_chapter}) 'active': chapter.url_name == active_chapter})
...@@ -146,7 +144,8 @@ def get_module(user, request, location, student_module_cache, course_id, positio ...@@ -146,7 +144,8 @@ def get_module(user, request, location, student_module_cache, course_id, positio
log.exception("Error in get_module") log.exception("Error in get_module")
return None 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. 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 ...@@ -268,7 +267,7 @@ def _get_module(user, request, location, student_module_cache, course_id, positi
module.get_html = replace_static_urls( module.get_html = replace_static_urls(
_get_html, _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)) course_namespace = module.location._replace(category=None, name=None))
# Allow URLs of the form '/course/' refer to the root of multicourse directory # 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 @@ ...@@ -2,7 +2,7 @@
<h2> ${display_name} </h2> <h2> ${display_name} </h2>
% endif % 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"> <div class="tc-wrapper">
<article class="video-wrapper"> <article class="video-wrapper">
<section class="video-player"> <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 # Python libraries to install that are local to the mitx repo
-e common/lib/capa -e common/lib/capa
-e common/lib/xmodule -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