From daaa04c6ea0e1702e9f8685169fcace2358aace2 Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Wed, 31 Jul 2024 19:38:53 +0530 Subject: [PATCH] [WEB-2092] fix: added unique constraints for project, module and states (#5281) * fix: added unique constraints * chore: migration indetaton --- 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 | 75 +++++++++++++++++++ apiserver/plane/db/models/issue.py | 10 ++- apiserver/plane/db/models/module.py | 10 ++- apiserver/plane/db/models/project.py | 27 ++++++- 17 files changed, 160 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 d03af1a8b2..90e3e1b427 100644 --- a/apiserver/plane/api/serializers/cycle.py +++ b/apiserver/plane/api/serializers/cycle.py @@ -40,6 +40,7 @@ class CycleSerializer(BaseSerializer): "workspace", "project", "owned_by", + "deleted_at", ] diff --git a/apiserver/plane/api/serializers/module.py b/apiserver/plane/api/serializers/module.py index 01a2010646..a768cd26c6 100644 --- a/apiserver/plane/api/serializers/module.py +++ b/apiserver/plane/api/serializers/module.py @@ -39,6 +39,7 @@ class ModuleSerializer(BaseSerializer): "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 ce354ba5f3..d1fea20230 100644 --- a/apiserver/plane/api/serializers/project.py +++ b/apiserver/plane/api/serializers/project.py @@ -31,6 +31,7 @@ class ProjectSerializer(BaseSerializer): "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 9dd116fc70..2d044eafa4 100644 --- a/apiserver/plane/api/views/cycle.py +++ b/apiserver/plane/api/views/cycle.py @@ -404,6 +404,10 @@ class CycleAPIEndpoint(BaseAPIView): ) # 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 ecbf045823..63985fd4d1 100644 --- a/apiserver/plane/api/views/module.py +++ b/apiserver/plane/api/views/module.py @@ -301,6 +301,10 @@ class ModuleAPIEndpoint(BaseAPIView): 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 222c951509..ba71937abc 100644 --- a/apiserver/plane/app/serializers/module.py +++ b/apiserver/plane/app/serializers/module.py @@ -39,6 +39,7 @@ class ModuleWriteSerializer(BaseSerializer): "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 1bbc580c11..948608f792 100644 --- a/apiserver/plane/app/serializers/project.py +++ b/apiserver/plane/app/serializers/project.py @@ -28,6 +28,7 @@ class ProjectSerializer(BaseSerializer): 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 921f2d442b..1258aa6086 100644 --- a/apiserver/plane/app/views/cycle/base.py +++ b/apiserver/plane/app/views/cycle/base.py @@ -1082,6 +1082,10 @@ class CycleViewSet(BaseViewSet): ) # 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 1932ae1697..895289ec0d 100644 --- a/apiserver/plane/app/views/cycle/issue.py +++ b/apiserver/plane/app/views/cycle/issue.py @@ -20,6 +20,7 @@ from rest_framework.response import Response from plane.app.permissions import ( ProjectEntityPermission, ) + # Module imports from .. import BaseViewSet from plane.app.serializers import ( @@ -45,7 +46,6 @@ from plane.utils.paginator import ( SubGroupedOffsetPaginator, ) -# Module imports class CycleIssueViewSet(BaseViewSet): serializer_class = CycleIssueSerializer @@ -334,7 +334,7 @@ class CycleIssueViewSet(BaseViewSet): 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 c2b8ad6ff7..c084d58ffe 100644 --- a/apiserver/plane/app/views/issue/attachment.py +++ b/apiserver/plane/app/views/issue/attachment.py @@ -14,7 +14,7 @@ from rest_framework.parsers import MultiPartParser, FormParser 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 @@ class IssueAttachmentEndpoint(BaseAPIView): 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 2e5a3d99de..4d1203f077 100644 --- a/apiserver/plane/app/views/module/base.py +++ b/apiserver/plane/app/views/module/base.py @@ -773,6 +773,10 @@ class ModuleViewSet(BaseViewSet): 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 53665b943e..689d394927 100644 --- a/apiserver/plane/app/views/module/issue.py +++ b/apiserver/plane/app/views/module/issue.py @@ -250,7 +250,6 @@ class ModuleIssueViewSet(BaseViewSet): removed_modules = request.data.get("removed_modules", []) project = Project.objects.get(pk=project_id) - if modules: _ = ModuleIssue.objects.bulk_create( [ @@ -284,7 +283,7 @@ class ModuleIssueViewSet(BaseViewSet): ] 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 @@ class ModuleIssueViewSet(BaseViewSet): 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 @@ class ModuleIssueViewSet(BaseViewSet): 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 @@ class ModuleIssueViewSet(BaseViewSet): 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 cbc8be470b..8f5b5d03dd 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 0000000000..33fcf3fe37 --- /dev/null +++ b/apiserver/plane/db/migrations/0074_alter_label_unique_together_and_more.py @@ -0,0 +1,75 @@ +# 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 4dbaa71c27..629dc0eb56 100644 --- a/apiserver/plane/db/models/issue.py +++ b/apiserver/plane/db/models/issue.py @@ -8,6 +8,7 @@ from django.core.exceptions import ValidationError 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 694771f88b..62991cee94 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 9e04bb4c7d..88bfd4ca04 100644 --- a/apiserver/plane/db/models/project.py +++ b/apiserver/plane/db/models/project.py @@ -5,6 +5,7 @@ from uuid import uuid4 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 @@ class Project(BaseModel): 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"