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_Lot2(page: Page): """是否显示 TA_Lot2。Risk 正常流程会写入 path;单测时 models 里也有兜底。""" return page.participant.vars.get('path', 'CE→Lot2') == 'CE→Lot2' def order_is_84(page: Page): """本 app 默认顺序为 8w→4w。""" return page.participant.vars.get('order', '84') == '84' #def weeks_now(page: Page): # """根据 84 与当前段(1..18 / 19..36)返回 8 或 4。""" # # ✅ 如果我们明确已经进入 4w 阶段 # if page.participant.vars.get('force_4w', False): # return 4 # in_first_segment = page.round_number <= 18 # if order_is_84(page): # return 8 if in_first_segment else 4 # else: # # 兜底:维持 48(与另一个 app 相同)逻辑 # return 4 if in_first_segment else 8 def weeks_now(page: Page): seq = page.participant.vars.get('discount_seq') order84 = (seq in (3, 4)) # seq=1/2 -> 48, seq=3/4 -> 84 in_first_segment = page.round_number <= 18 if order84: return 8 if in_first_segment else 4 else: return 4 if in_first_segment else 8 def is_before_4w(page: Page): """在第18轮之后、19轮正式开始前,用于显示 4w 的 Instructions 与 Quiz""" # 仅在 round 18 时显示一次(8w 刚结束) 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_4w=pv.get('force_4w'), dbg_ins4w_shown=pv.get('ins4w_shown'), dbg_quiz4w_shown=pv.get('quiz4w_shown'), dbg_mid4w_shown=pv.get('mid4w_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_8w(DebugPage): template_name = 'Time_Adaptive_Lot_2_84/Instructions_8w.html' def is_displayed(self): return show_Lot2(self) and self.round_number == 1 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 class Instructions(DebugPage): template_name = 'Time_Adaptive_Lot_2_84/Instructions.html' def is_displayed(self): shown = self.participant.vars.get('ins4w_shown', False) return show_Lot2(self) and self.round_number == 19 and not shown 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 def before_next_page(self): self.participant.vars['ins4w_shown'] = True # ---------- Quiz ---------- class Time_Lot_Quiz_8w(DebugPage): """8w 的 Quiz:此 app 中在第 1 轮开始显示""" template_name = 'Time_Adaptive_Lot_2_84/Time_Lot_Quiz_8w.html' def is_displayed(self): return show_Lot2(self) and self.round_number == 1 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!' class Time_Lot_Quiz(DebugPage): template_name = 'Time_Adaptive_Lot_2_84/Time_Lot_Quiz.html' def is_displayed(self): shown = self.participant.vars.get('quiz4w_shown', False) return show_Lot2(self) and self.round_number == 19 and not shown 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!' # ---------- Discount_Lot_1 ---------- class Discount_Lot_1_8w(DebugPage): template_name = 'Time_Adaptive_Lot_2_84/Discount_Lot_1_8w.html' def is_displayed(self): return show_Lot2(self) and weeks_now(self) == 8 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 = 2 if self.round_number <= 18 else 1 self.player.current_week = 8 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': 8, }) 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() # class Discount_Lot_1(Discount_Lot_1_8w): template_name = 'Time_Adaptive_Lot_2_84/Discount_Lot_1.html' def is_displayed(self): return show_Lot2(self) and weeks_now(self) == 4 and (self.participant.vars['page'] == 1) def vars_for_template(self): ctx = super().vars_for_template() self.player.current_week = 4 ctx['weeks'] = 4 return ctx # ---------- Discount_Lot_2 ---------- class Discount_Lot_2_8w(DebugPage): template_name = 'Time_Adaptive_Lot_2_84/Discount_Lot_2_8w.html' def is_displayed(self): return show_Lot2(self) and weeks_now(self) == 8 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 = 2 if self.round_number <= 18 else 1 self.player.current_week = 8 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': 8, }) 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(Discount_Lot_2_8w): template_name = 'Time_Adaptive_Lot_2_84/Discount_Lot_2.html' def is_displayed(self): return show_Lot2(self) and weeks_now(self) == 4 and (self.participant.vars['page'] == 2) def vars_for_template(self): ctx = super().vars_for_template() self.player.current_week = 4 ctx['weeks'] = 4 return ctx # ---------- Discount_Lot_3 ---------- class Discount_Lot_3_8w(Discount_Lot_1): template_name = 'Time_Adaptive_Lot_2_84/Discount_Lot_3_8w.html' def is_displayed(self): return show_Lot2(self) and weeks_now(self) == 8 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 = 2 if self.round_number <= 18 else 1 self.player.current_week = 8 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': 8, }) 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(Discount_Lot_3_8w): template_name = 'Time_Adaptive_Lot_2_84/Discount_Lot_3.html' def is_displayed(self): return show_Lot2(self) and weeks_now(self) == 4 and (self.participant.vars['page'] == 3) def vars_for_template(self): ctx = super().vars_for_template() self.player.current_week = 4 ctx['weeks'] = 4 return ctx # ---------- Middle screen ---------- class Middle_Screen_8w(DebugPage): template_name = 'Time_Adaptive_Lot_2_84/Middle_Screen_8w.html' def is_displayed(self): return show_Lot2(self) and weeks_now(self) == 8 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=8) ) 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(DebugPage): template_name = 'Time_Adaptive_Lot_2_84/Middle_Screen.html' def is_displayed(self): # 8w 的三个 block 起点:19, 25, 31 return show_Lot2(self) and weeks_now(self) == 4 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=4) ) return ctx def before_next_page(self): self.player.set_page() # ---------- Last page ---------- class Last_Page_8w(DebugPage): template_name = 'Time_Adaptive_Lot_2_84/Last_Page_8w.html' # 4w 段结束:只在第 18 轮 def is_displayed(self): return show_Lot2(self) and (self.round_number == 18) 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 before_next_page(self): self.participant.vars['force_8w'] = True # ✅ 防止 8w choices 在 round 19 抢跑 self.participant.vars['page'] = 0 # ✅ 可选:重置“只显示一次”的标记,避免你测试多次后被挡住 self.participant.vars.pop('ins4w_shown', None) self.participant.vars.pop('quiz4w_shown', None) self.participant.vars.pop('mid4w_shown', None) class Last_Page(DebugPage): template_name = 'Time_Adaptive_Lot_2_84/Last_Page.html' # 8w 段结束:第 36 轮 def is_displayed(self): return show_Lot2(self) and (self.round_number == 36) 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 # 页面顺序:我们把 4w 与 8w 版本都放进来,靠 is_displayed 控制显示哪一个 page_sequence = [ # 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, # 4w 开场 Instructions, Time_Lot_Quiz, Middle_Screen, # 4w 的选择页 Discount_Lot_1, Discount_Lot_2, Discount_Lot_3, # —— 4w 结束页(第 18 轮) —— 必须在切换到 8w 的指引页之前 Last_Page ]