Pages and templates¶
Pages are the building blocks of uproot experiments. Each page represents a screen that participants see—instructions, questions, feedback, or results. Pages are defined as Python classes and rendered using HTML templates.
Defining a page¶
A page is a class that inherits from Page:
This minimal page displays the template Welcome.html from your app's templates folder. Every page needs a corresponding template file.
The page_order list¶
The page_order list defines which pages participants see and in what sequence:
Participants progress through pages in order. You can use SmoothOperators to randomize, repeat, or conditionally select pages.
Templates¶
Templates are HTML files that define what participants see. uproot uses Jinja2 for templating.
Template naming and location¶
By default, uproot looks for a template matching the page class name:
To use a custom template path:
Basic template structure¶
A typical template extends the base layout and defines content:
{% extends "_uproot/Page.html" %}
{% block content %}
<h1>Welcome to the experiment</h1>
<p>Thank you for participating.</p>
{% endblock %}
The _uproot/Page.html base template provides the form wrapper, navigation buttons, and styling. Your content goes in the content block.
Adding a form¶
To collect data, include form fields in your template:
{% extends "_uproot/Page.html" %}
{% block content %}
<h1>Your decision</h1>
{{ form.amount.label }}
{{ form.amount }}
{% endblock %}
See Collecting data with forms for details on defining form fields.
The context method¶
Use the context method to pass data from Python to your template:
class Results(Page):
@classmethod
def context(page, player):
return dict(
earnings=player.payoff,
partner_choice=other_in_group(player).choice,
)
Then use these variables in your template:
{% extends "_uproot/Page.html" %}
{% block content %}
<h1>Results</h1>
<p>You earned {{ earnings }} points.</p>
<p>Your partner chose: {{ partner_choice }}</p>
{% endblock %}
The context method receives page (the page class) and player (the current participant's data).
Built-in template variables¶
Every template has access to these variables:
| Variable | Description |
|---|---|
player |
The current participant's data |
form |
The form instance (if the page has fields) |
page |
The page class |
C |
Constants defined in your app's C class |
session |
The current session |
_("text") |
Translation function for internationalization |
Accessing player data¶
Using constants¶
Define constants in your app:
Use them in templates:
Static files¶
App-specific static files¶
Place static files (images, CSS, JavaScript) in a static/ folder within your app:
Reference them using appstatic():
<img src="{{ appstatic('diagram.png') }}" alt="Diagram">
<link rel="stylesheet" href="{{ appstatic('custom.css') }}">
Project-wide static files¶
For files shared across apps, use a project-level static folder and projectstatic().
Conditional page display¶
Use the show method to conditionally display pages:
class BonusRound(Page):
@classmethod
def show(page, player):
return player.score >= 80 # Only show if score is high enough
Pages where show returns False are skipped automatically.
Role-based pages¶
A common pattern for multiplayer experiments:
class ProposerDecision(Page):
@classmethod
def show(page, player):
return player.role == "proposer"
class ResponderDecision(Page):
@classmethod
def show(page, player):
return player.role == "responder"
Allowing back navigation¶
By default, participants can only move forward. To allow going back:
This adds a "Back" button that lets participants revisit and change previous answers.
Note
Back navigation only re-displays pages—it doesn't undo any data changes or re-run page logic.
NoshowPage for logic-only pages¶
Sometimes you need to run code without displaying anything to participants. Use NoshowPage:
class CalculatePayoffs(NoshowPage):
@classmethod
def after_always_once(page, player):
player.payoff = player.correct_answers * 10
NoshowPage runs its lifecycle methods but never renders a template. Use it for:
- Calculating scores or payoffs
- Initializing player data
- Setting up randomization
See NoshowPage in the big5_short example
Page lifecycle methods¶
Pages have several methods that run at different points:
| Method | When it runs |
|---|---|
show |
Before displaying—return False to skip the page |
context |
Before rendering—return template variables |
before_once |
Once per player, before first display |
before_always_once |
Before each display |
after_once |
Once per player, after first submission |
after_always_once |
After each submission |
before_next |
Just before advancing to the next page |
Example: one-time initialization¶
class Task(Page):
@classmethod
def before_once(page, player):
# Runs once when the player first sees this page
player.start_time = time()
Example: cleanup after submission¶
class Task(Page):
@classmethod
def after_always_once(page, player):
# Runs after each submission
player.attempts += 1
See Page methods reference for the complete list.
JavaScript variables¶
To pass data to JavaScript, use the jsvars method:
class TradingGame(Page):
@classmethod
def jsvars(page, player):
return dict(
initial_price=player.price,
max_trades=C.MAX_TRADES,
)
Access these in your template's JavaScript:
Complete example¶
Here's a complete page with context, conditional display, and form handling:
class Offer(Page):
allow_back = True
fields = dict(
amount=IntegerField(
label="How much do you offer?",
min=0,
max=100,
),
)
@classmethod
def show(page, player):
return player.role == "proposer"
@classmethod
def context(page, player):
return dict(
endowment=C.ENDOWMENT,
partner=other_in_group(player).name,
)
@classmethod
def before_next(page, player):
player.offer_made = True