Displaying results¶
Results pages show participants their outcomes, payoffs, and other players' choices. uproot uses Jinja2 templates with full access to Python builtins, enabling calculations and logic directly in your templates.
Basic results display¶
Create a Results page and pass data via the templatevars method:
class Results(Page):
@classmethod
def templatevars(page, player):
return dict(
other=player.other_in_group,
)
In the template, access player data and context variables:
{% extends "Base.html" %}
{% block main %}
<p>You chose to {{ "cooperate" if player.cooperate else "defect" }}.</p>
<p>Your partner chose to {{ "cooperate" if other.cooperate else "defect" }}.</p>
<p>Your payoff is <b>{{ player.payoff }}</b>.</p>
{% endblock main %}
See the prisoners_dilemma example
Accessing other players' data¶
Two-player groups¶
Access the other player via the virtual field player.other_in_group directly in templates, or pass it explicitly via templatevars:
class Results(Page):
@classmethod
def templatevars(page, player):
return dict(other=player.other_in_group)
See the trust_game example · ultimatum_game example
Larger groups¶
Pass a list of other players:
class Results(Page):
@classmethod
def templatevars(page, player):
return dict(others=player.others_in_group)
<p>Other guesses:
{% for other in others %}
{{ other.guess }}{% if not loop.last %}, {% endif %}
{% endfor %}
</p>
See the beauty_contest example · minimum_effort_game example
Formatting numbers¶
The to filter¶
Format decimal places with the | to(n) filter:
<p>Your score: {{ player.score | to(1) }}</p> <!-- 7.3 -->
<p>Amount: ${{ player.amount | to(2) }}</p> <!-- $12.50 -->
<p>Percentage: {{ player.pct | to(0) }}%</p> <!-- 85% -->
See the big5 example · focal_point example
The fmtnum filter¶
For currency and units with prefix/suffix:
{{ player.payoff | fmtnum(pre="$", places=2) }} <!-- $10.50 -->
{{ player.payoff | fmtnum(post=" EUR", places=2) }} <!-- 10.50 EUR -->
{{ player.change | fmtnum(pre="$", places=2) }} <!-- −$5.00 (uses minus sign) -->
Calculations in templates¶
uproot passes all Python builtins to templates. Perform calculations directly:
<!-- Arithmetic -->
<p>Total: {{ player.claim + other.claim }}</p>
<p>Tripled amount: {{ sent * 3 }}</p>
<p>Share: {{ player.contribution / total * 100 | to(1) }}%</p>
<!-- Comparisons -->
{% if player.claim + other.claim <= 100 %}
<p>The sum is $100 or less.</p>
{% else %}
<p>The sum exceeds $100.</p>
{% endif %}
Using Python builtins¶
Call sum(), max(), min(), len(), range(), enumerate(), zip(), and other builtins:
<!-- Sum contributions -->
<p>Group total: {{ sum(p.contribution for p in others) + player.contribution }}</p>
<!-- Find extremes -->
<p>Highest bid: {{ max(p.bid for p in others) }}</p>
<!-- Enumerate items -->
{% for i, item in enumerate(items) %}
<p>{{ i + 1 }}. {{ item }}</p>
{% endfor %}
<!-- Zip lists together -->
{% for option_a, option_b in zip(options_a, options_b) %}
<tr>
<td>{{ option_a }}</td>
<td>{{ option_b }}</td>
</tr>
{% endfor %}
Conditional display¶
Role-based results¶
Show different content based on player role:
{% if player.trustor %}
<p>You sent <b>{{ sent }}</b>, which was tripled to {{ tripled }}.</p>
<p>The other player returned <b>{{ returned }}</b> to you.</p>
{% else %}
<p>The other player sent {{ sent }}, tripled to <b>{{ tripled }}</b>.</p>
<p>You returned <b>{{ returned }}</b>.</p>
{% endif %}
See the trust_game example · dictator_game example
Outcome-based messages¶
{% if player.winner %}
<p><b>You won!</b></p>
{% else %}
<p>You did not win this round.</p>
{% endif %}
See the beauty_contest example
History tables¶
Using player.along()¶
For repeated games, iterate through all rounds with player.along():
<table class="table">
<thead>
<tr>
<th>Round</th>
<th>Your number</th>
</tr>
</thead>
<tbody>
{% for round, data in player.along("round") %}
<tr>
<td>{{ round }}</td>
<td>{{ data.number }}</td>
</tr>
{% endfor %}
</tbody>
</table>
The along("round") method returns tuples of (round_number, player_data_for_that_round).
Using player.within()¶
Access a specific round's data with player.within(round=n):
<table class="table">
<thead>
<tr>
<th>Round</th>
<th>You</th>
<th>Partner</th>
</tr>
</thead>
<tbody>
{% for round in rounds_so_far %}
<tr>
<td>{{ round }}</td>
<td>
{% if player.within(round=round).cooperate %}
Cooperated
{% else %}
Defected
{% endif %}
</td>
<td>
{% if other.within(round=round).cooperate %}
Cooperated
{% else %}
Defected
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
Pass rounds_so_far from templatevars:
class Decision(Page):
@classmethod
def templatevars(page, player):
return dict(
other=player.other_in_group,
rounds_so_far=range(1, player.round),
)
See the prisoners_dilemma_repeated example
Accessing constants¶
The C class is available in templates:
Available filters¶
| Filter | Purpose | Example |
|---|---|---|
to(n) |
Format to n decimal places | {{ x \| to(2) }} → 3.14 |
fmtnum(pre, post, places) |
Format with prefix/suffix | {{ x \| fmtnum(pre="$") }} |
tojson |
Convert to JSON | {{ data \| tojson }} |
repr |
Python repr | {{ x \| repr }} |
tojson and | safe¶
tojson converts a Python value to a JSON string. Jinja2's autoescaping then HTML-encodes the result. This is the right default: it keeps values safe in HTML attributes and element content.
Most of the time, tojson alone is all you need. Numbers, booleans, and None produce JSON like 42, true, or null — no characters that autoescaping would change. Even inside <script> blocks, autoescaping is a no-op for these types, so | safe is unnecessary:
{# These all work without | safe, everywhere — including <script> blocks #}
{{ player.score | tojson }} {# 42 #}
{{ player.is_buyer | tojson }} {# true #}
{{ player.profit | tojson }} {# null #}
| safe is only needed when you put a value that may contain strings into a <script> block, because autoescaping would turn " into " and break JavaScript parsing:
<script>
{# results contains strings → must use | safe inside <script> #}
let data = {{ results | tojson | safe }};
</script>
In HTML (attributes, element content), never add | safe — autoescaping is what protects you:
{# Safe: autoescaping prevents XSS even if player.response contains malicious strings #}
<div x-data="{ response: {{ player.response | tojson }} }">
Warning
Only use | safe inside <script> blocks, and only when the value may contain strings. Adding | safe in HTML attributes or element content disables escaping and can introduce XSS vulnerabilities.
Summary¶
| Feature | Purpose |
|---|---|
templatevars(page, player) |
Pass variables to template |
player.other_in_group |
Get partner in 2-player group |
player.others_in_group |
Get all other group members |
players(group) |
Get all group members (function form) |
player.along("round") |
Iterate all rounds |
player.within(round=n) |
Access specific round data |
{{ expression }} |
Output values |
{% if %}...{% endif %} |
Conditional display |
{% for %}...{% endfor %} |
Loops |
| Python builtins | sum(), max(), range(), etc. |