218 lines
8.0 KiB
Python
218 lines
8.0 KiB
Python
"""
|
|
CorrectMap: A utility class to store and manage graded responses to CAPA questions.
|
|
Provides methods to track correctness, points, messages, hints, and queue state.
|
|
"""
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# class used to store graded responses to CAPA questions
|
|
#
|
|
# Used by responsetypes and capa_problem
|
|
|
|
|
|
class CorrectMap:
|
|
"""
|
|
Stores map between answer_id and response evaluation result for each question
|
|
in a capa problem. The response evaluation result for each answer_id includes
|
|
(correctness, npoints, msg, hint, hintmode).
|
|
|
|
- correctness : 'correct', 'incorrect', 'partially-correct', or 'incomplete'
|
|
- npoints : None, or integer specifying number of points awarded for this answer_id
|
|
- msg : string (may have HTML) giving extra message response
|
|
(displayed below textline or textbox)
|
|
- hint : string (may have HTML) giving optional hint
|
|
(displayed below textline or textbox, above msg)
|
|
- hintmode : one of (None,'on_request','always') criteria for displaying hint
|
|
- queuestate : Dict {key:'', time:''} where key is a secret string, and time is a string dump
|
|
of a DateTime object in the format '%Y%m%d%H%M%S'. Is None when not queued
|
|
|
|
Behaves as a dict.
|
|
"""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
# start with empty dict
|
|
self.cmap = {}
|
|
self.items = self.cmap.items
|
|
self.keys = self.cmap.keys
|
|
self.overall_message = ""
|
|
self.set(*args, **kwargs)
|
|
|
|
def __getitem__(self, *args, **kwargs):
|
|
return self.cmap.__getitem__(*args, **kwargs)
|
|
|
|
def __iter__(self):
|
|
return self.cmap.__iter__()
|
|
|
|
# See the documentation for 'set_dict' for the use of kwargs
|
|
def set( # pylint: disable=too-many-positional-arguments,too-many-arguments
|
|
self,
|
|
answer_id=None,
|
|
correctness=None,
|
|
npoints=None,
|
|
msg="",
|
|
hint="",
|
|
hintmode=None,
|
|
queuestate=None,
|
|
answervariable=None,
|
|
**kwargs, # pylint: disable=unused-argument
|
|
):
|
|
"""
|
|
Set or update the stored evaluation result for a given answer_id.
|
|
Unused kwargs are ignored for compatibility with older formats.
|
|
"""
|
|
|
|
if answer_id is not None:
|
|
self.cmap[answer_id] = {
|
|
"correctness": correctness,
|
|
"npoints": npoints,
|
|
"msg": msg,
|
|
"hint": hint,
|
|
"hintmode": hintmode,
|
|
"queuestate": queuestate,
|
|
"answervariable": answervariable,
|
|
}
|
|
|
|
def __repr__(self):
|
|
return repr(self.cmap)
|
|
|
|
def get_dict(self):
|
|
"""
|
|
return dict version of self
|
|
"""
|
|
return self.cmap
|
|
|
|
def set_dict(self, correct_map):
|
|
"""
|
|
Set internal dict of CorrectMap to provided correct_map dict
|
|
|
|
correct_map is saved by LMS as a plaintext JSON dump of the correctmap dict. This
|
|
means that when the definition of CorrectMap (e.g. its properties) are altered,
|
|
an existing correct_map dict will not coincide with the newest CorrectMap format as
|
|
defined by self.set.
|
|
|
|
For graceful migration, feed the contents of each correct map to self.set, rather than
|
|
making a direct copy of the given correct_map dict. This way, the common keys between
|
|
the incoming correct_map dict and the new CorrectMap instance will be written, while
|
|
mismatched keys will be gracefully ignored.
|
|
|
|
Special migration case:
|
|
If correct_map is a one-level dict, then convert it to the new dict of dicts format.
|
|
|
|
"""
|
|
# empty current dict
|
|
self.__init__() # pylint: disable=unnecessary-dunder-call
|
|
|
|
if not correct_map:
|
|
return
|
|
|
|
# create new dict entries
|
|
if not isinstance(list(correct_map.values())[0], dict):
|
|
# special migration
|
|
for k in correct_map:
|
|
self.set(k, correctness=correct_map[k])
|
|
else:
|
|
for k in correct_map:
|
|
self.set(k, **correct_map[k])
|
|
|
|
def is_correct(self, answer_id):
|
|
"""
|
|
Takes an answer_id
|
|
Returns true if the problem is correct OR partially correct.
|
|
"""
|
|
if answer_id in self.cmap:
|
|
return self.cmap[answer_id]["correctness"] in ["correct", "partially-correct"]
|
|
return None
|
|
|
|
def is_partially_correct(self, answer_id):
|
|
"""
|
|
Takes an answer_id
|
|
Returns true if the problem is partially correct.
|
|
"""
|
|
if answer_id in self.cmap:
|
|
return self.cmap[answer_id]["correctness"] == "partially-correct"
|
|
return None
|
|
|
|
def is_queued(self, answer_id):
|
|
"""Return True if the answer has a non-empty queue state."""
|
|
return answer_id in self.cmap and self.cmap[answer_id]["queuestate"] is not None
|
|
|
|
def is_right_queuekey(self, answer_id, test_key):
|
|
"""Return True if the queued answer matches the provided queue key."""
|
|
return self.is_queued(answer_id) and self.cmap[answer_id]["queuestate"]["key"] == test_key
|
|
|
|
def get_queuetime_str(self, answer_id):
|
|
"""Return the stored queue timestamp string for the given answer."""
|
|
if self.cmap[answer_id]["queuestate"]:
|
|
return self.cmap[answer_id]["queuestate"]["time"]
|
|
|
|
return None
|
|
|
|
def get_npoints(self, answer_id):
|
|
"""Return the number of points for an answer, used for partial credit."""
|
|
npoints = self.get_property(answer_id, "npoints")
|
|
if npoints is not None:
|
|
return npoints
|
|
|
|
if self.is_correct(answer_id):
|
|
return 1
|
|
|
|
# if not correct and no points have been assigned, return 0
|
|
return 0
|
|
|
|
def set_property(self, answer_id, prop, value):
|
|
"""Set a specific property value for the given answer_id."""
|
|
if answer_id in self.cmap:
|
|
self.cmap[answer_id][prop] = value
|
|
else:
|
|
self.cmap[answer_id] = {prop: value}
|
|
|
|
def get_property(self, answer_id, prop, default=None):
|
|
"""Return the specified property for an answer, or a default value."""
|
|
if answer_id in self.cmap:
|
|
return self.cmap[answer_id].get(prop, default)
|
|
return default
|
|
|
|
def get_correctness(self, answer_id):
|
|
"""Return the correctness value for the given answer."""
|
|
return self.get_property(answer_id, "correctness")
|
|
|
|
def get_msg(self, answer_id):
|
|
"""Return the feedback message for the given answer."""
|
|
return self.get_property(answer_id, "msg", "")
|
|
|
|
def get_hint(self, answer_id):
|
|
"""Return the hint text associated with the given answer."""
|
|
return self.get_property(answer_id, "hint", "")
|
|
|
|
def get_hintmode(self, answer_id):
|
|
"""Return the hint display mode for the given answer."""
|
|
return self.get_property(answer_id, "hintmode", None)
|
|
|
|
def set_hint_and_mode(self, answer_id, hint, hintmode):
|
|
"""
|
|
- hint : (string) HTML text for hint
|
|
- hintmode : (string) mode for hint display ('always' or 'on_request')
|
|
"""
|
|
self.set_property(answer_id, "hint", hint)
|
|
self.set_property(answer_id, "hintmode", hintmode)
|
|
|
|
def update(self, other_cmap):
|
|
"""
|
|
Update this CorrectMap with the contents of another CorrectMap
|
|
"""
|
|
if not isinstance(other_cmap, CorrectMap):
|
|
raise Exception( # pylint: disable=broad-exception-raised
|
|
f"CorrectMap.update called with invalid argument {other_cmap}"
|
|
)
|
|
self.cmap.update(other_cmap.get_dict())
|
|
self.set_overall_message(other_cmap.get_overall_message())
|
|
|
|
def set_overall_message(self, message_str):
|
|
"""Set a message that applies to the question as a whole,
|
|
rather than to individual inputs."""
|
|
self.overall_message = str(message_str) if message_str else ""
|
|
|
|
def get_overall_message(self):
|
|
"""Retrieve a message that applies to the question as a whole.
|
|
If no message is available, returns the empty string"""
|
|
return self.overall_message
|