# APP: oath_follower (Second Mover)
# FILE: pages.py
from otree.api import *
from otree import settings as otree_settings
from settings import OATH_TEXTS
from .models import check_slots_available, assign_first_mover, assign_treatment
import os, urllib.request, urllib.parse, json
def is_active(player):
"""Shared display condition: consented and study not full."""
return (
player.field_maybe_none('control_question') == '1'
and not player.participant.vars.get('slots_full', False)
)
def oath_text(player):
"""Return the oath text for this treatment (empty string = no oath)."""
return player.participant.vars.get('oath_text') or ''
def has_oath(player):
"""True if this treatment has an oath."""
return bool(oath_text(player))
class Welcome(Page):
form_model = 'player'
form_fields = ['control_question', 'recaptcha_token']
@staticmethod
def vars_for_template(player):
return dict(showup_fee=otree_settings.SHOWUP_FEE)
@staticmethod
def is_displayed(player):
if not check_slots_available(player):
player.participant.vars['slots_full'] = True
return False
return True
@staticmethod
def error_message(player, values):
if values['control_question'] != '1':
return "If you do not agree to take part in this study, please close your browser tab."
@staticmethod
def before_next_page(player, timeout_happened=False):
token = player.field_maybe_none('recaptcha_token') or ''
if not token:
return
secret = os.environ.get('RECAPTCHA_SECRET_KEY', '')
if not secret:
return
data = urllib.parse.urlencode({'secret': secret, 'response': token}).encode()
try:
req = urllib.request.urlopen(
'https://www.google.com/recaptcha/api/siteverify',
data=data, timeout=5
)
result = json.loads(req.read())
player.recaptcha_score = result.get('score')
except Exception:
player.recaptcha_score = None
class StudyFull(Page):
"""Shown when all first-mover slots have been filled."""
@staticmethod
def is_displayed(player):
return player.participant.vars.get('slots_full', False)
class EndStudy(Page):
"""Shown to participants who declined to participate."""
@staticmethod
def is_displayed(player):
return player.field_maybe_none('control_question') == '2'
class ProlificID(Page):
@staticmethod
def is_displayed(player):
return is_active(player)
@staticmethod
def vars_for_template(player):
# Ensure Prolific_ID is set from participant.label (set when participant clicks link)
if player.participant.label:
player.Prolific_ID = player.participant.label
return dict(prolific_id=player.field_maybe_none('Prolific_ID') or 'Not set')
class General_Instructions(Page):
@staticmethod
def is_displayed(player):
return is_active(player)
@staticmethod
def before_next_page(player, timeout_happened=False):
# Assign treatment here so Description renders with correct oath text
assign_treatment(player)
player.treatment = player.participant.vars.get('treatment', '')
class Description(Page):
@staticmethod
def is_displayed(player):
return is_active(player)
@staticmethod
def vars_for_template(player):
return dict(oath_text=oath_text(player))
class ComprehensionQuiz(Page):
form_model = 'player'
form_fields = ['quiz_payoff_a', 'quiz_payoff_b', 'quiz_payoff_c']
@staticmethod
def is_displayed(player):
return is_active(player)
@staticmethod
def error_message(player, values):
player.quiz_attempts += 1
correct = {'quiz_payoff_a': 30, 'quiz_payoff_b': 25, 'quiz_payoff_c': 20}
explanations = {
'quiz_payoff_a': "Member A contributes 5, keeps 15. Total contributions = 30, so project share = 0.5 × 30 = 15. Payoff = 15 + 15 = 30.",
'quiz_payoff_b': "Member B contributes 10, keeps 10. Project share = 15. Payoff = 10 + 15 = 25.",
'quiz_payoff_c': "Member C contributes 15, keeps 5. Project share = 15. Payoff = 5 + 15 = 20.",
}
errors = {}
all_correct = True
for field, answer in correct.items():
if values[field] != answer:
all_correct = False
errors[field] = explanations[field]
if all_correct:
return None
player.participant.vars['quiz_error_a'] = errors.get('quiz_payoff_a', '')
player.participant.vars['quiz_error_b'] = errors.get('quiz_payoff_b', '')
player.participant.vars['quiz_error_c'] = errors.get('quiz_payoff_c', '')
return "One or more answers are incorrect. Please review the explanations below and try again."
@staticmethod
def vars_for_template(player):
return dict(
attempt_number=player.quiz_attempts + 1,
oath_text=oath_text(player),
quiz_error_a=player.participant.vars.get('quiz_error_a', ''),
quiz_error_b=player.participant.vars.get('quiz_error_b', ''),
quiz_error_c=player.participant.vars.get('quiz_error_c', ''),
)
@staticmethod
def before_next_page(player, timeout_happened=False):
# Only called when quiz is passed — safe to assign first mover now
assign_first_mover(player)
class Leaders_oath(Page):
form_model = 'player'
form_fields = ['first_mover_guess']
@staticmethod
def is_displayed(player):
return is_active(player)
@staticmethod
def before_next_page(player, timeout_happened=False):
player.first_mover_guess_correct = (
int(player.first_mover_guess) == int(player.observed_first_contribution)
)
@staticmethod
def vars_for_template(player):
return dict(
first_contribution=player.observed_first_contribution,
first_took_oath=player.observed_take_oath,
oath_text=oath_text(player),
has_oath=has_oath(player),
)
class Contributions(Page):
form_model = 'player'
form_fields = ['contribution']
@staticmethod
def is_displayed(player):
return is_active(player)
@staticmethod
def vars_for_template(player):
return dict(
first_contribution=int(player.observed_first_contribution),
oath_text=oath_text(player),
has_oath=has_oath(player),
)
class SecondMoverGuess(Page):
form_model = 'player'
form_fields = ['other_second_mover_guess']
@staticmethod
def before_next_page(player, timeout_happened=False):
# other_second_mover_guess correctness can only be checked post-hoc
pass
@staticmethod
def is_displayed(player):
return is_active(player)
@staticmethod
def vars_for_template(player):
return dict(oath_text=oath_text(player))
class ConnectionQuestionnaire(Page):
form_model = 'player'
@staticmethod
def is_displayed(player):
return is_active(player)
@staticmethod
def error_message(player, values):
if not values.get('ios_number'):
return 'Please select one of the circle pairs to indicate your connection with the First Mover.'
@staticmethod
def before_next_page(player, timeout_happened=False):
# Compute distance/overlap server-side from ios_number (1-7)
# so we don't rely on the JavaScript hidden field updates
n = player.field_maybe_none('ios_number') or 1
player.ios_distance = round((7 - n) / 6, 3)
player.ios_overlap = round((n - 1) / 6, 3)
if has_oath(player):
recall = player.field_maybe_none('oath_taken_recall')
if recall is not None:
player.oath_recall_correct = (recall == player.observed_take_oath)
@staticmethod
def get_form_fields(player):
form_fields = ['ios_number']
if has_oath(player):
form_fields.append('oath_taken_recall')
form_fields.append('oath_recall')
form_fields.extend([
'age_questionnaire',
'gender_questionnaire',
'political_affiliation',
'religious_affiliation',
'religious_affiliation_other',
])
return form_fields
@staticmethod
def vars_for_template(player):
return dict(
has_oath=has_oath(player),
)
class FaithQuestionnaire(Page):
form_model = 'player'
form_fields = [
'faith_pray_daily',
'faith_meaning_purpose',
'faith_active',
'faith_enjoy_community',
'faith_impacts_decisions',
'attention_check',
'additional_comments',
]
@staticmethod
def is_displayed(player):
return is_active(player)
@staticmethod
def before_next_page(player, timeout_happened=False):
player.completed = True
bonus = 0
if player.field_maybe_none('first_mover_guess_correct'):
bonus += 4
if player.field_maybe_none('oath_recall_correct'):
bonus += 4
player.bonus_tokens = bonus
class Thanks(Page):
@staticmethod
def vars_for_template(player):
return dict(showup_fee=otree_settings.SHOWUP_FEE)
@staticmethod
def is_displayed(player):
return is_active(player)
@staticmethod
def before_next_page(player, timeout_happened=False):
pass # completed and bonus_tokens set on FaithQuestionnaire
page_sequence = [
Welcome,
StudyFull,
EndStudy,
ProlificID,
General_Instructions,
Description,
ComprehensionQuiz,
Leaders_oath,
Contributions,
SecondMoverGuess,
ConnectionQuestionnaire,
FaithQuestionnaire,
Thanks,
]