From 68b6472189066b11241ec7ac455fe5dab368fc58 Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Wed, 31 Jul 2024 19:11:05 +0530 Subject: [PATCH 1/2] fix: added unique constraints --- apiserver/plane/api/serializers/cycle.py | 1 + apiserver/plane/api/serializers/module.py | 1 + apiserver/plane/api/serializers/project.py | 1 + apiserver/plane/api/views/cycle.py | 4 ++ apiserver/plane/api/views/module.py | 4 ++ apiserver/plane/app/serializers/module.py | 1 + apiserver/plane/app/serializers/project.py | 1 + apiserver/plane/app/views/cycle/base.py | 4 ++ apiserver/plane/app/views/cycle/issue.py | 4 +- apiserver/plane/app/views/issue/attachment.py | 15 +++++- apiserver/plane/app/views/module/base.py | 4 ++ apiserver/plane/app/views/module/issue.py | 9 ++-- .../plane/bgtasks/issue_activites_task.py | 3 +- ...74_alter_label_unique_together_and_more.py | 49 +++++++++++++++++++ apiserver/plane/db/models/issue.py | 10 +++- apiserver/plane/db/models/module.py | 10 +++- apiserver/plane/db/models/project.py | 27 +++++++++- 17 files changed, 134 insertions(+), 14 deletions(-) create mode 100644 apiserver/plane/db/migrations/0074_alter_label_unique_together_and_more.py diff --git a/apiserver/plane/api/serializers/cycle.py b/apiserver/plane/api/serializers/cycle.py index d03af1a8b27..90e3e1b427d 100644 --- a/apiserver/plane/api/serializers/cycle.py +++ b/apiserver/plane/api/serializers/cycle.py @@ -40,6 +40,7 @@ class Meta: "workspace", "project", "owned_by", + "deleted_at", ] diff --git a/apiserver/plane/api/serializers/module.py b/apiserver/plane/api/serializers/module.py index 01a20106460..a768cd26c6d 100644 --- a/apiserver/plane/api/serializers/module.py +++ b/apiserver/plane/api/serializers/module.py @@ -39,6 +39,7 @@ class Meta: "updated_by", "created_at", "updated_at", + "deleted_at", ] def to_representation(self, instance): diff --git a/apiserver/plane/api/serializers/project.py b/apiserver/plane/api/serializers/project.py index ce354ba5f3a..d1fea202308 100644 --- a/apiserver/plane/api/serializers/project.py +++ b/apiserver/plane/api/serializers/project.py @@ -31,6 +31,7 @@ class Meta: "updated_at", "created_by", "updated_by", + "deleted_at", ] def validate(self, data): diff --git a/apiserver/plane/api/views/cycle.py b/apiserver/plane/api/views/cycle.py index 9dd116fc70a..2d044eafa49 100644 --- a/apiserver/plane/api/views/cycle.py +++ b/apiserver/plane/api/views/cycle.py @@ -404,6 +404,10 @@ def delete(self, request, slug, project_id, pk): ) # Delete the cycle cycle.delete() + # Delete the cycle issues + CycleIssue.objects.filter( + cycle_id=self.kwargs.get("pk"), + ).delete() return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/api/views/module.py b/apiserver/plane/api/views/module.py index ecbf045823f..63985fd4d1c 100644 --- a/apiserver/plane/api/views/module.py +++ b/apiserver/plane/api/views/module.py @@ -301,6 +301,10 @@ def delete(self, request, slug, project_id, pk): epoch=int(timezone.now().timestamp()), ) module.delete() + # Delete the module issues + ModuleIssue.objects.filter( + module=pk, + ).delete() return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/serializers/module.py b/apiserver/plane/app/serializers/module.py index 222c9515098..ba71937abcf 100644 --- a/apiserver/plane/app/serializers/module.py +++ b/apiserver/plane/app/serializers/module.py @@ -39,6 +39,7 @@ class Meta: "created_at", "updated_at", "archived_at", + "deleted_at", ] def to_representation(self, instance): diff --git a/apiserver/plane/app/serializers/project.py b/apiserver/plane/app/serializers/project.py index 1bbc580c119..948608f7921 100644 --- a/apiserver/plane/app/serializers/project.py +++ b/apiserver/plane/app/serializers/project.py @@ -28,6 +28,7 @@ class Meta: fields = "__all__" read_only_fields = [ "workspace", + "deleted_at", ] def create(self, validated_data): diff --git a/apiserver/plane/app/views/cycle/base.py b/apiserver/plane/app/views/cycle/base.py index 921f2d442ba..1258aa6086d 100644 --- a/apiserver/plane/app/views/cycle/base.py +++ b/apiserver/plane/app/views/cycle/base.py @@ -1082,6 +1082,10 @@ def destroy(self, request, slug, project_id, pk): ) # Delete the cycle cycle.delete() + # Delete the cycle issues + CycleIssue.objects.filter( + cycle_id=self.kwargs.get("pk"), + ).delete() return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/cycle/issue.py b/apiserver/plane/app/views/cycle/issue.py index 1932ae1697e..895289ec0d4 100644 --- a/apiserver/plane/app/views/cycle/issue.py +++ b/apiserver/plane/app/views/cycle/issue.py @@ -20,6 +20,7 @@ from plane.app.permissions import ( ProjectEntityPermission, ) + # Module imports from .. import BaseViewSet from plane.app.serializers import ( @@ -45,7 +46,6 @@ SubGroupedOffsetPaginator, ) -# Module imports class CycleIssueViewSet(BaseViewSet): serializer_class = CycleIssueSerializer @@ -334,7 +334,7 @@ def create(self, request, slug, project_id, cycle_id): return Response({"message": "success"}, status=status.HTTP_201_CREATED) def destroy(self, request, slug, project_id, cycle_id, issue_id): - cycle_issue = CycleIssue.objects.get( + cycle_issue = CycleIssue.objects.filter( issue_id=issue_id, workspace__slug=slug, project_id=project_id, diff --git a/apiserver/plane/app/views/issue/attachment.py b/apiserver/plane/app/views/issue/attachment.py index c2b8ad6ff7b..c084d58ffea 100644 --- a/apiserver/plane/app/views/issue/attachment.py +++ b/apiserver/plane/app/views/issue/attachment.py @@ -14,7 +14,7 @@ from .. import BaseAPIView from plane.app.serializers import IssueAttachmentSerializer from plane.app.permissions import ProjectEntityPermission -from plane.db.models import IssueAttachment +from plane.db.models import IssueAttachment, ProjectMember from plane.bgtasks.issue_activites_task import issue_activity @@ -49,6 +49,19 @@ def post(self, request, slug, project_id, issue_id): def delete(self, request, slug, project_id, issue_id, pk): issue_attachment = IssueAttachment.objects.get(pk=pk) + if issue_attachment.created_by_id != request.user.id and ( + not ProjectMember.objects.filter( + workspace__slug=slug, + member=request.user, + role=20, + project_id=project_id, + is_active=True, + ).exists() + ): + return Response( + {"error": "Only admin or creator can delete the attachment"}, + status=status.HTTP_403_FORBIDDEN, + ) issue_attachment.asset.delete(save=False) issue_attachment.delete() issue_activity.delay( diff --git a/apiserver/plane/app/views/module/base.py b/apiserver/plane/app/views/module/base.py index 2e5a3d99de4..4d1203f077a 100644 --- a/apiserver/plane/app/views/module/base.py +++ b/apiserver/plane/app/views/module/base.py @@ -773,6 +773,10 @@ def destroy(self, request, slug, project_id, pk): for issue in module_issues ] module.delete() + # Delete the module issues + ModuleIssue.objects.filter( + module=pk, + ).delete() return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/module/issue.py b/apiserver/plane/app/views/module/issue.py index 53665b943ed..689d394927f 100644 --- a/apiserver/plane/app/views/module/issue.py +++ b/apiserver/plane/app/views/module/issue.py @@ -250,7 +250,6 @@ def create_issue_modules(self, request, slug, project_id, issue_id): removed_modules = request.data.get("removed_modules", []) project = Project.objects.get(pk=project_id) - if modules: _ = ModuleIssue.objects.bulk_create( [ @@ -284,7 +283,7 @@ def create_issue_modules(self, request, slug, project_id, issue_id): ] for module_id in removed_modules: - module_issue = ModuleIssue.objects.get( + module_issue = ModuleIssue.objects.filter( workspace__slug=slug, project_id=project_id, module_id=module_id, @@ -297,7 +296,7 @@ def create_issue_modules(self, request, slug, project_id, issue_id): issue_id=str(issue_id), project_id=str(project_id), current_instance=json.dumps( - {"module_name": module_issue.module.name} + {"module_name": module_issue.first().module.name} ), epoch=int(timezone.now().timestamp()), notification=True, @@ -308,7 +307,7 @@ def create_issue_modules(self, request, slug, project_id, issue_id): return Response({"message": "success"}, status=status.HTTP_201_CREATED) def destroy(self, request, slug, project_id, module_id, issue_id): - module_issue = ModuleIssue.objects.get( + module_issue = ModuleIssue.objects.filter( workspace__slug=slug, project_id=project_id, module_id=module_id, @@ -321,7 +320,7 @@ def destroy(self, request, slug, project_id, module_id, issue_id): issue_id=str(issue_id), project_id=str(project_id), current_instance=json.dumps( - {"module_name": module_issue.module.name} + {"module_name": module_issue.first().module.name} ), epoch=int(timezone.now().timestamp()), notification=True, diff --git a/apiserver/plane/bgtasks/issue_activites_task.py b/apiserver/plane/bgtasks/issue_activites_task.py index cbc8be470b6..8f5b5d03dd0 100644 --- a/apiserver/plane/bgtasks/issue_activites_task.py +++ b/apiserver/plane/bgtasks/issue_activites_task.py @@ -672,6 +672,7 @@ def delete_issue_activity( IssueActivity( project_id=project_id, workspace_id=workspace_id, + issue_id=issue_id, comment="deleted the issue", verb="deleted", actor_id=actor_id, @@ -879,7 +880,6 @@ def delete_cycle_issue_activity( cycle_name = requested_data.get("cycle_name", "") cycle = Cycle.objects.filter(pk=cycle_id).first() issues = requested_data.get("issues") - for issue in issues: current_issue = Issue.objects.filter(pk=issue).first() if issue: @@ -1774,4 +1774,3 @@ def issue_activity( except Exception as e: log_exception(e) return - diff --git a/apiserver/plane/db/migrations/0074_alter_label_unique_together_and_more.py b/apiserver/plane/db/migrations/0074_alter_label_unique_together_and_more.py new file mode 100644 index 00000000000..5c054ca2b49 --- /dev/null +++ b/apiserver/plane/db/migrations/0074_alter_label_unique_together_and_more.py @@ -0,0 +1,49 @@ +# Generated by Django 4.2.11 on 2024-07-31 12:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('db', '0073_analyticview_deleted_at_apiactivitylog_deleted_at_and_more'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='label', + unique_together={('name', 'project', 'deleted_at')}, + ), + migrations.AlterUniqueTogether( + name='module', + unique_together={('name', 'project', 'deleted_at')}, + ), + migrations.AlterUniqueTogether( + name='project', + unique_together={('identifier', 'workspace', 'deleted_at'), ('name', 'workspace', 'deleted_at')}, + ), + migrations.AlterUniqueTogether( + name='projectidentifier', + unique_together={('name', 'workspace', 'deleted_at')}, + ), + migrations.AddConstraint( + model_name='label', + constraint=models.UniqueConstraint(condition=models.Q(('deleted_at__isnull', True)), fields=('name', 'project'), name='label_unique_name_project_when_deleted_at_null'), + ), + migrations.AddConstraint( + model_name='module', + constraint=models.UniqueConstraint(condition=models.Q(('deleted_at__isnull', True)), fields=('name', 'project'), name='module_unique_name_project_when_deleted_at_null'), + ), + migrations.AddConstraint( + model_name='project', + constraint=models.UniqueConstraint(condition=models.Q(('deleted_at__isnull', True)), fields=('identifier', 'workspace'), name='project_unique_identifier_workspace_when_deleted_at_null'), + ), + migrations.AddConstraint( + model_name='project', + constraint=models.UniqueConstraint(condition=models.Q(('deleted_at__isnull', True)), fields=('name', 'workspace'), name='project_unique_name_workspace_when_deleted_at_null'), + ), + migrations.AddConstraint( + model_name='projectidentifier', + constraint=models.UniqueConstraint(condition=models.Q(('deleted_at__isnull', True)), fields=('name', 'workspace'), name='unique_name_workspace_when_deleted_at_null'), + ), + ] diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py index 4dbaa71c27d..629dc0eb565 100644 --- a/apiserver/plane/db/models/issue.py +++ b/apiserver/plane/db/models/issue.py @@ -8,6 +8,7 @@ from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models, transaction from django.utils import timezone +from django.db.models import Q # Module imports from plane.utils.html_processor import strip_tags @@ -534,7 +535,14 @@ class Label(ProjectBaseModel): external_id = models.CharField(max_length=255, blank=True, null=True) class Meta: - unique_together = ["name", "project"] + unique_together = ["name", "project", "deleted_at"] + constraints = [ + models.UniqueConstraint( + fields=['name', 'project'], + condition=Q(deleted_at__isnull=True), + name='label_unique_name_project_when_deleted_at_null' + ) + ] verbose_name = "Label" verbose_name_plural = "Labels" db_table = "labels" diff --git a/apiserver/plane/db/models/module.py b/apiserver/plane/db/models/module.py index 694771f88b1..62991cee940 100644 --- a/apiserver/plane/db/models/module.py +++ b/apiserver/plane/db/models/module.py @@ -1,6 +1,7 @@ # Django imports from django.conf import settings from django.db import models +from django.db.models import Q # Module imports from .project import ProjectBaseModel @@ -96,7 +97,14 @@ class Module(ProjectBaseModel): logo_props = models.JSONField(default=dict) class Meta: - unique_together = ["name", "project"] + unique_together = ["name", "project", "deleted_at"] + constraints = [ + models.UniqueConstraint( + fields=['name', 'project'], + condition=Q(deleted_at__isnull=True), + name='module_unique_name_project_when_deleted_at_null' + ) + ] verbose_name = "Module" verbose_name_plural = "Modules" db_table = "modules" diff --git a/apiserver/plane/db/models/project.py b/apiserver/plane/db/models/project.py index 9e04bb4c7d7..88bfd4ca048 100644 --- a/apiserver/plane/db/models/project.py +++ b/apiserver/plane/db/models/project.py @@ -5,6 +5,7 @@ from django.conf import settings from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models +from django.db.models import Q # Modeule imports from plane.db.mixins import AuditModel @@ -124,7 +125,22 @@ def __str__(self): return f"{self.name} <{self.workspace.name}>" class Meta: - unique_together = [["identifier", "workspace"], ["name", "workspace"]] + unique_together = [ + ["identifier", "workspace", "deleted_at"], + ["name", "workspace", "deleted_at"], + ] + constraints = [ + models.UniqueConstraint( + fields=["identifier", "workspace"], + condition=Q(deleted_at__isnull=True), + name="project_unique_identifier_workspace_when_deleted_at_null", + ), + models.UniqueConstraint( + fields=["name", "workspace"], + condition=Q(deleted_at__isnull=True), + name="project_unique_name_workspace_when_deleted_at_null", + ), + ] verbose_name = "Project" verbose_name_plural = "Projects" db_table = "projects" @@ -223,7 +239,14 @@ class ProjectIdentifier(AuditModel): name = models.CharField(max_length=12, db_index=True) class Meta: - unique_together = ["name", "workspace"] + unique_together = ["name", "workspace", "deleted_at"] + constraints = [ + models.UniqueConstraint( + fields=["name", "workspace"], + condition=Q(deleted_at__isnull=True), + name="unique_name_workspace_when_deleted_at_null", + ) + ] verbose_name = "Project Identifier" verbose_name_plural = "Project Identifiers" db_table = "project_identifiers" From 8c0b977ecb221dd1e717dade42e869941ee253d7 Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Wed, 31 Jul 2024 19:28:19 +0530 Subject: [PATCH 2/2] chore: migration indetaton --- ...74_alter_label_unique_together_and_more.py | 64 +++++++++++++------ 1 file changed, 45 insertions(+), 19 deletions(-) diff --git a/apiserver/plane/db/migrations/0074_alter_label_unique_together_and_more.py b/apiserver/plane/db/migrations/0074_alter_label_unique_together_and_more.py index 5c054ca2b49..33fcf3fe374 100644 --- a/apiserver/plane/db/migrations/0074_alter_label_unique_together_and_more.py +++ b/apiserver/plane/db/migrations/0074_alter_label_unique_together_and_more.py @@ -6,44 +6,70 @@ class Migration(migrations.Migration): dependencies = [ - ('db', '0073_analyticview_deleted_at_apiactivitylog_deleted_at_and_more'), + ( + "db", + "0073_analyticview_deleted_at_apiactivitylog_deleted_at_and_more", + ), ] operations = [ migrations.AlterUniqueTogether( - name='label', - unique_together={('name', 'project', 'deleted_at')}, + name="label", + unique_together={("name", "project", "deleted_at")}, ), migrations.AlterUniqueTogether( - name='module', - unique_together={('name', 'project', 'deleted_at')}, + name="module", + unique_together={("name", "project", "deleted_at")}, ), migrations.AlterUniqueTogether( - name='project', - unique_together={('identifier', 'workspace', 'deleted_at'), ('name', 'workspace', 'deleted_at')}, + name="project", + unique_together={ + ("identifier", "workspace", "deleted_at"), + ("name", "workspace", "deleted_at"), + }, ), migrations.AlterUniqueTogether( - name='projectidentifier', - unique_together={('name', 'workspace', 'deleted_at')}, + name="projectidentifier", + unique_together={("name", "workspace", "deleted_at")}, ), migrations.AddConstraint( - model_name='label', - constraint=models.UniqueConstraint(condition=models.Q(('deleted_at__isnull', True)), fields=('name', 'project'), name='label_unique_name_project_when_deleted_at_null'), + model_name="label", + constraint=models.UniqueConstraint( + condition=models.Q(("deleted_at__isnull", True)), + fields=("name", "project"), + name="label_unique_name_project_when_deleted_at_null", + ), ), migrations.AddConstraint( - model_name='module', - constraint=models.UniqueConstraint(condition=models.Q(('deleted_at__isnull', True)), fields=('name', 'project'), name='module_unique_name_project_when_deleted_at_null'), + model_name="module", + constraint=models.UniqueConstraint( + condition=models.Q(("deleted_at__isnull", True)), + fields=("name", "project"), + name="module_unique_name_project_when_deleted_at_null", + ), ), migrations.AddConstraint( - model_name='project', - constraint=models.UniqueConstraint(condition=models.Q(('deleted_at__isnull', True)), fields=('identifier', 'workspace'), name='project_unique_identifier_workspace_when_deleted_at_null'), + model_name="project", + constraint=models.UniqueConstraint( + condition=models.Q(("deleted_at__isnull", True)), + fields=("identifier", "workspace"), + name="project_unique_identifier_workspace_when_deleted_at_null", + ), ), migrations.AddConstraint( - model_name='project', - constraint=models.UniqueConstraint(condition=models.Q(('deleted_at__isnull', True)), fields=('name', 'workspace'), name='project_unique_name_workspace_when_deleted_at_null'), + model_name="project", + constraint=models.UniqueConstraint( + condition=models.Q(("deleted_at__isnull", True)), + fields=("name", "workspace"), + name="project_unique_name_workspace_when_deleted_at_null", + ), ), migrations.AddConstraint( - model_name='projectidentifier', - constraint=models.UniqueConstraint(condition=models.Q(('deleted_at__isnull', True)), fields=('name', 'workspace'), name='unique_name_workspace_when_deleted_at_null'), + model_name="projectidentifier", + constraint=models.UniqueConstraint( + condition=models.Q(("deleted_at__isnull", True)), + fields=("name", "workspace"), + name="unique_name_workspace_when_deleted_at_null", + ), ), ]