from itertools import count from otree.api import * from decimal import * from settings import SESSION_CONFIG_DEFAULTS from wisconsin import Intro author = "Nathaniel Archer Lawrence, LEMMA, Université Panthéon-Assas - Paris II" doc = """ Consumption simulation with interface based off of 'Shopping app (online grocery store)' from oTree demos, see: . """ def read_csv(): import csv f = open(__name__ + '/catalog.csv', encoding='utf-8-sig') rows = [row for row in csv.DictReader(f)] for row in rows: # all values in CSV are string unless you convert them row['unit_price'] = float(row['unit_price']) return rows class C(BaseConstants): NAME_IN_URL = 'instructions' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 NUM_QUESTIONS = 11 ## Remuneration constants PARTICIPATION_FEE = SESSION_CONFIG_DEFAULTS['participation_fee'] CONVERSION_FACTOR = SESSION_CONFIG_DEFAULTS['conversion_factor'] EUROS_EXAMPLE = 10 CONVERTED_EXAMPLE = CONVERSION_FACTOR * EUROS_EXAMPLE CORRECT_ANSWER_FEE = cu(SESSION_CONFIG_DEFAULTS['correct_answer_fee'] * CONVERSION_FACTOR) PERCENT_ESTIMATION_RANGE = SESSION_CONFIG_DEFAULTS['percent_estimation_range'] PERCENT_ESTIMATION_RANGE_CORRECT = 50 PERCENT_ESTIMATION_RANGE_LOW = PERCENT_ESTIMATION_RANGE_CORRECT - PERCENT_ESTIMATION_RANGE PERCENT_ESTIMATION_RANGE_HIGH = PERCENT_ESTIMATION_RANGE_CORRECT + PERCENT_ESTIMATION_RANGE TIME_LIMIT = SESSION_CONFIG_DEFAULTS['time_limit'] # task constants NUM_PERIODS = 120 INITIAL_ENDOWMENT = cu(SESSION_CONFIG_DEFAULTS['initial_endowment']) INCOME = cu(SESSION_CONFIG_DEFAULTS['income']) INTEREST_RATE = SESSION_CONFIG_DEFAULTS['interest_rate'] INTEREST_PERCENT = round(INTEREST_RATE*100,2) INTEREST_ROUNDED = round(INTEREST_RATE,4) INTEREST_EARNED = cu((INITIAL_ENDOWMENT + INCOME) * INTEREST_RATE ) CONSUMPTION_RATE = 1 # amount of good consumed from stock balance each period MONETARY_POLICY = SESSION_CONFIG_DEFAULTS['monetary_policy'] TOTAL_CASH = cu(INITIAL_ENDOWMENT + INCOME) # list of products taken from csv file PRODUCTS = read_csv() # SKU = 'stock keeping unit' = product ID PRODUCTS_DICT = {row['sku']: row for row in PRODUCTS} ## For practice questions QUANTITY = 4 QUANTITY_SAVINGS = QUANTITY - 2 NEW_CASH = TOTAL_CASH - (PRODUCTS_DICT['1']['unit_price'] * QUANTITY_SAVINGS) NEW_STOCK = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): ## Session indices day = models.IntegerField() app_sequence = models.IntegerField() inf_sequence = models.IntegerField() intervention = models.IntegerField() ### choices = [[value,label],[value,label],...] q1 = models.IntegerField( label='The final value of which of these balances determines your performance-based remuneration for the Savings Game?', widget=widgets.RadioSelect ) q2 = models.IntegerField( label='''What is the minimum amout you must have in your Stock before clicking the "Finalize Purchase" button to survive to the next month?''', widget=widgets.RadioSelect ) q3 = models.IntegerField( label='Can the interest rate change during the Savings Game?', widget=widgets.RadioSelect ) q4 = models.IntegerField( label='Suppose a player had 100₮ in Savings Account last month. If they earn 10₮ in interest and receive 20₮ in monthly salary, what will their Total Cash be?', widget=widgets.RadioSelect ) q5 = models.IntegerField( label='', widget=widgets.RadioSelect ) q6 = models.IntegerField( choices = [ [1,'1 – Not at all confident to have made the right decision'], [2,'2'], [3,'3'], [4,'4'], [5,'5 – Absolutely confident to have made the right decision'] ], label='How confident are you that you made the right decision this month?', widget=widgets.RadioSelect ) q7 = models.FloatField() ## Check: Select given quantity q8 = models.FloatField() ## Check: Select for given Final Savings q9 = models.FloatField() ## Check: Select for given Stock q10 = models.IntegerField() total_price = models.CurrencyField(initial=0) initial_savings = models.CurrencyField(initial=0) cashOnHand = models.CurrencyField(initial=0) finalSavings = models.CurrencyField(initial=0) finalStock = models.IntegerField(initial=0) interestEarned = models.CurrencyField(initial=0) newPrice = models.FloatField(initial=C.PRODUCTS_DICT['1']['unit_price']) q11 = models.IntegerField( label='Which of the following values cannot change at any point during the Savings Game?', widget=widgets.RadioSelect ) def q1_choices(player): import random choices = [[1,'Savings Account'],[2,'Stock'],[3,'Total Cash'],[4,'Interest earned']] random.shuffle(choices) return choices def q2_choices(player): import random choices = [[1,'1'],[2,'0'],[3,'3'],[4,'120']] random.shuffle(choices) return choices def q3_choices(player): import random choices = [[1,'No'], [2,'Yes'], [3,'It depends how much is in Savings Account'], [4,'It depends how much is in Total Cash']] random.shuffle(choices) return choices def q4_choices(player): import random choices = [[1,cu(130)], [2,cu(110)], [3,cu(120)], [4,cu(100)], [5,'We do not have sufficient information to determine.']] random.shuffle(choices) return choices def q5_choices(player): import random choices = [[1,'There is no minimum as long as my Stock requirement is met'], [2,'1'], [3,'-1'], [4,'120']] random.shuffle(choices) return choices def q11_choices(player): import random choices = [[1,'The interest rate paid on Savings Account'], [2,'The price of Food'], [3,'The Interest Earned Last Month'], [4,'None of the above']] random.shuffle(choices) return choices class Item(ExtraModel): player = models.Link(Player) sku = models.StringField() name = models.StringField() quantity = models.IntegerField() unit_price = models.CurrencyField() newPrice = models.FloatField(initial=C.PRODUCTS_DICT['1']['unit_price']) # Calculate total price of item with quantity selected def total_price(item: Item): return item.quantity * item.unit_price # Convert information about items into a dictionary def to_dict(item: Item): return dict( sku=item.sku, name=item.name, quantity=item.quantity, total_price=total_price(item) ) # Send info to HTML def live_method(player: Player, data): if player.round_number == 1: player.newPrice = float(C.PRODUCTS_DICT['1']['unit_price']) if 'sku' in data: sku = data['sku'] delta = data['delta'] product = C.PRODUCTS_DICT[sku] matches = Item.filter(player=player, sku=sku) if matches: [item] = matches item.quantity += delta if item.quantity <= 0: item.delete() else: if delta > 0: Item.create( player=player, quantity=delta, sku=sku, name=product['name'], unit_price=player.newPrice ) items = Item.filter(player=player) item_dicts = [to_dict(item) for item in items] player.total_price = sum([total_price(item) for item in items]) player.initial_savings = C.INITIAL_ENDOWMENT player.cashOnHand = player.initial_savings + C.INCOME player.finalSavings = player.cashOnHand - player.total_price player.finalStock = sum([item.quantity for item in items]) player.interestEarned = 0 player.newPrice = player.newPrice return { player.id_in_group: dict( items=item_dicts, total_price=player.total_price, initial_savings=player.initial_savings, interestEarned=player.interestEarned, cashOnHand=player.cashOnHand, finalSavings=player.finalSavings, finalStock=player.finalStock, newPrice=player.newPrice, ) } # PAGES class Instructions_1(Page): counter_questions = 0 class Instructions_2(Page): live_method = live_method counter_questions = Instructions_1.counter_questions + 1 ## For progress bars @staticmethod def vars_for_template(player: Player): return dict( percentage=round(Instructions_2.counter_questions / C.NUM_QUESTIONS * 100,) ) class Instructions_3(Page): live_method = live_method form_model = 'player' form_fields = ['q8'] counter_questions = Instructions_2.counter_questions + 1 ## For progress bars @staticmethod def vars_for_template(player: Player): return dict( percentage=round(Instructions_3.counter_questions / C.NUM_QUESTIONS * 100,) ) @staticmethod def error_message(player, values): solutions = dict( q8=C.QUANTITY ) error_messages = dict( q8='The Quantity in My Cart is not {} units of Food. Please, review the instructions and adjust the Quantity in My Cart.'.format(C.QUANTITY) ) for field_name in solutions: if values[field_name] != solutions[field_name]: error_messages = error_messages[field_name] return error_messages class Instructions_4(Page): live_method = live_method form_model = 'player' form_fields = ['q9'] counter_questions = Instructions_3.counter_questions + len(Instructions_3.form_fields) ## For progress bars @staticmethod def vars_for_template(player: Player): return dict( percentage=round(Instructions_4.counter_questions / C.NUM_QUESTIONS * 100,) ) @staticmethod def error_message(player, values): solutions = dict( q9=float(C.NEW_CASH) ) error_messages = dict( q9='The value of Savings Account is not {}. Please, review the instructions. Adjust the Quantity in My Cart while paying attention to the value in Savings Account.'.format(C.NEW_CASH) ) for field_name in solutions: if values[field_name] != solutions[field_name]: error_messages = error_messages[field_name] return error_messages class Instructions_5(Page): live_method = live_method form_model = 'player' form_fields = ['q10'] counter_questions = Instructions_4.counter_questions + len(Instructions_4.form_fields) ## For progress bars @staticmethod def vars_for_template(player: Player): return dict( percentage=round(Instructions_5.counter_questions / C.NUM_QUESTIONS * 100,) ) @staticmethod def error_message(player, values): solutions = dict( q10=C.NEW_STOCK ) error_messages = dict( q10='Your Stock does not have {} units. Please try adjusting the quantity in My Cart while paying attention to the value in Stock.'.format(C.NEW_STOCK) ) for field_name in solutions: if values[field_name] != solutions[field_name]: error_messages = error_messages[field_name] return error_messages class Instructions_6(Page): form_model = 'player' form_fields = ['q1'] counter_questions = Instructions_5.counter_questions + len(Instructions_5.form_fields) ## For progress bars @staticmethod def vars_for_template(player: Player): return dict( percentage=round(Instructions_6.counter_questions / C.NUM_QUESTIONS * 100,) ) @staticmethod def error_message(player, values): solutions = dict( q1=1, ) error_messages = dict( q1='That is incorrect. Please, review the instructions and correct your answer.' ) for field_name in solutions: if values[field_name] != solutions[field_name]: error_messages = error_messages[field_name] return error_messages class Instructions_7(Page): form_model = 'player' form_fields = ['q2'] counter_questions = Instructions_6.counter_questions + len(Instructions_6.form_fields) ## For progress bars @staticmethod def vars_for_template(player: Player): return dict( percentage=round(Instructions_7.counter_questions / C.NUM_QUESTIONS * 100,) ) @staticmethod def error_message(player, values): solutions = dict( q2=1, ) error_messages = dict( q2='That is incorrect. Please, review the instructions and correct your answer.' ) for field_name in solutions: if values[field_name] != solutions[field_name]: error_messages = error_messages[field_name] return error_messages class Instructions_8(Page): live_method = live_method form_model = 'player' form_fields = ['q3'] counter_questions = Instructions_7.counter_questions + len(Instructions_7.form_fields) ## For progress bars @staticmethod def vars_for_template(player: Player): return dict( percentage=round(Instructions_8.counter_questions / C.NUM_QUESTIONS * 100,) ) @staticmethod def error_message(player, values): solutions = dict( q3=1 ) error_messages = dict( q3='That is incorrect. The interest rate (%) does not change. The Interest Earned Last Month may vary based on the amount in your Savings Account when you end a month.' ) for field_name in solutions: if values[field_name] != solutions[field_name]: error_messages = error_messages[field_name] return error_messages class Instructions_9(Page): live_method = live_method form_model = 'player' form_fields = ['q4'] counter_questions = Instructions_8.counter_questions + len(Instructions_8.form_fields) ## For progress bars @staticmethod def vars_for_template(player: Player): return dict( percentage=round(Instructions_9.counter_questions / C.NUM_QUESTIONS * 100,) ) @staticmethod def error_message(player, values): solutions = dict( q4=1 ) error_messages = dict( q4='That is incorrect. Note that Total Cash = Savings Account (from previous month) + Interest Earned Last Month + Monthly Salary.' ) for field_name in solutions: if values[field_name] != solutions[field_name]: error_messages = error_messages[field_name] return error_messages class Instructions_10(Page): form_model = 'player' form_fields = ['q6'] counter_questions = Instructions_9.counter_questions + len(Instructions_9.form_fields) ## For progress bars @staticmethod def vars_for_template(player: Player): return dict( percentage=round(Instructions_10.counter_questions / C.NUM_QUESTIONS * 100,) ) @staticmethod def error_message(player, values): solutions = dict( q6=3 ) error_messages = dict( q6='''That is incorrect. "Completely Indifferent" means the value halfway between "1 – Not at all confident to have made the right decision" and "5 – Absolutely confident to have made the right decision"''' ) for field_name in solutions: if values[field_name] != solutions[field_name]: error_messages = error_messages[field_name] return error_messages class Instructions_11(Page): form_model = 'player' form_fields = ['q7'] counter_questions = Instructions_10.counter_questions + len(Instructions_10.form_fields) ## For progress bars @staticmethod def vars_for_template(player: Player): return dict( percentage=round(Instructions_11.counter_questions / C.NUM_QUESTIONS * 100,) ) @staticmethod def error_message(player, values): solutions = dict( q7=1 ) error_messages = dict( q7='You have not placed the slider on {}%. Try adjusting the slider again.'.format(solutions['q7']) ) for field_name in solutions: if values[field_name] != solutions[field_name]: error_messages = error_messages[field_name] return error_messages class Results(Page): counter_questions = Instructions_11.counter_questions + len(Instructions_11.form_fields) print(counter_questions) ## For progress bars @staticmethod def vars_for_template(player: Player): return dict( percentage=round(Results.counter_questions / C.NUM_QUESTIONS * 100,) ) page_sequence = [ Instructions_1, Instructions_2, Instructions_3, Instructions_4, Instructions_5, Instructions_6, Instructions_7, Instructions_8, Instructions_9, Instructions_10, Instructions_11, Results ]