diff --git a/wagtail/core/migrations/0047_add_workflow_models.py b/wagtail/core/migrations/0047_add_workflow_models.py index 4994363eba..84624563ca 100755 --- a/wagtail/core/migrations/0047_add_workflow_models.py +++ b/wagtail/core/migrations/0047_add_workflow_models.py @@ -1,4 +1,4 @@ -# Generated by Django 3.0.3 on 2020-02-17 10:19 +# Generated by Django 3.0.3 on 2020-02-18 15:33 from django.conf import settings from django.db import migrations, models @@ -10,9 +10,9 @@ import wagtail.core.models class Migration(migrations.Migration): dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), ('contenttypes', '0002_remove_content_type_name'), ('wagtailcore', '0046_site_name_remove_null'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), ('auth', '0011_update_proxy_permissions'), ] @@ -76,7 +76,7 @@ class Migration(migrations.Migration): ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('status', models.CharField(choices=[('in_progress', 'In progress'), ('approved', 'Approved'), ('rejected', 'Rejected'), ('cancelled', 'Cancelled')], default='in_progress', max_length=50, verbose_name='status')), ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')), - ('current_task_state', models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, to='wagtailcore.TaskState', verbose_name='current task state')), + ('current_task_state', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='wagtailcore.TaskState', verbose_name='current task state')), ('page', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workflow_states', to='wagtailcore.Page', verbose_name='page')), ('requested_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='requested_workflows', to=settings.AUTH_USER_MODEL, verbose_name='requested by')), ('workflow', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workflow_states', to='wagtailcore.Workflow', verbose_name='workflow')), diff --git a/wagtail/core/models.py b/wagtail/core/models.py index 665dcc54e3..9926d62c6d 100644 --- a/wagtail/core/models.py +++ b/wagtail/core/models.py @@ -2785,12 +2785,22 @@ class WorkflowState(models.Model): editable=True, on_delete=models.SET_NULL, related_name='requested_workflows') - current_task_state = models.OneToOneField('TaskState', on_delete=models.SET_NULL, null=True, blank=False, + current_task_state = models.OneToOneField('TaskState', on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_("current task state")) # allows a custom function to be called on finishing the Workflow successfully. on_finish = import_string(getattr(settings, 'WAGTAIL_FINISH_WORKFLOW_ACTION', 'wagtail.core.workflows.publish_workflow_state')) + def clean(self): + super().clean() + # The unique constraint is conditional, and so not supported on the MySQL backend - so an additional check is done here + if WorkflowState.objects.filter(status=self.STATUS_IN_PROGRESS, page=self.page).exclude(pk=self.pk).exists(): + raise ValidationError(_('There may only be one in progress workflow state per page.')) + + def save(self, *args, **kwargs): + self.full_clean() + return super().save(*args, **kwargs) + def __str__(self): return _("Workflow '{0}' on Page '{1}': {2}").format(self.workflow, self.page, self.status) @@ -2884,7 +2894,7 @@ class WorkflowState(models.Model): class Meta: verbose_name = _('Workflow state') verbose_name_plural = _('Workflow states') - # prevent multiple STATUS_IN_PROGRESS workflows for the same page + # prevent multiple STATUS_IN_PROGRESS workflows for the same page. This is not supported by MySQL, so is checked additionally on save. constraints = [ models.UniqueConstraint(fields=['page'], condition=Q(status='in_progress'), name='unique_in_progress_workflow') ] diff --git a/wagtail/core/tests/test_workflow.py b/wagtail/core/tests/test_workflow.py index 872417c648..ead218e437 100644 --- a/wagtail/core/tests/test_workflow.py +++ b/wagtail/core/tests/test_workflow.py @@ -3,6 +3,7 @@ import datetime import pytz from django.contrib.auth import get_user_model from django.contrib.auth.models import Group +from django.core.exceptions import ValidationError from django.db.utils import IntegrityError from django.test import TestCase, override_settings @@ -115,7 +116,7 @@ class TestWorkflows(TestCase): def test_error_when_starting_multiple_in_progress_workflows(self): # test trying to start multiple status='in_progress' workflows on a single page will trigger an IntegrityError self.start_workflow_on_homepage() - with self.assertRaises(IntegrityError): + with self.assertRaises((IntegrityError, ValidationError)): self.start_workflow_on_homepage() @freeze_time("2017-01-01 12:00:00")