from otree.api import * import random import time from pprint import pprint doc = """ QUESTIONS - Do I need a second ExtraModel database that stores the worker-employer pairs? CURRENT IMPORTANT TO-DO's: - Add instructions 1. market 2. work - Add payment page - Add currencies - Add information about payoffs in page. SMALL STUFF: - use NUM_JOBS instead of MAX_WORKERS BIG-PICTURE DECISIONS: - Is there a better layout for the market page? - Might be danger that people accept offers while others cancel them at the same time. How can this be made more stable? - Should I do the treatment with two different apps? Probably yes LATER TO-DO's: - Put some border around the tables - Make offers clickable. - Use easier job ids to display. Is it a problem if they are non-unique across groups? - show employers a message "your offer has been accepted" - collect timestamps of offers - pass num_employers and num_workers through session_config - set roles with 'roles'? - Integrate time into box with other information - Let workers click on rows in the offer table to accept? - Give employers an id that ranges from 1 to NUM_EMPLOYERS - Background style - Style offers table to have less width than card at the top. - Nicer offers table - Add information about allowable values of effort and wage - Add job offers as cards or list groups? - Fix employer id's to be numbered from 1 """ class C(BaseConstants): NAME_IN_URL = 'giftexchange' INSTRUCTIONS_TEMPLATE = 'giftexchange/instructions.html' PLAYERS_PER_GROUP = 2 NUM_ROUNDS = 1 NUM_EMPLOYERS = 1 NUM_WORKERS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): is_finished = models.BooleanField(initial=False) num_job_offers = models.IntegerField(initial=0) num_unmatched_workers = models.IntegerField(initial=C.NUM_WORKERS) start_timestamp = models.IntegerField() class Player(BasePlayer): is_employer = models.BooleanField() string_role = models.StringField() is_employed = models.BooleanField(initial=False) num_workers = models.IntegerField(initial=0, min=0, max=3) wait = models.BooleanField(initial=False) max_workers = models.BooleanField(initial=False) wage_received = models.IntegerField(min=0, max=100) effort_requested = models.IntegerField(min=1, max=10) effort_choice = models.IntegerField(choices=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], widget=widgets.RadioSelectHorizontal, label="Please choose an effort level:") matched_with_id = models.IntegerField() # id of the employer total_wage_paid = models.IntegerField(initial=0) total_effort_received = models.IntegerField(initial=0) class Offer(ExtraModel): group = models.Link(Group) job_id = models.IntegerField() employer_id = models.IntegerField() worker_id = models.IntegerField() wage = models.IntegerField(min=0, max=100) effort = models.IntegerField(min=0, max=10) effort_given = models.IntegerField(min=0, max=10) status = models.StringField() # FUNCTIONS def creating_session(subsession: Subsession): if subsession.round_number == 1: players = subsession.get_players() listmax = C.PLAYERS_PER_GROUP + 1 x = random.sample(range(1, listmax), C.PLAYERS_PER_GROUP) for p in players: p.temp_id = x[(p.id_in_group - 1)] if p.temp_id <= C.NUM_EMPLOYERS: p.participant.is_employer = True else: p.participant.is_employer = False for p in players: if p.participant.is_employer: p.is_employer = True p.string_role = 'employer' else: p.is_employer = False p.string_role = 'worker' def to_dict(offer: Offer): return dict( job_id=offer.job_id, employer_id=offer.employer_id, worker_id=offer.worker_id, wage=offer.wage, effort=offer.effort, effort_given=offer.effort_given, status=offer.status, ) # PAGES class Introduction(Page): @staticmethod def is_displayed(player: Player): session = player.session return session.config['final'] class WaitToStart(WaitPage): #group_by_arrival_time = True body_text = "Waiting for other players in your group to arrive." @staticmethod def is_displayed(player: Player): session = player.session return session.config['final'] @staticmethod def after_all_players_arrive(group: Group): group.start_timestamp = int(time.time()) class MarketPage(Page): timer_text = 'Time left in the market stage:' @staticmethod def get_timeout_seconds(player: Player): session = player.session return session.config['market_timeout_seconds'] @staticmethod def js_vars(player: Player): return dict(my_id=player.id_in_group, is_employer=player.is_employer) @staticmethod def live_method(player: Player, data): group = player.group # print('Received: ', data, 'from player', player.id_in_group, 'in group', player.group_id) if data['information_type'] == 'offer': group.num_job_offers += 1 Offer.create( group=group, job_id=int(str(player.group_id) + '0' + str(group.num_job_offers)), employer_id=player.id_in_group, worker_id='', wage=data['wage'], effort=data['effort'], effort_given=None, status='open', ) for p in group.get_players(): if p.id_in_group == data['employer_id']: p.wait = True elif data['information_type'] == 'accept': group.num_job_offers -= 1 group.num_unmatched_workers -= 1 if group.num_unmatched_workers == 0: group.is_finished = True current_offer = Offer.filter(group=group, job_id=data['job_id']) for o in current_offer: o.status = 'accepted' o.worker_id = player.id_in_group for p in group.get_players(): if p.id_in_group == data['employer_id']: p.num_workers += 1 p.wait = False if p.num_workers == 3: p.max_workers = True if p.id_in_group == data['worker_id']: p.is_employed = True p.wait = True p.wage_received = data['wage'] p.effort_requested = data['effort'] p.matched_with_id = data['employer_id'] p.total_wage_paid += data['wage'] elif data['information_type'] == 'cancel': group.num_job_offers -= 1 current_offer = Offer.filter(group=group, job_id=data['job_id']) for o in current_offer: o.status = 'cancelled' for p in group.get_players(): if p.id_in_group == data['employer_id']: p.wait = False elif data['information_type'] == 'load': pass else: print('unknown message type: ', data['information_type']) offers_to_show = sorted(Offer.filter(group=group), key=lambda o: o.job_id, reverse=True) offers_list = [to_dict(o) for o in offers_to_show] market_information = dict(workers_left=group.num_unmatched_workers, open_offers=sum(i['status'] == 'open' for i in offers_list)) # page_information is not player specific because you are using 'player.max_workers' and 'player.wait'. # 'player' is defined as argument of live_send() and it is the single player sending the data to o-tree and triggering live_send() # page_information = dict(is_finished=group.is_finished, # max_workers=player.max_workers, # wait=player.wait) # print('Market information', market_information) # print('Page information', page_information) # print('Offers', offers_list) data_to_return = { p.id_in_group: dict( # shifting it here to 'for p in group.get_players()' loop will make it player specific, looping for # all players in the group page_information=dict(is_finished=group.is_finished, max_workers=p.max_workers, wait=p.wait), market_information=market_information, offers=offers_list, ) for p in group.get_players() } #pprint(data_to_return) return data_to_return @staticmethod def before_next_page(player: Player, timeout_happened): if timeout_happened: player.group.is_finished = True class WorkPage(Page): form_model = 'player' form_fields = ['effort_choice'] @staticmethod def is_displayed(player: Player): return player.is_employed @staticmethod def vars_for_template(player: Player): return dict( is_employer=player.is_employer, string_role=player.string_role, wage_received=player.wage_received, effort_requested=player.effort_requested, matched_with_id=player.matched_with_id, ) @staticmethod def before_next_page(player: Player, timeout_happened): group = player.group players = group.get_players() effort_costs = {1: 0, 2: 1, 3: 2, 4: 4, 5: 6, 6: 8, 7: 10, 8: 12, 9: 15, 10: 18} offers = Offer.filter(group=group) for o in offers: o.effort_given = [p.effort_choice for p in players if p.id_in_group == o.worker_id and o.status == 'accepted'][0] for p in players: if p.is_employer is True: p.total_effort_received = sum([o.effort_given for o in offers if o.employer_id == p.id_in_group]) p.total_wage_paid = sum([o.wage for o in offers if o.employer_id == p.id_in_group]) if p.num_workers == 0: p.payoff = 0 elif p.num_workers == 1: p.payoff = 10 * p.total_effort_received - p.total_wage_paid elif p.num_workers == 2: p.payoff = 7 * p.total_effort_received - p.total_wage_paid elif p.num_workers == 3: p.payoff = 5 * p.total_effort_received - p.total_wage_paid else: print('Player', p.id_in_group, 'had too many workers!') else: if p.is_employed: effort_cost = effort_costs[p.effort_choice] p.payoff = p.wage_received - effort_cost else: p.payoff = 5 class ResultsWaitPage(WaitPage): body_text = "Waiting for workers to finish the effort stage." class Results(Page): @staticmethod def vars_for_template(player: Player): group = player.group for p in group.get_players(): player_in_all_rounds = player.in_all_rounds() return dict( is_employer=player.is_employer, is_employed=player.is_employed, num_workers=player.num_workers, wage_received=player.field_maybe_none('wage_received'), total_wage_paid=player.field_maybe_none('total_wage_paid'), total_effort_received=player.total_effort_received, effort_choice=player.field_maybe_none('effort_choice'), total_payoff=sum([p.payoff for p in player_in_all_rounds]), # paying_round=session.vars['paying_round'], ) page_sequence = [Introduction, WaitToStart, MarketPage, WorkPage, ResultsWaitPage, Results] # DATABASE def custom_export(player): # top row yield ['group', 'job_id_unique', 'job_id', 'employer_id', 'worker_id', 'wage', 'effort', 'effort_given', 'status'] # data rows offers = Offer.filter() for offer in offers: yield [offer.group, offer.job_id_unique, offer.job_id, offer.employer_id, offer.worker_id, offer.wage, offer.effort, offer.effort_given, offer.status]