from otree.api import ( models, widgets, BaseConstants, BaseSubsession, BaseGroup, BasePlayer, Currency as c, currency_range ) from django.conf import settings from django.db import models as djmodels from django.db.models import F import random import csv from .fields import PickleField from string import ascii_lowercase from .reps import Allocation as AllObj, Tax, Worker from .widgets import BackButton import json from django.utils.safestring import mark_safe author = ' Niklas Wallmeier, Achim Voß, Philipp Chapkovski,' doc = """ Reallocation game """ # TODO: get rid of old allocations.json everywhere. class Constants(BaseConstants): name_in_url = 'reallocation' max_total_wrong_answers = 20 players_per_group = 4 num_others = players_per_group - 1 num_low_players = 2 num_high_players = players_per_group - num_low_players # TODO: move to adjustable settings? num_practice_tasks = 1 max_num_tasks = 8 # TODO: do we need to make it adjustable? that is the max number of tasks they can do. max_tasks_per_person = int(2 * (max_num_tasks / players_per_group)) # TODO: should we make it more sofisticated? num_rounds = 1 default_next_btn_secs = 3 sections = ["Introduction", "Instructions", "Questions", "Study"] with open('reallocation/data/tasks.csv') as f: taskstubs = list(csv.reader(f)) with open('reallocation/data/allocations.json') as json_file: allocation_data = json.load(json_file) with open('reallocation/data/altalloc.json') as json_file2: for_vue = json.load(json_file2) with open('reallocation/data/cqs.json') as json_file: cqs_data = json.load(json_file) assert len(taskstubs) >= max_num_tasks, 'You do no have enough tasks to run. Re-run prepare_tasks.py' class Subsession(BaseSubsession): partial = models.BooleanField(doc='Is this full or partial reallocation treatment', choices=((False, 'full'), (True, 'partial'))) signalling = models.BooleanField(doc='Is this sginalling treatment') tax_rate_1, tax_rate_2, tax_rate_3 = [models.FloatField() for _ in range(3)] taxes = PickleField() @property def tax_rates(self): return [getattr(self, f'tax_rate_{i}') for i in range(1, 4)] @property def allocation_params(self): return Constants.allocation_data[self.get_partial_display()] def set_config(self): """Reading settings and assign them to subsession variables, just to have them in DB later on for export.""" general_params = settings.GENERAL_ALLOCATION_SETTINGS for k in general_params.keys(): setattr(self, k, self.session.config.get(k)) self.partial = self.session.config.get('partial', False) self.signalling = self.session.config.get('signalling', False) def taxes_disp(self): return ', '.join([i.rate_desc for i in self.taxes]) def creating_session(self): self.set_config() self.create_cqs() for g in self.get_groups(): for p in self.get_players(): p.low_efficiency = p.id_in_group <= Constants.num_low_players g.allocation_dictator_id = random.randint(1, Constants.players_per_group) g.tax_rate_id = random.randint(1, 3) g.actual_tax_rate = g.get_chosen_tax_rate() def create_cqs(self): def define_correct_answer(i): if isinstance(i, int): return i return i[self.get_partial_display()] def shuffle_choices(ichoices, correct_answer): correct_answer_str = ichoices[define_correct_answer(correct_answer)] random.shuffle(ichoices) new_correct_id = ichoices.index(correct_answer_str) return ichoices, new_correct_id cqs_set = [] for p in self.get_players(): cqs_data = Constants.cqs_data.copy() cqs = [] for i in cqs_data: choices, correct_id = shuffle_choices(i['choices'].copy(), i['correct_answer']) cqs.append(CQ(player=p, label=mark_safe(i['label']), possible_choices=choices, correct_answer=correct_id)) cqs_set += cqs CQ.objects.bulk_create(cqs_set) class Group(BaseGroup): tax_rate_id = models.IntegerField() actual_tax_rate = models.FloatField() allocation_dictator_id = models.IntegerField() individual_share = models.CurrencyField(doc='share from common pool generated by taxes') total_contribution = models.CurrencyField(doc='total income by all members') total_tax = models.CurrencyField(doc='tax from total income') allocation = PickleField() tasks_for_low = models.IntegerField() tasks_for_high = models.IntegerField() def get_dictator(self): """Returns specific player randomly chosen in advance whose allocation decisions will be implemented.""" return self.get_player_by_id(self.allocation_dictator_id) def get_tax_desc(self): return "{0:.0%}".format(self.actual_tax_rate) def get_chosen_tax_rate(self): """Returns a tax rate that will be implemented for calculating payoffs.""" return getattr(self.subsession, f'tax_rate_{self.tax_rate_id}') def get_chosen_allocation(self): """Given a combination of a specific dictator and a tax rate it returns reallocation decision""" return getattr(self.get_dictator(), f'reallocation_tax{self.tax_rate_id}') def updated_allocation_dict(self): tax = self.actual_tax_rate Aset = self.allocation['A'] AincomeBeforeTax = Aset['capacity'] * Aset['productivity'] Bset = self.allocation['B'] BincomeBeforeTax = Bset['capacity'] * Bset['productivity'] total_income = AincomeBeforeTax * Aset['number'] + BincomeBeforeTax * Bset['number'] taxes = total_income * tax individual_share = taxes / Constants.players_per_group Aset['income_before_tax'] = AincomeBeforeTax Bset['income_before_tax'] = BincomeBeforeTax Aset['income_after_tax'] = round(AincomeBeforeTax * (1 - tax) + individual_share, 2) Bset['income_after_tax'] = round(BincomeBeforeTax * (1 - tax) + individual_share, 2) return {"A": Aset, "B": Bset} def set_payoffs(self): for p in self.get_players(): p.set_individual_earnings() self.total_contribution = sum([p.individual_earning for p in self.get_players()]) self.total_tax = self.total_contribution * self.actual_tax_rate self.individual_share = self.total_tax / Constants.players_per_group for p in self.get_players(): p.payoff = p.income_after_tax + self.individual_share def set_allocation(self): """Find corresponding allocation in db and assing number of tasks to do for both types, and assign in which rounds to work for each type. """ allocate = self.get_chosen_allocation() currentsubset = Constants.for_vue[self.subsession.get_partial_display()] formatted_alloc = str(int(allocate)) self.allocation = currentsubset[formatted_alloc] self.tasks_for_low = self.allocation['A']['capacity'] self.tasks_for_high = self.allocation['B']['capacity'] class Player(BasePlayer): eligible=models.BooleanField(initial=True) signal = models.BooleanField(label='Would you like to do the tasks just for fun?', widget=widgets.RadioSelectHorizontal) back = models.BooleanField(initial=False, widget=BackButton, label='') individual_earning = models.CurrencyField(doc='earnings before tax') income_after_tax = models.CurrencyField(doc='earnings after tax but before redistribution') low_efficiency = models.BooleanField(doc='is this player a low efficiency type?') payment_per_task = models.FloatField(doc='How much money each participant gets from each task') tasks_to_do = models.IntegerField(doc='How many tasks this specific player should do') reallocation_choices = PickleField() reallocation_tax1, reallocation_tax2, reallocation_tax3 = [ models.BooleanField(doc='Allocation choice for specific task level', widget=widgets.RadioSelect, label='') for _ in range(3)] def create_tasks(self): tasks = [] correction = 0 # TODO:? practice p = self p.payment_per_task = self.group.allocation[p.role()]['productivity'] p.tasks_to_do = self.group.tasks_for_low if p.low_efficiency else self.group.tasks_for_high if p.signal: p.tasks_to_do = self.group.tasks_for_high practice = True else: practice = False for t in range(p.tasks_to_do): cur_task = Constants.taskstubs[t + correction] tasks.append(Task(practice=practice, owner=p, d=cur_task[2:], question=cur_task[1], correct_answer=cur_task[0])) Task.objects.bulk_create(tasks) def role(self): return 'A' if self.low_efficiency else 'B' def set_individual_earnings(self): ntasks_solved = self.tasks.filter(practice=False, answer=F('correct_answer')).count() self.individual_earning = ntasks_solved * self.payment_per_task self.income_after_tax = self.individual_earning * (1 - self.group.actual_tax_rate) def get_next_task(self, practice=False): return self.tasks.filter(practice=practice, answer__isnull=True).first() class Task(djmodels.Model): owner = djmodels.ForeignKey(to=Player, related_name='tasks', on_delete=djmodels.CASCADE) d = PickleField() question = models.StringField() correct_answer = models.IntegerField() answer = models.IntegerField() practice = models.BooleanField() def get_table(self): return zip(ascii_lowercase[:len(self.d)], self.d) class CQ(djmodels.Model): player = djmodels.ForeignKey(to=Player, related_name='cqs', on_delete=djmodels.CASCADE) label = models.StringField() possible_choices = PickleField() answer = models.IntegerField() correct_answer = models.IntegerField() counter = models.IntegerField(initial=0)