Skip to content

Commit f37d0f1

Browse files
committed
Fix a bug with quiz hints when using :randomized: option
Fix pick-any with :randomized: option. Only show !-hints for options that were included in the random sample, and emit hints in the order the options were displayed to the student. Fixes #160
1 parent 3aff549 commit f37d0f1

1 file changed

Lines changed: 74 additions & 24 deletions

File tree

access/types/forms.py

Lines changed: 74 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -675,7 +675,43 @@ def grade_field(self, i, configuration):
675675
methods = method.split("-")
676676
mods = methods[1:]
677677

678-
for fb in configuration.get("feedback", []):
678+
# For randomized checkbox questions, only show 'not'-type hints for
679+
# options that were included in the random sample (i.e., shown to the
680+
# student). Options outside the sample were never displayed, so the
681+
# student could not have selected them; triggering their '!' hints would
682+
# reveal information about options the student never saw.
683+
# sampled_option_values_ordered preserves display order so that hints
684+
# can be shown in the same order the options appeared on screen.
685+
sampled_option_values = None
686+
sampled_option_values_ordered = None
687+
if t == "checkbox":
688+
field = self.fields.get(name)
689+
random_sample_str = getattr(field, 'random_sample', '') if field else ''
690+
if random_sample_str:
691+
sample_indices_ordered = [int(x) for x in random_sample_str.split('-')]
692+
all_options = configuration.get("options", [])
693+
sampled_option_values_ordered = [
694+
self.option_name(idx, all_options[idx])
695+
for idx in sample_indices_ordered
696+
if idx < len(all_options)
697+
]
698+
sampled_option_values = set(sampled_option_values_ordered)
699+
700+
# For randomized checkbox questions, process feedback entries in display
701+
# order so that hints appear in the same sequence the options were shown.
702+
feedback_list = configuration.get("feedback", [])
703+
if t == "checkbox" and sampled_option_values_ordered:
704+
option_display_pos = {
705+
val: idx for idx, val in enumerate(sampled_option_values_ordered)
706+
}
707+
feedback_list = sorted(
708+
feedback_list,
709+
key=lambda fb: option_display_pos.get(
710+
fb.get('value', ''), len(sampled_option_values_ordered)
711+
),
712+
)
713+
714+
for fb in feedback_list:
679715
new_hint = fb.get('label', None)
680716
comparison = fb.get('value', '')
681717
if not new_hint:
@@ -700,6 +736,15 @@ def grade_field(self, i, configuration):
700736
r = self.compare_values(methods_used, value, comparison, **float_tolerances)
701737
add = not r if fb.get('not', False) else r
702738

739+
# For randomized checkbox questions, suppress 'not'-type hints for
740+
# options that were not included in the random sample. Those options
741+
# were never shown to the student, so they can't be expected to have
742+
# selected them.
743+
if (t == "checkbox" and fb.get('not', False)
744+
and sampled_option_values is not None
745+
and comparison not in sampled_option_values):
746+
add = False
747+
703748
# Checkbox-hints should be linkable with their options:
704749
if t == "checkbox" and checkbox_feedback and add:
705750
if fb.get('not'):
@@ -791,29 +836,34 @@ def grade_checkbox(self, configuration, value, hints=None, name=''):
791836
# If no correct answers are set in configuration, points are granted to
792837
# an empty answer only.
793838
correct = True
794-
i = 0
795-
for opt in configuration.get("options", []):
796-
if not is_randomized or i in sample:
797-
name = self.option_name(i, opt)
798-
correct_answer = opt.get("correct", False)
799-
# correct_answer may be boolean or string "neutral"
800-
if correct_answer is True:
801-
correct_count += 1
802-
non_neutral_count += 1
803-
if name not in value:
804-
wrong_answers += 1
805-
correct = False
806-
self.append_hint_checkbox(hints, opt, checkbox_feedback, False)
807-
elif correct_answer is False:
808-
non_neutral_count += 1
809-
if value is not None and name in value:
810-
correct = False
811-
wrong_answers += 1
812-
self.append_hint_checkbox(hints, opt, checkbox_feedback, True)
813-
elif correct_answer == "neutral":
814-
if value is not None and name in value:
815-
self.append_hint_checkbox(hints, opt, checkbox_feedback, True)
816-
i += 1
839+
options = configuration.get("options", [])
840+
# When randomized, iterate in display order (the order the student saw
841+
# the options) so that hints appear in the same order as the options.
842+
options_iter = (
843+
((idx, options[idx]) for idx in sample)
844+
if is_randomized
845+
else enumerate(options)
846+
)
847+
for i, opt in options_iter:
848+
name = self.option_name(i, opt)
849+
correct_answer = opt.get("correct", False)
850+
# correct_answer may be boolean or string "neutral"
851+
if correct_answer is True:
852+
correct_count += 1
853+
non_neutral_count += 1
854+
if name not in value:
855+
wrong_answers += 1
856+
correct = False
857+
self.append_hint_checkbox(hints, opt, checkbox_feedback, False)
858+
elif correct_answer is False:
859+
non_neutral_count += 1
860+
if value is not None and name in value:
861+
correct = False
862+
wrong_answers += 1
863+
self.append_hint_checkbox(hints, opt, checkbox_feedback, True)
864+
elif correct_answer == "neutral":
865+
if value is not None and name in value:
866+
self.append_hint_checkbox(hints, opt, checkbox_feedback, True)
817867

818868
# If partial_points are set, the variable 'correct' becomes a float
819869
# less than one instead of a boolean. It will be used to multiply

0 commit comments

Comments
 (0)