Handling submissions¶
This chapter covers everything that happens after a form is submitted: storing data, notifying stakeholders, supporting multi-step flows, and letting users resume incomplete submissions.
Storing uploaded files¶
When saving a form submission that includes file uploads, move the in-memory file objects to permanent storage before serialising the data as JSON:
from django.core.files import File
def save_files(instance, form):
data = form.cleaned_data.copy()
for key, value in data.items():
if isinstance(value, File):
data[key] = uploads_storage.save(
f"{instance._meta.label_lower}/{instance.pk}/{value.name}",
value,
)
return data
def process_form(request, form, *, configured_form):
instance = MyModel.objects.create(
email=form.cleaned_data["email"],
configured_form=configured_form,
)
instance.data = save_files(instance, form)
instance.save()
Sending email notifications¶
from content_editor.contents import contents_for_item
from feincms3_forms.reporting import get_loaders, value_default
def send_notifications_to_managers(data, *, configured_form, url=""):
recipients = configured_form.send_notifications_to or [
row[1] for row in settings.MANAGERS
]
contents = contents_for_item(configured_form, plugins=renderer.plugins())
loaders = get_loaders(contents)
values = [value_default(loader(data)) for loader in loaders]
mail = render_to_mail(
"forms/notification_mail",
{"configured_form": configured_form, "values": values, "url": url},
to=recipients,
)
mail.send()
Email template (forms/notification_mail.txt):
A new {{ configured_form.name }} has been submitted.
{% for value in values %}
{{ value.label }}: {{ value.value }}
{% endfor %}
{% if url %}View in admin: {{ url }}{% endif %}
Incremental data merging for multi-step forms¶
Use the dict merge operator to accumulate data across multiple form steps:
def process_step_one(request, form, *, configured_form):
submission = Submission.objects.create(
configured_form=configured_form, data={}
)
submission.data = save_files(submission, form)
submission.save()
return HttpResponseRedirect(submission.get_next_step_url())
def process_step_two(request, form, *, submission):
# | preserves data from previous steps
submission.data = submission.data | save_files(submission, form)
submission.save()
return HttpResponseRedirect(submission.get_report_url())
Continue later (save progress)¶
Allow users to save an incomplete form and resume later:
from django.core import signing
class ConfiguredForm(forms_models.ConfiguredForm):
FORMS = [
forms_models.FormType(
key="grant-proposal",
label="grant proposal",
regions=[Region(key="form", title="form")],
form_class="app.forms.forms.GrantProposalForm",
process="app.forms.forms.process_grant_proposal_form",
allow_continue_later=True,
),
]
def process_grant_proposal_form(request, form, *, configured_form):
form.instance.data = form.instance.data | save_files(form.instance, form)
form.instance.save()
if "_continue" in request.POST:
messages.success(request, "The proposal has been saved.")
return HttpResponseRedirect(form.instance.get_proposal_url())
messages.success(request, "The proposal has been sent.")
# send notifications...
Template submit buttons:
<button type="submit" name="_submit">{% trans "Submit" %}</button>
{% if configured_form.type.allow_continue_later %}
<button type="submit" name="_continue">{% trans "Save and continue later" %}</button>
{% endif %}
Signed URLs for submission access¶
Use Django’s signing framework for tamper-proof URLs that don’t require authentication:
from django.core.signing import Signer
_signer = Signer(salt="submissions")
class SubmissionQuerySet(models.QuerySet):
def get_by_code(self, code):
return self.get(pk=_signer.unsign(code))
class Submission(models.Model):
objects = SubmissionQuerySet.as_manager()
def get_report_url(self):
return reverse_app("forms", "report", kwargs={"code": _signer.sign(self.pk)})
def signed_submission(func):
@wraps(func)
def view(request, **kwargs):
if "code" not in kwargs:
return func(request, **kwargs)
try:
submission = Submission.objects.get_by_code(kwargs.pop("code"))
except Submission.DoesNotExist:
messages.error(request, "The submission does not exist.")
except Exception:
messages.error(request, "The link is invalid.")
else:
return func(request, submission=submission, **kwargs)
return HttpResponseRedirect("../../../")
return view