Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apiserver/plane/api/serializers/cycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class Meta:
"workspace",
"project",
"owned_by",
"deleted_at",
]


Expand Down
1 change: 1 addition & 0 deletions apiserver/plane/api/serializers/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class Meta:
"updated_by",
"created_at",
"updated_at",
"deleted_at",
]

def to_representation(self, instance):
Expand Down
1 change: 1 addition & 0 deletions apiserver/plane/api/serializers/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class Meta:
"updated_at",
"created_by",
"updated_by",
"deleted_at",
]

def validate(self, data):
Expand Down
4 changes: 4 additions & 0 deletions apiserver/plane/api/views/cycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down
4 changes: 4 additions & 0 deletions apiserver/plane/api/views/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down
1 change: 1 addition & 0 deletions apiserver/plane/app/serializers/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class Meta:
"created_at",
"updated_at",
"archived_at",
"deleted_at",
]

def to_representation(self, instance):
Expand Down
1 change: 1 addition & 0 deletions apiserver/plane/app/serializers/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class Meta:
fields = "__all__"
read_only_fields = [
"workspace",
"deleted_at",
]

def create(self, validated_data):
Expand Down
4 changes: 4 additions & 0 deletions apiserver/plane/app/views/cycle/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down
4 changes: 2 additions & 2 deletions apiserver/plane/app/views/cycle/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from plane.app.permissions import (
ProjectEntityPermission,
)

# Module imports
from .. import BaseViewSet
from plane.app.serializers import (
Expand All @@ -45,7 +46,6 @@
SubGroupedOffsetPaginator,
)

# Module imports

class CycleIssueViewSet(BaseViewSet):
serializer_class = CycleIssueSerializer
Expand Down Expand Up @@ -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,
Expand Down
15 changes: 14 additions & 1 deletion apiserver/plane/app/views/issue/attachment.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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,
)
Comment on lines +52 to +64
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure proper error handling and security measures.

The condition correctly restricts deletion to either the creator of the attachment or a user with a specific role (admin) within the project. However, consider handling the case where IssueAttachment.objects.get(pk=pk) raises a DoesNotExist exception to avoid potential server errors.

+ from django.core.exceptions import ObjectDoesNotExist

    def delete(self, request, slug, project_id, issue_id, pk):
+       try:
            issue_attachment = IssueAttachment.objects.get(pk=pk)
+       except ObjectDoesNotExist:
+           return Response(
+               {"error": "Issue attachment not found"},
+               status=status.HTTP_404_NOT_FOUND,
+           )
        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(
            type="attachment.activity.deleted",
            requested_data=None,
            actor_id=str(self.request.user.id),
            issue_id=str(self.kwargs.get("issue_id", None)),
            project_id=str(self.kwargs.get("project_id", None)),
            current_instance=None,
            epoch=int(timezone.now().timestamp()),
            notification=True,
            origin=request.META.get("HTTP_ORIGIN"),
        )

        return Response(status=status.HTTP_204_NO_CONTENT)
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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,
)
from django.core.exceptions import ObjectDoesNotExist
def delete(self, request, slug, project_id, issue_id, pk):
try:
issue_attachment = IssueAttachment.objects.get(pk=pk)
except ObjectDoesNotExist:
return Response(
{"error": "Issue attachment not found"},
status=status.HTTP_404_NOT_FOUND,
)
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(
type="attachment.activity.deleted",
requested_data=None,
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("issue_id", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
return Response(status=status.HTTP_204_NO_CONTENT)

issue_attachment.asset.delete(save=False)
issue_attachment.delete()
issue_activity.delay(
Expand Down
4 changes: 4 additions & 0 deletions apiserver/plane/app/views/module/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down
9 changes: 4 additions & 5 deletions apiserver/plane/app/views/module/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
[
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down
3 changes: 1 addition & 2 deletions apiserver/plane/bgtasks/issue_activites_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -1774,4 +1774,3 @@ def issue_activity(
except Exception as e:
log_exception(e)
return

Original file line number Diff line number Diff line change
@@ -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",
),
),
]
10 changes: 9 additions & 1 deletion apiserver/plane/db/models/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down
10 changes: 9 additions & 1 deletion apiserver/plane/db/models/module.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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"
Expand Down
27 changes: 25 additions & 2 deletions apiserver/plane/db/models/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down