from otree.api import Currency as c, currency_range from otree.models import subsession from ._builtin import Page, WaitPage from .models import Constants from datetime import datetime, timedelta # ============== helpers ============== def show_Lot1(page: Page): """是否显示 TA_Lot1。Risk 正常流程会写入 path;单测时 models 里也有兜底。""" return page.participant.vars.get('path', 'Lot1→CE') == 'Lot1→CE' def order_is_48(page: Page): return page.participant.vars.get('order', '48') == '48' #def weeks_now(page: Page): # """根据 48/84 与当前段(1..18 / 19..36)返回 4 或 8。""" # # ✅ 如果我们明确已经进入 8w 阶段 # if page.participant.vars.get('force_8w', False): # return 8 # in_first_segment = page.round_number <= 18 # if order_is_48(page): # return 4 if in_first_segment else 8 # else: # return 8 if in_first_segment else 4 def weeks_now(page: Page): seq = page.participant.vars.get('discount_seq') order48 = (seq in (1, 2)) # seq=1/2 -> 48, seq=3/4 -> 84 in_first_segment = page.round_number <= 18 if order48: return 4 if in_first_segment else 8 else: return 8 if in_first_segment else 4 def is_before_8w(page: Page): """在第18轮之后、19轮正式开始前,用于显示 8w 的 Instructions 与 Quiz""" # 仅在 round 18 时显示一次(4w 刚结束) return page.round_number == 18 def round_index_1_to_6(page: Page): """将一段内 18 轮映射为 1..6(三次)。""" r = page.round_number if page.round_number <= 18 else (page.round_number - 18) if r < 7: return r elif r < 13: return r - 6 else: return r - 12 def block_last_round(page: Page): """是否是每 6 轮的最后一轮。""" return page.round_number % 6 == 0 # ===================================== # ---------- Debug dict ---------- def dbg(page: Page): pv = page.participant.vars return dict( dbg_round=page.round_number, dbg_page=pv.get('page'), dbg_node=pv.get('node'), dbg_weeks_now=weeks_now(page), dbg_force_8w=pv.get('force_8w'), dbg_ins8w_shown=pv.get('ins8w_shown'), dbg_quiz8w_shown=pv.get('quiz8w_shown'), dbg_mid8w_shown=pv.get('mid8w_shown'), dbg_path=pv.get('path'), dbg_order=pv.get('order'), dbg_discount_seq=pv.get('discount_seq'), ) class DebugPage(Page): def vars_for_template(self): # 子类如果也写了 vars_for_template,就用 super() 合并 return dbg(self) # ---------- Instructions ---------- class Instructions(DebugPage): template_name = 'Time_Adaptive_Lot_1/Instructions.html' def is_displayed(self): return show_Lot1(self) and self.round_number == 1 and weeks_now(self) == 4 def vars_for_template(self): w = 4 date_str = (datetime.today() + timedelta(weeks=w)).strftime('%A, %B %d, %Y') ctx = super().vars_for_template() ctx.update(dict(date_28=date_str, weeks=w)) return ctx class Instructions_8w(DebugPage): template_name = 'Time_Adaptive_Lot_1/Instructions_8w.html' def is_displayed(self): shown = self.participant.vars.get('ins8w_shown', False) return show_Lot1(self) and self.round_number == 19 and not shown and weeks_now(self) == 8 def vars_for_template(self): w = 8 date_str = (datetime.today() + timedelta(weeks=w)).strftime('%A, %B %d, %Y') ctx = super().vars_for_template() ctx.update(dict(date_56=date_str, weeks=w)) return ctx def before_next_page(self): self.participant.vars['ins8w_shown'] = True # ---------- Quiz ---------- class Time_Lot_Quiz(DebugPage): template_name = 'Time_Adaptive_Lot_1/Time_Lot_Quiz.html' def is_displayed(self): return show_Lot1(self) and self.round_number == 1 and weeks_now(self) == 4 form_model = 'player' form_fields = ['q1a', 'q1b', 'q2'] ans1a = c(6.2) ans1b = 3 ans2 = 2 def vars_for_template(self): ctx = super().vars_for_template() ctx.update(dict(weeks=4)) return ctx def error_message(self, values): if self.player.incorrect_attempts is None: self.player.incorrect_attempts = 0 if values['q1a'] != self.ans1a or values['q1b'] != self.ans1b or values['q2'] != self.ans2: self.player.incorrect_attempts += 1 if values['q1a'] != self.ans1a: return 'The amount you entered in question 1a is incorrect!' if values['q1b'] != self.ans1b: return 'Your response to question 1b is incorrect!' if values['q2'] != self.ans2: return 'Your response to question 2 is incorrect!' class Time_Lot_Quiz_8w(DebugPage): template_name = 'Time_Adaptive_Lot_1/Time_Lot_Quiz_8w.html' def is_displayed(self): shown = self.participant.vars.get('quiz8w_shown', False) return show_Lot1(self) and self.round_number == 19 and not shown and weeks_now(self) == 8 form_model = 'player' form_fields = ['q1a_8w', 'q1b_8w', 'q2_8w'] ans1a_8w = c(12.2) ans1b_8w = 3 ans2_8w = 3 def vars_for_template(self): ctx = super().vars_for_template() ctx.update(dict(weeks=8)) return ctx def error_message(self, values): if self.player.incorrect_attempts is None: self.player.incorrect_attempts = 0 if values['q1a_8w'] != self.ans1a_8w or values['q1b_8w'] != self.ans1b_8w or values['q2_8w'] != self.ans2_8w: self.player.incorrect_attempts += 1 if values['q1a_8w'] != self.ans1a_8w: return 'The amount you entered in question 1a is incorrect!' if values['q1b_8w'] != self.ans1b_8w: return 'Your response to question 1b is incorrect!' if values['q2_8w'] != self.ans2_8w: return 'Your response to question 2 is incorrect!' def before_next_page(self): self.participant.vars['quiz8w_shown'] = True # ---------- Discount_Lot1 ---------- class Discount_Lot_1(DebugPage): template_name = 'Time_Adaptive_Lot_1/Discount_Lot_1.html' def is_displayed(self): return show_Lot1(self) and weeks_now(self) == 4 and (self.participant.vars['page'] == 1) form_model = 'player' form_fields = ['choice'] def vars_for_template(self): ctx = super().vars_for_template() if self.round_number in (1, 7, 13, 19, 25, 31): self.participant.vars['node'] = 1 round_index = round_index_1_to_6(self) choice_index = 6 if block_last_round(self) else self.participant.vars['node'] self.participant.vars['early_high'] = Constants.lot1_early['high'] self.participant.vars['early_low'] = Constants.lot1_early['low'] self.participant.vars['late_high'] = eval("Constants.lot1_late_" + str(round_index))['high_' + str(choice_index)] self.participant.vars['late_low'] = eval("Constants.lot1_late_" + str(round_index))['low_' + str(choice_index)] self.player.segment_index = 1 if self.round_number <= 18 else 2 self.player.current_week = 4 ctx.update({ 'lot_high_early': c(Constants.lot1_early['high']), 'lot_low_early': c(Constants.lot1_early['low']), 'lot_high_late': c(eval("Constants.lot1_late_" + str(round_index))['high_' + str(choice_index)]), 'lot_low_late': c(eval("Constants.lot1_late_" + str(round_index))['low_' + str(choice_index)]), 'lot_node': self.participant.vars['node'], 'weeks': 4, }) return ctx def before_next_page(self): # print("BEFORE Lot1 before_next_page", "round", self.round_number, "page", self.participant.vars.get('page')) self.player.set_payment_choice() self.player.set_node() self.player.set_options() #self.player.set_page() # print("AFTER Lot1 before_next_page", "round", self.round_number, "page", self.participant.vars.get('page')) class Discount_Lot_1_8w(Discount_Lot_1): template_name = 'Time_Adaptive_Lot_1/Discount_Lot_1_8w.html' def is_displayed(self): return show_Lot1(self) and weeks_now(self) == 8 and (self.participant.vars['page'] == 1) def vars_for_template(self): ctx = super().vars_for_template() self.player.current_week = 8 ctx['weeks'] = 8 return ctx # ---------- Discount_Lot2 ---------- class Discount_Lot_2(DebugPage): template_name = 'Time_Adaptive_Lot_1/Discount_Lot_2.html' def is_displayed(self): # print("CHECK Lot2", "round", self.round_number, "page", self.participant.vars.get('page'), "weeks", # weeks_now(self)) return show_Lot1(self) and weeks_now(self) == 4 and (self.participant.vars['page'] == 2) form_model = 'player' form_fields = ['choice'] def vars_for_template(self): ctx = super().vars_for_template() if self.round_number in (1, 7, 13, 19, 25, 31): self.participant.vars['node'] = 1 round_index = round_index_1_to_6(self) if block_last_round(self): choice_index = self.player.participant.vars['replica'] round_index = 3 else: choice_index = self.player.participant.vars['node'] if round_index == 3: self.player.participant.vars['replica'] = choice_index self.participant.vars['early_high'] = Constants.lot2_early['high'] self.participant.vars['early_low'] = Constants.lot2_early['low'] self.participant.vars['late_high'] = eval("Constants.lot2_late_" + str(round_index))['high_' + str(choice_index)] self.participant.vars['late_low'] = eval("Constants.lot2_late_" + str(round_index))['low_' + str(choice_index)] self.player.segment_index = 1 if self.round_number <= 18 else 2 self.player.current_week = 4 ctx.update({ 'lot_high_early': c(Constants.lot2_early['high']), 'lot_low_early': c(Constants.lot2_early['low']), 'lot_high_late': c(eval("Constants.lot2_late_" + str(round_index))['high_' + str(choice_index)]), 'lot_low_late': c(eval("Constants.lot2_late_" + str(round_index))['low_' + str(choice_index)]), 'lot_node': self.participant.vars['node'], 'weeks': 4, }) return ctx def before_next_page(self): # print("BEFORE Lot2 before_next_page", "round", self.round_number, "page", self.participant.vars.get('page')) self.player.set_payment_choice() self.player.set_node() self.player.set_options() #self.player.set_page() # print("AFTER Lot2 before_next_page", "round", self.round_number, "page", self.participant.vars.get('page')) class Discount_Lot_2_8w(Discount_Lot_2): template_name = 'Time_Adaptive_Lot_1/Discount_Lot_2_8w.html' def is_displayed(self): return show_Lot1(self) and weeks_now(self) == 8 and (self.participant.vars['page'] == 2) def vars_for_template(self): ctx = super().vars_for_template() self.player.current_week = 8 ctx['weeks'] = 8 return ctx # ---------- Discount_Lot3 ---------- class Discount_Lot_3(DebugPage): template_name = 'Time_Adaptive_Lot_1/Discount_Lot_3.html' def is_displayed(self): #print("CHECK Lot3", "round", self.round_number, "page", self.participant.vars.get('page'), "weeks", # weeks_now(self)) return show_Lot1(self) and weeks_now(self) == 4 and (self.participant.vars['page'] == 3) form_model = 'player' form_fields = ['choice'] def vars_for_template(self): ctx = super().vars_for_template() if self.round_number in (1, 7, 13, 19, 25, 31): self.participant.vars['node'] = 1 round_index = round_index_1_to_6(self) if block_last_round(self): choice_index = self.player.participant.vars['replica'] round_index = 2 else: choice_index = self.player.participant.vars['node'] if round_index == 2: self.player.participant.vars['replica'] = choice_index self.participant.vars['early_high'] = Constants.lot3_early['high'] self.participant.vars['early_low'] = Constants.lot3_early['low'] self.participant.vars['late_high'] = eval("Constants.lot3_late_" + str(round_index))['high_' + str(choice_index)] self.participant.vars['late_low'] = eval("Constants.lot3_late_" + str(round_index))['low_' + str(choice_index)] self.player.segment_index = 1 if self.round_number <= 18 else 2 self.player.current_week = 4 ctx.update({ 'lot_high_early': c(Constants.lot3_early['high']), 'lot_low_early': c(Constants.lot3_early['low']), 'lot_high_late': c(eval("Constants.lot3_late_" + str(round_index))['high_' + str(choice_index)]), 'lot_low_late': c(eval("Constants.lot3_late_" + str(round_index))['low_' + str(choice_index)]), 'lot_node': self.participant.vars['node'], 'weeks': 4, }) return ctx def before_next_page(self): #print("BEFORE Lot3 before_next_page", "round", self.round_number, "page", self.participant.vars.get('page')) self.player.set_payment_choice() self.player.set_node() self.player.set_options() #self.player.set_page() #print("AFTER Lot3 before_next_page", "round", self.round_number, "page", self.participant.vars.get('page')) class Discount_Lot_3_8w(Discount_Lot_3): template_name = 'Time_Adaptive_Lot_1/Discount_Lot_3_8w.html' def is_displayed(self): return show_Lot1(self) and weeks_now(self) == 8 and (self.participant.vars['page'] == 3) def vars_for_template(self): ctx = super().vars_for_template() self.player.current_week = 8 ctx['weeks'] = 8 return ctx # ---------- Middle screen ---------- class Middle_Screen(DebugPage): template_name = 'Time_Adaptive_Lot_1/Middle_Screen.html' def is_displayed(self): return show_Lot1(self) and weeks_now(self) == 4 and (self.round_number in (1, 7, 13)) def vars_for_template(self): ctx = super().vars_for_template() ctx.update(dict(round_number=self.round_number, weeks=4)) return ctx def before_next_page(self): #print("MIDDLE BEFORE set_page", "round", self.round_number, "page", self.participant.vars.get('page')) self.player.set_page() #print("MIDDLE AFTER set_page", "round", self.round_number, "page", self.participant.vars.get('page')) class Middle_Screen_8w(Page): template_name = 'Time_Adaptive_Lot_1/Middle_Screen_8w.html' def is_displayed(self): # 8w 的三个 block 起点:19, 25, 31 return show_Lot1(self) and weeks_now(self) == 8 and (self.round_number in (19, 25, 31)) def vars_for_template(self): ctx = super().vars_for_template() ctx.update(dict(round_number=self.round_number, weeks=8)) return ctx def before_next_page(self): self.player.set_page() # ---------- Last page ---------- class Last_Page(DebugPage): template_name = 'Time_Adaptive_Lot_1/Last_Page.html' # 4w 段结束:只在第 18 轮 def is_displayed(self): return show_Lot1(self) and (self.round_number == 18) and (weeks_now(self) == 4) def vars_for_template(self): ctx = super().vars_for_template() ctx.update(dict(round_number=self.round_number, lot_node=self.participant.vars.get('node'), weeks=4)) return ctx def before_next_page(self): self.participant.vars['force_8w'] = True # ✅ 防止 8w choices 在 round 19 抢跑 self.participant.vars['page'] = 0 # ✅ 可选:重置“只显示一次”的标记,避免你测试多次后被挡住 self.participant.vars.pop('ins8w_shown', None) self.participant.vars.pop('quiz8w_shown', None) self.participant.vars.pop('mid8w_shown', None) class Last_Page_8w(DebugPage): template_name = 'Time_Adaptive_Lot_1/Last_Page_8w.html' # 8w 段结束:第 36 轮 def is_displayed(self): return show_Lot1(self) and (self.round_number == 36) and (weeks_now(self) == 8) def vars_for_template(self): ctx = super().vars_for_template() ctx.update(dict(round_number=self.round_number, lot_node=self.participant.vars.get('node'), weeks=8)) return ctx def app_after_this_page(self, upcoming_apps): seq = self.participant.vars.get('discount_seq') second_app = { 1: 'Time_Adaptive_CE', # Lot1_48 -> CE_48 2: 'Time_Adaptive_Lot_2', # CE_48 -> Lot2_48 3: 'Time_Adaptive_CE_84', # Lot1_84 -> CE_84 4: 'Time_Adaptive_Lot_2_84', # CE_84 -> Lot2_84 }[seq] return second_app page_sequence = [ # 4w 开场 Instructions, Time_Lot_Quiz, Middle_Screen, # 4w 的选择页 Discount_Lot_1, Discount_Lot_2, Discount_Lot_3, # —— 4w 结束页(第 18 轮) —— 必须在切换到 8w 的指引页之前 Last_Page, # 8w 开场(仍在第 18 轮显示说明与测验,然后第 19 轮正式进入 8w) Instructions_8w, Time_Lot_Quiz_8w, Middle_Screen_8w, # 8w 的选择页 Discount_Lot_1_8w, Discount_Lot_2_8w, Discount_Lot_3_8w, # —— 8w 结束页(第 36 轮) —— 放到最后 Last_Page_8w, ]