Form builder building blocks for feincms3-forms
Project description
feincms3-formbuilder
feincms3-formbuilder provides the abstract models, views, processing helpers, renderer factory, admin utilities, and templates needed to build a form-builder app on top of feincms3-forms. Its relationship to feincms3-forms mirrors the relationship of feincms3 to django-content-editor: the lower-level library defines the protocol; feincms3-formbuilder wires everything together so that projects only need to write the thin, project-specific layer.
Installation
pip install feincms3-formbuilder
Add the app to INSTALLED_APPS:
INSTALLED_APPS = [
...
"content_editor",
"feincms3_forms",
"feincms3_formbuilder",
...
]
Models
Create four concrete models in your app.
ConfiguredForm
Subclass AbstractConfiguredForm, add any project fields (e.g. a slug), and
override FORMS to point validate and process at your own functions:
# myapp/models.py
from content_editor.models import Region, create_plugin_base
from django.db import models
from feincms3_forms import models as forms_models
from feincms3_formbuilder.models import (
AbstractConfiguredForm,
AbstractFormStep,
AbstractFormSubmission,
)
class ConfiguredForm(AbstractConfiguredForm):
slug = models.SlugField(unique=True, blank=True)
FORMS = [
forms_models.FormType(
key="simple",
label="simple form",
regions=[
Region(key="form", title="Form fields"),
Region(key="success", title="Success message"),
],
form_class="django.forms.Form",
validate="myapp.validation.validate_configured_form",
process="myapp.processing.process_simple_form",
),
forms_models.FormType(
key="multistep",
label="multi-step form",
regions=lambda configured_form: (
(
[
Region(key=step.region_key, title=step.title)
for step in configured_form.steps.all()
]
if configured_form.pk
else []
)
+ [Region(key="success", title="Success message")]
),
form_class="django.forms.Form",
validate="myapp.validation.validate_configured_form",
process="myapp.processing.process_multistep_form",
),
]
FormStep
Subclass AbstractFormStep and add a FK to ConfiguredForm. The
AbstractFormStep provides title, an auto-generated identifier (used as
the region key), and ordering:
class FormStep(AbstractFormStep):
configured_form = models.ForeignKey(
ConfiguredForm,
on_delete=models.CASCADE,
related_name="steps",
)
class Meta(AbstractFormStep.Meta):
unique_together = [
("configured_form", "ordering"),
("configured_form", "identifier"),
]
FormSubmission
Subclass AbstractFormSubmission, add a FK to ConfiguredForm, and override
get_formatted_data to pass your field model:
class FormSubmission(AbstractFormSubmission):
configured_form = models.ForeignKey(
ConfiguredForm,
on_delete=models.CASCADE,
related_name="submissions",
)
def get_formatted_data(self):
return super().get_formatted_data(field_model=SimpleField)
AbstractFormSubmission stores submitted_at, data (JSON), ip_address,
user_agent, and optional related_content_type / related_object_id
generic FK fields (used for the submission-ref feature described below).
AbstractConfiguredForm and AbstractFormStep both ship with created_at
(auto_now_add=True) and updated_at (auto_now=True). The default
ordering on AbstractConfiguredForm is ["-created_at"].
SimpleField and proxy models
Create the plugin base, a SimpleField model, and proxy models for each
field type you want to support:
ConfiguredFormPlugin = create_plugin_base(ConfiguredForm)
class SimpleField(forms_models.SimpleFieldBase, ConfiguredFormPlugin):
class Meta:
verbose_name = "form field"
verbose_name_plural = "form fields"
Text = SimpleField.proxy(SimpleField.Type.TEXT)
Email = SimpleField.proxy(SimpleField.Type.EMAIL)
URL = SimpleField.proxy(SimpleField.Type.URL)
Date = SimpleField.proxy(SimpleField.Type.DATE)
Integer = SimpleField.proxy(SimpleField.Type.INTEGER)
Textarea = SimpleField.proxy(SimpleField.Type.TEXTAREA)
Checkbox = SimpleField.proxy(SimpleField.Type.CHECKBOX)
Select = SimpleField.proxy(SimpleField.Type.SELECT)
Radio = SimpleField.proxy(SimpleField.Type.RADIO)
SelectMultiple = SimpleField.proxy(SimpleField.Type.SELECT_MULTIPLE)
CheckboxSelectMultiple = SimpleField.proxy(SimpleField.Type.CHECKBOX_SELECT_MULTIPLE)
You can add further non-field plugins (e.g. a RichText) the same way any
django-content-editor plugin is added.
Processing
A process function receives the request and validated data and must return
an HttpResponse. Use the create_submission and render_success_region
helpers to keep the implementation minimal.
Simple form — receives a bound, valid form:
# myapp/processing.py
from feincms3_formbuilder.processing import create_submission, render_success_region
from myapp.models import FormSubmission
from myapp.renderer import renderer
def process_simple_form(request, form, *, configured_form):
data = dict(form.cleaned_data)
create_submission(request, configured_form, data, submission_model=FormSubmission)
return render_success_region(request, configured_form, renderer=renderer)
Multi-step form — receives accumulated_data collected across all steps:
def process_multistep_form(request, configured_form, accumulated_data):
data = dict(accumulated_data)
create_submission(request, configured_form, data, submission_model=FormSubmission)
return render_success_region(request, configured_form, renderer=renderer)
create_submission automatically extracts the _ref token (see
Templatetags) from data, verifies it, and stores the
resolved generic FK on the submission.
Notifications
feincms3-formbuilder ships an optional notification module that lets a
project send confirmation/staff emails after a form submission. The
package provides the abstract model, validator, and helper; the project
owns the concrete model, admin integration, and editor widget.
Concrete FormNotification model
from feincms3_formbuilder.notifications import AbstractFormNotification
class FormNotification(AbstractFormNotification):
configured_form = models.ForeignKey(
ConfiguredForm,
on_delete=models.CASCADE,
related_name="notifications",
)
AbstractFormNotification provides three fields:
| Field | Purpose |
|---|---|
recipients |
Comma-separated emails or a Django template variable that resolves to one (e.g. {{ form_data.email }}) |
subject |
Plain-text subject; supports template variables |
body |
HTML body; supports template variables; rendered with autoescape on |
The recipients field is validated at save time (via validators=[validate_recipients] on the field):
- Empty values are rejected.
- If the value contains any
{{ … }}it is accepted as-is (the package cannot inspect what's in the project's context). - Otherwise each comma-separated token must validate as an email.
Sending notifications from process()
# myapp/processing.py
from feincms3_formbuilder.processing import create_submission, render_success_region
from feincms3_formbuilder.notifications import send_form_notifications
def process_simple_form(request, form, *, configured_form):
data = dict(form.cleaned_data)
submission = create_submission(
request, configured_form, data, submission_model=FormSubmission,
)
send_form_notifications(
configured_form.notifications.all(),
context={"form_data": data, "submission": submission},
)
return render_success_region(request, configured_form, renderer=renderer)
context is a plain dict; whatever keys you place there are available
to the editor as Django template variables in recipients, subject,
and body. The form_data key is the documented standard (used by the
notification body's help text); other keys are project-specific.
Variables for editors
Documented out of the box:
{{ form_data.<field_name> }}— any cleaned value from the form
Anything else (a submission link, a related-object link, a project-
specific identifier) is whatever the project decides to put in context.
Failure handling
send_form_notifications defaults to fail_silently=True: per-notification
failures (template syntax errors, invalid rendered recipients, SMTP errors)
are logged via the feincms3_formbuilder.notifications logger at ERROR
and the remaining notifications continue to send. Pass
fail_silently=False to re-raise instead — useful in tests.
FORMBUILDER_FROM_EMAIL setting
The From address used for every notification is, in order:
settings.FORMBUILDER_FROM_EMAILif set and non-emptysettings.DEFAULT_FROM_EMAIL
Admin integration
The package ships no admin classes for notifications. Wire your inline in your project admin:
class FormNotificationInline(admin.TabularInline):
model = FormNotification
extra = 0
@admin.register(ConfiguredForm)
class ConfiguredFormAdmin(admin.ModelAdmin):
inlines = [
FormStepInline.for_model(FormStep),
FormNotificationInline,
*simple_field_inlines(SimpleField),
]
For a rich-text editor on body, use formfield_overrides or a custom
ModelForm:
from django_prose_editor.fields import ProseEditorFormField
class FormNotificationInlineForm(forms.ModelForm):
body = ProseEditorFormField()
class Meta:
model = FormNotification
fields = "__all__"
class FormNotificationInline(admin.TabularInline):
model = FormNotification
form = FormNotificationInlineForm
Extending with extra fields
Projects that want from_email / reply_to / bcc / cc add fields
to their concrete subclass and pass a custom send_one to the helper:
from feincms3_formbuilder.notifications import send_form_notifications
def my_send_one(notification, context):
# Build EmailMultiAlternatives including notification.reply_to etc.
...
send_form_notifications(
configured_form.notifications.all(),
context={"form_data": data, "submission": submission},
send_one=my_send_one,
)
Validation
Implement a validate function that returns a list of error strings. Use the
validate_with_renderer helper so that field-name uniqueness is checked across
all plugins registered with your renderer:
# myapp/validation.py
from feincms3_formbuilder.models import validate_with_renderer
from myapp.renderer import renderer
def validate_configured_form(configured_form):
return validate_with_renderer(configured_form, renderer)
Renderer
Call create_form_renderer() with your field-producing plugin models as
positional arguments and any non-field plugins via extra_plugins:
# myapp/renderer.py
from feincms3.renderer import template_renderer
from feincms3_formbuilder.renderer import create_form_renderer
from myapp.models import NewsletterField, RichText, SimpleField
renderer = create_form_renderer(
SimpleField,
NewsletterField,
extra_plugins={
RichText: template_renderer("myapp/richtext.html"),
},
)
create_form_renderer(*field_models, extra_plugins=None) returns a
RegionRenderer where:
- Every model in
field_modelsis wired to the built-inrender_form_fieldhandler, which renders each field usingfeincms3_formbuilder/form_field.html. Pass any number of plugin models here — they all share that same wrapper template. - Every model in
extra_pluginsis registered with the renderer callable you provide. Use this for plugins that are not form fields (e.g. aRichTextblock) or for field plugins that need different outer markup thanform_field.html— in that case write a custom renderer that callsform.get_form_fields(plugin)itself.
If you want every field to render through your own template, override
feincms3_formbuilder/form_field.html in your project's templates directory
rather than registering each model individually.
Admin
Use ConfiguredFormAdmin together with the simple_field_inlines() helper and
FormStepInline:
# myapp/admin.py
from django.contrib import admin
from feincms3_formbuilder.admin import FormStepInline, simple_field_inlines
from myapp.models import ConfiguredForm, FormStep, SimpleField
@admin.register(ConfiguredForm)
class ConfiguredFormAdmin(admin.ModelAdmin):
inlines = [
FormStepInline.for_model(FormStep),
*simple_field_inlines(SimpleField),
]
simple_field_inlines(model) returns one SimpleFieldInline per field type,
each pre-configured with a Material Icons button and a deny_regions({"success"})
constraint so that field plugins cannot be placed in the success region.
FormStepInline is an OrderableAdmin TabularInline. Bind it to your
concrete FormStep model with FormStepInline.for_model(FormStep), or by
subclassing and setting model explicitly.
For the submission admin, subclass BaseFormSubmissionAdmin:
from feincms3_formbuilder.admin import BaseFormSubmissionAdmin
from myapp.models import FormSubmission
@admin.register(FormSubmission)
class FormSubmissionAdmin(BaseFormSubmissionAdmin):
pass # add project-specific actions, list_filter etc. here
BaseFormSubmissionAdmin ships:
list_display,list_filter,date_hierarchy, and a two-sectionfieldsets(main data + related object) covering every field onAbstractFormSubmissionplus the consumer's requiredconfigured_formFK.formatted_data_display— callsobj.get_formatted_data().related_object_link— resolves the generic FK (related_content_type/related_object_id) to an admin change-page link, or-when unset.has_add_permission()returningFalse(submissions are user-generated).
Views and URLs
Write a thin wrapper that looks up the ConfiguredForm and dispatches to
simple_form_view or multistep_form_view:
# myapp/views.py
from django.shortcuts import get_object_or_404
from feincms3_formbuilder.views import multistep_form_view, simple_form_view
from myapp.models import ConfiguredForm
from myapp.renderer import renderer
def form_view(request, slug):
configured_form = get_object_or_404(ConfiguredForm, slug=slug)
if configured_form.form_type == "multistep":
return multistep_form_view(request, configured_form, renderer=renderer)
return simple_form_view(request, configured_form, renderer=renderer)
# myapp/urls.py
from django.urls import path
from myapp import views
app_name = "forms"
urlpatterns = [
path("<slug:slug>/", views.form_view, name="form"),
]
The dispatch lives in your project because your project owns the FORMS
configuration that defines which form types exist. The "multistep" string
above must match the key= you set on the corresponding FormType in
FORMS.
multistep_form_view walks all regions whose key starts with
STEP_REGION_PREFIX ("step_") — this matches AbstractFormStep.region_key.
Pass get_step_regions= (a callable (configured_form) -> list[Region]) to
override the selection, e.g. to mix step regions with project-specific
content regions.
Templates
The package ships three minimal templates under
feincms3_formbuilder/:
| Template | Used by |
|---|---|
form.html |
simple_form_view — wraps the form in a <form> tag with a Submit button |
multistep_form.html |
multistep_form_view — adds step navigation, Back / Next / Submit buttons |
form_field.html |
render_form_field — renders label, widget, help text, and errors for each field |
Override any of them by creating a file at the same path inside your project's
template directories. For example, to style the step navigation, copy
feincms3_formbuilder/multistep_form.html into your app's
templates/feincms3_formbuilder/ directory and modify it as needed.
Templatetags
Load feincms3_formbuilder_tags to access the make_submission_ref filter.
It signs a content-type / object-id pair so that a form submission can be
linked back to a related object (e.g. an event registration linked to an event):
{% load feincms3_formbuilder_tags %}
<form method="post">
{% csrf_token %}
<input type="hidden" name="_ref" value="{{ event|make_submission_ref }}">
...
</form>
When create_submission processes the form data it pops _ref, verifies the
signature, and stores the resolved generic FK on the submission. You can then
query submissions for a specific object:
FormSubmission.objects.for_related_object(event)
The view layer also reads ?ref= from the GET query string and pre-fills it
into the form's initial data under the key _ref. This lets you link to a
form with ?ref={{ obj|make_submission_ref }} and have the token survive
through the form submission, provided your form class declares a hidden
_ref field:
from django import forms
class BaseForm(forms.Form):
_ref = forms.CharField(required=False, widget=forms.HiddenInput)
If your form class has no _ref field the initial value is silently ignored.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file feincms3_formbuilder-0.3.3.tar.gz.
File metadata
- Download URL: feincms3_formbuilder-0.3.3.tar.gz
- Upload date:
- Size: 19.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4e85dd7ad428dba27f8961bd6012b610b01f0d7395f62badcd3d3392caa05d2c
|
|
| MD5 |
8352e7eb9421b6cdfdc49357bf94865b
|
|
| BLAKE2b-256 |
ddc8d48cec5bf235f8a80ec5944570f7832e5c7f6c5b32c662fa409b868a95e6
|
Provenance
The following attestation bundles were made for feincms3_formbuilder-0.3.3.tar.gz:
Publisher:
publish.yml on fabiangermann/feincms3-formbuilder
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
feincms3_formbuilder-0.3.3.tar.gz -
Subject digest:
4e85dd7ad428dba27f8961bd6012b610b01f0d7395f62badcd3d3392caa05d2c - Sigstore transparency entry: 1840117718
- Sigstore integration time:
-
Permalink:
fabiangermann/feincms3-formbuilder@2ce5be110610c9dd81dc8a67bf2f5c092bf9e4fb -
Branch / Tag:
refs/tags/v0.3.3 - Owner: https://github.com/fabiangermann
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@2ce5be110610c9dd81dc8a67bf2f5c092bf9e4fb -
Trigger Event:
push
-
Statement type:
File details
Details for the file feincms3_formbuilder-0.3.3-py3-none-any.whl.
File metadata
- Download URL: feincms3_formbuilder-0.3.3-py3-none-any.whl
- Upload date:
- Size: 26.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6ab6b15070b5b237fe35024f7887a0b224199bda2a802e945c2eaab2377c4d1e
|
|
| MD5 |
3d72e5aefa62d61eae9df5fcd867c972
|
|
| BLAKE2b-256 |
bfb390314e5d7e7eccbce3b1528ac54f0c9cda889e08a6b8ee155b876b663a84
|
Provenance
The following attestation bundles were made for feincms3_formbuilder-0.3.3-py3-none-any.whl:
Publisher:
publish.yml on fabiangermann/feincms3-formbuilder
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
feincms3_formbuilder-0.3.3-py3-none-any.whl -
Subject digest:
6ab6b15070b5b237fe35024f7887a0b224199bda2a802e945c2eaab2377c4d1e - Sigstore transparency entry: 1840117867
- Sigstore integration time:
-
Permalink:
fabiangermann/feincms3-formbuilder@2ce5be110610c9dd81dc8a67bf2f5c092bf9e4fb -
Branch / Tag:
refs/tags/v0.3.3 - Owner: https://github.com/fabiangermann
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@2ce5be110610c9dd81dc8a67bf2f5c092bf9e4fb -
Trigger Event:
push
-
Statement type: