Metadata-Version: 2.4
Name: conjunto-formbuilder
Version: 0.1.0
Summary: Visual form builder for conjunto: assemble forms and store their definition as JSON.
Author-email: Christian González <christian.gonzalez@nerdocs.at>
License-Expression: AGPL-3.0-or-later
Classifier: Framework :: Django
Requires-Python: >=3.13
Description-Content-Type: text/markdown
Requires-Dist: django>=6.0
Requires-Dist: conjunto>=0.7.7
Requires-Dist: gdaps>=0.17

# conjunto-formbuilder

A visual **form builder** for [conjunto](https://gitlab.com/nerdocs/conjunto)-based Django projects. Forms are
assembled in a UI and stored as **JSON** — the JSON definition is the contract: `build_form(schema)` turns it into a
live Django form for rendering, validation and cleaned data, without hand-written field code.

The package depends only on **Django** and **conjunto**, so it runs in any conjunto project.

## Install

```bash
pip install conjunto-formbuilder
```

```python
# settings.py
INSTALLED_APPS = [
    # …
    "conjunto_formbuilder",
]
```

## Concept

```
[ Builder UI ]  ──►  schema (JSON)  ──►  build_form(schema)  ──►  Django Form
```

A field descriptor carries more than `type`/`label`/`required`: grid layout coordinates (`x`/`y`/`w`/`h`) and optional
`source` (queryset autocomplete) / `provision` (prefill from a registered data source) descriptors.

```python
from conjunto_formbuilder.build import build_form

schema = {
    "version": 1,
    "layout": "grid",
    "columns": 12,
    "fields": [
        {"name": "first_name", "type": "text", "label": "First name", "required": True,
         "grid": {"x": 0, "y": 0, "w": 6, "h": 1}},
        {"name": "birth_date", "type": "date", "label": "Date of birth",
         "grid": {"x": 6, "y": 0, "w": 6, "h": 1}},
    ],
}

form = build_form(schema, data=request.POST or None)
if form.is_valid():
    ...  # form.cleaned_data
```

## Extending

- **Field types** — register a `FieldType` with `conjunto_formbuilder.registry.register_field_type`.

### Data sources

A field's `source` (autocomplete) and `provision` (prefill) descriptors never name a model or queryset directly — that
would let a stored form definition reach into arbitrary tables. Instead they name a **provider** by its `key`. A
provider is a `conjunto_formbuilder.datasources.IDataSource` (a GDAPS interface) you implement; it returns data through
its own, typically tenant-aware, queryset and is responsible for validating the `params` it accepts.

```python
from conjunto_formbuilder.datasources import IDataSource
from myapp.models import Country


class CountrySource(IDataSource):
    """Generic model-queryset provider: autocomplete + prefill for countries."""

    key = "myapp.country"

    def get_queryset(self, request, params):
        # Scope here (tenant, permissions, params) — this is the trust boundary.
        return Country.objects.all()

    def search(self, request, params, query):
        # Candidates for the typed query; the options partial renders each as
        # ``{{ obj.pk }}`` / ``{{ obj }}``.
        return self.get_queryset(request, params).filter(name__icontains=query)[:20]

    def resolve(self, request, params):
        # A single value to prefill the field with, or ``None``.
        return self.get_queryset(request, params).values_list("pk", flat=True).first()
```

Reference it from a field:

```python
{"name": "country", "type": "autocomplete",
 "source":    {"provider": "myapp.country", "params": {}},
 "provision": {"provider": "myapp.country", "params": {}}}
```

`build_form` wires the autocomplete widget's URL to the `AutocompleteOptionsView`, carrying the provider key (and any
`params`) as query arguments; the widget sends the typed text as `?q=…`. The view resolves the provider against the
registry only — an unknown key yields `404`. When the builder URLs are mounted under a different namespace, override the
reversed URL name via the `autocomplete_url_name=` argument of `build_form_class` or the
`CONJUNTO_FORMBUILDER_AUTOCOMPLETE_URL_NAME` setting.

## License

AGPL-3.0-or-later.
