from otree.api import * import json import random doc = """ Shaping Social Norms experiment aligned with chemin de fer 2.0. """ def build_active_games(enable_dictator_games): active_games = [3, 4] if enable_dictator_games: active_games = [1, 2] + active_games return active_games def build_debug_condition_assets(enable_dictator_games, active_inf_conditions): all_conditions = [ ('g1_noinfo', (1, 'noinfo'), 'Dictateur observation passive - NoInfo'), ('g1_info', (1, 'info'), 'Dictateur observation passive - Info'), ('g1_info_default', (1, 'info_default'), 'Dictateur observation passive - Info_Default'), ('g2_noinfo', (2, 'noinfo'), 'Dictateur observation active - NoInfo'), ('g2_info', (2, 'info'), 'Dictateur observation active - Info'), ('g2_info_default', (2, 'info_default'), 'Dictateur observation active - Info_Default'), ('g3_noinfo', (3, 'noinfo'), 'Observateur passif - NoInfo'), ('g3_info', (3, 'info'), 'Observateur passif - Info'), ('g3_info_default', (3, 'info_default'), 'Observateur passif - Info_Default'), ('g4_noinfo', (4, 'noinfo'), 'Observateur actif - NoInfo'), ('g4_info', (4, 'info'), 'Observateur actif - Info'), ('g4_info_default', (4, 'info_default'), 'Observateur actif - Info_Default'), ] active_games = build_active_games(enable_dictator_games) filtered = [ row for row in all_conditions if row[1][0] in active_games and row[1][1] in active_inf_conditions ] return ( {key: value for key, value, _label in filtered}, [(key, label) for key, _value, label in filtered], ) class C(BaseConstants): NAME_IN_URL = 'shaping_social_norms' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 ENDOWMENT_EUR = 25 DON = 100 LOTTERY_PROB = 1 / 20 BELIEF_BONUS_EUR = 1 DECISION_RATING_BONUS_DRAW_MAX = 16 ASSOC_BELIEF_BONUS_DRAW_MAX = 100 SHOW_UP_FEE = 4 DURATION = 15 ARUP1 = 'Médecins Sans Frontières' ARUP2 = 'Société Française de Céramique' ARUP_LIST = [ARUP1, ARUP2] ASS_DESCRIPTION = ( "MSF est une association médicale humanitaire internationale créée en 1971 en France. " "Elle porte assistance dans plus de 70 pays à des populations dont la vie ou la santé " "est menacée notamment lors des conflits, d’épidémies et de catastrophes naturelles." ) # Legacy aliases kept to avoid breaking older code paths. DONATION_EUR = DON SHOW_UP_FEE_EUR = SHOW_UP_FEE DURATION_MIN = DURATION INF_NOINFO = 'noinfo' INF_INFO = 'info' INF_INFO_DEFAULT = 'info_default' INFO_CHOICE_YES = 'Oui je souhaite voir l’identité de l’ARUP' INFO_CHOICE_NO = 'Non je ne souhaite pas voir l’identité de l’ARUP' INFO_CHOICE_OPTIONS = [INFO_CHOICE_YES, INFO_CHOICE_NO] ENABLE_DICTATOR_GAMES = True ALL_GAMES = [1, 2, 3, 4] ALL_INF_CONDITIONS = [INF_NOINFO, INF_INFO, INF_INFO_DEFAULT] ACTIVE_GAMES = build_active_games(ENABLE_DICTATOR_GAMES) ACTIVE_INF_CONDITIONS = ALL_INF_CONDITIONS DEBUG_CONDITION_MAP, DEBUG_CONDITION_CHOICES = build_debug_condition_assets( ENABLE_DICTATOR_GAMES, ACTIVE_INF_CONDITIONS, ) class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): # consent & basic info consent = models.BooleanField( choices=[[True, 'J’accepte de participer à cette étude'], [False, 'Je n\'accepte pas de participer à cette étude']], widget=widgets.RadioSelect, label='', ) yapper_id = models.IntegerField( min=0, max=999999, label='Afin de pouvoir participer, merci de bien vouloir saisir votre identifiant :', ) age = models.IntegerField( min=18, max=99, label='Quel âge avez-vous ?', ) education = models.StringField( choices=[ 'Sans diplôme', 'Certificat d’études primaires', 'Ancien brevet BEPC', 'Certificat d’aptitude professionnelle (CAP)', 'Brevet d’enseignement professionnel (BEP)', 'BAC d’enseignement technique et professionnel', 'BAC d’enseignement général', 'BAC +2 (DUT, BTS, D\\EUG)', 'Diplôme de l’enseignement supérieur (2ème ou 3ème cycles, grande école)', ], widget=widgets.RadioSelect, label='Quel est le diplôme le plus élevé que vous avez obtenu ?', ) employment_status = models.StringField( choices=['Etudiant(e)', 'En emploi', 'Chomeur(se)', 'Inactif(ve)', 'Retraité(e)'], widget=widgets.RadioSelect, label='Quelle est votre situation professionnelle ?', ) income_monthly = models.StringField( choices=[ 'Moins de 1000 euros par mois', 'De 1001 à 1500 euros par mois', 'De 1501 à 1750 euros par mois', 'De 1751 à 2000 euros par mois', 'De 2001 à 2500 euros par mois', 'De 2501 à 3000 euros par mois', 'De 3001 à 4000 euros par mois', 'De 4001 à 5000 euros par mois', 'De 5001 à 7000 euros par mois', 'Plus de 7001 euros par mois', 'Ne souhaite pas répondre', ], widget=widgets.RadioSelect, label='Si vous additionnez toutes les sources de revenus de votre foyer, dans quel intervalle se situe le revenu net mensuel de votre foyer :', ) gender = models.StringField( choices=['Homme', 'Femme'], widget=widgets.RadioSelect, label='Quel est votre sexe ou genre sexuel actuel ?', ) marital_status = models.StringField( choices=['Célibataire', 'Marié(e)', 'Pacsé(e)', 'Divorcé(e)', 'Veuf/Veuve'], widget=widgets.RadioSelect, label='Quelle est votre situation matrimoniale ?', ) children = models.StringField( choices=['Oui', 'Non'], widget=widgets.RadioSelect, label='Avez-vous des enfants ?', ) global_preference = models.StringField( choices=[ 'Tout à fait comme moi', 'Comme moi', 'Un peu comme moi', 'Assez peu comme moi', 'Pas du tout comme moi', ], widget=widgets.RadioSelect, label='', ) helped_stranger_last_month = models.StringField( choices=['Oui', 'Non'], widget=widgets.RadioSelect, label='', ) volunteered_last_month = models.StringField( choices=['Oui', 'Non'], widget=widgets.RadioSelect, label='', ) family_support = models.IntegerField( min=1, max=11, choices=list(range(1, 12)), widget=widgets.RadioSelectHorizontal, label='', ) math_agreement = models.IntegerField( min=1, max=11, choices=list(range(1, 12)), widget=widgets.RadioSelectHorizontal, label='', ) attention_check = models.IntegerField( label='Combien font 2 + 2 ?', ) # admin review: manual condition override debug_condition_choice = models.StringField( blank=True, choices=C.DEBUG_CONDITION_CHOICES, widget=widgets.RadioSelect, label='Choisir la condition de départ', ) # info choice (ARUP) info_choice = models.StringField( choices=C.INFO_CHOICE_OPTIONS, widget=widgets.RadioSelect, label='' ) info_default_opt_out = models.BooleanField( blank=True, widget=widgets.CheckboxInput, label=C.INFO_CHOICE_NO, ) info_default_choice = models.StringField( blank=True, choices=C.INFO_CHOICE_OPTIONS, widget=widgets.RadioSelect, label='', ) # donation decision donation_choice = models.StringField( choices=['keep', 'donate'], widget=widgets.RadioSelect, label='' ) # beliefs expected_moral_rating = models.IntegerField(min=1, max=5, label='') expected_give_rate = models.IntegerField(min=0, max=100, label='') # evaluation (observer) eval_info_choice = models.StringField( choices=C.INFO_CHOICE_OPTIONS, widget=widgets.RadioSelect, label='' ) eval_info_default_opt_out = models.BooleanField( blank=True, widget=widgets.CheckboxInput, label=C.INFO_CHOICE_NO, ) eval_info_default_choice = models.StringField( blank=True, choices=C.INFO_CHOICE_OPTIONS, widget=widgets.RadioSelect, label='', ) moral_rating = models.IntegerField( min=1, max=5, choices=[1, 2, 3, 4, 5], widget=widgets.RadioSelectHorizontal, label='', ) moral_comment = models.LongStringField(blank=True, label='Vous pouvez laisser un court commentaire (optionnel).') expected_peer_rating = models.IntegerField(min=1, max=5, label='') expected_peer_give = models.IntegerField(min=0, max=100, label='') # bloc D arup_belief_1 = models.IntegerField(min=0, max=100, label='') arup_belief_2 = models.IntegerField(min=0, max=100, label='') arup_beneficial = models.IntegerField( min=1, max=5, choices=[1, 2, 3, 4, 5], widget=widgets.RadioSelectHorizontal, label='', ) arup_budget_percent = models.IntegerField(min=0, max=100, label='') income_monthly_post = models.StringField( choices=[ 'Moins de 1000 euros par mois', 'De 1001 à 1500 euros par mois', 'De 1501 à 1750 euros par mois', 'De 1751 à 2000 euros par mois', 'De 2001 à 2500 euros par mois', 'De 2501 à 3000 euros par mois', 'De 3001 à 4000 euros par mois', 'De 4001 à 5000 euros par mois', 'De 5001 à 7000 euros par mois', 'Plus de 7001 euros par mois', 'Ne souhaite pas répondre', ], widget=widgets.RadioSelect, label='', ) altruism_charity = models.IntegerField(min=1, max=5, choices=[1, 2, 3, 4, 5], widget=widgets.RadioSelectHorizontal, label='') altruism_stranger = models.IntegerField(min=1, max=5, choices=[1, 2, 3, 4, 5], widget=widgets.RadioSelectHorizontal, label='') altruism_volunteer = models.IntegerField(min=1, max=5, choices=[1, 2, 3, 4, 5], widget=widgets.RadioSelectHorizontal, label='') altruism_blood = models.IntegerField(min=1, max=5, choices=[1, 2, 3, 4, 5], widget=widgets.RadioSelectHorizontal, label='') self_esteem_1 = models.IntegerField(min=1, max=4, choices=[1, 2, 3, 4], widget=widgets.RadioSelectHorizontal, label='') self_esteem_2 = models.IntegerField(min=1, max=4, choices=[1, 2, 3, 4], widget=widgets.RadioSelectHorizontal, label='') self_esteem_3 = models.IntegerField(min=1, max=4, choices=[1, 2, 3, 4], widget=widgets.RadioSelectHorizontal, label='') self_esteem_4 = models.IntegerField(min=1, max=4, choices=[1, 2, 3, 4], widget=widgets.RadioSelectHorizontal, label='') self_esteem_5 = models.IntegerField(min=1, max=4, choices=[1, 2, 3, 4], widget=widgets.RadioSelectHorizontal, label='') self_esteem_6 = models.IntegerField(min=1, max=4, choices=[1, 2, 3, 4], widget=widgets.RadioSelectHorizontal, label='') self_esteem_7 = models.IntegerField(min=1, max=4, choices=[1, 2, 3, 4], widget=widgets.RadioSelectHorizontal, label='') self_esteem_8 = models.IntegerField(min=1, max=4, choices=[1, 2, 3, 4], widget=widgets.RadioSelectHorizontal, label='') self_esteem_9 = models.IntegerField(min=1, max=4, choices=[1, 2, 3, 4], widget=widgets.RadioSelectHorizontal, label='') self_esteem_10 = models.IntegerField(min=1, max=4, choices=[1, 2, 3, 4], widget=widgets.RadioSelectHorizontal, label='') # bloc E share_contact_consent = models.BooleanField( blank=True, widget=widgets.CheckboxInput, label='Je confirme', ) # outcomes lottery_win = models.BooleanField(initial=False) # HELPERS def json_compact(value): return json.dumps(value, ensure_ascii=False, sort_keys=True) def get_randomization_audit(player: Player): return player.participant.vars.setdefault('randomization_audit', {}) def remember_randomization(player: Player, audit_key: str, payload, *, overwrite=False): audit = get_randomization_audit(player) if overwrite or audit_key not in audit: audit[audit_key] = payload return audit[audit_key] def trace_static_assignment(player: Player, audit_key: str, selected_value, *, source: str, options=None, extra=None, overwrite=False): payload = dict( kind='static_assignment', source=source, selected_value=selected_value, ) if options is not None: payload['options'] = list(options) if extra: payload.update(extra) remember_randomization(player, audit_key, payload, overwrite=overwrite) return selected_value def trace_random_choice(player: Player, audit_key: str, options, *, source: str): stored = get_randomization_audit(player).get(audit_key) if stored is not None: return stored.get('selected_value') options_list = list(options) draw_uniform = random.random() selected_index = min(int(draw_uniform * len(options_list)), len(options_list) - 1) selected_value = options_list[selected_index] remember_randomization( player, audit_key, dict( kind='choice', source=source, options=options_list, draw_uniform=draw_uniform, selected_index=selected_index, selected_value=selected_value, ), ) return selected_value def trace_random_int(player: Player, audit_key: str, min_inclusive: int, max_inclusive: int, *, source: str): stored = get_randomization_audit(player).get(audit_key) if stored is not None: return stored.get('selected_value') draw_uniform = random.random() span = max_inclusive - min_inclusive + 1 selected_offset = min(int(draw_uniform * span), span - 1) selected_value = min_inclusive + selected_offset remember_randomization( player, audit_key, dict( kind='integer', source=source, range_inclusive=[min_inclusive, max_inclusive], draw_uniform=draw_uniform, selected_offset=selected_offset, selected_value=selected_value, ), ) return selected_value def get_game(player: Player): return player.participant.vars['game'] def get_inf(player: Player): return player.participant.vars['inf'] def get_duration(player: Player): return player.participant.vars.get('duration', player.participant.vars.get('duration_min')) def get_don(player: Player): return player.participant.vars.get('don', player.participant.vars.get('donation')) def get_show_up_fee(player: Player): return player.participant.vars.get('show_up_fee') def has_initial_decision_block(player: Player): return get_game(player) in [1, 2, 4] def has_post_evaluation_decision_block(player: Player): return get_game(player) == 3 def has_decision_block(player: Player): return has_initial_decision_block(player) or has_post_evaluation_decision_block(player) def has_decision_belief_block(player: Player): return get_game(player) in [1, 2] def feedback_message_enabled(player: Player): return get_game(player) in [1, 2] def has_evaluation_block(player: Player): return get_game(player) in [3, 4] def needs_observed_choice(player: Player): return has_evaluation_block(player) def feedback_source_game(target: Player): return { 1: 3, 2: 4, 3: 3, 4: 4, }.get(get_game(target)) PAGE_TITLE_SPECS = { 'WelcomeA': dict(block='A'), 'InfoDelay': dict(block='A'), 'CompensationIntro': dict(block='A'), 'Age': dict(block='A'), 'Education': dict(block='A'), 'Employment': dict(block='A'), 'Income': dict(block='A'), 'Gender': dict(block='A'), 'MaritalStatus': dict(block='A'), 'Children': dict(block='A'), 'GlobalPreference': dict(block='A'), 'HelpedStrangerLastMonth': dict(block='A'), 'VolunteeredLastMonth': dict(block='A'), 'FamilySupport': dict(block='A'), 'MathAgreement': dict(block='A'), 'AttentionCheck': dict(block='B', part='1.0'), 'AttentionCheckFailed': dict(block='B', part='1.0.1'), 'DecisionIntro': dict(block='B', part='1.1'), 'InfoChoice': dict(block='B', part='1.1.1'), 'InfoNoChoice': dict(block='B', part='1.1.1'), 'InfoDefaultChoice': dict(block='B', part='1.1.1'), 'ShowARUP': dict(block='B', part='1.1.2'), 'DecisionObserversInfo': dict(block='B', part='1.2'), 'DecisionObserversType': dict(block='B', part='1.3'), 'Decision': dict(block='B', part='1.4'), 'DecisionRecorded': dict(block='B', part='2.1'), 'DecisionBeliefRating': dict(block='B', part='2.2'), 'DecisionBeliefGive': dict(block='B', part='3'), 'DecisionIntroAfterEvaluation': dict(block='B', part='1.1'), 'InfoChoiceAfterEvaluation': dict(block='B', part='1.1.1'), 'InfoNoChoiceAfterEvaluation': dict(block='B', part='1.1.1'), 'InfoDefaultChoiceAfterEvaluation': dict(block='B', part='1.1.1'), 'ShowARUPAfterEvaluation': dict(block='B', part='1.1.2'), 'DecisionObserversInfoAfterEvaluation': dict(block='B', part='1.2'), 'DecisionObserversTypeAfterEvaluation': dict(block='B', part='1.3'), 'DecisionAfterEvaluation': dict(block='B', part='1.4'), 'DecisionRecordedAfterEvaluation': dict(block='B', part='2.1'), 'DecisionBeliefRatingAfterEvaluation': dict(block='B', part='2.2'), 'DecisionBeliefGiveAfterEvaluation': dict(block='B', part='3'), 'EvaluationIntro': dict(block='C', part='1.1.0'), 'EvalInfoChoice': dict(block='C', part='1.1.1'), 'EvalInfoNoChoice': dict(block='C', part='1.1.1'), 'EvalInfoDefaultChoice': dict(block='C', part='1.1.1'), 'EvalShowARUP': dict(block='C', part='1.1.2'), 'Evaluation': dict(block='C', part='1.2'), 'EvaluationBeliefRating': dict(block='D', part='1.1'), 'EvaluationBeliefGive': dict(block='D', part='1.2'), 'PostIntro': dict(block='D', part='1.0'), 'AssocBeliefIntro': dict(block='D', part='1.3'), 'AssocBelief': dict(block='D', part='1.4'), 'AssocBenefit': dict(block='D', part='2.1'), 'AssocBudget': dict(block='D', part='2.2'), 'Results': dict(block='E', part='1'), 'FinalCode': dict(block='E', part='2'), } BLOCK_SEQUENCE_BY_GAME = { 1: ['B', 'D', 'E'], 2: ['B', 'D', 'E'], 3: ['C', 'B', 'D', 'E'], 4: ['B', 'C', 'D', 'E'], } def get_display_block_for_page(page_name: str, game: int): spec = PAGE_TITLE_SPECS.get(page_name) if not spec: return None block = spec['block'] if game == 3 and page_name in {'AttentionCheck', 'AttentionCheckFailed'}: return 'C' return block def get_part_number_for_block(game: int, block: str): sequence = BLOCK_SEQUENCE_BY_GAME.get(game) if not sequence or block not in sequence: return None return sequence.index(block) + 1 def get_block_badge_title(page): player = getattr(page, 'player', None) if player is None: return '' game = get_game(player) page_name = type(page).__name__ spec = PAGE_TITLE_SPECS.get(page_name) if not spec: return '' block = get_display_block_for_page(page_name, game) if block is None: return '' part_number = get_part_number_for_block(game, block) if part_number is None: return '' return f"Partie {part_number}" Page.block_badge_title = property(get_block_badge_title) def observers_role_text(player: Player): if get_game(player) == 1: return ( "L’évaluation que nous demandons à ces 3 participants " "est la seule décision que ces participants auront à prendre." ) def observers_info_text(player: Player): inf = get_inf(player) if inf == C.INF_NOINFO: return "Comme vous, ces participants ne connaîtront pas l’identité de l’Association Reconnue d’Utilité Publique qui a été choisie avant le début de cette étude." if inf == C.INF_INFO: return "Comme vous, ces participants auront pu choisir de voir l’identité de l’Association Reconnue d’Utilité Publique qui a été choisie avant le début de cette étude." return "Comme vous, ces participants auront pu choisir de ne pas voir l’identité de l’Association Reconnue d’Utilité Publique qui a été choisie avant le début de cette étude." def recorded_role_text(player: Player): if get_game(player) in [2, 4]: return ( "Comme indiqué précédemment, nous allons présenter votre décision à 3 participants " "qui auront eu à faire exactement le même choix que vous (en décidant soit de conserver la somme " "qu’ils ont obtenue, soit d’aider l’Association Reconnue d’Utilité Publique qui a été choisie) et " ) def recorded_info_text(player: Player): inf = get_inf(player) if inf == C.INF_NOINFO: return "qui comme vous ne connaîtront pas l’identité de l’Association Reconnue d’Utilité Publique qui a été choisie." if inf == C.INF_INFO: return "qui comme vous auront pu choisir de voir l’identité de l’Association Reconnue d’Utilité Publique qui a été choisie." return "qui comme vous auront pu choisir de ne pas voir l’identité de l’Association Reconnue d’Utilité Publique qui a été choisie." def get_yapper_from_participant_label(player: Player): """ If participant.label follows the lab rule (numeric id, max 6 digits), reuse it as Yapper ID to avoid manual re-entry. """ label = (player.participant.label or '').strip() if label.isdigit() and len(label) <= 6: return int(label) return None def passed_attention_check(player: Player): return player.field_maybe_none('attention_check') == 5 def condition_picker_enabled(session): return ( session.config.get('condition_picker', False) and session.config.get('fixed_game') is None and session.config.get('fixed_inf') is None ) def stable_choice_order(player: Player, key: str, options): stored = player.participant.vars.get(key) seed = f'{player.session.code}:{player.participant.code}:{key}' if isinstance(stored, list) and sorted(stored) == sorted(options): if key not in get_randomization_audit(player): remember_randomization( player, key, dict( kind='seeded_shuffle', source='stable_choice_order', seed=seed, options=list(options), ordered=stored, restored_from_storage=True, ), ) return stored ordered = list(options) rng = random.Random(seed) rng.shuffle(ordered) player.participant.vars[key] = ordered remember_randomization( player, key, dict( kind='seeded_shuffle', source='stable_choice_order', seed=seed, options=list(options), ordered=ordered, ), overwrite=True, ) return ordered def ensure_lottery(player: Player): # Draw exactly once per participant, then keep the outcome stable. if player.participant.vars.get('lottery_draw_done'): lottery_trace = get_randomization_audit(player).get('lottery') if lottery_trace is not None: player.lottery_win = bool(lottery_trace.get('win')) return draw_uniform = random.random() player.lottery_win = draw_uniform < C.LOTTERY_PROB remember_randomization( player, 'lottery', dict( kind='bernoulli', source='lottery_draw', draw_uniform=draw_uniform, threshold=C.LOTTERY_PROB, win=bool(player.lottery_win), ), overwrite=True, ) player.participant.vars['lottery_draw_done'] = True def feedback_pool_for_player(target: Player, all_players): target_choice = target.field_maybe_none('donation_choice') source_game = feedback_source_game(target) if target_choice is None or source_game is None: return [] return [ p for p in all_players if p.participant.code != target.participant.code and get_game(p) == source_game and get_inf(p) == get_inf(target) and p.participant.vars.get('observed_choice') == target_choice and passed_attention_check(p) and p.field_maybe_none('moral_rating') is not None ] def feedback_sample_details(target: Player, all_players, sample_size=3): evaluators = sorted(feedback_pool_for_player(target, all_players), key=lambda p: p.participant.code) pool_codes = [p.participant.code for p in evaluators] seed = f'{target.session.code}:{target.participant.code}:feedback' if not evaluators: remember_randomization( target, 'feedback_sample', dict( kind='seeded_sample', source='feedback_sampling', seed=seed, pool_codes=[], sample_size_requested=sample_size, sampled_codes=[], ), overwrite=True, ) return dict( sampled_players=[], sampled_codes=[], pool_codes=[], comments=[], mean_rating=None, ) if len(evaluators) <= sample_size: sampled_players = evaluators else: rng = random.Random(seed) sampled_codes = sorted(rng.sample(pool_codes, sample_size)) by_code = {p.participant.code: p for p in evaluators} sampled_players = [by_code[code] for code in sampled_codes] sampled_codes = [p.participant.code for p in sampled_players] comments = [comment for comment in ((p.field_maybe_none('moral_comment') or '').strip() for p in sampled_players) if comment] mean_rating = sum(p.moral_rating for p in sampled_players) / len(sampled_players) remember_randomization( target, 'feedback_sample', dict( kind='seeded_sample', source='feedback_sampling', seed=seed, pool_codes=pool_codes, sample_size_requested=sample_size, sampled_codes=sampled_codes, ), overwrite=True, ) return dict( sampled_players=sampled_players, sampled_codes=sampled_codes, pool_codes=pool_codes, comments=comments, mean_rating=mean_rating, ) def build_feedback_message(target: Player, all_players, feedback_details=None): ensure_lottery(target) if not feedback_message_enabled(target): return '' if not has_decision_block(target) or target.field_maybe_none('donation_choice') is None: return '' details = feedback_details or feedback_sample_details(target, all_players) sampled = details['sampled_players'] if not sampled: return '' endowment = target.participant.vars['endowment'] don = get_don(target) arup = target.participant.vars['arup1'] mean_rating = details['mean_rating'] mean_str = f'{mean_rating:.2f}'.rstrip('0').rstrip('.') comments = details['comments'] lottery_sentence = ( f'Au cours de cette étude, vous aviez obtenu la somme de {endowment} euros.' if target.lottery_win else f'Au cours de cette étude, vous n’aviez pas obtenu la somme de {endowment} euros.' ) if target.donation_choice == 'keep': decision_sentence = f'Vous aviez décidé de conserver la somme de {endowment} euros en cas d’obtention.' else: decision_sentence = ( f'Vous aviez décidé de demander un don de {don} euros en votre nom à {arup} en cas d’obtention.' ) lines = [ lottery_sentence, decision_sentence, '', f'L’évaluation moyenne de cette décision réalisée par {len(sampled)} autres participants est égale à :', mean_str, ] if comments: lines.extend(['', 'Ces évaluations étaient assorties des commentaires suivants :']) lines.extend([f'- {comment}' for comment in comments]) return '\n'.join(lines) def assign_observed_choice_with_rotation(player: Player): """ Perfect within-treatment rotation between choice 1 and choice 2. choice 1 = donate, choice 2 = keep. Uses session-level counters to stay consistent even if treatment is changed in the manual picker. """ if not needs_observed_choice(player): return key = (player.participant.vars['game'], player.participant.vars['inf']) counts = player.session.vars.setdefault('observed_choice_rotation_counts', {}) key_str = f"{key[0]}|{key[1]}" idx = counts.get(key_str, 0) player.participant.vars['observed_choice'] = 'donate' if idx % 2 == 0 else 'keep' counts[key_str] = idx + 1 remember_randomization( player, 'observed_choice_assignment', dict( kind='rotation', source='perfect_within_treatment_rotation', rotation_bucket=key_str, rotation_index=idx, selected_value=player.participant.vars['observed_choice'], ), overwrite=True, ) def donation_realized(player: Player): ensure_lottery(player) return bool(player.lottery_win and player.field_maybe_none('donation_choice') == 'donate') def lottery_payment_component(player: Player): ensure_lottery(player) if player.field_maybe_none('donation_choice') == 'keep' and player.lottery_win: return player.participant.vars['endowment'] return 0 def decision_rating_bonus_details(target: Player, all_players, feedback_details=None): if not has_decision_belief_block(target): return dict(applicable=False, payout_eur=0) response = target.field_maybe_none('expected_moral_rating') if response is None: return dict(applicable=False, payout_eur=0) stored = get_audit_payload(target, 'decision_rating_bonus') if stored: return stored details = feedback_details or feedback_sample_details(target, all_players) mean_rating = details['mean_rating'] payload = dict( kind='threshold_bonus', source='decision_rating_bonus', response=response, draw_range=[0, C.DECISION_RATING_BONUS_DRAW_MAX], ) if mean_rating is None: payload.update( realized_mean_rating='', distance_squared='', draw_uniform='', draw_value='', win=False, payout_eur=0, unresolved_reason='missing_feedback_sample', ) remember_randomization(target, 'decision_rating_bonus', payload, overwrite=True) return payload draw_uniform = random.random() draw_value = C.DECISION_RATING_BONUS_DRAW_MAX * draw_uniform distance_squared = (response - mean_rating) ** 2 win = draw_value > distance_squared payload.update( realized_mean_rating=mean_rating, distance_squared=distance_squared, draw_uniform=draw_uniform, draw_value=draw_value, win=bool(win), payout_eur=C.BELIEF_BONUS_EUR if win else 0, ) remember_randomization(target, 'decision_rating_bonus', payload, overwrite=True) return payload def assoc_belief_bonus_details(player: Player): response_order = player.participant.vars.get('arup_order') if response_order not in [1, 2]: return dict(applicable=False, payout_eur=0) stored = get_audit_payload(player, 'assoc_belief_bonus') if stored: return stored if response_order == 1: first_displayed_assoc = player.participant.vars['arup1'] first_displayed_field = 'arup_belief_1' first_displayed_response = player.field_maybe_none('arup_belief_1') first_displayed_is_selected = True else: first_displayed_assoc = player.participant.vars['arup2'] first_displayed_field = 'arup_belief_2' first_displayed_response = player.field_maybe_none('arup_belief_2') first_displayed_is_selected = False payload = dict( kind='binary_probability_bonus', source='assoc_belief_bonus', first_displayed_assoc=first_displayed_assoc, first_displayed_field=first_displayed_field, first_displayed_response=first_displayed_response if first_displayed_response is not None else '', first_displayed_is_selected=bool(first_displayed_is_selected), ) if first_displayed_response is None: payload.update( draw_1_uniform='', draw_2_uniform='', draw_1_value='', draw_2_value='', lower_threshold='', upper_threshold='', win=False, payout_eur=0, unresolved_reason='missing_assoc_belief_response', ) remember_randomization(player, 'assoc_belief_bonus', payload, overwrite=True) return payload draw_1_uniform = random.random() draw_2_uniform = random.random() draw_1_value = C.ASSOC_BELIEF_BONUS_DRAW_MAX * draw_1_uniform draw_2_value = C.ASSOC_BELIEF_BONUS_DRAW_MAX * draw_2_uniform lower_threshold = min(draw_1_value, draw_2_value) upper_threshold = max(draw_1_value, draw_2_value) if first_displayed_is_selected: win = first_displayed_response > lower_threshold else: win = first_displayed_response < upper_threshold payload.update( draw_1_uniform=draw_1_uniform, draw_2_uniform=draw_2_uniform, draw_1_value=draw_1_value, draw_2_value=draw_2_value, lower_threshold=lower_threshold, upper_threshold=upper_threshold, win=bool(win), payout_eur=C.BELIEF_BONUS_EUR if win else 0, ) remember_randomization(player, 'assoc_belief_bonus', payload, overwrite=True) return payload def variable_payment_details(player: Player, all_players, feedback_details=None): decision_rating_bonus = decision_rating_bonus_details( player, all_players, feedback_details=feedback_details, ) assoc_belief_bonus = assoc_belief_bonus_details(player) lottery_component = lottery_payment_component(player) variable_bonus = ( lottery_component + decision_rating_bonus.get('payout_eur', 0) + assoc_belief_bonus.get('payout_eur', 0) ) unresolved_reasons = [ payload.get('unresolved_reason') for payload in [decision_rating_bonus, assoc_belief_bonus] if payload.get('unresolved_reason') ] return dict( lottery_component_eur=lottery_component, decision_rating_bonus_eur=decision_rating_bonus.get('payout_eur', 0), assoc_belief_bonus_eur=assoc_belief_bonus.get('payout_eur', 0), variable_bonus_eur_excl_showup=variable_bonus, total_payment_eur_incl_showup=get_show_up_fee(player) + variable_bonus, variable_bonus_status='computed_in_app' if not unresolved_reasons else ';'.join(unresolved_reasons), bonus_formula_owner='shaping_social_norms_app', ) def observed_choice_rating_label(player: Player): if player.participant.vars.get('observed_choice') == 'donate': return "d’aider l’Association Reconnue d’Utilité Publique" return 'de conserver la somme' def empty_feedback_details(): return dict( sampled_players=[], sampled_codes=[], pool_codes=[], comments=[], mean_rating=None, ) def get_audit_payload(player: Player, audit_key: str): return get_randomization_audit(player).get(audit_key, {}) def audit_json_field(payload, key: str): return json_compact(payload[key]) if key in payload else '' def audit_range_value(payload, index: int): range_inclusive = payload.get('range_inclusive') if isinstance(range_inclusive, (list, tuple)) and len(range_inclusive) == 2: return range_inclusive[index] return '' def creating_session(subsession: Subsession): players = subsession.get_players() # 1) Assign treatment variables first (game/inf and constants). for p in players: fixed_game = p.session.config.get('fixed_game') fixed_inf = p.session.config.get('fixed_inf') if fixed_game is not None and fixed_game not in C.ALL_GAMES: raise ValueError(f"Invalid fixed_game={fixed_game}. Allowed values: {C.ALL_GAMES}") allowed_fixed_inf = C.ALL_INF_CONDITIONS if fixed_inf is not None and fixed_inf not in allowed_fixed_inf: raise ValueError( f"Invalid fixed_inf={fixed_inf}. Allowed values: {allowed_fixed_inf}" ) if fixed_game is not None: p.participant.vars['game'] = trace_static_assignment( p, 'game_assignment', fixed_game, source='fixed_game_config', options=C.ALL_GAMES, ) else: p.participant.vars['game'] = trace_random_choice( p, 'game_assignment', C.ACTIVE_GAMES, source='session_creation_random_game', ) if fixed_inf is not None: p.participant.vars['inf'] = trace_static_assignment( p, 'inf_assignment', fixed_inf, source='fixed_inf_config', options=C.ALL_INF_CONDITIONS, ) else: p.participant.vars['inf'] = trace_random_choice( p, 'inf_assignment', C.ACTIVE_INF_CONDITIONS, source='session_creation_random_inf', ) p.participant.vars['arup1'] = C.ARUP1 p.participant.vars['arup2'] = C.ARUP2 p.participant.vars['endowment'] = C.ENDOWMENT_EUR p.participant.vars['don'] = C.DON p.participant.vars['show_up_fee'] = C.SHOW_UP_FEE p.participant.vars['duration'] = C.DURATION p.participant.vars['ass_description'] = C.ASS_DESCRIPTION # Legacy keys kept for backward compatibility. p.participant.vars['donation'] = C.DON p.participant.vars['duration_min'] = C.DURATION p.participant.vars['yapper_id_auto'] = trace_random_int( p, 'yapper_id_auto_generation', 100000, 999999, source='debug_yapper_id_generation', ) p.participant.vars['arup_order'] = trace_random_choice( p, 'arup_display_order', [1, 2], source='assoc_belief_display_order', ) p.participant.vars['lottery_draw_done'] = False # 2) Assign observed choice rotation unless condition will be overridden in the manual picker. for p in players: defer_to_picker = condition_picker_enabled(p.session) if not defer_to_picker: assign_observed_choice_with_rotation(p) # PAGES class Consent(Page): form_model = 'player' form_fields = ['consent'] @staticmethod def error_message(player: Player, values): if values['consent'] is False: return 'Vous devez accepter pour continuer.' class WelcomeA(Page): form_model = 'player' @staticmethod def get_form_fields(player: Player): if player.session.config.get('debug', False): player.yapper_id = player.participant.vars['yapper_id_auto'] return [] auto_yapper = get_yapper_from_participant_label(player) if auto_yapper is not None: player.yapper_id = auto_yapper return [] return ['yapper_id'] @staticmethod def vars_for_template(player: Player): prefilled_yapper = None if player.session.config.get('debug', False): prefilled_yapper = player.participant.vars['yapper_id_auto'] player.yapper_id = prefilled_yapper else: prefilled_yapper = get_yapper_from_participant_label(player) if prefilled_yapper is not None: player.yapper_id = prefilled_yapper return dict( duration=get_duration(player), debug=player.session.config.get('debug', False), auto_yapper=player.participant.vars['yapper_id_auto'], prefilled_yapper=prefilled_yapper, ) @staticmethod def error_message(player: Player, values): if 'yapper_id' not in values: return yapper_id = values.get('yapper_id') if yapper_id is None: return 'Merci de saisir votre identifiant.' if yapper_id < 0 or yapper_id > 999999: return 'Votre identifiant doit être un entier composé de 6 chiffres maximum.' class ConditionPicker(Page): form_model = 'player' form_fields = ['debug_condition_choice'] @staticmethod def is_displayed(player: Player): return condition_picker_enabled(player.session) @staticmethod def before_next_page(player: Player, timeout_happened): choice = player.field_maybe_none('debug_condition_choice') if choice: game, inf = C.DEBUG_CONDITION_MAP[choice] previous_game = player.participant.vars.get('game') previous_inf = player.participant.vars.get('inf') player.participant.vars['game'] = game player.participant.vars['inf'] = inf remember_randomization( player, 'condition_override', dict( kind='manual_override', source='condition_picker', selected_condition=choice, previous_game=previous_game, previous_inf=previous_inf, selected_game=game, selected_inf=inf, ), overwrite=True, ) # In manual picker mode, assign observed choice only after final treatment is known. if player.participant.vars.get('observed_choice') is None: assign_observed_choice_with_rotation(player) class InfoDelay(Page): pass class CompensationIntro(Page): @staticmethod def vars_for_template(player: Player): return dict(show_up_fee=get_show_up_fee(player), game=get_game(player)) class Age(Page): form_model = 'player' form_fields = ['age'] class Education(Page): form_model = 'player' form_fields = ['education'] class Employment(Page): form_model = 'player' form_fields = ['employment_status'] class Income(Page): form_model = 'player' form_fields = ['income_monthly'] class Gender(Page): form_model = 'player' form_fields = ['gender'] class MaritalStatus(Page): form_model = 'player' form_fields = ['marital_status'] class Children(Page): form_model = 'player' form_fields = ['children'] class GlobalPreference(Page): form_model = 'player' form_fields = ['global_preference'] class HelpedStrangerLastMonth(Page): form_model = 'player' form_fields = ['helped_stranger_last_month'] class VolunteeredLastMonth(Page): form_model = 'player' form_fields = ['volunteered_last_month'] class FamilySupport(Page): form_model = 'player' form_fields = ['family_support'] class MathAgreement(Page): form_model = 'player' form_fields = ['math_agreement'] class AttentionCheck(Page): form_model = 'player' form_fields = ['attention_check'] @staticmethod def is_displayed(player: Player): return get_game(player) in [1, 2, 3, 4] class AttentionCheckFailed(Page): @staticmethod def is_displayed(player: Player): # B1.0.1 shown only when the attention check failed. return get_game(player) in [1, 2, 3, 4] and player.field_maybe_none('attention_check') != 5 @staticmethod def app_after_this_page(player: Player, upcoming_apps): # Block failed participants by exiting this app after B1.0.1. return upcoming_apps[0] if upcoming_apps else None class DecisionIntro(Page): @staticmethod def is_displayed(player: Player): return has_initial_decision_block(player) and passed_attention_check(player) @staticmethod def vars_for_template(player: Player): return dict( game=get_game(player), endowment=player.participant.vars['endowment'], don=get_don(player), ) class InfoChoice(Page): form_model = 'player' form_fields = ['info_choice'] @staticmethod def is_displayed(player: Player): return has_initial_decision_block(player) and get_inf(player) == C.INF_INFO and passed_attention_check(player) @staticmethod def vars_for_template(player: Player): return dict( choice_order=stable_choice_order(player, 'info_choice_order', C.INFO_CHOICE_OPTIONS), current_choice=player.field_maybe_none('info_choice'), ) class InfoNoChoice(Page): @staticmethod def is_displayed(player: Player): return ( get_inf(player) == C.INF_NOINFO and has_initial_decision_block(player) and passed_attention_check(player) ) class InfoDefaultChoice(Page): form_model = 'player' form_fields = ['info_default_opt_out'] @staticmethod def is_displayed(player: Player): return has_initial_decision_block(player) and get_inf(player) == C.INF_INFO_DEFAULT and passed_attention_check(player) class ShowARUP(Page): @staticmethod def is_displayed(player: Player): return ( has_initial_decision_block(player) and get_inf(player) in [C.INF_INFO, C.INF_INFO_DEFAULT] and passed_attention_check(player) ) @staticmethod def vars_for_template(player: Player): inf = get_inf(player) show_arup = False if inf == C.INF_INFO: show_arup = player.field_maybe_none('info_choice') == C.INFO_CHOICE_YES elif inf == C.INF_INFO_DEFAULT: show_arup = not bool(player.info_default_opt_out) return dict(arup=player.participant.vars['arup1'], show_arup=show_arup) class DecisionObserversInfo(Page): @staticmethod def is_displayed(player: Player): return has_initial_decision_block(player) and passed_attention_check(player) class DecisionObserversType(Page): allow_back_button = True @staticmethod def is_displayed(player: Player): return has_initial_decision_block(player) and passed_attention_check(player) @staticmethod def vars_for_template(player: Player): return dict( role_text=observers_role_text(player), info_text=observers_info_text(player), ) class Decision(Page): form_model = 'player' form_fields = ['donation_choice'] @staticmethod def is_displayed(player: Player): return has_initial_decision_block(player) and passed_attention_check(player) @staticmethod def vars_for_template(player: Player): return dict(endowment=player.participant.vars['endowment'], don=get_don(player)) class DecisionRecorded(Page): @staticmethod def is_displayed(player: Player): return get_game(player) in [1, 2, 4] and passed_attention_check(player) @staticmethod def vars_for_template(player: Player): return dict( show_belief_bonus_intro=has_decision_belief_block(player), role_text=recorded_role_text(player) if has_decision_belief_block(player) else '', info_text=recorded_info_text(player) if has_decision_belief_block(player) else '', ) class DecisionBeliefRating(Page): form_model = 'player' form_fields = ['expected_moral_rating'] @staticmethod def is_displayed(player: Player): return has_decision_belief_block(player) and passed_attention_check(player) class DecisionBeliefGive(Page): form_model = 'player' form_fields = ['expected_give_rate'] @staticmethod def is_displayed(player: Player): return has_decision_belief_block(player) and passed_attention_check(player) @staticmethod def vars_for_template(player: Player): return dict(don=get_don(player)) class EvaluationIntro(Page): @staticmethod def is_displayed(player: Player): return has_evaluation_block(player) and passed_attention_check(player) @staticmethod def vars_for_template(player: Player): return dict( endowment=player.participant.vars['endowment'], don=get_don(player), game=get_game(player), # Optional screenshots to mirror the two screens shown in C1.1.0. # Can be set via session config: # screen_b11_image_url="..." # screen_b12_image_url="..." screen_b11_image_url=player.session.config.get('screen_b11_image_url', ''), screen_b12_image_url=player.session.config.get('screen_b12_image_url', ''), ) class EvalInfoChoice(Page): form_model = 'player' form_fields = ['eval_info_choice'] @staticmethod def is_displayed(player: Player): return has_evaluation_block(player) and get_inf(player) == C.INF_INFO and passed_attention_check(player) @staticmethod def vars_for_template(player: Player): return dict( choice_order=stable_choice_order(player, 'eval_info_choice_order', C.INFO_CHOICE_OPTIONS), current_choice=player.field_maybe_none('eval_info_choice'), ) class EvalInfoDefaultChoice(Page): form_model = 'player' form_fields = ['eval_info_default_opt_out'] @staticmethod def is_displayed(player: Player): return has_evaluation_block(player) and get_inf(player) == C.INF_INFO_DEFAULT and passed_attention_check(player) class EvalInfoNoChoice(Page): @staticmethod def is_displayed(player: Player): return has_evaluation_block(player) and get_inf(player) == C.INF_NOINFO and passed_attention_check(player) class EvalShowARUP(Page): @staticmethod def is_displayed(player: Player): return has_evaluation_block(player) and get_inf(player) in [C.INF_INFO, C.INF_INFO_DEFAULT] and passed_attention_check(player) @staticmethod def vars_for_template(player: Player): inf = get_inf(player) show_arup = False if inf == C.INF_INFO: show_arup = player.field_maybe_none('eval_info_choice') == C.INFO_CHOICE_YES elif inf == C.INF_INFO_DEFAULT: show_arup = not bool(player.eval_info_default_opt_out) return dict(arup=player.participant.vars['arup1'], show_arup=show_arup) class Evaluation(Page): form_model = 'player' form_fields = ['moral_rating', 'moral_comment'] @staticmethod def is_displayed(player: Player): return has_evaluation_block(player) and passed_attention_check(player) @staticmethod def vars_for_template(player: Player): current_rating = player.field_maybe_none('moral_rating') return dict( endowment=player.participant.vars['endowment'], don=get_don(player), observed_choice=player.participant.vars['observed_choice'], current_moral_rating=str(current_rating) if current_rating is not None else '', ) class EvaluationBeliefRating(Page): form_model = 'player' form_fields = ['expected_peer_rating'] @staticmethod def is_displayed(player: Player): return has_evaluation_block(player) and passed_attention_check(player) @staticmethod def vars_for_template(player: Player): return dict(observed_choice_rating_label=observed_choice_rating_label(player)) class EvaluationBeliefGive(Page): form_model = 'player' form_fields = ['expected_peer_give'] @staticmethod def is_displayed(player: Player): return has_evaluation_block(player) and passed_attention_check(player) @staticmethod def vars_for_template(player: Player): return dict(don=get_don(player)) class EvaluationBeliefs(Page): """ Backward-compatibility shim after splitting C2/C3 into two pages. Kept hidden to avoid 404 on stale links/sessions. """ @staticmethod def is_displayed(player: Player): return False @staticmethod def vars_for_template(player: Player): return dict( don=get_don(player), observed_choice_rating_label=observed_choice_rating_label(player), ) # Game 3 reuses the legacy post-evaluation block-B pages to preserve passive observation # before the participant's own donation decision. class DecisionIntroAfterEvaluation(Page): template_name = 'shaping_social_norms/DecisionIntro.html' @staticmethod def is_displayed(player: Player): return has_post_evaluation_decision_block(player) and passed_attention_check(player) @staticmethod def vars_for_template(player: Player): return dict( game=get_game(player), endowment=player.participant.vars['endowment'], don=get_don(player), ) class InfoChoiceAfterEvaluation(Page): template_name = 'shaping_social_norms/InfoChoice.html' form_model = 'player' form_fields = ['info_choice'] @staticmethod def is_displayed(player: Player): return ( has_post_evaluation_decision_block(player) and get_inf(player) == C.INF_INFO and passed_attention_check(player) ) @staticmethod def vars_for_template(player: Player): return dict( choice_order=stable_choice_order(player, 'info_choice_order', C.INFO_CHOICE_OPTIONS), current_choice=player.field_maybe_none('info_choice'), ) class InfoNoChoiceAfterEvaluation(Page): template_name = 'shaping_social_norms/InfoNoChoice.html' @staticmethod def is_displayed(player: Player): return ( has_post_evaluation_decision_block(player) and get_inf(player) == C.INF_NOINFO and passed_attention_check(player) ) class InfoDefaultChoiceAfterEvaluation(Page): template_name = 'shaping_social_norms/InfoDefaultChoice.html' form_model = 'player' form_fields = ['info_default_opt_out'] @staticmethod def is_displayed(player: Player): return ( has_post_evaluation_decision_block(player) and get_inf(player) == C.INF_INFO_DEFAULT and passed_attention_check(player) ) class ShowARUPAfterEvaluation(Page): template_name = 'shaping_social_norms/ShowARUP.html' @staticmethod def is_displayed(player: Player): return ( has_post_evaluation_decision_block(player) and get_inf(player) in [C.INF_INFO, C.INF_INFO_DEFAULT] and passed_attention_check(player) ) @staticmethod def vars_for_template(player: Player): inf = get_inf(player) show_arup = False if inf == C.INF_INFO: show_arup = player.field_maybe_none('info_choice') == C.INFO_CHOICE_YES elif inf == C.INF_INFO_DEFAULT: show_arup = not bool(player.info_default_opt_out) return dict(arup=player.participant.vars['arup1'], show_arup=show_arup) class DecisionObserversInfoAfterEvaluation(Page): template_name = 'shaping_social_norms/DecisionObserversInfo.html' @staticmethod def is_displayed(player: Player): return has_post_evaluation_decision_block(player) and passed_attention_check(player) class DecisionObserversTypeAfterEvaluation(Page): template_name = 'shaping_social_norms/DecisionObserversType.html' allow_back_button = True @staticmethod def is_displayed(player: Player): return has_post_evaluation_decision_block(player) and passed_attention_check(player) @staticmethod def vars_for_template(player: Player): return dict( role_text=observers_role_text(player), info_text=observers_info_text(player), ) class DecisionAfterEvaluation(Page): template_name = 'shaping_social_norms/Decision.html' form_model = 'player' form_fields = ['donation_choice'] @staticmethod def is_displayed(player: Player): return has_post_evaluation_decision_block(player) and passed_attention_check(player) @staticmethod def vars_for_template(player: Player): return dict(endowment=player.participant.vars['endowment'], don=get_don(player)) class DecisionRecordedAfterEvaluation(Page): template_name = 'shaping_social_norms/DecisionRecorded.html' @staticmethod def is_displayed(player: Player): return has_post_evaluation_decision_block(player) and passed_attention_check(player) @staticmethod def vars_for_template(player: Player): return dict( show_belief_bonus_intro=False, role_text='', info_text='', ) class DecisionBeliefRatingAfterEvaluation(Page): template_name = 'shaping_social_norms/DecisionBeliefRating.html' form_model = 'player' form_fields = ['expected_moral_rating'] @staticmethod def is_displayed(player: Player): return False class DecisionBeliefGiveAfterEvaluation(Page): template_name = 'shaping_social_norms/DecisionBeliefGive.html' form_model = 'player' form_fields = ['expected_give_rate'] @staticmethod def is_displayed(player: Player): return False @staticmethod def vars_for_template(player: Player): return dict(don=get_don(player)) class PostIntro(Page): @staticmethod def is_displayed(player: Player): return passed_attention_check(player) class AssocBeliefIntro(Page): @staticmethod def is_displayed(player: Player): return passed_attention_check(player) class AssocBelief(Page): form_model = 'player' form_fields = ['arup_belief_1', 'arup_belief_2'] @staticmethod def is_displayed(player: Player): return passed_attention_check(player) @staticmethod def vars_for_template(player: Player): return dict( arup1=player.participant.vars['arup1'], arup2=player.participant.vars['arup2'], arup_order=player.participant.vars['arup_order'], ) class AssocBenefit(Page): form_model = 'player' form_fields = ['arup_beneficial'] @staticmethod def is_displayed(player: Player): return passed_attention_check(player) @staticmethod def vars_for_template(player: Player): current_value = player.field_maybe_none('arup_beneficial') return dict(current_arup_beneficial=str(current_value) if current_value is not None else '') class AssocBudget(Page): form_model = 'player' form_fields = ['arup_budget_percent'] @staticmethod def is_displayed(player: Player): return passed_attention_check(player) class IncomePost(Page): form_model = 'player' form_fields = ['income_monthly_post'] @staticmethod def is_displayed(player: Player): return False class Altruism(Page): form_model = 'player' form_fields = ['altruism_charity', 'altruism_stranger', 'altruism_volunteer', 'altruism_blood'] @staticmethod def is_displayed(player: Player): return False class AltruismLegacy(Page): """ Backward-compatibility shim after removing Altruism from the active flow. Kept hidden to avoid 404 on stale session links. """ @staticmethod def is_displayed(player: Player): return False class SelfEsteem(Page): form_model = 'player' form_fields = [ 'self_esteem_1', 'self_esteem_2', 'self_esteem_3', 'self_esteem_4', 'self_esteem_5', 'self_esteem_6', 'self_esteem_7', 'self_esteem_8', 'self_esteem_9', 'self_esteem_10', ] @staticmethod def is_displayed(player: Player): return False class SelfEsteemLegacy(Page): """ Backward-compatibility shim after removing SelfEsteem from active flow. """ @staticmethod def is_displayed(player: Player): return False class Results(Page): form_model = 'player' @staticmethod def get_form_fields(player: Player): ensure_lottery(player) if has_decision_block(player) and donation_realized(player): return ['share_contact_consent'] return [] @staticmethod def is_displayed(player: Player): return passed_attention_check(player) @staticmethod def vars_for_template(player: Player): # lottery draw at end ensure_lottery(player) return dict( lottery_win=player.lottery_win, arup=player.participant.vars['arup1'], endowment=player.participant.vars['endowment'], don=get_don(player), show_up_fee=get_show_up_fee(player), ass_description=player.participant.vars['ass_description'], game=get_game(player), has_donation_decision=has_decision_block(player), donation_choice=player.field_maybe_none('donation_choice'), donation_occurs=donation_realized(player), feedback_by_email=feedback_message_enabled(player), ) class FinalCode(Page): @staticmethod def is_displayed(player: Player): return passed_attention_check(player) @staticmethod def vars_for_template(player: Player): code = player.participant.vars.get('code_validation') if not code: suffix = trace_random_int( player, 'code_validation_generation', 100000, 999999, source='final_code_generation', ) code = f"C{suffix}" player.participant.vars['code_validation'] = code trace = get_randomization_audit(player).get('code_validation_generation', {}) trace['code_validation'] = code remember_randomization(player, 'code_validation_generation', trace, overwrite=True) return dict(code_validation=code) class Debug(Page): @staticmethod def is_displayed(player: Player): return False @staticmethod def vars_for_template(player: Player): return dict( game=get_game(player), inf=get_inf(player), arup=player.participant.vars['arup1'], endowment=player.participant.vars['endowment'], don=get_don(player), ) page_sequence = [ Consent, ConditionPicker, WelcomeA, InfoDelay, CompensationIntro, Age, Education, Employment, Income, Gender, MaritalStatus, Children, MathAgreement, GlobalPreference, HelpedStrangerLastMonth, VolunteeredLastMonth, FamilySupport, AttentionCheck, AttentionCheckFailed, # Bloc B - donation DecisionIntro, InfoChoice, InfoNoChoice, InfoDefaultChoice, ShowARUP, DecisionObserversInfo, DecisionObserversType, Decision, DecisionRecorded, DecisionBeliefRating, DecisionBeliefGive, # Bloc C - observation EvaluationIntro, EvalInfoChoice, EvalInfoDefaultChoice, EvalInfoNoChoice, EvalShowARUP, Evaluation, # Post-evaluation decision block for Game 3. DecisionIntroAfterEvaluation, InfoChoiceAfterEvaluation, InfoNoChoiceAfterEvaluation, InfoDefaultChoiceAfterEvaluation, ShowARUPAfterEvaluation, DecisionObserversInfoAfterEvaluation, DecisionObserversTypeAfterEvaluation, DecisionAfterEvaluation, DecisionRecordedAfterEvaluation, DecisionBeliefRatingAfterEvaluation, # Bloc D - post questions PostIntro, DecisionBeliefGiveAfterEvaluation, EvaluationBeliefRating, EvaluationBeliefGive, EvaluationBeliefs, AssocBeliefIntro, AssocBelief, AssocBenefit, AssocBudget, IncomePost, SelfEsteem, # Bloc E - final Results, FinalCode, ] def custom_export(players): """ Export file for payment operations (Full Factory handoff): - payment-relevant raw inputs - all persisted randomization traces flattened into explicit columns - donation realized flag (depends on lottery and decision) - message content - in-app computation of the participant's variable payment excluding show-up fee """ yield [ 'session_code', 'participant_code', 'participant_id_yapper', 'game', 'inf', 'observed_choice_assigned', 'show_up_fee_eur', 'endowment_eur', 'donation_eur', 'lottery_draw_uniform', 'lottery_draw_threshold', 'lottery_win', 'donation_choice', 'donation_realized', 'lottery_payment_component_eur', 'decision_rating_bonus_component_eur', 'assoc_belief_bonus_component_eur', 'variable_bonus_eur_excl_showup', 'total_payment_eur_incl_showup', 'variable_bonus_status', 'bonus_formula_owner', 'expected_moral_rating', 'expected_peer_rating', 'expected_give_rate', 'expected_peer_give', 'feedback_mean_rating', 'feedback_sampled_codes_json', 'feedback_comments_json', 'arup_for_donations', 'arup_display_order', 'arup_belief_1', 'arup_belief_2', 'share_contact_consent', 'audit_randomization_event_count', 'audit_game_assignment_kind', 'audit_game_assignment_source', 'audit_game_assignment_options_json', 'audit_game_assignment_draw_uniform', 'audit_game_assignment_selected_index', 'audit_game_assignment_selected_value', 'audit_inf_assignment_kind', 'audit_inf_assignment_source', 'audit_inf_assignment_options_json', 'audit_inf_assignment_draw_uniform', 'audit_inf_assignment_selected_index', 'audit_inf_assignment_selected_value', 'audit_yapper_id_auto_generation_kind', 'audit_yapper_id_auto_generation_source', 'audit_yapper_id_auto_generation_range_min_inclusive', 'audit_yapper_id_auto_generation_range_max_inclusive', 'audit_yapper_id_auto_generation_draw_uniform', 'audit_yapper_id_auto_generation_selected_offset', 'audit_yapper_id_auto_generation_selected_value', 'audit_arup_display_order_kind', 'audit_arup_display_order_source', 'audit_arup_display_order_options_json', 'audit_arup_display_order_draw_uniform', 'audit_arup_display_order_selected_index', 'audit_arup_display_order_selected_value', 'audit_observed_choice_assignment_kind', 'audit_observed_choice_assignment_source', 'audit_observed_choice_assignment_rotation_bucket', 'audit_observed_choice_assignment_rotation_index', 'audit_observed_choice_assignment_selected_value', 'audit_info_choice_order_kind', 'audit_info_choice_order_source', 'audit_info_choice_order_seed', 'audit_info_choice_order_options_json', 'audit_info_choice_order_ordered_json', 'audit_info_choice_order_restored_from_storage', 'audit_eval_info_choice_order_kind', 'audit_eval_info_choice_order_source', 'audit_eval_info_choice_order_seed', 'audit_eval_info_choice_order_options_json', 'audit_eval_info_choice_order_ordered_json', 'audit_eval_info_choice_order_restored_from_storage', 'audit_lottery_kind', 'audit_lottery_source', 'audit_lottery_draw_uniform', 'audit_lottery_threshold', 'audit_lottery_win', 'audit_decision_rating_bonus_kind', 'audit_decision_rating_bonus_source', 'audit_decision_rating_bonus_response', 'audit_decision_rating_bonus_realized_mean_rating', 'audit_decision_rating_bonus_distance_squared', 'audit_decision_rating_bonus_draw_range_min', 'audit_decision_rating_bonus_draw_range_max', 'audit_decision_rating_bonus_draw_uniform', 'audit_decision_rating_bonus_draw_value', 'audit_decision_rating_bonus_win', 'audit_decision_rating_bonus_payout_eur', 'audit_assoc_belief_bonus_kind', 'audit_assoc_belief_bonus_source', 'audit_assoc_belief_bonus_first_displayed_assoc', 'audit_assoc_belief_bonus_first_displayed_field', 'audit_assoc_belief_bonus_first_displayed_response', 'audit_assoc_belief_bonus_first_displayed_is_selected', 'audit_assoc_belief_bonus_draw_1_uniform', 'audit_assoc_belief_bonus_draw_2_uniform', 'audit_assoc_belief_bonus_draw_1_value', 'audit_assoc_belief_bonus_draw_2_value', 'audit_assoc_belief_bonus_lower_threshold', 'audit_assoc_belief_bonus_upper_threshold', 'audit_assoc_belief_bonus_win', 'audit_assoc_belief_bonus_payout_eur', 'audit_feedback_sample_kind', 'audit_feedback_sample_source', 'audit_feedback_sample_seed', 'audit_feedback_sample_pool_codes_json', 'audit_feedback_sample_sample_size_requested', 'audit_feedback_sample_sampled_codes_json', 'audit_code_validation_generation_kind', 'audit_code_validation_generation_source', 'audit_code_validation_generation_range_min_inclusive', 'audit_code_validation_generation_range_max_inclusive', 'audit_code_validation_generation_draw_uniform', 'audit_code_validation_generation_selected_offset', 'audit_code_validation_generation_selected_value', 'audit_code_validation_generation_code_validation', 'audit_condition_override_kind', 'audit_condition_override_source', 'audit_condition_override_selected_condition', 'audit_condition_override_previous_game', 'audit_condition_override_previous_information_condition', 'audit_condition_override_selected_game', 'audit_condition_override_selected_information_condition', 'randomization_audit_json', 'message_content', 'code_validation', ] for p in players: ensure_lottery(p) donation_choice = p.field_maybe_none('donation_choice') donation_happened = donation_realized(p) randomization_audit = get_randomization_audit(p) lottery_trace = get_audit_payload(p, 'lottery') feedback_details = feedback_sample_details(p, players) if feedback_message_enabled(p) else empty_feedback_details() payment_details = variable_payment_details(p, players, feedback_details=feedback_details) game_assignment_trace = get_audit_payload(p, 'game_assignment') inf_assignment_trace = get_audit_payload(p, 'inf_assignment') yapper_id_trace = get_audit_payload(p, 'yapper_id_auto_generation') arup_display_order_trace = get_audit_payload(p, 'arup_display_order') observed_choice_trace = get_audit_payload(p, 'observed_choice_assignment') info_choice_order_trace = get_audit_payload(p, 'info_choice_order') eval_info_choice_order_trace = get_audit_payload(p, 'eval_info_choice_order') decision_rating_bonus_trace = get_audit_payload(p, 'decision_rating_bonus') assoc_belief_bonus_trace = get_audit_payload(p, 'assoc_belief_bonus') feedback_sample_trace = get_audit_payload(p, 'feedback_sample') code_validation_trace = get_audit_payload(p, 'code_validation_generation') condition_override_trace = get_audit_payload(p, 'condition_override') yield [ p.session.code, p.participant.code, p.field_maybe_none('yapper_id'), get_game(p), get_inf(p), p.participant.vars.get('observed_choice', ''), get_show_up_fee(p), p.participant.vars['endowment'], get_don(p), lottery_trace.get('draw_uniform', ''), lottery_trace.get('threshold', ''), int(bool(p.lottery_win)), donation_choice, int(donation_happened), payment_details['lottery_component_eur'], payment_details['decision_rating_bonus_eur'], payment_details['assoc_belief_bonus_eur'], payment_details['variable_bonus_eur_excl_showup'], payment_details['total_payment_eur_incl_showup'], payment_details['variable_bonus_status'], payment_details['bonus_formula_owner'], p.field_maybe_none('expected_moral_rating'), p.field_maybe_none('expected_peer_rating'), p.field_maybe_none('expected_give_rate'), p.field_maybe_none('expected_peer_give'), '' if feedback_details['mean_rating'] is None else feedback_details['mean_rating'], json_compact(feedback_details['sampled_codes']), json_compact(feedback_details['comments']), p.participant.vars['arup1'], p.participant.vars['arup_order'], p.field_maybe_none('arup_belief_1'), p.field_maybe_none('arup_belief_2'), int(bool(p.field_maybe_none('share_contact_consent'))), len(randomization_audit), game_assignment_trace.get('kind', ''), game_assignment_trace.get('source', ''), audit_json_field(game_assignment_trace, 'options'), game_assignment_trace.get('draw_uniform', ''), game_assignment_trace.get('selected_index', ''), game_assignment_trace.get('selected_value', ''), inf_assignment_trace.get('kind', ''), inf_assignment_trace.get('source', ''), audit_json_field(inf_assignment_trace, 'options'), inf_assignment_trace.get('draw_uniform', ''), inf_assignment_trace.get('selected_index', ''), inf_assignment_trace.get('selected_value', ''), yapper_id_trace.get('kind', ''), yapper_id_trace.get('source', ''), audit_range_value(yapper_id_trace, 0), audit_range_value(yapper_id_trace, 1), yapper_id_trace.get('draw_uniform', ''), yapper_id_trace.get('selected_offset', ''), yapper_id_trace.get('selected_value', ''), arup_display_order_trace.get('kind', ''), arup_display_order_trace.get('source', ''), audit_json_field(arup_display_order_trace, 'options'), arup_display_order_trace.get('draw_uniform', ''), arup_display_order_trace.get('selected_index', ''), arup_display_order_trace.get('selected_value', ''), observed_choice_trace.get('kind', ''), observed_choice_trace.get('source', ''), observed_choice_trace.get('rotation_bucket', ''), observed_choice_trace.get('rotation_index', ''), observed_choice_trace.get('selected_value', ''), info_choice_order_trace.get('kind', ''), info_choice_order_trace.get('source', ''), info_choice_order_trace.get('seed', ''), audit_json_field(info_choice_order_trace, 'options'), audit_json_field(info_choice_order_trace, 'ordered'), info_choice_order_trace.get('restored_from_storage', ''), eval_info_choice_order_trace.get('kind', ''), eval_info_choice_order_trace.get('source', ''), eval_info_choice_order_trace.get('seed', ''), audit_json_field(eval_info_choice_order_trace, 'options'), audit_json_field(eval_info_choice_order_trace, 'ordered'), eval_info_choice_order_trace.get('restored_from_storage', ''), lottery_trace.get('kind', ''), lottery_trace.get('source', ''), lottery_trace.get('draw_uniform', ''), lottery_trace.get('threshold', ''), lottery_trace.get('win', ''), decision_rating_bonus_trace.get('kind', ''), decision_rating_bonus_trace.get('source', ''), decision_rating_bonus_trace.get('response', ''), decision_rating_bonus_trace.get('realized_mean_rating', ''), decision_rating_bonus_trace.get('distance_squared', ''), audit_range_value(decision_rating_bonus_trace, 0), audit_range_value(decision_rating_bonus_trace, 1), decision_rating_bonus_trace.get('draw_uniform', ''), decision_rating_bonus_trace.get('draw_value', ''), decision_rating_bonus_trace.get('win', ''), decision_rating_bonus_trace.get('payout_eur', ''), assoc_belief_bonus_trace.get('kind', ''), assoc_belief_bonus_trace.get('source', ''), assoc_belief_bonus_trace.get('first_displayed_assoc', ''), assoc_belief_bonus_trace.get('first_displayed_field', ''), assoc_belief_bonus_trace.get('first_displayed_response', ''), assoc_belief_bonus_trace.get('first_displayed_is_selected', ''), assoc_belief_bonus_trace.get('draw_1_uniform', ''), assoc_belief_bonus_trace.get('draw_2_uniform', ''), assoc_belief_bonus_trace.get('draw_1_value', ''), assoc_belief_bonus_trace.get('draw_2_value', ''), assoc_belief_bonus_trace.get('lower_threshold', ''), assoc_belief_bonus_trace.get('upper_threshold', ''), assoc_belief_bonus_trace.get('win', ''), assoc_belief_bonus_trace.get('payout_eur', ''), feedback_sample_trace.get('kind', ''), feedback_sample_trace.get('source', ''), feedback_sample_trace.get('seed', ''), audit_json_field(feedback_sample_trace, 'pool_codes'), feedback_sample_trace.get('sample_size_requested', ''), audit_json_field(feedback_sample_trace, 'sampled_codes'), code_validation_trace.get('kind', ''), code_validation_trace.get('source', ''), audit_range_value(code_validation_trace, 0), audit_range_value(code_validation_trace, 1), code_validation_trace.get('draw_uniform', ''), code_validation_trace.get('selected_offset', ''), code_validation_trace.get('selected_value', ''), code_validation_trace.get('code_validation', ''), condition_override_trace.get('kind', ''), condition_override_trace.get('source', ''), condition_override_trace.get('selected_condition', ''), condition_override_trace.get('previous_game', ''), condition_override_trace.get('previous_inf', ''), condition_override_trace.get('selected_game', ''), condition_override_trace.get('selected_inf', ''), json_compact(randomization_audit), build_feedback_message(p, players, feedback_details=feedback_details), p.participant.vars.get('code_validation', ''), ]