WIP. Data loads, but not all of it
This commit is contained in:
@@ -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)
|
||||
def __init__(self, system, location, descriptor, model_data):
|
||||
XModule.__init__(self, system, location, descriptor, model_data)
|
||||
|
||||
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
|
||||
|
||||
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,8 +412,9 @@ 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
|
||||
# answers (eg <solution>) may have embedded images
|
||||
# but be careful, some problems are using non-string answer dicts
|
||||
new_answers = dict()
|
||||
for answer_id in answers:
|
||||
@@ -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
|
||||
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}
|
||||
@@ -554,11 +521,11 @@ class CapaModule(XModule):
|
||||
# 'success' will always be incorrect
|
||||
event_info['correct_map'] = correct_map.get_dict()
|
||||
event_info['success'] = success
|
||||
event_info['attempts'] = self.attempts
|
||||
event_info['attempts'] = self.attempts
|
||||
self.system.track_function('save_problem_check', event_info)
|
||||
|
||||
if hasattr(self.system,'psychometrics_handler'): # update PsychometricsData using callback
|
||||
self.system.psychometrics_handler(self.get_instance_state())
|
||||
if hasattr(self.system, 'psychometrics_handler'): # update PsychometricsData using callback
|
||||
self.system.psychometrics_handler(self.get_instance_state())
|
||||
|
||||
# render problem into HTML
|
||||
html = self.get_problem_html(encapsulate=False)
|
||||
@@ -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
|
||||
|
||||
|
||||
79
common/lib/xmodule/xmodule/model.py
Normal file
79
common/lib/xmodule/xmodule/model.py
Normal file
@@ -0,0 +1,79 @@
|
||||
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,12 +187,13 @@ 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():
|
||||
parent_tracker.add_parent(child.location, descriptor.location)
|
||||
if hasattr(descriptor, 'children'):
|
||||
for child in descriptor.children:
|
||||
parent_tracker.add_parent(child.location, descriptor.location)
|
||||
return descriptor
|
||||
|
||||
render_template = lambda: ''
|
||||
@@ -425,14 +426,14 @@ 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>
|
||||
self.load_extra_content(system, course_descriptor, 'course_info', self.data_dir / course_dir / 'info', course_dir, url_name)
|
||||
|
||||
# now import all static tabs which are expected to be stored in
|
||||
# in <content_dir>/tabs or <content_dir>/tabs/<url_name>
|
||||
# in <content_dir>/tabs or <content_dir>/tabs/<url_name>
|
||||
self.load_extra_content(system, course_descriptor, 'static_tab', self.data_dir / course_dir / 'tabs', course_dir, url_name)
|
||||
|
||||
self.load_extra_content(system, course_descriptor, 'custom_tag_template', self.data_dir / course_dir / 'custom_tags', course_dir, url_name)
|
||||
@@ -444,30 +445,30 @@ class XMLModuleStore(ModuleStoreBase):
|
||||
|
||||
def load_extra_content(self, system, course_descriptor, category, base_dir, course_dir, url_name):
|
||||
if url_name:
|
||||
path = base_dir / url_name
|
||||
path = base_dir / url_name
|
||||
|
||||
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
|
||||
self.modules[course_descriptor.id][module.location] = module
|
||||
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)))
|
||||
logging.exception("Failed to load {0}. Skipping... Exception: {1}".format(filepath, str(e)))
|
||||
system.error_tracker("ERROR: " + str(e))
|
||||
|
||||
def get_instance(self, course_id, location, depth=0):
|
||||
|
||||
@@ -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__)
|
||||
|
||||
|
||||
@@ -287,9 +287,9 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
filepath = cls._format_filepath(xml_object.tag, name_to_pathname(url_name))
|
||||
definition_xml = cls.load_file(filepath, system.resources_fs, location)
|
||||
else:
|
||||
definition_xml = xml_object # this is just a pointer, not the real definition content
|
||||
definition_xml = xml_object # this is just a pointer, not the real definition content
|
||||
|
||||
definition = cls.load_definition(definition_xml, system, location) # note this removes metadata
|
||||
definition = cls.load_definition(definition_xml, system, location) # note this removes metadata
|
||||
# VS[compat] -- make Ike's github preview links work in both old and
|
||||
# new file layouts
|
||||
if is_pointer_tag(xml_object):
|
||||
@@ -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
|
||||
|
||||
12
jenkins/base.sh
Normal file
12
jenkins/base.sh
Normal file
@@ -0,0 +1,12 @@
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
2
rakefile
2
rakefile
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user