# pages.py from otree.api import Page, WaitPage # modern import path import time, json, random from .models import Constants from django.utils.translation import gettext as _ # ugettext → gettext # --------------------------------------------------------------------- # Helper – call this from every page that needs the common context # --------------------------------------------------------------------- def shared_vars(page): return { 'total_letters': Constants.total_letters, 'd_total': Constants.d_total, 'b_total': Constants.b_total, 'num_rounds': Constants.num_rounds, 'fixed_payoff': Constants.fixed_payoff, 'bonus_payoff': Constants.bonus_payoff, } # --------------------------------------------------------------------- # Utility – determines when the whole task is finished # --------------------------------------------------------------------- def should_end_task(player): return player.participant.vars.get('task_done', False) # --------------------------------------------------------------------- # Base class: any page **without its own `vars_for_template`** inherits this # --------------------------------------------------------------------- class BasePage(Page): def vars_for_template(self): return shared_vars(self) # --------------------------------------------------------------------- # Pages # --------------------------------------------------------------------- class Welcome(BasePage): def is_displayed(self): return self.round_number == 1 and not should_end_task(self.player) class Consent(BasePage): form_model = 'player' form_fields = ['consent', 'submit_noConsent'] def is_displayed(self): return self.round_number == 1 and not should_end_task(self.player) def before_next_page(self): self.participant.vars['consent'] = self.player.consent class Demographics(BasePage): form_model = 'player' form_fields = ['gender', 'gender_others', 'age', 'birth_month'] def is_displayed(self): return self.round_number == 1 and not should_end_task(self.player) class Instructions(BasePage): def is_displayed(self): return self.round_number == 1 and not should_end_task(self.player) def before_next_page(self): # start the global timer self.participant.vars['expiry_timestamp'] = time.time() + self.player.task_timer # ------------------------------------------------------------------- # TaskTrial –– oTree Lite-compatible (Starlette, no request.POST) # ------------------------------------------------------------------- class TaskTrial(BasePage): form_model = 'player' # keep using Player form_fields = [] # will be supplied dynamically timer_text = 'Time left to complete as many matrices as possible:' # Tell oTree which 100 checkbox fields exist for this round def get_form_fields(self): data = json.loads(self.player.letter_choices_data) return data['fields'] # e.g. ['choice_1', ..., 'choice_100'] def is_displayed(self): still_time = self.participant.vars['expiry_timestamp'] - time.time() > 0 return still_time and not should_end_task(self.player) def vars_for_template(self): data = json.loads(self.player.letter_choices_data) combined = list( zip( range(1, Constants.total_letters + 1), data['fields'], data['letters'], ) ) return { 'choices': combined, 'round_number': self.subsession.round_number, **shared_vars(self), } def get_timeout_seconds(self): return self.participant.vars['expiry_timestamp'] - time.time() def before_next_page(self): data = json.loads(self.player.letter_choices_data) # ✨ READ ANSWERS FROM PLAYER ATTRIBUTES, NOT request.POST ✨ responses = [getattr(self.player, field) for field in data['fields']] data['responses'] = responses self.player.letter_choices_data = json.dumps(data) self.player.count_effort() self.player.check_correctness() if self.round_number == 1: self.player.total_correct = self.player.correct else: prev_total = self.player.in_round(self.round_number - 1).total_correct self.player.total_correct = prev_total + self.player.correct class ResultsTrial(BasePage): def is_displayed(self): expired = self.participant.vars['expiry_timestamp'] - time.time() <= 0 not_shown_yet = not self.participant.vars.get('summary_shown', False) return expired and not_shown_yet def before_next_page(self): # mark experiment finished for this participant self.player.endgame = 1 #self.player.save() self.participant.vars['task_done'] = True self.participant.vars['summary_shown'] = True # avoid duplicates def vars_for_template(self): table_rows = [ { 'round_number': p.round_number, 'd_crossed': round(p.d_crossed), 'b_crossed': round(p.b_crossed), 'is_correct': round(p.is_correct), } for p in self.player.in_all_rounds() ] self.participant.vars['t1_results'] = table_rows return { 'table_rows': table_rows, 'total_payoff': self.player.total_correct, **shared_vars(self), } class RoundKiller(Page): # ← was WaitPage def is_displayed(self): return True # still runs for every player def before_next_page(self): self.player.update_endgame_second() # --------------------------------------------------------------------- # Page sequence # --------------------------------------------------------------------- page_sequence = [ Welcome, Consent, Demographics, Instructions, TaskTrial, ResultsTrial, # 🧹 auto-skips the rest of the rounds ]