Commit b963d8b5 by Victor Shnayder

Initial progress display.

* add module_from_xml param to I4xSystem
* use it to implement xmodule.get_children()
* fix a few comments here and there
* Render-time progress display for seq and vertical modules.
  - Computes fraction of subproblems done.
* Pass problem state back to js during ajax calls.
* general cleanup in capa_module.py
* add progress_changed and progress fields to json returned from each ajax handler

* Coffeescript changes to hook up sequence tracking of problem progress

* net result: sequence 'a' tags now have a progress class
* properly set css class on initial load
* fire event when progress changes after ajax calls
* also save state in 'progress' property of problems-wrapper tag
* event handler finds those tags, computes updated progress
parent a1269353
...@@ -168,7 +168,8 @@ class LoncapaProblem(object): ...@@ -168,7 +168,8 @@ class LoncapaProblem(object):
def get_score(self): def get_score(self):
''' '''
Compute score for this problem. The score is the number of points awarded. Compute score for this problem. The score is the number of points awarded.
Returns an integer, from 0 to get_max_score(). Returns a dictionary {'score': integer, from 0 to get_max_score(),
'total': get_max_score()}.
''' '''
correct = 0 correct = 0
for key in self.correct_map: for key in self.correct_map:
......
...@@ -11,6 +11,7 @@ from datetime import timedelta ...@@ -11,6 +11,7 @@ from datetime import timedelta
from lxml import etree from lxml import etree
from x_module import XModule, XModuleDescriptor from x_module import XModule, XModuleDescriptor
from progress import Progress
from capa.capa_problem import LoncapaProblem from capa.capa_problem import LoncapaProblem
from capa.responsetypes import StudentInputError from capa.responsetypes import StudentInputError
...@@ -79,24 +80,41 @@ class Module(XModule): ...@@ -79,24 +80,41 @@ class Module(XModule):
def get_xml_tags(c): def get_xml_tags(c):
return ["problem"] return ["problem"]
def get_state(self): def get_state(self):
state = self.lcp.get_state() state = self.lcp.get_state()
state['attempts'] = self.attempts state['attempts'] = self.attempts
return json.dumps(state) return json.dumps(state)
def get_score(self): def get_score(self):
return self.lcp.get_score() return self.lcp.get_score()
def max_score(self): def max_score(self):
return self.lcp.get_max_score() return self.lcp.get_max_score()
def get_progress(self):
''' For now, just return score / max_score
'''
d = self.get_score()
score = d['score']
total = d['total']
return Progress(score, total)
def get_html(self): def get_html(self):
return self.system.render_template('problem_ajax.html', { return self.system.render_template('problem_ajax.html', {
'id': self.item_id, 'id': self.item_id,
'ajax_url': self.ajax_url, 'ajax_url': self.ajax_url,
}) })
def get_problem_html(self, encapsulate=True): def get_problem_html(self, encapsulate=True):
'''Return html for the problem. Adds check, reset, save buttons
as necessary based on the problem config and state.'''
html = self.lcp.get_html() html = self.lcp.get_html()
content = {'name': self.name, content = {'name': self.name,
'html': html, 'html': html,
...@@ -109,7 +127,7 @@ class Module(XModule): ...@@ -109,7 +127,7 @@ class Module(XModule):
reset_button = True reset_button = True
save_button = True save_button = True
# If we're after deadline, or user has exhuasted attempts, # If we're after deadline, or user has exhausted attempts,
# question is read-only. # question is read-only.
if self.closed(): if self.closed():
check_button = False check_button = False
...@@ -154,11 +172,13 @@ class Module(XModule): ...@@ -154,11 +172,13 @@ class Module(XModule):
'attempts_used': self.attempts, 'attempts_used': self.attempts,
'attempts_allowed': self.max_attempts, 'attempts_allowed': self.max_attempts,
'explain': explain, 'explain': explain,
'progress': self.get_progress(),
} }
html = self.system.render_template('problem.html', context) html = self.system.render_template('problem.html', context)
if encapsulate: if encapsulate:
html = '<div id="problem_{id}" class="problem" data-url="{ajax_url}">'.format(id=self.item_id, ajax_url=self.ajax_url) + html + "</div>" html = '<div id="problem_{id}" class="problem" data-url="{ajax_url}">'.format(
id=self.item_id, ajax_url=self.ajax_url) + html + "</div>"
return html return html
...@@ -170,7 +190,8 @@ class Module(XModule): ...@@ -170,7 +190,8 @@ class Module(XModule):
dom2 = etree.fromstring(xml) dom2 = etree.fromstring(xml)
self.explanation = "problems/" + only_one(dom2.xpath('/problem/@explain'), default="closed") self.explanation = "problems/" + only_one(dom2.xpath('/problem/@explain'),
default="closed")
# TODO: Should be converted to: self.explanation=only_one(dom2.xpath('/problem/@explain'), default="closed") # TODO: Should be converted to: self.explanation=only_one(dom2.xpath('/problem/@explain'), default="closed")
self.explain_available = only_one(dom2.xpath('/problem/@explain_available')) self.explain_available = only_one(dom2.xpath('/problem/@explain_available'))
...@@ -190,19 +211,19 @@ class Module(XModule): ...@@ -190,19 +211,19 @@ class Module(XModule):
self.grace_period = None self.grace_period = None
self.close_date = self.display_due_date self.close_date = self.display_due_date
self.max_attempts =only_one(dom2.xpath('/problem/@attempts')) self.max_attempts = only_one(dom2.xpath('/problem/@attempts'))
if len(self.max_attempts)>0: if len(self.max_attempts) > 0:
self.max_attempts =int(self.max_attempts) self.max_attempts = int(self.max_attempts)
else: else:
self.max_attempts =None self.max_attempts = None
self.show_answer =only_one(dom2.xpath('/problem/@showanswer')) self.show_answer = only_one(dom2.xpath('/problem/@showanswer'))
if self.show_answer =="": if self.show_answer == "":
self.show_answer ="closed" self.show_answer = "closed"
self.rerandomize =only_one(dom2.xpath('/problem/@rerandomize')) self.rerandomize = only_one(dom2.xpath('/problem/@rerandomize'))
if self.rerandomize =="" or self.rerandomize=="always" or self.rerandomize=="true": if self.rerandomize == "" or self.rerandomize=="always" or self.rerandomize=="true":
self.rerandomize="always" self.rerandomize="always"
elif self.rerandomize=="false" or self.rerandomize=="per_student": elif self.rerandomize=="false" or self.rerandomize=="per_student":
self.rerandomize="per_student" self.rerandomize="per_student"
...@@ -253,23 +274,33 @@ class Module(XModule): ...@@ -253,23 +274,33 @@ class Module(XModule):
def handle_ajax(self, dispatch, get): def handle_ajax(self, dispatch, get):
''' '''
This is called by courseware.module_render, to handle an AJAX call. "get" is request.POST This is called by courseware.module_render, to handle an AJAX call.
"get" is request.POST.
Returns a json dictionary:
{ 'progress_changed' : True/False,
'progress' : 'none'/'in_progress'/'done',
<other request-specific values here > }
''' '''
if dispatch=='problem_get': handlers = {
response = self.get_problem(get) 'problem_get': self.get_problem,
elif False: #self.close_date > 'problem_check': self.check_problem,
return json.dumps({"error":"Past due date"}) 'problem_reset': self.reset_problem,
elif dispatch=='problem_check': 'problem_save': self.save_problem,
response = self.check_problem(get) 'problem_show': self.get_answer,
elif dispatch=='problem_reset': }
response = self.reset_problem(get)
elif dispatch=='problem_save': if dispatch not in handlers:
response = self.save_problem(get) return 'Error'
elif dispatch=='problem_show':
response = self.get_answer(get) before = self.get_progress()
else: d = handlers[dispatch](get)
return "Error" after = self.get_progress()
return response d.update({
'progress_changed' : after != before,
'progress' : after.ternary_str(),
})
return json.dumps(d, cls=ComplexEncoder)
def closed(self): def closed(self):
''' Is the student still allowed to submit answers? ''' ''' Is the student still allowed to submit answers? '''
...@@ -283,24 +314,22 @@ class Module(XModule): ...@@ -283,24 +314,22 @@ class Module(XModule):
def answer_available(self): def answer_available(self):
''' Is the user allowed to see an answer? ''' Is the user allowed to see an answer?
TODO: simplify.
''' '''
if self.show_answer == '': if self.show_answer == '':
return False return False
if self.show_answer == "never": if self.show_answer == "never":
return False return False
if self.show_answer == 'attempted' and self.attempts == 0:
return False if self.show_answer == 'attempted':
if self.show_answer == 'attempted' and self.attempts > 0: return self.attempts > 0
return True
if self.show_answer == 'answered' and self.lcp.done: if self.show_answer == 'answered':
return True return self.lcp.done
if self.show_answer == 'answered' and not self.lcp.done:
return False if self.show_answer == 'closed':
if self.show_answer == 'closed' and self.closed(): return self.closed()
return True
if self.show_answer == 'closed' and not self.closed():
return False
if self.show_answer == 'always': if self.show_answer == 'always':
return True return True
raise self.system.exception404 #TODO: Not 404 raise self.system.exception404 #TODO: Not 404
...@@ -310,45 +339,64 @@ class Module(XModule): ...@@ -310,45 +339,64 @@ class Module(XModule):
For the "show answer" button. For the "show answer" button.
TODO: show answer events should be logged here, not just in the problem.js TODO: show answer events should be logged here, not just in the problem.js
Returns the answers: {'answers' : answers}
''' '''
if not self.answer_available(): if not self.answer_available():
raise self.system.exception404 raise self.system.exception404
else: else:
answers = self.lcp.get_question_answers() answers = self.lcp.get_question_answers()
return json.dumps(answers, return {'answers' : answers}
cls=ComplexEncoder)
# Figure out if we should move these to capa_problem? # Figure out if we should move these to capa_problem?
def get_problem(self, get): def get_problem(self, get):
''' Same as get_problem_html -- if we want to reconfirm we ''' Return results of get_problem_html, as a simple dict for json-ing.
have the right thing e.g. after several AJAX calls.''' { 'html': <the-html> }
return self.get_problem_html(encapsulate=False)
Used if we want to reconfirm we have the right thing e.g. after
several AJAX calls.
'''
return {'html' : self.get_problem_html(encapsulate=False)}
@staticmethod
def make_dict_of_responses(get):
'''Make dictionary of student responses (aka "answers")
get is POST dictionary.
'''
answers = dict()
for key in get:
# e.g. input_resistor_1 ==> resistor_1
answers['_'.join(key.split('_')[1:])] = get[key]
return answers
def check_problem(self, get): def check_problem(self, get):
''' Checks whether answers to a problem are correct, and ''' Checks whether answers to a problem are correct, and
returns a map of correct/incorrect answers''' returns a map of correct/incorrect answers:
{'success' : bool,
'contents' : html}
'''
event_info = dict() event_info = dict()
event_info['state'] = self.lcp.get_state() event_info['state'] = self.lcp.get_state()
event_info['filename'] = self.filename event_info['filename'] = self.filename
# make a dict of all the student responses ("answers"). answers = self.make_dict_of_responses(get)
answers=dict()
# input_resistor_1 ==> resistor_1
for key in get:
answers['_'.join(key.split('_')[1:])]=get[key]
event_info['answers']=answers event_info['answers'] = answers
# Too late. Cannot submit # Too late. Cannot submit
if self.closed(): if self.closed():
event_info['failure']='closed' event_info['failure'] = 'closed'
self.tracker('save_problem_check_fail', event_info) self.tracker('save_problem_check_fail', event_info)
# TODO: probably not 404?
raise self.system.exception404 raise self.system.exception404
# Problem submitted. Student should reset before checking # Problem submitted. Student should reset before checking
# again. # again.
if self.lcp.done and self.rerandomize == "always": if self.lcp.done and self.rerandomize == "always":
event_info['failure']='unreset' event_info['failure'] = 'unreset'
self.tracker('save_problem_check_fail', event_info) self.tracker('save_problem_check_fail', event_info)
raise self.system.exception404 raise self.system.exception404
...@@ -357,89 +405,107 @@ class Module(XModule): ...@@ -357,89 +405,107 @@ class Module(XModule):
lcp_id = self.lcp.problem_id lcp_id = self.lcp.problem_id
correct_map = self.lcp.grade_answers(answers) correct_map = self.lcp.grade_answers(answers)
except StudentInputError as inst: except StudentInputError as inst:
self.lcp = LoncapaProblem(self.filestore.open(self.filename), id=lcp_id, state=old_state, system=self.system) # TODO: why is this line here?
self.lcp = LoncapaProblem(self.filestore.open(self.filename),
id=lcp_id, state=old_state, system=self.system)
traceback.print_exc() traceback.print_exc()
return json.dumps({'success':inst.message}) return {'success': inst.message}
except: except:
self.lcp = LoncapaProblem(self.filestore.open(self.filename), id=lcp_id, state=old_state, system=self.system) # TODO: why is this line here?
self.lcp = LoncapaProblem(self.filestore.open(self.filename),
id=lcp_id, state=old_state, system=self.system)
traceback.print_exc() traceback.print_exc()
raise Exception,"error in capa_module" raise Exception,"error in capa_module"
return json.dumps({'success':'Unknown Error'}) # TODO: Dead code... is this a bug, or just old?
return {'success':'Unknown Error'}
self.attempts = self.attempts + 1 self.attempts = self.attempts + 1
self.lcp.done=True self.lcp.done = True
success = 'correct' # success = correct if ALL questions in this problem are correct # success = correct if ALL questions in this problem are correct
success = 'correct'
for answer_id in correct_map: for answer_id in correct_map:
if not correct_map.is_correct(answer_id): if not correct_map.is_correct(answer_id):
success = 'incorrect' success = 'incorrect'
event_info['correct_map']=correct_map.get_dict() # log this in the tracker event_info['correct_map'] = correct_map.get_dict() # log this in the tracker
event_info['success']=success event_info['success'] = success
self.tracker('save_problem_check', event_info) self.tracker('save_problem_check', event_info)
try: try:
html = self.get_problem_html(encapsulate=False) # render problem into HTML html = self.get_problem_html(encapsulate=False) # render problem into HTML
except Exception,err: except Exception,err:
log.error('failed to generate html') log.error('failed to generate html')
raise Exception,err raise Exception, err
return json.dumps({'success': success, return {'success': success,
'contents': html, 'contents': html,
}) }
def save_problem(self, get): def save_problem(self, get):
'''
Save the passed in answers.
Returns a dict { 'success' : bool, ['error' : error-msg]},
with the error key only present if success is False.
'''
event_info = dict() event_info = dict()
event_info['state'] = self.lcp.get_state() event_info['state'] = self.lcp.get_state()
event_info['filename'] = self.filename event_info['filename'] = self.filename
answers=dict() answers = self.make_dict_of_responses(get)
for key in get:
answers['_'.join(key.split('_')[1:])]=get[key]
event_info['answers'] = answers event_info['answers'] = answers
# Too late. Cannot submit # Too late. Cannot submit
if self.closed(): if self.closed():
event_info['failure']='closed' event_info['failure'] = 'closed'
self.tracker('save_problem_fail', event_info) self.tracker('save_problem_fail', event_info)
return "Problem is closed" return {'success': False,
'error': "Problem is closed"}
# Problem submitted. Student should reset before saving # Problem submitted. Student should reset before saving
# again. # again.
if self.lcp.done and self.rerandomize == "always": if self.lcp.done and self.rerandomize == "always":
event_info['failure']='done' event_info['failure'] = 'done'
self.tracker('save_problem_fail', event_info) self.tracker('save_problem_fail', event_info)
return "Problem needs to be reset prior to save." return {'success' : False,
'error' : "Problem needs to be reset prior to save."}
self.lcp.student_answers=answers self.lcp.student_answers = answers
# TODO: should this be save_problem_fail? Looks like success to me...
self.tracker('save_problem_fail', event_info) self.tracker('save_problem_fail', event_info)
return json.dumps({'success':True}) return {'success': True}
def reset_problem(self, get): def reset_problem(self, get):
''' Changes problem state to unfinished -- removes student answers, ''' Changes problem state to unfinished -- removes student answers,
and causes problem to rerender itself. ''' and causes problem to rerender itself.
Returns problem html as { 'html' : html-string }.
'''
event_info = dict() event_info = dict()
event_info['old_state']=self.lcp.get_state() event_info['old_state'] = self.lcp.get_state()
event_info['filename']=self.filename event_info['filename'] = self.filename
if self.closed(): if self.closed():
event_info['failure']='closed' event_info['failure'] = 'closed'
self.tracker('reset_problem_fail', event_info) self.tracker('reset_problem_fail', event_info)
return "Problem is closed" return "Problem is closed"
if not self.lcp.done: if not self.lcp.done:
event_info['failure']='not_done' event_info['failure'] = 'not_done'
self.tracker('reset_problem_fail', event_info) self.tracker('reset_problem_fail', event_info)
return "Refresh the page and make an attempt before resetting." return "Refresh the page and make an attempt before resetting."
self.lcp.do_reset() # call method in LoncapaProblem to reset itself self.lcp.do_reset()
if self.rerandomize == "always": if self.rerandomize == "always":
self.lcp.seed=None # reset random number generator seed (note the self.lcp.get_state() in next line) # reset random number generator seed (note the self.lcp.get_state() in next line)
self.lcp.seed=None
self.lcp=LoncapaProblem(self.filestore.open(self.filename), self.item_id, self.lcp.get_state(), system=self.system) self.lcp = LoncapaProblem(self.filestore.open(self.filename),
self.item_id, self.lcp.get_state(), system=self.system)
event_info['new_state']=self.lcp.get_state() event_info['new_state'] = self.lcp.get_state()
self.tracker('reset_problem', event_info) self.tracker('reset_problem', event_info)
return json.dumps(self.get_problem_html(encapsulate=False)) return {'html' : self.get_problem_html(encapsulate=False)}
...@@ -13,6 +13,8 @@ class Progress(object): ...@@ -13,6 +13,8 @@ class Progress(object):
Progress can only represent Progress for modules where that makes sense. Other Progress can only represent Progress for modules where that makes sense. Other
modules (e.g. html) should return None from get_progress(). modules (e.g. html) should return None from get_progress().
TODO: add tag for module type? Would allow for smarter merging.
''' '''
def __init__(self, a, b): def __init__(self, a, b):
......
import json import json
import logging
from lxml import etree from lxml import etree
from x_module import XModule, XModuleDescriptor from x_module import XModule, XModuleDescriptor
from xmodule.progress import Progress
log = logging.getLogger("mitx.common.lib.seq_module")
# HACK: This shouldn't be hard-coded to two types # HACK: This shouldn't be hard-coded to two types
# OBSOLETE: This obsoletes 'type' # OBSOLETE: This obsoletes 'type'
...@@ -37,6 +41,16 @@ class Module(XModule): ...@@ -37,6 +41,16 @@ class Module(XModule):
self.render() self.render()
return self.destroy_js return self.destroy_js
def get_progress(self):
''' Return the total progress, adding total done and total available.
(assumes that each submodule uses the same "units" for progress.)
'''
# TODO: Cache progress or children array?
children = self.get_children()
progresses = [child.get_progress() for child in children]
progress = reduce(Progress.add_counts, progresses)
return progress
def handle_ajax(self, dispatch, get): # TODO: bounds checking def handle_ajax(self, dispatch, get): # TODO: bounds checking
''' get = request.POST instance ''' ''' get = request.POST instance '''
if dispatch=='goto_position': if dispatch=='goto_position':
...@@ -53,10 +67,15 @@ class Module(XModule): ...@@ -53,10 +67,15 @@ class Module(XModule):
titles = ["\n".join([i.get("name").strip() for i in e.iter() if i.get("name") is not None]) \ titles = ["\n".join([i.get("name").strip() for i in e.iter() if i.get("name") is not None]) \
for e in self.xmltree] for e in self.xmltree]
children = self.get_children()
progresses = [child.get_progress() for child in children]
self.contents = self.rendered_children() self.contents = self.rendered_children()
for contents, title in zip(self.contents, titles): for contents, title, progress in zip(self.contents, titles, progresses):
contents['title'] = title contents['title'] = title
contents['progress_str'] = str(progress) if progress is not None else ""
contents['progress_stat'] = progress.ternary_str() if progress is not None else ""
for (content, element_class) in zip(self.contents, child_classes): for (content, element_class) in zip(self.contents, child_classes):
new_class = 'other' new_class = 'other'
...@@ -68,16 +87,17 @@ class Module(XModule): ...@@ -68,16 +87,17 @@ class Module(XModule):
# Split </script> tags -- browsers handle this as end # Split </script> tags -- browsers handle this as end
# of script, even if it occurs mid-string. Do this after json.dumps()ing # of script, even if it occurs mid-string. Do this after json.dumps()ing
# so that we can be sure of the quotations being used # so that we can be sure of the quotations being used
params={'items':json.dumps(self.contents).replace('</script>', '<"+"/script>'), params={'items': json.dumps(self.contents).replace('</script>', '<"+"/script>'),
'id':self.item_id, 'id': self.item_id,
'position': self.position, 'position': self.position,
'titles':titles, 'titles': titles,
'tag':self.xmltree.tag} 'tag': self.xmltree.tag}
if self.xmltree.tag in ['sequential', 'videosequence']: if self.xmltree.tag in ['sequential', 'videosequence']:
self.content = self.system.render_template('seq_module.html', params) self.content = self.system.render_template('seq_module.html', params)
if self.xmltree.tag == 'tab': if self.xmltree.tag == 'tab':
self.content = self.system.render_template('tab_module.html', params) self.content = self.system.render_template('tab_module.html', params)
log.debug("rendered content: %s", content)
self.rendered = True self.rendered = True
def __init__(self, system, xml, item_id, state=None): def __init__(self, system, xml, item_id, state=None):
......
import json import json
from x_module import XModule, XModuleDescriptor from x_module import XModule, XModuleDescriptor
from xmodule.progress import Progress
from lxml import etree from lxml import etree
class ModuleDescriptor(XModuleDescriptor): class ModuleDescriptor(XModuleDescriptor):
pass pass
class Module(XModule): class Module(XModule):
''' Layout module for laying out submodules vertically.'''
id_attribute = 'id' id_attribute = 'id'
def get_state(self): def get_state(self):
...@@ -21,6 +23,13 @@ class Module(XModule): ...@@ -21,6 +23,13 @@ class Module(XModule):
'items': self.contents 'items': self.contents
}) })
def get_progress(self):
# TODO: Cache progress or children array?
children = self.get_children()
progresses = [child.get_progress() for child in children]
progress = reduce(Progress.add_counts, progresses)
return progress
def __init__(self, system, xml, item_id, state=None): def __init__(self, system, xml, item_id, state=None):
XModule.__init__(self, system, xml, item_id, state) XModule.__init__(self, system, xml, item_id, state)
xmltree=etree.fromstring(xml) xmltree=etree.fromstring(xml)
......
...@@ -59,6 +59,13 @@ class XModule(object): ...@@ -59,6 +59,13 @@ class XModule(object):
else: else:
raise "We should iterate through children and find a default name" raise "We should iterate through children and find a default name"
def get_children(self):
'''
Return module instances for all the children of this module.
'''
children = [self.module_from_xml(e) for e in self.__xmltree]
return children
def rendered_children(self): def rendered_children(self):
''' '''
Render all children. Render all children.
...@@ -92,6 +99,7 @@ class XModule(object): ...@@ -92,6 +99,7 @@ class XModule(object):
self.tracker = system.track_function self.tracker = system.track_function
self.filestore = system.filestore self.filestore = system.filestore
self.render_function = system.render_function self.render_function = system.render_function
self.module_from_xml = system.module_from_xml
self.DEBUG = system.DEBUG self.DEBUG = system.DEBUG
self.system = system self.system = system
......
...@@ -174,6 +174,8 @@ def get_score(user, problem, cache, coursename=None): ...@@ -174,6 +174,8 @@ def get_score(user, problem, cache, coursename=None):
else: else:
## HACK 1: We shouldn't specifically reference capa_module ## HACK 1: We shouldn't specifically reference capa_module
## HACK 2: Backwards-compatibility: This should be written when a grade is saved, and removed from the system ## HACK 2: Backwards-compatibility: This should be written when a grade is saved, and removed from the system
# TODO: These are no longer correct params for I4xSystem -- figure out what this code
# does, clean it up.
from module_render import I4xSystem from module_render import I4xSystem
system = I4xSystem(None, None, None, coursename=coursename) system = I4xSystem(None, None, None, coursename=coursename)
total=float(xmodule.capa_module.Module(system, etree.tostring(problem), "id").max_score()) total=float(xmodule.capa_module.Module(system, etree.tostring(problem), "id").max_score())
......
...@@ -34,7 +34,8 @@ class I4xSystem(object): ...@@ -34,7 +34,8 @@ class I4xSystem(object):
and user, or other environment-specific info. and user, or other environment-specific info.
''' '''
def __init__(self, ajax_url, track_function, render_function, def __init__(self, ajax_url, track_function, render_function,
render_template, request=None, filestore=None): module_from_xml, render_template, request=None,
filestore=None):
''' '''
Create a closure around the system environment. Create a closure around the system environment.
...@@ -43,6 +44,8 @@ class I4xSystem(object): ...@@ -43,6 +44,8 @@ class I4xSystem(object):
or otherwise tracking the event. or otherwise tracking the event.
TODO: Not used, and has inconsistent args in different TODO: Not used, and has inconsistent args in different
files. Update or remove. files. Update or remove.
module_from_xml - function that takes (module_xml) and returns a corresponding
module instance object.
render_function - function that takes (module_xml) and renders it, render_function - function that takes (module_xml) and renders it,
returning a dictionary with a context for rendering the returning a dictionary with a context for rendering the
module to html. Dictionary will contain keys 'content' module to html. Dictionary will contain keys 'content'
...@@ -62,6 +65,7 @@ class I4xSystem(object): ...@@ -62,6 +65,7 @@ class I4xSystem(object):
if settings.DEBUG: if settings.DEBUG:
log.info("[courseware.module_render.I4xSystem] filestore path = %s", log.info("[courseware.module_render.I4xSystem] filestore path = %s",
filestore) filestore)
self.module_from_xml = module_from_xml
self.render_function = render_function self.render_function = render_function
self.render_template = render_template self.render_template = render_template
self.exception404 = Http404 self.exception404 = Http404
...@@ -127,6 +131,18 @@ def grade_histogram(module_id): ...@@ -127,6 +131,18 @@ def grade_histogram(module_id):
return [] return []
return grades return grades
def make_module_from_xml_fn(user, request, student_module_cache, position):
'''Create the make_from_xml() function'''
def module_from_xml(xml):
'''Modules need a way to convert xml to instance objects.
Pass the rest of the context through.'''
(instance, sm, module_type) = get_module(
user, request, xml, student_module_cache, position)
return instance
return module_from_xml
def get_module(user, request, module_xml, student_module_cache, position=None): def get_module(user, request, module_xml, student_module_cache, position=None):
''' Get an instance of the xmodule class corresponding to module_xml, ''' Get an instance of the xmodule class corresponding to module_xml,
setting the state based on an existing StudentModule, or creating one if none setting the state based on an existing StudentModule, or creating one if none
...@@ -165,6 +181,9 @@ def get_module(user, request, module_xml, student_module_cache, position=None): ...@@ -165,6 +181,9 @@ def get_module(user, request, module_xml, student_module_cache, position=None):
# Setup system context for module instance # Setup system context for module instance
ajax_url = settings.MITX_ROOT_URL + '/modx/' + module_type + '/' + module_id + '/' ajax_url = settings.MITX_ROOT_URL + '/modx/' + module_type + '/' + module_id + '/'
module_from_xml = make_module_from_xml_fn(
user, request, student_module_cache, position)
system = I4xSystem(track_function = make_track_function(request), system = I4xSystem(track_function = make_track_function(request),
render_function = lambda xml: render_x_module( render_function = lambda xml: render_x_module(
user, request, xml, student_module_cache, position), user, request, xml, student_module_cache, position),
...@@ -172,6 +191,7 @@ def get_module(user, request, module_xml, student_module_cache, position=None): ...@@ -172,6 +191,7 @@ def get_module(user, request, module_xml, student_module_cache, position=None):
ajax_url = ajax_url, ajax_url = ajax_url,
request = request, request = request,
filestore = OSFS(data_root), filestore = OSFS(data_root),
module_from_xml = module_from_xml,
) )
# pass position specified in URL to module through I4xSystem # pass position specified in URL to module through I4xSystem
system.set('position', position) system.set('position', position)
...@@ -295,9 +315,17 @@ def modx_dispatch(request, module=None, dispatch=None, id=None): ...@@ -295,9 +315,17 @@ def modx_dispatch(request, module=None, dispatch=None, id=None):
response = HttpResponse(json.dumps({'success': error_msg})) response = HttpResponse(json.dumps({'success': error_msg}))
return response return response
# TODO: This doesn't have a cache of child student modules. Just
# passing the current one. If ajax calls end up needing children,
# this won't work (but fixing it may cause performance issues...)
# Figure out :)
module_from_xml = make_module_from_xml_fn(
request.user, request, [s], None)
# Create the module # Create the module
system = I4xSystem(track_function = make_track_function(request), system = I4xSystem(track_function = make_track_function(request),
render_function = None, render_function = None,
module_from_xml = module_from_xml,
render_template = render_to_string, render_template = render_to_string,
ajax_url = ajax_url, ajax_url = ajax_url,
request = request, request = request,
...@@ -316,7 +344,11 @@ def modx_dispatch(request, module=None, dispatch=None, id=None): ...@@ -316,7 +344,11 @@ def modx_dispatch(request, module=None, dispatch=None, id=None):
return response return response
# Let the module handle the AJAX # Let the module handle the AJAX
try:
ajax_return = instance.handle_ajax(dispatch, request.POST) ajax_return = instance.handle_ajax(dispatch, request.POST)
except:
log.exception("error processing ajax call")
raise
# Save the state back to the database # Save the state back to the database
s.state = instance.get_state() s.state = instance.get_state()
......
...@@ -17,12 +17,20 @@ class @Problem ...@@ -17,12 +17,20 @@ class @Problem
@$('section.action input.save').click @save @$('section.action input.save').click @save
@$('input.math').keyup(@refreshMath).each(@refreshMath) @$('input.math').keyup(@refreshMath).each(@refreshMath)
update_progress: (response) =>
if response.progress_changed
@element.attr progress: response.progress
@element.trigger('progressChanged')
render: (content) -> render: (content) ->
if content if content
@element.html(content) @element.html(content)
@bind() @bind()
else else
@element.load @content_url, @bind $.postWithPrefix "/modx/problem/#{@id}/problem_get", '', (response) =>
@element.html(response.html)
@bind()
check: => check: =>
Logger.log 'problem_check', @answers Logger.log 'problem_check', @answers
...@@ -30,19 +38,22 @@ class @Problem ...@@ -30,19 +38,22 @@ class @Problem
switch response.success switch response.success
when 'incorrect', 'correct' when 'incorrect', 'correct'
@render(response.contents) @render(response.contents)
@update_progress response
else else
alert(response.success) alert(response.success)
reset: => reset: =>
Logger.log 'problem_reset', @answers Logger.log 'problem_reset', @answers
$.postWithPrefix "/modx/problem/#{@id}/problem_reset", id: @id, (content) => $.postWithPrefix "/modx/problem/#{@id}/problem_reset", id: @id, (response) =>
@render(content) @render(response.html)
@update_progress response
show: => show: =>
if !@element.hasClass 'showed' if !@element.hasClass 'showed'
Logger.log 'problem_show', problem: @id Logger.log 'problem_show', problem: @id
$.postWithPrefix "/modx/problem/#{@id}/problem_show", (response) => $.postWithPrefix "/modx/problem/#{@id}/problem_show", (response) =>
$.each response, (key, value) => answers = response.answers
$.each answers, (key, value) =>
if $.isArray(value) if $.isArray(value)
for choice in value for choice in value
@$("label[for='input_#{key}_#{choice}']").attr correct_answer: 'true' @$("label[for='input_#{key}_#{choice}']").attr correct_answer: 'true'
...@@ -51,6 +62,7 @@ class @Problem ...@@ -51,6 +62,7 @@ class @Problem
MathJax.Hub.Queue ["Typeset", MathJax.Hub] MathJax.Hub.Queue ["Typeset", MathJax.Hub]
@$('.show').val 'Hide Answer' @$('.show').val 'Hide Answer'
@element.addClass 'showed' @element.addClass 'showed'
@update_progress response
else else
@$('[id^=answer_], [id^=solution_]').text '' @$('[id^=answer_], [id^=solution_]').text ''
@$('[correct_answer]').attr correct_answer: null @$('[correct_answer]').attr correct_answer: null
...@@ -62,6 +74,7 @@ class @Problem ...@@ -62,6 +74,7 @@ class @Problem
$.postWithPrefix "/modx/problem/#{@id}/problem_save", @answers, (response) => $.postWithPrefix "/modx/problem/#{@id}/problem_save", @answers, (response) =>
if response.success if response.success
alert 'Saved' alert 'Saved'
@update_progress response
refreshMath: (event, element) => refreshMath: (event, element) =>
element = event.target unless element element = event.target unless element
......
...@@ -2,6 +2,7 @@ class @Sequence ...@@ -2,6 +2,7 @@ class @Sequence
constructor: (@id, @elements, @tag, position) -> constructor: (@id, @elements, @tag, position) ->
@element = $("#sequence_#{@id}") @element = $("#sequence_#{@id}")
@buildNavigation() @buildNavigation()
@initProgress()
@bind() @bind()
@render position @render position
...@@ -11,11 +12,52 @@ class @Sequence ...@@ -11,11 +12,52 @@ class @Sequence
bind: -> bind: ->
@$('#sequence-list a').click @goto @$('#sequence-list a').click @goto
initProgress: ->
@progressTable = {} # "#problem_#{id}" -> progress
hookUpProgressEvent: ->
$('.problems-wrapper').bind 'progressChanged', @updateProgress
mergeProgress: (p1, p2) ->
if p1 == "done" and p2 == "done"
return "done"
# not done, so if any progress on either, in_progress
w1 = p1 == "done" or p1 == "in_progress"
w2 = p2 == "done" or p2 == "in_progress"
if w1 or w2
return "in_progress"
return "none"
updateProgress: =>
new_progress = "none"
_this = this
$('.problems-wrapper').each (index) ->
progress = $(this).attr 'progress'
new_progress = _this.mergeProgress progress, new_progress
@progressTable[@position] = new_progress
@setProgress(new_progress, @link_for(@position))
setProgress: (progress, element) ->
element.removeClass('progress-none')
.removeClass('progress-some')
.removeClass('progress-done')
switch progress
when 'none' then element.addClass('progress-none')
when 'in_progress' then element.addClass('progress-some')
when 'done' then element.addClass('progress-done')
buildNavigation: -> buildNavigation: ->
$.each @elements, (index, item) => $.each @elements, (index, item) =>
link = $('<a>').attr class: "seq_#{item.type}_inactive", 'data-element': index + 1 link = $('<a>').attr class: "seq_#{item.type}_inactive", 'data-element': index + 1
title = $('<p>').html(item.title) title = $('<p>').html(item.title)
# TODO: add item.progress_str either to the title or somewhere else.
# Make sure it gets updated after ajax calls
list_item = $('<li>').append(link.append(title)) list_item = $('<li>').append(link.append(title))
@setProgress item.progress_stat, link
@$('#sequence-list').append list_item @$('#sequence-list').append list_item
toggleArrows: => toggleArrows: =>
...@@ -43,6 +85,7 @@ class @Sequence ...@@ -43,6 +85,7 @@ class @Sequence
MathJax.Hub.Queue(["Typeset", MathJax.Hub]) MathJax.Hub.Queue(["Typeset", MathJax.Hub])
@position = new_position @position = new_position
@toggleArrows() @toggleArrows()
@hookUpProgressEvent()
@element.trigger 'contentChanged' @element.trigger 'contentChanged'
goto: (event) => goto: (event) =>
...@@ -67,7 +110,17 @@ class @Sequence ...@@ -67,7 +110,17 @@ class @Sequence
@$("#sequence-list a[data-element=#{position}]") @$("#sequence-list a[data-element=#{position}]")
mark_visited: (position) -> mark_visited: (position) ->
@link_for(position).attr class: "seq_#{@elements[position - 1].type}_visited" # Don't overwrite class attribute to avoid changing Progress class
type = @elements[position - 1].type
element = @link_for(position)
element.removeClass("seq_#{type}_inactive")
.removeClass("seq_#{type}_active")
.addClass("seq_#{type}_visited")
mark_active: (position) -> mark_active: (position) ->
@link_for(position).attr class: "seq_#{@elements[position - 1].type}_active" # Don't overwrite class attribute to avoid changing Progress class
type = @elements[position - 1].type
element = @link_for(position)
element.removeClass("seq_#{type}_inactive")
.removeClass("seq_#{type}_visited")
.addClass("seq_#{type}_active")
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