""" pages.py — Page definitions for the norm experiment. Page sequence: 1. Consent 2. GeneralInstructions 3. GeneralComprehension 4. ConditionAssignment (WaitPage / behind-the-scenes) 5. ConditionInstructions 6. ConditionComprehension 7. TaskBlock1 (Dictator or Risk, randomised) 8. TaskBlock2 (remaining task) 9. ConsistencyChecks 10. FinalPayoff 11. ProlificRedirect """ import json import time import random from otree.api import Page, WaitPage from .constants import ( CONDITIONS, DICTATOR_PAIRS, RISK_PAIRS, CONSISTENCY_CHECKS, ACTUAL_NORMS, DICTATOR_BY_ID, RISK_BY_ID, DICTATOR_ENDOWMENT, ) from .instructions_text import ( GENERAL_INSTRUCTIONS_HTML, get_condition_instructions_html, DICTATOR_INSTRUCTIONS_HTML, RISK_INSTRUCTIONS_HTML, ) # --------------------------------------------------------------------------- # Utility: timestamp helpers # --------------------------------------------------------------------------- def _now(): return time.time() # --------------------------------------------------------------------------- # Debug helper # --------------------------------------------------------------------------- def _is_debug(page): return page.session.config.get('debug', False) # --------------------------------------------------------------------------- # 0. Debug Setup (only shown when debug=True in settings.py) # Lets you pick condition and task order, auto-sets consent, # and skips all instructions so you land straight at the tasks. # --------------------------------------------------------------------------- class DebugSetup(Page): form_model = 'player' form_fields = ['debug_condition', 'debug_task_order'] def is_displayed(self): return _is_debug(self) def before_next_page(self, timeout_happened=False): p = self.player # Auto-consent p.consent = 'consent' # Set condition from dropdown p.condition = p.debug_condition or 'threshold' # Set task order from dropdown order_choice = p.debug_task_order or 'dictator_first' if order_choice == 'dictator_first': p.task_order = json.dumps(['dictator', 'risk']) p.participant.vars['task_instr_order'] = ['dictator', 'risk'] else: p.task_order = json.dumps(['risk', 'dictator']) p.participant.vars['task_instr_order'] = ['risk', 'dictator'] p.initialise_lr_assignments() # --------------------------------------------------------------------------- # 1. Consent # --------------------------------------------------------------------------- class Consent(Page): form_model = 'player' form_fields = ['consent'] def is_displayed(self): return not _is_debug(self) def before_next_page(self, timeout_happened=False): self.player.record_timestamp('consent', end=_now()) self.player.prolific_id = self.participant.label or '' def vars_for_template(self): return {'page_start': _now()} def js_vars(self): return {'page_start': _now()} def error_message(self, values): pass class NoConsent(Page): """Shown only when participant did NOT consent.""" def is_displayed(self): return self.player.consent == 'not_consent' class ProlificIDFallback(Page): form_model = 'player' form_fields = ['prolific_id'] def is_displayed(self): return (self.player.consent == 'consent' and not self.player.field_maybe_none('prolific_id')) def error_message(self, values): pid = values.get('prolific_id', '') if not pid or len(pid) < 15: return 'Please enter your Prolific ID before continuing.' def vars_for_template(self): return {} # --------------------------------------------------------------------------- # 2. General Instructions # --------------------------------------------------------------------------- class GeneralInstructions(Page): def is_displayed(self): return self.player.consent == 'consent' and not _is_debug(self) def before_next_page(self, timeout_happened=False): self.player.record_timestamp('general_instructions', end=_now()) def vars_for_template(self): self.player.record_timestamp('general_instructions', start=_now()) return { 'general_instructions_html': GENERAL_INSTRUCTIONS_HTML, } # --------------------------------------------------------------------------- # 3. General Comprehension Questions # --------------------------------------------------------------------------- class GeneralComprehension(Page): form_model = 'player' form_fields = ['gen_q1', 'gen_q2', 'gen_q3', 'gen_q4'] CORRECT = { 'gen_q1': 'c', # allocation task affects both own and other's payment 'gen_q2': 'c', # lottery task only affects own payment 'gen_q3': 'a', # separate group who completed study earlier 'gen_q4': 'c', # one randomly selected decision paid as bonus } def is_displayed(self): return self.player.consent == 'consent' def error_message(self, values): wrong_fields = [] for field, correct in self.CORRECT.items(): if values.get(field) != correct: counter_field = field + '_errors' setattr(self.player, counter_field, getattr(self.player, counter_field, 0) + 1) wrong_fields.append(field) self.player.participant.vars['gen_submitted'] = { f: values.get(f, '') for f in self.CORRECT.keys() } if wrong_fields: self.player.participant.vars['gen_wrong_fields'] = wrong_fields return 'Not all questions are correct. Please have another careful look at the questions marked below.' self.player.participant.vars['gen_wrong_fields'] = [] return None def before_next_page(self, timeout_happened=False): self.player.record_timestamp('general_comprehension', end=_now()) self.player.participant.vars['gen_wrong_fields'] = [] self.player.participant.vars['gen_submitted'] = {} def vars_for_template(self): self.player.record_timestamp('general_comprehension', start=_now()) wrong = self.player.participant.vars.get('gen_wrong_fields', []) return { 'general_instructions_html': GENERAL_INSTRUCTIONS_HTML, 'gen_q1_errors': self.player.gen_q1_errors, 'gen_q2_errors': self.player.gen_q2_errors, 'gen_q3_errors': self.player.gen_q3_errors, 'gen_q4_errors': self.player.gen_q4_errors, 'gen_q1_wrong': 'gen_q1' in wrong, 'gen_q2_wrong': 'gen_q2' in wrong, 'gen_q3_wrong': 'gen_q3' in wrong, 'gen_q4_wrong': 'gen_q4' in wrong, 'gen_q1_prev': self.player.participant.vars.get('gen_submitted', {}).get('gen_q1', ''), 'gen_q2_prev': self.player.participant.vars.get('gen_submitted', {}).get('gen_q2', ''), 'gen_q3_prev': self.player.participant.vars.get('gen_submitted', {}).get('gen_q3', ''), 'gen_q4_prev': self.player.participant.vars.get('gen_submitted', {}).get('gen_q4', ''), } # --------------------------------------------------------------------------- # 3b & 3c. Allocation Task and Lottery Task Instructions + Comprehension # Shown between general and condition-specific instructions, in random order. # --------------------------------------------------------------------------- class AllocationInstructions(Page): def is_displayed(self): return self.player.consent == 'consent' and not _is_debug(self) def vars_for_template(self): self.player.record_timestamp('dictator_instructions', start=_now()) return { 'dictator_instructions_html': DICTATOR_INSTRUCTIONS_HTML, 'dictator_endowment': DICTATOR_ENDOWMENT, } def before_next_page(self, timeout_happened=False): self.player.record_timestamp('dictator_instructions', end=_now()) class AllocationComprehension(Page): form_model = 'player' form_fields = ['dict_q1', 'dict_q2'] CORRECT = {'dict_q1': 'b', 'dict_q2': 'a'} def is_displayed(self): return self.player.consent == 'consent' and not _is_debug(self) def error_message(self, values): wrong_fields = [] for field, correct in self.CORRECT.items(): if values.get(field) != correct: counter_field = field + '_errors' setattr(self.player, counter_field, getattr(self.player, counter_field, 0) + 1) wrong_fields.append(field) self.player.participant.vars['dict_submitted'] = { f: values.get(f, '') for f in self.CORRECT.keys() } if wrong_fields: self.player.participant.vars['dict_wrong_fields'] = wrong_fields return 'Not all questions are correct. Please have another careful look at the questions marked below.' self.player.participant.vars['dict_wrong_fields'] = [] return None def vars_for_template(self): self.player.record_timestamp('dictator_comprehension', start=_now()) wrong = self.player.participant.vars.get('dict_wrong_fields', []) subm = self.player.participant.vars.get('dict_submitted', {}) return { 'dictator_instructions_html': DICTATOR_INSTRUCTIONS_HTML, 'wrong_json': json.dumps({f: (f in wrong) for f in ['dict_q1', 'dict_q2']}), 'prev_json': json.dumps({f: subm.get(f, '') for f in ['dict_q1', 'dict_q2']}), } def before_next_page(self, timeout_happened=False): self.player.record_timestamp('dictator_comprehension', end=_now()) self.player.participant.vars['dict_wrong_fields'] = [] self.player.participant.vars['dict_submitted'] = {} class LotteryInstructions(Page): def is_displayed(self): return self.player.consent == 'consent' and not _is_debug(self) def vars_for_template(self): self.player.record_timestamp('risk_instructions', start=_now()) return { 'risk_instructions_html': RISK_INSTRUCTIONS_HTML, } def before_next_page(self, timeout_happened=False): self.player.record_timestamp('risk_instructions', end=_now()) class LotteryComprehension(Page): form_model = 'player' form_fields = ['risk_q1'] CORRECT = {'risk_q1': 'c'} def is_displayed(self): return self.player.consent == 'consent' and not _is_debug(self) def error_message(self, values): if values.get('risk_q1') != self.CORRECT['risk_q1']: self.player.risk_q1_errors = getattr(self.player, 'risk_q1_errors', 0) + 1 self.player.participant.vars['risk_submitted'] = {'risk_q1': values.get('risk_q1', '')} self.player.participant.vars['risk_wrong_fields'] = ['risk_q1'] return 'That answer is incorrect. Please have another careful look at the question below.' self.player.participant.vars['risk_wrong_fields'] = [] self.player.participant.vars['risk_submitted'] = {} return None def vars_for_template(self): self.player.record_timestamp('risk_comprehension', start=_now()) wrong = self.player.participant.vars.get('risk_wrong_fields', []) subm = self.player.participant.vars.get('risk_submitted', {}) return { 'risk_instructions_html': RISK_INSTRUCTIONS_HTML, 'wrong_json': json.dumps({'risk_q1': 'risk_q1' in wrong}), 'prev_json': json.dumps({'risk_q1': subm.get('risk_q1', '')}), } def before_next_page(self, timeout_happened=False): self.player.record_timestamp('risk_comprehension', end=_now()) self.player.participant.vars['risk_wrong_fields'] = [] self.player.participant.vars['risk_submitted'] = {} # --------------------------------------------------------------------------- # Helper: random task instruction order, initialised once per participant # --------------------------------------------------------------------------- def _instr_order(player): """Return ['dictator','risk'] or ['risk','dictator'], consistent per participant.""" if 'task_instr_order' not in player.participant.vars: order = ['dictator', 'risk'] if random.random() < 0.5 else ['risk', 'dictator'] player.participant.vars['task_instr_order'] = order return player.participant.vars['task_instr_order'] # Slot subclasses: oTree requires distinct class objects in page_sequence. # Each pair of Slot1/Slot2 subclasses shows the appropriate task first or second. class AllocationInstructionsSlot1(AllocationInstructions): template_name = 'dm_experiment/AllocationInstructions.html' def is_displayed(self): return self.player.consent == 'consent' and not _is_debug(self) and _instr_order(self.player)[0] == 'dictator' class AllocationComprehensionSlot1(AllocationComprehension): template_name = 'dm_experiment/AllocationComprehension.html' def is_displayed(self): return self.player.consent == 'consent' and not _is_debug(self) and _instr_order(self.player)[0] == 'dictator' class LotteryInstructionsSlot1(LotteryInstructions): template_name = 'dm_experiment/LotteryInstructions.html' def is_displayed(self): return self.player.consent == 'consent' and not _is_debug(self) and _instr_order(self.player)[0] == 'risk' class LotteryComprehensionSlot1(LotteryComprehension): template_name = 'dm_experiment/LotteryComprehension.html' def is_displayed(self): return self.player.consent == 'consent' and not _is_debug(self) and _instr_order(self.player)[0] == 'risk' class AllocationInstructionsSlot2(AllocationInstructions): template_name = 'dm_experiment/AllocationInstructions.html' def is_displayed(self): return self.player.consent == 'consent' and not _is_debug(self) and _instr_order(self.player)[1] == 'dictator' class AllocationComprehensionSlot2(AllocationComprehension): template_name = 'dm_experiment/AllocationComprehension.html' def is_displayed(self): return self.player.consent == 'consent' and not _is_debug(self) and _instr_order(self.player)[1] == 'dictator' class LotteryInstructionsSlot2(LotteryInstructions): template_name = 'dm_experiment/LotteryInstructions.html' def is_displayed(self): return self.player.consent == 'consent' and not _is_debug(self) and _instr_order(self.player)[1] == 'risk' class LotteryComprehensionSlot2(LotteryComprehension): template_name = 'dm_experiment/LotteryComprehension.html' def is_displayed(self): return self.player.consent == 'consent' and not _is_debug(self) and _instr_order(self.player)[1] == 'risk' # --------------------------------------------------------------------------- # 4. Condition Assignment (hidden page — runs logic silently) # --------------------------------------------------------------------------- class ConditionAssignment(Page): """Hidden page that assigns condition and randomises task order.""" def is_displayed(self): return False def app_after_this_page(self, upcoming_apps): pass def before_next_page(self, timeout_happened=False): p = self.player if p.consent != 'consent': return # In debug mode, condition and task order were already set by DebugSetup if _is_debug(self): return p.assign_condition() p.initialise_task_order() p.initialise_lr_assignments() # --------------------------------------------------------------------------- # 5. Condition-specific Instructions # --------------------------------------------------------------------------- class SpecificInstructions(Page): def is_displayed(self): return self.player.consent == 'consent' and not _is_debug(self) def vars_for_template(self): if not self.player.field_maybe_none('condition'): self.player.assign_condition() self.player.initialise_task_order() self.player.initialise_lr_assignments() self.player.record_timestamp('condition_instructions', start=_now()) condition = self.player.condition return { 'condition': condition, 'condition_instructions_html': get_condition_instructions_html(condition), } def before_next_page(self, timeout_happened=False): self.player.record_timestamp('condition_instructions', end=_now()) # --------------------------------------------------------------------------- # 6. Condition-specific Comprehension # --------------------------------------------------------------------------- class SpecificComprehension(Page): form_model = 'player' CORRECT = { 'threshold': { 'cond_q1': 'c', # slider all the way right = always right 'cond_q2': 'b', # threshold 6, norm 4 -> right option 'cond_q3': 'b', # threshold 10, norm 9 -> right option 'cond_q4': 'b', # genuine preferences }, 'staircase': { 'cond_q1': 'b', # threshold 6, norm 4 -> right option 'cond_q2': 'b', # threshold 10, norm 9 -> right option 'cond_q3': 'b', # genuine preferences }, 'full_MPL': { 'cond_q1': 'b', # genuine preferences 'cond_q2': 'b', # right option implemented because that is what was chosen }, } form_fields = ['cond_q1', 'cond_q2', 'cond_q3', 'cond_q4'] def is_displayed(self): return self.player.consent == 'consent' and not _is_debug(self) def error_message(self, values): cond = self.player.field_maybe_none('condition') or 'threshold' correct_map = self.CORRECT.get(cond, {}) wrong_fields = [] for field, correct in correct_map.items(): if values.get(field) != correct: counter_field = field + '_errors' setattr(self.player, counter_field, getattr(self.player, counter_field, 0) + 1) wrong_fields.append(field) self.player.participant.vars['cond_submitted'] = { f: values.get(f, '') for f in correct_map.keys() } if wrong_fields: self.player.participant.vars['cond_wrong_fields'] = wrong_fields return 'Not all questions are correct. Please have another careful look at the questions marked below.' self.player.participant.vars['cond_wrong_fields'] = [] return None def vars_for_template(self): self.player.record_timestamp('condition_comprehension', start=_now()) cond = self.player.field_maybe_none('condition') or 'threshold' wrong = self.player.participant.vars.get('cond_wrong_fields', []) subm = self.player.participant.vars.get('cond_submitted', {}) wrong_json = json.dumps({f: (f in wrong) for f in ['cond_q1', 'cond_q2', 'cond_q3', 'cond_q4']}) prev_json = json.dumps({f: subm.get(f, '') for f in ['cond_q1', 'cond_q2', 'cond_q3', 'cond_q4']}) return { 'condition': cond, 'condition_instructions_html': get_condition_instructions_html(cond), 'wrong_json': wrong_json, 'prev_json': prev_json, } def before_next_page(self, timeout_happened=False): self.player.record_timestamp('condition_comprehension', end=_now()) self.player.participant.vars['cond_wrong_fields'] = [] self.player.participant.vars['cond_submitted'] = {} # --------------------------------------------------------------------------- # 7 & 8. Task Blocks — shared base class # --------------------------------------------------------------------------- class _TaskBlockBase(Page): block_index = None # set in subclass def is_displayed(self): return self.player.consent == 'consent' def _get_task_type(self): order = self.player.get_task_order() return order[self.block_index] def vars_for_template(self): task_type = self._get_task_type() pairs = self.player.get_pairs(task_type) lr_assign = self.player.get_lr_assignments(task_type) condition = self.player.field_maybe_none('condition') or 'threshold' situations = [] for idx, (pair, a_is_left) in enumerate(zip(pairs, lr_assign)): opt_a = pair['option_a'] opt_b = pair['option_b'] left = opt_a if a_is_left else opt_b right = opt_b if a_is_left else opt_a situations.append({ 'id': idx, 'left': left, 'right': right, 'a_is_left': a_is_left, 'task_type': task_type, }) random.shuffle(situations) self.player.record_timestamp(f'task_block_{self.block_index}', start=_now()) return { 'task_type': task_type, 'situations': json.dumps(situations), 'condition': condition, 'num_pairs': len(pairs), 'dictator_endowment': DICTATOR_ENDOWMENT, } def before_next_page(self, timeout_happened=False): """Read decisions and staircase state from POST data and save to player.""" task_type = self._get_task_type() decisions_json = self._form_data.get('task_decisions', '[]') sc_json = self._form_data.get('staircase_state_data', '{}') order_json = self._form_data.get('situation_order_data', '[]') try: decisions = json.loads(decisions_json) self.player.save_decisions(task_type, decisions) self.player.populate_split_fields(task_type) except (json.JSONDecodeError, Exception): pass try: self.player.staircase_state = sc_json except Exception: pass try: if task_type == 'dictator': self.player.dictator_situation_order = order_json else: self.player.risk_situation_order = order_json except Exception: pass self.player.record_timestamp(f'task_block_{self.block_index}', end=_now()) class TaskBlock1(_TaskBlockBase): block_index = 0 class TaskBlock2(_TaskBlockBase): block_index = 1 # --------------------------------------------------------------------------- # 8b. Final Decisions Instructions + Comprehension # --------------------------------------------------------------------------- class AdditionalDecisionsInstructions(Page): def is_displayed(self): return self.player.consent == 'consent' def vars_for_template(self): self.player.record_timestamp('additional_decisions_instructions', start=_now()) return {} def before_next_page(self, timeout_happened=False): self.player.record_timestamp('additional_decisions_instructions', end=_now()) class AdditionalDecisionsComprehension(Page): form_model = 'player' form_fields = ['final_q1', 'final_q2'] CORRECT = {'final_q1': 'a', 'final_q2': 'b'} def is_displayed(self): return self.player.consent == 'consent' def error_message(self, values): wrong_fields = [] for field, correct in self.CORRECT.items(): if values.get(field) != correct: counter_field = field + '_errors' setattr(self.player, counter_field, getattr(self.player, counter_field, 0) + 1) wrong_fields.append(field) self.player.participant.vars['final_wrong_fields'] = wrong_fields self.player.participant.vars['final_submitted'] = { f: values.get(f, '') for f in self.CORRECT.keys() } if wrong_fields: return 'Not all questions are correct. Please have another careful look at the questions marked below.' self.player.participant.vars['final_wrong_fields'] = [] return None def vars_for_template(self): wrong = self.player.participant.vars.get('final_wrong_fields', []) subm = self.player.participant.vars.get('final_submitted', {}) return { 'wrong_json': json.dumps({f: (f in wrong) for f in ['final_q1', 'final_q2']}), 'prev_json': json.dumps({f: subm.get(f, '') for f in ['final_q1', 'final_q2']}), } def before_next_page(self, timeout_happened=False): self.player.participant.vars['final_wrong_fields'] = [] self.player.participant.vars['final_submitted'] = {} # --------------------------------------------------------------------------- # 9. Consistency Checks # --------------------------------------------------------------------------- class AdditionalDecisions(Page): def is_displayed(self): return self.player.consent == 'consent' def vars_for_template(self): self.player.record_timestamp('consistency_checks', start=_now()) checks = [] for idx, check in enumerate(CONSISTENCY_CHECKS): task_type = check['task_type'] opt_a_id = check['option_a_id'] opt_b_id = check['option_b_id'] norm_key = (task_type, opt_a_id, opt_b_id) n_chose_a = ACTUAL_NORMS.get(norm_key, 5) if task_type == 'dictator': opt_a = DICTATOR_BY_ID[opt_a_id] opt_b = DICTATOR_BY_ID[opt_b_id] else: opt_a = RISK_BY_ID[opt_a_id] opt_b = RISK_BY_ID[opt_b_id] a_is_left = random.choice([True, False]) left = opt_a if a_is_left else opt_b right = opt_b if a_is_left else opt_a n_left = n_chose_a if a_is_left else (10 - n_chose_a) checks.append({ 'index': idx, 'task_type': task_type, 'left': left, 'right': right, 'a_is_left': a_is_left, 'n_left': n_left, 'n_right': 10 - n_left, }) dictator_checks = [c for c in checks if c['task_type'] == 'dictator'] risk_checks = [c for c in checks if c['task_type'] == 'risk'] random.shuffle(dictator_checks) random.shuffle(risk_checks) if random.random() < 0.5: checks = dictator_checks + risk_checks else: checks = risk_checks + dictator_checks return {'checks': json.dumps(checks), 'dictator_endowment': DICTATOR_ENDOWMENT} def before_next_page(self, timeout_happened=False): decisions_json = self._form_data.get('consistency_decisions_data', '[]') try: self.player.consistency_decisions = decisions_json except Exception: pass self.player.record_timestamp('consistency_checks', end=_now()) # --------------------------------------------------------------------------- # 10. Final Payoff # --------------------------------------------------------------------------- class FinalPayoff(Page): def is_displayed(self): return self.player.consent == 'consent' def vars_for_template(self): import logging logger = logging.getLogger(__name__) logger.warning("=== FinalPayoff: starting vars_for_template ===") self.player.record_timestamp('final_payoff', start=_now()) # Only calculate once — skip if already done if not self.player.field_maybe_none('payoff_explanation') or self.player.payoff_explanation == '{}': self.player.calculate_payoff() import json as _json p = self.player exp = _json.loads(p.payoff_explanation or '{}') from .constants import MU_TO_GBP # if not already imported return { 'payoff': p.payoff, 'payoff_raw': float(p.payoff), 'payoff_gbp': round(float(p.payoff) * MU_TO_GBP, 2), 'mu_to_gbp': MU_TO_GBP, 'other_payoff': p.other_payoff, 'risk_outcome': p.risk_outcome_realised, 'selected_option': p.selected_option_chosen, 'exp': exp, } def before_next_page(self, timeout_happened=False): self.player.record_timestamp('final_payoff', end=_now()) # --------------------------------------------------------------------------- # 11. Prolific Redirect # --------------------------------------------------------------------------- class ProlificRedirect(Page): def is_displayed(self): return self.player.consent == 'consent' def vars_for_template(self): prolific_url = self.session.config.get( 'prolific_completion_url', 'https://app.prolific.com/submissions/complete?cc=C197921G' ) return {'prolific_url': prolific_url} # --------------------------------------------------------------------------- # Cognitive Uncertainty (shown after each task block) # --------------------------------------------------------------------------- class intermediateQuestion1(Page): form_model = "player" def get_form_fields(self): order = self.player.get_task_order() if order[0] == "dictator": return ["cognitive_uncertainty_dictator"] else: return ["cognitive_uncertainty_risk"] def is_displayed(self): return self.player.consent == "consent" def vars_for_template(self): order = self.player.get_task_order() is_dictator = order[0] == "dictator" return { "task_label": "Allocation Task" if is_dictator else "Lottery Task", "show_dictator": is_dictator, } def error_message(self, values): order = self.player.get_task_order() field = "cognitive_uncertainty_dictator" if order[0] == "dictator" else "cognitive_uncertainty_risk" if values.get(field) is None: return "Please respond to all questions before continuing." class intermediateQuestion2(Page): form_model = "player" def get_form_fields(self): order = self.player.get_task_order() if order[1] == "dictator": return ["cognitive_uncertainty_dictator"] else: return ["cognitive_uncertainty_risk"] def is_displayed(self): return self.player.consent == "consent" def vars_for_template(self): order = self.player.get_task_order() is_dictator = order[1] == "dictator" return { "task_label": "Allocation Task" if is_dictator else "Lottery Task", "show_dictator": is_dictator, } def error_message(self, values): order = self.player.get_task_order() field = "cognitive_uncertainty_dictator" if order[1] == "dictator" else "cognitive_uncertainty_risk" if values.get(field) is None: return "Please respond to all questions before continuing." # --------------------------------------------------------------------------- # IOS Scale (shown before ConsistencyChecks) # --------------------------------------------------------------------------- class IOSScale(Page): form_model = "player" form_fields = ["ios_scale"] def is_displayed(self): return self.player.consent == "consent" # --------------------------------------------------------------------------- # Ex Post Questionnaire (split into sub-pages for clarity) # --------------------------------------------------------------------------- # Item definitions for the questionnaire scales _MAJ_ITEMS = [ ("maj_q1", "As long as I have a few people who also do what I do, I do not care about others who do not do the same as I do."), ("maj_q2", "The thought of a few people doing the opposite of what I do often makes me more hesitant about my actions."), ("maj_q3", "If a few people do not contribute to public services, like taxes, then I also do not want to contribute."), ("maj_q4", "In unfamiliar situations, I usually just follow the crowd until I know what to do."), ("maj_q5", "I feel comfortable with my decision as long as most people around me make the same choice."), ("maj_q6", "I am more concerned about being in line with the majority than about what any single individual thinks of me."), ("maj_q7", "I tend to base my decisions on what most people do, rather than on the behaviour of specific individuals."), ("maj_q8", "I feel reassured in my choices when I know that most people around me would make the same decision."), ("maj_q9", "I only start to question my behaviour when I notice that the majority of people around me would act differently."), ("maj_q10", "Even if the majority does what I do, knowing that a few people do the opposite makes me uncomfortable."), ("maj_q11", "I am more likely to change my behaviour when most people around me act differently than when just a few do."), ("maj_q12", "I am comfortable going against the majority as long as at least a few others share my position."), ] _SNES_ITEMS = [ ("snes_q1", "I go out of my way to follow social norms."), ("snes_q2", "We should not always have to follow a set of social rules."), ("snes_q3", "People should always be able to behave as they wish rather than trying to fit the norm."), ("snes_q4", "There is a correct way to behave in every situation."), ("snes_q5", "If more people followed society's rules, the world would be a better place."), ("snes_q6", "People need to follow life's unwritten rules every bit as strictly as they follow the written rules."), ("snes_q7", "There are lots of vital customs that people should follow as members of society."), ("snes_q8", "The standards that society expects us to meet are far too restrictive."), ("snes_q9", "People who do what society expects of them lead happier lives."), ("snes_q10", "Our society is built on unwritten rules that members need to follow."), ("snes_q11", "I am at ease only when everyone around me is adhering to society's norms."), ("snes_q12", "We would be happier if we did not try to follow society's norms."), ("snes_q13", "My idea of a perfect world would be one with few social expectations."), ("snes_q14", "I always do my best to follow society's rules."), ] def _seeded_shuffle(lst, seed): """Return a shuffled copy of lst using a deterministic seed.""" import random as _rng r = _rng.Random(seed) out = list(lst) r.shuffle(out) return out class ExPostQuestionnaire1(Page): form_model = "player" form_fields = [ "maj_q1","maj_q2","maj_q3","maj_q4","maj_q5","maj_q6", "maj_q7","maj_q8","maj_q9","maj_q10","maj_q11","maj_q12", "attention_check_maj", ] def is_displayed(self): return self.player.consent == "consent" def vars_for_template(self): self.player.record_timestamp("questionnaire1", start=_now()) code = self.participant.code seed_maj = sum(ord(c) for c in code) * 3 maj_shuffled = _seeded_shuffle(_MAJ_ITEMS, seed_maj) # Insert attention check at fixed midpoint of the shuffled list maj_with_check = ( maj_shuffled[:6] + [("attention_check_maj", "To show that you are paying attention, please select number 3.")] + maj_shuffled[6:] ) import json as _json return {"maj_order_json": _json.dumps([f for f, _ in maj_with_check])} class ExPostQuestionnaire2(Page): form_model = "player" form_fields = [ "snes_q1","snes_q2","snes_q3","snes_q4","snes_q5","snes_q6","snes_q7", "snes_q8","snes_q9","snes_q10","snes_q11","snes_q12","snes_q13","snes_q14", "attention_check_snes", ] def is_displayed(self): return self.player.consent == "consent" def vars_for_template(self): self.player.record_timestamp("questionnaire2", start=_now()) code = self.participant.code seed_snes = sum(ord(c) for c in code) * 7 snes_shuffled = _seeded_shuffle(_SNES_ITEMS, seed_snes) # Insert attention check at fixed midpoint of the shuffled list snes_with_check = ( snes_shuffled[:7] + [("attention_check_snes", "To show that you are paying attention, please select number 6.")] + snes_shuffled[7:] ) import json as _json return {"snes_order_json": _json.dumps([f for f, _ in snes_with_check])} class ExPostQuestionnaire3(Page): form_model = "player" form_fields = ["contrarian"] def is_displayed(self): return self.player.consent == "consent" def error_message(self, values): if not values.get("contrarian"): return "Please respond to all questions before continuing." class ExPostQuestionnaire4(Page): form_model = "player" form_fields = ["q_understood_general", "q_understood_lottery", "q_understood_allocation", "q_understood_decision", "q_understood_actual", "q_difficulty", "q_effort"] def is_displayed(self): return self.player.consent == "consent" def error_message(self, values): missing = [f for f in self.form_fields if values.get(f) is None] if missing: return "Please respond to all questions before continuing." class ExPostQuestionnaire5(Page): form_model = "player" form_fields = ["q_strategy"] def is_displayed(self): return self.player.consent == "consent" class ExPostQuestionnaire6(Page): form_model = "player" form_fields = ["q_comments"] def is_displayed(self): return self.player.consent == "consent" def before_next_page(self, timeout_happened=False): self.player.record_timestamp("questionnaire", end=_now()) # --------------------------------------------------------------------------- # Demographics # --------------------------------------------------------------------------- class Demographics(Page): form_model = "player" form_fields = ["demo_age", "demo_gender", "demo_gender_self", "demo_education", "demo_ses"] def is_displayed(self): return self.player.consent == "consent" def vars_for_template(self): self.player.record_timestamp("demographics", start=_now()) return {} def before_next_page(self, timeout_happened=False): self.player.record_timestamp("demographics", end=_now()) # --------------------------------------------------------------------------- # Page sequence # --------------------------------------------------------------------------- page_sequence = [ DebugSetup, # only shown when debug=True in settings.py Consent, NoConsent, ProlificIDFallback, GeneralInstructions, GeneralComprehension, # Task instructions — whichever task is drawn first AllocationInstructionsSlot1, AllocationComprehensionSlot1, LotteryInstructionsSlot1, LotteryComprehensionSlot1, # Task instructions — whichever task is drawn second AllocationInstructionsSlot2, AllocationComprehensionSlot2, LotteryInstructionsSlot2, LotteryComprehensionSlot2, # Condition-specific instructions ConditionAssignment, SpecificInstructions, SpecificComprehension, TaskBlock1, intermediateQuestion1, TaskBlock2, intermediateQuestion2, IOSScale, AdditionalDecisionsInstructions, AdditionalDecisionsComprehension, AdditionalDecisions, ExPostQuestionnaire1, ExPostQuestionnaire2, ExPostQuestionnaire3, ExPostQuestionnaire4, ExPostQuestionnaire5, ExPostQuestionnaire6, Demographics, FinalPayoff, ProlificRedirect, ]