HTML form: GET the form, POST it, redirect on success

The classic Post/Redirect/Get pattern, implemented with EDRI's HTML API:

  1. GET /contact — renders an empty form.

  2. POST /contact — validates input, stores it, then issues a RedirectResponseDirective to send the browser to a thank-you page.

  3. GET /contact/thanks — renders confirmation.

The point of the redirect is to prevent the browser from re-submitting on refresh.


Events

# events/contact.py
from http import HTTPMethod

from edri.api.dataclass.api_event import api, ApiType
from edri.dataclass.event import Event, event
from edri.dataclass.response import Response, response
from edri.utility.function import inject
from edri.utility.validation import StringValidator


@response
class ContactFormResponse(Response):
    error: str | None


@response
class ContactThanksResponse(Response):
    pass


@api(
    url="/contact",
    template="contact/form.j2",
    method=HTTPMethod.GET,
    exclude={ApiType.WS},
)
class ContactForm(Event):
    response: ContactFormResponse


@api(
    url="/contact",
    template="contact/form.j2",        # rendered only if validation fails
    method=HTTPMethod.POST,
    exclude={ApiType.WS},
)
class ContactSubmit(Event):
    name: inject(StringValidator, minimum_length=1, maximum_length=80)
    message: inject(StringValidator, minimum_length=1, maximum_length=2000)
    response: ContactFormResponse


@api(
    url="/contact/thanks",
    template="contact/thanks.j2",
    method=HTTPMethod.GET,
    exclude={ApiType.WS},
)
class ContactThanks(Event):
    response: ContactThanksResponse

Manager

# managers/contact.py
from edri.abstract import ManagerBase, request
from edri.dataclass.directive.html import RedirectResponseDirective
from edri.dataclass.response import ResponseStatus

from events.contact import ContactForm, ContactSubmit, ContactThanks


class ContactManager(ManagerBase):

    def __init__(self, router_queue):
        super().__init__(router_queue)
        self._messages: list[dict] = []

    @request
    def show_form(self, event: ContactForm) -> None:
        event.response.error = None

    @request
    def submit(self, event: ContactSubmit) -> None:
        # `StringValidator` already enforced length, so this is a stand-in
        # for *business* validation that wasn't worth a custom validator.
        if "@" in event.name:
            event.response.error = "Please enter a name, not an email."
            event.response.set_status(ResponseStatus.FAILED)
            return

        self._messages.append({"name": event.name, "message": event.message})

        # PRG: tell the browser to GET /contact/thanks.
        event.response.set_status(ResponseStatus.OK)
        event.response.add_directive(RedirectResponseDirective(ContactThanks))

    @request
    def thanks(self, event: ContactThanks) -> None:
        # Nothing to fill in — the template is static.
        event.response.set_status(ResponseStatus.OK)

A few things worth pointing out:

  • RedirectResponseDirective(ContactThanks) takes the event class, not a URL. The HTML layer expands it to the URL declared on ContactThanks.@api(url=...). If the target event had path placeholders, you'd pass them as keyword arguments (RedirectResponseDirective(UserPage, user_id=42)).

  • When validation succeeds, the directive overrides the default 200 rendering with a 302 redirect; the template is not used.

  • When validation fails, no directive is added — the template contact/form.j2 renders and can show data.error.


Templates

templates/contact/form.j2:

<!doctype html>
<html>
<body>
  <h1>Contact us</h1>
  {% if data.error %}
    <p style="color:red">{{ data.error }}</p>
  {% endif %}
  <form action="{% url 'ContactSubmit' %}" method="post">
    <label>Name: <input name="name" required></label><br>
    <label>Message: <textarea name="message" required></textarea></label><br>
    <button type="submit">Send</button>
  </form>
</body>
</html>

templates/contact/thanks.j2:

<!doctype html>
<html>
<body>
  <h1>Thanks!</h1>
  <p>Your message was received.</p>
  <a href="{% url 'ContactForm' %}">Send another</a>
</body>
</html>

The {% url 'EventName' %} tag turns the event class name into the URL declared on its @api(...) decorator — see URL generation in templates.


Entry point

# main.py
from edri import EDRI

from managers.contact import ContactManager

if __name__ == "__main__":
    edri = EDRI()
    edri.add_component(ContactManager(edri.router_queue))
    edri.start_api()
    edri.run()

Make sure EDRI_TEMPLATE_PATH points at the directory containing your contact/ folder (default: templates/). See Templates and assets.


Trying it

# render the form
curl -sL http://localhost:8080/contact

# submit
curl -sL -X POST http://localhost:8080/contact \
     -d "name=Alice&message=Hello"
# -> 302 Location: /contact/thanks  -> the thanks page

# submit with a validation failure
curl -sL -X POST http://localhost:8080/contact \
     -d "name=a@b.c&message=oops"
# -> 200 with the form, response.error rendered