This guide covers all configuration options for Django Forms Workflows.
- Basic Setup
- Data Sources
- LDAP Integration
- Database Integration
- Cross-Instance Sync
- Workflows
- Email Notifications
- File Uploads
- Security
- Advanced Options
# Basic installation
pip install django-forms-workflows
# With LDAP support
pip install django-forms-workflows[ldap]
# With MS SQL Server support
pip install django-forms-workflows[mssql]
# With PostgreSQL support
pip install django-forms-workflows[postgresql]
# With all optional dependencies
pip install django-forms-workflows[all]# settings.py
INSTALLED_APPS = [
# Django apps
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# Third-party apps
'nested_admin',
'crispy_forms',
'crispy_bootstrap5',
# Django Forms Workflows
'django_forms_workflows',
# Your apps
# ...
]# settings.py
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
CRISPY_TEMPLATE_PACK = "bootstrap5"python manage.py migrate django_forms_workflows# urls.py
from django.urls import path, include
urlpatterns = [
path('_nested_admin/', include('nested_admin.urls')),
path('admin/', admin.site.urls),
path('forms/', include('django_forms_workflows.urls')),
# ...
]Django Forms Workflows supports multiple data sources for prefilling form fields.
# settings.py
FORMS_WORKFLOWS = {
# Enable/disable features
'ENABLE_APPROVALS': True,
'ENABLE_AUDIT_LOG': True,
'ENABLE_FILE_UPLOADS': True,
# File upload settings
'MAX_FILE_SIZE': 10 * 1024 * 1024, # 10MB
'ALLOWED_FILE_EXTENSIONS': ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'jpg', 'png'],
# Data source configuration
'DATABASE_SOURCE': {
'database_alias': 'external_db', # Django database alias
'user_id_field': 'external_id', # UserProfile field for lookups
'default_schema': 'dbo', # Default schema if not specified
},
}-
User Data Source (
user.*)user.emailuser.first_nameuser.last_nameuser.full_nameuser.username
-
LDAP Data Source (
ldap.*)ldap.departmentldap.titleldap.managerldap.phoneldap.employee_id
-
Database Data Source (
{{ db.schema.table.column }})- Query external databases
- Requires configuration (see below)
You can register custom data sources:
# myapp/data_sources.py
from django_forms_workflows.data_sources import DataSource, register_data_source
class SalesforceSource(DataSource):
def get_value(self, user, field_name, **kwargs):
# Query Salesforce API
from simple_salesforce import Salesforce
sf = Salesforce(...)
contact = sf.Contact.get_by_custom_id('Email', user.email)
return contact.get(field_name)
def is_available(self):
from django.conf import settings
return hasattr(settings, 'SALESFORCE_CONFIG')
# Register the source
register_data_source('salesforce', SalesforceSource)Then use in forms:
prefill_source = "salesforce.AccountName"
pip install django-forms-workflows[ldap]# settings.py
import ldap
from django_auth_ldap.config import LDAPSearch, ActiveDirectoryGroupType
# LDAP Server Configuration
AUTH_LDAP_SERVER_URI = "ldap://ldap.example.com:389"
AUTH_LDAP_BIND_DN = "CN=Service Account,OU=Service Accounts,DC=example,DC=com"
AUTH_LDAP_BIND_PASSWORD = "your-password"
# Connection options
AUTH_LDAP_CONNECTION_OPTIONS = {
ldap.OPT_DEBUG_LEVEL: 0,
ldap.OPT_REFERRALS: 0,
ldap.OPT_X_TLS_REQUIRE_CERT: ldap.OPT_X_TLS_NEVER,
ldap.OPT_NETWORK_TIMEOUT: 5, # 5 second timeout
ldap.OPT_TIMEOUT: 5,
}
# User search
AUTH_LDAP_USER_SEARCH = LDAPSearch(
"DC=example,DC=com",
ldap.SCOPE_SUBTREE,
"(sAMAccountName=%(user)s)" # For Active Directory
)
# Attribute mapping
AUTH_LDAP_USER_ATTR_MAP = {
"first_name": "givenName",
"last_name": "sn",
"email": "mail",
}
# Group search (optional)
AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
"DC=example,DC=com",
ldap.SCOPE_SUBTREE,
"(objectClass=group)"
)
AUTH_LDAP_GROUP_TYPE = ActiveDirectoryGroupType()
# Mirror groups to Django
AUTH_LDAP_MIRROR_GROUPS = True
# Authentication backends
AUTHENTICATION_BACKENDS = [
'django_auth_ldap.backend.LDAPBackend',
'django.contrib.auth.backends.ModelBackend', # Fallback
]Create a custom LDAP backend:
# myapp/ldap_backend.py
from django_auth_ldap.backend import LDAPBackend
from django_forms_workflows.models import UserProfile
class CustomLDAPBackend(LDAPBackend):
def authenticate_ldap_user(self, ldap_user, password):
user = super().authenticate_ldap_user(ldap_user, password)
if user:
# Sync LDAP attributes to UserProfile
profile, created = UserProfile.objects.get_or_create(user=user)
if hasattr(ldap_user, 'attrs'):
attrs = ldap_user.attrs
profile.employee_id = attrs.get('employeeID', [''])[0]
profile.department = attrs.get('department', [''])[0]
profile.title = attrs.get('title', [''])[0]
profile.phone = attrs.get('telephoneNumber', [''])[0]
profile.save()
return userThen use your custom backend:
# settings.py
AUTHENTICATION_BACKENDS = [
'myapp.ldap_backend.CustomLDAPBackend',
'django.contrib.auth.backends.ModelBackend',
]# settings.py
DATABASES = {
'default': {
# Your primary database (PostgreSQL recommended)
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'forms_db',
'USER': 'postgres',
'PASSWORD': 'password',
'HOST': 'localhost',
'PORT': '5432',
},
'external_db': {
# External database for prefill data
'ENGINE': 'mssql', # or 'django.db.backends.mysql', etc.
'NAME': 'legacy_db',
'USER': 'readonly_user',
'PASSWORD': 'password',
'HOST': 'legacy-server.example.com',
'PORT': '1433',
'OPTIONS': {
'driver': 'ODBC Driver 18 for SQL Server',
'extra_params': 'TrustServerCertificate=yes',
},
},
}
# Configure database source
FORMS_WORKFLOWS = {
'DATABASE_SOURCE': {
'database_alias': 'external_db',
'user_id_field': 'external_id', # UserProfile field
'default_schema': 'dbo',
},
}In Django Admin, set the prefill_source field:
{{ db.hr.employees.department }}
{{ db.hr.staff.phone_number }}
{{ employees.email }} # Uses default schema
The system will:
- Get the user's
external_idfrom their UserProfile - Query:
SELECT [column] FROM [schema].[table] WHERE ID_NUMBER = ? - Return the value to prefill the form field
Django Forms Workflows supports pushing and pulling form definitions (including all fields, workflow stages, prefill sources, and post-submission actions) between multiple running instances. This is useful for promoting forms from a test or staging environment to production without shell access.
Each instance exposes two HTTP endpoints under the app's URL prefix:
| Endpoint | Method | Purpose |
|---|---|---|
forms-sync/export/ |
GET |
Export forms as JSON (optionally filtered by slug) |
forms-sync/import/ |
POST |
Import a JSON payload of forms |
Both endpoints require a Bearer token that must match FORMS_SYNC_API_TOKEN on the receiving instance.
Set on every instance that should accept or serve sync requests:
# settings.py
FORMS_SYNC_API_TOKEN = "your-shared-secret-here"If this setting is absent the endpoints return 403 Forbidden, effectively disabling sync.
Security note: use a long random secret and keep it out of version control (e.g. via environment variable:
FORMS_SYNC_API_TOKEN = env("FORMS_SYNC_API_TOKEN")).
Pre-configure named remote instances so that admin users can pick a destination from a dropdown instead of typing URLs and tokens every time. This is especially valuable in Kubernetes environments where kubectl exec is inconvenient.
# settings.py
FORMS_SYNC_REMOTES = [
{
"name": "Test Site",
"url": "https://test.example.com",
"token": "test-site-secret",
},
{
"name": "Production",
"url": "https://prod.example.com",
"token": "prod-site-secret",
},
]Each entry requires:
name— display label shown in the admin dropdownurl— base URL of the remote Django instance (no trailing slash required)token— theFORMS_SYNC_API_TOKENconfigured on that remote
After configuration, the Form Definitions changelist in Django Admin gains three toolbar buttons:
| Button | URL | Description |
|---|---|---|
| ↓ Pull from Remote | sync-pull/ |
Multi-step: pick remote → preview forms → import selected |
| ↑ Push All to Remote | sync-push/ |
Push all (or selected) forms to a destination instance |
| ⤴ Import JSON | sync-import/ |
Upload a .json file or paste raw JSON |
There is also an "Export selected forms as JSON" and "Push selected forms to a remote instance" bulk action available from the changelist action dropdown.
All import operations accept a conflict parameter controlling what happens when a form with the same slug already exists on the destination:
| Mode | Behaviour |
|---|---|
update (default) |
Overwrite the existing form definition |
skip |
Leave the existing form untouched |
new_slug |
Create a copy with an _imported suffix on the slug |
For scripted or CI-based sync workflows:
# Pull all forms from a remote instance
python manage.py pull_forms --source-url https://test.example.com --token SECRET
# Pull specific forms only
python manage.py pull_forms --source-url https://test.example.com --token SECRET \
--slugs time-off-request,onboarding
# Push selected forms to a remote instance
python manage.py push_forms --dest-url https://prod.example.com --token SECRET \
--slugs time-off-request,onboarding
# Dry-run (preview without writing)
python manage.py pull_forms --source-url https://test.example.com --token SECRET --dry-run
# Use skip conflict mode
python manage.py pull_forms --source-url https://test.example.com --token SECRET \
--conflict skip