Compare commits

..
658 changed files with 13843 additions and 16884 deletions
+2 -2
View File
@@ -83,7 +83,7 @@ jobs:
endpoint: ${{ env.BUILDX_ENDPOINT }}
- name: Build and Push to Docker Hub
uses: docker/build-push-action@v6.9.0
uses: docker/build-push-action@v5.1.0
with:
context: ./aio
file: ./aio/Dockerfile-base-full
@@ -124,7 +124,7 @@ jobs:
endpoint: ${{ env.BUILDX_ENDPOINT }}
- name: Build and Push to Docker Hub
uses: docker/build-push-action@v6.9.0
uses: docker/build-push-action@v5.1.0
with:
context: ./aio
file: ./aio/Dockerfile-base-slim
+2 -2
View File
@@ -128,7 +128,7 @@ jobs:
uses: actions/checkout@v4
- name: Build and Push to Docker Hub
uses: docker/build-push-action@v6.9.0
uses: docker/build-push-action@v5.1.0
with:
context: .
file: ./aio/Dockerfile-app
@@ -188,7 +188,7 @@ jobs:
uses: actions/checkout@v4
- name: Build and Push to Docker Hub
uses: docker/build-push-action@v6.9.0
uses: docker/build-push-action@v5.1.0
with:
context: .
file: ./aio/Dockerfile-app
+1 -1
View File
@@ -367,7 +367,7 @@ jobs:
- name: Create Release
id: create_release
uses: softprops/action-gh-release@v2.1.0
uses: softprops/action-gh-release@v2.0.8
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
with:
+4 -4
View File
@@ -29,11 +29,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -46,7 +46,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v3
uses: github/codeql-action/autobuild@v2
# ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -59,6 +59,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@v2
with:
category: "/language:${{matrix.language}}"
+1 -1
View File
@@ -79,7 +79,7 @@ jobs:
uses: actions/checkout@v4
- name: Build and Push to Docker Hub
uses: docker/build-push-action@v6.9.0
uses: docker/build-push-action@v5.1.0
with:
context: .
file: ./aio/Dockerfile-app
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v4.1.1
with:
fetch-depth: 0 # Fetch all history for all branches and tags
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
contents: read
steps:
- name: Checkout Code
uses: actions/checkout@v4
uses: actions/checkout@v4.1.1
with:
persist-credentials: false
fetch-depth: 0
-1
View File
@@ -4,7 +4,6 @@ FROM python:3.12.5-alpine AS backend
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
ENV INSTANCE_CHANGELOG_URL https://api.plane.so/api/public/anchor/8e1c2e4c7bc5493eb7731be3862f6960/pages/
WORKDIR /code
-1
View File
@@ -4,7 +4,6 @@ FROM python:3.12.5-alpine AS backend
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
ENV INSTANCE_CHANGELOG_URL https://api.plane.so/api/public/anchor/8e1c2e4c7bc5493eb7731be3862f6960/pages/
RUN apk --no-cache add \
"bash~=5.2" \
+21 -7
View File
@@ -26,7 +26,9 @@ def update_description():
updated_issues.append(issue)
Issue.objects.bulk_update(
updated_issues, ["description_html", "description_stripped"], batch_size=100
updated_issues,
["description_html", "description_stripped"],
batch_size=100,
)
print("Success")
except Exception as e:
@@ -40,7 +42,9 @@ def update_comments():
updated_issue_comments = []
for issue_comment in issue_comments:
issue_comment.comment_html = f"<p>{issue_comment.comment_stripped}</p>"
issue_comment.comment_html = (
f"<p>{issue_comment.comment_stripped}</p>"
)
updated_issue_comments.append(issue_comment)
IssueComment.objects.bulk_update(
@@ -99,7 +103,9 @@ def updated_issue_sort_order():
issue.sort_order = issue.sequence_id * random.randint(100, 500)
updated_issues.append(issue)
Issue.objects.bulk_update(updated_issues, ["sort_order"], batch_size=100)
Issue.objects.bulk_update(
updated_issues, ["sort_order"], batch_size=100
)
print("Success")
except Exception as e:
print(e)
@@ -137,7 +143,9 @@ def update_project_cover_images():
project.cover_image = project_cover_images[random.randint(0, 19)]
updated_projects.append(project)
Project.objects.bulk_update(updated_projects, ["cover_image"], batch_size=100)
Project.objects.bulk_update(
updated_projects, ["cover_image"], batch_size=100
)
print("Success")
except Exception as e:
print(e)
@@ -186,7 +194,9 @@ def update_label_color():
def create_slack_integration():
try:
_ = Integration.objects.create(provider="slack", network=2, title="Slack")
_ = Integration.objects.create(
provider="slack", network=2, title="Slack"
)
print("Success")
except Exception as e:
print(e)
@@ -212,12 +222,16 @@ def update_integration_verified():
def update_start_date():
try:
issues = Issue.objects.filter(state__group__in=["started", "completed"])
issues = Issue.objects.filter(
state__group__in=["started", "completed"]
)
updated_issues = []
for issue in issues:
issue.start_date = issue.created_at.date()
updated_issues.append(issue)
Issue.objects.bulk_update(updated_issues, ["start_date"], batch_size=500)
Issue.objects.bulk_update(
updated_issues, ["start_date"], batch_size=500
)
print("Success")
except Exception as e:
print(e)
+3 -1
View File
@@ -3,7 +3,9 @@ import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "plane.settings.production")
os.environ.setdefault(
"DJANGO_SETTINGS_MODULE", "plane.settings.production"
)
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
@@ -25,7 +25,10 @@ class APIKeyAuthentication(authentication.BaseAuthentication):
def validate_api_token(self, token):
try:
api_token = APIToken.objects.get(
Q(Q(expired_at__gt=timezone.now()) | Q(expired_at__isnull=True)),
Q(
Q(expired_at__gt=timezone.now())
| Q(expired_at__isnull=True)
),
token=token,
is_active=True,
)
+1 -1
View File
@@ -80,4 +80,4 @@ class ServiceTokenRateThrottle(SimpleRateThrottle):
request.META["X-RateLimit-Remaining"] = max(0, available)
request.META["X-RateLimit-Reset"] = reset_time
return allowed
return allowed
+6 -2
View File
@@ -13,5 +13,9 @@ from .issue import (
)
from .state import StateLiteSerializer, StateSerializer
from .cycle import CycleSerializer, CycleIssueSerializer, CycleLiteSerializer
from .module import ModuleSerializer, ModuleIssueSerializer, ModuleLiteSerializer
from .intake import IntakeIssueSerializer
from .module import (
ModuleSerializer,
ModuleIssueSerializer,
ModuleLiteSerializer,
)
from .inbox import InboxIssueSerializer
+3 -1
View File
@@ -102,6 +102,8 @@ class BaseSerializer(serializers.ModelSerializer):
response[expand] = exp_serializer.data
else:
# You might need to handle this case differently
response[expand] = getattr(instance, f"{expand}_id", None)
response[expand] = getattr(
instance, f"{expand}_id", None
)
return response
+8 -2
View File
@@ -23,7 +23,9 @@ class CycleSerializer(BaseSerializer):
and data.get("end_date", None) is not None
and data.get("start_date", None) > data.get("end_date", None)
):
raise serializers.ValidationError("Start date cannot exceed end date")
raise serializers.ValidationError(
"Start date cannot exceed end date"
)
return data
class Meta:
@@ -48,7 +50,11 @@ class CycleIssueSerializer(BaseSerializer):
class Meta:
model = CycleIssue
fields = "__all__"
read_only_fields = ["workspace", "project", "cycle"]
read_only_fields = [
"workspace",
"project",
"cycle",
]
class CycleLiteSerializer(BaseSerializer):
@@ -1,16 +1,15 @@
# Module improts
from .base import BaseSerializer
from .issue import IssueExpandSerializer
from plane.db.models import IntakeIssue
from rest_framework import serializers
from plane.db.models import InboxIssue
class IntakeIssueSerializer(BaseSerializer):
class InboxIssueSerializer(BaseSerializer):
issue_detail = IssueExpandSerializer(read_only=True, source="issue")
inbox = serializers.UUIDField(source="intake.id", read_only=True)
class Meta:
model = IntakeIssue
model = InboxIssue
fields = "__all__"
read_only_fields = [
"id",
+61 -18
View File
@@ -49,13 +49,25 @@ class IssueSerializer(BaseSerializer):
required=False,
)
type_id = serializers.PrimaryKeyRelatedField(
source="type", queryset=IssueType.objects.all(), required=False, allow_null=True
source="type",
queryset=IssueType.objects.all(),
required=False,
allow_null=True,
)
class Meta:
model = Issue
read_only_fields = ["id", "workspace", "project", "updated_by", "updated_at"]
exclude = ["description", "description_stripped"]
read_only_fields = [
"id",
"workspace",
"project",
"updated_by",
"updated_at",
]
exclude = [
"description",
"description_stripped",
]
def validate(self, data):
if (
@@ -63,7 +75,9 @@ class IssueSerializer(BaseSerializer):
and data.get("target_date", None) is not None
and data.get("start_date", None) > data.get("target_date", None)
):
raise serializers.ValidationError("Start date cannot exceed target date")
raise serializers.ValidationError(
"Start date cannot exceed target date"
)
try:
if data.get("description_html", None) is not None:
@@ -85,14 +99,16 @@ class IssueSerializer(BaseSerializer):
# Validate labels are from project
if data.get("labels", []):
data["labels"] = Label.objects.filter(
project_id=self.context.get("project_id"), id__in=data["labels"]
project_id=self.context.get("project_id"),
id__in=data["labels"],
).values_list("id", flat=True)
# Check state is from the project only else raise validation error
if (
data.get("state")
and not State.objects.filter(
project_id=self.context.get("project_id"), pk=data.get("state").id
project_id=self.context.get("project_id"),
pk=data.get("state").id,
).exists()
):
raise serializers.ValidationError(
@@ -103,7 +119,8 @@ class IssueSerializer(BaseSerializer):
if (
data.get("parent")
and not Issue.objects.filter(
workspace_id=self.context.get("workspace_id"), pk=data.get("parent").id
workspace_id=self.context.get("workspace_id"),
pk=data.get("parent").id,
).exists()
):
raise serializers.ValidationError(
@@ -130,7 +147,9 @@ class IssueSerializer(BaseSerializer):
issue_type = issue_type
issue = Issue.objects.create(
**validated_data, project_id=project_id, type=issue_type
**validated_data,
project_id=project_id,
type=issue_type,
)
# Issue Audit Users
@@ -245,9 +264,13 @@ class IssueSerializer(BaseSerializer):
]
if "labels" in self.fields:
if "labels" in self.expand:
data["labels"] = LabelSerializer(instance.labels.all(), many=True).data
data["labels"] = LabelSerializer(
instance.labels.all(), many=True
).data
else:
data["labels"] = [str(label.id) for label in instance.labels.all()]
data["labels"] = [
str(label.id) for label in instance.labels.all()
]
return data
@@ -255,7 +278,11 @@ class IssueSerializer(BaseSerializer):
class IssueLiteSerializer(BaseSerializer):
class Meta:
model = Issue
fields = ["id", "sequence_id", "project_id"]
fields = [
"id",
"sequence_id",
"project_id",
]
read_only_fields = fields
@@ -307,7 +334,8 @@ class IssueLinkSerializer(BaseSerializer):
# Validation if url already exists
def create(self, validated_data):
if IssueLink.objects.filter(
url=validated_data.get("url"), issue_id=validated_data.get("issue_id")
url=validated_data.get("url"),
issue_id=validated_data.get("issue_id"),
).exists():
raise serializers.ValidationError(
{"error": "URL already exists for this Issue"}
@@ -317,7 +345,8 @@ class IssueLinkSerializer(BaseSerializer):
def update(self, instance, validated_data):
if (
IssueLink.objects.filter(
url=validated_data.get("url"), issue_id=instance.issue_id
url=validated_data.get("url"),
issue_id=instance.issue_id,
)
.exclude(pk=instance.id)
.exists()
@@ -358,7 +387,10 @@ class IssueCommentSerializer(BaseSerializer):
"created_at",
"updated_at",
]
exclude = ["comment_stripped", "comment_json"]
exclude = [
"comment_stripped",
"comment_json",
]
def validate(self, data):
try:
@@ -375,27 +407,38 @@ class IssueCommentSerializer(BaseSerializer):
class IssueActivitySerializer(BaseSerializer):
class Meta:
model = IssueActivity
exclude = ["created_by", "updated_by"]
exclude = [
"created_by",
"updated_by",
]
class CycleIssueSerializer(BaseSerializer):
cycle = CycleSerializer(read_only=True)
class Meta:
fields = ["cycle"]
fields = [
"cycle",
]
class ModuleIssueSerializer(BaseSerializer):
module = ModuleSerializer(read_only=True)
class Meta:
fields = ["module"]
fields = [
"module",
]
class LabelLiteSerializer(BaseSerializer):
class Meta:
model = Label
fields = ["id", "name", "color"]
fields = [
"id",
"name",
"color",
]
class IssueExpandSerializer(BaseSerializer):
+13 -5
View File
@@ -53,11 +53,14 @@ class ModuleSerializer(BaseSerializer):
and data.get("target_date", None) is not None
and data.get("start_date", None) > data.get("target_date", None)
):
raise serializers.ValidationError("Start date cannot exceed target date")
raise serializers.ValidationError(
"Start date cannot exceed target date"
)
if data.get("members", []):
data["members"] = ProjectMember.objects.filter(
project_id=self.context.get("project_id"), member_id__in=data["members"]
project_id=self.context.get("project_id"),
member_id__in=data["members"],
).values_list("member_id", flat=True)
return data
@@ -71,7 +74,9 @@ class ModuleSerializer(BaseSerializer):
module_name = validated_data.get("name")
if module_name:
# Lookup for the module name in the module table for that project
if Module.objects.filter(name=module_name, project_id=project_id).exists():
if Module.objects.filter(
name=module_name, project_id=project_id
).exists():
raise serializers.ValidationError(
{"error": "Module with this name already exists"}
)
@@ -102,7 +107,9 @@ class ModuleSerializer(BaseSerializer):
if module_name:
# Lookup for the module name in the module table for that project
if (
Module.objects.filter(name=module_name, project=instance.project)
Module.objects.filter(
name=module_name, project=instance.project
)
.exclude(id=instance.id)
.exists()
):
@@ -165,7 +172,8 @@ class ModuleLinkSerializer(BaseSerializer):
# Validation if url already exists
def create(self, validated_data):
if ModuleLink.objects.filter(
url=validated_data.get("url"), module_id=validated_data.get("module_id")
url=validated_data.get("url"),
module_id=validated_data.get("module_id"),
).exists():
raise serializers.ValidationError(
{"error": "URL already exists for this Issue"}
+11 -4
View File
@@ -2,7 +2,11 @@
from rest_framework import serializers
# Module imports
from plane.db.models import Project, ProjectIdentifier, WorkspaceMember
from plane.db.models import (
Project,
ProjectIdentifier,
WorkspaceMember,
)
from .base import BaseSerializer
@@ -16,7 +20,6 @@ class ProjectSerializer(BaseSerializer):
member_role = serializers.IntegerField(read_only=True)
is_deployed = serializers.BooleanField(read_only=True)
cover_image_url = serializers.CharField(read_only=True)
inbox_view = serializers.BooleanField(read_only=True, source="intake_view")
class Meta:
model = Project
@@ -63,12 +66,16 @@ class ProjectSerializer(BaseSerializer):
def create(self, validated_data):
identifier = validated_data.get("identifier", "").strip().upper()
if identifier == "":
raise serializers.ValidationError(detail="Project Identifier is required")
raise serializers.ValidationError(
detail="Project Identifier is required"
)
if ProjectIdentifier.objects.filter(
name=identifier, workspace_id=self.context["workspace_id"]
).exists():
raise serializers.ValidationError(detail="Project Identifier is taken")
raise serializers.ValidationError(
detail="Project Identifier is taken"
)
project = Project.objects.create(
**validated_data, workspace_id=self.context["workspace_id"]
+9 -4
View File
@@ -7,9 +7,9 @@ class StateSerializer(BaseSerializer):
def validate(self, data):
# If the default is being provided then make all other states default False
if data.get("default", False):
State.objects.filter(project_id=self.context.get("project_id")).update(
default=False
)
State.objects.filter(
project_id=self.context.get("project_id")
).update(default=False)
return data
class Meta:
@@ -30,5 +30,10 @@ class StateSerializer(BaseSerializer):
class StateLiteSerializer(BaseSerializer):
class Meta:
model = State
fields = ["id", "name", "color", "group"]
fields = [
"id",
"name",
"color",
"group",
]
read_only_fields = fields
+5 -1
View File
@@ -8,5 +8,9 @@ class WorkspaceLiteSerializer(BaseSerializer):
class Meta:
model = Workspace
fields = ["name", "slug", "id"]
fields = [
"name",
"slug",
"id",
]
read_only_fields = fields
+2 -2
View File
@@ -3,7 +3,7 @@ from .state import urlpatterns as state_patterns
from .issue import urlpatterns as issue_patterns
from .cycle import urlpatterns as cycle_patterns
from .module import urlpatterns as module_patterns
from .intake import urlpatterns as intake_patterns
from .inbox import urlpatterns as inbox_patterns
from .member import urlpatterns as member_patterns
urlpatterns = [
@@ -12,6 +12,6 @@ urlpatterns = [
*issue_patterns,
*cycle_patterns,
*module_patterns,
*intake_patterns,
*inbox_patterns,
*member_patterns,
]
+17
View File
@@ -0,0 +1,17 @@
from django.urls import path
from plane.api.views import InboxIssueAPIEndpoint
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/",
InboxIssueAPIEndpoint.as_view(),
name="inbox-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/<uuid:issue_id>/",
InboxIssueAPIEndpoint.as_view(),
name="inbox-issue",
),
]
-27
View File
@@ -1,27 +0,0 @@
from django.urls import path
from plane.api.views import IntakeIssueAPIEndpoint
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/",
IntakeIssueAPIEndpoint.as_view(),
name="inbox-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/<uuid:issue_id>/",
IntakeIssueAPIEndpoint.as_view(),
name="inbox-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/intake-issues/",
IntakeIssueAPIEndpoint.as_view(),
name="intake-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/intake-issues/<uuid:issue_id>/",
IntakeIssueAPIEndpoint.as_view(),
name="intake-issue",
),
]
+4 -2
View File
@@ -1,11 +1,13 @@
from django.urls import path
from plane.api.views import ProjectMemberAPIEndpoint
from plane.api.views import (
ProjectMemberAPIEndpoint,
)
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<str:project_id>/members/",
ProjectMemberAPIEndpoint.as_view(),
name="users",
)
),
]
+7 -2
View File
@@ -1,10 +1,15 @@
from django.urls import path
from plane.api.views import ProjectAPIEndpoint, ProjectArchiveUnarchiveAPIEndpoint
from plane.api.views import (
ProjectAPIEndpoint,
ProjectArchiveUnarchiveAPIEndpoint,
)
urlpatterns = [
path(
"workspaces/<str:slug>/projects/", ProjectAPIEndpoint.as_view(), name="project"
"workspaces/<str:slug>/projects/",
ProjectAPIEndpoint.as_view(),
name="project",
),
path(
"workspaces/<str:slug>/projects/<uuid:pk>/",
+2 -1
View File
@@ -27,4 +27,5 @@ from .module import (
from .member import ProjectMemberAPIEndpoint
from .intake import IntakeIssueAPIEndpoint
from .inbox import InboxIssueAPIEndpoint
+18 -7
View File
@@ -37,9 +37,13 @@ class TimezoneMixin:
class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
authentication_classes = [APIKeyAuthentication]
authentication_classes = [
APIKeyAuthentication,
]
permission_classes = [IsAuthenticated]
permission_classes = [
IsAuthenticated,
]
def filter_queryset(self, queryset):
for backend in list(self.filter_backends):
@@ -52,7 +56,8 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
if api_key:
service_token = APIToken.objects.filter(
token=api_key, is_service=True
token=api_key,
is_service=True,
).first()
if service_token:
@@ -118,7 +123,9 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
def finalize_response(self, request, response, *args, **kwargs):
# Call super to get the default response
response = super().finalize_response(request, response, *args, **kwargs)
response = super().finalize_response(
request, response, *args, **kwargs
)
# Add custom headers if they exist in the request META
ratelimit_remaining = request.META.get("X-RateLimit-Remaining")
@@ -147,13 +154,17 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
@property
def fields(self):
fields = [
field for field in self.request.GET.get("fields", "").split(",") if field
field
for field in self.request.GET.get("fields", "").split(",")
if field
]
return fields if fields else None
@property
def expand(self):
expand = [
expand for expand in self.request.GET.get("expand", "").split(",") if expand
expand
for expand in self.request.GET.get("expand", "").split(",")
if expand
]
return expand if expand else None
return expand if expand else None
+137 -72
View File
@@ -25,7 +25,10 @@ from rest_framework import status
from rest_framework.response import Response
# Module imports
from plane.api.serializers import CycleIssueSerializer, CycleSerializer
from plane.api.serializers import (
CycleIssueSerializer,
CycleSerializer,
)
from plane.app.permissions import ProjectEntityPermission
from plane.bgtasks.issue_activities_task import issue_activity
from plane.db.models import (
@@ -54,7 +57,9 @@ class CycleAPIEndpoint(BaseAPIView):
serializer_class = CycleSerializer
model = Cycle
webhook_event = "cycle"
permission_classes = [ProjectEntityPermission]
permission_classes = [
ProjectEntityPermission,
]
def get_queryset(self):
return (
@@ -73,7 +78,6 @@ class CycleAPIEndpoint(BaseAPIView):
filter=Q(
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -84,7 +88,6 @@ class CycleAPIEndpoint(BaseAPIView):
issue_cycle__issue__state__group="completed",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -95,7 +98,6 @@ class CycleAPIEndpoint(BaseAPIView):
issue_cycle__issue__state__group="cancelled",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -106,7 +108,6 @@ class CycleAPIEndpoint(BaseAPIView):
issue_cycle__issue__state__group="started",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -117,7 +118,6 @@ class CycleAPIEndpoint(BaseAPIView):
issue_cycle__issue__state__group="unstarted",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -128,7 +128,6 @@ class CycleAPIEndpoint(BaseAPIView):
issue_cycle__issue__state__group="backlog",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -138,18 +137,26 @@ class CycleAPIEndpoint(BaseAPIView):
def get(self, request, slug, project_id, pk=None):
if pk:
queryset = self.get_queryset().filter(archived_at__isnull=True).get(pk=pk)
queryset = (
self.get_queryset().filter(archived_at__isnull=True).get(pk=pk)
)
data = CycleSerializer(
queryset, fields=self.fields, expand=self.expand
queryset,
fields=self.fields,
expand=self.expand,
).data
return Response(data, status=status.HTTP_200_OK)
return Response(
data,
status=status.HTTP_200_OK,
)
queryset = self.get_queryset().filter(archived_at__isnull=True)
cycle_view = request.GET.get("cycle_view", "all")
# Current Cycle
if cycle_view == "current":
queryset = queryset.filter(
start_date__lte=timezone.now(), end_date__gte=timezone.now()
start_date__lte=timezone.now(),
end_date__gte=timezone.now(),
)
data = CycleSerializer(
queryset, many=True, fields=self.fields, expand=self.expand
@@ -163,7 +170,10 @@ class CycleAPIEndpoint(BaseAPIView):
request=request,
queryset=(queryset),
on_results=lambda cycles: CycleSerializer(
cycles, many=True, fields=self.fields, expand=self.expand
cycles,
many=True,
fields=self.fields,
expand=self.expand,
).data,
)
@@ -174,38 +184,53 @@ class CycleAPIEndpoint(BaseAPIView):
request=request,
queryset=(queryset),
on_results=lambda cycles: CycleSerializer(
cycles, many=True, fields=self.fields, expand=self.expand
cycles,
many=True,
fields=self.fields,
expand=self.expand,
).data,
)
# Draft Cycles
if cycle_view == "draft":
queryset = queryset.filter(end_date=None, start_date=None)
queryset = queryset.filter(
end_date=None,
start_date=None,
)
return self.paginate(
request=request,
queryset=(queryset),
on_results=lambda cycles: CycleSerializer(
cycles, many=True, fields=self.fields, expand=self.expand
cycles,
many=True,
fields=self.fields,
expand=self.expand,
).data,
)
# Incomplete Cycles
if cycle_view == "incomplete":
queryset = queryset.filter(
Q(end_date__gte=timezone.now()) | Q(end_date__isnull=True)
Q(end_date__gte=timezone.now()) | Q(end_date__isnull=True),
)
return self.paginate(
request=request,
queryset=(queryset),
on_results=lambda cycles: CycleSerializer(
cycles, many=True, fields=self.fields, expand=self.expand
cycles,
many=True,
fields=self.fields,
expand=self.expand,
).data,
)
return self.paginate(
request=request,
queryset=(queryset),
on_results=lambda cycles: CycleSerializer(
cycles, many=True, fields=self.fields, expand=self.expand
cycles,
many=True,
fields=self.fields,
expand=self.expand,
).data,
)
@@ -242,7 +267,10 @@ class CycleAPIEndpoint(BaseAPIView):
},
status=status.HTTP_409_CONFLICT,
)
serializer.save(project_id=project_id, owned_by=request.user)
serializer.save(
project_id=project_id,
owned_by=request.user,
)
# Send the model activity
model_activity.delay(
model_name="cycle",
@@ -253,8 +281,12 @@ class CycleAPIEndpoint(BaseAPIView):
slug=slug,
origin=request.META.get("HTTP_ORIGIN"),
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
return Response(
serializer.data, status=status.HTTP_201_CREATED
)
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
else:
return Response(
{
@@ -264,7 +296,9 @@ class CycleAPIEndpoint(BaseAPIView):
)
def patch(self, request, slug, project_id, pk):
cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
cycle = Cycle.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk
)
current_instance = json.dumps(
CycleSerializer(cycle).data, cls=DjangoJSONEncoder
@@ -282,7 +316,9 @@ class CycleAPIEndpoint(BaseAPIView):
if "sort_order" in request_data:
# Can only change sort order
request_data = {
"sort_order": request_data.get("sort_order", cycle.sort_order)
"sort_order": request_data.get(
"sort_order", cycle.sort_order
)
}
else:
return Response(
@@ -329,7 +365,9 @@ class CycleAPIEndpoint(BaseAPIView):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, slug, project_id, pk):
cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
cycle = Cycle.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk
)
if cycle.owned_by_id != request.user.id and (
not ProjectMember.objects.filter(
workspace__slug=slug,
@@ -345,9 +383,9 @@ class CycleAPIEndpoint(BaseAPIView):
)
cycle_issues = list(
CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list(
"issue", flat=True
)
CycleIssue.objects.filter(
cycle_id=self.kwargs.get("pk")
).values_list("issue", flat=True)
)
issue_activity.delay(
@@ -369,13 +407,17 @@ class CycleAPIEndpoint(BaseAPIView):
cycle.delete()
# Delete the user favorite cycle
UserFavorite.objects.filter(
entity_type="cycle", entity_identifier=pk, project_id=project_id
entity_type="cycle",
entity_identifier=pk,
project_id=project_id,
).delete()
return Response(status=status.HTTP_204_NO_CONTENT)
class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
permission_classes = [ProjectEntityPermission]
permission_classes = [
ProjectEntityPermission,
]
def get_queryset(self):
return (
@@ -395,7 +437,6 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
filter=Q(
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -406,7 +447,6 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
issue_cycle__issue__state__group="completed",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -417,7 +457,6 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
issue_cycle__issue__state__group="cancelled",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -428,7 +467,6 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
issue_cycle__issue__state__group="started",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -439,7 +477,6 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
issue_cycle__issue__state__group="unstarted",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -450,11 +487,12 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
issue_cycle__issue__state__group="backlog",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
),
)
)
.annotate(total_estimates=Sum("issue_cycle__issue__estimate_point"))
.annotate(
total_estimates=Sum("issue_cycle__issue__estimate_point")
)
.annotate(
completed_estimates=Sum(
"issue_cycle__issue__estimate_point",
@@ -462,7 +500,6 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
issue_cycle__issue__state__group="completed",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -473,7 +510,6 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
issue_cycle__issue__state__group="started",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -486,7 +522,10 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
request=request,
queryset=(self.get_queryset()),
on_results=lambda cycles: CycleSerializer(
cycles, many=True, fields=self.fields, expand=self.expand
cycles,
many=True,
fields=self.fields,
expand=self.expand,
).data,
)
@@ -529,12 +568,16 @@ class CycleIssueAPIEndpoint(BaseAPIView):
model = CycleIssue
webhook_event = "cycle_issue"
bulk = True
permission_classes = [ProjectEntityPermission]
permission_classes = [
ProjectEntityPermission,
]
def get_queryset(self):
return (
CycleIssue.objects.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("issue_id"))
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("issue_id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -572,11 +615,11 @@ class CycleIssueAPIEndpoint(BaseAPIView):
# List
order_by = request.GET.get("order_by", "created_at")
issues = (
Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id, issue_cycle__deleted_at__isnull=True
)
Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id)
.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -612,7 +655,10 @@ class CycleIssueAPIEndpoint(BaseAPIView):
request=request,
queryset=(issues),
on_results=lambda issues: CycleSerializer(
issues, many=True, fields=self.fields, expand=self.expand
issues,
many=True,
fields=self.fields,
expand=self.expand,
).data,
)
@@ -621,7 +667,8 @@ class CycleIssueAPIEndpoint(BaseAPIView):
if not issues:
return Response(
{"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST
{"error": "Issues are required"},
status=status.HTTP_400_BAD_REQUEST,
)
cycle = Cycle.objects.get(
@@ -630,7 +677,9 @@ class CycleIssueAPIEndpoint(BaseAPIView):
# Get all CycleIssues already created
cycle_issues = list(
CycleIssue.objects.filter(~Q(cycle_id=cycle_id), issue_id__in=issues)
CycleIssue.objects.filter(
~Q(cycle_id=cycle_id), issue_id__in=issues
)
)
existing_issues = [
@@ -675,7 +724,9 @@ class CycleIssueAPIEndpoint(BaseAPIView):
)
# Update the cycle issues
CycleIssue.objects.bulk_update(updated_records, ["cycle_id"], batch_size=100)
CycleIssue.objects.bulk_update(
updated_records, ["cycle_id"], batch_size=100
)
# Capture Issue Activity
issue_activity.delay(
@@ -734,7 +785,9 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
"""
permission_classes = [ProjectEntityPermission]
permission_classes = [
ProjectEntityPermission,
]
def post(self, request, slug, project_id, cycle_id):
new_cycle_id = request.data.get("new_cycle_id", False)
@@ -759,7 +812,6 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
filter=Q(
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -770,7 +822,6 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
issue_cycle__issue__state__group="completed",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -781,7 +832,6 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
issue_cycle__issue__state__group="cancelled",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -792,7 +842,6 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
issue_cycle__issue__state__group="started",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -803,7 +852,6 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
issue_cycle__issue__state__group="unstarted",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -814,7 +862,6 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
issue_cycle__issue__state__group="backlog",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -831,7 +878,6 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
assignee_estimate_data = (
Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
)
@@ -860,7 +906,9 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
)
.values("display_name", "assignee_id", "avatar", "avatar_url")
.annotate(
total_estimates=Sum(Cast("estimate_point__value", FloatField()))
total_estimates=Sum(
Cast("estimate_point__value", FloatField())
)
)
.annotate(
completed_estimates=Sum(
@@ -889,7 +937,9 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
{
"display_name": item["display_name"],
"assignee_id": (
str(item["assignee_id"]) if item["assignee_id"] else None
str(item["assignee_id"])
if item["assignee_id"]
else None
),
"avatar": item.get("avatar", None),
"avatar_url": item.get("avatar_url", None),
@@ -903,7 +953,6 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
label_distribution_data = (
Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
)
@@ -912,7 +961,9 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
.annotate(label_id=F("labels__id"))
.values("label_name", "color", "label_id")
.annotate(
total_estimates=Sum(Cast("estimate_point__value", FloatField()))
total_estimates=Sum(
Cast("estimate_point__value", FloatField())
)
)
.annotate(
completed_estimates=Sum(
@@ -949,7 +1000,9 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
{
"label_name": item["label_name"],
"color": item["color"],
"label_id": (str(item["label_id"]) if item["label_id"] else None),
"label_id": (
str(item["label_id"]) if item["label_id"] else None
),
"total_estimates": item["total_estimates"],
"completed_estimates": item["completed_estimates"],
"pending_estimates": item["pending_estimates"],
@@ -961,7 +1014,6 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
assignee_distribution = (
Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
)
@@ -981,7 +1033,8 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
),
# If `avatar_asset` is None, fall back to using `avatar` field directly
When(
assignees__avatar_asset__isnull=True, then="assignees__avatar"
assignees__avatar_asset__isnull=True,
then="assignees__avatar",
),
default=Value(None),
output_field=models.CharField(),
@@ -990,8 +1043,12 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
.values("display_name", "assignee_id", "avatar_url")
.annotate(
total_issues=Count(
"id", filter=Q(archived_at__isnull=True, is_draft=False)
)
"id",
filter=Q(
archived_at__isnull=True,
is_draft=False,
),
),
)
.annotate(
completed_issues=Count(
@@ -1035,7 +1092,6 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
label_distribution = (
Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
)
@@ -1045,8 +1101,12 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
.values("label_name", "color", "label_id")
.annotate(
total_issues=Count(
"id", filter=Q(archived_at__isnull=True, is_draft=False)
)
"id",
filter=Q(
archived_at__isnull=True,
is_draft=False,
),
),
)
.annotate(
completed_issues=Count(
@@ -1076,7 +1136,9 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
{
"label_name": item["label_name"],
"color": item["color"],
"label_id": (str(item["label_id"]) if item["label_id"] else None),
"label_id": (
str(item["label_id"]) if item["label_id"] else None
),
"total_issues": item["total_issues"],
"completed_issues": item["completed_issues"],
"pending_issues": item["pending_issues"],
@@ -1121,7 +1183,10 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
}
current_cycle.save(update_fields=["progress_snapshot"])
if new_cycle.end_date is not None and new_cycle.end_date < timezone.now():
if (
new_cycle.end_date is not None
and new_cycle.end_date < timezone.now()
):
return Response(
{
"error": "The cycle where the issues are transferred is already completed"
@@ -14,47 +14,60 @@ from rest_framework import status
from rest_framework.response import Response
# Module imports
from plane.api.serializers import IntakeIssueSerializer, IssueSerializer
from plane.api.serializers import InboxIssueSerializer, IssueSerializer
from plane.app.permissions import ProjectLitePermission
from plane.bgtasks.issue_activities_task import issue_activity
from plane.db.models import Intake, IntakeIssue, Issue, Project, ProjectMember, State
from plane.db.models import (
Inbox,
InboxIssue,
Issue,
Project,
ProjectMember,
State,
)
from .base import BaseAPIView
class IntakeIssueAPIEndpoint(BaseAPIView):
class InboxIssueAPIEndpoint(BaseAPIView):
"""
This viewset automatically provides `list`, `create`, `retrieve`,
`update` and `destroy` actions related to intake issues.
`update` and `destroy` actions related to inbox issues.
"""
permission_classes = [ProjectLitePermission]
permission_classes = [
ProjectLitePermission,
]
serializer_class = IntakeIssueSerializer
model = IntakeIssue
serializer_class = InboxIssueSerializer
model = InboxIssue
filterset_fields = ["status"]
filterset_fields = [
"status",
]
def get_queryset(self):
intake = Intake.objects.filter(
inbox = Inbox.objects.filter(
workspace__slug=self.kwargs.get("slug"),
project_id=self.kwargs.get("project_id"),
).first()
project = Project.objects.get(
workspace__slug=self.kwargs.get("slug"), pk=self.kwargs.get("project_id")
workspace__slug=self.kwargs.get("slug"),
pk=self.kwargs.get("project_id"),
)
if intake is None and not project.intake_view:
return IntakeIssue.objects.none()
if inbox is None and not project.inbox_view:
return InboxIssue.objects.none()
return (
IntakeIssue.objects.filter(
Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True),
InboxIssue.objects.filter(
Q(snoozed_till__gte=timezone.now())
| Q(snoozed_till__isnull=True),
workspace__slug=self.kwargs.get("slug"),
project_id=self.kwargs.get("project_id"),
intake_id=intake.id,
inbox_id=inbox.id,
)
.select_related("issue", "workspace", "project")
.order_by(self.kwargs.get("order_by", "-created_at"))
@@ -62,37 +75,49 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
def get(self, request, slug, project_id, issue_id=None):
if issue_id:
intake_issue_queryset = self.get_queryset().get(issue_id=issue_id)
intake_issue_data = IntakeIssueSerializer(
intake_issue_queryset, fields=self.fields, expand=self.expand
inbox_issue_queryset = self.get_queryset().get(issue_id=issue_id)
inbox_issue_data = InboxIssueSerializer(
inbox_issue_queryset,
fields=self.fields,
expand=self.expand,
).data
return Response(intake_issue_data, status=status.HTTP_200_OK)
return Response(
inbox_issue_data,
status=status.HTTP_200_OK,
)
issue_queryset = self.get_queryset()
return self.paginate(
request=request,
queryset=(issue_queryset),
on_results=lambda intake_issues: IntakeIssueSerializer(
intake_issues, many=True, fields=self.fields, expand=self.expand
on_results=lambda inbox_issues: InboxIssueSerializer(
inbox_issues,
many=True,
fields=self.fields,
expand=self.expand,
).data,
)
def post(self, request, slug, project_id):
if not request.data.get("issue", {}).get("name", False):
return Response(
{"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
{"error": "Name is required"},
status=status.HTTP_400_BAD_REQUEST,
)
intake = Intake.objects.filter(
inbox = Inbox.objects.filter(
workspace__slug=slug, project_id=project_id
).first()
project = Project.objects.get(workspace__slug=slug, pk=project_id)
project = Project.objects.get(
workspace__slug=slug,
pk=project_id,
)
# Intake view
if intake is None and not project.intake_view:
# Inbox view
if inbox is None and not project.inbox_view:
return Response(
{
"error": "Intake is not enabled for this project enable it through the project's api"
"error": "Inbox is not enabled for this project enable it through the project's api"
},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -106,14 +131,15 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
"none",
]:
return Response(
{"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
{"error": "Invalid priority"},
status=status.HTTP_400_BAD_REQUEST,
)
# Create or get state
state, _ = State.objects.get_or_create(
name="Triage",
group="triage",
description="Default state for managing all Intake Issues",
description="Default state for managing all Inbox Issues",
project_id=project_id,
color="#ff7700",
is_triage=True,
@@ -131,12 +157,12 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
state=state,
)
# create an intake issue
intake_issue = IntakeIssue.objects.create(
intake_id=intake.id,
# create an inbox issue
inbox_issue = InboxIssue.objects.create(
inbox_id=inbox.id,
project_id=project_id,
issue=issue,
source=request.data.get("source", "IN-APP"),
source=request.data.get("source", "in-app"),
)
# Create an Issue Activity
issue_activity.delay(
@@ -147,34 +173,32 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
project_id=str(project_id),
current_instance=None,
epoch=int(timezone.now().timestamp()),
intake=str(intake_issue.id),
inbox=str(inbox_issue.id),
)
serializer = IntakeIssueSerializer(intake_issue)
serializer = InboxIssueSerializer(inbox_issue)
return Response(serializer.data, status=status.HTTP_200_OK)
def patch(self, request, slug, project_id, issue_id):
intake = Intake.objects.filter(
inbox = Inbox.objects.filter(
workspace__slug=slug, project_id=project_id
).first()
project = Project.objects.get(workspace__slug=slug, pk=project_id)
# Intake view
if intake is None and not project.intake_view:
# Inbox view
if inbox is None:
return Response(
{
"error": "Intake is not enabled for this project enable it through the project's api"
"error": "Inbox is not enabled for this project enable it through the project's api"
},
status=status.HTTP_400_BAD_REQUEST,
)
# Get the intake issue
intake_issue = IntakeIssue.objects.get(
# Get the inbox issue
inbox_issue = InboxIssue.objects.get(
issue_id=issue_id,
workspace__slug=slug,
project_id=project_id,
intake_id=intake.id,
inbox_id=inbox.id,
)
# Get the project member
@@ -186,11 +210,11 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
)
# Only project members admins and created_by users can access this endpoint
if project_member.role <= 5 and str(intake_issue.created_by_id) != str(
if project_member.role <= 5 and str(inbox_issue.created_by_id) != str(
request.user.id
):
return Response(
{"error": "You cannot edit intake issues"},
{"error": "You cannot edit inbox issues"},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -205,7 +229,7 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
distinct=True,
filter=Q(
~Q(labels__id__isnull=True)
& Q(label_issue__deleted_at__isnull=True)
& Q(label_issue__deleted_at__isnull=True),
),
),
Value([], output_field=ArrayField(UUIDField())),
@@ -222,7 +246,11 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
),
Value([], output_field=ArrayField(UUIDField())),
),
).get(pk=issue_id, workspace__slug=slug, project_id=project_id)
).get(
pk=issue_id,
workspace__slug=slug,
project_id=project_id,
)
# Only allow guests to edit name and description
if project_member.role <= 5:
issue_data = {
@@ -230,10 +258,14 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
"description_html": issue_data.get(
"description_html", issue.description_html
),
"description": issue_data.get("description", issue.description),
"description": issue_data.get(
"description", issue.description
),
}
issue_serializer = IssueSerializer(issue, data=issue_data, partial=True)
issue_serializer = IssueSerializer(
issue, data=issue_data, partial=True
)
if issue_serializer.is_valid():
current_instance = issue
@@ -251,7 +283,7 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
cls=DjangoJSONEncoder,
),
epoch=int(timezone.now().timestamp()),
intake=(intake_issue.id),
inbox=(inbox_issue.id),
)
issue_serializer.save()
else:
@@ -259,13 +291,13 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
# Only project admins and members can edit intake issue attributes
# Only project admins and members can edit inbox issue attributes
if project_member.role > 15:
serializer = IntakeIssueSerializer(
intake_issue, data=request.data, partial=True
serializer = InboxIssueSerializer(
inbox_issue, data=request.data, partial=True
)
current_instance = json.dumps(
IntakeIssueSerializer(intake_issue).data, cls=DjangoJSONEncoder
InboxIssueSerializer(inbox_issue).data, cls=DjangoJSONEncoder
)
if serializer.is_valid():
@@ -273,10 +305,14 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
# Update the issue state if the issue is rejected or marked as duplicate
if serializer.data["status"] in [-1, 2]:
issue = Issue.objects.get(
pk=issue_id, workspace__slug=slug, project_id=project_id
pk=issue_id,
workspace__slug=slug,
project_id=project_id,
)
state = State.objects.filter(
group="cancelled", workspace__slug=slug, project_id=project_id
group="cancelled",
workspace__slug=slug,
project_id=project_id,
).first()
if state is not None:
issue.state = state
@@ -285,14 +321,18 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
# Update the issue state if it is accepted
if serializer.data["status"] in [1]:
issue = Issue.objects.get(
pk=issue_id, workspace__slug=slug, project_id=project_id
pk=issue_id,
workspace__slug=slug,
project_id=project_id,
)
# Update the issue state only if it is in triage state
if issue.state.is_triage:
# Move to default state
state = State.objects.filter(
workspace__slug=slug, project_id=project_id, default=True
workspace__slug=slug,
project_id=project_id,
default=True,
).first()
if state is not None:
issue.state = state
@@ -300,8 +340,10 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
# create a activity for status change
issue_activity.delay(
type="intake.activity.created",
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
type="inbox.activity.created",
requested_data=json.dumps(
request.data, cls=DjangoJSONEncoder
),
actor_id=str(request.user.id),
issue_id=str(issue_id),
project_id=str(project_id),
@@ -309,42 +351,48 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
epoch=int(timezone.now().timestamp()),
notification=False,
origin=request.META.get("HTTP_ORIGIN"),
intake=str(intake_issue.id),
inbox=str(inbox_issue.id),
)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
else:
return Response(
IntakeIssueSerializer(intake_issue).data, status=status.HTTP_200_OK
InboxIssueSerializer(inbox_issue).data,
status=status.HTTP_200_OK,
)
def delete(self, request, slug, project_id, issue_id):
intake = Intake.objects.filter(
inbox = Inbox.objects.filter(
workspace__slug=slug, project_id=project_id
).first()
project = Project.objects.get(workspace__slug=slug, pk=project_id)
project = Project.objects.get(
workspace__slug=slug,
pk=project_id,
)
# Intake view
if intake is None and not project.intake_view:
# Inbox view
if inbox is None and not project.inbox_view:
return Response(
{
"error": "Intake is not enabled for this project enable it through the project's api"
"error": "Inbox is not enabled for this project enable it through the project's api"
},
status=status.HTTP_400_BAD_REQUEST,
)
# Get the intake issue
intake_issue = IntakeIssue.objects.get(
# Get the inbox issue
inbox_issue = InboxIssue.objects.get(
issue_id=issue_id,
workspace__slug=slug,
project_id=project_id,
intake_id=intake.id,
inbox_id=inbox.id,
)
# Check the issue status
if intake_issue.status in [-2, -1, 0, 2]:
if inbox_issue.status in [-2, -1, 0, 2]:
# Delete the issue also
issue = Issue.objects.filter(
workspace__slug=slug, project_id=project_id, pk=issue_id
@@ -364,5 +412,5 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
)
issue.delete()
intake_issue.delete()
inbox_issue.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
+192 -57
View File
@@ -73,7 +73,9 @@ class WorkspaceIssueAPIEndpoint(BaseAPIView):
def get_queryset(self):
return (
Issue.issue_objects.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -89,10 +91,14 @@ class WorkspaceIssueAPIEndpoint(BaseAPIView):
.order_by(self.kwargs.get("order_by", "-created_at"))
).distinct()
def get(self, request, slug, project__identifier=None, issue__identifier=None):
def get(
self, request, slug, project__identifier=None, issue__identifier=None
):
if issue__identifier and project__identifier:
issue = Issue.issue_objects.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -102,7 +108,11 @@ class WorkspaceIssueAPIEndpoint(BaseAPIView):
sequence_id=issue__identifier,
)
return Response(
IssueSerializer(issue, fields=self.fields, expand=self.expand).data,
IssueSerializer(
issue,
fields=self.fields,
expand=self.expand,
).data,
status=status.HTTP_200_OK,
)
@@ -116,13 +126,17 @@ class IssueAPIEndpoint(BaseAPIView):
model = Issue
webhook_event = "issue"
permission_classes = [ProjectEntityPermission]
permission_classes = [
ProjectEntityPermission,
]
serializer_class = IssueSerializer
def get_queryset(self):
return (
Issue.issue_objects.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -150,25 +164,41 @@ class IssueAPIEndpoint(BaseAPIView):
project_id=project_id,
)
return Response(
IssueSerializer(issue, fields=self.fields, expand=self.expand).data,
IssueSerializer(
issue,
fields=self.fields,
expand=self.expand,
).data,
status=status.HTTP_200_OK,
)
if pk:
issue = Issue.issue_objects.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
).get(workspace__slug=slug, project_id=project_id, pk=pk)
return Response(
IssueSerializer(issue, fields=self.fields, expand=self.expand).data,
IssueSerializer(
issue,
fields=self.fields,
expand=self.expand,
).data,
status=status.HTTP_200_OK,
)
# Custom ordering for priority and state
priority_order = ["urgent", "high", "medium", "low", "none"]
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
state_order = [
"backlog",
"unstarted",
"started",
"completed",
"cancelled",
]
order_by_param = request.GET.get("order_by", "-created_at")
@@ -201,7 +231,9 @@ class IssueAPIEndpoint(BaseAPIView):
# Priority Ordering
if order_by_param == "priority" or order_by_param == "-priority":
priority_order = (
priority_order if order_by_param == "priority" else priority_order[::-1]
priority_order
if order_by_param == "priority"
else priority_order[::-1]
)
issue_queryset = issue_queryset.annotate(
priority_order=Case(
@@ -249,7 +281,9 @@ class IssueAPIEndpoint(BaseAPIView):
else order_by_param
)
).order_by(
"-max_values" if order_by_param.startswith("-") else "max_values"
"-max_values"
if order_by_param.startswith("-")
else "max_values"
)
else:
issue_queryset = issue_queryset.order_by(order_by_param)
@@ -258,7 +292,10 @@ class IssueAPIEndpoint(BaseAPIView):
request=request,
queryset=(issue_queryset),
on_results=lambda issues: IssueSerializer(
issues, many=True, fields=self.fields, expand=self.expand
issues,
many=True,
fields=self.fields,
expand=self.expand,
).data,
)
@@ -302,16 +339,22 @@ class IssueAPIEndpoint(BaseAPIView):
serializer.save()
# Refetch the issue
issue = Issue.objects.filter(
workspace__slug=slug, project_id=project_id, pk=serializer.data["id"]
workspace__slug=slug,
project_id=project_id,
pk=serializer.data["id"],
).first()
issue.created_at = request.data.get("created_at", timezone.now())
issue.created_by_id = request.data.get("created_by", request.user.id)
issue.created_by_id = request.data.get(
"created_by", request.user.id
)
issue.save(update_fields=["created_at", "created_by"])
# Track the issue
issue_activity.delay(
type="issue.activity.created",
requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder),
requested_data=json.dumps(
self.request.data, cls=DjangoJSONEncoder
),
actor_id=str(request.user.id),
issue_id=str(serializer.data.get("id", None)),
project_id=str(project_id),
@@ -348,7 +391,9 @@ class IssueAPIEndpoint(BaseAPIView):
# Get the requested data, encode it as django object and pass it
# to serializer to validation
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
requested_data = json.dumps(
self.request.data, cls=DjangoJSONEncoder
)
serializer = IssueSerializer(
issue,
data=request.data,
@@ -406,7 +451,9 @@ class IssueAPIEndpoint(BaseAPIView):
# If any of the created_at or created_by is present, update
# the issue with the provided data, else return with the
# default states given.
issue.created_at = request.data.get("created_at", timezone.now())
issue.created_at = request.data.get(
"created_at", timezone.now()
)
issue.created_by_id = request.data.get(
"created_by", request.user.id
)
@@ -423,8 +470,12 @@ class IssueAPIEndpoint(BaseAPIView):
current_instance=None,
epoch=int(timezone.now().timestamp()),
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
return Response(
serializer.data, status=status.HTTP_201_CREATED
)
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
else:
return Response(
{"error": "external_id and external_source are required"},
@@ -432,7 +483,9 @@ class IssueAPIEndpoint(BaseAPIView):
)
def patch(self, request, slug, project_id, pk=None):
issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
issue = Issue.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk
)
project = Project.objects.get(pk=project_id)
current_instance = json.dumps(
IssueSerializer(issue).data, cls=DjangoJSONEncoder
@@ -441,7 +494,10 @@ class IssueAPIEndpoint(BaseAPIView):
serializer = IssueSerializer(
issue,
data=request.data,
context={"project_id": project_id, "workspace_id": project.workspace_id},
context={
"project_id": project_id,
"workspace_id": project.workspace_id,
},
partial=True,
)
if serializer.is_valid():
@@ -479,7 +535,9 @@ class IssueAPIEndpoint(BaseAPIView):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, slug, project_id, pk=None):
issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
issue = Issue.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk
)
if issue.created_by_id != request.user.id and (
not ProjectMember.objects.filter(
workspace__slug=slug,
@@ -518,7 +576,9 @@ class LabelAPIEndpoint(BaseAPIView):
serializer_class = LabelSerializer
model = Label
permission_classes = [ProjectMemberPermission]
permission_classes = [
ProjectMemberPermission,
]
def get_queryset(self):
return (
@@ -565,8 +625,12 @@ class LabelAPIEndpoint(BaseAPIView):
)
serializer.save(project_id=project_id)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
return Response(
serializer.data, status=status.HTTP_201_CREATED
)
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
except IntegrityError:
label = Label.objects.filter(
workspace__slug=slug,
@@ -587,11 +651,18 @@ class LabelAPIEndpoint(BaseAPIView):
request=request,
queryset=(self.get_queryset()),
on_results=lambda labels: LabelSerializer(
labels, many=True, fields=self.fields, expand=self.expand
labels,
many=True,
fields=self.fields,
expand=self.expand,
).data,
)
label = self.get_queryset().get(pk=pk)
serializer = LabelSerializer(label, fields=self.fields, expand=self.expand)
serializer = LabelSerializer(
label,
fields=self.fields,
expand=self.expand,
)
return Response(serializer.data, status=status.HTTP_200_OK)
def patch(self, request, slug, project_id, pk=None):
@@ -634,7 +705,9 @@ class IssueLinkAPIEndpoint(BaseAPIView):
"""
permission_classes = [ProjectEntityPermission]
permission_classes = [
ProjectEntityPermission,
]
model = IssueLink
serializer_class = IssueLinkSerializer
@@ -657,32 +730,46 @@ class IssueLinkAPIEndpoint(BaseAPIView):
if pk is None:
issue_links = self.get_queryset()
serializer = IssueLinkSerializer(
issue_links, fields=self.fields, expand=self.expand
issue_links,
fields=self.fields,
expand=self.expand,
)
return self.paginate(
request=request,
queryset=(self.get_queryset()),
on_results=lambda issue_links: IssueLinkSerializer(
issue_links, many=True, fields=self.fields, expand=self.expand
issue_links,
many=True,
fields=self.fields,
expand=self.expand,
).data,
)
issue_link = self.get_queryset().get(pk=pk)
serializer = IssueLinkSerializer(
issue_link, fields=self.fields, expand=self.expand
issue_link,
fields=self.fields,
expand=self.expand,
)
return Response(serializer.data, status=status.HTTP_200_OK)
def post(self, request, slug, project_id, issue_id):
serializer = IssueLinkSerializer(data=request.data)
if serializer.is_valid():
serializer.save(project_id=project_id, issue_id=issue_id)
serializer.save(
project_id=project_id,
issue_id=issue_id,
)
link = IssueLink.objects.get(pk=serializer.data["id"])
link.created_by_id = request.data.get("created_by", request.user.id)
link.created_by_id = request.data.get(
"created_by", request.user.id
)
link.save(update_fields=["created_by"])
issue_activity.delay(
type="link.activity.created",
requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder),
requested_data=json.dumps(
serializer.data, cls=DjangoJSONEncoder
),
issue_id=str(self.kwargs.get("issue_id")),
project_id=str(self.kwargs.get("project_id")),
actor_id=str(link.created_by_id),
@@ -694,13 +781,19 @@ class IssueLinkAPIEndpoint(BaseAPIView):
def patch(self, request, slug, project_id, issue_id, pk):
issue_link = IssueLink.objects.get(
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
workspace__slug=slug,
project_id=project_id,
issue_id=issue_id,
pk=pk,
)
requested_data = json.dumps(request.data, cls=DjangoJSONEncoder)
current_instance = json.dumps(
IssueLinkSerializer(issue_link).data, cls=DjangoJSONEncoder
IssueLinkSerializer(issue_link).data,
cls=DjangoJSONEncoder,
)
serializer = IssueLinkSerializer(
issue_link, data=request.data, partial=True
)
serializer = IssueLinkSerializer(issue_link, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
issue_activity.delay(
@@ -717,10 +810,14 @@ class IssueLinkAPIEndpoint(BaseAPIView):
def delete(self, request, slug, project_id, issue_id, pk):
issue_link = IssueLink.objects.get(
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
workspace__slug=slug,
project_id=project_id,
issue_id=issue_id,
pk=pk,
)
current_instance = json.dumps(
IssueLinkSerializer(issue_link).data, cls=DjangoJSONEncoder
IssueLinkSerializer(issue_link).data,
cls=DjangoJSONEncoder,
)
issue_activity.delay(
type="link.activity.deleted",
@@ -745,11 +842,15 @@ class IssueCommentAPIEndpoint(BaseAPIView):
serializer_class = IssueCommentSerializer
model = IssueComment
webhook_event = "issue_comment"
permission_classes = [ProjectLitePermission]
permission_classes = [
ProjectLitePermission,
]
def get_queryset(self):
return (
IssueComment.objects.filter(workspace__slug=self.kwargs.get("slug"))
IssueComment.objects.filter(
workspace__slug=self.kwargs.get("slug")
)
.filter(project_id=self.kwargs.get("project_id"))
.filter(issue_id=self.kwargs.get("issue_id"))
.filter(
@@ -776,14 +877,19 @@ class IssueCommentAPIEndpoint(BaseAPIView):
if pk:
issue_comment = self.get_queryset().get(pk=pk)
serializer = IssueCommentSerializer(
issue_comment, fields=self.fields, expand=self.expand
issue_comment,
fields=self.fields,
expand=self.expand,
)
return Response(serializer.data, status=status.HTTP_200_OK)
return self.paginate(
request=request,
queryset=(self.get_queryset()),
on_results=lambda issue_comment: IssueCommentSerializer(
issue_comment, many=True, fields=self.fields, expand=self.expand
issue_comment,
many=True,
fields=self.fields,
expand=self.expand,
).data,
)
@@ -816,11 +922,17 @@ class IssueCommentAPIEndpoint(BaseAPIView):
serializer = IssueCommentSerializer(data=request.data)
if serializer.is_valid():
serializer.save(
project_id=project_id, issue_id=issue_id, actor=request.user
project_id=project_id,
issue_id=issue_id,
actor=request.user,
)
issue_comment = IssueComment.objects.get(
pk=serializer.data.get("id")
)
issue_comment = IssueComment.objects.get(pk=serializer.data.get("id"))
# Update the created_at and the created_by and save the comment
issue_comment.created_at = request.data.get("created_at", timezone.now())
issue_comment.created_at = request.data.get(
"created_at", timezone.now()
)
issue_comment.created_by_id = request.data.get(
"created_by", request.user.id
)
@@ -828,7 +940,9 @@ class IssueCommentAPIEndpoint(BaseAPIView):
issue_activity.delay(
type="comment.activity.created",
requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder),
requested_data=json.dumps(
serializer.data, cls=DjangoJSONEncoder
),
actor_id=str(issue_comment.created_by_id),
issue_id=str(self.kwargs.get("issue_id")),
project_id=str(self.kwargs.get("project_id")),
@@ -840,17 +954,24 @@ class IssueCommentAPIEndpoint(BaseAPIView):
def patch(self, request, slug, project_id, issue_id, pk):
issue_comment = IssueComment.objects.get(
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
workspace__slug=slug,
project_id=project_id,
issue_id=issue_id,
pk=pk,
)
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
current_instance = json.dumps(
IssueCommentSerializer(issue_comment).data, cls=DjangoJSONEncoder
IssueCommentSerializer(issue_comment).data,
cls=DjangoJSONEncoder,
)
# Validation check if the issue already exists
if (
request.data.get("external_id")
and (issue_comment.external_id != str(request.data.get("external_id")))
and (
issue_comment.external_id
!= str(request.data.get("external_id"))
)
and IssueComment.objects.filter(
project_id=project_id,
workspace__slug=slug,
@@ -887,10 +1008,14 @@ class IssueCommentAPIEndpoint(BaseAPIView):
def delete(self, request, slug, project_id, issue_id, pk):
issue_comment = IssueComment.objects.get(
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
workspace__slug=slug,
project_id=project_id,
issue_id=issue_id,
pk=pk,
)
current_instance = json.dumps(
IssueCommentSerializer(issue_comment).data, cls=DjangoJSONEncoder
IssueCommentSerializer(issue_comment).data,
cls=DjangoJSONEncoder,
)
issue_comment.delete()
issue_activity.delay(
@@ -906,7 +1031,9 @@ class IssueCommentAPIEndpoint(BaseAPIView):
class IssueActivityAPIEndpoint(BaseAPIView):
permission_classes = [ProjectEntityPermission]
permission_classes = [
ProjectEntityPermission,
]
def get(self, request, slug, project_id, issue_id, pk=None):
issue_activities = (
@@ -931,14 +1058,19 @@ class IssueActivityAPIEndpoint(BaseAPIView):
request=request,
queryset=(issue_activities),
on_results=lambda issue_activity: IssueActivitySerializer(
issue_activity, many=True, fields=self.fields, expand=self.expand
issue_activity,
many=True,
fields=self.fields,
expand=self.expand,
).data,
)
class IssueAttachmentEndpoint(BaseAPIView):
serializer_class = IssueAttachmentSerializer
permission_classes = [ProjectEntityPermission]
permission_classes = [
ProjectEntityPermission,
]
model = FileAsset
parser_classes = (MultiPartParser, FormParser)
@@ -977,7 +1109,10 @@ class IssueAttachmentEndpoint(BaseAPIView):
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=json.dumps(serializer.data, cls=DjangoJSONEncoder),
current_instance=json.dumps(
serializer.data,
cls=DjangoJSONEncoder,
),
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
+29 -8
View File
@@ -13,14 +13,24 @@ from rest_framework import status
# Module imports
from .base import BaseAPIView
from plane.api.serializers import UserLiteSerializer
from plane.db.models import User, Workspace, Project, WorkspaceMember, ProjectMember
from plane.db.models import (
User,
Workspace,
Project,
WorkspaceMember,
ProjectMember,
)
from plane.app.permissions import ProjectMemberPermission
from plane.app.permissions import (
ProjectMemberPermission,
)
# API endpoint to get and insert users inside the workspace
class ProjectMemberAPIEndpoint(BaseAPIView):
permission_classes = [ProjectMemberPermission]
permission_classes = [
ProjectMemberPermission,
]
# Get all the users that are present inside the workspace
def get(self, request, slug, project_id):
@@ -38,7 +48,10 @@ class ProjectMemberAPIEndpoint(BaseAPIView):
# Get all the users that are present inside the workspace
users = UserLiteSerializer(
User.objects.filter(id__in=project_members), many=True
User.objects.filter(
id__in=project_members,
),
many=True,
).data
return Response(users, status=status.HTTP_200_OK)
@@ -65,7 +78,8 @@ class ProjectMemberAPIEndpoint(BaseAPIView):
validate_email(email)
except ValidationError:
return Response(
{"error": "Invalid email provided"}, status=status.HTTP_400_BAD_REQUEST
{"error": "Invalid email provided"},
status=status.HTTP_400_BAD_REQUEST,
)
workspace = Workspace.objects.filter(slug=slug).first()
@@ -94,7 +108,9 @@ class ProjectMemberAPIEndpoint(BaseAPIView):
).first()
if project_member:
return Response(
{"error": "User is already part of the workspace and project"},
{
"error": "User is already part of the workspace and project"
},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -115,14 +131,18 @@ class ProjectMemberAPIEndpoint(BaseAPIView):
# Create a workspace member for the user if not already a member
if not workspace_member:
workspace_member = WorkspaceMember.objects.create(
workspace=workspace, member=user, role=request.data.get("role", 5)
workspace=workspace,
member=user,
role=request.data.get("role", 5),
)
workspace_member.save()
# Create a project member for the user if not already a member
if not project_member:
project_member = ProjectMember.objects.create(
project=project, member=user, role=request.data.get("role", 5)
project=project,
member=user,
role=request.data.get("role", 5),
)
project_member.save()
@@ -130,3 +150,4 @@ class ProjectMemberAPIEndpoint(BaseAPIView):
user_data = UserLiteSerializer(user).data
return Response(user_data, status=status.HTTP_201_CREATED)
+98 -45
View File
@@ -43,7 +43,9 @@ class ModuleAPIEndpoint(BaseAPIView):
"""
model = Module
permission_classes = [ProjectEntityPermission]
permission_classes = [
ProjectEntityPermission,
]
serializer_class = ModuleSerializer
webhook_event = "module"
@@ -58,7 +60,9 @@ class ModuleAPIEndpoint(BaseAPIView):
.prefetch_related(
Prefetch(
"link_module",
queryset=ModuleLink.objects.select_related("module", "created_by"),
queryset=ModuleLink.objects.select_related(
"module", "created_by"
),
)
)
.annotate(
@@ -67,10 +71,9 @@ class ModuleAPIEndpoint(BaseAPIView):
filter=Q(
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
issue_module__deleted_at__isnull=True,
),
distinct=True,
)
),
)
.annotate(
completed_issues=Count(
@@ -79,7 +82,6 @@ class ModuleAPIEndpoint(BaseAPIView):
issue_module__issue__state__group="completed",
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
issue_module__deleted_at__isnull=True,
),
distinct=True,
)
@@ -91,7 +93,6 @@ class ModuleAPIEndpoint(BaseAPIView):
issue_module__issue__state__group="cancelled",
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
issue_module__deleted_at__isnull=True,
),
distinct=True,
)
@@ -103,7 +104,6 @@ class ModuleAPIEndpoint(BaseAPIView):
issue_module__issue__state__group="started",
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
issue_module__deleted_at__isnull=True,
),
distinct=True,
)
@@ -115,7 +115,6 @@ class ModuleAPIEndpoint(BaseAPIView):
issue_module__issue__state__group="unstarted",
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
issue_module__deleted_at__isnull=True,
),
distinct=True,
)
@@ -127,7 +126,6 @@ class ModuleAPIEndpoint(BaseAPIView):
issue_module__issue__state__group="backlog",
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
issue_module__deleted_at__isnull=True,
),
distinct=True,
)
@@ -139,7 +137,10 @@ class ModuleAPIEndpoint(BaseAPIView):
project = Project.objects.get(pk=project_id, workspace__slug=slug)
serializer = ModuleSerializer(
data=request.data,
context={"project_id": project_id, "workspace_id": project.workspace_id},
context={
"project_id": project_id,
"workspace_id": project.workspace_id,
},
)
if serializer.is_valid():
if (
@@ -182,7 +183,9 @@ class ModuleAPIEndpoint(BaseAPIView):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def patch(self, request, slug, project_id, pk):
module = Module.objects.get(pk=pk, project_id=project_id, workspace__slug=slug)
module = Module.objects.get(
pk=pk, project_id=project_id, workspace__slug=slug
)
current_instance = json.dumps(
ModuleSerializer(module).data, cls=DjangoJSONEncoder
@@ -194,7 +197,10 @@ class ModuleAPIEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST,
)
serializer = ModuleSerializer(
module, data=request.data, context={"project_id": project_id}, partial=True
module,
data=request.data,
context={"project_id": project_id},
partial=True,
)
if serializer.is_valid():
if (
@@ -234,21 +240,33 @@ class ModuleAPIEndpoint(BaseAPIView):
def get(self, request, slug, project_id, pk=None):
if pk:
queryset = self.get_queryset().filter(archived_at__isnull=True).get(pk=pk)
queryset = (
self.get_queryset().filter(archived_at__isnull=True).get(pk=pk)
)
data = ModuleSerializer(
queryset, fields=self.fields, expand=self.expand
queryset,
fields=self.fields,
expand=self.expand,
).data
return Response(data, status=status.HTTP_200_OK)
return Response(
data,
status=status.HTTP_200_OK,
)
return self.paginate(
request=request,
queryset=(self.get_queryset().filter(archived_at__isnull=True)),
on_results=lambda modules: ModuleSerializer(
modules, many=True, fields=self.fields, expand=self.expand
modules,
many=True,
fields=self.fields,
expand=self.expand,
).data,
)
def delete(self, request, slug, project_id, pk):
module = Module.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
module = Module.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk
)
if module.created_by_id != request.user.id and (
not ProjectMember.objects.filter(
workspace__slug=slug,
@@ -264,7 +282,9 @@ class ModuleAPIEndpoint(BaseAPIView):
)
module_issues = list(
ModuleIssue.objects.filter(module_id=pk).values_list("issue", flat=True)
ModuleIssue.objects.filter(module_id=pk).values_list(
"issue", flat=True
)
)
issue_activity.delay(
type="module.activity.deleted",
@@ -278,15 +298,24 @@ class ModuleAPIEndpoint(BaseAPIView):
actor_id=str(request.user.id),
issue_id=None,
project_id=str(project_id),
current_instance=json.dumps({"module_name": str(module.name)}),
current_instance=json.dumps(
{
"module_name": str(module.name),
}
),
epoch=int(timezone.now().timestamp()),
)
module.delete()
# Delete the module issues
ModuleIssue.objects.filter(module=pk, project_id=project_id).delete()
ModuleIssue.objects.filter(
module=pk,
project_id=project_id,
).delete()
# Delete the user favorite module
UserFavorite.objects.filter(
entity_type="module", entity_identifier=pk, project_id=project_id
entity_type="module",
entity_identifier=pk,
project_id=project_id,
).delete()
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -303,12 +332,16 @@ class ModuleIssueAPIEndpoint(BaseAPIView):
webhook_event = "module_issue"
bulk = True
permission_classes = [ProjectEntityPermission]
permission_classes = [
ProjectEntityPermission,
]
def get_queryset(self):
return (
ModuleIssue.objects.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("issue"))
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("issue")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -334,11 +367,11 @@ class ModuleIssueAPIEndpoint(BaseAPIView):
def get(self, request, slug, project_id, module_id):
order_by = request.GET.get("order_by", "created_at")
issues = (
Issue.issue_objects.filter(
issue_module__module_id=module_id, issue_module__deleted_at__isnull=True
)
Issue.issue_objects.filter(issue_module__module_id=module_id)
.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -373,7 +406,10 @@ class ModuleIssueAPIEndpoint(BaseAPIView):
request=request,
queryset=(issues),
on_results=lambda issues: IssueSerializer(
issues, many=True, fields=self.fields, expand=self.expand
issues,
many=True,
fields=self.fields,
expand=self.expand,
).data,
)
@@ -381,7 +417,8 @@ class ModuleIssueAPIEndpoint(BaseAPIView):
issues = request.data.get("issues", [])
if not len(issues):
return Response(
{"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST
{"error": "Issues are required"},
status=status.HTTP_400_BAD_REQUEST,
)
module = Module.objects.get(
workspace__slug=slug, project_id=project_id, pk=module_id
@@ -428,10 +465,16 @@ class ModuleIssueAPIEndpoint(BaseAPIView):
)
ModuleIssue.objects.bulk_create(
record_to_create, batch_size=10, ignore_conflicts=True
record_to_create,
batch_size=10,
ignore_conflicts=True,
)
ModuleIssue.objects.bulk_update(records_to_update, ["module"], batch_size=10)
ModuleIssue.objects.bulk_update(
records_to_update,
["module"],
batch_size=10,
)
# Capture Issue Activity
issue_activity.delay(
@@ -467,7 +510,10 @@ class ModuleIssueAPIEndpoint(BaseAPIView):
issue_activity.delay(
type="module.activity.deleted",
requested_data=json.dumps(
{"module_id": str(module_id), "issues": [str(module_issue.issue_id)]}
{
"module_id": str(module_id),
"issues": [str(module_issue.issue_id)],
}
),
actor_id=str(request.user.id),
issue_id=str(issue_id),
@@ -479,7 +525,9 @@ class ModuleIssueAPIEndpoint(BaseAPIView):
class ModuleArchiveUnarchiveAPIEndpoint(BaseAPIView):
permission_classes = [ProjectEntityPermission]
permission_classes = [
ProjectEntityPermission,
]
def get_queryset(self):
return (
@@ -493,7 +541,9 @@ class ModuleArchiveUnarchiveAPIEndpoint(BaseAPIView):
.prefetch_related(
Prefetch(
"link_module",
queryset=ModuleLink.objects.select_related("module", "created_by"),
queryset=ModuleLink.objects.select_related(
"module", "created_by"
),
)
)
.annotate(
@@ -502,10 +552,9 @@ class ModuleArchiveUnarchiveAPIEndpoint(BaseAPIView):
filter=Q(
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
issue_module__deleted_at__isnull=True,
),
distinct=True,
)
),
)
.annotate(
completed_issues=Count(
@@ -514,7 +563,6 @@ class ModuleArchiveUnarchiveAPIEndpoint(BaseAPIView):
issue_module__issue__state__group="completed",
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
issue_module__deleted_at__isnull=True,
),
distinct=True,
)
@@ -526,7 +574,6 @@ class ModuleArchiveUnarchiveAPIEndpoint(BaseAPIView):
issue_module__issue__state__group="cancelled",
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
issue_module__deleted_at__isnull=True,
),
distinct=True,
)
@@ -538,7 +585,6 @@ class ModuleArchiveUnarchiveAPIEndpoint(BaseAPIView):
issue_module__issue__state__group="started",
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
issue_module__deleted_at__isnull=True,
),
distinct=True,
)
@@ -550,7 +596,6 @@ class ModuleArchiveUnarchiveAPIEndpoint(BaseAPIView):
issue_module__issue__state__group="unstarted",
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
issue_module__deleted_at__isnull=True,
),
distinct=True,
)
@@ -562,7 +607,6 @@ class ModuleArchiveUnarchiveAPIEndpoint(BaseAPIView):
issue_module__issue__state__group="backlog",
issue_module__issue__archived_at__isnull=True,
issue_module__issue__is_draft=False,
issue_module__deleted_at__isnull=True,
),
distinct=True,
)
@@ -575,15 +619,22 @@ class ModuleArchiveUnarchiveAPIEndpoint(BaseAPIView):
request=request,
queryset=(self.get_queryset()),
on_results=lambda modules: ModuleSerializer(
modules, many=True, fields=self.fields, expand=self.expand
modules,
many=True,
fields=self.fields,
expand=self.expand,
).data,
)
def post(self, request, slug, project_id, pk):
module = Module.objects.get(pk=pk, project_id=project_id, workspace__slug=slug)
module = Module.objects.get(
pk=pk, project_id=project_id, workspace__slug=slug
)
if module.status not in ["completed", "cancelled"]:
return Response(
{"error": "Only completed or cancelled modules can be archived"},
{
"error": "Only completed or cancelled modules can be archived"
},
status=status.HTTP_400_BAD_REQUEST,
)
module.archived_at = timezone.now()
@@ -597,7 +648,9 @@ class ModuleArchiveUnarchiveAPIEndpoint(BaseAPIView):
return Response(status=status.HTTP_204_NO_CONTENT)
def delete(self, request, slug, project_id, pk):
module = Module.objects.get(pk=pk, project_id=project_id, workspace__slug=slug)
module = Module.objects.get(
pk=pk, project_id=project_id, workspace__slug=slug
)
module.archived_at = None
module.save()
return Response(status=status.HTTP_204_NO_CONTENT)
+71 -32
View File
@@ -18,7 +18,7 @@ from plane.app.permissions import ProjectBasePermission
# Module imports
from plane.db.models import (
Cycle,
Intake,
Inbox,
IssueUserProperty,
Module,
Project,
@@ -39,7 +39,9 @@ class ProjectAPIEndpoint(BaseAPIView):
model = Project
webhook_event = "project"
permission_classes = [ProjectBasePermission]
permission_classes = [
ProjectBasePermission,
]
def get_queryset(self):
return (
@@ -52,7 +54,10 @@ class ProjectAPIEndpoint(BaseAPIView):
| Q(network=2)
)
.select_related(
"workspace", "workspace__owner", "default_assignee", "project_lead"
"workspace",
"workspace__owner",
"default_assignee",
"project_lead",
)
.annotate(
is_member=Exists(
@@ -66,7 +71,9 @@ class ProjectAPIEndpoint(BaseAPIView):
)
.annotate(
total_members=ProjectMember.objects.filter(
project_id=OuterRef("id"), member__is_bot=False, is_active=True
project_id=OuterRef("id"),
member__is_bot=False,
is_active=True,
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
@@ -118,7 +125,8 @@ class ProjectAPIEndpoint(BaseAPIView):
Prefetch(
"project_projectmember",
queryset=ProjectMember.objects.filter(
workspace__slug=slug, is_active=True
workspace__slug=slug,
is_active=True,
).select_related("member"),
)
)
@@ -128,11 +136,18 @@ class ProjectAPIEndpoint(BaseAPIView):
request=request,
queryset=(projects),
on_results=lambda projects: ProjectSerializer(
projects, many=True, fields=self.fields, expand=self.expand
projects,
many=True,
fields=self.fields,
expand=self.expand,
).data,
)
project = self.get_queryset().get(workspace__slug=slug, pk=pk)
serializer = ProjectSerializer(project, fields=self.fields, expand=self.expand)
serializer = ProjectSerializer(
project,
fields=self.fields,
expand=self.expand,
)
return Response(serializer.data, status=status.HTTP_200_OK)
def post(self, request, slug):
@@ -146,11 +161,14 @@ class ProjectAPIEndpoint(BaseAPIView):
# Add the user as Administrator to the project
_ = ProjectMember.objects.create(
project_id=serializer.data["id"], member=request.user, role=20
project_id=serializer.data["id"],
member=request.user,
role=20,
)
# Also create the issue property for the user
_ = IssueUserProperty.objects.create(
project_id=serializer.data["id"], user=request.user
project_id=serializer.data["id"],
user=request.user,
)
if serializer.data["project_lead"] is not None and str(
@@ -218,7 +236,11 @@ class ProjectAPIEndpoint(BaseAPIView):
]
)
project = self.get_queryset().filter(pk=serializer.data["id"]).first()
project = (
self.get_queryset()
.filter(pk=serializer.data["id"])
.first()
)
# Model activity
model_activity.delay(
@@ -232,8 +254,13 @@ class ProjectAPIEndpoint(BaseAPIView):
)
serializer = ProjectSerializer(project)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
return Response(
serializer.data, status=status.HTTP_201_CREATED
)
return Response(
serializer.errors,
status=status.HTTP_400_BAD_REQUEST,
)
except IntegrityError as e:
if "already exists" in str(e):
return Response(
@@ -242,7 +269,8 @@ class ProjectAPIEndpoint(BaseAPIView):
)
except Workspace.DoesNotExist:
return Response(
{"error": "Workspace does not exist"}, status=status.HTTP_404_NOT_FOUND
{"error": "Workspace does not exist"},
status=status.HTTP_404_NOT_FOUND,
)
except ValidationError:
return Response(
@@ -257,11 +285,6 @@ class ProjectAPIEndpoint(BaseAPIView):
current_instance = json.dumps(
ProjectSerializer(project).data, cls=DjangoJSONEncoder
)
intake_view = request.data.get(
"inbox_view", request.data.get("intake_view", False)
)
if project.archived_at:
return Response(
{"error": "Archived project cannot be updated"},
@@ -270,20 +293,21 @@ class ProjectAPIEndpoint(BaseAPIView):
serializer = ProjectSerializer(
project,
data={**request.data, "intake_view": intake_view},
data={**request.data},
context={"workspace_id": workspace.id},
partial=True,
)
if serializer.is_valid():
serializer.save()
if serializer.data["intake_view"]:
intake = Intake.objects.filter(
project=project, is_default=True
if serializer.data["inbox_view"]:
inbox = Inbox.objects.filter(
project=project,
is_default=True,
).first()
if not intake:
Intake.objects.create(
name=f"{project.name} Intake",
if not inbox:
Inbox.objects.create(
name=f"{project.name} Inbox",
project=project,
is_default=True,
)
@@ -292,13 +316,17 @@ class ProjectAPIEndpoint(BaseAPIView):
State.objects.get_or_create(
name="Triage",
group="triage",
description="Default state for managing all Intake Issues",
description="Default state for managing all Inbox Issues",
project_id=pk,
color="#ff7700",
is_triage=True,
)
project = self.get_queryset().filter(pk=serializer.data["id"]).first()
project = (
self.get_queryset()
.filter(pk=serializer.data["id"])
.first()
)
model_activity.delay(
model_name="project",
@@ -312,7 +340,9 @@ class ProjectAPIEndpoint(BaseAPIView):
serializer = ProjectSerializer(project)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
except IntegrityError as e:
if "already exists" in str(e):
return Response(
@@ -321,7 +351,8 @@ class ProjectAPIEndpoint(BaseAPIView):
)
except (Project.DoesNotExist, Workspace.DoesNotExist):
return Response(
{"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND
{"error": "Project does not exist"},
status=status.HTTP_404_NOT_FOUND,
)
except ValidationError:
return Response(
@@ -333,20 +364,28 @@ class ProjectAPIEndpoint(BaseAPIView):
project = Project.objects.get(pk=pk, workspace__slug=slug)
# Delete the user favorite cycle
UserFavorite.objects.filter(
entity_type="project", entity_identifier=pk, project_id=pk
entity_type="project",
entity_identifier=pk,
project_id=pk,
).delete()
project.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
class ProjectArchiveUnarchiveAPIEndpoint(BaseAPIView):
permission_classes = [ProjectBasePermission]
permission_classes = [
ProjectBasePermission,
]
def post(self, request, slug, project_id):
project = Project.objects.get(pk=project_id, workspace__slug=slug)
project.archived_at = timezone.now()
project.save()
UserFavorite.objects.filter(workspace__slug=slug, project=project_id).delete()
UserFavorite.objects.filter(
workspace__slug=slug,
project=project_id,
).delete()
return Response(status=status.HTTP_204_NO_CONTENT)
def delete(self, request, slug, project_id):
+17 -5
View File
@@ -16,7 +16,9 @@ from .base import BaseAPIView
class StateAPIEndpoint(BaseAPIView):
serializer_class = StateSerializer
model = State
permission_classes = [ProjectEntityPermission]
permission_classes = [
ProjectEntityPermission,
]
def get_queryset(self):
return (
@@ -65,7 +67,9 @@ class StateAPIEndpoint(BaseAPIView):
serializer.save(project_id=project_id)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
except IntegrityError:
state = State.objects.filter(
workspace__slug=slug,
@@ -92,13 +96,19 @@ class StateAPIEndpoint(BaseAPIView):
request=request,
queryset=(self.get_queryset()),
on_results=lambda states: StateSerializer(
states, many=True, fields=self.fields, expand=self.expand
states,
many=True,
fields=self.fields,
expand=self.expand,
).data,
)
def delete(self, request, slug, project_id, state_id):
state = State.objects.get(
is_triage=False, pk=state_id, project_id=project_id, workspace__slug=slug
is_triage=False,
pk=state_id,
project_id=project_id,
workspace__slug=slug,
)
if state.default:
@@ -112,7 +122,9 @@ class StateAPIEndpoint(BaseAPIView):
if issue_exist:
return Response(
{"error": "The state is not empty, only empty states can be deleted"},
{
"error": "The state is not empty, only empty states can be deleted"
},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -25,7 +25,10 @@ class APIKeyAuthentication(authentication.BaseAuthentication):
def validate_api_token(self, token):
try:
api_token = APIToken.objects.get(
Q(Q(expired_at__gt=timezone.now()) | Q(expired_at__isnull=True)),
Q(
Q(expired_at__gt=timezone.now())
| Q(expired_at__isnull=True)
),
token=token,
is_active=True,
)
+1 -1
View File
@@ -12,4 +12,4 @@ from .project import (
ProjectMemberPermission,
ProjectLitePermission,
)
from .base import allow_permission, ROLE
from .base import allow_permission, ROLE
+3 -2
View File
@@ -5,7 +5,6 @@ from rest_framework import status
from enum import Enum
class ROLE(Enum):
ADMIN = 20
MEMBER = 15
@@ -16,6 +15,7 @@ def allow_permission(allowed_roles, level="PROJECT", creator=False, model=None):
def decorator(view_func):
@wraps(view_func)
def _wrapped_view(instance, request, *args, **kwargs):
# Check for creator if required
if creator and model:
obj = model.objects.filter(
@@ -26,7 +26,8 @@ def allow_permission(allowed_roles, level="PROJECT", creator=False, model=None):
# Convert allowed_roles to their values if they are enum members
allowed_role_values = [
role.value if isinstance(role, ROLE) else role for role in allowed_roles
role.value if isinstance(role, ROLE) else role
for role in allowed_roles
]
# Check role permissions
+6 -2
View File
@@ -18,7 +18,9 @@ class ProjectBasePermission(BasePermission):
## Safe Methods -> Handle the filtering logic in queryset
if request.method in SAFE_METHODS:
return WorkspaceMember.objects.filter(
workspace__slug=view.workspace_slug, member=request.user, is_active=True
workspace__slug=view.workspace_slug,
member=request.user,
is_active=True,
).exists()
## Only workspace owners or admins can create the projects
@@ -48,7 +50,9 @@ class ProjectMemberPermission(BasePermission):
## Safe Methods -> Handle the filtering logic in queryset
if request.method in SAFE_METHODS:
return ProjectMember.objects.filter(
workspace__slug=view.workspace_slug, member=request.user, is_active=True
workspace__slug=view.workspace_slug,
member=request.user,
is_active=True,
).exists()
## Only workspace owners or admins can create the projects
if request.method == "POST":
+12 -4
View File
@@ -50,7 +50,9 @@ class WorkspaceOwnerPermission(BasePermission):
return False
return WorkspaceMember.objects.filter(
workspace__slug=view.workspace_slug, member=request.user, role=Admin
workspace__slug=view.workspace_slug,
member=request.user,
role=Admin,
).exists()
@@ -75,7 +77,9 @@ class WorkspaceEntityPermission(BasePermission):
## Safe Methods -> Handle the filtering logic in queryset
if request.method in SAFE_METHODS:
return WorkspaceMember.objects.filter(
workspace__slug=view.workspace_slug, member=request.user, is_active=True
workspace__slug=view.workspace_slug,
member=request.user,
is_active=True,
).exists()
return WorkspaceMember.objects.filter(
@@ -92,7 +96,9 @@ class WorkspaceViewerPermission(BasePermission):
return False
return WorkspaceMember.objects.filter(
member=request.user, workspace__slug=view.workspace_slug, is_active=True
member=request.user,
workspace__slug=view.workspace_slug,
is_active=True,
).exists()
@@ -102,5 +108,7 @@ class WorkspaceUserPermission(BasePermission):
return False
return WorkspaceMember.objects.filter(
member=request.user, workspace__slug=view.workspace_slug, is_active=True
member=request.user,
workspace__slug=view.workspace_slug,
is_active=True,
).exists()
+14 -9
View File
@@ -36,7 +36,9 @@ from .project import (
ProjectMemberRoleSerializer,
)
from .state import StateSerializer, StateLiteSerializer
from .view import IssueViewSerializer
from .view import (
IssueViewSerializer,
)
from .cycle import (
CycleSerializer,
CycleIssueSerializer,
@@ -55,7 +57,7 @@ from .issue import (
IssueFlatSerializer,
IssueStateSerializer,
IssueLinkSerializer,
IssueIntakeSerializer,
IssueInboxSerializer,
IssueLiteSerializer,
IssueAttachmentSerializer,
IssueSubscriberSerializer,
@@ -100,17 +102,20 @@ from .estimate import (
WorkspaceEstimateSerializer,
)
from .intake import (
IntakeSerializer,
IntakeIssueSerializer,
IssueStateIntakeSerializer,
IntakeIssueLiteSerializer,
IntakeIssueDetailSerializer,
from .inbox import (
InboxSerializer,
InboxIssueSerializer,
IssueStateInboxSerializer,
InboxIssueLiteSerializer,
InboxIssueDetailSerializer,
)
from .analytic import AnalyticViewSerializer
from .notification import NotificationSerializer, UserNotificationPreferenceSerializer
from .notification import (
NotificationSerializer,
UserNotificationPreferenceSerializer,
)
from .exporter import ExporterHistorySerializer
+4 -1
View File
@@ -7,7 +7,10 @@ class AnalyticViewSerializer(BaseSerializer):
class Meta:
model = AnalyticView
fields = "__all__"
read_only_fields = ["workspace", "query"]
read_only_fields = [
"workspace",
"query",
]
def create(self, validated_data):
query_params = validated_data.get("query_dict", {})
+6 -1
View File
@@ -6,4 +6,9 @@ class FileAssetSerializer(BaseSerializer):
class Meta:
model = FileAsset
fields = "__all__"
read_only_fields = ["created_by", "updated_by", "created_at", "updated_at"]
read_only_fields = [
"created_by",
"updated_by",
"created_at",
"updated_at",
]
+52 -51
View File
@@ -49,47 +49,44 @@ class DynamicBaseSerializer(BaseSerializer):
allowed.append(list(item.keys())[0])
for field in allowed:
if field not in self.fields:
from . import (
WorkspaceLiteSerializer,
ProjectLiteSerializer,
UserLiteSerializer,
StateLiteSerializer,
IssueSerializer,
LabelSerializer,
CycleIssueSerializer,
IssueLiteSerializer,
IssueRelationSerializer,
IntakeIssueLiteSerializer,
IssueReactionLiteSerializer,
IssueLinkLiteSerializer,
RelatedIssueSerializer,
)
from . import (
WorkspaceLiteSerializer,
ProjectLiteSerializer,
UserLiteSerializer,
StateLiteSerializer,
IssueSerializer,
LabelSerializer,
CycleIssueSerializer,
IssueLiteSerializer,
IssueRelationSerializer,
InboxIssueLiteSerializer,
IssueReactionLiteSerializer,
IssueLinkLiteSerializer,
)
# Expansion mapper
expansion = {
"user": UserLiteSerializer,
"workspace": WorkspaceLiteSerializer,
"project": ProjectLiteSerializer,
"default_assignee": UserLiteSerializer,
"project_lead": UserLiteSerializer,
"state": StateLiteSerializer,
"created_by": UserLiteSerializer,
"issue": IssueSerializer,
"actor": UserLiteSerializer,
"owned_by": UserLiteSerializer,
"members": UserLiteSerializer,
"assignees": UserLiteSerializer,
"labels": LabelSerializer,
"issue_cycle": CycleIssueSerializer,
"parent": IssueLiteSerializer,
"issue_relation": IssueRelationSerializer,
"issue_intake": IntakeIssueLiteSerializer,
"issue_related": RelatedIssueSerializer,
"issue_reactions": IssueReactionLiteSerializer,
"issue_link": IssueLinkLiteSerializer,
"sub_issues": IssueLiteSerializer,
}
# Expansion mapper
expansion = {
"user": UserLiteSerializer,
"workspace": WorkspaceLiteSerializer,
"project": ProjectLiteSerializer,
"default_assignee": UserLiteSerializer,
"project_lead": UserLiteSerializer,
"state": StateLiteSerializer,
"created_by": UserLiteSerializer,
"issue": IssueSerializer,
"actor": UserLiteSerializer,
"owned_by": UserLiteSerializer,
"members": UserLiteSerializer,
"assignees": UserLiteSerializer,
"labels": LabelSerializer,
"issue_cycle": CycleIssueSerializer,
"parent": IssueLiteSerializer,
"issue_relation": IssueRelationSerializer,
"issue_inbox": InboxIssueLiteSerializer,
"issue_reactions": IssueReactionLiteSerializer,
"issue_link": IssueLinkLiteSerializer,
"sub_issues": IssueLiteSerializer,
}
if field not in self.fields and field in expansion:
self.fields[field] = expansion[field](
@@ -102,12 +99,11 @@ class DynamicBaseSerializer(BaseSerializer):
"labels",
"issue_cycle",
"issue_relation",
"issue_intake",
"issue_inbox",
"issue_reactions",
"issue_attachment",
"issue_link",
"sub_issues",
"issue_related",
]
else False
)
@@ -132,12 +128,11 @@ class DynamicBaseSerializer(BaseSerializer):
LabelSerializer,
CycleIssueSerializer,
IssueRelationSerializer,
IntakeIssueLiteSerializer,
InboxIssueLiteSerializer,
IssueLiteSerializer,
IssueReactionLiteSerializer,
IssueAttachmentLiteSerializer,
IssueLinkLiteSerializer,
RelatedIssueSerializer,
)
# Expansion mapper
@@ -158,8 +153,7 @@ class DynamicBaseSerializer(BaseSerializer):
"issue_cycle": CycleIssueSerializer,
"parent": IssueLiteSerializer,
"issue_relation": IssueRelationSerializer,
"issue_intake": IntakeIssueLiteSerializer,
"issue_related": RelatedIssueSerializer,
"issue_inbox": InboxIssueLiteSerializer,
"issue_reactions": IssueReactionLiteSerializer,
"issue_attachment": IssueAttachmentLiteSerializer,
"issue_link": IssueLinkLiteSerializer,
@@ -178,10 +172,15 @@ class DynamicBaseSerializer(BaseSerializer):
response[expand] = exp_serializer.data
else:
# You might need to handle this case differently
response[expand] = getattr(instance, f"{expand}_id", None)
response[expand] = getattr(
instance, f"{expand}_id", None
)
# Check if issue_attachments is in fields or expand
if "issue_attachments" in self.fields or "issue_attachments" in self.expand:
if (
"issue_attachments" in self.fields
or "issue_attachments" in self.expand
):
# Import the model here to avoid circular imports
from plane.db.models import FileAsset
@@ -194,9 +193,11 @@ class DynamicBaseSerializer(BaseSerializer):
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
)
# Serialize issue_attachments and add them to the response
response["issue_attachments"] = IssueAttachmentLiteSerializer(
issue_attachments, many=True
).data
response["issue_attachments"] = (
IssueAttachmentLiteSerializer(
issue_attachments, many=True
).data
)
else:
response["issue_attachments"] = []
+24 -6
View File
@@ -4,7 +4,11 @@ from rest_framework import serializers
# Module imports
from .base import BaseSerializer
from .issue import IssueStateSerializer
from plane.db.models import Cycle, CycleIssue, CycleUserProperties
from plane.db.models import (
Cycle,
CycleIssue,
CycleUserProperties,
)
class CycleWriteSerializer(BaseSerializer):
@@ -14,13 +18,20 @@ class CycleWriteSerializer(BaseSerializer):
and data.get("end_date", None) is not None
and data.get("start_date", None) > data.get("end_date", None)
):
raise serializers.ValidationError("Start date cannot exceed end date")
raise serializers.ValidationError(
"Start date cannot exceed end date"
)
return data
class Meta:
model = Cycle
fields = "__all__"
read_only_fields = ["workspace", "project", "owned_by", "archived_at"]
read_only_fields = [
"workspace",
"project",
"owned_by",
"archived_at",
]
class CycleSerializer(BaseSerializer):
@@ -76,11 +87,18 @@ class CycleIssueSerializer(BaseSerializer):
class Meta:
model = CycleIssue
fields = "__all__"
read_only_fields = ["workspace", "project", "cycle"]
read_only_fields = [
"workspace",
"project",
"cycle",
]
class CycleUserPropertiesSerializer(BaseSerializer):
class Meta:
model = CycleUserProperties
fields = "__all__"
read_only_fields = ["workspace", "project", "cycle" "user"]
read_only_fields = [
"workspace",
"project",
"cycle" "user",
]
+29 -8
View File
@@ -22,10 +22,16 @@ from plane.db.models import (
class DraftIssueCreateSerializer(BaseSerializer):
# ids
state_id = serializers.PrimaryKeyRelatedField(
source="state", queryset=State.objects.all(), required=False, allow_null=True
source="state",
queryset=State.objects.all(),
required=False,
allow_null=True,
)
parent_id = serializers.PrimaryKeyRelatedField(
source="parent", queryset=Issue.objects.all(), required=False, allow_null=True
source="parent",
queryset=Issue.objects.all(),
required=False,
allow_null=True,
)
label_ids = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
@@ -63,7 +69,9 @@ class DraftIssueCreateSerializer(BaseSerializer):
and data.get("target_date", None) is not None
and data.get("start_date", None) > data.get("target_date", None)
):
raise serializers.ValidationError("Start date cannot exceed target date")
raise serializers.ValidationError(
"Start date cannot exceed target date"
)
return data
def create(self, validated_data):
@@ -78,7 +86,9 @@ class DraftIssueCreateSerializer(BaseSerializer):
# Create Issue
issue = DraftIssue.objects.create(
**validated_data, workspace_id=workspace_id, project_id=project_id
**validated_data,
workspace_id=workspace_id,
project_id=project_id,
)
# Issue Audit Users
@@ -229,11 +239,20 @@ class DraftIssueCreateSerializer(BaseSerializer):
class DraftIssueSerializer(BaseSerializer):
# ids
cycle_id = serializers.PrimaryKeyRelatedField(read_only=True)
module_ids = serializers.ListField(child=serializers.UUIDField(), required=False)
module_ids = serializers.ListField(
child=serializers.UUIDField(),
required=False,
)
# Many to many
label_ids = serializers.ListField(child=serializers.UUIDField(), required=False)
assignee_ids = serializers.ListField(child=serializers.UUIDField(), required=False)
label_ids = serializers.ListField(
child=serializers.UUIDField(),
required=False,
)
assignee_ids = serializers.ListField(
child=serializers.UUIDField(),
required=False,
)
class Meta:
model = DraftIssue
@@ -267,5 +286,7 @@ class DraftIssueDetailSerializer(DraftIssueSerializer):
description_html = serializers.CharField()
class Meta(DraftIssueSerializer.Meta):
fields = DraftIssueSerializer.Meta.fields + ["description_html"]
fields = DraftIssueSerializer.Meta.fields + [
"description_html",
]
read_only_fields = fields
+23 -5
View File
@@ -7,10 +7,14 @@ from rest_framework import serializers
class EstimateSerializer(BaseSerializer):
class Meta:
model = Estimate
fields = "__all__"
read_only_fields = ["workspace", "project"]
read_only_fields = [
"workspace",
"project",
]
class EstimatePointSerializer(BaseSerializer):
@@ -19,13 +23,19 @@ class EstimatePointSerializer(BaseSerializer):
raise serializers.ValidationError("Estimate points are required")
value = data.get("value")
if value and len(value) > 20:
raise serializers.ValidationError("Value can't be more than 20 characters")
raise serializers.ValidationError(
"Value can't be more than 20 characters"
)
return data
class Meta:
model = EstimatePoint
fields = "__all__"
read_only_fields = ["estimate", "workspace", "project"]
read_only_fields = [
"estimate",
"workspace",
"project",
]
class EstimateReadSerializer(BaseSerializer):
@@ -34,7 +44,11 @@ class EstimateReadSerializer(BaseSerializer):
class Meta:
model = Estimate
fields = "__all__"
read_only_fields = ["points", "name", "description"]
read_only_fields = [
"points",
"name",
"description",
]
class WorkspaceEstimateSerializer(BaseSerializer):
@@ -43,4 +57,8 @@ class WorkspaceEstimateSerializer(BaseSerializer):
class Meta:
model = Estimate
fields = "__all__"
read_only_fields = ["points", "name", "description"]
read_only_fields = [
"points",
"name",
"description",
]
+3 -1
View File
@@ -5,7 +5,9 @@ from .user import UserLiteSerializer
class ExporterHistorySerializer(BaseSerializer):
initiated_by_detail = UserLiteSerializer(source="initiated_by", read_only=True)
initiated_by_detail = UserLiteSerializer(
source="initiated_by", read_only=True
)
class Meta:
model = ExporterHistory
+16 -2
View File
@@ -1,9 +1,18 @@
from rest_framework import serializers
from plane.db.models import UserFavorite, Cycle, Module, Issue, IssueView, Page, Project
from plane.db.models import (
UserFavorite,
Cycle,
Module,
Issue,
IssueView,
Page,
Project,
)
class ProjectFavoriteLiteSerializer(serializers.ModelSerializer):
class Meta:
model = Project
fields = ["id", "name", "logo_props"]
@@ -24,18 +33,21 @@ class PageFavoriteLiteSerializer(serializers.ModelSerializer):
class CycleFavoriteLiteSerializer(serializers.ModelSerializer):
class Meta:
model = Cycle
fields = ["id", "name", "logo_props", "project_id"]
class ModuleFavoriteLiteSerializer(serializers.ModelSerializer):
class Meta:
model = Module
fields = ["id", "name", "logo_props", "project_id"]
class ViewFavoriteSerializer(serializers.ModelSerializer):
class Meta:
model = IssueView
fields = ["id", "name", "logo_props", "project_id"]
@@ -77,7 +89,9 @@ class UserFavoriteSerializer(serializers.ModelSerializer):
entity_type = obj.entity_type
entity_identifier = obj.entity_identifier
entity_model, entity_serializer = get_entity_model_and_serializer(entity_type)
entity_model, entity_serializer = get_entity_model_and_serializer(
entity_type
)
if entity_model and entity_serializer:
try:
entity = entity_model.objects.get(pk=entity_identifier)
+6 -2
View File
@@ -7,9 +7,13 @@ from plane.db.models import Importer
class ImporterSerializer(BaseSerializer):
initiated_by_detail = UserLiteSerializer(source="initiated_by", read_only=True)
initiated_by_detail = UserLiteSerializer(
source="initiated_by", read_only=True
)
project_detail = ProjectLiteSerializer(source="project", read_only=True)
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
workspace_detail = WorkspaceLiteSerializer(
source="workspace", read_only=True
)
class Meta:
model = Importer
@@ -3,28 +3,35 @@ from rest_framework import serializers
# Module imports
from .base import BaseSerializer
from .issue import IssueIntakeSerializer, LabelLiteSerializer, IssueDetailSerializer
from .issue import (
IssueInboxSerializer,
LabelLiteSerializer,
IssueDetailSerializer,
)
from .project import ProjectLiteSerializer
from .state import StateLiteSerializer
from .user import UserLiteSerializer
from plane.db.models import Intake, IntakeIssue, Issue
from plane.db.models import Inbox, InboxIssue, Issue
class IntakeSerializer(BaseSerializer):
class InboxSerializer(BaseSerializer):
project_detail = ProjectLiteSerializer(source="project", read_only=True)
pending_issue_count = serializers.IntegerField(read_only=True)
class Meta:
model = Intake
model = Inbox
fields = "__all__"
read_only_fields = ["project", "workspace"]
read_only_fields = [
"project",
"workspace",
]
class IntakeIssueSerializer(BaseSerializer):
issue = IssueIntakeSerializer(read_only=True)
class InboxIssueSerializer(BaseSerializer):
issue = IssueInboxSerializer(read_only=True)
class Meta:
model = IntakeIssue
model = InboxIssue
fields = [
"id",
"status",
@@ -34,7 +41,10 @@ class IntakeIssueSerializer(BaseSerializer):
"issue",
"created_by",
]
read_only_fields = ["project", "workspace"]
read_only_fields = [
"project",
"workspace",
]
def to_representation(self, instance):
# Pass the annotated fields to the Issue instance if they exist
@@ -43,14 +53,14 @@ class IntakeIssueSerializer(BaseSerializer):
return super().to_representation(instance)
class IntakeIssueDetailSerializer(BaseSerializer):
class InboxIssueDetailSerializer(BaseSerializer):
issue = IssueDetailSerializer(read_only=True)
duplicate_issue_detail = IssueIntakeSerializer(
duplicate_issue_detail = IssueInboxSerializer(
read_only=True, source="duplicate_to"
)
class Meta:
model = IntakeIssue
model = InboxIssue
fields = [
"id",
"status",
@@ -60,7 +70,10 @@ class IntakeIssueDetailSerializer(BaseSerializer):
"source",
"issue",
]
read_only_fields = ["project", "workspace"]
read_only_fields = [
"project",
"workspace",
]
def to_representation(self, instance):
# Pass the annotated fields to the Issue instance if they exist
@@ -72,20 +85,24 @@ class IntakeIssueDetailSerializer(BaseSerializer):
return super().to_representation(instance)
class IntakeIssueLiteSerializer(BaseSerializer):
class InboxIssueLiteSerializer(BaseSerializer):
class Meta:
model = IntakeIssue
model = InboxIssue
fields = ["id", "status", "duplicate_to", "snoozed_till", "source"]
read_only_fields = fields
class IssueStateIntakeSerializer(BaseSerializer):
class IssueStateInboxSerializer(BaseSerializer):
state_detail = StateLiteSerializer(read_only=True, source="state")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
label_details = LabelLiteSerializer(
read_only=True, source="labels", many=True
)
assignee_details = UserLiteSerializer(
read_only=True, source="assignees", many=True
)
sub_issues_count = serializers.IntegerField(read_only=True)
issue_intake = IntakeIssueLiteSerializer(read_only=True, many=True)
issue_inbox = InboxIssueLiteSerializer(read_only=True, many=True)
class Meta:
model = Issue
+147 -35
View File
@@ -60,7 +60,12 @@ class IssueProjectLiteSerializer(BaseSerializer):
class Meta:
model = Issue
fields = ["id", "project_detail", "name", "sequence_id"]
fields = [
"id",
"project_detail",
"name",
"sequence_id",
]
read_only_fields = fields
@@ -69,10 +74,16 @@ class IssueProjectLiteSerializer(BaseSerializer):
class IssueCreateSerializer(BaseSerializer):
# ids
state_id = serializers.PrimaryKeyRelatedField(
source="state", queryset=State.objects.all(), required=False, allow_null=True
source="state",
queryset=State.objects.all(),
required=False,
allow_null=True,
)
parent_id = serializers.PrimaryKeyRelatedField(
source="parent", queryset=Issue.objects.all(), required=False, allow_null=True
source="parent",
queryset=Issue.objects.all(),
required=False,
allow_null=True,
)
label_ids = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
@@ -84,8 +95,6 @@ class IssueCreateSerializer(BaseSerializer):
write_only=True,
required=False,
)
project_id = serializers.UUIDField(source="project.id", read_only=True)
workspace_id = serializers.UUIDField(source="workspace.id", read_only=True)
class Meta:
model = Issue
@@ -113,7 +122,9 @@ class IssueCreateSerializer(BaseSerializer):
and data.get("target_date", None) is not None
and data.get("start_date", None) > data.get("target_date", None)
):
raise serializers.ValidationError("Start date cannot exceed target date")
raise serializers.ValidationError(
"Start date cannot exceed target date"
)
return data
def create(self, validated_data):
@@ -125,7 +136,10 @@ class IssueCreateSerializer(BaseSerializer):
default_assignee_id = self.context["default_assignee_id"]
# Create Issue
issue = Issue.objects.create(**validated_data, project_id=project_id)
issue = Issue.objects.create(
**validated_data,
project_id=project_id,
)
# Issue Audit Users
created_by_id = issue.created_by_id
@@ -229,7 +243,9 @@ class IssueActivitySerializer(BaseSerializer):
actor_detail = UserLiteSerializer(read_only=True, source="actor")
issue_detail = IssueFlatSerializer(read_only=True, source="issue")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
workspace_detail = WorkspaceLiteSerializer(
read_only=True, source="workspace"
)
class Meta:
model = IssueActivity
@@ -240,7 +256,11 @@ class IssueUserPropertySerializer(BaseSerializer):
class Meta:
model = IssueUserProperty
fields = "__all__"
read_only_fields = ["user", "workspace", "project"]
read_only_fields = [
"user",
"workspace",
"project",
]
class LabelSerializer(BaseSerializer):
@@ -255,20 +275,30 @@ class LabelSerializer(BaseSerializer):
"workspace_id",
"sort_order",
]
read_only_fields = ["workspace", "project"]
read_only_fields = [
"workspace",
"project",
]
class LabelLiteSerializer(BaseSerializer):
class Meta:
model = Label
fields = ["id", "name", "color"]
fields = [
"id",
"name",
"color",
]
class IssueLabelSerializer(BaseSerializer):
class Meta:
model = IssueLabel
fields = "__all__"
read_only_fields = ["workspace", "project"]
read_only_fields = [
"workspace",
"project",
]
class IssueRelationSerializer(BaseSerializer):
@@ -284,8 +314,17 @@ class IssueRelationSerializer(BaseSerializer):
class Meta:
model = IssueRelation
fields = ["id", "project_id", "sequence_id", "relation_type", "name"]
read_only_fields = ["workspace", "project"]
fields = [
"id",
"project_id",
"sequence_id",
"relation_type",
"name",
]
read_only_fields = [
"workspace",
"project",
]
class RelatedIssueSerializer(BaseSerializer):
@@ -293,14 +332,25 @@ class RelatedIssueSerializer(BaseSerializer):
project_id = serializers.PrimaryKeyRelatedField(
source="issue.project_id", read_only=True
)
sequence_id = serializers.IntegerField(source="issue.sequence_id", read_only=True)
sequence_id = serializers.IntegerField(
source="issue.sequence_id", read_only=True
)
name = serializers.CharField(source="issue.name", read_only=True)
relation_type = serializers.CharField(read_only=True)
class Meta:
model = IssueRelation
fields = ["id", "project_id", "sequence_id", "relation_type", "name"]
read_only_fields = ["workspace", "project"]
fields = [
"id",
"project_id",
"sequence_id",
"relation_type",
"name",
]
read_only_fields = [
"workspace",
"project",
]
class IssueAssigneeSerializer(BaseSerializer):
@@ -408,7 +458,8 @@ class IssueLinkSerializer(BaseSerializer):
# Validation if url already exists
def create(self, validated_data):
if IssueLink.objects.filter(
url=validated_data.get("url"), issue_id=validated_data.get("issue_id")
url=validated_data.get("url"),
issue_id=validated_data.get("issue_id"),
).exists():
raise serializers.ValidationError(
{"error": "URL already exists for this Issue"}
@@ -418,7 +469,8 @@ class IssueLinkSerializer(BaseSerializer):
def update(self, instance, validated_data):
if (
IssueLink.objects.filter(
url=validated_data.get("url"), issue_id=instance.issue_id
url=validated_data.get("url"),
issue_id=instance.issue_id,
)
.exclude(pk=instance.id)
.exists()
@@ -446,6 +498,7 @@ class IssueLinkLiteSerializer(BaseSerializer):
class IssueAttachmentSerializer(BaseSerializer):
asset_url = serializers.CharField(read_only=True)
class Meta:
@@ -483,20 +536,37 @@ class IssueReactionSerializer(BaseSerializer):
class Meta:
model = IssueReaction
fields = "__all__"
read_only_fields = ["workspace", "project", "issue", "actor", "deleted_at"]
read_only_fields = [
"workspace",
"project",
"issue",
"actor",
"deleted_at",
]
class IssueReactionLiteSerializer(DynamicBaseSerializer):
class Meta:
model = IssueReaction
fields = ["id", "actor", "issue", "reaction"]
fields = [
"id",
"actor",
"issue",
"reaction",
]
class CommentReactionSerializer(BaseSerializer):
class Meta:
model = CommentReaction
fields = "__all__"
read_only_fields = ["workspace", "project", "comment", "actor", "deleted_at"]
read_only_fields = [
"workspace",
"project",
"comment",
"actor",
"deleted_at",
]
class IssueVoteSerializer(BaseSerializer):
@@ -504,7 +574,14 @@ class IssueVoteSerializer(BaseSerializer):
class Meta:
model = IssueVote
fields = ["issue", "vote", "workspace", "project", "actor", "actor_detail"]
fields = [
"issue",
"vote",
"workspace",
"project",
"actor",
"actor_detail",
]
read_only_fields = fields
@@ -512,7 +589,9 @@ class IssueCommentSerializer(BaseSerializer):
actor_detail = UserLiteSerializer(read_only=True, source="actor")
issue_detail = IssueFlatSerializer(read_only=True, source="issue")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
workspace_detail = WorkspaceLiteSerializer(
read_only=True, source="workspace"
)
comment_reactions = CommentReactionSerializer(read_only=True, many=True)
is_member = serializers.BooleanField(read_only=True)
@@ -536,15 +615,25 @@ class IssueStateFlatSerializer(BaseSerializer):
class Meta:
model = Issue
fields = ["id", "sequence_id", "name", "state_detail", "project_detail"]
fields = [
"id",
"sequence_id",
"name",
"state_detail",
"project_detail",
]
# Issue Serializer with state details
class IssueStateSerializer(DynamicBaseSerializer):
label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
label_details = LabelLiteSerializer(
read_only=True, source="labels", many=True
)
state_detail = StateLiteSerializer(read_only=True, source="state")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
assignee_details = UserLiteSerializer(
read_only=True, source="assignees", many=True
)
sub_issues_count = serializers.IntegerField(read_only=True)
attachment_count = serializers.IntegerField(read_only=True)
link_count = serializers.IntegerField(read_only=True)
@@ -554,8 +643,11 @@ class IssueStateSerializer(DynamicBaseSerializer):
fields = "__all__"
class IssueIntakeSerializer(DynamicBaseSerializer):
label_ids = serializers.ListField(child=serializers.UUIDField(), required=False)
class IssueInboxSerializer(DynamicBaseSerializer):
label_ids = serializers.ListField(
child=serializers.UUIDField(),
required=False,
)
class Meta:
model = Issue
@@ -575,11 +667,20 @@ class IssueIntakeSerializer(DynamicBaseSerializer):
class IssueSerializer(DynamicBaseSerializer):
# ids
cycle_id = serializers.PrimaryKeyRelatedField(read_only=True)
module_ids = serializers.ListField(child=serializers.UUIDField(), required=False)
module_ids = serializers.ListField(
child=serializers.UUIDField(),
required=False,
)
# Many to many
label_ids = serializers.ListField(child=serializers.UUIDField(), required=False)
assignee_ids = serializers.ListField(child=serializers.UUIDField(), required=False)
label_ids = serializers.ListField(
child=serializers.UUIDField(),
required=False,
)
assignee_ids = serializers.ListField(
child=serializers.UUIDField(),
required=False,
)
# Count items
sub_issues_count = serializers.IntegerField(read_only=True)
@@ -621,7 +722,11 @@ class IssueSerializer(DynamicBaseSerializer):
class IssueLiteSerializer(DynamicBaseSerializer):
class Meta:
model = Issue
fields = ["id", "sequence_id", "project_id"]
fields = [
"id",
"sequence_id",
"project_id",
]
read_only_fields = fields
@@ -630,7 +735,10 @@ class IssueDetailSerializer(IssueSerializer):
is_subscribed = serializers.BooleanField(read_only=True)
class Meta(IssueSerializer.Meta):
fields = IssueSerializer.Meta.fields + ["description_html", "is_subscribed"]
fields = IssueSerializer.Meta.fields + [
"description_html",
"is_subscribed",
]
read_only_fields = fields
@@ -666,4 +774,8 @@ class IssueSubscriberSerializer(BaseSerializer):
class Meta:
model = IssueSubscriber
fields = "__all__"
read_only_fields = ["workspace", "project", "issue"]
read_only_fields = [
"workspace",
"project",
"issue",
]
+20 -7
View File
@@ -21,7 +21,10 @@ from plane.db.models import (
class ModuleWriteSerializer(BaseSerializer):
lead_id = serializers.PrimaryKeyRelatedField(
source="lead", queryset=User.objects.all(), required=False, allow_null=True
source="lead",
queryset=User.objects.all(),
required=False,
allow_null=True,
)
member_ids = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
@@ -45,7 +48,9 @@ class ModuleWriteSerializer(BaseSerializer):
def to_representation(self, instance):
data = super().to_representation(instance)
data["member_ids"] = [str(member.id) for member in instance.members.all()]
data["member_ids"] = [
str(member.id) for member in instance.members.all()
]
return data
def validate(self, data):
@@ -54,7 +59,9 @@ class ModuleWriteSerializer(BaseSerializer):
and data.get("target_date", None) is not None
and data.get("start_date", None) > data.get("target_date", None)
):
raise serializers.ValidationError("Start date cannot exceed target date")
raise serializers.ValidationError(
"Start date cannot exceed target date"
)
return data
def create(self, validated_data):
@@ -64,7 +71,9 @@ class ModuleWriteSerializer(BaseSerializer):
module_name = validated_data.get("name")
if module_name:
# Lookup for the module name in the module table for that project
if Module.objects.filter(name=module_name, project=project).exists():
if Module.objects.filter(
name=module_name, project=project
).exists():
raise serializers.ValidationError(
{"error": "Module with this name already exists"}
)
@@ -95,7 +104,9 @@ class ModuleWriteSerializer(BaseSerializer):
if module_name:
# Lookup for the module name in the module table for that project
if (
Module.objects.filter(name=module_name, project=instance.project)
Module.objects.filter(
name=module_name, project=instance.project
)
.exclude(id=instance.id)
.exists()
):
@@ -192,7 +203,8 @@ class ModuleLinkSerializer(BaseSerializer):
def create(self, validated_data):
validated_data["url"] = self.validate_url(validated_data.get("url"))
if ModuleLink.objects.filter(
url=validated_data.get("url"), module_id=validated_data.get("module_id")
url=validated_data.get("url"),
module_id=validated_data.get("module_id"),
).exists():
raise serializers.ValidationError({"error": "URL already exists."})
return super().create(validated_data)
@@ -201,7 +213,8 @@ class ModuleLinkSerializer(BaseSerializer):
validated_data["url"] = self.validate_url(validated_data.get("url"))
if (
ModuleLink.objects.filter(
url=validated_data.get("url"), module_id=instance.module_id
url=validated_data.get("url"),
module_id=instance.module_id,
)
.exclude(pk=instance.id)
.exists()
@@ -8,9 +8,10 @@ from rest_framework import serializers
class NotificationSerializer(BaseSerializer):
triggered_by_details = UserLiteSerializer(read_only=True, source="triggered_by")
triggered_by_details = UserLiteSerializer(
read_only=True, source="triggered_by"
)
is_inbox_issue = serializers.BooleanField(read_only=True)
is_intake_issue = serializers.BooleanField(read_only=True)
is_mentioned_notification = serializers.BooleanField(read_only=True)
class Meta:
+31 -8
View File
@@ -22,8 +22,14 @@ class PageSerializer(BaseSerializer):
required=False,
)
# Many to many
label_ids = serializers.ListField(child=serializers.UUIDField(), required=False)
project_ids = serializers.ListField(child=serializers.UUIDField(), required=False)
label_ids = serializers.ListField(
child=serializers.UUIDField(),
required=False,
)
project_ids = serializers.ListField(
child=serializers.UUIDField(),
required=False,
)
class Meta:
model = Page
@@ -48,7 +54,10 @@ class PageSerializer(BaseSerializer):
"label_ids",
"project_ids",
]
read_only_fields = ["workspace", "owned_by"]
read_only_fields = [
"workspace",
"owned_by",
]
def create(self, validated_data):
labels = validated_data.pop("labels", None)
@@ -118,7 +127,9 @@ class PageDetailSerializer(PageSerializer):
description_html = serializers.CharField()
class Meta(PageSerializer.Meta):
fields = PageSerializer.Meta.fields + ["description_html"]
fields = PageSerializer.Meta.fields + [
"description_html",
]
class SubPageSerializer(BaseSerializer):
@@ -127,7 +138,10 @@ class SubPageSerializer(BaseSerializer):
class Meta:
model = PageLog
fields = "__all__"
read_only_fields = ["workspace", "page"]
read_only_fields = [
"workspace",
"page",
]
def get_entity_details(self, obj):
entity_name = obj.entity_name
@@ -144,7 +158,10 @@ class PageLogSerializer(BaseSerializer):
class Meta:
model = PageLog
fields = "__all__"
read_only_fields = ["workspace", "page"]
read_only_fields = [
"workspace",
"page",
]
class PageVersionSerializer(BaseSerializer):
@@ -161,7 +178,10 @@ class PageVersionSerializer(BaseSerializer):
"created_by",
"updated_by",
]
read_only_fields = ["workspace", "page"]
read_only_fields = [
"workspace",
"page",
]
class PageVersionDetailSerializer(BaseSerializer):
@@ -181,4 +201,7 @@ class PageVersionDetailSerializer(BaseSerializer):
"created_by",
"updated_by",
]
read_only_fields = ["workspace", "page"]
read_only_fields = [
"workspace",
"page",
]
+33 -11
View File
@@ -4,7 +4,10 @@ from rest_framework import serializers
# Module imports
from .base import BaseSerializer, DynamicBaseSerializer
from plane.app.serializers.workspace import WorkspaceLiteSerializer
from plane.app.serializers.user import UserLiteSerializer, UserAdminLiteSerializer
from plane.app.serializers.user import (
UserLiteSerializer,
UserAdminLiteSerializer,
)
from plane.db.models import (
Project,
ProjectMember,
@@ -16,23 +19,31 @@ from plane.db.models import (
class ProjectSerializer(BaseSerializer):
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
inbox_view = serializers.BooleanField(read_only=True, source="intake_view")
workspace_detail = WorkspaceLiteSerializer(
source="workspace", read_only=True
)
class Meta:
model = Project
fields = "__all__"
read_only_fields = ["workspace", "deleted_at"]
read_only_fields = [
"workspace",
"deleted_at",
]
def create(self, validated_data):
identifier = validated_data.get("identifier", "").strip().upper()
if identifier == "":
raise serializers.ValidationError(detail="Project Identifier is required")
raise serializers.ValidationError(
detail="Project Identifier is required"
)
if ProjectIdentifier.objects.filter(
name=identifier, workspace_id=self.context["workspace_id"]
).exists():
raise serializers.ValidationError(detail="Project Identifier is taken")
raise serializers.ValidationError(
detail="Project Identifier is taken"
)
project = Project.objects.create(
**validated_data, workspace_id=self.context["workspace_id"]
)
@@ -71,7 +82,9 @@ class ProjectSerializer(BaseSerializer):
return project
# If not same fail update
raise serializers.ValidationError(detail="Project Identifier is already taken")
raise serializers.ValidationError(
detail="Project Identifier is already taken"
)
class ProjectLiteSerializer(BaseSerializer):
@@ -106,7 +119,6 @@ class ProjectListSerializer(DynamicBaseSerializer):
anchor = serializers.CharField(read_only=True)
members = serializers.SerializerMethodField()
cover_image_url = serializers.CharField(read_only=True)
inbox_view = serializers.BooleanField(read_only=True, source="intake_view")
def get_members(self, obj):
project_members = getattr(obj, "members_list", None)
@@ -200,16 +212,26 @@ class ProjectMemberLiteSerializer(BaseSerializer):
class DeployBoardSerializer(BaseSerializer):
project_details = ProjectLiteSerializer(read_only=True, source="project")
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
workspace_detail = WorkspaceLiteSerializer(
read_only=True, source="workspace"
)
class Meta:
model = DeployBoard
fields = "__all__"
read_only_fields = ["workspace", "project", "anchor"]
read_only_fields = [
"workspace",
"project",
"anchor",
]
class ProjectPublicMemberSerializer(BaseSerializer):
class Meta:
model = ProjectPublicMember
fields = "__all__"
read_only_fields = ["workspace", "project", "member"]
read_only_fields = [
"workspace",
"project",
"member",
]
+10 -2
View File
@@ -19,11 +19,19 @@ class StateSerializer(BaseSerializer):
"description",
"sequence",
]
read_only_fields = ["workspace", "project"]
read_only_fields = [
"workspace",
"project",
]
class StateLiteSerializer(BaseSerializer):
class Meta:
model = State
fields = ["id", "name", "color", "group"]
fields = [
"id",
"name",
"color",
"group",
]
read_only_fields = fields
+43 -11
View File
@@ -2,7 +2,13 @@
from rest_framework import serializers
# Module import
from plane.db.models import Account, Profile, User, Workspace, WorkspaceMemberInvite
from plane.db.models import (
Account,
Profile,
User,
Workspace,
WorkspaceMemberInvite,
)
from .base import BaseSerializer
@@ -11,7 +17,11 @@ class UserSerializer(BaseSerializer):
class Meta:
model = User
# Exclude password field from the serializer
fields = [field.name for field in User._meta.fields if field.name != "password"]
fields = [
field.name
for field in User._meta.fields
if field.name != "password"
]
# Make all system fields and email read only
read_only_fields = [
"id",
@@ -46,6 +56,7 @@ class UserSerializer(BaseSerializer):
class UserMeSerializer(BaseSerializer):
class Meta:
model = User
fields = [
@@ -76,7 +87,11 @@ class UserMeSettingsSerializer(BaseSerializer):
class Meta:
model = User
fields = ["id", "email", "workspace"]
fields = [
"id",
"email",
"workspace",
]
read_only_fields = fields
def get_workspace(self, obj):
@@ -113,7 +128,8 @@ class UserMeSettingsSerializer(BaseSerializer):
else:
fallback_workspace = (
Workspace.objects.filter(
workspace_member__member_id=obj.id, workspace_member__is_active=True
workspace_member__member_id=obj.id,
workspace_member__is_active=True,
)
.order_by("created_at")
.first()
@@ -122,10 +138,14 @@ class UserMeSettingsSerializer(BaseSerializer):
"last_workspace_id": None,
"last_workspace_slug": None,
"fallback_workspace_id": (
fallback_workspace.id if fallback_workspace is not None else None
fallback_workspace.id
if fallback_workspace is not None
else None
),
"fallback_workspace_slug": (
fallback_workspace.slug if fallback_workspace is not None else None
fallback_workspace.slug
if fallback_workspace is not None
else None
),
"invites": workspace_invites,
}
@@ -143,7 +163,10 @@ class UserLiteSerializer(BaseSerializer):
"is_bot",
"display_name",
]
read_only_fields = ["id", "is_bot"]
read_only_fields = [
"id",
"is_bot",
]
class UserAdminLiteSerializer(BaseSerializer):
@@ -160,7 +183,10 @@ class UserAdminLiteSerializer(BaseSerializer):
"email",
"last_login_medium",
]
read_only_fields = ["id", "is_bot"]
read_only_fields = [
"id",
"is_bot",
]
class ChangePasswordSerializer(serializers.Serializer):
@@ -181,7 +207,9 @@ class ChangePasswordSerializer(serializers.Serializer):
if data.get("new_password") != data.get("confirm_password"):
raise serializers.ValidationError(
{"error": "Confirm password should be same as the new password."}
{
"error": "Confirm password should be same as the new password."
}
)
return data
@@ -199,11 +227,15 @@ class ProfileSerializer(BaseSerializer):
class Meta:
model = Profile
fields = "__all__"
read_only_fields = ["user"]
read_only_fields = [
"user",
]
class AccountSerializer(BaseSerializer):
class Meta:
model = Account
fields = "__all__"
read_only_fields = ["user"]
read_only_fields = [
"user",
]
+13 -4
View File
@@ -47,9 +47,13 @@ class WebhookSerializer(DynamicBaseSerializer):
# Additional validation for multiple request domains and their subdomains
request = self.context.get("request")
disallowed_domains = ["plane.so"] # Add your disallowed domains here
disallowed_domains = [
"plane.so",
] # Add your disallowed domains here
if request:
request_host = request.get_host().split(":")[0] # Remove port if present
request_host = request.get_host().split(":")[
0
] # Remove port if present
disallowed_domains.append(request_host)
# Check if hostname is a subdomain or exact match of any disallowed domain
@@ -95,7 +99,9 @@ class WebhookSerializer(DynamicBaseSerializer):
# Additional validation for multiple request domains and their subdomains
request = self.context.get("request")
disallowed_domains = ["plane.so"] # Add your disallowed domains here
disallowed_domains = [
"plane.so",
] # Add your disallowed domains here
if request:
request_host = request.get_host().split(":")[
0
@@ -116,7 +122,10 @@ class WebhookSerializer(DynamicBaseSerializer):
class Meta:
model = Webhook
fields = "__all__"
read_only_fields = ["workspace", "secret_key"]
read_only_fields = [
"workspace",
"secret_key",
]
class WebhookLogSerializer(DynamicBaseSerializer):
+19 -6
View File
@@ -47,7 +47,11 @@ class WorkSpaceSerializer(DynamicBaseSerializer):
class WorkspaceLiteSerializer(BaseSerializer):
class Meta:
model = Workspace
fields = ["name", "slug", "id"]
fields = [
"name",
"slug",
"id",
]
read_only_fields = fields
@@ -62,7 +66,6 @@ class WorkSpaceMemberSerializer(DynamicBaseSerializer):
class WorkspaceMemberMeSerializer(BaseSerializer):
draft_issue_count = serializers.IntegerField(read_only=True)
class Meta:
model = WorkspaceMember
fields = "__all__"
@@ -98,7 +101,9 @@ class WorkSpaceMemberInviteSerializer(BaseSerializer):
class TeamSerializer(BaseSerializer):
members_detail = UserLiteSerializer(read_only=True, source="members", many=True)
members_detail = UserLiteSerializer(
read_only=True, source="members", many=True
)
members = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()),
write_only=True,
@@ -135,7 +140,9 @@ class TeamSerializer(BaseSerializer):
members = validated_data.pop("members")
TeamMember.objects.filter(team=instance).delete()
team_members = [
TeamMember(member=member, team=instance, workspace=instance.workspace)
TeamMember(
member=member, team=instance, workspace=instance.workspace
)
for member in members
]
TeamMember.objects.bulk_create(team_members, batch_size=10)
@@ -147,11 +154,17 @@ class WorkspaceThemeSerializer(BaseSerializer):
class Meta:
model = WorkspaceTheme
fields = "__all__"
read_only_fields = ["workspace", "actor"]
read_only_fields = [
"workspace",
"actor",
]
class WorkspaceUserPropertiesSerializer(BaseSerializer):
class Meta:
model = WorkspaceUserProperties
fields = "__all__"
read_only_fields = ["workspace", "user"]
read_only_fields = [
"workspace",
"user",
]
+2 -2
View File
@@ -5,7 +5,7 @@ from .cycle import urlpatterns as cycle_urls
from .dashboard import urlpatterns as dashboard_urls
from .estimate import urlpatterns as estimate_urls
from .external import urlpatterns as external_urls
from .intake import urlpatterns as intake_urls
from .inbox import urlpatterns as inbox_urls
from .issue import urlpatterns as issue_urls
from .module import urlpatterns as module_urls
from .notification import urlpatterns as notification_urls
@@ -25,7 +25,7 @@ urlpatterns = [
*dashboard_urls,
*estimate_urls,
*external_urls,
*intake_urls,
*inbox_urls,
*issue_urls,
*module_urls,
*notification_urls,
+10 -2
View File
@@ -26,7 +26,11 @@ urlpatterns = [
FileAssetEndpoint.as_view(),
name="file-assets",
),
path("users/file-assets/", UserAssetsEndpoint.as_view(), name="user-file-assets"),
path(
"users/file-assets/",
UserAssetsEndpoint.as_view(),
name="user-file-assets",
),
path(
"users/file-assets/<str:asset_key>/",
UserAssetsEndpoint.as_view(),
@@ -34,7 +38,11 @@ urlpatterns = [
),
path(
"workspaces/file-assets/<uuid:workspace_id>/<str:asset_key>/restore/",
FileAssetViewSet.as_view({"post": "restore"}),
FileAssetViewSet.as_view(
{
"post": "restore",
}
),
name="file-assets-restore",
),
# V2 Endpoints
+23 -4
View File
@@ -17,7 +17,12 @@ from plane.app.views import (
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/",
CycleViewSet.as_view({"get": "list", "post": "create"}),
CycleViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-cycle",
),
path(
@@ -34,7 +39,12 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/cycle-issues/",
CycleIssueViewSet.as_view({"get": "list", "post": "create"}),
CycleIssueViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-issue-cycle",
),
path(
@@ -56,12 +66,21 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-cycles/",
CycleFavoriteViewSet.as_view({"get": "list", "post": "create"}),
CycleFavoriteViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="user-favorite-cycle",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-cycles/<uuid:cycle_id>/",
CycleFavoriteViewSet.as_view({"delete": "destroy"}),
CycleFavoriteViewSet.as_view(
{
"delete": "destroy",
}
),
name="user-favorite-cycle",
),
path(
+22 -4
View File
@@ -16,24 +16,42 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/",
BulkEstimatePointEndpoint.as_view({"get": "list", "post": "create"}),
BulkEstimatePointEndpoint.as_view(
{
"get": "list",
"post": "create",
}
),
name="bulk-create-estimate-points",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:estimate_id>/",
BulkEstimatePointEndpoint.as_view(
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="bulk-create-estimate-points",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:estimate_id>/estimate-points/",
EstimatePointEndpoint.as_view({"post": "create"}),
EstimatePointEndpoint.as_view(
{
"post": "create",
}
),
name="estimate-points",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:estimate_id>/estimate-points/<estimate_point_id>/",
EstimatePointEndpoint.as_view({"patch": "partial_update", "delete": "destroy"}),
EstimatePointEndpoint.as_view(
{
"patch": "partial_update",
"delete": "destroy",
}
),
name="estimate-points",
),
]
+6 -2
View File
@@ -6,13 +6,17 @@ from plane.app.views import GPTIntegrationEndpoint, WorkspaceGPTIntegrationEndpo
urlpatterns = [
path("unsplash/", UnsplashEndpoint.as_view(), name="unsplash"),
path(
"unsplash/",
UnsplashEndpoint.as_view(),
name="unsplash",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/ai-assistant/",
GPTIntegrationEndpoint.as_view(),
name="importer",
),
path(
path(
"workspaces/<str:slug>/ai-assistant/",
WorkspaceGPTIntegrationEndpoint.as_view(),
name="importer",
+53
View File
@@ -0,0 +1,53 @@
from django.urls import path
from plane.app.views import (
InboxViewSet,
InboxIssueViewSet,
)
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/",
InboxViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="inbox",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/<uuid:pk>/",
InboxViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="inbox",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/",
InboxIssueViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="inbox-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/<uuid:pk>/",
InboxIssueViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="inbox-issue",
),
]
-56
View File
@@ -1,56 +0,0 @@
from django.urls import path
from plane.app.views import IntakeViewSet, IntakeIssueViewSet
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/intakes/",
IntakeViewSet.as_view({"get": "list", "post": "create"}),
name="intake",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/intakes/<uuid:pk>/",
IntakeViewSet.as_view(
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
),
name="intake",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/intake-issues/",
IntakeIssueViewSet.as_view({"get": "list", "post": "create"}),
name="intake-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/intake-issues/<uuid:pk>/",
IntakeIssueViewSet.as_view(
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
),
name="intake-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/",
IntakeViewSet.as_view({"get": "list", "post": "create"}),
name="inbox",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/<uuid:pk>/",
IntakeViewSet.as_view(
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
),
name="inbox",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/",
IntakeIssueViewSet.as_view({"get": "list", "post": "create"}),
name="inbox-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/<uuid:pk>/",
IntakeIssueViewSet.as_view(
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
),
name="inbox-issue",
),
]
+78 -27
View File
@@ -21,9 +21,7 @@ from plane.app.views import (
BulkArchiveIssuesEndpoint,
DeletedIssuesListViewSet,
IssuePaginatedViewSet,
IssueDetailEndpoint,
IssueAttachmentV2Endpoint,
IssueBulkUpdateDateEndpoint,
)
urlpatterns = [
@@ -34,15 +32,14 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/",
IssueViewSet.as_view({"get": "list", "post": "create"}),
IssueViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues-detail/",
IssueDetailEndpoint.as_view(),
name="project-issue-detail",
),
# updated v1 paginated issues
# updated v2 paginated issues
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/v2/issues/",
@@ -63,7 +60,12 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-labels/",
LabelViewSet.as_view({"get": "list", "post": "create"}),
LabelViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-issue-labels",
),
path(
@@ -101,7 +103,12 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-links/",
IssueLinkViewSet.as_view({"get": "list", "post": "create"}),
IssueLinkViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-issue-links",
),
path(
@@ -154,7 +161,12 @@ urlpatterns = [
## IssueComments
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/comments/",
IssueCommentViewSet.as_view({"get": "list", "post": "create"}),
IssueCommentViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-issue-comment",
),
path(
@@ -173,7 +185,12 @@ urlpatterns = [
# Issue Subscribers
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-subscribers/",
IssueSubscriberViewSet.as_view({"get": "list", "post": "create"}),
IssueSubscriberViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-issue-subscribers",
),
path(
@@ -184,7 +201,11 @@ urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/subscribe/",
IssueSubscriberViewSet.as_view(
{"get": "subscription_status", "post": "subscribe", "delete": "unsubscribe"}
{
"get": "subscription_status",
"post": "subscribe",
"delete": "unsubscribe",
}
),
name="project-issue-subscribers",
),
@@ -192,24 +213,42 @@ urlpatterns = [
# Issue Reactions
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/reactions/",
IssueReactionViewSet.as_view({"get": "list", "post": "create"}),
IssueReactionViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-issue-reactions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/reactions/<str:reaction_code>/",
IssueReactionViewSet.as_view({"delete": "destroy"}),
IssueReactionViewSet.as_view(
{
"delete": "destroy",
}
),
name="project-issue-reactions",
),
## End Issue Reactions
# Comment Reactions
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/comments/<uuid:comment_id>/reactions/",
CommentReactionViewSet.as_view({"get": "list", "post": "create"}),
CommentReactionViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-issue-comment-reactions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/comments/<uuid:comment_id>/reactions/<str:reaction_code>/",
CommentReactionViewSet.as_view({"delete": "destroy"}),
CommentReactionViewSet.as_view(
{
"delete": "destroy",
}
),
name="project-issue-comment-reactions",
),
## End Comment Reactions
@@ -223,13 +262,21 @@ urlpatterns = [
## Issue Archives
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-issues/",
IssueArchiveViewSet.as_view({"get": "list"}),
IssueArchiveViewSet.as_view(
{
"get": "list",
}
),
name="project-issue-archive",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:pk>/archive/",
IssueArchiveViewSet.as_view(
{"get": "retrieve", "post": "archive", "delete": "unarchive"}
{
"get": "retrieve",
"post": "archive",
"delete": "unarchive",
}
),
name="project-issue-archive-unarchive",
),
@@ -237,12 +284,21 @@ urlpatterns = [
## Issue Relation
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-relation/",
IssueRelationViewSet.as_view({"get": "list", "post": "create"}),
IssueRelationViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="issue-relation",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/remove-relation/",
IssueRelationViewSet.as_view({"post": "remove_relation"}),
IssueRelationViewSet.as_view(
{
"post": "remove_relation",
}
),
name="issue-relation",
),
## End Issue Relation
@@ -251,9 +307,4 @@ urlpatterns = [
DeletedIssuesListViewSet.as_view(),
name="deleted-issues",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-dates/",
IssueBulkUpdateDateEndpoint.as_view(),
name="project-issue-dates",
),
]
+34 -6
View File
@@ -14,7 +14,12 @@ from plane.app.views import (
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/",
ModuleViewSet.as_view({"get": "list", "post": "create"}),
ModuleViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-modules",
),
path(
@@ -31,12 +36,21 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/modules/",
ModuleIssueViewSet.as_view({"post": "create_issue_modules"}),
ModuleIssueViewSet.as_view(
{
"post": "create_issue_modules",
}
),
name="issue-module",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/issues/",
ModuleIssueViewSet.as_view({"post": "create_module_issues", "get": "list"}),
ModuleIssueViewSet.as_view(
{
"post": "create_module_issues",
"get": "list",
}
),
name="project-module-issues",
),
path(
@@ -53,7 +67,12 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-links/",
ModuleLinkViewSet.as_view({"get": "list", "post": "create"}),
ModuleLinkViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-issue-module-links",
),
path(
@@ -70,12 +89,21 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-modules/",
ModuleFavoriteViewSet.as_view({"get": "list", "post": "create"}),
ModuleFavoriteViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="user-favorite-module",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-modules/<uuid:module_id>/",
ModuleFavoriteViewSet.as_view({"delete": "destroy"}),
ModuleFavoriteViewSet.as_view(
{
"delete": "destroy",
}
),
name="user-favorite-module",
),
path(
+27 -5
View File
@@ -12,24 +12,42 @@ from plane.app.views import (
urlpatterns = [
path(
"workspaces/<str:slug>/users/notifications/",
NotificationViewSet.as_view({"get": "list"}),
NotificationViewSet.as_view(
{
"get": "list",
}
),
name="notifications",
),
path(
"workspaces/<str:slug>/users/notifications/<uuid:pk>/",
NotificationViewSet.as_view(
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="notifications",
),
path(
"workspaces/<str:slug>/users/notifications/<uuid:pk>/read/",
NotificationViewSet.as_view({"post": "mark_read", "delete": "mark_unread"}),
NotificationViewSet.as_view(
{
"post": "mark_read",
"delete": "mark_unread",
}
),
name="notifications",
),
path(
"workspaces/<str:slug>/users/notifications/<uuid:pk>/archive/",
NotificationViewSet.as_view({"post": "archive", "delete": "unarchive"}),
NotificationViewSet.as_view(
{
"post": "archive",
"delete": "unarchive",
}
),
name="notifications",
),
path(
@@ -39,7 +57,11 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/users/notifications/mark-all-read/",
MarkAllReadNotificationViewSet.as_view({"post": "create"}),
MarkAllReadNotificationViewSet.as_view(
{
"post": "create",
}
),
name="mark-all-read-notifications",
),
path(
+40 -7
View File
@@ -14,38 +14,66 @@ from plane.app.views import (
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/",
PageViewSet.as_view({"get": "list", "post": "create"}),
PageViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-pages",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/",
PageViewSet.as_view(
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-pages",
),
# favorite pages
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/favorite-pages/<uuid:pk>/",
PageFavoriteViewSet.as_view({"post": "create", "delete": "destroy"}),
PageFavoriteViewSet.as_view(
{
"post": "create",
"delete": "destroy",
}
),
name="user-favorite-pages",
),
# archived pages
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/archive/",
PageViewSet.as_view({"post": "archive", "delete": "unarchive"}),
PageViewSet.as_view(
{
"post": "archive",
"delete": "unarchive",
}
),
name="project-page-archive-unarchive",
),
# lock and unlock
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/lock/",
PageViewSet.as_view({"post": "lock", "delete": "unlock"}),
PageViewSet.as_view(
{
"post": "lock",
"delete": "unlock",
}
),
name="project-pages-lock-unlock",
),
# private and public page
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/access/",
PageViewSet.as_view({"post": "access"}),
PageViewSet.as_view(
{
"post": "access",
}
),
name="project-pages-access",
),
path(
@@ -65,7 +93,12 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/description/",
PagesDescriptionViewSet.as_view({"get": "retrieve", "patch": "partial_update"}),
PagesDescriptionViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
}
),
name="page-description",
),
path(
+62 -11
View File
@@ -21,7 +21,12 @@ from plane.app.views import (
urlpatterns = [
path(
"workspaces/<str:slug>/projects/",
ProjectViewSet.as_view({"get": "list", "post": "create"}),
ProjectViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project",
),
path(
@@ -43,17 +48,32 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/invitations/",
ProjectInvitationsViewset.as_view({"get": "list", "post": "create"}),
ProjectInvitationsViewset.as_view(
{
"get": "list",
"post": "create",
},
),
name="project-member-invite",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/invitations/<uuid:pk>/",
ProjectInvitationsViewset.as_view({"get": "retrieve", "delete": "destroy"}),
ProjectInvitationsViewset.as_view(
{
"get": "retrieve",
"delete": "destroy",
}
),
name="project-member-invite",
),
path(
"users/me/workspaces/<str:slug>/projects/invitations/",
UserProjectInvitationsViewset.as_view({"get": "list", "post": "create"}),
UserProjectInvitationsViewset.as_view(
{
"get": "list",
"post": "create",
},
),
name="user-project-invitations",
),
path(
@@ -68,19 +88,32 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/members/",
ProjectMemberViewSet.as_view({"get": "list", "post": "create"}),
ProjectMemberViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-member",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/members/<uuid:pk>/",
ProjectMemberViewSet.as_view(
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-member",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/members/leave/",
ProjectMemberViewSet.as_view({"post": "leave"}),
ProjectMemberViewSet.as_view(
{
"post": "leave",
}
),
name="project-member",
),
path(
@@ -100,12 +133,21 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/user-favorite-projects/",
ProjectFavoritesViewSet.as_view({"get": "list", "post": "create"}),
ProjectFavoritesViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-favorite",
),
path(
"workspaces/<str:slug>/user-favorite-projects/<uuid:project_id>/",
ProjectFavoritesViewSet.as_view({"delete": "destroy"}),
ProjectFavoritesViewSet.as_view(
{
"delete": "destroy",
}
),
name="project-favorite",
),
path(
@@ -115,13 +157,22 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/",
DeployBoardViewSet.as_view({"get": "list", "post": "create"}),
DeployBoardViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-deploy-board",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/<uuid:pk>/",
DeployBoardViewSet.as_view(
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-deploy-board",
),
+4 -1
View File
@@ -1,7 +1,10 @@
from django.urls import path
from plane.app.views import GlobalSearchEndpoint, IssueSearchEndpoint
from plane.app.views import (
GlobalSearchEndpoint,
IssueSearchEndpoint,
)
urlpatterns = [
+16 -3
View File
@@ -7,19 +7,32 @@ from plane.app.views import StateViewSet
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/states/",
StateViewSet.as_view({"get": "list", "post": "create"}),
StateViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-states",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/states/<uuid:pk>/",
StateViewSet.as_view(
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-state",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/states/<uuid:pk>/mark-default/",
StateViewSet.as_view({"post": "mark_as_default"}),
StateViewSet.as_view(
{
"post": "mark_as_default",
}
),
name="project-state",
),
]
+44 -10
View File
@@ -22,30 +22,60 @@ urlpatterns = [
path(
"users/me/",
UserEndpoint.as_view(
{"get": "retrieve", "patch": "partial_update", "delete": "deactivate"}
{
"get": "retrieve",
"patch": "partial_update",
"delete": "deactivate",
}
),
name="users",
),
path("users/session/", UserSessionEndpoint.as_view(), name="user-session"),
path(
"users/session/",
UserSessionEndpoint.as_view(),
name="user-session",
),
path(
"users/me/settings/",
UserEndpoint.as_view({"get": "retrieve_user_settings"}),
UserEndpoint.as_view(
{
"get": "retrieve_user_settings",
}
),
name="users",
),
# Profile
path("users/me/profile/", ProfileEndpoint.as_view(), name="accounts"),
path(
"users/me/profile/",
ProfileEndpoint.as_view(),
name="accounts",
),
# End profile
# Accounts
path("users/me/accounts/", AccountEndpoint.as_view(), name="accounts"),
path("users/me/accounts/<uuid:pk>/", AccountEndpoint.as_view(), name="accounts"),
path(
"users/me/accounts/",
AccountEndpoint.as_view(),
name="accounts",
),
path(
"users/me/accounts/<uuid:pk>/",
AccountEndpoint.as_view(),
name="accounts",
),
## End Accounts
path(
"users/me/instance-admin/",
UserEndpoint.as_view({"get": "retrieve_instance_admin"}),
UserEndpoint.as_view(
{
"get": "retrieve_instance_admin",
}
),
name="users",
),
path(
"users/me/onboard/", UpdateUserOnBoardedEndpoint.as_view(), name="user-onboard"
"users/me/onboard/",
UpdateUserOnBoardedEndpoint.as_view(),
name="user-onboard",
),
path(
"users/me/tour-completed/",
@@ -53,11 +83,15 @@ urlpatterns = [
name="user-tour",
),
path(
"users/me/activities/", UserActivityEndpoint.as_view(), name="user-activities"
"users/me/activities/",
UserActivityEndpoint.as_view(),
name="user-activities",
),
# user workspaces
path(
"users/me/workspaces/", UserWorkSpacesEndpoint.as_view(), name="user-workspace"
"users/me/workspaces/",
UserWorkSpacesEndpoint.as_view(),
name="user-workspace",
),
# User Graphs
path(
+28 -5
View File
@@ -12,7 +12,12 @@ from plane.app.views import (
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/views/",
IssueViewViewSet.as_view({"get": "list", "post": "create"}),
IssueViewViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-view",
),
path(
@@ -29,7 +34,12 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/views/",
WorkspaceViewViewSet.as_view({"get": "list", "post": "create"}),
WorkspaceViewViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="global-view",
),
path(
@@ -46,17 +56,30 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/issues/",
WorkspaceViewIssuesViewSet.as_view({"get": "list"}),
WorkspaceViewIssuesViewSet.as_view(
{
"get": "list",
}
),
name="global-view-issues",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-views/",
IssueViewFavoriteViewSet.as_view({"get": "list", "post": "create"}),
IssueViewFavoriteViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="user-favorite-view",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-views/<uuid:view_id>/",
IssueViewFavoriteViewSet.as_view({"delete": "destroy"}),
IssueViewFavoriteViewSet.as_view(
{
"delete": "destroy",
}
),
name="user-favorite-view",
),
]
+5 -1
View File
@@ -8,7 +8,11 @@ from plane.app.views import (
urlpatterns = [
path("workspaces/<str:slug>/webhooks/", WebhookEndpoint.as_view(), name="webhooks"),
path(
"workspaces/<str:slug>/webhooks/",
WebhookEndpoint.as_view(),
name="webhooks",
),
path(
"workspaces/<str:slug>/webhooks/<uuid:pk>/",
WebhookEndpoint.as_view(),
+61 -11
View File
@@ -39,7 +39,12 @@ urlpatterns = [
),
path(
"workspaces/",
WorkSpaceViewSet.as_view({"get": "list", "post": "create"}),
WorkSpaceViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="workspace",
),
path(
@@ -56,20 +61,34 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/invitations/",
WorkspaceInvitationsViewset.as_view({"get": "list", "post": "create"}),
WorkspaceInvitationsViewset.as_view(
{
"get": "list",
"post": "create",
},
),
name="workspace-invitations",
),
path(
"workspaces/<str:slug>/invitations/<uuid:pk>/",
WorkspaceInvitationsViewset.as_view(
{"delete": "destroy", "get": "retrieve", "patch": "partial_update"}
{
"delete": "destroy",
"get": "retrieve",
"patch": "partial_update",
}
),
name="workspace-invitations",
),
# user workspace invitations
path(
"users/me/workspaces/invitations/",
UserWorkspaceInvitationsViewSet.as_view({"get": "list", "post": "create"}),
UserWorkspaceInvitationsViewSet.as_view(
{
"get": "list",
"post": "create",
},
),
name="user-workspace-invitations",
),
path(
@@ -91,18 +110,31 @@ urlpatterns = [
path(
"workspaces/<str:slug>/members/<uuid:pk>/",
WorkSpaceMemberViewSet.as_view(
{"patch": "partial_update", "delete": "destroy", "get": "retrieve"}
{
"patch": "partial_update",
"delete": "destroy",
"get": "retrieve",
}
),
name="workspace-member",
),
path(
"workspaces/<str:slug>/members/leave/",
WorkSpaceMemberViewSet.as_view({"post": "leave"}),
WorkSpaceMemberViewSet.as_view(
{
"post": "leave",
},
),
name="leave-workspace-members",
),
path(
"workspaces/<str:slug>/teams/",
TeamMemberViewSet.as_view({"get": "list", "post": "create"}),
TeamMemberViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="workspace-team-members",
),
path(
@@ -134,13 +166,22 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/workspace-themes/",
WorkspaceThemeViewSet.as_view({"get": "list", "post": "create"}),
WorkspaceThemeViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="workspace-themes",
),
path(
"workspaces/<str:slug>/workspace-themes/<uuid:pk>/",
WorkspaceThemeViewSet.as_view(
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="workspace-themes",
),
@@ -216,13 +257,22 @@ urlpatterns = [
),
path(
"workspaces/<str:slug>/draft-issues/",
WorkspaceDraftIssueViewSet.as_view({"get": "list", "post": "create"}),
WorkspaceDraftIssueViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="workspace-draft-issues",
),
path(
"workspaces/<str:slug>/draft-issues/<uuid:pk>/",
WorkspaceDraftIssueViewSet.as_view(
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="workspace-drafts-issues",
),
+63 -22
View File
@@ -59,8 +59,12 @@ from .workspace.invite import (
WorkspaceJoinEndpoint,
UserWorkspaceInvitationsViewSet,
)
from .workspace.label import WorkspaceLabelsEndpoint
from .workspace.state import WorkspaceStatesEndpoint
from .workspace.label import (
WorkspaceLabelsEndpoint,
)
from .workspace.state import (
WorkspaceStatesEndpoint,
)
from .workspace.user import (
UserLastProjectWithWorkspaceEndpoint,
WorkspaceUserProfileIssuesEndpoint,
@@ -71,9 +75,15 @@ from .workspace.user import (
UserActivityGraphEndpoint,
UserIssueCompletedGraphEndpoint,
)
from .workspace.estimate import WorkspaceEstimatesEndpoint
from .workspace.module import WorkspaceModulesEndpoint
from .workspace.cycle import WorkspaceCyclesEndpoint
from .workspace.estimate import (
WorkspaceEstimatesEndpoint,
)
from .workspace.module import (
WorkspaceModulesEndpoint,
)
from .workspace.cycle import (
WorkspaceCyclesEndpoint,
)
from .state.base import StateViewSet
from .view.base import (
@@ -93,10 +103,18 @@ from .cycle.base import (
CycleAnalyticsEndpoint,
CycleProgressEndpoint,
)
from .cycle.issue import CycleIssueViewSet
from .cycle.archive import CycleArchiveUnarchiveEndpoint
from .cycle.issue import (
CycleIssueViewSet,
)
from .cycle.archive import (
CycleArchiveUnarchiveEndpoint,
)
from .asset.base import FileAssetEndpoint, UserAssetsEndpoint, FileAssetViewSet
from .asset.base import (
FileAssetEndpoint,
UserAssetsEndpoint,
FileAssetViewSet,
)
from .asset.v2 import (
WorkspaceFileAssetEndpoint,
UserAssetsV2Endpoint,
@@ -112,11 +130,11 @@ from .issue.base import (
BulkDeleteIssuesEndpoint,
DeletedIssuesListViewSet,
IssuePaginatedViewSet,
IssueDetailEndpoint,
IssueBulkUpdateDateEndpoint,
)
from .issue.activity import IssueActivityEndpoint
from .issue.activity import (
IssueActivityEndpoint,
)
from .issue.archive import IssueArchiveViewSet, BulkArchiveIssuesEndpoint
@@ -126,19 +144,35 @@ from .issue.attachment import (
IssueAttachmentV2Endpoint,
)
from .issue.comment import IssueCommentViewSet, CommentReactionViewSet
from .issue.comment import (
IssueCommentViewSet,
CommentReactionViewSet,
)
from .issue.label import LabelViewSet, BulkCreateIssueLabelsEndpoint
from .issue.label import (
LabelViewSet,
BulkCreateIssueLabelsEndpoint,
)
from .issue.link import IssueLinkViewSet
from .issue.link import (
IssueLinkViewSet,
)
from .issue.relation import IssueRelationViewSet
from .issue.relation import (
IssueRelationViewSet,
)
from .issue.reaction import IssueReactionViewSet
from .issue.reaction import (
IssueReactionViewSet,
)
from .issue.sub_issue import SubIssuesEndpoint
from .issue.sub_issue import (
SubIssuesEndpoint,
)
from .issue.subscriber import IssueSubscriberViewSet
from .issue.subscriber import (
IssueSubscriberViewSet,
)
from .module.base import (
ModuleViewSet,
@@ -147,11 +181,18 @@ from .module.base import (
ModuleUserPropertiesEndpoint,
)
from .module.issue import ModuleIssueViewSet
from .module.issue import (
ModuleIssueViewSet,
)
from .module.archive import ModuleArchiveUnarchiveEndpoint
from .module.archive import (
ModuleArchiveUnarchiveEndpoint,
)
from .api import ApiTokenEndpoint, ServiceApiTokenEndpoint
from .api import (
ApiTokenEndpoint,
ServiceApiTokenEndpoint,
)
from .page.base import (
PageViewSet,
@@ -177,7 +218,7 @@ from .estimate.base import (
EstimatePointEndpoint,
)
from .intake.base import IntakeViewSet, IntakeIssueViewSet
from .inbox.base import InboxViewSet, InboxIssueViewSet
from .analytic.base import (
AnalyticsEndpoint,
+80 -24
View File
@@ -22,7 +22,14 @@ from plane.app.permissions import allow_permission, ROLE
class AnalyticsEndpoint(BaseAPIView):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
@allow_permission(
[
ROLE.ADMIN,
ROLE.MEMBER,
],
level="WORKSPACE",
)
def get(self, request, slug):
x_axis = request.GET.get("x_axis", False)
y_axis = request.GET.get("y_axis", False)
@@ -43,7 +50,10 @@ class AnalyticsEndpoint(BaseAPIView):
"completed_at",
]
valid_yaxis = ["issue_count", "estimate"]
valid_yaxis = [
"issue_count",
"estimate",
]
# Check for x-axis and y-axis as thery are required parameters
if (
@@ -60,7 +70,9 @@ class AnalyticsEndpoint(BaseAPIView):
)
# If segment is present it cannot be same as x-axis
if segment and (segment not in valid_xaxis_segment or x_axis == segment):
if segment and (
segment not in valid_xaxis_segment or x_axis == segment
):
return Response(
{
"error": "Both segment and x axis cannot be same and segment should be valid"
@@ -85,7 +97,10 @@ class AnalyticsEndpoint(BaseAPIView):
state_details = {}
if x_axis in ["state_id"] or segment in ["state_id"]:
state_details = (
Issue.issue_objects.filter(workspace__slug=slug, **filters)
Issue.issue_objects.filter(
workspace__slug=slug,
**filters,
)
.distinct("state_id")
.order_by("state_id")
.values("state_id", "state__name", "state__color")
@@ -97,8 +112,8 @@ class AnalyticsEndpoint(BaseAPIView):
Issue.objects.filter(
workspace__slug=slug,
**filters,
labels__id__isnull=False,
label_issue__deleted_at__isnull=True,
labels__id__isnull=False
& Q(label_issue__deleted_at__isnull=True),
)
.distinct("labels__id")
.order_by("labels__id")
@@ -148,17 +163,21 @@ class AnalyticsEndpoint(BaseAPIView):
)
cycle_details = {}
if x_axis in ["issue_cycle__cycle_id"] or segment in ["issue_cycle__cycle_id"]:
if x_axis in ["issue_cycle__cycle_id"] or segment in [
"issue_cycle__cycle_id"
]:
cycle_details = (
Issue.issue_objects.filter(
workspace__slug=slug,
**filters,
issue_cycle__cycle_id__isnull=False,
issue_cycle__deleted_at__isnull=True,
)
.distinct("issue_cycle__cycle_id")
.order_by("issue_cycle__cycle_id")
.values("issue_cycle__cycle_id", "issue_cycle__cycle__name")
.values(
"issue_cycle__cycle_id",
"issue_cycle__cycle__name",
)
)
module_details = {}
@@ -170,11 +189,13 @@ class AnalyticsEndpoint(BaseAPIView):
workspace__slug=slug,
**filters,
issue_module__module_id__isnull=False,
issue_module__deleted_at__isnull=True,
)
.distinct("issue_module__module_id")
.order_by("issue_module__module_id")
.values("issue_module__module_id", "issue_module__module__name")
.values(
"issue_module__module_id",
"issue_module__module__name",
)
)
return Response(
@@ -194,7 +215,9 @@ class AnalyticsEndpoint(BaseAPIView):
class AnalyticViewViewset(BaseViewSet):
permission_classes = [WorkSpaceAdminPermission]
permission_classes = [
WorkSpaceAdminPermission,
]
model = AnalyticView
serializer_class = AnalyticViewSerializer
@@ -204,14 +227,25 @@ class AnalyticViewViewset(BaseViewSet):
def get_queryset(self):
return self.filter_queryset(
super().get_queryset().filter(workspace__slug=self.kwargs.get("slug"))
super()
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
)
class SavedAnalyticEndpoint(BaseAPIView):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
@allow_permission(
[
ROLE.ADMIN,
ROLE.MEMBER,
],
level="WORKSPACE",
)
def get(self, request, slug, analytic_id):
analytic_view = AnalyticView.objects.get(pk=analytic_id, workspace__slug=slug)
analytic_view = AnalyticView.objects.get(
pk=analytic_id, workspace__slug=slug
)
filter = analytic_view.query
queryset = Issue.issue_objects.filter(**filter)
@@ -237,7 +271,14 @@ class SavedAnalyticEndpoint(BaseAPIView):
class ExportAnalyticsEndpoint(BaseAPIView):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
@allow_permission(
[
ROLE.ADMIN,
ROLE.MEMBER,
],
level="WORKSPACE",
)
def post(self, request, slug):
x_axis = request.data.get("x_axis", False)
y_axis = request.data.get("y_axis", False)
@@ -258,7 +299,10 @@ class ExportAnalyticsEndpoint(BaseAPIView):
"completed_at",
]
valid_yaxis = ["issue_count", "estimate"]
valid_yaxis = [
"issue_count",
"estimate",
]
# Check for x-axis and y-axis as thery are required parameters
if (
@@ -275,7 +319,9 @@ class ExportAnalyticsEndpoint(BaseAPIView):
)
# If segment is present it cannot be same as x-axis
if segment and (segment not in valid_xaxis_segment or x_axis == segment):
if segment and (
segment not in valid_xaxis_segment or x_axis == segment
):
return Response(
{
"error": "Both segment and x axis cannot be same and segment should be valid"
@@ -296,10 +342,13 @@ class ExportAnalyticsEndpoint(BaseAPIView):
class DefaultAnalyticsEndpoint(BaseAPIView):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
def get(self, request, slug):
filters = issue_filters(request.GET, "GET")
base_issues = Issue.issue_objects.filter(workspace__slug=slug, **filters)
base_issues = Issue.issue_objects.filter(
workspace__slug=slug, **filters
)
total_issues = base_issues.count()
@@ -312,7 +361,9 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
)
open_issues_groups = ["backlog", "unstarted", "started"]
open_issues_queryset = state_groups.filter(state__group__in=open_issues_groups)
open_issues_queryset = state_groups.filter(
state__group__in=open_issues_groups
)
open_issues = open_issues_queryset.count()
open_issues_classified = (
@@ -354,7 +405,8 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
),
# If `avatar_asset` is None, fall back to using `avatar` field directly
When(
created_by__avatar_asset__isnull=True, then="created_by__avatar"
created_by__avatar_asset__isnull=True,
then="created_by__avatar",
),
default=Value(None),
output_field=models.CharField(),
@@ -387,7 +439,8 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
),
# If `avatar_asset` is None, fall back to using `avatar` field directly
When(
assignees__avatar_asset__isnull=True, then="assignees__avatar"
assignees__avatar_asset__isnull=True,
then="assignees__avatar",
),
default=Value(None),
output_field=models.CharField(),
@@ -414,7 +467,8 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
),
# If `avatar_asset` is None, fall back to using `avatar` field directly
When(
assignees__avatar_asset__isnull=True, then="assignees__avatar"
assignees__avatar_asset__isnull=True,
then="assignees__avatar",
),
default=Value(None),
output_field=models.CharField(),
@@ -423,7 +477,9 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
.order_by("-count")
)
open_estimate_sum = open_issues_queryset.aggregate(sum=Sum("point"))["sum"]
open_estimate_sum = open_issues_queryset.aggregate(sum=Sum("point"))[
"sum"
]
total_estimate_sum = base_issues.aggregate(sum=Sum("point"))["sum"]
return Response(
+35 -9
View File
@@ -13,7 +13,9 @@ from plane.app.permissions import WorkspaceOwnerPermission
class ApiTokenEndpoint(BaseAPIView):
permission_classes = [WorkspaceOwnerPermission]
permission_classes = [
WorkspaceOwnerPermission,
]
def post(self, request, slug):
label = request.data.get("label", str(uuid4().hex))
@@ -35,7 +37,10 @@ class ApiTokenEndpoint(BaseAPIView):
serializer = APITokenSerializer(api_token)
# Token will be only visible while creating
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(
serializer.data,
status=status.HTTP_201_CREATED,
)
def get(self, request, slug, pk=None):
if pk is None:
@@ -53,14 +58,23 @@ class ApiTokenEndpoint(BaseAPIView):
def delete(self, request, slug, pk):
api_token = APIToken.objects.get(
workspace__slug=slug, user=request.user, pk=pk, is_service=False
workspace__slug=slug,
user=request.user,
pk=pk,
is_service=False,
)
api_token.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
def patch(self, request, slug, pk):
api_token = APIToken.objects.get(workspace__slug=slug, user=request.user, pk=pk)
serializer = APITokenSerializer(api_token, data=request.data, partial=True)
api_token = APIToken.objects.get(
workspace__slug=slug,
user=request.user,
pk=pk,
)
serializer = APITokenSerializer(
api_token, data=request.data, partial=True
)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
@@ -68,17 +82,25 @@ class ApiTokenEndpoint(BaseAPIView):
class ServiceApiTokenEndpoint(BaseAPIView):
permission_classes = [WorkspaceOwnerPermission]
permission_classes = [
WorkspaceOwnerPermission,
]
def post(self, request, slug):
workspace = Workspace.objects.get(slug=slug)
api_token = APIToken.objects.filter(
workspace=workspace, is_service=True
workspace=workspace,
is_service=True,
).first()
if api_token:
return Response({"token": str(api_token.token)}, status=status.HTTP_200_OK)
return Response(
{
"token": str(api_token.token),
},
status=status.HTTP_200_OK,
)
else:
# Check the user type
user_type = 1 if request.user.is_bot else 0
@@ -92,5 +114,9 @@ class ServiceApiTokenEndpoint(BaseAPIView):
is_service=True,
)
return Response(
{"token": str(api_token.token)}, status=status.HTTP_201_CREATED
{
"token": str(api_token.token),
},
status=status.HTTP_201_CREATED,
)
+21 -9
View File
@@ -10,7 +10,11 @@ from plane.app.serializers import FileAssetSerializer
class FileAssetEndpoint(BaseAPIView):
parser_classes = (MultiPartParser, FormParser, JSONParser)
parser_classes = (
MultiPartParser,
FormParser,
JSONParser,
)
"""
A viewset for viewing and editing task instances.
@@ -24,7 +28,8 @@ class FileAssetEndpoint(BaseAPIView):
files, context={"request": request}, many=True
)
return Response(
{"data": serializer.data, "status": True}, status=status.HTTP_200_OK
{"data": serializer.data, "status": True},
status=status.HTTP_200_OK,
)
else:
return Response(
@@ -45,7 +50,7 @@ class FileAssetEndpoint(BaseAPIView):
asset_key = str(workspace_id) + "/" + asset_key
file_asset = FileAsset.objects.get(asset=asset_key)
file_asset.is_deleted = True
file_asset.save(update_fields=["is_deleted"])
file_asset.save()
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -54,7 +59,7 @@ class FileAssetViewSet(BaseViewSet):
asset_key = str(workspace_id) + "/" + asset_key
file_asset = FileAsset.objects.get(asset=asset_key)
file_asset.is_deleted = False
file_asset.save(update_fields=["is_deleted"])
file_asset.save()
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -62,11 +67,16 @@ class UserAssetsEndpoint(BaseAPIView):
parser_classes = (MultiPartParser, FormParser)
def get(self, request, asset_key):
files = FileAsset.objects.filter(asset=asset_key, created_by=request.user)
files = FileAsset.objects.filter(
asset=asset_key, created_by=request.user
)
if files.exists():
serializer = FileAssetSerializer(files, context={"request": request})
serializer = FileAssetSerializer(
files, context={"request": request}
)
return Response(
{"data": serializer.data, "status": True}, status=status.HTTP_200_OK
{"data": serializer.data, "status": True},
status=status.HTTP_200_OK,
)
else:
return Response(
@@ -82,7 +92,9 @@ class UserAssetsEndpoint(BaseAPIView):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, asset_key):
file_asset = FileAsset.objects.get(asset=asset_key, created_by=request.user)
file_asset = FileAsset.objects.get(
asset=asset_key, created_by=request.user
)
file_asset.is_deleted = True
file_asset.save(update_fields=["is_deleted"])
file_asset.save()
return Response(status=status.HTTP_204_NO_CONTENT)
+199 -83
View File
@@ -13,7 +13,12 @@ from rest_framework.permissions import AllowAny
# Module imports
from ..base import BaseAPIView
from plane.db.models import FileAsset, Workspace, Project, User
from plane.db.models import (
FileAsset,
Workspace,
Project,
User,
)
from plane.settings.storage import S3Storage
from plane.app.permissions import allow_permission, ROLE
from plane.utils.cache import invalidate_cache_directly
@@ -29,7 +34,7 @@ class UserAssetsV2Endpoint(BaseAPIView):
return
asset.is_deleted = True
asset.deleted_at = timezone.now()
asset.save(update_fields=["is_deleted", "deleted_at"])
asset.save()
return
def entity_asset_save(self, asset_id, entity_type, asset, request):
@@ -44,7 +49,10 @@ class UserAssetsV2Endpoint(BaseAPIView):
user.avatar_asset_id = asset_id
user.save()
invalidate_cache_directly(
path="/api/users/me/", url_params=False, user=True, request=request
path="/api/users/me/",
url_params=False,
user=True,
request=request,
)
invalidate_cache_directly(
path="/api/users/me/settings/",
@@ -64,7 +72,10 @@ class UserAssetsV2Endpoint(BaseAPIView):
user.cover_image_asset_id = asset_id
user.save()
invalidate_cache_directly(
path="/api/users/me/", url_params=False, user=True, request=request
path="/api/users/me/",
url_params=False,
user=True,
request=request,
)
invalidate_cache_directly(
path="/api/users/me/settings/",
@@ -82,7 +93,10 @@ class UserAssetsV2Endpoint(BaseAPIView):
user.avatar_asset_id = None
user.save()
invalidate_cache_directly(
path="/api/users/me/", url_params=False, user=True, request=request
path="/api/users/me/",
url_params=False,
user=True,
request=request,
)
invalidate_cache_directly(
path="/api/users/me/settings/",
@@ -97,7 +111,10 @@ class UserAssetsV2Endpoint(BaseAPIView):
user.cover_image_asset_id = None
user.save()
invalidate_cache_directly(
path="/api/users/me/", url_params=False, user=True, request=request
path="/api/users/me/",
url_params=False,
user=True,
request=request,
)
invalidate_cache_directly(
path="/api/users/me/settings/",
@@ -121,7 +138,10 @@ class UserAssetsV2Endpoint(BaseAPIView):
# Check if the entity type is allowed
if not entity_type or entity_type not in ["USER_AVATAR", "USER_COVER"]:
return Response(
{"error": "Invalid entity type.", "status": False},
{
"error": "Invalid entity type.",
"status": False,
},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -141,7 +161,11 @@ class UserAssetsV2Endpoint(BaseAPIView):
# Create a File Asset
asset = FileAsset.objects.create(
attributes={"name": name, "type": type, "size": size_limit},
attributes={
"name": name,
"type": type,
"size": size_limit,
},
asset=asset_key,
size=size_limit,
user=request.user,
@@ -153,7 +177,9 @@ class UserAssetsV2Endpoint(BaseAPIView):
storage = S3Storage(request=request)
# Generate a presigned URL to share an S3 object
presigned_url = storage.generate_presigned_post(
object_name=asset_key, file_type=type, file_size=size_limit
object_name=asset_key,
file_type=type,
file_size=size_limit,
)
# Return the presigned URL
return Response(
@@ -183,7 +209,7 @@ class UserAssetsV2Endpoint(BaseAPIView):
# update the attributes
asset.attributes = request.data.get("attributes", asset.attributes)
# save the asset
asset.save(update_fields=["is_uploaded", "attributes"])
asset.save()
return Response(status=status.HTTP_204_NO_CONTENT)
def delete(self, request, asset_id):
@@ -194,7 +220,7 @@ class UserAssetsV2Endpoint(BaseAPIView):
self.entity_asset_delete(
entity_type=asset.entity_type, asset=asset, request=request
)
asset.save(update_fields=["is_deleted", "deleted_at"])
asset.save()
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -204,33 +230,45 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
def get_entity_id_field(self, entity_type, entity_id):
# Workspace Logo
if entity_type == FileAsset.EntityTypeContext.WORKSPACE_LOGO:
return {"workspace_id": entity_id}
return {
"workspace_id": entity_id,
}
# Project Cover
if entity_type == FileAsset.EntityTypeContext.PROJECT_COVER:
return {"project_id": entity_id}
return {
"project_id": entity_id,
}
# User Avatar and Cover
if entity_type in [
FileAsset.EntityTypeContext.USER_AVATAR,
FileAsset.EntityTypeContext.USER_COVER,
]:
return {"user_id": entity_id}
return {
"user_id": entity_id,
}
# Issue Attachment and Description
if entity_type in [
FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
FileAsset.EntityTypeContext.ISSUE_DESCRIPTION,
]:
return {"issue_id": entity_id}
return {
"issue_id": entity_id,
}
# Page Description
if entity_type == FileAsset.EntityTypeContext.PAGE_DESCRIPTION:
return {"page_id": entity_id}
return {
"page_id": entity_id,
}
# Comment Description
if entity_type == FileAsset.EntityTypeContext.COMMENT_DESCRIPTION:
return {"comment_id": entity_id}
return {
"comment_id": entity_id,
}
return {}
def asset_delete(self, asset_id):
@@ -241,7 +279,7 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
# Mark the asset as deleted
asset.is_deleted = True
asset.deleted_at = timezone.now()
asset.save(update_fields=["is_deleted", "deleted_at"])
asset.save()
return
def entity_asset_save(self, asset_id, entity_type, asset, request):
@@ -258,7 +296,10 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
workspace.logo_asset_id = asset_id
workspace.save()
invalidate_cache_directly(
path="/api/workspaces/", url_params=False, user=False, request=request
path="/api/workspaces/",
url_params=False,
user=False,
request=request,
)
invalidate_cache_directly(
path="/api/users/me/workspaces/",
@@ -267,13 +308,16 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
request=request,
)
invalidate_cache_directly(
path="/api/instances/", url_params=False, user=False, request=request
path="/api/instances/",
url_params=False,
user=False,
request=request,
)
return
# Project Cover
elif entity_type == FileAsset.EntityTypeContext.PROJECT_COVER:
project = Project.objects.filter(id=asset.project_id).first()
project = Project.objects.filter(id=asset.workspace_id).first()
if project is None:
return
# Delete the previous cover image
@@ -296,7 +340,10 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
workspace.logo_asset_id = None
workspace.save()
invalidate_cache_directly(
path="/api/workspaces/", url_params=False, user=False, request=request
path="/api/workspaces/",
url_params=False,
user=False,
request=request,
)
invalidate_cache_directly(
path="/api/users/me/workspaces/",
@@ -305,7 +352,10 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
request=request,
)
invalidate_cache_directly(
path="/api/instances/", url_params=False, user=False, request=request
path="/api/instances/",
url_params=False,
user=False,
request=request,
)
return
# Project Cover
@@ -329,18 +379,15 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
# Check if the entity type is allowed
if entity_type not in FileAsset.EntityTypeContext.values:
return Response(
{"error": "Invalid entity type.", "status": False},
{
"error": "Invalid entity type.",
"status": False,
},
status=status.HTTP_400_BAD_REQUEST,
)
# Check if the file type is allowed
allowed_types = [
"image/jpeg",
"image/png",
"image/webp",
"image/jpg",
"image/gif",
]
allowed_types = ["image/jpeg", "image/png", "image/webp", "image/jpg"]
if type not in allowed_types:
return Response(
{
@@ -361,7 +408,11 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
# Create a File Asset
asset = FileAsset.objects.create(
attributes={"name": name, "type": type, "size": size_limit},
attributes={
"name": name,
"type": type,
"size": size_limit,
},
asset=asset_key,
size=size_limit,
workspace=workspace,
@@ -376,7 +427,9 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
storage = S3Storage(request=request)
# Generate a presigned URL to share an S3 object
presigned_url = storage.generate_presigned_post(
object_name=asset_key, file_type=type, file_size=size_limit
object_name=asset_key,
file_type=type,
file_size=size_limit,
)
# Return the presigned URL
return Response(
@@ -406,7 +459,7 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
# update the attributes
asset.attributes = request.data.get("attributes", asset.attributes)
# save the asset
asset.save(update_fields=["is_uploaded", "attributes"])
asset.save()
return Response(status=status.HTTP_204_NO_CONTENT)
def delete(self, request, slug, asset_id):
@@ -417,7 +470,7 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
self.entity_asset_delete(
entity_type=asset.entity_type, asset=asset, request=request
)
asset.save(update_fields=["is_deleted", "deleted_at"])
asset.save()
return Response(status=status.HTTP_204_NO_CONTENT)
def get(self, request, slug, asset_id):
@@ -427,14 +480,18 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
# Check if the asset is uploaded
if not asset.is_uploaded:
return Response(
{"error": "The requested asset could not be found."},
{
"error": "The requested asset could not be found.",
},
status=status.HTTP_404_NOT_FOUND,
)
# Get the presigned URL
storage = S3Storage(request=request)
# Generate a presigned URL to share an S3 object
signed_url = storage.generate_presigned_url(object_name=asset.asset.name)
signed_url = storage.generate_presigned_url(
object_name=asset.asset.name,
)
# Redirect to the signed URL
return HttpResponseRedirect(signed_url)
@@ -442,7 +499,9 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
class StaticFileAssetEndpoint(BaseAPIView):
"""This endpoint is used to get the signed URL for a static asset."""
permission_classes = [AllowAny]
permission_classes = [
AllowAny,
]
def get(self, request, asset_id):
# get the asset id
@@ -451,7 +510,9 @@ class StaticFileAssetEndpoint(BaseAPIView):
# Check if the asset is uploaded
if not asset.is_uploaded:
return Response(
{"error": "The requested asset could not be found."},
{
"error": "The requested asset could not be found.",
},
status=status.HTTP_404_NOT_FOUND,
)
@@ -463,14 +524,19 @@ class StaticFileAssetEndpoint(BaseAPIView):
FileAsset.EntityTypeContext.PROJECT_COVER,
]:
return Response(
{"error": "Invalid entity type.", "status": False},
{
"error": "Invalid entity type.",
"status": False,
},
status=status.HTTP_400_BAD_REQUEST,
)
# Get the presigned URL
storage = S3Storage(request=request)
# Generate a presigned URL to share an S3 object
signed_url = storage.generate_presigned_url(object_name=asset.asset.name)
signed_url = storage.generate_presigned_url(
object_name=asset.asset.name,
)
# Redirect to the signed URL
return HttpResponseRedirect(signed_url)
@@ -483,7 +549,7 @@ class AssetRestoreEndpoint(BaseAPIView):
asset = FileAsset.all_objects.get(id=asset_id, workspace__slug=slug)
asset.is_deleted = False
asset.deleted_at = None
asset.save(update_fields=["is_deleted", "deleted_at"])
asset.save()
return Response(status=status.HTTP_204_NO_CONTENT)
@@ -492,34 +558,50 @@ class ProjectAssetEndpoint(BaseAPIView):
def get_entity_id_field(self, entity_type, entity_id):
if entity_type == FileAsset.EntityTypeContext.WORKSPACE_LOGO:
return {"workspace_id": entity_id}
return {
"workspace_id": entity_id,
}
if entity_type == FileAsset.EntityTypeContext.PROJECT_COVER:
return {"project_id": entity_id}
return {
"project_id": entity_id,
}
if entity_type in [
FileAsset.EntityTypeContext.USER_AVATAR,
FileAsset.EntityTypeContext.USER_COVER,
]:
return {"user_id": entity_id}
return {
"user_id": entity_id,
}
if entity_type in [
FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
FileAsset.EntityTypeContext.ISSUE_DESCRIPTION,
]:
return {"issue_id": entity_id}
return {
"issue_id": entity_id,
}
if entity_type == FileAsset.EntityTypeContext.PAGE_DESCRIPTION:
return {"page_id": entity_id}
return {
"page_id": entity_id,
}
if entity_type == FileAsset.EntityTypeContext.COMMENT_DESCRIPTION:
return {"comment_id": entity_id}
return {
"comment_id": entity_id,
}
if entity_type == FileAsset.EntityTypeContext.DRAFT_ISSUE_DESCRIPTION:
return {"draft_issue_id": entity_id}
return {
"draft_issue_id": entity_id,
}
return {}
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
@allow_permission(
[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST],
)
def post(self, request, slug, project_id):
name = request.data.get("name")
type = request.data.get("type", "image/jpeg")
@@ -530,18 +612,15 @@ class ProjectAssetEndpoint(BaseAPIView):
# Check if the entity type is allowed
if entity_type not in FileAsset.EntityTypeContext.values:
return Response(
{"error": "Invalid entity type.", "status": False},
{
"error": "Invalid entity type.",
"status": False,
},
status=status.HTTP_400_BAD_REQUEST,
)
# Check if the file type is allowed
allowed_types = [
"image/jpeg",
"image/png",
"image/webp",
"image/jpg",
"image/gif",
]
allowed_types = ["image/jpeg", "image/png", "image/webp", "image/jpg"]
if type not in allowed_types:
return Response(
{
@@ -562,7 +641,11 @@ class ProjectAssetEndpoint(BaseAPIView):
# Create a File Asset
asset = FileAsset.objects.create(
attributes={"name": name, "type": type, "size": size_limit},
attributes={
"name": name,
"type": type,
"size": size_limit,
},
asset=asset_key,
size=size_limit,
workspace=workspace,
@@ -576,7 +659,9 @@ class ProjectAssetEndpoint(BaseAPIView):
storage = S3Storage(request=request)
# Generate a presigned URL to share an S3 object
presigned_url = storage.generate_presigned_post(
object_name=asset_key, file_type=type, file_size=size_limit
object_name=asset_key,
file_type=type,
file_size=size_limit,
)
# Return the presigned URL
return Response(
@@ -588,10 +673,14 @@ class ProjectAssetEndpoint(BaseAPIView):
status=status.HTTP_200_OK,
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
@allow_permission(
[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST],
)
def patch(self, request, slug, project_id, pk):
# get the asset id
asset = FileAsset.objects.get(id=pk)
asset = FileAsset.objects.get(
id=pk,
)
# get the storage metadata
asset.is_uploaded = True
# get the storage metadata
@@ -601,49 +690,53 @@ class ProjectAssetEndpoint(BaseAPIView):
# update the attributes
asset.attributes = request.data.get("attributes", asset.attributes)
# save the asset
asset.save(update_fields=["is_uploaded", "attributes"])
asset.save()
return Response(status=status.HTTP_204_NO_CONTENT)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def delete(self, request, slug, project_id, pk):
# Get the asset
asset = FileAsset.objects.get(
id=pk, workspace__slug=slug, project_id=project_id
id=pk,
workspace__slug=slug,
project_id=project_id,
)
# Check deleted assets
asset.is_deleted = True
asset.deleted_at = timezone.now()
# Save the asset
asset.save(update_fields=["is_deleted", "deleted_at"])
asset.save()
return Response(status=status.HTTP_204_NO_CONTENT)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def get(self, request, slug, project_id, pk):
# get the asset id
asset = FileAsset.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk
workspace__slug=slug,
project_id=project_id,
pk=pk,
)
# Check if the asset is uploaded
if not asset.is_uploaded:
return Response(
{"error": "The requested asset could not be found."},
{
"error": "The requested asset could not be found.",
},
status=status.HTTP_404_NOT_FOUND,
)
# Get the presigned URL
storage = S3Storage(request=request)
# Generate a presigned URL to share an S3 object
signed_url = storage.generate_presigned_url(object_name=asset.asset.name)
signed_url = storage.generate_presigned_url(
object_name=asset.asset.name,
)
# Redirect to the signed URL
return HttpResponseRedirect(signed_url)
class ProjectBulkAssetEndpoint(BaseAPIView):
def save_project_cover(self, asset, project_id):
project = Project.objects.get(id=project_id)
project.cover_image_asset_id = asset.id
project.save()
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def post(self, request, slug, project_id, entity_id):
@@ -652,36 +745,59 @@ class ProjectBulkAssetEndpoint(BaseAPIView):
# Check if the asset ids are provided
if not asset_ids:
return Response(
{"error": "No asset ids provided."}, status=status.HTTP_400_BAD_REQUEST
{
"error": "No asset ids provided.",
},
status=status.HTTP_400_BAD_REQUEST,
)
# get the asset id
assets = FileAsset.objects.filter(id__in=asset_ids, workspace__slug=slug)
assets = FileAsset.objects.filter(
id__in=asset_ids,
workspace__slug=slug,
)
# Get the first asset
asset = assets.first()
if not asset:
return Response(
{"error": "The requested asset could not be found."},
{
"error": "The requested asset could not be found.",
},
status=status.HTTP_404_NOT_FOUND,
)
# Check if the asset is uploaded
if asset.entity_type == FileAsset.EntityTypeContext.PROJECT_COVER:
assets.update(project_id=project_id)
[self.save_project_cover(asset, project_id) for asset in assets]
assets.update(
project_id=project_id,
)
if asset.entity_type == FileAsset.EntityTypeContext.ISSUE_DESCRIPTION:
assets.update(issue_id=entity_id)
assets.update(
issue_id=entity_id,
)
if asset.entity_type == FileAsset.EntityTypeContext.COMMENT_DESCRIPTION:
assets.update(comment_id=entity_id)
if (
asset.entity_type
== FileAsset.EntityTypeContext.COMMENT_DESCRIPTION
):
assets.update(
comment_id=entity_id,
)
if asset.entity_type == FileAsset.EntityTypeContext.PAGE_DESCRIPTION:
assets.update(page_id=entity_id)
assets.update(
page_id=entity_id,
)
if asset.entity_type == FileAsset.EntityTypeContext.DRAFT_ISSUE_DESCRIPTION:
assets.update(draft_issue_id=entity_id)
if (
asset.entity_type
== FileAsset.EntityTypeContext.DRAFT_ISSUE_DESCRIPTION
):
assets.update(
draft_issue_id=entity_id,
)
return Response(status=status.HTTP_204_NO_CONTENT)
+35 -11
View File
@@ -43,11 +43,18 @@ class TimezoneMixin:
class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
model = None
permission_classes = [IsAuthenticated]
permission_classes = [
IsAuthenticated,
]
filter_backends = (DjangoFilterBackend, SearchFilter)
filter_backends = (
DjangoFilterBackend,
SearchFilter,
)
authentication_classes = [BaseSessionAuthentication]
authentication_classes = [
BaseSessionAuthentication,
]
filterset_fields = []
@@ -58,7 +65,9 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
return self.model.objects.all()
except Exception as e:
log_exception(e)
raise APIException("Please check the view", status.HTTP_400_BAD_REQUEST)
raise APIException(
"Please check the view", status.HTTP_400_BAD_REQUEST
)
def handle_exception(self, exc):
"""
@@ -137,24 +146,35 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
@property
def fields(self):
fields = [
field for field in self.request.GET.get("fields", "").split(",") if field
field
for field in self.request.GET.get("fields", "").split(",")
if field
]
return fields if fields else None
@property
def expand(self):
expand = [
expand for expand in self.request.GET.get("expand", "").split(",") if expand
expand
for expand in self.request.GET.get("expand", "").split(",")
if expand
]
return expand if expand else None
class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
permission_classes = [IsAuthenticated]
permission_classes = [
IsAuthenticated,
]
filter_backends = (DjangoFilterBackend, SearchFilter)
filter_backends = (
DjangoFilterBackend,
SearchFilter,
)
authentication_classes = [BaseSessionAuthentication]
authentication_classes = [
BaseSessionAuthentication,
]
filterset_fields = []
@@ -231,13 +251,17 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
@property
def fields(self):
fields = [
field for field in self.request.GET.get("fields", "").split(",") if field
field
for field in self.request.GET.get("fields", "").split(",")
if field
]
return fields if fields else None
@property
def expand(self):
expand = [
expand for expand in self.request.GET.get("expand", "").split(",") if expand
expand
for expand in self.request.GET.get("expand", "").split(",")
if expand
]
return expand if expand else None
+68 -40
View File
@@ -34,6 +34,7 @@ from .. import BaseAPIView
class CycleArchiveUnarchiveEndpoint(BaseAPIView):
def get_queryset(self):
favorite_subquery = UserFavorite.objects.filter(
user=self.request.user,
@@ -47,11 +48,12 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
estimate_point__estimate__type="points",
state__group="backlog",
issue_cycle__cycle_id=OuterRef("pk"),
issue_cycle__deleted_at__isnull=True,
)
.values("issue_cycle__cycle_id")
.annotate(
backlog_estimate_point=Sum(Cast("estimate_point__value", FloatField()))
backlog_estimate_point=Sum(
Cast("estimate_point__value", FloatField())
)
)
.values("backlog_estimate_point")[:1]
)
@@ -60,7 +62,6 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
estimate_point__estimate__type="points",
state__group="unstarted",
issue_cycle__cycle_id=OuterRef("pk"),
issue_cycle__deleted_at__isnull=True,
)
.values("issue_cycle__cycle_id")
.annotate(
@@ -75,11 +76,12 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
estimate_point__estimate__type="points",
state__group="started",
issue_cycle__cycle_id=OuterRef("pk"),
issue_cycle__deleted_at__isnull=True,
)
.values("issue_cycle__cycle_id")
.annotate(
started_estimate_point=Sum(Cast("estimate_point__value", FloatField()))
started_estimate_point=Sum(
Cast("estimate_point__value", FloatField())
)
)
.values("started_estimate_point")[:1]
)
@@ -88,7 +90,6 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
estimate_point__estimate__type="points",
state__group="cancelled",
issue_cycle__cycle_id=OuterRef("pk"),
issue_cycle__deleted_at__isnull=True,
)
.values("issue_cycle__cycle_id")
.annotate(
@@ -103,7 +104,6 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
estimate_point__estimate__type="points",
state__group="completed",
issue_cycle__cycle_id=OuterRef("pk"),
issue_cycle__deleted_at__isnull=True,
)
.values("issue_cycle__cycle_id")
.annotate(
@@ -117,11 +117,12 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
Issue.issue_objects.filter(
estimate_point__estimate__type="points",
issue_cycle__cycle_id=OuterRef("pk"),
issue_cycle__deleted_at__isnull=True,
)
.values("issue_cycle__cycle_id")
.annotate(
total_estimate_points=Sum(Cast("estimate_point__value", FloatField()))
total_estimate_points=Sum(
Cast("estimate_point__value", FloatField())
)
)
.values("total_estimate_points")[:1]
)
@@ -146,7 +147,9 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
.prefetch_related(
Prefetch(
"issue_cycle__issue__labels",
queryset=Label.objects.only("name", "color", "id").distinct(),
queryset=Label.objects.only(
"name", "color", "id"
).distinct(),
)
)
.annotate(is_favorite=Exists(favorite_subquery))
@@ -228,7 +231,9 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
& Q(end_date__gte=timezone.now()),
then=Value("CURRENT"),
),
When(start_date__gt=timezone.now(), then=Value("UPCOMING")),
When(
start_date__gt=timezone.now(), then=Value("UPCOMING")
),
When(end_date__lt=timezone.now(), then=Value("COMPLETED")),
When(
Q(start_date__isnull=True) & Q(end_date__isnull=True),
@@ -243,7 +248,9 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
ArrayAgg(
"issue_cycle__issue__assignees__id",
distinct=True,
filter=~Q(issue_cycle__issue__assignees__id__isnull=True),
filter=~Q(
issue_cycle__issue__assignees__id__isnull=True
),
),
Value([], output_field=ArrayField(UUIDField())),
)
@@ -252,42 +259,48 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
backlog_estimate_points=Coalesce(
Subquery(backlog_estimate_point),
Value(0, output_field=FloatField()),
)
),
)
.annotate(
unstarted_estimate_points=Coalesce(
Subquery(unstarted_estimate_point),
Value(0, output_field=FloatField()),
)
),
)
.annotate(
started_estimate_points=Coalesce(
Subquery(started_estimate_point),
Value(0, output_field=FloatField()),
)
),
)
.annotate(
cancelled_estimate_points=Coalesce(
Subquery(cancelled_estimate_point),
Value(0, output_field=FloatField()),
)
),
)
.annotate(
completed_estimate_points=Coalesce(
Subquery(completed_estimate_point),
Value(0, output_field=FloatField()),
)
),
)
.annotate(
total_estimate_points=Coalesce(
Subquery(total_estimate_point), Value(0, output_field=FloatField())
)
Subquery(total_estimate_point),
Value(0, output_field=FloatField()),
),
)
.order_by("-is_favorite", "name")
.distinct()
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
@allow_permission(
[
ROLE.ADMIN,
ROLE.MEMBER,
]
)
def get(self, request, slug, project_id, pk=None):
if pk is None:
queryset = (
@@ -323,7 +336,9 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
return Response(queryset, status=status.HTTP_200_OK)
else:
queryset = (
self.get_queryset().filter(archived_at__isnull=False).filter(pk=pk)
self.get_queryset()
.filter(archived_at__isnull=False)
.filter(pk=pk)
)
data = (
self.get_queryset()
@@ -333,7 +348,6 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
project_id=self.kwargs.get("project_id"),
parent__isnull=False,
issue_cycle__cycle_id=pk,
issue_cycle__deleted_at__isnull=True,
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
@@ -388,7 +402,6 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
assignee_distribution = (
Issue.issue_objects.filter(
issue_cycle__cycle_id=pk,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
)
@@ -416,7 +429,9 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
)
.values("display_name", "assignee_id", "avatar_url")
.annotate(
total_estimates=Sum(Cast("estimate_point__value", FloatField()))
total_estimates=Sum(
Cast("estimate_point__value", FloatField())
)
)
.annotate(
completed_estimates=Sum(
@@ -444,7 +459,6 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
label_distribution = (
Issue.issue_objects.filter(
issue_cycle__cycle_id=pk,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
)
@@ -453,7 +467,9 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
.annotate(label_id=F("labels__id"))
.values("label_name", "color", "label_id")
.annotate(
total_estimates=Sum(Cast("estimate_point__value", FloatField()))
total_estimates=Sum(
Cast("estimate_point__value", FloatField())
)
)
.annotate(
completed_estimates=Sum(
@@ -484,19 +500,20 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
}
if data["start_date"] and data["end_date"]:
data["estimate_distribution"]["completion_chart"] = burndown_plot(
queryset=queryset,
slug=slug,
project_id=project_id,
plot_type="points",
cycle_id=pk,
data["estimate_distribution"]["completion_chart"] = (
burndown_plot(
queryset=queryset,
slug=slug,
project_id=project_id,
plot_type="points",
cycle_id=pk,
)
)
# Assignee Distribution
assignee_distribution = (
Issue.issue_objects.filter(
issue_cycle__cycle_id=pk,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
)
@@ -533,8 +550,12 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
)
.annotate(
total_issues=Count(
"id", filter=Q(archived_at__isnull=True, is_draft=False)
)
"id",
filter=Q(
archived_at__isnull=True,
is_draft=False,
),
),
)
.annotate(
completed_issues=Count(
@@ -563,7 +584,6 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
label_distribution = (
Issue.issue_objects.filter(
issue_cycle__cycle_id=pk,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
)
@@ -573,8 +593,12 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
.values("label_name", "color", "label_id")
.annotate(
total_issues=Count(
"id", filter=Q(archived_at__isnull=True, is_draft=False)
)
"id",
filter=Q(
archived_at__isnull=True,
is_draft=False,
),
),
)
.annotate(
completed_issues=Count(
@@ -614,7 +638,10 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
cycle_id=pk,
)
return Response(data, status=status.HTTP_200_OK)
return Response(
data,
status=status.HTTP_200_OK,
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def post(self, request, slug, project_id, cycle_id):
@@ -637,7 +664,8 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
workspace__slug=slug,
).delete()
return Response(
{"archived_at": str(cycle.archived_at)}, status=status.HTTP_200_OK
{"archived_at": str(cycle.archived_at)},
status=status.HTTP_200_OK,
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
+111 -63
View File
@@ -89,7 +89,9 @@ class CycleViewSet(BaseViewSet):
.prefetch_related(
Prefetch(
"issue_cycle__issue__labels",
queryset=Label.objects.only("name", "color", "id").distinct(),
queryset=Label.objects.only(
"name", "color", "id"
).distinct(),
)
)
.annotate(is_favorite=Exists(favorite_subquery))
@@ -100,7 +102,7 @@ class CycleViewSet(BaseViewSet):
filter=Q(
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
issue_cycle__issue__deleted_at__isnull=True,
),
)
)
@@ -112,7 +114,7 @@ class CycleViewSet(BaseViewSet):
issue_cycle__issue__state__group="completed",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
issue_cycle__issue__deleted_at__isnull=True,
),
)
)
@@ -123,7 +125,9 @@ class CycleViewSet(BaseViewSet):
& Q(end_date__gte=timezone.now()),
then=Value("CURRENT"),
),
When(start_date__gt=timezone.now(), then=Value("UPCOMING")),
When(
start_date__gt=timezone.now(), then=Value("UPCOMING")
),
When(end_date__lt=timezone.now(), then=Value("COMPLETED")),
When(
Q(start_date__isnull=True) & Q(end_date__isnull=True),
@@ -138,7 +142,9 @@ class CycleViewSet(BaseViewSet):
ArrayAgg(
"issue_cycle__issue__assignees__id",
distinct=True,
filter=~Q(issue_cycle__issue__assignees__id__isnull=True)
filter=~Q(
issue_cycle__issue__assignees__id__isnull=True
)
& (
Q(
issue_cycle__issue__issue_assignee__deleted_at__isnull=True
@@ -163,7 +169,8 @@ class CycleViewSet(BaseViewSet):
# Current Cycle
if cycle_view == "current":
queryset = queryset.filter(
start_date__lte=timezone.now(), end_date__gte=timezone.now()
start_date__lte=timezone.now(),
end_date__gte=timezone.now(),
)
data = queryset.values(
@@ -234,7 +241,10 @@ class CycleViewSet(BaseViewSet):
):
serializer = CycleWriteSerializer(data=request.data)
if serializer.is_valid():
serializer.save(project_id=project_id, owned_by=request.user)
serializer.save(
project_id=project_id,
owned_by=request.user,
)
cycle = (
self.get_queryset()
.filter(pk=serializer.data["id"])
@@ -278,7 +288,9 @@ class CycleViewSet(BaseViewSet):
origin=request.META.get("HTTP_ORIGIN"),
)
return Response(cycle, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
else:
return Response(
{
@@ -309,7 +321,9 @@ class CycleViewSet(BaseViewSet):
if "sort_order" in request_data:
# Can only change sort order for a completed cycle``
request_data = {
"sort_order": request_data.get("sort_order", cycle.sort_order)
"sort_order": request_data.get(
"sort_order", cycle.sort_order
)
}
else:
return Response(
@@ -319,7 +333,9 @@ class CycleViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST,
)
serializer = CycleWriteSerializer(cycle, data=request.data, partial=True)
serializer = CycleWriteSerializer(
cycle, data=request.data, partial=True
)
if serializer.is_valid():
serializer.save()
cycle = queryset.values(
@@ -363,9 +379,16 @@ class CycleViewSet(BaseViewSet):
return Response(cycle, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
@allow_permission(
[
ROLE.ADMIN,
ROLE.MEMBER,
]
)
def retrieve(self, request, slug, project_id, pk):
queryset = self.get_queryset().filter(archived_at__isnull=True).filter(pk=pk)
queryset = (
self.get_queryset().filter(archived_at__isnull=True).filter(pk=pk)
)
data = (
self.get_queryset()
.filter(pk=pk)
@@ -375,7 +398,6 @@ class CycleViewSet(BaseViewSet):
project_id=self.kwargs.get("project_id"),
parent__isnull=False,
issue_cycle__cycle_id=pk,
issue_cycle__deleted_at__isnull=True,
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
@@ -413,7 +435,8 @@ class CycleViewSet(BaseViewSet):
if data is None:
return Response(
{"error": "Cycle not found"}, status=status.HTTP_404_NOT_FOUND
{"error": "Cycle not found"},
status=status.HTTP_404_NOT_FOUND,
)
queryset = queryset.first()
@@ -425,11 +448,16 @@ class CycleViewSet(BaseViewSet):
user_id=request.user.id,
project_id=project_id,
)
return Response(data, status=status.HTTP_200_OK)
return Response(
data,
status=status.HTTP_200_OK,
)
@allow_permission([ROLE.ADMIN], creator=True, model=Cycle)
def destroy(self, request, slug, project_id, pk):
cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
cycle = Cycle.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk
)
if cycle.owned_by_id != request.user.id and not (
ProjectMember.objects.filter(
workspace__slug=slug,
@@ -445,9 +473,9 @@ class CycleViewSet(BaseViewSet):
)
cycle_issues = list(
CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list(
"issue", flat=True
)
CycleIssue.objects.filter(
cycle_id=self.kwargs.get("pk")
).values_list("issue", flat=True)
)
issue_activity.delay(
@@ -573,7 +601,6 @@ class TransferCycleIssueEndpoint(BaseAPIView):
filter=Q(
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -585,7 +612,6 @@ class TransferCycleIssueEndpoint(BaseAPIView):
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__issue__deleted_at__isnull=True,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -597,7 +623,6 @@ class TransferCycleIssueEndpoint(BaseAPIView):
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__issue__deleted_at__isnull=True,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -609,7 +634,6 @@ class TransferCycleIssueEndpoint(BaseAPIView):
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__issue__deleted_at__isnull=True,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -621,7 +645,6 @@ class TransferCycleIssueEndpoint(BaseAPIView):
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__issue__deleted_at__isnull=True,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -633,7 +656,6 @@ class TransferCycleIssueEndpoint(BaseAPIView):
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__issue__deleted_at__isnull=True,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -650,7 +672,6 @@ class TransferCycleIssueEndpoint(BaseAPIView):
assignee_estimate_data = (
Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
)
@@ -678,7 +699,9 @@ class TransferCycleIssueEndpoint(BaseAPIView):
)
.values("display_name", "assignee_id", "avatar_url")
.annotate(
total_estimates=Sum(Cast("estimate_point__value", FloatField()))
total_estimates=Sum(
Cast("estimate_point__value", FloatField())
)
)
.annotate(
completed_estimates=Sum(
@@ -707,7 +730,9 @@ class TransferCycleIssueEndpoint(BaseAPIView):
{
"display_name": item["display_name"],
"assignee_id": (
str(item["assignee_id"]) if item["assignee_id"] else None
str(item["assignee_id"])
if item["assignee_id"]
else None
),
"avatar": item.get("avatar"),
"avatar_url": item.get("avatar_url"),
@@ -721,7 +746,6 @@ class TransferCycleIssueEndpoint(BaseAPIView):
label_distribution_data = (
Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
)
@@ -730,7 +754,9 @@ class TransferCycleIssueEndpoint(BaseAPIView):
.annotate(label_id=F("labels__id"))
.values("label_name", "color", "label_id")
.annotate(
total_estimates=Sum(Cast("estimate_point__value", FloatField()))
total_estimates=Sum(
Cast("estimate_point__value", FloatField())
)
)
.annotate(
completed_estimates=Sum(
@@ -767,7 +793,9 @@ class TransferCycleIssueEndpoint(BaseAPIView):
{
"label_name": item["label_name"],
"color": item["color"],
"label_id": (str(item["label_id"]) if item["label_id"] else None),
"label_id": (
str(item["label_id"]) if item["label_id"] else None
),
"total_estimates": item["total_estimates"],
"completed_estimates": item["completed_estimates"],
"pending_estimates": item["pending_estimates"],
@@ -779,7 +807,6 @@ class TransferCycleIssueEndpoint(BaseAPIView):
assignee_distribution = (
Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
)
@@ -798,7 +825,8 @@ class TransferCycleIssueEndpoint(BaseAPIView):
),
# If `avatar_asset` is None, fall back to using `avatar` field directly
When(
assignees__avatar_asset__isnull=True, then="assignees__avatar"
assignees__avatar_asset__isnull=True,
then="assignees__avatar",
),
default=Value(None),
output_field=models.CharField(),
@@ -807,8 +835,12 @@ class TransferCycleIssueEndpoint(BaseAPIView):
.values("display_name", "assignee_id", "avatar_url")
.annotate(
total_issues=Count(
"id", filter=Q(archived_at__isnull=True, is_draft=False)
)
"id",
filter=Q(
archived_at__isnull=True,
is_draft=False,
),
),
)
.annotate(
completed_issues=Count(
@@ -852,7 +884,6 @@ class TransferCycleIssueEndpoint(BaseAPIView):
label_distribution = (
Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
)
@@ -862,8 +893,12 @@ class TransferCycleIssueEndpoint(BaseAPIView):
.values("label_name", "color", "label_id")
.annotate(
total_issues=Count(
"id", filter=Q(archived_at__isnull=True, is_draft=False)
)
"id",
filter=Q(
archived_at__isnull=True,
is_draft=False,
),
),
)
.annotate(
completed_issues=Count(
@@ -893,7 +928,9 @@ class TransferCycleIssueEndpoint(BaseAPIView):
{
"label_name": item["label_name"],
"color": item["color"],
"label_id": (str(item["label_id"]) if item["label_id"] else None),
"label_id": (
str(item["label_id"]) if item["label_id"] else None
),
"total_issues": item["total_issues"],
"completed_issues": item["completed_issues"],
"pending_issues": item["pending_issues"],
@@ -938,7 +975,10 @@ class TransferCycleIssueEndpoint(BaseAPIView):
}
current_cycle.save(update_fields=["progress_snapshot"])
if new_cycle.end_date is not None and new_cycle.end_date < timezone.now():
if (
new_cycle.end_date is not None
and new_cycle.end_date < timezone.now()
):
return Response(
{
"error": "The cycle where the issues are transferred is already completed"
@@ -1001,7 +1041,9 @@ class CycleUserPropertiesEndpoint(BaseAPIView):
workspace__slug=slug,
)
cycle_properties.filters = request.data.get("filters", cycle_properties.filters)
cycle_properties.filters = request.data.get(
"filters", cycle_properties.filters
)
cycle_properties.display_filters = request.data.get(
"display_filters", cycle_properties.display_filters
)
@@ -1032,11 +1074,12 @@ class CycleProgressEndpoint(BaseAPIView):
Issue.issue_objects.filter(
estimate_point__estimate__type="points",
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
)
.annotate(value_as_float=Cast("estimate_point__value", FloatField()))
.annotate(
value_as_float=Cast("estimate_point__value", FloatField())
)
.aggregate(
backlog_estimate_point=Sum(
Case(
@@ -1074,14 +1117,15 @@ class CycleProgressEndpoint(BaseAPIView):
)
),
total_estimate_points=Sum(
"value_as_float", default=Value(0), output_field=FloatField()
"value_as_float",
default=Value(0),
output_field=FloatField(),
),
)
)
backlog_issues = Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
state__group="backlog",
@@ -1089,7 +1133,6 @@ class CycleProgressEndpoint(BaseAPIView):
unstarted_issues = Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
state__group="unstarted",
@@ -1097,7 +1140,6 @@ class CycleProgressEndpoint(BaseAPIView):
started_issues = Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
state__group="started",
@@ -1105,7 +1147,6 @@ class CycleProgressEndpoint(BaseAPIView):
cancelled_issues = Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
state__group="cancelled",
@@ -1113,7 +1154,6 @@ class CycleProgressEndpoint(BaseAPIView):
completed_issues = Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
state__group="completed",
@@ -1121,20 +1161,23 @@ class CycleProgressEndpoint(BaseAPIView):
total_issues = Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
).count()
return Response(
{
"backlog_estimate_points": aggregate_estimates["backlog_estimate_point"]
"backlog_estimate_points": aggregate_estimates[
"backlog_estimate_point"
]
or 0,
"unstarted_estimate_points": aggregate_estimates[
"unstarted_estimate_point"
]
or 0,
"started_estimate_points": aggregate_estimates["started_estimate_point"]
"started_estimate_points": aggregate_estimates[
"started_estimate_point"
]
or 0,
"cancelled_estimate_points": aggregate_estimates[
"cancelled_estimate_point"
@@ -1144,7 +1187,9 @@ class CycleProgressEndpoint(BaseAPIView):
"completed_estimate_points"
]
or 0,
"total_estimate_points": aggregate_estimates["total_estimate_points"],
"total_estimate_points": aggregate_estimates[
"total_estimate_points"
],
"backlog_issues": backlog_issues,
"total_issues": total_issues,
"completed_issues": completed_issues,
@@ -1157,12 +1202,15 @@ class CycleProgressEndpoint(BaseAPIView):
class CycleAnalyticsEndpoint(BaseAPIView):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def get(self, request, slug, project_id, cycle_id):
analytic_type = request.GET.get("type", "issues")
cycle = (
Cycle.objects.filter(
workspace__slug=slug, project_id=project_id, id=cycle_id
workspace__slug=slug,
project_id=project_id,
id=cycle_id,
)
.annotate(
total_issues=Count(
@@ -1172,7 +1220,6 @@ class CycleAnalyticsEndpoint(BaseAPIView):
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__issue__deleted_at__isnull=True,
issue_cycle__deleted_at__isnull=True,
),
)
)
@@ -1200,7 +1247,6 @@ class CycleAnalyticsEndpoint(BaseAPIView):
assignee_distribution = (
Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
)
@@ -1228,7 +1274,9 @@ class CycleAnalyticsEndpoint(BaseAPIView):
)
.values("display_name", "assignee_id", "avatar_url")
.annotate(
total_estimates=Sum(Cast("estimate_point__value", FloatField()))
total_estimates=Sum(
Cast("estimate_point__value", FloatField())
)
)
.annotate(
completed_estimates=Sum(
@@ -1256,7 +1304,6 @@ class CycleAnalyticsEndpoint(BaseAPIView):
label_distribution = (
Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
)
@@ -1265,7 +1312,9 @@ class CycleAnalyticsEndpoint(BaseAPIView):
.annotate(label_id=F("labels__id"))
.values("label_name", "color", "label_id")
.annotate(
total_estimates=Sum(Cast("estimate_point__value", FloatField()))
total_estimates=Sum(
Cast("estimate_point__value", FloatField())
)
)
.annotate(
completed_estimates=Sum(
@@ -1301,7 +1350,6 @@ class CycleAnalyticsEndpoint(BaseAPIView):
assignee_distribution = (
Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
project_id=project_id,
workspace__slug=slug,
)
@@ -1332,7 +1380,7 @@ class CycleAnalyticsEndpoint(BaseAPIView):
total_issues=Count(
"assignee_id",
filter=Q(archived_at__isnull=True, is_draft=False),
)
),
)
.annotate(
completed_issues=Count(
@@ -1360,7 +1408,6 @@ class CycleAnalyticsEndpoint(BaseAPIView):
label_distribution = (
Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
project_id=project_id,
workspace__slug=slug,
)
@@ -1370,7 +1417,8 @@ class CycleAnalyticsEndpoint(BaseAPIView):
.values("label_name", "color", "label_id")
.annotate(
total_issues=Count(
"label_id", filter=Q(archived_at__isnull=True, is_draft=False)
"label_id",
filter=Q(archived_at__isnull=True, is_draft=False),
)
)
.annotate(
+65 -28
View File
@@ -12,12 +12,19 @@ from django.views.decorators.gzip import gzip_page
from rest_framework import status
from rest_framework.response import Response
# Module imports
from .. import BaseViewSet
from plane.app.serializers import CycleIssueSerializer
from plane.app.serializers import (
CycleIssueSerializer,
)
from plane.bgtasks.issue_activities_task import issue_activity
from plane.db.models import Cycle, CycleIssue, Issue, FileAsset, IssueLink
from plane.db.models import (
Cycle,
CycleIssue,
Issue,
FileAsset,
IssueLink,
)
from plane.utils.grouper import (
issue_group_values,
issue_on_results,
@@ -25,7 +32,10 @@ from plane.utils.grouper import (
)
from plane.utils.issue_filters import issue_filters
from plane.utils.order_queryset import order_issue_queryset
from plane.utils.paginator import GroupedOffsetPaginator, SubGroupedOffsetPaginator
from plane.utils.paginator import (
GroupedOffsetPaginator,
SubGroupedOffsetPaginator,
)
from plane.app.permissions import allow_permission, ROLE
@@ -36,14 +46,19 @@ class CycleIssueViewSet(BaseViewSet):
webhook_event = "cycle_issue"
bulk = True
filterset_fields = ["issue__labels__id", "issue__assignees__id"]
filterset_fields = [
"issue__labels__id",
"issue__assignees__id",
]
def get_queryset(self):
return self.filter_queryset(
super()
.get_queryset()
.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("issue_id"))
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("issue_id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -65,20 +80,26 @@ class CycleIssueViewSet(BaseViewSet):
)
@method_decorator(gzip_page)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
@allow_permission(
[
ROLE.ADMIN,
ROLE.MEMBER,
]
)
def list(self, request, slug, project_id, cycle_id):
order_by_param = request.GET.get("order_by", "created_at")
filters = issue_filters(request.query_params, "GET")
issue_queryset = (
Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id, issue_cycle__deleted_at__isnull=True
)
Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id)
.filter(project_id=project_id)
.filter(workspace__slug=slug)
.filter(**filters)
.select_related("workspace", "project", "state", "parent")
.prefetch_related(
"assignees", "labels", "issue_module__module", "issue_cycle__cycle"
"assignees",
"labels",
"issue_module__module",
"issue_cycle__cycle",
)
.filter(**filters)
.annotate(
@@ -104,7 +125,9 @@ class CycleIssueViewSet(BaseViewSet):
.values("count")
)
.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -116,7 +139,8 @@ class CycleIssueViewSet(BaseViewSet):
issue_queryset = issue_queryset.filter(**filters)
# Issue queryset
issue_queryset, order_by_param = order_issue_queryset(
issue_queryset=issue_queryset, order_by_param=order_by_param
issue_queryset=issue_queryset,
order_by_param=order_by_param,
)
# Group by
@@ -125,7 +149,9 @@ class CycleIssueViewSet(BaseViewSet):
# issue queryset
issue_queryset = issue_queryset_grouper(
queryset=issue_queryset, group_by=group_by, sub_group_by=sub_group_by
queryset=issue_queryset,
group_by=group_by,
sub_group_by=sub_group_by,
)
if group_by:
@@ -145,7 +171,9 @@ class CycleIssueViewSet(BaseViewSet):
order_by=order_by_param,
queryset=issue_queryset,
on_results=lambda issues: issue_on_results(
group_by=group_by, issues=issues, sub_group_by=sub_group_by
group_by=group_by,
issues=issues,
sub_group_by=sub_group_by,
),
paginator_cls=SubGroupedOffsetPaginator,
group_by_fields=issue_group_values(
@@ -163,10 +191,10 @@ class CycleIssueViewSet(BaseViewSet):
group_by_field_name=group_by,
sub_group_by_field_name=sub_group_by,
count_filter=Q(
Q(issue_intake__status=1)
| Q(issue_intake__status=-1)
| Q(issue_intake__status=2)
| Q(issue_intake__isnull=True),
Q(issue_inbox__status=1)
| Q(issue_inbox__status=-1)
| Q(issue_inbox__status=2)
| Q(issue_inbox__isnull=True),
archived_at__isnull=True,
is_draft=False,
),
@@ -179,7 +207,9 @@ class CycleIssueViewSet(BaseViewSet):
order_by=order_by_param,
queryset=issue_queryset,
on_results=lambda issues: issue_on_results(
group_by=group_by, issues=issues, sub_group_by=sub_group_by
group_by=group_by,
issues=issues,
sub_group_by=sub_group_by,
),
paginator_cls=GroupedOffsetPaginator,
group_by_fields=issue_group_values(
@@ -190,10 +220,10 @@ class CycleIssueViewSet(BaseViewSet):
),
group_by_field_name=group_by,
count_filter=Q(
Q(issue_intake__status=1)
| Q(issue_intake__status=-1)
| Q(issue_intake__status=2)
| Q(issue_intake__isnull=True),
Q(issue_inbox__status=1)
| Q(issue_inbox__status=-1)
| Q(issue_inbox__status=2)
| Q(issue_inbox__isnull=True),
archived_at__isnull=True,
is_draft=False,
),
@@ -215,7 +245,8 @@ class CycleIssueViewSet(BaseViewSet):
if not issues:
return Response(
{"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST
{"error": "Issues are required"},
status=status.HTTP_400_BAD_REQUEST,
)
cycle = Cycle.objects.get(
@@ -232,9 +263,13 @@ class CycleIssueViewSet(BaseViewSet):
# Get all CycleIssues already created
cycle_issues = list(
CycleIssue.objects.filter(~Q(cycle_id=cycle_id), issue_id__in=issues)
CycleIssue.objects.filter(
~Q(cycle_id=cycle_id), issue_id__in=issues
)
)
existing_issues = [str(cycle_issue.issue_id) for cycle_issue in cycle_issues]
existing_issues = [
str(cycle_issue.issue_id) for cycle_issue in cycle_issues
]
new_issues = list(set(issues) - set(existing_issues))
# New issues to create
@@ -273,7 +308,9 @@ class CycleIssueViewSet(BaseViewSet):
)
# Update the cycle issues
CycleIssue.objects.bulk_update(updated_records, ["cycle_id"], batch_size=100)
CycleIssue.objects.bulk_update(
updated_records, ["cycle_id"], batch_size=100
)
# Capture Issue Activity
issue_activity.delay(
type="cycle.activity.created",
+65 -23
View File
@@ -227,7 +227,7 @@ def dashboard_assigned_issues(self, request, slug):
distinct=True,
filter=Q(
~Q(labels__id__isnull=True)
& Q(label_issue__deleted_at__isnull=True)
& Q(label_issue__deleted_at__isnull=True),
),
),
Value([], output_field=ArrayField(UUIDField())),
@@ -260,7 +260,10 @@ def dashboard_assigned_issues(self, request, slug):
)
if WorkspaceMember.objects.filter(
workspace__slug=slug, member=request.user, role=5, is_active=True
workspace__slug=slug,
member=request.user,
role=5,
is_active=True,
).exists():
assigned_issues = assigned_issues.filter(created_by=request.user)
@@ -268,7 +271,10 @@ def dashboard_assigned_issues(self, request, slug):
priority_order = ["urgent", "high", "medium", "low", "none"]
assigned_issues = assigned_issues.annotate(
priority_order=Case(
*[When(priority=p, then=Value(i)) for i, p in enumerate(priority_order)],
*[
When(priority=p, then=Value(i))
for i, p in enumerate(priority_order)
],
output_field=CharField(),
)
).order_by("priority_order")
@@ -294,7 +300,9 @@ def dashboard_assigned_issues(self, request, slug):
completed_issues_count = assigned_issues.filter(
state__group__in=["completed"]
).count()
completed_issues = assigned_issues.filter(state__group__in=["completed"])[:5]
completed_issues = assigned_issues.filter(
state__group__in=["completed"]
)[:5]
return Response(
{
"issues": IssueSerializer(
@@ -399,7 +407,7 @@ def dashboard_created_issues(self, request, slug):
distinct=True,
filter=Q(
~Q(labels__id__isnull=True)
& Q(label_issue__deleted_at__isnull=True)
& Q(label_issue__deleted_at__isnull=True),
),
),
Value([], output_field=ArrayField(UUIDField())),
@@ -436,7 +444,10 @@ def dashboard_created_issues(self, request, slug):
priority_order = ["urgent", "high", "medium", "low", "none"]
created_issues = created_issues.annotate(
priority_order=Case(
*[When(priority=p, then=Value(i)) for i, p in enumerate(priority_order)],
*[
When(priority=p, then=Value(i))
for i, p in enumerate(priority_order)
],
output_field=CharField(),
)
).order_by("priority_order")
@@ -462,7 +473,9 @@ def dashboard_created_issues(self, request, slug):
completed_issues_count = created_issues.filter(
state__group__in=["completed"]
).count()
completed_issues = created_issues.filter(state__group__in=["completed"])[:5]
completed_issues = created_issues.filter(
state__group__in=["completed"]
)[:5]
return Response(
{
"issues": IssueSerializer(completed_issues, many=True).data,
@@ -517,7 +530,10 @@ def dashboard_issues_by_state_groups(self, request, slug):
extra_filters = {}
if WorkspaceMember.objects.filter(
workspace__slug=slug, member=request.user, role=5, is_active=True
workspace__slug=slug,
member=request.user,
role=5,
is_active=True,
).exists():
extra_filters = {"created_by": request.user}
@@ -554,7 +570,10 @@ def dashboard_issues_by_priority(self, request, slug):
extra_filters = {}
if WorkspaceMember.objects.filter(
workspace__slug=slug, member=request.user, role=5, is_active=True
workspace__slug=slug,
member=request.user,
role=5,
is_active=True,
).exists():
extra_filters = {"created_by": request.user}
@@ -579,7 +598,8 @@ def dashboard_issues_by_priority(self, request, slug):
# Prepare output including all groups with their counts
output_data = [
{"priority": group, "count": count} for group, count in all_groups.items()
{"priority": group, "count": count}
for group, count in all_groups.items()
]
return Response(output_data, status=status.HTTP_200_OK)
@@ -596,7 +616,8 @@ def dashboard_recent_activity(self, request, slug):
).select_related("actor", "workspace", "issue", "project")[:8]
return Response(
IssueActivitySerializer(queryset, many=True).data, status=status.HTTP_200_OK
IssueActivitySerializer(queryset, many=True).data,
status=status.HTTP_200_OK,
)
@@ -626,14 +647,22 @@ def dashboard_recent_projects(self, request, slug):
).exclude(id__in=unique_project_ids)
# Append additional project IDs to the existing list
unique_project_ids.update(additional_projects.values_list("id", flat=True))
unique_project_ids.update(
additional_projects.values_list("id", flat=True)
)
return Response(list(unique_project_ids)[:4], status=status.HTTP_200_OK)
return Response(
list(unique_project_ids)[:4],
status=status.HTTP_200_OK,
)
def dashboard_recent_collaborators(self, request, slug):
project_members_with_activities = (
WorkspaceMember.objects.filter(workspace__slug=slug, is_active=True)
WorkspaceMember.objects.filter(
workspace__slug=slug,
is_active=True,
)
.annotate(
active_issue_count=Count(
Case(
@@ -658,7 +687,10 @@ def dashboard_recent_collaborators(self, request, slug):
.order_by("-active_issue_count")
.distinct()
)
return Response((project_members_with_activities), status=status.HTTP_200_OK)
return Response(
(project_members_with_activities),
status=status.HTTP_200_OK,
)
class DashboardEndpoint(BaseAPIView):
@@ -707,13 +739,14 @@ class DashboardEndpoint(BaseAPIView):
updated_dashboard_widgets = []
for widget_key in widgets_to_fetch:
widget = Widget.objects.filter(key=widget_key).values_list(
"id", flat=True
)
widget = Widget.objects.filter(
key=widget_key
).values_list("id", flat=True)
if widget:
updated_dashboard_widgets.append(
DashboardWidget(
widget_id=widget, dashboard_id=dashboard.id
widget_id=widget,
dashboard_id=dashboard.id,
)
)
@@ -780,7 +813,11 @@ class DashboardEndpoint(BaseAPIView):
func = WIDGETS_MAPPER.get(widget_key)
if func is not None:
response = func(self, request=request, slug=slug)
response = func(
self,
request=request,
slug=slug,
)
if isinstance(response, Response):
return response
@@ -793,7 +830,8 @@ class DashboardEndpoint(BaseAPIView):
class WidgetsEndpoint(BaseAPIView):
def patch(self, request, dashboard_id, widget_id):
dashboard_widget = DashboardWidget.objects.filter(
widget_id=widget_id, dashboard_id=dashboard_id
widget_id=widget_id,
dashboard_id=dashboard_id,
).first()
dashboard_widget.is_visible = request.data.get(
"is_visible", dashboard_widget.is_visible
@@ -801,6 +839,10 @@ class WidgetsEndpoint(BaseAPIView):
dashboard_widget.sort_order = request.data.get(
"sort_order", dashboard_widget.sort_order
)
dashboard_widget.filters = request.data.get("filters", dashboard_widget.filters)
dashboard_widget.filters = request.data.get(
"filters", dashboard_widget.filters
)
dashboard_widget.save()
return Response({"message": "successfully updated"}, status=status.HTTP_200_OK)
return Response(
{"message": "successfully updated"}, status=status.HTTP_200_OK
)
-1
View File
@@ -1,6 +1,5 @@
# views.py
from django.http import JsonResponse
def custom_404_view(request, exception=None):
return JsonResponse({"error": "Page not found."}, status=404)
+75 -23
View File
@@ -11,7 +11,11 @@ from rest_framework import status
# Module imports
from ..base import BaseViewSet, BaseAPIView
from plane.app.permissions import ProjectEntityPermission, allow_permission, ROLE
from plane.app.permissions import (
ProjectEntityPermission,
allow_permission,
ROLE,
)
from plane.db.models import Project, Estimate, EstimatePoint, Issue
from plane.app.serializers import (
EstimateSerializer,
@@ -28,7 +32,13 @@ def generate_random_name(length=10):
class ProjectEstimatePointEndpoint(BaseAPIView):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
@allow_permission(
[
ROLE.ADMIN,
ROLE.MEMBER,
]
)
def get(self, request, slug, project_id):
project = Project.objects.get(workspace__slug=slug, pk=project_id)
if project.estimate_id is not None:
@@ -43,13 +53,17 @@ class ProjectEstimatePointEndpoint(BaseAPIView):
class BulkEstimatePointEndpoint(BaseViewSet):
permission_classes = [ProjectEntityPermission]
permission_classes = [
ProjectEntityPermission,
]
model = Estimate
serializer_class = EstimateSerializer
def list(self, request, slug, project_id):
estimates = (
Estimate.objects.filter(workspace__slug=slug, project_id=project_id)
Estimate.objects.filter(
workspace__slug=slug, project_id=project_id
)
.prefetch_related("points")
.select_related("workspace", "project")
)
@@ -77,7 +91,9 @@ class BulkEstimatePointEndpoint(BaseViewSet):
data=request.data.get("estimate_points"), many=True
)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
estimate_points = EstimatePoint.objects.bulk_create(
[
@@ -105,12 +121,16 @@ class BulkEstimatePointEndpoint(BaseViewSet):
pk=estimate_id, workspace__slug=slug, project_id=project_id
)
serializer = EstimateReadSerializer(estimate)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(
serializer.data,
status=status.HTTP_200_OK,
)
@invalidate_cache(
path="/api/workspaces/:slug/estimates/", url_params=True, user=False
)
def partial_update(self, request, slug, project_id, estimate_id):
if not len(request.data.get("estimate_points", [])):
return Response(
{"error": "Estimate points are required"},
@@ -120,15 +140,20 @@ class BulkEstimatePointEndpoint(BaseViewSet):
estimate = Estimate.objects.get(pk=estimate_id)
if request.data.get("estimate"):
estimate.name = request.data.get("estimate").get("name", estimate.name)
estimate.type = request.data.get("estimate").get("type", estimate.type)
estimate.name = request.data.get("estimate").get(
"name", estimate.name
)
estimate.type = request.data.get("estimate").get(
"type", estimate.type
)
estimate.save()
estimate_points_data = request.data.get("estimate_points", [])
estimate_points = EstimatePoint.objects.filter(
pk__in=[
estimate_point.get("id") for estimate_point in estimate_points_data
estimate_point.get("id")
for estimate_point in estimate_points_data
],
workspace__slug=slug,
project_id=project_id,
@@ -153,11 +178,16 @@ class BulkEstimatePointEndpoint(BaseViewSet):
updated_estimate_points.append(estimate_point)
EstimatePoint.objects.bulk_update(
updated_estimate_points, ["key", "value"], batch_size=10
updated_estimate_points,
["key", "value"],
batch_size=10,
)
estimate_serializer = EstimateReadSerializer(estimate)
return Response(estimate_serializer.data, status=status.HTTP_200_OK)
return Response(
estimate_serializer.data,
status=status.HTTP_200_OK,
)
@invalidate_cache(
path="/api/workspaces/:slug/estimates/", url_params=True, user=False
@@ -171,6 +201,7 @@ class BulkEstimatePointEndpoint(BaseViewSet):
class EstimatePointEndpoint(BaseViewSet):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def create(self, request, slug, project_id, estimate_id):
# TODO: add a key validation if the same key already exists
@@ -182,13 +213,18 @@ class EstimatePointEndpoint(BaseViewSet):
key = request.data.get("key", 0)
value = request.data.get("value", "")
estimate_point = EstimatePoint.objects.create(
estimate_id=estimate_id, project_id=project_id, key=key, value=value
estimate_id=estimate_id,
project_id=project_id,
key=key,
value=value,
)
serializer = EstimatePointSerializer(estimate_point).data
return Response(serializer, status=status.HTTP_200_OK)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def partial_update(self, request, slug, project_id, estimate_id, estimate_point_id):
def partial_update(
self, request, slug, project_id, estimate_id, estimate_point_id
):
# TODO: add a key validation if the same key already exists
estimate_point = EstimatePoint.objects.get(
pk=estimate_point_id,
@@ -200,15 +236,21 @@ class EstimatePointEndpoint(BaseViewSet):
estimate_point, data=request.data, partial=True
)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def destroy(self, request, slug, project_id, estimate_id, estimate_point_id):
def destroy(
self, request, slug, project_id, estimate_id, estimate_point_id
):
new_estimate_id = request.data.get("new_estimate_id", None)
estimate_points = EstimatePoint.objects.filter(
estimate_id=estimate_id, project_id=project_id, workspace__slug=slug
estimate_id=estimate_id,
project_id=project_id,
workspace__slug=slug,
)
# update all the issues with the new estimate
if new_estimate_id:
@@ -223,8 +265,10 @@ class EstimatePointEndpoint(BaseViewSet):
requested_data=json.dumps(
{
"estimate_point": (
str(new_estimate_id) if new_estimate_id else None
)
str(new_estimate_id)
if new_estimate_id
else None
),
}
),
actor_id=str(request.user.id),
@@ -236,7 +280,7 @@ class EstimatePointEndpoint(BaseViewSet):
str(issue.estimate_point_id)
if issue.estimate_point_id
else None
)
),
}
),
epoch=int(timezone.now().timestamp()),
@@ -251,7 +295,11 @@ class EstimatePointEndpoint(BaseViewSet):
for issue in issues:
issue_activity.delay(
type="issue.activity.updated",
requested_data=json.dumps({"estimate_point": None}),
requested_data=json.dumps(
{
"estimate_point": None,
}
),
actor_id=str(request.user.id),
issue_id=issue.id,
project_id=str(project_id),
@@ -261,14 +309,16 @@ class EstimatePointEndpoint(BaseViewSet):
str(issue.estimate_point_id)
if issue.estimate_point_id
else None
)
),
}
),
epoch=int(timezone.now().timestamp()),
)
# delete the estimate point
old_estimate_point = EstimatePoint.objects.filter(pk=estimate_point_id).first()
old_estimate_point = EstimatePoint.objects.filter(
pk=estimate_point_id
).first()
# rearrange the estimate points
updated_estimate_points = []
@@ -278,7 +328,9 @@ class EstimatePointEndpoint(BaseViewSet):
updated_estimate_points.append(estimate_point)
EstimatePoint.objects.bulk_update(
updated_estimate_points, ["key"], batch_size=10
updated_estimate_points,
["key"],
batch_size=10,
)
old_estimate_point.delete()
+11 -4
View File
@@ -51,7 +51,9 @@ class ExportIssuesEndpoint(BaseAPIView):
slug=slug,
)
return Response(
{"message": "Once the export is ready you will be able to download it"},
{
"message": "Once the export is ready you will be able to download it"
},
status=status.HTTP_200_OK,
)
else:
@@ -60,13 +62,18 @@ class ExportIssuesEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST,
)
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
@allow_permission(
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE"
)
def get(self, request, slug):
exporter_history = ExporterHistory.objects.filter(
workspace__slug=slug, type="issue_exports"
workspace__slug=slug,
type="issue_exports",
).select_related("workspace", "initiated_by")
if request.GET.get("per_page", False) and request.GET.get("cursor", False):
if request.GET.get("per_page", False) and request.GET.get(
"cursor", False
):
return self.paginate(
order_by=request.GET.get("order_by", "-created_at"),
request=request,
+31 -10
View File
@@ -13,11 +13,15 @@ from rest_framework import status
from ..base import BaseAPIView
from plane.app.permissions import allow_permission, ROLE
from plane.db.models import Workspace, Project
from plane.app.serializers import ProjectLiteSerializer, WorkspaceLiteSerializer
from plane.app.serializers import (
ProjectLiteSerializer,
WorkspaceLiteSerializer,
)
from plane.license.utils.instance_value import get_configuration_value
class GPTIntegrationEndpoint(BaseAPIView):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def post(self, request, slug, project_id):
OPENAI_API_KEY, GPT_ENGINE = get_configuration_value(
@@ -46,15 +50,19 @@ class GPTIntegrationEndpoint(BaseAPIView):
if not task:
return Response(
{"error": "Task is required"}, status=status.HTTP_400_BAD_REQUEST
{"error": "Task is required"},
status=status.HTTP_400_BAD_REQUEST,
)
final_text = task + "\n" + prompt
client = OpenAI(api_key=OPENAI_API_KEY)
client = OpenAI(
api_key=OPENAI_API_KEY,
)
response = client.chat.completions.create(
model=GPT_ENGINE, messages=[{"role": "user", "content": final_text}]
model=GPT_ENGINE,
messages=[{"role": "user", "content": final_text}],
)
workspace = Workspace.objects.get(slug=slug)
@@ -74,7 +82,10 @@ class GPTIntegrationEndpoint(BaseAPIView):
class WorkspaceGPTIntegrationEndpoint(BaseAPIView):
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
@allow_permission(
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE"
)
def post(self, request, slug):
OPENAI_API_KEY, GPT_ENGINE = get_configuration_value(
[
@@ -102,21 +113,29 @@ class WorkspaceGPTIntegrationEndpoint(BaseAPIView):
if not task:
return Response(
{"error": "Task is required"}, status=status.HTTP_400_BAD_REQUEST
{"error": "Task is required"},
status=status.HTTP_400_BAD_REQUEST,
)
final_text = task + "\n" + prompt
client = OpenAI(api_key=OPENAI_API_KEY)
client = OpenAI(
api_key=OPENAI_API_KEY,
)
response = client.chat.completions.create(
model=GPT_ENGINE, messages=[{"role": "user", "content": final_text}]
model=GPT_ENGINE,
messages=[{"role": "user", "content": final_text}],
)
text = response.choices[0].message.content.strip()
text_html = text.replace("\n", "<br/>")
return Response(
{"response": text, "response_html": text_html}, status=status.HTTP_200_OK
{
"response": text,
"response_html": text_html,
},
status=status.HTTP_200_OK,
)
@@ -145,7 +164,9 @@ class UnsplashEndpoint(BaseAPIView):
else f"https://api.unsplash.com/photos/?client_id={UNSPLASH_ACCESS_KEY}&page={page}&per_page={per_page}"
)
headers = {"Content-Type": "application/json"}
headers = {
"Content-Type": "application/json",
}
resp = requests.get(url=url, headers=headers)
return Response(resp.json(), status=resp.status_code)
@@ -18,8 +18,8 @@ from rest_framework.response import Response
from ..base import BaseViewSet
from plane.app.permissions import allow_permission, ROLE
from plane.db.models import (
Intake,
IntakeIssue,
Inbox,
InboxIssue,
Issue,
State,
IssueLink,
@@ -31,17 +31,18 @@ from plane.db.models import (
from plane.app.serializers import (
IssueCreateSerializer,
IssueSerializer,
IntakeSerializer,
IntakeIssueSerializer,
IntakeIssueDetailSerializer,
InboxSerializer,
InboxIssueSerializer,
InboxIssueDetailSerializer,
)
from plane.utils.issue_filters import issue_filters
from plane.bgtasks.issue_activities_task import issue_activity
class IntakeViewSet(BaseViewSet):
serializer_class = IntakeSerializer
model = Intake
class InboxViewSet(BaseViewSet):
serializer_class = InboxSerializer
model = Inbox
def get_queryset(self):
return (
@@ -53,7 +54,8 @@ class IntakeViewSet(BaseViewSet):
)
.annotate(
pending_issue_count=Count(
"issue_intake", filter=Q(issue_intake__status=-2)
"issue_inbox",
filter=Q(issue_inbox__status=-2),
)
)
.select_related("workspace", "project")
@@ -61,8 +63,11 @@ class IntakeViewSet(BaseViewSet):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def list(self, request, slug, project_id):
intake = self.get_queryset().first()
return Response(IntakeSerializer(intake).data, status=status.HTTP_200_OK)
inbox = self.get_queryset().first()
return Response(
InboxSerializer(inbox).data,
status=status.HTTP_200_OK,
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def perform_create(self, serializer):
@@ -70,24 +75,27 @@ class IntakeViewSet(BaseViewSet):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def destroy(self, request, slug, project_id, pk):
intake = Intake.objects.filter(
inbox = Inbox.objects.filter(
workspace__slug=slug, project_id=project_id, pk=pk
).first()
# Handle default intake delete
if intake.is_default:
# Handle default inbox delete
if inbox.is_default:
return Response(
{"error": "You cannot delete the default intake"},
{"error": "You cannot delete the default inbox"},
status=status.HTTP_400_BAD_REQUEST,
)
intake.delete()
inbox.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
class IntakeIssueViewSet(BaseViewSet):
serializer_class = IntakeIssueSerializer
model = IntakeIssue
class InboxIssueViewSet(BaseViewSet):
filterset_fields = ["statulls"]
serializer_class = InboxIssueSerializer
model = InboxIssue
filterset_fields = [
"status",
]
def get_queryset(self):
return (
@@ -99,8 +107,8 @@ class IntakeIssueViewSet(BaseViewSet):
.prefetch_related("assignees", "labels", "issue_module__module")
.prefetch_related(
Prefetch(
"issue_intake",
queryset=IntakeIssue.objects.only(
"issue_inbox",
queryset=InboxIssue.objects.only(
"status", "duplicate_to", "snoozed_till", "source"
),
)
@@ -128,7 +136,9 @@ class IntakeIssueViewSet(BaseViewSet):
.values("count")
)
.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -140,7 +150,7 @@ class IntakeIssueViewSet(BaseViewSet):
distinct=True,
filter=Q(
~Q(labels__id__isnull=True)
& Q(label_issue__deleted_at__isnull=True)
& Q(label_issue__deleted_at__isnull=True),
),
),
Value([], output_field=ArrayField(UUIDField())),
@@ -174,17 +184,19 @@ class IntakeIssueViewSet(BaseViewSet):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def list(self, request, slug, project_id):
intake_id = Intake.objects.filter(
inbox_id = Inbox.objects.filter(
workspace__slug=slug, project_id=project_id
).first()
project = Project.objects.get(pk=project_id)
filters = issue_filters(request.GET, "GET", "issue__")
intake_issue = (
IntakeIssue.objects.filter(
intake_id=intake_id.id, project_id=project_id, **filters
inbox_issue = (
InboxIssue.objects.filter(
inbox_id=inbox_id.id, project_id=project_id, **filters
)
.select_related("issue")
.prefetch_related("issue__labels")
.prefetch_related(
"issue__labels",
)
.annotate(
label_ids=Coalesce(
ArrayAgg(
@@ -199,14 +211,14 @@ class IntakeIssueViewSet(BaseViewSet):
)
)
).order_by(request.GET.get("order_by", "-issue__created_at"))
# Intake status filter
intake_status = [
# inbox status filter
inbox_status = [
item
for item in request.GET.get("status", "-2").split(",")
if item != "null"
]
if intake_status:
intake_issue = intake_issue.filter(status__in=intake_status)
if inbox_status:
inbox_issue = inbox_issue.filter(status__in=inbox_status)
if (
ProjectMember.objects.filter(
@@ -218,12 +230,13 @@ class IntakeIssueViewSet(BaseViewSet):
).exists()
and not project.guest_view_all_features
):
intake_issue = intake_issue.filter(created_by=request.user)
inbox_issue = inbox_issue.filter(created_by=request.user)
return self.paginate(
request=request,
queryset=(intake_issue),
on_results=lambda intake_issues: IntakeIssueSerializer(
intake_issues, many=True
queryset=(inbox_issue),
on_results=lambda inbox_issues: InboxIssueSerializer(
inbox_issues,
many=True,
).data,
)
@@ -231,7 +244,8 @@ class IntakeIssueViewSet(BaseViewSet):
def create(self, request, slug, project_id):
if not request.data.get("issue", {}).get("name", False):
return Response(
{"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
{"error": "Name is required"},
status=status.HTTP_400_BAD_REQUEST,
)
# Check for valid priority
@@ -243,9 +257,20 @@ class IntakeIssueViewSet(BaseViewSet):
"none",
]:
return Response(
{"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
{"error": "Invalid priority"},
status=status.HTTP_400_BAD_REQUEST,
)
# Create or get state
state, _ = State.objects.get_or_create(
name="Triage",
group="triage",
description="Default state for managing all Inbox Issues",
project_id=project_id,
color="#ff7700",
is_triage=True,
)
# create an issue
project = Project.objects.get(pk=project_id)
serializer = IssueCreateSerializer(
@@ -258,15 +283,15 @@ class IntakeIssueViewSet(BaseViewSet):
)
if serializer.is_valid():
serializer.save()
intake_id = Intake.objects.filter(
inbox_id = Inbox.objects.filter(
workspace__slug=slug, project_id=project_id
).first()
# create an intake issue
intake_issue = IntakeIssue.objects.create(
intake_id=intake_id.id,
# create an inbox issue
inbox_issue = InboxIssue.objects.create(
inbox_id=inbox_id.id,
project_id=project_id,
issue_id=serializer.data["id"],
source=request.data.get("source", "IN-APP"),
source=request.data.get("source", "in-app"),
)
# Create an Issue Activity
issue_activity.delay(
@@ -279,11 +304,14 @@ class IntakeIssueViewSet(BaseViewSet):
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
intake=str(intake_issue.id),
inbox=str(inbox_issue.id),
)
intake_issue = (
IntakeIssue.objects.select_related("issue")
.prefetch_related("issue__labels", "issue__assignees")
inbox_issue = (
InboxIssue.objects.select_related("issue")
.prefetch_related(
"issue__labels",
"issue__assignees",
)
.annotate(
label_ids=Coalesce(
ArrayAgg(
@@ -291,7 +319,9 @@ class IntakeIssueViewSet(BaseViewSet):
distinct=True,
filter=Q(
~Q(issue__labels__id__isnull=True)
& Q(issue__label_issue__deleted_at__isnull=True)
& Q(
issue__label_issue__deleted_at__isnull=True
)
),
),
Value([], output_field=ArrayField(UUIDField())),
@@ -301,32 +331,36 @@ class IntakeIssueViewSet(BaseViewSet):
"issue__assignees__id",
distinct=True,
filter=~Q(issue__assignees__id__isnull=True)
& Q(issue__assignees__member_project__is_active=True),
& Q(
issue__assignees__member_project__is_active=True
),
),
Value([], output_field=ArrayField(UUIDField())),
),
)
.get(
intake_id=intake_id.id,
inbox_id=inbox_id.id,
issue_id=serializer.data["id"],
project_id=project_id,
)
)
serializer = IntakeIssueDetailSerializer(intake_issue)
serializer = InboxIssueDetailSerializer(inbox_issue)
return Response(serializer.data, status=status.HTTP_200_OK)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
@allow_permission(allowed_roles=[ROLE.ADMIN], creator=True, model=Issue)
def partial_update(self, request, slug, project_id, pk):
intake_id = Intake.objects.filter(
inbox_id = Inbox.objects.filter(
workspace__slug=slug, project_id=project_id
).first()
intake_issue = IntakeIssue.objects.get(
inbox_issue = InboxIssue.objects.get(
issue_id=pk,
workspace__slug=slug,
project_id=project_id,
intake_id=intake_id,
inbox_id=inbox_id,
)
# Get the project member
project_member = ProjectMember.objects.get(
@@ -336,11 +370,11 @@ class IntakeIssueViewSet(BaseViewSet):
is_active=True,
)
# Only project members admins and created_by users can access this endpoint
if project_member.role <= 5 and str(intake_issue.created_by_id) != str(
if project_member.role <= 5 and str(inbox_issue.created_by_id) != str(
request.user.id
):
return Response(
{"error": "You cannot edit intake issues"},
{"error": "You cannot edit inbox issues"},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -370,7 +404,11 @@ class IntakeIssueViewSet(BaseViewSet):
),
Value([], output_field=ArrayField(UUIDField())),
),
).get(pk=intake_issue.issue_id, workspace__slug=slug, project_id=project_id)
).get(
pk=inbox_issue.issue_id,
workspace__slug=slug,
project_id=project_id,
)
# Only allow guests to edit name and description
if project_member.role <= 5:
issue_data = {
@@ -378,7 +416,9 @@ class IntakeIssueViewSet(BaseViewSet):
"description_html": issue_data.get(
"description_html", issue.description_html
),
"description": issue_data.get("description", issue.description),
"description": issue_data.get(
"description", issue.description
),
}
issue_serializer = IssueCreateSerializer(
@@ -403,7 +443,7 @@ class IntakeIssueViewSet(BaseViewSet):
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
intake=str(intake_issue.id),
inbox=str(inbox_issue.id),
)
issue_serializer.save()
else:
@@ -411,25 +451,27 @@ class IntakeIssueViewSet(BaseViewSet):
issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
# Only project admins and members can edit intake issue attributes
# Only project admins and members can edit inbox issue attributes
if project_member.role > 15:
serializer = IntakeIssueSerializer(
intake_issue, data=request.data, partial=True
serializer = InboxIssueSerializer(
inbox_issue, data=request.data, partial=True
)
current_instance = json.dumps(
IntakeIssueSerializer(intake_issue).data, cls=DjangoJSONEncoder
InboxIssueSerializer(inbox_issue).data, cls=DjangoJSONEncoder
)
if serializer.is_valid():
serializer.save()
# Update the issue state if the issue is rejected or marked as duplicate
if serializer.data["status"] in [-1, 2]:
issue = Issue.objects.get(
pk=intake_issue.issue_id,
pk=inbox_issue.issue_id,
workspace__slug=slug,
project_id=project_id,
)
state = State.objects.filter(
group="cancelled", workspace__slug=slug, project_id=project_id
group="cancelled",
workspace__slug=slug,
project_id=project_id,
).first()
if state is not None:
issue.state = state
@@ -438,7 +480,7 @@ class IntakeIssueViewSet(BaseViewSet):
# Update the issue state if it is accepted
if serializer.data["status"] in [1]:
issue = Issue.objects.get(
pk=intake_issue.issue_id,
pk=inbox_issue.issue_id,
workspace__slug=slug,
project_id=project_id,
)
@@ -447,15 +489,19 @@ class IntakeIssueViewSet(BaseViewSet):
if issue.state.is_triage:
# Move to default state
state = State.objects.filter(
workspace__slug=slug, project_id=project_id, default=True
workspace__slug=slug,
project_id=project_id,
default=True,
).first()
if state is not None:
issue.state = state
issue.save()
# create a activity for status change
issue_activity.delay(
type="intake.activity.created",
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
type="inbox.activity.created",
requested_data=json.dumps(
request.data, cls=DjangoJSONEncoder
),
actor_id=str(request.user.id),
issue_id=str(pk),
project_id=str(project_id),
@@ -463,12 +509,15 @@ class IntakeIssueViewSet(BaseViewSet):
epoch=int(timezone.now().timestamp()),
notification=False,
origin=request.META.get("HTTP_ORIGIN"),
intake=(intake_issue.id),
inbox=(inbox_issue.id),
)
intake_issue = (
IntakeIssue.objects.select_related("issue")
.prefetch_related("issue__labels", "issue__assignees")
inbox_issue = (
InboxIssue.objects.select_related("issue")
.prefetch_related(
"issue__labels",
"issue__assignees",
)
.annotate(
label_ids=Coalesce(
ArrayAgg(
@@ -476,7 +525,9 @@ class IntakeIssueViewSet(BaseViewSet):
distinct=True,
filter=Q(
~Q(issue__labels__id__isnull=True)
& Q(issue__label_issue__deleted_at__isnull=True)
& Q(
issue__label_issue__deleted_at__isnull=True
)
),
),
Value([], output_field=ArrayField(UUIDField())),
@@ -487,32 +538,49 @@ class IntakeIssueViewSet(BaseViewSet):
distinct=True,
filter=Q(
~Q(issue__assignees__id__isnull=True)
& Q(issue__issue_assignee__deleted_at__isnull=True)
& Q(
issue__issue_assignee__deleted_at__isnull=True
)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
)
.get(intake_id=intake_id.id, issue_id=pk, project_id=project_id)
.get(
inbox_id=inbox_id.id,
issue_id=pk,
project_id=project_id,
)
)
serializer = IntakeIssueDetailSerializer(intake_issue).data
serializer = InboxIssueDetailSerializer(inbox_issue).data
return Response(serializer, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
else:
serializer = IntakeIssueDetailSerializer(intake_issue).data
serializer = InboxIssueDetailSerializer(inbox_issue).data
return Response(serializer, status=status.HTTP_200_OK)
@allow_permission(
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], creator=True, model=Issue
allowed_roles=[
ROLE.ADMIN,
ROLE.MEMBER,
ROLE.GUEST,
],
creator=True,
model=Issue,
)
def retrieve(self, request, slug, project_id, pk):
intake_id = Intake.objects.filter(
inbox_id = Inbox.objects.filter(
workspace__slug=slug, project_id=project_id
).first()
project = Project.objects.get(pk=project_id)
intake_issue = (
IntakeIssue.objects.select_related("issue")
.prefetch_related("issue__labels", "issue__assignees")
inbox_issue = (
InboxIssue.objects.select_related("issue")
.prefetch_related(
"issue__labels",
"issue__assignees",
)
.annotate(
label_ids=Coalesce(
ArrayAgg(
@@ -537,7 +605,7 @@ class IntakeIssueViewSet(BaseViewSet):
Value([], output_field=ArrayField(UUIDField())),
),
)
.get(intake_id=intake_id.id, issue_id=pk, project_id=project_id)
.get(inbox_id=inbox_id.id, issue_id=pk, project_id=project_id)
)
if (
ProjectMember.objects.filter(
@@ -548,34 +616,37 @@ class IntakeIssueViewSet(BaseViewSet):
is_active=True,
).exists()
and not project.guest_view_all_features
and not intake_issue.created_by == request.user
and not inbox_issue.created_by == request.user
):
return Response(
{"error": "You are not allowed to view this issue"},
status=status.HTTP_400_BAD_REQUEST,
)
issue = IntakeIssueDetailSerializer(intake_issue).data
return Response(issue, status=status.HTTP_200_OK)
issue = InboxIssueDetailSerializer(inbox_issue).data
return Response(
issue,
status=status.HTTP_200_OK,
)
@allow_permission(allowed_roles=[ROLE.ADMIN], creator=True, model=Issue)
def destroy(self, request, slug, project_id, pk):
intake_id = Intake.objects.filter(
inbox_id = Inbox.objects.filter(
workspace__slug=slug, project_id=project_id
).first()
intake_issue = IntakeIssue.objects.get(
inbox_issue = InboxIssue.objects.get(
issue_id=pk,
workspace__slug=slug,
project_id=project_id,
intake_id=intake_id,
inbox_id=inbox_id,
)
# Check the issue status
if intake_issue.status in [-2, -1, 0, 2]:
if inbox_issue.status in [-2, -1, 0, 2]:
# Delete the issue also
issue = Issue.objects.filter(
workspace__slug=slug, project_id=project_id, pk=pk
).first()
issue.delete()
intake_issue.delete()
inbox_issue.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
+31 -7
View File
@@ -2,7 +2,10 @@
from itertools import chain
# Django imports
from django.db.models import Prefetch, Q
from django.db.models import (
Prefetch,
Q,
)
from django.utils.decorators import method_decorator
from django.views.decorators.gzip import gzip_page
@@ -12,16 +15,35 @@ from rest_framework import status
# Module imports
from .. import BaseAPIView
from plane.app.serializers import IssueActivitySerializer, IssueCommentSerializer
from plane.app.permissions import ProjectEntityPermission, allow_permission, ROLE
from plane.db.models import IssueActivity, IssueComment, CommentReaction
from plane.app.serializers import (
IssueActivitySerializer,
IssueCommentSerializer,
)
from plane.app.permissions import (
ProjectEntityPermission,
allow_permission,
ROLE,
)
from plane.db.models import (
IssueActivity,
IssueComment,
CommentReaction,
)
class IssueActivityEndpoint(BaseAPIView):
permission_classes = [ProjectEntityPermission]
permission_classes = [
ProjectEntityPermission,
]
@method_decorator(gzip_page)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
@allow_permission(
[
ROLE.ADMIN,
ROLE.MEMBER,
ROLE.GUEST,
]
)
def get(self, request, slug, project_id, issue_id):
filters = {}
if request.GET.get("created_at__gt", None) is not None:
@@ -57,7 +79,9 @@ class IssueActivityEndpoint(BaseAPIView):
)
)
)
issue_activities = IssueActivitySerializer(issue_activities, many=True).data
issue_activities = IssueActivitySerializer(
issue_activities, many=True
).data
issue_comments = IssueCommentSerializer(issue_comments, many=True).data
if request.GET.get("activity_type", None) == "issue-property":
+69 -27
View File
@@ -12,7 +12,9 @@ from django.views.decorators.gzip import gzip_page
from rest_framework import status
from rest_framework.response import Response
from plane.app.permissions import ProjectEntityPermission
from plane.app.permissions import (
ProjectEntityPermission,
)
from plane.app.serializers import (
IssueFlatSerializer,
IssueSerializer,
@@ -25,7 +27,7 @@ from plane.db.models import (
IssueLink,
IssueSubscriber,
IssueReaction,
CycleIssue,
CycleIssue
)
from plane.utils.grouper import (
issue_group_values,
@@ -34,7 +36,10 @@ from plane.utils.grouper import (
)
from plane.utils.issue_filters import issue_filters
from plane.utils.order_queryset import order_issue_queryset
from plane.utils.paginator import GroupedOffsetPaginator, SubGroupedOffsetPaginator
from plane.utils.paginator import (
GroupedOffsetPaginator,
SubGroupedOffsetPaginator,
)
from plane.app.permissions import allow_permission, ROLE
from plane.utils.error_codes import ERROR_CODES
@@ -83,7 +88,9 @@ class IssueArchiveViewSet(BaseViewSet):
.values("count")
)
.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -91,7 +98,12 @@ class IssueArchiveViewSet(BaseViewSet):
)
@method_decorator(gzip_page)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
@allow_permission(
[
ROLE.ADMIN,
ROLE.MEMBER,
]
)
def list(self, request, slug, project_id):
filters = issue_filters(request.query_params, "GET")
show_sub_issues = request.GET.get("show_sub_issues", "true")
@@ -107,7 +119,8 @@ class IssueArchiveViewSet(BaseViewSet):
)
# Issue queryset
issue_queryset, order_by_param = order_issue_queryset(
issue_queryset=issue_queryset, order_by_param=order_by_param
issue_queryset=issue_queryset,
order_by_param=order_by_param,
)
# Group by
@@ -116,7 +129,9 @@ class IssueArchiveViewSet(BaseViewSet):
# issue queryset
issue_queryset = issue_queryset_grouper(
queryset=issue_queryset, group_by=group_by, sub_group_by=sub_group_by
queryset=issue_queryset,
group_by=group_by,
sub_group_by=sub_group_by,
)
if group_by:
@@ -136,7 +151,9 @@ class IssueArchiveViewSet(BaseViewSet):
order_by=order_by_param,
queryset=issue_queryset,
on_results=lambda issues: issue_on_results(
group_by=group_by, issues=issues, sub_group_by=sub_group_by
group_by=group_by,
issues=issues,
sub_group_by=sub_group_by,
),
paginator_cls=SubGroupedOffsetPaginator,
group_by_fields=issue_group_values(
@@ -154,10 +171,10 @@ class IssueArchiveViewSet(BaseViewSet):
group_by_field_name=group_by,
sub_group_by_field_name=sub_group_by,
count_filter=Q(
Q(issue_intake__status=1)
| Q(issue_intake__status=-1)
| Q(issue_intake__status=2)
| Q(issue_intake__isnull=True),
Q(issue_inbox__status=1)
| Q(issue_inbox__status=-1)
| Q(issue_inbox__status=2)
| Q(issue_inbox__isnull=True),
archived_at__isnull=True,
is_draft=False,
),
@@ -170,7 +187,9 @@ class IssueArchiveViewSet(BaseViewSet):
order_by=order_by_param,
queryset=issue_queryset,
on_results=lambda issues: issue_on_results(
group_by=group_by, issues=issues, sub_group_by=sub_group_by
group_by=group_by,
issues=issues,
sub_group_by=sub_group_by,
),
paginator_cls=GroupedOffsetPaginator,
group_by_fields=issue_group_values(
@@ -181,10 +200,10 @@ class IssueArchiveViewSet(BaseViewSet):
),
group_by_field_name=group_by,
count_filter=Q(
Q(issue_intake__status=1)
| Q(issue_intake__status=-1)
| Q(issue_intake__status=2)
| Q(issue_intake__isnull=True),
Q(issue_inbox__status=1)
| Q(issue_inbox__status=-1)
| Q(issue_inbox__status=2)
| Q(issue_inbox__isnull=True),
archived_at__isnull=True,
is_draft=False,
),
@@ -200,7 +219,12 @@ class IssueArchiveViewSet(BaseViewSet):
),
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
@allow_permission(
[
ROLE.ADMIN,
ROLE.MEMBER,
]
)
def retrieve(self, request, slug, project_id, pk=None):
issue = (
self.get_queryset()
@@ -208,7 +232,9 @@ class IssueArchiveViewSet(BaseViewSet):
.prefetch_related(
Prefetch(
"issue_reactions",
queryset=IssueReaction.objects.select_related("issue", "actor"),
queryset=IssueReaction.objects.select_related(
"issue", "actor"
),
)
)
.prefetch_related(
@@ -239,17 +265,24 @@ class IssueArchiveViewSet(BaseViewSet):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def archive(self, request, slug, project_id, pk=None):
issue = Issue.issue_objects.get(
workspace__slug=slug, project_id=project_id, pk=pk
workspace__slug=slug,
project_id=project_id,
pk=pk,
)
if issue.state.group not in ["completed", "cancelled"]:
return Response(
{"error": "Can only archive completed or cancelled state group issue"},
{
"error": "Can only archive completed or cancelled state group issue"
},
status=status.HTTP_400_BAD_REQUEST,
)
issue_activity.delay(
type="issue.activity.updated",
requested_data=json.dumps(
{"archived_at": str(timezone.now().date()), "automation": False}
{
"archived_at": str(timezone.now().date()),
"automation": False,
}
),
actor_id=str(request.user.id),
issue_id=str(issue.id),
@@ -296,7 +329,9 @@ class IssueArchiveViewSet(BaseViewSet):
class BulkArchiveIssuesEndpoint(BaseAPIView):
permission_classes = [ProjectEntityPermission]
permission_classes = [
ProjectEntityPermission,
]
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def post(self, request, slug, project_id):
@@ -304,7 +339,8 @@ class BulkArchiveIssuesEndpoint(BaseAPIView):
if not len(issue_ids):
return Response(
{"error": "Issue IDs are required"}, status=status.HTTP_400_BAD_REQUEST
{"error": "Issue IDs are required"},
status=status.HTTP_400_BAD_REQUEST,
)
issues = Issue.objects.filter(
@@ -315,7 +351,9 @@ class BulkArchiveIssuesEndpoint(BaseAPIView):
if issue.state.group not in ["completed", "cancelled"]:
return Response(
{
"error_code": ERROR_CODES["INVALID_ARCHIVE_STATE_GROUP"],
"error_code": ERROR_CODES[
"INVALID_ARCHIVE_STATE_GROUP"
],
"error_message": "INVALID_ARCHIVE_STATE_GROUP",
},
status=status.HTTP_400_BAD_REQUEST,
@@ -323,7 +361,10 @@ class BulkArchiveIssuesEndpoint(BaseAPIView):
issue_activity.delay(
type="issue.activity.updated",
requested_data=json.dumps(
{"archived_at": str(timezone.now().date()), "automation": False}
{
"archived_at": str(timezone.now().date()),
"automation": False,
}
),
actor_id=str(request.user.id),
issue_id=str(issue.id),
@@ -340,5 +381,6 @@ class BulkArchiveIssuesEndpoint(BaseAPIView):
Issue.objects.bulk_update(bulk_archive_issues, ["archived_at"])
return Response(
{"archived_at": str(timezone.now().date())}, status=status.HTTP_200_OK
{"archived_at": str(timezone.now().date())},
status=status.HTTP_200_OK,
)
+46 -10
View File
@@ -45,7 +45,10 @@ class IssueAttachmentEndpoint(BaseAPIView):
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=json.dumps(serializer.data, cls=DjangoJSONEncoder),
current_instance=json.dumps(
serializer.data,
cls=DjangoJSONEncoder,
),
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
@@ -72,7 +75,13 @@ class IssueAttachmentEndpoint(BaseAPIView):
return Response(status=status.HTTP_204_NO_CONTENT)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
@allow_permission(
[
ROLE.ADMIN,
ROLE.MEMBER,
ROLE.GUEST,
]
)
def get(self, request, slug, project_id, issue_id):
issue_attachments = FileAsset.objects.filter(
issue_id=issue_id, workspace__slug=slug, project_id=project_id
@@ -82,6 +91,7 @@ class IssueAttachmentEndpoint(BaseAPIView):
class IssueAttachmentV2Endpoint(BaseAPIView):
serializer_class = IssueAttachmentSerializer
model = FileAsset
@@ -93,7 +103,10 @@ class IssueAttachmentV2Endpoint(BaseAPIView):
if not type or type not in settings.ATTACHMENT_MIME_TYPES:
return Response(
{"error": "Invalid file type.", "status": False},
{
"error": "Invalid file type.",
"status": False,
},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -108,7 +121,11 @@ class IssueAttachmentV2Endpoint(BaseAPIView):
# Create a File Asset
asset = FileAsset.objects.create(
attributes={"name": name, "type": type, "size": size_limit},
attributes={
"name": name,
"type": type,
"size": size_limit,
},
asset=asset_key,
size=size_limit,
workspace_id=workspace.id,
@@ -122,7 +139,9 @@ class IssueAttachmentV2Endpoint(BaseAPIView):
storage = S3Storage(request=request)
# Generate a presigned URL to share an S3 object
presigned_url = storage.generate_presigned_post(
object_name=asset_key, file_type=type, file_size=size_limit
object_name=asset_key,
file_type=type,
file_size=size_limit,
)
# Return the presigned URL
return Response(
@@ -158,7 +177,13 @@ class IssueAttachmentV2Endpoint(BaseAPIView):
return Response(status=status.HTTP_204_NO_CONTENT)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
@allow_permission(
[
ROLE.ADMIN,
ROLE.MEMBER,
ROLE.GUEST,
]
)
def get(self, request, slug, project_id, issue_id, pk=None):
if pk:
# Get the asset
@@ -169,7 +194,10 @@ class IssueAttachmentV2Endpoint(BaseAPIView):
# Check if the asset is uploaded
if not asset.is_uploaded:
return Response(
{"error": "The asset is not uploaded.", "status": False},
{
"error": "The asset is not uploaded.",
"status": False,
},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -193,7 +221,13 @@ class IssueAttachmentV2Endpoint(BaseAPIView):
serializer = IssueAttachmentSerializer(issue_attachments, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
@allow_permission(
[
ROLE.ADMIN,
ROLE.MEMBER,
ROLE.GUEST,
]
)
def patch(self, request, slug, project_id, issue_id, pk):
issue_attachment = FileAsset.objects.get(
pk=pk, workspace__slug=slug, project_id=project_id
@@ -208,7 +242,10 @@ class IssueAttachmentV2Endpoint(BaseAPIView):
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=json.dumps(serializer.data, cls=DjangoJSONEncoder),
current_instance=json.dumps(
serializer.data,
cls=DjangoJSONEncoder,
),
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
@@ -216,7 +253,6 @@ class IssueAttachmentV2Endpoint(BaseAPIView):
# Update the attachment
issue_attachment.is_uploaded = True
issue_attachment.created_by = request.user
# Get the storage metadata
if not issue_attachment.storage_metadata:
+105 -246
View File
@@ -15,8 +15,6 @@ from django.db.models import (
UUIDField,
Value,
Subquery,
Case,
When,
)
from django.db.models.functions import Coalesce
from django.utils import timezone
@@ -54,7 +52,10 @@ from plane.utils.grouper import (
)
from plane.utils.issue_filters import issue_filters
from plane.utils.order_queryset import order_issue_queryset
from plane.utils.paginator import GroupedOffsetPaginator, SubGroupedOffsetPaginator
from plane.utils.paginator import (
GroupedOffsetPaginator,
SubGroupedOffsetPaginator,
)
from .. import BaseAPIView, BaseViewSet
from plane.utils.user_timezone_converter import user_timezone_converter
from plane.bgtasks.recent_visited_task import recent_visited_task
@@ -69,10 +70,13 @@ class IssueListEndpoint(BaseAPIView):
if not issue_ids:
return Response(
{"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST
{"error": "Issues are required"},
status=status.HTTP_400_BAD_REQUEST,
)
issue_ids = [issue_id for issue_id in issue_ids.split(",") if issue_id != ""]
issue_ids = [
issue_id for issue_id in issue_ids.split(",") if issue_id != ""
]
queryset = (
Issue.issue_objects.filter(
@@ -104,7 +108,9 @@ class IssueListEndpoint(BaseAPIView):
.values("count")
)
.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -117,7 +123,8 @@ class IssueListEndpoint(BaseAPIView):
issue_queryset = queryset.filter(**filters)
# Issue queryset
issue_queryset, _ = order_issue_queryset(
issue_queryset=issue_queryset, order_by_param=order_by_param
issue_queryset=issue_queryset,
order_by_param=order_by_param,
)
# Group by
@@ -126,7 +133,9 @@ class IssueListEndpoint(BaseAPIView):
# issue queryset
issue_queryset = issue_queryset_grouper(
queryset=issue_queryset, group_by=group_by, sub_group_by=sub_group_by
queryset=issue_queryset,
group_by=group_by,
sub_group_by=sub_group_by,
)
recent_visited_task.delay(
@@ -188,13 +197,21 @@ class IssueViewSet(BaseViewSet):
model = Issue
webhook_event = "issue"
search_fields = ["name"]
search_fields = [
"name",
]
filterset_fields = ["state__name", "assignees__id", "workspace__id"]
filterset_fields = [
"state__name",
"assignees__id",
"workspace__id",
]
def get_queryset(self):
return (
Issue.issue_objects.filter(project_id=self.kwargs.get("project_id"))
Issue.issue_objects.filter(
project_id=self.kwargs.get("project_id")
)
.filter(workspace__slug=self.kwargs.get("slug"))
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels", "issue_module__module")
@@ -221,7 +238,9 @@ class IssueViewSet(BaseViewSet):
.values("count")
)
.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -233,7 +252,9 @@ class IssueViewSet(BaseViewSet):
def list(self, request, slug, project_id):
extra_filters = {}
if request.GET.get("updated_at__gt", None) is not None:
extra_filters = {"updated_at__gt": request.GET.get("updated_at__gt")}
extra_filters = {
"updated_at__gt": request.GET.get("updated_at__gt")
}
project = Project.objects.get(pk=project_id, workspace__slug=slug)
filters = issue_filters(request.query_params, "GET")
@@ -244,7 +265,8 @@ class IssueViewSet(BaseViewSet):
# Issue queryset
issue_queryset, order_by_param = order_issue_queryset(
issue_queryset=issue_queryset, order_by_param=order_by_param
issue_queryset=issue_queryset,
order_by_param=order_by_param,
)
# Group by
@@ -253,7 +275,9 @@ class IssueViewSet(BaseViewSet):
# issue queryset
issue_queryset = issue_queryset_grouper(
queryset=issue_queryset, group_by=group_by, sub_group_by=sub_group_by
queryset=issue_queryset,
group_by=group_by,
sub_group_by=sub_group_by,
)
recent_visited_task.delay(
@@ -290,7 +314,9 @@ class IssueViewSet(BaseViewSet):
order_by=order_by_param,
queryset=issue_queryset,
on_results=lambda issues: issue_on_results(
group_by=group_by, issues=issues, sub_group_by=sub_group_by
group_by=group_by,
issues=issues,
sub_group_by=sub_group_by,
),
paginator_cls=SubGroupedOffsetPaginator,
group_by_fields=issue_group_values(
@@ -308,10 +334,10 @@ class IssueViewSet(BaseViewSet):
group_by_field_name=group_by,
sub_group_by_field_name=sub_group_by,
count_filter=Q(
Q(issue_intake__status=1)
| Q(issue_intake__status=-1)
| Q(issue_intake__status=2)
| Q(issue_intake__isnull=True),
Q(issue_inbox__status=1)
| Q(issue_inbox__status=-1)
| Q(issue_inbox__status=2)
| Q(issue_inbox__isnull=True),
archived_at__isnull=True,
is_draft=False,
),
@@ -323,7 +349,9 @@ class IssueViewSet(BaseViewSet):
order_by=order_by_param,
queryset=issue_queryset,
on_results=lambda issues: issue_on_results(
group_by=group_by, issues=issues, sub_group_by=sub_group_by
group_by=group_by,
issues=issues,
sub_group_by=sub_group_by,
),
paginator_cls=GroupedOffsetPaginator,
group_by_fields=issue_group_values(
@@ -334,10 +362,10 @@ class IssueViewSet(BaseViewSet):
),
group_by_field_name=group_by,
count_filter=Q(
Q(issue_intake__status=1)
| Q(issue_intake__status=-1)
| Q(issue_intake__status=2)
| Q(issue_intake__isnull=True),
Q(issue_inbox__status=1)
| Q(issue_inbox__status=-1)
| Q(issue_inbox__status=2)
| Q(issue_inbox__isnull=True),
archived_at__isnull=True,
is_draft=False,
),
@@ -371,7 +399,9 @@ class IssueViewSet(BaseViewSet):
# Track the issue
issue_activity.delay(
type="issue.activity.created",
requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder),
requested_data=json.dumps(
self.request.data, cls=DjangoJSONEncoder
),
actor_id=str(request.user.id),
issue_id=str(serializer.data.get("id", None)),
project_id=str(project_id),
@@ -382,7 +412,9 @@ class IssueViewSet(BaseViewSet):
)
issue = (
issue_queryset_grouper(
queryset=self.get_queryset().filter(pk=serializer.data["id"]),
queryset=self.get_queryset().filter(
pk=serializer.data["id"]
),
group_by=None,
sub_group_by=None,
)
@@ -434,46 +466,19 @@ class IssueViewSet(BaseViewSet):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@allow_permission(
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], creator=True, model=Issue
allowed_roles=[
ROLE.ADMIN,
ROLE.MEMBER,
ROLE.GUEST,
],
creator=True,
model=Issue,
)
def retrieve(self, request, slug, project_id, pk=None):
project = Project.objects.get(pk=project_id, workspace__slug=slug)
issue = (
Issue.objects.filter(project_id=self.kwargs.get("project_id"))
.filter(workspace__slug=self.kwargs.get("slug"))
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels", "issue_module__module")
.annotate(
cycle_id=Case(
When(
issue_cycle__cycle__deleted_at__isnull=True,
then=F("issue_cycle__cycle_id"),
),
default=None,
)
)
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
attachment_count=FileAsset.objects.filter(
issue_id=OuterRef("id"),
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
self.get_queryset()
.filter(pk=pk)
.annotate(
label_ids=Coalesce(
@@ -482,7 +487,7 @@ class IssueViewSet(BaseViewSet):
distinct=True,
filter=Q(
~Q(labels__id__isnull=True)
& Q(label_issue__deleted_at__isnull=True)
& Q(label_issue__deleted_at__isnull=True),
),
),
Value([], output_field=ArrayField(UUIDField())),
@@ -515,7 +520,9 @@ class IssueViewSet(BaseViewSet):
.prefetch_related(
Prefetch(
"issue_reactions",
queryset=IssueReaction.objects.select_related("issue", "actor"),
queryset=IssueReaction.objects.select_related(
"issue", "actor"
),
)
)
.prefetch_related(
@@ -586,7 +593,7 @@ class IssueViewSet(BaseViewSet):
distinct=True,
filter=Q(
~Q(labels__id__isnull=True)
& Q(label_issue__deleted_at__isnull=True)
& Q(label_issue__deleted_at__isnull=True),
),
),
Value([], output_field=ArrayField(UUIDField())),
@@ -622,7 +629,8 @@ class IssueViewSet(BaseViewSet):
if not issue:
return Response(
{"error": "Issue not found"}, status=status.HTTP_404_NOT_FOUND
{"error": "Issue not found"},
status=status.HTTP_404_NOT_FOUND,
)
current_instance = json.dumps(
@@ -630,7 +638,9 @@ class IssueViewSet(BaseViewSet):
)
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
serializer = IssueCreateSerializer(issue, data=request.data, partial=True)
serializer = IssueCreateSerializer(
issue, data=request.data, partial=True
)
if serializer.is_valid():
serializer.save()
issue_activity.delay(
@@ -658,7 +668,9 @@ class IssueViewSet(BaseViewSet):
@allow_permission([ROLE.ADMIN], creator=True, model=Issue)
def destroy(self, request, slug, project_id, pk=None):
issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
issue = Issue.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk
)
issue.delete()
issue_activity.delay(
@@ -679,10 +691,13 @@ class IssueUserDisplayPropertyEndpoint(BaseAPIView):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def patch(self, request, slug, project_id):
issue_property = IssueUserProperty.objects.get(
user=request.user, project_id=project_id
user=request.user,
project_id=project_id,
)
issue_property.filters = request.data.get("filters", issue_property.filters)
issue_property.filters = request.data.get(
"filters", issue_property.filters
)
issue_property.display_filters = request.data.get(
"display_filters", issue_property.display_filters
)
@@ -693,7 +708,13 @@ class IssueUserDisplayPropertyEndpoint(BaseAPIView):
serializer = IssueUserPropertySerializer(issue_property)
return Response(serializer.data, status=status.HTTP_201_CREATED)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
@allow_permission(
[
ROLE.ADMIN,
ROLE.MEMBER,
ROLE.GUEST,
]
)
def get(self, request, slug, project_id):
issue_property, _ = IssueUserProperty.objects.get_or_create(
user=request.user, project_id=project_id
@@ -709,7 +730,8 @@ class BulkDeleteIssuesEndpoint(BaseAPIView):
if not len(issue_ids):
return Response(
{"error": "Issue IDs are required"}, status=status.HTTP_400_BAD_REQUEST
{"error": "Issue IDs are required"},
status=status.HTTP_400_BAD_REQUEST,
)
issues = Issue.issue_objects.filter(
@@ -733,7 +755,10 @@ class DeletedIssuesListViewSet(BaseAPIView):
if request.GET.get("updated_at__gt", None) is not None:
filters = {"updated_at__gt": request.GET.get("updated_at__gt")}
deleted_issues = (
Issue.all_objects.filter(workspace__slug=slug, project_id=project_id)
Issue.all_objects.filter(
workspace__slug=slug,
project_id=project_id,
)
.filter(Q(archived_at__isnull=False) | Q(deleted_at__isnull=False))
.filter(**filters)
.values_list("id", flat=True)
@@ -752,7 +777,9 @@ class IssuePaginatedViewSet(BaseViewSet):
)
return (
issue_queryset.select_related("workspace", "project", "state", "parent")
issue_queryset.select_related(
"workspace", "project", "state", "parent"
)
.prefetch_related("assignees", "labels", "issue_module__module")
.annotate(
cycle_id=Subquery(
@@ -777,7 +804,9 @@ class IssuePaginatedViewSet(BaseViewSet):
.values("count")
)
.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -867,7 +896,7 @@ class IssuePaginatedViewSet(BaseViewSet):
distinct=True,
filter=Q(
~Q(labels__id__isnull=True)
& Q(label_issue__deleted_at__isnull=True)
& Q(label_issue__deleted_at__isnull=True),
),
),
Value([], output_field=ArrayField(UUIDField())),
@@ -908,173 +937,3 @@ class IssuePaginatedViewSet(BaseViewSet):
)
return Response(paginated_data, status=status.HTTP_200_OK)
class IssueDetailEndpoint(BaseAPIView):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def get(self, request, slug, project_id):
filters = issue_filters(request.query_params, "GET")
issue = (
Issue.issue_objects.filter(workspace__slug=slug, project_id=project_id)
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels", "issue_module__module")
.annotate(
cycle_id=Subquery(
CycleIssue.objects.filter(
issue=OuterRef("id"), deleted_at__isnull=True
).values("cycle_id")[:1]
)
)
.annotate(
label_ids=Coalesce(
ArrayAgg(
"labels__id",
distinct=True,
filter=Q(
~Q(labels__id__isnull=True)
& Q(label_issue__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
assignee_ids=Coalesce(
ArrayAgg(
"assignees__id",
distinct=True,
filter=Q(
~Q(assignees__id__isnull=True)
& Q(assignees__member_project__is_active=True)
& Q(issue_assignee__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
module_ids=Coalesce(
ArrayAgg(
"issue_module__module_id",
distinct=True,
filter=Q(
~Q(issue_module__module_id__isnull=True)
& Q(issue_module__module__archived_at__isnull=True)
& Q(issue_module__deleted_at__isnull=True)
),
),
Value([], output_field=ArrayField(UUIDField())),
),
)
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
attachment_count=FileAsset.objects.filter(
issue_id=OuterRef("id"),
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
)
issue = issue.filter(**filters)
order_by_param = request.GET.get("order_by", "-created_at")
# Issue queryset
issue, order_by_param = order_issue_queryset(
issue_queryset=issue, order_by_param=order_by_param
)
return self.paginate(
request=request,
order_by=order_by_param,
queryset=(issue),
on_results=lambda issue: IssueSerializer(
issue, many=True, fields=self.fields, expand=self.expand
).data,
)
class IssueBulkUpdateDateEndpoint(BaseAPIView):
def validate_dates(self, current_start, current_target, new_start, new_target):
"""
Validate that start date is before target date.
"""
start = new_start or current_start
target = new_target or current_target
if start and target and start > target:
return False
return True
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def post(self, request, slug, project_id):
updates = request.data.get("updates", [])
issue_ids = [update["id"] for update in updates]
epoch = int(timezone.now().timestamp())
# Fetch all relevant issues in a single query
issues = list(Issue.objects.filter(id__in=issue_ids))
issues_dict = {str(issue.id): issue for issue in issues}
issues_to_update = []
for update in updates:
issue_id = update["id"]
issue = issues_dict.get(issue_id)
if not issue:
continue
start_date = update.get("start_date")
target_date = update.get("target_date")
validate_dates = self.validate_dates(
issue.start_date, issue.target_date, start_date, target_date
)
if not validate_dates:
return Response(
{"message": "Start date cannot exceed target date"},
status=status.HTTP_400_BAD_REQUEST,
)
if start_date:
issue_activity.delay(
type="issue.activity.updated",
requested_data=json.dumps({"start_date": update.get("start_date")}),
current_instance=json.dumps({"start_date": str(issue.start_date)}),
issue_id=str(issue_id),
actor_id=str(request.user.id),
project_id=str(project_id),
epoch=epoch,
)
issue.start_date = start_date
issues_to_update.append(issue)
if target_date:
issue_activity.delay(
type="issue.activity.updated",
requested_data=json.dumps(
{"target_date": update.get("target_date")}
),
current_instance=json.dumps(
{"target_date": str(issue.target_date)}
),
issue_id=str(issue_id),
actor_id=str(request.user.id),
project_id=str(project_id),
epoch=epoch,
)
issue.target_date = target_date
issues_to_update.append(issue)
# Bulk update issues
Issue.objects.bulk_update(issues_to_update, ["start_date", "target_date"])
return Response(
{"message": "Issues updated successfully"}, status=status.HTTP_200_OK
)
+65 -15
View File
@@ -12,9 +12,18 @@ from rest_framework import status
# Module imports
from .. import BaseViewSet
from plane.app.serializers import IssueCommentSerializer, CommentReactionSerializer
from plane.app.serializers import (
IssueCommentSerializer,
CommentReactionSerializer,
)
from plane.app.permissions import allow_permission, ROLE
from plane.db.models import IssueComment, ProjectMember, CommentReaction, Project, Issue
from plane.db.models import (
IssueComment,
ProjectMember,
CommentReaction,
Project,
Issue,
)
from plane.bgtasks.issue_activities_task import issue_activity
@@ -23,7 +32,10 @@ class IssueCommentViewSet(BaseViewSet):
model = IssueComment
webhook_event = "issue_comment"
filterset_fields = ["issue__id", "workspace__id"]
filterset_fields = [
"issue__id",
"workspace__id",
]
def get_queryset(self):
return self.filter_queryset(
@@ -53,7 +65,13 @@ class IssueCommentViewSet(BaseViewSet):
.distinct()
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
@allow_permission(
[
ROLE.ADMIN,
ROLE.MEMBER,
ROLE.GUEST,
]
)
def create(self, request, slug, project_id, issue_id):
project = Project.objects.get(pk=project_id)
issue = Issue.objects.get(pk=issue_id)
@@ -75,11 +93,15 @@ class IssueCommentViewSet(BaseViewSet):
serializer = IssueCommentSerializer(data=request.data)
if serializer.is_valid():
serializer.save(
project_id=project_id, issue_id=issue_id, actor=request.user
project_id=project_id,
issue_id=issue_id,
actor=request.user,
)
issue_activity.delay(
type="comment.activity.created",
requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder),
requested_data=json.dumps(
serializer.data, cls=DjangoJSONEncoder
),
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("issue_id")),
project_id=str(self.kwargs.get("project_id")),
@@ -91,14 +113,22 @@ class IssueCommentViewSet(BaseViewSet):
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@allow_permission(allowed_roles=[ROLE.ADMIN], creator=True, model=IssueComment)
@allow_permission(
allowed_roles=[ROLE.ADMIN],
creator=True,
model=IssueComment,
)
def partial_update(self, request, slug, project_id, issue_id, pk):
issue_comment = IssueComment.objects.get(
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
workspace__slug=slug,
project_id=project_id,
issue_id=issue_id,
pk=pk,
)
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
current_instance = json.dumps(
IssueCommentSerializer(issue_comment).data, cls=DjangoJSONEncoder
IssueCommentSerializer(issue_comment).data,
cls=DjangoJSONEncoder,
)
serializer = IssueCommentSerializer(
issue_comment, data=request.data, partial=True
@@ -119,13 +149,19 @@ class IssueCommentViewSet(BaseViewSet):
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@allow_permission(allowed_roles=[ROLE.ADMIN], creator=True, model=IssueComment)
@allow_permission(
allowed_roles=[ROLE.ADMIN], creator=True, model=IssueComment
)
def destroy(self, request, slug, project_id, issue_id, pk):
issue_comment = IssueComment.objects.get(
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
workspace__slug=slug,
project_id=project_id,
issue_id=issue_id,
pk=pk,
)
current_instance = json.dumps(
IssueCommentSerializer(issue_comment).data, cls=DjangoJSONEncoder
IssueCommentSerializer(issue_comment).data,
cls=DjangoJSONEncoder,
)
issue_comment.delete()
issue_activity.delay(
@@ -162,12 +198,20 @@ class CommentReactionViewSet(BaseViewSet):
.distinct()
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
@allow_permission(
[
ROLE.ADMIN,
ROLE.MEMBER,
ROLE.GUEST,
]
)
def create(self, request, slug, project_id, comment_id):
serializer = CommentReactionSerializer(data=request.data)
if serializer.is_valid():
serializer.save(
project_id=project_id, actor_id=request.user.id, comment_id=comment_id
project_id=project_id,
actor_id=request.user.id,
comment_id=comment_id,
)
issue_activity.delay(
type="comment_reaction.activity.created",
@@ -183,7 +227,13 @@ class CommentReactionViewSet(BaseViewSet):
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
@allow_permission(
[
ROLE.ADMIN,
ROLE.MEMBER,
ROLE.GUEST,
]
)
def destroy(self, request, slug, project_id, comment_id, reaction_code):
comment_reaction = CommentReaction.objects.get(
workspace__slug=slug,
+25 -8
View File
@@ -12,14 +12,19 @@ from rest_framework import status
from .. import BaseViewSet, BaseAPIView
from plane.app.serializers import LabelSerializer
from plane.app.permissions import allow_permission, ProjectBasePermission, ROLE
from plane.db.models import Project, Label
from plane.db.models import (
Project,
Label,
)
from plane.utils.cache import invalidate_cache
class LabelViewSet(BaseViewSet):
serializer_class = LabelSerializer
model = Label
permission_classes = [ProjectBasePermission]
permission_classes = [
ProjectBasePermission,
]
def get_queryset(self):
return self.filter_queryset(
@@ -35,27 +40,39 @@ class LabelViewSet(BaseViewSet):
.order_by("sort_order")
)
@invalidate_cache(path="/api/workspaces/:slug/labels/", url_params=True, user=False)
@invalidate_cache(
path="/api/workspaces/:slug/labels/", url_params=True, user=False
)
@allow_permission([ROLE.ADMIN])
def create(self, request, slug, project_id):
try:
serializer = LabelSerializer(data=request.data)
if serializer.is_valid():
serializer.save(project_id=project_id)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
return Response(
serializer.data, status=status.HTTP_201_CREATED
)
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
except IntegrityError:
return Response(
{"error": "Label with the same name already exists in the project"},
{
"error": "Label with the same name already exists in the project"
},
status=status.HTTP_400_BAD_REQUEST,
)
@invalidate_cache(path="/api/workspaces/:slug/labels/", url_params=True, user=False)
@invalidate_cache(
path="/api/workspaces/:slug/labels/", url_params=True, user=False
)
@allow_permission([ROLE.ADMIN])
def partial_update(self, request, *args, **kwargs):
return super().partial_update(request, *args, **kwargs)
@invalidate_cache(path="/api/workspaces/:slug/labels/", url_params=True, user=False)
@invalidate_cache(
path="/api/workspaces/:slug/labels/", url_params=True, user=False
)
@allow_permission([ROLE.ADMIN])
def destroy(self, request, *args, **kwargs):
return super().destroy(request, *args, **kwargs)

Some files were not shown because too many files have changed in this diff Show More