Page methods¶
Page methods are @classmethods that receive page (the page class) and usually player (the current participant) as arguments. Standard page hooks can be sync or async; wait-page callbacks such as after_grouping and all_here are sync-only.
Lifecycle overview¶
For a forward visit to a page, the lifecycle is:
show— Decide whether the page is displayedearly— Run before renderingbefore_always_once— Run once when this page position is reachedbefore_once— Run once per player, on first visit onlyfields— Determine form fieldstemplatevars/jsvars— Prepare template and JS data- (The page is rendered and displayed.)
- (The participant submits.)
validate— Check submitted databefore_form_save— Run before ordinary form fields are savedstealth_fields/handle_stealth_fields— Handle manual fieldsmay_proceed— Gate before advancingafter_once— Run once per player, on first successful submission onlyafter_always_once— Run once after this page position is submitted
The timeout method configures the deadline while the page is rendered. The browser submits when the deadline is reached. If the server receives a request after the deadline, timeout_reached handles that request before normal form processing; validation and may_proceed are skipped, and the page advances. A direct visit to the current page can omit early, because that hook runs when entering a new page position.
show¶
Decides whether the page should be displayed. Return False to skip the page entirely.
| Parameter | Description |
|---|---|
page |
The page class |
player |
The current player |
| Returns | bool — True to show (default), False to skip |
early¶
The earliest hook in the page lifecycle. Runs before before_always_once. Receives the HTTP request as an additional keyword argument.
@classmethod
def early(page, player, request):
player.user_agent = request.headers.get("user-agent", "")
| Parameter | Description |
|---|---|
page |
The page class |
player |
The current player |
request |
The Starlette Request object |
before_once¶
Runs once per player, the first time they see this page. Does not run again if the player navigates back and returns.
| Parameter | Description |
|---|---|
page |
The page class |
player |
The current player |
before_always_once¶
Runs once when this page position is reached. Use this for setup that should also happen for internal or skipped pages.
@classmethod
def before_always_once(page, player):
player.visit_count = getattr(player, "visit_count", 0) + 1
| Parameter | Description |
|---|---|
page |
The page class |
player |
The current player |
fields¶
Returns the form fields for this page. Can be a dictionary (static) or a method (dynamic).
Static fields¶
Dynamic fields¶
@classmethod
def fields(page, player):
max_offer = player.other_in_group.endowment
return dict(
offer=IntegerField(label="Your offer", min=0, max=max_offer),
)
| Parameter | Description |
|---|---|
page |
The page class |
player |
The current player |
| Returns | dict mapping field names to field instances |
templatevars¶
Returns variables available in the page template.
@classmethod
def templatevars(page, player):
return dict(
partner=player.other_in_group,
total=sum(p.contribution for p in player.group.players),
)
| Parameter | Description |
|---|---|
page |
The page class |
player |
The current player |
| Returns | dict of template variables, or None |
templatevars may return None instead of a dict — this is treated the same as returning {}.
jsvars¶
Returns variables available in JavaScript as uproot.vars.
@classmethod
def jsvars(page, player):
return dict(
initial_price=player.price,
max_trades=C.MAX_TRADES,
)
Access in JavaScript:
| Parameter | Description |
|---|---|
page |
The page class |
player |
The current player |
| Returns | dict of JavaScript variables |
validate¶
Custom validation of submitted form data. Called after built-in field validation passes.
@classmethod
def validate(page, player, data):
if data["give_a"] + data["give_b"] > 100:
return "Total cannot exceed 100"
| Parameter | Description |
|---|---|
page |
The page class |
player |
The current player |
data |
dict of submitted values |
| Returns | Error message(s), or nothing if valid |
Return types:
str— Single error displayed at the top of the formlist[str]— Multiple errors displayed at the topdict[str, str | list[str]]— Per-field errors displayed next to each field
@classmethod
def validate(page, player, data):
errors = {}
if data["min_price"] > data["max_price"]:
errors["min_price"] = "Must be less than max price"
errors["max_price"] = "Must be greater than min price"
return errors or None
See the input_validation example
before_form_save¶
Runs after built-in and custom validation passes, immediately before ordinary
form fields are saved to the player. The submitted data mapping is read-only;
use this hook for checks or side effects that need the validated values before
they are persisted.
| Parameter | Description |
|---|---|
page |
The page class |
player |
The current player |
data |
Read-only mapping of validated form values |
may_proceed¶
Gate that controls whether the player can advance. Return False to keep the player on the page.
@classmethod
def may_proceed(page, player):
from time import time
return time() >= player.session.detection_period_until
| Parameter | Description |
|---|---|
page |
The page class |
player |
The current player |
| Returns | bool — True to allow proceeding (default), False to block |
after_once¶
Runs once per player, after the first forward submission. Does not run on back navigation or revisits.
| Parameter | Description |
|---|---|
page |
The page class |
player |
The current player |
after_always_once¶
Runs once after this page position is submitted in the forward direction.
| Parameter | Description |
|---|---|
page |
The page class |
player |
The current player |
Note
after_once and after_always_once are not available on GroupCreatingWait or SynchronizingWait pages.
timeout¶
Sets a page timeout in seconds. Can be a static value or a method.
Static timeout¶
Dynamic timeout¶
| Parameter | Description |
|---|---|
page |
The page class |
player |
The current player |
| Returns | float seconds until timeout, or None to disable |
timeout_reached¶
Called when the page timeout expires, before the page auto-advances.
| Parameter | Description |
|---|---|
page |
The page class |
player |
The current player |
stealth_fields¶
Specifies which fields should not be saved automatically. Can be a list or a method.
Static stealth fields¶
Dynamic stealth fields¶
@classmethod
def stealth_fields(page, player):
return ["response"] if player.condition == "special" else []
| Parameter | Description |
|---|---|
page |
The page class |
player |
The current player |
| Returns | list[str] of field names to handle manually |
handle_stealth_fields¶
Processes stealth fields. Called after validation. Return error strings to reject the submission.
@classmethod
async def handle_stealth_fields(page, player, data):
code = data["code"]
if code != "secret123":
return "Invalid access code"
player.verified = True
| Parameter | Description |
|---|---|
page |
The page class |
player |
The current player |
data |
dict of stealth field values |
| Returns | Error string or list of error strings, or nothing if valid |
See the payment_data example · input_validation example
Page class attributes¶
| Attribute | Default | Description |
|---|---|---|
allow_back |
False |
Show a "Back" button |
template |
{AppName}/{ClassName}.html |
Custom template path |
keep_values |
False |
Re-populate form from player data on re-render |
Wait page methods¶
GroupCreatingWait¶
| Attribute/Method | Description |
|---|---|
group_size |
Required. Number of players per group |
after_grouping(page, group) |
Called once when the group forms |
SynchronizingWait¶
| Attribute/Method | Description |
|---|---|
synchronize |
"group" (default) or "session" |
all_here(page, group) |
Called when all group members arrive |
all_here(page, session) |
Called when all session members arrive (if synchronize = "session") |
wait_for(page, player) |
Override to customize who to wait for |