diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ddb189895f..d69e5c5d41 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -448,7 +448,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - start: ['2.7.4', '2.7.8'] + start: ['2.8.2'] steps: - uses: actions/checkout@v4 diff --git a/src/openforms/analytics_tools/migrations/0001_initial_to_v300.py b/src/openforms/analytics_tools/migrations/0001_initial_to_v300.py new file mode 100644 index 0000000000..0832d5b4bc --- /dev/null +++ b/src/openforms/analytics_tools/migrations/0001_initial_to_v300.py @@ -0,0 +1,367 @@ +# Generated by Django 4.2.17 on 2024-12-27 13:03 + +import django.db.models.deletion +from django.db import migrations, models + +import openforms.analytics_tools.validators + + +class Migration(migrations.Migration): + + replaces = [ + ("analytics_tools", "0001_initial"), + ("analytics_tools", "0002_auto_20230119_1500"), + ("analytics_tools", "0003_cspsetting_identifier"), + ( + "analytics_tools", + "0004_analyticstoolsconfiguration_enable_piwik_pro_tag_manager", + ), + ("analytics_tools", "0005_auto_20231206_1202"), + ("analytics_tools", "0006_auto_20240112_1046"), + ( + "analytics_tools", + "0007_alter_analyticstoolsconfiguration_analytics_cookie_consent_group", + ), + ( + "analytics_tools", + "0008_analyticstoolsconfiguration_govmetric_secure_guid_form_abort_and_more", + ), + ("analytics_tools", "0009_convert_ids_govmetric"), + ( + "analytics_tools", + "0010_remove_analyticstoolsconfiguration_govmetric_secure_guid_and_more", + ), + ( + "analytics_tools", + "0011_analyticstoolsconfiguration_enable_expoints_analytics_and_more", + ), + ] + + dependencies = [ + ("cookie_consent", "0002_auto__add_logitem"), + ("config", "0001_initial_to_v250"), + ] + + operations = [ + migrations.CreateModel( + name="AnalyticsToolsConfiguration", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "gtm_code", + models.CharField( + blank=True, + help_text="Typically looks like 'GTM-XXXX'. Supplying this installs Google Tag Manager.", + max_length=50, + verbose_name="Google Tag Manager code", + ), + ), + ( + "ga_code", + models.CharField( + blank=True, + help_text="Typically looks like 'UA-XXXXX-Y'. Supplying this installs Google Analytics.", + max_length=50, + verbose_name="Google Analytics code", + ), + ), + ( + "enable_google_analytics", + models.BooleanField( + default=False, + help_text="Enabling this installs Google Analytics", + verbose_name="enable google analytics", + ), + ), + ( + "matomo_url", + models.URLField( + blank=True, + help_text="The base URL of your Matomo server, e.g. 'https://matomo.example.com'.", + max_length=255, + validators=[ + openforms.analytics_tools.validators.validate_no_trailing_slash + ], + verbose_name="Matomo server URL", + ), + ), + ( + "matomo_site_id", + models.PositiveIntegerField( + blank=True, + help_text="The 'idsite' of the website you're tracking in Matomo.", + null=True, + verbose_name="Matomo site ID", + ), + ), + ( + "enable_matomo_site_analytics", + models.BooleanField( + default=False, + help_text="Enabling this installs Matomo", + verbose_name="enable matomo site analytics", + ), + ), + ( + "piwik_url", + models.URLField( + blank=True, + help_text="The base URL of your Piwik server, e.g. 'https://piwik.example.com'.", + max_length=255, + validators=[ + openforms.analytics_tools.validators.validate_no_trailing_slash + ], + verbose_name="Piwik server URL", + ), + ), + ( + "piwik_site_id", + models.PositiveIntegerField( + blank=True, + help_text="The 'idsite' of the website you're tracking in Piwik.", + null=True, + verbose_name="Piwik site ID", + ), + ), + ( + "enable_piwik_site_analytics", + models.BooleanField( + default=False, + help_text="Enabling this installs Piwik", + verbose_name="enable piwik site analytics", + ), + ), + ( + "piwik_pro_url", + models.URLField( + blank=True, + help_text="The base URL of your Piwik PRO server, e.g. 'https://your-instance-name.piwik.pro'.", + max_length=255, + validators=[ + openforms.analytics_tools.validators.validate_no_trailing_slash + ], + verbose_name="Piwik PRO server URL", + ), + ), + ( + "piwik_pro_site_id", + models.UUIDField( + blank=True, + help_text="The 'idsite' of the website you're tracking in Piwik PRO. https://help.piwik.pro/support/questions/find-website-id/", + null=True, + verbose_name="Piwik PRO site ID", + ), + ), + ( + "enable_piwik_pro_site_analytics", + models.BooleanField( + default=False, + help_text="Enabling this installs Piwik PRO Analytics", + verbose_name="enable Piwik PRO Site Analytics", + ), + ), + ( + "siteimprove_id", + models.CharField( + blank=True, + help_text="Your SiteImprove ID - you can find this from the embed snippet example, which should contain a URL like '//siteimproveanalytics.com/js/siteanalyze_XXXXX.js'. The XXXXX is your ID.", + max_length=10, + verbose_name="SiteImprove ID", + ), + ), + ( + "enable_siteimprove_analytics", + models.BooleanField( + default=False, + help_text="Enabling this installs SiteImprove", + verbose_name="enable siteImprove analytics", + ), + ), + ( + "analytics_cookie_consent_group", + models.ForeignKey( + help_text="The cookie group used for analytical cookies. The analytics scripts are loaded only if this cookie group is accepted by the end-user.", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="cookie_consent.cookiegroup", + ), + ), + ( + "enable_piwik_pro_tag_manager", + models.BooleanField( + default=False, + help_text="Enabling this installs Piwik PRO Tag Manager.", + verbose_name="enable piwik Pro Tag Manager", + ), + ), + ( + "enable_govmetric_analytics", + models.BooleanField( + default=False, + help_text="This enables GovMetric to collect data while a user fills in a form and it adds a button at the end of a form to fill in a client satisfaction survey.", + verbose_name="enable GovMetric analytics", + ), + ), + ( + "govmetric_secure_guid_form_aborted", + models.CharField( + blank=True, + help_text="Your GovMetric secure GUID for when a form is aborted - This is an optional value. It is created by KLANTINFOCUS when a list of questions is created. It is a string that is unique per set of questions.", + max_length=50, + verbose_name="GovMetric secure GUID form aborted", + ), + ), + ( + "govmetric_secure_guid_form_finished", + models.CharField( + blank=True, + help_text="Your GovMetric secure GUID for when a form is finished - This is an optional value. It is created by KLANTINFOCUS when a list of questions is created. It is a string that is unique per set of questions.", + max_length=50, + verbose_name="GovMetric secure GUID form finished", + ), + ), + ( + "govmetric_source_id_form_aborted", + models.CharField( + blank=True, + help_text="Your GovMetric source ID for when a form is aborted - This is created by KLANTINFOCUS when a list of questions is created. It is a numerical value that is unique per set of questions.", + max_length=10, + verbose_name="GovMetric source ID form aborted", + ), + ), + ( + "govmetric_source_id_form_finished", + models.CharField( + blank=True, + help_text="Your GovMetric source ID for when a form is finished - This is created by KLANTINFOCUS when a list of questions is created. It is a numerical value that is unique per set of questions.", + max_length=10, + verbose_name="GovMetric source ID form finished", + ), + ), + ( + "govmetric_secure_guid_form_aborted_en", + models.CharField( + blank=True, + help_text="Your GovMetric secure GUID for when a form is aborted - This is an optional value. It is created by KLANTINFOCUS when a list of questions is created. It is a string that is unique per set of questions.", + max_length=50, + null=True, + verbose_name="GovMetric secure GUID form aborted", + ), + ), + ( + "govmetric_secure_guid_form_aborted_nl", + models.CharField( + blank=True, + help_text="Your GovMetric secure GUID for when a form is aborted - This is an optional value. It is created by KLANTINFOCUS when a list of questions is created. It is a string that is unique per set of questions.", + max_length=50, + null=True, + verbose_name="GovMetric secure GUID form aborted", + ), + ), + ( + "govmetric_secure_guid_form_finished_en", + models.CharField( + blank=True, + help_text="Your GovMetric secure GUID for when a form is finished - This is an optional value. It is created by KLANTINFOCUS when a list of questions is created. It is a string that is unique per set of questions.", + max_length=50, + null=True, + verbose_name="GovMetric secure GUID form finished", + ), + ), + ( + "govmetric_secure_guid_form_finished_nl", + models.CharField( + blank=True, + help_text="Your GovMetric secure GUID for when a form is finished - This is an optional value. It is created by KLANTINFOCUS when a list of questions is created. It is a string that is unique per set of questions.", + max_length=50, + null=True, + verbose_name="GovMetric secure GUID form finished", + ), + ), + ( + "govmetric_source_id_form_aborted_en", + models.CharField( + blank=True, + help_text="Your GovMetric source ID for when a form is aborted - This is created by KLANTINFOCUS when a list of questions is created. It is a numerical value that is unique per set of questions.", + max_length=10, + null=True, + verbose_name="GovMetric source ID form aborted", + ), + ), + ( + "govmetric_source_id_form_aborted_nl", + models.CharField( + blank=True, + help_text="Your GovMetric source ID for when a form is aborted - This is created by KLANTINFOCUS when a list of questions is created. It is a numerical value that is unique per set of questions.", + max_length=10, + null=True, + verbose_name="GovMetric source ID form aborted", + ), + ), + ( + "govmetric_source_id_form_finished_en", + models.CharField( + blank=True, + help_text="Your GovMetric source ID for when a form is finished - This is created by KLANTINFOCUS when a list of questions is created. It is a numerical value that is unique per set of questions.", + max_length=10, + null=True, + verbose_name="GovMetric source ID form finished", + ), + ), + ( + "govmetric_source_id_form_finished_nl", + models.CharField( + blank=True, + help_text="Your GovMetric source ID for when a form is finished - This is created by KLANTINFOCUS when a list of questions is created. It is a numerical value that is unique per set of questions.", + max_length=10, + null=True, + verbose_name="GovMetric source ID form finished", + ), + ), + ( + "enable_expoints_analytics", + models.BooleanField( + default=False, + help_text="This adds a button at the end of a form to fill in a client satisfaction survey using Expoints.", + verbose_name="enable Expoints analytics", + ), + ), + ( + "expoints_config_uuid", + models.CharField( + blank=True, + help_text="The UUID used to retrieve the configuration from Expoints to initialize the client satisfaction survey.", + max_length=50, + verbose_name="Expoints configuration identifier", + ), + ), + ( + "expoints_organization_name", + models.SlugField( + blank=True, + help_text="The name of your organization as registered in Expoints. This is used to construct the URL to communicate with Expoints.", + verbose_name="Expoints organization name", + ), + ), + ( + "expoints_use_test_mode", + models.BooleanField( + default=False, + help_text="Indicates whether or not the test mode should be enabled. If enabled, filled out surveys won't actually be sent, to avoid cluttering Expoints while testing.", + verbose_name="use Expoints test mode", + ), + ), + ], + options={ + "verbose_name": "Analytics tools configuration", + }, + ), + ] diff --git a/src/openforms/config/migrations/0054_v250_to_v270.py b/src/openforms/config/migrations/0054_v250_to_v270.py index cdebf60654..113c5f7302 100644 --- a/src/openforms/config/migrations/0054_v250_to_v270.py +++ b/src/openforms/config/migrations/0054_v250_to_v270.py @@ -5,59 +5,12 @@ from django.db import migrations, models import tinymce.models -from flags.conditions import boolean_condition import openforms.config.models.config import openforms.emails.validators import openforms.payments.validators import openforms.template.validators -FIELDS = ( - "enable_demo_plugins", - "display_sdk_information", -) - - -def move_from_config_to_flagstate(apps, _): - GlobalConfiguration = apps.get_model("config", "GlobalConfiguration") - FlagState = apps.get_model("flags", "FlagState") - - # ensure we have an instance, for a fresh install, this will set up the - # defaults explicitly. - config = GlobalConfiguration.objects.first() or GlobalConfiguration() - for field in FIELDS: - current_value = getattr(config, field) - FlagState.objects.get_or_create( - name=field.upper(), - defaults={ - "condition": "boolean", - "value": str(current_value), - "required": False, - }, - ) - - -def move_from_flagstate_to_config(apps, _): - GlobalConfiguration = apps.get_model("config", "GlobalConfiguration") - FlagState = apps.get_model("flags", "FlagState") - - # if there's no config, there's nothing to do - config = GlobalConfiguration.objects.first() - if config is None: - return - - for field in FIELDS: - flag_state = FlagState.objects.filter( - name=field.upper(), condition="boolean" - ).first() - if flag_state is None: - continue - - value = boolean_condition(flag_state.value) - setattr(config, field, value) - - config.save() - class Migration(migrations.Migration): @@ -162,10 +115,8 @@ class Migration(migrations.Migration): model_name="globalconfiguration", name="show_form_link_in_cosign_email", ), - migrations.RunPython( - code=move_from_config_to_flagstate, - reverse_code=move_from_flagstate_to_config, - ), + # RunPython operation removed as part of 3.0 release cycle - these migrations are + # guaranteed to have been executed on Open Forms 2.8.x for existing instances. migrations.RemoveField( model_name="globalconfiguration", name="display_sdk_information", diff --git a/src/openforms/config/migrations/0055_v270_to_v300.py b/src/openforms/config/migrations/0055_v270_to_v300.py new file mode 100644 index 0000000000..06f082a042 --- /dev/null +++ b/src/openforms/config/migrations/0055_v270_to_v300.py @@ -0,0 +1,741 @@ +# Generated by Django 4.2.17 on 2024-12-27 13:58 + +import functools +import re + +import django.core.validators +from django.db import migrations, models +from django.db.migrations.state import StateApps + +import django_jsonform.models.fields +import tinymce.models + +import openforms.config.models.config +import openforms.emails.validators +import openforms.template.validators +import openforms.utils.translations +from openforms.utils.migrations_utils.fix_default_translation import ( + FixDefaultTranslations, +) + + +def replace_tag(tpl: str) -> str: + return re.sub( + r"{%\s*summary\s*%}", + "{% confirmation_summary %}", + tpl, + ) + + +def update_summary_tags(apps: StateApps, _): + GlobalConfiguration = apps.get_model("config", "GlobalConfiguration") + config = GlobalConfiguration.objects.first() + if config is None: + return + + # the cosign fields are new in 3.0.0 so they're not expected to hold the legacy + # tag. + config.submission_confirmation_template_en = replace_tag( + config.submission_confirmation_template_en + ) + config.submission_confirmation_template_nl = replace_tag( + config.submission_confirmation_template_nl + ) + config.confirmation_email_content_nl = replace_tag( + config.confirmation_email_content_nl + ) + config.confirmation_email_content_en = replace_tag( + config.confirmation_email_content_en + ) + config.save() + + +class Migration(migrations.Migration): + + replaces = [ + ( + "config", + "0059_alter_globalconfiguration_form_upload_default_file_types", + ), + ( + "config", + "0062_merge_backport_zip_mimetypes", + ), + ( + "config", + "0055_globalconfiguration_email_verification_request_content_and_more", + ), + ("config", "0056_disable_prefill_objects_api_plugin"), + ("config", "0060_merge_20240920_1816"), + ("config", "0063_merge_20240923_1612"), + ("config", "0064_alter_globalconfiguration_submissions_removal_limit"), + ( + "config", + "0065_globalconfiguration_cosign_submission_confirmation_template_and_more", + ), + ( + "config", + "0066_alter_globalconfiguration_cosign_submission_confirmation_template_and_more", + ), + ( + "config", + "0067_globalconfiguration_cosign_confirmation_email_content_and_more", + ), + ("config", "0068_alter_globalconfiguration_cosign_request_template_and_more"), + ("config", "0068_update_summary_tags"), + ("config", "0069_maptilelayer"), + ] + + dependencies = [ + ("config", "0054_v250_to_v270"), + ] + + operations = [ + migrations.AlterField( + model_name="globalconfiguration", + name="form_upload_default_file_types", + field=django_jsonform.models.fields.ArrayField( + base_field=models.CharField( + choices=[ + ("*", "any filetype"), + ("image/heic", ".heic"), + ("image/png", ".png"), + ("image/jpeg", ".jpg"), + ("application/pdf", ".pdf"), + ("application/vnd.ms-excel", ".xls"), + ( + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + ".xlsx", + ), + ("text/csv", ".csv"), + ("text/plain", ".txt"), + ("application/msword", ".doc"), + ( + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + ".docx", + ), + ( + "application/vnd.oasis.opendocument.*,application/vnd.stardivision.*,application/vnd.sun.xml.*", + "Open Office", + ), + ( + "application/zip,application/zip-compressed,application/x-zip-compressed", + ".zip", + ), + ("application/vnd.rar", ".rar"), + ("application/x-tar", ".tar"), + ("application/vnd.ms-outlook", ".msg"), + ( + "application/acad.dwg,application/autocad_dwg.dwg,application/dwg.dwg,application/x-acad.dwg,application/x-autocad.dwg,application/x-dwg.dwg,drawing/dwg.dwg,image/vnd.dwg,image/x-dwg.dwg", + ".dwg", + ), + ], + max_length=256, + ), + blank=True, + default=list, + help_text="Provide a list of default allowed file upload types. If empty, all extensions are allowed.", + size=None, + verbose_name="Default allowed file upload types", + ), + ), + migrations.AddField( + model_name="globalconfiguration", + name="email_verification_request_content", + field=tinymce.models.HTMLField( + default=functools.partial( + openforms.config.models.config._render, + *("emails/email_verification/request.html",), + **{} + ), + help_text="Content of the email verification email message.", + validators=[ + openforms.template.validators.DjangoTemplateValidator(), + openforms.emails.validators.URLSanitationValidator(), + ], + verbose_name="content", + ), + ), + migrations.AddField( + model_name="globalconfiguration", + name="email_verification_request_content_en", + field=tinymce.models.HTMLField( + default=functools.partial( + openforms.config.models.config._render, + *("emails/email_verification/request.html",), + **{} + ), + help_text="Content of the email verification email message.", + null=True, + validators=[ + openforms.template.validators.DjangoTemplateValidator(), + openforms.emails.validators.URLSanitationValidator(), + ], + verbose_name="content", + ), + ), + migrations.AddField( + model_name="globalconfiguration", + name="email_verification_request_content_nl", + field=tinymce.models.HTMLField( + default=functools.partial( + openforms.config.models.config._render, + *("emails/email_verification/request.html",), + **{} + ), + help_text="Content of the email verification email message.", + null=True, + validators=[ + openforms.template.validators.DjangoTemplateValidator(), + openforms.emails.validators.URLSanitationValidator(), + ], + verbose_name="content", + ), + ), + migrations.AddField( + model_name="globalconfiguration", + name="email_verification_request_subject", + field=models.CharField( + default=functools.partial( + openforms.config.models.config._render, + *("emails/email_verification/subject.txt",), + **{} + ), + help_text="Subject of the email verification email.", + max_length=1000, + validators=[openforms.template.validators.DjangoTemplateValidator()], + verbose_name="subject", + ), + ), + migrations.AddField( + model_name="globalconfiguration", + name="email_verification_request_subject_en", + field=models.CharField( + default=functools.partial( + openforms.config.models.config._render, + *("emails/email_verification/subject.txt",), + **{} + ), + help_text="Subject of the email verification email.", + max_length=1000, + null=True, + validators=[openforms.template.validators.DjangoTemplateValidator()], + verbose_name="subject", + ), + ), + migrations.AddField( + model_name="globalconfiguration", + name="email_verification_request_subject_nl", + field=models.CharField( + default=functools.partial( + openforms.config.models.config._render, + *("emails/email_verification/subject.txt",), + **{} + ), + help_text="Subject of the email verification email.", + max_length=1000, + null=True, + validators=[openforms.template.validators.DjangoTemplateValidator()], + verbose_name="subject", + ), + ), + migrations.AlterField( + model_name="globalconfiguration", + name="all_submissions_removal_limit", + field=models.PositiveIntegerField( + default=90, + help_text="Amount of days when all submissions will be permanently deleted", + validators=[django.core.validators.MinValueValidator(0)], + verbose_name="all submissions removal limit", + ), + ), + migrations.AlterField( + model_name="globalconfiguration", + name="errored_submissions_removal_limit", + field=models.PositiveIntegerField( + default=30, + help_text="Amount of days errored submissions will remain before being removed", + validators=[django.core.validators.MinValueValidator(0)], + verbose_name="errored submission removal limit", + ), + ), + migrations.AlterField( + model_name="globalconfiguration", + name="incomplete_submissions_removal_limit", + field=models.PositiveIntegerField( + default=7, + help_text="Amount of days incomplete submissions will remain before being removed", + validators=[django.core.validators.MinValueValidator(0)], + verbose_name="incomplete submission removal limit", + ), + ), + migrations.AlterField( + model_name="globalconfiguration", + name="successful_submissions_removal_limit", + field=models.PositiveIntegerField( + default=7, + help_text="Amount of days successful submissions will remain before being removed", + validators=[django.core.validators.MinValueValidator(0)], + verbose_name="successful submission removal limit", + ), + ), + migrations.AddField( + model_name="globalconfiguration", + name="cosign_submission_confirmation_title", + field=models.CharField( + default=functools.partial( + openforms.utils.translations.get_default, + *("Request not complete yet",), + **{} + ), + help_text="The content of the confirmation page title for submissions requiring cosigning.", + max_length=200, + validators=[openforms.template.validators.DjangoTemplateValidator()], + verbose_name="cosign submission confirmation title", + ), + ), + migrations.AddField( + model_name="globalconfiguration", + name="cosign_submission_confirmation_title_en", + field=models.CharField( + default=functools.partial( + openforms.utils.translations.get_default, + *("Request not complete yet",), + **{} + ), + help_text="The content of the confirmation page title for submissions requiring cosigning.", + max_length=200, + null=True, + validators=[openforms.template.validators.DjangoTemplateValidator()], + verbose_name="cosign submission confirmation title", + ), + ), + migrations.AddField( + model_name="globalconfiguration", + name="cosign_submission_confirmation_title_nl", + field=models.CharField( + default=functools.partial( + openforms.utils.translations.get_default, + *("Request not complete yet",), + **{} + ), + help_text="The content of the confirmation page title for submissions requiring cosigning.", + max_length=200, + null=True, + validators=[openforms.template.validators.DjangoTemplateValidator()], + verbose_name="cosign submission confirmation title", + ), + ), + migrations.AddField( + model_name="globalconfiguration", + name="submission_confirmation_title", + field=models.CharField( + default=functools.partial( + openforms.utils.translations.get_default, + *("Confirmation: {{ public_reference }}",), + **{} + ), + help_text="The content of the confirmation page title. You can (and should) use the 'public_reference' variable so the users have a reference in case they need to contact the customer service.", + max_length=200, + validators=[openforms.template.validators.DjangoTemplateValidator()], + verbose_name="submission confirmation title", + ), + ), + migrations.AddField( + model_name="globalconfiguration", + name="submission_confirmation_title_en", + field=models.CharField( + default=functools.partial( + openforms.utils.translations.get_default, + *("Confirmation: {{ public_reference }}",), + **{} + ), + help_text="The content of the confirmation page title. You can (and should) use the 'public_reference' variable so the users have a reference in case they need to contact the customer service.", + max_length=200, + null=True, + validators=[openforms.template.validators.DjangoTemplateValidator()], + verbose_name="submission confirmation title", + ), + ), + migrations.AddField( + model_name="globalconfiguration", + name="submission_confirmation_title_nl", + field=models.CharField( + default=functools.partial( + openforms.utils.translations.get_default, + *("Confirmation: {{ public_reference }}",), + **{} + ), + help_text="The content of the confirmation page title. You can (and should) use the 'public_reference' variable so the users have a reference in case they need to contact the customer service.", + max_length=200, + null=True, + validators=[openforms.template.validators.DjangoTemplateValidator()], + verbose_name="submission confirmation title", + ), + ), + migrations.AddField( + model_name="globalconfiguration", + name="cosign_submission_confirmation_template", + field=tinymce.models.HTMLField( + default=functools.partial( + openforms.config.models.config._render, + *("config/default_cosign_submission_confirmation.html",), + **{} + ), + help_text="The content of the submission confirmation page for submissions requiring cosigning. The variables 'public_reference' and 'cosigner_email' are available. We strongly advise you to include the 'public_reference' in case users need to contact the customer service.", + validators=[ + openforms.template.validators.DjangoTemplateValidator( + backend="openforms.template.openforms_backend" + ) + ], + verbose_name="cosign submission confirmation template", + ), + ), + migrations.AddField( + model_name="globalconfiguration", + name="cosign_submission_confirmation_template_en", + field=tinymce.models.HTMLField( + default=functools.partial( + openforms.config.models.config._render, + *("config/default_cosign_submission_confirmation.html",), + **{} + ), + help_text="The content of the submission confirmation page for submissions requiring cosigning. The variables 'public_reference' and 'cosigner_email' are available. We strongly advise you to include the 'public_reference' in case users need to contact the customer service.", + null=True, + validators=[ + openforms.template.validators.DjangoTemplateValidator( + backend="openforms.template.openforms_backend" + ) + ], + verbose_name="cosign submission confirmation template", + ), + ), + migrations.AddField( + model_name="globalconfiguration", + name="cosign_submission_confirmation_template_nl", + field=tinymce.models.HTMLField( + default=functools.partial( + openforms.config.models.config._render, + *("config/default_cosign_submission_confirmation.html",), + **{} + ), + help_text="The content of the submission confirmation page for submissions requiring cosigning. The variables 'public_reference' and 'cosigner_email' are available. We strongly advise you to include the 'public_reference' in case users need to contact the customer service.", + null=True, + validators=[ + openforms.template.validators.DjangoTemplateValidator( + backend="openforms.template.openforms_backend" + ) + ], + verbose_name="cosign submission confirmation template", + ), + ), + migrations.AddField( + model_name="globalconfiguration", + name="cosign_confirmation_email_content", + field=tinymce.models.HTMLField( + default=functools.partial( + openforms.config.models.config._render, + *("emails/confirmation/cosign_content.html",), + **{} + ), + help_text="Content of the confirmation email message when the form requires cosigning. Can be overridden on the form level.", + validators=[ + openforms.template.validators.DjangoTemplateValidator( + backend="openforms.template.openforms_backend", + required_template_tags=[ + "payment_information", + "cosign_information", + ], + ), + openforms.emails.validators.URLSanitationValidator(), + ], + verbose_name="cosign content", + ), + ), + migrations.AddField( + model_name="globalconfiguration", + name="cosign_confirmation_email_content_en", + field=tinymce.models.HTMLField( + default=functools.partial( + openforms.config.models.config._render, + *("emails/confirmation/cosign_content.html",), + **{} + ), + help_text="Content of the confirmation email message when the form requires cosigning. Can be overridden on the form level.", + null=True, + validators=[ + openforms.template.validators.DjangoTemplateValidator( + backend="openforms.template.openforms_backend", + required_template_tags=[ + "payment_information", + "cosign_information", + ], + ), + openforms.emails.validators.URLSanitationValidator(), + ], + verbose_name="cosign content", + ), + ), + migrations.AddField( + model_name="globalconfiguration", + name="cosign_confirmation_email_content_nl", + field=tinymce.models.HTMLField( + default=functools.partial( + openforms.config.models.config._render, + *("emails/confirmation/cosign_content.html",), + **{} + ), + help_text="Content of the confirmation email message when the form requires cosigning. Can be overridden on the form level.", + null=True, + validators=[ + openforms.template.validators.DjangoTemplateValidator( + backend="openforms.template.openforms_backend", + required_template_tags=[ + "payment_information", + "cosign_information", + ], + ), + openforms.emails.validators.URLSanitationValidator(), + ], + verbose_name="cosign content", + ), + ), + migrations.AddField( + model_name="globalconfiguration", + name="cosign_confirmation_email_subject", + field=models.CharField( + default=functools.partial( + openforms.config.models.config._render, + *("emails/confirmation/cosign_subject.txt",), + **{} + ), + help_text="Subject of the confirmation email message when the form requires cosigning. Can be overridden on the form level.", + max_length=1000, + validators=[openforms.template.validators.DjangoTemplateValidator()], + verbose_name="cosign subject", + ), + ), + migrations.AddField( + model_name="globalconfiguration", + name="cosign_confirmation_email_subject_en", + field=models.CharField( + default=functools.partial( + openforms.config.models.config._render, + *("emails/confirmation/cosign_subject.txt",), + **{} + ), + help_text="Subject of the confirmation email message when the form requires cosigning. Can be overridden on the form level.", + max_length=1000, + null=True, + validators=[openforms.template.validators.DjangoTemplateValidator()], + verbose_name="cosign subject", + ), + ), + migrations.AddField( + model_name="globalconfiguration", + name="cosign_confirmation_email_subject_nl", + field=models.CharField( + default=functools.partial( + openforms.config.models.config._render, + *("emails/confirmation/cosign_subject.txt",), + **{} + ), + help_text="Subject of the confirmation email message when the form requires cosigning. Can be overridden on the form level.", + max_length=1000, + null=True, + validators=[openforms.template.validators.DjangoTemplateValidator()], + verbose_name="cosign subject", + ), + ), + migrations.AlterField( + model_name="globalconfiguration", + name="confirmation_email_content", + field=tinymce.models.HTMLField( + default=functools.partial( + openforms.config.models.config._render, + *("emails/confirmation/content.html",), + **{} + ), + help_text="Content of the confirmation email message. Can be overridden on the form level", + validators=[ + openforms.template.validators.DjangoTemplateValidator( + backend="openforms.template.openforms_backend", + required_template_tags=[ + "appointment_information", + "payment_information", + ], + ), + openforms.emails.validators.URLSanitationValidator(), + ], + verbose_name="content", + ), + ), + migrations.AlterField( + model_name="globalconfiguration", + name="confirmation_email_content_en", + field=tinymce.models.HTMLField( + default=functools.partial( + openforms.config.models.config._render, + *("emails/confirmation/content.html",), + **{} + ), + help_text="Content of the confirmation email message. Can be overridden on the form level", + null=True, + validators=[ + openforms.template.validators.DjangoTemplateValidator( + backend="openforms.template.openforms_backend", + required_template_tags=[ + "appointment_information", + "payment_information", + ], + ), + openforms.emails.validators.URLSanitationValidator(), + ], + verbose_name="content", + ), + ), + migrations.AlterField( + model_name="globalconfiguration", + name="confirmation_email_content_nl", + field=tinymce.models.HTMLField( + default=functools.partial( + openforms.config.models.config._render, + *("emails/confirmation/content.html",), + **{} + ), + help_text="Content of the confirmation email message. Can be overridden on the form level", + null=True, + validators=[ + openforms.template.validators.DjangoTemplateValidator( + backend="openforms.template.openforms_backend", + required_template_tags=[ + "appointment_information", + "payment_information", + ], + ), + openforms.emails.validators.URLSanitationValidator(), + ], + verbose_name="content", + ), + ), + migrations.AlterField( + model_name="globalconfiguration", + name="cosign_request_template", + field=tinymce.models.HTMLField( + default=functools.partial( + openforms.config.models.config._render, + *("emails/co_sign/request.html",), + **{} + ), + help_text="Content of the co-sign request email. The available template variables are: 'form_name', 'submission_date', 'form_url' and 'code'.", + validators=[ + openforms.template.validators.DjangoTemplateValidator( + backend="openforms.template.openforms_backend" + ), + openforms.emails.validators.URLSanitationValidator(), + ], + verbose_name="co-sign request template", + ), + ), + migrations.AlterField( + model_name="globalconfiguration", + name="cosign_request_template_en", + field=tinymce.models.HTMLField( + default=functools.partial( + openforms.config.models.config._render, + *("emails/co_sign/request.html",), + **{} + ), + help_text="Content of the co-sign request email. The available template variables are: 'form_name', 'submission_date', 'form_url' and 'code'.", + null=True, + validators=[ + openforms.template.validators.DjangoTemplateValidator( + backend="openforms.template.openforms_backend" + ), + openforms.emails.validators.URLSanitationValidator(), + ], + verbose_name="co-sign request template", + ), + ), + migrations.AlterField( + model_name="globalconfiguration", + name="cosign_request_template_nl", + field=tinymce.models.HTMLField( + default=functools.partial( + openforms.config.models.config._render, + *("emails/co_sign/request.html",), + **{} + ), + help_text="Content of the co-sign request email. The available template variables are: 'form_name', 'submission_date', 'form_url' and 'code'.", + null=True, + validators=[ + openforms.template.validators.DjangoTemplateValidator( + backend="openforms.template.openforms_backend" + ), + openforms.emails.validators.URLSanitationValidator(), + ], + verbose_name="co-sign request template", + ), + ), + migrations.RunPython( + FixDefaultTranslations( + app_label="config", + model="GlobalConfiguration", + fields=( + "cosign_submission_confirmation_template", + "cosign_submission_confirmation_title", + "submission_confirmation_title", + "cosign_confirmation_email_content", + "cosign_confirmation_email_subject", + ), + ), + migrations.RunPython.noop, + ), + migrations.RunPython( + code=update_summary_tags, + reverse_code=migrations.RunPython.noop, + ), + migrations.CreateModel( + name="MapTileLayer", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "identifier", + models.SlugField( + help_text="A unique identifier for the tile layer.", + unique=True, + verbose_name="identifier", + ), + ), + ( + "url", + models.URLField( + help_text="URL to the tile layer image, used to define the map component background. To ensure correct functionality of the map, EPSG 28992 projection should be used. Example value: https://service.pdok.nl/brt/achtergrondkaart/wmts/v2_0/standaard/EPSG:28992/{z}/{x}/{y}.png", + max_length=255, + verbose_name="tile layer url", + ), + ), + ( + "label", + models.CharField( + help_text="An easily recognizable name for the tile layer, used to identify it.", + max_length=100, + verbose_name="label", + ), + ), + ], + options={ + "verbose_name": "map tile layer", + "verbose_name_plural": "map tile layers", + "ordering": ("label",), + }, + ), + ] diff --git a/src/openforms/config/migrations/0056_disable_prefill_objects_api_plugin.py b/src/openforms/config/migrations/0056_disable_prefill_objects_api_plugin.py index 73895261a7..c7cacd16cf 100644 --- a/src/openforms/config/migrations/0056_disable_prefill_objects_api_plugin.py +++ b/src/openforms/config/migrations/0056_disable_prefill_objects_api_plugin.py @@ -3,17 +3,6 @@ from django.db import migrations -def disable_objects_api_prefill_plugin(apps, _): - GlobalConfiguration = apps.get_model("config", "GlobalConfiguration") - config, _ = GlobalConfiguration.objects.get_or_create( - pk=1 - ) # must match GlobalConfiguration.singleton_instance_id - - config.plugin_configuration.setdefault("prefill", {}) - config.plugin_configuration["prefill"].setdefault("objects_api", {"enabled": False}) - config.save() - - class Migration(migrations.Migration): dependencies = [ @@ -23,8 +12,6 @@ class Migration(migrations.Migration): ), ] - operations = [ - migrations.RunPython( - disable_objects_api_prefill_plugin, migrations.RunPython.noop - ), - ] + # RunPython operation removed as part of 3.0 release cycle - these migrations are + # guaranteed to have been executed on Open Forms 2.8.x for existing instances. + operations = [] diff --git a/src/openforms/config/migrations/0059_alter_globalconfiguration_form_upload_default_file_types.py b/src/openforms/config/migrations/0059_alter_globalconfiguration_form_upload_default_file_types.py index 0a49a892c0..1a9b1d3515 100644 --- a/src/openforms/config/migrations/0059_alter_globalconfiguration_form_upload_default_file_types.py +++ b/src/openforms/config/migrations/0059_alter_globalconfiguration_form_upload_default_file_types.py @@ -4,27 +4,6 @@ import django_jsonform.models.fields -from openforms.config.constants import UploadFileType - - -def add_extra_zip_mimetypes(apps, _): - """ - Set up the correct zip mimetypes. - - This ensures all the allowed mimetypes concerning zip files are included. - """ - GlobalConfiguration = apps.get_model("config", "GlobalConfiguration") - if not GlobalConfiguration.objects.exists(): - return - - config = GlobalConfiguration.objects.get() - if "application/zip" not in config.form_upload_default_file_types: - return - - config.form_upload_default_file_types.remove("application/zip") - config.form_upload_default_file_types.append(UploadFileType.zip) - config.save(update_fields=("form_upload_default_file_types",)) - class Migration(migrations.Migration): @@ -81,8 +60,6 @@ class Migration(migrations.Migration): verbose_name="Default allowed file upload types", ), ), - migrations.RunPython( - add_extra_zip_mimetypes, - migrations.RunPython.noop, - ), + # RunPython operation removed as part of 3.0 release cycle - these migrations are + # guaranteed to have been executed on Open Forms 2.8.x for existing instances. ] diff --git a/src/openforms/config/migrations/0068_update_summary_tags.py b/src/openforms/config/migrations/0068_update_summary_tags.py index c3ddc00912..30e91ca95a 100644 --- a/src/openforms/config/migrations/0068_update_summary_tags.py +++ b/src/openforms/config/migrations/0068_update_summary_tags.py @@ -1,40 +1,12 @@ # Generated by Django 4.2.16 on 2024-11-29 18:47 -import re from django.db import migrations -from django.db.migrations.state import StateApps +from django.utils.module_loading import import_string - -def replace_tag(tpl: str) -> str: - return re.sub( - r"{%\s*summary\s*%}", - "{% confirmation_summary %}", - tpl, - ) - - -def update_summary_tags(apps: StateApps, _): - GlobalConfiguration = apps.get_model("config", "GlobalConfiguration") - config = GlobalConfiguration.objects.first() - if config is None: - return - - # the cosign fields are new in 3.0.0 so they're not expected to hold the legacy - # tag. - config.submission_confirmation_template_en = replace_tag( - config.submission_confirmation_template_en - ) - config.submission_confirmation_template_nl = replace_tag( - config.submission_confirmation_template_nl - ) - config.confirmation_email_content_nl = replace_tag( - config.confirmation_email_content_nl - ) - config.confirmation_email_content_en = replace_tag( - config.confirmation_email_content_en - ) - config.save() +update_summary_tags = import_string( + "openforms.config.migrations.0055_v270_to_v300.update_summary_tags" +) class Migration(migrations.Migration): diff --git a/src/openforms/config/tests/test_migrations.py b/src/openforms/config/tests/test_migrations.py index 9f66ffc799..65bc6c01d1 100644 --- a/src/openforms/config/tests/test_migrations.py +++ b/src/openforms/config/tests/test_migrations.py @@ -3,69 +3,10 @@ from openforms.utils.tests.test_migrations import TestMigrations -class MigrateFeatureFlagsTests(TestMigrations): - app = "config" - migrate_from = "0001_initial_to_v250" - migrate_to = "0054_v250_to_v270" - - def setUpBeforeMigration(self, apps: StateApps): - GlobalConfiguration = apps.get_model("config", "GlobalConfiguration") - GlobalConfiguration.objects.update_or_create( - pk=1, - defaults={ - "enable_demo_plugins": True, - "display_sdk_information": False, - }, - ) - FlagState = apps.get_model("flags", "FlagState") - FlagState.objects.all().delete() - - def test_feature_flags_created(self): - FlagState = self.apps.get_model("flags", "FlagState") - flags = FlagState.objects.all() - - self.assertEqual(len(flags), 2) - by_name = {flag.name: flag for flag in flags} - - flag1 = by_name["ENABLE_DEMO_PLUGINS"] - self.assertEqual(flag1.condition, "boolean") - self.assertEqual(flag1.value, "True") - - flag2 = by_name["DISPLAY_SDK_INFORMATION"] - self.assertEqual(flag2.condition, "boolean") - self.assertEqual(flag2.value, "False") - - -class ReverseMigrateFeatureFlagsTests(TestMigrations): - app = "config" - migrate_from = "0054_v250_to_v270" - migrate_to = "0001_initial_to_v250" - - def setUpBeforeMigration(self, apps: StateApps): - FlagState = apps.get_model("flags", "FlagState") - FlagState.objects.all().delete() - FlagState.objects.create( - name="ENABLE_DEMO_PLUGINS", condition="boolean", value="yes" - ) - FlagState.objects.create( - name="UNRELATED", condition="invalid", value="irrelevant" - ) - - GlobalConfiguration = apps.get_model("config", "GlobalConfiguration") - GlobalConfiguration.objects.get_or_create(pk=1) - - def test_feature_flags_created(self): - GlobalConfiguration = self.apps.get_model("config", "GlobalConfiguration") - config = GlobalConfiguration.objects.get() - - self.assertTrue(config.enable_demo_plugins) - self.assertFalse(config.display_sdk_information) - - class MigrateSummaryTag(TestMigrations): app = "config" - migrate_from = "0068_alter_globalconfiguration_cosign_request_template_and_more" - migrate_to = "0068_update_summary_tags" + migrate_from = "0054_v250_to_v270" + migrate_to = "0055_v270_to_v300" def setUpBeforeMigration(self, apps: StateApps): GlobalConfiguration = apps.get_model("config", "GlobalConfiguration") diff --git a/src/openforms/contrib/objects_api/migrations/0001_initial.py b/src/openforms/contrib/objects_api/migrations/0001_initial.py index b4ba342ae1..bf6ebc1ab9 100644 --- a/src/openforms/contrib/objects_api/migrations/0001_initial.py +++ b/src/openforms/contrib/objects_api/migrations/0001_initial.py @@ -1,8 +1,9 @@ # Generated by Django 4.2.16 on 2024-09-12 17:42 import django.core.validators -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models + import openforms.utils.validators @@ -15,235 +16,230 @@ class Migration(migrations.Migration): ] operations = [ - migrations.SeparateDatabaseAndState( - database_operations=[], - state_operations=[ - migrations.CreateModel( - name="ObjectsAPIGroupConfig", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "name", - models.CharField( - help_text="A recognisable name for this set of Objects APIs.", - max_length=255, - verbose_name="name", - ), - ), - ( - "catalogue_domain", - models.CharField( - blank=True, - help_text="The 'domein' attribute for the Catalogus resource in the Catalogi API.", - max_length=5, - validators=[ - django.core.validators.RegexValidator( - "^[A-Z]*$", - code="invalid", - message="Value must be all uppercase letters.", - ) - ], - verbose_name="catalogus domain", - ), - ), - ( - "catalogue_rsin", - models.CharField( - blank=True, - help_text="The 'rsin' attribute for the Catalogus resource in the Catalogi API.", - max_length=9, - validators=[openforms.utils.validators.RSINValidator()], - verbose_name="catalogus RSIN", - ), - ), - ( - "organisatie_rsin", - models.CharField( - blank=True, - help_text="Default RSIN of organization, which creates the INFORMATIEOBJECT", - max_length=9, - validators=[openforms.utils.validators.RSINValidator()], - verbose_name="organisation RSIN", - ), - ), - ( - "iot_submission_report", - models.CharField( - blank=True, - help_text="Description of the document type in the Catalogi API to be used for the submission report PDF (i.e. the INFORMATIEOBJECTTYPE.omschrijving attribute). The appropriate version will automatically be selected based on the submission timestamp and validity dates of the document type versions.", - max_length=80, - verbose_name="submission report document type description", - ), - ), - ( - "iot_submission_csv", - models.CharField( - blank=True, - help_text="Description of the document type in the Catalogi API to be used for the submission report CSV (i.e. the INFORMATIEOBJECTTYPE.omschrijving attribute). The appropriate version will automatically be selected based on the submission timestamp and validity dates of the document type versions.", - max_length=80, - verbose_name="submission report CSV document type description", - ), - ), - ( - "iot_attachment", - models.CharField( - blank=True, - help_text="Description of the document type in the Catalogi API to be used for the submission attachments (i.e. the INFORMATIEOBJECTTYPE.omschrijving attribute). The appropriate version will automatically be selected based on the submission timestamp and validity dates of the document type versions.", - max_length=80, - verbose_name="attachment document type description", - ), - ), - ( - "informatieobjecttype_submission_report", - models.URLField( - blank=True, - help_text="Default URL that points to the INFORMATIEOBJECTTYPE in the Catalogi API to be used for the submission report PDF", - max_length=1000, - verbose_name="submission report informatieobjecttype", - ), - ), - ( - "informatieobjecttype_submission_csv", - models.URLField( - blank=True, - help_text="Default URL that points to the INFORMATIEOBJECTTYPE in the Catalogi API to be used for the submission report CSV", - max_length=1000, - verbose_name="submission report CSV informatieobjecttype", - ), - ), - ( - "informatieobjecttype_attachment", - models.URLField( - blank=True, - help_text="Default URL that points to the INFORMATIEOBJECTTYPE in the Catalogi API to be used for the submission attachments", - max_length=1000, - verbose_name="attachment informatieobjecttype", - ), - ), - ( - "catalogi_service", - models.ForeignKey( - blank=True, - limit_choices_to={"api_type": "ztc"}, - null=True, - on_delete=django.db.models.deletion.PROTECT, - related_name="+", - to="zgw_consumers.service", - verbose_name="Catalogi API", - ), - ), - ( - "drc_service", - models.ForeignKey( - blank=True, - limit_choices_to={"api_type": "drc"}, - null=True, - on_delete=django.db.models.deletion.PROTECT, - related_name="+", - to="zgw_consumers.service", - verbose_name="Documenten API", - ), - ), - ( - "objects_service", - models.ForeignKey( - limit_choices_to={"api_type": "orc"}, - null=True, - on_delete=django.db.models.deletion.PROTECT, - related_name="+", - to="zgw_consumers.service", - verbose_name="Objects API", - ), - ), - ( - "objecttypes_service", - models.ForeignKey( - limit_choices_to={"api_type": "orc"}, - null=True, - on_delete=django.db.models.deletion.PROTECT, - related_name="+", - to="zgw_consumers.service", - verbose_name="Objecttypes API", - ), - ), - ], - options={ - "verbose_name": "Objects API group", - "verbose_name_plural": "Objects API groups", - "db_table": "registrations_objects_api_objectsapigroupconfig", - }, - ), - migrations.AddConstraint( - model_name="objectsapigroupconfig", - constraint=models.CheckConstraint( - check=models.Q( - models.Q(("catalogue_domain", ""), ("catalogue_rsin", "")), - models.Q( - models.Q(("catalogue_domain", ""), _negated=True), - models.Q(("catalogue_rsin", ""), _negated=True), - ), - _connector="OR", - ), - name="catalogue_composite_key", - violation_error_message="You must specify both domain and RSIN to uniquely identify a catalogue.", - ), - ), - migrations.AddConstraint( - model_name="objectsapigroupconfig", - constraint=models.CheckConstraint( - check=models.Q( - ("iot_submission_report", ""), - models.Q( - models.Q(("iot_submission_report", ""), _negated=True), - models.Q(("catalogue_domain", ""), _negated=True), - models.Q(("catalogue_rsin", ""), _negated=True), - ), - _connector="OR", - ), - name="iot_report_requires_catalogue", - violation_error_message="You must specify a catalogue when specifying the submission report PDF document type.", - ), - ), - migrations.AddConstraint( - model_name="objectsapigroupconfig", - constraint=models.CheckConstraint( - check=models.Q( - ("iot_submission_csv", ""), - models.Q( - models.Q(("iot_submission_csv", ""), _negated=True), - models.Q(("catalogue_domain", ""), _negated=True), - models.Q(("catalogue_rsin", ""), _negated=True), - ), - _connector="OR", - ), - name="iot_csv_requires_catalogue", - violation_error_message="You must specify a catalogue when specifying the submission report CSV document type.", - ), - ), - migrations.AddConstraint( - model_name="objectsapigroupconfig", - constraint=models.CheckConstraint( - check=models.Q( - ("iot_attachment", ""), - models.Q( - models.Q(("iot_attachment", ""), _negated=True), - models.Q(("catalogue_domain", ""), _negated=True), - models.Q(("catalogue_rsin", ""), _negated=True), - ), - _connector="OR", - ), - name="iot_attachment_requires_catalogue", - violation_error_message="You must specify a catalogue when specifying the submission attachment document type.", + migrations.CreateModel( + name="ObjectsAPIGroupConfig", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "name", + models.CharField( + help_text="A recognisable name for this set of Objects APIs.", + max_length=255, + verbose_name="name", + ), + ), + ( + "catalogue_domain", + models.CharField( + blank=True, + help_text="The 'domein' attribute for the Catalogus resource in the Catalogi API.", + max_length=5, + validators=[ + django.core.validators.RegexValidator( + "^[A-Z]*$", + code="invalid", + message="Value must be all uppercase letters.", + ) + ], + verbose_name="catalogus domain", + ), + ), + ( + "catalogue_rsin", + models.CharField( + blank=True, + help_text="The 'rsin' attribute for the Catalogus resource in the Catalogi API.", + max_length=9, + validators=[openforms.utils.validators.RSINValidator()], + verbose_name="catalogus RSIN", + ), + ), + ( + "organisatie_rsin", + models.CharField( + blank=True, + help_text="Default RSIN of organization, which creates the INFORMATIEOBJECT", + max_length=9, + validators=[openforms.utils.validators.RSINValidator()], + verbose_name="organisation RSIN", + ), + ), + ( + "iot_submission_report", + models.CharField( + blank=True, + help_text="Description of the document type in the Catalogi API to be used for the submission report PDF (i.e. the INFORMATIEOBJECTTYPE.omschrijving attribute). The appropriate version will automatically be selected based on the submission timestamp and validity dates of the document type versions.", + max_length=80, + verbose_name="submission report document type description", + ), + ), + ( + "iot_submission_csv", + models.CharField( + blank=True, + help_text="Description of the document type in the Catalogi API to be used for the submission report CSV (i.e. the INFORMATIEOBJECTTYPE.omschrijving attribute). The appropriate version will automatically be selected based on the submission timestamp and validity dates of the document type versions.", + max_length=80, + verbose_name="submission report CSV document type description", + ), + ), + ( + "iot_attachment", + models.CharField( + blank=True, + help_text="Description of the document type in the Catalogi API to be used for the submission attachments (i.e. the INFORMATIEOBJECTTYPE.omschrijving attribute). The appropriate version will automatically be selected based on the submission timestamp and validity dates of the document type versions.", + max_length=80, + verbose_name="attachment document type description", + ), + ), + ( + "informatieobjecttype_submission_report", + models.URLField( + blank=True, + help_text="Default URL that points to the INFORMATIEOBJECTTYPE in the Catalogi API to be used for the submission report PDF", + max_length=1000, + verbose_name="submission report informatieobjecttype", + ), + ), + ( + "informatieobjecttype_submission_csv", + models.URLField( + blank=True, + help_text="Default URL that points to the INFORMATIEOBJECTTYPE in the Catalogi API to be used for the submission report CSV", + max_length=1000, + verbose_name="submission report CSV informatieobjecttype", + ), + ), + ( + "informatieobjecttype_attachment", + models.URLField( + blank=True, + help_text="Default URL that points to the INFORMATIEOBJECTTYPE in the Catalogi API to be used for the submission attachments", + max_length=1000, + verbose_name="attachment informatieobjecttype", + ), + ), + ( + "catalogi_service", + models.ForeignKey( + blank=True, + limit_choices_to={"api_type": "ztc"}, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="+", + to="zgw_consumers.service", + verbose_name="Catalogi API", + ), + ), + ( + "drc_service", + models.ForeignKey( + blank=True, + limit_choices_to={"api_type": "drc"}, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="+", + to="zgw_consumers.service", + verbose_name="Documenten API", + ), + ), + ( + "objects_service", + models.ForeignKey( + limit_choices_to={"api_type": "orc"}, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="+", + to="zgw_consumers.service", + verbose_name="Objects API", + ), + ), + ( + "objecttypes_service", + models.ForeignKey( + limit_choices_to={"api_type": "orc"}, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="+", + to="zgw_consumers.service", + verbose_name="Objecttypes API", ), ), ], - ) + options={ + "verbose_name": "Objects API group", + "verbose_name_plural": "Objects API groups", + "db_table": "registrations_objects_api_objectsapigroupconfig", + }, + ), + migrations.AddConstraint( + model_name="objectsapigroupconfig", + constraint=models.CheckConstraint( + check=models.Q( + models.Q(("catalogue_domain", ""), ("catalogue_rsin", "")), + models.Q( + models.Q(("catalogue_domain", ""), _negated=True), + models.Q(("catalogue_rsin", ""), _negated=True), + ), + _connector="OR", + ), + name="catalogue_composite_key", + violation_error_message="You must specify both domain and RSIN to uniquely identify a catalogue.", + ), + ), + migrations.AddConstraint( + model_name="objectsapigroupconfig", + constraint=models.CheckConstraint( + check=models.Q( + ("iot_submission_report", ""), + models.Q( + models.Q(("iot_submission_report", ""), _negated=True), + models.Q(("catalogue_domain", ""), _negated=True), + models.Q(("catalogue_rsin", ""), _negated=True), + ), + _connector="OR", + ), + name="iot_report_requires_catalogue", + violation_error_message="You must specify a catalogue when specifying the submission report PDF document type.", + ), + ), + migrations.AddConstraint( + model_name="objectsapigroupconfig", + constraint=models.CheckConstraint( + check=models.Q( + ("iot_submission_csv", ""), + models.Q( + models.Q(("iot_submission_csv", ""), _negated=True), + models.Q(("catalogue_domain", ""), _negated=True), + models.Q(("catalogue_rsin", ""), _negated=True), + ), + _connector="OR", + ), + name="iot_csv_requires_catalogue", + violation_error_message="You must specify a catalogue when specifying the submission report CSV document type.", + ), + ), + migrations.AddConstraint( + model_name="objectsapigroupconfig", + constraint=models.CheckConstraint( + check=models.Q( + ("iot_attachment", ""), + models.Q( + models.Q(("iot_attachment", ""), _negated=True), + models.Q(("catalogue_domain", ""), _negated=True), + models.Q(("catalogue_rsin", ""), _negated=True), + ), + _connector="OR", + ), + name="iot_attachment_requires_catalogue", + violation_error_message="You must specify a catalogue when specifying the submission attachment document type.", + ), + ), ] diff --git a/src/openforms/contrib/objects_api/migrations/0002_objectsapigroupconfig_identifier.py b/src/openforms/contrib/objects_api/migrations/0002_objectsapigroupconfig_identifier.py index 4b0d7ee14c..5cec634f94 100644 --- a/src/openforms/contrib/objects_api/migrations/0002_objectsapigroupconfig_identifier.py +++ b/src/openforms/contrib/objects_api/migrations/0002_objectsapigroupconfig_identifier.py @@ -24,11 +24,6 @@ class Migration(migrations.Migration): dependencies = [ ("objects_api", "0001_initial"), - # Related to https://github.com/open-formulieren/open-forms/issues/4654 - # Because the table was moved from registrations_objects_api to this app, - # there needs to be a dependency here to ensure the table is actually created - # when running tests - ("registrations_objects_api", "0025_delete_objectsapigroupconfig"), ] operations = [ diff --git a/src/openforms/forms/migrations/0097_extra_mimetypes_in_file_type.py b/src/openforms/forms/migrations/0097_extra_mimetypes_in_file_type.py index 4d22d4ffb2..eb9aeff0ab 100644 --- a/src/openforms/forms/migrations/0097_extra_mimetypes_in_file_type.py +++ b/src/openforms/forms/migrations/0097_extra_mimetypes_in_file_type.py @@ -2,8 +2,6 @@ from django.db import migrations -from ..migration_operations import ConvertComponentsOperation - class Migration(migrations.Migration): @@ -11,8 +9,6 @@ class Migration(migrations.Migration): ("forms", "0092_v250_to_v267"), ] - operations = [ - ConvertComponentsOperation( - "file", "ensure_extra_zip_mimetypes_exist_in_file_type" - ), - ] + # RunPython operation removed as part of 3.0 release cycle - these migrations are + # guaranteed to have been executed on Open Forms 2.8.x for existing instances. + operations = [] diff --git a/src/openforms/forms/migrations/0098_v270_to_v300.py b/src/openforms/forms/migrations/0098_v270_to_v300.py new file mode 100644 index 0000000000..9aee848943 --- /dev/null +++ b/src/openforms/forms/migrations/0098_v270_to_v300.py @@ -0,0 +1,332 @@ +# Generated by Django 4.2.17 on 2024-12-27 14:35 + +import re +from decimal import Decimal + +import django.core.validators +from django.db import migrations, models +from django.db.migrations.state import StateApps + +import tinymce.models + +import csp_post_processor.fields +from openforms.forms.constants import LogicActionTypes +from openforms.forms.migration_operations import ConvertComponentsOperation +from openforms.variables.constants import FormVariableDataTypes, FormVariableSources + +VARIABLE_NAME = "Total price" +VARIABLE_KEY = "totalPrice" + + +def _assignment_action(key: str, value: Decimal): + return { + "variable": key, + "action": { + "type": LogicActionTypes.variable, + "value": str(value), + }, + } + + +def convert_price_logic_rules_to_price_variable(apps: StateApps, _): + """ + For each form that has price logic rules, create a variable to hold the price and + add normal logic rules. + """ + Form = apps.get_model("forms", "Form") + forms_with_pricelogic = ( + Form.objects.filter(formpricelogic__isnull=False) + .exclude(product__isnull=True) + .distinct() + ) + + for form in forms_with_pricelogic.iterator(): + product = form.product + rules = form.formpricelogic_set.all() + + # create a variable to hold the result. + variable_keys = set(form.formvariable_set.values_list("key", flat=True)) + variable_key = VARIABLE_KEY + variable_name = VARIABLE_NAME + counter = 0 + while variable_key in variable_keys: + counter += 1 + variable_key = f"{variable_key}{counter}" + variable_name = f"{variable_name}{counter}" + if counter > 100: + raise RuntimeError( + "Could not generate a unique key without looping too long" + ) + + price_variable = form.formvariable_set.create( + form_definition=None, + name=variable_name, + key=variable_key, + source=FormVariableSources.user_defined, + data_type=FormVariableDataTypes.float, + ) + form.price_variable_key = price_variable.key + form.save() + + max_order = ( + last_rule.order + if (last_rule := form.formlogic_set.order_by("order").last()) + else 0 + ) + + # set up regular logic rules for each price logic rule + for rule in rules: + max_order += 1 + form.formlogic_set.create( + description="Converted price logic rule", + order=max_order, + is_advanced=True, + json_logic_trigger=rule.json_logic_trigger, + actions=[_assignment_action(form.price_variable_key, rule.price)], + ) + + # create one fallback rule in case none of the triggers hit + composite_negated_trigger = { + "!": {"or": [rule.json_logic_trigger for rule in rules]} + } + max_order += 1 + form.formlogic_set.create( + description="Converted price logic rule", + order=max_order, + is_advanced=True, + json_logic_trigger=composite_negated_trigger, + actions=[_assignment_action(form.price_variable_key, product.price)], + ) + + rules.delete() + + +class Migration(migrations.Migration): + + replaces = [ + ("forms", "0098_form_introduction_page_content_and_more"), + ("forms", "0099_form_show_summary_progress"), + ("forms", "0097_extra_mimetypes_in_file_type"), + ("forms", "0098_merge_20240920_1808"), + ("forms", "0100_merge_20240920_1816"), + ("forms", "0101_form_price_variable_key"), + ("forms", "0101_fix_empty_default_value"), + ("forms", "0102_merge_20241022_1143"), + ("forms", "0103_remove_formvariable_prefill_config_empty_or_complete_and_more"), + ("forms", "0104_select_datatype_string"), + ("forms", "0105_alter_form_all_submissions_removal_limit_and_more"), + ("forms", "0106_convert_price_logic_rules"), + ("forms", "0107_form_submission_counter_form_submission_limit"), + ] + + dependencies = [ + ("forms", "0097_v267_to_v270"), + ] + + operations = [ + migrations.AddField( + model_name="form", + name="introduction_page_content", + field=csp_post_processor.fields.CSPPostProcessedWYSIWYGField( + base_field=tinymce.models.HTMLField( + blank=True, + help_text="Content for the introduction page that leads to the start page of the form. Leave blank to disable the introduction page.", + verbose_name="introduction page", + ), + blank=True, + help_text="Content for the introduction page that leads to the start page of the form. Leave blank to disable the introduction page.", + verbose_name="introduction page", + ), + ), + migrations.AddField( + model_name="form", + name="introduction_page_content_en", + field=csp_post_processor.fields.CSPPostProcessedWYSIWYGField( + base_field=tinymce.models.HTMLField( + blank=True, + help_text="Content for the introduction page that leads to the start page of the form. Leave blank to disable the introduction page.", + verbose_name="introduction page", + ), + blank=True, + help_text="Content for the introduction page that leads to the start page of the form. Leave blank to disable the introduction page.", + null=True, + verbose_name="introduction page", + ), + ), + migrations.AddField( + model_name="form", + name="introduction_page_content_nl", + field=csp_post_processor.fields.CSPPostProcessedWYSIWYGField( + base_field=tinymce.models.HTMLField( + blank=True, + help_text="Content for the introduction page that leads to the start page of the form. Leave blank to disable the introduction page.", + verbose_name="introduction page", + ), + blank=True, + help_text="Content for the introduction page that leads to the start page of the form. Leave blank to disable the introduction page.", + null=True, + verbose_name="introduction page", + ), + ), + migrations.AddField( + model_name="form", + name="show_summary_progress", + field=models.BooleanField( + default=False, + help_text="Whether to display the short progress summary, indicating the current step number and total amount of steps.", + verbose_name="show summary of the progress", + ), + ), + migrations.AddField( + model_name="form", + name="price_variable_key", + field=models.TextField( + blank=True, + help_text="Key of the variable that contains the calculated submission price.", + validators=[ + django.core.validators.RegexValidator( + message="Invalid variable key. It must only contain alphanumeric characters, underscores, dots and dashes and should not be ended by dash or dot.", + regex=re.compile("^(\\w|\\w[\\w.\\-]*\\w)$"), + ) + ], + verbose_name="price variable key", + ), + ), + ConvertComponentsOperation( + component_type="textfield", + identifier="fix_empty_default_value", + ), + ConvertComponentsOperation( + component_type="email", + identifier="fix_empty_default_value", + ), + ConvertComponentsOperation( + component_type="time", + identifier="fix_empty_default_value", + ), + ConvertComponentsOperation( + component_type="phoneNumber", + identifier="fix_empty_default_value", + ), + ConvertComponentsOperation( + component_type="textarea", + identifier="fix_empty_default_value", + ), + ConvertComponentsOperation( + component_type="iban", + identifier="fix_empty_default_value", + ), + ConvertComponentsOperation( + component_type="licenseplate", + identifier="fix_empty_default_value", + ), + migrations.RemoveConstraint( + model_name="formvariable", + name="prefill_config_empty_or_complete", + ), + migrations.AddField( + model_name="formvariable", + name="prefill_options", + field=models.JSONField( + blank=True, default=dict, verbose_name="prefill options" + ), + ), + migrations.AddConstraint( + model_name="formvariable", + constraint=models.CheckConstraint( + check=models.Q( + models.Q( + models.Q( + ("prefill_plugin", ""), + ("prefill_attribute", ""), + ("prefill_options", {}), + ), + models.Q( + models.Q(("prefill_plugin", ""), _negated=True), + ("prefill_attribute", ""), + models.Q(("prefill_options", {}), _negated=True), + ("source", "user_defined"), + ), + models.Q( + models.Q(("prefill_plugin", ""), _negated=True), + models.Q(("prefill_attribute", ""), _negated=True), + ("prefill_options", {}), + ), + _connector="OR", + ) + ), + name="prefill_config_component_or_user_defined", + ), + ), + ConvertComponentsOperation( + component_type="select", + identifier="set_datatype_string", + ), + migrations.AlterField( + model_name="form", + name="all_submissions_removal_limit", + field=models.PositiveIntegerField( + blank=True, + help_text="Amount of days when all submissions of this form will be permanently deleted. Leave blank to use value in General Configuration.", + null=True, + validators=[django.core.validators.MinValueValidator(0)], + verbose_name="all submissions removal limit", + ), + ), + migrations.AlterField( + model_name="form", + name="errored_submissions_removal_limit", + field=models.PositiveIntegerField( + blank=True, + help_text="Amount of days errored submissions of this form will remain before being removed. Leave blank to use value in General Configuration.", + null=True, + validators=[django.core.validators.MinValueValidator(0)], + verbose_name="errored submission removal limit", + ), + ), + migrations.AlterField( + model_name="form", + name="incomplete_submissions_removal_limit", + field=models.PositiveIntegerField( + blank=True, + help_text="Amount of days incomplete submissions of this form will remain before being removed. Leave blank to use value in General Configuration.", + null=True, + validators=[django.core.validators.MinValueValidator(0)], + verbose_name="incomplete submission removal limit", + ), + ), + migrations.AlterField( + model_name="form", + name="successful_submissions_removal_limit", + field=models.PositiveIntegerField( + blank=True, + help_text="Amount of days successful submissions of this form will remain before being removed. Leave blank to use value in General Configuration.", + null=True, + validators=[django.core.validators.MinValueValidator(0)], + verbose_name="successful submission removal limit", + ), + ), + migrations.RunPython( + code=convert_price_logic_rules_to_price_variable, + reverse_code=migrations.RunPython.noop, + ), + migrations.AddField( + model_name="form", + name="submission_counter", + field=models.PositiveIntegerField( + default=0, + help_text="Counter to track how many submissions have been completed for the specific form. This works in combination with the maximum allowed submissions per form and can be reset via the frontend.", + verbose_name="submissions counter", + ), + ), + migrations.AddField( + model_name="form", + name="submission_limit", + field=models.PositiveIntegerField( + blank=True, + help_text="Maximum number of allowed submissions per form. Leave this empty if no limit is needed.", + null=True, + verbose_name="maximum allowed submissions", + ), + ), + ] diff --git a/src/openforms/forms/migrations/0106_convert_price_logic_rules.py b/src/openforms/forms/migrations/0106_convert_price_logic_rules.py index 885c92a45e..ffc3fa97f0 100644 --- a/src/openforms/forms/migrations/0106_convert_price_logic_rules.py +++ b/src/openforms/forms/migrations/0106_convert_price_logic_rules.py @@ -1,97 +1,12 @@ # Generated by Django 4.2.16 on 2024-11-25 15:32 -from decimal import Decimal from django.db import migrations -from django.db.migrations.state import StateApps +from django.utils.module_loading import import_string -from openforms.forms.constants import LogicActionTypes -from openforms.variables.constants import FormVariableDataTypes, FormVariableSources - -VARIABLE_NAME = "Total price" -VARIABLE_KEY = "totalPrice" - - -def _assignment_action(key: str, value: Decimal): - return { - "variable": key, - "action": { - "type": LogicActionTypes.variable, - "value": str(value), - }, - } - - -def convert_price_logic_rules_to_price_variable(apps: StateApps, _): - """ - For each form that has price logic rules, create a variable to hold the price and - add normal logic rules. - """ - Form = apps.get_model("forms", "Form") - forms_with_pricelogic = ( - Form.objects.filter(formpricelogic__isnull=False) - .exclude(product__isnull=True) - .distinct() - ) - - for form in forms_with_pricelogic.iterator(): - product = form.product - rules = form.formpricelogic_set.all() - - # create a variable to hold the result. - variable_keys = set(form.formvariable_set.values_list("key", flat=True)) - variable_key = VARIABLE_KEY - variable_name = VARIABLE_NAME - counter = 0 - while variable_key in variable_keys: - counter += 1 - variable_key = f"{variable_key}{counter}" - variable_name = f"{variable_name}{counter}" - if counter > 100: - raise RuntimeError( - "Could not generate a unique key without looping too long" - ) - - price_variable = form.formvariable_set.create( - form_definition=None, - name=variable_name, - key=variable_key, - source=FormVariableSources.user_defined, - data_type=FormVariableDataTypes.float, - ) - form.price_variable_key = price_variable.key - form.save() - - max_order = ( - last_rule.order - if (last_rule := form.formlogic_set.order_by("order").last()) - else 0 - ) - - # set up regular logic rules for each price logic rule - for rule in rules: - max_order += 1 - form.formlogic_set.create( - description="Converted price logic rule", - order=max_order, - is_advanced=True, - json_logic_trigger=rule.json_logic_trigger, - actions=[_assignment_action(form.price_variable_key, rule.price)], - ) - - # create one fallback rule in case none of the triggers hit - composite_negated_trigger = { - "!": {"or": [rule.json_logic_trigger for rule in rules]} - } - max_order += 1 - form.formlogic_set.create( - description="Converted price logic rule", - order=max_order, - is_advanced=True, - json_logic_trigger=composite_negated_trigger, - actions=[_assignment_action(form.price_variable_key, product.price)], - ) - - rules.delete() +convert_price_logic_rules_to_price_variable = import_string( + "openforms.forms.migrations" + ".0098_v270_to_v300.convert_price_logic_rules_to_price_variable" +) class Migration(migrations.Migration): diff --git a/src/openforms/forms/tests/test_migrations.py b/src/openforms/forms/tests/test_migrations.py index fea9e26022..ac7fa02fb2 100644 --- a/src/openforms/forms/tests/test_migrations.py +++ b/src/openforms/forms/tests/test_migrations.py @@ -8,8 +8,8 @@ class FormLogicMigrationTests(TestMigrations): app = "forms" - migrate_from = "0105_alter_form_all_submissions_removal_limit_and_more" - migrate_to = "0106_convert_price_logic_rules" + migrate_from = "0097_v267_to_v270" + migrate_to = "0098_v270_to_v300" def setUpBeforeMigration(self, apps: StateApps): # set up some variants that will each be hit for different submissions. After @@ -60,8 +60,8 @@ def test_price_variable_created(self): class DuplicatePriceVariableMigrationTests(TestMigrations): app = "forms" - migrate_from = "0105_alter_form_all_submissions_removal_limit_and_more" - migrate_to = "0106_convert_price_logic_rules" + migrate_from = "0097_v267_to_v270" + migrate_to = "0098_v270_to_v300" def setUpBeforeMigration(self, apps: StateApps): # set up some variants that will each be hit for different submissions. After diff --git a/src/openforms/registrations/contrib/objects_api/migrations/0016_v267_to_v300.py b/src/openforms/registrations/contrib/objects_api/migrations/0016_v267_to_v300.py new file mode 100644 index 0000000000..381da52bc4 --- /dev/null +++ b/src/openforms/registrations/contrib/objects_api/migrations/0016_v267_to_v300.py @@ -0,0 +1,83 @@ +# Generated by Django 4.2.17 on 2024-12-27 15:05 + +from django.db import migrations + + +class Migration(migrations.Migration): + + replaces = [ + ("registrations_objects_api", "0016_objectsapigroupconfig"), + ("registrations_objects_api", "0017_move_singleton_data"), + ( + "registrations_objects_api", + "0018_remove_objectsapiconfig_catalogi_service_and_more", + ), + ("registrations_objects_api", "0019_add_default_objects_api_group"), + ("registrations_objects_api", "0020_objecttype_url_to_uuid"), + ( + "registrations_objects_api", + "0021_objectsapigroupconfig_catalogue_domain_and_more", + ), + ( + "registrations_objects_api", + "0022_objectsapigroupconfig_iot_attachment_and_more", + ), + ( + "registrations_objects_api", + "0023_alter_objectsapigroupconfig_catalogue_domain", + ), + ( + "registrations_objects_api", + "0024_alter_objectsapigroupconfig_catalogi_service_and_more", + ), + ("registrations_objects_api", "0025_delete_objectsapigroupconfig"), + ] + + dependencies = [ + ("forms", "0097_v267_to_v270"), + ("registrations_objects_api", "0001_initial_to_v267"), + ("zgw_consumers", "0020_service_timeout"), + ] + + operations = [ + migrations.RemoveField( + model_name="objectsapiconfig", + name="catalogi_service", + ), + migrations.RemoveField( + model_name="objectsapiconfig", + name="drc_service", + ), + migrations.RemoveField( + model_name="objectsapiconfig", + name="informatieobjecttype_attachment", + ), + migrations.RemoveField( + model_name="objectsapiconfig", + name="informatieobjecttype_submission_csv", + ), + migrations.RemoveField( + model_name="objectsapiconfig", + name="informatieobjecttype_submission_report", + ), + migrations.RemoveField( + model_name="objectsapiconfig", + name="objects_service", + ), + migrations.RemoveField( + model_name="objectsapiconfig", + name="objecttype", + ), + migrations.RemoveField( + model_name="objectsapiconfig", + name="objecttype_version", + ), + migrations.RemoveField( + model_name="objectsapiconfig", + name="objecttypes_service", + ), + migrations.RemoveField( + model_name="objectsapiconfig", + name="organisatie_rsin", + ), + ] diff --git a/src/openforms/registrations/contrib/zgw_apis/migrations/0001_initial_to_v280.py b/src/openforms/registrations/contrib/zgw_apis/migrations/0001_initial_to_v280.py new file mode 100644 index 0000000000..b1c7a813a5 --- /dev/null +++ b/src/openforms/registrations/contrib/zgw_apis/migrations/0001_initial_to_v280.py @@ -0,0 +1,232 @@ +# Generated by Django 4.2.17 on 2024-12-27 16:11 + +import django.core.validators +import django.db.models.deletion +from django.db import migrations, models + +import openforms.registrations.contrib.zgw_apis.models +import openforms.template.validators +import openforms.utils.validators + + +class Migration(migrations.Migration): + + replaces = [ + ("zgw_apis", "0001_initial"), + ("zgw_apis", "0002_auto_20210514_1129"), + ("zgw_apis", "0003_auto_20210521_1352"), + ("zgw_apis", "0004_auto_20210902_2120"), + ("zgw_apis", "0005_auto_20230221_1552"), + ("zgw_apis", "0006_zgwapigroupconfig"), + ("zgw_apis", "0007_move_singleton_data"), + ("zgw_apis", "0008_auto_20230608_1443"), + ("zgw_apis", "0009_add_default"), + ("zgw_apis", "0010_zgwapigroupconfig_content_json"), + ("zgw_apis", "0011_move_zgw_api_group_defaults_to_form"), + ("zgw_apis", "0012_remove_zgwapigroupconfig_informatieobjecttype_and_more"), + ("zgw_apis", "0013_set_zgw_api_group"), + ("zgw_apis", "0014_zgwapigroupconfig_catalogue_domain_and_more"), + ] + + dependencies = [ + ("zgw_consumers", "0019_alter_service_uuid"), + ("forms", "0097_v267_to_v270"), + ] + + operations = [ + migrations.CreateModel( + name="ZGWApiGroupConfig", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "name", + models.CharField( + help_text="A recognisable name for this set of ZGW APIs.", + max_length=255, + verbose_name="name", + ), + ), + ( + "organisatie_rsin", + models.CharField( + blank=True, + help_text="Default RSIN of organization, which creates the ZAAK", + max_length=9, + validators=[openforms.utils.validators.RSINValidator()], + verbose_name="organisation RSIN", + ), + ), + ( + "zaak_vertrouwelijkheidaanduiding", + models.CharField( + blank=True, + choices=[ + ("openbaar", "Openbaar"), + ("beperkt_openbaar", "Beperkt openbaar"), + ("intern", "Intern"), + ("zaakvertrouwelijk", "Zaakvertrouwelijk"), + ("vertrouwelijk", "Vertrouwelijk"), + ("confidentieel", "Confidentieel"), + ("geheim", "Geheim"), + ("zeer_geheim", "Zeer geheim"), + ], + help_text="Indication of the level to which extend the ZAAK is meant to be public. Can be overridden in the Registration tab of a given form.", + max_length=24, + verbose_name="vertrouwelijkheidaanduiding zaak", + ), + ), + ( + "doc_vertrouwelijkheidaanduiding", + models.CharField( + blank=True, + choices=[ + ("openbaar", "Openbaar"), + ("beperkt_openbaar", "Beperkt openbaar"), + ("intern", "Intern"), + ("zaakvertrouwelijk", "Zaakvertrouwelijk"), + ("vertrouwelijk", "Vertrouwelijk"), + ("confidentieel", "Confidentieel"), + ("geheim", "Geheim"), + ("zeer_geheim", "Zeer geheim"), + ], + help_text="Indication of the level to which extend the document associated with the ZAAK is meant to be public. Can be overridden in the file upload component of a given form.", + max_length=24, + verbose_name="vertrouwelijkheidaanduiding document", + ), + ), + ( + "auteur", + models.CharField( + default="Aanvrager", max_length=200, verbose_name="auteur" + ), + ), + ( + "drc_service", + models.ForeignKey( + limit_choices_to={"api_type": "drc"}, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="zgwset_drc_config", + to="zgw_consumers.service", + verbose_name="Documenten API", + ), + ), + ( + "zrc_service", + models.ForeignKey( + limit_choices_to={"api_type": "zrc"}, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="zgwset_zrc_config", + to="zgw_consumers.service", + verbose_name="Zaken API", + ), + ), + ( + "ztc_service", + models.ForeignKey( + limit_choices_to={"api_type": "ztc"}, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="zgwset_ztc_config", + to="zgw_consumers.service", + verbose_name="Catalogi API", + ), + ), + ( + "content_json", + models.TextField( + blank=True, + default=openforms.registrations.contrib.zgw_apis.models.get_content_text, + help_text="This template is evaluated with the submission data and the resulting JSON is sent to the objects API.", + validators=[ + openforms.template.validators.DjangoTemplateValidator( + backend="openforms.template.openforms_backend" + ) + ], + verbose_name="objects API - JSON content template", + ), + ), + ( + "catalogue_domain", + models.CharField( + blank=True, + help_text="The 'domein' attribute for the Catalogus resource in the Catalogi API.", + max_length=5, + validators=[ + django.core.validators.RegexValidator( + "^[A-Z]*$", + code="invalid", + message="Value must be all uppercase letters.", + ) + ], + verbose_name="catalogus domain", + ), + ), + ( + "catalogue_rsin", + models.CharField( + blank=True, + help_text="The 'rsin' attribute for the Catalogus resource in the Catalogi API.", + max_length=9, + validators=[openforms.utils.validators.RSINValidator()], + verbose_name="catalogus RSIN", + ), + ), + ], + options={ + "verbose_name": "ZGW API set", + "verbose_name_plural": "ZGW API sets", + }, + ), + migrations.CreateModel( + name="ZgwConfig", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "default_zgw_api_group", + models.ForeignKey( + help_text="Which set of ZGW APIs should be used as default.", + null=True, + on_delete=django.db.models.deletion.PROTECT, + to="zgw_apis.zgwapigroupconfig", + verbose_name="default ZGW API set.", + ), + ), + ], + options={ + "verbose_name": "ZGW API's configuration", + }, + ), + migrations.AddConstraint( + model_name="zgwapigroupconfig", + constraint=models.CheckConstraint( + check=models.Q( + models.Q(("catalogue_domain", ""), ("catalogue_rsin", "")), + models.Q( + models.Q(("catalogue_domain", ""), _negated=True), + models.Q(("catalogue_rsin", ""), _negated=True), + ), + _connector="OR", + ), + name="registrations_zgw_apis_catalogue_composite_key", + violation_error_message="You must specify both domain and RSIN to uniquely identify a catalogue.", + ), + ), + ] diff --git a/src/openforms/registrations/contrib/zgw_apis/tests/test_migrations.py b/src/openforms/registrations/contrib/zgw_apis/tests/test_migrations.py index d44b339e53..3e10e495c0 100644 --- a/src/openforms/registrations/contrib/zgw_apis/tests/test_migrations.py +++ b/src/openforms/registrations/contrib/zgw_apis/tests/test_migrations.py @@ -5,7 +5,7 @@ class MigrateToExplicitObjectsAPIGroupsTests(TestMigrations): app = "zgw_apis" - migrate_from = "0014_zgwapigroupconfig_catalogue_domain_and_more" + migrate_from = "0001_initial_to_v280" migrate_to = "0015_explicit_objects_api_groups" def setUpBeforeMigration(self, apps: StateApps): diff --git a/src/openforms/submissions/migrations/0002_v230_to_v300.py b/src/openforms/submissions/migrations/0002_v230_to_v300.py new file mode 100644 index 0000000000..57615fb1e7 --- /dev/null +++ b/src/openforms/submissions/migrations/0002_v230_to_v300.py @@ -0,0 +1,271 @@ +# Generated by Django 4.2.17 on 2024-12-27 16:04 + +import re + +import django.core.validators +import django.db.models.deletion +from django.db import migrations, models + +import django_jsonform.models.fields + +import openforms.submissions.models.email_verification +import openforms.submissions.models.submission_value_variable + + +class Migration(migrations.Migration): + + replaces = [ + ("submissions", "0002_change_json_encoder"), + ("submissions", "0003_cleanup_urls"), + ("submissions", "0004_auto_20231128_1536"), + ("submissions", "0005_temporaryfileupload_legacy_and_more"), + ("submissions", "0006_set_legacy_true"), + ("submissions", "0007_add_legacy_constraint"), + ("submissions", "0008_submission_initial_data_reference"), + ( + "submissions", + "0009_submission_only_completed_submission_has_finalised_registration_backend_key", + ), + ("submissions", "0010_emailverification"), + ("submissions", "0011_remove_submissionstep__data"), + ("submissions", "0012_alter_submission_price"), + ( + "submissions", + "0013_remove_temporaryfileupload_non_legacy_submission_not_null_and_more", + ), + ("submissions", "0013_remove_submission_previous_submission"), + ("submissions", "0014_merge_20241211_1732"), + ] + + dependencies = [ + ("submissions", "0001_initial_to_openforms_v230"), + ] + + operations = [ + migrations.AlterField( + model_name="submissionvaluevariable", + name="value", + field=models.JSONField( + blank=True, + encoder=openforms.submissions.models.submission_value_variable.ValueEncoder, + help_text="The value of the variable", + null=True, + verbose_name="value", + ), + ), + migrations.AddField( + model_name="submission", + name="cosign_confirmation_email_sent", + field=models.BooleanField( + default=False, + help_text="Has the confirmation email been sent after the submission has successfully been cosigned?", + verbose_name="cosign confirmation email sent", + ), + ), + migrations.AddField( + model_name="submission", + name="cosign_request_email_sent", + field=models.BooleanField( + default=False, + help_text="Has the email to request a co-sign been sent?", + verbose_name="cosign request email sent", + ), + ), + migrations.AddField( + model_name="submission", + name="payment_complete_confirmation_email_sent", + field=models.BooleanField( + default=False, + help_text="Has the confirmation emails been sent after successful payment?", + verbose_name="payment complete confirmation email sent", + ), + ), + migrations.CreateModel( + name="PostCompletionMetadata", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "tasks_ids", + django_jsonform.models.fields.ArrayField( + base_field=models.CharField( + blank=True, max_length=255, verbose_name="celery task ID" + ), + blank=True, + default=list, + help_text="Celery task IDs of the tasks queued once a post submission event happens.", + size=None, + verbose_name="task IDs", + ), + ), + ( + "created_on", + models.DateTimeField(auto_now_add=True, verbose_name="created on"), + ), + ( + "trigger_event", + models.CharField( + choices=[ + ("on_completion", "On completion"), + ("on_payment_complete", "On payment complete"), + ("on_cosign_complete", "On cosign complete"), + ("on_retry", "On retry"), + ], + help_text="Which event scheduled these tasks.", + max_length=100, + verbose_name="created by", + ), + ), + ( + "submission", + models.ForeignKey( + help_text="Submission to which the result belongs to.", + on_delete=django.db.models.deletion.CASCADE, + to="submissions.submission", + verbose_name="submission", + ), + ), + ], + options={ + "verbose_name": "post completion metadata", + "verbose_name_plural": "post completion metadata", + }, + ), + migrations.AddConstraint( + model_name="postcompletionmetadata", + constraint=models.UniqueConstraint( + condition=models.Q(("trigger_event", "on_completion")), + fields=("submission",), + name="unique_on_completion_event", + ), + ), + migrations.AddField( + model_name="temporaryfileupload", + name="submission", + field=models.ForeignKey( + help_text="Submission the temporary file upload belongs to.", + on_delete=django.db.models.deletion.CASCADE, + to="submissions.submission", + verbose_name="submission", + ), + ), + migrations.AddField( + model_name="submission", + name="initial_data_reference", + field=models.CharField( + blank=True, + help_text="An identifier that can be passed as a querystring when the form is started. Initial form field values are pre-populated from the retrieved data. During registration, the object may be updated again (or a new record may be created). This can be an object reference in the Objects API, for example.", + verbose_name="initial data reference", + ), + ), + migrations.AddConstraint( + model_name="submission", + constraint=models.CheckConstraint( + check=models.Q( + ("finalised_registration_backend_key", ""), + models.Q( + models.Q( + ("finalised_registration_backend_key", ""), _negated=True + ), + ("completed_on__isnull", False), + ), + _connector="OR", + ), + name="only_completed_submission_has_finalised_registration_backend_key", + violation_error_message="Only completed submissions may persist a finalised registration backend key.", + ), + ), + migrations.CreateModel( + name="EmailVerification", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "component_key", + models.TextField( + help_text="Key of the email component in the submission's form.", + validators=[ + django.core.validators.RegexValidator( + message="Invalid variable key. It must only contain alphanumeric characters, underscores, dots and dashes and should not be ended by dash or dot.", + regex=re.compile("^(\\w|\\w[\\w.\\-]*\\w)$"), + ) + ], + verbose_name="component key", + ), + ), + ( + "email", + models.EmailField( + help_text="The email address that is being verified.", + max_length=254, + verbose_name="email address", + ), + ), + ( + "verification_code", + models.CharField( + default=openforms.submissions.models.email_verification.generate_verification_code, + max_length=6, + verbose_name="verification code", + ), + ), + ( + "verified_on", + models.DateTimeField( + blank=True, + help_text="Unverified emails have no timestamp set.", + null=True, + verbose_name="verification timestamp", + ), + ), + ( + "submission", + models.ForeignKey( + help_text="The submission during which the email verification was initiated.", + on_delete=django.db.models.deletion.CASCADE, + to="submissions.submission", + verbose_name="submission", + ), + ), + ], + options={ + "verbose_name": "email verification", + "verbose_name_plural": "email verifications", + }, + ), + migrations.RemoveField( + model_name="submissionstep", + name="_data", + ), + migrations.AlterField( + model_name="submission", + name="price", + field=models.DecimalField( + blank=True, + decimal_places=2, + editable=False, + help_text="Cost of this submission. Either derived from the related product, or set through logic rules. The price is calculated and saved on submission completion.", + max_digits=10, + null=True, + verbose_name="price", + ), + ), + migrations.RemoveField( + model_name="submission", + name="previous_submission", + ), + ] diff --git a/src/openforms/submissions/migrations/0009_submission_only_completed_submission_has_finalised_registration_backend_key.py b/src/openforms/submissions/migrations/0009_submission_only_completed_submission_has_finalised_registration_backend_key.py index f785331049..a771a8d7f6 100644 --- a/src/openforms/submissions/migrations/0009_submission_only_completed_submission_has_finalised_registration_backend_key.py +++ b/src/openforms/submissions/migrations/0009_submission_only_completed_submission_has_finalised_registration_backend_key.py @@ -3,17 +3,6 @@ from django.db import migrations, models -def clear_registration_backend_key_for_incomplete_submissions(apps, _): - Submission = apps.get_model("submissions", "Submission") - - # clear the value of the registration backend key for submissions that are not - # completed yet - qs = Submission.objects.filter(completed_on__isnull=True).exclude( - finalised_registration_backend_key="" - ) - qs.update(finalised_registration_backend_key="") - - class Migration(migrations.Migration): dependencies = [ @@ -21,10 +10,8 @@ class Migration(migrations.Migration): ] operations = [ - migrations.RunPython( - clear_registration_backend_key_for_incomplete_submissions, - migrations.RunPython.noop, - ), + # RunPython operation removed as part of 3.0 release cycle - these migrations are + # guaranteed to have been executed on Open Forms 2.8.x for existing instances. migrations.AddConstraint( model_name="submission", constraint=models.CheckConstraint( diff --git a/src/openforms/upgrades/upgrade_paths.py b/src/openforms/upgrades/upgrade_paths.py index 3f2f6f39b2..67a0932e81 100644 --- a/src/openforms/upgrades/upgrade_paths.py +++ b/src/openforms/upgrades/upgrade_paths.py @@ -78,7 +78,7 @@ def run_checks(self) -> bool: UPGRADE_PATHS = { "3.0": UpgradeConstraint( valid_ranges={ - VersionRange(minimum="2.8.0"), + VersionRange(minimum="2.8.2"), }, scripts=["check_temporary_uploads", "check_api_groups_null"], ), @@ -87,11 +87,6 @@ def run_checks(self) -> bool: VersionRange(minimum="2.7.4"), }, ), - "3.0.0": UpgradeConstraint( - valid_ranges={ - VersionRange(minimum="2.8.2"), - }, - ), }