from otree.api import *
import random
from scenario_text import SCENARIOS
class C(BaseConstants):
NAME_IN_URL = 'scenario_feedback'
PLAYERS_PER_GROUP = None
NUM_ROUNDS = 1
PILOT_RATE_IDS = [1, 2, 3, 4, 5]
class Subsession(BaseSubsession):
pass
class Group(BaseGroup):
pass
class Player(BasePlayer):
implemented_rate_id = models.IntegerField(blank=True)
slider_rate = models.FloatField(blank=True)
def _is_pilot(session) -> bool:
return bool(session.config.get('sessionPilot', False))
def _slider_rate_for(session, rate_id: int) -> float:
if rate_id == 1:
key = 'sliders_rate'
else:
key = f'sliders_rate{rate_id}'
v = session.config.get(key)
if v is None:
raise RuntimeError(f"Missing {key} in session config.")
return float(v)
def _ensure_implemented_scenario_id(p) -> int:
"""
If already set upstream, keep it. Otherwise draw uniformly from {1,2,3}.
"""
sid_raw = p.vars.get('implemented_scenario_id')
if sid_raw is None:
sid_raw = random.choice([1, 2, 3])
p.vars['implemented_scenario_id'] = sid_raw
sid = int(sid_raw)
if sid not in SCENARIOS:
raise RuntimeError(f"implemented_scenario_id={sid} not found in SCENARIOS.")
return sid
def _ensure_implemented_rate_id(session, p) -> int | None:
"""
Pilot only. If already set upstream, keep it. Otherwise draw uniformly from PILOT_RATE_IDS.
In main sessions, returns None.
"""
if not _is_pilot(session):
return None
rid_raw = p.vars.get('implemented_rate_id')
if rid_raw is None:
rid_raw = random.choice(C.PILOT_RATE_IDS)
p.vars['implemented_rate_id'] = rid_raw
rid = int(rid_raw)
if rid not in C.PILOT_RATE_IDS:
raise RuntimeError(f"implemented_rate_id={rid} is invalid; expected one of {C.PILOT_RATE_IDS}.")
return rid
def _read_time_allocation_seconds(session, p, sid: int, rid: int | None):
"""
Returns (puzzle_seconds, slider_seconds).
Main: reads scenario_{sid}_puzzle_minutes and scenario_{sid}_slider_minutes.
Pilot: reads scenario_{sid}_puzzle_minutes_r{rid} and scenario_{sid}_slider_minutes_r{rid}.
"""
if _is_pilot(session):
if rid is None:
raise RuntimeError("Pilot session requires implemented_rate_id but got None.")
puzzle_seconds = p.vars.get(f'scenario_{sid}_puzzle_minutes_r{rid}')
slider_seconds = p.vars.get(f'scenario_{sid}_slider_minutes_r{rid}')
else:
puzzle_seconds = p.vars.get(f'scenario_{sid}_puzzle_minutes')
slider_seconds = p.vars.get(f'scenario_{sid}_slider_minutes')
return puzzle_seconds, slider_seconds
def _scenario_id_from_label(label: str) -> int | None:
"""
Map scenario letter used in WTP (A/B/C) to scenario id used in SCENARIOS (1/2/3).
Returns None if label is missing or invalid.
"""
mapping = {'A': 1, 'B': 2, 'C': 3}
return mapping.get(label)
def build_ranking_table_force_you_5th():
"""
Returns (your_rank, table_rows) such that:
- "Vous" is always rank 5
- Opponents are P1..P9
- Cells are placeholders: "P1 performance", "P1 productivité", etc.
- If show_opponents is False, opponent performance/productivity are blank
"""
order = ["P1", "P2", "P3", "P4", "Vous", "P5", "P6", "P7", "P8", "P9"]
rows = []
your_rank = None
for rank, label in enumerate(order, start=1):
if label == "Vous":
your_rank = rank
rows.append(dict(
rank=rank,
time= "Votre décision",
participant="Vous",
performance="Votre performance",
productivity="Votre productivité",
))
else:
rows.append(dict(
rank=rank,
participant=label,
time= "10 minutes",
performance=f"La performance de {label}",
productivity=f"La productivité de {label}"
))
return your_rank, rows
def build_ranking_table_force_you_5th_NoScores():
"""
Returns (your_rank, table_rows) such that:
- "Vous" is always rank 5
- Opponents are P1..P9
- Cells are placeholders: "P1 performance", "P1 productivité", etc.
- If show_opponents is False, opponent performance/productivity are blank
"""
order = ["P1", "P2", "P3", "P4", "Vous", "P5", "P6", "P7", "P8", "P9"]
rows = []
your_rank = None
for rank, label in enumerate(order, start=1):
if label == "Vous":
your_rank = rank
rows.append(dict(
rank=rank,
participant="Vous",
time="Votre décision",
performance="Votre performance",
productivity="Votre productivité",
))
else:
rows.append(dict(
rank=rank,
participant=label,
time="10 minutes",
performance="",
productivity="",
))
return your_rank, rows
class SelectionFeedback(Page):
@staticmethod
def vars_for_template(player: Player):
p = player.participant
session = player.session
use_wtp = bool(p.vars.get('use_wtp', False))
is_pilot = _is_pilot(session)
# Implemented scenario:
# - In main, you may set it upstream via WTP (keep it if present).
# - In pilot, WTP is absent, so we draw it here if missing.
sid = _ensure_implemented_scenario_id(p)
scenario = SCENARIOS[sid]
# Implemented slider rate for pilot (single call, no duplication)
rid = _ensure_implemented_rate_id(session, p)
slider_rate = _slider_rate_for(session, rid) if is_pilot else None
slider_rate = round(slider_rate * 7, 2) if is_pilot else None
# store for export
player.implemented_rate_id = rid
player.slider_rate = slider_rate
# Read the correct time allocation based on (sid, rid)
puzzle_seconds, slider_seconds = _read_time_allocation_seconds(session, p, sid, rid)
puzzle_minutes = int(puzzle_seconds) // 60 if puzzle_seconds is not None else None
slider_minutes = int(slider_seconds) // 60 if slider_seconds is not None else None
# WTP-related info only if WTP was played
wtp_pair_label = None
wtp_scenario_left = None
wtp_scenario_right = None
wtp_amount_left = None
wtp_amount_right = None
has_wtp_amounts = False
wtp_left_title = ""
wtp_right_title = ""
wtp_left_rappels_html = ""
wtp_right_rappels_html = ""
base_payment_euros = None
if use_wtp:
wtp_pair_label = p.vars.get('wtp_pay_pair_label')
wtp_scenario_left = p.vars.get('wtp_pay_scenario_left')
wtp_scenario_right = p.vars.get('wtp_pay_scenario_right')
wtp_amount_left_cents = p.vars.get('wtp_pay_amount_left_cents')
wtp_amount_right_cents = p.vars.get('wtp_pay_amount_right_cents')
wtp_amount_left = (int(wtp_amount_left_cents) / 100) if wtp_amount_left_cents is not None else None
wtp_amount_right = (int(wtp_amount_right_cents) / 100) if wtp_amount_right_cents is not None else None
has_wtp_amounts = (wtp_amount_left is not None and wtp_amount_right is not None)
# Recompose full left/right titles + rappels exactly like in WTPDecision
left_id = _scenario_id_from_label(wtp_scenario_left)
right_id = _scenario_id_from_label(wtp_scenario_right)
if left_id is not None and left_id in SCENARIOS:
wtp_left_title = SCENARIOS[left_id].get("title", "")
wtp_left_rappels_html = SCENARIOS[left_id].get("rappels_html", "")
if right_id is not None and right_id in SCENARIOS:
wtp_right_title = SCENARIOS[right_id].get("title", "")
wtp_right_rappels_html = SCENARIOS[right_id].get("rappels_html", "")
base_payment_cents = p.vars.get('wtp_base_payment_cents')
if base_payment_cents is None:
base_payment_cents = p.vars.get('guaranteed_payment_cents')
base_payment_euros = (int(base_payment_cents) / 100) if base_payment_cents is not None else None
# For display
your_rank, table_rows = build_ranking_table_force_you_5th()
your_rankNoScores, table_rowsNoScores = build_ranking_table_force_you_5th_NoScores()
return dict(
is_pilot=is_pilot,
implemented_scenario_id=sid,
implemented_scenario_label=scenario.get("label", str(sid)),
implemented_scenario_title=scenario.get("title", f"Scénario {scenario.get('label', sid)}"),
implemented_scenario_rappels_html=scenario.get("rappels_html", ""),
implemented_rate_id=rid,
slider_rate=slider_rate,
has_slider_rate=(slider_rate is not None),
puzzle_minutes=puzzle_minutes,
slider_minutes=slider_minutes,
has_time_allocation=(puzzle_minutes is not None and slider_minutes is not None),
use_wtp=use_wtp,
wtp_pair_label=wtp_pair_label,
wtp_scenario_left=wtp_scenario_left,
wtp_scenario_right=wtp_scenario_right,
wtp_amount_left=wtp_amount_left,
wtp_amount_right=wtp_amount_right,
has_wtp_amounts=has_wtp_amounts,
wtp_left_title=wtp_left_title,
wtp_right_title=wtp_right_title,
wtp_left_rappels_html=wtp_left_rappels_html,
wtp_right_rappels_html=wtp_right_rappels_html,
has_wtp_pair_label=(wtp_pair_label is not None and wtp_pair_label != ''),
base_payment_euros=base_payment_euros,
has_base_payment=(base_payment_euros is not None),
your_rank=your_rank,
table_rows=table_rows,
your_rankNoScores=your_rankNoScores,
table_rowsNoScores=table_rowsNoScores,
)
page_sequence = [SelectionFeedback]