# APP: oathexp (First Mover) # FILE: models.py from otree.api import ( models, widgets, BaseConstants, BaseSubsession, BaseGroup, BasePlayer, ) author = 'Zeyu Qiu' doc = """ Oath experiment - First Mover (leader) app """ class Constants(BaseConstants): name_in_url = 'decision_experiment' players_per_group = None num_rounds = 1 pounds = 1/20 # Exchange rate: 20 tokens = £1.00 showup_fee = 1.00 # Show-up fee in GBP endowment = 20 class Subsession(BaseSubsession): def creating_session(self): for p in self.get_players(): p.Prolific_ID = p.participant.label class Group(BaseGroup): pass class Player(BasePlayer): Prolific_ID = models.StringField(label="Prolific ID") recaptcha_score = models.FloatField(blank=True) recaptcha_token = models.LongStringField(blank=True) completed = models.BooleanField(initial=False) treatment = models.StringField(blank=True) # e.g. 'T1', 'T2', ... bonus_tokens = models.IntegerField(initial=0) # incentive tokens earned # Comprehension quiz quiz_payoff_a = models.IntegerField( label="What is the total payoff for group member A?", min=0, max=60 ) quiz_payoff_b = models.IntegerField( label="What is the total payoff for group member B?", min=0, max=60 ) quiz_payoff_c = models.IntegerField( label="What is the total payoff for group member C?", min=0, max=60 ) quiz_attempts = models.IntegerField(initial=0) control_question = models.StringField( choices=[ ['1', 'I agree to take part. Take me to the study.'], ['2', 'I do not agree to take part in this study. Take me back to Prolific.'], ], label="Do you agree to participate in this study?", widget=widgets.RadioSelect ) take_oath = models.BooleanField( label='Would you like to take the oath?', choices=[[True, 'Yes'], [False, 'No']], ) typed_oath = models.LongStringField( label='Please type the oath' ) typed_initials = models.StringField( label='Please type the first letter of your name' ) oath_signed = models.BooleanField( label='I confirm that I am taking this oath voluntarily and intend to honour it.', widget=widgets.CheckboxInput, blank=True, initial=False, ) contribution = models.IntegerField( label="How many tokens do you want to contribute to the group project (between 0 and 20 tokens)?", min=0, max=20 ) leader_belief_1 = models.IntegerField( label="On average, how many tokens do you think each Second Mover will contribute? (between 0 and 20 tokens)", min=0, max=20 ) leader_belief_2 = models.IntegerField( label="How many tokens do you think the Second Movers expect you to contribute (between 0 and 20 tokens)?", min=0, max=20 ) oath_recall = models.LongStringField( label="Please explain what the oath was about in your own words.", blank=True ) # ── Questionnaire (Part 1 — demographics) ───────────────────────────── age_questionnaire = models.IntegerField( label="What is your age?", min=18, max=120 ) gender_questionnaire = models.StringField( label="What is your gender?", choices=[ ['Male', 'Male'], ['Female', 'Female'], ['Non-binary', 'Non-binary / Third gender'], ['Prefer not to say', 'Prefer not to say'] ], widget=widgets.RadioSelect ) political_affiliation = models.StringField( label="Which of the following best describes your political identification?", choices=[ ['Democrat', 'Democrat'], ['Republican', 'Republican'], ['Independent', 'Independent'], ['Other', 'Other'], ['Prefer not to say', 'Prefer not to say'] ], widget=widgets.RadioSelect ) religious_affiliation = models.StringField( label="What is your religious denomination or affiliation?", choices=[ ['Catholic', 'Christian (Roman Catholic)'], ['Protestant', 'Christian (Protestant)'], ['Jewish', 'Jewish'], ['Muslim', 'Muslim'], ['Hindu', 'Hindu'], ['Buddhist', 'Buddhist'], ['Atheist', 'Atheist / Agnostic'], ['Other', 'Other'] ], widget=widgets.RadioSelect ) religious_affiliation_other = models.StringField( label="If Other, please specify:", blank=True ) # IOS connection to Second Movers ios_distance = models.FloatField() ios_overlap = models.FloatField() ios_number = models.IntegerField(blank=True) # ── Questionnaire (Part 2 — faith scale) ────────────────────────────── faith_pray_daily = models.IntegerField( label="I pray daily.", choices=[ [1, '1 = Strongly disagree'], [2, '2 = Disagree'], [3, '3 = Agree'], [4, '4 = Strongly agree'] ], widget=widgets.RadioSelect ) faith_meaning_purpose = models.IntegerField( label="I look to my faith as providing meaning and purpose in my life.", choices=[ [1, '1 = Strongly disagree'], [2, '2 = Disagree'], [3, '3 = Agree'], [4, '4 = Strongly agree'] ], widget=widgets.RadioSelect ) faith_active = models.IntegerField( label="I consider myself active in my faith or church.", choices=[ [1, '1 = Strongly disagree'], [2, '2 = Disagree'], [3, '3 = Agree'], [4, '4 = Strongly agree'] ], widget=widgets.RadioSelect ) faith_enjoy_community = models.IntegerField( label="I enjoy being around others who share my faith.", choices=[ [1, '1 = Strongly disagree'], [2, '2 = Disagree'], [3, '3 = Agree'], [4, '4 = Strongly agree'] ], widget=widgets.RadioSelect ) faith_impacts_decisions = models.IntegerField( label="My faith impacts many of my decisions.", choices=[ [1, '1 = Strongly disagree'], [2, '2 = Disagree'], [3, '3 = Agree'], [4, '4 = Strongly agree'] ], widget=widgets.RadioSelect ) attention_check = models.StringField( label="Based on the text you read before, what is your favourite drink?", choices=[ ['Orange juice', 'Orange juice'], ['Coffee', 'Coffee'], ['Tea', 'Tea'], ['Water', 'Water'], ['Lemonade', 'Lemonade'], ['Cola', 'Cola'], ], widget=widgets.RadioSelect ) additional_comments = models.LongStringField( label="Do you have any additional comments about the experiment?", blank=True ) def vars_for_admin_report(subsession): """Live treatment allocation monitor for the leader app.""" import time DROPOUT_THRESHOLD_MINUTES = 30 # participants inactive this long are likely dropouts players = subsession.get_players() now = time.time() # Per-treatment aggregates treatment_data = {t: dict( treatment=t, assigned=0, completed=0, likely_active=0, likely_dropped=0, never_started=0, took_oath=0, declined_oath=0, contributions=[], ) for t in ['T1', 'T2', 'T3', 'T4', 'T5', 'unassigned']} total_completed = 0 total_active = 0 total_dropped = 0 total_never_started = 0 for p in players: treatment = p.field_maybe_none('treatment') or 'unassigned' if treatment not in treatment_data: treatment = 'unassigned' completed = p.field_maybe_none('completed') or False reached_game = bool(p.field_maybe_none('treatment')) # Use participant's last activity time to estimate dropout last_active = p.participant._last_request_timestamp minutes_inactive = (now - last_active) / 60 if last_active else 9999 td = treatment_data[treatment] td['assigned'] += 1 if completed: td['completed'] += 1 total_completed += 1 elif not reached_game: td['never_started'] += 1 total_never_started += 1 elif minutes_inactive < DROPOUT_THRESHOLD_MINUTES: td['likely_active'] += 1 total_active += 1 else: td['likely_dropped'] += 1 total_dropped += 1 # Oath stats (only meaningful for T2-T5) take_oath = p.field_maybe_none('take_oath') if take_oath is True: td['took_oath'] += 1 elif take_oath is False and treatment != 'T1': td['declined_oath'] += 1 # Contribution contribution = p.field_maybe_none('contribution') if contribution is not None: td['contributions'].append(contribution) rows = [] for t in ['T1', 'T2', 'T3', 'T4', 'T5']: td = treatment_data[t] contribs = td['contributions'] avg = round(sum(contribs) / len(contribs), 1) if contribs else '—' rows.append(dict( treatment=t, assigned=td['assigned'], completed=td['completed'], likely_active=td['likely_active'], likely_dropped=td['likely_dropped'], never_started=td['never_started'], took_oath=td['took_oath'], declined_oath=td['declined_oath'], avg_contribution=avg, )) return dict( total=len(players), total_completed=total_completed, total_active=total_active, total_dropped=total_dropped, total_never_started=total_never_started, dropout_threshold=DROPOUT_THRESHOLD_MINUTES, rows=rows, )