diff --git a/docs/developers/backend/core/submissions.rst b/docs/developers/backend/core/submissions.rst index 8767e521e8..9089db6ea3 100644 --- a/docs/developers/backend/core/submissions.rst +++ b/docs/developers/backend/core/submissions.rst @@ -17,7 +17,7 @@ Globally, the various actions and plugin categories are processed in order: #. If applicable, an :ref:`appointment ` is created. If this fails, the submission is blocked and the user sees an error - message and can try again (note: this step is deprecated and not needed for the new appointment flow). + message and can try again. #. Pre-registration step. Each :ref:`registration plugin ` can perform pre-registration task, like for example generating and setting a submission reference ID. If no registration backend is configured, then an internal ID is generated and set on the submission. @@ -39,9 +39,8 @@ Globally, the various actions and plugin categories are processed in order: the previous actions, the confirmation email shows different contents. The confirmation email can show submission details, appointment details -(including links to cancel or change the appointment), payment details -(including a link to pay if not done so already), cosign details and custom information as part -of the form. +(including links to cancel the appointment), payment details(including a link to pay if not done so already), +cosign details and custom information as part of the form. Under the hood -------------- diff --git a/docs/manual/forms/examples/_assets/appointment_creation_2_2.png b/docs/manual/forms/examples/_assets/appointment_creation_2_2.png deleted file mode 100644 index a0beddfb62..0000000000 Binary files a/docs/manual/forms/examples/_assets/appointment_creation_2_2.png and /dev/null differ diff --git a/docs/manual/forms/examples/appointment_creation.rst b/docs/manual/forms/examples/appointment_creation.rst deleted file mode 100644 index 321cdaba60..0000000000 --- a/docs/manual/forms/examples/appointment_creation.rst +++ /dev/null @@ -1,83 +0,0 @@ -======================= -Afspraak maken (legacy) -======================= - -.. warning:: Deze manier van afspraken configureren wordt uitgefaseerd en zal in versie - 3.0 van Open Formulieren verwijderd worden. U kunt beter de - :ref:`nieuwe afsprakenconfiguratie ` gebruiken. - -In dit voorbeeld tonen we u hoe u een formulier kan maken zodat de gebruikers een afspraak -kan maken met het formulier. - -In dit voorbeeld gaan we er van uit dat u een -:ref:`eenvoudig formulier ` kan maken. - -Configuratie -============ - -* :ref:`Appointment configuratie ` - -Formulier maken -=============== - -1. Maak een formulier aan met de volgende gegevens: - - * **Naam**: Afspraak demo - -2. Klik op het tabblad **Stappen en velden**. -3. Klik aan de linkerkant op **Stap toevoegen** en selecteer **Maak een nieuwe - formulierdefinitie**. -4. Onder de sectie **(Herbruikbare) stapgegevens** vul het volgende in: - - * **Naam**: Afspraakgegevens - -5. Scroll naar de sectie **Velden**. -6. Sleep een **Select** component op het witte vlak, vul de volgende - gegevens in en druk daarna op **Opslaan**: - - * **Label**: Producten - -7. Sleep een **Select** component op het witte vlak, vul de volgende - gegevens in en druk daarna op **Opslaan**: - - * **Label**: Locaties - -8. Sleep een **Select** component op het witte vlak, vul de volgende - gegevens in en druk daarna op **Opslaan**: - - * **Label**: Datums - -9. Sleep een **Select** component op het witte vlak, vul de volgende - gegevens in en druk daarna op **Opslaan**: - - * **Label**: Tijden - -10. Sleep een **Tekstveld** component op het witte vlak, vul de volgende - gegevens in en druk daarna op **Opslaan**: - - * **Label**: Achternaam - -11. Sleep een **Datum** component op het witte vlak, vul de volgende - gegevens in en druk daarna op **Opslaan**: - - * **Label**: Geboortedatum - -12. (Optioneel) Sleep een **Telefoonnummer** component op het witte vlak, vul de volgende - gegevens in en druk daarna op **Opslaan**: - - * **Label**: Telefoonnummer - -13. Klik op het tabblad **Afspraken**. -14. Kies de juiste componenten voor elke veld in dit tabblad. - -.. image:: _assets/appointment_creation_2_2.png - -15. Klik onderaan op **Opslaan** om het formulier volledig op te slaan. - - -.. note:: - - De velden *Achternaam*, *Geboortedatum*, en *Telefoonnummer* mogen in een aparte formulierdefinitie (stap) - aanwezig zijn maar de velden *Producten*, *Locaties*, *Datums*, en *Tijden* moeten in dezelfde - formulierdefinitie (stap) aanwezig zijn. - diff --git a/docs/manual/forms/examples/index.rst b/docs/manual/forms/examples/index.rst index 47beeb3821..ed10944d6d 100644 --- a/docs/manual/forms/examples/index.rst +++ b/docs/manual/forms/examples/index.rst @@ -24,15 +24,3 @@ Voorbeelden suwinet form_with_geometry camunda7 - -====================== -Verouderde voorbeelden -====================== - -Deze voorbeelden beschrijven functionaliteit die uitgefaseerd wordt. We raden af om nog -nieuwe formulieren hiermee te bouwen. - -.. toctree:: - :maxdepth: 1 - - appointment_creation diff --git a/docs/manual/templates.rst b/docs/manual/templates.rst index 38dd952022..7c7311c68f 100644 --- a/docs/manual/templates.rst +++ b/docs/manual/templates.rst @@ -337,9 +337,8 @@ Voorbeeld *Opmerkingen* Geen opmerkingen - Als u uw afspraak wilt annuleren of wijzigen kunt u dat hieronder doen. + Als u uw afspraak wilt annuleren kunt u dat hieronder doen. Afspraak annuleren: https://example.com/... - Afspraak wijzigen: https://example.com/... **Betaalinformatie** diff --git a/src/openforms/appointments/admin.py b/src/openforms/appointments/admin.py index 250f5f210e..02f423e04f 100644 --- a/src/openforms/appointments/admin.py +++ b/src/openforms/appointments/admin.py @@ -1,11 +1,10 @@ from copy import copy -from typing import Literal from django.contrib import admin from django.contrib.admin.widgets import AdminTextInputWidget from django.utils import timezone -from django.utils.html import format_html_join -from django.utils.translation import gettext, gettext_lazy as _ +from django.utils.html import format_html +from django.utils.translation import gettext_lazy as _ from solo.admin import SingletonModelAdmin @@ -86,20 +85,8 @@ def get_object_actions(self, obj: AppointmentInfo) -> str: if obj.start_time and obj.start_time <= timezone.now(): return "-" - actions: list[tuple[Literal["cancel", "change"], str]] = [ - ("cancel", gettext("Cancel")), - ] - - # legacy appointments have the change option - is_legacy = not obj.submission.form.is_appointment - if is_legacy: - actions.append(("change", gettext("Change"))) - - links = ( - (BasePlugin.get_link(obj.submission, verb=action), label) - for action, label in actions - ) - return format_html_join(" | ", '{}', links) + link = BasePlugin.get_cancel_link(obj.submission) + return format_html('{}', link, _("Cancel")) get_object_actions.short_description = _("Appointment actions") diff --git a/src/openforms/appointments/api/views.py b/src/openforms/appointments/api/views.py index a05ca5aa21..b9638073e1 100644 --- a/src/openforms/appointments/api/views.py +++ b/src/openforms/appointments/api/views.py @@ -1,5 +1,4 @@ import logging -import warnings from copy import copy from django.db import transaction @@ -52,14 +51,6 @@ logger = logging.getLogger(__name__) -def warn_serializer_validation_deprecation(): - warnings.warn( - "Starting with Open Forms 3.0, input validation will raise HTTP 400 instead " - "of returning an empty result list.", - DeprecationWarning, - ) - - # TODO: see openforms.validations.api.serializers.ValidatorsFilterSerializer.as_openapi_params # and https://github.com/open-formulieren/open-forms/issues/611 @@ -99,14 +90,7 @@ class ProductsListView(ListMixin, APIView): def get_objects(self): serializer = ProductInputSerializer(data=self.request.query_params) - is_valid = serializer.is_valid() - # TODO: ideally we want to use raise_exception=True, but the SDK and the way - # that Formio work is that we can't prevent the invalid request from firing. - # Instead, we just return an empty result list which populates dropdowns with - # empty options. - if not is_valid: - warn_serializer_validation_deprecation() - return [] + serializer.is_valid(raise_exception=True) plugin = get_plugin() config = AppointmentsConfig().get_solo() @@ -141,14 +125,8 @@ class LocationsListView(ListMixin, APIView): def get_objects(self): serializer = LocationInputSerializer(data=self.request.query_params) - is_valid = serializer.is_valid() - # TODO: ideally we want to use raise_exception=True, but the SDK and the way - # that Formio work is that we can't prevent the invalid request from firing. - # Instead, we just return an empty result list which populates dropdowns with - # empty options. - if not is_valid: - warn_serializer_validation_deprecation() - return [] + serializer.is_valid(raise_exception=True) + products = serializer.validated_data["product_id"] plugin = get_plugin() with elasticapm.capture_span( @@ -178,14 +156,7 @@ class DatesListView(ListMixin, APIView): def get_objects(self): serializer = DateInputSerializer(data=self.request.query_params) - is_valid = serializer.is_valid() - # TODO: ideally we want to use raise_exception=True, but the SDK and the way - # that Formio work is that we can't prevent the invalid request from firing. - # Instead, we just return an empty result list which populates dropdowns with - # empty options. - if not is_valid: - warn_serializer_validation_deprecation() - return [] + serializer.is_valid(raise_exception=True) products = serializer.validated_data["product_id"] location = serializer.validated_data["location_id"] @@ -226,14 +197,7 @@ class TimesListView(ListMixin, APIView): def get_objects(self): serializer = TimeInputSerializer(data=self.request.query_params) - is_valid = serializer.is_valid() - # TODO: ideally we want to use raise_exception=True, but the SDK and the way - # that Formio work is that we can't prevent the invalid request from firing. - # Instead, we just return an empty result list which populates dropdowns with - # empty options. - if not is_valid: - warn_serializer_validation_deprecation() - return [] + serializer.is_valid(raise_exception=True) products = serializer.validated_data["product_id"] location = serializer.validated_data["location_id"] diff --git a/src/openforms/appointments/base.py b/src/openforms/appointments/base.py index e71f81bc04..0367aa3315 100644 --- a/src/openforms/appointments/base.py +++ b/src/openforms/appointments/base.py @@ -49,21 +49,6 @@ def __str__(self): return self.identifier -@dataclass() -class Customer: - """ - Deprecated in favour of the :class:`CustomerDetails`. - """ - - last_name: str - birthdate: date - initials: str | None = None - phonenumber: str | None = None - - def __str__(self): - return self.last_name - - F = TypeVar("F", bound=TextChoices) # generic type for the plugin-specific enum of field names @@ -210,7 +195,7 @@ def create_appointment( products: list[Product], location: Location, start_at: datetime, - client: CustomerDetails[F] | Customer, + client: CustomerDetails[F], remarks: str = "", ) -> str: """ @@ -219,7 +204,7 @@ def create_appointment( :param products: List of :class:`Product`, as obtained from :meth:`get_available_products`. :param location: An :class:`Location`, as obtained from :meth:`get_locations`. :param start_at: A `datetime` to start the appointment, as obtained from :meth:`get_dates`. - :param client: A :class:`Customer` that holds client details. + :param client: A :class:`CustomerDetails` that holds client details. :param remarks: A ``str`` for additional remarks, added to the appointment. :returns: An appointment identifier as ``str``. :raises AppointmentCreateFailed: If the appointment could not be created. @@ -249,11 +234,11 @@ def get_appointment_details(self, identifier: str) -> AppointmentDetails: # cosmetics @staticmethod - def get_link(submission: Submission, verb: str) -> str: + def get_cancel_link(submission: Submission) -> str: token = submission_appointment_token_generator.make_token(submission) path = reverse( - f"appointments:appointments-verify-{verb.lower()}-appointment-link", + "appointments:appointments-verify-cancel-appointment-link", kwargs={ "token": token, "submission_uuid": submission.uuid, @@ -261,11 +246,3 @@ def get_link(submission: Submission, verb: str) -> str: ) return build_absolute_uri(path) - - @classmethod - def get_cancel_link(cls, submission: Submission) -> str: - return cls.get_link(submission, "cancel") - - @classmethod - def get_change_link(cls, submission: Submission) -> str: - return cls.get_link(submission, "change") diff --git a/src/openforms/appointments/contrib/demo/plugin.py b/src/openforms/appointments/contrib/demo/plugin.py index fe3cb57808..aec28e3ae0 100644 --- a/src/openforms/appointments/contrib/demo/plugin.py +++ b/src/openforms/appointments/contrib/demo/plugin.py @@ -43,7 +43,7 @@ def get_dates(self, products, location, start_at=None, end_at=None): def get_times(self, products, location, day): today = timezone.localdate() - times = (time(12, 0), time(15, 15), time(15, 45)) + times = (time(12, 0), time(15, 15), time(15, 45), time(17, 45)) return [timezone.make_aware(datetime.combine(today, _time)) for _time in times] def get_required_customer_fields( diff --git a/src/openforms/appointments/contrib/demo/tests/test_endpoints.py b/src/openforms/appointments/contrib/demo/tests/test_endpoints.py index 8cd8063aaa..dab878cce6 100644 --- a/src/openforms/appointments/contrib/demo/tests/test_endpoints.py +++ b/src/openforms/appointments/contrib/demo/tests/test_endpoints.py @@ -71,7 +71,7 @@ def test_times_list(self): self.assertEqual(response.status_code, status.HTTP_200_OK) times = response.json() - self.assertEqual(len(times), 3) + self.assertEqual(len(times), 4) def test_required_customer_fields_list(self): endpoint = reverse("api:appointments-customer-fields") diff --git a/src/openforms/appointments/contrib/jcc/plugin.py b/src/openforms/appointments/contrib/jcc/plugin.py index 40586a2703..0879caacea 100644 --- a/src/openforms/appointments/contrib/jcc/plugin.py +++ b/src/openforms/appointments/contrib/jcc/plugin.py @@ -1,5 +1,4 @@ import logging -import warnings from collections import Counter from contextlib import contextmanager from datetime import date, datetime @@ -20,14 +19,7 @@ from openforms.plugins.exceptions import InvalidPluginConfiguration from openforms.utils.date import TIMEZONE_AMS, datetime_in_amsterdam -from ...base import ( - AppointmentDetails, - BasePlugin, - Customer, - CustomerDetails, - Location, - Product, -) +from ...base import AppointmentDetails, BasePlugin, CustomerDetails, Location, Product from ...exceptions import ( AppointmentCreateFailed, AppointmentDeleteFailed, @@ -251,28 +243,10 @@ def create_appointment( products: list[Product], location: Location, start_at: datetime, - client: CustomerDetails[CustomerFields] | Customer, + client: CustomerDetails[CustomerFields], remarks: str = "", ) -> str: product_ids = squash_ids(products) - - # Phasing out Customer in favour of CustomerDetails, so convert to the new type - if isinstance(client, Customer): - warnings.warn( - "Fixed customer fields via the Customer class are deprecated, use " - "dynamic CustomerDetails with 'get_required_customer_fields' instead.", - DeprecationWarning, - ) - client = CustomerDetails( - details={ - CustomerFields.last_name: client.last_name, - CustomerFields.birthday: client.birthdate.isoformat(), - # Phone number is often required for appointment, - # use fake phone number if no client phone number - CustomerFields.main_tel: client.phonenumber or "0123456789", - } - ) - customer_details = { FIELD_TO_XML_NAME[key]: value for key, value in client.details.items() } diff --git a/src/openforms/appointments/contrib/jcc/tests/test_api_endpoints.py b/src/openforms/appointments/contrib/jcc/tests/test_api_endpoints.py index c013d116eb..0aafab55a1 100644 --- a/src/openforms/appointments/contrib/jcc/tests/test_api_endpoints.py +++ b/src/openforms/appointments/contrib/jcc/tests/test_api_endpoints.py @@ -93,8 +93,8 @@ def test_get_locations_multiple_products(self, m): def test_get_locations_returns_400_when_no_product_id_is_given(self): response = self.client.get(self.endpoint) - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), []) + self.assertEqual(response.status_code, 400) + self.assertEqual(response.json()["invalidParams"][0]["code"], "required") def test_get_locations_returns_403_when_no_active_sessions(self): self._clear_session() @@ -165,8 +165,10 @@ def test_get_dates_returns_400_when_missing_query_params(self): for query_param in [{}, {"product_id": 79}, {"location_id": 1}]: with self.subTest(query_param=query_param): response = self.client.get(self.endpoint, query_param) - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), []) + self.assertEqual(response.status_code, 400) + self.assertEqual( + response.json()["invalidParams"][0]["code"], "required" + ) def test_get_dates_returns_403_when_no_active_sessions(self): self._clear_session() @@ -242,8 +244,10 @@ def test_get_times_returns_400_when_missing_query_params(self): ]: with self.subTest(query_param=query_param): response = self.client.get(self.endpoint, query_param) - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), []) + self.assertEqual(response.status_code, 400) + self.assertEqual( + response.json()["invalidParams"][0]["code"], "required" + ) def test_get_times_returns_403_when_no_active_sessions(self): self._clear_session() diff --git a/src/openforms/appointments/contrib/jcc/tests/test_plugin.py b/src/openforms/appointments/contrib/jcc/tests/test_plugin.py index 18b91eb21a..5153b45b81 100644 --- a/src/openforms/appointments/contrib/jcc/tests/test_plugin.py +++ b/src/openforms/appointments/contrib/jcc/tests/test_plugin.py @@ -15,7 +15,7 @@ from openforms.utils.xml import fromstring from soap.tests.factories import SoapServiceFactory -from ....base import AppointmentDetails, Customer, CustomerDetails, Location, Product +from ....base import AppointmentDetails, CustomerDetails, Location, Product from ....core import book from ....exceptions import ( AppointmentCreateFailed, @@ -251,7 +251,12 @@ def test_get_required_customer_fields(self, m): def test_create_appointment(self, m): product = Product(identifier="1", code="PASAAN", name="Paspoort aanvraag") location = Location(identifier="1", name="Maykin Media") - client = Customer(last_name="Doe", birthdate=date(1980, 1, 1)) + client = CustomerDetails( + details={ + CustomerFields.last_name: "Doe", + CustomerFields.birthday: "1980-01-01", + } + ) m.post( "http://example.com/soap11", @@ -665,7 +670,12 @@ def test_get_times_unexpected_exception(self, m): def test_create_appointment_failure(self, m): product = Product(identifier="1", code="PASAAN", name="Paspoort aanvraag") location = Location(identifier="1", name="Maykin Media") - client = Customer(last_name="Doe", birthdate=date(1980, 1, 1)) + client = CustomerDetails( + details={ + CustomerFields.last_name: "Doe", + CustomerFields.birthday: "1980-01-01", + } + ) start_at = datetime(2021, 8, 23, 6, 0, 0).replace(tzinfo=timezone.utc) m.post( "http://example.com/soap11", @@ -681,7 +691,12 @@ def test_create_appointment_failure(self, m): def test_create_appointment_unexpected_exception(self, m): product = Product(identifier="1", code="PASAAN", name="Paspoort aanvraag") location = Location(identifier="1", name="Maykin Media") - client = Customer(last_name="Doe", birthdate=date(1980, 1, 1)) + client = CustomerDetails( + details={ + CustomerFields.last_name: "Doe", + CustomerFields.birthday: "1980-01-01", + } + ) start_at = datetime(2021, 8, 23, 6, 0, 0).replace(tzinfo=timezone.utc) m.post(requests_mock.ANY, exc=IOError("tubes are closed")) diff --git a/src/openforms/appointments/contrib/qmatic/plugin.py b/src/openforms/appointments/contrib/qmatic/plugin.py index d49b3aa206..e47423360c 100644 --- a/src/openforms/appointments/contrib/qmatic/plugin.py +++ b/src/openforms/appointments/contrib/qmatic/plugin.py @@ -1,6 +1,5 @@ import json import logging -import warnings from collections import Counter from contextlib import contextmanager from datetime import date, datetime @@ -19,14 +18,7 @@ from openforms.formio.typing import Component from openforms.plugins.exceptions import InvalidPluginConfiguration -from ...base import ( - AppointmentDetails, - BasePlugin, - Customer, - CustomerDetails, - Location, - Product, -) +from ...base import AppointmentDetails, BasePlugin, CustomerDetails, Location, Product from ...exceptions import ( AppointmentCreateFailed, AppointmentDeleteFailed, @@ -73,25 +65,6 @@ def wrapper(*args, **kwargs) -> T: return decorator -def normalize_customer_details(client: _CustomerDetails | Customer) -> _CustomerDetails: - # Phasing out Customer in favour of CustomerDetails, so convert to the new type - if isinstance(client, Customer): - warnings.warn( - "Fixed customer fields via the Customer class are deprecated, use " - "dynamic CustomerDetails with 'get_required_customer_fields' instead.", - DeprecationWarning, - ) - client = _CustomerDetails( - details={ - CustomerFields.last_name: client.last_name, - CustomerFields.birthday: client.birthdate.isoformat(), - CustomerFields.first_name: client.initials or "", - CustomerFields.phone_number: client.phonenumber or "", - } - ) - return client - - @register("qmatic") class QmaticAppointment(BasePlugin[CustomerFields]): """ @@ -314,11 +287,10 @@ def create_appointment( products: list[Product], location: Location, start_at: datetime, - client: _CustomerDetails | Customer, + client: _CustomerDetails, remarks: str = "", ) -> str: assert products, "Can't book for empty products" - customer = normalize_customer_details(client) product_names = ", ".join(sorted({product.name for product in products})) unique_product_ids, num_customers = self._count_products(products) @@ -328,7 +300,7 @@ def create_appointment( # we repeat the same customer information for every customer, as we currently # don't support getting the contact details for each individual customer "customers": [ - {choice: value for choice, value in customer.details.items() if value} + {choice: value for choice, value in client.details.items() if value} ] * num_customers, "services": [{"publicId": product_id} for product_id in unique_product_ids], diff --git a/src/openforms/appointments/contrib/qmatic/tests/test_api_endpoints.py b/src/openforms/appointments/contrib/qmatic/tests/test_api_endpoints.py index 25494c48f6..74577a85ac 100644 --- a/src/openforms/appointments/contrib/qmatic/tests/test_api_endpoints.py +++ b/src/openforms/appointments/contrib/qmatic/tests/test_api_endpoints.py @@ -84,8 +84,8 @@ def test_get_locations_returns_all_locations_for_a_product(self, m): def test_get_locations_returns_400_when_no_product_id_is_given(self): response = self.client.get(self.endpoint) - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), []) + self.assertEqual(response.status_code, 400) + self.assertEqual(response.json()["invalidParams"][0]["code"], "required") def test_get_locations_returns_403_when_no_active_sessions(self): self._clear_session() @@ -156,8 +156,10 @@ def test_get_dates_returns_400_when_missing_query_params(self): for query_param in [{}, {"product_id": 79}, {"location_id": 1}]: with self.subTest(query_param=query_param): response = self.client.get(self.endpoint, query_param) - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), []) + self.assertEqual(response.status_code, 400) + self.assertEqual( + response.json()["invalidParams"][0]["code"], "required" + ) def test_get_dates_returns_403_when_no_active_sessions(self): self._clear_session() @@ -232,8 +234,10 @@ def test_get_times_returns_400_when_missing_query_params(self): ]: with self.subTest(query_param=query_param): response = self.client.get(self.endpoint, query_param) - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), []) + self.assertEqual(response.status_code, 400) + self.assertEqual( + response.json()["invalidParams"][0]["code"], "required" + ) def test_get_times_returns_403_when_no_active_sessions(self): self._clear_session() diff --git a/src/openforms/appointments/contrib/qmatic/tests/test_plugin.py b/src/openforms/appointments/contrib/qmatic/tests/test_plugin.py index ec98262675..5e94a00b21 100644 --- a/src/openforms/appointments/contrib/qmatic/tests/test_plugin.py +++ b/src/openforms/appointments/contrib/qmatic/tests/test_plugin.py @@ -15,7 +15,7 @@ ) from openforms.utils.tests.logging import disable_logging -from ....base import AppointmentDetails, Customer, Location, Product +from ....base import AppointmentDetails, CustomerDetails, Location, Product from ....core import book from ....exceptions import ( AppointmentCreateFailed, @@ -247,7 +247,12 @@ def test_create_appointment(self, m): location = Location( identifier="f364d92b7fa07a48c4ecc862de30c47", name="Branch 1" ) - client = Customer(last_name="Doe", birthdate=date(1980, 1, 1)) + client = CustomerDetails( + details={ + CustomerFields.last_name: "Doe", + CustomerFields.birthday: "1980-01-01", + } + ) day = datetime(2016, 12, 6, 9, 0, 0) m.get( @@ -529,7 +534,12 @@ def test_get_times_unexpected_exception(self, m): def test_create_appointment_failure(self, m): product = Product(identifier="1", code="PASAAN", name="Paspoort aanvraag") location = Location(identifier="1", name="Maykin Media") - client = Customer(last_name="Doe", birthdate=date(1980, 1, 1)) + client = CustomerDetails( + details={ + CustomerFields.last_name: "Doe", + CustomerFields.birthday: "1980-01-01", + } + ) start_at = timezone.make_aware(datetime(2021, 8, 23, 6, 0, 0)) m.get( f"{self.api_root}v1/branches/1", @@ -576,7 +586,12 @@ def test_create_appointment_failure(self, m): def test_create_appointment_unexpected_exception(self, m): product = Product(identifier="1", code="PASAAN", name="Paspoort aanvraag") location = Location(identifier="1", name="Maykin Media") - client = Customer(last_name="Doe", birthdate=date(1980, 1, 1)) + client = CustomerDetails( + details={ + CustomerFields.last_name: "Doe", + CustomerFields.birthday: "1980-01-01", + } + ) start_at = datetime(2021, 8, 23, 6, 0, 0).replace(tzinfo=timezone.utc) m.get( f"{self.api_root}v1/branches/1", diff --git a/src/openforms/appointments/core.py b/src/openforms/appointments/core.py index 08a6811132..4767564d2f 100644 --- a/src/openforms/appointments/core.py +++ b/src/openforms/appointments/core.py @@ -23,7 +23,6 @@ ) from .models import Appointment, AppointmentInfo from .registry import register -from .utils import cancel_previous_submission_appointment __all__ = ["book_for_submission"] @@ -116,5 +115,4 @@ def book_for_submission(submission: Submission) -> str: logevent.appointment_register_failure(appointment_info, plugin, e) raise AppointmentRegistrationFailed("Unable to create appointment") from e - cancel_previous_submission_appointment(submission) return appointment_id diff --git a/src/openforms/appointments/management/commands/appointment.py b/src/openforms/appointments/management/commands/appointment.py index adab85dbd4..c602f50fbb 100644 --- a/src/openforms/appointments/management/commands/appointment.py +++ b/src/openforms/appointments/management/commands/appointment.py @@ -7,7 +7,7 @@ from openforms.submissions.models import Submission -from ...base import Customer +from ...base import CustomerDetails from ...core import book_for_submission from ...registry import register from ...utils import get_plugin @@ -122,7 +122,9 @@ def create_booking(self): # Customer - customer = Customer(last_name="Doe", birthdate=date(1970, 1, 1)) + customer = CustomerDetails( + details={"lastName": "Doe", "birthdate": date(1970, 1, 1)} + ) # Book diff --git a/src/openforms/appointments/tasks.py b/src/openforms/appointments/tasks.py index 4411a750ed..537bf6cb6f 100644 --- a/src/openforms/appointments/tasks.py +++ b/src/openforms/appointments/tasks.py @@ -1,5 +1,4 @@ import logging -import warnings from celery_once import QueueOnce @@ -8,8 +7,6 @@ from .core import book_for_submission from .exceptions import AppointmentRegistrationFailed, NoAppointmentForm -from .models import AppointmentInfo -from .utils import book_appointment_for_submission __all__ = ["maybe_register_appointment"] @@ -28,30 +25,13 @@ def maybe_register_appointment(submission_id: int) -> None | str: If the submission is for a form which is configured to create appointments, ensure that the appointment is registered in the configured backend. - This can either not be needed, be successful or fail. Either way, the result should + This can either be successful or fail. Either way, the result should be stored in the database. If appointment registration fails, this feedback should find its way back to the end-user. """ - warnings.warn( - "This task is deprecated because of the new appointment flow.", - PendingDeprecationWarning, - ) - logger.info("Registering appointment for submission %d (if needed!)", submission_id) + logger.info("Registering appointment for submission %d", submission_id) submission = Submission.objects.select_related("form").get(id=submission_id) - try: - appointment_id = submission.appointment_info.appointment_id - except AppointmentInfo.DoesNotExist: - pass - else: - # idempotency - do not register a new appointment if there already is one. - if appointment_id: - logger.info( - "Submission %s already has an appointment ID, aborting.", submission.pk - ) - return - - # Try the new appointments implementation first try: return book_for_submission(submission=submission) except NoAppointmentForm: @@ -63,15 +43,3 @@ def maybe_register_appointment(submission_id: int) -> None | str: extra={"submission": submission_id}, ) raise - - # otherwise, fall back to the old form - logger.info("Attempting old appointment booking for submission %r", submission_id) - try: - book_appointment_for_submission(submission) - except AppointmentRegistrationFailed as exc: - logger.info( - "Appointment registration failed, aborting workflow.", - exc_info=exc, - extra={"submission": submission_id}, - ) - raise diff --git a/src/openforms/appointments/tests/test_admin.py b/src/openforms/appointments/tests/test_admin.py index b01a871218..62fc10a9cd 100644 --- a/src/openforms/appointments/tests/test_admin.py +++ b/src/openforms/appointments/tests/test_admin.py @@ -52,7 +52,7 @@ class TestPlugin(DemoAppointment): @disable_admin_mfa() class AppointmentInfoAdminTests(WebTest): @freeze_time("2021-11-26T17:00:00+01:00") - def test_cancel_and_change_links_only_for_superuser(self): + def test_cancel_link_only_for_superuser(self): normal, staff = [ UserFactory.create(user_permissions=["view_appointmentinfo"]), StaffUserFactory.create(user_permissions=["view_appointmentinfo"]), @@ -84,34 +84,7 @@ def test_cancel_and_change_links_only_for_superuser(self): self.assertFalse(object_actions_col) @freeze_time("2021-11-26T17:00:00+01:00") - def test_cancel_and_change_links_legacy(self): - user = SuperUserFactory.create() - # appointment in the past - AppointmentInfoFactory.create( - registration_ok=True, start_time="2021-11-01T17:00:00+01:00" - ) - # appointment in the future - AppointmentInfoFactory.create( - registration_ok=True, start_time="2021-11-30T17:00:00+01:00" - ) - - changelist = self.app.get( - reverse("admin:appointments_appointmentinfo_changelist"), user=user - ) - - self.assertEqual(changelist.status_code, 200) - object_actions_col = changelist.pyquery(".field-get_object_actions") - - # future appointment - app1_links = object_actions_col.eq(0).find("a") - self.assertEqual(len(app1_links), 2) - - # past appointment - app2_links = object_actions_col.eq(1).find("a") - self.assertEqual(len(app2_links), 0) - - @freeze_time("2021-11-26T17:00:00+01:00") - def test_cancel_and_change_links(self): + def test_cancel_link(self): user = SuperUserFactory.create() # appointment in the past diff --git a/src/openforms/appointments/tests/test_api_products.py b/src/openforms/appointments/tests/test_api_products.py index 9863111f99..0f0eb05a83 100644 --- a/src/openforms/appointments/tests/test_api_products.py +++ b/src/openforms/appointments/tests/test_api_products.py @@ -65,6 +65,5 @@ def test_list_products_with_existing_product_invalid_query_param(self): response = self.client.get(self.endpoint, {"product_id": [""]}) - # XXX in 3.0, this will become HTTP 400 - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), []) + self.assertEqual(response.status_code, 400) + self.assertEqual(response.json()["invalidParams"][0]["code"], "blank") diff --git a/src/openforms/appointments/tests/test_base.py b/src/openforms/appointments/tests/test_base.py index c3d208250a..bbb069c61f 100644 --- a/src/openforms/appointments/tests/test_base.py +++ b/src/openforms/appointments/tests/test_base.py @@ -32,20 +32,3 @@ def test_get_cancel_link(self): ) cancel_url = f"https://example.com{cancel_path}" self.assertEqual(cancel_url, result) - - @override_settings(BASE_URL="https://example.com/") - def test_get_change_link(self): - submission = SubmissionFactory.create(completed=True) - AppointmentInfoFactory.create(submission=submission, registration_ok=True) - - result = self.plugin.get_change_link(submission) - - change_path = reverse( - "appointments:appointments-verify-change-appointment-link", - kwargs={ - "token": submission_appointment_token_generator.make_token(submission), - "submission_uuid": submission.uuid, - }, - ) - change_url = f"https://example.com{change_path}" - self.assertEqual(change_url, result) diff --git a/src/openforms/appointments/tests/test_booking_appointment.py b/src/openforms/appointments/tests/test_booking_appointment.py index b3c474aacb..c6efb77af4 100644 --- a/src/openforms/appointments/tests/test_booking_appointment.py +++ b/src/openforms/appointments/tests/test_booking_appointment.py @@ -1,6 +1,6 @@ import os import sys -from datetime import datetime, timedelta +from datetime import datetime from unittest.mock import patch from django.test import TestCase @@ -21,11 +21,7 @@ ) from ..models import AppointmentInfo from ..registry import Registry -from .factories import ( - AppointmentFactory, - AppointmentInfoFactory, - AppointmentProductFactory, -) +from .factories import AppointmentFactory, AppointmentProductFactory register = Registry() register("demo")(DemoAppointment) @@ -155,48 +151,3 @@ def test_plugin_invocation(self): ), remarks="", ) - - def test_cancels_previous_appointment(self): - new_completed_on = timezone.now() - timedelta(hours=1) - # new submission, to replace the previous one - submission = SubmissionFactory.create( - form__is_appointment_form=True, - has_previous_submission=True, - previous_submission__completed=True, - previous_submission__completed_on=new_completed_on - timedelta(hours=12), - ) - AppointmentFactory.create(submission=submission, plugin="demo") - # set the data of the previous submission - AppointmentFactory.create( - submission=submission.previous_submission, plugin="demo" - ) - AppointmentInfoFactory.create( - submission=submission.previous_submission, - registration_ok=True, - appointment_id="98765", - ) - - with patch("openforms.appointments.core.register", new=register): - with supress_output(sys.stdout, os.devnull): - book_for_submission(submission) - - info_by_id = { - info.appointment_id: info for info in AppointmentInfo.objects.all() - } - with self.subTest("New submissions/appointment"): - self.assertIn("test 1", info_by_id) - self.assertEqual( - info_by_id["test 1"].status, AppointmentDetailsStatus.success - ) - - with self.subTest("Cancelled submission/appointment"): - self.assertIn("98765", info_by_id) - self.assertEqual( - info_by_id["98765"].status, AppointmentDetailsStatus.cancelled - ) - - with self.subTest("Audit logging"): - log_records = TimelineLogProxy.objects.all() - events = {lr.event for lr in log_records} - self.assertIn("appointment_cancel_start", events) - self.assertIn("appointment_cancel_success", events) diff --git a/src/openforms/appointments/tests/test_confirmation_emails.py b/src/openforms/appointments/tests/test_confirmation_emails.py index bba646d163..be806a825d 100644 --- a/src/openforms/appointments/tests/test_confirmation_emails.py +++ b/src/openforms/appointments/tests/test_confirmation_emails.py @@ -270,7 +270,7 @@ def test_appointment_data_present_in_confirmation_email(self): self.assertTagWithTextIn("td", "Email", message_html) self.assertTagWithTextIn("td", "austin@powers.net", message_html) - def test_cancel_and_change_instructions(self): + def test_cancel_instructions(self): appointment = AppointmentFactory.create( plugin="with-email", submission__language_code="nl", @@ -293,12 +293,9 @@ def test_cancel_and_change_instructions(self): plugin = register["with-email"] cancel_link = plugin.get_cancel_link(appointment.submission) - change_link = plugin.get_change_link(appointment.submission) with self.subTest(type="HTML"): self.assertIn(cancel_link, message_html) - self.assertNotIn(change_link, message_html) with self.subTest(type="plain text"): self.assertIn(cancel_link, message_text) - self.assertNotIn(change_link, message_text) diff --git a/src/openforms/appointments/tests/test_tasks_appointments.py b/src/openforms/appointments/tests/test_tasks_appointments.py deleted file mode 100644 index 60d1e3bb16..0000000000 --- a/src/openforms/appointments/tests/test_tasks_appointments.py +++ /dev/null @@ -1,148 +0,0 @@ -from unittest.mock import patch - -from django.test import TestCase - -from ..exceptions import AppointmentRegistrationFailed -from ..tasks import maybe_register_appointment -from .factories import AppointmentInfoFactory, SubmissionFactory - - -class LegacyAppointmentRegistrationTaskTests(TestCase): - def test_happy_flow(self): - submission = SubmissionFactory.create(completed=True) - - with patch( - "openforms.appointments.tasks.book_appointment_for_submission" - ) as mock_book: - maybe_register_appointment(submission.id) - - mock_book.assert_called_once() - - submission.refresh_from_db() - self.assertFalse(submission.needs_on_completion_retry) - - def test_appointment_registration_fails(self): - submission = SubmissionFactory.create(completed=True) - - with patch( - "openforms.appointments.tasks.book_appointment_for_submission" - ) as mock_book: - mock_book.side_effect = AppointmentRegistrationFailed("Failed") - with self.assertRaises(AppointmentRegistrationFailed): - maybe_register_appointment(submission.id) - - mock_book.assert_called_once() - - submission.refresh_from_db() - # NO automatic retry - user gets feedback and needs to correct/retry - self.assertFalse(submission.needs_on_completion_retry) - - def test_no_appointment_registered_yet(self): - submission = SubmissionFactory.create(completed=True) - - with patch( - "openforms.appointments.tasks.book_appointment_for_submission" - ) as mock_book: - maybe_register_appointment(submission.id) - - mock_book.assert_called_once_with(submission) - - def test_appointment_was_already_registered(self): - appointment_info = AppointmentInfoFactory.create( - submission__completed=True, - registration_ok=True, - ) - - with patch( - "openforms.appointments.tasks.book_appointment_for_submission" - ) as mock_book: - maybe_register_appointment(appointment_info.submission.id) - - mock_book.assert_not_called() - - def test_previous_registration_failed(self): - appointment_info = AppointmentInfoFactory.create( - submission__completed=True, - registration_failed=True, - ) - - with patch( - "openforms.appointments.tasks.book_appointment_for_submission" - ) as mock_book: - maybe_register_appointment(appointment_info.submission.id) - - mock_book.assert_called_once_with(appointment_info.submission) - - -class AppointmentRegistrationTaskTests(TestCase): - def test_happy_flow(self): - submission = SubmissionFactory.create( - completed=True, form__is_appointment_form=True - ) - - with patch("openforms.appointments.tasks.book_for_submission") as mock_book: - maybe_register_appointment(submission.id) - - mock_book.assert_called_once() - - submission.refresh_from_db() - self.assertFalse(submission.needs_on_completion_retry) - - def test_form_not_marked_as_appointment_form(self): - submission = SubmissionFactory.create( - completed=True, form__is_appointment_form=False - ) - with patch( - "openforms.appointments.tasks.book_appointment_for_submission" - ) as mock_legacy_book: - maybe_register_appointment(submission.id) - - mock_legacy_book.assert_called_once_with(submission) - - def test_appointment_registration_fails(self): - submission = SubmissionFactory.create( - completed=True, form__is_appointment_form=True - ) - - with patch("openforms.appointments.tasks.book_for_submission") as mock_book: - mock_book.side_effect = AppointmentRegistrationFailed("Failed") - with self.assertRaises(AppointmentRegistrationFailed): - maybe_register_appointment(submission.id) - - mock_book.assert_called_once() - - submission.refresh_from_db() - # NO automatic retry - user gets feedback and needs to correct/retry - self.assertFalse(submission.needs_on_completion_retry) - - def test_no_appointment_registered_yet(self): - submission = SubmissionFactory.create( - completed=True, form__is_appointment_form=True - ) - - with patch("openforms.appointments.tasks.book_for_submission") as mock_book: - maybe_register_appointment(submission.id) - - mock_book.assert_called_once_with(submission=submission) - - def test_appointment_was_already_registered(self): - appointment_info = AppointmentInfoFactory.create( - submission__completed=True, - registration_ok=True, - ) - - with patch("openforms.appointments.tasks.book_for_submission") as mock_book: - maybe_register_appointment(appointment_info.submission.id) - - mock_book.assert_not_called() - - def test_previous_registration_failed(self): - appointment_info = AppointmentInfoFactory.create( - submission__completed=True, - registration_failed=True, - ) - - with patch("openforms.appointments.tasks.book_for_submission") as mock_book: - maybe_register_appointment(appointment_info.submission.id) - - mock_book.assert_called_once_with(submission=appointment_info.submission) diff --git a/src/openforms/appointments/tests/test_utils.py b/src/openforms/appointments/tests/test_utils.py index 775c8213b2..478482b410 100644 --- a/src/openforms/appointments/tests/test_utils.py +++ b/src/openforms/appointments/tests/test_utils.py @@ -1,605 +1,6 @@ -from datetime import timedelta - from django.test import TestCase -from django.utils import timezone -from django.utils.translation import gettext as _ - -import requests_mock -from freezegun import freeze_time - -from openforms.forms.tests.factories import ( - FormDefinitionFactory, - FormFactory, - FormStepFactory, -) -from openforms.logging.models import TimelineLogProxy -from openforms.submissions.tests.factories import ( - SubmissionFactory, - SubmissionStepFactory, -) - -from ..constants import AppointmentDetailsStatus -from ..contrib.jcc.tests.test_plugin import mock_response -from ..exceptions import AppointmentCreateFailed, AppointmentRegistrationFailed -from ..models import AppointmentInfo -from ..utils import ( - book_appointment_for_submission, - cancel_previous_submission_appointment, - create_base64_qrcode, - find_first_appointment_step, - get_formatted_phone_number, -) -from .factories import AppointmentInfoFactory -from .utils import setup_jcc - - -# Deprecated/legacy tests -class BookAppointmentForSubmissionTest(TestCase): - @classmethod - def setUpTestData(cls): - super().setUpTestData() - - setup_jcc() - - def test_creating_appointment_with_no_appointment_information_does_nothing(self): - submission = SubmissionFactory.create() - book_appointment_for_submission(submission) - submission.refresh_from_db() - self.assertFalse(AppointmentInfo.objects.exists()) - self.assertFalse(TimelineLogProxy.objects.exists()) - - def test_appointment_step_not_applicable(self): - form = FormFactory.create() - step = FormStepFactory.create( - form=form, - form_definition__configuration={ - "display": "form", - "components": [ - { - "key": "product", - "type": "textfield", - "appointments": {"showProducts": True}, - "label": "Product", - }, - { - "key": "location", - "type": "textfield", - "appointments": {"showLocations": True}, - "label": "Location", - }, - { - "key": "time", - "type": "textfield", - "appointments": {"showTimes": True}, - "label": "Time", - }, - ], - }, - ) - submission = SubmissionFactory.create(form=form) - SubmissionStepFactory.create( - submission=submission, - data={}, - form_step=step, - ) - - book_appointment_for_submission(submission) - self.assertFalse(AppointmentInfo.objects.exists()) - self.assertFalse(TimelineLogProxy.objects.exists()) - - def test_creating_appointment_with_missing_or_not_filled_in_appointment_information_adds_error_message( - self, - ): - form = FormFactory.create() - form_definition_1 = FormDefinitionFactory.create( - configuration={ - "display": "form", - "components": [ - { - "key": "product", - "type": "textfield", - "appointments": {"showProducts": True}, - "label": "Product", - }, - { - "key": "time", - "type": "textfield", - "appointments": {"showTimes": True}, - "label": "Time", - }, - ], - } - ) - form_definition_2 = FormDefinitionFactory.create( - configuration={ - "display": "form", - "components": [ - { - "key": "lastName", - "type": "textfield", - "appointments": {"lastName": True}, - "label": "Last Name", - }, - { - "key": "birthDate", - "type": "textfield", - "appointments": {"birthDate": True}, - "label": "Date of Birth", - }, - ], - } - ) - form_step_1 = FormStepFactory.create( - form=form, form_definition=form_definition_1 - ) - form_step_2 = FormStepFactory.create( - form=form, form_definition=form_definition_2 - ) - submission = SubmissionFactory.create(form=form) - SubmissionStepFactory.create( - submission=submission, - data={ - "product": {"identifier": "79", "name": "Paspoort"}, - "time": "2021-08-25T17:00:00", - }, - form_step=form_step_1, - ) - SubmissionStepFactory.create( - submission=submission, - data={ - "lastName": "Maykin", - "birthDate": "", - }, - form_step=form_step_2, - ) - - with self.assertRaises(AppointmentRegistrationFailed): - book_appointment_for_submission(submission) - - info = AppointmentInfo.objects.filter( - submission=submission, - status=AppointmentDetailsStatus.missing_info, - ).get() - - self.assertEqual( - info.error_information, - _("The following appointment fields should be filled out: {fields}").format( - fields="Date of Birth, locationIDAndName" - ), - ) - self.assertEqual( - TimelineLogProxy.objects.filter( - template="logging/events/appointment_register_skip.txt" - ).count(), - 1, - ) - - @requests_mock.Mocker() - def test_creating_appointment_properly_creates_appointment_and_adds_appointment_information( - self, m - ): - form = FormFactory.create() - form_definition_1 = FormDefinitionFactory.create( - configuration={ - "display": "form", - "components": [ - { - "key": "product", - "type": "textfield", - "appointments": {"showProducts": True}, - "label": "Product", - }, - { - "key": "location", - "type": "textfield", - "appointments": {"showLocations": True}, - "label": "Location", - }, - { - "key": "time", - "type": "textfield", - "appointments": {"showTimes": True}, - "label": "Time", - }, - ], - } - ) - form_definition_2 = FormDefinitionFactory.create( - configuration={ - "display": "form", - "components": [ - { - "key": "lastName", - "type": "textfield", - "appointments": {"lastName": True}, - "label": "Last Name", - }, - { - "key": "birthDate", - "type": "textfield", - "appointments": {"birthDate": True}, - "label": "Date of Birth", - }, - ], - } - ) - form_step_1 = FormStepFactory.create( - form=form, form_definition=form_definition_1 - ) - form_step_2 = FormStepFactory.create( - form=form, form_definition=form_definition_2 - ) - submission = SubmissionFactory.create(form=form) - SubmissionStepFactory.create( - submission=submission, - data={ - "product": {"identifier": "79", "name": "Paspoort"}, - "location": {"identifier": "1", "name": "Amsterdam"}, - "time": "2021-08-25T17:00:00+02:00", - }, - form_step=form_step_1, - ) - SubmissionStepFactory.create( - submission=submission, - data={ - "lastName": "Maykin", - "birthDate": "1990-08-01", - }, - form_step=form_step_2, - ) - - m.post( - "http://example.com/soap11", - text=mock_response("bookGovAppointmentResponse.xml"), - ) - - book_appointment_for_submission(submission) - self.assertTrue( - AppointmentInfo.objects.filter( - appointment_id="1234567890", - submission=submission, - status=AppointmentDetailsStatus.success, - ).exists() - ) - self.assertEqual( - TimelineLogProxy.objects.filter( - template="logging/events/appointment_register_start.txt" - ).count(), - 1, - ) - self.assertEqual( - TimelineLogProxy.objects.filter( - template="logging/events/appointment_register_success.txt" - ).count(), - 1, - ) - - @freeze_time("2021-08-21T18:00:00+02:00") - @requests_mock.Mocker() - def test_creating_appointment_deletes_previous_appointment_when_one_exists(self, m): - new_completed_on = timezone.now() - timedelta(hours=1) - base_data = { - "product": {"identifier": "79", "name": "Paspoort"}, - "location": {"identifier": "1", "name": "Amsterdam"}, - "lastName": "Maykin", - "birthDate": "1990-08-01", - } - submission = SubmissionFactory.from_components( - completed=True, - completed_on=new_completed_on, - components_list=[ - { - "key": "product", - "type": "textfield", - "appointments": {"showProducts": True}, - "label": "Product", - }, - { - "key": "location", - "type": "textfield", - "appointments": {"showLocations": True}, - "label": "Location", - }, - { - "key": "time", - "type": "textfield", - "appointments": {"showTimes": True}, - "label": "Time", - }, - { - "key": "lastName", - "type": "textfield", - "appointments": {"lastName": True}, - "label": "Last Name", - }, - { - "key": "birthDate", - "type": "textfield", - "appointments": {"birthDate": True}, - "label": "Date of Birth", - }, - ], - submitted_data={**base_data, "time": "2021-08-26T17:00:00+02:00"}, - has_previous_submission=True, - previous_submission__completed=True, - previous_submission__completed_on=new_completed_on - timedelta(hours=12), - ) - - # set the data of the previous submission - appointment_info = AppointmentInfoFactory.create( - submission=submission.previous_submission, - registration_ok=True, - appointment_id="98765", - ) - SubmissionStepFactory.create( - submission=submission.previous_submission, - form_step=submission.form.formstep_set.get(), - data={**base_data, "time": "2021-08-25T17:00:00+02:00"}, - ) - m.post( - "http://example.com/soap11", - [ - {"text": mock_response("bookGovAppointmentResponse.xml")}, - {"text": mock_response("deleteGovAppointmentResponse.xml")}, - ], - ) - - book_appointment_for_submission(submission) - - self.assertTrue( - AppointmentInfo.objects.filter( - appointment_id="1234567890", - submission=submission, - status=AppointmentDetailsStatus.success, - ).exists() - ) - - appointment_info.refresh_from_db() - self.assertEqual(appointment_info.status, AppointmentDetailsStatus.cancelled) - self.assertEqual( - TimelineLogProxy.objects.filter( - template="logging/events/appointment_register_start.txt" - ).count(), - 1, - ) - self.assertEqual( - TimelineLogProxy.objects.filter( - template="logging/events/appointment_register_success.txt" - ).count(), - 1, - ) - - @requests_mock.Mocker() - def test_failed_creating_appointment_adds_error_message_to_submission(self, m): - form = FormFactory.create() - form_definition_1 = FormDefinitionFactory.create( - configuration={ - "display": "form", - "components": [ - { - "key": "product", - "type": "textfield", - "appointments": {"showProducts": True}, - "label": "Product", - }, - { - "key": "location", - "type": "textfield", - "appointments": {"showLocations": True}, - "label": "Location", - }, - { - "key": "time", - "type": "textfield", - "appointments": {"showTimes": True}, - "label": "Time", - }, - ], - } - ) - form_definition_2 = FormDefinitionFactory.create( - configuration={ - "display": "form", - "components": [ - { - "key": "lastName", - "type": "textfield", - "appointments": {"lastName": True}, - "label": "Last Name", - }, - { - "key": "birthDate", - "type": "textfield", - "appointments": {"birthDate": True}, - "label": "Date of Birth", - }, - ], - } - ) - form_step_1 = FormStepFactory.create( - form=form, form_definition=form_definition_1 - ) - form_step_2 = FormStepFactory.create( - form=form, form_definition=form_definition_2 - ) - submission = SubmissionFactory.create(form=form) - SubmissionStepFactory.create( - submission=submission, - data={ - "product": {"identifier": "79", "name": "Paspoort"}, - "location": {"identifier": "1", "name": "Amsterdam"}, - "time": "2021-08-25T17:00:00+02:00", - }, - form_step=form_step_1, - ) - SubmissionStepFactory.create( - submission=submission, - data={ - "lastName": "Maykin", - "birthDate": "1990-08-01", - }, - form_step=form_step_2, - ) - - m.post( - "http://example.com/soap11", - exc=AppointmentCreateFailed, - ) - - with self.assertRaises(AppointmentRegistrationFailed): - book_appointment_for_submission(submission) - - self.assertTrue( - AppointmentInfo.objects.filter( - submission=submission, - status=AppointmentDetailsStatus.failed, - ).exists() - ) - self.assertEqual( - TimelineLogProxy.objects.filter( - template="logging/events/appointment_register_start.txt" - ).count(), - 1, - ) - self.assertEqual( - TimelineLogProxy.objects.filter( - template="logging/events/appointment_register_failure.txt" - ).count(), - 1, - ) - - @requests_mock.Mocker() - def test_failed_creating_appointment_when_submission_previously_failed(self, m): - form = FormFactory.create() - form_definition_1 = FormDefinitionFactory.create( - configuration={ - "display": "form", - "components": [ - { - "key": "product", - "type": "textfield", - "appointments": {"showProducts": True}, - "label": "Product", - }, - { - "key": "location", - "type": "textfield", - "appointments": {"showLocations": True}, - "label": "Location", - }, - { - "key": "time", - "type": "textfield", - "appointments": {"showTimes": True}, - "label": "Time", - }, - ], - } - ) - form_definition_2 = FormDefinitionFactory.create( - configuration={ - "display": "form", - "components": [ - { - "key": "lastName", - "type": "textfield", - "appointments": {"lastName": True}, - "label": "Last Name", - }, - { - "key": "birthDate", - "type": "textfield", - "appointments": {"birthDate": True}, - "label": "Date of Birth", - }, - ], - } - ) - form_step_1 = FormStepFactory.create( - form=form, form_definition=form_definition_1 - ) - form_step_2 = FormStepFactory.create( - form=form, form_definition=form_definition_2 - ) - submission = SubmissionFactory.create(form=form) - first_appointment_info = AppointmentInfoFactory.create( - status=AppointmentDetailsStatus.failed, - error_information="Failed to make appointment", - submission=submission, - ) - SubmissionStepFactory.create( - submission=submission, - data={ - "product": {"identifier": "79", "name": "Paspoort"}, - "location": {"identifier": "1", "name": "Amsterdam"}, - "time": "2021-08-25T17:00:00+02:00", - }, - form_step=form_step_1, - ) - SubmissionStepFactory.create( - submission=submission, - data={ - "lastName": "Maykin", - "birthDate": "1990-08-01", - }, - form_step=form_step_2, - ) - - m.post( - "http://example.com/soap11", - exc=AppointmentCreateFailed, - ) - - with self.assertRaises(AppointmentRegistrationFailed): - book_appointment_for_submission(submission) - - submission.refresh_from_db() - second_appointment_info = submission.appointment_info - - self.assertNotEqual(first_appointment_info.pk, second_appointment_info.pk) - - self.assertEqual( - TimelineLogProxy.objects.filter( - template="logging/events/appointment_register_start.txt" - ).count(), - 1, - ) - self.assertEqual( - TimelineLogProxy.objects.filter( - template="logging/events/appointment_register_failure.txt" - ).count(), - 1, - ) - - def test_cancelling_previous_appointments_nothing_to_cancel(self): - submission1 = SubmissionFactory.create(has_previous_submission=True) - AppointmentInfoFactory.create( - submission=submission1.previous_submission, - registration_failed=True, - ) - submission2 = SubmissionFactory.create(has_previous_submission=True) - AppointmentInfoFactory.create( - submission=submission2.previous_submission, - registration_ok=True, - appointment_id="", - ) - submissions = [ - ("no previous", SubmissionFactory.create(has_previous_submission=False)), - ( - "no appointment info", - SubmissionFactory.create(has_previous_submission=True), - ), - ("failed registration", submission1), - ("succeeded registration but no id", submission2), - ] - - for label, submission in submissions: - with self.subTest(type=label): - with requests_mock.Mocker() as m: - cancel_previous_submission_appointment(submission) - - self.assertFalse(m.called) +from ..utils import create_base64_qrcode, get_formatted_phone_number class UtilsTests(TestCase): @@ -626,43 +27,6 @@ def test_create_base64_qrcode(self): self.assertEqual(result, expected) - def test_find_first_appointment_step_returns_None_for_no_appointment_data(self): - submission = SubmissionFactory.from_components( - components_list=[ - { - "type": "textfield", - "key": "placeholder", - } - ] - ) - - step = find_first_appointment_step(submission.form) - - self.assertIsNone(step) - - def test_find_first_appointment_step_finds_correct_step(self): - submission = SubmissionFactory.from_components( - components_list=[ - { - "key": "product", - "type": "textfield", - "appointments": {"showProducts": True}, - "label": "Product", - }, - { - "key": "time", - "type": "textfield", - "appointments": {"showTimes": True}, - "label": "Time", - }, - ] - ) - - step = find_first_appointment_step(submission.form) - - self.assertIsNotNone(step) - self.assertEqual(step, submission.submissionstep_set.get().form_step) - class GetFormattedPhoneNumberTest(TestCase): def test_get_formatted_phone_number_returns_expected_format(self): diff --git a/src/openforms/appointments/tests/test_views.py b/src/openforms/appointments/tests/test_views.py index f14b713419..926e5e0d43 100644 --- a/src/openforms/appointments/tests/test_views.py +++ b/src/openforms/appointments/tests/test_views.py @@ -1,6 +1,5 @@ import uuid from datetime import datetime, timezone -from decimal import Decimal from django.test import TestCase from django.urls import reverse @@ -10,20 +9,12 @@ from openforms.authentication.contrib.digid.constants import DIGID_DEFAULT_LOA from openforms.authentication.service import FORM_AUTH_SESSION_KEY, AuthAttribute -from openforms.forms.tests.factories import FormFactory from openforms.frontend.tests import FrontendRedirectMixin -from openforms.logging.models import TimelineLogProxy -from openforms.payments.constants import PaymentStatus -from openforms.payments.tests.factories import SubmissionPaymentFactory from openforms.submissions.constants import SUBMISSIONS_SESSION_KEY -from openforms.submissions.models import Submission -from openforms.submissions.tests.factories import ( - SubmissionFactory, - SubmissionStepFactory, -) +from openforms.submissions.tests.factories import SubmissionFactory from ..tokens import submission_appointment_token_generator -from .factories import AppointmentFactory, AppointmentInfoFactory +from .factories import AppointmentInfoFactory @freeze_time("2021-07-15T21:15:00Z") @@ -338,546 +329,3 @@ def test_invalid_auth_value_raises_exception(self): self.assertEqual(403, response.status_code) self.assertNotIn(SUBMISSIONS_SESSION_KEY, self.client.session) - - -@freeze_time("2021-07-15T21:15:00Z") -class VerifyChangeAppointmentLinkViewTests(FrontendRedirectMixin, TestCase): - def test_good_token_and_submission_redirect_and_add_submission_to_session(self): - submission = SubmissionFactory.from_components( - completed=True, - components_list=[ - { - "key": "product", - "appointments": {"showProducts": True}, - "label": "Product", - }, - { - "key": "time", - "appointments": {"showTimes": True}, - "label": "Time", - }, - ], - submitted_data={ - "product": {"identifier": "79", "name": "Paspoort"}, - "time": "2021-08-25T17:00:00", - }, - form_url="http://maykinmedia.nl/myform/", - bsn="000000000", - ) - form_definition = submission.form.formstep_set.get().form_definition - AppointmentInfoFactory.create( - submission=submission, - registration_ok=True, - start_time=datetime(2021, 7, 21, 12, 00, 00, tzinfo=timezone.utc), - ) - - endpoint = reverse( - "appointments:appointments-verify-change-appointment-link", - kwargs={ - "token": submission_appointment_token_generator.make_token(submission), - "submission_uuid": submission.uuid, - }, - ) - # Add form_auth to session, as the authentication plugin would do it - session = self.client.session - session[FORM_AUTH_SESSION_KEY] = { - "plugin": "digid", - "attribute": AuthAttribute.bsn, - "value": "000000000", - "loa": DIGID_DEFAULT_LOA, - } - session.save() - - # one day after token generation - with freeze_time("2021-07-16T21:15:00Z"): - response = self.client.get(endpoint) - - new_submission = Submission.objects.exclude(id=submission.id).get() - # after initiating change, we expect the bsn to be stored in plain text (again) - self.assertEqual(new_submission.auth_info.value, "000000000") - - self.assertRedirectsToFrontend( - response, - frontend_base_url=submission.form_url, - action="resume", - action_params={ - "step_slug": form_definition.slug, - "submission_uuid": str(new_submission.uuid), - }, - fetch_redirect_response=False, - ) - - # Assert new submission was created - self.assertEqual(Submission.objects.count(), 2) - # Assert old submission not stored in session - self.assertNotIn( - str(submission.uuid), self.client.session[SUBMISSIONS_SESSION_KEY] - ) - # Assert new submission is stored in session - self.assertIn( - str(new_submission.uuid), self.client.session[SUBMISSIONS_SESSION_KEY] - ) - - def test_403_response_with_unfound_submission(self): - endpoint = reverse( - "appointments:appointments-verify-change-appointment-link", - kwargs={ - "token": "irrelevant", - "submission_uuid": uuid.uuid4(), - }, - ) - - response = self.client.get(endpoint) - - self.assertEqual(response.status_code, 403) - - def test_403_response_with_bad_token(self): - submission = SubmissionFactory.create(completed=True) - AppointmentInfoFactory.create( - submission=submission, - registration_ok=True, - start_time=datetime(2021, 7, 21, 12, 00, 00, tzinfo=timezone.utc), - ) - - endpoint = reverse( - "appointments:appointments-verify-change-appointment-link", - kwargs={ - "token": "bad", - "submission_uuid": submission.uuid, - }, - ) - - response = self.client.get(endpoint) - - self.assertEqual(response.status_code, 403) - - def test_token_invalid_after_appointment_time(self): - submission = SubmissionFactory.create(completed=True) - AppointmentInfoFactory.create( - submission=submission, - registration_ok=True, - start_time=datetime(2021, 7, 21, 12, 00, 00, tzinfo=timezone.utc), - ) - - endpoint = reverse( - "appointments:appointments-verify-change-appointment-link", - kwargs={ - "token": submission_appointment_token_generator.make_token(submission), - "submission_uuid": submission.uuid, - }, - ) - - with freeze_time("2021-07-22T12:00:00Z"): - response = self.client.get(endpoint) - - self.assertEqual(response.status_code, 403) - - def test_token_valid_on_same_day_appointment(self): - submission = SubmissionFactory.from_components( - completed=True, - components_list=[ - { - "key": "product", - "appointments": {"showProducts": True}, - "label": "Product", - }, - { - "key": "time", - "appointments": {"showTimes": True}, - "label": "Time", - }, - ], - submitted_data={ - "product": {"identifier": "79", "name": "Paspoort"}, - "time": "2021-08-25T17:00:00", - }, - form_url="http://maykinmedia.nl/myform/", - ) - AppointmentInfoFactory.create( - submission=submission, - registration_ok=True, - start_time=datetime(2021, 7, 21, 12, 00, 00, tzinfo=timezone.utc), - ) - - endpoint = reverse( - "appointments:appointments-verify-change-appointment-link", - kwargs={ - "token": submission_appointment_token_generator.make_token(submission), - "submission_uuid": submission.uuid, - }, - ) - - with freeze_time("2021-07-21T11:59:59Z"): - response = self.client.get(endpoint) - - self.assertEqual(response.status_code, 302) - - def test_redirect_to_first_step_when_appointment_form_definition_can_not_be_found( - self, - ): - form = FormFactory.create() - submission = SubmissionFactory.create( - form=form, completed=True, form_url="http://maykinmedia.nl/myform/" - ) - SubmissionStepFactory.create( - form_step__form=form, - form_step__form_definition__slug="step-1", - form_step__form_definition__configuration={ - "display": "form", - "components": [ - { - "key": "name", - "label": "Name", - "type": "textfield", - }, - ], - }, - submission=submission, - data={ - "Name": "Maykin", - }, - ) - SubmissionStepFactory.create( - submission=submission, - form_step__form=form, - form_step__form_definition__slug="step-2", - form_step__form_definition__configuration={ - "display": "form", - "components": [ - { - "key": "product", - "appointments": {"showProducts": False}, - "label": "Product", - "type": "select", - }, - { - "key": "time", - "appointments": {"showTimes": False}, - "label": "Time", - "type": "select", - }, - ], - }, - data={ - "product": {"identifier": "79", "name": "Paspoort"}, - "time": "2021-08-25T17:00:00", - }, - ) - AppointmentInfoFactory.create( - submission=submission, - registration_ok=True, - start_time=datetime(2021, 7, 21, 12, 00, 00, tzinfo=timezone.utc), - ) - - endpoint = reverse( - "appointments:appointments-verify-change-appointment-link", - kwargs={ - "token": submission_appointment_token_generator.make_token(submission), - "submission_uuid": submission.uuid, - }, - ) - - # one day after token generation - with freeze_time("2021-07-16T21:15:00Z"): - response = self.client.get(endpoint) - - new_submission = Submission.objects.exclude(id=submission.id).get() - - self.assertRedirectsToFrontend( - response, - frontend_base_url=submission.form_url, - action="resume", - action_params={ - "step_slug": "step-1", - "submission_uuid": str(new_submission.uuid), - }, - fetch_redirect_response=False, - ) - - def test_redirects_to_auth_if_form_requires_login(self): - submission = SubmissionFactory.create( - form__generate_minimal_setup=True, - form__formstep__form_definition__login_required=True, - auth_info__plugin="digid", - completed=True, - ) - AppointmentInfoFactory.create( - submission=submission, - registration_ok=True, - start_time=datetime(2021, 7, 21, 12, 00, 00, tzinfo=timezone.utc), - ) - - endpoint = reverse( - "appointments:appointments-verify-change-appointment-link", - kwargs={ - "token": submission_appointment_token_generator.make_token(submission), - "submission_uuid": submission.uuid, - }, - ) - expected_redirect_url = furl( - f"http://testserver/auth/{submission.form.slug}/digid/start" - ) - expected_redirect_url.args["next"] = f"http://testserver{endpoint}" - - response = self.client.get(endpoint) - - self.assertRedirects( - response, expected_redirect_url.url, fetch_redirect_response=False - ) - self.assertNotIn(SUBMISSIONS_SESSION_KEY, self.client.session) - - def test_after_successful_auth_redirects_to_form(self): - submission = SubmissionFactory.create( - form__generate_minimal_setup=True, - form__formstep__form_definition__login_required=True, - form__formstep__form_definition__slug="test-step", - auth_info__plugin="digid", - form_url="http://testserver/myform/", - completed=True, - auth_info__value="123456782", - ) - AppointmentInfoFactory.create( - submission=submission, - registration_ok=True, - start_time=datetime(2021, 7, 21, 12, 00, 00, tzinfo=timezone.utc), - ) - - endpoint = reverse( - "appointments:appointments-verify-change-appointment-link", - kwargs={ - "token": submission_appointment_token_generator.make_token(submission), - "submission_uuid": submission.uuid, - }, - ) - - # Add form_auth to session, as the authentication plugin would do it - session = self.client.session - session[FORM_AUTH_SESSION_KEY] = { - "plugin": "digid", - "attribute": AuthAttribute.bsn, - "value": "123456782", - "loa": DIGID_DEFAULT_LOA, - } - session.save() - - response = self.client.get(endpoint) - new_submission = Submission.objects.filter( - previous_submission=submission - ).first() - - self.assertIsNotNone(new_submission) - - self.assertRedirectsToFrontend( - response, - frontend_base_url=submission.form_url, - action="resume", - action_params={ - "step_slug": "test-step", - "submission_uuid": str(new_submission.uuid), - }, - fetch_redirect_response=False, - ) - - self.assertIn(SUBMISSIONS_SESSION_KEY, self.client.session) - self.assertIn( - str(new_submission.uuid), self.client.session[SUBMISSIONS_SESSION_KEY] - ) - - def test_invalid_auth_plugin_raises_exception(self): - submission = SubmissionFactory.create( - form__generate_minimal_setup=True, - form__formstep__form_definition__login_required=True, - form__formstep__form_definition__slug="test-step", - auth_info__plugin="wrong-plugin", - form_url="http://testserver/myform/", - completed=True, - auth_info__value="123456782", - ) - AppointmentInfoFactory.create( - submission=submission, - registration_ok=True, - start_time=datetime(2021, 7, 21, 12, 00, 00, tzinfo=timezone.utc), - ) - - endpoint = reverse( - "appointments:appointments-verify-change-appointment-link", - kwargs={ - "token": submission_appointment_token_generator.make_token(submission), - "submission_uuid": submission.uuid, - }, - ) - - # Add form_auth to session, as the authentication plugin would do it - session = self.client.session - session[FORM_AUTH_SESSION_KEY] = { - "plugin": "digid", - "attribute": AuthAttribute.bsn, - "value": "123456782", - "loa": DIGID_DEFAULT_LOA, - } - session.save() - - response = self.client.get(endpoint) - - self.assertEqual(403, response.status_code) - self.assertNotIn(SUBMISSIONS_SESSION_KEY, self.client.session) - - def test_invalid_auth_attribute_raises_exception(self): - submission = SubmissionFactory.create( - form__generate_minimal_setup=True, - form__formstep__form_definition__login_required=True, - form__formstep__form_definition__slug="test-step", - auth_info__plugin="digid", - form_url="http://testserver/myform/", - completed=True, - auth_info__value="123456782", - auth_info__attribute=AuthAttribute.kvk, - ) - AppointmentInfoFactory.create( - submission=submission, - registration_ok=True, - start_time=datetime(2021, 7, 21, 12, 00, 00, tzinfo=timezone.utc), - ) - - endpoint = reverse( - "appointments:appointments-verify-change-appointment-link", - kwargs={ - "token": submission_appointment_token_generator.make_token(submission), - "submission_uuid": submission.uuid, - }, - ) - - # Add form_auth to session, as the authentication plugin would do it - session = self.client.session - session[FORM_AUTH_SESSION_KEY] = { - "plugin": "digid", - "attribute": AuthAttribute.bsn, - "value": "123456782", - "loa": DIGID_DEFAULT_LOA, - } - session.save() - - response = self.client.get(endpoint) - - self.assertEqual(403, response.status_code) - self.assertNotIn(SUBMISSIONS_SESSION_KEY, self.client.session) - - def test_invalid_auth_value_raises_exception(self): - submission = SubmissionFactory.create( - form__generate_minimal_setup=True, - form__formstep__form_definition__login_required=True, - form__formstep__form_definition__slug="test-step", - auth_info__plugin="digid", - form_url="http://testserver/myform/", - completed=True, - auth_info__value="wrong-bsn", - ) - AppointmentInfoFactory.create( - submission=submission, - registration_ok=True, - start_time=datetime(2021, 7, 21, 12, 00, 00, tzinfo=timezone.utc), - ) - - endpoint = reverse( - "appointments:appointments-verify-change-appointment-link", - kwargs={ - "token": submission_appointment_token_generator.make_token(submission), - "submission_uuid": submission.uuid, - }, - ) - - # Add form_auth to session, as the authentication plugin would do it - session = self.client.session - session[FORM_AUTH_SESSION_KEY] = { - "plugin": "digid", - "attribute": AuthAttribute.bsn, - "value": "123456782", - "loa": DIGID_DEFAULT_LOA, - } - session.save() - - response = self.client.get(endpoint) - - self.assertEqual(403, response.status_code) - self.assertNotIn(SUBMISSIONS_SESSION_KEY, self.client.session) - - def test_change_appointment_details_after_payment(self): - submission = SubmissionFactory.from_components( - with_public_registration_reference=True, - components_list=[ - { - "key": "product", - "appointments": {"showProducts": True}, - "label": "Product", - }, - { - "key": "time", - "appointments": {"showTimes": True}, - "label": "Time", - }, - ], - submitted_data={ - "product": {"identifier": "79", "name": "Paspoort"}, - "time": "2021-08-25T17:00:00", - }, - form_url="http://maykinmedia.nl/myform/", - form__product__price=Decimal("12.34"), - form__payment_backend="test", - ) - submission_payment = SubmissionPaymentFactory.for_submission( - submission, status=PaymentStatus.completed - ) - AppointmentInfoFactory.create( - submission=submission, - registration_ok=True, - start_time=datetime(2021, 7, 21, 12, 00, 00, tzinfo=timezone.utc), - ) - - endpoint = reverse( - "appointments:appointments-verify-change-appointment-link", - kwargs={ - "token": submission_appointment_token_generator.make_token(submission), - "submission_uuid": submission.uuid, - }, - ) - - self.assertTrue(submission.payment_required) - self.assertTrue(submission.payment_user_has_paid) - - # one day after token generation - with freeze_time("2021-07-16T21:15:00Z"): - self.client.get(endpoint) - - new_submission = Submission.objects.exclude(id=submission.id).get() - submission_payment.refresh_from_db() - - self.assertTrue(new_submission.payment_required) - self.assertTrue(new_submission.payment_user_has_paid) - self.assertEqual(submission_payment.submission, new_submission) - self.assertEqual( - TimelineLogProxy.objects.filter( - template="logging/events/transfer_payment_to_submission_copy.txt" - ).count(), - 1, - ) - - def test_change_new_style_appointment(self): - appointment = AppointmentFactory.create( - appointment_info__registration_ok=True, - appointment_info__start_time=datetime( - 2021, 7, 21, 12, 00, 00, tzinfo=timezone.utc - ), - ) - submission = appointment.submission - url = reverse( - "appointments:appointments-verify-change-appointment-link", - kwargs={ - "token": submission_appointment_token_generator.make_token(submission), - "submission_uuid": submission.uuid, - }, - ) - - # one day after token generation - with freeze_time("2021-07-16T21:15:00Z"): - with self.assertRaises(RuntimeError): - self.client.get(url) diff --git a/src/openforms/appointments/urls.py b/src/openforms/appointments/urls.py index 47a1fcc858..64cb7e79b4 100644 --- a/src/openforms/appointments/urls.py +++ b/src/openforms/appointments/urls.py @@ -1,6 +1,6 @@ from django.urls import path -from .views import VerifyCancelAppointmentLinkView, VerifyChangeAppointmentLinkView +from .views import VerifyCancelAppointmentLinkView app_name = "appointments" @@ -10,9 +10,4 @@ VerifyCancelAppointmentLinkView.as_view(), name="appointments-verify-cancel-appointment-link", ), - path( - "//verify-change", - VerifyChangeAppointmentLinkView.as_view(), - name="appointments-verify-change-appointment-link", - ), ] diff --git a/src/openforms/appointments/utils.py b/src/openforms/appointments/utils.py index 2d9213dfab..515a46c2db 100644 --- a/src/openforms/appointments/utils.py +++ b/src/openforms/appointments/utils.py @@ -2,25 +2,15 @@ import io import logging import re -import warnings -from datetime import datetime - -from django.utils.translation import gettext_lazy as _ import elasticapm import qrcode -from openforms.forms.models import Form, FormStep from openforms.logging import logevent from openforms.submissions.models import Submission -from .base import BasePlugin, Customer, Location, Product -from .constants import AppointmentDetailsStatus -from .exceptions import ( - AppointmentCreateFailed, - AppointmentDeleteFailed, - AppointmentRegistrationFailed, -) +from .base import BasePlugin +from .exceptions import AppointmentDeleteFailed from .models import Appointment, AppointmentInfo, AppointmentsConfig from .registry import register @@ -36,18 +26,6 @@ def get_plugin(plugin: str = "") -> BasePlugin: return register[plugin] -def get_missing_fields_labels( - appointment_data: dict, missing_fields_keys: list[str] -) -> list[str]: - labels = [] - for key in missing_fields_keys: - if label := appointment_data.get(key, {}).get("label"): - labels.append(label) - else: - labels.append(key) - return sorted(labels) - - def get_formatted_phone_number(phone_number: str | None) -> str | None: """ Remove any character that isn't numeric or a space, +, or - character @@ -61,168 +39,6 @@ def get_formatted_phone_number(phone_number: str | None) -> str | None: return phone_number[:16] -@elasticapm.capture_span(span_type="app.appointments.book") -def book_appointment_for_submission(submission: Submission) -> None: - warnings.warn( - "Old-style appointments are deprecated, please update the form to use " - "the reworked appointments.", - DeprecationWarning, - ) - try: - # Delete the previous appointment info if there is one since - # since a new one will be created - # This function will be called multiple times on a failure so - # this is the case a previous appointment_info may exist - submission.appointment_info.delete() - except AppointmentInfo.DoesNotExist: - pass - - appointment_data = submission.get_merged_appointment_data() - - expected_information = [ - "productIDAndName", - "locationIDAndName", - "appStartTime", - "clientLastName", - "clientDateOfBirth", - ] - - absent_or_empty_information = [] - - for key in expected_information: - # there is a non-empty value, continue - this is good - if appointment_data.get(key, {}).get("value"): - continue - absent_or_empty_information.append(key) - - # Submission was never intended to make an appointment so just return - if set(absent_or_empty_information) == set(expected_information): - return - - # Partially filled out form (or appointment fields are present in the form and not - # filled at all). Note that the "contract" states an exception gets raised here - # which aborts the celery chain execution so that the end-user can be shown the - # error information. - if absent_or_empty_information: - # Incomplete information to make an appointment - logevent.appointment_register_skip(submission) - missing_fields_labels = get_missing_fields_labels( - appointment_data, absent_or_empty_information - ) - error_information = _( - "The following appointment fields should be filled out: {fields}" - ).format(fields=", ".join(missing_fields_labels)) - AppointmentInfo.objects.create( - status=AppointmentDetailsStatus.missing_info, - error_information=error_information, - submission=submission, - ) - raise AppointmentRegistrationFailed( - "No registration attempted because of incomplete information. " - ) - - product = Product( - identifier=appointment_data["productIDAndName"]["value"]["identifier"], - name=appointment_data["productIDAndName"]["value"]["name"], - ) - location = Location( - identifier=appointment_data["locationIDAndName"]["value"]["identifier"], - name=appointment_data["locationIDAndName"]["value"]["name"], - ) - appointment_client = Customer( - last_name=appointment_data["clientLastName"]["value"], - birthdate=datetime.strptime( - appointment_data["clientDateOfBirth"]["value"], "%Y-%m-%d" - ).date(), - phonenumber=get_formatted_phone_number( - appointment_data.get("clientPhoneNumber", {}).get("value") - ), - ) - start_at = datetime.strptime( - appointment_data["appStartTime"]["value"], "%Y-%m-%dT%H:%M:%S%z" - ) - - plugin = get_plugin() - try: - logevent.appointment_register_start(submission, plugin) - appointment_id = plugin.create_appointment( - [product], location, start_at, appointment_client - ) - appointment_info = AppointmentInfo.objects.create( - status=AppointmentDetailsStatus.success, - appointment_id=appointment_id, - submission=submission, - start_time=start_at, - ) - logevent.appointment_register_success(appointment_info, plugin) - except AppointmentCreateFailed as e: - logger.error("Appointment creation failed", exc_info=e) - # This is displayed to the end-user! - error_information = _( - "A technical error occurred while we tried to book your appointment. " - "Please verify if all the data is correct or try again later." - ) - appointment_info = AppointmentInfo.objects.create( - status=AppointmentDetailsStatus.failed, - error_information=error_information, - submission=submission, - ) - logevent.appointment_register_failure(appointment_info, plugin, e) - raise AppointmentRegistrationFailed("Unable to create appointment") from e - - cancel_previous_submission_appointment(submission) - - -@elasticapm.capture_span(span_type="app.appointments.cancel") -def cancel_previous_submission_appointment(submission: Submission) -> None: - """ - Given a submission, check if there's a previous appointment to cancel. - """ - if not (previous_submission := submission.previous_submission): - logger.debug( - "Submission %s has no known previous appointment to cancel", submission.uuid - ) - return - - # check if there's anything to cancel at all - try: - appointment_info = previous_submission.appointment_info - except AppointmentInfo.DoesNotExist: - logger.debug( - "Submission %s has no known previous appointment to cancel", submission.uuid - ) - return - - if ( - appointment_info.status != AppointmentDetailsStatus.success - or not appointment_info.appointment_id - ): - logger.debug( - "Submission %s has no known previous appointment to cancel", submission.uuid - ) - return - - # check for new-style appointments - appointment = Appointment.objects.filter(submission=previous_submission).first() - plugin = get_plugin(plugin=appointment.plugin if appointment else "") - - logger.debug( - "Attempting to cancel appointment %s of submission %s", - appointment_info.appointment_id, - submission.uuid, - ) - logevent.appointment_cancel_start(appointment_info, plugin) - - try: - delete_appointment_for_submission(previous_submission, plugin=plugin) - except AppointmentDeleteFailed: - logger.warning( - "Deleting the appointment %s of submission %s failed", - appointment_info.appointment_id, - submission.uuid, - ) - - @elasticapm.capture_span(span_type="app.appointments.delete") def delete_appointment_for_submission(submission: Submission, plugin=None) -> None: """ @@ -260,55 +76,6 @@ def create_base64_qrcode(text): return base64.b64encode(buffer.read()).decode("ascii") -def find_first_appointment_step(form: Form) -> FormStep | None: - """ - Find the first step in a form dealing with appointments. - - This looks at the component configuration for each step and detects if a component - is holding appointment-related meta-information. If no such step is found, ``None`` - is returned. - """ - for form_step in form.formstep_set.select_related("form_definition"): - for component in form_step.iter_components(recursive=True): - if "appointments" not in component: - continue - - if component["appointments"].get("showProducts"): - return form_step - - # no component in any form step found that satisfies - return None - - -def get_confirmation_mail_suffix(submission: Submission) -> str: - """ - Determine the suffix, if appropriate for the subject of the confirmation mail. - - If this submission is related to an appointment and previous submission, - append an "updated" marker to the subject (see #680). - """ - # if there's no related previous submission, it cannot be an update - if not submission.previous_submission_id: - return "" - - # if there's no appointment info attached to the previous submission, it cannot be - # an update - try: - appointment_info = submission.previous_submission.appointment_info - except AppointmentInfo.DoesNotExist: - return "" - - # if the previous appointment was not cancelled, it cannot be an update - # TODO: what to do when we did succesfully create a new appointment, but the old - # one deletion failed? there are now two appointments open. - # submission.appointment_info.status == AppointmentDetailsStatus.success and - # submission.previous_submission.appointment_info.status == AppointmentDetailsStatus.success - if appointment_info.status != AppointmentDetailsStatus.cancelled: - return "" - - return _("(updated)") - - def get_appointment(submission: Submission) -> Appointment | None: if not submission.form.is_appointment: return None diff --git a/src/openforms/appointments/views.py b/src/openforms/appointments/views.py index 5361d73711..1ada27b71e 100644 --- a/src/openforms/appointments/views.py +++ b/src/openforms/appointments/views.py @@ -2,13 +2,11 @@ from django.views.generic import RedirectView -from openforms.authentication.service import FORM_AUTH_SESSION_KEY, store_auth_details from openforms.frontend import get_frontend_redirect_url from openforms.submissions.models import Submission from openforms.submissions.views import ResumeFormMixin from .tokens import submission_appointment_token_generator -from .utils import find_first_appointment_step logger = logging.getLogger(__name__) @@ -25,44 +23,3 @@ def get_form_resume_url(self, submission: Submission) -> str: "submission_uuid": str(submission.uuid), }, ) - - -class VerifyChangeAppointmentLinkView(ResumeFormMixin, RedirectView): - token_generator = submission_appointment_token_generator - - def custom_submission_modifications(self, submission: Submission) -> Submission: - # note that we need to ensure the plain text auth attribute value needs to be set again - # for machinery relying on it to work. hashes are one-way, so we need to use the session - # data. - new_submission = Submission.objects.copy(submission) - if (form_auth := self.request.session.get(FORM_AUTH_SESSION_KEY)) is not None: - store_auth_details(new_submission, form_auth) - return new_submission - - def get_form_resume_url(self, submission: Submission) -> str: - next_step = find_first_appointment_step(submission.form) - # simplified flow for new-style appointments - if submission.form.is_appointment: - raise RuntimeError( - "New style appointments do not support the change appointment flow." - ) - - if next_step is None: - # Should not happen but redirect to first step if it does - logger.warning( - "Could not find the appointment step for submission %s," - "redirecting user to first step in form", - submission.uuid, - ) - next_step = submission.form.formstep_set.first() - - assert next_step is not None, "Form has no steps to redirect to!" - - return get_frontend_redirect_url( - submission, - action="resume", - action_params={ - "step_slug": next_step.slug, - "submission_uuid": str(submission.uuid), - }, - ) diff --git a/src/openforms/emails/templates/emails/templatetags/appointment_information.html b/src/openforms/emails/templates/emails/templatetags/appointment_information.html index 253af5965f..dac4b5f037 100644 --- a/src/openforms/emails/templates/emails/templatetags/appointment_information.html +++ b/src/openforms/emails/templates/emails/templatetags/appointment_information.html @@ -53,17 +53,12 @@

{% trans "Your contact details" %}<

- {% trans "If you want to cancel or change your appointment, you can do so below." %} + {% trans "If you want to cancel your appointment, you can do so below." %}

- {% trans "Cancel appointment" %} + {% trans "If you wish to change your appointment, please cancel it using the link below and create a new one." %}

- {% if appointment_change_link %} - {% trans "Change appointment" %} - {% else %} - {% trans "If you wish to change your appointment, please cancel it using the above link and create a new one." %} - {% endif %} + {% trans "Cancel appointment" %}

- {% endif %} diff --git a/src/openforms/emails/templates/emails/templatetags/appointment_information.txt b/src/openforms/emails/templates/emails/templatetags/appointment_information.txt index 76d9dcb371..b673c13c2c 100644 --- a/src/openforms/emails/templates/emails/templatetags/appointment_information.txt +++ b/src/openforms/emails/templates/emails/templatetags/appointment_information.txt @@ -23,7 +23,5 @@ {% trans "If you want to cancel or change your appointment, you can do so below." %} {% trans "Cancel appointment" %}: {{ appointment_cancel_link }} -{% if appointment_change_link %}{% trans "Change appointment" %}: {{ appointment_change_link }} -{% else %}{% trans "If you wish to change your appointment, please cancel it using the above link and create a new one." %} -{% endif %} +{% trans "If you wish to change your appointment, please cancel it using the above link and create a new one." %} {% endif %}{% endautoescape %} diff --git a/src/openforms/emails/templatetags/appointments.py b/src/openforms/emails/templatetags/appointments.py index 87d8435f78..902e0587d0 100644 --- a/src/openforms/emails/templatetags/appointments.py +++ b/src/openforms/emails/templatetags/appointments.py @@ -22,10 +22,12 @@ def appointment_information(context): else: template_name = "emails/templatetags/appointment_information.html" - # check for new style appointments + # check for appointments submission = context["_submission"] appointment = get_appointment(submission) - plugin_id = appointment.plugin if appointment else "" + + assert appointment + plugin_id = appointment.plugin plugin = get_plugin(plugin=plugin_id) @@ -37,8 +39,5 @@ def appointment_information(context): as_html=not as_text, ), "appointment_cancel_link": plugin.get_cancel_link(context["_submission"]), - "appointment_change_link": ( - plugin.get_change_link(context["_submission"]) if not appointment else "" - ), } return render_to_string(template_name, tag_context) diff --git a/src/openforms/emails/templatetags/form_summary.py b/src/openforms/emails/templatetags/form_summary.py index 0ace645333..3fc5b93c45 100644 --- a/src/openforms/emails/templatetags/form_summary.py +++ b/src/openforms/emails/templatetags/form_summary.py @@ -16,7 +16,7 @@ @register.simple_tag(takes_context=True) def confirmation_summary(context): submission = context["_submission"] - # if it's a new-style appointment submission, there are no steps or summary to render + # There are no steps or summary to render in new-style appointment submission if get_appointment(submission) is not None: return "" diff --git a/src/openforms/emails/tests/test_confirmation_email.py b/src/openforms/emails/tests/test_confirmation_email.py index 2c58b64b84..34f18a53f0 100644 --- a/src/openforms/emails/tests/test_confirmation_email.py +++ b/src/openforms/emails/tests/test_confirmation_email.py @@ -1,7 +1,5 @@ import inspect -import re from copy import deepcopy -from datetime import date, datetime from decimal import Decimal from unittest.mock import patch @@ -10,18 +8,14 @@ from django.template import TemplateSyntaxError from django.test import TestCase, override_settings from django.urls import reverse -from django.utils.html import format_html from django.utils.translation import gettext as _ -from openforms.appointments.base import ( - AppointmentDetails, - BasePlugin, - Location, - Product, -) from openforms.appointments.constants import AppointmentDetailsStatus from openforms.appointments.contrib.demo.plugin import DemoAppointment -from openforms.appointments.tests.factories import AppointmentInfoFactory +from openforms.appointments.tests.factories import ( + AppointmentFactory, + AppointmentInfoFactory, +) from openforms.config.models import GlobalConfiguration from openforms.forms.tests.factories import FormStepFactory from openforms.payments.constants import PaymentStatus @@ -32,7 +26,7 @@ ) from openforms.submissions.utils import send_confirmation_email from openforms.tests.utils import NOOP_CACHES -from openforms.utils.tests.html_assert import HTMLAssertMixin, strip_all_attributes +from openforms.utils.tests.html_assert import HTMLAssertMixin from openforms.utils.urls import build_absolute_uri from ..confirmation_emails import ( @@ -105,9 +99,6 @@ class FixedCancelAndChangeLinkPlugin(DemoAppointment): def get_cancel_link(self, submission) -> str: return "http://fake.nl/api/v2/submission-uuid/token/verify/" - def get_change_link(self, submission) -> str: - return "http://fake.nl/api/v2/submission-uuid/token/change/" - @override_settings(CACHES=NOOP_CACHES) class ConfirmationEmailTests(HTMLAssertMixin, TestCase): @@ -249,14 +240,9 @@ def test_appointment_information(self, get_plugin_mock): config.email_template_netloc_allowlist = ["fake.nl"] config.save() - submission = SubmissionFactory.create() - AppointmentInfoFactory.create( - status=AppointmentDetailsStatus.success, - appointment_id="123456789", - submission=submission, - ) + appointment = AppointmentFactory.create(appointment_info__registration_ok=True) email = ConfirmationEmailTemplate(content="{% appointment_information %}") - context = get_confirmation_email_context_data(submission) + context = get_confirmation_email_context_data(appointment.submission) rendered_content = render_email_template(email.content, context) self.assertIn("Test product 1", rendered_content) @@ -274,13 +260,6 @@ def test_appointment_information(self, get_plugin_mock): rendered_content, ) - self.assertInHTML( - '' - + _("Change appointment") - + "", - rendered_content, - ) - def test_appointment_information_with_no_appointment_id(self): submission = SubmissionFactory.create() AppointmentInfoFactory.create( @@ -586,51 +565,6 @@ def test_email_payment_completed(self): self.assertNotIn(url, rendered_content) -class TestAppointmentPlugin(BasePlugin): - def get_available_products(self, current_products=None): - return [ - Product(identifier="1", name="Test product 1"), - Product(identifier="2", name="Test product 2"), - ] - - def get_locations(self, products): - return [Location(identifier="1", name="Test location")] - - def get_dates(self, products, location, start_at=None, end_at=None): - return [date(2021, 1, 1)] - - def get_times(self, products, location, day): - return [datetime(2021, 1, 1, 12, 0)] - - def get_required_customer_fields(self, products): - return [] - - def create_appointment(self, products, location, start_at, client, remarks=None): - return "1" - - def delete_appointment(self, identifier: str) -> None: - return - - def get_appointment_details(self, identifier: str): - return AppointmentDetails( - identifier=identifier, - products=[ - Product(identifier="1", name="Test product 1 & 2"), - Product(identifier="2", name="Test product 3"), - ], - location=Location( - identifier="1", - name="Test location", - city="Teststad", - postalcode="1234ab", - ), - start_at=datetime(2021, 1, 1, 12, 0), - end_at=datetime(2021, 1, 1, 12, 15), - remarks="Remarks", - other={"Some": "

Data

"}, - ) - - @override_settings(DEFAULT_FROM_EMAIL="foo@sender.com") class ConfirmationEmailRenderingIntegrationTest(HTMLAssertMixin, TestCase): template = """ @@ -654,233 +588,6 @@ class ConfirmationEmailRenderingIntegrationTest(HTMLAssertMixin, TestCase): """ maxDiff = None - @patch( - "openforms.emails.templatetags.appointments.get_plugin", - return_value=TestAppointmentPlugin("test"), - ) - def test_send_confirmation_mail_text_kitchensink(self, appointment_plugin_mock): - config = GlobalConfiguration.get_solo() - config.email_template_netloc_allowlist = ["gemeente.nl"] - config.save() - - conf = deepcopy(NESTED_COMPONENT_CONF) - conf["components"].append( - { - "id": "erttrr", - "key": "file", - "type": "file", - "label": "File", - "showInEmail": True, - } - ) - - submission = SubmissionFactory.from_components( - conf["components"], - { - "name": "Foo", - "lastName": "de Bar & de Baas", - "email": "foo@bar.baz", - "file": [ - { - "url": "http://server/api/v2/submissions/files/62f2ec22-da7d-4385-b719-b8637c1cd483", - "data": { - "url": "http://server/api/v2/submissions/files/62f2ec22-da7d-4385-b719-b8637c1cd483", - "form": "", - "name": "my-image.jpg", - "size": 46114, - "baseUrl": "http://server/form", - "project": "", - }, - "name": "my-image-12305610-2da4-4694-a341-ccb919c3d543.jpg", - "size": 46114, - "type": "image/jpg", - "storage": "url", - "originalName": "my-image.jpg", - } - ], - }, - registration_success=True, - public_registration_reference="xyz123", - form__product__price=Decimal("12.34"), - form__product__information="

info line 1

\r\n

info line 2

\r\n

info line 3

", - form__payment_backend="test", - form_url="http://server/form", - co_sign_data={ - "plugin": "digid", - "identifier": "123456782", - "fields": { - "voornaam": "Tina", - "geslachtsnaam": "Shikari", - }, - "representation": "T. Shikari", - }, - ) - AppointmentInfoFactory.create( - status=AppointmentDetailsStatus.success, - appointment_id="123456789", - submission=submission, - ) - self.assertTrue(submission.payment_required) - self.assertFalse(submission.payment_user_has_paid) - - template = inspect.cleandoc(self.template) - ConfirmationEmailTemplateFactory.create( - form=submission.form, subject="My Subject", content=template - ) - first_step_name = submission.submissionstep_set.all()[ - 0 - ].form_step.form_definition.name - - send_confirmation_email(submission) - - self.assertEqual(len(mail.outbox), 1) - - message = mail.outbox[0] - self.assertEqual(message.subject, "My Subject") - self.assertEqual(message.recipients(), ["foo@bar.baz"]) - self.assertEqual(message.from_email, "foo@sender.com") - - ref = submission.public_registration_reference - - url_exp = r"https?://[a-z0-9:/._-]+" - pay_line = _( - "Payment of EUR %(payment_price)s is required. You can pay using the link below." - ) % {"payment_price": "12,34"} - - with self.subTest("text"): - expected_text = inspect.cleandoc( - f""" - Geachte heer/mevrouw, - - Wij hebben uw inzending, met referentienummer {ref}, in goede orde ontvangen. - - Kijk voor meer informatie op de homepage (#URL#) - - {_("Summary")} - - {first_step_name} - - - A fieldset - - Name: Foo - - Last name: de Bar & de Baas - - File: {_("attachment: %s") % "my-image.jpg"} - - {_("Co-signed by")}: T. Shikari - - {_("Appointment information")} - - {_("Products")}: - - Test product 1 & 2 - - Test product 3 - - {_("Location")}: - Test location - 1234ab Teststad - - {_("Date and time")}: - 1 januari 2021, 12:00 - 12:15 - - {_("Remarks")}: - Remarks - - Some: - Data - - {_("If you want to cancel or change your appointment, you can do so below.")} - {_("Cancel appointment")}: #URL# - {_("Change appointment")}: #URL# - - info line 1 - info line 2 - info line 3 - - {_("Payment information")} - - {pay_line} - {_("Go to the payment page")}: #URL# - - Met vriendelijke groet, - - Open Formulieren - """ - ).lstrip() - - # process to keep tests sane (random tokens) - text = message.body.rstrip() - text = re.sub(url_exp, "#URL#", text) - self.assertEqual(expected_text, text) - self.assertNotIn("', message_html) - - message_html_only_tags = strip_all_attributes(message_html) - # check co-sign data presence - self.assertInHTML( - format_html( - " {label} T. Shikari ", - label=_("Co-signed by"), - ), - message_html_only_tags, - ) - - # fieldset and step containers should be visible - self.assertInHTML( - format_html("

{}

", first_step_name), - message_html_only_tags, - ) - - # renderer should ignore hidden inputs - self.assertNotIn("Hidden input", message_html_only_tags) - self.assertNotIn("A fieldset with hidden children", message_html_only_tags) - self.assertNotIn("Hidden input 2", message_html_only_tags) - - with self.subTest("attachments"): - # file uploads may not be added as attachments, see #1193 - self.assertEqual(message.attachments, []) - - @patch( - "openforms.emails.templatetags.appointments.get_plugin", - return_value=TestAppointmentPlugin("test"), - ) - def test_html_in_subject(self, appointment_plugin_mock): - """Assert that HTML is not escaped in Email subjects""" - - conf = deepcopy(NESTED_COMPONENT_CONF) - - submission = SubmissionFactory.from_components( - conf["components"], - { - "name": "John", - "lastName": "Doe", - "email": "foo@bar.baz", - }, - registration_success=True, - ) - submission.form.name = "Foo's bar" - - template = inspect.cleandoc(self.template) - - ConfirmationEmailTemplateFactory.create( - form=submission.form, subject="Subject: {{ form_name }}", content=template - ) - - send_confirmation_email(submission) - - self.assertEqual(len(mail.outbox), 1) - - message = mail.outbox[0] - self.assertEqual(message.subject, "Subject: Foo's bar") - def test_templatetag_alias(self): submission = SubmissionFactory.from_components( [ diff --git a/src/openforms/formio/formatters/tests/files/appointments_components.json b/src/openforms/formio/formatters/tests/files/appointments_components.json index 5d5f657ba7..e69de29bb2 100644 --- a/src/openforms/formio/formatters/tests/files/appointments_components.json +++ b/src/openforms/formio/formatters/tests/files/appointments_components.json @@ -1,805 +0,0 @@ -{ - "display": "form", - "components": [ - { - "id": "e1vezz1", - "key": "appointmentProduct", - "type": "select", - "input": true, - "label": "Select Appointment Product", - "limit": 100, - "filter": "", - "hidden": false, - "idPath": "id", - "prefix": "", - "suffix": "", - "unique": false, - "widget": null, - "dataSrc": "values", - "dbIndex": false, - "overlay": { - "top": "", - "left": "", - "style": "", - "width": "", - "height": "" - }, - "tooltip": "", - "disabled": false, - "lazyLoad": true, - "multiple": false, - "redrawOn": "", - "tabindex": "", - "template": "{{ item.label }}", - "validate": { - "custom": "", - "unique": false, - "plugins": [], - "multiple": false, - "required": true, - "customPrivate": false, - "onlyAvailableItems": false, - "strictDateValidation": false - }, - "autofocus": false, - "encrypted": false, - "hideLabel": false, - "indexeddb": { - "filter": {} - }, - "minSearch": 0, - "modalEdit": false, - "protected": false, - "refreshOn": "", - "tableView": true, - "attributes": {}, - "errorLabel": "", - "persistent": true, - "properties": {}, - "validateOn": "change", - "clearOnHide": true, - "conditional": { - "eq": "", - "show": null, - "when": null - }, - "customClass": "", - "description": "", - "fuseOptions": { - "include": "score", - "threshold": 0.3 - }, - "ignoreCache": false, - "placeholder": "", - "searchField": "", - "showInEmail": true, - "authenticate": false, - "defaultValue": "", - "registration": { - "attribute": "" - }, - "selectFields": "", - "customOptions": {}, - "dataGridLabel": false, - "labelPosition": "top", - "readOnlyValue": false, - "searchEnabled": true, - "showCharCount": false, - "showWordCount": false, - "uniqueOptions": false, - "valueProperty": "", - "calculateValue": "", - "clearOnRefresh": false, - "useExactSearch": false, - "calculateServer": false, - "isSensitiveData": false, - "selectThreshold": 0.3, - "allowMultipleMasks": false, - "customDefaultValue": "", - "allowCalculateOverride": false, - "appointments": { - "showProducts": true - } - }, - { - "id": "e1vezz2", - "key": "appointmentProductEmpty", - "type": "select", - "input": true, - "label": "Select Appointment Product Empty", - "limit": 100, - "filter": "", - "hidden": false, - "idPath": "id", - "prefix": "", - "suffix": "", - "unique": false, - "widget": null, - "dataSrc": "values", - "dbIndex": false, - "overlay": { - "top": "", - "left": "", - "style": "", - "width": "", - "height": "" - }, - "tooltip": "", - "disabled": false, - "lazyLoad": true, - "multiple": false, - "redrawOn": "", - "tabindex": "", - "template": "{{ item.label }}", - "validate": { - "custom": "", - "unique": false, - "plugins": [], - "multiple": false, - "required": true, - "customPrivate": false, - "onlyAvailableItems": false, - "strictDateValidation": false - }, - "autofocus": false, - "encrypted": false, - "hideLabel": false, - "indexeddb": { - "filter": {} - }, - "minSearch": 0, - "modalEdit": false, - "protected": false, - "refreshOn": "", - "tableView": true, - "attributes": {}, - "errorLabel": "", - "persistent": true, - "properties": {}, - "validateOn": "change", - "clearOnHide": true, - "conditional": { - "eq": "", - "show": null, - "when": null - }, - "customClass": "", - "description": "", - "fuseOptions": { - "include": "score", - "threshold": 0.3 - }, - "ignoreCache": false, - "placeholder": "", - "searchField": "", - "showInEmail": true, - "authenticate": false, - "defaultValue": "", - "registration": { - "attribute": "" - }, - "selectFields": "", - "customOptions": {}, - "dataGridLabel": false, - "labelPosition": "top", - "readOnlyValue": false, - "searchEnabled": true, - "showCharCount": false, - "showWordCount": false, - "uniqueOptions": false, - "valueProperty": "", - "calculateValue": "", - "clearOnRefresh": false, - "useExactSearch": false, - "calculateServer": false, - "isSensitiveData": false, - "selectThreshold": 0.3, - "allowMultipleMasks": false, - "customDefaultValue": "", - "allowCalculateOverride": false, - "appointments": { - "showProducts": true - } - }, - { - "id": "e1vezz3", - "key": "appointmentLocation", - "type": "select", - "input": true, - "label": "Select Appointment Location", - "limit": 100, - "filter": "", - "hidden": false, - "idPath": "id", - "prefix": "", - "suffix": "", - "unique": false, - "widget": null, - "dataSrc": "values", - "dbIndex": false, - "overlay": { - "top": "", - "left": "", - "style": "", - "width": "", - "height": "" - }, - "tooltip": "", - "disabled": false, - "lazyLoad": true, - "multiple": false, - "redrawOn": "", - "tabindex": "", - "template": "{{ item.label }}", - "validate": { - "custom": "", - "unique": false, - "plugins": [], - "multiple": false, - "required": true, - "customPrivate": false, - "onlyAvailableItems": false, - "strictDateValidation": false - }, - "autofocus": false, - "encrypted": false, - "hideLabel": false, - "indexeddb": { - "filter": {} - }, - "minSearch": 0, - "modalEdit": false, - "protected": false, - "refreshOn": "", - "tableView": true, - "attributes": {}, - "errorLabel": "", - "persistent": true, - "properties": {}, - "validateOn": "change", - "clearOnHide": true, - "conditional": { - "eq": "", - "show": null, - "when": null - }, - "customClass": "", - "description": "", - "fuseOptions": { - "include": "score", - "threshold": 0.3 - }, - "ignoreCache": false, - "placeholder": "", - "searchField": "", - "showInEmail": true, - "authenticate": false, - "defaultValue": "", - "registration": { - "attribute": "" - }, - "selectFields": "", - "customOptions": {}, - "dataGridLabel": false, - "labelPosition": "top", - "readOnlyValue": false, - "searchEnabled": true, - "showCharCount": false, - "showWordCount": false, - "uniqueOptions": false, - "valueProperty": "", - "calculateValue": "", - "clearOnRefresh": false, - "useExactSearch": false, - "calculateServer": false, - "isSensitiveData": false, - "selectThreshold": 0.3, - "allowMultipleMasks": false, - "customDefaultValue": "", - "allowCalculateOverride": false, - "appointments": { - "showLocations": true - } - }, - { - "id": "e1vezz4", - "key": "appointmentLocationEmpty", - "type": "select", - "input": true, - "label": "Select Appointment Location Empty", - "limit": 100, - "filter": "", - "hidden": false, - "idPath": "id", - "prefix": "", - "suffix": "", - "unique": false, - "widget": null, - "dataSrc": "values", - "dbIndex": false, - "overlay": { - "top": "", - "left": "", - "style": "", - "width": "", - "height": "" - }, - "tooltip": "", - "disabled": false, - "lazyLoad": true, - "multiple": false, - "redrawOn": "", - "tabindex": "", - "template": "{{ item.label }}", - "validate": { - "custom": "", - "unique": false, - "plugins": [], - "multiple": false, - "required": true, - "customPrivate": false, - "onlyAvailableItems": false, - "strictDateValidation": false - }, - "autofocus": false, - "encrypted": false, - "hideLabel": false, - "indexeddb": { - "filter": {} - }, - "minSearch": 0, - "modalEdit": false, - "protected": false, - "refreshOn": "", - "tableView": true, - "attributes": {}, - "errorLabel": "", - "persistent": true, - "properties": {}, - "validateOn": "change", - "clearOnHide": true, - "conditional": { - "eq": "", - "show": null, - "when": null - }, - "customClass": "", - "description": "", - "fuseOptions": { - "include": "score", - "threshold": 0.3 - }, - "ignoreCache": false, - "placeholder": "", - "searchField": "", - "showInEmail": true, - "authenticate": false, - "defaultValue": "", - "registration": { - "attribute": "" - }, - "selectFields": "", - "customOptions": {}, - "dataGridLabel": false, - "labelPosition": "top", - "readOnlyValue": false, - "searchEnabled": true, - "showCharCount": false, - "showWordCount": false, - "uniqueOptions": false, - "valueProperty": "", - "calculateValue": "", - "clearOnRefresh": false, - "useExactSearch": false, - "calculateServer": false, - "isSensitiveData": false, - "selectThreshold": 0.3, - "allowMultipleMasks": false, - "customDefaultValue": "", - "allowCalculateOverride": false, - "appointments": { - "showLocations": true - } - }, - { - "id": "e1vezz5", - "key": "appointmentDate", - "type": "select", - "input": true, - "label": "Select Appointment Date", - "limit": 100, - "filter": "", - "hidden": false, - "idPath": "id", - "prefix": "", - "suffix": "", - "unique": false, - "widget": null, - "dataSrc": "values", - "dbIndex": false, - "overlay": { - "top": "", - "left": "", - "style": "", - "width": "", - "height": "" - }, - "tooltip": "", - "disabled": false, - "lazyLoad": true, - "multiple": false, - "redrawOn": "", - "tabindex": "", - "template": "{{ item.label }}", - "validate": { - "custom": "", - "unique": false, - "plugins": [], - "multiple": false, - "required": true, - "customPrivate": false, - "onlyAvailableItems": false, - "strictDateValidation": false - }, - "autofocus": false, - "encrypted": false, - "hideLabel": false, - "indexeddb": { - "filter": {} - }, - "minSearch": 0, - "modalEdit": false, - "protected": false, - "refreshOn": "", - "tableView": true, - "attributes": {}, - "errorLabel": "", - "persistent": true, - "properties": {}, - "validateOn": "change", - "clearOnHide": true, - "conditional": { - "eq": "", - "show": null, - "when": null - }, - "customClass": "", - "description": "", - "fuseOptions": { - "include": "score", - "threshold": 0.3 - }, - "ignoreCache": false, - "placeholder": "", - "searchField": "", - "showInEmail": true, - "authenticate": false, - "defaultValue": "", - "registration": { - "attribute": "" - }, - "selectFields": "", - "customOptions": {}, - "dataGridLabel": false, - "labelPosition": "top", - "readOnlyValue": false, - "searchEnabled": true, - "showCharCount": false, - "showWordCount": false, - "uniqueOptions": false, - "valueProperty": "", - "calculateValue": "", - "clearOnRefresh": false, - "useExactSearch": false, - "calculateServer": false, - "isSensitiveData": false, - "selectThreshold": 0.3, - "allowMultipleMasks": false, - "customDefaultValue": "", - "allowCalculateOverride": false, - "appointments": { - "showDates": true - } - }, - { - "id": "e1vezz6", - "key": "appointmentDateEmpty", - "type": "select", - "input": true, - "label": "Select Appointment Date Empty", - "limit": 100, - "filter": "", - "hidden": false, - "idPath": "id", - "prefix": "", - "suffix": "", - "unique": false, - "widget": null, - "dataSrc": "values", - "dbIndex": false, - "overlay": { - "top": "", - "left": "", - "style": "", - "width": "", - "height": "" - }, - "tooltip": "", - "disabled": false, - "lazyLoad": true, - "multiple": false, - "redrawOn": "", - "tabindex": "", - "template": "{{ item.label }}", - "validate": { - "custom": "", - "unique": false, - "plugins": [], - "multiple": false, - "required": true, - "customPrivate": false, - "onlyAvailableItems": false, - "strictDateValidation": false - }, - "autofocus": false, - "encrypted": false, - "hideLabel": false, - "indexeddb": { - "filter": {} - }, - "minSearch": 0, - "modalEdit": false, - "protected": false, - "refreshOn": "", - "tableView": true, - "attributes": {}, - "errorLabel": "", - "persistent": true, - "properties": {}, - "validateOn": "change", - "clearOnHide": true, - "conditional": { - "eq": "", - "show": null, - "when": null - }, - "customClass": "", - "description": "", - "fuseOptions": { - "include": "score", - "threshold": 0.3 - }, - "ignoreCache": false, - "placeholder": "", - "searchField": "", - "showInEmail": true, - "authenticate": false, - "defaultValue": "", - "registration": { - "attribute": "" - }, - "selectFields": "", - "customOptions": {}, - "dataGridLabel": false, - "labelPosition": "top", - "readOnlyValue": false, - "searchEnabled": true, - "showCharCount": false, - "showWordCount": false, - "uniqueOptions": false, - "valueProperty": "", - "calculateValue": "", - "clearOnRefresh": false, - "useExactSearch": false, - "calculateServer": false, - "isSensitiveData": false, - "selectThreshold": 0.3, - "allowMultipleMasks": false, - "customDefaultValue": "", - "allowCalculateOverride": false, - "appointments": { - "showDates": true - } - }, - { - "id": "e1vezz7", - "key": "appointmentTime", - "type": "select", - "input": true, - "label": "Select Appointment Time", - "limit": 100, - "filter": "", - "hidden": false, - "idPath": "id", - "prefix": "", - "suffix": "", - "unique": false, - "widget": null, - "dataSrc": "values", - "dbIndex": false, - "overlay": { - "top": "", - "left": "", - "style": "", - "width": "", - "height": "" - }, - "tooltip": "", - "disabled": false, - "lazyLoad": true, - "multiple": false, - "redrawOn": "", - "tabindex": "", - "template": "{{ item.label }}", - "validate": { - "custom": "", - "unique": false, - "plugins": [], - "multiple": false, - "required": true, - "customPrivate": false, - "onlyAvailableItems": false, - "strictDateValidation": false - }, - "autofocus": false, - "encrypted": false, - "hideLabel": false, - "indexeddb": { - "filter": {} - }, - "minSearch": 0, - "modalEdit": false, - "protected": false, - "refreshOn": "", - "tableView": true, - "attributes": {}, - "errorLabel": "", - "persistent": true, - "properties": {}, - "validateOn": "change", - "clearOnHide": true, - "conditional": { - "eq": "", - "show": null, - "when": null - }, - "customClass": "", - "description": "", - "fuseOptions": { - "include": "score", - "threshold": 0.3 - }, - "ignoreCache": false, - "placeholder": "", - "searchField": "", - "showInEmail": true, - "authenticate": false, - "defaultValue": "", - "registration": { - "attribute": "" - }, - "selectFields": "", - "customOptions": {}, - "dataGridLabel": false, - "labelPosition": "top", - "readOnlyValue": false, - "searchEnabled": true, - "showCharCount": false, - "showWordCount": false, - "uniqueOptions": false, - "valueProperty": "", - "calculateValue": "", - "clearOnRefresh": false, - "useExactSearch": false, - "calculateServer": false, - "isSensitiveData": false, - "selectThreshold": 0.3, - "allowMultipleMasks": false, - "customDefaultValue": "", - "allowCalculateOverride": false, - "appointments": { - "showTimes": true - } - }, - { - "id": "e1vezz8", - "key": "appointmentTimeEmpty", - "type": "select", - "input": true, - "label": "Select Appointment Time Empty", - "limit": 100, - "filter": "", - "hidden": false, - "idPath": "id", - "prefix": "", - "suffix": "", - "unique": false, - "widget": null, - "dataSrc": "values", - "dbIndex": false, - "overlay": { - "top": "", - "left": "", - "style": "", - "width": "", - "height": "" - }, - "tooltip": "", - "disabled": false, - "lazyLoad": true, - "multiple": false, - "redrawOn": "", - "tabindex": "", - "template": "{{ item.label }}", - "validate": { - "custom": "", - "unique": false, - "plugins": [], - "multiple": false, - "required": true, - "customPrivate": false, - "onlyAvailableItems": false, - "strictDateValidation": false - }, - "autofocus": false, - "encrypted": false, - "hideLabel": false, - "indexeddb": { - "filter": {} - }, - "minSearch": 0, - "modalEdit": false, - "protected": false, - "refreshOn": "", - "tableView": true, - "attributes": {}, - "errorLabel": "", - "persistent": true, - "properties": {}, - "validateOn": "change", - "clearOnHide": true, - "conditional": { - "eq": "", - "show": null, - "when": null - }, - "customClass": "", - "description": "", - "fuseOptions": { - "include": "score", - "threshold": 0.3 - }, - "ignoreCache": false, - "placeholder": "", - "searchField": "", - "showInEmail": true, - "authenticate": false, - "defaultValue": "", - "registration": { - "attribute": "" - }, - "selectFields": "", - "customOptions": {}, - "dataGridLabel": false, - "labelPosition": "top", - "readOnlyValue": false, - "searchEnabled": true, - "showCharCount": false, - "showWordCount": false, - "uniqueOptions": false, - "valueProperty": "", - "calculateValue": "", - "clearOnRefresh": false, - "useExactSearch": false, - "calculateServer": false, - "isSensitiveData": false, - "selectThreshold": 0.3, - "allowMultipleMasks": false, - "customDefaultValue": "", - "allowCalculateOverride": false, - "appointments": { - "showTimes": true - } - } - ] -} diff --git a/src/openforms/formio/formatters/tests/files/appointments_data.json b/src/openforms/formio/formatters/tests/files/appointments_data.json index 888c38843e..e69de29bb2 100644 --- a/src/openforms/formio/formatters/tests/files/appointments_data.json +++ b/src/openforms/formio/formatters/tests/files/appointments_data.json @@ -1,10 +0,0 @@ -{ - "appointmentProduct": {"identifier": "foo", "name": "Foo Product"}, - "appointmentProductEmpty": "", - "appointmentLocation": {"identifier": "foo", "name": "Foo Location"}, - "appointmentLocationEmpty": "", - "appointmentDate": "2022-02-14", - "appointmentDateEmpty": "", - "appointmentTime": "2022-02-14T08:15:00+01:00", - "appointmentTimeEmpty": "" -} diff --git a/src/openforms/formio/formatters/tests/test_kitchensink.py b/src/openforms/formio/formatters/tests/test_kitchensink.py index 82029675b1..02ddf5b90f 100644 --- a/src/openforms/formio/formatters/tests/test_kitchensink.py +++ b/src/openforms/formio/formatters/tests/test_kitchensink.py @@ -106,24 +106,3 @@ def run_kitchensink_test(self, name_data, name_printable): for label, value in text_printed.items(): with self.subTest(f"{label} -> '{value}'"): self.assertEqual(value, text_values[label]) - - def test_appointments_formio(self): - configuration = load_json("appointments_components.json") - data = load_json("appointments_data.json") - text_printed = load_json("appointments_printable_text.json") - - # for sanity - self.assertFlatConfiguration(configuration) - - self.assertEqual(len(text_printed), len(data)) - - submission = SubmissionFactory.from_components( - configuration["components"], submitted_data=data, completed=True - ) - - printable_data = _get_printable_data(submission) - text_values = dict(printable_data) - - for label, value in text_printed.items(): - with self.subTest(f"{label} -> '{value}'"): - self.assertEqual(value, text_values[label]) diff --git a/src/openforms/js/compiled-lang/en.json b/src/openforms/js/compiled-lang/en.json index 790c4a6d3e..dd8f7fbfa0 100644 --- a/src/openforms/js/compiled-lang/en.json +++ b/src/openforms/js/compiled-lang/en.json @@ -89,12 +89,6 @@ "value": "Default Value" } ], - "/c2LWu": [ - { - "type": 0, - "value": "Locations Component" - } - ], "/fAEsY": [ { "type": 0, @@ -107,12 +101,6 @@ "value": "Make this component read only." } ], - "/hIJ8m": [ - { - "type": 0, - "value": "Dates Component" - } - ], "/iusiS": [ { "type": 0, @@ -559,12 +547,6 @@ "value": " files to upload." } ], - "4iGsG1": [ - { - "type": 0, - "value": "Birth Date Component" - } - ], "4sFGgA": [ { "type": 0, @@ -623,12 +605,6 @@ "value": "Configure" } ], - "5OXIff": [ - { - "type": 0, - "value": "Component where the birth date should be entered" - } - ], "5P+YgL": [ { "type": 0, @@ -1285,12 +1261,6 @@ "value": "Email payment subject" } ], - "BCjZNo": [ - { - "type": 0, - "value": "Component where the phone number should be entered" - } - ], "BGZD6J": [ { "type": 0, @@ -2353,12 +2323,6 @@ "value": "Hidden" } ], - "JtOgcq": [ - { - "type": 0, - "value": "Last Name Component" - } - ], "K/Oijt": [ { "type": 0, @@ -2701,12 +2665,6 @@ "value": "A short instruction shown below the signature pad." } ], - "NwTdLM": [ - { - "type": 0, - "value": "Component where locations for an appointment will be shown" - } - ], "NyyJMK": [ { "type": 0, @@ -2885,12 +2843,6 @@ "value": "Number of years. Empty values are ignored." } ], - "PiB6Y1": [ - { - "type": 0, - "value": "Times Component" - } - ], "PtZ96W": [ { "type": 0, @@ -3955,12 +3907,6 @@ "value": " attribute" } ], - "aUChz/": [ - { - "type": 0, - "value": "Products Component" - } - ], "aWwaAq": [ { "type": 0, @@ -4981,12 +4927,6 @@ "value": "Expression" } ], - "iq0ppL": [ - { - "type": 0, - "value": "Experimental mode. Indicates whether appointments are enabled for this form." - } - ], "iqAbXt": [ { "type": 0, @@ -6065,12 +6005,6 @@ "value": "Are you sure you want to remove this mapping?" } ], - "t99ynC": [ - { - "type": 0, - "value": "Component where the last name should be entered" - } - ], "tJQoMq": [ { "type": 0, @@ -6379,24 +6313,12 @@ "value": "The regular expression pattern test that the field value must pass before the form can be submitted." } ], - "wjwFwT": [ - { - "type": 0, - "value": "Component where times for an appointment will be shown" - } - ], "wnTTZr": [ { "type": 0, "value": "ID of the drive to use. If left empty, the default drive will be used." } ], - "wnxKT/": [ - { - "type": 0, - "value": "Component where products for an appointment will be shown" - } - ], "wqCiwc": [ { "type": 0, @@ -6445,12 +6367,6 @@ "value": "Submission confirmation template" } ], - "xRQrla": [ - { - "type": 0, - "value": "Appointments" - } - ], "xTfMk5": [ { "type": 0, @@ -6469,6 +6385,12 @@ "value": "Zoom level" } ], + "xxyIg/": [ + { + "type": 0, + "value": "Indicates whether appointments are enabled for this form." + } + ], "xyVtKp": [ { "type": 0, @@ -6569,12 +6491,6 @@ "value": "(unset)" } ], - "yYw2Rb": [ - { - "type": 0, - "value": "Phone Number Component" - } - ], "yrz/oC": [ { "type": 0, @@ -6617,12 +6533,6 @@ "value": "Use globally configured map component settings" } ], - "zLTTBw": [ - { - "type": 0, - "value": "Component where dates for an appointment will be shown" - } - ], "zQE4f2": [ { "type": 0, diff --git a/src/openforms/js/compiled-lang/nl.json b/src/openforms/js/compiled-lang/nl.json index e354b6c13e..f80acfc0da 100644 --- a/src/openforms/js/compiled-lang/nl.json +++ b/src/openforms/js/compiled-lang/nl.json @@ -89,12 +89,6 @@ "value": "Standaardwaarde" } ], - "/c2LWu": [ - { - "type": 0, - "value": "Locatie veld" - } - ], "/fAEsY": [ { "type": 0, @@ -107,12 +101,6 @@ "value": "Maak dit component alleen-lezen" } ], - "/hIJ8m": [ - { - "type": 0, - "value": "Datum veld" - } - ], "/iusiS": [ { "type": 0, @@ -559,12 +547,6 @@ "value": " bestanden om te uploaden." } ], - "4iGsG1": [ - { - "type": 0, - "value": "Geboortedatum veld" - } - ], "4sFGgA": [ { "type": 0, @@ -623,12 +605,6 @@ "value": "Instellen" } ], - "5OXIff": [ - { - "type": 0, - "value": "Veld waar de geboortedatum voor een afspraak komt te staan" - } - ], "5P+YgL": [ { "type": 0, @@ -1289,12 +1265,6 @@ "value": "Onderwerp betaalstatusmail" } ], - "BCjZNo": [ - { - "type": 0, - "value": "Veld waar het telefoonnummer voor een afspraak komt te staan" - } - ], "BGZD6J": [ { "type": 0, @@ -2374,12 +2344,6 @@ "value": "Verborgen" } ], - "JtOgcq": [ - { - "type": 0, - "value": "Achternaam veld" - } - ], "K/Oijt": [ { "type": 0, @@ -2722,12 +2686,6 @@ "value": "Een korte instructie onder het handtekenvlak." } ], - "NwTdLM": [ - { - "type": 0, - "value": "Veld waar beschikbare locaties voor een afspraak komen te staan" - } - ], "NyyJMK": [ { "type": 0, @@ -2906,12 +2864,6 @@ "value": "Aantal jaren. Lege waarden worden genegeerd." } ], - "PiB6Y1": [ - { - "type": 0, - "value": "Tijd veld" - } - ], "PtZ96W": [ { "type": 0, @@ -3973,12 +3925,6 @@ "value": "-attribuut" } ], - "aUChz/": [ - { - "type": 0, - "value": "Product veld" - } - ], "aWwaAq": [ { "type": 0, @@ -5003,12 +4949,6 @@ "value": "Expressie" } ], - "iq0ppL": [ - { - "type": 0, - "value": "Bij het inschakelen van de afsprakenmodus doorloopt de gebruiker een vaste flow." - } - ], "iqAbXt": [ { "type": 0, @@ -6087,12 +6027,6 @@ "value": "Ben je zeker dat je deze koppeling wil verwijderen?" } ], - "t99ynC": [ - { - "type": 0, - "value": "Veld waar de achternaam voor een afspraak komt te staan" - } - ], "tJQoMq": [ { "type": 0, @@ -6401,24 +6335,12 @@ "value": "Het patroon van reguliere expressie waar de waarde aan moet voldoen voor het formulier kan verstuurd worden." } ], - "wjwFwT": [ - { - "type": 0, - "value": "Veld waar beschikbare tijden voor een afspraak komen te staan" - } - ], "wnTTZr": [ { "type": 0, "value": "ID van de schijf/drive waar bestanden terecht moeten komen. Indien leeg, dan wordt de standaard-drive gebruikt." } ], - "wnxKT/": [ - { - "type": 0, - "value": "Veld waar beschikbare producten voor een afspraak komen te staan" - } - ], "wqCiwc": [ { "type": 0, @@ -6467,12 +6389,6 @@ "value": "Sjabloon bevestigingspagina" } ], - "xRQrla": [ - { - "type": 0, - "value": "Afspraken" - } - ], "xTfMk5": [ { "type": 0, @@ -6491,6 +6407,12 @@ "value": "Zoomniveau" } ], + "xxyIg/": [ + { + "type": 0, + "value": "Indicates whether appointments are enabled for this form." + } + ], "xyVtKp": [ { "type": 0, @@ -6591,12 +6513,6 @@ "value": "(niet ingesteld)" } ], - "yYw2Rb": [ - { - "type": 0, - "value": "Telefoonnummer veld" - } - ], "yrz/oC": [ { "type": 0, @@ -6639,12 +6555,6 @@ "value": "Gebruik algemene kaartinstellingen" } ], - "zLTTBw": [ - { - "type": 0, - "value": "Veld waar beschikbare datums voor een afspraak komen te staan" - } - ], "zQE4f2": [ { "type": 0, diff --git a/src/openforms/js/components/admin/form_design/Appointments.js b/src/openforms/js/components/admin/form_design/Appointments.js deleted file mode 100644 index 2895bcf64f..0000000000 --- a/src/openforms/js/components/admin/form_design/Appointments.js +++ /dev/null @@ -1,280 +0,0 @@ -import get from 'lodash/get'; -import PropTypes from 'prop-types'; -import React, {useContext} from 'react'; -import {FormattedMessage} from 'react-intl'; - -import ComponentSelection from 'components/admin/forms/ComponentSelection'; -import Field from 'components/admin/forms/Field'; -import Fieldset from 'components/admin/forms/Fieldset'; -import FormRow from 'components/admin/forms/FormRow'; - -import {FormContext} from './Context'; - -const PREFIX = 'appointments'; // prefix to use in the Formio.js component JSON - -const KEYS = [ - 'showProducts', - 'showLocations', - 'showDates', - 'showTimes', - 'lastName', - 'birthDate', - 'phoneNumber', -]; - -const AppointmentConfigurationComponentSelection = ({ - currentConfiguration, - configKey, - filterType, - onChange, -}) => ( - component.type === filterType} - /> -); - -AppointmentConfigurationComponentSelection.propTypes = { - currentConfiguration: PropTypes.objectOf(PropTypes.string).isRequired, - configKey: PropTypes.oneOf(KEYS).isRequired, - filterType: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired, -}; - -const Appointments = ({onChange}) => { - const formContext = useContext(FormContext); - const availableComponents = formContext.components; - - // extract the current values from the component definitions - const configuration = {}; // key: appointment configuration key, value: component key - Object.entries(availableComponents).forEach(([componentKey, component]) => { - // check the component for any key present - for (const configKey of KEYS) { - const fullPath = `${PREFIX}.${configKey}`; - const value = get(component, fullPath, null); - if (!value) continue; - - configuration[configKey] = componentKey; - // if a config key is found, break out so we exit at the first hit. There - // should only ever be one hit, but in case something was corrupted, don't - // let it break our own state. - break; - } - }); - - /** - * On form field change handler. - * - * We simply invoke the parent handler, but ensure we use the prefixed name so that - * the appropriate custom Formio.js component properties can be set - * @param {Event} event The React event for the changed DOM element. - * @return {void} - */ - const onFieldChange = event => { - const {name, value} = event.target; - const prefixedName = `${PREFIX}.${name}`; - const fakeEvent = { - target: { - name: prefixedName, - value: value, - }, - }; - onChange(fakeEvent); - }; - - return ( - <> -
- - - } - helpText={ - - } - > - - - -
-
- - - } - helpText={ - - } - > - - - -
-
- - - } - helpText={ - - } - > - - - -
-
- - - } - helpText={ - - } - > - - - -
-
- - - } - helpText={ - - } - > - - - -
-
- - - } - helpText={ - - } - > - - - -
-
- - - } - helpText={ - - } - > - - - -
- - ); -}; - -Appointments.propTypes = { - onChange: PropTypes.func.isRequired, -}; - -export default Appointments; -export {KEYS}; diff --git a/src/openforms/js/components/admin/form_design/FormConfigurationFields.js b/src/openforms/js/components/admin/form_design/FormConfigurationFields.js index c242a57f3b..221600dbae 100644 --- a/src/openforms/js/components/admin/form_design/FormConfigurationFields.js +++ b/src/openforms/js/components/admin/form_design/FormConfigurationFields.js @@ -513,7 +513,7 @@ const FormConfigurationFields = ({ } helpText={ } diff --git a/src/openforms/js/components/admin/form_design/form-creation-form.js b/src/openforms/js/components/admin/form_design/form-creation-form.js index 3fd8752430..3e23c72b93 100644 --- a/src/openforms/js/components/admin/form_design/form-creation-form.js +++ b/src/openforms/js/components/admin/form_design/form-creation-form.js @@ -1,6 +1,5 @@ import {produce} from 'immer'; import cloneDeep from 'lodash/cloneDeep'; -import getObjectValue from 'lodash/get'; import groupBy from 'lodash/groupBy'; import set from 'lodash/set'; import sortBy from 'lodash/sortBy'; @@ -20,7 +19,6 @@ import {APIError, NotAuthenticatedError} from 'utils/exception'; import {post} from 'utils/fetch'; import {getUniqueRandomString} from 'utils/random'; -import Appointments, {KEYS as APPOINTMENT_CONFIG_KEYS} from './Appointments'; import Confirmation from './Confirmation'; import {APIContext, FormContext} from './Context'; import DataRemoval from './DataRemoval'; @@ -559,77 +557,7 @@ function reducer(draft, action) { draft.formSteps = [...updatedSteps, ...draft.formSteps.slice(index + 1)]; break; } - case 'APPOINTMENT_CONFIGURATION_CHANGED': { - // deconstruct the 'event' which holds the information on which config param - // was changed and to which component it is (now) set. - const { - target: {name, value: selectedComponentKey}, - } = action.payload; - - // name is in the form "appointments." - const [prefix, configKey] = name.split('.'); - - // utility to find the component for a given appointment config option - const findComponentForConfigKey = configKey => { - const name = `${prefix}.${configKey}`; - return findComponent(draft.formSteps, component => getObjectValue(component, name, false)); - }; - // first, ensure that if the value was changed, the old component is cleared - const currentComponentForConfigKey = findComponentForConfigKey(configKey); - if (currentComponentForConfigKey) { - // wipe the entire appointments configuration - set(currentComponentForConfigKey, prefix, {}); - } - - // next, handle setting the config to the new component - const selectedComponent = findComponent( - draft.formSteps, - component => component.key === selectedComponentKey - ); - set(selectedComponent, name, true); - - // finally, handle the dependencies of all appointment configuration - we need - // to check and update all keys, even the one that wasn't change, because options - // can be set in non-logical order in the UI. - for (const otherConfigKey of APPOINTMENT_CONFIG_KEYS) { - const relevantComponent = findComponentForConfigKey(otherConfigKey); - if (!relevantComponent) continue; - - switch (otherConfigKey) { - // no dependencies, do nothing - case 'showProducts': - case 'lastName': - case 'birthDate': - case 'phoneNumber': - break; - // reverse order without breaks, since every component builds on top of - // the others - case 'showTimes': { - // add the date selection component information - const dateComponent = findComponentForConfigKey('showDates'); - if (dateComponent) set(relevantComponent, `${prefix}.dateComponent`, dateComponent.key); - } - case 'showDates': { - // add the location selection component information - const locationComponent = findComponentForConfigKey('showLocations'); - if (locationComponent) - set(relevantComponent, `${prefix}.locationComponent`, locationComponent.key); - } - case 'showLocations': { - // add the product selection component information - const productComponent = findComponentForConfigKey('showProducts'); - if (productComponent) - set(relevantComponent, `${prefix}.productComponent`, productComponent.key); - break; - } - default: { - throw new Error(`Unknown config key: ${configKey}`); - } - } - } - break; - } /** * Form Logic rules actions */ @@ -1345,14 +1273,6 @@ const FormCreationForm = ({formUuid, formUrl, formHistoryUrl, outgoingRequestsUr ) : null} )} - {!isAppointment && ( - - - - )} {!isAppointment && ( variableHasErrors(variable))}> @@ -1467,19 +1387,6 @@ const FormCreationForm = ({formUuid, formUrl, formHistoryUrl, outgoingRequestsUr )} - {!isAppointment && ( - - { - dispatch({ - type: 'APPOINTMENT_CONFIGURATION_CHANGED', - payload: event, - }); - }} - /> - - )} - {!isAppointment && ( SubmissionStep | None: submission_state = self.load_execution_state() return submission_state.get_last_completed_step() - def get_merged_appointment_data(self) -> dict[str, dict[str, str | dict]]: - component_config_key_to_appointment_key = { - "appointments.showProducts": "productIDAndName", - "appointments.showLocations": "locationIDAndName", - "appointments.showTimes": "appStartTime", - "appointments.lastName": "clientLastName", - "appointments.birthDate": "clientDateOfBirth", - "appointments.phoneNumber": "clientPhoneNumber", - } - - merged_data = self.data - appointment_data = {} - - for component in self.form.iter_components(recursive=True): - # is this component any of the keys were looking for? - for ( - component_key, - appointment_key, - ) in component_config_key_to_appointment_key.items(): - is_the_right_component = glom(component, component_key, default=False) - if not is_the_right_component: - continue - - # it is the right component, get the value and store it - appointment_data[appointment_key] = { - "label": component["label"], - "value": merged_data.get(component["key"]), - } - break - - return appointment_data - @property def data(self) -> dict[str, Any]: """The filled-in data of the submission. diff --git a/src/openforms/submissions/query.py b/src/openforms/submissions/query.py index 6abc781138..0d611447a4 100644 --- a/src/openforms/submissions/query.py +++ b/src/openforms/submissions/query.py @@ -1,9 +1,8 @@ from __future__ import annotations -import copy from typing import TYPE_CHECKING -from django.db import models, transaction +from django.db import models from django.db.models import ( Case, CharField, @@ -18,11 +17,9 @@ from django.utils import timezone from openforms.config.models import GlobalConfiguration -from openforms.logging import logevent -from openforms.payments.models import SubmissionPayment if TYPE_CHECKING: - from .models import Submission + from .models import Submission # noqa class SubmissionQuerySet(models.QuerySet["Submission"]): @@ -58,60 +55,3 @@ def annotate_removal_fields( ) return annotation - - -class SubmissionManager(models.Manager.from_queryset(SubmissionQuerySet)): - @transaction.atomic - def copy( - self, - original: "Submission", - fields=( - "form", - "form_url", - ), - ) -> "Submission": - """ - Copy an existing submission into a new, cleaned submission record. - - The new submission has the meta fields cleared, but existing submitted data - copied over. - - :arg submission: An existing :class:`Submission` instance - :arg fields: iterable of model field names to copy - """ - from .models import SubmissionStep - - new_instance = self.create( - previous_submission=original, # store the reference from where it was copied - **{field: getattr(original, field) for field in fields}, - ) - if hasattr(original, "auth_info"): - new_auth_info = copy.deepcopy(original.auth_info) - new_auth_info.pk = None - new_auth_info.submission = new_instance - new_auth_info.save() - - new_steps = [] - related_steps_manager = original.submissionstep_set - for step in related_steps_manager.all(): - new_steps.append( - SubmissionStep( - submission=new_instance, - form_step=step.form_step, - data=step.data, - ) - ) - related_steps_manager.bulk_create(new_steps) - - if original.payment_required and original.payment_user_has_paid: - submission_payment = SubmissionPayment.objects.get(submission=original) - submission_payment.submission = new_instance - submission_payment.save() - - logevent.payment_transfer_to_new_submission( - submission_payment=submission_payment, - old_submission=original, - new_submission=new_instance, - ) - - return new_instance diff --git a/src/openforms/submissions/tasks/__init__.py b/src/openforms/submissions/tasks/__init__.py index 38c80485a1..b890edee7d 100644 --- a/src/openforms/submissions/tasks/__init__.py +++ b/src/openforms/submissions/tasks/__init__.py @@ -33,8 +33,7 @@ def on_post_submission_event(submission_id: int, event: PostSubmissionEvents) -> # this can run any time because they have been claimed earlier cleanup_temporary_files_for.delay(submission_id) - # If the form involves appointments and no appointment has been scheduled yet, schedule it. - # Todo: deprecated => Not needed with the new appointment flow + # Register an appointment if the submission is for a form which is configured to create appointments. register_appointment_task = maybe_register_appointment.si(submission_id) # Perform any pre-registration task specified by the registration plugin. If no registration plugin is configured, diff --git a/src/openforms/submissions/tests/factories.py b/src/openforms/submissions/tests/factories.py index 77d4e1ba7a..92093e66d8 100644 --- a/src/openforms/submissions/tests/factories.py +++ b/src/openforms/submissions/tests/factories.py @@ -113,12 +113,6 @@ class Params: registration_status=RegistrationStatuses.in_progress, pre_registration_completed=True, ) - has_previous_submission = factory.Trait( - previous_submission=factory.SubFactory( - "openforms.submissions.tests.factories.SubmissionFactory", - form=factory.SelfAttribute("..form"), - ) - ) with_report = factory.Trait( report=factory.RelatedFactory( "openforms.submissions.tests.factories.SubmissionReportFactory", diff --git a/src/openforms/submissions/tests/test_models.py b/src/openforms/submissions/tests/test_models.py index ac8e757e76..282059960f 100644 --- a/src/openforms/submissions/tests/test_models.py +++ b/src/openforms/submissions/tests/test_models.py @@ -223,137 +223,6 @@ def test_submission_delete_file_uploads_cascade_file_already_gone(self): ) self.assertFalse(attachment.content.storage.exists(attachment.content.path)) - def test_get_merged_appointment_data(self): - form = FormFactory.create() - form_definition_1 = FormDefinitionFactory.create( - configuration={ - "display": "form", - "components": [ - { - "key": "product", - "type": "textfield", - "appointments": {"showProducts": True}, - "label": "Product", - }, - { - "key": "location", - "type": "textfield", - "appointments": {"showLocations": True}, - "label": "Location", - }, - { - "key": "time", - "type": "textfield", - "appointments": {"showTimes": True}, - "label": "Time", - }, - ], - } - ) - form_definition_2 = FormDefinitionFactory.create( - configuration={ - "display": "form", - "components": [ - { - "key": "lastName", - "type": "textfield", - "appointments": {"lastName": True}, - "label": "Last Name", - }, - { - "key": "birthDate", - "type": "date", - "appointments": {"birthDate": True}, - "label": "Date of Birth", - }, - { - "key": "phoneNumber", - "type": "textfield", - "appointments": {"phoneNumber": True}, - "label": "Phone Number", - }, - { - "key": "randomAttribute", - "type": "textfield", - "appointments": {"birthDate": False}, - "label": "Random attribute", - }, - ], - } - ) - form_step_1 = FormStepFactory.create( - form=form, form_definition=form_definition_1 - ) - form_step_2 = FormStepFactory.create( - form=form, form_definition=form_definition_2 - ) - submission = SubmissionFactory.create(form=form) - SubmissionStepFactory.create( - submission=submission, - data={ - "product": {"identifier": "79", "name": "Paspoort"}, - "location": {"identifier": "1", "name": "Amsterdam"}, - "time": "2021-08-25T17:00:00", - }, - form_step=form_step_1, - ) - SubmissionStepFactory.create( - submission=submission, - data={ - "lastName": "Maykin", - "birthDate": "1990-08-01", - "phoneNumber": "+31 20 753 05 23", - "randomAttribute": "This is some random stuff", - }, - form_step=form_step_2, - ) - - self.assertEqual( - submission.get_merged_appointment_data(), - { - "productIDAndName": { - "label": "Product", - "value": {"identifier": "79", "name": "Paspoort"}, - }, - "locationIDAndName": { - "label": "Location", - "value": {"identifier": "1", "name": "Amsterdam"}, - }, - "appStartTime": {"label": "Time", "value": "2021-08-25T17:00:00"}, - "clientLastName": {"label": "Last Name", "value": "Maykin"}, - "clientDateOfBirth": {"label": "Date of Birth", "value": "1990-08-01"}, - "clientPhoneNumber": { - "label": "Phone Number", - "value": "+31 20 753 05 23", - }, - }, - ) - - def test_copy_submission(self): - submission = SubmissionFactory.create(auth_info__value="bsn1") - SubmissionStepFactory.create( - submission=submission, - data={"key1": "value1", "key2": "value2"}, - form_step=FormStepFactory.create(), - ) - SubmissionStepFactory.create( - submission=submission, - data={"key3": "value-b"}, - form_step=FormStepFactory.create(), - ) - - new_submission = Submission.objects.copy(submission) - - self.assertEqual(new_submission.form, submission.form) - self.assertEqual(new_submission.form_url, submission.form_url) - self.assertEqual(new_submission.previous_submission, submission) - self.assertEqual(new_submission.data, submission.data) - self.assertEqual(new_submission.auth_info.value, "bsn1") - self.assertEqual(new_submission.auth_info.plugin, "digid") - self.assertEqual(new_submission.auth_info.attribute, AuthAttribute.bsn) - self.assertNotEqual(new_submission.id, submission.id) - self.assertNotEqual(new_submission.uuid, submission.uuid) - @override_settings(CORS_ALLOW_ALL_ORIGINS=True) def test_co_sign_data_validation(self): extra_fields = { diff --git a/src/openforms/submissions/tests/test_post_submission_event.py b/src/openforms/submissions/tests/test_post_submission_event.py index bffe0ecab4..e4a975d28b 100644 --- a/src/openforms/submissions/tests/test_post_submission_event.py +++ b/src/openforms/submissions/tests/test_post_submission_event.py @@ -10,13 +10,11 @@ from privates.test import temp_private_root from testfixtures import LogCapture -from openforms.appointments.exceptions import AppointmentRegistrationFailed -from openforms.appointments.tests.utils import setup_jcc from openforms.authentication.service import AuthAttribute from openforms.config.models import GlobalConfiguration from openforms.emails.tests.factories import ConfirmationEmailTemplateFactory from openforms.forms.constants import LogicActionTypes, PropertyTypes -from openforms.forms.tests.factories import FormDefinitionFactory, FormLogicFactory +from openforms.forms.tests.factories import FormLogicFactory from openforms.payments.constants import PaymentStatus from openforms.payments.tests.factories import SubmissionPaymentFactory from openforms.registrations.base import PreRegistrationResult @@ -849,21 +847,6 @@ def test_payment_status_update_retry_flow(self): self.assertEqual(len(mails), 0) self.assertNotEqual(submission.auth_info.value, "111222333") - def test_submission_completed_incomplete_appointment(self): - setup_jcc() - components = FormDefinitionFactory.build(is_appointment=True).configuration[ - "components" - ] - submission = SubmissionFactory.from_components( - completed=True, - form__registration_backend="", - components_list=components, - submitted_data={"product": {"identifier": "79", "name": "Paspoort"}}, - ) - - with self.assertRaises(AppointmentRegistrationFailed): - on_post_submission_event(submission.id, PostSubmissionEvents.on_completion) - def test_cosign_not_required_and_not_filled_in_proceeds_with_registration(self): submission = SubmissionFactory.from_components( components_list=[ diff --git a/src/openforms/submissions/tests/test_tasks_confirmation_emails.py b/src/openforms/submissions/tests/test_tasks_confirmation_emails.py index a5b6363816..2194501216 100644 --- a/src/openforms/submissions/tests/test_tasks_confirmation_emails.py +++ b/src/openforms/submissions/tests/test_tasks_confirmation_emails.py @@ -6,12 +6,10 @@ from django.core import mail from django.db import close_old_connections from django.test import TestCase, TransactionTestCase, override_settings -from django.utils.translation import gettext as _, override as override_language +from django.utils.translation import override as override_language from privates.test import temp_private_root -from openforms.appointments.constants import AppointmentDetailsStatus -from openforms.appointments.tests.factories import AppointmentInfoFactory from openforms.config.models import GlobalConfiguration from openforms.emails.models import ConfirmationEmailTemplate from openforms.emails.tests.factories import ConfirmationEmailTemplateFactory @@ -477,47 +475,6 @@ def test_completed_submission_after_timeout_with_confirmation_email_when_already # assert that no e-mail was sent self.assertEqual(len(mail.outbox), 0) - @override_settings(DEFAULT_FROM_EMAIL="info@open-forms.nl") - def test_send_confirmation_email_when_appointment_is_changed(self): - submission = SubmissionFactory.from_components( - completed=True, - components_list=[ - { - "key": "email", - "type": "email", - "label": "Email", - "confirmationRecipient": True, - }, - ], - submitted_data={"email": "test@test.nl"}, - has_previous_submission=True, - ) - AppointmentInfoFactory.create( - submission=submission.previous_submission, - status=AppointmentDetailsStatus.cancelled, - ) - # add a second step - SubmissionStepFactory.create( - submission=submission, - form_step__form=submission.form, - data={"foo": "bar"}, - ) - ConfirmationEmailTemplateFactory.create( - form=submission.form, - subject="Confirmation mail", - content="Information filled in: {{foo}}", - ) - - # "execute" the celery task - with override_settings(CELERY_TASK_ALWAYS_EAGER=True): - schedule_emails(submission.id) - - # Verify that email was sent - self.assertEqual(len(mail.outbox), 1) - - message = mail.outbox[0] - self.assertEqual(message.subject, f"Confirmation mail {_('(updated)')}") - def test_template_is_rendered_in_submission_language(self): """ Assert a subset of the components with particularly weird APIs is translated correctly. diff --git a/src/openforms/submissions/utils.py b/src/openforms/submissions/utils.py index d2bf778f2e..762b5f036a 100644 --- a/src/openforms/submissions/utils.py +++ b/src/openforms/submissions/utils.py @@ -15,7 +15,6 @@ from rest_framework.request import Request from rest_framework.reverse import reverse -from openforms.appointments.utils import get_confirmation_mail_suffix from openforms.emails.confirmation_emails import ( get_confirmation_email_context_data, get_confirmation_email_templates, @@ -190,9 +189,6 @@ def send_confirmation_email(submission: Submission) -> None: subject_template, context, rendering_text=True, disable_autoescape=True ).strip() - if subject_suffix := get_confirmation_mail_suffix(submission): - subject = f"{subject} {subject_suffix}" - html_content = render_email_template(content_template, context) text_content = strip_tags_plus( render_email_template(content_template, context, rendering_text=True),