# 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, ]