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
]