Skip to content

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:

  1. show — Decide whether the page is displayed
  2. early — Run before rendering
  3. before_always_once — Run once when this page position is reached
  4. before_once — Run once per player, on first visit only
  5. fields — Determine form fields
  6. templatevars / jsvars — Prepare template and JS data
  7. (The page is rendered and displayed.)
  8. (The participant submits.)
  9. validate — Check submitted data
  10. before_form_save — Run before ordinary form fields are saved
  11. stealth_fields / handle_stealth_fields — Handle manual fields
  12. may_proceed — Gate before advancing
  13. after_once — Run once per player, on first successful submission only
  14. after_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.

@classmethod
def show(page, player):
    return player.role == "proposer"
Parameter Description
page The page class
player The current player
Returns boolTrue 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.

@classmethod
def before_once(page, player):
    player.start_time = time()
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

class Survey(Page):
    fields = dict(
        age=IntegerField(label="Age", min=18, max=100),
    )

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:

const price = uproot.vars.initial_price;
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 form
  • list[str] — Multiple errors displayed at the top
  • dict[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.

@classmethod
def before_form_save(page, player, data):
    player.submitted_at = time()
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 boolTrue 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.

@classmethod
def after_once(page, player):
    player.completed_task = True
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.

@classmethod
def after_always_once(page, player):
    player.attempts += 1
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

class TimedTask(Page):
    timeout = 60

Dynamic timeout

@classmethod
def timeout(page, player):
    return max(0, player.deadline - time())
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.

@classmethod
def timeout_reached(page, player):
    player.timed_out = True
    player.score = 0
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

class MyPage(Page):
    stealth_fields = ["code", "iban"]

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