from otree.api import * import json from datetime import datetime, timezone from .lexicon_en import Lexicon as LexiconEN from .lexicon_fr import Lexicon as LexiconFR def get_language_and_lexicon(player, strict=True): from .lexicon_fr import Lexicon as LexiconFR from .lexicon_en import Lexicon as LexiconEN lang = ( player.participant.vars['language'] if strict else player.participant.vars.get('language') or player.field_maybe_none('language') ) if lang not in ['fr', 'en']: raise ValueError(f"Unsupported or missing language: {lang}") if lang == 'fr': return LexiconFR, 'fr' else: return LexiconEN, 'en' class C(BaseConstants): NAME_IN_URL = 'investment_test' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 HARDCODED_FORECAST = 0.07 # 7% HARDCODED_REALIZED_RETURN = 0.05 # 5% class Subsession(BaseSubsession): pass class Group(BaseGroup): pass def utc_now_iso(): return datetime.now(timezone.utc).isoformat() def append_page_timestamp(player, field_name, page_name): raw = player.field_maybe_none(field_name) or '{}' try: payload = json.loads(raw) except json.JSONDecodeError: payload = {} if field_name == 'page_shown_at_utc' and page_name in payload: return payload[page_name] = utc_now_iso() setattr(player, field_name, json.dumps(payload, ensure_ascii=True)) class Player(BasePlayer): return_expectation = models.FloatField(min=None) portfolio_allocation = models.FloatField(min=0, max=100) realized_return = models.FloatField() payoff_this_round = models.FloatField() page_shown_at_utc = models.LongStringField(blank=True) page_submitted_at_utc = models.LongStringField(blank=True) # FUNCTIONS def set_payoff(player: Player): endowment = player.session.config.get('endowment_investment', 100) invested = endowment * player.portfolio_allocation / 100 non_invested = endowment - invested player.realized_return = C.HARDCODED_REALIZED_RETURN player.payoff_this_round = non_invested + invested * (1 + player.realized_return) # PAGES class InvestmentDecisionPage(Page): form_model = 'player' form_fields = ['portfolio_allocation', 'return_expectation'] @staticmethod def error_message(player, values): if values.get('return_expectation') is None: return 'Please enter your expected return' forecast = values.get('return_expectation') if abs(forecast * 10 - round(forecast * 10)) > 1e-9: return 'Expected return must have at most one decimal place' if values.get('portfolio_allocation') is None: return 'Please set your portfolio allocation using the slider' return None @staticmethod def before_next_page(player, timeout_happened): append_page_timestamp(player, 'page_submitted_at_utc', InvestmentDecisionPage.__name__) set_payoff(player) @staticmethod def vars_for_template(player): append_page_timestamp(player, 'page_shown_at_utc', InvestmentDecisionPage.__name__) Lexicon, lang = get_language_and_lexicon(player) return dict( Lexicon=Lexicon, lang=lang, forecast=C.HARDCODED_FORECAST * 100, graph_url='/static/graphs/graph_example.png?v=3' ) class Transition(Page): @staticmethod def vars_for_template(player): append_page_timestamp(player, 'page_shown_at_utc', Transition.__name__) Lexicon, lang = get_language_and_lexicon(player) return dict( Lexicon=Lexicon, lang=lang, ) @staticmethod def before_next_page(player, timeout_happened): append_page_timestamp(player, 'page_submitted_at_utc', Transition.__name__) page_sequence = [InvestmentDecisionPage, Transition]