WIP: Trying to get tests working
This commit is contained in:
@@ -14,7 +14,7 @@
|
||||
|
||||
import logging
|
||||
|
||||
log = logging.getLogger("mitx." + __name__)
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
from django.template import Context
|
||||
from django.http import HttpResponse
|
||||
|
||||
@@ -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>"
|
||||
|
||||
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,
|
||||
'xqa_key': module.lms.xqa_key,
|
||||
'source_file' : source_file,
|
||||
|
||||
@@ -72,7 +72,7 @@ global_context = {'random': random,
|
||||
# These should be removed from HTML output, including all subelements
|
||||
html_problem_semantics = ["codeparam", "responseparam", "answer", "script", "hintgroup"]
|
||||
|
||||
log = logging.getLogger('mitx.' + __name__)
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# main class for this module
|
||||
|
||||
@@ -17,7 +17,7 @@ from lxml import etree
|
||||
import xml.sax.saxutils as saxutils
|
||||
from registry import TagRegistry
|
||||
|
||||
log = logging.getLogger('mitx.' + __name__)
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
registry = TagRegistry()
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ import sys
|
||||
|
||||
from registry import TagRegistry
|
||||
|
||||
log = logging.getLogger('mitx.' + __name__)
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
#########################################################################
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ from lxml import etree
|
||||
from lxml.html.soupparser import fromstring as fromstring_bs # uses Beautiful Soup!!! FIXME?
|
||||
import xqueue_interface
|
||||
|
||||
log = logging.getLogger('mitx.' + __name__)
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
@@ -7,7 +7,7 @@ import logging
|
||||
import requests
|
||||
|
||||
|
||||
log = logging.getLogger('mitx.' + __name__)
|
||||
log = logging.getLogger(__name__)
|
||||
dateformat = '%Y%m%d%H%M%S'
|
||||
|
||||
def make_hashkey(seed):
|
||||
|
||||
@@ -551,8 +551,7 @@ class CapaModule(XModule):
|
||||
msg = "Error checking problem: " + str(err)
|
||||
msg += '\nTraceback:\n' + traceback.format_exc()
|
||||
return {'success': msg}
|
||||
log.exception("Error in capa_module problem checking")
|
||||
raise Exception("error in capa_module")
|
||||
raise
|
||||
|
||||
self.attempts = self.attempts + 1
|
||||
self.lcp.done = True
|
||||
|
||||
@@ -131,9 +131,9 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
if self.start is None:
|
||||
msg = "Course loaded without a valid start date. id = %s" % self.id
|
||||
# hack it -- start in 1970
|
||||
self.lms.start = time.gmtime(0)
|
||||
self.start = time.gmtime(0)
|
||||
log.critical(msg)
|
||||
system.error_tracker(msg)
|
||||
self.system.error_tracker(msg)
|
||||
|
||||
# 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)
|
||||
|
||||
@@ -8,6 +8,7 @@ from xmodule.x_module import XModule
|
||||
from xmodule.editing_module import JSONEditingDescriptor
|
||||
from xmodule.errortracker import exc_info_to_str
|
||||
from xmodule.modulestore import Location
|
||||
from .model import String, Scope
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -52,6 +53,10 @@ class ErrorDescriptor(JSONEditingDescriptor):
|
||||
"""
|
||||
module_class = ErrorModule
|
||||
|
||||
contents = String(scope=Scope.content)
|
||||
error_msg = String(scope=Scope.content)
|
||||
display_name = String(scope=Scope.settings)
|
||||
|
||||
@classmethod
|
||||
def _construct(self, system, contents, error_msg, location):
|
||||
|
||||
@@ -66,15 +71,12 @@ class ErrorDescriptor(JSONEditingDescriptor):
|
||||
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
|
||||
model_data = {'display_name': 'Error: ' + location.name}
|
||||
model_data = {
|
||||
'error_msg': str(error_msg),
|
||||
'contents': contents,
|
||||
'display_name': 'Error: ' + location.name
|
||||
}
|
||||
return ErrorDescriptor(
|
||||
system,
|
||||
location,
|
||||
@@ -84,7 +86,7 @@ class ErrorDescriptor(JSONEditingDescriptor):
|
||||
def get_context(self):
|
||||
return {
|
||||
'module': self,
|
||||
'data': self.definition['data']['contents'],
|
||||
'data': self.contents,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
@@ -100,10 +102,7 @@ class ErrorDescriptor(JSONEditingDescriptor):
|
||||
def from_descriptor(cls, descriptor, error_msg='Error not available'):
|
||||
return cls._construct(
|
||||
descriptor.system,
|
||||
json.dumps({
|
||||
'definition': descriptor.definition,
|
||||
'metadata': descriptor.metadata,
|
||||
}, indent=4),
|
||||
json.dumps(descriptor._model_data, indent=4),
|
||||
error_msg,
|
||||
location=descriptor.location,
|
||||
)
|
||||
@@ -147,14 +146,14 @@ class ErrorDescriptor(JSONEditingDescriptor):
|
||||
files, etc. That would just get re-wrapped on import.
|
||||
'''
|
||||
try:
|
||||
xml = etree.fromstring(self.definition['data']['contents'])
|
||||
xml = etree.fromstring(self.contents)
|
||||
return etree.tostring(xml)
|
||||
except etree.XMLSyntaxError:
|
||||
# still not valid.
|
||||
root = etree.Element('error')
|
||||
root.text = self.definition['data']['contents']
|
||||
root.text = self.contents
|
||||
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)
|
||||
|
||||
|
||||
|
||||
30
common/lib/xmodule/xmodule/fields.py
Normal file
30
common/lib/xmodule/xmodule/fields.py
Normal file
@@ -0,0 +1,30 @@
|
||||
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):
|
||||
if instance is None:
|
||||
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:
|
||||
return self.computed_default(instance)
|
||||
|
||||
return self.default
|
||||
|
||||
return self.from_json(instance._model_data[self.name])
|
||||
|
||||
def __set__(self, instance, value):
|
||||
instance._model_data[self.name] = self.to_json(value)
|
||||
|
||||
@@ -166,18 +166,18 @@ class Namespace(Plugin):
|
||||
super(Namespace, self).__setattr__(name, value)
|
||||
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 container_class_attr.__set__(container)
|
||||
return namespace_attr.__set__(container, value)
|
||||
|
||||
def __delattr__(self, name):
|
||||
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 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,
|
||||
|
||||
etree.set_default_parser(edx_xml_parser)
|
||||
|
||||
log = logging.getLogger('mitx.' + __name__)
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# VS[compat]
|
||||
@@ -160,7 +160,6 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
|
||||
etree.tostring(xml_data), self, self.org,
|
||||
self.course, xmlstore.default_class)
|
||||
except Exception as err:
|
||||
print err, self.load_error_modules
|
||||
if not self.load_error_modules:
|
||||
raise
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ from .xml import XMLModuleStore
|
||||
from .exceptions import DuplicateItemError
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.contentstore.content import StaticContent, XASSET_SRCREF_PREFIX
|
||||
from xmodule.model import Scope
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -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
|
||||
for module in module_store.modules[course_id].itervalues():
|
||||
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
|
||||
|
||||
if static_content_store is not None:
|
||||
@@ -159,18 +160,17 @@ def import_from_xml(store, data_dir, course_dirs=None,
|
||||
|
||||
module.definition['children'] = new_locs
|
||||
|
||||
|
||||
if module.category == 'course':
|
||||
# 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
|
||||
# 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 -
|
||||
# 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,
|
||||
# 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 module.tabs is None or len(module.tabs) == 0:
|
||||
module.tabs = [{"type": "courseware"},
|
||||
{"type": "course_info", "name": "Course Info"},
|
||||
module.tabs = [{"type": "courseware"},
|
||||
{"type": "course_info", "name": "Course Info"},
|
||||
{"type": "discussion", "name": "Discussion"},
|
||||
{"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,
|
||||
|
||||
course_items.append(module)
|
||||
|
||||
if 'data' in module.definition:
|
||||
module_data = module.definition['data']
|
||||
|
||||
if hasattr(module, 'data'):
|
||||
# cdodge: now go through any link references to '/static/' and make sure we've imported
|
||||
# it as a StaticContent asset
|
||||
try:
|
||||
try:
|
||||
remap_dict = {}
|
||||
|
||||
# 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
|
||||
# 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.
|
||||
# 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
|
||||
# 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
|
||||
lxml_rewrite_links(module_data, lambda link: verify_content_links(module, course_data_path,
|
||||
static_content_store, link, remap_dict))
|
||||
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,
|
||||
static_content_store, link, remap_dict))
|
||||
|
||||
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))
|
||||
|
||||
store.update_item(module.location, module_data)
|
||||
store.update_item(module.location, module.data)
|
||||
|
||||
if 'children' in module.definition:
|
||||
store.update_children(module.location, module.definition['children'])
|
||||
if module.has_children:
|
||||
store.update_children(module.location, module.children)
|
||||
|
||||
# NOTE: It's important to use own_metadata here to avoid writing
|
||||
# inherited metadata everywhere.
|
||||
store.update_metadata(module.location, dict(module.own_metadata))
|
||||
metadata = {}
|
||||
for field in module.fields + module.lms.fields:
|
||||
# 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
|
||||
|
||||
|
||||
@@ -33,19 +33,33 @@ class DbModel(MutableMapping):
|
||||
def __repr__(self):
|
||||
return "<{0.__class__.__name__} {0._module_cls!r}>".format(self)
|
||||
|
||||
def __str__(self):
|
||||
return str(dict(self.iteritems()))
|
||||
|
||||
def _getfield(self, name):
|
||||
if (not hasattr(self._module_cls, name) or
|
||||
not isinstance(getattr(self._module_cls, name), ModelType)):
|
||||
# First, get the field from the class, if defined
|
||||
module_field = getattr(self._module_cls, name, None)
|
||||
if module_field is not None and isinstance(module_field, ModelType):
|
||||
return module_field
|
||||
|
||||
raise KeyError(name)
|
||||
# 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)
|
||||
|
||||
return getattr(self._module_cls, 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
|
||||
|
||||
# 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):
|
||||
field = self._getfield(name)
|
||||
print name, field
|
||||
module = field.scope.module
|
||||
|
||||
if module == ModuleScope.ALL:
|
||||
@@ -88,5 +102,5 @@ class DbModel(MutableMapping):
|
||||
def keys(self):
|
||||
fields = [field.name for field in self._module_cls.fields]
|
||||
for namespace_name in self._module_cls.namespaces:
|
||||
fields.extend(field.name for field in getattr(self._module_cls, namespace_name))
|
||||
fields.extend(field.name for field in getattr(self._module_cls, namespace_name).fields)
|
||||
return fields
|
||||
|
||||
@@ -25,6 +25,7 @@ from .stringify import stringify_children
|
||||
from .x_module import XModule
|
||||
from .xml_module import XmlDescriptor
|
||||
from xmodule.modulestore import Location
|
||||
from .model import List, String, Scope, Int
|
||||
|
||||
log = logging.getLogger("mitx.courseware")
|
||||
|
||||
@@ -61,67 +62,21 @@ class SelfAssessmentModule(XModule):
|
||||
js = {'coffee': [resource_string(__name__, 'js/src/selfassessment/display.coffee')]}
|
||||
js_module_name = "SelfAssessment"
|
||||
|
||||
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)
|
||||
student_answers = List(scope=Scope.student_state, default=[])
|
||||
scores = List(scope=Scope.student_state, default=[])
|
||||
hints = List(scope=Scope.student_state, default=[])
|
||||
state = String(scope=Scope.student_state, default=INITIAL)
|
||||
|
||||
"""
|
||||
Definition file should have 4 blocks -- prompt, rubric, submitmessage, hintprompt,
|
||||
and two optional attributes:
|
||||
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.
|
||||
# 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)
|
||||
|
||||
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))
|
||||
|
||||
self.attempts = instance_state.get('attempts', 0)
|
||||
|
||||
self.max_attempts = int(self.metadata.get('attempts', MAX_ATTEMPTS))
|
||||
|
||||
self.rubric = definition['rubric']
|
||||
self.prompt = definition['prompt']
|
||||
self.submit_message = definition['submitmessage']
|
||||
self.hint_prompt = definition['hintprompt']
|
||||
attempts = Int(scope=Scope.student_state, default=0), Int
|
||||
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)
|
||||
|
||||
def _allow_reset(self):
|
||||
"""Can the module be reset?"""
|
||||
@@ -432,6 +387,21 @@ class SelfAssessmentDescriptor(XmlDescriptor, EditingDescriptor):
|
||||
js = {'coffee': [resource_string(__name__, 'js/src/html/edit.coffee')]}
|
||||
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
|
||||
def definition_from_xml(cls, xml_object, system):
|
||||
"""
|
||||
|
||||
@@ -30,7 +30,8 @@ i4xs = ModuleSystem(
|
||||
debug=True,
|
||||
xqueue={'interface':None, 'callback_url':'/', 'default_queuename': 'testqueue', 'waittime': 10},
|
||||
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):
|
||||
self.assertEqual(re_import_descriptor.__class__.__name__,
|
||||
'ErrorDescriptor')
|
||||
|
||||
self.assertEqual(descriptor.definition['data'],
|
||||
re_import_descriptor.definition['data'])
|
||||
self.assertEqual(descriptor.contents,
|
||||
re_import_descriptor.contents)
|
||||
self.assertEqual(descriptor.error_msg,
|
||||
re_import_descriptor.error_msg)
|
||||
|
||||
def test_fixed_xml_tag(self):
|
||||
"""Make sure a tag that's been fixed exports as the original tag type"""
|
||||
@@ -126,23 +128,19 @@ class ImportTestCase(unittest.TestCase):
|
||||
url_name = 'test1'
|
||||
start_xml = '''
|
||||
<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">
|
||||
<html url_name="h" display_name="H">Two houses, ...</html>
|
||||
</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)
|
||||
|
||||
print descriptor, descriptor.metadata
|
||||
self.assertEqual(descriptor.metadata['graceperiod'], v)
|
||||
self.assertEqual(descriptor.metadata['unicorn'], 'purple')
|
||||
print descriptor, descriptor._model_data
|
||||
self.assertEqual(descriptor.lms.due, v)
|
||||
|
||||
# Check that the child inherits graceperiod correctly
|
||||
# Check that the child inherits due correctly
|
||||
child = descriptor.get_children()[0]
|
||||
self.assertEqual(child.metadata['graceperiod'], v)
|
||||
|
||||
# check that the child does _not_ inherit any unicorns
|
||||
self.assertTrue('unicorn' not in child.metadata)
|
||||
self.assertEqual(child.lms.due, v)
|
||||
|
||||
# Now export and check things
|
||||
resource_fs = MemoryFS()
|
||||
@@ -169,12 +167,12 @@ class ImportTestCase(unittest.TestCase):
|
||||
# did we successfully strip the url_name from the definition contents?
|
||||
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
|
||||
with resource_fs.open('chapter/ch.xml') as f:
|
||||
chapter_xml = etree.fromstring(f.read())
|
||||
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):
|
||||
"""
|
||||
@@ -216,7 +214,7 @@ class ImportTestCase(unittest.TestCase):
|
||||
def check_for_key(key, node):
|
||||
"recursive check for presence of key"
|
||||
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():
|
||||
check_for_key(key, c)
|
||||
|
||||
@@ -244,15 +242,15 @@ class ImportTestCase(unittest.TestCase):
|
||||
toy_ch = toy.get_children()[0]
|
||||
two_toys_ch = two_toys.get_children()[0]
|
||||
|
||||
self.assertEqual(toy_ch.display_name, "Overview")
|
||||
self.assertEqual(two_toys_ch.display_name, "Two Toy Overview")
|
||||
self.assertEqual(toy_ch.lms.display_name, "Overview")
|
||||
self.assertEqual(two_toys_ch.lms.display_name, "Two Toy Overview")
|
||||
|
||||
# Also check that the grading policy loaded
|
||||
self.assertEqual(two_toys.grade_cutoffs['C'], 0.5999)
|
||||
|
||||
# Also check that keys from policy are run through the
|
||||
# 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):
|
||||
@@ -271,8 +269,8 @@ class ImportTestCase(unittest.TestCase):
|
||||
location = Location(["i4x", "edX", "toy", "video", "Welcome"])
|
||||
toy_video = modulestore.get_instance(toy_id, location)
|
||||
two_toy_video = modulestore.get_instance(two_toy_id, location)
|
||||
self.assertEqual(toy_video.metadata['youtube'], "1.0:p2Q6BrNhdh8")
|
||||
self.assertEqual(two_toy_video.metadata['youtube'], "1.0:p2Q6BrNhdh9")
|
||||
self.assertEqual(toy_video.youtube, "1.0:p2Q6BrNhdh8")
|
||||
self.assertEqual(two_toy_video.youtube, "1.0:p2Q6BrNhdh9")
|
||||
|
||||
|
||||
def test_colon_in_url_name(self):
|
||||
@@ -306,7 +304,7 @@ class ImportTestCase(unittest.TestCase):
|
||||
cloc = course.location
|
||||
loc = Location(cloc.tag, cloc.org, cloc.course, 'html', 'secret:toylab')
|
||||
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):
|
||||
"""
|
||||
@@ -351,4 +349,4 @@ class ImportTestCase(unittest.TestCase):
|
||||
location = Location(["i4x", "edX", "sa_test", "selfassessment", "SampleQuestion"])
|
||||
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
|
||||
self.assertEqual(sa_sample.metadata['attempts'], '10')
|
||||
self.assertEqual(sa_sample.max_attempts, '10')
|
||||
|
||||
8
common/lib/xmodule/xmodule/tests/test_model.py
Normal file
8
common/lib/xmodule/xmodule/tests/test_model.py
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
class ModelMetaclassTester(object):
|
||||
__metaclass__ = ModelMetaclass
|
||||
|
||||
field_a = Int(scope=Scope.settings)
|
||||
field_b = Int(scope=Scope.content)
|
||||
|
||||
def test_model_metaclass():
|
||||
0
common/lib/xmodule/xmodule/tests/test_runtime.py
Normal file
0
common/lib/xmodule/xmodule/tests/test_runtime.py
Normal file
@@ -1,7 +1,6 @@
|
||||
import logging
|
||||
import yaml
|
||||
import os
|
||||
import time
|
||||
|
||||
from lxml import etree
|
||||
from pprint import pprint
|
||||
@@ -10,38 +9,14 @@ from pkg_resources import resource_listdir, resource_string, resource_isdir
|
||||
|
||||
from xmodule.modulestore import Location
|
||||
|
||||
from .model import ModelMetaclass, ParentModelMetaclass, NamespacesMetaclass, ModelType
|
||||
from .model import ModelMetaclass, ParentModelMetaclass, NamespacesMetaclass
|
||||
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):
|
||||
pass
|
||||
|
||||
log = logging.getLogger('mitx.' + __name__)
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def dummy_track(event_type, event):
|
||||
|
||||
@@ -311,11 +311,13 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
# Set/override any metadata specified by policy
|
||||
k = policy_key(location)
|
||||
if k in system.policy:
|
||||
if k == 'video/labintro': print k, metadata, system.policy[k]
|
||||
cls.apply_policy(metadata, system.policy[k])
|
||||
|
||||
model_data = {}
|
||||
model_data.update(metadata)
|
||||
model_data.update(definition)
|
||||
if k == 'video/labintro': print model_data
|
||||
|
||||
return cls(
|
||||
system,
|
||||
|
||||
@@ -148,7 +148,7 @@ def grade(student, request, course, student_module_cache=None, keep_raw_scores=F
|
||||
format_scores = []
|
||||
for section in sections:
|
||||
section_descriptor = section['section_descriptor']
|
||||
section_name = section_descriptor.display_name
|
||||
section_name = section_descriptor.lms.display_name
|
||||
|
||||
should_grade_section = False
|
||||
# If we haven't seen a single problem in the section, we don't have to grade it at all! We can assume 0%
|
||||
@@ -276,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)
|
||||
for chapter_module in course_module.get_display_items():
|
||||
# Skip if the chapter is hidden
|
||||
hidden = chapter_module._model_data.get('hide_from_toc','false')
|
||||
if hidden.lower() == 'true':
|
||||
if chapter_module.lms.hide_from_toc:
|
||||
continue
|
||||
|
||||
sections = []
|
||||
for section_module in chapter_module.get_display_items():
|
||||
# Skip if the section is hidden
|
||||
hidden = section_module._model_data.get('hide_from_toc','false')
|
||||
if hidden.lower() == 'true':
|
||||
if section_module.lms.hide_from_toc:
|
||||
continue
|
||||
|
||||
# Same for sections
|
||||
@@ -309,17 +307,17 @@ def progress_summary(student, request, course, student_module_cache):
|
||||
|
||||
format = section_module.lms.format
|
||||
sections.append({
|
||||
'display_name': section_module.display_name,
|
||||
'display_name': section_module.lms.display_name,
|
||||
'url_name': section_module.url_name,
|
||||
'scores': scores,
|
||||
'section_total': section_total,
|
||||
'format': format,
|
||||
'due': section_module.due,
|
||||
'due': section_module.lms.due,
|
||||
'graded': graded,
|
||||
})
|
||||
|
||||
chapters.append({'course': course.display_name,
|
||||
'display_name': chapter_module.display_name,
|
||||
chapters.append({'course': course.lms.display_name,
|
||||
'display_name': chapter_module.lms.display_name,
|
||||
'url_name': chapter_module.url_name,
|
||||
'sections': sections})
|
||||
|
||||
|
||||
@@ -92,6 +92,7 @@ def toc_for_course(user, request, course, active_chapter, active_section):
|
||||
|
||||
chapters = list()
|
||||
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:
|
||||
continue
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ def instructor_dashboard(request, course_id):
|
||||
data = [['# Enrolled', CourseEnrollment.objects.filter(course_id=course_id).count()]]
|
||||
data += compute_course_stats(course).items()
|
||||
if request.user.is_staff:
|
||||
data.append(['metadata', escape(str(course.metadata))])
|
||||
data.append(['metadata', escape(str(course._model_data))])
|
||||
datatable['data'] = data
|
||||
|
||||
def return_csv(fn, datatable):
|
||||
|
||||
@@ -3,7 +3,7 @@ import logging
|
||||
import requests
|
||||
import settings
|
||||
|
||||
log = logging.getLogger('mitx.' + __name__)
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
def strip_none(dic):
|
||||
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>
|
||||
%if source_file:
|
||||
source_url = <a href="${source_url}">${source_file | h}</a>
|
||||
%endif
|
||||
%for name, field in fields:
|
||||
${name} = <pre style="display:inline-block">${field | h}</pre>
|
||||
%endfor
|
||||
<div>
|
||||
<header><h3>Module Fields</h3></header>
|
||||
%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}
|
||||
</div>
|
||||
%if render_histogram:
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
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):
|
||||
hide_from_toc = Boolean(
|
||||
hide_from_toc = StringyBoolean(
|
||||
help="Whether to display this module in the table of contents",
|
||||
default=False,
|
||||
scope=Scope.settings
|
||||
@@ -16,6 +25,7 @@ class LmsNamespace(Namespace):
|
||||
help="What format this module is in (used for deciding which "
|
||||
"grader to apply, and what to show in the TOC)",
|
||||
scope=Scope.settings,
|
||||
default='',
|
||||
)
|
||||
|
||||
display_name = String(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Python libraries to install that are local to the mitx repo
|
||||
-e common/lib/capa
|
||||
-e common/lib/xmodule
|
||||
-e lms
|
||||
-e .
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="edX LMS",
|
||||
name="edX Apps",
|
||||
version="0.1",
|
||||
install_requires=['distribute'],
|
||||
requires=[
|
||||
'xmodule',
|
||||
],
|
||||
|
||||
py_modules=['lms.xmodule_namespace'],
|
||||
# See http://guide.python-distribute.org/creation.html#entry-points
|
||||
# for a description of entry_points
|
||||
entry_points={
|
||||
Reference in New Issue
Block a user