Commit 01411ae6 by Calen Pennington

WIP: Trying to get tests working

parent 7e224f58
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
import logging import logging
log = logging.getLogger("mitx." + __name__) log = logging.getLogger(__name__)
from django.template import Context from django.template import Context
from django.http import HttpResponse from django.http import HttpResponse
......
...@@ -131,6 +131,7 @@ def add_histogram(get_html, module, user): ...@@ -131,6 +131,7 @@ def add_histogram(get_html, module, user):
is_released = "<font color='red'>Yes!</font>" if (now > mstart) else "<font color='green'>Not yet</font>" is_released = "<font color='red'>Yes!</font>" if (now > mstart) else "<font color='green'>Not yet</font>"
staff_context = {'fields': [(field.name, getattr(module, field.name)) for field in module.fields], staff_context = {'fields': [(field.name, getattr(module, field.name)) for field in module.fields],
'lms_fields': [(field.name, getattr(module.lms, field.name)) for field in module.lms.fields],
'location': module.location, 'location': module.location,
'xqa_key': module.lms.xqa_key, 'xqa_key': module.lms.xqa_key,
'source_file' : source_file, 'source_file' : source_file,
......
...@@ -72,7 +72,7 @@ global_context = {'random': random, ...@@ -72,7 +72,7 @@ global_context = {'random': random,
# These should be removed from HTML output, including all subelements # These should be removed from HTML output, including all subelements
html_problem_semantics = ["codeparam", "responseparam", "answer", "script", "hintgroup"] html_problem_semantics = ["codeparam", "responseparam", "answer", "script", "hintgroup"]
log = logging.getLogger('mitx.' + __name__) log = logging.getLogger(__name__)
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
# main class for this module # main class for this module
......
...@@ -17,7 +17,7 @@ from lxml import etree ...@@ -17,7 +17,7 @@ from lxml import etree
import xml.sax.saxutils as saxutils import xml.sax.saxutils as saxutils
from registry import TagRegistry from registry import TagRegistry
log = logging.getLogger('mitx.' + __name__) log = logging.getLogger(__name__)
registry = TagRegistry() registry = TagRegistry()
......
...@@ -44,7 +44,7 @@ import sys ...@@ -44,7 +44,7 @@ import sys
from registry import TagRegistry from registry import TagRegistry
log = logging.getLogger('mitx.' + __name__) log = logging.getLogger(__name__)
######################################################################### #########################################################################
......
...@@ -33,7 +33,7 @@ from lxml import etree ...@@ -33,7 +33,7 @@ from lxml import etree
from lxml.html.soupparser import fromstring as fromstring_bs # uses Beautiful Soup!!! FIXME? from lxml.html.soupparser import fromstring as fromstring_bs # uses Beautiful Soup!!! FIXME?
import xqueue_interface import xqueue_interface
log = logging.getLogger('mitx.' + __name__) log = logging.getLogger(__name__)
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
......
...@@ -7,7 +7,7 @@ import logging ...@@ -7,7 +7,7 @@ import logging
import requests import requests
log = logging.getLogger('mitx.' + __name__) log = logging.getLogger(__name__)
dateformat = '%Y%m%d%H%M%S' dateformat = '%Y%m%d%H%M%S'
def make_hashkey(seed): def make_hashkey(seed):
......
...@@ -551,8 +551,7 @@ class CapaModule(XModule): ...@@ -551,8 +551,7 @@ class CapaModule(XModule):
msg = "Error checking problem: " + str(err) msg = "Error checking problem: " + str(err)
msg += '\nTraceback:\n' + traceback.format_exc() msg += '\nTraceback:\n' + traceback.format_exc()
return {'success': msg} return {'success': msg}
log.exception("Error in capa_module problem checking") raise
raise Exception("error in capa_module")
self.attempts = self.attempts + 1 self.attempts = self.attempts + 1
self.lcp.done = True self.lcp.done = True
......
...@@ -131,9 +131,9 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -131,9 +131,9 @@ class CourseDescriptor(SequenceDescriptor):
if self.start is None: if self.start is None:
msg = "Course loaded without a valid start date. id = %s" % self.id msg = "Course loaded without a valid start date. id = %s" % self.id
# hack it -- start in 1970 # hack it -- start in 1970
self.lms.start = time.gmtime(0) self.start = time.gmtime(0)
log.critical(msg) log.critical(msg)
system.error_tracker(msg) self.system.error_tracker(msg)
# NOTE: relies on the modulestore to call set_grading_policy() right after # NOTE: relies on the modulestore to call set_grading_policy() right after
# init. (Modulestore is in charge of figuring out where to load the policy from) # init. (Modulestore is in charge of figuring out where to load the policy from)
......
...@@ -8,6 +8,7 @@ from xmodule.x_module import XModule ...@@ -8,6 +8,7 @@ from xmodule.x_module import XModule
from xmodule.editing_module import JSONEditingDescriptor from xmodule.editing_module import JSONEditingDescriptor
from xmodule.errortracker import exc_info_to_str from xmodule.errortracker import exc_info_to_str
from xmodule.modulestore import Location from xmodule.modulestore import Location
from .model import String, Scope
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -52,6 +53,10 @@ class ErrorDescriptor(JSONEditingDescriptor): ...@@ -52,6 +53,10 @@ class ErrorDescriptor(JSONEditingDescriptor):
""" """
module_class = ErrorModule module_class = ErrorModule
contents = String(scope=Scope.content)
error_msg = String(scope=Scope.content)
display_name = String(scope=Scope.settings)
@classmethod @classmethod
def _construct(self, system, contents, error_msg, location): def _construct(self, system, contents, error_msg, location):
...@@ -66,15 +71,12 @@ class ErrorDescriptor(JSONEditingDescriptor): ...@@ -66,15 +71,12 @@ class ErrorDescriptor(JSONEditingDescriptor):
name=hashlib.sha1(contents).hexdigest() name=hashlib.sha1(contents).hexdigest()
) )
definition = {
'data': {
'error_msg': str(error_msg),
'contents': contents,
}
}
# real metadata stays in the content, but add a display name # real metadata stays in the content, but add a display name
model_data = {'display_name': 'Error: ' + location.name} model_data = {
'error_msg': str(error_msg),
'contents': contents,
'display_name': 'Error: ' + location.name
}
return ErrorDescriptor( return ErrorDescriptor(
system, system,
location, location,
...@@ -84,7 +86,7 @@ class ErrorDescriptor(JSONEditingDescriptor): ...@@ -84,7 +86,7 @@ class ErrorDescriptor(JSONEditingDescriptor):
def get_context(self): def get_context(self):
return { return {
'module': self, 'module': self,
'data': self.definition['data']['contents'], 'data': self.contents,
} }
@classmethod @classmethod
...@@ -100,10 +102,7 @@ class ErrorDescriptor(JSONEditingDescriptor): ...@@ -100,10 +102,7 @@ class ErrorDescriptor(JSONEditingDescriptor):
def from_descriptor(cls, descriptor, error_msg='Error not available'): def from_descriptor(cls, descriptor, error_msg='Error not available'):
return cls._construct( return cls._construct(
descriptor.system, descriptor.system,
json.dumps({ json.dumps(descriptor._model_data, indent=4),
'definition': descriptor.definition,
'metadata': descriptor.metadata,
}, indent=4),
error_msg, error_msg,
location=descriptor.location, location=descriptor.location,
) )
...@@ -147,14 +146,14 @@ class ErrorDescriptor(JSONEditingDescriptor): ...@@ -147,14 +146,14 @@ class ErrorDescriptor(JSONEditingDescriptor):
files, etc. That would just get re-wrapped on import. files, etc. That would just get re-wrapped on import.
''' '''
try: try:
xml = etree.fromstring(self.definition['data']['contents']) xml = etree.fromstring(self.contents)
return etree.tostring(xml) return etree.tostring(xml)
except etree.XMLSyntaxError: except etree.XMLSyntaxError:
# still not valid. # still not valid.
root = etree.Element('error') root = etree.Element('error')
root.text = self.definition['data']['contents'] root.text = self.contents
err_node = etree.SubElement(root, 'error_msg') err_node = etree.SubElement(root, 'error_msg')
err_node.text = self.definition['data']['error_msg'] err_node.text = self.error_msg
return etree.tostring(root) return etree.tostring(root)
......
import time
import logging
from .model import ModelType
log = logging.getLogger(__name__)
class Date(ModelType):
time_format = "%Y-%m-%dT%H:%M"
def from_json(self, value):
"""
Parse an optional metadata key containing a time: if present, complain
if it doesn't parse.
Return None if not present or invalid.
"""
try:
return time.strptime(value, self.time_format)
except ValueError as e:
msg = "Field {0} has bad value '{1}': '{2}'".format(
self._name, value, e)
log.warning(msg)
return None
def to_json(self, value):
"""
Convert a time struct to a string
"""
return time.strftime(self.time_format, value)
...@@ -43,14 +43,14 @@ class ModelType(object): ...@@ -43,14 +43,14 @@ class ModelType(object):
if instance is None: if instance is None:
return self return self
if self.name not in instance._model_data: try:
return self.from_json(instance._model_data[self.name])
except KeyError:
if self.default is None and self.computed_default is not None: if self.default is None and self.computed_default is not None:
return self.computed_default(instance) return self.computed_default(instance)
return self.default 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] = self.to_json(value) instance._model_data[self.name] = self.to_json(value)
...@@ -166,18 +166,18 @@ class Namespace(Plugin): ...@@ -166,18 +166,18 @@ class Namespace(Plugin):
super(Namespace, self).__setattr__(name, value) super(Namespace, self).__setattr__(name, value)
return return
container_class_attr = getattr(type(container), name, None) namespace_attr = getattr(type(self), name, None)
if container_class_attr is None or not isinstance(container_class_attr, ModelType): if namespace_attr is None or not isinstance(namespace_attr, ModelType):
return super(Namespace, self).__setattr__(name, value) return super(Namespace, self).__setattr__(name, value)
return container_class_attr.__set__(container) return namespace_attr.__set__(container, value)
def __delattr__(self, name): def __delattr__(self, name):
container = super(Namespace, self).__getattribute__('_container') container = super(Namespace, self).__getattribute__('_container')
container_class_attr = getattr(type(container), name, None) namespace_attr = getattr(type(self), name, None)
if container_class_attr is None or not isinstance(container_class_attr, ModelType): if namespace_attr is None or not isinstance(namespace_attr, ModelType):
return super(Namespace, self).__detattr__(name) return super(Namespace, self).__detattr__(name)
return container_class_attr.__delete__(container) return namespace_attr.__delete__(container)
...@@ -28,7 +28,7 @@ edx_xml_parser = etree.XMLParser(dtd_validation=False, load_dtd=False, ...@@ -28,7 +28,7 @@ edx_xml_parser = etree.XMLParser(dtd_validation=False, load_dtd=False,
etree.set_default_parser(edx_xml_parser) etree.set_default_parser(edx_xml_parser)
log = logging.getLogger('mitx.' + __name__) log = logging.getLogger(__name__)
# VS[compat] # VS[compat]
...@@ -160,7 +160,6 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): ...@@ -160,7 +160,6 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
etree.tostring(xml_data), self, self.org, etree.tostring(xml_data), self, self.org,
self.course, xmlstore.default_class) self.course, xmlstore.default_class)
except Exception as err: except Exception as err:
print err, self.load_error_modules
if not self.load_error_modules: if not self.load_error_modules:
raise raise
......
...@@ -8,6 +8,7 @@ from .xml import XMLModuleStore ...@@ -8,6 +8,7 @@ from .xml import XMLModuleStore
from .exceptions import DuplicateItemError from .exceptions import DuplicateItemError
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.contentstore.content import StaticContent, XASSET_SRCREF_PREFIX from xmodule.contentstore.content import StaticContent, XASSET_SRCREF_PREFIX
from xmodule.model import Scope
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -123,7 +124,7 @@ def import_from_xml(store, data_dir, course_dirs=None, ...@@ -123,7 +124,7 @@ def import_from_xml(store, data_dir, course_dirs=None,
# Quick scan to get course Location as well as the course_data_path # Quick scan to get course Location as well as the course_data_path
for module in module_store.modules[course_id].itervalues(): for module in module_store.modules[course_id].itervalues():
if module.category == 'course': if module.category == 'course':
course_data_path = path(data_dir) / module.metadata['data_dir'] course_data_path = path(data_dir) / module.data_dir
course_location = module.location course_location = module.location
if static_content_store is not None: if static_content_store is not None:
...@@ -159,18 +160,17 @@ def import_from_xml(store, data_dir, course_dirs=None, ...@@ -159,18 +160,17 @@ def import_from_xml(store, data_dir, course_dirs=None,
module.definition['children'] = new_locs module.definition['children'] = new_locs
if module.category == 'course': if module.category == 'course':
# HACK: for now we don't support progress tabs. There's a special metadata configuration setting for this. # HACK: for now we don't support progress tabs. There's a special metadata configuration setting for this.
module.metadata['hide_progress_tab'] = True module.hide_progress_tab = True
# cdodge: more hacks (what else). Seems like we have a problem when importing a course (like 6.002) which # cdodge: more hacks (what else). Seems like we have a problem when importing a course (like 6.002) which
# does not have any tabs defined in the policy file. The import goes fine and then displays fine in LMS, # does not have any tabs defined in the policy file. The import goes fine and then displays fine in LMS,
# but if someone tries to add a new tab in the CMS, then the LMS barfs because it expects that - # but if someone tries to add a new tab in the CMS, then the LMS barfs because it expects that -
# if there is *any* tabs - then there at least needs to be some predefined ones # if there is *any* tabs - then there at least needs to be some predefined ones
if module.tabs is None or len(module.tabs) == 0: if module.tabs is None or len(module.tabs) == 0:
module.tabs = [{"type": "courseware"}, module.tabs = [{"type": "courseware"},
{"type": "course_info", "name": "Course Info"}, {"type": "course_info", "name": "Course Info"},
{"type": "discussion", "name": "Discussion"}, {"type": "discussion", "name": "Discussion"},
{"type": "wiki", "name": "Wiki"}] # note, add 'progress' when we can support it on Edge {"type": "wiki", "name": "Wiki"}] # note, add 'progress' when we can support it on Edge
...@@ -180,39 +180,43 @@ def import_from_xml(store, data_dir, course_dirs=None, ...@@ -180,39 +180,43 @@ def import_from_xml(store, data_dir, course_dirs=None,
course_items.append(module) course_items.append(module)
if 'data' in module.definition: if hasattr(module, 'data'):
module_data = module.definition['data']
# cdodge: now go through any link references to '/static/' and make sure we've imported # cdodge: now go through any link references to '/static/' and make sure we've imported
# it as a StaticContent asset # it as a StaticContent asset
try: try:
remap_dict = {} remap_dict = {}
# use the rewrite_links as a utility means to enumerate through all links # use the rewrite_links as a utility means to enumerate through all links
# in the module data. We use that to load that reference into our asset store # in the module data. We use that to load that reference into our asset store
# IMPORTANT: There appears to be a bug in lxml.rewrite_link which makes us not be able to # IMPORTANT: There appears to be a bug in lxml.rewrite_link which makes us not be able to
# do the rewrites natively in that code. # do the rewrites natively in that code.
# For example, what I'm seeing is <img src='foo.jpg' /> -> <img src='bar.jpg'> # For example, what I'm seeing is <img src='foo.jpg' /> -> <img src='bar.jpg'>
# Note the dropped element closing tag. This causes the LMS to fail when rendering modules - that's # Note the dropped element closing tag. This causes the LMS to fail when rendering modules - that's
# no good, so we have to do this kludge # no good, so we have to do this kludge
if isinstance(module_data, str) or isinstance(module_data, unicode): # some module 'data' fields are non strings which blows up the link traversal code if isinstance(module.data, str) or isinstance(module.data, unicode): # some module 'data' fields are non strings which blows up the link traversal code
lxml_rewrite_links(module_data, lambda link: verify_content_links(module, course_data_path, lxml_rewrite_links(module.data, lambda link: verify_content_links(module, course_data_path,
static_content_store, link, remap_dict)) static_content_store, link, remap_dict))
for key in remap_dict.keys(): for key in remap_dict.keys():
module_data = module_data.replace(key, remap_dict[key]) module.data = module.data.replace(key, remap_dict[key])
except Exception, e: except Exception:
logging.exception("failed to rewrite links on {0}. Continuing...".format(module.location)) logging.exception("failed to rewrite links on {0}. Continuing...".format(module.location))
store.update_item(module.location, module_data) store.update_item(module.location, module.data)
if 'children' in module.definition: if module.has_children:
store.update_children(module.location, module.definition['children']) store.update_children(module.location, module.children)
# NOTE: It's important to use own_metadata here to avoid writing metadata = {}
# inherited metadata everywhere. for field in module.fields + module.lms.fields:
store.update_metadata(module.location, dict(module.own_metadata)) # Only save metadata that wasn't inherited
if (field.scope == Scope.settings and
field.name in module._inherited_metadata and
field.name in module._model_data):
metadata[field.name] = module._model_data[field.name]
store.update_metadata(module.location, metadata)
return module_store, course_items return module_store, course_items
......
...@@ -33,19 +33,33 @@ class DbModel(MutableMapping): ...@@ -33,19 +33,33 @@ class DbModel(MutableMapping):
def __repr__(self): def __repr__(self):
return "<{0.__class__.__name__} {0._module_cls!r}>".format(self) return "<{0.__class__.__name__} {0._module_cls!r}>".format(self)
def __str__(self):
return str(dict(self.iteritems()))
def _getfield(self, name): def _getfield(self, name):
if (not hasattr(self._module_cls, name) or # First, get the field from the class, if defined
not isinstance(getattr(self._module_cls, name), ModelType)): module_field = getattr(self._module_cls, name, None)
if module_field is not None and isinstance(module_field, ModelType):
raise KeyError(name) return module_field
# If the class doesn't have the field, and it also
# doesn't have any namespaces, then the the name isn't a field
# so KeyError
if not hasattr(self._module_cls, 'namespaces'):
return KeyError(name)
# Resolve the field name in the first namespace where it's
# available
for namespace_name in self._module_cls.namespaces:
namespace = getattr(self._module_cls, namespace_name)
namespace_field = getattr(type(namespace), name, None)
if namespace_field is not None and isinstance(module_field, ModelType):
return namespace_field
return getattr(self._module_cls, name) # Not in the class or in any of the namespaces, so name
# really doesn't name a field
raise KeyError(name)
def _key(self, name): def _key(self, name):
field = self._getfield(name) field = self._getfield(name)
print name, field
module = field.scope.module module = field.scope.module
if module == ModuleScope.ALL: if module == ModuleScope.ALL:
...@@ -88,5 +102,5 @@ class DbModel(MutableMapping): ...@@ -88,5 +102,5 @@ class DbModel(MutableMapping):
def keys(self): def keys(self):
fields = [field.name for field in self._module_cls.fields] fields = [field.name for field in self._module_cls.fields]
for namespace_name in self._module_cls.namespaces: for namespace_name in self._module_cls.namespaces:
fields.extend(field.name for field in getattr(self._module_cls, namespace_name)) fields.extend(field.name for field in getattr(self._module_cls, namespace_name).fields)
return fields return fields
...@@ -25,6 +25,7 @@ from .stringify import stringify_children ...@@ -25,6 +25,7 @@ from .stringify import stringify_children
from .x_module import XModule from .x_module import XModule
from .xml_module import XmlDescriptor from .xml_module import XmlDescriptor
from xmodule.modulestore import Location from xmodule.modulestore import Location
from .model import List, String, Scope, Int
log = logging.getLogger("mitx.courseware") log = logging.getLogger("mitx.courseware")
...@@ -61,67 +62,21 @@ class SelfAssessmentModule(XModule): ...@@ -61,67 +62,21 @@ class SelfAssessmentModule(XModule):
js = {'coffee': [resource_string(__name__, 'js/src/selfassessment/display.coffee')]} js = {'coffee': [resource_string(__name__, 'js/src/selfassessment/display.coffee')]}
js_module_name = "SelfAssessment" js_module_name = "SelfAssessment"
def __init__(self, system, location, definition, descriptor, student_answers = List(scope=Scope.student_state, default=[])
instance_state=None, shared_state=None, **kwargs): scores = List(scope=Scope.student_state, default=[])
XModule.__init__(self, system, location, definition, descriptor, hints = List(scope=Scope.student_state, default=[])
instance_state, shared_state, **kwargs) state = String(scope=Scope.student_state, default=INITIAL)
""" # Used for progress / grading. Currently get credit just for
Definition file should have 4 blocks -- prompt, rubric, submitmessage, hintprompt, # completion (doesn't matter if you self-assessed correct/incorrect).
and two optional attributes: max_score = Int(scope=Scope.settings, default=MAX_SCORE)
attempts, which should be an integer that defaults to 1.
If it's > 1, the student will be able to re-submit after they see
the rubric.
max_score, which should be an integer that defaults to 1.
It defines the maximum number of points a student can get. Assumed to be integer scale
from 0 to max_score, with an interval of 1.
Note: all the submissions are stored.
Sample file:
<selfassessment attempts="1" max_score="1">
<prompt>
Insert prompt text here. (arbitrary html)
</prompt>
<rubric>
Insert grading rubric here. (arbitrary html)
</rubric>
<hintprompt>
Please enter a hint below: (arbitrary html)
</hintprompt>
<submitmessage>
Thanks for submitting! (arbitrary html)
</submitmessage>
</selfassessment>
"""
# Load instance state
if instance_state is not None:
instance_state = json.loads(instance_state)
else:
instance_state = {}
# Note: score responses are on scale from 0 to max_score
self.student_answers = instance_state.get('student_answers', [])
self.scores = instance_state.get('scores', [])
self.hints = instance_state.get('hints', [])
self.state = instance_state.get('state', 'initial')
# Used for progress / grading. Currently get credit just for
# completion (doesn't matter if you self-assessed correct/incorrect).
self._max_score = int(self.metadata.get('max_score', MAX_SCORE)) attempts = Int(scope=Scope.student_state, default=0), Int
max_attempts = Int(scope=Scope.settings, default=MAX_ATTEMPTS)
self.attempts = instance_state.get('attempts', 0) rubric = String(scope=Scope.content)
prompt = String(scope=Scope.content)
self.max_attempts = int(self.metadata.get('attempts', MAX_ATTEMPTS)) submit_message = String(scope=Scope.content)
hint_prompt = String(scope=Scope.content)
self.rubric = definition['rubric']
self.prompt = definition['prompt']
self.submit_message = definition['submitmessage']
self.hint_prompt = definition['hintprompt']
def _allow_reset(self): def _allow_reset(self):
"""Can the module be reset?""" """Can the module be reset?"""
...@@ -432,6 +387,21 @@ class SelfAssessmentDescriptor(XmlDescriptor, EditingDescriptor): ...@@ -432,6 +387,21 @@ class SelfAssessmentDescriptor(XmlDescriptor, EditingDescriptor):
js = {'coffee': [resource_string(__name__, 'js/src/html/edit.coffee')]} js = {'coffee': [resource_string(__name__, 'js/src/html/edit.coffee')]}
js_module_name = "HTMLEditingDescriptor" js_module_name = "HTMLEditingDescriptor"
# The capa format specifies that what we call max_attempts in the code
# is the attribute `attempts`. This will do that conversion
metadata_translations = dict(XmlDescriptor.metadata_translations)
metadata_translations['attempts'] = 'max_attempts'
# Used for progress / grading. Currently get credit just for
# completion (doesn't matter if you self-assessed correct/incorrect).
max_score = Int(scope=Scope.settings, default=MAX_SCORE)
max_attempts = Int(scope=Scope.settings, default=MAX_ATTEMPTS)
rubric = String(scope=Scope.content)
prompt = String(scope=Scope.content)
submit_message = String(scope=Scope.content)
hint_prompt = String(scope=Scope.content)
@classmethod @classmethod
def definition_from_xml(cls, xml_object, system): def definition_from_xml(cls, xml_object, system):
""" """
......
...@@ -30,7 +30,8 @@ i4xs = ModuleSystem( ...@@ -30,7 +30,8 @@ i4xs = ModuleSystem(
debug=True, debug=True,
xqueue={'interface':None, 'callback_url':'/', 'default_queuename': 'testqueue', 'waittime': 10}, xqueue={'interface':None, 'callback_url':'/', 'default_queuename': 'testqueue', 'waittime': 10},
node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"), node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"),
anonymous_student_id = 'student' anonymous_student_id = 'student',
xmodule_model_data = lambda x: x,
) )
......
...@@ -91,8 +91,10 @@ class ImportTestCase(unittest.TestCase): ...@@ -91,8 +91,10 @@ class ImportTestCase(unittest.TestCase):
self.assertEqual(re_import_descriptor.__class__.__name__, self.assertEqual(re_import_descriptor.__class__.__name__,
'ErrorDescriptor') 'ErrorDescriptor')
self.assertEqual(descriptor.definition['data'], self.assertEqual(descriptor.contents,
re_import_descriptor.definition['data']) re_import_descriptor.contents)
self.assertEqual(descriptor.error_msg,
re_import_descriptor.error_msg)
def test_fixed_xml_tag(self): def test_fixed_xml_tag(self):
"""Make sure a tag that's been fixed exports as the original tag type""" """Make sure a tag that's been fixed exports as the original tag type"""
...@@ -126,23 +128,19 @@ class ImportTestCase(unittest.TestCase): ...@@ -126,23 +128,19 @@ class ImportTestCase(unittest.TestCase):
url_name = 'test1' url_name = 'test1'
start_xml = ''' start_xml = '''
<course org="{org}" course="{course}" <course org="{org}" course="{course}"
graceperiod="{grace}" url_name="{url_name}" unicorn="purple"> due="{due}" url_name="{url_name}" unicorn="purple">
<chapter url="hi" url_name="ch" display_name="CH"> <chapter url="hi" url_name="ch" display_name="CH">
<html url_name="h" display_name="H">Two houses, ...</html> <html url_name="h" display_name="H">Two houses, ...</html>
</chapter> </chapter>
</course>'''.format(grace=v, org=ORG, course=COURSE, url_name=url_name) </course>'''.format(due=v, org=ORG, course=COURSE, url_name=url_name)
descriptor = system.process_xml(start_xml) descriptor = system.process_xml(start_xml)
print descriptor, descriptor.metadata print descriptor, descriptor._model_data
self.assertEqual(descriptor.metadata['graceperiod'], v) self.assertEqual(descriptor.lms.due, v)
self.assertEqual(descriptor.metadata['unicorn'], 'purple')
# Check that the child inherits graceperiod correctly # Check that the child inherits due correctly
child = descriptor.get_children()[0] child = descriptor.get_children()[0]
self.assertEqual(child.metadata['graceperiod'], v) self.assertEqual(child.lms.due, v)
# check that the child does _not_ inherit any unicorns
self.assertTrue('unicorn' not in child.metadata)
# Now export and check things # Now export and check things
resource_fs = MemoryFS() resource_fs = MemoryFS()
...@@ -169,12 +167,12 @@ class ImportTestCase(unittest.TestCase): ...@@ -169,12 +167,12 @@ class ImportTestCase(unittest.TestCase):
# did we successfully strip the url_name from the definition contents? # did we successfully strip the url_name from the definition contents?
self.assertTrue('url_name' not in course_xml.attrib) self.assertTrue('url_name' not in course_xml.attrib)
# Does the chapter tag now have a graceperiod attribute? # Does the chapter tag now have a due attribute?
# hardcoded path to child # hardcoded path to child
with resource_fs.open('chapter/ch.xml') as f: with resource_fs.open('chapter/ch.xml') as f:
chapter_xml = etree.fromstring(f.read()) chapter_xml = etree.fromstring(f.read())
self.assertEqual(chapter_xml.tag, 'chapter') self.assertEqual(chapter_xml.tag, 'chapter')
self.assertFalse('graceperiod' in chapter_xml.attrib) self.assertFalse('due' in chapter_xml.attrib)
def test_is_pointer_tag(self): def test_is_pointer_tag(self):
""" """
...@@ -216,7 +214,7 @@ class ImportTestCase(unittest.TestCase): ...@@ -216,7 +214,7 @@ class ImportTestCase(unittest.TestCase):
def check_for_key(key, node): def check_for_key(key, node):
"recursive check for presence of key" "recursive check for presence of key"
print "Checking {0}".format(node.location.url()) print "Checking {0}".format(node.location.url())
self.assertTrue(key in node.metadata) self.assertTrue(key in node._model_data)
for c in node.get_children(): for c in node.get_children():
check_for_key(key, c) check_for_key(key, c)
...@@ -244,15 +242,15 @@ class ImportTestCase(unittest.TestCase): ...@@ -244,15 +242,15 @@ class ImportTestCase(unittest.TestCase):
toy_ch = toy.get_children()[0] toy_ch = toy.get_children()[0]
two_toys_ch = two_toys.get_children()[0] two_toys_ch = two_toys.get_children()[0]
self.assertEqual(toy_ch.display_name, "Overview") self.assertEqual(toy_ch.lms.display_name, "Overview")
self.assertEqual(two_toys_ch.display_name, "Two Toy Overview") self.assertEqual(two_toys_ch.lms.display_name, "Two Toy Overview")
# Also check that the grading policy loaded # Also check that the grading policy loaded
self.assertEqual(two_toys.grade_cutoffs['C'], 0.5999) self.assertEqual(two_toys.grade_cutoffs['C'], 0.5999)
# Also check that keys from policy are run through the # Also check that keys from policy are run through the
# appropriate attribute maps -- 'graded' should be True, not 'true' # appropriate attribute maps -- 'graded' should be True, not 'true'
self.assertEqual(toy.metadata['graded'], True) self.assertEqual(toy.lms.graded, True)
def test_definition_loading(self): def test_definition_loading(self):
...@@ -271,8 +269,8 @@ class ImportTestCase(unittest.TestCase): ...@@ -271,8 +269,8 @@ class ImportTestCase(unittest.TestCase):
location = Location(["i4x", "edX", "toy", "video", "Welcome"]) location = Location(["i4x", "edX", "toy", "video", "Welcome"])
toy_video = modulestore.get_instance(toy_id, location) toy_video = modulestore.get_instance(toy_id, location)
two_toy_video = modulestore.get_instance(two_toy_id, location) two_toy_video = modulestore.get_instance(two_toy_id, location)
self.assertEqual(toy_video.metadata['youtube'], "1.0:p2Q6BrNhdh8") self.assertEqual(toy_video.youtube, "1.0:p2Q6BrNhdh8")
self.assertEqual(two_toy_video.metadata['youtube'], "1.0:p2Q6BrNhdh9") self.assertEqual(two_toy_video.youtube, "1.0:p2Q6BrNhdh9")
def test_colon_in_url_name(self): def test_colon_in_url_name(self):
...@@ -306,7 +304,7 @@ class ImportTestCase(unittest.TestCase): ...@@ -306,7 +304,7 @@ class ImportTestCase(unittest.TestCase):
cloc = course.location cloc = course.location
loc = Location(cloc.tag, cloc.org, cloc.course, 'html', 'secret:toylab') loc = Location(cloc.tag, cloc.org, cloc.course, 'html', 'secret:toylab')
html = modulestore.get_instance(course_id, loc) html = modulestore.get_instance(course_id, loc)
self.assertEquals(html.display_name, "Toy lab") self.assertEquals(html.lms.display_name, "Toy lab")
def test_url_name_mangling(self): def test_url_name_mangling(self):
""" """
...@@ -351,4 +349,4 @@ class ImportTestCase(unittest.TestCase): ...@@ -351,4 +349,4 @@ class ImportTestCase(unittest.TestCase):
location = Location(["i4x", "edX", "sa_test", "selfassessment", "SampleQuestion"]) location = Location(["i4x", "edX", "sa_test", "selfassessment", "SampleQuestion"])
sa_sample = modulestore.get_instance(sa_id, location) sa_sample = modulestore.get_instance(sa_id, location)
#10 attempts is hard coded into SampleQuestion, which is the url_name of a selfassessment xml tag #10 attempts is hard coded into SampleQuestion, which is the url_name of a selfassessment xml tag
self.assertEqual(sa_sample.metadata['attempts'], '10') self.assertEqual(sa_sample.max_attempts, '10')
class ModelMetaclassTester(object):
__metaclass__ = ModelMetaclass
field_a = Int(scope=Scope.settings)
field_b = Int(scope=Scope.content)
def test_model_metaclass():
import logging import logging
import yaml import yaml
import os import os
import time
from lxml import etree from lxml import etree
from pprint import pprint from pprint import pprint
...@@ -10,38 +9,14 @@ from pkg_resources import resource_listdir, resource_string, resource_isdir ...@@ -10,38 +9,14 @@ from pkg_resources import resource_listdir, resource_string, resource_isdir
from xmodule.modulestore import Location from xmodule.modulestore import Location
from .model import ModelMetaclass, ParentModelMetaclass, NamespacesMetaclass, ModelType from .model import ModelMetaclass, ParentModelMetaclass, NamespacesMetaclass
from .plugin import Plugin from .plugin import Plugin
class Date(ModelType):
time_format = "%Y-%m-%dT%H:%M"
def from_json(self, value):
"""
Parse an optional metadata key containing a time: if present, complain
if it doesn't parse.
Return None if not present or invalid.
"""
try:
return time.strptime(value, self.time_format)
except ValueError as e:
msg = "Field {0} has bad value '{1}': '{2}'".format(
self._name, value, e)
log.warning(msg)
return None
def to_json(self, value):
"""
Convert a time struct to a string
"""
return time.strftime(self.time_format, value)
class XModuleMetaclass(ParentModelMetaclass, NamespacesMetaclass, ModelMetaclass): class XModuleMetaclass(ParentModelMetaclass, NamespacesMetaclass, ModelMetaclass):
pass pass
log = logging.getLogger('mitx.' + __name__) log = logging.getLogger(__name__)
def dummy_track(event_type, event): def dummy_track(event_type, event):
......
...@@ -311,11 +311,13 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -311,11 +311,13 @@ class XmlDescriptor(XModuleDescriptor):
# Set/override any metadata specified by policy # Set/override any metadata specified by policy
k = policy_key(location) k = policy_key(location)
if k in system.policy: if k in system.policy:
if k == 'video/labintro': print k, metadata, system.policy[k]
cls.apply_policy(metadata, system.policy[k]) cls.apply_policy(metadata, system.policy[k])
model_data = {} model_data = {}
model_data.update(metadata) model_data.update(metadata)
model_data.update(definition) model_data.update(definition)
if k == 'video/labintro': print model_data
return cls( return cls(
system, system,
......
...@@ -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.display_name section_name = section_descriptor.lms.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,15 +276,13 @@ def progress_summary(student, request, course, student_module_cache): ...@@ -276,15 +276,13 @@ 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._model_data.get('hide_from_toc','false') if chapter_module.lms.hide_from_toc:
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._model_data.get('hide_from_toc','false') if section_module.lms.hide_from_toc:
if hidden.lower() == 'true':
continue continue
# Same for sections # Same for sections
...@@ -309,17 +307,17 @@ def progress_summary(student, request, course, student_module_cache): ...@@ -309,17 +307,17 @@ def progress_summary(student, request, course, student_module_cache):
format = section_module.lms.format format = section_module.lms.format
sections.append({ sections.append({
'display_name': section_module.display_name, 'display_name': section_module.lms.display_name,
'url_name': section_module.url_name, 'url_name': section_module.url_name,
'scores': scores, 'scores': scores,
'section_total': section_total, 'section_total': section_total,
'format': format, 'format': format,
'due': section_module.due, 'due': section_module.lms.due,
'graded': graded, 'graded': graded,
}) })
chapters.append({'course': course.display_name, chapters.append({'course': course.lms.display_name,
'display_name': chapter_module.display_name, 'display_name': chapter_module.lms.display_name,
'url_name': chapter_module.url_name, 'url_name': chapter_module.url_name,
'sections': sections}) 'sections': sections})
......
...@@ -92,6 +92,7 @@ def toc_for_course(user, request, course, active_chapter, active_section): ...@@ -92,6 +92,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():
print chapter, chapter._model_data, chapter._model_data.get('hide_from_toc'), chapter.lms.hide_from_toc
if chapter.lms.hide_from_toc: if chapter.lms.hide_from_toc:
continue continue
......
...@@ -76,7 +76,7 @@ def instructor_dashboard(request, course_id): ...@@ -76,7 +76,7 @@ def instructor_dashboard(request, course_id):
data = [['# Enrolled', CourseEnrollment.objects.filter(course_id=course_id).count()]] data = [['# Enrolled', CourseEnrollment.objects.filter(course_id=course_id).count()]]
data += compute_course_stats(course).items() data += compute_course_stats(course).items()
if request.user.is_staff: if request.user.is_staff:
data.append(['metadata', escape(str(course.metadata))]) data.append(['metadata', escape(str(course._model_data))])
datatable['data'] = data datatable['data'] = data
def return_csv(fn, datatable): def return_csv(fn, datatable):
......
...@@ -3,7 +3,7 @@ import logging ...@@ -3,7 +3,7 @@ import logging
import requests import requests
import settings import settings
log = logging.getLogger('mitx.' + __name__) log = logging.getLogger(__name__)
def strip_none(dic): def strip_none(dic):
return dict([(k, v) for k, v in dic.iteritems() if v is not None]) return dict([(k, v) for k, v in dic.iteritems() if v is not None])
......
...@@ -46,9 +46,22 @@ github = <a href="${edit_link}">${edit_link | h}</a> ...@@ -46,9 +46,22 @@ github = <a href="${edit_link}">${edit_link | h}</a>
%if source_file: %if source_file:
source_url = <a href="${source_url}">${source_file | h}</a> source_url = <a href="${source_url}">${source_file | h}</a>
%endif %endif
%for name, field in fields: <div>
${name} = <pre style="display:inline-block">${field | h}</pre> <header><h3>Module Fields</h3></header>
%endfor %for name, field in fields:
<div>
${name} = <pre style="display:inline-block">${field | h}</pre>
</div>
%endfor
</div>
<div>
<header><h3>edX Fields</h3></header>
%for name, field in lms_fields:
<div>
${name} = <pre style="display:inline-block">${field | h}</pre>
</div>
%endfor
</div>
category = ${category | h} category = ${category | h}
</div> </div>
%if render_histogram: %if render_histogram:
......
from xmodule.model import Namespace, Boolean, Scope, String, List from xmodule.model import Namespace, Boolean, Scope, String, List
from xmodule.x_module import Date from xmodule.fields import Date
class StringyBoolean(Boolean):
def from_json(self, value):
print "StringyBoolean ", value
if isinstance(value, basestring):
return value.lower() == 'true'
return value
class LmsNamespace(Namespace): class LmsNamespace(Namespace):
hide_from_toc = Boolean( hide_from_toc = StringyBoolean(
help="Whether to display this module in the table of contents", help="Whether to display this module in the table of contents",
default=False, default=False,
scope=Scope.settings scope=Scope.settings
...@@ -16,6 +25,7 @@ class LmsNamespace(Namespace): ...@@ -16,6 +25,7 @@ class LmsNamespace(Namespace):
help="What format this module is in (used for deciding which " help="What format this module is in (used for deciding which "
"grader to apply, and what to show in the TOC)", "grader to apply, and what to show in the TOC)",
scope=Scope.settings, scope=Scope.settings,
default='',
) )
display_name = String( display_name = String(
......
# 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 -e .
from setuptools import setup, find_packages from setuptools import setup, find_packages
setup( setup(
name="edX LMS", name="edX Apps",
version="0.1", version="0.1",
install_requires=['distribute'], install_requires=['distribute'],
requires=[ requires=[
'xmodule', 'xmodule',
], ],
py_modules=['lms.xmodule_namespace'],
# See http://guide.python-distribute.org/creation.html#entry-points # See http://guide.python-distribute.org/creation.html#entry-points
# for a description of entry_points # for a description of entry_points
entry_points={ entry_points={
......
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