diff --git a/authentik/tasks/migrations/0007_task_dependencies_alter_task_aggregated_status_and_more.py b/authentik/tasks/migrations/0007_alter_task_aggregated_status_alter_task_state_and_more.py similarity index 56% rename from authentik/tasks/migrations/0007_task_dependencies_alter_task_aggregated_status_and_more.py rename to authentik/tasks/migrations/0007_alter_task_aggregated_status_alter_task_state_and_more.py index 602adb2b2c..46e9fc834b 100644 --- a/authentik/tasks/migrations/0007_task_dependencies_alter_task_aggregated_status_and_more.py +++ b/authentik/tasks/migrations/0007_alter_task_aggregated_status_alter_task_state_and_more.py @@ -1,5 +1,6 @@ -# Generated by Django 5.2.15 on 2026-06-16 12:54 +# Generated by Django 5.2.15 on 2026-06-17 11:43 +import django.db.models.deletion from django.db import migrations, models @@ -10,14 +11,6 @@ class Migration(migrations.Migration): ] operations = [ - migrations.AddField( - model_name="task", - name="dependencies", - field=models.ManyToManyField( - to="authentik_tasks.task", - verbose_name="Tasks that must complete for this task to run.", - ), - ), migrations.AlterField( model_name="task", name="aggregated_status", @@ -55,4 +48,51 @@ class Migration(migrations.Migration): help_text="Task status", ), ), + migrations.CreateModel( + name="TaskDependency", + fields=[ + ( + "pk", + models.CompositePrimaryKey( + "task", + "dependency", + blank=True, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ( + "dependency", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="+", + to="authentik_tasks.task", + ), + ), + ( + "task", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="+", + to="authentik_tasks.task", + ), + ), + ], + options={ + "verbose_name": "Task dependency", + "verbose_name_plural": "Task dependencies", + "default_permissions": [], + }, + ), + migrations.AddField( + model_name="task", + name="dependencies", + field=models.ManyToManyField( + through="authentik_tasks.TaskDependency", + through_fields=("task", "dependency"), + to="authentik_tasks.task", + verbose_name="Tasks that must complete for this task to run.", + ), + ), ] diff --git a/authentik/tasks/models.py b/authentik/tasks/models.py index 3284a15d34..4a119410e8 100644 --- a/authentik/tasks/models.py +++ b/authentik/tasks/models.py @@ -34,6 +34,15 @@ class TaskStatus(models.TextChoices): class Task(InternallyManagedMixin, SerializerModel, TaskBase): + # Overridden from TaskBase to use a through model + dependencies = models.ManyToManyField( + "self", + verbose_name=_("Tasks that must complete for this task to run."), + symmetrical=False, + through="TaskDependency", + through_fields=("task", "dependency"), + ) + tenant = models.ForeignKey( Tenant, on_delete=models.CASCADE, @@ -150,6 +159,20 @@ class Task(InternallyManagedMixin, SerializerModel, TaskBase): self.log(self.uid, TaskStatus.ERROR, message, **attributes) +class TaskDependency(models.Model): + pk = models.CompositePrimaryKey("task", "dependency") + task = models.ForeignKey(Task, on_delete=models.CASCADE, related_name="+") + dependency = models.ForeignKey(Task, on_delete=models.CASCADE, related_name="+") + + class Meta: + default_permissions = [] + verbose_name = _("Task dependency") + verbose_name_plural = _("Task dependencies") + + def __str__(self): + return f"Task {self.pk[0]} dependency on {self.pk[1]}" + + class TaskLog(InternallyManagedMixin, models.Model): id = models.UUIDField(default=uuid4, primary_key=True, editable=False)