skilly. Buy ad slot
All skills
Backend · Frontend · Security / AGENT SKILL

skrift-forms

Smarter-Dev/smarter-dev
0 installs 1 GitHub stars
0

Work with Skrift's Pydantic-backed forms, CSRF protection, template rendering, and hooks.
Skrift form system — Pydantic-backed forms with CSRF protection, template rendering, and hook integration.

BEFORE YOU INSTALL

Understand the trade-offs.

SECURITY REVIEW

Not yet assessed

Review the original instructions and requested permissions before installing.

No security review is available for this catalog entry yet.

SKILL QUALITY

Not yet assessed

How clearly the skill guides your agent, how complete its workflow is, and how you can check the outcome.

No quality assessment is available for this catalog entry yet.

The full skill.

Original instructions from the publisher’s SKILL.md

# Skrift Form System

Skrift forms combine Pydantic validation with CSRF protection, template rendering, and hook integration.

## Architecture

```
FormModel/BaseModel → Form(model, request) → validate() → hooks → data
      │                      │                    │           │
      │                      │                    │           ├─ form_{name}_validated
      │                      │                    │           └─ form_validated
      │                      │                    │
      │                      │                    ├─ CSRF verify (hmac.compare_digest)
      │                      │                    ├─ Token rotation (single-use)
      │                      │                    ├─ Checkbox injection (bool fields)
      │                      │                    └─ Pydantic validation
      │                      │
      │                      ├─ render() → Template("form", name).try_render()
      │                      ├─ csrf_field() → hidden input
      │                      ├─ fields → dict[str, BoundField]
      │                      └─ errors → dict[str, str]
      │
      └─ Registered in _form_registry (by form_name)
```

## Defining Forms

### FormModel (auto-registers)

```python
from skrift.forms import FormModel
from pydantic import Field, EmailStr

class ContactForm(FormModel, form_name="contact"):
    name: str
    email: EmailStr
    message: str = Field(json_schema_extra={"widget": "textarea"})
```

### @form() Decorator (registers a plain BaseModel)

```python
from pydantic import BaseModel
from skrift.forms import form

@form("newsletter", action="/subscribe", method="post")
class NewsletterForm(BaseModel):
    email: str
```

### Field Customization via `json_schema_extra`

| Key | Values | Purpose |
|-----|--------|---------|
| `label` | `str` | Custom label text |
| `widget` | `"textarea"`, `"select"`, `"checkbox"` | Widget type |
| `input_type` | `"email"`, `"password"`, etc. | HTML input type |
| `help_text` | `str` | Help text below field |
| `choices` | `list[tuple[str, str]]` | Options for select widget |
| `attrs` | `dict[str, str]` | Extra HTML attributes |

Full example:

```python
class ContactForm(FormModel, form_name="contact"):
    name: str = Field(json_schema_extra={
        "label": "Your Name",
        "attrs": {"placeholder": "Jane Doe"},
    })
    email: EmailStr = Field(json_schema_extra={
        "label": "Email Address",
        "input_type": "email",
        "help_text": "We'll never share your email.",
    })
    subject: str = Field(json_schema_extra={
        "widget": "select",
        "choices": [
            ("general", "General Inquiry"),
            ("support", "Technical Support"),
        ],
    })
    message: str = Field(json_schema_extra={
        "widget": "textarea",
        "attrs": {"rows": "6"},
    })
    subscribe: bool = Field(default=False, json_schema_extra={
        "label": "Subscribe to newsletter",
    })
```

## Controller GET/POST Pattern

```python
from litestar import Controller, get, post, Request
from litestar.response import Template as TemplateResponse, Redirect
from skrift.forms import Form

class ContactController(Controller):
    path = "/contact"

    @get("/")
    async def show(self, request: Request) -> TemplateResponse:
        form = Form(ContactForm, request)
        return TemplateResponse("contact.html", context={"form": form})

    @post("/")
    async def submit(self, request: Request) -> TemplateResponse | Redirect:
        form = Form(ContactForm, request)
        if await form.validate():
            # form.data is a validated ContactForm instance
            await process_contact(form.data)
            return Redirect("/contact?thanks=1")
        return TemplateResponse("contact.html", context={"form": form})
```

## Template Rendering

### Automatic

```html
{{ form.render() }}
{{ form.render(submit_label="Send") }}
```

### Manual

```html
<form method="{{ form.method }}">
    {{ form.csrf_field() }}
    {% for field in form %}
        {{ field.label_tag() }}
        {{ field.widget() }}
    {% endfor %}
    <button type="submit">Send</button>
</form>
```

### Custom Form Template

Template hierarchy: `form-{name}.html` → `form.html` → programmatic fallback.

```html
{# templates/form-contact.html #}
<form method="{{ form.method }}" class="contact-form">
    {{ form.csrf_field() }}

    {% if form.form_error %}
        <div class="alert">{{ form.form_error }}</div>
    {% endif %}

    <div class="row">
        <div class="col">
            {{ form['name'].label_tag() }}
            {{ form['name'].widget(class_="form-input") }}
        </div>
        <div class="col">
            {{ form['email'].label_tag() }}
            {{ form['email'].widget(class_="form-input") }}
        </div>
    </div>

    {{ form['message'].label_tag() }}
    {{ form['message'].widget(class_="form-input", rows="8") }}

    <button type="submit">{{ submit_label }}</button>
</form>
```

## CSRF Flow

1. `Form.__init__()` — Creates `secrets.token_urlsafe(32)` in session if absent (key: `_csrf_token`)
2. `csrf_field()` — Renders `<input type="hidden" name="_csrf" value="{token}">`
3. `validate()` — Checks submitted `_csrf` against session token via `hmac.compare_digest()`
4. Token rotation — New token generated after successful check (single-use)

## Registration Mechanism

Forms are registered in a global `_form_registry` dict (in `model.py`):
- `FormModel.__init_subclass__()` auto-registers on class creation
- `@form()` decorator registers explicitly
- `get_form_model(name)` retrieves by name, raises `LookupError` if missing

## Hook Integration

Two filter hooks fire after successful validation:

```python
# Form-specific: e.g. "form_contact_validated"
self.data = await hooks.apply_filters(f"form_{self.name}_validated", self.data)

# Global: "form_validated"
self.data = await hooks.apply_filters("form_validated", self.data, self.name)
```

Example hook:

```python
from skrift.hooks import filter

@filter("form_contact_validated")
async def sanitize_contact(data):
    data.message = data.message.strip()
    return data

@filter("form_validated")
async def log_all_forms(data, name):
    print(f"Form '{name}' submitted")
    return data
```

## Jinja2 Global

The `Form` class is registered as a Jinja2 global, making it accessible in templates without passing it through context.

## Key Files

| File | Purpose |
|------|---------|
| `skrift/forms/core.py` | `Form` — CSRF, validation, rendering |
| `skrift/forms/model.py` | `FormModel` base class, `_form_registry`, `get_form_model()` |
| `skrift/forms/fields.py` | `BoundField` — field bound to form instance |
| `skrift/forms/decorators.py` | `@form()` decorator |
| `skrift/templates/form.html` | Default form template |