diff --git a/src/openapi.yaml b/src/openapi.yaml index ac2561fa52..f88e9d2eb2 100644 --- a/src/openapi.yaml +++ b/src/openapi.yaml @@ -1336,6 +1336,8 @@ paths: - `deactivateOn` - `isDeleted` - `submissionConfirmationTemplate` + - `submissionMaximumAllowed` + - `submissionCounter` - `askPrivacyConsent` - `askStatementOfTruth` - `submissionsRemovalOptions` @@ -1407,6 +1409,8 @@ paths: - `deactivateOn` - `isDeleted` - `submissionConfirmationTemplate` + - `submissionMaximumAllowed` + - `submissionCounter` - `askPrivacyConsent` - `askStatementOfTruth` - `submissionsRemovalOptions` @@ -1922,6 +1926,8 @@ paths: - `deactivateOn` - `isDeleted` - `submissionConfirmationTemplate` + - `submissionMaximumAllowed` + - `submissionCounter` - `askPrivacyConsent` - `askStatementOfTruth` - `submissionsRemovalOptions` @@ -1999,6 +2005,8 @@ paths: - `deactivateOn` - `isDeleted` - `submissionConfirmationTemplate` + - `submissionMaximumAllowed` + - `submissionCounter` - `askPrivacyConsent` - `askStatementOfTruth` - `submissionsRemovalOptions` @@ -2080,6 +2088,8 @@ paths: - `deactivateOn` - `isDeleted` - `submissionConfirmationTemplate` + - `submissionMaximumAllowed` + - `submissionCounter` - `askPrivacyConsent` - `askStatementOfTruth` - `submissionsRemovalOptions` @@ -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 @@ -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 @@ -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. @@ -7768,6 +7870,8 @@ components: - resumeLinkLifetime - slug - steps + - submissionCounter + - submissionLimitReached - submissionReportDownloadLinkTitle - submissionStatementsConfiguration - url @@ -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 @@ -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 @@ -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 @@ -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. diff --git a/src/openforms/forms/admin/form.py b/src/openforms/forms/admin/form.py index 3e4f5f815e..74bd24d5f4 100644 --- a/src/openforms/forms/admin/form.py +++ b/src/openforms/forms/admin/form.py @@ -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 @@ -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=None) + & Q(submission_maximum_allowed=F("submission_counter")) + ) + + class FormDeletedListFilter(admin.ListFilter): title = _("is deleted") parameter_name = "deleted" @@ -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", @@ -129,6 +145,7 @@ class FormAdmin( "maintenance_mode", "translation_enabled", FormDeletedListFilter, + FormReachedSubmissionLimitListFilter, ) search_fields = ("uuid", "name", "internal_name", "slug") diff --git a/src/openforms/forms/api/serializers/form.py b/src/openforms/forms/api/serializers/form.py index 71b6d69b45..768a24203b 100644 --- a/src/openforms/forms/api/serializers/form.py +++ b/src/openforms/forms/api/serializers/form.py @@ -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 ) @@ -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", @@ -299,6 +309,7 @@ class Meta: "active", "required_fields_with_asterisk", "submission_allowed", + "submission_limit_reached", "suspension_allowed", "send_confirmation_email", "appointment_options", @@ -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 @@ -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( diff --git a/src/openforms/forms/api/viewsets.py b/src/openforms/forms/api/viewsets.py index e0a81e87e2..9ea9d941ef 100644 --- a/src/openforms/forms/api/viewsets.py +++ b/src/openforms/forms/api/viewsets.py @@ -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() + + updated_data = { + "uuid": instance.uuid, + "submission_counter": instance.submission_counter, + } + return Response(updated_data, status=status.HTTP_200_OK) + @extend_schema( summary=_("Prepare form edit admin message"), tags=["admin"], diff --git a/src/openforms/forms/migrations/0107_form_submission_counter_and_more.py b/src/openforms/forms/migrations/0107_form_submission_counter_and_more.py new file mode 100644 index 0000000000..cf9ec9f4e2 --- /dev/null +++ b/src/openforms/forms/migrations/0107_form_submission_counter_and_more.py @@ -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", + ), + ), + ] diff --git a/src/openforms/forms/models/form.py b/src/openforms/forms/models/form.py index f08dccb956..2f531c7e80 100644 --- a/src/openforms/forms/models/form.py +++ b/src/openforms/forms/models/form.py @@ -124,6 +124,23 @@ class Form(models.Model): ) # submission + submission_maximum_allowed = models.PositiveIntegerField( + _("maximum allowed submissions"), + null=True, + blank=True, + help_text=_( + "Maximum number of allowed submissions per form. Leave this empty 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 and can be " + "reset via the frontend." + ), + ) submission_confirmation_template = HTMLField( _("submission confirmation template"), help_text=_( @@ -380,12 +397,27 @@ 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 ( + limit := self.submission_maximum_allowed + ) and limit == self.submission_counter: + return True + return False + def get_absolute_url(self): return reverse("forms:form-detail", kwargs={"slug": self.slug}) @@ -485,6 +517,7 @@ def copy(self): ) copy.slug = _("{slug}-copy").format(slug=self.slug) copy.product = self.product + copy.submission_counter = 0 # name translations diff --git a/src/openforms/forms/tests/test_models.py b/src/openforms/forms/tests/test_models.py index b112a7d2fe..73252f53e4 100644 --- a/src/openforms/forms/tests/test_models.py +++ b/src/openforms/forms/tests/test_models.py @@ -53,6 +53,18 @@ def test_registration_backend_display_multiple_backends(self): form.get_registration_backend_display(), "Backend #1, Backend #2" ) + def test_form_is_unavailable_when_limit_reached(self): + form: Form = FormFactory.create( + submission_maximum_allowed=2, submission_counter=2 + ) + self.assertFalse(form.is_available) + + def test_form_is_available_when_limit_not_reached(self): + form: Form = FormFactory.create( + submission_maximum_allowed=2, submission_counter=1 + ) + self.assertTrue(form.is_available) + @override_settings(LANGUAGE_CODE="en") def test_registration_backend_display_marks_misconfigs(self): form: Form = FormFactory.create() diff --git a/src/openforms/forms/tests/test_serializers.py b/src/openforms/forms/tests/test_serializers.py index d77f864a4b..85c9220e13 100644 --- a/src/openforms/forms/tests/test_serializers.py +++ b/src/openforms/forms/tests/test_serializers.py @@ -2,6 +2,7 @@ from django.contrib.auth.models import AnonymousUser from django.test import RequestFactory, TestCase +from django.utils.translation import gettext as _ from hypothesis import given from hypothesis.extra.django import TestCase as HypothesisTestCase @@ -333,3 +334,47 @@ def test_patching_registrations_with_a_booboo(self): self.assertEqual(backend2.name, "#2") self.assertEqual(backend2.backend, "email") self.assertEqual(backend2.options["to_emails"], ["me@example.com"]) + + def test_form_with_submission_max_and_submission_counter(self): + context = {"request": None} + + with self.subTest("submission_max_allowed equal to submission_counter"): + form = FormFactory.create( + submission_maximum_allowed=2, submission_counter=2 + ) + data = FormSerializer(context=context).to_representation(form) + serializer = FormSerializer(instance=form, data=data) + + expected_error = _( + "The maximum amount of allowed submissions must be bigger than the existing amount of submissions.Consider resetting the submissions counter." + ) + + self.assertFalse(serializer.is_valid()) + self.assertIn("submission_maximum_allowed", serializer.errors) + self.assertEqual( + serializer.errors["submission_maximum_allowed"][0], expected_error + ) + with self.subTest("submission_max_allowed bigger than submission_counter"): + form = FormFactory.create( + submission_maximum_allowed=2, submission_counter=1 + ) + data = FormSerializer(context=context).to_representation(form) + serializer = FormSerializer(instance=form, data=data) + + self.assertTrue(serializer.is_valid()) + with self.subTest("submission_max_allowed smaller than submission_counter"): + form = FormFactory.create( + submission_maximum_allowed=1, submission_counter=2 + ) + data = FormSerializer(context=context).to_representation(form) + serializer = FormSerializer(instance=form, data=data) + + expected_error = _( + "The maximum amount of allowed submissions must be bigger than the existing amount of submissions.Consider resetting the submissions counter." + ) + + self.assertFalse(serializer.is_valid()) + self.assertIn("submission_maximum_allowed", serializer.errors) + self.assertEqual( + serializer.errors["submission_maximum_allowed"][0], expected_error + ) diff --git a/src/openforms/js/compiled-lang/en.json b/src/openforms/js/compiled-lang/en.json index dd8f7fbfa0..a840602510 100644 --- a/src/openforms/js/compiled-lang/en.json +++ b/src/openforms/js/compiled-lang/en.json @@ -89,6 +89,12 @@ "value": "Default Value" } ], + "/cUjWg": [ + { + "type": 0, + "value": "The maximum number of allowed submissions for this form. Leave this empty if no limit is needed." + } + ], "/fAEsY": [ { "type": 0, @@ -1963,6 +1969,12 @@ "value": "Use existing form definition" } ], + "GJ8Ok2": [ + { + "type": 0, + "value": "Reset submissions counter" + } + ], "GO9yud": [ { "type": 0, @@ -2145,6 +2157,12 @@ "value": "Appointment enabled" } ], + "HrSXGN": [ + { + "type": 0, + "value": "You are about to reset the submissions counter and this action is irreversible. Are you sure that you want to do this?" + } + ], "HuAm1K": [ { "type": 0, @@ -5547,6 +5565,12 @@ "value": "Suffix (e.g. m²)" } ], + "oWOr9u": [ + { + "type": 0, + "value": "Submission" + } + ], "oXOxWz": [ { "type": 0, @@ -5947,6 +5971,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, diff --git a/src/openforms/js/compiled-lang/nl.json b/src/openforms/js/compiled-lang/nl.json index f80acfc0da..3c2baf40dc 100644 --- a/src/openforms/js/compiled-lang/nl.json +++ b/src/openforms/js/compiled-lang/nl.json @@ -89,6 +89,12 @@ "value": "Standaardwaarde" } ], + "/cUjWg": [ + { + "type": 0, + "value": "The maximum number of allowed submissions for this form. Leave this empty if no limit is needed." + } + ], "/fAEsY": [ { "type": 0, @@ -1984,6 +1990,12 @@ "value": "Gebruik bestaande formulierdefinitie" } ], + "GJ8Ok2": [ + { + "type": 0, + "value": "Reset submissions counter" + } + ], "GO9yud": [ { "type": 0, @@ -2166,6 +2178,12 @@ "value": "Is afspraakformulier?" } ], + "HrSXGN": [ + { + "type": 0, + "value": "You are about to reset the submissions counter and this action is irreversible. Are you sure that you want to do this?" + } + ], "HuAm1K": [ { "type": 0, @@ -5569,6 +5587,12 @@ "value": "Suffix (bijv. m²)" } ], + "oWOr9u": [ + { + "type": 0, + "value": "Submission" + } + ], "oXOxWz": [ { "type": 0, @@ -5969,6 +5993,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, diff --git a/src/openforms/js/components/admin/form_design/SubmissionFields.js b/src/openforms/js/components/admin/form_design/SubmissionFields.js new file mode 100644 index 0000000000..787ad215b2 --- /dev/null +++ b/src/openforms/js/components/admin/form_design/SubmissionFields.js @@ -0,0 +1,112 @@ +import PropTypes from 'prop-types'; +import {useContext, useEffect, useState} from 'react'; +import {FormattedMessage, useIntl} from 'react-intl'; + +import {APIContext} from 'components/admin/form_design/Context'; +import ActionButton from 'components/admin/forms/ActionButton'; +import Field from 'components/admin/forms/Field'; +import Fieldset from 'components/admin/forms/Fieldset'; +import FormRow from 'components/admin/forms/FormRow'; +import {NumberInput} from 'components/admin/forms/Inputs'; +import {FormException} from 'utils/exception'; +import {patch} from 'utils/fetch'; + +import useConfirm from './useConfirm'; + +export const SubmissionLimitFields = ({submissionMaximumAllowed, formUuid, onChange}) => { + const intl = useIntl(); + const {csrftoken} = useContext(APIContext); + const {ConfirmationModal, confirmationModalProps, openConfirmationModal} = useConfirm(); + const [confirmationResult, setConfirmationResult] = useState(null); + + const handleChange = event => { + const {name, value: initialValue} = event.target; + // the backend must receive a value or null since it's a nullable integer field + const value = initialValue === '' ? null : initialValue; + const updatedEvent = {...event, target: {...event.target, name, value}}; + onChange(updatedEvent); + }; + + const resetCounter = async () => { + try { + const resetResult = await patch( + `/api/v2/forms/${formUuid}/reset_submission_counter`, + csrftoken, + {}, + true + ); + if (!resetResult.ok) { + throw new FormException( + 'An error occurred while trying to reset the counter.', + resetResult.data + ); + } + } catch (e) { + return null; + } + return resetResult.data; + }; + + useEffect(() => { + if (confirmationResult === true) { + resetCounter(); + } + }, [confirmationResult]); + + return ( + <> +
+ +