Commit 0dd5191a by Justin Riley

randomize_module: add support for history tracking

History tracking enables never selecting the same child twice. Currently
this functionality is tied to the randomize xmodule's use_randrange
attribute.

TODO: Add new xml attribute `no_repeats` to toggle history tracking
parent eee555fb
import json import json
import logging
import random import random
import logging
from collections import OrderedDict
from xmodule.x_module import XModule from xmodule.x_module import XModule
from xmodule.seq_module import SequenceDescriptor from xmodule.seq_module import SequenceDescriptor
from lxml import etree from lxml import etree
from xblock.fields import Scope, Integer from xblock.fields import Scope, String
from xblock.fragment import Fragment from xblock.fragment import Fragment
log = logging.getLogger('edx.' + __name__) log = logging.getLogger('edx.' + __name__)
class RandomizeFields(object): class RandomizeFields(object):
choice = Integer(help="Which random child was chosen", choice = String(help="Which random child was chosen",
scope=Scope.user_state)
history = String(help="History of choices (json)",
scope=Scope.user_state) scope=Scope.user_state)
...@@ -46,43 +49,67 @@ class RandomizeModule(RandomizeFields, XModule): ...@@ -46,43 +49,67 @@ class RandomizeModule(RandomizeFields, XModule):
# it calls get_child_descriptors() internally, but that doesn't work # it calls get_child_descriptors() internally, but that doesn't work
# until we've picked a choice # until we've picked a choice
# import pudb; pudb.set_trace() # import pudb; pudb.set_trace()
children = self.descriptor.get_children()
xml_attrs = self.descriptor.xml_attributes or [] xml_attrs = self.descriptor.xml_attributes or []
use_randrange = 'use_randrange' in xml_attrs use_randrange = 'use_randrange' in xml_attrs
# TODO: use this attr to toggle storing history and never showing the # TODO: use this attr to toggle storing history and never showing the
# same problem twice # same problem twice
# no_repeats = xml_attrs.has_key('no_repeats') # no_repeats = xml_attrs.has_key('no_repeats')
num_choices = len(children) no_repeats = use_randrange
self.pick_choice(use_randrange=use_randrange, no_repeats=no_repeats)
if self.choice > num_choices:
# Oops. Children changed. Reset. def pick_choice(self, use_randrange=None, no_repeats=None):
children = [c.location.url() for c in self.descriptor.get_children()]
choices = self.get_choices(no_repeats=no_repeats)
num_choices = len(choices)
if self.choice is not None and self.choice not in children:
# Children changed. Reset.
self.choice = None self.choice = None
if self.choice is None: if self.choice is None:
# choose one based on the system seed, or randomly if that's not # choose one based on the system seed, or randomly if that's not
# available # available
if num_choices > 0: if num_choices > 0:
keys = choices.keys()
if self.system.seed is not None and not use_randrange: if self.system.seed is not None and not use_randrange:
self.choice = self.system.seed % num_choices choice = keys[self.system.seed % num_choices]
log.debug('using seed for %s choice=%s' % log.debug('using seed for %s choice=%s' %
(str(self.location), self.choice)) (str(self.location), self.choice))
else: else:
self.choice = random.randrange(0, num_choices) choice = random.choice(keys)
log.debug('using randrange for %s' % str(self.location)) log.debug('using randrange for %s' % str(self.location))
self.choice = choice
else: else:
log.debug('error in randomize: num_choices = %s' % num_choices) log.debug('error in randomize: num_choices = %s' % num_choices)
if self.choice is not None: if self.choice is not None:
self.child_descriptor = self.descriptor.get_children()[self.choice] self.child_descriptor = choices[self.choice]
# Now get_children() should return a list with one element # Now get_children() should return a list with one element
log.debug("choice=%s in %s, children of randomize module " log.debug("choice=%s in %s, children of randomize module "
"(should be only 1): %s", self.choice, "(should be only 1): %s", self.choice,
str(self.location), self.get_children()) str(self.location), self.get_children())
self.child = self.get_children()[0] self.child = self.get_children()[0]
if no_repeats:
child_loc = self.child.location.url()
history = json.loads(self.history or '[]')
if child_loc not in history:
history.append(child_loc)
self.history = json.dumps(history)
else: else:
self.child_descriptor = None self.child_descriptor = None
self.child = None self.child = None
def get_choices(self, no_repeats=None):
children = self.descriptor.get_children()
if self.choice is None and no_repeats:
history = json.loads(self.history or '[]')
children = [c for c in children if c.location.url() not in history]
return OrderedDict([(c.location.url(), c) for c in children])
def get_choice_index(self, choice=None, choices=None):
choice = choice or self.choice
choices = choices or self.get_choices().keys()
return choices.index(choice)
def get_child_descriptors(self): def get_child_descriptors(self):
""" """
For grading--return just the chosen child. For grading--return just the chosen child.
...@@ -97,30 +124,32 @@ class RandomizeModule(RandomizeFields, XModule): ...@@ -97,30 +124,32 @@ class RandomizeModule(RandomizeFields, XModule):
return Fragment(content=u"<div>Nothing to randomize between</div>") return Fragment(content=u"<div>Nothing to randomize between</div>")
child_html = self.child.render('student_view', context) child_html = self.child.render('student_view', context)
if self.system.user_is_staff: if self.system.user_is_staff:
choices = self.get_choices().keys()
dishtml = self.system.render_template('randomize_control.html', { dishtml = self.system.render_template('randomize_control.html', {
'element_id': self.location.html_id(), 'element_id': self.location.html_id(),
'is_staff': self.system.user_is_staff, 'is_staff': self.system.user_is_staff,
'ajax_url': self.system.ajax_url, 'ajax_url': self.system.ajax_url,
'choice': self.choice, 'choice': self.get_choice_index(choices=choices),
'num_choices': len(self.descriptor.get_children()), 'num_choices': len(choices),
}) })
# html = '<html><p>Welcome, staff. Randomize loc=%s ; # html = '<html><p>Welcome, staff. Randomize loc=%s ;
# Choice=%s</p><br/><hr/></br/>' % (str(self.location), # Choice=%s</p><br/><hr/></br/>' % (str(self.location),
# self.choice) # self.choice)
return Fragment(u"<html>" + dishtml + child_html.content + return Fragment(
u"</html>") u"<html>" + dishtml + child_html.content + u"</html>")
return child_html return child_html
def handle_ajax(self, dispatch, data): def handle_ajax(self, dispatch, data):
choices = self.get_choices().keys()
index = self.get_choice_index(choices=choices)
if dispatch == 'next': if dispatch == 'next':
self.choice = self.choice + 1 self.choice = choices[index + 1]
elif dispatch == 'jump': elif dispatch == 'jump':
log.debug('jump, data=%s' % data) log.debug('jump, data=%s' % data)
self.choice = int(data['choice']) self.choice = choices[int(data['choice'])]
num_choices = len(self.descriptor.get_children()) #num_choices = len(choices)
if self.choice >= num_choices: #if self.choice >= num_choices:
self.choice = 0 #self.choice = 0
result = {'ret': "choice=%s" % self.choice} result = {'ret': "choice=%s" % self.choice}
return json.dumps(result) return json.dumps(result)
......
...@@ -75,11 +75,14 @@ class TreeNodeSet(list): ...@@ -75,11 +75,14 @@ class TreeNodeSet(list):
msg = ("Resetting all <problem> and <randomize> in tree of parent " msg = ("Resetting all <problem> and <randomize> in tree of parent "
"module %s\n" % self.parent_module) "module %s\n" % self.parent_module)
for module in self.rset + self.pset: for module in self.rset + self.pset:
old_state = (module.smstate.state if module.smstate is not None old_state = json.loads(module.smstate.state if module.smstate is
else {}) not None else '{}')
msg += " Resetting %s, old state=%s\n" % (module, old_state) msg += " Resetting %s, old state=%s\n" % (module, old_state)
new_state = {}
if 'history' in old_state:
new_state['history'] = old_state['history']
if module.smstate is not None: if module.smstate is not None:
module.smstate.state = '{}' module.smstate.state = json.dumps(new_state)
module.smstate.grade = None module.smstate.grade = None
module.smstate.save() module.smstate.save()
return msg return msg
...@@ -211,13 +214,13 @@ class ProctorModuleInfo(object): ...@@ -211,13 +214,13 @@ class ProctorModuleInfo(object):
# started a problem base this on the "choice" from the randmize # started a problem base this on the "choice" from the randmize
# module state # module state
try: try:
sm.choice = int(json.loads(sm.state)['choice']) sm.choice = json.loads(sm.state)['choice']
except Exception: except Exception:
sm.choice = None sm.choice = None
if sm.choice is not None: if sm.choice is not None:
try: try:
sm.problem = self.ms.get_instance( sm.problem = self.ms.get_instance(
self.course.id, rpmod.ra_rand.children[sm.choice]) self.course.id, sm.choice)
sm.problem_name = sm.problem.display_name sm.problem_name = sm.problem.display_name
except Exception: except Exception:
log.exception("Failed to get rand child " log.exception("Failed to get rand child "
...@@ -308,8 +311,9 @@ class ProctorModuleInfo(object): ...@@ -308,8 +311,9 @@ class ProctorModuleInfo(object):
def get_assignments_attempted_and_failed(self, student, do_reset=False): def get_assignments_attempted_and_failed(self, student, do_reset=False):
status = self.get_student_status(student) status = self.get_student_status(student)
failed = [self._get_od_for_assignment(a) for a in status['assignments'] failed = [self._get_od_for_assignment(student, a)
if a['attempted'] and a['earned'] != a['possible']] for a in status['assignments'] if a['attempted'] and
a['earned'] != a['possible']]
for f in failed: for f in failed:
log.info( log.info(
"Student %s Assignment %s attempted '%s' but failed " "Student %s Assignment %s attempted '%s' but failed "
...@@ -317,8 +321,8 @@ class ProctorModuleInfo(object): ...@@ -317,8 +321,8 @@ class ProctorModuleInfo(object):
f['earned'], f['possible'])) f['earned'], f['possible']))
if do_reset: if do_reset:
try: try:
log.debug('resetting %s for student %s' % log.info('resetting %s for student %s' %
(f['assignment'], f['username'])) (f['assignment'], f['username']))
cloc = self.course.location cloc = self.course.location
assi_url = Location(cloc.tag, cloc.org, assi_url = Location(cloc.tag, cloc.org,
cloc.course, 'proctor', cloc.course, 'proctor',
......
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