from otree.api import * import json import math import time c = cu doc = '' class C(BaseConstants): # built-in constants NAME_IN_URL = 'Labour_Decisions_Beta' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): expiry = models.FloatField(initial=0) remaining_work_time = models.IntegerField(initial=0) remaining_work_time1 = models.IntegerField(initial=0) remaining_work_time2 = models.IntegerField(initial=0) TypingNo = models.IntegerField(initial=0) Typing1No = models.IntegerField(initial=0) Typing2No = models.IntegerField(initial=0) string_time_left_history = models.LongStringField(initial='[]') quiz_bonus = models.FloatField() UQ1 = models.StringField(choices=[['100', '100 lab dollars'], ['600', '600 lab dollars'], ['3000', '3000 lab dollars'], ['20', '20 lab dollars']], label='(1) If you on average spend 20 seconds per string, which is five strings per 100 seconds. You earn 20 lab dollars per strings, no tax. What would you earn spending all 600 seconds on the task?', widget=widgets.RadioSelect) UQ2 = models.StringField(choices=[['600', '600 lab dollars'], ['100', '100 lab dollars'], ['0', '0 lab dollars'], ['300', '300 lab dollars']], label='(2) Similar to above, what would you earn spending all 0 seconds on the task, that is, you click start break right away?', widget=widgets.RadioSelect) UQ3 = models.StringField(choices=[['450', '450 lab dollars'], ['1500', '1500 lab dollars'], ['300', '300 lab dollars'], ['600', '600 lab dollars']], label='(3) Similar to above, what would you earn spending half, 300 seconds, on the task, then click start break?', widget=widgets.RadioSelect) UQ4 = models.StringField(choices=[['100', '100 seconds'], ['0', '0 seconds'], ['20', '20 seconds'], ['300', '300 seconds']], label='(4) Similar to above, if there is a tax, how much time your spend on the task would earn you most lab dollars?', widget=widgets.RadioSelect) failed_UQ1 = models.IntegerField(initial=0) failed_UQ2 = models.IntegerField(initial=0) failed_UQ3 = models.IntegerField(initial=0) failed_UQ4 = models.IntegerField(initial=0) Student = models.BooleanField(blank=True, choices=[[True, 'Yes'], [False, 'No']], label='Are you currently a student?', widget=widgets.RadioSelectHorizontal) Faculty = models.StringField(blank=True, choices=[['1', 'Arts & Science Program '], ['2', 'DeGroote School of Business (Faculty of Business)'], ['3', 'Faculty of Engineering '], ['4', 'Faculty of Health Sciences '], ['5', 'Faculty of Humanities '], ['6', 'Faculty of Science '], ['7', 'Faculty of Social Sciences '], ['8', 'Other']], label='If you are currently a student, please indicate your faculty:', widget=widgets.RadioSelect) StudyYear = models.IntegerField(blank=True, choices=[[1, '1st year undergraduate'], [2, '2nd year undergraduate'], [3, '3rd year undergraduate'], [4, '4th year undergraduate'], [5, '5th year undergraduate'], [6, 'M.A.'], [7, 'Ph.D.'], [8, 'Post-doctoral student'], [9, 'Does not apply']], label='If you are a student, what is your year of study?', widget=widgets.RadioSelect) Education = models.IntegerField( choices=[[1, 'Primary School'], [2, 'High School'], [3, 'Some College/University'], [4, 'Undergraduate Degree'], [5, 'Graduate or Professional Degree'], [6, 'Does not apply']], label='If you are not currently a student, what is your highest level of education?', widget=widgets.RadioSelect) Region = models.IntegerField(blank=True, choices=[[1, 'North America'], [2, 'Central/South America'], [3, 'Africa'], [4, 'Asia'], [5, 'Australia/New Zealand'], [6, 'Europe'], [7, 'Other region']], label='In what region of the world did you grow up?', widget=widgets.RadioSelect) Age = models.IntegerField(label='What is your age', max=125, min=13) Sex = models.StringField( choices=[['1', 'Female'], ['2', 'Male'], ['3', 'Intersex'], ['4', 'Other'], ['5', 'Choose not to respond']], label='What is your sex (biological)?', widget=widgets.RadioSelectHorizontal) Income = models.IntegerField(choices=[[1, 'Less than $30,000'], [2, '$30,000 to 59,999'], [3, '$60,000 to $99,999'], [4, '$100,000 to $129,999'], [5, '$130,000 to $159,999'], [6, '$160,000 or more'], [7, 'Do not know/ Prefer not to say']], label='Which income bracket did your household fall into in 2024?', widget=widgets.RadioSelect) Fund = models.IntegerField(blank=True, choices=[[1, '0% (none)'], [2, 'Between 0% and 25%'], [3, 'Between 25% and 50%'], [4, 'Between 50% and 75%'], [5, 'Between 75% and 100%'], [6, '100% (all)']], label='What percentage of your individual expenses is your employment income able to fund?', widget=widgets.RadioSelect) Econview = models.IntegerField( choices=[[1, 'Very Liberal'], [2, 'Liberal'], [3, 'Moderate'], [4, 'Conservative'], [5, 'Very Conservative'], [6, 'No Opinion/ Other']], label='In terms of your economic views, are you', widget=widgets.RadioSelect) PoliView = models.IntegerField( choices=[[1, 'Very Liberal'], [2, 'Liberal'], [3, 'Moderate'], [4, 'Conservative'], [5, 'Very Conservative'], [6, 'No Opinion/ Other']], label='In terms of your political views, are you', widget=widgets.RadioSelect) Toolong = models.IntegerField( choices=[[1, 'Extremely Short'], [2, ''], [3, ''], [4, ''], [5, ''], [6, 'Extremely Long']], label='How do you feel about the break time', widget=widgets.RadioSelect) Comments = models.LongStringField(blank=True, label='Below is a text box in which you may enter any comments you wish about the experiment:') Insclear = models.IntegerField( choices=[[1, '1 (I did not understand at all)'], [2, '2'], [3, '3'], [4, '4'], [5, '5'], [6, '6'], [7, '7 (I completely understood)']], label='Please indicate how clear the instructions in the experiment were to you.', widget=widgets.RadioSelectHorizontal) Risk = models.IntegerField( choices=[[1, '1 (unwilling)'], [2, '2'], [3, '3'], [4, '4'], [5, '5'], [6, '6'], [7, '7 '], [8, '8'], [9, '9'], [10, '10 (very willing)']], label='How willing are you to take risks, in general?', widget=widgets.RadioSelectHorizontal) def round_up_to_quarter(x): return cu(math.ceil(float(x) * 4) / 4) def live_notax(player: Player, data): group = player.group participant = player.participant import random SPECIAL_CHARS = ['!', '@', '#', '$', '%', '^', '&', '*', '('] pre_tax_per_string = 20 after_tax_per_string = 20 remaining = max(0, int(player.expiry - time.time())) msg_type = data.get('type') if msg_type == 'save_time_left': player.remaining_work_time = remaining participant.vars['feedback_msg'] = '' return {player.id_in_group: {'type': 'go_next'}} if msg_type == 'string_time_left': history = json.loads(player.string_time_left_history) history.append(remaining) player.string_time_left_history = json.dumps(history) if 'correct_ans' not in participant.vars: participant.vars['correct_ans'] = ''.join( random.choice(SPECIAL_CHARS) for _ in range(5) ) if 'average_time' not in participant.vars: participant.vars['average_time'] = 0 if 'completed_count' not in participant.vars: participant.vars['completed_count'] = 0 if 'pre_tax_earnings' not in participant.vars: participant.vars['pre_tax_earnings'] = 0 if 'after_tax_earnings' not in participant.vars: participant.vars['after_tax_earnings'] = 0 # Only proceed with answer checking if 'answer' key exists if 'typed_answer' in data: typed = data.get('typed_answer', '').strip() correct_ans = participant.vars.get('correct_ans', '') if typed == correct_ans: participant.vars['correct_ans'] = ''.join(random.choice(SPECIAL_CHARS) for _ in range(5)) participant.vars['completed_count'] += 1 participant.vars['average_time'] = (600 - remaining) / participant.vars['completed_count'] participant.vars['pre_tax_earnings'] += pre_tax_per_string participant.vars['after_tax_earnings'] += after_tax_per_string player.TypingNo += 1 participant.vars['feedback_msg'] = 'Correct' else: participant.vars['feedback_msg'] = 'Please try again' response = dict( new_target=participant.vars['correct_ans'], completed_count=participant.vars.get('completed_count', 0), average_time=round(participant.vars.get('average_time', 0), 2), pre_tax_earnings=round(participant.vars.get('pre_tax_earnings', 0), 2), after_tax_earnings=round(participant.vars.get('after_tax_earnings', 0), 2), pre_tax_per_string=pre_tax_per_string, after_tax_per_string=after_tax_per_string, feedback_msg=participant.vars.get('feedback_msg', ''), ) return {player.id_in_group: response} def live_progtax(player: Player, data): group = player.group participant = player.participant import random import math SPECIAL_CHARS = ['!', '@', '#', '$', '%', '^', '&', '*', '('] pre_tax_per_string = 20 after_tax_per_string = 20 if data.get('type') == 'save_time_left': player.remaining_work_time1 = int(data.get('time_left', 0)) participant.vars['feedback_msg'] = '' return {player.id_in_group: {'type': 'go_next'}} if data.get('type') == 'string_time_left': history = json.loads(player.string_time_left_history) history.append(int(data.get('time_left', 0))) player.string_time_left_history = json.dumps(history) if 'correct_ans' not in participant.vars: participant.vars['correct_ans'] = ''.join( random.choice(SPECIAL_CHARS) for _ in range(5) ) if 'completed_count2' not in participant.vars: participant.vars['completed_count2'] = 0 if 'pre_tax_earnings2' not in participant.vars: participant.vars['pre_tax_earnings2'] = 0 if 'after_tax_earnings2' not in participant.vars: participant.vars['after_tax_earnings2'] = 0 if 'after_tax_per_string3' not in participant.vars: participant.vars['after_tax_per_string3'] = 20 # Only proceed with answer checking if 'answer' key exists if 'typed_answer' in data: typed = data.get('typed_answer', '').strip() correct_ans = participant.vars.get('correct_ans', '') if typed == correct_ans: participant.vars['correct_ans'] = ''.join(random.choice(SPECIAL_CHARS) for _ in range(5)) participant.vars['completed_count2'] += 1 after_tax_per_string -= math.floor((participant.vars['completed_count2']-1) / 10)*2 participant.vars['pre_tax_earnings2'] += pre_tax_per_string participant.vars['after_tax_earnings2'] += after_tax_per_string player.Typing1No += 1 participant.vars['feedback_msg'] = 'Correct' participant.vars['after_tax_per_string3'] = after_tax_per_string after_tax_per_string = after_tax_per_string - math.floor(participant.vars['completed_count2'] / 10)*2 + math.floor((participant.vars['completed_count2']-1) / 10)*2 else: participant.vars['feedback_msg'] = 'Please try again' response = dict( new_target=participant.vars['correct_ans'], completed_count=participant.vars.get('completed_count2', 0), pre_tax_earnings=round(participant.vars.get('pre_tax_earnings2', 0), 2), after_tax_earnings=round(participant.vars.get('after_tax_earnings2', 0), 2), pre_tax_per_string=pre_tax_per_string, after_tax_per_string=round(after_tax_per_string, 2), feedback_msg=participant.vars.get('feedback_msg', ''), ) return {player.id_in_group: response} def live_flatax(player: Player, data): group = player.group participant = player.participant import random SPECIAL_CHARS = ['!', '@', '#', '$', '%', '^', '&', '*', '('] pre_tax_per_string = 20 after_tax_per_string = participant.vars['after_tax_per_string3'] if data.get('type') == 'save_time_left': player.remaining_work_time2 = int(data.get('time_left', 0)) return {player.id_in_group: {'type': 'go_next'}} if data.get('type') == 'string_time_left': history = json.loads(player.string_time_left_history) history.append(int(data.get('time_left', 0))) player.string_time_left_history = json.dumps(history) if 'correct_ans' not in participant.vars: participant.vars['correct_ans'] = ''.join( random.choice(SPECIAL_CHARS) for _ in range(5) ) if 'completed_count3' not in participant.vars: participant.vars['completed_count3'] = 0 if 'pre_tax_earnings3' not in participant.vars: participant.vars['pre_tax_earnings3'] = 0 if 'after_tax_earnings3' not in participant.vars: participant.vars['after_tax_earnings3'] = 0 # Only proceed with answer checking if 'answer' key exists if 'typed_answer' in data: typed = data.get('typed_answer', '').strip() correct_ans = participant.vars.get('correct_ans', '') if typed == correct_ans: participant.vars['correct_ans'] = ''.join(random.choice(SPECIAL_CHARS) for _ in range(5)) participant.vars['completed_count3'] += 1 participant.vars['pre_tax_earnings3'] += pre_tax_per_string participant.vars['after_tax_earnings3'] += after_tax_per_string player.Typing2No += 1 participant.vars['feedback_msg'] = 'Correct' else: participant.vars['feedback_msg'] = 'Please try again' response = dict( new_target=participant.vars['correct_ans'], completed_count=participant.vars.get('completed_count3', 0), pre_tax_earnings=round(participant.vars.get('pre_tax_earnings3', 0), 2), after_tax_earnings=round(participant.vars.get('after_tax_earnings3', 0), 2), pre_tax_per_string=pre_tax_per_string, after_tax_per_string=round(after_tax_per_string, 2), feedback_msg=participant.vars.get('feedback_msg', ''), ) return {player.id_in_group: response} class Intro(Page): pass class Task(Page): pass class NoTax(Page): pass class Quiz(Page): form_model = 'player' form_fields = ['UQ1', 'UQ2', 'UQ3', 'UQ4'] @staticmethod def error_message(player: Player, values): solutions = dict(UQ1='600', UQ2='600', UQ3='600', UQ4='0') errors = {} for name, correct_answer in solutions.items(): if values[name] != correct_answer: errors[ name] = 'Your answer to this question is incorrect. Please try again or raise your hand for help.' field_name = f'failed_{name}' current_count = getattr(player, field_name) setattr(player, field_name, current_count + 1) bonus = 0 if player.failed_UQ1 == 0: bonus += 0.25 if player.failed_UQ2 == 0: bonus += 0.25 if player.failed_UQ3 == 0: bonus += 0.25 if player.failed_UQ4 == 0: bonus += 0.25 player.quiz_bonus = bonus return errors if errors else None class Ready(Page): @staticmethod def before_next_page(player, timeout_happened): player.expiry = time.time() + 600 class Typing(Page): form_model = 'player' live_method = live_notax @staticmethod def get_timeout_seconds(player): return max(0, player.expiry - time.time()) @staticmethod def vars_for_template(player): return dict( expiry_unix_ms=int(player.expiry * 1000), initial_time_left=max(0, int(player.expiry - time.time())), ) @staticmethod def before_next_page(player, timeout_happened): if timeout_happened: player.remaining_work_time = 0 player.participant.vars['feedback_msg'] = '' class Break(Page): timer_text = 'Break time remaining:' @staticmethod def get_timeout_seconds(player): return 480 + player.remaining_work_time class ProgT(Page): pass class Typing2(Page): form_model = 'player' live_method = live_progtax class Break1(Page): timer_text = 'Break time remaining:' @staticmethod def get_timeout_seconds(player): return 480 + player.remaining_work_time1 class FlaT(Page): form_model = 'player' @staticmethod def vars_for_template(player: Player): participant = player.participant after_tax = participant.vars['after_tax_per_string3'] tax_rate = (1 - after_tax / 20) * 100 return { 'aftertaxstring': f"{after_tax:.0f}", 'taxrate': f"{tax_rate:.0f}", } class Typing3(Page): form_model = 'player' live_method = live_flatax class Break2(Page): timer_text = 'Break time remaining:' @staticmethod def is_displayed(player: Player): return player.remaining_work_time > 1 @staticmethod def get_timeout_seconds(player): return player.remaining_work_time2 class Questionnaire(Page): form_model = 'player' form_fields = ['Student', 'Faculty', 'StudyYear', 'Education', 'Region', 'Age', 'Sex', 'Income', 'Fund', 'Risk', 'Econview', 'PoliView', 'Toolong', 'Insclear', 'Comments'] @staticmethod def is_displayed(player: Player): return True class Result(Page): form_model = 'player' @staticmethod def vars_for_template(player: Player): participant = player.participant after_tax_earnings = participant.vars.get('after_tax_earnings') after_tax_earnings2 = participant.vars.get('after_tax_earnings2') after_tax_earnings3 = participant.vars.get('after_tax_earnings3') participant.payoff = player.quiz_bonus + 5 +(after_tax_earnings + after_tax_earnings2 + after_tax_earnings3 + player.remaining_work_time + player.remaining_work_time1 + player.remaining_work_time2)/200 participant.payoff = round_up_to_quarter(participant.payoff) return { 'aftertaxearnings': f"{after_tax_earnings:.0f}", 'aftertaxearnings2': f"{after_tax_earnings2:.0f}", 'aftertaxearnings3': f"{after_tax_earnings3:.0f}", 'RT1': f"{player.remaining_work_time:.0f}", 'RT2': f"{player.remaining_work_time1:.0f}", 'RT3': f"{player.remaining_work_time2:.0f}", 'T1': f"{player.TypingNo:.0f}", 'T2': f"{player.Typing1No:.0f}", 'T3': f"{player.Typing2No:.0f}", } page_sequence = [Intro, Task, NoTax, Quiz, Ready, Typing, Break, ProgT, Typing2, Break1, FlaT, Typing3, Break2, Questionnaire, Result]