Skip to content

Commit

Permalink
[#4321] First attempt to limit the amount of submissions per form
Browse files Browse the repository at this point in the history
  • Loading branch information
vaszig committed Dec 3, 2024
1 parent 0bda3f3 commit c23efc9
Show file tree
Hide file tree
Showing 15 changed files with 286 additions and 8 deletions.
29 changes: 28 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(
# ~Q(submission_maximum_allowed=0)
# & 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_display",
"get_authentication_backends_display",
"get_payment_backend_display",
"get_registration_backend_display",
Expand All @@ -129,11 +145,22 @@ class FormAdmin(
"maintenance_mode",
"translation_enabled",
FormDeletedListFilter,
FormReachedSubmissionLimitListFilter,
)
search_fields = ("uuid", "name", "internal_name", "slug")

change_list_template = "admin/forms/form/change_list.html"

def submission_maximum_allowed_display(self, obj):
"""Show a friendly label for the submission_maximum_allowed field."""
return (
obj.submission_maximum_allowed
if obj.submission_maximum_allowed != 0
else "-"
)

submission_maximum_allowed_display.short_description = "Max submissions"

def changelist_view(self, request, extra_context=None):
if request.GET.get("_async"):
return self._async_changelist_view(request)
Expand Down
14 changes: 14 additions & 0 deletions src/openforms/forms/api/serializers/form.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,8 @@ class Meta:
"introduction_page_content",
"explanation_template",
"submission_allowed",
"submission_maximum_allowed",
"submission_counter_reset",
"suspension_allowed",
"ask_privacy_consent",
"ask_statement_of_truth",
Expand Down Expand Up @@ -311,6 +313,8 @@ class Meta:
"active",
"required_fields_with_asterisk",
"submission_allowed",
"submission_maximum_allowed",
"submission_counter_reset",
"suspension_allowed",
"appointment_options",
"resume_link_lifetime",
Expand Down Expand Up @@ -405,6 +409,16 @@ def _update_v2_registration_backend(self, form, validated_data):

@transaction.atomic()
def update(self, instance, validated_data):
# # This is the case where we used to have a submission_maximum_allowed limit and then we
# # changed it to zero (no limit) or to something else. The counter should be reset as well,
# # in order to be able to rely on it again (This does not affect the form statistics).
# if (
# validated_data["submission_maximum_allowed"] == 0
# or validated_data["submission_maximum_allowed"]
# <= instance.submission_counter
# ):
# instance.submission_counter = 0

confirmation_email_template = validated_data.pop(
"confirmation_email_template", None
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Generated by Django 4.2.16 on 2024-12-03 15:48

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.",
verbose_name="submissions counter",
),
),
migrations.AddField(
model_name="form",
name="submission_counter_reset",
field=models.BooleanField(
default=True,
help_text="Whether the counter of the submissions should be reset or not.",
verbose_name="submission counter reset",
),
),
migrations.AddField(
model_name="form",
name="submission_maximum_allowed",
field=models.PositiveIntegerField(
blank=True,
default=0,
help_text="Maximum number of allowed submissions per form. Set this to zero (0) if no limit is needed.",
verbose_name="maximum allowed submissions",
),
),
]
41 changes: 39 additions & 2 deletions src/openforms/forms/models/form.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,27 @@ class Form(models.Model):
)

# submission
submission_maximum_allowed = models.PositiveIntegerField(
_("maximum allowed submissions"),
default=0,
blank=True,
help_text=_(
"Maximum number of allowed submissions per form. Set this to zero (0) if no limit is needed."
),
)
submission_counter = models.PositiveIntegerField(
_("submissions counter"),
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."
),
)
submission_counter_reset = models.BooleanField(
_("submission counter reset"),
default=True,
help_text=_("Whether the counter of the submissions should be reset or not."),
)
submission_confirmation_template = HTMLField(
_("submission confirmation template"),
help_text=_(
Expand Down Expand Up @@ -382,12 +403,28 @@ def __str__(self):
@property
def is_available(self) -> bool:
"""
Soft deleted, deactivated or forms in maintenance mode are not available.
Soft deleted, deactivated, forms in maintenance mode or forms which have reached the
submission limit are not available.
"""
if any((self._is_deleted, not self.active, self.maintenance_mode)):
if any(
(
self._is_deleted,
not self.active,
self.maintenance_mode,
self.has_reached_submissions_limit(),
)
):
return False
return True

def has_reached_submissions_limit(self) -> bool:
if (
self.submission_maximum_allowed != 0
and self.submission_maximum_allowed <= self.submission_counter
):
return True
return False

def get_absolute_url(self):
return reverse("forms:form-detail", kwargs={"slug": self.slug})

Expand Down
24 changes: 24 additions & 0 deletions src/openforms/js/compiled-lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,12 @@
"value": "Something went wrong while retrieving the available object types."
}
],
"6umAJ3": [
{
"type": 0,
"value": "Whether the counter of the submissions should be reset or not."
}
],
"7/Bhpw": [
{
"type": 0,
Expand Down Expand Up @@ -3185,6 +3191,12 @@
"value": "Add variable"
}
],
"SmwbfM": [
{
"type": 0,
"value": "Submission counter reset"
}
],
"Su4nqf": [
{
"type": 0,
Expand Down Expand Up @@ -5463,6 +5475,12 @@
"value": "Version"
}
],
"n8/j1j": [
{
"type": 0,
"value": "The maximum number of allowed submissions for this form. Set this to zero (0) if no limit is needed."
}
],
"n9T2Oy": [
{
"type": 0,
Expand Down Expand Up @@ -5989,6 +6007,12 @@
"value": "Enable to attach file uploads to the registration email. If set, this overrides the global default. Form designers should take special care to ensure that the total file upload sizes do not exceed the email size limit."
}
],
"sQekFr": [
{
"type": 0,
"value": "Maximum allowed number of submissions"
}
],
"sR9GVQ": [
{
"type": 0,
Expand Down
24 changes: 24 additions & 0 deletions src/openforms/js/compiled-lang/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,12 @@
"value": "Er gings iets fout bij het ophalen van de beschikabre objecttypes."
}
],
"6umAJ3": [
{
"type": 0,
"value": "Whether the counter of the submissions should be reset or not."
}
],
"7/Bhpw": [
{
"type": 0,
Expand Down Expand Up @@ -3202,6 +3208,12 @@
"value": "Variabele toevoegen"
}
],
"SmwbfM": [
{
"type": 0,
"value": "Submission counter reset"
}
],
"Su4nqf": [
{
"type": 0,
Expand Down Expand Up @@ -5485,6 +5497,12 @@
"value": "Versie"
}
],
"n8/j1j": [
{
"type": 0,
"value": "The maximum number of allowed submissions for this form. Set this to zero (0) if no limit is needed."
}
],
"n9T2Oy": [
{
"type": 0,
Expand Down Expand Up @@ -6011,6 +6029,12 @@
"value": "Vink aan om gebruikersbestanden als bijlage aan de registratiemail toe te voegen. Als een waarde gezet is, dan heeft deze hogere prioriteit dan de globale configuratie. Formulierbeheerders moeten ervoor zorgen dat de totale maximale bestandsgrootte onder de maximale e-mailbestandsgrootte blijft."
}
],
"sQekFr": [
{
"type": 0,
"value": "Maximum allowed number of submissions"
}
],
"sR9GVQ": [
{
"type": 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import LoAOverrideOption from 'components/admin/form_design/authentication/LoAOv
import Field from 'components/admin/forms/Field';
import Fieldset from 'components/admin/forms/Fieldset';
import FormRow from 'components/admin/forms/FormRow';
import {Checkbox, DateTimeInput, TextInput} from 'components/admin/forms/Inputs';
import {Checkbox, DateTimeInput, NumberInput, TextInput} from 'components/admin/forms/Inputs';
import Select from 'components/admin/forms/Select';
import {getTranslatedChoices} from 'utils/i18n';

Expand Down Expand Up @@ -107,6 +107,8 @@ const FormConfigurationFields = ({
maintenanceMode,
translationEnabled,
submissionAllowed,
submissionMaximumAllowed,
submissionCounterReset,
suspensionAllowed,
askPrivacyConsent,
askStatementOfTruth,
Expand Down Expand Up @@ -432,6 +434,44 @@ const FormConfigurationFields = ({
/>
</Field>
</FormRow>
<FormRow>
<Field
name="form.submissionMaximumAllowed"
label={
<FormattedMessage
defaultMessage="Maximum allowed number of submissions"
description="Form submissionMaximumAllowed field label"
/>
}
helpText={
<FormattedMessage
defaultMessage="The maximum number of allowed submissions for this form. Set this to zero (0) if no limit is needed."
description="Form submissionMaximumAllowed field help text"
/>
}
>
<NumberInput value={submissionMaximumAllowed || ''} onChange={onChange} min="0" />
</Field>
<Field>
<Checkbox
name="form.submissionCounterReset"
label={
<FormattedMessage
defaultMessage="Submission counter reset"
description="Form submissionCounterReset field label"
/>
}
helpText={
<FormattedMessage
defaultMessage="Whether the counter of the submissions should be reset or not."
description="Form submissionCounterReset field help text"
/>
}
checked={submissionCounterReset}
onChange={event => onCheckboxChange(event, submissionCounterReset)}
/>
</Field>
</FormRow>

{!isAppointment && (
<FormRow>
Expand Down Expand Up @@ -538,6 +578,7 @@ FormConfigurationFields.propTypes = {
maintenanceMode: PropTypes.bool.isRequired,
translationEnabled: PropTypes.bool.isRequired,
submissionAllowed: PropTypes.oneOf(SUMBISSION_ALLOWED_CHOICES.map(opt => opt[0])),
submissionMaximumAllowed: PropTypes.number.isRequired,
suspensionAllowed: PropTypes.bool.isRequired,
askPrivacyConsent: statementChoices.isRequired,
askStatementOfTruth: statementChoices.isRequired,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ const initialFormState = {
maintenanceMode: false,
translationEnabled: false,
submissionAllowed: 'yes',
submissionMaximumAllowed: 0,
submissionCounterReset: false,
suspensionAllowed: true,
askPrivacyConsent: 'global_setting',
askStatementOfTruth: 'global_setting',
Expand Down Expand Up @@ -172,6 +174,8 @@ const FORM_FIELDS_TO_TAB_NAMES = {
translationEnabled: 'form',
confirmationEmailTemplate: 'submission-confirmation',
submissionAllowed: 'form',
submissionMaximumAllowed: 'form',
submissionCounterReset: 'form',
registrationBackends: 'registration',
product: 'product-payment',
paymentBackend: 'product-payment',
Expand Down
Loading

0 comments on commit c23efc9

Please sign in to comment.