diff --git a/common/lib/capa/capa_problem.py b/common/lib/capa/capa_problem.py
index 4ee2a2113d..a06ac1d7b6 100644
--- a/common/lib/capa/capa_problem.py
+++ b/common/lib/capa/capa_problem.py
@@ -169,7 +169,8 @@ class LoncapaProblem(object):
def get_score(self):
'''
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
for key in self.correct_map:
diff --git a/common/lib/xmodule/capa_module.py b/common/lib/xmodule/capa_module.py
index 55534f8a3e..7c75d1666a 100644
--- a/common/lib/xmodule/capa_module.py
+++ b/common/lib/xmodule/capa_module.py
@@ -11,6 +11,7 @@ from datetime import timedelta
from lxml import etree
from x_module import XModule, XModuleDescriptor
+from progress import Progress
from capa.capa_problem import LoncapaProblem
from capa.responsetypes import StudentInputError
@@ -79,24 +80,41 @@ class Module(XModule):
def get_xml_tags(c):
return ["problem"]
+
def get_state(self):
state = self.lcp.get_state()
state['attempts'] = self.attempts
return json.dumps(state)
+
def get_score(self):
return self.lcp.get_score()
+
def max_score(self):
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):
return self.system.render_template('problem_ajax.html', {
'id': self.item_id,
'ajax_url': self.ajax_url,
})
+
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()
content = {'name': self.name,
'html': html,
@@ -109,7 +127,7 @@ class Module(XModule):
reset_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.
if self.closed():
check_button = False
@@ -154,11 +172,13 @@ class Module(XModule):
'attempts_used': self.attempts,
'attempts_allowed': self.max_attempts,
'explain': explain,
+ 'progress': self.get_progress(),
}
html = self.system.render_template('problem.html', context)
if encapsulate:
- html = '
'.format(id=self.item_id, ajax_url=self.ajax_url) + html + "
"
+ html = ''.format(
+ id=self.item_id, ajax_url=self.ajax_url) + html + "
"
return html
@@ -170,7 +190,8 @@ class Module(XModule):
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")
self.explain_available = only_one(dom2.xpath('/problem/@explain_available'))
@@ -190,19 +211,19 @@ class Module(XModule):
self.grace_period = None
self.close_date = self.display_due_date
- self.max_attempts =only_one(dom2.xpath('/problem/@attempts'))
- if len(self.max_attempts)>0:
- self.max_attempts =int(self.max_attempts)
+ self.max_attempts = only_one(dom2.xpath('/problem/@attempts'))
+ if len(self.max_attempts) > 0:
+ self.max_attempts = int(self.max_attempts)
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 =="":
- self.show_answer ="closed"
+ if self.show_answer == "":
+ self.show_answer = "closed"
- self.rerandomize =only_one(dom2.xpath('/problem/@rerandomize'))
- if self.rerandomize =="" or self.rerandomize=="always" or self.rerandomize=="true":
+ self.rerandomize = only_one(dom2.xpath('/problem/@rerandomize'))
+ if self.rerandomize == "" or self.rerandomize=="always" or self.rerandomize=="true":
self.rerandomize="always"
elif self.rerandomize=="false" or self.rerandomize=="per_student":
self.rerandomize="per_student"
@@ -253,23 +274,33 @@ class Module(XModule):
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',
+ }
'''
- if dispatch=='problem_get':
- response = self.get_problem(get)
- elif False: #self.close_date >
- return json.dumps({"error":"Past due date"})
- elif dispatch=='problem_check':
- response = self.check_problem(get)
- elif dispatch=='problem_reset':
- response = self.reset_problem(get)
- elif dispatch=='problem_save':
- response = self.save_problem(get)
- elif dispatch=='problem_show':
- response = self.get_answer(get)
- else:
- return "Error"
- return response
+ handlers = {
+ 'problem_get': self.get_problem,
+ 'problem_check': self.check_problem,
+ 'problem_reset': self.reset_problem,
+ 'problem_save': self.save_problem,
+ 'problem_show': self.get_answer,
+ }
+
+ if dispatch not in handlers:
+ return 'Error'
+
+ before = self.get_progress()
+ d = handlers[dispatch](get)
+ after = self.get_progress()
+ d.update({
+ 'progress_changed' : after != before,
+ 'progress' : after.ternary_str(),
+ })
+ return json.dumps(d, cls=ComplexEncoder)
def closed(self):
''' Is the student still allowed to submit answers? '''
@@ -283,24 +314,22 @@ class Module(XModule):
def answer_available(self):
''' Is the user allowed to see an answer?
- TODO: simplify.
'''
if self.show_answer == '':
return False
+
if self.show_answer == "never":
return False
- if self.show_answer == 'attempted' and self.attempts == 0:
- return False
- if self.show_answer == 'attempted' and self.attempts > 0:
- return True
- if self.show_answer == 'answered' and self.lcp.done:
- return True
- if self.show_answer == 'answered' and not self.lcp.done:
- return False
- if self.show_answer == 'closed' and self.closed():
- return True
- if self.show_answer == 'closed' and not self.closed():
- return False
+
+ if self.show_answer == 'attempted':
+ return self.attempts > 0
+
+ if self.show_answer == 'answered':
+ return self.lcp.done
+
+ if self.show_answer == 'closed':
+ return self.closed()
+
if self.show_answer == 'always':
return True
raise self.system.exception404 #TODO: Not 404
@@ -310,45 +339,64 @@ class Module(XModule):
For the "show answer" button.
TODO: show answer events should be logged here, not just in the problem.js
+
+ Returns the answers: {'answers' : answers}
'''
if not self.answer_available():
raise self.system.exception404
else:
answers = self.lcp.get_question_answers()
- return json.dumps(answers,
- cls=ComplexEncoder)
+ return {'answers' : answers}
+
# Figure out if we should move these to capa_problem?
def get_problem(self, get):
- ''' Same as get_problem_html -- if we want to reconfirm we
- have the right thing e.g. after several AJAX calls.'''
- return self.get_problem_html(encapsulate=False)
+ ''' Return results of get_problem_html, as a simple dict for json-ing.
+ { 'html': }
+
+ 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):
''' 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['state'] = self.lcp.get_state()
event_info['filename'] = self.filename
- # make a dict of all the student responses ("answers").
- answers=dict()
- # input_resistor_1 ==> resistor_1
- for key in get:
- answers['_'.join(key.split('_')[1:])]=get[key]
+ answers = self.make_dict_of_responses(get)
- event_info['answers']=answers
+ event_info['answers'] = answers
# Too late. Cannot submit
if self.closed():
- event_info['failure']='closed'
+ event_info['failure'] = 'closed'
self.tracker('save_problem_check_fail', event_info)
+ # TODO: probably not 404?
raise self.system.exception404
# Problem submitted. Student should reset before checking
# again.
if self.lcp.done and self.rerandomize == "always":
- event_info['failure']='unreset'
+ event_info['failure'] = 'unreset'
self.tracker('save_problem_check_fail', event_info)
raise self.system.exception404
@@ -357,89 +405,107 @@ class Module(XModule):
lcp_id = self.lcp.problem_id
correct_map = self.lcp.grade_answers(answers)
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()
- return json.dumps({'success':inst.message})
+ return {'success': inst.message}
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()
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.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:
if not correct_map.is_correct(answer_id):
success = 'incorrect'
- event_info['correct_map']=correct_map.get_dict() # log this in the tracker
- event_info['success']=success
+ event_info['correct_map'] = correct_map.get_dict() # log this in the tracker
+ event_info['success'] = success
self.tracker('save_problem_check', event_info)
try:
html = self.get_problem_html(encapsulate=False) # render problem into HTML
except Exception,err:
log.error('failed to generate html')
- raise Exception,err
+ raise Exception, err
+
+ return {'success': success,
+ 'contents': html,
+ }
- return json.dumps({'success': success,
- 'contents': html,
- })
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['state'] = self.lcp.get_state()
event_info['filename'] = self.filename
- answers=dict()
- for key in get:
- answers['_'.join(key.split('_')[1:])]=get[key]
+ answers = self.make_dict_of_responses(get)
event_info['answers'] = answers
# Too late. Cannot submit
if self.closed():
- event_info['failure']='closed'
+ event_info['failure'] = 'closed'
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
# again.
if self.lcp.done and self.rerandomize == "always":
- event_info['failure']='done'
+ event_info['failure'] = 'done'
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)
- return json.dumps({'success':True})
+ return {'success': True}
def reset_problem(self, get):
''' 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['old_state']=self.lcp.get_state()
- event_info['filename']=self.filename
+ event_info['old_state'] = self.lcp.get_state()
+ event_info['filename'] = self.filename
if self.closed():
- event_info['failure']='closed'
+ event_info['failure'] = 'closed'
self.tracker('reset_problem_fail', event_info)
return "Problem is closed"
if not self.lcp.done:
- event_info['failure']='not_done'
+ event_info['failure'] = 'not_done'
self.tracker('reset_problem_fail', event_info)
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":
- self.lcp.seed=None # reset random number generator seed (note the self.lcp.get_state() in next line)
-
- self.lcp=LoncapaProblem(self.filestore.open(self.filename), self.item_id, self.lcp.get_state(), system=self.system)
+ # 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)
- event_info['new_state']=self.lcp.get_state()
+ event_info['new_state'] = self.lcp.get_state()
self.tracker('reset_problem', event_info)
- return json.dumps(self.get_problem_html(encapsulate=False))
+ return {'html' : self.get_problem_html(encapsulate=False)}
diff --git a/common/lib/xmodule/progress.py b/common/lib/xmodule/progress.py
index 1ce5d821f3..b9e242f2b2 100644
--- a/common/lib/xmodule/progress.py
+++ b/common/lib/xmodule/progress.py
@@ -13,6 +13,8 @@ class Progress(object):
Progress can only represent Progress for modules where that makes sense. Other
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):
diff --git a/common/lib/xmodule/seq_module.py b/common/lib/xmodule/seq_module.py
index b394227aa7..598fc4443e 100644
--- a/common/lib/xmodule/seq_module.py
+++ b/common/lib/xmodule/seq_module.py
@@ -1,8 +1,12 @@
import json
+import logging
from lxml import etree
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
# OBSOLETE: This obsoletes 'type'
@@ -37,6 +41,16 @@ class Module(XModule):
self.render()
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
''' get = request.POST instance '''
if dispatch=='goto_position':
@@ -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]) \
for e in self.xmltree]
+ children = self.get_children()
+ progresses = [child.get_progress() for child in 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['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):
new_class = 'other'
@@ -68,16 +87,17 @@ class Module(XModule):
# Split tags -- browsers handle this as end
# 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
- params={'items':json.dumps(self.contents).replace('', '<"+"/script>'),
- 'id':self.item_id,
+ params={'items': json.dumps(self.contents).replace('', '<"+"/script>'),
+ 'id': self.item_id,
'position': self.position,
- 'titles':titles,
- 'tag':self.xmltree.tag}
+ 'titles': titles,
+ 'tag': self.xmltree.tag}
if self.xmltree.tag in ['sequential', 'videosequence']:
self.content = self.system.render_template('seq_module.html', params)
if self.xmltree.tag == 'tab':
self.content = self.system.render_template('tab_module.html', params)
+ log.debug("rendered content: %s", content)
self.rendered = True
def __init__(self, system, xml, item_id, state=None):
diff --git a/common/lib/xmodule/vertical_module.py b/common/lib/xmodule/vertical_module.py
index 7eb6dda0b8..b3feec8bae 100644
--- a/common/lib/xmodule/vertical_module.py
+++ b/common/lib/xmodule/vertical_module.py
@@ -1,12 +1,14 @@
import json
from x_module import XModule, XModuleDescriptor
+from xmodule.progress import Progress
from lxml import etree
class ModuleDescriptor(XModuleDescriptor):
pass
class Module(XModule):
+ ''' Layout module for laying out submodules vertically.'''
id_attribute = 'id'
def get_state(self):
@@ -21,6 +23,13 @@ class Module(XModule):
'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):
XModule.__init__(self, system, xml, item_id, state)
xmltree=etree.fromstring(xml)
diff --git a/common/lib/xmodule/x_module.py b/common/lib/xmodule/x_module.py
index 3a5bd05286..043a830500 100644
--- a/common/lib/xmodule/x_module.py
+++ b/common/lib/xmodule/x_module.py
@@ -59,6 +59,13 @@ class XModule(object):
else:
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):
'''
Render all children.
@@ -92,6 +99,7 @@ class XModule(object):
self.tracker = system.track_function
self.filestore = system.filestore
self.render_function = system.render_function
+ self.module_from_xml = system.module_from_xml
self.DEBUG = system.DEBUG
self.system = system
diff --git a/lms/djangoapps/courseware/grades.py b/lms/djangoapps/courseware/grades.py
index 354cf5991f..4a11ec2d51 100644
--- a/lms/djangoapps/courseware/grades.py
+++ b/lms/djangoapps/courseware/grades.py
@@ -174,6 +174,8 @@ def get_score(user, problem, cache, coursename=None):
else:
## 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
+ # TODO: These are no longer correct params for I4xSystem -- figure out what this code
+ # does, clean it up.
from module_render import I4xSystem
system = I4xSystem(None, None, None, coursename=coursename)
total=float(xmodule.capa_module.Module(system, etree.tostring(problem), "id").max_score())
diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py
index af5fec6b85..9b5e7e4940 100644
--- a/lms/djangoapps/courseware/module_render.py
+++ b/lms/djangoapps/courseware/module_render.py
@@ -34,7 +34,8 @@ class I4xSystem(object):
and user, or other environment-specific info.
'''
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.
@@ -43,6 +44,8 @@ class I4xSystem(object):
or otherwise tracking the event.
TODO: Not used, and has inconsistent args in different
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,
returning a dictionary with a context for rendering the
module to html. Dictionary will contain keys 'content'
@@ -62,6 +65,7 @@ class I4xSystem(object):
if settings.DEBUG:
log.info("[courseware.module_render.I4xSystem] filestore path = %s",
filestore)
+ self.module_from_xml = module_from_xml
self.render_function = render_function
self.render_template = render_template
self.exception404 = Http404
@@ -127,6 +131,18 @@ def grade_histogram(module_id):
return []
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):
''' Get an instance of the xmodule class corresponding to module_xml,
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):
# Setup system context for module instance
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),
render_function = lambda xml: render_x_module(
user, request, xml, student_module_cache, position),
@@ -172,6 +191,7 @@ def get_module(user, request, module_xml, student_module_cache, position=None):
ajax_url = ajax_url,
request = request,
filestore = OSFS(data_root),
+ module_from_xml = module_from_xml,
)
# pass position specified in URL to module through I4xSystem
system.set('position', position)
@@ -295,9 +315,17 @@ def modx_dispatch(request, module=None, dispatch=None, id=None):
response = HttpResponse(json.dumps({'success': error_msg}))
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
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,
ajax_url = ajax_url,
request = request,
@@ -316,7 +344,11 @@ def modx_dispatch(request, module=None, dispatch=None, id=None):
return response
# Let the module handle the AJAX
- ajax_return = instance.handle_ajax(dispatch, request.POST)
+ try:
+ ajax_return = instance.handle_ajax(dispatch, request.POST)
+ except:
+ log.exception("error processing ajax call")
+ raise
# Save the state back to the database
s.state = instance.get_state()
diff --git a/lms/static/coffee/src/modules/problem.coffee b/lms/static/coffee/src/modules/problem.coffee
index aad41f23d4..1b254d5d3f 100644
--- a/lms/static/coffee/src/modules/problem.coffee
+++ b/lms/static/coffee/src/modules/problem.coffee
@@ -17,12 +17,20 @@ class @Problem
@$('section.action input.save').click @save
@$('input.math').keyup(@refreshMath).each(@refreshMath)
+ update_progress: (response) =>
+ if response.progress_changed
+ @element.attr progress: response.progress
+ @element.trigger('progressChanged')
+
render: (content) ->
if content
@element.html(content)
@bind()
else
- @element.load @content_url, @bind
+ $.postWithPrefix "/modx/problem/#{@id}/problem_get", '', (response) =>
+ @element.html(response.html)
+ @bind()
+
check: =>
Logger.log 'problem_check', @answers
@@ -30,19 +38,22 @@ class @Problem
switch response.success
when 'incorrect', 'correct'
@render(response.contents)
+ @update_progress response
else
alert(response.success)
reset: =>
Logger.log 'problem_reset', @answers
- $.postWithPrefix "/modx/problem/#{@id}/problem_reset", id: @id, (content) =>
- @render(content)
+ $.postWithPrefix "/modx/problem/#{@id}/problem_reset", id: @id, (response) =>
+ @render(response.html)
+ @update_progress response
show: =>
if !@element.hasClass 'showed'
Logger.log 'problem_show', problem: @id
$.postWithPrefix "/modx/problem/#{@id}/problem_show", (response) =>
- $.each response, (key, value) =>
+ answers = response.answers
+ $.each answers, (key, value) =>
if $.isArray(value)
for choice in value
@$("label[for='input_#{key}_#{choice}']").attr correct_answer: 'true'
@@ -51,6 +62,7 @@ class @Problem
MathJax.Hub.Queue ["Typeset", MathJax.Hub]
@$('.show').val 'Hide Answer'
@element.addClass 'showed'
+ @update_progress response
else
@$('[id^=answer_], [id^=solution_]').text ''
@$('[correct_answer]').attr correct_answer: null
@@ -62,6 +74,7 @@ class @Problem
$.postWithPrefix "/modx/problem/#{@id}/problem_save", @answers, (response) =>
if response.success
alert 'Saved'
+ @update_progress response
refreshMath: (event, element) =>
element = event.target unless element
diff --git a/lms/static/coffee/src/modules/sequence.coffee b/lms/static/coffee/src/modules/sequence.coffee
index 463bf419fc..72a1c82ab6 100644
--- a/lms/static/coffee/src/modules/sequence.coffee
+++ b/lms/static/coffee/src/modules/sequence.coffee
@@ -2,6 +2,7 @@ class @Sequence
constructor: (@id, @elements, @tag, position) ->
@element = $("#sequence_#{@id}")
@buildNavigation()
+ @initProgress()
@bind()
@render position
@@ -11,11 +12,52 @@ class @Sequence
bind: ->
@$('#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: ->
$.each @elements, (index, item) =>
link = $('').attr class: "seq_#{item.type}_inactive", 'data-element': index + 1
title = $('').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 = $('
').append(link.append(title))
+ @setProgress item.progress_stat, link
+
@$('#sequence-list').append list_item
toggleArrows: =>
@@ -36,13 +78,14 @@ class @Sequence
if @position != undefined
@mark_visited @position
$.postWithPrefix "/modx/#{@tag}/#{@id}/goto_position", position: new_position
-
+
@mark_active new_position
@$('#seq_content').html @elements[new_position - 1].content
MathJax.Hub.Queue(["Typeset", MathJax.Hub])
@position = new_position
@toggleArrows()
+ @hookUpProgressEvent()
@element.trigger 'contentChanged'
goto: (event) =>
@@ -67,7 +110,17 @@ class @Sequence
@$("#sequence-list a[data-element=#{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) ->
- @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")