Files
edx-platform/xmodule/capa/correctmap.py
2026-01-07 16:39:11 +05:00

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