Metadata-Version: 2.4
Name: uofastforms
Version: 0.1.0
Summary: WTForms form generator and renderer for UOFastORM-backed UniData/U2 models
Author-email: RokiPark <info@rokipark.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/RokiPark/UOFastForms
Project-URL: Repository, https://github.com/RokiPark/UOFastForms
Project-URL: Bug Tracker, https://github.com/RokiPark/UOFastForms/issues
Keywords: wtforms,unidata,u2,uofastorm,flask,forms,rokipark
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
Classifier: Topic :: Database
Classifier: Framework :: Flask
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: WTForms>=3.0
Provides-Extra: flask
Requires-Dist: Flask>=2.0; extra == "flask"
Provides-Extra: uopy
Requires-Dist: uopy; extra == "uopy"
Provides-Extra: all
Requires-Dist: Flask>=2.0; extra == "all"
Requires-Dist: uopy; extra == "all"
Dynamic: license-file

# UOFastForms

**WTForms form generator and renderer for [UOFastORM](https://github.com/RokiPark/UOFastORM)-backed UniData/U2 models.**

> A [RokiPark](https://github.com/RokiPark) open-source project.

UOFastForms automatically builds WTForms `Form` classes from your `UopyModel` field metadata, handles U2 multi-value (MV) data, integrates lookup popups, and supports server-side validation via cataloged U2 BASIC subroutines — all with Bootstrap 5-ready Jinja2 macros.

---

## Features

- **Auto-generated forms** from `UopyModel._field_map` and `_mv_fields` metadata
- **FieldConfig** enrichment — override types, labels, validators, choices, and render attributes per field
- **Multi-value field support** — `MVTextAreaField` (newline-separated) and `MVFieldList` (dynamic repeater)
- **Lookup popups** — declarative `LookupModel` generates `data-lookup-*` HTML attributes for JS integration
- **Server-side validation** — `RemoteValidator` calls cataloged U2 BASIC subroutines and maps errors back to form fields
- **Flask Blueprint** — `register_forms(app)` wires up Jinja2 template globals in one line
- **Bootstrap 5 macros** — `render_form`, `render_field`, `render_mv_fieldlist`, `render_form_errors`

---

## Installation

```bash
pip install uofastforms
```

With Flask rendering support:

```bash
pip install uofastforms[flask]
```

With U2 server-side validation:

```bash
pip install uofastforms[all]
```

---

## Quick Start

### 1. Define a FormModel

```python
from uofastforms import FormModel, FieldConfig, RemoteValidator, register_forms
from wtforms.validators import DataRequired, Length

class ClientFormModel(FormModel):
    model = ClientModel          # your UopyModel subclass

    remote_validator = RemoteValidator(
        subroutine_name="VALIDATE.CLIENT",
        arg_count=3,
        input_arg=0,
        output_arg=1,
        fatal_arg=2,
        session_factory=lambda: uopy.connect(**cfg),
    )

    fields = {
        "company":     FieldConfig(label="Company", required=True,
                                   validators=[Length(max=100)]),
        "fname":       FieldConfig(label="First Name"),
        "lname":       FieldConfig(label="Last Name"),
        "phone":       FieldConfig(label="Phones", description="One per line"),
        "contact_ids": FieldConfig(exclude=True),
    }
```

### 2. Register with Flask (once, in app factory)

```python
from uofastforms import register_forms

def create_app():
    app = Flask(__name__)
    register_forms(app)
    return app
```

### 3. Use in a view

```python
@bp.route('/clients/<id>/edit', methods=['GET', 'POST'])
def edit_client(id):
    client = ClientModel(session, id)
    form = ClientFormModel.get_form(
        obj=client          if request.method == 'GET'  else None,
        data=request.form   if request.method == 'POST' else None,
    )
    if request.method == 'POST' and form.validate():
        ClientFormModel.populate_obj(form, client)
        client.update()
        return redirect(url_for('.list'))
    return render_template('client_edit.html', form=form)
```

### 4. Render in a Jinja2 template

```jinja2
{% from 'uofast_forms/form_macros.html' import render_form %}

{{ render_form(form, action=url_for('.edit_client', id=client.record_id)) }}
```

---

## API Reference

### `FormModel`

Base class for all form definitions. Subclass it and declare `model`, `fields`, and optionally `remote_validator`.

| Class attribute     | Type                     | Description |
|---------------------|--------------------------|-------------|
| `model`             | `type[UopyModel]`        | Required. The UopyModel subclass this form is based on. |
| `fields`            | `dict[str, FieldConfig]` | Per-field enrichment overrides. |
| `remote_validator`  | `RemoteValidator`        | Optional. Runs after local validation. |

**Class methods:**

```python
FormModel.get_form_class(base_class=Form) -> type[Form]
```
Returns (and caches) the generated WTForms `Form` subclass.

```python
FormModel.get_form(obj=None, data=None, base_class=Form, **kwargs) -> Form
```
Creates a form instance, optionally pre-populated from a `UopyModel` instance (`obj`) or raw POST data (`data`).

```python
FormModel.populate_obj(form, obj) -> None
```
Writes validated form data back to a `UopyModel` instance.

```python
FormModel.get_lookup_attrs(prop_name) -> dict[str, str]
```
Returns `data-lookup-*` HTML attributes for a field that has a `LookupModel` or `linked_file` configured.

---

### `FieldConfig`

Per-field enrichment dataclass.

```python
@dataclass
class FieldConfig:
    field_type: str | None = None       # "string", "integer", "float", "boolean",
                                        # "date", "datetime", "select", "textarea",
                                        # "email", "url", "password", "hidden"
    label: str | None = None            # Human-readable label (auto-prettified if None)
    description: str | None = None      # Help text shown below the field
    required: bool = False              # Prepends DataRequired validator
    validators: list = field(default_factory=list)
    choices: list | None = None         # [(value, label), ...] for SelectField
    render_kw: dict = field(default_factory=dict)
    exclude: bool = False               # Omit this field from the form entirely
    mv_style: str = "textarea"          # "textarea" or "fieldlist" for MV fields
    default: Any = None
    linked_file: str | None = None      # U2 file name for lookup popup
    linked_display_field: str | None = None
    linked_fill: dict | None = None     # {db_field: form_prop} auto-fill map
    lookup: LookupModel | None = None   # Full LookupModel (overrides linked_* attrs)
```

---

### `LookupModel`

Declarative configuration for field search-and-select popups.

```python
class ProductLookup(LookupModel):
    model = ProductModel
    display_fields = ["code", "description", "price"]
    search_field = "description"
    fill = {"code": "product_code", "description": "product_desc"}
```

Use with `FieldConfig(lookup=ProductLookup)`. The `get_lookup_attrs()` method returns the full `data-lookup-*` attribute dict for HTML rendering.

---

### `RemoteValidator`

WTForms validator that calls a cataloged U2 BASIC subroutine for server-side business-rule validation.

```python
RemoteValidator(
    subroutine_name="VALIDATE.CLIENT",
    arg_count=3,
    input_arg=0,          # index of arg receiving JSON form data
    output_arg=1,         # index of arg returning JSON error dict
    fatal_arg=2,          # index of arg returning a fatal error string
    session_factory=...,  # callable returning a live uopy.Session
    field_name_map=None,  # optional {subroutine_name: form_prop_name} mapping
)
```

**Subroutine contract:**

| Arg index | Direction | Content |
|-----------|-----------|---------|
| `input_arg`  | IN  | JSON string of form data, e.g. `{"company": "Acme", "phone": ["555-1234"]}` |
| `output_arg` | OUT | JSON string of field errors, e.g. `{"company": "Name already taken"}` or `""` |
| `fatal_arg`  | OUT | Fatal (non-field) error string; empty string = success |

---

### `MVTextAreaField` / `MVFieldList`

WTForms field types for U2 multi-value data.

- **`MVTextAreaField`** — Renders as a `<textarea>` with one value per line. Use `to_list()` / `populate_from_list(values)` for programmatic access.
- **`MVFieldList`** — Renders as a dynamic repeater (requires companion JS). Use `make_mv_fieldlist(field_type, label, ...)` factory to create instances.

---

### Flask Integration

```python
from uofastforms import register_forms, forms_bp

# Option A — app factory helper (recommended)
register_forms(app)

# Option B — manual Blueprint registration
app.register_blueprint(forms_bp)
```

`register_forms` also adds `uofast_form_errors(form)` as a Jinja2 global — a helper that returns a flat list of `(field_label, error_message)` tuples.

---

### Jinja2 Macros

Import from `uofast_forms/form_macros.html`:

```jinja2
{% from 'uofast_forms/form_macros.html' import render_form, render_field,
                                                render_mv_fieldlist, render_form_errors %}
```

| Macro | Description |
|-------|-------------|
| `render_form(form, action, method, submit_label, css_class, enctype)` | Full `<form>` with all fields and submit button |
| `render_field(field, label_class, input_class, wrapper_class)` | Single Bootstrap 5 form-group |
| `render_mv_fieldlist(field, add_label, input_class, wrapper_class)` | Dynamic MV repeater |
| `render_form_errors(form)` | Dismissable Bootstrap 5 alert listing all errors |

All macros use **Bootstrap 5** CSS classes. Browser-native validation is disabled (`novalidate`) — WTForms and `RemoteValidator` handle all validation.

---

## Requirements

| Dependency | Required | Notes |
|------------|----------|-------|
| `WTForms >= 3.0` | Yes | Core form framework |
| `Flask >= 2.0` | Optional | Required for `register_forms`, `forms_bp`, and macros |
| `uopy` | Optional | Required for `RemoteValidator` |
| `UOFastORM` | Runtime | `UopyModel` subclasses must come from your UOFastORM models |

---

## License

MIT — see [LICENSE](LICENSE).

---

Maintained by [RokiPark](https://github.com/RokiPark) · [github.com/RokiPark/UOFastForms](https://github.com/RokiPark/UOFastForms)
