From 5b5cc0eca6826f2a7127d4b82d23e68be698c2a7 Mon Sep 17 00:00:00 2001 From: vasileios Date: Fri, 6 Dec 2024 16:36:21 +0100 Subject: [PATCH] [#3283] Removed-refactored appointments according to the new flow --- .../appointments/contrib/demo/plugin.py | 2 +- .../appointments/contrib/jcc/plugin.py | 30 +- .../appointments/contrib/qmatic/plugin.py | 34 +- src/openforms/appointments/core.py | 2 - .../management/commands/appointment.py | 6 +- src/openforms/appointments/tasks.py | 36 +- src/openforms/appointments/utils.py | 237 +----- .../tests/files/appointments_components.json | 805 ------------------ .../tests/files/appointments_data.json | 10 - src/openforms/logging/logevent.py | 16 - src/openforms/submissions/admin.py | 3 +- ...3_remove_submission_previous_submission.py | 17 + .../submissions/models/submission.py | 41 +- src/openforms/submissions/query.py | 62 +- src/openforms/submissions/tasks/__init__.py | 3 +- src/openforms/submissions/tests/factories.py | 6 - src/openforms/submissions/utils.py | 4 - 17 files changed, 36 insertions(+), 1278 deletions(-) create mode 100644 src/openforms/submissions/migrations/0013_remove_submission_previous_submission.py 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/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/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/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/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/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/logging/logevent.py b/src/openforms/logging/logevent.py index 4c5fe0d91c..c50634f453 100644 --- a/src/openforms/logging/logevent.py +++ b/src/openforms/logging/logevent.py @@ -428,22 +428,6 @@ def payment_register_failure(payment: SubmissionPayment, plugin, error: Exceptio ) -def payment_transfer_to_new_submission( - submission_payment: SubmissionPayment, - old_submission: Submission, - new_submission: Submission, -): - _create_log( - object=old_submission, - event="transfer_payment_to_submission_copy", - extra_data={ - "payment_uuid": str(submission_payment.uuid), - "old_submission_id": old_submission.id, - "new_submission_id": new_submission.id, - }, - ) - - # - - - diff --git a/src/openforms/submissions/admin.py b/src/openforms/submissions/admin.py index 9a41a71a02..a392a00a23 100644 --- a/src/openforms/submissions/admin.py +++ b/src/openforms/submissions/admin.py @@ -325,14 +325,13 @@ class SubmissionAdmin(admin.ModelAdmin): "privacy_policy_accepted", "statement_of_truth_accepted", "_is_cleaned", - "previous_submission", "initial_data_reference", ), "classes": ("collapse",), }, ), ) - raw_id_fields = ("form", "previous_submission") + raw_id_fields = ("form",) actions = [ "export_csv", "export_json", diff --git a/src/openforms/submissions/migrations/0013_remove_submission_previous_submission.py b/src/openforms/submissions/migrations/0013_remove_submission_previous_submission.py new file mode 100644 index 0000000000..ac43edb7f6 --- /dev/null +++ b/src/openforms/submissions/migrations/0013_remove_submission_previous_submission.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.16 on 2024-12-02 11:15 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("submissions", "0012_alter_submission_price"), + ] + + operations = [ + migrations.RemoveField( + model_name="submission", + name="previous_submission", + ), + ] diff --git a/src/openforms/submissions/models/submission.py b/src/openforms/submissions/models/submission.py index c770490228..967c381991 100644 --- a/src/openforms/submissions/models/submission.py +++ b/src/openforms/submissions/models/submission.py @@ -35,7 +35,7 @@ ) from ..cosigning import CosignState from ..pricing import get_submission_price -from ..query import SubmissionManager +from ..query import SubmissionQuerySet from ..serializers import CoSignDataSerializer from .submission_step import SubmissionStep @@ -296,11 +296,6 @@ class Submission(models.Model): ), ) - # relation to earlier submission which is altered after processing - previous_submission = models.ForeignKey( - "submissions.Submission", on_delete=models.SET_NULL, null=True, blank=True - ) - language_code = models.CharField( _("language code"), max_length=2, @@ -319,7 +314,7 @@ class Submission(models.Model): help_text=_("The key of the registration backend to use."), ) - objects = SubmissionManager() + objects = SubmissionQuerySet.as_manager() _form_login_required: bool | None = None # can be set via annotation _prefilled_data = None @@ -695,38 +690,6 @@ def get_last_completed_step(self) -> 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..222e014cc1 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,8 +17,6 @@ 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 @@ -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/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),