Commit 8ba41635 by Calen Pennington

WIP. Data loads, but not all of it

parent 3d6cbf47
......@@ -499,7 +499,7 @@ def load_preview_module(request, preview_id, descriptor, instance_state, shared_
"""
system = preview_module_system(request, preview_id, descriptor)
try:
module = descriptor.xmodule_constructor(system)(instance_state, shared_state)
module = descriptor.xmodule(system)
except:
module = ErrorDescriptor.from_descriptor(
descriptor,
......
......@@ -83,7 +83,7 @@ class LoncapaProblem(object):
Main class for capa Problems.
'''
def __init__(self, problem_text, id, state=None, seed=None, system=None):
def __init__(self, problem_text, id, correct_map=None, done=None, seed=None, system=None):
'''
Initializes capa Problem.
......@@ -91,7 +91,8 @@ class LoncapaProblem(object):
- problem_text (string): xml defining the problem
- id (string): identifier for this problem; often a filename (no spaces)
- state (dict): student state
- correct_map (dict): data specifying whether the student has completed the problem
- done (bool): Whether the student has answered the problem
- seed (int): random number generator seed (int)
- system (ModuleSystem): ModuleSystem instance which provides OS,
rendering, and user context
......@@ -103,16 +104,11 @@ class LoncapaProblem(object):
self.problem_id = id
self.system = system
self.seed = seed
self.done = done
self.correct_map = CorrectMap()
if state:
if 'seed' in state:
self.seed = state['seed']
if 'student_answers' in state:
self.student_answers = state['student_answers']
if 'correct_map' in state:
self.correct_map.set_dict(state['correct_map'])
if 'done' in state:
self.done = state['done']
if correct_map is not None:
self.correct_map.set_dict(correct_map)
# TODO: Does this deplete the Linux entropy pool? Is this fast enough?
if not self.seed:
......
......@@ -7,6 +7,7 @@ from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
from xmodule.xml_module import XmlDescriptor
from xmodule.exceptions import InvalidDefinitionError
from .model import String, Scope
DEFAULT = "_DEFAULT_GROUP"
......@@ -68,37 +69,7 @@ class ABTestDescriptor(RawDescriptor, XmlDescriptor):
template_dir_name = "abtest"
def __init__(self, system, definition=None, **kwargs):
"""
definition is a dictionary with the following layout:
{'data': {
'experiment': 'the name of the experiment',
'group_portions': {
'group_a': 0.1,
'group_b': 0.2
},
'group_contents': {
'group_a': [
'url://for/content/module/1',
'url://for/content/module/2',
],
'group_b': [
'url://for/content/module/3',
],
DEFAULT: [
'url://for/default/content/1'
]
}
},
'children': [
'url://for/content/module/1',
'url://for/content/module/2',
'url://for/content/module/3',
'url://for/default/content/1',
]}
"""
kwargs['shared_state_key'] = definition['data']['experiment']
RawDescriptor.__init__(self, system, definition, **kwargs)
experiment = String(help="Experiment that this A/B test belongs to", scope=Scope.content)
@classmethod
def definition_from_xml(cls, xml_object, system):
......
......@@ -19,6 +19,9 @@ from progress import Progress
from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
from xmodule.exceptions import NotFoundError
from .model import Int, Scope, ModuleScope, ModelType, String, Boolean, Object, Float
Date = Timedelta = ModelType
log = logging.getLogger("mitx.courseware")
......@@ -77,6 +80,17 @@ class CapaModule(XModule):
'''
icon_class = 'problem'
attempts = Int(help="Number of attempts taken by the student on this problem", default=0, scope=Scope.student_state)
max_attempts = Int(help="Maximum number of attempts that a student is allowed", scope=Scope.settings)
due = Date(help="Date that this problem is due by", scope=Scope.settings)
graceperiod = Timedelta(help="Amount of time after the due date that submissions will be accepted", scope=Scope.settings)
show_answer = String(help="When to show the problem answer to the student", scope=Scope.settings, default="closed")
force_save_button = Boolean(help="Whether to force the save button to appear on the page", scope=Scope.settings)
rerandomize = String(help="When to rerandomize the problem", default="always")
data = String(help="XML data for the problem", scope=Scope.content)
correct_map = Object(help="Dictionary with the correctness of current student answers", scope=Scope.student_state)
done = Boolean(help="Whether the student has answered the problem", scope=Scope.student_state)
js = {'coffee': [resource_string(__name__, 'js/src/capa/display.coffee'),
resource_string(__name__, 'js/src/collapsible.coffee'),
resource_string(__name__, 'js/src/javascript_loader.coffee'),
......@@ -87,51 +101,15 @@ class CapaModule(XModule):
js_module_name = "Problem"
css = {'scss': [resource_string(__name__, 'css/capa/display.scss')]}
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.attempts = 0
self.max_attempts = None
dom2 = etree.fromstring(definition['data'])
display_due_date_string = self.metadata.get('due', None)
if display_due_date_string is not None:
self.display_due_date = dateutil.parser.parse(display_due_date_string)
#log.debug("Parsed " + display_due_date_string +
# " to " + str(self.display_due_date))
else:
self.display_due_date = None
def __init__(self, system, location, descriptor, model_data):
XModule.__init__(self, system, location, descriptor, model_data)
grace_period_string = self.metadata.get('graceperiod', None)
if grace_period_string is not None and self.display_due_date:
self.grace_period = parse_timedelta(grace_period_string)
self.close_date = self.display_due_date + self.grace_period
if self.graceperiod is not None and self.due:
self.close_date = self.due + self.graceperiod
#log.debug("Then parsed " + grace_period_string +
# " to closing date" + str(self.close_date))
else:
self.grace_period = None
self.close_date = self.display_due_date
self.max_attempts = self.metadata.get('attempts', None)
if self.max_attempts is not None:
self.max_attempts = int(self.max_attempts)
self.show_answer = self.metadata.get('showanswer', 'closed')
self.force_save_button = self.metadata.get('force_save_button', 'false')
if self.show_answer == "":
self.show_answer = "closed"
if instance_state is not None:
instance_state = json.loads(instance_state)
if instance_state is not None and 'attempts' in instance_state:
self.attempts = instance_state['attempts']
self.name = only_one(dom2.xpath('/problem/@name'))
self.close_date = self.due
if self.rerandomize == 'never':
self.seed = 1
......@@ -148,8 +126,8 @@ class CapaModule(XModule):
try:
# TODO (vshnayder): move as much as possible of this work and error
# checking to descriptor load time
self.lcp = LoncapaProblem(self.definition['data'], self.location.html_id(),
instance_state, seed=self.seed, system=self.system)
self.lcp = LoncapaProblem(self.data, self.location.html_id(),
self.correct_map, self.done, self.seed, self.system)
except Exception as err:
msg = 'cannot create LoncapaProblem {loc}: {err}'.format(
loc=self.location.url(), err=err)
......@@ -168,33 +146,21 @@ class CapaModule(XModule):
(self.location.url(), msg))
self.lcp = LoncapaProblem(
problem_text, self.location.html_id(),
instance_state, seed=self.seed, system=self.system)
self.correct_map, self.done, self.seed, self.system)
else:
# add extra info and raise
raise Exception(msg), None, sys.exc_info()[2]
@property
def rerandomize(self):
"""
Property accessor that returns self.metadata['rerandomize'] in a
canonical form
"""
rerandomize = self.metadata.get('rerandomize', 'always')
if rerandomize in ("", "always", "true"):
return "always"
elif rerandomize in ("false", "per_student"):
return "per_student"
elif rerandomize == "never":
return "never"
elif rerandomize == "onreset":
return "onreset"
else:
raise Exception("Invalid rerandomize attribute " + rerandomize)
if self.rerandomize in ("", "true"):
self.rerandomize = "always"
elif self.rerandomize == "false":
self.rerandomize = "per_student"
def get_instance_state(self):
state = self.lcp.get_state()
state['attempts'] = self.attempts
return json.dumps(state)
def sync_lcp_state(self):
lcp_state = self.lcp.get_state()
self.done = lcp_state['done']
self.correct_map = lcp_state['correct_map']
self.seed = lcp_state['seed']
def get_score(self):
return self.lcp.get_score()
......@@ -211,7 +177,7 @@ class CapaModule(XModule):
if total > 0:
try:
return Progress(score, total)
except Exception as err:
except Exception:
log.exception("Got bad progress")
return None
return None
......@@ -261,8 +227,8 @@ class CapaModule(XModule):
# Next, generate a fresh LoncapaProblem
self.lcp = LoncapaProblem(self.definition['data'], self.location.html_id(),
state=None, # Tabula rasa
seed=self.seed, system=self.system)
self.sync_lcp_state()
# Prepend a scary warning to the student
warning = '<div class="capa_reset">'\
......@@ -280,8 +246,8 @@ class CapaModule(XModule):
html = warning
try:
html += self.lcp.get_html()
except Exception, err: # Couldn't do it. Give up
log.exception(err)
except Exception: # Couldn't do it. Give up
log.exception("Unable to generate html from LoncapaProblem")
raise
content = {'name': self.display_name,
......@@ -311,7 +277,7 @@ class CapaModule(XModule):
# User submitted a problem, and hasn't reset. We don't want
# more submissions.
if self.lcp.done and self.rerandomize == "always":
if self.done and self.rerandomize == "always":
check_button = False
save_button = False
......@@ -320,7 +286,7 @@ class CapaModule(XModule):
reset_button = False
# User hasn't submitted an answer yet -- we don't want resets
if not self.lcp.done:
if not self.done:
reset_button = False
# We may not need a "save" button if infinite number of attempts and
......@@ -406,7 +372,7 @@ class CapaModule(XModule):
return self.attempts > 0
if self.show_answer == 'answered':
return self.lcp.done
return self.done
if self.show_answer == 'closed':
return self.closed()
......@@ -429,6 +395,7 @@ class CapaModule(XModule):
queuekey = get['queuekey']
score_msg = get['xqueue_body']
self.lcp.update_score(score_msg, queuekey)
self.sync_lcp_state()
return dict() # No AJAX return is needed
......@@ -445,6 +412,7 @@ class CapaModule(XModule):
raise NotFoundError('Answer is not available')
else:
answers = self.lcp.get_question_answers()
self.sync_lcp_state()
# answers (eg <solution>) may have embedded images
# but be careful, some problems are using non-string answer dicts
......@@ -512,7 +480,7 @@ class CapaModule(XModule):
raise NotFoundError('Problem is closed')
# Problem submitted. Student should reset before checking again
if self.lcp.done and self.rerandomize == "always":
if self.done and self.rerandomize == "always":
event_info['failure'] = 'unreset'
self.system.track_function('save_problem_check_fail', event_info)
raise NotFoundError('Problem must be reset before it can be checked again')
......@@ -522,14 +490,13 @@ class CapaModule(XModule):
current_time = datetime.datetime.now()
prev_submit_time = self.lcp.get_recentmost_queuetime()
waittime_between_requests = self.system.xqueue['waittime']
if (current_time-prev_submit_time).total_seconds() < waittime_between_requests:
if (current_time - prev_submit_time).total_seconds() < waittime_between_requests:
msg = 'You must wait at least %d seconds between submissions' % waittime_between_requests
return {'success': msg, 'html': ''} # Prompts a modal dialog in ajax callback
try:
old_state = self.lcp.get_state()
lcp_id = self.lcp.problem_id
correct_map = self.lcp.grade_answers(answers)
self.sync_lcp_state()
except StudentInputError as inst:
log.exception("StudentInputError in capa_module:problem_check")
return {'success': inst.message}
......@@ -557,7 +524,7 @@ class CapaModule(XModule):
event_info['attempts'] = self.attempts
self.system.track_function('save_problem_check', event_info)
if hasattr(self.system,'psychometrics_handler'): # update PsychometricsData using callback
if hasattr(self.system, 'psychometrics_handler'): # update PsychometricsData using callback
self.system.psychometrics_handler(self.get_instance_state())
# render problem into HTML
......@@ -589,7 +556,7 @@ class CapaModule(XModule):
# Problem submitted. Student should reset before saving
# again.
if self.lcp.done and self.rerandomize == "always":
if self.done and self.rerandomize == "always":
event_info['failure'] = 'done'
self.system.track_function('save_problem_fail', event_info)
return {'success': False,
......@@ -617,7 +584,7 @@ class CapaModule(XModule):
return {'success': False,
'error': "Problem is closed"}
if not self.lcp.done:
if not self.done:
event_info['failure'] = 'not_done'
self.system.track_function('reset_problem_fail', event_info)
return {'success': False,
......@@ -629,9 +596,13 @@ class CapaModule(XModule):
# in next line)
self.lcp.seed = None
self.lcp = LoncapaProblem(self.definition['data'],
self.location.html_id(), self.lcp.get_state(),
system=self.system)
self.lcp = LoncapaProblem(self.data,
self.location.html_id(),
self.lcp.correct_map,
self.lcp.done,
self.lcp.seed,
self.system)
self.sync_lcp_state()
event_info['new_state'] = self.lcp.get_state()
self.system.track_function('reset_problem', event_info)
......@@ -647,6 +618,8 @@ class CapaDescriptor(RawDescriptor):
module_class = CapaModule
weight = Float(help="How much to weight this problem by", scope=Scope.settings)
stores_state = True
has_score = True
template_dir_name = 'problem'
......@@ -665,12 +638,3 @@ class CapaDescriptor(RawDescriptor):
'problems/' + path[8:],
path[8:],
]
def __init__(self, *args, **kwargs):
super(CapaDescriptor, self).__init__(*args, **kwargs)
weight_string = self.metadata.get('weight', None)
if weight_string:
self.weight = float(weight_string)
else:
self.weight = None
......@@ -12,6 +12,9 @@ import requests
import time
import copy
from .model import Scope, ModelType, List, String, Object, Boolean
Date = ModelType
log = logging.getLogger(__name__)
......@@ -21,6 +24,39 @@ edx_xml_parser = etree.XMLParser(dtd_validation=False, load_dtd=False,
class CourseDescriptor(SequenceDescriptor):
module_class = SequenceModule
textbooks = List(help="List of pairs of (title, url) for textbooks used in this course", default=[], scope=Scope.content)
wiki_slug = String(help="Slug that points to the wiki for this course", scope=Scope.content)
enrollment_start = Date(help="Date that enrollment for this class is opened", scope=Scope.settings)
enrollment_end = Date(help="Date that enrollment for this class is closed", 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)
grading_policy = Object(help="Grading policy definition for this class", scope=Scope.content)
info_sidebar_name = String(scope=Scope.settings, default='Course Handouts')
# An extra property is used rather than the wiki_slug/number because
# there are courses that change the number for different runs. This allows
# courses to share the same css_class across runs even if they have
# different numbers.
#
# TODO get rid of this as soon as possible or potentially build in a robust
# way to add in course-specific styling. There needs to be a discussion
# about the right way to do this, but arjun will address this ASAP. Also
# note that the courseware template needs to change when this is removed.
css_class = String(help="DO NOT USE THIS", scope=Scope.settings)
# TODO: This is a quick kludge to allow CS50 (and other courses) to
# specify their own discussion forums as external links by specifying a
# "discussion_link" in their policy JSON file. This should later get
# folded in with Syllabus, Course Info, and additional Custom tabs in a
# more sensible framework later.
discussion_link = String(help="DO NOT USE THIS", scope=Scope.settings)
# TODO: same as above, intended to let internal CS50 hide the progress tab
# until we get grade integration set up.
# Explicit comparison to True because we always want to return a bool.
hide_progress_tab = Boolean(help="DO NOT USE THIS", scope=Scope.settings)
template_dir_name = 'course'
class Textbook:
......@@ -69,10 +105,11 @@ class CourseDescriptor(SequenceDescriptor):
return table_of_contents
def __init__(self, system, definition=None, **kwargs):
super(CourseDescriptor, self).__init__(system, definition, **kwargs)
def __init__(self, *args, **kwargs):
super(CourseDescriptor, self).__init__(*args, **kwargs)
self.textbooks = []
for title, book_url in self.definition['data']['textbooks']:
for title, book_url in self.textbooks:
try:
self.textbooks.append(self.Textbook(title, book_url))
except:
......@@ -81,7 +118,8 @@ class CourseDescriptor(SequenceDescriptor):
log.exception("Couldn't load textbook ({0}, {1})".format(title, book_url))
continue
self.wiki_slug = self.definition['data']['wiki_slug'] or self.location.course
if self.wiki_slug is None:
self.wiki_slug = self.location.course
msg = None
if self.start is None:
......@@ -98,7 +136,7 @@ class CourseDescriptor(SequenceDescriptor):
# disable the syllabus content for courses that do not provide a syllabus
self.syllabus_present = self.system.resources_fs.exists(path('syllabus'))
self.set_grading_policy(self.definition['data'].get('grading_policy', None))
self.set_grading_policy(self.grading_policy)
def defaut_grading_policy(self):
"""
......@@ -203,7 +241,7 @@ class CourseDescriptor(SequenceDescriptor):
# cdodge: import the grading policy information that is on disk and put into the
# descriptor 'definition' bucket as a dictionary so that it is persisted in the DB
instance.definition['data']['grading_policy'] = policy
instance.grading_policy = policy
# now set the current instance. set_grading_policy() will apply some inheritance rules
instance.set_grading_policy(policy)
......@@ -395,38 +433,14 @@ class CourseDescriptor(SequenceDescriptor):
@property
def start_date_text(self):
displayed_start = self._try_parse_time('advertised_start') or self.start
return time.strftime("%b %d, %Y", displayed_start)
return time.strftime("%b %d, %Y", self.advertised_start or self.start)
@property
def end_date_text(self):
return time.strftime("%b %d, %Y", self.end)
# An extra property is used rather than the wiki_slug/number because
# there are courses that change the number for different runs. This allows
# courses to share the same css_class across runs even if they have
# different numbers.
#
# TODO get rid of this as soon as possible or potentially build in a robust
# way to add in course-specific styling. There needs to be a discussion
# about the right way to do this, but arjun will address this ASAP. Also
# note that the courseware template needs to change when this is removed.
@property
def css_class(self):
return self.metadata.get('css_class', '')
@property
def info_sidebar_name(self):
return self.metadata.get('info_sidebar_name', 'Course Handouts')
@property
def discussion_link(self):
"""TODO: This is a quick kludge to allow CS50 (and other courses) to
specify their own discussion forums as external links by specifying a
"discussion_link" in their policy JSON file. This should later get
folded in with Syllabus, Course Info, and additional Custom tabs in a
more sensible framework later."""
return self.metadata.get('discussion_link', None)
@property
def forum_posts_allowed(self):
......@@ -443,12 +457,6 @@ class CourseDescriptor(SequenceDescriptor):
return True
@property
def hide_progress_tab(self):
"""TODO: same as above, intended to let internal CS50 hide the progress tab
until we get grade integration set up."""
# Explicit comparison to True because we always want to return a bool.
return self.metadata.get('hide_progress_tab') == True
@property
def end_of_course_survey_url(self):
......
......@@ -74,12 +74,11 @@ class ErrorDescriptor(JSONEditingDescriptor):
}
# real metadata stays in the content, but add a display name
metadata = {'display_name': 'Error: ' + location.name}
model_data = {'display_name': 'Error: ' + location.name}
return ErrorDescriptor(
system,
definition,
location=location,
metadata=metadata
location,
model_data,
)
def get_context(self):
......
......@@ -21,20 +21,19 @@ class MakoModuleDescriptor(XModuleDescriptor):
the descriptor as the `module` parameter to that template
"""
def __init__(self, system, definition=None, **kwargs):
def __init__(self, system, location, model_data):
if getattr(system, 'render_template', None) is None:
raise TypeError('{system} must have a render_template function'
' in order to use a MakoDescriptor'.format(
system=system))
super(MakoModuleDescriptor, self).__init__(system, definition, **kwargs)
super(MakoModuleDescriptor, self).__init__(system, location, model_data)
def get_context(self):
"""
Return the context to render the mako template with
"""
return {'module': self,
'metadata': self.metadata,
'editable_metadata_fields' : self.editable_metadata_fields
'editable_metadata_fields': self.editable_fields
}
def get_html(self):
......@@ -44,6 +43,6 @@ class MakoModuleDescriptor(XModuleDescriptor):
# cdodge: encapsulate a means to expose "editable" metadata fields (i.e. not internal system metadata)
@property
def editable_metadata_fields(self):
subset = [name for name in self.metadata.keys() if name not in self.system_metadata_fields]
subset = [field.name for field in self.fields if field.name not in self.system_metadata_fields]
return subset
from collections import namedtuple
class ModuleScope(object):
USAGE, DEFINITION, TYPE, ALL = xrange(4)
class Scope(namedtuple('ScopeBase', 'student module')):
pass
Scope.content = Scope(student=False, module=ModuleScope.DEFINITION)
Scope.student_state = Scope(student=True, module=ModuleScope.USAGE)
Scope.settings = Scope(student=True, module=ModuleScope.USAGE)
Scope.student_preferences = Scope(student=True, module=ModuleScope.TYPE)
Scope.student_info = Scope(student=True, module=ModuleScope.ALL)
class ModelType(object):
sequence = 0
def __init__(self, help=None, default=None, scope=Scope.content):
self._seq = self.sequence
self._name = "unknown"
self.help = help
self.default = default
self.scope = scope
ModelType.sequence += 1
@property
def name(self):
return self._name
def __get__(self, instance, owner):
if instance is None:
return self
return instance._model_data.get(self.name, self.default)
def __set__(self, instance, value):
instance._model_data[self.name] = value
def __delete__(self, instance):
del instance._model_data[self.name]
def __repr__(self):
return "<{0.__class__.__name} {0.__name__}>".format(self)
def __lt__(self, other):
return self._seq < other._seq
Int = Float = Boolean = Object = List = String = Any = ModelType
class ModelMetaclass(type):
def __new__(cls, name, bases, attrs):
# Find registered methods
reg_methods = {}
for value in attrs.itervalues():
for reg_type, names in getattr(value, "_method_registrations", {}).iteritems():
for n in names:
reg_methods[reg_type + n] = value
attrs['registered_methods'] = reg_methods
if attrs.get('has_children', False):
attrs['children'] = ModelType(help='The children of this XModule', default=[], scope=None)
@property
def child_map(self):
return dict((child.name, child) for child in self.children)
attrs['child_map'] = child_map
fields = []
for n, v in attrs.items():
if isinstance(v, ModelType):
v._name = n
fields.append(v)
fields.sort()
attrs['fields'] = fields
return super(ModelMetaclass, cls).__new__(cls, name, bases, attrs)
......@@ -187,11 +187,12 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
err_msg
)
descriptor.metadata['data_dir'] = course_dir
setattr(descriptor, 'data_dir', course_dir)
xmlstore.modules[course_id][descriptor.location] = descriptor
for child in descriptor.get_children():
if hasattr(descriptor, 'children'):
for child in descriptor.children:
parent_tracker.add_parent(child.location, descriptor.location)
return descriptor
......@@ -425,7 +426,7 @@ class XMLModuleStore(ModuleStoreBase):
# breaks metadata inheritance via get_children(). Instead
# (actually, in addition to, for now), we do a final inheritance pass
# after we have the course descriptor.
XModuleDescriptor.compute_inherited_metadata(course_descriptor)
#XModuleDescriptor.compute_inherited_metadata(course_descriptor)
# now import all pieces of course_info which is expected to be stored
# in <content_dir>/info or <content_dir>/info/<url_name>
......@@ -449,22 +450,22 @@ class XMLModuleStore(ModuleStoreBase):
if not os.path.exists(path):
path = base_dir
for filepath in glob.glob(path/ '*'):
for filepath in glob.glob(path / '*'):
with open(filepath) as f:
try:
html = f.read().decode('utf-8')
# tabs are referenced in policy.json through a 'slug' which is just the filename without the .html suffix
slug = os.path.splitext(os.path.basename(filepath))[0]
loc = Location('i4x', course_descriptor.location.org, course_descriptor.location.course, category, slug)
module = HtmlDescriptor(system, definition={'data' : html}, **{'location' : loc})
module = HtmlDescriptor(system, loc, {'data': html})
# VS[compat]:
# Hack because we need to pull in the 'display_name' for static tabs (because we need to edit them)
# from the course policy
if category == "static_tab":
for tab in course_descriptor.tabs or []:
if tab.get('url_slug') == slug:
module.metadata['display_name'] = tab['name']
module.metadata['data_dir'] = course_dir
module.display_name = tab['name']
module.data_dir = course_dir
self.modules[course_descriptor.id][module.location] = module
except Exception, e:
logging.exception("Failed to load {0}. Skipping... Exception: {1}".format(filepath, str(e)))
......
......@@ -67,10 +67,6 @@ class CustomTagDescriptor(RawDescriptor):
return template.render(**params)
def __init__(self, system, definition, **kwargs):
'''Render and save the template for this descriptor instance'''
super(CustomTagDescriptor, self).__init__(system, definition, **kwargs)
@property
def rendered_html(self):
return self.render_template(self.system, self.definition['data'])
......
......@@ -2,19 +2,41 @@ import logging
import pkg_resources
import yaml
import os
import time
from functools import partial
from lxml import etree
from pprint import pprint
from collections import namedtuple
from pkg_resources import resource_listdir, resource_string, resource_isdir
from xmodule.modulestore import Location
from xmodule.timeparse import parse_time, stringify_time
from .model import ModelMetaclass, String, Scope, ModuleScope, ModelType
from xmodule.contentstore.content import StaticContent, XASSET_SRCREF_PREFIX
from xmodule.modulestore.exceptions import ItemNotFoundError
import time
Date = ModelType
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)
log = logging.getLogger('mitx.' + __name__)
......@@ -157,6 +179,10 @@ class XModule(HTMLSnippet):
See the HTML module for a simple example.
'''
__metaclass__ = ModelMetaclass
display_name = String(help="Display name for this module", scope=Scope(student=False, module=ModuleScope.USAGE))
# The default implementation of get_icon_class returns the icon_class
# attribute of the class
#
......@@ -165,8 +191,7 @@ class XModule(HTMLSnippet):
# in the module
icon_class = 'other'
def __init__(self, system, location, definition, descriptor,
instance_state=None, shared_state=None, **kwargs):
def __init__(self, system, location, descriptor, model_data):
'''
Construct a new xmodule
......@@ -214,63 +239,25 @@ class XModule(HTMLSnippet):
'''
self.system = system
self.location = Location(location)
self.definition = definition
self.descriptor = descriptor
self.instance_state = instance_state
self.shared_state = shared_state
self.id = self.location.url()
self.url_name = self.location.name
self.category = self.location.category
self.metadata = kwargs.get('metadata', {})
self._loaded_children = None
self._model_data = model_data
@property
def display_name(self):
'''
Return a display name for the module: use display_name if defined in
metadata, otherwise convert the url name.
'''
return self.metadata.get('display_name',
self.url_name.replace('_', ' '))
if self.display_name is None:
self.display_name = self.url_name.replace('_', ' ')
def __unicode__(self):
return '<x_module(id={0})>'.format(self.id)
def get_children(self):
'''
Return module instances for all the children of this module.
'''
if self._loaded_children is None:
child_locations = self.get_children_locations()
children = [self.system.get_module(loc) for loc in child_locations]
# 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 get_children_locations(self):
'''
Returns the locations of each of child modules.
Overriding this changes the behavior of get_children and
anything that uses get_children, such as get_display_items.
This method will not instantiate the modules of the children
unless absolutely necessary, so it is cheaper to call than get_children
These children will be the same children returned by the
descriptor unless descriptor.has_dynamic_children() is true.
'''
return self.definition.get('children', [])
def get_display_items(self):
'''
Returns a list of descendent module instances that will display
immediately inside this module.
'''
items = []
for child in self.get_children():
for child in self.children():
items.extend(child.displayable_items())
return items
......@@ -290,18 +277,6 @@ class XModule(HTMLSnippet):
### Functions used in the LMS
def get_instance_state(self):
''' State of the object, as stored in the database
'''
return '{}'
def get_shared_state(self):
'''
Get state that should be shared with other instances
using the same 'shared_state_key' attribute.
'''
return '{}'
def get_score(self):
''' Score the student received on the problem.
'''
......@@ -391,7 +366,10 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
"""
entry_point = "xmodule.v1"
module_class = XModule
__metaclass__ = ModelMetaclass
display_name = String(help="Display name for this module", scope=Scope(student=False, module=ModuleScope.USAGE))
start = Date(help="Start time when this module is visible", scope=Scope(student=False, module=ModuleScope.USAGE))
# Attributes for inspection of the descriptor
stores_state = False # Indicates whether the xmodule state should be
# stored in a database (independent of shared state)
......@@ -424,8 +402,8 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
# ============================= STRUCTURAL MANIPULATION ===================
def __init__(self,
system,
definition=None,
**kwargs):
location,
model_data):
"""
Construct a new XModuleDescriptor. The only required arguments are the
system, used for interaction with external resources, and the
......@@ -467,116 +445,36 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
instance of the module data
"""
self.system = system
self.metadata = kwargs.get('metadata', {})
self.definition = definition if definition is not None else {}
self.location = Location(kwargs.get('location'))
self.location = Location(location)
self.url_name = self.location.name
self.category = self.location.category
self.shared_state_key = kwargs.get('shared_state_key')
self._model_data = model_data
self._child_instances = None
self._inherited_metadata = set()
@property
def display_name(self):
'''
Return a display name for the module: use display_name if defined in
metadata, otherwise convert the url name.
'''
return self.metadata.get('display_name',
self.url_name.replace('_', ' '))
@property
def start(self):
"""
If self.metadata contains start, return it. Else return None.
"""
if 'start' not in self.metadata:
return None
return self._try_parse_time('start')
@start.setter
def start(self, value):
if isinstance(value, time.struct_time):
self.metadata['start'] = stringify_time(value)
@property
def own_metadata(self):
"""
Return the metadata that is not inherited, but was defined on this module.
"""
return dict((k, v) for k, v in self.metadata.items()
if k not in self._inherited_metadata)
@staticmethod
def compute_inherited_metadata(node):
"""Given a descriptor, traverse all of its descendants and do metadata
inheritance. Should be called on a CourseDescriptor after importing a
course.
NOTE: This means that there is no such thing as lazy loading at the
moment--this accesses all the children."""
for c in node.get_children():
c.inherit_metadata(node.metadata)
XModuleDescriptor.compute_inherited_metadata(c)
def inherit_metadata(self, metadata):
"""
Updates this module with metadata inherited from a containing module.
Only metadata specified in self.inheritable_metadata will
be inherited
"""
# Set all inheritable metadata from kwargs that are
# in self.inheritable_metadata and aren't already set in metadata
for attr in self.inheritable_metadata:
if attr not in self.metadata and attr in metadata:
self._inherited_metadata.add(attr)
self.metadata[attr] = metadata[attr]
def get_children(self):
"""Returns a list of XModuleDescriptor instances for the children of
this module"""
if self._child_instances is None:
self._child_instances = []
for child_loc in self.definition.get('children', []):
try:
child = self.system.load_item(child_loc)
except ItemNotFoundError:
log.exception('Unable to load item {loc}, skipping'.format(loc=child_loc))
continue
# TODO (vshnayder): this should go away once we have
# proper inheritance support in mongo. The xml
# datastore does all inheritance on course load.
child.inherit_metadata(self.metadata)
self._child_instances.append(child)
return self._child_instances
def get_child_by_url_name(self, url_name):
"""
Return a child XModuleDescriptor with the specified url_name, if it exists, and None otherwise.
"""
for c in self.get_children():
for c in self.children:
if c.url_name == url_name:
return c
return None
def xmodule_constructor(self, system):
def xmodule(self, system):
"""
Returns a constructor for an XModule. This constructor takes two
arguments: instance_state and shared_state, and returns a fully
instantiated XModule
"""
return partial(
self.module_class,
return self.module_class(
system,
self.location,
self.definition,
self,
metadata=self.metadata
system.xmodule_model_data(self.model_data),
)
def has_dynamic_children(self):
"""
Returns True if this descriptor has dynamic children for a given
......@@ -701,31 +599,14 @@ class XModuleDescriptor(Plugin, HTMLSnippet, ResourceTemplates):
return eq
def __repr__(self):
return ("{class_}({system!r}, {definition!r}, location={location!r},"
" metadata={metadata!r})".format(
return ("{class_}({system!r}, location={location!r},"
" model_data={model_data!r})".format(
class_=self.__class__.__name__,
system=self.system,
definition=self.definition,
location=self.location,
metadata=self.metadata
model_data=self._model_data,
))
# ================================ Internal helpers =======================
def _try_parse_time(self, key):
"""
Parse an optional metadata key containing a time: if present, complain
if it doesn't parse.
Return None if not present or invalid.
"""
if key in self.metadata:
try:
return parse_time(self.metadata[key])
except ValueError as e:
msg = "Descriptor {0} loaded with a bad metadata key '{1}': '{2}'".format(
self.location.url(), self.metadata[key], e)
log.warning(msg)
return None
class DescriptorSystem(object):
......@@ -867,6 +748,9 @@ class ModuleSystem(object):
'''provide uniform access to attributes (like etree)'''
self.__dict__[attr] = val
def xmodule_module_data(self, module_data):
return module_data
def __repr__(self):
return repr(self.__dict__)
......
......@@ -299,13 +299,13 @@ class XmlDescriptor(XModuleDescriptor):
metadata = cls.load_metadata(definition_xml)
# move definition metadata into dict
dmdata = definition.get('definition_metadata','')
dmdata = definition.get('definition_metadata', '')
if dmdata:
metadata['definition_metadata_raw'] = dmdata
try:
metadata.update(json.loads(dmdata))
except Exception as err:
log.debug('Error %s in loading metadata %s' % (err,dmdata))
log.debug('Error %s in loading metadata %s' % (err, dmdata))
metadata['definition_metadata_err'] = str(err)
# Set/override any metadata specified by policy
......@@ -313,11 +313,14 @@ class XmlDescriptor(XModuleDescriptor):
if k in system.policy:
cls.apply_policy(metadata, system.policy[k])
model_data = {}
model_data.update(metadata)
model_data.update(definition)
return cls(
system,
definition,
location=location,
metadata=metadata,
location,
model_data,
)
@classmethod
......
function github_status {
gcli status create mitx mitx $GIT_COMMIT \
--params=$1 \
target_url:$BUILD_URL \
description:"Build #$BUILD_NUMBER is running" \
-f csv
}
function github_mark_failed_on_exit {
trap '[ $? == "0" ] || github_status state:failed' EXIT
}
\ No newline at end of file
......@@ -85,7 +85,7 @@ def course_image_url(course):
"""Try to look up the image url for the course. If it's not found,
log an error and return the dead link"""
if isinstance(modulestore(), XMLModuleStore):
path = course.metadata['data_dir'] + "/images/course_image.jpg"
path = course.data_dir + "/images/course_image.jpg"
return try_staticfiles_lookup(path)
else:
loc = course.location._replace(tag='c4x', category='asset', name='images_course_image.jpg')
......@@ -162,7 +162,9 @@ def get_course_about_section(course, section_key):
key=section_key, url=course.location.url()))
return None
elif section_key == "title":
return course.metadata.get('display_name', course.url_name)
if course.display_name is None:
return course.url_name
return course.display_name
elif section_key == "university":
return course.location.org
elif section_key == "number":
......@@ -220,7 +222,7 @@ def get_course_syllabus_section(course, section_key):
filepath = find_file(fs, dirs, section_key + ".html")
with fs.open(filepath) as htmlFile:
return replace_urls(htmlFile.read().decode('utf-8'),
course.metadata['data_dir'], course_namespace=course.location)
course.data_dir, course_namespace=course.location)
except ResourceNotFoundError:
log.exception("Missing syllabus section {key} in course {url}".format(
key=section_key, url=course.location.url()))
......
......@@ -245,7 +245,7 @@ def _get_module(user, request, location, student_module_cache, course_id, positi
make_psychometrics_data_update_handler(instance_module))
try:
module = descriptor.xmodule_constructor(system)(instance_state, shared_state)
module = descriptor.xmodule(system)
except:
log.exception("Error creating module from descriptor {0}".format(descriptor))
......@@ -259,7 +259,7 @@ def _get_module(user, request, location, student_module_cache, course_id, positi
error_msg=exc_info_to_str(sys.exc_info()))
# Make an error module
return err_descriptor.xmodule_constructor(system)(None, None)
return err_descriptor.xmodule(system)
_get_html = module.get_html
......
......@@ -40,7 +40,7 @@ end
def django_admin(system, env, command, *args)
django_admin = ENV['DJANGO_ADMIN_PATH'] || select_executable('django-admin.py', 'django-admin')
return "#{django_admin} #{command} --settings=#{system}.envs.#{env} --pythonpath=. #{args.join(' ')}"
return "#{django_admin} #{command} --traceback --settings=#{system}.envs.#{env} --pythonpath=. #{args.join(' ')}"
end
def django_for_jasmine(system, django_reload)
......
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