diff --git a/apps/api/plane/db/migrations/0106_auto_20250912_0845.py b/apps/api/plane/db/migrations/0106_auto_20250912_0845.py new file mode 100644 index 000000000..8a0813fc1 --- /dev/null +++ b/apps/api/plane/db/migrations/0106_auto_20250912_0845.py @@ -0,0 +1,152 @@ +# Generated by Django 4.2.22 on 2025-09-12 08:45 +import uuid +import django +from django.conf import settings +from django.db import migrations, models + + +def set_page_sort_order(apps, schema_editor): + Page = apps.get_model("db", "Page") + + batch_size = 3000 + sort_order = 100 + + # Get page IDs ordered by name using the historical model + # This should include all pages regardless of soft-delete status + page_ids = list(Page.objects.all().order_by("name").values_list("id", flat=True)) + + updated_pages = [] + for page_id in page_ids: + # Create page instance with minimal data + updated_pages.append(Page(id=page_id, sort_order=sort_order)) + sort_order += 100 + + # Bulk update when batch is full + if len(updated_pages) >= batch_size: + Page.objects.bulk_update( + updated_pages, ["sort_order"], batch_size=batch_size + ) + updated_pages = [] + + # Update remaining pages + if updated_pages: + Page.objects.bulk_update(updated_pages, ["sort_order"], batch_size=batch_size) + + +def reverse_set_page_sort_order(apps, schema_editor): + Page = apps.get_model("db", "Page") + Page.objects.update(sort_order=Page.DEFAULT_SORT_ORDER) + + +class Migration(migrations.Migration): + + dependencies = [ + ("db", "0105_alter_project_cycle_view_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="ProjectWebhook", + fields=[ + ( + "created_at", + models.DateTimeField(auto_now_add=True, verbose_name="Created At"), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "deleted_at", + models.DateTimeField( + blank=True, null=True, verbose_name="Deleted At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + ( + "webhook", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="project_webhooks", + to="db.webhook", + ), + ), + ( + "workspace", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + ], + options={ + "verbose_name": "Project Webhook", + "verbose_name_plural": "Project Webhooks", + "db_table": "project_webhooks", + "ordering": ("-created_at",), + }, + ), + migrations.AddConstraint( + model_name="projectwebhook", + constraint=models.UniqueConstraint( + condition=models.Q(("deleted_at__isnull", True)), + fields=("project", "webhook"), + name="project_webhook_unique_project_webhook_when_deleted_at_null", + ), + ), + migrations.AlterUniqueTogether( + name="projectwebhook", + unique_together={("project", "webhook", "deleted_at")}, + ), + migrations.AlterField( + model_name="issuerelation", + name="relation_type", + field=models.CharField( + default="blocked_by", max_length=20, verbose_name="Issue Relation Type" + ), + ), + migrations.RunPython( + set_page_sort_order, reverse_code=reverse_set_page_sort_order + ), + ] diff --git a/apps/api/plane/db/models/issue.py b/apps/api/plane/db/models/issue.py index b8efd6ae7..2baf8ace1 100644 --- a/apps/api/plane/db/models/issue.py +++ b/apps/api/plane/db/models/issue.py @@ -284,6 +284,7 @@ class IssueRelationChoices(models.TextChoices): BLOCKED_BY = "blocked_by", "Blocked By" START_BEFORE = "start_before", "Start Before" FINISH_BEFORE = "finish_before", "Finish Before" + IMPLEMENTED_BY = "implemented_by", "Implemented By" class IssueRelation(ProjectBaseModel): @@ -295,7 +296,6 @@ class IssueRelation(ProjectBaseModel): ) relation_type = models.CharField( max_length=20, - choices=IssueRelationChoices.choices, verbose_name="Issue Relation Type", default=IssueRelationChoices.BLOCKED_BY, ) diff --git a/apps/api/plane/db/models/page.py b/apps/api/plane/db/models/page.py index 71fc49c45..4d465cd58 100644 --- a/apps/api/plane/db/models/page.py +++ b/apps/api/plane/db/models/page.py @@ -19,6 +19,7 @@ def get_view_props(): class Page(BaseModel): PRIVATE_ACCESS = 1 PUBLIC_ACCESS = 0 + DEFAULT_SORT_ORDER = 65535 ACCESS_CHOICES = ((PRIVATE_ACCESS, "Private"), (PUBLIC_ACCESS, "Public")) @@ -57,7 +58,7 @@ class Page(BaseModel): ) moved_to_page = models.UUIDField(null=True, blank=True) moved_to_project = models.UUIDField(null=True, blank=True) - sort_order = models.FloatField(default=65535) + sort_order = models.FloatField(default=DEFAULT_SORT_ORDER) external_id = models.CharField(max_length=255, null=True, blank=True) external_source = models.CharField(max_length=255, null=True, blank=True) diff --git a/apps/api/plane/db/models/webhook.py b/apps/api/plane/db/models/webhook.py index b1428523b..189ccb279 100644 --- a/apps/api/plane/db/models/webhook.py +++ b/apps/api/plane/db/models/webhook.py @@ -7,7 +7,7 @@ from django.db import models from django.core.exceptions import ValidationError # Module imports -from plane.db.models import BaseModel +from plane.db.models import BaseModel, ProjectBaseModel def generate_token(): @@ -90,3 +90,24 @@ class WebhookLog(BaseModel): def __str__(self): return f"{self.event_type} {str(self.webhook)}" + + + +class ProjectWebhook(ProjectBaseModel): + webhook = models.ForeignKey( + "db.Webhook", on_delete=models.CASCADE, related_name="project_webhooks" + ) + + class Meta: + unique_together = ["project", "webhook", "deleted_at"] + constraints = [ + models.UniqueConstraint( + fields=["project", "webhook"], + condition=models.Q(deleted_at__isnull=True), + name="project_webhook_unique_project_webhook_when_deleted_at_null", + ) + ] + verbose_name = "Project Webhook" + verbose_name_plural = "Project Webhooks" + db_table = "project_webhooks" + ordering = ("-created_at",) \ No newline at end of file diff --git a/apps/api/plane/utils/issue_relation_mapper.py b/apps/api/plane/utils/issue_relation_mapper.py index f3188eb26..19d65c111 100644 --- a/apps/api/plane/utils/issue_relation_mapper.py +++ b/apps/api/plane/utils/issue_relation_mapper.py @@ -6,12 +6,14 @@ def get_inverse_relation(relation_type): "blocking": "blocked_by", "start_before": "start_after", "finish_before": "finish_after", + "implemented_by": "implements", + "implements": "implemented_by", } return relation_mapping.get(relation_type, relation_type) def get_actual_relation(relation_type): - # This function is used to get the actual relation type which is store in database + # This function is used to get the actual relation type which is stored in database actual_relation = { "start_after": "start_before", "finish_after": "finish_before", @@ -19,6 +21,8 @@ def get_actual_relation(relation_type): "blocked_by": "blocked_by", "start_before": "start_before", "finish_before": "finish_before", + "implemented_by": "implemented_by", + "implements": "implemented_by", } return actual_relation.get(relation_type, relation_type)