from otree.api import * def timer(player, seconds): """Return seconds if timers are enabled, else None.""" return seconds if player.session.config.get('enable_timers', False) else None class C(BaseConstants): NAME_IN_URL = 'triangle_demo' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): attempts = models.IntegerField(initial=0) quiz_action = models.StringField(blank=True) quiz_passed = models.BooleanField(initial=False) class Demo(Page): form_model = 'player' form_fields = ['quiz_action'] @staticmethod def vars_for_template(player: Player): return dict(attempts_left=max(0, 2 - player.attempts)) @staticmethod def error_message(player: Player, values): """ Hub pattern: block page advance on wrong answers (< 2 tries) by returning an error string. On the 2nd wrong attempt, set dropout and allow submit to proceed (no error string). """ action = (values.get('quiz_action') or '').strip() if action == 'wrong': player.attempts += 1 if player.attempts >= 2: player.participant.dropout = True return # no message => submission proceeds; app_after_this_page will redirect return 'You did not choose the correct answer. Please try again.' elif action == 'correct': # allow submit; before_next_page marks passed return @staticmethod def get_timeout_seconds(player): return timer(player, 300) @staticmethod def before_next_page(player: Player, timeout_happened): # finalize state based on the last action if player.quiz_action == 'correct': player.quiz_passed = True # clear for next render player.quiz_action = '' import time player.participant.wait_page_arrival = time.time() if timeout_happened: player.participant.dropout = True @staticmethod def app_after_this_page(player: Player, timeout_happened): # follow your other quiz: on dropout, jump to End_Page if getattr(player.participant, 'dropout', False): return 'End_Page' page_sequence = [Demo]