From b9b8ae71055ba3ad2a8589bd7d4bc54f14539e31 Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Fri, 27 Dec 2024 14:02:21 +0100 Subject: [PATCH 01/10] :wrench: De-duplicate upgrade path checks --- src/openforms/upgrades/upgrade_paths.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) 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"), - }, - ), } From 693d3aa83653956b05320f2fc0308bc313a08997 Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Fri, 27 Dec 2024 14:05:21 +0100 Subject: [PATCH 02/10] :card_file_box: [#4920] Squash analytics_tools migrations Squashed all migrations into a single operation. Requires 2.8.0 to have been run (which is enforced by the upgrade checks). Once 3.0 is released, the original migrations can be removed. --- .../migrations/0001_initial_to_v300.py | 367 ++++++++++++++++++ 1 file changed, 367 insertions(+) create mode 100644 src/openforms/analytics_tools/migrations/0001_initial_to_v300.py 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", + }, + ), + ] From 7439c211d5a2b2561f3e68f4f231a0c3a85c6867 Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Fri, 27 Dec 2024 14:45:52 +0100 Subject: [PATCH 03/10] :card_file_box: [#4920] Empty obsoleted data migrations Replaced some RunPython operations that are guaranteed to have been run on Open Forms 2.8.x with empty operations so that we can properly optimize migrations during squashing. --- .../config/migrations/0054_v250_to_v270.py | 53 +---------------- ...0056_disable_prefill_objects_api_plugin.py | 19 +----- ...guration_form_upload_default_file_types.py | 27 +-------- src/openforms/config/tests/test_migrations.py | 59 ------------------- 4 files changed, 7 insertions(+), 151 deletions(-) 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/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/tests/test_migrations.py b/src/openforms/config/tests/test_migrations.py index 9f66ffc799..0083246306 100644 --- a/src/openforms/config/tests/test_migrations.py +++ b/src/openforms/config/tests/test_migrations.py @@ -3,65 +3,6 @@ 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" From 50d6c06907608f3b7837a3ec5a857518322e5887 Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Fri, 27 Dec 2024 15:07:44 +0100 Subject: [PATCH 04/10] :card_file_box: [#4920] Result of squashmigrations from 0055 to 0069 Manually commented out some of the RunPython to hopefully better optimize the squashed migration, and added those operations back in to the squashed migration (mostly the resetting of the default template translations which is a workaround). The update_summary_tags in the original migration now imports the definition from the squashed migration rather than not executing it at all, as some dev-environments might miss this step if they pull master not frequently enough. This migration can be further optimized by hand, see the next commit. --- .../config/migrations/0055_v270_to_v300.py | 684 ++++++++++++++++++ .../migrations/0068_update_summary_tags.py | 36 +- 2 files changed, 688 insertions(+), 32 deletions(-) create mode 100644 src/openforms/config/migrations/0055_v270_to_v300.py 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..782a62a82d --- /dev/null +++ b/src/openforms/config/migrations/0055_v270_to_v300.py @@ -0,0 +1,684 @@ +# 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 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", + "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.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/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): From 025d6d71af0c4cce279b40f1da449a6159824a65 Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Fri, 27 Dec 2024 15:14:56 +0100 Subject: [PATCH 05/10] :card_file_box: [#4920] Include the 0001-0059-0062 branch in the squashed migration Due to the forward-ports we ended up with multiple merge migrations each overlapping a bit, and 0063 brought them all together. Squashmigrations can only handle a single branch of migrations and left out the forward-ported migrations 0059 and 0062. These can safely be included manually as well, as they basically only update the model state in migrations. --- .../config/migrations/0055_v270_to_v300.py | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/src/openforms/config/migrations/0055_v270_to_v300.py b/src/openforms/config/migrations/0055_v270_to_v300.py index 782a62a82d..06f082a042 100644 --- a/src/openforms/config/migrations/0055_v270_to_v300.py +++ b/src/openforms/config/migrations/0055_v270_to_v300.py @@ -7,6 +7,7 @@ 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 @@ -52,6 +53,14 @@ def update_summary_tags(apps: StateApps, _): 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", @@ -82,6 +91,54 @@ class Migration(migrations.Migration): ] 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", From da6c07b579113711658dc86ba8e75b880a154060 Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Fri, 27 Dec 2024 15:42:32 +0100 Subject: [PATCH 06/10] :card_file_box: [#4920] Squash forms app migrations Similar to the config app, migrations were squashed and some merge migrations were involved, but this time Django picked it up correctly. The price logic rules conversion was moved and in the original migration a dynamic import is now used instead to avoid duplicating the implementation. --- .../0097_extra_mimetypes_in_file_type.py | 10 +- .../forms/migrations/0098_v270_to_v300.py | 332 ++++++++++++++++++ .../0106_convert_price_logic_rules.py | 95 +---- src/openforms/forms/tests/test_migrations.py | 8 +- 4 files changed, 344 insertions(+), 101 deletions(-) create mode 100644 src/openforms/forms/migrations/0098_v270_to_v300.py 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 From d76d5e57f90707e769aadb3a3f791f8c3339c2c4 Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Fri, 27 Dec 2024 16:43:16 +0100 Subject: [PATCH 07/10] :card_file_box: [#4920] Clean up and squash objects API migrations The Objects API group config model was moved from the registrations app to the general purpose contrib app for the Objects API, as it is used by both prefill and registration plugins. This involved some dark migrations magic with SeparateDatabaseAndState to move the model without breaking the existing table or messing with table names. Now that we're guaranteed existing instances have upgrade to 2.8, we know that the tables are properly in the right place and the necessary migrations have been executed, allowing us to turn them into normal migrations so that fresh installs can take the direct path rather than the detour, and it allows us to clean up some migration code. The rest of the migrations in the contrib app are not yet squashed, as they are primarly operations around RunPython that need to be executed on 3.0 instances. --- src/openforms/config/tests/test_migrations.py | 4 +- .../objects_api/migrations/0001_initial.py | 454 +++++++++--------- .../0002_objectsapigroupconfig_identifier.py | 5 - .../migrations/0016_v267_to_v300.py | 83 ++++ 4 files changed, 310 insertions(+), 236 deletions(-) create mode 100644 src/openforms/registrations/contrib/objects_api/migrations/0016_v267_to_v300.py diff --git a/src/openforms/config/tests/test_migrations.py b/src/openforms/config/tests/test_migrations.py index 0083246306..65bc6c01d1 100644 --- a/src/openforms/config/tests/test_migrations.py +++ b/src/openforms/config/tests/test_migrations.py @@ -5,8 +5,8 @@ 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/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", + ), + ] From bcde66c56105473c2f2fcbe968a7c7e967ffb1da Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Fri, 27 Dec 2024 17:07:37 +0100 Subject: [PATCH 08/10] :card_file_box: [#4920] Squashed submissions migrations Squashed all migrations since 2.3.0 into a single file, optimizing away some in-between states for refactors that happened along the way. RunPython operations have been emptied since they're guaranteed to have run on OF 2.8.x, per the upgrade requirements. --- .../migrations/0002_v230_to_v300.py | 271 ++++++++++++++++++ ..._has_finalised_registration_backend_key.py | 17 +- 2 files changed, 273 insertions(+), 15 deletions(-) create mode 100644 src/openforms/submissions/migrations/0002_v230_to_v300.py 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( From a2d8e48649e4286c0da8285bcad913e4a005a3a5 Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Fri, 27 Dec 2024 17:12:48 +0100 Subject: [PATCH 09/10] :card_file_box: [#4920] Squash ZGW API's registration plugin migrations These have never been squashed before. Only the migrations before 3.0 are squashed, as the migrations in 3.0 contain mostly RunPython operations that still need to be executed. --- .../migrations/0001_initial_to_v280.py | 232 ++++++++++++++++++ .../contrib/zgw_apis/tests/test_migrations.py | 2 +- 2 files changed, 233 insertions(+), 1 deletion(-) create mode 100644 src/openforms/registrations/contrib/zgw_apis/migrations/0001_initial_to_v280.py 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): From 7d6be9c8a3ff8262eaf4608df0dcca9427ee7c6b Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Fri, 27 Dec 2024 17:58:58 +0100 Subject: [PATCH 10/10] :construction_worker: [#4920] Upgrade CI job for upgrade simulation We require users to be on OF 2.8 before they can upgrade to 3.0 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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