Skip to content

Commit

Permalink
♻️ [#4510] Refactor the submission completion validation implementation
Browse files Browse the repository at this point in the history
The completion validation + communicating the status URL back to
the frontend was a weird and unconventional construct. The API
documentation was also outdated with what is actually being
returned.

This has now been reworked and stabilized to ensure a consistent
format of validation errors that can be picked up and processed
by the frontend/SDK reliably.
  • Loading branch information
sergei-maertens committed Feb 4, 2025
1 parent 3f79ba2 commit be16c74
Show file tree
Hide file tree
Showing 7 changed files with 256 additions and 182 deletions.
1 change: 1 addition & 0 deletions pyright.pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ include = [
"src/openforms/registrations/contrib/objects_api/typing.py",
"src/openforms/registrations/contrib/zgw_apis/",
# core submissions app
"src/openforms/submissions/api/validation.py",
"src/openforms/submissions/cosigning.py",
"src/openforms/submissions/report.py",
# our own template app/package on top of Django
Expand Down
85 changes: 53 additions & 32 deletions src/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4873,11 +4873,26 @@ paths:
status endpoint that a retry is needed, the ID is added back to the session.
---
**Warning**
**Validation errors**
The validation errors are returned in the usual `invalidParams` structure. The
following errors can occur:
* name `privacyPolicyAccepted` - unchecked while accepting it is required
* name `statementOfTruthAccepted` - unchecked while accepting it is required
* name `steps[i].nonFieldErrors`:
* code `blocked` - a logic rule prevents the step from being submitted,
which blocks the submission as a whole from being completed.
* code `incomplete` - there is no or incomplete step data submitted for the
step at this index.
* name `steps[i].data.*` - validation errors related to the formio components
in the step data.
Additionally, if you try to complete a submission that doesn't allow this (due
to form-level configuration), you will get an HTTP 403 error.
The schema of the validation errors response is currently marked as
experimental. See our versioning policy in the developer documentation for
what this means.
---
summary: Complete a submission
parameters:
Expand All @@ -4898,14 +4913,13 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/Submission'
$ref: '#/components/schemas/SubmissionCompletion'
application/x-www-form-urlencoded:
schema:
$ref: '#/components/schemas/Submission'
$ref: '#/components/schemas/SubmissionCompletion'
multipart/form-data:
schema:
$ref: '#/components/schemas/Submission'
required: true
$ref: '#/components/schemas/SubmissionCompletion'
security:
- cookieAuth: []
responses:
Expand All @@ -4928,7 +4942,22 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/CompletionValidation'
$ref: '#/components/schemas/ValidationError'
description: ''
headers:
X-Session-Expires-In:
$ref: '#/components/headers/X-Session-Expires-In'
X-CSRFToken:
$ref: '#/components/headers/X-CSRFToken'
X-Is-Form-Designer:
$ref: '#/components/headers/X-Is-Form-Designer'
Content-Language:
$ref: '#/components/headers/Content-Language'
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/Exception'
description: ''
headers:
X-Session-Expires-In:
Expand Down Expand Up @@ -6966,29 +6995,6 @@ components:
- depth
- name
- url
CompletionValidation:
type: object
properties:
incompleteSteps:
type: array
items:
type: string
maxItems: 0
submissionAllowed:
$ref: '#/components/schemas/SubmissionAllowedEnum'
privacyPolicyAccepted:
type: boolean
statementOfTruthAccepted:
type: boolean
containsBlockedSteps:
type: boolean
required:
- containsBlockedSteps
- incompleteSteps
- privacyPolicyAccepted
- statementOfTruthAccepted
- submissionAllowed
x-experimental: true
ComponentProperty:
type: object
properties:
Expand Down Expand Up @@ -9928,10 +9934,25 @@ components:
- representation
SubmissionCompletion:
type: object
description: |-
Validate the submission completion.
The input validation requires the statement checkboxes to be provided before
completion can be considered. If that is fine, we run validation on the state of
the submission, which is a bit "weird" in the sense that we're double-checking
earlier user-input and not directly validating ``request.data`` items.
If all is well, we return the status URL to check the background processing,
otherwise the validation errors are raised for the frontend to handle.
properties:
privacyPolicyAccepted:
type: boolean
statementOfTruthAccepted:
type: boolean
statusUrl:
type: string
format: uri
readOnly: true
title: status check endpoint
description: The API endpoint where the background processing status can
be checked. After calling the completion endpoint, this status URL should
Expand Down
23 changes: 3 additions & 20 deletions src/openforms/submissions/api/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@

from django.utils.translation import gettext_lazy as _

from rest_framework.fields import BooleanField, ChoiceField
from rest_framework.fields import BooleanField
from rest_framework_nested.relations import NestedHyperlinkedRelatedField

from openforms.forms.constants import SubmissionAllowedChoices

from .validators import CheckCheckboxAccepted


Expand Down Expand Up @@ -64,7 +62,7 @@ def get_url(self, obj, view_name, request, format):

class TruthDeclarationAcceptedField(BooleanField):
default_validators = [
CheckCheckboxAccepted(
CheckCheckboxAccepted( # pyright: ignore
"ask_statement_of_truth",
_("You must declare the form to be filled out truthfully."),
)
Expand All @@ -73,23 +71,8 @@ class TruthDeclarationAcceptedField(BooleanField):

class PrivacyPolicyAcceptedField(BooleanField):
default_validators = [
CheckCheckboxAccepted(
CheckCheckboxAccepted( # pyright: ignore
"ask_privacy_consent",
_("You must accept the privacy policy."),
)
]


# TODO: ideally this should be moved into a permission-check in the view rather than
# input validation, as the end-user cannot correct this 'mistake'.
class SubmissionAllowedField(ChoiceField):
default_error_messages = {"invalid": _("Submission of this form is not allowed.")}

def __init__(self, *args, **kwargs):
kwargs["choices"] = SubmissionAllowedChoices.choices
super().__init__(*args, **kwargs)

def to_internal_value(self, form_configuration_value: SubmissionAllowedChoices):
if form_configuration_value != SubmissionAllowedChoices.yes:
self.fail("invalid")
return form_configuration_value
Loading

0 comments on commit be16c74

Please sign in to comment.