from otree.api import * import json import math import time c = cu doc = '' class C(BaseConstants): # built-in constants NAME_IN_URL = 'Labour_Decisions_MTRPF0410' 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) seconds_on_page = models.FloatField() seconds_on_page1 = models.FloatField() seconds_on_page2 = models.FloatField() miss = models.IntegerField(initial=0) miss1 = models.IntegerField(initial=0) miss2 = 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) Suppose that, on average, you spend 20 seconds completing each string, which means you complete 5 strings every 100 seconds. If you earn 20 lab dollars for each string and there is no tax, how much would you earn if you spend all 600 seconds working 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 the question above, how much would you earn if you spend 0 seconds working on the task, that is, if 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 the question above, how much would you earn if you spend 300 seconds working on the task and 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 the question above, if earnings from completed strings are taxed, how much time should you spend on the task to earn the 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, '1 (Extremely Short)'], [2, '2'], [3, '3'], [4, '4'], [5, '5'], [6, '6 (Extremely Long)']], label='What do you think about the length of the break time?', widget=widgets.RadioSelectHorizontal) 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' player.miss += 1 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 remaining = max(0, int(player.expiry - time.time())) msg_type = data.get('type') if msg_type == 'save_time_left': player.remaining_work_time1 = 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_time2' not in participant.vars: participant.vars['average_time2'] = 0 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['average_time2'] = (600 - remaining) / participant.vars['completed_count2'] 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' player.miss1 += 1 response = dict( new_target=participant.vars['correct_ans'], completed_count=participant.vars.get('completed_count2', 0), average_time=round(participant.vars.get('average_time2', 0), 2), 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'] remaining = max(0, int(player.expiry - time.time())) msg_type = data.get('type') if msg_type == 'save_time_left': player.remaining_work_time2 = 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_ans3' not in participant.vars: participant.vars['correct_ans3'] = ''.join( random.choice(SPECIAL_CHARS) for _ in range(5) ) if 'average_time' not in participant.vars: participant.vars['average_time'] = 0 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['average_time3'] = (600 - remaining) / participant.vars['completed_count3'] 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' player.miss2 += 1 response = dict( new_target=participant.vars['correct_ans'], completed_count=participant.vars.get('completed_count3', 0), average_time=round(participant.vars.get('average_time3', 0), 2), 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 Headset(Page): pass class Intro(Page): pass class Task(Page): pass class NoTax(Page): form_model = 'player' form_fields = ['seconds_on_page'] 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): form_model = 'player' form_fields = ['seconds_on_page1'] class Ready1(Page): @staticmethod def before_next_page(player, timeout_happened): player.expiry = time.time() + 600 class Typing2(Page): form_model = 'player' live_method = live_progtax @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_time1 = 0 player.participant.vars['feedback_msg'] = '' 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' form_fields = ['seconds_on_page2'] @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 Ready2(Page): @staticmethod def before_next_page(player, timeout_happened): player.expiry = time.time() + 600 class Typing3(Page): form_model = 'player' live_method = live_flatax @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}", } @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_time2 = 0 player.participant.vars['feedback_msg'] = '' class Break2(Page): timer_text = 'Break time remaining:' @staticmethod def is_displayed(player: Player): return player.remaining_work_time2 > 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 = [Headset, Intro, Task, NoTax, Quiz, Ready, Typing, Break, ProgT, Ready1, Typing2, Break1, FlaT, Ready2, Typing3, Break2, Questionnaire, Result]