Skip to content

Commit

Permalink
[#4321] Allow specific amount of submissions per form
Browse files Browse the repository at this point in the history
  • Loading branch information
vaszig committed Dec 11, 2024
1 parent 27a9852 commit e7e6d5a
Show file tree
Hide file tree
Showing 22 changed files with 649 additions and 12 deletions.
122 changes: 118 additions & 4 deletions src/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1336,6 +1336,8 @@ paths:
- `deactivateOn`
- `isDeleted`
- `submissionConfirmationTemplate`
- `submissionMaximumAllowed`
- `submissionCounter`
- `askPrivacyConsent`
- `askStatementOfTruth`
- `submissionsRemovalOptions`
Expand Down Expand Up @@ -1407,6 +1409,8 @@ paths:
- `deactivateOn`
- `isDeleted`
- `submissionConfirmationTemplate`
- `submissionMaximumAllowed`
- `submissionCounter`
- `askPrivacyConsent`
- `askStatementOfTruth`
- `submissionsRemovalOptions`
Expand Down Expand Up @@ -1922,6 +1926,8 @@ paths:
- `deactivateOn`
- `isDeleted`
- `submissionConfirmationTemplate`
- `submissionMaximumAllowed`
- `submissionCounter`
- `askPrivacyConsent`
- `askStatementOfTruth`
- `submissionsRemovalOptions`
Expand Down Expand Up @@ -1999,6 +2005,8 @@ paths:
- `deactivateOn`
- `isDeleted`
- `submissionConfirmationTemplate`
- `submissionMaximumAllowed`
- `submissionCounter`
- `askPrivacyConsent`
- `askStatementOfTruth`
- `submissionsRemovalOptions`
Expand Down Expand Up @@ -2080,6 +2088,8 @@ paths:
- `deactivateOn`
- `isDeleted`
- `submissionConfirmationTemplate`
- `submissionMaximumAllowed`
- `submissionCounter`
- `askPrivacyConsent`
- `askStatementOfTruth`
- `submissionsRemovalOptions`
Expand Down Expand Up @@ -2497,6 +2507,88 @@ paths:
$ref: '#/components/headers/X-Is-Form-Designer'
Content-Language:
$ref: '#/components/headers/Content-Language'
/api/v2/forms/{uuid_or_slug}/reset_submission_counter:
patch:
operationId: forms_reset_submission_counter_partial_update
description: |-
Manage forms.
Forms are collections of form steps, where each form step points to a formio.js
form definition. Multiple definitions are combined in logical steps to build a
multi-step/page form for end-users to fill out. Form definitions can be (and are)
re-used among different forms.
**Warning: the response data depends on user permissions**
Non-staff users receive a subset of the documented fields which are used
for internal form configuration. These fields are:
- `internalName`
- `registrationBackends`
- `authenticationBackendOptions`
- `paymentBackend`
- `paymentBackendOptions`
- `priceVariableKey`
- `product`
- `category`
- `theme`
- `activateOn`
- `deactivateOn`
- `isDeleted`
- `submissionConfirmationTemplate`
- `submissionMaximumAllowed`
- `submissionCounter`
- `askPrivacyConsent`
- `askStatementOfTruth`
- `submissionsRemovalOptions`
- `confirmationEmailTemplate`
- `displayMainWebsiteLink`
- `includeConfirmationPageContentInPdf`
- `translations`
- `brpPersonenRequestOptions`
parameters:
- in: header
name: X-CSP-Nonce
schema:
type: string
description: The value of the CSP nonce generated by the page embedding the
SDK. If provided, fields containing rich text from WYSIWYG editors will
be post-processed to allow inline styles with the provided nonce. If the
embedding page emits a `style-src` policy containing `unsafe-inline`, then
you can omit this header without losing functionality. We recommend favouring
the nonce mechanism though.
- in: path
name: uuid_or_slug
schema:
type: integer
description: A unique integer value identifying this form.
required: true
tags:
- forms
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/PatchedForm'
security:
- tokenAuth: []
- cookieAuth: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/Form'
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'
/api/v2/forms/{uuid_or_slug}/variables:
get:
operationId: forms_variables_list
Expand Down Expand Up @@ -7507,7 +7599,7 @@ components:
Note that this schema is used for both non-admin users filling out forms and
admin users designing forms. The fields that are only relevant for admin users are:
`internalName`, `registrationBackends`, `authenticationBackendOptions`, `paymentBackend`, `paymentBackendOptions`, `priceVariableKey`, `product`, `category`, `theme`, `activateOn`, `deactivateOn`, `isDeleted`, `submissionConfirmationTemplate`, `askPrivacyConsent`, `askStatementOfTruth`, `submissionsRemovalOptions`, `confirmationEmailTemplate`, `displayMainWebsiteLink`, `includeConfirmationPageContentInPdf`, `translations`, `brpPersonenRequestOptions`.
`internalName`, `registrationBackends`, `authenticationBackendOptions`, `paymentBackend`, `paymentBackendOptions`, `priceVariableKey`, `product`, `category`, `theme`, `activateOn`, `deactivateOn`, `isDeleted`, `submissionConfirmationTemplate`, `submissionMaximumAllowed`, `submissionCounter`, `askPrivacyConsent`, `askStatementOfTruth`, `submissionsRemovalOptions`, `confirmationEmailTemplate`, `displayMainWebsiteLink`, `includeConfirmationPageContentInPdf`, `translations`, `brpPersonenRequestOptions`.
properties:
uuid:
type: string
Expand Down Expand Up @@ -7653,6 +7745,16 @@ components:
* `yes` - Yes
* `no_with_overview` - No (with overview page)
* `no_without_overview` - No (without overview page)
submissionMaximumAllowed:
type: integer
nullable: true
submissionCounter:
type: integer
readOnly: true
nullable: true
submissionLimitReached:
type: boolean
readOnly: true
suspensionAllowed:
type: boolean
description: Whether the user is allowed to suspend this form or not.
Expand Down Expand Up @@ -7768,6 +7870,8 @@ components:
- resumeLinkLifetime
- slug
- steps
- submissionCounter
- submissionLimitReached
- submissionReportDownloadLinkTitle
- submissionStatementsConfiguration
- url
Expand Down Expand Up @@ -8014,7 +8118,7 @@ components:
Note that this schema is used for both non-admin users filling out forms and
admin users designing forms. The fields that are only relevant for admin users are:
`internalName`, `registrationBackends`, `authenticationBackendOptions`, `paymentBackend`, `paymentBackendOptions`, `priceVariableKey`, `product`, `category`, `theme`, `activateOn`, `deactivateOn`, `isDeleted`, `submissionConfirmationTemplate`, `askPrivacyConsent`, `askStatementOfTruth`, `submissionsRemovalOptions`, `confirmationEmailTemplate`, `displayMainWebsiteLink`, `includeConfirmationPageContentInPdf`, `translations`, `brpPersonenRequestOptions`.
`internalName`, `registrationBackends`, `authenticationBackendOptions`, `paymentBackend`, `paymentBackendOptions`, `priceVariableKey`, `product`, `category`, `theme`, `activateOn`, `deactivateOn`, `isDeleted`, `submissionConfirmationTemplate`, `submissionMaximumAllowed`, `submissionCounter`, `askPrivacyConsent`, `askStatementOfTruth`, `submissionsRemovalOptions`, `confirmationEmailTemplate`, `displayMainWebsiteLink`, `includeConfirmationPageContentInPdf`, `translations`, `brpPersonenRequestOptions`.
properties:
name:
type: string
Expand Down Expand Up @@ -8185,7 +8289,7 @@ components:
Note that this schema is used for both non-admin users filling out forms and
admin users designing forms. The fields that are only relevant for admin users are:
`internalName`, `registrationBackends`, `authenticationBackendOptions`, `paymentBackend`, `paymentBackendOptions`, `priceVariableKey`, `product`, `category`, `theme`, `activateOn`, `deactivateOn`, `isDeleted`, `submissionConfirmationTemplate`, `askPrivacyConsent`, `askStatementOfTruth`, `submissionsRemovalOptions`, `confirmationEmailTemplate`, `displayMainWebsiteLink`, `includeConfirmationPageContentInPdf`, `translations`, `brpPersonenRequestOptions`.
`internalName`, `registrationBackends`, `authenticationBackendOptions`, `paymentBackend`, `paymentBackendOptions`, `priceVariableKey`, `product`, `category`, `theme`, `activateOn`, `deactivateOn`, `isDeleted`, `submissionConfirmationTemplate`, `submissionMaximumAllowed`, `submissionCounter`, `askPrivacyConsent`, `askStatementOfTruth`, `submissionsRemovalOptions`, `confirmationEmailTemplate`, `displayMainWebsiteLink`, `includeConfirmationPageContentInPdf`, `translations`, `brpPersonenRequestOptions`.
properties:
name:
type: string
Expand Down Expand Up @@ -9112,7 +9216,7 @@ components:
Note that this schema is used for both non-admin users filling out forms and
admin users designing forms. The fields that are only relevant for admin users are:
`internalName`, `registrationBackends`, `authenticationBackendOptions`, `paymentBackend`, `paymentBackendOptions`, `priceVariableKey`, `product`, `category`, `theme`, `activateOn`, `deactivateOn`, `isDeleted`, `submissionConfirmationTemplate`, `askPrivacyConsent`, `askStatementOfTruth`, `submissionsRemovalOptions`, `confirmationEmailTemplate`, `displayMainWebsiteLink`, `includeConfirmationPageContentInPdf`, `translations`, `brpPersonenRequestOptions`.
`internalName`, `registrationBackends`, `authenticationBackendOptions`, `paymentBackend`, `paymentBackendOptions`, `priceVariableKey`, `product`, `category`, `theme`, `activateOn`, `deactivateOn`, `isDeleted`, `submissionConfirmationTemplate`, `submissionMaximumAllowed`, `submissionCounter`, `askPrivacyConsent`, `askStatementOfTruth`, `submissionsRemovalOptions`, `confirmationEmailTemplate`, `displayMainWebsiteLink`, `includeConfirmationPageContentInPdf`, `translations`, `brpPersonenRequestOptions`.
properties:
uuid:
type: string
Expand Down Expand Up @@ -9258,6 +9362,16 @@ components:
* `yes` - Yes
* `no_with_overview` - No (with overview page)
* `no_without_overview` - No (without overview page)
submissionMaximumAllowed:
type: integer
nullable: true
submissionCounter:
type: integer
readOnly: true
nullable: true
submissionLimitReached:
type: boolean
readOnly: true
suspensionAllowed:
type: boolean
description: Whether the user is allowed to suspend this form or not.
Expand Down
19 changes: 18 additions & 1 deletion src/openforms/forms/admin/form.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django.contrib import admin, messages
from django.contrib.admin.templatetags.admin_list import result_headers
from django.db.models import Count
from django.db.models import Count, F, Q
from django.http.response import HttpResponse, HttpResponseRedirect
from django.template.response import TemplateResponse
from django.urls import path, reverse
Expand Down Expand Up @@ -48,6 +48,21 @@ class FormStepInline(OrderedTabularInline):
extra = 1


class FormReachedSubmissionLimitListFilter(admin.SimpleListFilter):
title = _("has reached submission limit")
parameter_name = "submission_limit"

def lookups(self, request, model_admin):
return [("unavailable", "Unavailable for submission")]

def queryset(self, request, queryset):
if self.value() == "unavailable":
return queryset.filter(

Check warning on line 60 in src/openforms/forms/admin/form.py

View check run for this annotation

Codecov / codecov/patch

src/openforms/forms/admin/form.py#L60

Added line #L60 was not covered by tests
~Q(submission_maximum_allowed=None)
& Q(submission_maximum_allowed=F("submission_counter"))
)


class FormDeletedListFilter(admin.ListFilter):
title = _("is deleted")
parameter_name = "deleted"
Expand Down Expand Up @@ -112,6 +127,7 @@ class FormAdmin(
"active",
"maintenance_mode",
"translation_enabled",
"submission_maximum_allowed",
"get_authentication_backends_display",
"get_payment_backend_display",
"get_registration_backend_display",
Expand All @@ -129,6 +145,7 @@ class FormAdmin(
"maintenance_mode",
"translation_enabled",
FormDeletedListFilter,
FormReachedSubmissionLimitListFilter,
)
search_fields = ("uuid", "name", "internal_name", "slug")

Expand Down
29 changes: 29 additions & 0 deletions src/openforms/forms/api/serializers/form.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,13 @@ class FormSerializer(PublicFieldsSerializerMixin, serializers.ModelSerializer):
"of type 'checkbox'."
),
)
submission_maximum_allowed = serializers.IntegerField(
allow_null=True, required=False
)
submission_counter = serializers.IntegerField(
allow_null=True, required=False, read_only=True
)
submission_limit_reached = serializers.SerializerMethodField()
brp_personen_request_options = BRPPersonenRequestOptionsSerializer(
required=False, allow_null=True
)
Expand Down Expand Up @@ -257,6 +264,9 @@ class Meta:
"introduction_page_content",
"explanation_template",
"submission_allowed",
"submission_maximum_allowed",
"submission_counter",
"submission_limit_reached",
"suspension_allowed",
"ask_privacy_consent",
"ask_statement_of_truth",
Expand Down Expand Up @@ -299,6 +309,7 @@ class Meta:
"active",
"required_fields_with_asterisk",
"submission_allowed",
"submission_limit_reached",
"suspension_allowed",
"send_confirmation_email",
"appointment_options",
Expand Down Expand Up @@ -448,6 +459,17 @@ def validate_auto_login_backend(self, attrs):
}
)

def validate_submission_maximum_allowed(self, value):
if form := self.instance:
if value and value <= form.submission_counter:
raise serializers.ValidationError(
_(
"The maximum amount of allowed submissions must be bigger than the existing amount of submissions."
"Consider resetting the submissions counter."
)
)
return value

def get_required_fields_with_asterisk(self, obj) -> bool:
config = GlobalConfiguration.get_solo()
return config.form_display_required_with_asterisk
Expand Down Expand Up @@ -513,6 +535,13 @@ def get_cosign_has_link_in_email(self, obj: Form) -> bool:
config = GlobalConfiguration.get_solo()
return config.cosign_request_template_has_link

@extend_schema_field(OpenApiTypes.BOOL)
def get_submission_limit_reached(self, obj: Form):
if obj and obj.submission_maximum_allowed:
if obj.submission_maximum_allowed == obj.submission_counter:
return True
return False


FormSerializer.__doc__ = FormSerializer.__doc__.format(
admin_fields=", ".join(
Expand Down
12 changes: 12 additions & 0 deletions src/openforms/forms/api/viewsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,18 @@ def perform_destroy(self, instance):
instance._is_deleted = True
instance.save()

@action(detail=True, methods=["patch"])
def reset_submission_counter(self, request, **kwargs):
instance = self.get_object()
instance.submission_counter = 0
instance.save()

Check warning on line 428 in src/openforms/forms/api/viewsets.py

View check run for this annotation

Codecov / codecov/patch

src/openforms/forms/api/viewsets.py#L426-L428

Added lines #L426 - L428 were not covered by tests

updated_data = {

Check warning on line 430 in src/openforms/forms/api/viewsets.py

View check run for this annotation

Codecov / codecov/patch

src/openforms/forms/api/viewsets.py#L430

Added line #L430 was not covered by tests
"uuid": instance.uuid,
"submission_counter": instance.submission_counter,
}
return Response(updated_data, status=status.HTTP_200_OK)

Check warning on line 434 in src/openforms/forms/api/viewsets.py

View check run for this annotation

Codecov / codecov/patch

src/openforms/forms/api/viewsets.py#L434

Added line #L434 was not covered by tests

@extend_schema(
summary=_("Prepare form edit admin message"),
tags=["admin"],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Generated by Django 4.2.16 on 2024-12-05 13:41

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("forms", "0106_convert_price_logic_rules"),
]

operations = [
migrations.AddField(
model_name="form",
name="submission_counter",
field=models.PositiveIntegerField(
default=0,
help_text="Counter to track how many submissions have been completed for the specific form. This works in combination with the maximum allowed submissions per form and can be reset via the frontend.",
verbose_name="submissions counter",
),
),
migrations.AddField(
model_name="form",
name="submission_maximum_allowed",
field=models.PositiveIntegerField(
blank=True,
help_text="Maximum number of allowed submissions per form. Leave this empty if no limit is needed.",
null=True,
verbose_name="maximum allowed submissions",
),
),
]
Loading

0 comments on commit e7e6d5a

Please sign in to comment.