# Formkit-Ninja
A Django-Ninja framework for FormKit schemas and form submissions
## Why
FormKit out of the box has awesome schema support - this lets us integrate FormKit instances as Django models
- Upload / edit / download basic FormKit schemas
- Translated "option" values from the Django admin
- Reorder "options" and schema nodes
- List and Fetch schemas for different form types
## Use
To use, `pip install formkit-ninja` and add the following to settings `INSTALLED_APPS`:
```py
INSTALLED_APPS = [
...
"formkit_ninja",
"ninja",
...
]
```
## Quick Start for New Users
⭐ **NEW** - Create complete data collection apps with minimal coding!
```bash
# 1. Create a FormKit schema
./manage.py create_schema --label "Contact Form"
# 2. Bootstrap a complete Django app
./manage.py bootstrap_app --schema-label "Contact Form" --app-name contacts
# 3. Add to INSTALLED_APPS and migrate
# (Edit settings.py to add 'contacts')
./manage.py makemigrations && ./manage.py migrate
# 4. Start collecting data!
./manage.py runserver
```
See the [Quick Start Guide](https://github.com/catalpainternational/formkit-ninja/blob/HEAD/docs/quick_start.md) for a complete walkthrough.
## Code Generation
⭐ **NEW in v0.8.1** - Database-driven code generation! Configure type mappings and field overrides through Django admin without writing Python code.
formkit-ninja can automatically generate Django models, Pydantic schemas, admin classes, and API endpoints from your FormKit schemas.
### Database-Driven Configuration
Configure code generation rules through the Django admin:
```python
# Django Admin → Code generation configs
formkit_type = "text"
node_name = "district"
django_type = "ForeignKey"
django_args = {"to": "pnds_data.zDistrict", "on_delete": "models.CASCADE"}
```
Generates:
```python
# models.py
district = models.ForeignKey("pnds_data.zDistrict", on_delete=models.CASCADE)
```
### Quick Start
Generate code from all schemas in your database:
```bash
./manage.py generate_code --app-name myapp --output-dir ./myapp/generated
```
Generate code for a specific schema:
```bash
./manage.py generate_code --app-name myapp --output-dir ./myapp/generated --schema-label "My Form"
```
### Generated Files
The code generator creates the following files:
- `models.py` - Django models for groups and repeaters
- `schemas.py` - Django Ninja output schemas
- `schemas_in.py` - Django Ninja input schemas (Pydantic BaseModel)
- `admin.py` - Django admin classes
- `api.py` - Django Ninja API endpoints
### Extensibility
formkit-ninja provides multiple extension points for customizing code generation:
- **Database-Driven Config**: Configure through Django admin (no code needed!) ⭐ NEW
- **Custom Type Converters**: Add support for custom FormKit node types
- **Custom NodePath**: Extend NodePath with project-specific logic
- **Plugin System**: Bundle multiple extensions together
- **Custom Templates**: Override Jinja2 templates for generated code
See the [Database-Driven Code Generation](https://github.com/catalpainternational/formkit-ninja/blob/HEAD/docs/database_code_generation.md) guide for the new database configuration feature, or the [Code Generation Guide](https://github.com/catalpainternational/formkit-ninja/blob/HEAD/docs/code_generation.md) for detailed documentation and examples.
## API
Formkit-Ninja provides a REST API for managing FormKit schema nodes. The API requires authentication and specific permissions.
### Authentication
All API endpoints require:
- **Authentication**: User must be logged in (session-based authentication)
- **Permission**: User must have the `formkit_ninja.change_formkitschemanode` permission
Unauthenticated requests receive `401 Unauthorized`. Authenticated users without the required permission receive `403 Forbidden`.
### Endpoints
#### Create or Update Node
**POST** `/api/formkit/create_or_update_node`
Creates a new node or updates an existing one.
**Request Body:**
- `uuid` (optional): UUID of node to update. If omitted, a new node is created.
- `parent_id` (optional): UUID of parent node (must be a group or repeater)
- `$formkit`: FormKit node type (e.g., "text", "group", "repeater")
- Other FormKit node properties (label, name, etc.)
**Response:**
- `200 OK`: Success, returns `NodeReturnType` with node data
- `400 Bad Request`: Invalid input (e.g., invalid parent, deleted node)
- `403 Forbidden`: Insufficient permissions
- `404 Not Found`: Node with provided UUID does not exist (for updates)
- `500 Internal Server Error`: Server error
**Update Behavior:**
- When `uuid` is provided, the node with that UUID is updated
- If the node doesn't exist, returns `404 Not Found`
- If the node is inactive (deleted), returns `400 Bad Request`
- Parent-child relationships are automatically created/updated when `parent_id` is provided
#### Delete Node
**DELETE** `/api/formkit/delete/{node_id}`
Soft deletes a node (sets `is_active=False`).
**Response:**
- `200 OK`: Success, returns `NodeInactiveType`
- `403 Forbidden`: Insufficient permissions
- `404 Not Found`: Node does not exist
### Response Formats
All successful responses return consistent data structures:
- **NodeReturnType**: For active nodes
- `key`: UUID of the node
- `node`: FormKit node data
- `last_updated`: Timestamp of last change
- `protected`: Whether the node is protected from deletion
- **NodeInactiveType**: For deleted nodes
- `key`: UUID of the node
- `is_active`: `false`
- `last_updated`: Timestamp of last change
- `protected`: Whether the node is protected
- **FormKitErrors**: For error responses
- `errors`: List of error messages
- `field_errors`: Dictionary of field-specific errors
### Validation
The API validates:
- **Parent existence**: If `parent_id` is provided, the parent node must exist and be a group or repeater
- **Node existence**: If `uuid` is provided for updates, the node must exist and be active
- **FormKit type**: The `$formkit` field must be a valid FormKit node type
## Test
Pull the repo:
```bash
gh repo clone catalpainternational/formkit-ninja
cd formkit-ninja
uv sync
```
### Database Setup
Tests and submission queryset annotations (e.g. `with_unresolved_flags()`, `with_import_failure()`) require PostgreSQL (pgtrigger, and `JSONBAgg`/`JSONObject` for annotations). Start a PostgreSQL container before running tests:
```bash
# Using Podman (recommended)
podman run --replace -d --name formkit-postgres -p 5434:5432 -e POSTGRES_HOST_AUTH_METHOD=trust docker.io/library/postgres:14-alpine
# OR using Docker
docker run -d --name formkit-postgres -p 5434:5432 -e POSTGRES_HOST_AUTH_METHOD=trust postgres:14-alpine
```
Then run tests:
```bash
uv run pytest
```
### Playwright
Some tests require playwright. Install it with:
```bash
uv run playwright install
```
**Note:** For full development setup with real data, see [DEVELOPMENT.md](https://github.com/catalpainternational/formkit-ninja/blob/HEAD/DEVELOPMENT.md).
## Lint
Format and lint code using `ruff`:
```bash
# Check formatting
uv run ruff format --check .
# Check linting
uv run ruff check .
```
## For Contributors
### Prerequisites
- Python 3.10-3.14
- `uv` for package management
- Podman or Docker for PostgreSQL database
- Playwright (for browser-based tests)
### Development Workflow
1. **Set up the project:**
```bash
uv sync
uv run playwright install
# Start PostgreSQL (see Database Setup above)
```
2. **Run tests:**
```bash
uv run pytest
```
3. **Check code quality:**
```bash
uv run ruff format --check .
uv run ruff check .
uv run mypy formkit_ninja tests/test_wire.py
```
4. **Test Driven Development (TDD):**
- Write tests *before* implementing features
- Ensure new code is covered by tests
- Use `pytest` as the testing framework
5. **Code Style:**
- Use `ruff` for formatting and linting
- Follow Python type hints for all function arguments and return values
- Adhere to SOLID principles
6. **Commit Messages:**
- Use [Conventional Commits](https://www.conventionalcommits.org/) specification
- Format: `<type>(<scope>): <subject>`
# Updating 'Protected' Nodes
If a node's been protected you cannot change or delete it. To do so, you'll need to temporarily disable the trigger which is on it.
`./manage.py pytrigger disable protect_node_deletes_and_updates`
Make changes
`./manage.py pgtrigger enable protect_node_deletes_and_updates`
See the documentation for more details: https://django-pgtrigger.readthedocs.io/en/2.3.0/commands.html?highlight=disable