Commit 04dd8ee6 by Victor Shnayder Committed by Matthew Mongeau

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 fd48e49a
...@@ -169,7 +169,8 @@ class LoncapaProblem(object): ...@@ -169,7 +169,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:
......
...@@ -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
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 # 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: =>
...@@ -36,13 +78,14 @@ class @Sequence ...@@ -36,13 +78,14 @@ class @Sequence
if @position != undefined if @position != undefined
@mark_visited @position @mark_visited @position
$.postWithPrefix "/modx/#{@tag}/#{@id}/goto_position", position: new_position $.postWithPrefix "/modx/#{@tag}/#{@id}/goto_position", position: new_position
@mark_active new_position @mark_active new_position
@$('#seq_content').html @elements[new_position - 1].content @$('#seq_content').html @elements[new_position - 1].content
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