from otree.api import * from time import time from markupsafe import Markup from itertools import product from random import choice doc = """ Your app description """ class C(BaseConstants): NAME_IN_URL = 'tax_instr' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 CUSTOM_POINTS_NAME = 'E' treatment_info = [True, False] treatment_multiplier = ['high', 'low'] num_players_in_group = 4 endowment = 100 audit_probability = 0.05 tax_rate = 0.4 max_num_timeouts = 3 timers = dict(instructions=60 * 3, instructions2=60 * 2, quiz=60 * 3) quiz = dict( quiz_1=dict( label='Da quante persone è composto il suo gruppo?', choices=[ [1, '2'], [2, '3'], [3, '4'], ], correct=3, feedback=dict( right=f'''Corretto: il suo gruppo è composto da lei e altre {num_players_in_group - 1} persone. Quindi in tutto ci sono {num_players_in_group} persone nel suo gruppo con cui interagirà per 10 rounds.''', wrong=f'''Errore: il suo gruppo è composto da lei e altre {num_players_in_group - 1} persone. Quindi in tutto ci sono {num_players_in_group} persone nel suo gruppo con cui interagirà per 10 rounds.''' ) ), quiz_2=dict( label='Di round in round, le persone che fanno parte del suo gruppo sono sempre le stesse o cambiano?', choices=[ [1, 'Sempre le stesse'], [2, 'Cambiano'] ], correct=1, feedback=dict( right=Markup(f'''Corretto: le persone che fanno parte del suo gruppo saranno sempre le stesse di round in round.'''), wrong=Markup(f'''Errore: le persone che fanno parte del suo gruppo saranno sempre le stesse di round in round.''') ) ), quiz_3=dict( label='In ogni round avete tutti reddito o reddito diverso?', choices=[ [1, 'Reddito uguale'], [2, 'Reddito diverso'] ], correct=1, feedback=dict( right=Markup(f'''Corretto: in ogni round avete tutti un reddito uguale a {endowment}{CUSTOM_POINTS_NAME}.'''), wrong=Markup(f'''Errore: in ogni round avete tutti un reddito uguale, non diverso. Esso ammonta a {endowment}{CUSTOM_POINTS_NAME}.''') ) ), quiz_4=dict( label=f'Cosa significa “dichiarare il reddito a fini fiscali data un’aliquota al {tax_rate:.0%}”?', choices=[ [1, Markup(f'''Significa che pagherò sempre {int(endowment * tax_rate)}{CUSTOM_POINTS_NAME} di tasse indipendentemente da quanto reddito decido di dichiarare''')], [2, f'Significa che pagherò una tassa del {tax_rate:.0%} sul reddito che decido di dichiarare'], [3, Markup(f'''Significa che pagherò {endowment // 2}{CUSTOM_POINTS_NAME} di tasse, ovvero metà del mio reddito''')], ], correct=2, feedback=dict( right=f'''Corretto: in ogni round dovrà volontariamente decidere quanto del suo reddito dichiarare a fini fiscali data un’aliquota al {tax_rate:.0%}. In altre parole, pagherà una tassa del {tax_rate:.0%} sul reddito dichiarato.''', wrong=f'''Errore: in ogni round dovrà volontariamente decidere quanto del suo reddito dichiarare a fini fiscali data un’aliquota al {tax_rate:.0%}. In altre parole, pagherà una tassa del {tax_rate:.0%} sul reddito dichiarato.''' ) ), quiz_5=dict( label='Se nel suo gruppo siete rimasti in 3, fra quante persone vengono redistribuite le tasse?', choices=[ [1, f'Non cambia nulla, le tasse vengono sempre redistribuite per 4 persone'], [2, 'Le tasse vengono redistribuite per 3 persone, ovvero i rimanenti nel gruppo'], [3, 'Le tasse non vengono mai redistribuite'], ], correct=2, feedback=dict( right=f'''Corretto: se a partire da un round specifico rimanete in 3 persone, le tasse vengono redistribuite per 3 persone, ovvero il numero delle persone rimanenti nel gruppo.''', wrong=f'''Errore: se a partire da un round specifico rimanete in 3 persone, le tasse vengono redistribuite per 3 persone, ovvero il numero delle persone rimanenti nel gruppo.''' ) ), quiz_6=dict( label='In quale circostanza potrà essere sanzionata/o?', choices=[ [1, 'Se il reddito dichiarato è inferiore al reddito effettivo'], [2, 'Se il reddito dichiarato è uguale al reddito effettivo'], [3, 'Se il reddito dichiarato è maggiore del reddito effettivo'], ], correct=1, feedback=dict( right=f'''Corretto: se in un round specifico avviene il controllo (esso può avvenire con una probabilità del {audit_probability:.0%}) e se il reddito dichiarato in quel round è inferiore al reddito effettivo sarà sanzionata/o con una multa uguale al doppio della tassa non pagata.''', wrong=f'''Errore: se in un round specifico avviene il controllo (esso può avvenire con una probabilità del {audit_probability:.0%}) e se il reddito dichiarato in quel round è inferiore al reddito effettivo sarà sanzionata/o con una multa uguale al doppio della tassa non pagata.''' ) ), quiz_7=dict( label='In quale circostanza sarà escluso dall’esperimento?', choices=[ [1, f'Se nell’arco dei 10 rounds il timer scadrà anche solo una volta'], [2, f'Se nell’arco dei 10 rounds il timer scadrà due volte'], [3, 'Non sarà mai escluso dall’esperimento'], ], correct=1, feedback=dict( right=f'''Corretto: c’è un timer per ogni decisione. Se nell’arco dei 10 rounds il timer scadrà anche solo una volta sarà escluso dall’esperimento e non riceverà alcun pagamento.''', wrong=f'''Errore: c’è un timer per ogni decisione. Se nell’arco dei 10 rounds il timer scadrà anche solo una volta sarà escluso dall’esperimento e non riceverà alcun pagamento.''' ) ) ) num_rounds_to_pay = 1 max_minutes_on_grouping_page = 5 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): dropout = models.BooleanField(initial=False) for name, q in C.quiz.items(): locals()[name] = models.IntegerField( label=q['label'], choices=q['choices'], widget=widgets.RadioSelect ) del name, q # FUNCTIONS def creating_session(subsession: Subsession): const = C session = subsession.session treatments = [f'{t[0]}_{t[1]}' for t in product(const.treatment_info, const.treatment_multiplier)] session.vars['treatments_assigned'] = {t: 0 for t in treatments} for p in subsession.session.get_participants(): p.vars['quiz_passed'], p.vars['quiz_errors'] = False, [] def set_timer_for_waiting(player: Player, waiting_var: str): pvars = player.participant.vars if waiting_var not in pvars: pvars[waiting_var] = time() def exclude_participants(players: list, const: C, waiting_var: str): now = time() excluded = [] for p in players: participant = p.participant if now - participant.vars[waiting_var] > 60 * const.max_minutes_on_grouping_page: participant.excluded = True excluded.append(p) if excluded: return excluded def group_by_arrival_time_method(subsession: Subsession, waiting_players: list) -> list: const = C ps_per_group = const.num_players_in_group ps = [p for p in waiting_players if not p.participant.dropout] for p in ps: set_timer_for_waiting(p, 'waiting_treatment_start') if len(ps) >= ps_per_group: return ps[:ps_per_group] excluded = exclude_participants(ps, const, 'waiting_treatment_start') if excluded: return excluded def set_treatments(group: Group): session = group.session treatments_assigned = session.vars['treatments_assigned'] m = min(treatments_assigned.values()) treatment = choice([t for t, n in treatments_assigned.items() if n == m]) session.vars['treatments_assigned'][treatment] += 1 info, multiplier = treatment.split('_') info = True if info == 'True' else False for p in group.get_players(): participant = p.participant participant.info = info participant.multiplier = multiplier # PAGES class GroupingPage(WaitPage): group_by_arrival_time = True @staticmethod def after_all_players_arrive(group: Group): return set_treatments(group) @staticmethod def is_displayed(player: Player): return player.round_number == 1 @staticmethod def app_after_this_page(player: Player, upcoming_apps): if player.participant.excluded: return upcoming_apps[-1] class Instructions(Page): timeout_seconds = C.timers['instructions'] @staticmethod def vars_for_template(player: Player): const = C return dict( tax_rate=f'{const.tax_rate:.0%}', audit_probability=f'{const.audit_probability:.0%}' ) @staticmethod def before_next_page(player: Player, timeout_happened): if timeout_happened: player.dropout = True player.participant.dropout = True @staticmethod def app_after_this_page(player: Player, upcoming_apps): participant = player.participant if participant.dropout: return upcoming_apps[-1] class Instructions2(Page): timeout_seconds = C.timers['instructions2'] @staticmethod def vars_for_template(player: Player): const = C return dict( tax_rate=f'{const.tax_rate:.0%}', audit_probability=f'{const.audit_probability:.0%}' ) @staticmethod def before_next_page(player: Player, timeout_happened): if timeout_happened: player.dropout = True player.participant.dropout = True @staticmethod def app_after_this_page(player: Player, upcoming_apps): participant = player.participant if participant.dropout: return upcoming_apps[-1] class Quiz(Page): form_model = 'player' form_fields = list(C.quiz) timeout_seconds = C.timers['quiz'] @staticmethod def vars_for_template(player: Player): const = C return dict( tax_rate=f'{const.tax_rate:.0%}', audit_probability=f'{const.audit_probability:.0%}' ) @staticmethod def js_vars(player: Player): print(player.participant.vars['quiz_errors']) return dict( quiz_errors=player.participant.vars['quiz_errors'] ) @staticmethod def error_message(player: Player, values): pvars = player.participant.vars if not pvars['quiz_passed']: errors = ['right' if values[q] == v['correct'] else 'wrong' for q, v in C.quiz.items()] pvars['quiz_errors'] = errors pvars['quiz_passed'] = all([e == 'right' for e in errors]) feedback = {q: v['feedback']['right'] if values[q] == v['correct'] else v['feedback']['wrong'] for q, v in C.quiz.items()} return feedback @staticmethod def before_next_page(player: Player, timeout_happened): if timeout_happened: player.dropout = True player.participant.dropout = True @staticmethod def app_after_this_page(player: Player, upcoming_apps): participant = player.participant if participant.dropout: return upcoming_apps[-1] page_sequence = [ GroupingPage, Instructions, Instructions2, Quiz ]