from otree.api import ( models, widgets, BaseConstants, BaseSubsession, BaseGroup, BasePlayer, Currency as c, currency_range, ) from django.db import models as djmodels from django.db.models import F from . import ret_functions import random author = '' doc = """ Leader selection """ class Constants(BaseConstants): name_in_url = 'EGX2020' players_per_group = 4 num_rounds = 1 show_up = 5 PLS_time = 7 LeaderDecision_time = 5 payment = 10 x1 = 1.5 x2 = 2 x3 = 2.5 charity_rate = 0.15 payment_threshold = 10 # change this back to 10 after testing! additional_threshold = 20 # change this back to 20 after testing! class Subsession(BaseSubsession): random_charity = models.IntegerField() random_lottery_outcome = models.IntegerField() random_leader4 = models.IntegerField() def creating_session(self): # we look for a corresponding Task Generator in our library (ret_functions.py) that contain all task-generating # functions. So the name of the generator in 'task_fun' parameter from settings.py should coincide with an # actual task-generating class from ret_functions. self.session.vars['task_fun'] = getattr(ret_functions, self.session.config['task']) # If a task generator gets some parameters (like a level of difficulty, or number of rows in a matrix etc.) # these parameters should be set in 'task_params' settings of an app, in a form of dictionary. For instance: # 'task_params': {'difficulty': 5} self.session.vars['task_params'] = self.session.config.get('task_params', {}) # for each player we call a function (defined in Player's model) called get_or_create_task # this is done so that when a RET page is shown to a player for the first time they would already have a task # to work on for p in self.get_players(): p.get_or_create_task() # create participant variable for access in the next app p.payoff = 0 p.participant.vars['task_payoff'] = 0 # set random number for determining which charity is chosen self.random_charity = random.randint(1, 5) self.random_lottery_outcome = random.randint(1, 2) self.random_leader4 = random.randint(1, 4) class Group(BaseGroup): count_leaders = models.IntegerField() group_tables = models.IntegerField(initial=0) group_income = models.CurrencyField(initial=0) donation = models.CurrencyField(initial=0) charity = models.IntegerField(choices=[[1, 'Australian Cancer Research Foundation'], [2, 'Earthwatch Institute'], [3, 'Habitat for Humanity Australia'], [4, 'Headspace'], [5, 'Starlight Children\'s Foundation']]) leader_risky1 = models.IntegerField(initial=0) leader_risky2 = models.IntegerField(initial=0) leader_risky3 = models.IntegerField(initial=0) leader_risky = models.IntegerField(initial=0) leader_safe = models.IntegerField(initial=100) leader_x = models.CurrencyField() leader_coin = models.IntegerField(choices=[[1, 'heads'], [2, 'tails']], initial=1) random_num_leader = models.IntegerField() def select_charity(self): self.charity = self.subsession.random_charity def group_total(self): # calculate Group Total for each player players = self.get_players() group_total = 0 for p in players: p.group_correct = sum([p.solved_additional_tables]) group_total += p.group_correct self.group_tables = group_total def select_leader(self): players = self.get_players() self.count_leaders = 0 for p in players: if p.Election == 'leader': self.count_leaders += 1 if self.count_leaders == 0 or self.count_leaders == 4: if p.id_in_group == self.subsession.random_leader4: p.IsLeader = True elif self.count_leaders == 1: self.get_player_by_role('leader').IsLeader = True elif self.count_leaders in [2, 3]: candidates = [p for p in players if p.Election == 'leader'] if p.id_in_group == random.choice(candidates): p.IsLeader = True def implement_leader_investment(self): players = self.get_players() self.group_income = c(self.group_tables * Constants.charity_rate) for p in players: # implement leader's investment for group if p.IsLeader: self.leader_risky1 = p.leader_invest1 self.leader_risky2 = p.leader_invest2 self.leader_risky3 = p.leader_invest3 self.leader_coin = p.random_num_coin self.random_num_leader = p.random_num_leader if p.IsLeader and p.random_num_leader == 1: self.leader_safe = (100 - p.leader_invest1) self.leader_risky = p.leader_invest1 self.leader_x = Constants.x1 elif p.IsLeader and p.random_num_leader == 2: self.leader_safe = (100 - p.leader_invest2) self.leader_risky = p.leader_invest2 self.leader_x = Constants.x2 elif p.IsLeader and p.random_num_leader == 3: self.leader_safe = (100 - p.leader_invest3) self.leader_risky = p.leader_invest3 self.leader_x = Constants.x3 if self.leader_coin == 2: self.donation = c(self.leader_safe * self.group_income / 100) + \ c(self.leader_risky * self.group_income * self.leader_x / 100) else: self.donation = c(self.leader_safe * self.group_income / 100) # if p.IsLeader and p.random_num_coin == 2: # self.donation = c(self.leader_safe * self.group_income / 100) + \ # c(self.leader_risky * self.group_income * self.leader_x / 100) # elif p.IsLeader and p.random_num_coin == 1: # self.donation = c(self.leader_safe * self.group_income / 100) def make_invest_field(label): return models.IntegerField( label=label, choices=[[0, '0% in risky option, 100% in safe option'], [10, '10% in risky option, 90% in safe option'], [20, '20% in risky option, 80% in safe option'], [30, '30% in risky option, 70% in safe option'], [40, '40% in risky option, 60% in safe option'], [50, '50% in risky option, 50% in safe option'], [60, '60% in risky option, 40% in safe option'], [70, '70% in risky option, 30% in safe option'], [80, '80% in risky option, 20% in safe option'], [90, '90% in risky option, 10% in safe option'], [100, '100% in risky option, 0% in safe option']] ) class Player(BasePlayer): # this method returns number of correct tasks solved in this round @property def num_tasks_correct(self): return self.tasks.filter(correct_answer=F('answer')).count() @property def num_tasks_correct_additional(self): return self.num_tasks_correct - self.solved_tables # this method returns total number of tasks to which a player provided an answer @property def num_tasks_total(self): return self.tasks.filter(answer__isnull=False).count() def generate_random_nums(self): self.random_num_x = random.randint(1, 3) self.random_num_coin = random.randint(1, 2) self.random_num_leader = random.randint(1, 3) # here we store all tasks solved in this specific round - for further analysis # tasks_dump = models.LongStringField(doc='to store all tasks with answers, diff level and feedback') tasks_dump = models.LongStringField(doc='to store all tasks with answers, diff level and feedback') solved_tasks = num_tasks_correct # fields for consent form accept = models.BooleanField( widget=widgets.RadioSelect, choices=[ [True, 'I agree'], [False, 'I do not agree'] ], initial=True) # variables for task and reporting solved_tables = models.IntegerField(initial=0) solved_additional_tables = models.IntegerField(initial=0) correct_total = models.IntegerField() group_correct = models.IntegerField() final_payoff = models.CurrencyField() finished = models.BooleanField() # The following method checks if there are any unfinished (with no answer) tasks. If yes, we return the unfinished # task. If there are no uncompleted tasks we create a new one using a task-generating function from session settings def get_or_create_task(self): unfinished_tasks = self.tasks.filter(answer__isnull=True) if unfinished_tasks.exists(): return unfinished_tasks.first() else: task = Task.create(self, self.session.vars['task_fun'], **self.session.vars['task_params']) task.save() return task def role(self): if self.Election == 'leader': return 'leader' else: return 'not_leader' # decision variables Election = models.StringField(choices=[['leader', 'I want to nominate myself to take the leader position.'], ['not_leader', 'I do NOT want to nominate myself ' 'to take the leader position.']], label='', initial='leader', widget=widgets.RadioSelect) # random variables random_num_x = models.IntegerField(initial=2) random_num_coin = models.IntegerField(choices=[[1, 'heads'], [2, 'tails']], initial=1) random_num_leader = models.IntegerField(initial=2) IsLeader = models.BooleanField(initial=False) # Lottery choices ind_invest1 = make_invest_field('When x = 1.5, I wish to allocate my individual earnings ' 'as follows') ind_invest2 = make_invest_field('When x = 2, I wish to allocate my individual earnings ' 'as follows') ind_invest3 = make_invest_field('When x = 2.5, I wish to allocate my individual earnings ' 'as follows') ind_invest_chosen = make_invest_field('') leader_invest1 = make_invest_field('When x = 1.5, I wish to allocate the charity\'s ' 'earnings as follows') leader_invest2 = make_invest_field('When x = 2, I wish to allocate the charity\'s ' 'earnings as follows') leader_invest3 = make_invest_field('When x = 2.5, I wish to allocate the charity\'s ' 'earnings as follows') AdditionalTables = models.IntegerField(label='How many additional tables would you like to complete? ' 'If you prefer not to work on any additional tables, please ' 'put in 0.', max=20) CompletedTenTables = models.BooleanField(initial=False) # quiz questions q1 = models.IntegerField(choices=[[1, 'True'], [0, 'False']], label='1) You will see the statement "I want to nominate myself to ' 'take the leader position"' ' checked on your screen. If you do NOT want to take the leader position, ' 'you need to uncheck this and, instead, check the statement option ' '"I do NOT want to nominate myself to take the leader position"', widget=widgets.RadioSelect) q2 = models.StringField( choices=[['a', 'a. Everyone will have an equal chance to be selected as the leader by the computer'], ['b', 'b. Everyone will have an equal chance to be selected as the leader if no one checks the ' 'statement "I do NOT want to nominate myself to take the leader position"'], ['c', 'c. Everyone will have an equal chance to be selected as the leader if everyone checks the ' 'statement "I do NOT want to nominate myself to take the leader position"'], ['d', 'd. Both (b) and (c)']], label='2) Which of the following statement(s) is/are true?', widget=widgets.RadioSelect) q3 = models.IntegerField(choices=[[1, 'True'], [0, 'False']], label='3) Consider a group composed of Participants A, B, C and D. ' 'Suppose that only Participant A expresses a desire NOT to take the leader position ' 'by selecting "I do NOT want to nominate myself to take the leader position". ' 'Then, this participant will definitely NOT be assigned as the leader by the ' 'computer', widget=widgets.RadioSelect) q4 = models.IntegerField(choices=[[1, 'True'], [0, 'False']], label='4) Suppose that Participant A, B and C all express a desire NOT to take the leader ' 'position. Then, Participant D will definitely be assigned as the leader ' 'by the computer', widget=widgets.RadioSelect) q5 = models.StringField(label='5) Suppose a participant allocates 70% of his/her $10 into the risky option ' 'and 30% into the safe option when the multiplier x = 1.5. ' 'If the coin toss comes up heads ' 'how much will s/he earn (excluding the show-up fee)? As a reminder, the amount ' 'allocated to the risky option is multiplied by 0 if the outcome is heads , ' 'and multiplied by x if the outcome is tails', choices=['$3.00', '$7.00', '$10.00', '$12.50'], widget=widgets.RadioSelect) q6i = models.StringField( label='I) How much has the group earned for the charity before any investments are made?', widget=widgets.RadioSelect, choices=['$6.00', '$7.50', '$8.00', '$9.00']) q6ii = models.StringField( label='II) How much will the charity receive in total?', widget=widgets.RadioSelect, choices=['$10.25', '$10.50', '$12.25', '$13.50']) quiz_incorrect = models.BooleanField() def q1_error_message(self, value): if value == False: return 'Your answer to Question 1) was incorrect. You will see the statement ' \ '"I want to nominate myself to take the leader position" checked on your screen. ' \ 'If you do NOT want to take the leader position, you need to uncheck this and, instead, ' \ 'check the option "I do NOT want to nominate myself to take the leader position".' def q2_error_message(self, value): if value == 'a': return 'Your answer to Question 2) was incorrect. ' \ 'Everyone has an equal chance to be selected as the leader only if ' \ 'everybody or nobody expresses a desire NOT to take the leader position. ' \ 'If only some of the group members express a desire NOT to be leader, ' \ 'the leader will be chosen randomly among the remaining participants who ' \ 'express a desire to take the leader position.' elif value == 'b': return 'Your answer to Question 2) was incorrect. ' \ 'While everyone has an equal chance to be selected as the leader if ' \ 'nobody expresses a desire NOT to take the leader position, ' \ 'this is also the case if everybody expresses a desire NOT to take the leader position.' elif value == 'c': return 'Your answer to Question 2) was incorrect. ' \ 'While everyone has an equal chance to be selected as the leader ' \ 'if everybody expresses a desire NOT to take the leader position, ' \ 'this is also the case if nobody expresses a desire NOT to take the leader position.' def q3_error_message(self, value): if value == False: return 'Your answer to Question 3) was incorrect. ' \ 'Suppose that only one participant expresses a desire NOT to take the leader position ' \ 'by selecting "I do NOT want to nominate myself to take the leader position". ' \ 'Then, this participant will definitely NOT be assigned as the leader by the computer.' def q4_error_message(self, value): if value == False: return 'Your answer to Question 4) was incorrect. Suppose that three participants ' \ 'express a desire NOT to take the leader ' \ 'position. Then, the participant who expressed ' \ 'a desire to be leader will definitely be assigned as the leader ' \ 'by the computer.' def q5_error_message(self, value): if value != '$3.00': return 'Your answer to Question 5) was incorrect. Since the outcome of the coin toss is heads, ' \ 'the amount allocated to the risky option is multiplied by 0. Therefore, s/he will only receive ' \ 'the amount allocated to the safe option. ' def q6i_error_message(self, value): if value != '$9.00': return 'Your answer to Question 6)I) was incorrect. The group will receive $0.15 for each additional table ' \ 'completed for the charity. Therefore, the amount earned is 60 x $0.15. ' def q6ii_error_message(self, value): if value != '$13.50': return 'Your answer to Question 6)II) was incorrect. ' \ 'Since the coin toss outcome was tails, ' \ 'the charity will receive 0.5 x $9.00 x 2 = $9.00 from the risky option. ' \ 'The charity will receive 0.5 x $9.00 = $4.50 from the safe option. ' \ 'Therefore, the total amount is $9.00 + $4.50. ' # This is a custom model that contains information about individual tasks. In each round, each player can have as many # tasks as they tried to solve (we can call for the set of all tasks solved by a player by calling for instance # player.tasks.all() # Each task has a body field, html_body - actual html code shown at each page, correct answer and an answer provided by # a player. In addition there are two automatically updated/created fields that track time of creation and of an update # These fields can be used to track how long each player works on each task class Task(djmodels.Model): class Meta: ordering = ['-created_at'] player = djmodels.ForeignKey(to=Player, related_name='tasks', on_delete=djmodels.PROTECT) body = models.LongStringField() html_body = models.LongStringField() correct_answer = models.StringField() answer = models.StringField(null=True, blank=True) created_at = djmodels.DateTimeField(auto_now_add=True) updated_at = djmodels.DateTimeField(auto_now=True) task_name = models.StringField() # the following method creates a new task, and requires as an input a task-generating function and (if any) some # parameters fed into task-generating function. @classmethod def create(cls, player, fun, **params): proto_task = fun(**params) task = cls(player=player, body=proto_task.body, html_body=proto_task.html_body, correct_answer=proto_task.correct_answer, task_name=proto_task.name) return task