@@ -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