From c4f40bc9cf6af3e0c12d48366e5cf10d253e53a7 Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Fri, 12 Sep 2025 15:00:15 +0530 Subject: [PATCH 1/6] chore: added issue relation and page sort order --- .../db/migrations/0106_auto_20250912_0845.py | 52 +++++++++++++++++++ apps/api/plane/db/models/issue.py | 2 +- apps/api/plane/utils/issue_relation_mapper.py | 6 ++- 3 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 apps/api/plane/db/migrations/0106_auto_20250912_0845.py 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 00000000000..77a756339db --- /dev/null +++ b/apps/api/plane/db/migrations/0106_auto_20250912_0845.py @@ -0,0 +1,52 @@ +# Generated by Django 4.2.22 on 2025-09-12 08:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("db", "0105_alter_project_cycle_view_and_more"), + ] + + 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 + ) + + operations = [ + 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), + ] diff --git a/apps/api/plane/db/models/issue.py b/apps/api/plane/db/models/issue.py index b8efd6ae736..2baf8ace116 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/utils/issue_relation_mapper.py b/apps/api/plane/utils/issue_relation_mapper.py index f3188eb2682..19d65c1112d 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) From 733cbf21aa759b0b3427ee49d28ae1ad2f890f62 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Fri, 12 Sep 2025 17:12:58 +0530 Subject: [PATCH 2/6] feat: add ProjectWebhook model to manage webhooks associated with projects --- apps/api/plane/db/models/webhook.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/apps/api/plane/db/models/webhook.py b/apps/api/plane/db/models/webhook.py index b1428523b49..189ccb279f4 100644 --- a/apps/api/plane/db/models/webhook.py +++ b/apps/api/plane/db/models/webhook.py @@ -7,7 +7,7 @@ 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 Meta: 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 From cec249bf4705b854a1a9ab50bbfe1af2a53ac732 Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Fri, 12 Sep 2025 17:15:16 +0530 Subject: [PATCH 3/6] chore: updated the migration file --- apps/api/plane/app/views/page/base.py | 18 ----- .../db/migrations/0106_auto_20250912_0845.py | 68 ++++++++++--------- 2 files changed, 36 insertions(+), 50 deletions(-) diff --git a/apps/api/plane/app/views/page/base.py b/apps/api/plane/app/views/page/base.py index e4ee1890b76..08a1a98507f 100644 --- a/apps/api/plane/app/views/page/base.py +++ b/apps/api/plane/app/views/page/base.py @@ -44,22 +44,6 @@ from plane.bgtasks.copy_s3_object import copy_s3_objects_of_description_and_assets -def unarchive_archive_page_and_descendants(page_id, archived_at): - # Your SQL query - sql = """ - WITH RECURSIVE descendants AS ( - SELECT id FROM pages WHERE id = %s - UNION ALL - SELECT pages.id FROM pages, descendants WHERE pages.parent_id = descendants.id - ) - UPDATE pages SET archived_at = %s WHERE id IN (SELECT id FROM descendants); - """ - - # Execute the SQL query - with connection.cursor() as cursor: - cursor.execute(sql, [page_id, archived_at]) - - class PageViewSet(BaseViewSet): serializer_class = PageSerializer model = Page @@ -326,8 +310,6 @@ def archive(self, request, slug, project_id, pk): workspace__slug=slug, ).delete() - unarchive_archive_page_and_descendants(pk, datetime.now()) - return Response({"archived_at": str(datetime.now())}, status=status.HTTP_200_OK) @allow_permission([ROLE.ADMIN], model=Page, creator=True) diff --git a/apps/api/plane/db/migrations/0106_auto_20250912_0845.py b/apps/api/plane/db/migrations/0106_auto_20250912_0845.py index 77a756339db..b570d48d4da 100644 --- a/apps/api/plane/db/migrations/0106_auto_20250912_0845.py +++ b/apps/api/plane/db/migrations/0106_auto_20250912_0845.py @@ -3,42 +3,44 @@ from django.db import migrations, models -class Migration(migrations.Migration): +def set_page_sort_order(apps, schema_editor): + Page = apps.get_model("db", "Page") - dependencies = [ - ("db", "0105_alter_project_cycle_view_and_more"), - ] + 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)) - 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: + 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=65535) + + +class Migration(migrations.Migration): + + dependencies = [ + ("db", "0105_alter_project_cycle_view_and_more"), + ] operations = [ migrations.AlterField( @@ -48,5 +50,7 @@ def set_page_sort_order(apps, schema_editor): default="blocked_by", max_length=20, verbose_name="Issue Relation Type" ), ), - migrations.RunPython(set_page_sort_order), + migrations.RunPython( + set_page_sort_order, reverse_code=reverse_set_page_sort_order + ), ] From 3bdd4b9591e5773ca531838497781b612366be46 Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Fri, 12 Sep 2025 17:16:32 +0530 Subject: [PATCH 4/6] chore: added migration --- .../db/migrations/0106_auto_20250912_0845.py | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/apps/api/plane/db/migrations/0106_auto_20250912_0845.py b/apps/api/plane/db/migrations/0106_auto_20250912_0845.py index b570d48d4da..2ae95acab23 100644 --- a/apps/api/plane/db/migrations/0106_auto_20250912_0845.py +++ b/apps/api/plane/db/migrations/0106_auto_20250912_0845.py @@ -43,6 +43,100 @@ class Migration(migrations.Migration): ] 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", From 758f72ed127a47074cda4361877e8cae1b5484a2 Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Fri, 12 Sep 2025 17:18:07 +0530 Subject: [PATCH 5/6] chore: reverted the page base code --- apps/api/plane/app/views/page/base.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/apps/api/plane/app/views/page/base.py b/apps/api/plane/app/views/page/base.py index 08a1a98507f..e4ee1890b76 100644 --- a/apps/api/plane/app/views/page/base.py +++ b/apps/api/plane/app/views/page/base.py @@ -44,6 +44,22 @@ from plane.bgtasks.copy_s3_object import copy_s3_objects_of_description_and_assets +def unarchive_archive_page_and_descendants(page_id, archived_at): + # Your SQL query + sql = """ + WITH RECURSIVE descendants AS ( + SELECT id FROM pages WHERE id = %s + UNION ALL + SELECT pages.id FROM pages, descendants WHERE pages.parent_id = descendants.id + ) + UPDATE pages SET archived_at = %s WHERE id IN (SELECT id FROM descendants); + """ + + # Execute the SQL query + with connection.cursor() as cursor: + cursor.execute(sql, [page_id, archived_at]) + + class PageViewSet(BaseViewSet): serializer_class = PageSerializer model = Page @@ -310,6 +326,8 @@ def archive(self, request, slug, project_id, pk): workspace__slug=slug, ).delete() + unarchive_archive_page_and_descendants(pk, datetime.now()) + return Response({"archived_at": str(datetime.now())}, status=status.HTTP_200_OK) @allow_permission([ROLE.ADMIN], model=Page, creator=True) From 673382831bb9c90e8491f06ed987c931bd0eac8d Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Fri, 12 Sep 2025 17:28:40 +0530 Subject: [PATCH 6/6] chore: added a variable for sort order in pages --- apps/api/plane/db/migrations/0106_auto_20250912_0845.py | 6 ++++-- apps/api/plane/db/models/page.py | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/api/plane/db/migrations/0106_auto_20250912_0845.py b/apps/api/plane/db/migrations/0106_auto_20250912_0845.py index 2ae95acab23..8a0813fc1e3 100644 --- a/apps/api/plane/db/migrations/0106_auto_20250912_0845.py +++ b/apps/api/plane/db/migrations/0106_auto_20250912_0845.py @@ -1,5 +1,7 @@ # 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 @@ -33,7 +35,7 @@ def set_page_sort_order(apps, schema_editor): def reverse_set_page_sort_order(apps, schema_editor): Page = apps.get_model("db", "Page") - Page.objects.update(sort_order=65535) + Page.objects.update(sort_order=Page.DEFAULT_SORT_ORDER) class Migration(migrations.Migration): diff --git a/apps/api/plane/db/models/page.py b/apps/api/plane/db/models/page.py index 71fc49c4573..4d465cd5887 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)