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. early: Run first, whenever navigation reaches this page position.
  2. before_always_once: Run once when this page position is reached.
  3. show: Decide whether the page is displayed.
  4. before_once: Run once, before the page is displayed for the first time.
  5. fields: Determine form fields.
  6. templatevars and jsvars: Prepare template and JS data.
  7. (The page is rendered and displayed.)
  8. (The participant submits.)
  9. validate: Check submitted data.
  10. stealth_fields and handle_stealth_fields: Handle manual fields.
  11. before_form_save: Run before ordinary form fields are saved.
  12. may_proceed: Gate before advancing.
  13. after_once: Run once, after the first submission that leaves the page.
  14. after_always_once: Run once when this page position is left forward.

A few rules explain all of this:

  • Entering. Entry runs early, then before_always_once. For forward navigation and loading, show then decides. A page that is not shown is skipped: after_always_once runs, and uproot enters the next position. A shown page is displayed: before_once runs, then the participant sees the page. Back first uses show to select an earlier page, then enters and displays it without checking show again. Staying after a submission also displays the current page without rechecking show.
  • Once. before_always_once, before_once, after_once and after_always_once run the first time their occasion arises at a page position, always, but only once. early runs whenever navigation reaches a position; reloading the page runs it only if it has never run there.
  • Leaving. A submission that passes all checks leaves the page if may_proceed allows it: after_once, after_always_once, then uproot enters the next position. Otherwise, the page is displayed again with the submitted values, and with error messages if the submission was rejected. Error messages never appear on another page.
  • Moves. If a page method moves the participant (with move_to_page or move_to_end), the method and the others of the same step (entering, show, skipping, displaying, leaving) finish, and then uproot enters the target. End cleanup waits for that step too. A move to the end outside a page request runs cleanup immediately, even if the participant never returns.
  • Other forms. A request that does not carry the form of the participant’s current page follows the loading path, and its form fields are not automatically saved. Hooks retain the actual HTTP request: early can still distinguish a POST from a GET. This happens, for example, when a participant submits a page just after an admin advanced them, or clicks Next twice and the first click already took them to the next page.

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, may_proceed is skipped, and the page advances, even if the participant clicked Back. Valid form data is saved. Data that is invalid, or rejected by validate or handle_stealth_fields, is discarded without an error message. uproot counts a request up to upd.TIMEOUT_TOLERANCE seconds (default: 1) before the deadline as timed out, too, but only if may_proceed allows the page to advance. See Timeouts and may_proceed.

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 bool: True to show (default), False to skip

early

The earliest hook in the page lifecycle. Runs whenever navigation reaches this page position, also if the page is not shown, before before_always_once. Reloading the page does not run it again. 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, also if they reach it with Back. 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, also if the page is not shown. 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 are:

  • 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 and after handle_stealth_fields (if any) accepts the submission, 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. It does not stop Back.

@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

Note

A page timeout overrides may_proceed once the deadline has passed. See Timeouts and may_proceed.

after_once

Runs once per player, after the first submission that leaves the page (or after its timeout). 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 when the player leaves this page position forward: after a submission (or a timeout), or when the page is skipped because it is not shown.

@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. It runs before the submitted form data is processed, so valid submitted values overwrite values that timeout_reached sets for the same fields. See Handling timeout expiration.

@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 and file uploads. Called after validation passes and before ordinary fields are saved. Return error strings to reject the submission: the participant stays on the page, and nothing from the submission is saved. Changes the handler makes to player itself are kept. See Stealth fields.

@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 all submitted field values, including stealth fields
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 Matching app .html file, then .md Custom template path relative to the project root
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