""" models.py — oTree data models for the norm experiment. All mutable state lives in Player fields (stored per-participant). Session-level state (e.g. the rotating condition counter) lives in session.vars so it persists across participants in the same session. """ import json import random from otree.api import BaseConstants, BaseSubsession, BaseGroup, BasePlayer, models, widgets from .constants import ( CONDITIONS, NUM_CONDITIONS, CONSISTENCY_CHECKS, DICTATOR_PAIRS, RISK_PAIRS, ACTUAL_NORMS, DICTATOR_BY_ID, RISK_BY_ID, DICTATOR_ENDOWMENT, ) from .constants import MU_TO_GBP # --------------------------------------------------------------------------- # oTree BaseConstants (referenced by oTree internals) # --------------------------------------------------------------------------- class Constants(BaseConstants): name_in_url = 'dm_experiment' players_per_group = None num_rounds = 1 # --------------------------------------------------------------------------- # Subsession — runs once per session round; good place for session-wide setup # --------------------------------------------------------------------------- class Subsession(BaseSubsession): def creating_session(self): """Called once when the session is created. Initialise the rotating condition counter if it doesn't exist yet.""" if 'condition_counter' not in self.session.vars: self.session.vars['condition_counter'] = 0 # --------------------------------------------------------------------------- # Group — not used in this individual-level experiment # --------------------------------------------------------------------------- class Group(BaseGroup): pass # --------------------------------------------------------------------------- # Player — one record per participant # --------------------------------------------------------------------------- class Player(BasePlayer): #Prolific ID prolific_id = models.StringField(blank=True) # ------------------------------------------------------------------ # Debug fields (only used when debug=True in settings.py) # ------------------------------------------------------------------ debug_condition = models.StringField( choices=[ ['threshold', 'Threshold'], ['staircase', 'Staircase'], ['full_MPL', 'Full MPL'], ], widget=widgets.RadioSelect, label='Condition', blank=True, ) debug_task_order = models.StringField( choices=[ ['dictator_first', 'Allocation Task first, then Lottery Task'], ['risk_first', 'Lottery Task first, then Allocation Task'], ], widget=widgets.RadioSelect, label='Task order', blank=True, ) # ------------------------------------------------------------------ # Consent # ------------------------------------------------------------------ consent = models.StringField( choices=[['consent', 'I consent'], ['not_consent', 'I do not consent']], widget=widgets.RadioSelect, label='Please indicate your decision:', ) # ------------------------------------------------------------------ # Condition assignment # ------------------------------------------------------------------ condition = models.StringField() # 'threshold' | 'staircase' | 'full_MPL' # ------------------------------------------------------------------ # General comprehension question wrong-answer counters # These are not shown to participants; they track error counts. # ------------------------------------------------------------------ gen_q1_errors = models.IntegerField(initial=0) gen_q2_errors = models.IntegerField(initial=0) gen_q3_errors = models.IntegerField(initial=0) gen_q4_errors = models.IntegerField(initial=0) # ------------------------------------------------------------------ # General comprehension questions (answers) # Replace with real questions later; placeholders here. # ------------------------------------------------------------------ gen_q1 = models.StringField(choices=[['a',''],['b',''],['c','']], widget=widgets.RadioSelect, label='', blank=True) gen_q2 = models.StringField(choices=[['a',''],['b',''],['c','']], widget=widgets.RadioSelect, label='', blank=True) gen_q3 = models.StringField(choices=[['a',''],['b',''],['c','']], widget=widgets.RadioSelect, label='', blank=True) gen_q4 = models.StringField(choices=[['a',''],['b',''],['c','']], widget=widgets.RadioSelect, label='', blank=True) # ------------------------------------------------------------------ # Condition-specific comprehension question wrong-answer counters # Threshold has 4 questions, staircase and full_MPL have 3 each. # ------------------------------------------------------------------ cond_q1_errors = models.IntegerField(initial=0) cond_q2_errors = models.IntegerField(initial=0) cond_q3_errors = models.IntegerField(initial=0) cond_q4_errors = models.IntegerField(initial=0) # threshold only # Condition-specific comprehension answers # All questions have 3 options: a, b, c cond_q1 = models.StringField(choices=[['a',''], ['b',''], ['c','']], widget=widgets.RadioSelect, label='', blank=True) cond_q2 = models.StringField(choices=[['a',''], ['b',''], ['c','']], widget=widgets.RadioSelect, label='', blank=True) cond_q3 = models.StringField(choices=[['a',''], ['b',''], ['c','']], widget=widgets.RadioSelect, label='', blank=True) cond_q4 = models.StringField(choices=[['a',''], ['b',''], ['c','']], widget=widgets.RadioSelect, label='', blank=True) # ------------------------------------------------------------------ # Task ordering: which block comes first ('dictator' or 'risk') # ------------------------------------------------------------------ task_order = models.StringField() # JSON list e.g. '["dictator","risk"]' # ------------------------------------------------------------------ # Decision data (stored as JSON blobs for flexibility) # Each blob is a list of decision records (see pages.py for structure). # ------------------------------------------------------------------ dictator_decisions = models.LongStringField(initial='[]') risk_decisions = models.LongStringField(initial='[]') # Left/right assignment for each pair (stored as JSON) dictator_lr_assignment = models.LongStringField(initial='[]') risk_lr_assignment = models.LongStringField(initial='[]') # Staircase state (only used in staircase condition) staircase_state = models.LongStringField(initial='{}') # ------------------------------------------------------------------ # Final decisions comprehension (before consistency checks) # ------------------------------------------------------------------ final_q1 = models.StringField(choices=[['a',''],['b',''],['c','']], widget=widgets.RadioSelect, label='', blank=True) final_q2 = models.StringField(choices=[['a',''],['b',''],['c','']], widget=widgets.RadioSelect, label='', blank=True) final_q1_errors = models.IntegerField(initial=0) final_q2_errors = models.IntegerField(initial=0) # ------------------------------------------------------------------ # Allocation Task comprehension questions # ------------------------------------------------------------------ dict_q1 = models.StringField(choices=[['a',''],['b',''],['c','']], widget=widgets.RadioSelect, label='', blank=True) dict_q2 = models.StringField(choices=[['a',''],['b',''],['c','']], widget=widgets.RadioSelect, label='', blank=True) dict_q1_errors = models.IntegerField(initial=0) dict_q2_errors = models.IntegerField(initial=0) # ------------------------------------------------------------------ # Lottery Task comprehension question # ------------------------------------------------------------------ risk_q1 = models.StringField(choices=[['a',''],['b',''],['c','']], widget=widgets.RadioSelect, label='', blank=True) risk_q1_errors = models.IntegerField(initial=0) # ------------------------------------------------------------------ # Consistency checks # ------------------------------------------------------------------ consistency_decisions = models.LongStringField(initial='[]') # ------------------------------------------------------------------ # Per-situation per-split choice fields # For each task (dictator / risk), each of the 15 situations, and each # of the 11 possible splits (0–10 people choosing left), we store the # option_id that the participant would choose given that split. # Field naming: {task}_sit_{situation_index}_split_{n_left} # task : 'dictator' or 'risk' # situation_index : 0–14 (matches the pair order in DICTATOR_PAIRS / RISK_PAIRS) # n_left : 0–10 (number of people in the earlier group who chose left) # ------------------------------------------------------------------ dictator_sit_0_split_0 = models.StringField(blank=True, initial="") dictator_sit_0_split_1 = models.StringField(blank=True, initial="") dictator_sit_0_split_2 = models.StringField(blank=True, initial="") dictator_sit_0_split_3 = models.StringField(blank=True, initial="") dictator_sit_0_split_4 = models.StringField(blank=True, initial="") dictator_sit_0_split_5 = models.StringField(blank=True, initial="") dictator_sit_0_split_6 = models.StringField(blank=True, initial="") dictator_sit_0_split_7 = models.StringField(blank=True, initial="") dictator_sit_0_split_8 = models.StringField(blank=True, initial="") dictator_sit_0_split_9 = models.StringField(blank=True, initial="") dictator_sit_0_split_10 = models.StringField(blank=True, initial="") dictator_sit_1_split_0 = models.StringField(blank=True, initial="") dictator_sit_1_split_1 = models.StringField(blank=True, initial="") dictator_sit_1_split_2 = models.StringField(blank=True, initial="") dictator_sit_1_split_3 = models.StringField(blank=True, initial="") dictator_sit_1_split_4 = models.StringField(blank=True, initial="") dictator_sit_1_split_5 = models.StringField(blank=True, initial="") dictator_sit_1_split_6 = models.StringField(blank=True, initial="") dictator_sit_1_split_7 = models.StringField(blank=True, initial="") dictator_sit_1_split_8 = models.StringField(blank=True, initial="") dictator_sit_1_split_9 = models.StringField(blank=True, initial="") dictator_sit_1_split_10 = models.StringField(blank=True, initial="") dictator_sit_2_split_0 = models.StringField(blank=True, initial="") dictator_sit_2_split_1 = models.StringField(blank=True, initial="") dictator_sit_2_split_2 = models.StringField(blank=True, initial="") dictator_sit_2_split_3 = models.StringField(blank=True, initial="") dictator_sit_2_split_4 = models.StringField(blank=True, initial="") dictator_sit_2_split_5 = models.StringField(blank=True, initial="") dictator_sit_2_split_6 = models.StringField(blank=True, initial="") dictator_sit_2_split_7 = models.StringField(blank=True, initial="") dictator_sit_2_split_8 = models.StringField(blank=True, initial="") dictator_sit_2_split_9 = models.StringField(blank=True, initial="") dictator_sit_2_split_10 = models.StringField(blank=True, initial="") dictator_sit_3_split_0 = models.StringField(blank=True, initial="") dictator_sit_3_split_1 = models.StringField(blank=True, initial="") dictator_sit_3_split_2 = models.StringField(blank=True, initial="") dictator_sit_3_split_3 = models.StringField(blank=True, initial="") dictator_sit_3_split_4 = models.StringField(blank=True, initial="") dictator_sit_3_split_5 = models.StringField(blank=True, initial="") dictator_sit_3_split_6 = models.StringField(blank=True, initial="") dictator_sit_3_split_7 = models.StringField(blank=True, initial="") dictator_sit_3_split_8 = models.StringField(blank=True, initial="") dictator_sit_3_split_9 = models.StringField(blank=True, initial="") dictator_sit_3_split_10 = models.StringField(blank=True, initial="") dictator_sit_4_split_0 = models.StringField(blank=True, initial="") dictator_sit_4_split_1 = models.StringField(blank=True, initial="") dictator_sit_4_split_2 = models.StringField(blank=True, initial="") dictator_sit_4_split_3 = models.StringField(blank=True, initial="") dictator_sit_4_split_4 = models.StringField(blank=True, initial="") dictator_sit_4_split_5 = models.StringField(blank=True, initial="") dictator_sit_4_split_6 = models.StringField(blank=True, initial="") dictator_sit_4_split_7 = models.StringField(blank=True, initial="") dictator_sit_4_split_8 = models.StringField(blank=True, initial="") dictator_sit_4_split_9 = models.StringField(blank=True, initial="") dictator_sit_4_split_10 = models.StringField(blank=True, initial="") dictator_sit_5_split_0 = models.StringField(blank=True, initial="") dictator_sit_5_split_1 = models.StringField(blank=True, initial="") dictator_sit_5_split_2 = models.StringField(blank=True, initial="") dictator_sit_5_split_3 = models.StringField(blank=True, initial="") dictator_sit_5_split_4 = models.StringField(blank=True, initial="") dictator_sit_5_split_5 = models.StringField(blank=True, initial="") dictator_sit_5_split_6 = models.StringField(blank=True, initial="") dictator_sit_5_split_7 = models.StringField(blank=True, initial="") dictator_sit_5_split_8 = models.StringField(blank=True, initial="") dictator_sit_5_split_9 = models.StringField(blank=True, initial="") dictator_sit_5_split_10 = models.StringField(blank=True, initial="") dictator_sit_6_split_0 = models.StringField(blank=True, initial="") dictator_sit_6_split_1 = models.StringField(blank=True, initial="") dictator_sit_6_split_2 = models.StringField(blank=True, initial="") dictator_sit_6_split_3 = models.StringField(blank=True, initial="") dictator_sit_6_split_4 = models.StringField(blank=True, initial="") dictator_sit_6_split_5 = models.StringField(blank=True, initial="") dictator_sit_6_split_6 = models.StringField(blank=True, initial="") dictator_sit_6_split_7 = models.StringField(blank=True, initial="") dictator_sit_6_split_8 = models.StringField(blank=True, initial="") dictator_sit_6_split_9 = models.StringField(blank=True, initial="") dictator_sit_6_split_10 = models.StringField(blank=True, initial="") dictator_sit_7_split_0 = models.StringField(blank=True, initial="") dictator_sit_7_split_1 = models.StringField(blank=True, initial="") dictator_sit_7_split_2 = models.StringField(blank=True, initial="") dictator_sit_7_split_3 = models.StringField(blank=True, initial="") dictator_sit_7_split_4 = models.StringField(blank=True, initial="") dictator_sit_7_split_5 = models.StringField(blank=True, initial="") dictator_sit_7_split_6 = models.StringField(blank=True, initial="") dictator_sit_7_split_7 = models.StringField(blank=True, initial="") dictator_sit_7_split_8 = models.StringField(blank=True, initial="") dictator_sit_7_split_9 = models.StringField(blank=True, initial="") dictator_sit_7_split_10 = models.StringField(blank=True, initial="") dictator_sit_8_split_0 = models.StringField(blank=True, initial="") dictator_sit_8_split_1 = models.StringField(blank=True, initial="") dictator_sit_8_split_2 = models.StringField(blank=True, initial="") dictator_sit_8_split_3 = models.StringField(blank=True, initial="") dictator_sit_8_split_4 = models.StringField(blank=True, initial="") dictator_sit_8_split_5 = models.StringField(blank=True, initial="") dictator_sit_8_split_6 = models.StringField(blank=True, initial="") dictator_sit_8_split_7 = models.StringField(blank=True, initial="") dictator_sit_8_split_8 = models.StringField(blank=True, initial="") dictator_sit_8_split_9 = models.StringField(blank=True, initial="") dictator_sit_8_split_10 = models.StringField(blank=True, initial="") dictator_sit_9_split_0 = models.StringField(blank=True, initial="") dictator_sit_9_split_1 = models.StringField(blank=True, initial="") dictator_sit_9_split_2 = models.StringField(blank=True, initial="") dictator_sit_9_split_3 = models.StringField(blank=True, initial="") dictator_sit_9_split_4 = models.StringField(blank=True, initial="") dictator_sit_9_split_5 = models.StringField(blank=True, initial="") dictator_sit_9_split_6 = models.StringField(blank=True, initial="") dictator_sit_9_split_7 = models.StringField(blank=True, initial="") dictator_sit_9_split_8 = models.StringField(blank=True, initial="") dictator_sit_9_split_9 = models.StringField(blank=True, initial="") dictator_sit_9_split_10 = models.StringField(blank=True, initial="") dictator_sit_10_split_0 = models.StringField(blank=True, initial="") dictator_sit_10_split_1 = models.StringField(blank=True, initial="") dictator_sit_10_split_2 = models.StringField(blank=True, initial="") dictator_sit_10_split_3 = models.StringField(blank=True, initial="") dictator_sit_10_split_4 = models.StringField(blank=True, initial="") dictator_sit_10_split_5 = models.StringField(blank=True, initial="") dictator_sit_10_split_6 = models.StringField(blank=True, initial="") dictator_sit_10_split_7 = models.StringField(blank=True, initial="") dictator_sit_10_split_8 = models.StringField(blank=True, initial="") dictator_sit_10_split_9 = models.StringField(blank=True, initial="") dictator_sit_10_split_10 = models.StringField(blank=True, initial="") dictator_sit_11_split_0 = models.StringField(blank=True, initial="") dictator_sit_11_split_1 = models.StringField(blank=True, initial="") dictator_sit_11_split_2 = models.StringField(blank=True, initial="") dictator_sit_11_split_3 = models.StringField(blank=True, initial="") dictator_sit_11_split_4 = models.StringField(blank=True, initial="") dictator_sit_11_split_5 = models.StringField(blank=True, initial="") dictator_sit_11_split_6 = models.StringField(blank=True, initial="") dictator_sit_11_split_7 = models.StringField(blank=True, initial="") dictator_sit_11_split_8 = models.StringField(blank=True, initial="") dictator_sit_11_split_9 = models.StringField(blank=True, initial="") dictator_sit_11_split_10 = models.StringField(blank=True, initial="") dictator_sit_12_split_0 = models.StringField(blank=True, initial="") dictator_sit_12_split_1 = models.StringField(blank=True, initial="") dictator_sit_12_split_2 = models.StringField(blank=True, initial="") dictator_sit_12_split_3 = models.StringField(blank=True, initial="") dictator_sit_12_split_4 = models.StringField(blank=True, initial="") dictator_sit_12_split_5 = models.StringField(blank=True, initial="") dictator_sit_12_split_6 = models.StringField(blank=True, initial="") dictator_sit_12_split_7 = models.StringField(blank=True, initial="") dictator_sit_12_split_8 = models.StringField(blank=True, initial="") dictator_sit_12_split_9 = models.StringField(blank=True, initial="") dictator_sit_12_split_10 = models.StringField(blank=True, initial="") dictator_sit_13_split_0 = models.StringField(blank=True, initial="") dictator_sit_13_split_1 = models.StringField(blank=True, initial="") dictator_sit_13_split_2 = models.StringField(blank=True, initial="") dictator_sit_13_split_3 = models.StringField(blank=True, initial="") dictator_sit_13_split_4 = models.StringField(blank=True, initial="") dictator_sit_13_split_5 = models.StringField(blank=True, initial="") dictator_sit_13_split_6 = models.StringField(blank=True, initial="") dictator_sit_13_split_7 = models.StringField(blank=True, initial="") dictator_sit_13_split_8 = models.StringField(blank=True, initial="") dictator_sit_13_split_9 = models.StringField(blank=True, initial="") dictator_sit_13_split_10 = models.StringField(blank=True, initial="") dictator_sit_14_split_0 = models.StringField(blank=True, initial="") dictator_sit_14_split_1 = models.StringField(blank=True, initial="") dictator_sit_14_split_2 = models.StringField(blank=True, initial="") dictator_sit_14_split_3 = models.StringField(blank=True, initial="") dictator_sit_14_split_4 = models.StringField(blank=True, initial="") dictator_sit_14_split_5 = models.StringField(blank=True, initial="") dictator_sit_14_split_6 = models.StringField(blank=True, initial="") dictator_sit_14_split_7 = models.StringField(blank=True, initial="") dictator_sit_14_split_8 = models.StringField(blank=True, initial="") dictator_sit_14_split_9 = models.StringField(blank=True, initial="") dictator_sit_14_split_10 = models.StringField(blank=True, initial="") risk_sit_0_split_0 = models.StringField(blank=True, initial="") risk_sit_0_split_1 = models.StringField(blank=True, initial="") risk_sit_0_split_2 = models.StringField(blank=True, initial="") risk_sit_0_split_3 = models.StringField(blank=True, initial="") risk_sit_0_split_4 = models.StringField(blank=True, initial="") risk_sit_0_split_5 = models.StringField(blank=True, initial="") risk_sit_0_split_6 = models.StringField(blank=True, initial="") risk_sit_0_split_7 = models.StringField(blank=True, initial="") risk_sit_0_split_8 = models.StringField(blank=True, initial="") risk_sit_0_split_9 = models.StringField(blank=True, initial="") risk_sit_0_split_10 = models.StringField(blank=True, initial="") risk_sit_1_split_0 = models.StringField(blank=True, initial="") risk_sit_1_split_1 = models.StringField(blank=True, initial="") risk_sit_1_split_2 = models.StringField(blank=True, initial="") risk_sit_1_split_3 = models.StringField(blank=True, initial="") risk_sit_1_split_4 = models.StringField(blank=True, initial="") risk_sit_1_split_5 = models.StringField(blank=True, initial="") risk_sit_1_split_6 = models.StringField(blank=True, initial="") risk_sit_1_split_7 = models.StringField(blank=True, initial="") risk_sit_1_split_8 = models.StringField(blank=True, initial="") risk_sit_1_split_9 = models.StringField(blank=True, initial="") risk_sit_1_split_10 = models.StringField(blank=True, initial="") risk_sit_2_split_0 = models.StringField(blank=True, initial="") risk_sit_2_split_1 = models.StringField(blank=True, initial="") risk_sit_2_split_2 = models.StringField(blank=True, initial="") risk_sit_2_split_3 = models.StringField(blank=True, initial="") risk_sit_2_split_4 = models.StringField(blank=True, initial="") risk_sit_2_split_5 = models.StringField(blank=True, initial="") risk_sit_2_split_6 = models.StringField(blank=True, initial="") risk_sit_2_split_7 = models.StringField(blank=True, initial="") risk_sit_2_split_8 = models.StringField(blank=True, initial="") risk_sit_2_split_9 = models.StringField(blank=True, initial="") risk_sit_2_split_10 = models.StringField(blank=True, initial="") risk_sit_3_split_0 = models.StringField(blank=True, initial="") risk_sit_3_split_1 = models.StringField(blank=True, initial="") risk_sit_3_split_2 = models.StringField(blank=True, initial="") risk_sit_3_split_3 = models.StringField(blank=True, initial="") risk_sit_3_split_4 = models.StringField(blank=True, initial="") risk_sit_3_split_5 = models.StringField(blank=True, initial="") risk_sit_3_split_6 = models.StringField(blank=True, initial="") risk_sit_3_split_7 = models.StringField(blank=True, initial="") risk_sit_3_split_8 = models.StringField(blank=True, initial="") risk_sit_3_split_9 = models.StringField(blank=True, initial="") risk_sit_3_split_10 = models.StringField(blank=True, initial="") risk_sit_4_split_0 = models.StringField(blank=True, initial="") risk_sit_4_split_1 = models.StringField(blank=True, initial="") risk_sit_4_split_2 = models.StringField(blank=True, initial="") risk_sit_4_split_3 = models.StringField(blank=True, initial="") risk_sit_4_split_4 = models.StringField(blank=True, initial="") risk_sit_4_split_5 = models.StringField(blank=True, initial="") risk_sit_4_split_6 = models.StringField(blank=True, initial="") risk_sit_4_split_7 = models.StringField(blank=True, initial="") risk_sit_4_split_8 = models.StringField(blank=True, initial="") risk_sit_4_split_9 = models.StringField(blank=True, initial="") risk_sit_4_split_10 = models.StringField(blank=True, initial="") risk_sit_5_split_0 = models.StringField(blank=True, initial="") risk_sit_5_split_1 = models.StringField(blank=True, initial="") risk_sit_5_split_2 = models.StringField(blank=True, initial="") risk_sit_5_split_3 = models.StringField(blank=True, initial="") risk_sit_5_split_4 = models.StringField(blank=True, initial="") risk_sit_5_split_5 = models.StringField(blank=True, initial="") risk_sit_5_split_6 = models.StringField(blank=True, initial="") risk_sit_5_split_7 = models.StringField(blank=True, initial="") risk_sit_5_split_8 = models.StringField(blank=True, initial="") risk_sit_5_split_9 = models.StringField(blank=True, initial="") risk_sit_5_split_10 = models.StringField(blank=True, initial="") risk_sit_6_split_0 = models.StringField(blank=True, initial="") risk_sit_6_split_1 = models.StringField(blank=True, initial="") risk_sit_6_split_2 = models.StringField(blank=True, initial="") risk_sit_6_split_3 = models.StringField(blank=True, initial="") risk_sit_6_split_4 = models.StringField(blank=True, initial="") risk_sit_6_split_5 = models.StringField(blank=True, initial="") risk_sit_6_split_6 = models.StringField(blank=True, initial="") risk_sit_6_split_7 = models.StringField(blank=True, initial="") risk_sit_6_split_8 = models.StringField(blank=True, initial="") risk_sit_6_split_9 = models.StringField(blank=True, initial="") risk_sit_6_split_10 = models.StringField(blank=True, initial="") risk_sit_7_split_0 = models.StringField(blank=True, initial="") risk_sit_7_split_1 = models.StringField(blank=True, initial="") risk_sit_7_split_2 = models.StringField(blank=True, initial="") risk_sit_7_split_3 = models.StringField(blank=True, initial="") risk_sit_7_split_4 = models.StringField(blank=True, initial="") risk_sit_7_split_5 = models.StringField(blank=True, initial="") risk_sit_7_split_6 = models.StringField(blank=True, initial="") risk_sit_7_split_7 = models.StringField(blank=True, initial="") risk_sit_7_split_8 = models.StringField(blank=True, initial="") risk_sit_7_split_9 = models.StringField(blank=True, initial="") risk_sit_7_split_10 = models.StringField(blank=True, initial="") risk_sit_8_split_0 = models.StringField(blank=True, initial="") risk_sit_8_split_1 = models.StringField(blank=True, initial="") risk_sit_8_split_2 = models.StringField(blank=True, initial="") risk_sit_8_split_3 = models.StringField(blank=True, initial="") risk_sit_8_split_4 = models.StringField(blank=True, initial="") risk_sit_8_split_5 = models.StringField(blank=True, initial="") risk_sit_8_split_6 = models.StringField(blank=True, initial="") risk_sit_8_split_7 = models.StringField(blank=True, initial="") risk_sit_8_split_8 = models.StringField(blank=True, initial="") risk_sit_8_split_9 = models.StringField(blank=True, initial="") risk_sit_8_split_10 = models.StringField(blank=True, initial="") risk_sit_9_split_0 = models.StringField(blank=True, initial="") risk_sit_9_split_1 = models.StringField(blank=True, initial="") risk_sit_9_split_2 = models.StringField(blank=True, initial="") risk_sit_9_split_3 = models.StringField(blank=True, initial="") risk_sit_9_split_4 = models.StringField(blank=True, initial="") risk_sit_9_split_5 = models.StringField(blank=True, initial="") risk_sit_9_split_6 = models.StringField(blank=True, initial="") risk_sit_9_split_7 = models.StringField(blank=True, initial="") risk_sit_9_split_8 = models.StringField(blank=True, initial="") risk_sit_9_split_9 = models.StringField(blank=True, initial="") risk_sit_9_split_10 = models.StringField(blank=True, initial="") risk_sit_10_split_0 = models.StringField(blank=True, initial="") risk_sit_10_split_1 = models.StringField(blank=True, initial="") risk_sit_10_split_2 = models.StringField(blank=True, initial="") risk_sit_10_split_3 = models.StringField(blank=True, initial="") risk_sit_10_split_4 = models.StringField(blank=True, initial="") risk_sit_10_split_5 = models.StringField(blank=True, initial="") risk_sit_10_split_6 = models.StringField(blank=True, initial="") risk_sit_10_split_7 = models.StringField(blank=True, initial="") risk_sit_10_split_8 = models.StringField(blank=True, initial="") risk_sit_10_split_9 = models.StringField(blank=True, initial="") risk_sit_10_split_10 = models.StringField(blank=True, initial="") risk_sit_11_split_0 = models.StringField(blank=True, initial="") risk_sit_11_split_1 = models.StringField(blank=True, initial="") risk_sit_11_split_2 = models.StringField(blank=True, initial="") risk_sit_11_split_3 = models.StringField(blank=True, initial="") risk_sit_11_split_4 = models.StringField(blank=True, initial="") risk_sit_11_split_5 = models.StringField(blank=True, initial="") risk_sit_11_split_6 = models.StringField(blank=True, initial="") risk_sit_11_split_7 = models.StringField(blank=True, initial="") risk_sit_11_split_8 = models.StringField(blank=True, initial="") risk_sit_11_split_9 = models.StringField(blank=True, initial="") risk_sit_11_split_10 = models.StringField(blank=True, initial="") risk_sit_12_split_0 = models.StringField(blank=True, initial="") risk_sit_12_split_1 = models.StringField(blank=True, initial="") risk_sit_12_split_2 = models.StringField(blank=True, initial="") risk_sit_12_split_3 = models.StringField(blank=True, initial="") risk_sit_12_split_4 = models.StringField(blank=True, initial="") risk_sit_12_split_5 = models.StringField(blank=True, initial="") risk_sit_12_split_6 = models.StringField(blank=True, initial="") risk_sit_12_split_7 = models.StringField(blank=True, initial="") risk_sit_12_split_8 = models.StringField(blank=True, initial="") risk_sit_12_split_9 = models.StringField(blank=True, initial="") risk_sit_12_split_10 = models.StringField(blank=True, initial="") risk_sit_13_split_0 = models.StringField(blank=True, initial="") risk_sit_13_split_1 = models.StringField(blank=True, initial="") risk_sit_13_split_2 = models.StringField(blank=True, initial="") risk_sit_13_split_3 = models.StringField(blank=True, initial="") risk_sit_13_split_4 = models.StringField(blank=True, initial="") risk_sit_13_split_5 = models.StringField(blank=True, initial="") risk_sit_13_split_6 = models.StringField(blank=True, initial="") risk_sit_13_split_7 = models.StringField(blank=True, initial="") risk_sit_13_split_8 = models.StringField(blank=True, initial="") risk_sit_13_split_9 = models.StringField(blank=True, initial="") risk_sit_13_split_10 = models.StringField(blank=True, initial="") risk_sit_14_split_0 = models.StringField(blank=True, initial="") risk_sit_14_split_1 = models.StringField(blank=True, initial="") risk_sit_14_split_2 = models.StringField(blank=True, initial="") risk_sit_14_split_3 = models.StringField(blank=True, initial="") risk_sit_14_split_4 = models.StringField(blank=True, initial="") risk_sit_14_split_5 = models.StringField(blank=True, initial="") risk_sit_14_split_6 = models.StringField(blank=True, initial="") risk_sit_14_split_7 = models.StringField(blank=True, initial="") risk_sit_14_split_8 = models.StringField(blank=True, initial="") risk_sit_14_split_9 = models.StringField(blank=True, initial="") risk_sit_14_split_10 = models.StringField(blank=True, initial="") # ------------------------------------------------------------------ # Payoff details # ------------------------------------------------------------------ selected_task = models.StringField() # 'dictator' | 'risk' | 'consistency' selected_pair_index = models.IntegerField() selected_option_chosen = models.StringField() # option_id chosen by participant # Dictator-specific payout to "other" other_payoff = models.FloatField(initial=0) # Risk-specific: outcome realised risk_outcome_realised = models.FloatField(initial=0) # Rich payoff explanation (JSON) passed to FinalPayoff template payoff_explanation = models.LongStringField(initial='{}') # ------------------------------------------------------------------ # Ex post questionnaire # ------------------------------------------------------------------ # Open text fields q_strategy = models.LongStringField(blank=True) # how they made choices q_comments = models.LongStringField(blank=True) # general comments # Instruction understanding (1=not at all, 7=very well) q_understood_general = models.IntegerField( choices=[[i, str(i)] for i in range(0, 11)], widget=widgets.RadioSelectHorizontal, label="", blank=True ) q_understood_lottery = models.IntegerField( choices=[[i, str(i)] for i in range(0, 11)], widget=widgets.RadioSelectHorizontal, label="", blank=True ) q_understood_allocation = models.IntegerField( choices=[[i, str(i)] for i in range(0, 11)], widget=widgets.RadioSelectHorizontal, label="", blank=True ) q_understood_decision = models.IntegerField( choices=[[i, str(i)] for i in range(0, 11)], widget=widgets.RadioSelectHorizontal, label="", blank=True ) q_understood_actual = models.IntegerField( choices=[[i, str(i)] for i in range(0, 11)], widget=widgets.RadioSelectHorizontal, label="", blank=True ) # Task difficulty and effort (ExPost section 4) q_difficulty = models.IntegerField( choices=[[i, str(i)] for i in range(0, 11)], widget=widgets.RadioSelectHorizontal, label="", blank=True ) q_effort = models.IntegerField( choices=[[i, str(i)] for i in range(0, 11)], widget=widgets.RadioSelectHorizontal, label="", blank=True ) # Attention checks (blank=True so no forced response) attention_check_maj = models.IntegerField(blank=True) attention_check_snes = models.IntegerField(blank=True) # Placeholder fields for survey scales — add more as needed # q_scale1_item1 = models.IntegerField(...) # ------------------------------------------------------------------ # Cognitive Uncertainty (0-10, asked after each task block) # ------------------------------------------------------------------ cognitive_uncertainty_dictator = models.IntegerField(choices=[[i,str(i)] for i in range(0,11)], widget=widgets.RadioSelectHorizontal, label="") cognitive_uncertainty_risk = models.IntegerField(choices=[[i,str(i)] for i in range(0,11)], widget=widgets.RadioSelectHorizontal, label="") # ------------------------------------------------------------------ # Inclusion of Other in Self (IOS Scale, Aron et al., 1992) # 1-7 Venn diagram overlap (1=no overlap, 7=near-complete overlap) # ------------------------------------------------------------------ ios_scale = models.IntegerField(blank=True) # ------------------------------------------------------------------ # Majority vs. Individual Norm Sensitivity Scale (12 items, 1-7) # ------------------------------------------------------------------ maj_q1 = models.IntegerField(choices=[[i,str(i)] for i in range(1,8)], widget=widgets.RadioSelectHorizontal, label="") maj_q2 = models.IntegerField(choices=[[i,str(i)] for i in range(1,8)], widget=widgets.RadioSelectHorizontal, label="") maj_q3 = models.IntegerField(choices=[[i,str(i)] for i in range(1,8)], widget=widgets.RadioSelectHorizontal, label="") maj_q4 = models.IntegerField(choices=[[i,str(i)] for i in range(1,8)], widget=widgets.RadioSelectHorizontal, label="") maj_q5 = models.IntegerField(choices=[[i,str(i)] for i in range(1,8)], widget=widgets.RadioSelectHorizontal, label="") maj_q6 = models.IntegerField(choices=[[i,str(i)] for i in range(1,8)], widget=widgets.RadioSelectHorizontal, label="") maj_q7 = models.IntegerField(choices=[[i,str(i)] for i in range(1,8)], widget=widgets.RadioSelectHorizontal, label="") maj_q8 = models.IntegerField(choices=[[i,str(i)] for i in range(1,8)], widget=widgets.RadioSelectHorizontal, label="") maj_q9 = models.IntegerField(choices=[[i,str(i)] for i in range(1,8)], widget=widgets.RadioSelectHorizontal, label="") maj_q10 = models.IntegerField(choices=[[i,str(i)] for i in range(1,8)], widget=widgets.RadioSelectHorizontal, label="") maj_q11 = models.IntegerField(choices=[[i,str(i)] for i in range(1,8)], widget=widgets.RadioSelectHorizontal, label="") maj_q12 = models.IntegerField(choices=[[i,str(i)] for i in range(1,8)], widget=widgets.RadioSelectHorizontal, label="") # ------------------------------------------------------------------ # Social Norm Espousal Scale (SNES; Bizer et al., 2014) # 14 items, 1-7; items 2,3,8,12,13 are reverse-coded # ------------------------------------------------------------------ snes_q1 = models.IntegerField(choices=[[i,str(i)] for i in range(1,8)], widget=widgets.RadioSelectHorizontal, label="") snes_q2 = models.IntegerField(choices=[[i,str(i)] for i in range(1,8)], widget=widgets.RadioSelectHorizontal, label="") snes_q3 = models.IntegerField(choices=[[i,str(i)] for i in range(1,8)], widget=widgets.RadioSelectHorizontal, label="") snes_q4 = models.IntegerField(choices=[[i,str(i)] for i in range(1,8)], widget=widgets.RadioSelectHorizontal, label="") snes_q5 = models.IntegerField(choices=[[i,str(i)] for i in range(1,8)], widget=widgets.RadioSelectHorizontal, label="") snes_q6 = models.IntegerField(choices=[[i,str(i)] for i in range(1,8)], widget=widgets.RadioSelectHorizontal, label="") snes_q7 = models.IntegerField(choices=[[i,str(i)] for i in range(1,8)], widget=widgets.RadioSelectHorizontal, label="") snes_q8 = models.IntegerField(choices=[[i,str(i)] for i in range(1,8)], widget=widgets.RadioSelectHorizontal, label="") snes_q9 = models.IntegerField(choices=[[i,str(i)] for i in range(1,8)], widget=widgets.RadioSelectHorizontal, label="") snes_q10 = models.IntegerField(choices=[[i,str(i)] for i in range(1,8)], widget=widgets.RadioSelectHorizontal, label="") snes_q11 = models.IntegerField(choices=[[i,str(i)] for i in range(1,8)], widget=widgets.RadioSelectHorizontal, label="") snes_q12 = models.IntegerField(choices=[[i,str(i)] for i in range(1,8)], widget=widgets.RadioSelectHorizontal, label="") snes_q13 = models.IntegerField(choices=[[i,str(i)] for i in range(1,8)], widget=widgets.RadioSelectHorizontal, label="") snes_q14 = models.IntegerField(choices=[[i,str(i)] for i in range(1,8)], widget=widgets.RadioSelectHorizontal, label="") # ------------------------------------------------------------------ # Contrarian self-identification (binary: yes / no) # ------------------------------------------------------------------ contrarian = models.StringField( choices=[["yes", "Yes"], ["no", "No"]], widget=widgets.RadioSelectHorizontal, label="", blank=True, ) # ------------------------------------------------------------------ # Demographics # ------------------------------------------------------------------ demo_age = models.IntegerField(blank=True, label="") demo_gender = models.StringField( choices=[ ["female", "Female"], ["male", "Male"], ["nonbinary", "Non-binary / third gender"], ["self_describe", "Prefer to self-describe"], ["no_answer", "Prefer not to say"], ], widget=widgets.RadioSelect, label="", blank=True, ) demo_gender_self = models.StringField(blank=True, label="") demo_education = models.StringField( choices=[ ["none", "No formal qualifications"], ["primary", "Primary school"], ["secondary", "Secondary school (i.e., High school or equivalent)"], ["vocational", "Vocational / technical qualification"], ["bachelor", "Bachelor degree or equivalent"], ["postgrad", "Postgraduate degree (Masters, PhD, or equivalent)"], ["no_answer", "Prefer not to say"], ], widget=widgets.RadioSelect, label="", blank=True, ) # MacArthur Scale of Subjective Social Status (1-10 ladder) demo_ses = models.IntegerField(choices=[[i,str(i)] for i in range(1,11)], widget=widgets.RadioSelectHorizontal, label="") # ------------------------------------------------------------------ # Page timestamps (stored as JSON: {page_name: {start, end, duration}}) # ------------------------------------------------------------------ page_timestamps = models.LongStringField(initial='{}') # ================================================================== # Helper methods # ================================================================== def assign_condition(self): """Assign condition using a rotating session-wide counter (balanced).""" counter = self.session.vars['condition_counter'] self.condition = CONDITIONS[counter % NUM_CONDITIONS] self.session.vars['condition_counter'] = counter + 1 def initialise_task_order(self): """Randomise whether dictator or risk comes first.""" order = ['dictator', 'risk'] random.shuffle(order) self.task_order = json.dumps(order) def get_task_order(self): raw = self.field_maybe_none('task_order') if not raw: self.initialise_task_order() self.initialise_lr_assignments() return json.loads(self.task_order) def initialise_lr_assignments(self): """For each pair in each task, randomly assign which option is 'left' vs 'right'. Stored as list of bools: True = option_a is left.""" d_assign = [random.choice([True, False]) for _ in DICTATOR_PAIRS] r_assign = [random.choice([True, False]) for _ in RISK_PAIRS] self.dictator_lr_assignment = json.dumps(d_assign) self.risk_lr_assignment = json.dumps(r_assign) def get_lr_assignments(self, task_type): if task_type == 'dictator': return json.loads(self.dictator_lr_assignment) return json.loads(self.risk_lr_assignment) def get_decisions(self, task_type): if task_type == 'dictator': return json.loads(self.dictator_decisions) return json.loads(self.risk_decisions) def save_decisions(self, task_type, decisions): blob = json.dumps(decisions) if task_type == 'dictator': self.dictator_decisions = blob else: self.risk_decisions = blob def get_pairs(self, task_type): return DICTATOR_PAIRS if task_type == 'dictator' else RISK_PAIRS def record_timestamp(self, page_name, start=None, end=None): """Store start/end/duration for a page in the page_timestamps field.""" ts = json.loads(self.page_timestamps) if page_name not in ts: ts[page_name] = {} if start is not None: ts[page_name]['start'] = start if end is not None: ts[page_name]['end'] = end s = ts[page_name].get('start') if s is not None: ts[page_name]['duration'] = end - s self.page_timestamps = json.dumps(ts) def populate_split_fields(self, task_type): """ After decisions are saved for a task block, fill the per-split fields. Fields are named {task}_sit_{pair_idx}_split_{n_chose_a} where: - pair_idx : global pair index (same for all participants) - n_chose_a : number of people (0-10) who chose option_A (the canonical first option in the pair from DICTATOR_PAIRS / RISK_PAIRS), regardless of which side it was shown on. This canonical framing makes fields comparable across participants even though left/right assignment is randomised. The stored value is the option_id chosen for that n_chose_a value. """ decisions = self.get_decisions(task_type) pairs = self.get_pairs(task_type) lr_assigns = self.get_lr_assignments(task_type) cond = self.condition for dec in decisions: # Use the global pair index so field names are consistent across # participants regardless of randomised presentation order. pair_idx = dec.get('situation_id', dec.get('situation_index')) if pair_idx is None or pair_idx >= len(pairs): continue pair = pairs[pair_idx] # Read left/right ids directly from the saved decision — these were # set by the JS at the time of presentation and are ground truth. left_id = dec.get('left_option_id') right_id = dec.get('right_option_id') a_is_left = dec.get('a_is_left', lr_assigns[pair_idx]) opt_a_id = pair['option_a']['option_id'] opt_b_id = pair['option_b']['option_id'] if cond == 'threshold': threshold = dec.get('threshold') if threshold is None: continue threshold = int(threshold) for n_chose_a in range(11): # Convert canonical n_chose_a to the n_left this participant saw n_left = n_chose_a if a_is_left else (10 - n_chose_a) chosen_id = left_id if n_left >= threshold else right_id setattr(self, f'{task_type}_sit_{pair_idx}_split_{n_chose_a}', chosen_id) elif cond == 'staircase': switching_point = dec.get('switching_point') if switching_point is None: continue switching_point = int(switching_point) for n_chose_a in range(11): n_left = n_chose_a if a_is_left else (10 - n_chose_a) chosen_id = left_id if n_left >= switching_point else right_id setattr(self, f'{task_type}_sit_{pair_idx}_split_{n_chose_a}', chosen_id) elif cond == 'full_MPL': # choices_map is keyed by n_left (as shown to participant). # Convert to canonical n_chose_a for storage. choices_map = dec.get('choices_map', {}) for n_chose_a in range(11): n_left = n_chose_a if a_is_left else (10 - n_chose_a) chosen_id = choices_map.get(str(n_left), '') setattr(self, f'{task_type}_sit_{pair_idx}_split_{n_chose_a}', chosen_id) def calculate_payoff(self): """ Randomly select one situation from all 38 (15 dictator + 15 risk + 8 consistency), determine the chosen option given actual norms, compute monetary payoff, and store a rich explanation dict for the FinalPayoff template. """ import random as rng # ── Build pool with equal weight ───────────────────────────────────── pool = [] d_decisions = json.loads(self.dictator_decisions) for dec in d_decisions: pair_idx = dec.get('situation_id', dec.get('situation_index')) if pair_idx is not None: pool.append(('main', 'dictator', pair_idx, dec)) r_decisions = json.loads(self.risk_decisions) for dec in r_decisions: pair_idx = dec.get('situation_id', dec.get('situation_index')) if pair_idx is not None: pool.append(('main', 'risk', pair_idx, dec)) c_decisions = json.loads(self.consistency_decisions) for i, dec in enumerate(c_decisions): pool.append(('consistency', None, i, dec)) if not pool: return # no data yet (debug / incomplete session) source, task_type, pair_idx, decision = rng.choice(pool) self.selected_task = task_type if source == 'main' else 'consistency' self.selected_pair_index = pair_idx # ── Determine chosen option and actual norm ─────────────────────────── exp = {} if source == 'consistency': check = CONSISTENCY_CHECKS[pair_idx] actual_task = check['task_type'] opt_a_id = check['option_a_id'] opt_b_id = check['option_b_id'] norm_key = (actual_task, opt_a_id, opt_b_id) n_chose_a = ACTUAL_NORMS.get(norm_key, 5) chosen_id = decision.get('chosen_option', '') exp['source'] = 'consistency' exp['actual_task'] = actual_task exp['n_chose_a'] = n_chose_a exp['opt_a_id'] = opt_a_id exp['opt_b_id'] = opt_b_id exp['chosen_id'] = chosen_id exp['chosen_side'] = 'A' if chosen_id == opt_a_id else 'B' exp['chose_left'] = False # not applicable for consistency but needed by template else: # main task actual_task = task_type pairs = self.get_pairs(task_type) pair = pairs[pair_idx] opt_a_id = pair['option_a']['option_id'] opt_b_id = pair['option_b']['option_id'] norm_key = (task_type, opt_a_id, opt_b_id) n_chose_a = ACTUAL_NORMS.get(norm_key, 5) lr_assign = self.get_lr_assignments(task_type) a_is_left = decision.get('a_is_left', lr_assign[pair_idx]) left_id = decision.get('left_option_id', opt_a_id if a_is_left else opt_b_id) right_id = decision.get('right_option_id', opt_b_id if a_is_left else opt_a_id) n_chose_left = n_chose_a if a_is_left else (10 - n_chose_a) cond = self.condition if cond == 'threshold': threshold = int(decision.get('threshold', 5)) chose_left = n_chose_left >= threshold chosen_id = left_id if chose_left else right_id exp['rule'] = ( f'Your threshold was {threshold}. {n_chose_left} out of 10 people ' f'in the earlier group chose the left option, which is ' f'{"at least" if chose_left else "fewer than"} your threshold of {threshold}.' ) exp['decision_type'] = 'threshold' exp['threshold'] = threshold elif cond == 'staircase': sp = int(decision.get('switching_point', 5)) chose_left = n_chose_left >= sp chosen_id = left_id if chose_left else right_id exp['rule'] = ( f'Your identified threshold was {sp}. {n_chose_left} out of 10 people ' f'in the earlier group chose the left option, which is ' f'{"at least" if chose_left else "fewer than"} your threshold of {sp}.' ) exp['decision_type'] = 'staircase' exp['threshold'] = sp else: # full_MPL choices_map = decision.get('choices_map', {}) chosen_id = choices_map.get(str(n_chose_left), left_id if a_is_left else right_id) chose_left = (chosen_id == left_id) exp['rule'] = ( f'For the split of {n_chose_left} out of 10 people choosing the left ' f'option, you chose the {"left" if chose_left else "right"} option.' ) exp['decision_type'] = 'full_MPL' exp['source'] = 'main' exp['actual_task'] = actual_task exp['n_chose_a'] = n_chose_a exp['n_chose_left'] = n_chose_left exp['opt_a_id'] = opt_a_id exp['opt_b_id'] = opt_b_id exp['chosen_id'] = chosen_id exp['a_is_left'] = a_is_left exp['chose_left'] = chose_left self.selected_option_chosen = exp.get('chosen_id', '') chosen_id = self.selected_option_chosen actual_task = exp.get('actual_task', 'dictator') # ── Compute monetary payoff ─────────────────────────────────────────── if actual_task == 'dictator': opt = DICTATOR_BY_ID.get(chosen_id, {}) own_pay = float(opt.get('own_payoff', 0)) other_pay = float(opt.get('transfer', 0)) * float(opt.get('multiplier', 1)) self.payoff = own_pay self.other_payoff = other_pay * MU_TO_GBP exp['own_payoff'] = own_pay exp['other_payoff'] = other_pay exp['transfer'] = float(opt.get('transfer', 0)) exp['multiplier'] = float(opt.get('multiplier', 1)) else: # risk opt = RISK_BY_ID.get(chosen_id, {}) p_left = float(opt.get('probability_left', 0.5)) if rng.random() < p_left: outcome = float(opt.get('left_outcome', 0)) exp['lottery_side'] = 'left' else: outcome = float(opt.get('right_outcome', 0)) exp['lottery_side'] = 'right' exp['left_outcome'] = float(opt.get('left_outcome', 0)) exp['right_outcome'] = float(opt.get('right_outcome', 0)) exp['prob_left'] = p_left exp['outcome'] = outcome self.risk_outcome_realised = outcome self.payoff = outcome # Store GBP bonus in oTree's built-in participant.payoff field self.participant.payoff = self.payoff * MU_TO_GBP self.payoff_explanation = json.dumps(exp)