A Django-Ninja framework for FormKit schemas and form submissions
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
To use, pip install formkit-ninja and add the following to settings INSTALLED_APPS:
INSTALLED_APPS = [
...
"formkit_ninja",
"ninja",
...
]⭐ NEW - Create complete data collection apps with minimal coding!
# 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 runserverSee the Quick Start Guide for a complete walkthrough.
⭐ 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.
Configure code generation rules through the Django admin:
# 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:
# models.py
district = models.ForeignKey("pnds_data.zDistrict", on_delete=models.CASCADE)Generate code from all schemas in your database:
./manage.py generate_code --app-name myapp --output-dir ./myapp/generatedGenerate code for a specific schema:
./manage.py generate_code --app-name myapp --output-dir ./myapp/generated --schema-label "My Form"The code generator creates the following files:
models.py- Django models for groups and repeatersschemas.py- Django Ninja output schemasschemas_in.py- Django Ninja input schemas (Pydantic BaseModel)admin.py- Django admin classesapi.py- Django Ninja API endpoints
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 guide for the new database configuration feature, or the Code Generation Guide for detailed documentation and examples.
Formkit-Ninja provides a REST API for managing FormKit schema nodes. The API requires authentication and specific permissions.
All API endpoints require:
- Authentication: User must be logged in (session-based authentication)
- Permission: User must have the
formkit_ninja.change_formkitschemanodepermission
Unauthenticated requests receive 401 Unauthorized. Authenticated users without the required permission receive 403 Forbidden.
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, returnsNodeReturnTypewith node data400 Bad Request: Invalid input (e.g., invalid parent, deleted node)403 Forbidden: Insufficient permissions404 Not Found: Node with provided UUID does not exist (for updates)500 Internal Server Error: Server error
Update Behavior:
- When
uuidis 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_idis provided
DELETE /api/formkit/delete/{node_id}
Soft deletes a node (sets is_active=False).
Response:
200 OK: Success, returnsNodeInactiveType403 Forbidden: Insufficient permissions404 Not Found: Node does not exist
All successful responses return consistent data structures:
-
NodeReturnType: For active nodes
key: UUID of the nodenode: FormKit node datalast_updated: Timestamp of last changeprotected: Whether the node is protected from deletion
-
NodeInactiveType: For deleted nodes
key: UUID of the nodeis_active:falselast_updated: Timestamp of last changeprotected: Whether the node is protected
-
FormKitErrors: For error responses
errors: List of error messagesfield_errors: Dictionary of field-specific errors
The API validates:
- Parent existence: If
parent_idis provided, the parent node must exist and be a group or repeater - Node existence: If
uuidis provided for updates, the node must exist and be active - FormKit type: The
$formkitfield must be a valid FormKit node type
Pull the repo:
gh repo clone catalpainternational/formkit-ninja
cd formkit-ninja
uv syncTests require PostgreSQL due to the pgtrigger dependency. Start a PostgreSQL container before running tests:
# Using Podman (recommended)
podman run -d --name formkit-postgres -p 5433:5432 -e POSTGRES_HOST_AUTH_METHOD=trust docker.io/library/postgres:14-alpine
# OR using Docker
docker run -d --name formkit-postgres -p 5433:5432 -e POSTGRES_HOST_AUTH_METHOD=trust postgres:14-alpineThen run tests:
uv run pytestSome tests require playwright. Install it with:
uv run playwright installNote: For full development setup with real data, see DEVELOPMENT.md.
Format and lint code using ruff:
# Check formatting
uv run ruff format --check .
# Check linting
uv run ruff check .- Python 3.10-3.14
uvfor package management- Podman or Docker for PostgreSQL database
- Playwright (for browser-based tests)
-
Set up the project:
uv sync uv run playwright install # Start PostgreSQL (see Database Setup above) -
Run tests:
uv run pytest
-
Check code quality:
uv run ruff format --check . uv run ruff check . uv run mypy formkit_ninja
-
Test Driven Development (TDD):
- Write tests before implementing features
- Ensure new code is covered by tests
- Use
pytestas the testing framework
-
Code Style:
- Use
rufffor formatting and linting - Follow Python type hints for all function arguments and return values
- Adhere to SOLID principles
- Use
-
Commit Messages:
- Use Conventional Commits specification
- Format:
<type>(<scope>): <subject>
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