mirror of
https://github.com/gosticks/plane.git
synced 2025-10-16 12:45:33 +00:00
[WEB-5044] fix: ruff lint and format errors (#7868)
* fix: lint errors * fix: file formatting * fix: code refactor
This commit is contained in:
parent
1fb22bd252
commit
9237f568dd
@ -9,4 +9,4 @@ class ApiConfig(AppConfig):
|
||||
try:
|
||||
import plane.utils.openapi.auth # noqa
|
||||
except ImportError:
|
||||
pass
|
||||
pass
|
||||
|
||||
@ -46,9 +46,7 @@ class AssetUpdateSerializer(serializers.Serializer):
|
||||
and upload confirmation for S3-based file storage workflows.
|
||||
"""
|
||||
|
||||
attributes = serializers.JSONField(
|
||||
required=False, help_text="Additional attributes to update for the asset"
|
||||
)
|
||||
attributes = serializers.JSONField(required=False, help_text="Additional attributes to update for the asset")
|
||||
|
||||
|
||||
class GenericAssetUploadSerializer(serializers.Serializer):
|
||||
@ -85,9 +83,7 @@ class GenericAssetUpdateSerializer(serializers.Serializer):
|
||||
upload completion marking and metadata finalization.
|
||||
"""
|
||||
|
||||
is_uploaded = serializers.BooleanField(
|
||||
default=True, help_text="Whether the asset has been successfully uploaded"
|
||||
)
|
||||
is_uploaded = serializers.BooleanField(default=True, help_text="Whether the asset has been successfully uploaded")
|
||||
|
||||
|
||||
class FileAssetSerializer(BaseSerializer):
|
||||
|
||||
@ -103,13 +103,9 @@ class BaseSerializer(serializers.ModelSerializer):
|
||||
# Check if field in expansion then expand the field
|
||||
if expand in expansion:
|
||||
if isinstance(response.get(expand), list):
|
||||
exp_serializer = expansion[expand](
|
||||
getattr(instance, expand), many=True
|
||||
)
|
||||
exp_serializer = expansion[expand](getattr(instance, expand), many=True)
|
||||
else:
|
||||
exp_serializer = expansion[expand](
|
||||
getattr(instance, expand)
|
||||
)
|
||||
exp_serializer = expansion[expand](getattr(instance, expand))
|
||||
response[expand] = exp_serializer.data
|
||||
else:
|
||||
# You might need to handle this case differently
|
||||
|
||||
@ -55,14 +55,9 @@ class CycleCreateSerializer(BaseSerializer):
|
||||
):
|
||||
raise serializers.ValidationError("Start date cannot exceed end date")
|
||||
|
||||
if (
|
||||
data.get("start_date", None) is not None
|
||||
and data.get("end_date", None) is not None
|
||||
):
|
||||
if data.get("start_date", None) is not None and data.get("end_date", None) is not None:
|
||||
project_id = self.initial_data.get("project_id") or (
|
||||
self.instance.project_id
|
||||
if self.instance and hasattr(self.instance, "project_id")
|
||||
else None
|
||||
self.instance.project_id if self.instance and hasattr(self.instance, "project_id") else None
|
||||
)
|
||||
|
||||
if not project_id:
|
||||
@ -166,9 +161,7 @@ class CycleIssueRequestSerializer(serializers.Serializer):
|
||||
cycle assignment and sprint planning workflows.
|
||||
"""
|
||||
|
||||
issues = serializers.ListField(
|
||||
child=serializers.UUIDField(), help_text="List of issue IDs to add to the cycle"
|
||||
)
|
||||
issues = serializers.ListField(child=serializers.UUIDField(), help_text="List of issue IDs to add to the cycle")
|
||||
|
||||
|
||||
class TransferCycleIssueRequestSerializer(serializers.Serializer):
|
||||
@ -179,6 +172,4 @@ class TransferCycleIssueRequestSerializer(serializers.Serializer):
|
||||
and relationship updates for sprint reallocation workflows.
|
||||
"""
|
||||
|
||||
new_cycle_id = serializers.UUIDField(
|
||||
help_text="ID of the target cycle to transfer issues to"
|
||||
)
|
||||
new_cycle_id = serializers.UUIDField(help_text="ID of the target cycle to transfer issues to")
|
||||
|
||||
@ -98,9 +98,7 @@ class IntakeIssueUpdateSerializer(BaseSerializer):
|
||||
and embedded issue updates for issue queue processing workflows.
|
||||
"""
|
||||
|
||||
issue = IssueForIntakeSerializer(
|
||||
required=False, help_text="Issue data to update in the intake issue"
|
||||
)
|
||||
issue = IssueForIntakeSerializer(required=False, help_text="Issue data to update in the intake issue")
|
||||
|
||||
class Meta:
|
||||
model = IntakeIssue
|
||||
@ -132,9 +130,5 @@ class IssueDataSerializer(serializers.Serializer):
|
||||
"""
|
||||
|
||||
name = serializers.CharField(max_length=255, help_text="Issue name")
|
||||
description_html = serializers.CharField(
|
||||
required=False, allow_null=True, help_text="Issue description HTML"
|
||||
)
|
||||
priority = serializers.ChoiceField(
|
||||
choices=Issue.PRIORITY_CHOICES, default="none", help_text="Issue priority"
|
||||
)
|
||||
description_html = serializers.CharField(required=False, allow_null=True, help_text="Issue description HTML")
|
||||
priority = serializers.ChoiceField(choices=Issue.PRIORITY_CHOICES, default="none", help_text="Issue priority")
|
||||
|
||||
@ -48,17 +48,13 @@ class IssueSerializer(BaseSerializer):
|
||||
"""
|
||||
|
||||
assignees = serializers.ListField(
|
||||
child=serializers.PrimaryKeyRelatedField(
|
||||
queryset=User.objects.values_list("id", flat=True)
|
||||
),
|
||||
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.values_list("id", flat=True)),
|
||||
write_only=True,
|
||||
required=False,
|
||||
)
|
||||
|
||||
labels = serializers.ListField(
|
||||
child=serializers.PrimaryKeyRelatedField(
|
||||
queryset=Label.objects.values_list("id", flat=True)
|
||||
),
|
||||
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.values_list("id", flat=True)),
|
||||
write_only=True,
|
||||
required=False,
|
||||
)
|
||||
@ -90,13 +86,9 @@ class IssueSerializer(BaseSerializer):
|
||||
|
||||
# Validate description content for security
|
||||
if data.get("description_html"):
|
||||
is_valid, error_msg, sanitized_html = validate_html_content(
|
||||
data["description_html"]
|
||||
)
|
||||
is_valid, error_msg, sanitized_html = validate_html_content(data["description_html"])
|
||||
if not is_valid:
|
||||
raise serializers.ValidationError(
|
||||
{"error": "html content is not valid"}
|
||||
)
|
||||
raise serializers.ValidationError({"error": "html content is not valid"})
|
||||
# Update the data with sanitized HTML if available
|
||||
if sanitized_html is not None:
|
||||
data["description_html"] = sanitized_html
|
||||
@ -104,9 +96,7 @@ class IssueSerializer(BaseSerializer):
|
||||
if data.get("description_binary"):
|
||||
is_valid, error_msg = validate_binary_data(data["description_binary"])
|
||||
if not is_valid:
|
||||
raise serializers.ValidationError(
|
||||
{"description_binary": "Invalid binary data"}
|
||||
)
|
||||
raise serializers.ValidationError({"description_binary": "Invalid binary data"})
|
||||
|
||||
# Validate assignees are from project
|
||||
if data.get("assignees", []):
|
||||
@ -126,13 +116,9 @@ class IssueSerializer(BaseSerializer):
|
||||
# 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
|
||||
).exists()
|
||||
and not State.objects.filter(project_id=self.context.get("project_id"), pk=data.get("state").id).exists()
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
"State is not valid please pass a valid state_id"
|
||||
)
|
||||
raise serializers.ValidationError("State is not valid please pass a valid state_id")
|
||||
|
||||
# Check parent issue is from workspace as it can be cross workspace
|
||||
if (
|
||||
@ -143,9 +129,7 @@ class IssueSerializer(BaseSerializer):
|
||||
pk=data.get("parent").id,
|
||||
).exists()
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
"Parent is not valid issue_id please pass a valid issue_id"
|
||||
)
|
||||
raise serializers.ValidationError("Parent is not valid issue_id please pass a valid issue_id")
|
||||
|
||||
if (
|
||||
data.get("estimate_point")
|
||||
@ -155,9 +139,7 @@ class IssueSerializer(BaseSerializer):
|
||||
pk=data.get("estimate_point").id,
|
||||
).exists()
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
"Estimate point is not valid please pass a valid estimate_point_id"
|
||||
)
|
||||
raise serializers.ValidationError("Estimate point is not valid please pass a valid estimate_point_id")
|
||||
|
||||
return data
|
||||
|
||||
@ -173,14 +155,10 @@ class IssueSerializer(BaseSerializer):
|
||||
|
||||
if not issue_type:
|
||||
# Get default issue type
|
||||
issue_type = IssueType.objects.filter(
|
||||
project_issue_types__project_id=project_id, is_default=True
|
||||
).first()
|
||||
issue_type = IssueType.objects.filter(project_issue_types__project_id=project_id, is_default=True).first()
|
||||
issue_type = issue_type
|
||||
|
||||
issue = Issue.objects.create(
|
||||
**validated_data, project_id=project_id, type=issue_type
|
||||
)
|
||||
issue = Issue.objects.create(**validated_data, project_id=project_id, type=issue_type)
|
||||
|
||||
# Issue Audit Users
|
||||
created_by_id = issue.created_by_id
|
||||
@ -312,35 +290,26 @@ class IssueSerializer(BaseSerializer):
|
||||
|
||||
data["assignees"] = UserLiteSerializer(
|
||||
User.objects.filter(
|
||||
pk__in=IssueAssignee.objects.filter(issue=instance).values_list(
|
||||
"assignee_id", flat=True
|
||||
)
|
||||
pk__in=IssueAssignee.objects.filter(issue=instance).values_list("assignee_id", flat=True)
|
||||
),
|
||||
many=True,
|
||||
).data
|
||||
else:
|
||||
data["assignees"] = [
|
||||
str(assignee)
|
||||
for assignee in IssueAssignee.objects.filter(
|
||||
issue=instance
|
||||
).values_list("assignee_id", flat=True)
|
||||
for assignee in IssueAssignee.objects.filter(issue=instance).values_list("assignee_id", flat=True)
|
||||
]
|
||||
if "labels" in self.fields:
|
||||
if "labels" in self.expand:
|
||||
data["labels"] = LabelSerializer(
|
||||
Label.objects.filter(
|
||||
pk__in=IssueLabel.objects.filter(issue=instance).values_list(
|
||||
"label_id", flat=True
|
||||
)
|
||||
pk__in=IssueLabel.objects.filter(issue=instance).values_list("label_id", flat=True)
|
||||
),
|
||||
many=True,
|
||||
).data
|
||||
else:
|
||||
data["labels"] = [
|
||||
str(label)
|
||||
for label in IssueLabel.objects.filter(issue=instance).values_list(
|
||||
"label_id", flat=True
|
||||
)
|
||||
str(label) for label in IssueLabel.objects.filter(issue=instance).values_list("label_id", flat=True)
|
||||
]
|
||||
|
||||
return data
|
||||
@ -452,12 +421,8 @@ class IssueLinkCreateSerializer(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")
|
||||
).exists():
|
||||
raise serializers.ValidationError(
|
||||
{"error": "URL already exists for this Issue"}
|
||||
)
|
||||
if IssueLink.objects.filter(url=validated_data.get("url"), issue_id=validated_data.get("issue_id")).exists():
|
||||
raise serializers.ValidationError({"error": "URL already exists for this Issue"})
|
||||
return IssueLink.objects.create(**validated_data)
|
||||
|
||||
|
||||
@ -478,15 +443,11 @@ class IssueLinkUpdateSerializer(IssueLinkCreateSerializer):
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
if (
|
||||
IssueLink.objects.filter(
|
||||
url=validated_data.get("url"), issue_id=instance.issue_id
|
||||
)
|
||||
IssueLink.objects.filter(url=validated_data.get("url"), issue_id=instance.issue_id)
|
||||
.exclude(pk=instance.id)
|
||||
.exists()
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
{"error": "URL already exists for this Issue"}
|
||||
)
|
||||
raise serializers.ValidationError({"error": "URL already exists for this Issue"})
|
||||
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
@ -677,17 +638,13 @@ class IssueExpandSerializer(BaseSerializer):
|
||||
expand = self.context.get("expand", [])
|
||||
if "labels" in expand:
|
||||
# Use prefetched data
|
||||
return LabelLiteSerializer(
|
||||
[il.label for il in obj.label_issue.all()], many=True
|
||||
).data
|
||||
return LabelLiteSerializer([il.label for il in obj.label_issue.all()], many=True).data
|
||||
return [il.label_id for il in obj.label_issue.all()]
|
||||
|
||||
def get_assignees(self, obj):
|
||||
expand = self.context.get("expand", [])
|
||||
if "assignees" in expand:
|
||||
return UserLiteSerializer(
|
||||
[ia.assignee for ia in obj.issue_assignee.all()], many=True
|
||||
).data
|
||||
return UserLiteSerializer([ia.assignee for ia in obj.issue_assignee.all()], many=True).data
|
||||
return [ia.assignee_id for ia in obj.issue_assignee.all()]
|
||||
|
||||
class Meta:
|
||||
@ -735,8 +692,6 @@ class IssueSearchSerializer(serializers.Serializer):
|
||||
id = serializers.CharField(required=True, help_text="Issue ID")
|
||||
name = serializers.CharField(required=True, help_text="Issue name")
|
||||
sequence_id = serializers.CharField(required=True, help_text="Issue sequence ID")
|
||||
project__identifier = serializers.CharField(
|
||||
required=True, help_text="Project identifier"
|
||||
)
|
||||
project__identifier = serializers.CharField(required=True, help_text="Project identifier")
|
||||
project_id = serializers.CharField(required=True, help_text="Project ID")
|
||||
workspace__slug = serializers.CharField(required=True, help_text="Workspace slug")
|
||||
|
||||
@ -76,9 +76,15 @@ class ModuleCreateSerializer(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():
|
||||
module = Module.objects.filter(name=module_name, project_id=project_id).first()
|
||||
if module:
|
||||
raise serializers.ValidationError(
|
||||
{"error": "Module with this name already exists"}
|
||||
{
|
||||
"id": str(module.id),
|
||||
"code": "MODULE_NAME_ALREADY_EXISTS",
|
||||
"error": "Module with this name already exists",
|
||||
"message": "Module with this name already exists",
|
||||
}
|
||||
)
|
||||
|
||||
module = Module.objects.create(**validated_data, project_id=project_id)
|
||||
@ -123,14 +129,8 @@ class ModuleUpdateSerializer(ModuleCreateSerializer):
|
||||
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=instance.project)
|
||||
.exclude(id=instance.id)
|
||||
.exists()
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
{"error": "Module with this name already exists"}
|
||||
)
|
||||
if Module.objects.filter(name=module_name, project=instance.project).exclude(id=instance.id).exists():
|
||||
raise serializers.ValidationError({"error": "Module with this name already exists"})
|
||||
|
||||
if members is not None:
|
||||
ModuleMember.objects.filter(module=instance).delete()
|
||||
@ -240,12 +240,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")
|
||||
).exists():
|
||||
raise serializers.ValidationError(
|
||||
{"error": "URL already exists for this Issue"}
|
||||
)
|
||||
if ModuleLink.objects.filter(url=validated_data.get("url"), module_id=validated_data.get("module_id")).exists():
|
||||
raise serializers.ValidationError({"error": "URL already exists for this Issue"})
|
||||
return ModuleLink.objects.create(**validated_data)
|
||||
|
||||
|
||||
|
||||
@ -66,9 +66,7 @@ class ProjectCreateSerializer(BaseSerializer):
|
||||
workspace_id=self.context["workspace_id"],
|
||||
member_id=data.get("project_lead"),
|
||||
).exists():
|
||||
raise serializers.ValidationError(
|
||||
"Project lead should be a user in the workspace"
|
||||
)
|
||||
raise serializers.ValidationError("Project lead should be a user in the workspace")
|
||||
|
||||
if data.get("default_assignee", None) is not None:
|
||||
# Check if the default assignee is a member of the workspace
|
||||
@ -76,9 +74,7 @@ class ProjectCreateSerializer(BaseSerializer):
|
||||
workspace_id=self.context["workspace_id"],
|
||||
member_id=data.get("default_assignee"),
|
||||
).exists():
|
||||
raise serializers.ValidationError(
|
||||
"Default assignee should be a user in the workspace"
|
||||
)
|
||||
raise serializers.ValidationError("Default assignee should be a user in the workspace")
|
||||
|
||||
return data
|
||||
|
||||
@ -87,14 +83,10 @@ class ProjectCreateSerializer(BaseSerializer):
|
||||
if identifier == "":
|
||||
raise serializers.ValidationError(detail="Project Identifier is required")
|
||||
|
||||
if ProjectIdentifier.objects.filter(
|
||||
name=identifier, workspace_id=self.context["workspace_id"]
|
||||
).exists():
|
||||
if ProjectIdentifier.objects.filter(name=identifier, workspace_id=self.context["workspace_id"]).exists():
|
||||
raise serializers.ValidationError(detail="Project Identifier is taken")
|
||||
|
||||
project = Project.objects.create(
|
||||
**validated_data, workspace_id=self.context["workspace_id"]
|
||||
)
|
||||
project = Project.objects.create(**validated_data, workspace_id=self.context["workspace_id"])
|
||||
return project
|
||||
|
||||
|
||||
@ -119,25 +111,17 @@ class ProjectUpdateSerializer(ProjectCreateSerializer):
|
||||
"""Update a project"""
|
||||
if (
|
||||
validated_data.get("default_state", None) is not None
|
||||
and not State.objects.filter(
|
||||
project=instance, id=validated_data.get("default_state")
|
||||
).exists()
|
||||
and not State.objects.filter(project=instance, id=validated_data.get("default_state")).exists()
|
||||
):
|
||||
# Check if the default state is a state in the project
|
||||
raise serializers.ValidationError(
|
||||
"Default state should be a state in the project"
|
||||
)
|
||||
raise serializers.ValidationError("Default state should be a state in the project")
|
||||
|
||||
if (
|
||||
validated_data.get("estimate", None) is not None
|
||||
and not Estimate.objects.filter(
|
||||
project=instance, id=validated_data.get("estimate")
|
||||
).exists()
|
||||
and not Estimate.objects.filter(project=instance, id=validated_data.get("estimate")).exists()
|
||||
):
|
||||
# Check if the estimate is a estimate in the project
|
||||
raise serializers.ValidationError(
|
||||
"Estimate should be a estimate in the project"
|
||||
)
|
||||
raise serializers.ValidationError("Estimate should be a estimate in the project")
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
@ -182,9 +166,7 @@ class ProjectSerializer(BaseSerializer):
|
||||
member_id=data.get("project_lead"),
|
||||
).exists()
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
"Project lead should be a user in the workspace"
|
||||
)
|
||||
raise serializers.ValidationError("Project lead should be a user in the workspace")
|
||||
|
||||
# Check default assignee should be a member of the workspace
|
||||
if (
|
||||
@ -194,23 +176,17 @@ class ProjectSerializer(BaseSerializer):
|
||||
member_id=data.get("default_assignee"),
|
||||
).exists()
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
"Default assignee should be a user in the workspace"
|
||||
)
|
||||
raise serializers.ValidationError("Default assignee should be a user in the workspace")
|
||||
|
||||
# Validate description content for security
|
||||
if "description_html" in data and data["description_html"]:
|
||||
if isinstance(data["description_html"], dict):
|
||||
is_valid, error_msg, sanitized_html = validate_html_content(
|
||||
str(data["description_html"])
|
||||
)
|
||||
is_valid, error_msg, sanitized_html = validate_html_content(str(data["description_html"]))
|
||||
# Update the data with sanitized HTML if available
|
||||
if sanitized_html is not None:
|
||||
data["description_html"] = sanitized_html
|
||||
if not is_valid:
|
||||
raise serializers.ValidationError(
|
||||
{"error": "html content is not valid"}
|
||||
)
|
||||
raise serializers.ValidationError({"error": "html content is not valid"})
|
||||
|
||||
return data
|
||||
|
||||
@ -219,14 +195,10 @@ class ProjectSerializer(BaseSerializer):
|
||||
if identifier == "":
|
||||
raise serializers.ValidationError(detail="Project Identifier is required")
|
||||
|
||||
if ProjectIdentifier.objects.filter(
|
||||
name=identifier, workspace_id=self.context["workspace_id"]
|
||||
).exists():
|
||||
if ProjectIdentifier.objects.filter(name=identifier, workspace_id=self.context["workspace_id"]).exists():
|
||||
raise serializers.ValidationError(detail="Project Identifier is taken")
|
||||
|
||||
project = Project.objects.create(
|
||||
**validated_data, workspace_id=self.context["workspace_id"]
|
||||
)
|
||||
project = Project.objects.create(**validated_data, workspace_id=self.context["workspace_id"])
|
||||
_ = ProjectIdentifier.objects.create(
|
||||
name=project.identifier,
|
||||
project=project,
|
||||
|
||||
@ -14,9 +14,7 @@ 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:
|
||||
|
||||
@ -14,9 +14,7 @@ urlpatterns = [
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/intake-issues/<uuid:issue_id>/",
|
||||
IntakeIssueDetailAPIEndpoint.as_view(
|
||||
http_method_names=["get", "patch", "delete"]
|
||||
),
|
||||
IntakeIssueDetailAPIEndpoint.as_view(http_method_names=["get", "patch", "delete"]),
|
||||
name="intake-issue",
|
||||
),
|
||||
]
|
||||
|
||||
@ -55,9 +55,7 @@ urlpatterns = [
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/links/<uuid:pk>/",
|
||||
IssueLinkDetailAPIEndpoint.as_view(
|
||||
http_method_names=["get", "patch", "delete"]
|
||||
),
|
||||
IssueLinkDetailAPIEndpoint.as_view(http_method_names=["get", "patch", "delete"]),
|
||||
name="link",
|
||||
),
|
||||
path(
|
||||
@ -67,9 +65,7 @@ urlpatterns = [
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/comments/<uuid:pk>/",
|
||||
IssueCommentDetailAPIEndpoint.as_view(
|
||||
http_method_names=["get", "patch", "delete"]
|
||||
),
|
||||
IssueCommentDetailAPIEndpoint.as_view(http_method_names=["get", "patch", "delete"]),
|
||||
name="comment",
|
||||
),
|
||||
path(
|
||||
@ -89,9 +85,7 @@ urlpatterns = [
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-attachments/<uuid:pk>/",
|
||||
IssueAttachmentDetailAPIEndpoint.as_view(
|
||||
http_method_names=["get", "patch", "delete"]
|
||||
),
|
||||
IssueAttachmentDetailAPIEndpoint.as_view(http_method_names=["get", "patch", "delete"]),
|
||||
name="issue-attachment",
|
||||
),
|
||||
]
|
||||
|
||||
@ -19,9 +19,7 @@ urlpatterns = [
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/archive/",
|
||||
ProjectArchiveUnarchiveAPIEndpoint.as_view(
|
||||
http_method_names=["post", "delete"]
|
||||
),
|
||||
ProjectArchiveUnarchiveAPIEndpoint.as_view(http_method_names=["post", "delete"]),
|
||||
name="project-archive-unarchive",
|
||||
),
|
||||
]
|
||||
|
||||
@ -158,9 +158,7 @@ class UserAssetEndpoint(BaseAPIView):
|
||||
# Get the presigned URL
|
||||
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
|
||||
)
|
||||
presigned_url = storage.generate_presigned_post(object_name=asset_key, file_type=type, file_size=size_limit)
|
||||
# Return the presigned URL
|
||||
return Response(
|
||||
{
|
||||
@ -236,9 +234,7 @@ class UserAssetEndpoint(BaseAPIView):
|
||||
asset.is_deleted = True
|
||||
asset.deleted_at = timezone.now()
|
||||
# get the entity and save the asset id for the request field
|
||||
self.entity_asset_delete(
|
||||
entity_type=asset.entity_type, asset=asset, request=request
|
||||
)
|
||||
self.entity_asset_delete(entity_type=asset.entity_type, asset=asset, request=request)
|
||||
asset.save(update_fields=["is_deleted", "deleted_at"])
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@ -335,9 +331,7 @@ class UserServerAssetEndpoint(BaseAPIView):
|
||||
# Get the presigned URL
|
||||
storage = S3Storage(request=request, is_server=True)
|
||||
# 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
|
||||
)
|
||||
presigned_url = storage.generate_presigned_post(object_name=asset_key, file_type=type, file_size=size_limit)
|
||||
# Return the presigned URL
|
||||
return Response(
|
||||
{
|
||||
@ -389,16 +383,15 @@ class UserServerAssetEndpoint(BaseAPIView):
|
||||
def delete(self, request, asset_id):
|
||||
"""Delete user server asset.
|
||||
|
||||
Delete a user profile asset (avatar or cover image) using server credentials and remove its reference from the user profile.
|
||||
This performs a soft delete by marking the asset as deleted and updating the user's profile.
|
||||
Delete a user profile asset (avatar or cover image) using server credentials and
|
||||
remove its reference from the user profile. This performs a soft delete by marking the
|
||||
asset as deleted and updating the user's profile.
|
||||
"""
|
||||
asset = FileAsset.objects.get(id=asset_id, user_id=request.user.id)
|
||||
asset.is_deleted = True
|
||||
asset.deleted_at = timezone.now()
|
||||
# get the entity and save the asset id for the request field
|
||||
self.entity_asset_delete(
|
||||
entity_type=asset.entity_type, asset=asset, request=request
|
||||
)
|
||||
self.entity_asset_delete(entity_type=asset.entity_type, asset=asset, request=request)
|
||||
asset.save(update_fields=["is_deleted", "deleted_at"])
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@ -430,9 +423,7 @@ class GenericAssetEndpoint(BaseAPIView):
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
|
||||
# Get the asset
|
||||
asset = FileAsset.objects.get(
|
||||
id=asset_id, workspace_id=workspace.id, is_deleted=False
|
||||
)
|
||||
asset = FileAsset.objects.get(id=asset_id, workspace_id=workspace.id, is_deleted=False)
|
||||
|
||||
# Check if the asset exists and is uploaded
|
||||
if not asset.is_uploaded:
|
||||
@ -458,13 +449,9 @@ class GenericAssetEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
except Workspace.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Workspace not found"}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
return Response({"error": "Workspace not found"}, status=status.HTTP_404_NOT_FOUND)
|
||||
except FileAsset.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Asset not found"}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
return Response({"error": "Asset not found"}, status=status.HTTP_404_NOT_FOUND)
|
||||
except Exception as e:
|
||||
log_exception(e)
|
||||
return Response(
|
||||
@ -566,14 +553,12 @@ class GenericAssetEndpoint(BaseAPIView):
|
||||
created_by=request.user,
|
||||
external_id=external_id,
|
||||
external_source=external_source,
|
||||
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT, # Using ISSUE_ATTACHMENT since we'll bind it to issues
|
||||
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT, # Using ISSUE_ATTACHMENT since we'll bind it to issues # noqa: E501
|
||||
)
|
||||
|
||||
# Get the presigned URL
|
||||
storage = S3Storage(request=request, is_server=True)
|
||||
presigned_url = storage.generate_presigned_post(
|
||||
object_name=asset_key, file_type=type, file_size=size_limit
|
||||
)
|
||||
presigned_url = storage.generate_presigned_post(object_name=asset_key, file_type=type, file_size=size_limit)
|
||||
|
||||
return Response(
|
||||
{
|
||||
@ -612,9 +597,7 @@ class GenericAssetEndpoint(BaseAPIView):
|
||||
and trigger metadata extraction.
|
||||
"""
|
||||
try:
|
||||
asset = FileAsset.objects.get(
|
||||
id=asset_id, workspace__slug=slug, is_deleted=False
|
||||
)
|
||||
asset = FileAsset.objects.get(id=asset_id, workspace__slug=slug, is_deleted=False)
|
||||
|
||||
# Update is_uploaded status
|
||||
asset.is_uploaded = request.data.get("is_uploaded", asset.is_uploaded)
|
||||
@ -627,6 +610,4 @@ class GenericAssetEndpoint(BaseAPIView):
|
||||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
except FileAsset.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Asset not found"}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
return Response({"error": "Asset not found"}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
@ -37,9 +37,7 @@ class TimezoneMixin:
|
||||
timezone.deactivate()
|
||||
|
||||
|
||||
class BaseAPIView(
|
||||
TimezoneMixin, GenericAPIView, ReadReplicaControlMixin, BasePaginator
|
||||
):
|
||||
class BaseAPIView(TimezoneMixin, GenericAPIView, ReadReplicaControlMixin, BasePaginator):
|
||||
authentication_classes = [APIKeyAuthentication]
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
@ -56,9 +54,7 @@ class BaseAPIView(
|
||||
api_key = self.request.headers.get("X-Api-Key")
|
||||
|
||||
if api_key:
|
||||
service_token = APIToken.objects.filter(
|
||||
token=api_key, is_service=True
|
||||
).first()
|
||||
service_token = APIToken.objects.filter(token=api_key, is_service=True).first()
|
||||
|
||||
if service_token:
|
||||
throttle_classes.append(ServiceTokenRateThrottle())
|
||||
@ -113,9 +109,7 @@ class BaseAPIView(
|
||||
if settings.DEBUG:
|
||||
from django.db import connection
|
||||
|
||||
print(
|
||||
f"{request.method} - {request.get_full_path()} of Queries: {len(connection.queries)}"
|
||||
)
|
||||
print(f"{request.method} - {request.get_full_path()} of Queries: {len(connection.queries)}")
|
||||
return response
|
||||
except Exception as exc:
|
||||
response = self.handle_exception(exc)
|
||||
@ -151,14 +145,10 @@ class BaseAPIView(
|
||||
|
||||
@property
|
||||
def fields(self):
|
||||
fields = [
|
||||
field for field in self.request.GET.get("fields", "").split(",") if field
|
||||
]
|
||||
fields = [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 = [expand for expand in self.request.GET.get("expand", "").split(",") if expand]
|
||||
return expand if expand else None
|
||||
|
||||
@ -171,7 +171,7 @@ class CycleListCreateAPIEndpoint(BaseAPIView):
|
||||
@cycle_docs(
|
||||
operation_id="list_cycles",
|
||||
summary="List cycles",
|
||||
description="Retrieve all cycles in a project. Supports filtering by cycle status like current, upcoming, completed, or draft.",
|
||||
description="Retrieve all cycles in a project. Supports filtering by cycle status like current, upcoming, completed, or draft.", # noqa: E501
|
||||
parameters=[
|
||||
CURSOR_PARAMETER,
|
||||
PER_PAGE_PARAMETER,
|
||||
@ -201,9 +201,7 @@ class CycleListCreateAPIEndpoint(BaseAPIView):
|
||||
|
||||
# Current Cycle
|
||||
if cycle_view == "current":
|
||||
queryset = queryset.filter(
|
||||
start_date__lte=timezone.now(), end_date__gte=timezone.now()
|
||||
)
|
||||
queryset = queryset.filter(start_date__lte=timezone.now(), end_date__gte=timezone.now())
|
||||
data = CycleSerializer(
|
||||
queryset,
|
||||
many=True,
|
||||
@ -260,9 +258,7 @@ class CycleListCreateAPIEndpoint(BaseAPIView):
|
||||
|
||||
# Incomplete Cycles
|
||||
if cycle_view == "incomplete":
|
||||
queryset = queryset.filter(
|
||||
Q(end_date__gte=timezone.now()) | Q(end_date__isnull=True)
|
||||
)
|
||||
queryset = queryset.filter(Q(end_date__gte=timezone.now()) | Q(end_date__isnull=True))
|
||||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(queryset),
|
||||
@ -289,7 +285,7 @@ class CycleListCreateAPIEndpoint(BaseAPIView):
|
||||
@cycle_docs(
|
||||
operation_id="create_cycle",
|
||||
summary="Create cycle",
|
||||
description="Create a new development cycle with specified name, description, and date range. Supports external ID tracking for integration purposes.",
|
||||
description="Create a new development cycle with specified name, description, and date range. Supports external ID tracking for integration purposes.", # noqa: E501
|
||||
request=OpenApiRequest(
|
||||
request=CycleCreateSerializer,
|
||||
examples=[CYCLE_CREATE_EXAMPLE],
|
||||
@ -308,12 +304,8 @@ class CycleListCreateAPIEndpoint(BaseAPIView):
|
||||
Create a new development cycle with specified name, description, and date range.
|
||||
Supports external ID tracking for integration purposes.
|
||||
"""
|
||||
if (
|
||||
request.data.get("start_date", None) is None
|
||||
and request.data.get("end_date", None) is None
|
||||
) or (
|
||||
request.data.get("start_date", None) is not None
|
||||
and request.data.get("end_date", None) is not None
|
||||
if (request.data.get("start_date", None) is None and request.data.get("end_date", None) is None) or (
|
||||
request.data.get("start_date", None) is not None and request.data.get("end_date", None) is not None
|
||||
):
|
||||
serializer = CycleCreateSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
@ -358,9 +350,7 @@ class CycleListCreateAPIEndpoint(BaseAPIView):
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
else:
|
||||
return Response(
|
||||
{
|
||||
"error": "Both start date and end date are either required or are to be null"
|
||||
},
|
||||
{"error": "Both start date and end date are either required or are to be null"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@ -487,7 +477,7 @@ class CycleDetailAPIEndpoint(BaseAPIView):
|
||||
@cycle_docs(
|
||||
operation_id="update_cycle",
|
||||
summary="Update cycle",
|
||||
description="Modify an existing cycle's properties like name, description, or date range. Completed cycles can only have their sort order changed.",
|
||||
description="Modify an existing cycle's properties like name, description, or date range. Completed cycles can only have their sort order changed.", # noqa: E501
|
||||
request=OpenApiRequest(
|
||||
request=CycleUpdateSerializer,
|
||||
examples=[CYCLE_UPDATE_EXAMPLE],
|
||||
@ -508,9 +498,7 @@ class CycleDetailAPIEndpoint(BaseAPIView):
|
||||
"""
|
||||
cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
|
||||
|
||||
current_instance = json.dumps(
|
||||
CycleSerializer(cycle).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
current_instance = json.dumps(CycleSerializer(cycle).data, cls=DjangoJSONEncoder)
|
||||
|
||||
if cycle.archived_at:
|
||||
return Response(
|
||||
@ -523,14 +511,10 @@ class CycleDetailAPIEndpoint(BaseAPIView):
|
||||
if cycle.end_date is not None and cycle.end_date < timezone.now():
|
||||
if "sort_order" in request_data:
|
||||
# Can only change sort order
|
||||
request_data = {
|
||||
"sort_order": request_data.get("sort_order", cycle.sort_order)
|
||||
}
|
||||
request_data = {"sort_order": request_data.get("sort_order", cycle.sort_order)}
|
||||
else:
|
||||
return Response(
|
||||
{
|
||||
"error": "The Cycle has already been completed so it cannot be edited"
|
||||
},
|
||||
{"error": "The Cycle has already been completed so it cannot be edited"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@ -542,9 +526,7 @@ class CycleDetailAPIEndpoint(BaseAPIView):
|
||||
and Cycle.objects.filter(
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
external_source=request.data.get(
|
||||
"external_source", cycle.external_source
|
||||
),
|
||||
external_source=request.data.get("external_source", cycle.external_source),
|
||||
external_id=request.data.get("external_id"),
|
||||
).exists()
|
||||
):
|
||||
@ -601,11 +583,7 @@ class CycleDetailAPIEndpoint(BaseAPIView):
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
cycle_issues = list(
|
||||
CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list(
|
||||
"issue", flat=True
|
||||
)
|
||||
)
|
||||
cycle_issues = list(CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list("issue", flat=True))
|
||||
|
||||
issue_activity.delay(
|
||||
type="cycle.activity.deleted",
|
||||
@ -625,9 +603,7 @@ class CycleDetailAPIEndpoint(BaseAPIView):
|
||||
# Delete the cycle
|
||||
cycle.delete()
|
||||
# Delete the user favorite cycle
|
||||
UserFavorite.objects.filter(
|
||||
entity_type="cycle", entity_identifier=pk, project_id=project_id
|
||||
).delete()
|
||||
UserFavorite.objects.filter(entity_type="cycle", entity_identifier=pk, project_id=project_id).delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
@ -765,15 +741,13 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
|
||||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(self.get_queryset()),
|
||||
on_results=lambda cycles: CycleSerializer(
|
||||
cycles, many=True, fields=self.fields, expand=self.expand
|
||||
).data,
|
||||
on_results=lambda cycles: CycleSerializer(cycles, many=True, fields=self.fields, expand=self.expand).data,
|
||||
)
|
||||
|
||||
@cycle_docs(
|
||||
operation_id="archive_cycle",
|
||||
summary="Archive cycle",
|
||||
description="Move a completed cycle to archived status for historical tracking. Only cycles that have ended can be archived.",
|
||||
description="Move a completed cycle to archived status for historical tracking. Only cycles that have ended can be archived.", # noqa: E501
|
||||
request={},
|
||||
responses={
|
||||
204: ARCHIVED_RESPONSE,
|
||||
@ -786,9 +760,7 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
|
||||
Move a completed cycle to archived status for historical tracking.
|
||||
Only cycles that have ended can be archived.
|
||||
"""
|
||||
cycle = Cycle.objects.get(
|
||||
pk=cycle_id, project_id=project_id, workspace__slug=slug
|
||||
)
|
||||
cycle = Cycle.objects.get(pk=cycle_id, project_id=project_id, workspace__slug=slug)
|
||||
if cycle.end_date >= timezone.now():
|
||||
return Response(
|
||||
{"error": "Only completed cycles can be archived"},
|
||||
@ -819,9 +791,7 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
|
||||
Restore an archived cycle to active status, making it available for regular use.
|
||||
The cycle will reappear in active cycle lists.
|
||||
"""
|
||||
cycle = Cycle.objects.get(
|
||||
pk=cycle_id, project_id=project_id, workspace__slug=slug
|
||||
)
|
||||
cycle = Cycle.objects.get(pk=cycle_id, project_id=project_id, workspace__slug=slug)
|
||||
cycle.archived_at = None
|
||||
cycle.save()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
@ -884,9 +854,7 @@ class CycleIssueListCreateAPIEndpoint(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, issue_cycle__deleted_at__isnull=True)
|
||||
.annotate(
|
||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
||||
.order_by()
|
||||
@ -923,15 +891,13 @@ class CycleIssueListCreateAPIEndpoint(BaseAPIView):
|
||||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(issues),
|
||||
on_results=lambda issues: IssueSerializer(
|
||||
issues, many=True, fields=self.fields, expand=self.expand
|
||||
).data,
|
||||
on_results=lambda issues: IssueSerializer(issues, many=True, fields=self.fields, expand=self.expand).data,
|
||||
)
|
||||
|
||||
@cycle_docs(
|
||||
operation_id="add_cycle_work_items",
|
||||
summary="Add Work Items to Cycle",
|
||||
description="Assign multiple work items to a cycle. Automatically handles bulk creation and updates with activity tracking.",
|
||||
description="Assign multiple work items to a cycle. Automatically handles bulk creation and updates with activity tracking.", # noqa: E501
|
||||
request=OpenApiRequest(
|
||||
request=CycleIssueRequestSerializer,
|
||||
examples=[CYCLE_ISSUE_REQUEST_EXAMPLE],
|
||||
@ -955,22 +921,24 @@ class CycleIssueListCreateAPIEndpoint(BaseAPIView):
|
||||
|
||||
if not issues:
|
||||
return Response(
|
||||
{"error": "Work items are required"}, status=status.HTTP_400_BAD_REQUEST
|
||||
{"error": "Work items are required", "code": "MISSING_WORK_ITEMS"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
cycle = Cycle.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=cycle_id
|
||||
)
|
||||
cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=cycle_id)
|
||||
|
||||
if cycle.end_date is not None and cycle.end_date < timezone.now():
|
||||
return Response(
|
||||
{
|
||||
"code": "CYCLE_COMPLETED",
|
||||
"message": "The Cycle has already been completed so no new issues can be added",
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Get all CycleWorkItems already created
|
||||
cycle_issues = list(
|
||||
CycleIssue.objects.filter(~Q(cycle_id=cycle_id), issue_id__in=issues)
|
||||
)
|
||||
|
||||
cycle_issues = list(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
|
||||
if str(cycle_issue.issue_id) in issues
|
||||
str(cycle_issue.issue_id) for cycle_issue in cycle_issues if str(cycle_issue.issue_id) in issues
|
||||
]
|
||||
new_issues = list(set(issues) - set(existing_issues))
|
||||
|
||||
@ -1021,9 +989,7 @@ class CycleIssueListCreateAPIEndpoint(BaseAPIView):
|
||||
current_instance=json.dumps(
|
||||
{
|
||||
"updated_cycle_issues": update_cycle_issue_activity,
|
||||
"created_cycle_issues": serializers.serialize(
|
||||
"json", created_records
|
||||
),
|
||||
"created_cycle_issues": serializers.serialize("json", created_records),
|
||||
}
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
@ -1099,9 +1065,7 @@ class CycleIssueDetailAPIEndpoint(BaseAPIView):
|
||||
cycle_id=cycle_id,
|
||||
issue_id=issue_id,
|
||||
)
|
||||
serializer = CycleIssueSerializer(
|
||||
cycle_issue, fields=self.fields, expand=self.expand
|
||||
)
|
||||
serializer = CycleIssueSerializer(cycle_issue, fields=self.fields, expand=self.expand)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@cycle_docs(
|
||||
@ -1154,7 +1118,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
||||
@cycle_docs(
|
||||
operation_id="transfer_cycle_work_items",
|
||||
summary="Transfer cycle work items",
|
||||
description="Move incomplete work items from the current cycle to a new target cycle. Captures progress snapshot and transfers only unfinished work items.",
|
||||
description="Move incomplete work items from the current cycle to a new target cycle. Captures progress snapshot and transfers only unfinished work items.", # noqa: E501
|
||||
request=OpenApiRequest(
|
||||
request=TransferCycleIssueRequestSerializer,
|
||||
examples=[TRANSFER_CYCLE_ISSUE_EXAMPLE],
|
||||
@ -1207,14 +1171,10 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
new_cycle = Cycle.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id, pk=new_cycle_id
|
||||
).first()
|
||||
new_cycle = Cycle.objects.filter(workspace__slug=slug, project_id=project_id, pk=new_cycle_id).first()
|
||||
|
||||
old_cycle = (
|
||||
Cycle.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id, pk=cycle_id
|
||||
)
|
||||
Cycle.objects.filter(workspace__slug=slug, project_id=project_id, pk=cycle_id)
|
||||
.annotate(
|
||||
total_issues=Count(
|
||||
"issue_cycle",
|
||||
@ -1324,9 +1284,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
||||
)
|
||||
)
|
||||
.values("display_name", "assignee_id", "avatar", "avatar_url")
|
||||
.annotate(
|
||||
total_estimates=Sum(Cast("estimate_point__value", FloatField()))
|
||||
)
|
||||
.annotate(total_estimates=Sum(Cast("estimate_point__value", FloatField())))
|
||||
.annotate(
|
||||
completed_estimates=Sum(
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
@ -1353,9 +1311,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
||||
assignee_estimate_distribution = [
|
||||
{
|
||||
"display_name": item["display_name"],
|
||||
"assignee_id": (
|
||||
str(item["assignee_id"]) if item["assignee_id"] else None
|
||||
),
|
||||
"assignee_id": (str(item["assignee_id"]) if item["assignee_id"] else None),
|
||||
"avatar": item.get("avatar", None),
|
||||
"avatar_url": item.get("avatar_url", None),
|
||||
"total_estimates": item["total_estimates"],
|
||||
@ -1376,9 +1332,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
||||
.annotate(color=F("labels__color"))
|
||||
.annotate(label_id=F("labels__id"))
|
||||
.values("label_name", "color", "label_id")
|
||||
.annotate(
|
||||
total_estimates=Sum(Cast("estimate_point__value", FloatField()))
|
||||
)
|
||||
.annotate(total_estimates=Sum(Cast("estimate_point__value", FloatField())))
|
||||
.annotate(
|
||||
completed_estimates=Sum(
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
@ -1445,19 +1399,13 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
||||
),
|
||||
),
|
||||
# If `avatar_asset` is None, fall back to using `avatar` field directly
|
||||
When(
|
||||
assignees__avatar_asset__isnull=True, then="assignees__avatar"
|
||||
),
|
||||
When(assignees__avatar_asset__isnull=True, then="assignees__avatar"),
|
||||
default=Value(None),
|
||||
output_field=models.CharField(),
|
||||
)
|
||||
)
|
||||
.values("display_name", "assignee_id", "avatar_url")
|
||||
.annotate(
|
||||
total_issues=Count(
|
||||
"id", filter=Q(archived_at__isnull=True, is_draft=False)
|
||||
)
|
||||
)
|
||||
.annotate(total_issues=Count("id", filter=Q(archived_at__isnull=True, is_draft=False)))
|
||||
.annotate(
|
||||
completed_issues=Count(
|
||||
"id",
|
||||
@ -1484,9 +1432,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
||||
assignee_distribution_data = [
|
||||
{
|
||||
"display_name": item["display_name"],
|
||||
"assignee_id": (
|
||||
str(item["assignee_id"]) if item["assignee_id"] else None
|
||||
),
|
||||
"assignee_id": (str(item["assignee_id"]) if item["assignee_id"] else None),
|
||||
"avatar": item.get("avatar", None),
|
||||
"avatar_url": item.get("avatar_url", None),
|
||||
"total_issues": item["total_issues"],
|
||||
@ -1508,11 +1454,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
||||
.annotate(color=F("labels__color"))
|
||||
.annotate(label_id=F("labels__id"))
|
||||
.values("label_name", "color", "label_id")
|
||||
.annotate(
|
||||
total_issues=Count(
|
||||
"id", filter=Q(archived_at__isnull=True, is_draft=False)
|
||||
)
|
||||
)
|
||||
.annotate(total_issues=Count("id", filter=Q(archived_at__isnull=True, is_draft=False)))
|
||||
.annotate(
|
||||
completed_issues=Count(
|
||||
"id",
|
||||
@ -1558,9 +1500,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
||||
cycle_id=cycle_id,
|
||||
)
|
||||
|
||||
current_cycle = Cycle.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id, pk=cycle_id
|
||||
).first()
|
||||
current_cycle = Cycle.objects.filter(workspace__slug=slug, project_id=project_id, pk=cycle_id).first()
|
||||
|
||||
current_cycle.progress_snapshot = {
|
||||
"total_issues": old_cycle.total_issues,
|
||||
@ -1588,9 +1528,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
||||
|
||||
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"
|
||||
},
|
||||
{"error": "The cycle where the issues are transferred is already completed"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@ -1614,9 +1552,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
||||
}
|
||||
)
|
||||
|
||||
cycle_issues = CycleIssue.objects.bulk_update(
|
||||
updated_cycles, ["cycle_id"], batch_size=100
|
||||
)
|
||||
cycle_issues = CycleIssue.objects.bulk_update(updated_cycles, ["cycle_id"], batch_size=100)
|
||||
|
||||
# Capture Issue Activity
|
||||
issue_activity.delay(
|
||||
|
||||
@ -62,9 +62,7 @@ class IntakeIssueListCreateAPIEndpoint(BaseAPIView):
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
).first()
|
||||
|
||||
project = Project.objects.get(
|
||||
workspace__slug=self.kwargs.get("slug"), pk=self.kwargs.get("project_id")
|
||||
)
|
||||
project = Project.objects.get(workspace__slug=self.kwargs.get("slug"), pk=self.kwargs.get("project_id"))
|
||||
|
||||
if intake is None or not project.intake_view:
|
||||
return IntakeIssue.objects.none()
|
||||
@ -83,7 +81,7 @@ class IntakeIssueListCreateAPIEndpoint(BaseAPIView):
|
||||
@intake_docs(
|
||||
operation_id="get_intake_work_items_list",
|
||||
summary="List intake work items",
|
||||
description="Retrieve all work items in the project's intake queue. Returns paginated results when listing all intake work items.",
|
||||
description="Retrieve all work items in the project's intake queue. Returns paginated results when listing all intake work items.", # noqa: E501
|
||||
parameters=[
|
||||
WORKSPACE_SLUG_PARAMETER,
|
||||
PROJECT_ID_PARAMETER,
|
||||
@ -119,7 +117,7 @@ class IntakeIssueListCreateAPIEndpoint(BaseAPIView):
|
||||
@intake_docs(
|
||||
operation_id="create_intake_work_item",
|
||||
summary="Create intake work item",
|
||||
description="Submit a new work item to the project's intake queue for review and triage. Automatically creates the work item with default triage state and tracks activity.",
|
||||
description="Submit a new work item to the project's intake queue for review and triage. Automatically creates the work item with default triage state and tracks activity.", # noqa: E501
|
||||
parameters=[
|
||||
WORKSPACE_SLUG_PARAMETER,
|
||||
PROJECT_ID_PARAMETER,
|
||||
@ -144,22 +142,16 @@ class IntakeIssueListCreateAPIEndpoint(BaseAPIView):
|
||||
Automatically creates the work item with default triage state and tracks activity.
|
||||
"""
|
||||
if not request.data.get("issue", {}).get("name", False):
|
||||
return Response(
|
||||
{"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
return Response({"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
intake = Intake.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
).first()
|
||||
intake = Intake.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:
|
||||
return Response(
|
||||
{
|
||||
"error": "Intake is not enabled for this project enable it through the project's api"
|
||||
},
|
||||
{"error": "Intake is not enabled for this project enable it through the project's api"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@ -171,17 +163,13 @@ class IntakeIssueListCreateAPIEndpoint(BaseAPIView):
|
||||
"urgent",
|
||||
"none",
|
||||
]:
|
||||
return Response(
|
||||
{"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
return Response({"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# create an issue
|
||||
issue = Issue.objects.create(
|
||||
name=request.data.get("issue", {}).get("name"),
|
||||
description=request.data.get("issue", {}).get("description", {}),
|
||||
description_html=request.data.get("issue", {}).get(
|
||||
"description_html", "<p></p>"
|
||||
),
|
||||
description_html=request.data.get("issue", {}).get("description_html", "<p></p>"),
|
||||
priority=request.data.get("issue", {}).get("priority", "none"),
|
||||
project_id=project_id,
|
||||
)
|
||||
@ -226,9 +214,7 @@ class IntakeIssueDetailAPIEndpoint(BaseAPIView):
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
).first()
|
||||
|
||||
project = Project.objects.get(
|
||||
workspace__slug=self.kwargs.get("slug"), pk=self.kwargs.get("project_id")
|
||||
)
|
||||
project = Project.objects.get(workspace__slug=self.kwargs.get("slug"), pk=self.kwargs.get("project_id"))
|
||||
|
||||
if intake is None or not project.intake_view:
|
||||
return IntakeIssue.objects.none()
|
||||
@ -267,15 +253,13 @@ class IntakeIssueDetailAPIEndpoint(BaseAPIView):
|
||||
Retrieve details of a specific intake work item.
|
||||
"""
|
||||
intake_issue_queryset = self.get_queryset().get(issue_id=issue_id)
|
||||
intake_issue_data = IntakeIssueSerializer(
|
||||
intake_issue_queryset, fields=self.fields, expand=self.expand
|
||||
).data
|
||||
intake_issue_data = IntakeIssueSerializer(intake_issue_queryset, fields=self.fields, expand=self.expand).data
|
||||
return Response(intake_issue_data, status=status.HTTP_200_OK)
|
||||
|
||||
@intake_docs(
|
||||
operation_id="update_intake_work_item",
|
||||
summary="Update intake work item",
|
||||
description="Modify an existing intake work item's properties or status for triage processing. Supports status changes like accept, reject, or mark as duplicate.",
|
||||
description="Modify an existing intake work item's properties or status for triage processing. Supports status changes like accept, reject, or mark as duplicate.", # noqa: E501
|
||||
parameters=[
|
||||
WORKSPACE_SLUG_PARAMETER,
|
||||
PROJECT_ID_PARAMETER,
|
||||
@ -300,18 +284,14 @@ class IntakeIssueDetailAPIEndpoint(BaseAPIView):
|
||||
Modify an existing intake work item's properties or status for triage processing.
|
||||
Supports status changes like accept, reject, or mark as duplicate.
|
||||
"""
|
||||
intake = Intake.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
).first()
|
||||
intake = Intake.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:
|
||||
return Response(
|
||||
{
|
||||
"error": "Intake is not enabled for this project enable it through the project's api"
|
||||
},
|
||||
{"error": "Intake is not enabled for this project enable it through the project's api"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@ -332,9 +312,7 @@ class IntakeIssueDetailAPIEndpoint(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(
|
||||
request.user.id
|
||||
):
|
||||
if project_member.role <= 5 and str(intake_issue.created_by_id) != str(request.user.id):
|
||||
return Response(
|
||||
{"error": "You cannot edit intake work items"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
@ -349,10 +327,7 @@ class IntakeIssueDetailAPIEndpoint(BaseAPIView):
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(labels__id__isnull=True)
|
||||
& Q(label_issue__deleted_at__isnull=True)
|
||||
),
|
||||
filter=Q(~Q(labels__id__isnull=True) & Q(label_issue__deleted_at__isnull=True)),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@ -373,9 +348,7 @@ class IntakeIssueDetailAPIEndpoint(BaseAPIView):
|
||||
if project_member.role <= 5:
|
||||
issue_data = {
|
||||
"name": issue_data.get("name", issue.name),
|
||||
"description_html": issue_data.get(
|
||||
"description_html", issue.description_html
|
||||
),
|
||||
"description_html": issue_data.get("description_html", issue.description_html),
|
||||
"description": issue_data.get("description", issue.description),
|
||||
}
|
||||
|
||||
@ -401,45 +374,31 @@ class IntakeIssueDetailAPIEndpoint(BaseAPIView):
|
||||
)
|
||||
issue_serializer.save()
|
||||
else:
|
||||
return Response(
|
||||
issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
return Response(issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Only project admins and members can edit intake issue attributes
|
||||
if project_member.role > 15:
|
||||
serializer = IntakeIssueUpdateSerializer(
|
||||
intake_issue, data=request.data, partial=True
|
||||
)
|
||||
current_instance = json.dumps(
|
||||
IntakeIssueSerializer(intake_issue).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
serializer = IntakeIssueUpdateSerializer(intake_issue, data=request.data, partial=True)
|
||||
current_instance = json.dumps(IntakeIssueSerializer(intake_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=issue_id, workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
state = State.objects.filter(
|
||||
group="cancelled", workspace__slug=slug, project_id=project_id
|
||||
).first()
|
||||
issue = Issue.objects.get(pk=issue_id, workspace__slug=slug, project_id=project_id)
|
||||
state = State.objects.filter(group="cancelled", workspace__slug=slug, project_id=project_id).first()
|
||||
if state is not None:
|
||||
issue.state = state
|
||||
issue.save()
|
||||
|
||||
# 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
|
||||
)
|
||||
issue = Issue.objects.get(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
|
||||
).first()
|
||||
state = State.objects.filter(workspace__slug=slug, project_id=project_id, default=True).first()
|
||||
if state is not None:
|
||||
issue.state = state
|
||||
issue.save()
|
||||
@ -461,14 +420,12 @@ class IntakeIssueDetailAPIEndpoint(BaseAPIView):
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
else:
|
||||
return Response(
|
||||
IntakeIssueSerializer(intake_issue).data, status=status.HTTP_200_OK
|
||||
)
|
||||
return Response(IntakeIssueSerializer(intake_issue).data, status=status.HTTP_200_OK)
|
||||
|
||||
@intake_docs(
|
||||
operation_id="delete_intake_work_item",
|
||||
summary="Delete intake work item",
|
||||
description="Permanently remove an intake work item from the triage queue. Also deletes the underlying work item if it hasn't been accepted yet.",
|
||||
description="Permanently remove an intake work item from the triage queue. Also deletes the underlying work item if it hasn't been accepted yet.", # noqa: E501
|
||||
parameters=[
|
||||
WORKSPACE_SLUG_PARAMETER,
|
||||
PROJECT_ID_PARAMETER,
|
||||
@ -484,18 +441,14 @@ class IntakeIssueDetailAPIEndpoint(BaseAPIView):
|
||||
Permanently remove an intake work item from the triage queue.
|
||||
Also deletes the underlying work item if it hasn't been accepted yet.
|
||||
"""
|
||||
intake = Intake.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
).first()
|
||||
intake = Intake.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:
|
||||
return Response(
|
||||
{
|
||||
"error": "Intake is not enabled for this project enable it through the project's api"
|
||||
},
|
||||
{"error": "Intake is not enabled for this project enable it through the project's api"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@ -510,9 +463,7 @@ class IntakeIssueDetailAPIEndpoint(BaseAPIView):
|
||||
# Check the issue status
|
||||
if intake_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
|
||||
).first()
|
||||
issue = Issue.objects.filter(workspace__slug=slug, project_id=project_id, pk=issue_id).first()
|
||||
if issue.created_by_id != request.user.id and (
|
||||
not ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
|
||||
@ -142,9 +142,8 @@ from plane.utils.openapi import (
|
||||
)
|
||||
from plane.bgtasks.work_item_link_task import crawl_work_item_link_title
|
||||
|
||||
def user_has_issue_permission(
|
||||
user_id, project_id, issue=None, allowed_roles=None, allow_creator=True
|
||||
):
|
||||
|
||||
def user_has_issue_permission(user_id, project_id, issue=None, allowed_roles=None, allow_creator=True):
|
||||
if allow_creator and issue is not None and user_id == issue.created_by_id:
|
||||
return True
|
||||
|
||||
@ -269,7 +268,7 @@ class IssueListCreateAPIEndpoint(BaseAPIView):
|
||||
@work_item_docs(
|
||||
operation_id="list_work_items",
|
||||
summary="List work items",
|
||||
description="Retrieve a paginated list of all work items in a project. Supports filtering, ordering, and field selection through query parameters.",
|
||||
description="Retrieve a paginated list of all work items in a project. Supports filtering, ordering, and field selection through query parameters.", # noqa: E501
|
||||
parameters=[
|
||||
CURSOR_PARAMETER,
|
||||
PER_PAGE_PARAMETER,
|
||||
@ -322,9 +321,7 @@ class IssueListCreateAPIEndpoint(BaseAPIView):
|
||||
self.get_queryset()
|
||||
.annotate(
|
||||
cycle_id=Subquery(
|
||||
CycleIssue.objects.filter(
|
||||
issue=OuterRef("id"), deleted_at__isnull=True
|
||||
).values("cycle_id")[:1]
|
||||
CycleIssue.objects.filter(issue=OuterRef("id"), deleted_at__isnull=True).values("cycle_id")[:1]
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
@ -344,21 +341,14 @@ class IssueListCreateAPIEndpoint(BaseAPIView):
|
||||
)
|
||||
)
|
||||
|
||||
total_issue_queryset = Issue.issue_objects.filter(
|
||||
project_id=project_id, workspace__slug=slug
|
||||
)
|
||||
total_issue_queryset = Issue.issue_objects.filter(project_id=project_id, workspace__slug=slug)
|
||||
|
||||
# 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 = priority_order if order_by_param == "priority" else priority_order[::-1]
|
||||
issue_queryset = issue_queryset.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")
|
||||
@ -370,17 +360,10 @@ class IssueListCreateAPIEndpoint(BaseAPIView):
|
||||
"-state__name",
|
||||
"-state__group",
|
||||
]:
|
||||
state_order = (
|
||||
state_order
|
||||
if order_by_param in ["state__name", "state__group"]
|
||||
else state_order[::-1]
|
||||
)
|
||||
state_order = state_order if order_by_param in ["state__name", "state__group"] else state_order[::-1]
|
||||
issue_queryset = issue_queryset.annotate(
|
||||
state_order=Case(
|
||||
*[
|
||||
When(state__group=state_group, then=Value(i))
|
||||
for i, state_group in enumerate(state_order)
|
||||
],
|
||||
*[When(state__group=state_group, then=Value(i)) for i, state_group in enumerate(state_order)],
|
||||
default=Value(len(state_order)),
|
||||
output_field=CharField(),
|
||||
)
|
||||
@ -393,14 +376,8 @@ class IssueListCreateAPIEndpoint(BaseAPIView):
|
||||
"-assignees__first_name",
|
||||
]:
|
||||
issue_queryset = issue_queryset.annotate(
|
||||
max_values=Max(
|
||||
order_by_param[1::]
|
||||
if order_by_param.startswith("-")
|
||||
else order_by_param
|
||||
)
|
||||
).order_by(
|
||||
"-max_values" if order_by_param.startswith("-") else "max_values"
|
||||
)
|
||||
max_values=Max(order_by_param[1::] if order_by_param.startswith("-") else order_by_param)
|
||||
).order_by("-max_values" if order_by_param.startswith("-") else "max_values")
|
||||
else:
|
||||
issue_queryset = issue_queryset.order_by(order_by_param)
|
||||
|
||||
@ -408,9 +385,7 @@ class IssueListCreateAPIEndpoint(BaseAPIView):
|
||||
request=request,
|
||||
queryset=(issue_queryset),
|
||||
total_count_queryset=total_issue_queryset,
|
||||
on_results=lambda issues: IssueSerializer(
|
||||
issues, many=True, fields=self.fields, expand=self.expand
|
||||
).data,
|
||||
on_results=lambda issues: IssueSerializer(issues, many=True, fields=self.fields, expand=self.expand).data,
|
||||
)
|
||||
|
||||
@work_item_docs(
|
||||
@ -476,9 +451,7 @@ class IssueListCreateAPIEndpoint(BaseAPIView):
|
||||
|
||||
serializer.save()
|
||||
# Refetch the issue
|
||||
issue = Issue.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id, pk=serializer.data["id"]
|
||||
).first()
|
||||
issue = Issue.objects.filter(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.save(update_fields=["created_at", "created_by"])
|
||||
@ -579,7 +552,7 @@ class IssueDetailAPIEndpoint(BaseAPIView):
|
||||
@work_item_docs(
|
||||
operation_id="put_work_item",
|
||||
summary="Update or create work item",
|
||||
description="Update an existing work item identified by external ID and source, or create a new one if it doesn't exist. Requires external_id and external_source parameters for identification.",
|
||||
description="Update an existing work item identified by external ID and source, or create a new one if it doesn't exist. Requires external_id and external_source parameters for identification.", # noqa: E501
|
||||
request=OpenApiRequest(
|
||||
request=IssueSerializer,
|
||||
examples=[ISSUE_UPSERT_EXAMPLE],
|
||||
@ -625,9 +598,7 @@ class IssueDetailAPIEndpoint(BaseAPIView):
|
||||
|
||||
# Get the current instance of the issue in order to track
|
||||
# changes and dispatch the issue activity
|
||||
current_instance = json.dumps(
|
||||
IssueSerializer(issue).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
current_instance = json.dumps(IssueSerializer(issue).data, cls=DjangoJSONEncoder)
|
||||
|
||||
# Get the requested data, encode it as django object and pass it
|
||||
# to serializer to validation
|
||||
@ -690,16 +661,12 @@ class IssueDetailAPIEndpoint(BaseAPIView):
|
||||
# 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_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"])
|
||||
|
||||
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),
|
||||
@ -717,7 +684,7 @@ class IssueDetailAPIEndpoint(BaseAPIView):
|
||||
@work_item_docs(
|
||||
operation_id="update_work_item",
|
||||
summary="Partially update work item",
|
||||
description="Partially update an existing work item with the provided fields. Supports external ID validation to prevent conflicts.",
|
||||
description="Partially update an existing work item with the provided fields. Supports external ID validation to prevent conflicts.", # noqa: E501
|
||||
parameters=[
|
||||
PROJECT_ID_PARAMETER,
|
||||
],
|
||||
@ -744,9 +711,7 @@ class IssueDetailAPIEndpoint(BaseAPIView):
|
||||
"""
|
||||
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
|
||||
)
|
||||
current_instance = json.dumps(IssueSerializer(issue).data, cls=DjangoJSONEncoder)
|
||||
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
|
||||
serializer = IssueSerializer(
|
||||
issue,
|
||||
@ -761,9 +726,7 @@ class IssueDetailAPIEndpoint(BaseAPIView):
|
||||
and Issue.objects.filter(
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
external_source=request.data.get(
|
||||
"external_source", issue.external_source
|
||||
),
|
||||
external_source=request.data.get("external_source", issue.external_source),
|
||||
external_id=request.data.get("external_id"),
|
||||
).exists()
|
||||
):
|
||||
@ -791,7 +754,7 @@ class IssueDetailAPIEndpoint(BaseAPIView):
|
||||
@work_item_docs(
|
||||
operation_id="delete_work_item",
|
||||
summary="Delete work item",
|
||||
description="Permanently delete an existing work item from the project. Only admins or the item creator can perform this action.",
|
||||
description="Permanently delete an existing work item from the project. Only admins or the item creator can perform this action.", # noqa: E501
|
||||
parameters=[
|
||||
PROJECT_ID_PARAMETER,
|
||||
],
|
||||
@ -821,9 +784,7 @@ class IssueDetailAPIEndpoint(BaseAPIView):
|
||||
{"error": "Only admin or creator can delete the work item"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
current_instance = json.dumps(
|
||||
IssueSerializer(issue).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
current_instance = json.dumps(IssueSerializer(issue).data, cls=DjangoJSONEncoder)
|
||||
issue.delete()
|
||||
issue_activity.delay(
|
||||
type="issue.activity.deleted",
|
||||
@ -959,9 +920,7 @@ class LabelListCreateAPIEndpoint(BaseAPIView):
|
||||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(self.get_queryset()),
|
||||
on_results=lambda labels: LabelSerializer(
|
||||
labels, many=True, fields=self.fields, expand=self.expand
|
||||
).data,
|
||||
on_results=lambda labels: LabelSerializer(labels, many=True, fields=self.fields, expand=self.expand).data,
|
||||
)
|
||||
|
||||
|
||||
@ -1033,9 +992,7 @@ class LabelDetailAPIEndpoint(LabelListCreateAPIEndpoint):
|
||||
and Label.objects.filter(
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
external_source=request.data.get(
|
||||
"external_source", label.external_source
|
||||
),
|
||||
external_source=request.data.get("external_source", label.external_source),
|
||||
external_id=request.data.get("external_id"),
|
||||
)
|
||||
.exclude(id=pk)
|
||||
@ -1162,9 +1119,7 @@ class IssueLinkListCreateAPIEndpoint(BaseAPIView):
|
||||
serializer = IssueLinkCreateSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
serializer.save(project_id=project_id, issue_id=issue_id)
|
||||
crawl_work_item_link_title.delay(
|
||||
serializer.instance.id, serializer.instance.url
|
||||
)
|
||||
crawl_work_item_link_title.delay(serializer.instance.id, serializer.instance.url)
|
||||
link = IssueLink.objects.get(pk=serializer.instance.id)
|
||||
link.created_by_id = request.data.get("created_by", request.user.id)
|
||||
link.save(update_fields=["created_by"])
|
||||
@ -1233,9 +1188,7 @@ class IssueLinkDetailAPIEndpoint(BaseAPIView):
|
||||
"""
|
||||
if pk is None:
|
||||
issue_links = self.get_queryset()
|
||||
serializer = IssueLinkSerializer(
|
||||
issue_links, fields=self.fields, expand=self.expand
|
||||
)
|
||||
serializer = IssueLinkSerializer(issue_links, fields=self.fields, expand=self.expand)
|
||||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(self.get_queryset()),
|
||||
@ -1244,9 +1197,7 @@ class IssueLinkDetailAPIEndpoint(BaseAPIView):
|
||||
).data,
|
||||
)
|
||||
issue_link = self.get_queryset().get(pk=pk)
|
||||
serializer = IssueLinkSerializer(
|
||||
issue_link, fields=self.fields, expand=self.expand
|
||||
)
|
||||
serializer = IssueLinkSerializer(issue_link, fields=self.fields, expand=self.expand)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@issue_link_docs(
|
||||
@ -1276,19 +1227,13 @@ class IssueLinkDetailAPIEndpoint(BaseAPIView):
|
||||
Modify the URL, title, or metadata of an existing issue link.
|
||||
Tracks all changes in issue activity logs.
|
||||
"""
|
||||
issue_link = IssueLink.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
|
||||
)
|
||||
issue_link = IssueLink.objects.get(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
|
||||
)
|
||||
current_instance = json.dumps(IssueLinkSerializer(issue_link).data, cls=DjangoJSONEncoder)
|
||||
serializer = IssueLinkSerializer(issue_link, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
crawl_work_item_link_title.delay(
|
||||
serializer.data.get("id"), serializer.data.get("url")
|
||||
)
|
||||
crawl_work_item_link_title.delay(serializer.data.get("id"), serializer.data.get("url"))
|
||||
issue_activity.delay(
|
||||
type="link.activity.updated",
|
||||
requested_data=requested_data,
|
||||
@ -1320,12 +1265,8 @@ class IssueLinkDetailAPIEndpoint(BaseAPIView):
|
||||
Permanently remove an external link from a work item.
|
||||
Records deletion activity for audit purposes.
|
||||
"""
|
||||
issue_link = IssueLink.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
|
||||
)
|
||||
current_instance = json.dumps(
|
||||
IssueLinkSerializer(issue_link).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
issue_link = IssueLink.objects.get(workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk)
|
||||
current_instance = json.dumps(IssueLinkSerializer(issue_link).data, cls=DjangoJSONEncoder)
|
||||
issue_activity.delay(
|
||||
type="link.activity.deleted",
|
||||
requested_data=json.dumps({"link_id": str(pk)}),
|
||||
@ -1461,15 +1402,12 @@ class IssueCommentListCreateAPIEndpoint(BaseAPIView):
|
||||
|
||||
serializer = IssueCommentCreateSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
serializer.save(
|
||||
project_id=project_id, issue_id=issue_id, actor=request.user
|
||||
)
|
||||
serializer.save(project_id=project_id, issue_id=issue_id, actor=request.user)
|
||||
issue_comment = IssueComment.objects.get(pk=serializer.instance.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_by_id = request.data.get(
|
||||
"created_by", request.user.id
|
||||
)
|
||||
issue_comment.created_by_id = request.data.get("created_by", request.user.id)
|
||||
issue_comment.actor_id = request.data.get("created_by", request.user.id)
|
||||
issue_comment.save(update_fields=["created_at", "created_by"])
|
||||
|
||||
issue_activity.delay(
|
||||
@ -1555,9 +1493,7 @@ class IssueCommentDetailAPIEndpoint(BaseAPIView):
|
||||
Retrieve details of a specific comment.
|
||||
"""
|
||||
issue_comment = self.get_queryset().get(pk=pk)
|
||||
serializer = IssueCommentSerializer(
|
||||
issue_comment, fields=self.fields, expand=self.expand
|
||||
)
|
||||
serializer = IssueCommentSerializer(issue_comment, fields=self.fields, expand=self.expand)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@issue_comment_docs(
|
||||
@ -1588,13 +1524,9 @@ class IssueCommentDetailAPIEndpoint(BaseAPIView):
|
||||
Modify the content of an existing comment on a work item.
|
||||
Validates external ID uniqueness if provided.
|
||||
"""
|
||||
issue_comment = IssueComment.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
|
||||
)
|
||||
issue_comment = IssueComment.objects.get(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
|
||||
)
|
||||
current_instance = json.dumps(IssueCommentSerializer(issue_comment).data, cls=DjangoJSONEncoder)
|
||||
|
||||
# Validation check if the issue already exists
|
||||
if (
|
||||
@ -1603,9 +1535,7 @@ class IssueCommentDetailAPIEndpoint(BaseAPIView):
|
||||
and IssueComment.objects.filter(
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
external_source=request.data.get(
|
||||
"external_source", issue_comment.external_source
|
||||
),
|
||||
external_source=request.data.get("external_source", issue_comment.external_source),
|
||||
external_id=request.data.get("external_id"),
|
||||
).exists()
|
||||
):
|
||||
@ -1617,9 +1547,7 @@ class IssueCommentDetailAPIEndpoint(BaseAPIView):
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
)
|
||||
|
||||
serializer = IssueCommentCreateSerializer(
|
||||
issue_comment, data=request.data, partial=True
|
||||
)
|
||||
serializer = IssueCommentCreateSerializer(issue_comment, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
issue_activity.delay(
|
||||
@ -1665,12 +1593,8 @@ class IssueCommentDetailAPIEndpoint(BaseAPIView):
|
||||
Permanently remove a comment from a work item.
|
||||
Records deletion activity for audit purposes.
|
||||
"""
|
||||
issue_comment = IssueComment.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
|
||||
)
|
||||
current_instance = json.dumps(
|
||||
IssueCommentSerializer(issue_comment).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
issue_comment = IssueComment.objects.get(workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk)
|
||||
current_instance = json.dumps(IssueCommentSerializer(issue_comment).data, cls=DjangoJSONEncoder)
|
||||
issue_comment.delete()
|
||||
issue_activity.delay(
|
||||
type="comment.activity.deleted",
|
||||
@ -1717,9 +1641,7 @@ class IssueActivityListAPIEndpoint(BaseAPIView):
|
||||
Excludes comment, vote, reaction, and draft activities.
|
||||
"""
|
||||
issue_activities = (
|
||||
IssueActivity.objects.filter(
|
||||
issue_id=issue_id, workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
IssueActivity.objects.filter(issue_id=issue_id, workspace__slug=slug, project_id=project_id)
|
||||
.filter(
|
||||
~Q(field__in=["comment", "vote", "reaction", "draft"]),
|
||||
project__project_projectmember__member=self.request.user,
|
||||
@ -1774,9 +1696,7 @@ class IssueActivityDetailAPIEndpoint(BaseAPIView):
|
||||
Excludes comment, vote, reaction, and draft activities.
|
||||
"""
|
||||
issue_activities = (
|
||||
IssueActivity.objects.filter(
|
||||
issue_id=issue_id, workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
IssueActivity.objects.filter(issue_id=issue_id, workspace__slug=slug, project_id=project_id)
|
||||
.filter(
|
||||
~Q(field__in=["comment", "vote", "reaction", "draft"]),
|
||||
project__project_projectmember__member=self.request.user,
|
||||
@ -1866,12 +1786,8 @@ class IssueAttachmentListCreateAPIEndpoint(BaseAPIView):
|
||||
name="Workspace not found",
|
||||
value={"error": "Workspace not found"},
|
||||
),
|
||||
OpenApiExample(
|
||||
name="Project not found", value={"error": "Project not found"}
|
||||
),
|
||||
OpenApiExample(
|
||||
name="Issue not found", value={"error": "Issue not found"}
|
||||
),
|
||||
OpenApiExample(name="Project not found", value={"error": "Project not found"}),
|
||||
OpenApiExample(name="Issue not found", value={"error": "Issue not found"}),
|
||||
],
|
||||
),
|
||||
},
|
||||
@ -1882,9 +1798,7 @@ class IssueAttachmentListCreateAPIEndpoint(BaseAPIView):
|
||||
Generate presigned URL for uploading file attachments to a work item.
|
||||
Validates file type and size before creating the attachment record.
|
||||
"""
|
||||
issue = Issue.objects.get(
|
||||
pk=issue_id, workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
issue = Issue.objects.get(pk=issue_id, workspace__slug=slug, project_id=project_id)
|
||||
# if the user is creator or admin,member then allow the upload
|
||||
if not user_has_issue_permission(
|
||||
request.user.id,
|
||||
@ -1970,9 +1884,7 @@ class IssueAttachmentListCreateAPIEndpoint(BaseAPIView):
|
||||
# Get the presigned URL
|
||||
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
|
||||
)
|
||||
presigned_url = storage.generate_presigned_post(object_name=asset_key, file_type=type, file_size=size_limit)
|
||||
# Return the presigned URL
|
||||
return Response(
|
||||
{
|
||||
@ -2032,9 +1944,7 @@ class IssueAttachmentDetailAPIEndpoint(BaseAPIView):
|
||||
ATTACHMENT_ID_PARAMETER,
|
||||
],
|
||||
responses={
|
||||
204: OpenApiResponse(
|
||||
description="Work item attachment deleted successfully"
|
||||
),
|
||||
204: OpenApiResponse(description="Work item attachment deleted successfully"),
|
||||
404: ATTACHMENT_NOT_FOUND_RESPONSE,
|
||||
},
|
||||
)
|
||||
@ -2044,9 +1954,7 @@ class IssueAttachmentDetailAPIEndpoint(BaseAPIView):
|
||||
Soft delete an attachment from a work item by marking it as deleted.
|
||||
Records deletion activity and triggers metadata cleanup.
|
||||
"""
|
||||
issue = Issue.objects.get(
|
||||
pk=issue_id, workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
issue = Issue.objects.get(pk=issue_id, workspace__slug=slug, project_id=project_id)
|
||||
# if the request user is creator or admin then delete the attachment
|
||||
if not user_has_issue_permission(
|
||||
request.user,
|
||||
@ -2060,9 +1968,7 @@ class IssueAttachmentDetailAPIEndpoint(BaseAPIView):
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
issue_attachment = FileAsset.objects.get(
|
||||
pk=pk, workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
issue_attachment = FileAsset.objects.get(pk=pk, workspace__slug=slug, project_id=project_id)
|
||||
issue_attachment.is_deleted = True
|
||||
issue_attachment.deleted_at = timezone.now()
|
||||
issue_attachment.save()
|
||||
@ -2136,9 +2042,7 @@ class IssueAttachmentDetailAPIEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
# Get the asset
|
||||
asset = FileAsset.objects.get(
|
||||
id=pk, workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
asset = FileAsset.objects.get(id=pk, workspace__slug=slug, project_id=project_id)
|
||||
|
||||
# Check if the asset is uploaded
|
||||
if not asset.is_uploaded:
|
||||
@ -2176,9 +2080,7 @@ class IssueAttachmentDetailAPIEndpoint(BaseAPIView):
|
||||
examples=[ATTACHMENT_UPLOAD_CONFIRM_EXAMPLE],
|
||||
),
|
||||
responses={
|
||||
204: OpenApiResponse(
|
||||
description="Work item attachment uploaded successfully"
|
||||
),
|
||||
204: OpenApiResponse(description="Work item attachment uploaded successfully"),
|
||||
400: INVALID_REQUEST_RESPONSE,
|
||||
404: ATTACHMENT_NOT_FOUND_RESPONSE,
|
||||
},
|
||||
@ -2190,9 +2092,7 @@ class IssueAttachmentDetailAPIEndpoint(BaseAPIView):
|
||||
Triggers activity logging and metadata extraction.
|
||||
"""
|
||||
|
||||
issue = Issue.objects.get(
|
||||
pk=issue_id, workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
issue = Issue.objects.get(pk=issue_id, workspace__slug=slug, project_id=project_id)
|
||||
# if the user is creator or admin then allow the upload
|
||||
if not user_has_issue_permission(
|
||||
request.user,
|
||||
@ -2206,9 +2106,7 @@ class IssueAttachmentDetailAPIEndpoint(BaseAPIView):
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
issue_attachment = FileAsset.objects.get(
|
||||
pk=pk, workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
issue_attachment = FileAsset.objects.get(pk=pk, workspace__slug=slug, project_id=project_id)
|
||||
serializer = IssueAttachmentSerializer(issue_attachment)
|
||||
|
||||
# Send this activity only if the attachment is not uploaded before
|
||||
|
||||
@ -74,9 +74,7 @@ class WorkspaceMemberAPIEndpoint(BaseAPIView):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
workspace_members = WorkspaceMember.objects.filter(
|
||||
workspace__slug=slug
|
||||
).select_related("member")
|
||||
workspace_members = WorkspaceMember.objects.filter(workspace__slug=slug).select_related("member")
|
||||
|
||||
# Get all the users with their roles
|
||||
users_with_roles = []
|
||||
@ -125,13 +123,11 @@ class ProjectMemberAPIEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
# Get the workspace members that are present inside the workspace
|
||||
project_members = ProjectMember.objects.filter(
|
||||
project_id=project_id, workspace__slug=slug
|
||||
).values_list("member_id", flat=True)
|
||||
project_members = ProjectMember.objects.filter(project_id=project_id, workspace__slug=slug).values_list(
|
||||
"member_id", flat=True
|
||||
)
|
||||
|
||||
# Get all the users that are present inside the workspace
|
||||
users = UserLiteSerializer(
|
||||
User.objects.filter(id__in=project_members), many=True
|
||||
).data
|
||||
users = UserLiteSerializer(User.objects.filter(id__in=project_members), many=True).data
|
||||
|
||||
return Response(users, status=status.HTTP_200_OK)
|
||||
|
||||
@ -394,9 +394,7 @@ class ModuleDetailAPIEndpoint(BaseAPIView):
|
||||
examples=[MODULE_UPDATE_EXAMPLE],
|
||||
),
|
||||
404: OpenApiResponse(description="Module not found"),
|
||||
409: OpenApiResponse(
|
||||
description="Module with same external ID already exists"
|
||||
),
|
||||
409: OpenApiResponse(description="Module with same external ID already exists"),
|
||||
},
|
||||
)
|
||||
def patch(self, request, slug, project_id, pk):
|
||||
@ -407,18 +405,14 @@ class ModuleDetailAPIEndpoint(BaseAPIView):
|
||||
"""
|
||||
module = Module.objects.get(pk=pk, project_id=project_id, workspace__slug=slug)
|
||||
|
||||
current_instance = json.dumps(
|
||||
ModuleSerializer(module).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
current_instance = json.dumps(ModuleSerializer(module).data, cls=DjangoJSONEncoder)
|
||||
|
||||
if module.archived_at:
|
||||
return Response(
|
||||
{"error": "Archived module cannot be edited"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
serializer = ModuleSerializer(
|
||||
module, data=request.data, context={"project_id": project_id}, partial=True
|
||||
)
|
||||
serializer = ModuleSerializer(module, data=request.data, context={"project_id": project_id}, partial=True)
|
||||
if serializer.is_valid():
|
||||
if (
|
||||
request.data.get("external_id")
|
||||
@ -426,9 +420,7 @@ class ModuleDetailAPIEndpoint(BaseAPIView):
|
||||
and Module.objects.filter(
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
external_source=request.data.get(
|
||||
"external_source", module.external_source
|
||||
),
|
||||
external_source=request.data.get("external_source", module.external_source),
|
||||
external_id=request.data.get("external_id"),
|
||||
).exists()
|
||||
):
|
||||
@ -514,9 +506,7 @@ class ModuleDetailAPIEndpoint(BaseAPIView):
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
module_issues = list(
|
||||
ModuleIssue.objects.filter(module_id=pk).values_list("issue", flat=True)
|
||||
)
|
||||
module_issues = list(ModuleIssue.objects.filter(module_id=pk).values_list("issue", flat=True))
|
||||
issue_activity.delay(
|
||||
type="module.activity.deleted",
|
||||
requested_data=json.dumps(
|
||||
@ -537,9 +527,7 @@ class ModuleDetailAPIEndpoint(BaseAPIView):
|
||||
# Delete the module issues
|
||||
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
|
||||
).delete()
|
||||
UserFavorite.objects.filter(entity_type="module", entity_identifier=pk, project_id=project_id).delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
@ -609,9 +597,7 @@ class ModuleIssueListCreateAPIEndpoint(BaseAPIView):
|
||||
"""
|
||||
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, issue_module__deleted_at__isnull=True)
|
||||
.annotate(
|
||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
||||
.order_by()
|
||||
@ -647,15 +633,13 @@ class ModuleIssueListCreateAPIEndpoint(BaseAPIView):
|
||||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(issues),
|
||||
on_results=lambda issues: IssueSerializer(
|
||||
issues, many=True, fields=self.fields, expand=self.expand
|
||||
).data,
|
||||
on_results=lambda issues: IssueSerializer(issues, many=True, fields=self.fields, expand=self.expand).data,
|
||||
)
|
||||
|
||||
@module_issue_docs(
|
||||
operation_id="add_module_work_items",
|
||||
summary="Add Work Items to Module",
|
||||
description="Assign multiple work items to a module or move them from another module. Automatically handles bulk creation and updates with activity tracking.",
|
||||
description="Assign multiple work items to a module or move them from another module. Automatically handles bulk creation and updates with activity tracking.", # noqa: E501
|
||||
parameters=[
|
||||
MODULE_ID_PARAMETER,
|
||||
],
|
||||
@ -681,16 +665,12 @@ class ModuleIssueListCreateAPIEndpoint(BaseAPIView):
|
||||
"""
|
||||
issues = request.data.get("issues", [])
|
||||
if not len(issues):
|
||||
return Response(
|
||||
{"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
module = Module.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=module_id
|
||||
)
|
||||
return Response({"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
module = Module.objects.get(workspace__slug=slug, project_id=project_id, pk=module_id)
|
||||
|
||||
issues = Issue.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id, pk__in=issues
|
||||
).values_list("id", flat=True)
|
||||
issues = Issue.objects.filter(workspace__slug=slug, project_id=project_id, pk__in=issues).values_list(
|
||||
"id", flat=True
|
||||
)
|
||||
|
||||
module_issues = list(ModuleIssue.objects.filter(issue_id__in=issues))
|
||||
|
||||
@ -699,11 +679,7 @@ class ModuleIssueListCreateAPIEndpoint(BaseAPIView):
|
||||
record_to_create = []
|
||||
|
||||
for issue in issues:
|
||||
module_issue = [
|
||||
module_issue
|
||||
for module_issue in module_issues
|
||||
if str(module_issue.issue_id) in issues
|
||||
]
|
||||
module_issue = [module_issue for module_issue in module_issues if str(module_issue.issue_id) in issues]
|
||||
|
||||
if len(module_issue):
|
||||
if module_issue[0].module_id != module_id:
|
||||
@ -728,9 +704,7 @@ class ModuleIssueListCreateAPIEndpoint(BaseAPIView):
|
||||
)
|
||||
)
|
||||
|
||||
ModuleIssue.objects.bulk_create(
|
||||
record_to_create, batch_size=10, ignore_conflicts=True
|
||||
)
|
||||
ModuleIssue.objects.bulk_create(record_to_create, batch_size=10, ignore_conflicts=True)
|
||||
|
||||
ModuleIssue.objects.bulk_update(records_to_update, ["module"], batch_size=10)
|
||||
|
||||
@ -744,9 +718,7 @@ class ModuleIssueListCreateAPIEndpoint(BaseAPIView):
|
||||
current_instance=json.dumps(
|
||||
{
|
||||
"updated_module_issues": update_module_issue_activity,
|
||||
"created_module_issues": serializers.serialize(
|
||||
"json", record_to_create
|
||||
),
|
||||
"created_module_issues": serializers.serialize("json", record_to_create),
|
||||
}
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
@ -871,9 +843,7 @@ class ModuleIssueDetailAPIEndpoint(BaseAPIView):
|
||||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(issues),
|
||||
on_results=lambda issues: IssueSerializer(
|
||||
issues, many=True, fields=self.fields, expand=self.expand
|
||||
).data,
|
||||
on_results=lambda issues: IssueSerializer(issues, many=True, fields=self.fields, expand=self.expand).data,
|
||||
)
|
||||
|
||||
@module_issue_docs(
|
||||
@ -904,9 +874,7 @@ class ModuleIssueDetailAPIEndpoint(BaseAPIView):
|
||||
module_issue.delete()
|
||||
issue_activity.delay(
|
||||
type="module.activity.deleted",
|
||||
requested_data=json.dumps(
|
||||
{"module_id": str(module_id), "issues": [str(module_issue.issue_id)]}
|
||||
),
|
||||
requested_data=json.dumps({"module_id": str(module_id), "issues": [str(module_issue.issue_id)]}),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(issue_id),
|
||||
project_id=str(project_id),
|
||||
|
||||
@ -79,9 +79,7 @@ class ProjectListCreateAPIEndpoint(BaseAPIView):
|
||||
)
|
||||
| Q(network=2)
|
||||
)
|
||||
.select_related(
|
||||
"workspace", "workspace__owner", "default_assignee", "project_lead"
|
||||
)
|
||||
.select_related("workspace", "workspace__owner", "default_assignee", "project_lead")
|
||||
.annotate(
|
||||
is_member=Exists(
|
||||
ProjectMember.objects.filter(
|
||||
@ -170,9 +168,9 @@ class ProjectListCreateAPIEndpoint(BaseAPIView):
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"project_projectmember",
|
||||
queryset=ProjectMember.objects.filter(
|
||||
workspace__slug=slug, is_active=True
|
||||
).select_related("member"),
|
||||
queryset=ProjectMember.objects.filter(workspace__slug=slug, is_active=True).select_related(
|
||||
"member"
|
||||
),
|
||||
)
|
||||
)
|
||||
.order_by(request.GET.get("order_by", "sort_order"))
|
||||
@ -211,24 +209,18 @@ class ProjectListCreateAPIEndpoint(BaseAPIView):
|
||||
"""
|
||||
try:
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
serializer = ProjectCreateSerializer(
|
||||
data={**request.data}, context={"workspace_id": workspace.id}
|
||||
)
|
||||
serializer = ProjectCreateSerializer(data={**request.data}, context={"workspace_id": workspace.id})
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
|
||||
# Add the user as Administrator to the project
|
||||
_ = ProjectMember.objects.create(
|
||||
project_id=serializer.instance.id, member=request.user, role=20
|
||||
)
|
||||
_ = ProjectMember.objects.create(project_id=serializer.instance.id, member=request.user, role=20)
|
||||
# Also create the issue property for the user
|
||||
_ = IssueUserProperty.objects.create(
|
||||
project_id=serializer.instance.id, user=request.user
|
||||
)
|
||||
_ = IssueUserProperty.objects.create(project_id=serializer.instance.id, user=request.user)
|
||||
|
||||
if serializer.instance.project_lead is not None and str(
|
||||
serializer.instance.project_lead
|
||||
) != str(request.user.id):
|
||||
if serializer.instance.project_lead is not None and str(serializer.instance.project_lead) != str(
|
||||
request.user.id
|
||||
):
|
||||
ProjectMember.objects.create(
|
||||
project_id=serializer.instance.id,
|
||||
member_id=serializer.instance.project_lead,
|
||||
@ -314,9 +306,7 @@ class ProjectListCreateAPIEndpoint(BaseAPIView):
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
)
|
||||
except Workspace.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Workspace does not exist"}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
return Response({"error": "Workspace does not exist"}, status=status.HTTP_404_NOT_FOUND)
|
||||
except ValidationError:
|
||||
return Response(
|
||||
{"identifier": "The project identifier is already taken"},
|
||||
@ -344,9 +334,7 @@ class ProjectDetailAPIEndpoint(BaseAPIView):
|
||||
)
|
||||
| Q(network=2)
|
||||
)
|
||||
.select_related(
|
||||
"workspace", "workspace__owner", "default_assignee", "project_lead"
|
||||
)
|
||||
.select_related("workspace", "workspace__owner", "default_assignee", "project_lead")
|
||||
.annotate(
|
||||
is_member=Exists(
|
||||
ProjectMember.objects.filter(
|
||||
@ -451,9 +439,7 @@ class ProjectDetailAPIEndpoint(BaseAPIView):
|
||||
try:
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
project = Project.objects.get(pk=pk)
|
||||
current_instance = json.dumps(
|
||||
ProjectSerializer(project).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
current_instance = json.dumps(ProjectSerializer(project).data, cls=DjangoJSONEncoder)
|
||||
|
||||
intake_view = request.data.get("intake_view", project.intake_view)
|
||||
|
||||
@ -473,9 +459,7 @@ class ProjectDetailAPIEndpoint(BaseAPIView):
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
if serializer.data["intake_view"]:
|
||||
intake = Intake.objects.filter(
|
||||
project=project, is_default=True
|
||||
).first()
|
||||
intake = Intake.objects.filter(project=project, is_default=True).first()
|
||||
if not intake:
|
||||
Intake.objects.create(
|
||||
name=f"{project.name} Intake",
|
||||
@ -505,9 +489,7 @@ class ProjectDetailAPIEndpoint(BaseAPIView):
|
||||
status=status.HTTP_409_CONFLICT,
|
||||
)
|
||||
except (Project.DoesNotExist, Workspace.DoesNotExist):
|
||||
return Response(
|
||||
{"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
return Response({"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND)
|
||||
except ValidationError:
|
||||
return Response(
|
||||
{"identifier": "The project identifier is already taken"},
|
||||
@ -533,9 +515,7 @@ class ProjectDetailAPIEndpoint(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
|
||||
).delete()
|
||||
UserFavorite.objects.filter(entity_type="project", entity_identifier=pk, project_id=pk).delete()
|
||||
project.delete()
|
||||
webhook_activity.delay(
|
||||
event="project",
|
||||
|
||||
@ -80,9 +80,7 @@ class StateListCreateAPIEndpoint(BaseAPIView):
|
||||
Supports external ID tracking for integration purposes.
|
||||
"""
|
||||
try:
|
||||
serializer = StateSerializer(
|
||||
data=request.data, context={"project_id": project_id}
|
||||
)
|
||||
serializer = StateSerializer(data=request.data, context={"project_id": project_id})
|
||||
if serializer.is_valid():
|
||||
if (
|
||||
request.data.get("external_id")
|
||||
@ -153,9 +151,7 @@ class StateListCreateAPIEndpoint(BaseAPIView):
|
||||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(self.get_queryset()),
|
||||
on_results=lambda states: StateSerializer(
|
||||
states, many=True, fields=self.fields, expand=self.expand
|
||||
).data,
|
||||
on_results=lambda states: StateSerializer(states, many=True, fields=self.fields, expand=self.expand).data,
|
||||
)
|
||||
|
||||
|
||||
@ -213,7 +209,7 @@ class StateDetailAPIEndpoint(BaseAPIView):
|
||||
@state_docs(
|
||||
operation_id="delete_state",
|
||||
summary="Delete state",
|
||||
description="Permanently remove a workflow state from a project. Default states and states with existing work items cannot be deleted.",
|
||||
description="Permanently remove a workflow state from a project. Default states and states with existing work items cannot be deleted.", # noqa: E501
|
||||
parameters=[
|
||||
STATE_ID_PARAMETER,
|
||||
],
|
||||
@ -228,9 +224,7 @@ class StateDetailAPIEndpoint(BaseAPIView):
|
||||
Permanently remove a workflow state from a project.
|
||||
Default states and states with existing work items cannot be deleted.
|
||||
"""
|
||||
state = State.objects.get(
|
||||
is_triage=False, pk=state_id, project_id=project_id, workspace__slug=slug
|
||||
)
|
||||
state = State.objects.get(is_triage=False, pk=state_id, project_id=project_id, workspace__slug=slug)
|
||||
|
||||
if state.default:
|
||||
return Response(
|
||||
@ -277,9 +271,7 @@ class StateDetailAPIEndpoint(BaseAPIView):
|
||||
Partially update an existing workflow state's properties like name, color, or group.
|
||||
Validates external ID uniqueness if provided.
|
||||
"""
|
||||
state = State.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=state_id
|
||||
)
|
||||
state = State.objects.get(workspace__slug=slug, project_id=project_id, pk=state_id)
|
||||
serializer = StateSerializer(state, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
if (
|
||||
@ -288,9 +280,7 @@ class StateDetailAPIEndpoint(BaseAPIView):
|
||||
and State.objects.filter(
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
external_source=request.data.get(
|
||||
"external_source", state.external_source
|
||||
),
|
||||
external_source=request.data.get("external_source", state.external_source),
|
||||
external_id=request.data.get("external_id"),
|
||||
).exists()
|
||||
):
|
||||
|
||||
@ -18,16 +18,12 @@ def allow_permission(allowed_roles, level="PROJECT", creator=False, model=None):
|
||||
def _wrapped_view(instance, request, *args, **kwargs):
|
||||
# Check for creator if required
|
||||
if creator and model:
|
||||
obj = model.objects.filter(
|
||||
id=kwargs["pk"], created_by=request.user
|
||||
).exists()
|
||||
obj = model.objects.filter(id=kwargs["pk"], created_by=request.user).exists()
|
||||
if obj:
|
||||
return view_func(instance, request, *args, **kwargs)
|
||||
|
||||
# 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
|
||||
]
|
||||
allowed_role_values = [role.value if isinstance(role, ROLE) else role for role in allowed_roles]
|
||||
|
||||
# Check role permissions
|
||||
if level == "WORKSPACE":
|
||||
@ -47,7 +43,7 @@ def allow_permission(allowed_roles, level="PROJECT", creator=False, model=None):
|
||||
is_active=True,
|
||||
).exists()
|
||||
|
||||
# Return if the user has the allowed role else if they are workspace admin and part of the project regardless of the role
|
||||
# Return if the user has the allowed role else if they are workspace admin and part of the project regardless of the role # noqa: E501
|
||||
if is_user_has_allowed_role:
|
||||
return view_func(instance, request, *args, **kwargs)
|
||||
elif (
|
||||
|
||||
@ -30,9 +30,7 @@ class ProjectPagePermission(BasePermission):
|
||||
project_id = view.kwargs.get("project_id")
|
||||
|
||||
# Hook for extended validation
|
||||
extended_access, role = self._check_access_and_get_role(
|
||||
request, slug, project_id
|
||||
)
|
||||
extended_access, role = self._check_access_and_get_role(request, slug, project_id)
|
||||
if extended_access is False:
|
||||
return False
|
||||
|
||||
@ -45,9 +43,7 @@ class ProjectPagePermission(BasePermission):
|
||||
|
||||
# Handle private page access
|
||||
if page.access == Page.PRIVATE_ACCESS:
|
||||
return self._has_private_page_action_access(
|
||||
request, slug, page, project_id
|
||||
)
|
||||
return self._has_private_page_action_access(request, slug, page, project_id)
|
||||
|
||||
# Handle public page access
|
||||
return self._has_public_page_action_access(request, role)
|
||||
|
||||
@ -168,13 +168,9 @@ class DynamicBaseSerializer(BaseSerializer):
|
||||
# Check if field in expansion then expand the field
|
||||
if expand in expansion:
|
||||
if isinstance(response.get(expand), list):
|
||||
exp_serializer = expansion[expand](
|
||||
getattr(instance, expand), many=True
|
||||
)
|
||||
exp_serializer = expansion[expand](getattr(instance, expand), many=True)
|
||||
else:
|
||||
exp_serializer = expansion[expand](
|
||||
getattr(instance, expand)
|
||||
)
|
||||
exp_serializer = expansion[expand](getattr(instance, expand))
|
||||
response[expand] = exp_serializer.data
|
||||
else:
|
||||
# You might need to handle this case differently
|
||||
@ -194,9 +190,7 @@ 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"] = []
|
||||
|
||||
|
||||
@ -16,10 +16,7 @@ class CycleWriteSerializer(BaseSerializer):
|
||||
and data.get("start_date", None) > data.get("end_date", None)
|
||||
):
|
||||
raise serializers.ValidationError("Start date cannot exceed end date")
|
||||
if (
|
||||
data.get("start_date", None) is not None
|
||||
and data.get("end_date", None) is not None
|
||||
):
|
||||
if data.get("start_date", None) is not None and data.get("end_date", None) is not None:
|
||||
project_id = (
|
||||
self.initial_data.get("project_id", None)
|
||||
or (self.instance and self.instance.project_id)
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
|
||||
# Django imports
|
||||
from django.utils import timezone
|
||||
|
||||
@ -75,13 +74,9 @@ class DraftIssueCreateSerializer(BaseSerializer):
|
||||
|
||||
# Validate description content for security
|
||||
if "description_html" in attrs and attrs["description_html"]:
|
||||
is_valid, error_msg, sanitized_html = validate_html_content(
|
||||
attrs["description_html"]
|
||||
)
|
||||
is_valid, error_msg, sanitized_html = validate_html_content(attrs["description_html"])
|
||||
if not is_valid:
|
||||
raise serializers.ValidationError(
|
||||
{"error": "html content is not valid"}
|
||||
)
|
||||
raise serializers.ValidationError({"error": "html content is not valid"})
|
||||
# Update the attrs with sanitized HTML if available
|
||||
if sanitized_html is not None:
|
||||
attrs["description_html"] = sanitized_html
|
||||
@ -89,9 +84,7 @@ class DraftIssueCreateSerializer(BaseSerializer):
|
||||
if "description_binary" in attrs and attrs["description_binary"]:
|
||||
is_valid, error_msg = validate_binary_data(attrs["description_binary"])
|
||||
if not is_valid:
|
||||
raise serializers.ValidationError(
|
||||
{"description_binary": "Invalid binary data"}
|
||||
)
|
||||
raise serializers.ValidationError({"description_binary": "Invalid binary data"})
|
||||
|
||||
# Validate assignees are from project
|
||||
if attrs.get("assignee_ids", []):
|
||||
@ -106,9 +99,9 @@ class DraftIssueCreateSerializer(BaseSerializer):
|
||||
if attrs.get("label_ids"):
|
||||
label_ids = [label.id for label in attrs["label_ids"]]
|
||||
attrs["label_ids"] = list(
|
||||
Label.objects.filter(
|
||||
project_id=self.context.get("project_id"), id__in=label_ids
|
||||
).values_list("id", flat=True)
|
||||
Label.objects.filter(project_id=self.context.get("project_id"), id__in=label_ids).values_list(
|
||||
"id", flat=True
|
||||
)
|
||||
)
|
||||
|
||||
# # Check state is from the project only else raise validation error
|
||||
@ -119,9 +112,7 @@ class DraftIssueCreateSerializer(BaseSerializer):
|
||||
pk=attrs.get("state").id,
|
||||
).exists()
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
"State is not valid please pass a valid state_id"
|
||||
)
|
||||
raise serializers.ValidationError("State is not valid please pass a valid state_id")
|
||||
|
||||
# # Check parent issue is from workspace as it can be cross workspace
|
||||
if (
|
||||
@ -131,9 +122,7 @@ class DraftIssueCreateSerializer(BaseSerializer):
|
||||
pk=attrs.get("parent").id,
|
||||
).exists()
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
"Parent is not valid issue_id please pass a valid issue_id"
|
||||
)
|
||||
raise serializers.ValidationError("Parent is not valid issue_id please pass a valid issue_id")
|
||||
|
||||
if (
|
||||
attrs.get("estimate_point")
|
||||
@ -142,9 +131,7 @@ class DraftIssueCreateSerializer(BaseSerializer):
|
||||
pk=attrs.get("estimate_point").id,
|
||||
).exists()
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
"Estimate point is not valid please pass a valid estimate_point_id"
|
||||
)
|
||||
raise serializers.ValidationError("Estimate point is not valid please pass a valid estimate_point_id")
|
||||
|
||||
return attrs
|
||||
|
||||
@ -159,9 +146,7 @@ class DraftIssueCreateSerializer(BaseSerializer):
|
||||
project_id = self.context["project_id"]
|
||||
|
||||
# Create Issue
|
||||
issue = DraftIssue.objects.create(
|
||||
**validated_data, workspace_id=workspace_id, project_id=project_id
|
||||
)
|
||||
issue = DraftIssue.objects.create(**validated_data, workspace_id=workspace_id, project_id=project_id)
|
||||
|
||||
# Issue Audit Users
|
||||
created_by_id = issue.created_by_id
|
||||
|
||||
@ -17,9 +17,7 @@ class PageFavoriteLiteSerializer(serializers.ModelSerializer):
|
||||
fields = ["id", "name", "logo_props", "project_id"]
|
||||
|
||||
def get_project_id(self, obj):
|
||||
project = (
|
||||
obj.projects.first()
|
||||
) # This gets the first project related to the Page
|
||||
project = obj.projects.first() # This gets the first project related to the Page
|
||||
return project.id if project else None
|
||||
|
||||
|
||||
|
||||
@ -45,9 +45,7 @@ class IntakeIssueSerializer(BaseSerializer):
|
||||
|
||||
class IntakeIssueDetailSerializer(BaseSerializer):
|
||||
issue = IssueDetailSerializer(read_only=True)
|
||||
duplicate_issue_detail = IssueIntakeSerializer(
|
||||
read_only=True, source="duplicate_to"
|
||||
)
|
||||
duplicate_issue_detail = IssueIntakeSerializer(read_only=True, source="duplicate_to")
|
||||
|
||||
class Meta:
|
||||
model = IntakeIssue
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
|
||||
# Django imports
|
||||
from django.utils import timezone
|
||||
from django.core.validators import URLValidator
|
||||
@ -127,13 +126,9 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
|
||||
# Validate description content for security
|
||||
if "description_html" in attrs and attrs["description_html"]:
|
||||
is_valid, error_msg, sanitized_html = validate_html_content(
|
||||
attrs["description_html"]
|
||||
)
|
||||
is_valid, error_msg, sanitized_html = validate_html_content(attrs["description_html"])
|
||||
if not is_valid:
|
||||
raise serializers.ValidationError(
|
||||
{"error": "html content is not valid"}
|
||||
)
|
||||
raise serializers.ValidationError({"error": "html content is not valid"})
|
||||
# Update the attrs with sanitized HTML if available
|
||||
if sanitized_html is not None:
|
||||
attrs["description_html"] = sanitized_html
|
||||
@ -141,9 +136,7 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
if "description_binary" in attrs and attrs["description_binary"]:
|
||||
is_valid, error_msg = validate_binary_data(attrs["description_binary"])
|
||||
if not is_valid:
|
||||
raise serializers.ValidationError(
|
||||
{"description_binary": "Invalid binary data"}
|
||||
)
|
||||
raise serializers.ValidationError({"description_binary": "Invalid binary data"})
|
||||
|
||||
# Validate assignees are from project
|
||||
if attrs.get("assignee_ids", []):
|
||||
@ -172,9 +165,7 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
pk=attrs.get("state").id,
|
||||
).exists()
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
"State is not valid please pass a valid state_id"
|
||||
)
|
||||
raise serializers.ValidationError("State is not valid please pass a valid state_id")
|
||||
|
||||
# Check parent issue is from workspace as it can be cross workspace
|
||||
if (
|
||||
@ -184,9 +175,7 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
pk=attrs.get("parent").id,
|
||||
).exists()
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
"Parent is not valid issue_id please pass a valid issue_id"
|
||||
)
|
||||
raise serializers.ValidationError("Parent is not valid issue_id please pass a valid issue_id")
|
||||
|
||||
if (
|
||||
attrs.get("estimate_point")
|
||||
@ -195,9 +184,7 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
pk=attrs.get("estimate_point").id,
|
||||
).exists()
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
"Estimate point is not valid please pass a valid estimate_point_id"
|
||||
)
|
||||
raise serializers.ValidationError("Estimate point is not valid please pass a valid estimate_point_id")
|
||||
|
||||
return attrs
|
||||
|
||||
@ -343,11 +330,7 @@ class IssueActivitySerializer(BaseSerializer):
|
||||
source_data = serializers.SerializerMethodField()
|
||||
|
||||
def get_source_data(self, obj):
|
||||
if (
|
||||
hasattr(obj, "issue")
|
||||
and hasattr(obj.issue, "source_data")
|
||||
and obj.issue.source_data
|
||||
):
|
||||
if hasattr(obj, "issue") and hasattr(obj.issue, "source_data") and obj.issue.source_data:
|
||||
return {
|
||||
"source": obj.issue.source_data[0].source,
|
||||
"source_email": obj.issue.source_data[0].source_email,
|
||||
@ -397,12 +380,8 @@ class IssueLabelSerializer(BaseSerializer):
|
||||
|
||||
class IssueRelationSerializer(BaseSerializer):
|
||||
id = serializers.UUIDField(source="related_issue.id", read_only=True)
|
||||
project_id = serializers.PrimaryKeyRelatedField(
|
||||
source="related_issue.project_id", read_only=True
|
||||
)
|
||||
sequence_id = serializers.IntegerField(
|
||||
source="related_issue.sequence_id", read_only=True
|
||||
)
|
||||
project_id = serializers.PrimaryKeyRelatedField(source="related_issue.project_id", read_only=True)
|
||||
sequence_id = serializers.IntegerField(source="related_issue.sequence_id", read_only=True)
|
||||
name = serializers.CharField(source="related_issue.name", read_only=True)
|
||||
relation_type = serializers.CharField(read_only=True)
|
||||
state_id = serializers.UUIDField(source="related_issue.state.id", read_only=True)
|
||||
@ -441,9 +420,7 @@ class IssueRelationSerializer(BaseSerializer):
|
||||
|
||||
class RelatedIssueSerializer(BaseSerializer):
|
||||
id = serializers.UUIDField(source="issue.id", read_only=True)
|
||||
project_id = serializers.PrimaryKeyRelatedField(
|
||||
source="issue.project_id", read_only=True
|
||||
)
|
||||
project_id = serializers.PrimaryKeyRelatedField(source="issue.project_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)
|
||||
@ -585,25 +562,17 @@ 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")
|
||||
).exists():
|
||||
raise serializers.ValidationError(
|
||||
{"error": "URL already exists for this Issue"}
|
||||
)
|
||||
if IssueLink.objects.filter(url=validated_data.get("url"), issue_id=validated_data.get("issue_id")).exists():
|
||||
raise serializers.ValidationError({"error": "URL already exists for this Issue"})
|
||||
return IssueLink.objects.create(**validated_data)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
if (
|
||||
IssueLink.objects.filter(
|
||||
url=validated_data.get("url"), issue_id=instance.issue_id
|
||||
)
|
||||
IssueLink.objects.filter(url=validated_data.get("url"), issue_id=instance.issue_id)
|
||||
.exclude(pk=instance.id)
|
||||
.exists()
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
{"error": "URL already exists for this Issue"}
|
||||
)
|
||||
raise serializers.ValidationError({"error": "URL already exists for this Issue"})
|
||||
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
@ -941,9 +910,7 @@ class IssueDetailSerializer(IssueSerializer):
|
||||
class IssuePublicSerializer(BaseSerializer):
|
||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||
state_detail = StateLiteSerializer(read_only=True, source="state")
|
||||
reactions = IssueReactionSerializer(
|
||||
read_only=True, many=True, source="issue_reactions"
|
||||
)
|
||||
reactions = IssueReactionSerializer(read_only=True, many=True, source="issue_reactions")
|
||||
votes = IssueVoteSerializer(read_only=True, many=True)
|
||||
|
||||
class Meta:
|
||||
|
||||
@ -65,9 +65,7 @@ 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=project).exists():
|
||||
raise serializers.ValidationError(
|
||||
{"error": "Module with this name already exists"}
|
||||
)
|
||||
raise serializers.ValidationError({"error": "Module with this name already exists"})
|
||||
|
||||
module = Module.objects.create(**validated_data, project=project)
|
||||
if members is not None:
|
||||
@ -94,14 +92,8 @@ 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=instance.project)
|
||||
.exclude(id=instance.id)
|
||||
.exists()
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
{"error": "Module with this name already exists"}
|
||||
)
|
||||
if Module.objects.filter(name=module_name, project=instance.project).exclude(id=instance.id).exists():
|
||||
raise serializers.ValidationError({"error": "Module with this name already exists"})
|
||||
|
||||
if members is not None:
|
||||
ModuleMember.objects.filter(module=instance).delete()
|
||||
@ -191,32 +183,24 @@ 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")
|
||||
).exists():
|
||||
if ModuleLink.objects.filter(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)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
validated_data["url"] = self.validate_url(validated_data.get("url"))
|
||||
if (
|
||||
ModuleLink.objects.filter(
|
||||
url=validated_data.get("url"), module_id=instance.module_id
|
||||
)
|
||||
ModuleLink.objects.filter(url=validated_data.get("url"), module_id=instance.module_id)
|
||||
.exclude(pk=instance.id)
|
||||
.exists()
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
{"error": "URL already exists for this Issue"}
|
||||
)
|
||||
raise serializers.ValidationError({"error": "URL already exists for this Issue"})
|
||||
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
class ModuleSerializer(DynamicBaseSerializer):
|
||||
member_ids = serializers.ListField(
|
||||
child=serializers.UUIDField(), required=False, allow_null=True
|
||||
)
|
||||
member_ids = serializers.ListField(child=serializers.UUIDField(), required=False, allow_null=True)
|
||||
is_favorite = serializers.BooleanField(read_only=True)
|
||||
total_issues = serializers.IntegerField(read_only=True)
|
||||
cancelled_issues = serializers.IntegerField(read_only=True)
|
||||
|
||||
@ -10,7 +10,6 @@ from plane.utils.content_validator import (
|
||||
)
|
||||
from plane.db.models import (
|
||||
Page,
|
||||
PageLog,
|
||||
PageLabel,
|
||||
Label,
|
||||
ProjectPage,
|
||||
@ -186,9 +185,7 @@ class PageBinaryUpdateSerializer(serializers.Serializer):
|
||||
# Validate the binary data
|
||||
is_valid, error_message = validate_binary_data(binary_data)
|
||||
if not is_valid:
|
||||
raise serializers.ValidationError(
|
||||
f"Invalid binary data: {error_message}"
|
||||
)
|
||||
raise serializers.ValidationError(f"Invalid binary data: {error_message}")
|
||||
|
||||
return binary_data
|
||||
except Exception as e:
|
||||
@ -209,7 +206,6 @@ class PageBinaryUpdateSerializer(serializers.Serializer):
|
||||
# Return sanitized HTML if available, otherwise return original
|
||||
return sanitized_html if sanitized_html is not None else value
|
||||
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
"""Update the page instance with validated data"""
|
||||
if "description_binary" in validated_data:
|
||||
|
||||
@ -47,9 +47,7 @@ class ProjectSerializer(BaseSerializer):
|
||||
project_id = self.instance.id if self.instance else None
|
||||
workspace_id = self.context["workspace_id"]
|
||||
|
||||
project = Project.objects.filter(
|
||||
identifier=identifier, workspace_id=workspace_id
|
||||
)
|
||||
project = Project.objects.filter(identifier=identifier, workspace_id=workspace_id)
|
||||
|
||||
if project_id:
|
||||
project = project.exclude(id=project_id)
|
||||
@ -64,17 +62,13 @@ class ProjectSerializer(BaseSerializer):
|
||||
def validate(self, data):
|
||||
# Validate description content for security
|
||||
if "description_html" in data and data["description_html"]:
|
||||
is_valid, error_msg, sanitized_html = validate_html_content(
|
||||
str(data["description_html"])
|
||||
)
|
||||
is_valid, error_msg, sanitized_html = validate_html_content(str(data["description_html"]))
|
||||
# Update the data with sanitized HTML if available
|
||||
if sanitized_html is not None:
|
||||
data["description_html"] = sanitized_html
|
||||
|
||||
if not is_valid:
|
||||
raise serializers.ValidationError(
|
||||
{"error": "html content is not valid"}
|
||||
)
|
||||
raise serializers.ValidationError({"error": "html content is not valid"})
|
||||
|
||||
return data
|
||||
|
||||
@ -83,9 +77,7 @@ class ProjectSerializer(BaseSerializer):
|
||||
|
||||
project = Project.objects.create(**validated_data, workspace_id=workspace_id)
|
||||
|
||||
ProjectIdentifier.objects.create(
|
||||
name=project.identifier, project=project, workspace_id=workspace_id
|
||||
)
|
||||
ProjectIdentifier.objects.create(name=project.identifier, project=project, workspace_id=workspace_id)
|
||||
|
||||
return project
|
||||
|
||||
@ -118,11 +110,7 @@ class ProjectListSerializer(DynamicBaseSerializer):
|
||||
project_members = getattr(obj, "members_list", None)
|
||||
if project_members is not None:
|
||||
# Filter members by the project ID
|
||||
return [
|
||||
member.member_id
|
||||
for member in project_members
|
||||
if member.is_active and not member.member.is_bot
|
||||
]
|
||||
return [member.member_id for member in project_members if member.is_active and not member.member.is_bot]
|
||||
return []
|
||||
|
||||
class Meta:
|
||||
|
||||
@ -91,9 +91,7 @@ class UserMeSettingsSerializer(BaseSerializer):
|
||||
read_only_fields = fields
|
||||
|
||||
def get_workspace(self, obj):
|
||||
workspace_invites = WorkspaceMemberInvite.objects.filter(
|
||||
email=obj.email
|
||||
).count()
|
||||
workspace_invites = WorkspaceMemberInvite.objects.filter(email=obj.email).count()
|
||||
|
||||
# profile
|
||||
profile = Profile.objects.get(user=obj)
|
||||
@ -110,43 +108,27 @@ class UserMeSettingsSerializer(BaseSerializer):
|
||||
workspace_member__member=obj.id,
|
||||
workspace_member__is_active=True,
|
||||
).first()
|
||||
logo_asset_url = (
|
||||
workspace.logo_asset.asset_url
|
||||
if workspace.logo_asset is not None
|
||||
else ""
|
||||
)
|
||||
logo_asset_url = workspace.logo_asset.asset_url if workspace.logo_asset is not None else ""
|
||||
return {
|
||||
"last_workspace_id": profile.last_workspace_id,
|
||||
"last_workspace_slug": (
|
||||
workspace.slug if workspace is not None else ""
|
||||
),
|
||||
"last_workspace_name": (
|
||||
workspace.name if workspace is not None else ""
|
||||
),
|
||||
"last_workspace_slug": (workspace.slug if workspace is not None else ""),
|
||||
"last_workspace_name": (workspace.name if workspace is not None else ""),
|
||||
"last_workspace_logo": (logo_asset_url),
|
||||
"fallback_workspace_id": profile.last_workspace_id,
|
||||
"fallback_workspace_slug": (
|
||||
workspace.slug if workspace is not None else ""
|
||||
),
|
||||
"fallback_workspace_slug": (workspace.slug if workspace is not None else ""),
|
||||
"invites": workspace_invites,
|
||||
}
|
||||
else:
|
||||
fallback_workspace = (
|
||||
Workspace.objects.filter(
|
||||
workspace_member__member_id=obj.id, workspace_member__is_active=True
|
||||
)
|
||||
Workspace.objects.filter(workspace_member__member_id=obj.id, workspace_member__is_active=True)
|
||||
.order_by("created_at")
|
||||
.first()
|
||||
)
|
||||
return {
|
||||
"last_workspace_id": None,
|
||||
"last_workspace_slug": None,
|
||||
"fallback_workspace_id": (
|
||||
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_id": (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),
|
||||
"invites": workspace_invites,
|
||||
}
|
||||
|
||||
@ -195,14 +177,10 @@ class ChangePasswordSerializer(serializers.Serializer):
|
||||
|
||||
def validate(self, data):
|
||||
if data.get("old_password") == data.get("new_password"):
|
||||
raise serializers.ValidationError(
|
||||
{"error": "New password cannot be same as old password."}
|
||||
)
|
||||
raise serializers.ValidationError({"error": "New password cannot be same as old password."})
|
||||
|
||||
if data.get("new_password") != data.get("confirm_password"):
|
||||
raise serializers.ValidationError(
|
||||
{"error": "Confirm password should be same as the new password."}
|
||||
)
|
||||
raise serializers.ValidationError({"error": "Confirm password should be same as the new password."})
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@ -21,29 +21,21 @@ class WebhookSerializer(DynamicBaseSerializer):
|
||||
# Extract the hostname from the URL
|
||||
hostname = urlparse(url).hostname
|
||||
if not hostname:
|
||||
raise serializers.ValidationError(
|
||||
{"url": "Invalid URL: No hostname found."}
|
||||
)
|
||||
raise serializers.ValidationError({"url": "Invalid URL: No hostname found."})
|
||||
|
||||
# Resolve the hostname to IP addresses
|
||||
try:
|
||||
ip_addresses = socket.getaddrinfo(hostname, None)
|
||||
except socket.gaierror:
|
||||
raise serializers.ValidationError(
|
||||
{"url": "Hostname could not be resolved."}
|
||||
)
|
||||
raise serializers.ValidationError({"url": "Hostname could not be resolved."})
|
||||
|
||||
if not ip_addresses:
|
||||
raise serializers.ValidationError(
|
||||
{"url": "No IP addresses found for the hostname."}
|
||||
)
|
||||
raise serializers.ValidationError({"url": "No IP addresses found for the hostname."})
|
||||
|
||||
for addr in ip_addresses:
|
||||
ip = ipaddress.ip_address(addr[4][0])
|
||||
if ip.is_loopback:
|
||||
raise serializers.ValidationError(
|
||||
{"url": "URL resolves to a blocked IP address."}
|
||||
)
|
||||
raise serializers.ValidationError({"url": "URL resolves to a blocked IP address."})
|
||||
|
||||
# Additional validation for multiple request domains and their subdomains
|
||||
request = self.context.get("request")
|
||||
@ -53,13 +45,8 @@ class WebhookSerializer(DynamicBaseSerializer):
|
||||
disallowed_domains.append(request_host)
|
||||
|
||||
# Check if hostname is a subdomain or exact match of any disallowed domain
|
||||
if any(
|
||||
hostname == domain or hostname.endswith("." + domain)
|
||||
for domain in disallowed_domains
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
{"url": "URL domain or its subdomain is not allowed."}
|
||||
)
|
||||
if any(hostname == domain or hostname.endswith("." + domain) for domain in disallowed_domains):
|
||||
raise serializers.ValidationError({"url": "URL domain or its subdomain is not allowed."})
|
||||
|
||||
return Webhook.objects.create(**validated_data)
|
||||
|
||||
@ -69,47 +56,32 @@ class WebhookSerializer(DynamicBaseSerializer):
|
||||
# Extract the hostname from the URL
|
||||
hostname = urlparse(url).hostname
|
||||
if not hostname:
|
||||
raise serializers.ValidationError(
|
||||
{"url": "Invalid URL: No hostname found."}
|
||||
)
|
||||
raise serializers.ValidationError({"url": "Invalid URL: No hostname found."})
|
||||
|
||||
# Resolve the hostname to IP addresses
|
||||
try:
|
||||
ip_addresses = socket.getaddrinfo(hostname, None)
|
||||
except socket.gaierror:
|
||||
raise serializers.ValidationError(
|
||||
{"url": "Hostname could not be resolved."}
|
||||
)
|
||||
raise serializers.ValidationError({"url": "Hostname could not be resolved."})
|
||||
|
||||
if not ip_addresses:
|
||||
raise serializers.ValidationError(
|
||||
{"url": "No IP addresses found for the hostname."}
|
||||
)
|
||||
raise serializers.ValidationError({"url": "No IP addresses found for the hostname."})
|
||||
|
||||
for addr in ip_addresses:
|
||||
ip = ipaddress.ip_address(addr[4][0])
|
||||
if ip.is_loopback:
|
||||
raise serializers.ValidationError(
|
||||
{"url": "URL resolves to a blocked IP address."}
|
||||
)
|
||||
raise serializers.ValidationError({"url": "URL resolves to a blocked IP address."})
|
||||
|
||||
# Additional validation for multiple request domains and their subdomains
|
||||
request = self.context.get("request")
|
||||
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
|
||||
if any(
|
||||
hostname == domain or hostname.endswith("." + domain)
|
||||
for domain in disallowed_domains
|
||||
):
|
||||
raise serializers.ValidationError(
|
||||
{"url": "URL domain or its subdomain is not allowed."}
|
||||
)
|
||||
if any(hostname == domain or hostname.endswith("." + domain) for domain in disallowed_domains):
|
||||
raise serializers.ValidationError({"url": "URL domain or its subdomain is not allowed."})
|
||||
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
@ -173,9 +173,7 @@ class WorkspaceUserLinkSerializer(BaseSerializer):
|
||||
)
|
||||
|
||||
if workspace_user_link.exists():
|
||||
raise serializers.ValidationError(
|
||||
{"error": "URL already exists for this workspace and owner"}
|
||||
)
|
||||
raise serializers.ValidationError({"error": "URL already exists for this workspace and owner"})
|
||||
|
||||
return super().create(validated_data)
|
||||
|
||||
@ -189,9 +187,7 @@ class WorkspaceUserLinkSerializer(BaseSerializer):
|
||||
)
|
||||
|
||||
if workspace_user_link.exclude(pk=instance.id).exists():
|
||||
raise serializers.ValidationError(
|
||||
{"error": "URL already exists for this workspace and owner"}
|
||||
)
|
||||
raise serializers.ValidationError({"error": "URL already exists for this workspace and owner"})
|
||||
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
@ -219,11 +215,7 @@ class IssueRecentVisitSerializer(serializers.ModelSerializer):
|
||||
return project.identifier if project else None
|
||||
|
||||
def get_assignees(self, obj):
|
||||
return list(
|
||||
obj.assignees.filter(issue_assignee__deleted_at__isnull=True).values_list(
|
||||
"id", flat=True
|
||||
)
|
||||
)
|
||||
return list(obj.assignees.filter(issue_assignee__deleted_at__isnull=True).values_list("id", flat=True))
|
||||
|
||||
|
||||
class ProjectRecentVisitSerializer(serializers.ModelSerializer):
|
||||
@ -234,9 +226,9 @@ class ProjectRecentVisitSerializer(serializers.ModelSerializer):
|
||||
fields = ["id", "name", "logo_props", "project_members", "identifier"]
|
||||
|
||||
def get_project_members(self, obj):
|
||||
members = ProjectMember.objects.filter(
|
||||
project_id=obj.id, member__is_bot=False, is_active=True
|
||||
).values_list("member", flat=True)
|
||||
members = ProjectMember.objects.filter(project_id=obj.id, member__is_bot=False, is_active=True).values_list(
|
||||
"member", flat=True
|
||||
)
|
||||
|
||||
return members
|
||||
|
||||
@ -257,11 +249,7 @@ class PageRecentVisitSerializer(serializers.ModelSerializer):
|
||||
]
|
||||
|
||||
def get_project_id(self, obj):
|
||||
return (
|
||||
obj.project_id
|
||||
if hasattr(obj, "project_id")
|
||||
else obj.projects.values_list("id", flat=True).first()
|
||||
)
|
||||
return obj.project_id if hasattr(obj, "project_id") else obj.projects.values_list("id", flat=True).first()
|
||||
|
||||
def get_project_identifier(self, obj):
|
||||
project = obj.projects.first()
|
||||
@ -319,13 +307,9 @@ class StickySerializer(BaseSerializer):
|
||||
def validate(self, data):
|
||||
# Validate description content for security
|
||||
if "description_html" in data and data["description_html"]:
|
||||
is_valid, error_msg, sanitized_html = validate_html_content(
|
||||
data["description_html"]
|
||||
)
|
||||
is_valid, error_msg, sanitized_html = validate_html_content(data["description_html"])
|
||||
if not is_valid:
|
||||
raise serializers.ValidationError(
|
||||
{"error": "html content is not valid"}
|
||||
)
|
||||
raise serializers.ValidationError({"error": "html content is not valid"})
|
||||
# Update the data with sanitized HTML if available
|
||||
if sanitized_html is not None:
|
||||
data["description_html"] = sanitized_html
|
||||
@ -333,9 +317,7 @@ class StickySerializer(BaseSerializer):
|
||||
if "description_binary" in data and data["description_binary"]:
|
||||
is_valid, error_msg = validate_binary_data(data["description_binary"])
|
||||
if not is_valid:
|
||||
raise serializers.ValidationError(
|
||||
{"description_binary": "Invalid binary data"}
|
||||
)
|
||||
raise serializers.ValidationError({"description_binary": "Invalid binary data"})
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@ -30,9 +30,7 @@ urlpatterns = [
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/analytic-view/<uuid:pk>/",
|
||||
AnalyticViewViewset.as_view(
|
||||
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
|
||||
),
|
||||
AnalyticViewViewset.as_view({"get": "retrieve", "patch": "partial_update", "delete": "destroy"}),
|
||||
name="analytic-view",
|
||||
),
|
||||
path(
|
||||
|
||||
@ -21,9 +21,7 @@ urlpatterns = [
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:estimate_id>/",
|
||||
BulkEstimatePointEndpoint.as_view(
|
||||
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
|
||||
),
|
||||
BulkEstimatePointEndpoint.as_view({"get": "retrieve", "patch": "partial_update", "delete": "destroy"}),
|
||||
name="bulk-create-estimate-points",
|
||||
),
|
||||
path(
|
||||
|
||||
@ -16,9 +16,7 @@ urlpatterns = [
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/intakes/<uuid:pk>/",
|
||||
IntakeViewSet.as_view(
|
||||
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
|
||||
),
|
||||
IntakeViewSet.as_view({"get": "retrieve", "patch": "partial_update", "delete": "destroy"}),
|
||||
name="intake",
|
||||
),
|
||||
path(
|
||||
@ -28,9 +26,7 @@ urlpatterns = [
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/intake-issues/<uuid:pk>/",
|
||||
IntakeIssueViewSet.as_view(
|
||||
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
|
||||
),
|
||||
IntakeIssueViewSet.as_view({"get": "retrieve", "patch": "partial_update", "delete": "destroy"}),
|
||||
name="intake-issue",
|
||||
),
|
||||
path(
|
||||
@ -40,9 +36,7 @@ urlpatterns = [
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/<uuid:pk>/",
|
||||
IntakeViewSet.as_view(
|
||||
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
|
||||
),
|
||||
IntakeViewSet.as_view({"get": "retrieve", "patch": "partial_update", "delete": "destroy"}),
|
||||
name="inbox",
|
||||
),
|
||||
path(
|
||||
@ -52,9 +46,7 @@ urlpatterns = [
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/<uuid:pk>/",
|
||||
IntakeIssueViewSet.as_view(
|
||||
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
|
||||
),
|
||||
IntakeIssueViewSet.as_view({"get": "retrieve", "patch": "partial_update", "delete": "destroy"}),
|
||||
name="inbox-issue",
|
||||
),
|
||||
path(
|
||||
|
||||
@ -187,9 +187,7 @@ urlpatterns = [
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/subscribe/",
|
||||
IssueSubscriberViewSet.as_view(
|
||||
{"get": "subscription_status", "post": "subscribe", "delete": "unsubscribe"}
|
||||
),
|
||||
IssueSubscriberViewSet.as_view({"get": "subscription_status", "post": "subscribe", "delete": "unsubscribe"}),
|
||||
name="project-issue-subscribers",
|
||||
),
|
||||
## End Issue Subscribers
|
||||
@ -232,9 +230,7 @@ urlpatterns = [
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:pk>/archive/",
|
||||
IssueArchiveViewSet.as_view(
|
||||
{"get": "retrieve", "post": "archive", "delete": "unarchive"}
|
||||
),
|
||||
IssueArchiveViewSet.as_view({"get": "retrieve", "post": "archive", "delete": "unarchive"}),
|
||||
name="project-issue-archive-unarchive",
|
||||
),
|
||||
## End Issue Archives
|
||||
|
||||
@ -17,9 +17,7 @@ urlpatterns = [
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/users/notifications/<uuid:pk>/",
|
||||
NotificationViewSet.as_view(
|
||||
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
|
||||
),
|
||||
NotificationViewSet.as_view({"get": "retrieve", "patch": "partial_update", "delete": "destroy"}),
|
||||
name="notifications",
|
||||
),
|
||||
path(
|
||||
|
||||
@ -22,9 +22,7 @@ urlpatterns = [
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/",
|
||||
PageViewSet.as_view(
|
||||
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
|
||||
),
|
||||
PageViewSet.as_view({"get": "retrieve", "patch": "partial_update", "delete": "destroy"}),
|
||||
name="project-pages",
|
||||
),
|
||||
# favorite pages
|
||||
|
||||
@ -77,9 +77,7 @@ urlpatterns = [
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/members/<uuid:pk>/",
|
||||
ProjectMemberViewSet.as_view(
|
||||
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
|
||||
),
|
||||
ProjectMemberViewSet.as_view({"get": "retrieve", "patch": "partial_update", "delete": "destroy"}),
|
||||
name="project-member",
|
||||
),
|
||||
path(
|
||||
@ -119,9 +117,7 @@ urlpatterns = [
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/<uuid:pk>/",
|
||||
DeployBoardViewSet.as_view(
|
||||
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
|
||||
),
|
||||
DeployBoardViewSet.as_view({"get": "retrieve", "patch": "partial_update", "delete": "destroy"}),
|
||||
name="project-deploy-board",
|
||||
),
|
||||
path(
|
||||
|
||||
@ -12,9 +12,7 @@ urlpatterns = [
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/states/<uuid:pk>/",
|
||||
StateViewSet.as_view(
|
||||
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
|
||||
),
|
||||
StateViewSet.as_view({"get": "retrieve", "patch": "partial_update", "delete": "destroy"}),
|
||||
name="project-state",
|
||||
),
|
||||
path(
|
||||
|
||||
@ -21,9 +21,7 @@ urlpatterns = [
|
||||
# User Profile
|
||||
path(
|
||||
"users/me/",
|
||||
UserEndpoint.as_view(
|
||||
{"get": "retrieve", "patch": "partial_update", "delete": "deactivate"}
|
||||
),
|
||||
UserEndpoint.as_view({"get": "retrieve", "patch": "partial_update", "delete": "deactivate"}),
|
||||
name="users",
|
||||
),
|
||||
path("users/session/", UserSessionEndpoint.as_view(), name="user-session"),
|
||||
@ -44,21 +42,15 @@ urlpatterns = [
|
||||
UserEndpoint.as_view({"get": "retrieve_instance_admin"}),
|
||||
name="users",
|
||||
),
|
||||
path(
|
||||
"users/me/onboard/", UpdateUserOnBoardedEndpoint.as_view(), name="user-onboard"
|
||||
),
|
||||
path("users/me/onboard/", UpdateUserOnBoardedEndpoint.as_view(), name="user-onboard"),
|
||||
path(
|
||||
"users/me/tour-completed/",
|
||||
UpdateUserTourCompletedEndpoint.as_view(),
|
||||
name="user-tour",
|
||||
),
|
||||
path(
|
||||
"users/me/activities/", UserActivityEndpoint.as_view(), name="user-activities"
|
||||
),
|
||||
path("users/me/activities/", UserActivityEndpoint.as_view(), name="user-activities"),
|
||||
# user workspaces
|
||||
path(
|
||||
"users/me/workspaces/", UserWorkSpacesEndpoint.as_view(), name="user-workspace"
|
||||
),
|
||||
path("users/me/workspaces/", UserWorkSpacesEndpoint.as_view(), name="user-workspace"),
|
||||
# User Graphs
|
||||
path(
|
||||
"users/me/workspaces/<str:slug>/activity-graph/",
|
||||
|
||||
@ -65,9 +65,7 @@ urlpatterns = [
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/invitations/<uuid:pk>/",
|
||||
WorkspaceInvitationsViewset.as_view(
|
||||
{"delete": "destroy", "get": "retrieve", "patch": "partial_update"}
|
||||
),
|
||||
WorkspaceInvitationsViewset.as_view({"delete": "destroy", "get": "retrieve", "patch": "partial_update"}),
|
||||
name="workspace-invitations",
|
||||
),
|
||||
# user workspace invitations
|
||||
@ -94,9 +92,7 @@ urlpatterns = [
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/members/<uuid:pk>/",
|
||||
WorkSpaceMemberViewSet.as_view(
|
||||
{"patch": "partial_update", "delete": "destroy", "get": "retrieve"}
|
||||
),
|
||||
WorkSpaceMemberViewSet.as_view({"patch": "partial_update", "delete": "destroy", "get": "retrieve"}),
|
||||
name="workspace-member",
|
||||
),
|
||||
path(
|
||||
@ -126,9 +122,7 @@ urlpatterns = [
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/workspace-themes/<uuid:pk>/",
|
||||
WorkspaceThemeViewSet.as_view(
|
||||
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
|
||||
),
|
||||
WorkspaceThemeViewSet.as_view({"get": "retrieve", "patch": "partial_update", "delete": "destroy"}),
|
||||
name="workspace-themes",
|
||||
),
|
||||
path(
|
||||
@ -208,9 +202,7 @@ urlpatterns = [
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/draft-issues/<uuid:pk>/",
|
||||
WorkspaceDraftIssueViewSet.as_view(
|
||||
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
|
||||
),
|
||||
WorkspaceDraftIssueViewSet.as_view({"get": "retrieve", "patch": "partial_update", "delete": "destroy"}),
|
||||
name="workspace-drafts-issues",
|
||||
),
|
||||
path(
|
||||
@ -226,9 +218,7 @@ urlpatterns = [
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/quick-links/<uuid:pk>/",
|
||||
QuickLinkViewSet.as_view(
|
||||
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
|
||||
),
|
||||
QuickLinkViewSet.as_view({"get": "retrieve", "patch": "partial_update", "delete": "destroy"}),
|
||||
name="workspace-quick-links",
|
||||
),
|
||||
# Widgets
|
||||
@ -254,9 +244,7 @@ urlpatterns = [
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/stickies/<uuid:pk>/",
|
||||
WorkspaceStickyViewSet.as_view(
|
||||
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
|
||||
),
|
||||
WorkspaceStickyViewSet.as_view({"get": "retrieve", "patch": "partial_update", "delete": "destroy"}),
|
||||
name="workspace-sticky",
|
||||
),
|
||||
# User Preference
|
||||
|
||||
@ -41,26 +41,16 @@ class AdvanceAnalyticsEndpoint(AdvanceAnalyticsBaseView):
|
||||
def get_filtered_count() -> int:
|
||||
if self.filters["analytics_date_range"]:
|
||||
return queryset.filter(
|
||||
created_at__gte=self.filters["analytics_date_range"]["current"][
|
||||
"gte"
|
||||
],
|
||||
created_at__lte=self.filters["analytics_date_range"]["current"][
|
||||
"lte"
|
||||
],
|
||||
created_at__gte=self.filters["analytics_date_range"]["current"]["gte"],
|
||||
created_at__lte=self.filters["analytics_date_range"]["current"]["lte"],
|
||||
).count()
|
||||
return queryset.count()
|
||||
|
||||
def get_previous_count() -> int:
|
||||
if self.filters["analytics_date_range"] and self.filters[
|
||||
"analytics_date_range"
|
||||
].get("previous"):
|
||||
if self.filters["analytics_date_range"] and self.filters["analytics_date_range"].get("previous"):
|
||||
return queryset.filter(
|
||||
created_at__gte=self.filters["analytics_date_range"]["previous"][
|
||||
"gte"
|
||||
],
|
||||
created_at__lte=self.filters["analytics_date_range"]["previous"][
|
||||
"lte"
|
||||
],
|
||||
created_at__gte=self.filters["analytics_date_range"]["previous"]["gte"],
|
||||
created_at__lte=self.filters["analytics_date_range"]["previous"]["lte"],
|
||||
).count()
|
||||
return 0
|
||||
|
||||
@ -71,39 +61,27 @@ class AdvanceAnalyticsEndpoint(AdvanceAnalyticsBaseView):
|
||||
|
||||
def get_overview_data(self) -> Dict[str, Dict[str, int]]:
|
||||
members_query = WorkspaceMember.objects.filter(
|
||||
workspace__slug=self._workspace_slug, is_active=True
|
||||
workspace__slug=self._workspace_slug, is_active=True, member__is_bot=False
|
||||
)
|
||||
|
||||
if self.request.GET.get("project_ids", None):
|
||||
project_ids = self.request.GET.get("project_ids", None)
|
||||
project_ids = [str(project_id) for project_id in project_ids.split(",")]
|
||||
members_query = ProjectMember.objects.filter(
|
||||
project_id__in=project_ids, is_active=True
|
||||
project_id__in=project_ids, is_active=True, member__is_bot=False
|
||||
)
|
||||
|
||||
return {
|
||||
"total_users": self.get_filtered_counts(members_query),
|
||||
"total_admins": self.get_filtered_counts(
|
||||
members_query.filter(role=ROLE.ADMIN.value)
|
||||
),
|
||||
"total_members": self.get_filtered_counts(
|
||||
members_query.filter(role=ROLE.MEMBER.value)
|
||||
),
|
||||
"total_guests": self.get_filtered_counts(
|
||||
members_query.filter(role=ROLE.GUEST.value)
|
||||
),
|
||||
"total_projects": self.get_filtered_counts(
|
||||
Project.objects.filter(**self.filters["project_filters"])
|
||||
),
|
||||
"total_work_items": self.get_filtered_counts(
|
||||
Issue.issue_objects.filter(**self.filters["base_filters"])
|
||||
),
|
||||
"total_cycles": self.get_filtered_counts(
|
||||
Cycle.objects.filter(**self.filters["base_filters"])
|
||||
),
|
||||
"total_admins": self.get_filtered_counts(members_query.filter(role=ROLE.ADMIN.value)),
|
||||
"total_members": self.get_filtered_counts(members_query.filter(role=ROLE.MEMBER.value)),
|
||||
"total_guests": self.get_filtered_counts(members_query.filter(role=ROLE.GUEST.value)),
|
||||
"total_projects": self.get_filtered_counts(Project.objects.filter(**self.filters["project_filters"])),
|
||||
"total_work_items": self.get_filtered_counts(Issue.issue_objects.filter(**self.filters["base_filters"])),
|
||||
"total_cycles": self.get_filtered_counts(Cycle.objects.filter(**self.filters["base_filters"])),
|
||||
"total_intake": self.get_filtered_counts(
|
||||
Issue.objects.filter(**self.filters["base_filters"]).filter(
|
||||
issue_intake__status__in=["-2", "0"]
|
||||
issue_intake__status__in=["-2", "-1", "0", "1", "2"] # TODO: Add description for reference.
|
||||
)
|
||||
),
|
||||
}
|
||||
@ -113,18 +91,10 @@ class AdvanceAnalyticsEndpoint(AdvanceAnalyticsBaseView):
|
||||
|
||||
return {
|
||||
"total_work_items": self.get_filtered_counts(base_queryset),
|
||||
"started_work_items": self.get_filtered_counts(
|
||||
base_queryset.filter(state__group="started")
|
||||
),
|
||||
"backlog_work_items": self.get_filtered_counts(
|
||||
base_queryset.filter(state__group="backlog")
|
||||
),
|
||||
"un_started_work_items": self.get_filtered_counts(
|
||||
base_queryset.filter(state__group="unstarted")
|
||||
),
|
||||
"completed_work_items": self.get_filtered_counts(
|
||||
base_queryset.filter(state__group="completed")
|
||||
),
|
||||
"started_work_items": self.get_filtered_counts(base_queryset.filter(state__group="started")),
|
||||
"backlog_work_items": self.get_filtered_counts(base_queryset.filter(state__group="backlog")),
|
||||
"un_started_work_items": self.get_filtered_counts(base_queryset.filter(state__group="unstarted")),
|
||||
"completed_work_items": self.get_filtered_counts(base_queryset.filter(state__group="completed")),
|
||||
}
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
|
||||
@ -153,9 +123,7 @@ class AdvanceAnalyticsStatsEndpoint(AdvanceAnalyticsBaseView):
|
||||
# Apply date range filter if available
|
||||
if self.filters["chart_period_range"]:
|
||||
start_date, end_date = self.filters["chart_period_range"]
|
||||
base_queryset = base_queryset.filter(
|
||||
created_at__date__gte=start_date, created_at__date__lte=end_date
|
||||
)
|
||||
base_queryset = base_queryset.filter(created_at__date__gte=start_date, created_at__date__lte=end_date)
|
||||
|
||||
return (
|
||||
base_queryset.values("project_id", "project__name")
|
||||
@ -212,24 +180,16 @@ class AdvanceAnalyticsChartEndpoint(AdvanceAnalyticsBaseView):
|
||||
}
|
||||
|
||||
total_work_items = base_queryset.filter(**date_filter).count()
|
||||
total_cycles = Cycle.objects.filter(
|
||||
**self.filters["base_filters"], **date_filter
|
||||
).count()
|
||||
total_modules = Module.objects.filter(
|
||||
**self.filters["base_filters"], **date_filter
|
||||
).count()
|
||||
total_cycles = Cycle.objects.filter(**self.filters["base_filters"], **date_filter).count()
|
||||
total_modules = Module.objects.filter(**self.filters["base_filters"], **date_filter).count()
|
||||
total_intake = Issue.objects.filter(
|
||||
issue_intake__isnull=False, **self.filters["base_filters"], **date_filter
|
||||
).count()
|
||||
total_members = WorkspaceMember.objects.filter(
|
||||
workspace__slug=self._workspace_slug, is_active=True, **date_filter
|
||||
).count()
|
||||
total_pages = ProjectPage.objects.filter(
|
||||
**self.filters["base_filters"], **date_filter
|
||||
).count()
|
||||
total_views = IssueView.objects.filter(
|
||||
**self.filters["base_filters"], **date_filter
|
||||
).count()
|
||||
total_pages = ProjectPage.objects.filter(**self.filters["base_filters"], **date_filter).count()
|
||||
total_views = IssueView.objects.filter(**self.filters["base_filters"], **date_filter).count()
|
||||
|
||||
data = {
|
||||
"work_items": total_work_items,
|
||||
@ -255,9 +215,7 @@ class AdvanceAnalyticsChartEndpoint(AdvanceAnalyticsBaseView):
|
||||
queryset = (
|
||||
Issue.issue_objects.filter(**self.filters["base_filters"])
|
||||
.select_related("workspace", "state", "parent")
|
||||
.prefetch_related(
|
||||
"assignees", "labels", "issue_module__module", "issue_cycle__cycle"
|
||||
)
|
||||
.prefetch_related("assignees", "labels", "issue_module__module", "issue_cycle__cycle")
|
||||
)
|
||||
|
||||
workspace = Workspace.objects.get(slug=self._workspace_slug)
|
||||
@ -266,9 +224,7 @@ class AdvanceAnalyticsChartEndpoint(AdvanceAnalyticsBaseView):
|
||||
# Apply date range filter if available
|
||||
if self.filters["chart_period_range"]:
|
||||
start_date, end_date = self.filters["chart_period_range"]
|
||||
queryset = queryset.filter(
|
||||
created_at__date__gte=start_date, created_at__date__lte=end_date
|
||||
)
|
||||
queryset = queryset.filter(created_at__date__gte=start_date, created_at__date__lte=end_date)
|
||||
|
||||
# Annotate by month and count
|
||||
monthly_stats = (
|
||||
@ -311,9 +267,7 @@ class AdvanceAnalyticsChartEndpoint(AdvanceAnalyticsBaseView):
|
||||
)
|
||||
# Move to next month
|
||||
if current_month.month == 12:
|
||||
current_month = current_month.replace(
|
||||
year=current_month.year + 1, month=1
|
||||
)
|
||||
current_month = current_month.replace(year=current_month.year + 1, month=1)
|
||||
else:
|
||||
current_month = current_month.replace(month=current_month.month + 1)
|
||||
|
||||
@ -338,17 +292,13 @@ class AdvanceAnalyticsChartEndpoint(AdvanceAnalyticsBaseView):
|
||||
queryset = (
|
||||
Issue.issue_objects.filter(**self.filters["base_filters"])
|
||||
.select_related("workspace", "state", "parent")
|
||||
.prefetch_related(
|
||||
"assignees", "labels", "issue_module__module", "issue_cycle__cycle"
|
||||
)
|
||||
.prefetch_related("assignees", "labels", "issue_module__module", "issue_cycle__cycle")
|
||||
)
|
||||
|
||||
# Apply date range filter if available
|
||||
if self.filters["chart_period_range"]:
|
||||
start_date, end_date = self.filters["chart_period_range"]
|
||||
queryset = queryset.filter(
|
||||
created_at__date__gte=start_date, created_at__date__lte=end_date
|
||||
)
|
||||
queryset = queryset.filter(created_at__date__gte=start_date, created_at__date__lte=end_date)
|
||||
|
||||
return Response(
|
||||
build_analytics_chart(queryset, x_axis, group_by),
|
||||
|
||||
@ -55,25 +55,16 @@ class AnalyticsEndpoint(BaseAPIView):
|
||||
valid_yaxis = ["issue_count", "estimate"]
|
||||
|
||||
# Check for x-axis and y-axis as thery are required parameters
|
||||
if (
|
||||
not x_axis
|
||||
or not y_axis
|
||||
or x_axis not in valid_xaxis_segment
|
||||
or y_axis not in valid_yaxis
|
||||
):
|
||||
if not x_axis or not y_axis or x_axis not in valid_xaxis_segment or y_axis not in valid_yaxis:
|
||||
return Response(
|
||||
{
|
||||
"error": "x-axis and y-axis dimensions are required and the values should be valid"
|
||||
},
|
||||
{"error": "x-axis and y-axis dimensions are required and the values should be valid"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# If segment is present it cannot be same as x-axis
|
||||
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"
|
||||
},
|
||||
{"error": "Both segment and x axis cannot be same and segment should be valid"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@ -87,9 +78,7 @@ class AnalyticsEndpoint(BaseAPIView):
|
||||
total_issues = queryset.count()
|
||||
|
||||
# Build the graph payload
|
||||
distribution = build_graph_plot(
|
||||
queryset=queryset, x_axis=x_axis, y_axis=y_axis, segment=segment
|
||||
)
|
||||
distribution = build_graph_plot(queryset=queryset, x_axis=x_axis, y_axis=y_axis, segment=segment)
|
||||
|
||||
state_details = {}
|
||||
if x_axis in ["state_id"] or segment in ["state_id"]:
|
||||
@ -118,10 +107,7 @@ class AnalyticsEndpoint(BaseAPIView):
|
||||
if x_axis in ["assignees__id"] or segment in ["assignees__id"]:
|
||||
assignee_details = (
|
||||
Issue.issue_objects.filter(
|
||||
Q(
|
||||
Q(assignees__avatar__isnull=False)
|
||||
| Q(assignees__avatar_asset__isnull=False)
|
||||
),
|
||||
Q(Q(assignees__avatar__isnull=False) | Q(assignees__avatar_asset__isnull=False)),
|
||||
workspace__slug=slug,
|
||||
**filters,
|
||||
)
|
||||
@ -171,9 +157,7 @@ class AnalyticsEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
module_details = {}
|
||||
if x_axis in ["issue_module__module_id"] or segment in [
|
||||
"issue_module__module_id"
|
||||
]:
|
||||
if x_axis in ["issue_module__module_id"] or segment in ["issue_module__module_id"]:
|
||||
module_details = (
|
||||
Issue.issue_objects.filter(
|
||||
workspace__slug=slug,
|
||||
@ -212,9 +196,7 @@ class AnalyticViewViewset(BaseViewSet):
|
||||
serializer.save(workspace_id=workspace.id)
|
||||
|
||||
def get_queryset(self):
|
||||
return self.filter_queryset(
|
||||
super().get_queryset().filter(workspace__slug=self.kwargs.get("slug"))
|
||||
)
|
||||
return self.filter_queryset(super().get_queryset().filter(workspace__slug=self.kwargs.get("slug")))
|
||||
|
||||
|
||||
class SavedAnalyticEndpoint(BaseAPIView):
|
||||
@ -235,9 +217,7 @@ class SavedAnalyticEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
segment = request.GET.get("segment", False)
|
||||
distribution = build_graph_plot(
|
||||
queryset=queryset, x_axis=x_axis, y_axis=y_axis, segment=segment
|
||||
)
|
||||
distribution = build_graph_plot(queryset=queryset, x_axis=x_axis, y_axis=y_axis, segment=segment)
|
||||
total_issues = queryset.count()
|
||||
return Response(
|
||||
{"total": total_issues, "distribution": distribution},
|
||||
@ -270,36 +250,23 @@ class ExportAnalyticsEndpoint(BaseAPIView):
|
||||
valid_yaxis = ["issue_count", "estimate"]
|
||||
|
||||
# Check for x-axis and y-axis as thery are required parameters
|
||||
if (
|
||||
not x_axis
|
||||
or not y_axis
|
||||
or x_axis not in valid_xaxis_segment
|
||||
or y_axis not in valid_yaxis
|
||||
):
|
||||
if not x_axis or not y_axis or x_axis not in valid_xaxis_segment or y_axis not in valid_yaxis:
|
||||
return Response(
|
||||
{
|
||||
"error": "x-axis and y-axis dimensions are required and the values should be valid"
|
||||
},
|
||||
{"error": "x-axis and y-axis dimensions are required and the values should be valid"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# If segment is present it cannot be same as x-axis
|
||||
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"
|
||||
},
|
||||
{"error": "Both segment and x axis cannot be same and segment should be valid"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
analytic_export_task.delay(
|
||||
email=request.user.email, data=request.data, slug=slug
|
||||
)
|
||||
analytic_export_task.delay(email=request.user.email, data=request.data, slug=slug)
|
||||
|
||||
return Response(
|
||||
{
|
||||
"message": f"Once the export is ready it will be emailed to you at {str(request.user.email)}"
|
||||
},
|
||||
{"message": f"Once the export is ready it will be emailed to you at {str(request.user.email)}"},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
@ -315,9 +282,7 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
|
||||
state_groups = base_issues.annotate(state_group=F("state__group"))
|
||||
|
||||
total_issues_classified = (
|
||||
state_groups.values("state_group")
|
||||
.annotate(state_count=Count("state_group"))
|
||||
.order_by("state_group")
|
||||
state_groups.values("state_group").annotate(state_count=Count("state_group")).order_by("state_group")
|
||||
)
|
||||
|
||||
open_issues_groups = ["backlog", "unstarted", "started"]
|
||||
@ -362,9 +327,7 @@ 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"
|
||||
),
|
||||
When(created_by__avatar_asset__isnull=True, then="created_by__avatar"),
|
||||
default=Value(None),
|
||||
output_field=models.CharField(),
|
||||
)
|
||||
@ -395,9 +358,7 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
|
||||
),
|
||||
),
|
||||
# If `avatar_asset` is None, fall back to using `avatar` field directly
|
||||
When(
|
||||
assignees__avatar_asset__isnull=True, then="assignees__avatar"
|
||||
),
|
||||
When(assignees__avatar_asset__isnull=True, then="assignees__avatar"),
|
||||
default=Value(None),
|
||||
output_field=models.CharField(),
|
||||
)
|
||||
@ -422,9 +383,7 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
|
||||
),
|
||||
),
|
||||
# If `avatar_asset` is None, fall back to using `avatar` field directly
|
||||
When(
|
||||
assignees__avatar_asset__isnull=True, then="assignees__avatar"
|
||||
),
|
||||
When(assignees__avatar_asset__isnull=True, then="assignees__avatar"),
|
||||
default=Value(None),
|
||||
output_field=models.CharField(),
|
||||
)
|
||||
@ -485,9 +444,7 @@ class ProjectStatsEndpoint(BaseAPIView):
|
||||
|
||||
if "completed_issues" in requested_fields:
|
||||
annotations["completed_issues"] = (
|
||||
Issue.issue_objects.filter(
|
||||
project_id=OuterRef("pk"), state__group="completed"
|
||||
)
|
||||
Issue.issue_objects.filter(project_id=OuterRef("pk"), state__group__in=["completed", "cancelled"])
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
@ -511,9 +468,7 @@ class ProjectStatsEndpoint(BaseAPIView):
|
||||
|
||||
if "total_members" in requested_fields:
|
||||
annotations["total_members"] = (
|
||||
ProjectMember.objects.filter(
|
||||
project_id=OuterRef("id"), member__is_bot=False, is_active=True
|
||||
)
|
||||
ProjectMember.objects.filter(project_id=OuterRef("id"), member__is_bot=False, is_active=True)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
|
||||
@ -42,12 +42,8 @@ class ProjectAdvanceAnalyticsEndpoint(ProjectAdvanceAnalyticsBaseView):
|
||||
def get_filtered_count() -> int:
|
||||
if self.filters["analytics_date_range"]:
|
||||
return queryset.filter(
|
||||
created_at__gte=self.filters["analytics_date_range"]["current"][
|
||||
"gte"
|
||||
],
|
||||
created_at__lte=self.filters["analytics_date_range"]["current"][
|
||||
"lte"
|
||||
],
|
||||
created_at__gte=self.filters["analytics_date_range"]["current"]["gte"],
|
||||
created_at__lte=self.filters["analytics_date_range"]["current"]["lte"],
|
||||
).count()
|
||||
return queryset.count()
|
||||
|
||||
@ -55,42 +51,30 @@ class ProjectAdvanceAnalyticsEndpoint(ProjectAdvanceAnalyticsBaseView):
|
||||
"count": get_filtered_count(),
|
||||
}
|
||||
|
||||
def get_work_items_stats(
|
||||
self, project_id, cycle_id=None, module_id=None
|
||||
) -> Dict[str, Dict[str, int]]:
|
||||
def get_work_items_stats(self, project_id, cycle_id=None, module_id=None) -> Dict[str, Dict[str, int]]:
|
||||
"""
|
||||
Returns work item stats for the workspace, or filtered by cycle_id or module_id if provided.
|
||||
"""
|
||||
base_queryset = None
|
||||
if cycle_id is not None:
|
||||
cycle_issues = CycleIssue.objects.filter(
|
||||
**self.filters["base_filters"], cycle_id=cycle_id
|
||||
).values_list("issue_id", flat=True)
|
||||
cycle_issues = CycleIssue.objects.filter(**self.filters["base_filters"], cycle_id=cycle_id).values_list(
|
||||
"issue_id", flat=True
|
||||
)
|
||||
base_queryset = Issue.issue_objects.filter(id__in=cycle_issues)
|
||||
elif module_id is not None:
|
||||
module_issues = ModuleIssue.objects.filter(
|
||||
**self.filters["base_filters"], module_id=module_id
|
||||
).values_list("issue_id", flat=True)
|
||||
module_issues = ModuleIssue.objects.filter(**self.filters["base_filters"], module_id=module_id).values_list(
|
||||
"issue_id", flat=True
|
||||
)
|
||||
base_queryset = Issue.issue_objects.filter(id__in=module_issues)
|
||||
else:
|
||||
base_queryset = Issue.issue_objects.filter(
|
||||
**self.filters["base_filters"], project_id=project_id
|
||||
)
|
||||
base_queryset = Issue.issue_objects.filter(**self.filters["base_filters"], project_id=project_id)
|
||||
|
||||
return {
|
||||
"total_work_items": self.get_filtered_counts(base_queryset),
|
||||
"started_work_items": self.get_filtered_counts(
|
||||
base_queryset.filter(state__group="started")
|
||||
),
|
||||
"backlog_work_items": self.get_filtered_counts(
|
||||
base_queryset.filter(state__group="backlog")
|
||||
),
|
||||
"un_started_work_items": self.get_filtered_counts(
|
||||
base_queryset.filter(state__group="unstarted")
|
||||
),
|
||||
"completed_work_items": self.get_filtered_counts(
|
||||
base_queryset.filter(state__group="completed")
|
||||
),
|
||||
"started_work_items": self.get_filtered_counts(base_queryset.filter(state__group="started")),
|
||||
"backlog_work_items": self.get_filtered_counts(base_queryset.filter(state__group="backlog")),
|
||||
"un_started_work_items": self.get_filtered_counts(base_queryset.filter(state__group="unstarted")),
|
||||
"completed_work_items": self.get_filtered_counts(base_queryset.filter(state__group="completed")),
|
||||
}
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
@ -101,9 +85,7 @@ class ProjectAdvanceAnalyticsEndpoint(ProjectAdvanceAnalyticsBaseView):
|
||||
cycle_id = request.GET.get("cycle_id", None)
|
||||
module_id = request.GET.get("module_id", None)
|
||||
return Response(
|
||||
self.get_work_items_stats(
|
||||
cycle_id=cycle_id, module_id=module_id, project_id=project_id
|
||||
),
|
||||
self.get_work_items_stats(cycle_id=cycle_id, module_id=module_id, project_id=project_id),
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
@ -116,9 +98,7 @@ class ProjectAdvanceAnalyticsStatsEndpoint(ProjectAdvanceAnalyticsBaseView):
|
||||
# Apply date range filter if available
|
||||
if self.filters["chart_period_range"]:
|
||||
start_date, end_date = self.filters["chart_period_range"]
|
||||
base_queryset = base_queryset.filter(
|
||||
created_at__date__gte=start_date, created_at__date__lte=end_date
|
||||
)
|
||||
base_queryset = base_queryset.filter(created_at__date__gte=start_date, created_at__date__lte=end_date)
|
||||
|
||||
return (
|
||||
base_queryset.values("project_id", "project__name")
|
||||
@ -132,24 +112,20 @@ class ProjectAdvanceAnalyticsStatsEndpoint(ProjectAdvanceAnalyticsBaseView):
|
||||
.order_by("project_id")
|
||||
)
|
||||
|
||||
def get_work_items_stats(
|
||||
self, project_id, cycle_id=None, module_id=None
|
||||
) -> Dict[str, Dict[str, int]]:
|
||||
def get_work_items_stats(self, project_id, cycle_id=None, module_id=None) -> Dict[str, Dict[str, int]]:
|
||||
base_queryset = None
|
||||
if cycle_id is not None:
|
||||
cycle_issues = CycleIssue.objects.filter(
|
||||
**self.filters["base_filters"], cycle_id=cycle_id
|
||||
).values_list("issue_id", flat=True)
|
||||
cycle_issues = CycleIssue.objects.filter(**self.filters["base_filters"], cycle_id=cycle_id).values_list(
|
||||
"issue_id", flat=True
|
||||
)
|
||||
base_queryset = Issue.issue_objects.filter(id__in=cycle_issues)
|
||||
elif module_id is not None:
|
||||
module_issues = ModuleIssue.objects.filter(
|
||||
**self.filters["base_filters"], module_id=module_id
|
||||
).values_list("issue_id", flat=True)
|
||||
module_issues = ModuleIssue.objects.filter(**self.filters["base_filters"], module_id=module_id).values_list(
|
||||
"issue_id", flat=True
|
||||
)
|
||||
base_queryset = Issue.issue_objects.filter(id__in=module_issues)
|
||||
else:
|
||||
base_queryset = Issue.issue_objects.filter(
|
||||
**self.filters["base_filters"], project_id=project_id
|
||||
)
|
||||
base_queryset = Issue.issue_objects.filter(**self.filters["base_filters"], project_id=project_id)
|
||||
return (
|
||||
base_queryset.annotate(display_name=F("assignees__display_name"))
|
||||
.annotate(assignee_id=F("assignees__id"))
|
||||
@ -166,30 +142,18 @@ class ProjectAdvanceAnalyticsStatsEndpoint(ProjectAdvanceAnalyticsBaseView):
|
||||
),
|
||||
),
|
||||
# If `avatar_asset` is None, fall back to using `avatar` field directly
|
||||
When(
|
||||
assignees__avatar_asset__isnull=True, then="assignees__avatar"
|
||||
),
|
||||
When(assignees__avatar_asset__isnull=True, then="assignees__avatar"),
|
||||
default=Value(None),
|
||||
output_field=models.CharField(),
|
||||
)
|
||||
)
|
||||
.values("display_name", "assignee_id", "avatar_url")
|
||||
.annotate(
|
||||
cancelled_work_items=Count(
|
||||
"id", filter=Q(state__group="cancelled"), distinct=True
|
||||
),
|
||||
completed_work_items=Count(
|
||||
"id", filter=Q(state__group="completed"), distinct=True
|
||||
),
|
||||
backlog_work_items=Count(
|
||||
"id", filter=Q(state__group="backlog"), distinct=True
|
||||
),
|
||||
un_started_work_items=Count(
|
||||
"id", filter=Q(state__group="unstarted"), distinct=True
|
||||
),
|
||||
started_work_items=Count(
|
||||
"id", filter=Q(state__group="started"), distinct=True
|
||||
),
|
||||
cancelled_work_items=Count("id", filter=Q(state__group="cancelled"), distinct=True),
|
||||
completed_work_items=Count("id", filter=Q(state__group="completed"), distinct=True),
|
||||
backlog_work_items=Count("id", filter=Q(state__group="backlog"), distinct=True),
|
||||
un_started_work_items=Count("id", filter=Q(state__group="unstarted"), distinct=True),
|
||||
started_work_items=Count("id", filter=Q(state__group="started"), distinct=True),
|
||||
)
|
||||
.order_by("display_name")
|
||||
)
|
||||
@ -204,9 +168,7 @@ class ProjectAdvanceAnalyticsStatsEndpoint(ProjectAdvanceAnalyticsBaseView):
|
||||
cycle_id = request.GET.get("cycle_id", None)
|
||||
module_id = request.GET.get("module_id", None)
|
||||
return Response(
|
||||
self.get_work_items_stats(
|
||||
project_id=project_id, cycle_id=cycle_id, module_id=module_id
|
||||
),
|
||||
self.get_work_items_stats(project_id=project_id, cycle_id=cycle_id, module_id=module_id),
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
@ -214,23 +176,19 @@ class ProjectAdvanceAnalyticsStatsEndpoint(ProjectAdvanceAnalyticsBaseView):
|
||||
|
||||
|
||||
class ProjectAdvanceAnalyticsChartEndpoint(ProjectAdvanceAnalyticsBaseView):
|
||||
def work_item_completion_chart(
|
||||
self, project_id, cycle_id=None, module_id=None
|
||||
) -> Dict[str, Any]:
|
||||
def work_item_completion_chart(self, project_id, cycle_id=None, module_id=None) -> Dict[str, Any]:
|
||||
# Get the base queryset
|
||||
queryset = (
|
||||
Issue.issue_objects.filter(**self.filters["base_filters"])
|
||||
.filter(project_id=project_id)
|
||||
.select_related("workspace", "state", "parent")
|
||||
.prefetch_related(
|
||||
"assignees", "labels", "issue_module__module", "issue_cycle__cycle"
|
||||
)
|
||||
.prefetch_related("assignees", "labels", "issue_module__module", "issue_cycle__cycle")
|
||||
)
|
||||
|
||||
if cycle_id is not None:
|
||||
cycle_issues = CycleIssue.objects.filter(
|
||||
**self.filters["base_filters"], cycle_id=cycle_id
|
||||
).values_list("issue_id", flat=True)
|
||||
cycle_issues = CycleIssue.objects.filter(**self.filters["base_filters"], cycle_id=cycle_id).values_list(
|
||||
"issue_id", flat=True
|
||||
)
|
||||
cycle = Cycle.objects.filter(id=cycle_id).first()
|
||||
if cycle and cycle.start_date:
|
||||
start_date = cycle.start_date.date()
|
||||
@ -240,9 +198,9 @@ class ProjectAdvanceAnalyticsChartEndpoint(ProjectAdvanceAnalyticsBaseView):
|
||||
queryset = cycle_issues
|
||||
|
||||
elif module_id is not None:
|
||||
module_issues = ModuleIssue.objects.filter(
|
||||
**self.filters["base_filters"], module_id=module_id
|
||||
).values_list("issue_id", flat=True)
|
||||
module_issues = ModuleIssue.objects.filter(**self.filters["base_filters"], module_id=module_id).values_list(
|
||||
"issue_id", flat=True
|
||||
)
|
||||
module = Module.objects.filter(id=module_id).first()
|
||||
if module and module.start_date:
|
||||
start_date = module.start_date
|
||||
@ -264,9 +222,7 @@ class ProjectAdvanceAnalyticsChartEndpoint(ProjectAdvanceAnalyticsBaseView):
|
||||
queryset.values("created_at__date")
|
||||
.annotate(
|
||||
created_count=Count("id"),
|
||||
completed_count=Count(
|
||||
"id", filter=Q(issue__state__group="completed")
|
||||
),
|
||||
completed_count=Count("id", filter=Q(issue__state__group="completed")),
|
||||
)
|
||||
.order_by("created_at__date")
|
||||
)
|
||||
@ -285,9 +241,7 @@ class ProjectAdvanceAnalyticsChartEndpoint(ProjectAdvanceAnalyticsBaseView):
|
||||
current_date = start_date
|
||||
while current_date <= end_date:
|
||||
date_str = current_date.strftime("%Y-%m-%d")
|
||||
stats = stats_dict.get(
|
||||
date_str, {"created_count": 0, "completed_count": 0}
|
||||
)
|
||||
stats = stats_dict.get(date_str, {"created_count": 0, "completed_count": 0})
|
||||
data.append(
|
||||
{
|
||||
"key": date_str,
|
||||
@ -302,9 +256,7 @@ class ProjectAdvanceAnalyticsChartEndpoint(ProjectAdvanceAnalyticsBaseView):
|
||||
# Apply date range filter if available
|
||||
if self.filters["chart_period_range"]:
|
||||
start_date, end_date = self.filters["chart_period_range"]
|
||||
queryset = queryset.filter(
|
||||
created_at__date__gte=start_date, created_at__date__lte=end_date
|
||||
)
|
||||
queryset = queryset.filter(created_at__date__gte=start_date, created_at__date__lte=end_date)
|
||||
|
||||
# Annotate by month and count
|
||||
monthly_stats = (
|
||||
@ -335,9 +287,7 @@ class ProjectAdvanceAnalyticsChartEndpoint(ProjectAdvanceAnalyticsBaseView):
|
||||
|
||||
while current_month <= last_month:
|
||||
date_str = current_month.strftime("%Y-%m-%d")
|
||||
stats = stats_dict.get(
|
||||
date_str, {"created_count": 0, "completed_count": 0}
|
||||
)
|
||||
stats = stats_dict.get(date_str, {"created_count": 0, "completed_count": 0})
|
||||
data.append(
|
||||
{
|
||||
"key": date_str,
|
||||
@ -349,9 +299,7 @@ class ProjectAdvanceAnalyticsChartEndpoint(ProjectAdvanceAnalyticsBaseView):
|
||||
)
|
||||
# Move to next month
|
||||
if current_month.month == 12:
|
||||
current_month = current_month.replace(
|
||||
year=current_month.year + 1, month=1
|
||||
)
|
||||
current_month = current_month.replace(year=current_month.year + 1, month=1)
|
||||
else:
|
||||
current_month = current_month.replace(month=current_month.month + 1)
|
||||
|
||||
@ -376,16 +324,14 @@ class ProjectAdvanceAnalyticsChartEndpoint(ProjectAdvanceAnalyticsBaseView):
|
||||
Issue.issue_objects.filter(**self.filters["base_filters"])
|
||||
.filter(project_id=project_id)
|
||||
.select_related("workspace", "state", "parent")
|
||||
.prefetch_related(
|
||||
"assignees", "labels", "issue_module__module", "issue_cycle__cycle"
|
||||
)
|
||||
.prefetch_related("assignees", "labels", "issue_module__module", "issue_cycle__cycle")
|
||||
)
|
||||
|
||||
# Apply cycle/module filters if present
|
||||
if cycle_id is not None:
|
||||
cycle_issues = CycleIssue.objects.filter(
|
||||
**self.filters["base_filters"], cycle_id=cycle_id
|
||||
).values_list("issue_id", flat=True)
|
||||
cycle_issues = CycleIssue.objects.filter(**self.filters["base_filters"], cycle_id=cycle_id).values_list(
|
||||
"issue_id", flat=True
|
||||
)
|
||||
queryset = queryset.filter(id__in=cycle_issues)
|
||||
|
||||
elif module_id is not None:
|
||||
@ -397,9 +343,7 @@ class ProjectAdvanceAnalyticsChartEndpoint(ProjectAdvanceAnalyticsBaseView):
|
||||
# Apply date range filter if available
|
||||
if self.filters["chart_period_range"]:
|
||||
start_date, end_date = self.filters["chart_period_range"]
|
||||
queryset = queryset.filter(
|
||||
created_at__date__gte=start_date, created_at__date__lte=end_date
|
||||
)
|
||||
queryset = queryset.filter(created_at__date__gte=start_date, created_at__date__lte=end_date)
|
||||
|
||||
return Response(
|
||||
build_analytics_chart(queryset, x_axis, group_by),
|
||||
@ -412,9 +356,7 @@ class ProjectAdvanceAnalyticsChartEndpoint(ProjectAdvanceAnalyticsBaseView):
|
||||
module_id = request.GET.get("module_id", None)
|
||||
|
||||
return Response(
|
||||
self.work_item_completion_chart(
|
||||
project_id=project_id, cycle_id=cycle_id, module_id=module_id
|
||||
),
|
||||
self.work_item_completion_chart(project_id=project_id, cycle_id=cycle_id, module_id=module_id),
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
|
||||
@ -65,9 +65,7 @@ class ServiceApiTokenEndpoint(BaseAPIView):
|
||||
def post(self, request: Request, slug: str) -> Response:
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
|
||||
api_token = APIToken.objects.filter(
|
||||
workspace=workspace, is_service=True
|
||||
).first()
|
||||
api_token = APIToken.objects.filter(workspace=workspace, is_service=True).first()
|
||||
|
||||
if api_token:
|
||||
return Response({"token": str(api_token.token)}, status=status.HTTP_200_OK)
|
||||
@ -83,6 +81,4 @@ class ServiceApiTokenEndpoint(BaseAPIView):
|
||||
user_type=user_type,
|
||||
is_service=True,
|
||||
)
|
||||
return Response(
|
||||
{"token": str(api_token.token)}, status=status.HTTP_201_CREATED
|
||||
)
|
||||
return Response({"token": str(api_token.token)}, status=status.HTTP_201_CREATED)
|
||||
|
||||
@ -20,12 +20,8 @@ class FileAssetEndpoint(BaseAPIView):
|
||||
asset_key = str(workspace_id) + "/" + asset_key
|
||||
files = FileAsset.objects.filter(asset=asset_key)
|
||||
if files.exists():
|
||||
serializer = FileAssetSerializer(
|
||||
files, context={"request": request}, many=True
|
||||
)
|
||||
return Response(
|
||||
{"data": serializer.data, "status": True}, status=status.HTTP_200_OK
|
||||
)
|
||||
serializer = FileAssetSerializer(files, context={"request": request}, many=True)
|
||||
return Response({"data": serializer.data, "status": True}, status=status.HTTP_200_OK)
|
||||
else:
|
||||
return Response(
|
||||
{"error": "Asset key does not exist", "status": False},
|
||||
@ -65,9 +61,7 @@ class UserAssetsEndpoint(BaseAPIView):
|
||||
files = FileAsset.objects.filter(asset=asset_key, created_by=request.user)
|
||||
if files.exists():
|
||||
serializer = FileAssetSerializer(files, context={"request": request})
|
||||
return Response(
|
||||
{"data": serializer.data, "status": True}, status=status.HTTP_200_OK
|
||||
)
|
||||
return Response({"data": serializer.data, "status": True}, status=status.HTTP_200_OK)
|
||||
else:
|
||||
return Response(
|
||||
{"error": "Asset key does not exist", "status": False},
|
||||
|
||||
@ -44,9 +44,7 @@ class UserAssetsV2Endpoint(BaseAPIView):
|
||||
# Save the new avatar
|
||||
user.avatar_asset_id = asset_id
|
||||
user.save()
|
||||
invalidate_cache_directly(
|
||||
path="/api/users/me/", url_params=False, user=True, request=request
|
||||
)
|
||||
invalidate_cache_directly(path="/api/users/me/", url_params=False, user=True, request=request)
|
||||
invalidate_cache_directly(
|
||||
path="/api/users/me/settings/",
|
||||
url_params=False,
|
||||
@ -64,9 +62,7 @@ class UserAssetsV2Endpoint(BaseAPIView):
|
||||
# Save the new cover image
|
||||
user.cover_image_asset_id = asset_id
|
||||
user.save()
|
||||
invalidate_cache_directly(
|
||||
path="/api/users/me/", url_params=False, user=True, request=request
|
||||
)
|
||||
invalidate_cache_directly(path="/api/users/me/", url_params=False, user=True, request=request)
|
||||
invalidate_cache_directly(
|
||||
path="/api/users/me/settings/",
|
||||
url_params=False,
|
||||
@ -82,9 +78,7 @@ class UserAssetsV2Endpoint(BaseAPIView):
|
||||
user = User.objects.get(id=asset.user_id)
|
||||
user.avatar_asset_id = None
|
||||
user.save()
|
||||
invalidate_cache_directly(
|
||||
path="/api/users/me/", url_params=False, user=True, request=request
|
||||
)
|
||||
invalidate_cache_directly(path="/api/users/me/", url_params=False, user=True, request=request)
|
||||
invalidate_cache_directly(
|
||||
path="/api/users/me/settings/",
|
||||
url_params=False,
|
||||
@ -97,9 +91,7 @@ class UserAssetsV2Endpoint(BaseAPIView):
|
||||
user = User.objects.get(id=asset.user_id)
|
||||
user.cover_image_asset_id = None
|
||||
user.save()
|
||||
invalidate_cache_directly(
|
||||
path="/api/users/me/", url_params=False, user=True, request=request
|
||||
)
|
||||
invalidate_cache_directly(path="/api/users/me/", url_params=False, user=True, request=request)
|
||||
invalidate_cache_directly(
|
||||
path="/api/users/me/settings/",
|
||||
url_params=False,
|
||||
@ -159,9 +151,7 @@ class UserAssetsV2Endpoint(BaseAPIView):
|
||||
# Get the presigned URL
|
||||
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
|
||||
)
|
||||
presigned_url = storage.generate_presigned_post(object_name=asset_key, file_type=type, file_size=size_limit)
|
||||
# Return the presigned URL
|
||||
return Response(
|
||||
{
|
||||
@ -198,9 +188,7 @@ class UserAssetsV2Endpoint(BaseAPIView):
|
||||
asset.is_deleted = True
|
||||
asset.deleted_at = timezone.now()
|
||||
# get the entity and save the asset id for the request field
|
||||
self.entity_asset_delete(
|
||||
entity_type=asset.entity_type, asset=asset, request=request
|
||||
)
|
||||
self.entity_asset_delete(entity_type=asset.entity_type, asset=asset, request=request)
|
||||
asset.save(update_fields=["is_deleted", "deleted_at"])
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@ -264,18 +252,14 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
|
||||
workspace.logo = ""
|
||||
workspace.logo_asset_id = asset_id
|
||||
workspace.save()
|
||||
invalidate_cache_directly(
|
||||
path="/api/workspaces/", url_params=False, user=False, request=request
|
||||
)
|
||||
invalidate_cache_directly(path="/api/workspaces/", url_params=False, user=False, request=request)
|
||||
invalidate_cache_directly(
|
||||
path="/api/users/me/workspaces/",
|
||||
url_params=False,
|
||||
user=True,
|
||||
request=request,
|
||||
)
|
||||
invalidate_cache_directly(
|
||||
path="/api/instances/", url_params=False, user=False, request=request
|
||||
)
|
||||
invalidate_cache_directly(path="/api/instances/", url_params=False, user=False, request=request)
|
||||
return
|
||||
|
||||
# Project Cover
|
||||
@ -302,18 +286,14 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
|
||||
return
|
||||
workspace.logo_asset_id = None
|
||||
workspace.save()
|
||||
invalidate_cache_directly(
|
||||
path="/api/workspaces/", url_params=False, user=False, request=request
|
||||
)
|
||||
invalidate_cache_directly(path="/api/workspaces/", url_params=False, user=False, request=request)
|
||||
invalidate_cache_directly(
|
||||
path="/api/users/me/workspaces/",
|
||||
url_params=False,
|
||||
user=True,
|
||||
request=request,
|
||||
)
|
||||
invalidate_cache_directly(
|
||||
path="/api/instances/", url_params=False, user=False, request=request
|
||||
)
|
||||
invalidate_cache_directly(path="/api/instances/", url_params=False, user=False, request=request)
|
||||
return
|
||||
# Project Cover
|
||||
elif entity_type == FileAsset.EntityTypeContext.PROJECT_COVER:
|
||||
@ -374,17 +354,13 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
|
||||
workspace=workspace,
|
||||
created_by=request.user,
|
||||
entity_type=entity_type,
|
||||
**self.get_entity_id_field(
|
||||
entity_type=entity_type, entity_id=entity_identifier
|
||||
),
|
||||
**self.get_entity_id_field(entity_type=entity_type, entity_id=entity_identifier),
|
||||
)
|
||||
|
||||
# Get the presigned URL
|
||||
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
|
||||
)
|
||||
presigned_url = storage.generate_presigned_post(object_name=asset_key, file_type=type, file_size=size_limit)
|
||||
# Return the presigned URL
|
||||
return Response(
|
||||
{
|
||||
@ -421,9 +397,7 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
|
||||
asset.is_deleted = True
|
||||
asset.deleted_at = timezone.now()
|
||||
# get the entity and save the asset id for the request field
|
||||
self.entity_asset_delete(
|
||||
entity_type=asset.entity_type, asset=asset, request=request
|
||||
)
|
||||
self.entity_asset_delete(entity_type=asset.entity_type, asset=asset, request=request)
|
||||
asset.save(update_fields=["is_deleted", "deleted_at"])
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@ -586,9 +560,7 @@ class ProjectAssetEndpoint(BaseAPIView):
|
||||
# Get the presigned URL
|
||||
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
|
||||
)
|
||||
presigned_url = storage.generate_presigned_post(object_name=asset_key, file_type=type, file_size=size_limit)
|
||||
# Return the presigned URL
|
||||
return Response(
|
||||
{
|
||||
@ -618,9 +590,7 @@ class ProjectAssetEndpoint(BaseAPIView):
|
||||
@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
|
||||
)
|
||||
asset = FileAsset.objects.get(id=pk, workspace__slug=slug, project_id=project_id)
|
||||
# Check deleted assets
|
||||
asset.is_deleted = True
|
||||
asset.deleted_at = timezone.now()
|
||||
@ -631,9 +601,7 @@ class ProjectAssetEndpoint(BaseAPIView):
|
||||
@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
|
||||
)
|
||||
asset = FileAsset.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
|
||||
|
||||
# Check if the asset is uploaded
|
||||
if not asset.is_uploaded:
|
||||
@ -666,9 +634,7 @@ 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
|
||||
)
|
||||
return Response({"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)
|
||||
@ -722,9 +688,7 @@ class AssetCheckEndpoint(BaseAPIView):
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def get(self, request, slug, asset_id):
|
||||
asset = FileAsset.all_objects.filter(
|
||||
id=asset_id, workspace__slug=slug, deleted_at__isnull=True
|
||||
).exists()
|
||||
asset = FileAsset.all_objects.filter(id=asset_id, workspace__slug=slug, deleted_at__isnull=True).exists()
|
||||
return Response({"exists": asset}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
|
||||
@ -72,11 +72,7 @@ class BaseViewSet(TimezoneMixin, ReadReplicaControlMixin, ModelViewSet, BasePagi
|
||||
response = super().handle_exception(exc)
|
||||
return response
|
||||
except Exception as e:
|
||||
(
|
||||
print(e, traceback.format_exc())
|
||||
if settings.DEBUG
|
||||
else print("Server Error")
|
||||
)
|
||||
(print(e, traceback.format_exc()) if settings.DEBUG else print("Server Error"))
|
||||
if isinstance(e, IntegrityError):
|
||||
return Response(
|
||||
{"error": "The payload is not valid"},
|
||||
@ -115,9 +111,7 @@ class BaseViewSet(TimezoneMixin, ReadReplicaControlMixin, ModelViewSet, BasePagi
|
||||
if settings.DEBUG:
|
||||
from django.db import connection
|
||||
|
||||
print(
|
||||
f"{request.method} - {request.get_full_path()} of Queries: {len(connection.queries)}"
|
||||
)
|
||||
print(f"{request.method} - {request.get_full_path()} of Queries: {len(connection.queries)}")
|
||||
|
||||
return response
|
||||
except Exception as exc:
|
||||
@ -139,16 +133,12 @@ class BaseViewSet(TimezoneMixin, ReadReplicaControlMixin, ModelViewSet, BasePagi
|
||||
|
||||
@property
|
||||
def fields(self):
|
||||
fields = [
|
||||
field for field in self.request.GET.get("fields", "").split(",") if field
|
||||
]
|
||||
fields = [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 = [expand for expand in self.request.GET.get("expand", "").split(",") if expand]
|
||||
return expand if expand else None
|
||||
|
||||
|
||||
@ -216,9 +206,7 @@ class BaseAPIView(TimezoneMixin, ReadReplicaControlMixin, APIView, BasePaginator
|
||||
if settings.DEBUG:
|
||||
from django.db import connection
|
||||
|
||||
print(
|
||||
f"{request.method} - {request.get_full_path()} of Queries: {len(connection.queries)}"
|
||||
)
|
||||
print(f"{request.method} - {request.get_full_path()} of Queries: {len(connection.queries)}")
|
||||
return response
|
||||
|
||||
except Exception as exc:
|
||||
@ -235,14 +223,10 @@ class BaseAPIView(TimezoneMixin, ReadReplicaControlMixin, APIView, BasePaginator
|
||||
|
||||
@property
|
||||
def fields(self):
|
||||
fields = [
|
||||
field for field in self.request.GET.get("fields", "").split(",") if field
|
||||
]
|
||||
fields = [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 = [expand for expand in self.request.GET.get("expand", "").split(",") if expand]
|
||||
return expand if expand else None
|
||||
|
||||
@ -50,9 +50,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
)
|
||||
.values("issue_cycle__cycle_id")
|
||||
.annotate(
|
||||
backlog_estimate_point=Sum(Cast("estimate_point__value", FloatField()))
|
||||
)
|
||||
.annotate(backlog_estimate_point=Sum(Cast("estimate_point__value", FloatField())))
|
||||
.values("backlog_estimate_point")[:1]
|
||||
)
|
||||
unstarted_estimate_point = (
|
||||
@ -63,11 +61,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
)
|
||||
.values("issue_cycle__cycle_id")
|
||||
.annotate(
|
||||
unstarted_estimate_point=Sum(
|
||||
Cast("estimate_point__value", FloatField())
|
||||
)
|
||||
)
|
||||
.annotate(unstarted_estimate_point=Sum(Cast("estimate_point__value", FloatField())))
|
||||
.values("unstarted_estimate_point")[:1]
|
||||
)
|
||||
started_estimate_point = (
|
||||
@ -78,9 +72,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
)
|
||||
.values("issue_cycle__cycle_id")
|
||||
.annotate(
|
||||
started_estimate_point=Sum(Cast("estimate_point__value", FloatField()))
|
||||
)
|
||||
.annotate(started_estimate_point=Sum(Cast("estimate_point__value", FloatField())))
|
||||
.values("started_estimate_point")[:1]
|
||||
)
|
||||
cancelled_estimate_point = (
|
||||
@ -91,11 +83,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
)
|
||||
.values("issue_cycle__cycle_id")
|
||||
.annotate(
|
||||
cancelled_estimate_point=Sum(
|
||||
Cast("estimate_point__value", FloatField())
|
||||
)
|
||||
)
|
||||
.annotate(cancelled_estimate_point=Sum(Cast("estimate_point__value", FloatField())))
|
||||
.values("cancelled_estimate_point")[:1]
|
||||
)
|
||||
completed_estimate_point = (
|
||||
@ -106,11 +94,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
)
|
||||
.values("issue_cycle__cycle_id")
|
||||
.annotate(
|
||||
completed_estimate_points=Sum(
|
||||
Cast("estimate_point__value", FloatField())
|
||||
)
|
||||
)
|
||||
.annotate(completed_estimate_points=Sum(Cast("estimate_point__value", FloatField())))
|
||||
.values("completed_estimate_points")[:1]
|
||||
)
|
||||
total_estimate_point = (
|
||||
@ -120,9 +104,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
issue_cycle__deleted_at__isnull=True,
|
||||
)
|
||||
.values("issue_cycle__cycle_id")
|
||||
.annotate(
|
||||
total_estimate_points=Sum(Cast("estimate_point__value", FloatField()))
|
||||
)
|
||||
.annotate(total_estimate_points=Sum(Cast("estimate_point__value", FloatField())))
|
||||
.values("total_estimate_points")[:1]
|
||||
)
|
||||
return (
|
||||
@ -138,9 +120,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_cycle__issue__assignees",
|
||||
queryset=User.objects.only(
|
||||
"avatar_asset", "first_name", "id"
|
||||
).distinct(),
|
||||
queryset=User.objects.only("avatar_asset", "first_name", "id").distinct(),
|
||||
)
|
||||
)
|
||||
.prefetch_related(
|
||||
@ -224,8 +204,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
.annotate(
|
||||
status=Case(
|
||||
When(
|
||||
Q(start_date__lte=timezone.now())
|
||||
& Q(end_date__gte=timezone.now()),
|
||||
Q(start_date__lte=timezone.now()) & Q(end_date__gte=timezone.now()),
|
||||
then=Value("CURRENT"),
|
||||
),
|
||||
When(start_date__gt=timezone.now(), then=Value("UPCOMING")),
|
||||
@ -279,9 +258,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
total_estimate_points=Coalesce(
|
||||
Subquery(total_estimate_point), Value(0, output_field=FloatField())
|
||||
)
|
||||
total_estimate_points=Coalesce(Subquery(total_estimate_point), Value(0, output_field=FloatField()))
|
||||
)
|
||||
.order_by("-is_favorite", "name")
|
||||
.distinct()
|
||||
@ -322,9 +299,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
).order_by("-is_favorite", "-created_at")
|
||||
return Response(queryset, status=status.HTTP_200_OK)
|
||||
else:
|
||||
queryset = (
|
||||
self.get_queryset().filter(archived_at__isnull=False).filter(pk=pk)
|
||||
)
|
||||
queryset = self.get_queryset().filter(archived_at__isnull=False).filter(pk=pk)
|
||||
data = (
|
||||
self.get_queryset()
|
||||
.filter(pk=pk)
|
||||
@ -415,9 +390,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
)
|
||||
)
|
||||
.values("display_name", "assignee_id", "avatar_url")
|
||||
.annotate(
|
||||
total_estimates=Sum(Cast("estimate_point__value", FloatField()))
|
||||
)
|
||||
.annotate(total_estimates=Sum(Cast("estimate_point__value", FloatField())))
|
||||
.annotate(
|
||||
completed_estimates=Sum(
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
@ -452,9 +425,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
.annotate(color=F("labels__color"))
|
||||
.annotate(label_id=F("labels__id"))
|
||||
.values("label_name", "color", "label_id")
|
||||
.annotate(
|
||||
total_estimates=Sum(Cast("estimate_point__value", FloatField()))
|
||||
)
|
||||
.annotate(total_estimates=Sum(Cast("estimate_point__value", FloatField())))
|
||||
.annotate(
|
||||
completed_estimates=Sum(
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
@ -531,11 +502,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
"avatar_url",
|
||||
"display_name",
|
||||
)
|
||||
.annotate(
|
||||
total_issues=Count(
|
||||
"id", filter=Q(archived_at__isnull=True, is_draft=False)
|
||||
)
|
||||
)
|
||||
.annotate(total_issues=Count("id", filter=Q(archived_at__isnull=True, is_draft=False)))
|
||||
.annotate(
|
||||
completed_issues=Count(
|
||||
"id",
|
||||
@ -571,11 +538,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
.annotate(color=F("labels__color"))
|
||||
.annotate(label_id=F("labels__id"))
|
||||
.values("label_name", "color", "label_id")
|
||||
.annotate(
|
||||
total_issues=Count(
|
||||
"id", filter=Q(archived_at__isnull=True, is_draft=False)
|
||||
)
|
||||
)
|
||||
.annotate(total_issues=Count("id", filter=Q(archived_at__isnull=True, is_draft=False)))
|
||||
.annotate(
|
||||
completed_issues=Count(
|
||||
"id",
|
||||
@ -618,9 +581,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def post(self, request, slug, project_id, cycle_id):
|
||||
cycle = Cycle.objects.get(
|
||||
pk=cycle_id, project_id=project_id, workspace__slug=slug
|
||||
)
|
||||
cycle = Cycle.objects.get(pk=cycle_id, project_id=project_id, workspace__slug=slug)
|
||||
|
||||
if cycle.end_date >= timezone.now():
|
||||
return Response(
|
||||
@ -636,15 +597,11 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
).delete()
|
||||
return Response(
|
||||
{"archived_at": str(cycle.archived_at)}, status=status.HTTP_200_OK
|
||||
)
|
||||
return Response({"archived_at": str(cycle.archived_at)}, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def delete(self, request, slug, project_id, cycle_id):
|
||||
cycle = Cycle.objects.get(
|
||||
pk=cycle_id, project_id=project_id, workspace__slug=slug
|
||||
)
|
||||
cycle = Cycle.objects.get(pk=cycle_id, project_id=project_id, workspace__slug=slug)
|
||||
cycle.archived_at = None
|
||||
cycle.save()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@ -46,7 +46,6 @@ from plane.db.models import (
|
||||
Label,
|
||||
User,
|
||||
Project,
|
||||
ProjectMember,
|
||||
UserRecentVisit,
|
||||
)
|
||||
from plane.utils.analytics_plot import burndown_plot
|
||||
@ -97,9 +96,7 @@ class CycleViewSet(BaseViewSet):
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_cycle__issue__assignees",
|
||||
queryset=User.objects.only(
|
||||
"avatar_asset", "first_name", "id"
|
||||
).distinct(),
|
||||
queryset=User.objects.only("avatar_asset", "first_name", "id").distinct(),
|
||||
)
|
||||
)
|
||||
.prefetch_related(
|
||||
@ -150,8 +147,7 @@ class CycleViewSet(BaseViewSet):
|
||||
.annotate(
|
||||
status=Case(
|
||||
When(
|
||||
Q(start_date__lte=current_time_in_utc)
|
||||
& Q(end_date__gte=current_time_in_utc),
|
||||
Q(start_date__lte=current_time_in_utc) & Q(end_date__gte=current_time_in_utc),
|
||||
then=Value("CURRENT"),
|
||||
),
|
||||
When(start_date__gt=current_time_in_utc, then=Value("UPCOMING")),
|
||||
@ -170,11 +166,7 @@ class CycleViewSet(BaseViewSet):
|
||||
"issue_cycle__issue__assignees__id",
|
||||
distinct=True,
|
||||
filter=~Q(issue_cycle__issue__assignees__id__isnull=True)
|
||||
& (
|
||||
Q(
|
||||
issue_cycle__issue__issue_assignee__deleted_at__isnull=True
|
||||
)
|
||||
),
|
||||
& (Q(issue_cycle__issue__issue_assignee__deleted_at__isnull=True)),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
)
|
||||
@ -205,9 +197,7 @@ class CycleViewSet(BaseViewSet):
|
||||
|
||||
# Current Cycle
|
||||
if cycle_view == "current":
|
||||
queryset = queryset.filter(
|
||||
start_date__lte=current_time_in_utc, end_date__gte=current_time_in_utc
|
||||
)
|
||||
queryset = queryset.filter(start_date__lte=current_time_in_utc, end_date__gte=current_time_in_utc)
|
||||
|
||||
data = queryset.values(
|
||||
# necessary fields
|
||||
@ -274,16 +264,10 @@ class CycleViewSet(BaseViewSet):
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def create(self, request, slug, project_id):
|
||||
if (
|
||||
request.data.get("start_date", None) is None
|
||||
and request.data.get("end_date", None) is None
|
||||
) or (
|
||||
request.data.get("start_date", None) is not None
|
||||
and request.data.get("end_date", None) is not None
|
||||
if (request.data.get("start_date", None) is None and request.data.get("end_date", None) is None) or (
|
||||
request.data.get("start_date", None) is not None and request.data.get("end_date", None) is not None
|
||||
):
|
||||
serializer = CycleWriteSerializer(
|
||||
data=request.data, context={"project_id": project_id}
|
||||
)
|
||||
serializer = CycleWriteSerializer(data=request.data, context={"project_id": project_id})
|
||||
if serializer.is_valid():
|
||||
serializer.save(project_id=project_id, owned_by=request.user)
|
||||
cycle = (
|
||||
@ -323,9 +307,7 @@ class CycleViewSet(BaseViewSet):
|
||||
project_timezone = project.timezone
|
||||
|
||||
datetime_fields = ["start_date", "end_date"]
|
||||
cycle = user_timezone_converter(
|
||||
cycle, datetime_fields, project_timezone
|
||||
)
|
||||
cycle = user_timezone_converter(cycle, datetime_fields, project_timezone)
|
||||
|
||||
# Send the model activity
|
||||
model_activity.delay(
|
||||
@ -341,17 +323,13 @@ class CycleViewSet(BaseViewSet):
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
else:
|
||||
return Response(
|
||||
{
|
||||
"error": "Both start date and end date are either required or are to be null"
|
||||
},
|
||||
{"error": "Both start date and end date are either required or are to be null"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def partial_update(self, request, slug, project_id, pk):
|
||||
queryset = self.get_queryset().filter(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
)
|
||||
queryset = self.get_queryset().filter(workspace__slug=slug, project_id=project_id, pk=pk)
|
||||
cycle = queryset.first()
|
||||
if cycle.archived_at:
|
||||
return Response(
|
||||
@ -359,29 +337,21 @@ class CycleViewSet(BaseViewSet):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
current_instance = json.dumps(
|
||||
CycleSerializer(cycle).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
current_instance = json.dumps(CycleSerializer(cycle).data, cls=DjangoJSONEncoder)
|
||||
|
||||
request_data = request.data
|
||||
|
||||
if cycle.end_date is not None and cycle.end_date < timezone.now():
|
||||
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)
|
||||
}
|
||||
request_data = {"sort_order": request_data.get("sort_order", cycle.sort_order)}
|
||||
else:
|
||||
return Response(
|
||||
{
|
||||
"error": "The Cycle has already been completed so it cannot be edited"
|
||||
},
|
||||
{"error": "The Cycle has already been completed so it cannot be edited"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
serializer = CycleWriteSerializer(
|
||||
cycle, data=request.data, partial=True, context={"project_id": project_id}
|
||||
)
|
||||
serializer = CycleWriteSerializer(cycle, data=request.data, partial=True, context={"project_id": project_id})
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
cycle = queryset.values(
|
||||
@ -481,9 +451,7 @@ class CycleViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
if data is None:
|
||||
return Response(
|
||||
{"error": "Cycle not found"}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
return Response({"error": "Cycle not found"}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
queryset = queryset.first()
|
||||
# Fetch the project timezone
|
||||
@ -505,11 +473,7 @@ class CycleViewSet(BaseViewSet):
|
||||
def destroy(self, request, slug, project_id, pk):
|
||||
cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
|
||||
|
||||
cycle_issues = list(
|
||||
CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list(
|
||||
"issue", flat=True
|
||||
)
|
||||
)
|
||||
cycle_issues = list(CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list("issue", flat=True))
|
||||
|
||||
issue_activity.delay(
|
||||
type="cycle.activity.deleted",
|
||||
@ -560,9 +524,7 @@ class CycleDateCheckEndpoint(BaseAPIView):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
start_date = convert_to_utc(
|
||||
date=str(start_date), project_id=project_id, is_start_date=True
|
||||
)
|
||||
start_date = convert_to_utc(date=str(start_date), project_id=project_id, is_start_date=True)
|
||||
end_date = convert_to_utc(
|
||||
date=str(end_date),
|
||||
project_id=project_id,
|
||||
@ -581,7 +543,7 @@ class CycleDateCheckEndpoint(BaseAPIView):
|
||||
if cycles.exists():
|
||||
return Response(
|
||||
{
|
||||
"error": "You have a cycle already on the given dates, if you want to create a draft cycle you can do that by removing dates",
|
||||
"error": "You have a cycle already on the given dates, if you want to create a draft cycle you can do that by removing dates", # noqa: E501
|
||||
"status": False,
|
||||
}
|
||||
)
|
||||
@ -635,14 +597,10 @@ class TransferCycleIssueEndpoint(BaseAPIView):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
new_cycle = Cycle.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id, pk=new_cycle_id
|
||||
).first()
|
||||
new_cycle = Cycle.objects.filter(workspace__slug=slug, project_id=project_id, pk=new_cycle_id).first()
|
||||
|
||||
old_cycle = (
|
||||
Cycle.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id, pk=cycle_id
|
||||
)
|
||||
Cycle.objects.filter(workspace__slug=slug, project_id=project_id, pk=cycle_id)
|
||||
.annotate(
|
||||
total_issues=Count(
|
||||
"issue_cycle",
|
||||
@ -755,9 +713,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
|
||||
)
|
||||
)
|
||||
.values("display_name", "assignee_id", "avatar_url")
|
||||
.annotate(
|
||||
total_estimates=Sum(Cast("estimate_point__value", FloatField()))
|
||||
)
|
||||
.annotate(total_estimates=Sum(Cast("estimate_point__value", FloatField())))
|
||||
.annotate(
|
||||
completed_estimates=Sum(
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
@ -784,9 +740,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
|
||||
assignee_estimate_distribution = [
|
||||
{
|
||||
"display_name": item["display_name"],
|
||||
"assignee_id": (
|
||||
str(item["assignee_id"]) if item["assignee_id"] else None
|
||||
),
|
||||
"assignee_id": (str(item["assignee_id"]) if item["assignee_id"] else None),
|
||||
"avatar": item.get("avatar"),
|
||||
"avatar_url": item.get("avatar_url"),
|
||||
"total_estimates": item["total_estimates"],
|
||||
@ -807,9 +761,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
|
||||
.annotate(color=F("labels__color"))
|
||||
.annotate(label_id=F("labels__id"))
|
||||
.values("label_name", "color", "label_id")
|
||||
.annotate(
|
||||
total_estimates=Sum(Cast("estimate_point__value", FloatField()))
|
||||
)
|
||||
.annotate(total_estimates=Sum(Cast("estimate_point__value", FloatField())))
|
||||
.annotate(
|
||||
completed_estimates=Sum(
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
@ -875,19 +827,13 @@ class TransferCycleIssueEndpoint(BaseAPIView):
|
||||
),
|
||||
),
|
||||
# If `avatar_asset` is None, fall back to using `avatar` field directly
|
||||
When(
|
||||
assignees__avatar_asset__isnull=True, then="assignees__avatar"
|
||||
),
|
||||
When(assignees__avatar_asset__isnull=True, then="assignees__avatar"),
|
||||
default=Value(None),
|
||||
output_field=models.CharField(),
|
||||
)
|
||||
)
|
||||
.values("display_name", "assignee_id", "avatar_url")
|
||||
.annotate(
|
||||
total_issues=Count(
|
||||
"id", filter=Q(archived_at__isnull=True, is_draft=False)
|
||||
)
|
||||
)
|
||||
.annotate(total_issues=Count("id", filter=Q(archived_at__isnull=True, is_draft=False)))
|
||||
.annotate(
|
||||
completed_issues=Count(
|
||||
"id",
|
||||
@ -914,9 +860,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
|
||||
assignee_distribution_data = [
|
||||
{
|
||||
"display_name": item["display_name"],
|
||||
"assignee_id": (
|
||||
str(item["assignee_id"]) if item["assignee_id"] else None
|
||||
),
|
||||
"assignee_id": (str(item["assignee_id"]) if item["assignee_id"] else None),
|
||||
"avatar": item.get("avatar"),
|
||||
"avatar_url": item.get("avatar_url"),
|
||||
"total_issues": item["total_issues"],
|
||||
@ -938,11 +882,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
|
||||
.annotate(color=F("labels__color"))
|
||||
.annotate(label_id=F("labels__id"))
|
||||
.values("label_name", "color", "label_id")
|
||||
.annotate(
|
||||
total_issues=Count(
|
||||
"id", filter=Q(archived_at__isnull=True, is_draft=False)
|
||||
)
|
||||
)
|
||||
.annotate(total_issues=Count("id", filter=Q(archived_at__isnull=True, is_draft=False)))
|
||||
.annotate(
|
||||
completed_issues=Count(
|
||||
"id",
|
||||
@ -988,9 +928,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
|
||||
cycle_id=cycle_id,
|
||||
)
|
||||
|
||||
current_cycle = Cycle.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id, pk=cycle_id
|
||||
).first()
|
||||
current_cycle = Cycle.objects.filter(workspace__slug=slug, project_id=project_id, pk=cycle_id).first()
|
||||
|
||||
current_cycle.progress_snapshot = {
|
||||
"total_issues": old_cycle.total_issues,
|
||||
@ -1018,9 +956,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
|
||||
|
||||
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"
|
||||
},
|
||||
{"error": "The cycle where the issues are transferred is already completed"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@ -1044,9 +980,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
|
||||
}
|
||||
)
|
||||
|
||||
cycle_issues = CycleIssue.objects.bulk_update(
|
||||
updated_cycles, ["cycle_id"], batch_size=100
|
||||
)
|
||||
cycle_issues = CycleIssue.objects.bulk_update(updated_cycles, ["cycle_id"], batch_size=100)
|
||||
|
||||
# Capture Issue Activity
|
||||
issue_activity.delay(
|
||||
@ -1080,12 +1014,8 @@ class CycleUserPropertiesEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
cycle_properties.filters = request.data.get("filters", cycle_properties.filters)
|
||||
cycle_properties.rich_filters = request.data.get(
|
||||
"rich_filters", cycle_properties.rich_filters
|
||||
)
|
||||
cycle_properties.display_filters = request.data.get(
|
||||
"display_filters", cycle_properties.display_filters
|
||||
)
|
||||
cycle_properties.rich_filters = request.data.get("rich_filters", cycle_properties.rich_filters)
|
||||
cycle_properties.display_filters = request.data.get("display_filters", cycle_properties.display_filters)
|
||||
cycle_properties.display_properties = request.data.get(
|
||||
"display_properties", cycle_properties.display_properties
|
||||
)
|
||||
@ -1109,13 +1039,9 @@ class CycleUserPropertiesEndpoint(BaseAPIView):
|
||||
class CycleProgressEndpoint(BaseAPIView):
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def get(self, request, slug, project_id, cycle_id):
|
||||
cycle = Cycle.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id, id=cycle_id
|
||||
).first()
|
||||
cycle = Cycle.objects.filter(workspace__slug=slug, project_id=project_id, id=cycle_id).first()
|
||||
if not cycle:
|
||||
return Response(
|
||||
{"error": "Cycle not found"}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
return Response({"error": "Cycle not found"}, status=status.HTTP_404_NOT_FOUND)
|
||||
aggregate_estimates = (
|
||||
Issue.issue_objects.filter(
|
||||
estimate_point__estimate__type="points",
|
||||
@ -1161,9 +1087,7 @@ class CycleProgressEndpoint(BaseAPIView):
|
||||
output_field=FloatField(),
|
||||
)
|
||||
),
|
||||
total_estimate_points=Sum(
|
||||
"value_as_float", default=Value(0), output_field=FloatField()
|
||||
),
|
||||
total_estimate_points=Sum("value_as_float", default=Value(0), output_field=FloatField()),
|
||||
)
|
||||
)
|
||||
if cycle.progress_snapshot:
|
||||
@ -1223,22 +1147,11 @@ class CycleProgressEndpoint(BaseAPIView):
|
||||
|
||||
return Response(
|
||||
{
|
||||
"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"]
|
||||
or 0,
|
||||
"cancelled_estimate_points": aggregate_estimates[
|
||||
"cancelled_estimate_point"
|
||||
]
|
||||
or 0,
|
||||
"completed_estimate_points": aggregate_estimates[
|
||||
"completed_estimate_points"
|
||||
]
|
||||
or 0,
|
||||
"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"] or 0,
|
||||
"cancelled_estimate_points": aggregate_estimates["cancelled_estimate_point"] or 0,
|
||||
"completed_estimate_points": aggregate_estimates["completed_estimate_points"] or 0,
|
||||
"total_estimate_points": aggregate_estimates["total_estimate_points"],
|
||||
"backlog_issues": backlog_issues,
|
||||
"total_issues": total_issues,
|
||||
@ -1256,9 +1169,7 @@ class CycleAnalyticsEndpoint(BaseAPIView):
|
||||
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
|
||||
)
|
||||
Cycle.objects.filter(workspace__slug=slug, project_id=project_id, id=cycle_id)
|
||||
.annotate(
|
||||
total_issues=Count(
|
||||
"issue_cycle__issue__id",
|
||||
@ -1341,9 +1252,7 @@ class CycleAnalyticsEndpoint(BaseAPIView):
|
||||
)
|
||||
)
|
||||
.values("display_name", "assignee_id", "avatar_url")
|
||||
.annotate(
|
||||
total_estimates=Sum(Cast("estimate_point__value", FloatField()))
|
||||
)
|
||||
.annotate(total_estimates=Sum(Cast("estimate_point__value", FloatField())))
|
||||
.annotate(
|
||||
completed_estimates=Sum(
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
@ -1378,9 +1287,7 @@ class CycleAnalyticsEndpoint(BaseAPIView):
|
||||
.annotate(color=F("labels__color"))
|
||||
.annotate(label_id=F("labels__id"))
|
||||
.values("label_name", "color", "label_id")
|
||||
.annotate(
|
||||
total_estimates=Sum(Cast("estimate_point__value", FloatField()))
|
||||
)
|
||||
.annotate(total_estimates=Sum(Cast("estimate_point__value", FloatField())))
|
||||
.annotate(
|
||||
completed_estimates=Sum(
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
@ -1482,11 +1389,7 @@ class CycleAnalyticsEndpoint(BaseAPIView):
|
||||
.annotate(color=F("labels__color"))
|
||||
.annotate(label_id=F("labels__id"))
|
||||
.values("label_name", "color", "label_id")
|
||||
.annotate(
|
||||
total_issues=Count(
|
||||
"label_id", filter=Q(archived_at__isnull=True, is_draft=False)
|
||||
)
|
||||
)
|
||||
.annotate(total_issues=Count("label_id", filter=Q(archived_at__isnull=True, is_draft=False)))
|
||||
.annotate(
|
||||
completed_issues=Count(
|
||||
"label_id",
|
||||
|
||||
@ -74,9 +74,7 @@ class CycleIssueViewSet(BaseViewSet):
|
||||
return (
|
||||
issues.annotate(
|
||||
cycle_id=Subquery(
|
||||
CycleIssue.objects.filter(
|
||||
issue=OuterRef("id"), deleted_at__isnull=True
|
||||
).values("cycle_id")[:1]
|
||||
CycleIssue.objects.filter(issue=OuterRef("id"), deleted_at__isnull=True).values("cycle_id")[:1]
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
@ -100,9 +98,7 @@ class CycleIssueViewSet(BaseViewSet):
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.prefetch_related(
|
||||
"assignees", "labels", "issue_module__module", "issue_cycle__cycle"
|
||||
)
|
||||
.prefetch_related("assignees", "labels", "issue_module__module", "issue_cycle__cycle")
|
||||
)
|
||||
|
||||
@method_decorator(gzip_page)
|
||||
@ -110,9 +106,7 @@ class CycleIssueViewSet(BaseViewSet):
|
||||
def list(self, request, slug, project_id, cycle_id):
|
||||
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, issue_cycle__deleted_at__isnull=True)
|
||||
.filter(project_id=project_id)
|
||||
.filter(workspace__slug=slug)
|
||||
)
|
||||
@ -140,18 +134,14 @@ class CycleIssueViewSet(BaseViewSet):
|
||||
sub_group_by = request.GET.get("sub_group_by", False)
|
||||
|
||||
# issue queryset
|
||||
issue_queryset = issue_queryset_grouper(
|
||||
queryset=issue_queryset, group_by=group_by, sub_group_by=sub_group_by
|
||||
)
|
||||
issue_queryset = issue_queryset_grouper(queryset=issue_queryset, group_by=group_by, sub_group_by=sub_group_by)
|
||||
|
||||
if group_by:
|
||||
# Check group and sub group value paginate
|
||||
if sub_group_by:
|
||||
if group_by == sub_group_by:
|
||||
return Response(
|
||||
{
|
||||
"error": "Group by and sub group by cannot have same parameters"
|
||||
},
|
||||
{"error": "Group by and sub group by cannot have same parameters"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
else:
|
||||
@ -223,9 +213,7 @@ class CycleIssueViewSet(BaseViewSet):
|
||||
request=request,
|
||||
queryset=issue_queryset,
|
||||
total_count_queryset=total_issue_queryset,
|
||||
on_results=lambda issues: issue_on_results(
|
||||
group_by=group_by, issues=issues, sub_group_by=sub_group_by
|
||||
),
|
||||
on_results=lambda issues: issue_on_results(group_by=group_by, issues=issues, sub_group_by=sub_group_by),
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
@ -233,26 +221,18 @@ class CycleIssueViewSet(BaseViewSet):
|
||||
issues = request.data.get("issues", [])
|
||||
|
||||
if not issues:
|
||||
return Response(
|
||||
{"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
return Response({"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
cycle = Cycle.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=cycle_id
|
||||
)
|
||||
cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=cycle_id)
|
||||
|
||||
if cycle.end_date is not None and cycle.end_date < timezone.now():
|
||||
return Response(
|
||||
{
|
||||
"error": "The Cycle has already been completed so no new issues can be added"
|
||||
},
|
||||
{"error": "The Cycle has already been completed so no new issues can be added"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Get all CycleIssues already created
|
||||
cycle_issues = list(
|
||||
CycleIssue.objects.filter(~Q(cycle_id=cycle_id), issue_id__in=issues)
|
||||
)
|
||||
cycle_issues = list(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]
|
||||
new_issues = list(set(issues) - set(existing_issues))
|
||||
|
||||
@ -303,9 +283,7 @@ class CycleIssueViewSet(BaseViewSet):
|
||||
current_instance=json.dumps(
|
||||
{
|
||||
"updated_cycle_issues": update_cycle_issue_activity,
|
||||
"created_cycle_issues": serializers.serialize(
|
||||
"json", created_records
|
||||
),
|
||||
"created_cycle_issues": serializers.serialize("json", created_records),
|
||||
}
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
|
||||
@ -56,9 +56,7 @@ class BulkEstimatePointEndpoint(BaseViewSet):
|
||||
serializer = EstimateReadSerializer(estimates, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@invalidate_cache(
|
||||
path="/api/workspaces/:slug/estimates/", url_params=True, user=False
|
||||
)
|
||||
@invalidate_cache(path="/api/workspaces/:slug/estimates/", url_params=True, user=False)
|
||||
def create(self, request, slug, project_id):
|
||||
estimate = request.data.get("estimate")
|
||||
estimate_name = estimate.get("name", generate_random_name())
|
||||
@ -73,9 +71,7 @@ class BulkEstimatePointEndpoint(BaseViewSet):
|
||||
|
||||
estimate_points = request.data.get("estimate_points", [])
|
||||
|
||||
serializer = EstimatePointSerializer(
|
||||
data=request.data.get("estimate_points"), many=True
|
||||
)
|
||||
serializer = EstimatePointSerializer(data=request.data.get("estimate_points"), many=True)
|
||||
if not serializer.is_valid():
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@ -101,15 +97,11 @@ class BulkEstimatePointEndpoint(BaseViewSet):
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
def retrieve(self, request, slug, project_id, estimate_id):
|
||||
estimate = Estimate.objects.get(
|
||||
pk=estimate_id, workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
estimate = Estimate.objects.get(pk=estimate_id, workspace__slug=slug, project_id=project_id)
|
||||
serializer = EstimateReadSerializer(estimate)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@invalidate_cache(
|
||||
path="/api/workspaces/:slug/estimates/", url_params=True, user=False
|
||||
)
|
||||
@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(
|
||||
@ -127,9 +119,7 @@ class BulkEstimatePointEndpoint(BaseViewSet):
|
||||
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
|
||||
],
|
||||
pk__in=[estimate_point.get("id") for estimate_point in estimate_points_data],
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
estimate_id=estimate_id,
|
||||
@ -138,34 +128,20 @@ class BulkEstimatePointEndpoint(BaseViewSet):
|
||||
updated_estimate_points = []
|
||||
for estimate_point in estimate_points:
|
||||
# Find the data for that estimate point
|
||||
estimate_point_data = [
|
||||
point
|
||||
for point in estimate_points_data
|
||||
if point.get("id") == str(estimate_point.id)
|
||||
]
|
||||
estimate_point_data = [point for point in estimate_points_data if point.get("id") == str(estimate_point.id)]
|
||||
if len(estimate_point_data):
|
||||
estimate_point.value = estimate_point_data[0].get(
|
||||
"value", estimate_point.value
|
||||
)
|
||||
estimate_point.key = estimate_point_data[0].get(
|
||||
"key", estimate_point.key
|
||||
)
|
||||
estimate_point.value = estimate_point_data[0].get("value", estimate_point.value)
|
||||
estimate_point.key = estimate_point_data[0].get("key", estimate_point.key)
|
||||
updated_estimate_points.append(estimate_point)
|
||||
|
||||
EstimatePoint.objects.bulk_update(
|
||||
updated_estimate_points, ["key", "value"], batch_size=10
|
||||
)
|
||||
EstimatePoint.objects.bulk_update(updated_estimate_points, ["key", "value"], batch_size=10)
|
||||
|
||||
estimate_serializer = EstimateReadSerializer(estimate)
|
||||
return Response(estimate_serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@invalidate_cache(
|
||||
path="/api/workspaces/:slug/estimates/", url_params=True, user=False
|
||||
)
|
||||
@invalidate_cache(path="/api/workspaces/:slug/estimates/", url_params=True, user=False)
|
||||
def destroy(self, request, slug, project_id, estimate_id):
|
||||
estimate = Estimate.objects.get(
|
||||
pk=estimate_id, workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
estimate = Estimate.objects.get(pk=estimate_id, workspace__slug=slug, project_id=project_id)
|
||||
estimate.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@ -196,9 +172,7 @@ class EstimatePointEndpoint(BaseViewSet):
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
)
|
||||
serializer = EstimatePointSerializer(
|
||||
estimate_point, data=request.data, partial=True
|
||||
)
|
||||
serializer = EstimatePointSerializer(estimate_point, data=request.data, partial=True)
|
||||
if not serializer.is_valid():
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
serializer.save()
|
||||
@ -220,24 +194,12 @@ class EstimatePointEndpoint(BaseViewSet):
|
||||
for issue in issues:
|
||||
issue_activity.delay(
|
||||
type="issue.activity.updated",
|
||||
requested_data=json.dumps(
|
||||
{
|
||||
"estimate_point": (
|
||||
str(new_estimate_id) if new_estimate_id else None
|
||||
)
|
||||
}
|
||||
),
|
||||
requested_data=json.dumps({"estimate_point": (str(new_estimate_id) if new_estimate_id else None)}),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=issue.id,
|
||||
project_id=str(project_id),
|
||||
current_instance=json.dumps(
|
||||
{
|
||||
"estimate_point": (
|
||||
str(issue.estimate_point_id)
|
||||
if issue.estimate_point_id
|
||||
else None
|
||||
)
|
||||
}
|
||||
{"estimate_point": (str(issue.estimate_point_id) if issue.estimate_point_id else None)}
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
)
|
||||
@ -256,13 +218,7 @@ class EstimatePointEndpoint(BaseViewSet):
|
||||
issue_id=issue.id,
|
||||
project_id=str(project_id),
|
||||
current_instance=json.dumps(
|
||||
{
|
||||
"estimate_point": (
|
||||
str(issue.estimate_point_id)
|
||||
if issue.estimate_point_id
|
||||
else None
|
||||
)
|
||||
}
|
||||
{"estimate_point": (str(issue.estimate_point_id) if issue.estimate_point_id else None)}
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
)
|
||||
@ -277,9 +233,7 @@ class EstimatePointEndpoint(BaseViewSet):
|
||||
estimate_point.key -= 1
|
||||
updated_estimate_points.append(estimate_point)
|
||||
|
||||
EstimatePoint.objects.bulk_update(
|
||||
updated_estimate_points, ["key"], batch_size=10
|
||||
)
|
||||
EstimatePoint.objects.bulk_update(updated_estimate_points, ["key"], batch_size=10)
|
||||
|
||||
old_estimate_point.delete()
|
||||
|
||||
|
||||
@ -62,18 +62,16 @@ class ExportIssuesEndpoint(BaseAPIView):
|
||||
|
||||
@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"
|
||||
).select_related("workspace", "initiated_by")
|
||||
exporter_history = ExporterHistory.objects.filter(workspace__slug=slug, type="issue_exports").select_related(
|
||||
"workspace", "initiated_by"
|
||||
)
|
||||
|
||||
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,
|
||||
queryset=exporter_history,
|
||||
on_results=lambda exporter_history: ExporterHistorySerializer(
|
||||
exporter_history, many=True
|
||||
).data,
|
||||
on_results=lambda exporter_history: ExporterHistorySerializer(exporter_history, many=True).data,
|
||||
)
|
||||
else:
|
||||
return Response(
|
||||
|
||||
23
apps/api/plane/app/views/external/base.py
vendored
23
apps/api/plane/app/views/external/base.py
vendored
@ -108,8 +108,7 @@ def get_llm_config() -> Tuple[str | None, str | None, str | None]:
|
||||
if model not in provider.models:
|
||||
log_exception(
|
||||
ValueError(
|
||||
f"Model {model} not supported by {provider.name}. "
|
||||
f"Supported models: {', '.join(provider.models)}"
|
||||
f"Model {model} not supported by {provider.name}. Supported models: {', '.join(provider.models)}"
|
||||
)
|
||||
)
|
||||
return None, None, None
|
||||
@ -117,9 +116,7 @@ def get_llm_config() -> Tuple[str | None, str | None, str | None]:
|
||||
return api_key, model, provider_key
|
||||
|
||||
|
||||
def get_llm_response(
|
||||
task, prompt, api_key: str, model: str, provider: str
|
||||
) -> Tuple[str | None, str | None]:
|
||||
def get_llm_response(task, prompt, api_key: str, model: str, provider: str) -> Tuple[str | None, str | None]:
|
||||
"""Helper to get LLM completion response"""
|
||||
final_text = task + "\n" + prompt
|
||||
try:
|
||||
@ -157,13 +154,9 @@ class GPTIntegrationEndpoint(BaseAPIView):
|
||||
|
||||
task = request.data.get("task", False)
|
||||
if not task:
|
||||
return Response(
|
||||
{"error": "Task is required"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
return Response({"error": "Task is required"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
text, error = get_llm_response(
|
||||
task, request.data.get("prompt", False), api_key, model, provider
|
||||
)
|
||||
text, error = get_llm_response(task, request.data.get("prompt", False), api_key, model, provider)
|
||||
if not text and error:
|
||||
return Response(
|
||||
{"error": "An internal error has occurred."},
|
||||
@ -197,13 +190,9 @@ class WorkspaceGPTIntegrationEndpoint(BaseAPIView):
|
||||
|
||||
task = request.data.get("task", False)
|
||||
if not task:
|
||||
return Response(
|
||||
{"error": "Task is required"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
return Response({"error": "Task is required"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
text, error = get_llm_response(
|
||||
task, request.data.get("prompt", False), api_key, model, provider
|
||||
)
|
||||
text, error = get_llm_response(task, request.data.get("prompt", False), api_key, model, provider)
|
||||
if not text and error:
|
||||
return Response(
|
||||
{"error": "An internal error has occurred."},
|
||||
|
||||
@ -60,11 +60,7 @@ class IntakeViewSet(BaseViewSet):
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
)
|
||||
.annotate(
|
||||
pending_issue_count=Count(
|
||||
"issue_intake", filter=Q(issue_intake__status=-2)
|
||||
)
|
||||
)
|
||||
.annotate(pending_issue_count=Count("issue_intake", filter=Q(issue_intake__status=-2)))
|
||||
.select_related("workspace", "project")
|
||||
)
|
||||
|
||||
@ -79,9 +75,7 @@ class IntakeViewSet(BaseViewSet):
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def destroy(self, request, slug, project_id, pk):
|
||||
intake = Intake.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
).first()
|
||||
intake = Intake.objects.filter(workspace__slug=slug, project_id=project_id, pk=pk).first()
|
||||
# Handle default intake delete
|
||||
if intake.is_default:
|
||||
return Response(
|
||||
@ -109,16 +103,12 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_intake",
|
||||
queryset=IntakeIssue.objects.only(
|
||||
"status", "duplicate_to", "snoozed_till", "source"
|
||||
),
|
||||
queryset=IntakeIssue.objects.only("status", "duplicate_to", "snoozed_till", "source"),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
cycle_id=Subquery(
|
||||
CycleIssue.objects.filter(
|
||||
issue=OuterRef("id"), deleted_at__isnull=True
|
||||
).values("cycle_id")[:1]
|
||||
CycleIssue.objects.filter(issue=OuterRef("id"), deleted_at__isnull=True).values("cycle_id")[:1]
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
@ -147,10 +137,7 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(labels__id__isnull=True)
|
||||
& Q(label_issue__deleted_at__isnull=True)
|
||||
),
|
||||
filter=Q(~Q(labels__id__isnull=True) & Q(label_issue__deleted_at__isnull=True)),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@ -183,20 +170,14 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def list(self, request, slug, project_id):
|
||||
intake = Intake.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
).first()
|
||||
intake = Intake.objects.filter(workspace__slug=slug, project_id=project_id).first()
|
||||
if not intake:
|
||||
return Response(
|
||||
{"error": "Intake not found"}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
return Response({"error": "Intake not found"}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
project = Project.objects.get(pk=project_id)
|
||||
filters = issue_filters(request.GET, "GET", "issue__")
|
||||
intake_issue = (
|
||||
IntakeIssue.objects.filter(
|
||||
intake_id=intake.id, project_id=project_id, **filters
|
||||
)
|
||||
IntakeIssue.objects.filter(intake_id=intake.id, project_id=project_id, **filters)
|
||||
.select_related("issue")
|
||||
.prefetch_related("issue__labels")
|
||||
.annotate(
|
||||
@ -204,21 +185,14 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"issue__labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(issue__labels__id__isnull=True)
|
||||
& Q(issue__label_issue__deleted_at__isnull=True)
|
||||
),
|
||||
filter=Q(~Q(issue__labels__id__isnull=True) & Q(issue__label_issue__deleted_at__isnull=True)),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
)
|
||||
)
|
||||
).order_by(request.GET.get("order_by", "-issue__created_at"))
|
||||
# Intake status filter
|
||||
intake_status = [
|
||||
item
|
||||
for item in request.GET.get("status", "-2").split(",")
|
||||
if item != "null"
|
||||
]
|
||||
intake_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)
|
||||
|
||||
@ -236,17 +210,13 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(intake_issue),
|
||||
on_results=lambda intake_issues: IntakeIssueSerializer(
|
||||
intake_issues, many=True
|
||||
).data,
|
||||
on_results=lambda intake_issues: IntakeIssueSerializer(intake_issues, many=True).data,
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
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
|
||||
)
|
||||
return Response({"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Check for valid priority
|
||||
if request.data.get("issue", {}).get("priority", "none") not in [
|
||||
@ -256,9 +226,7 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
"urgent",
|
||||
"none",
|
||||
]:
|
||||
return Response(
|
||||
{"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
return Response({"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# create an issue
|
||||
project = Project.objects.get(pk=project_id)
|
||||
@ -272,9 +240,7 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
intake_id = Intake.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
).first()
|
||||
intake_id = Intake.objects.filter(workspace__slug=slug, project_id=project_id).first()
|
||||
# create an intake issue
|
||||
intake_issue = IntakeIssue.objects.create(
|
||||
intake_id=intake_id.id,
|
||||
@ -311,8 +277,7 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
"issue__labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(issue__labels__id__isnull=True)
|
||||
& Q(issue__label_issue__deleted_at__isnull=True)
|
||||
~Q(issue__labels__id__isnull=True) & Q(issue__label_issue__deleted_at__isnull=True)
|
||||
),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
@ -340,9 +305,7 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN], creator=True, model=Issue)
|
||||
def partial_update(self, request, slug, project_id, pk):
|
||||
intake_id = Intake.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
).first()
|
||||
intake_id = Intake.objects.filter(workspace__slug=slug, project_id=project_id).first()
|
||||
intake_issue = IntakeIssue.objects.get(
|
||||
issue_id=pk,
|
||||
workspace__slug=slug,
|
||||
@ -371,10 +334,9 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
# Only project members admins and created_by users can access this endpoint
|
||||
if (
|
||||
(project_member and project_member.role <= ROLE.GUEST.value)
|
||||
and not is_workspace_admin
|
||||
) and str(intake_issue.created_by_id) != str(request.user.id):
|
||||
if ((project_member and project_member.role <= ROLE.GUEST.value) and not is_workspace_admin) and str(
|
||||
intake_issue.created_by_id
|
||||
) != str(request.user.id):
|
||||
return Response(
|
||||
{"error": "You cannot edit intake issues"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
@ -388,10 +350,7 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(labels__id__isnull=True)
|
||||
& Q(label_issue__deleted_at__isnull=True)
|
||||
),
|
||||
filter=Q(~Q(labels__id__isnull=True) & Q(label_issue__deleted_at__isnull=True)),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@ -399,10 +358,7 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"assignees__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(assignees__id__isnull=True)
|
||||
& Q(issue_assignee__deleted_at__isnull=True)
|
||||
),
|
||||
filter=Q(~Q(assignees__id__isnull=True) & Q(issue_assignee__deleted_at__isnull=True)),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@ -411,15 +367,11 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
if project_member and project_member.role <= ROLE.GUEST.value:
|
||||
issue_data = {
|
||||
"name": issue_data.get("name", issue.name),
|
||||
"description_html": issue_data.get(
|
||||
"description_html", issue.description_html
|
||||
),
|
||||
"description_html": issue_data.get("description_html", issue.description_html),
|
||||
"description": issue_data.get("description", issue.description),
|
||||
}
|
||||
|
||||
current_instance = json.dumps(
|
||||
IssueDetailSerializer(issue).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
current_instance = json.dumps(IssueDetailSerializer(issue).data, cls=DjangoJSONEncoder)
|
||||
|
||||
issue_serializer = IssueCreateSerializer(
|
||||
issue, data=issue_data, partial=True, context={"project_id": project_id}
|
||||
@ -449,20 +401,12 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
)
|
||||
issue_serializer.save()
|
||||
else:
|
||||
return Response(
|
||||
issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
return Response(issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Only project admins can edit intake issue attributes
|
||||
if (
|
||||
project_member and project_member.role > ROLE.MEMBER.value
|
||||
) or is_workspace_admin:
|
||||
serializer = IntakeIssueSerializer(
|
||||
intake_issue, data=request.data, partial=True
|
||||
)
|
||||
current_instance = json.dumps(
|
||||
IntakeIssueSerializer(intake_issue).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
if (project_member and project_member.role > ROLE.MEMBER.value) or is_workspace_admin:
|
||||
serializer = IntakeIssueSerializer(intake_issue, data=request.data, partial=True)
|
||||
current_instance = json.dumps(IntakeIssueSerializer(intake_issue).data, cls=DjangoJSONEncoder)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
# Update the issue state if the issue is rejected or marked as duplicate
|
||||
@ -472,9 +416,7 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
)
|
||||
state = State.objects.filter(
|
||||
group="cancelled", workspace__slug=slug, project_id=project_id
|
||||
).first()
|
||||
state = State.objects.filter(group="cancelled", workspace__slug=slug, project_id=project_id).first()
|
||||
if state is not None:
|
||||
issue.state = state
|
||||
issue.save()
|
||||
@ -490,9 +432,7 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
# 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
|
||||
).first()
|
||||
state = State.objects.filter(workspace__slug=slug, project_id=project_id, default=True).first()
|
||||
if state is not None:
|
||||
issue.state = state
|
||||
issue.save()
|
||||
@ -519,8 +459,7 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
"issue__labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(issue__labels__id__isnull=True)
|
||||
& Q(issue__label_issue__deleted_at__isnull=True)
|
||||
~Q(issue__labels__id__isnull=True) & Q(issue__label_issue__deleted_at__isnull=True)
|
||||
),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
@ -546,13 +485,9 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
serializer = IntakeIssueDetailSerializer(intake_issue).data
|
||||
return Response(serializer, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], creator=True, model=Issue
|
||||
)
|
||||
@allow_permission(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(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
).first()
|
||||
intake_id = Intake.objects.filter(workspace__slug=slug, project_id=project_id).first()
|
||||
project = Project.objects.get(pk=project_id)
|
||||
intake_issue = (
|
||||
IntakeIssue.objects.select_related("issue")
|
||||
@ -562,10 +497,7 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"issue__labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(issue__labels__id__isnull=True)
|
||||
& Q(issue__label_issue__deleted_at__isnull=True)
|
||||
),
|
||||
filter=Q(~Q(issue__labels__id__isnull=True) & Q(issue__label_issue__deleted_at__isnull=True)),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@ -574,8 +506,7 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
"issue__assignees__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(issue__assignees__id__isnull=True)
|
||||
& Q(issue__issue_assignee__deleted_at__isnull=True)
|
||||
~Q(issue__assignees__id__isnull=True) & Q(issue__issue_assignee__deleted_at__isnull=True)
|
||||
),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
@ -603,9 +534,7 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN], creator=True, model=Issue)
|
||||
def destroy(self, request, slug, project_id, pk):
|
||||
intake_id = Intake.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
).first()
|
||||
intake_id = Intake.objects.filter(workspace__slug=slug, project_id=project_id).first()
|
||||
intake_issue = IntakeIssue.objects.get(
|
||||
issue_id=pk,
|
||||
workspace__slug=slug,
|
||||
@ -616,9 +545,7 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
# Check the issue status
|
||||
if intake_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 = Issue.objects.filter(workspace__slug=slug, project_id=project_id, pk=pk).first()
|
||||
issue.delete()
|
||||
|
||||
intake_issue.delete()
|
||||
@ -630,18 +557,14 @@ class IntakeWorkItemDescriptionVersionEndpoint(BaseAPIView):
|
||||
paginated_data = results.values(*fields)
|
||||
|
||||
datetime_fields = ["created_at", "updated_at"]
|
||||
paginated_data = user_timezone_converter(
|
||||
paginated_data, datetime_fields, timezone
|
||||
)
|
||||
paginated_data = user_timezone_converter(paginated_data, datetime_fields, timezone)
|
||||
|
||||
return paginated_data
|
||||
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def get(self, request, slug, project_id, work_item_id, pk=None):
|
||||
project = Project.objects.get(pk=project_id)
|
||||
issue = Issue.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=work_item_id
|
||||
)
|
||||
issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=work_item_id)
|
||||
|
||||
if (
|
||||
ProjectMember.objects.filter(
|
||||
@ -667,9 +590,7 @@ class IntakeWorkItemDescriptionVersionEndpoint(BaseAPIView):
|
||||
pk=pk,
|
||||
)
|
||||
|
||||
serializer = IssueDescriptionVersionDetailSerializer(
|
||||
issue_description_version
|
||||
)
|
||||
serializer = IssueDescriptionVersionDetailSerializer(issue_description_version)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
cursor = request.GET.get("cursor", None)
|
||||
|
||||
@ -63,9 +63,7 @@ class IssueActivityEndpoint(BaseAPIView):
|
||||
issue_activities = issue_activities.prefetch_related(
|
||||
Prefetch(
|
||||
"issue__issue_intake",
|
||||
queryset=IntakeIssue.objects.only(
|
||||
"source_email", "source", "extra"
|
||||
),
|
||||
queryset=IntakeIssue.objects.only("source_email", "source", "extra"),
|
||||
to_attr="source_data",
|
||||
)
|
||||
)
|
||||
|
||||
@ -4,7 +4,7 @@ import json
|
||||
|
||||
# Django imports
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.db.models import F, Func, OuterRef, Q, Prefetch, Exists, Subquery, Count
|
||||
from django.db.models import OuterRef, Q, Prefetch, Exists, Subquery, Count
|
||||
from django.utils import timezone
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.gzip import gzip_page
|
||||
@ -57,9 +57,7 @@ class IssueArchiveViewSet(BaseViewSet):
|
||||
return (
|
||||
issues.annotate(
|
||||
cycle_id=Subquery(
|
||||
CycleIssue.objects.filter(
|
||||
issue=OuterRef("id"), deleted_at__isnull=True
|
||||
).values("cycle_id")[:1]
|
||||
CycleIssue.objects.filter(issue=OuterRef("id"), deleted_at__isnull=True).values("cycle_id")[:1]
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
@ -110,11 +108,7 @@ class IssueArchiveViewSet(BaseViewSet):
|
||||
|
||||
issue_queryset = self.get_queryset()
|
||||
|
||||
issue_queryset = (
|
||||
issue_queryset
|
||||
if show_sub_issues == "true"
|
||||
else issue_queryset.filter(parent__isnull=True)
|
||||
)
|
||||
issue_queryset = issue_queryset if show_sub_issues == "true" else issue_queryset.filter(parent__isnull=True)
|
||||
# Apply filtering from filterset
|
||||
issue_queryset = self.filter_queryset(issue_queryset)
|
||||
|
||||
@ -137,18 +131,14 @@ class IssueArchiveViewSet(BaseViewSet):
|
||||
sub_group_by = request.GET.get("sub_group_by", False)
|
||||
|
||||
# issue queryset
|
||||
issue_queryset = issue_queryset_grouper(
|
||||
queryset=issue_queryset, group_by=group_by, sub_group_by=sub_group_by
|
||||
)
|
||||
issue_queryset = issue_queryset_grouper(queryset=issue_queryset, group_by=group_by, sub_group_by=sub_group_by)
|
||||
|
||||
if group_by:
|
||||
# Check group and sub group value paginate
|
||||
if sub_group_by:
|
||||
if group_by == sub_group_by:
|
||||
return Response(
|
||||
{
|
||||
"error": "Group by and sub group by cannot have same parameters"
|
||||
},
|
||||
{"error": "Group by and sub group by cannot have same parameters"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
else:
|
||||
@ -220,9 +210,7 @@ class IssueArchiveViewSet(BaseViewSet):
|
||||
request=request,
|
||||
queryset=issue_queryset,
|
||||
total_count_queryset=total_issue_queryset,
|
||||
on_results=lambda issues: issue_on_results(
|
||||
group_by=group_by, issues=issues, sub_group_by=sub_group_by
|
||||
),
|
||||
on_results=lambda issues: issue_on_results(group_by=group_by, issues=issues, sub_group_by=sub_group_by),
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
@ -263,9 +251,7 @@ 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
|
||||
)
|
||||
issue = Issue.issue_objects.get(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"},
|
||||
@ -273,15 +259,11 @@ class IssueArchiveViewSet(BaseViewSet):
|
||||
)
|
||||
issue_activity.delay(
|
||||
type="issue.activity.updated",
|
||||
requested_data=json.dumps(
|
||||
{"archived_at": str(timezone.now().date()), "automation": False}
|
||||
),
|
||||
requested_data=json.dumps({"archived_at": str(timezone.now().date()), "automation": False}),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(issue.id),
|
||||
project_id=str(project_id),
|
||||
current_instance=json.dumps(
|
||||
IssueSerializer(issue).data, cls=DjangoJSONEncoder
|
||||
),
|
||||
current_instance=json.dumps(IssueSerializer(issue).data, cls=DjangoJSONEncoder),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
@ -289,9 +271,7 @@ class IssueArchiveViewSet(BaseViewSet):
|
||||
issue.archived_at = timezone.now().date()
|
||||
issue.save()
|
||||
|
||||
return Response(
|
||||
{"archived_at": str(issue.archived_at)}, status=status.HTTP_200_OK
|
||||
)
|
||||
return Response({"archived_at": str(issue.archived_at)}, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def unarchive(self, request, slug, project_id, pk=None):
|
||||
@ -307,9 +287,7 @@ class IssueArchiveViewSet(BaseViewSet):
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(issue.id),
|
||||
project_id=str(project_id),
|
||||
current_instance=json.dumps(
|
||||
IssueSerializer(issue).data, cls=DjangoJSONEncoder
|
||||
),
|
||||
current_instance=json.dumps(IssueSerializer(issue).data, cls=DjangoJSONEncoder),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
@ -328,13 +306,11 @@ class BulkArchiveIssuesEndpoint(BaseAPIView):
|
||||
issue_ids = request.data.get("issue_ids", [])
|
||||
|
||||
if not len(issue_ids):
|
||||
return Response(
|
||||
{"error": "Issue IDs are required"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
return Response({"error": "Issue IDs are required"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
issues = Issue.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id, pk__in=issue_ids
|
||||
).select_related("state")
|
||||
issues = Issue.objects.filter(workspace__slug=slug, project_id=project_id, pk__in=issue_ids).select_related(
|
||||
"state"
|
||||
)
|
||||
bulk_archive_issues = []
|
||||
for issue in issues:
|
||||
if issue.state.group not in ["completed", "cancelled"]:
|
||||
@ -347,15 +323,11 @@ class BulkArchiveIssuesEndpoint(BaseAPIView):
|
||||
)
|
||||
issue_activity.delay(
|
||||
type="issue.activity.updated",
|
||||
requested_data=json.dumps(
|
||||
{"archived_at": str(timezone.now().date()), "automation": False}
|
||||
),
|
||||
requested_data=json.dumps({"archived_at": str(timezone.now().date()), "automation": False}),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(issue.id),
|
||||
project_id=str(project_id),
|
||||
current_instance=json.dumps(
|
||||
IssueSerializer(issue).data, cls=DjangoJSONEncoder
|
||||
),
|
||||
current_instance=json.dumps(IssueSerializer(issue).data, cls=DjangoJSONEncoder),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
@ -364,6 +336,4 @@ class BulkArchiveIssuesEndpoint(BaseAPIView):
|
||||
bulk_archive_issues.append(issue)
|
||||
Issue.objects.bulk_update(bulk_archive_issues, ["archived_at"])
|
||||
|
||||
return Response(
|
||||
{"archived_at": str(timezone.now().date())}, status=status.HTTP_200_OK
|
||||
)
|
||||
return Response({"archived_at": str(timezone.now().date())}, status=status.HTTP_200_OK)
|
||||
|
||||
@ -75,9 +75,7 @@ class IssueAttachmentEndpoint(BaseAPIView):
|
||||
|
||||
@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
|
||||
)
|
||||
issue_attachments = FileAsset.objects.filter(issue_id=issue_id, workspace__slug=slug, project_id=project_id)
|
||||
serializer = IssueAttachmentSerializer(issue_attachments, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@ -123,9 +121,7 @@ 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
|
||||
)
|
||||
presigned_url = storage.generate_presigned_post(object_name=asset_key, file_type=type, file_size=size_limit)
|
||||
|
||||
# Return the presigned URL
|
||||
return Response(
|
||||
@ -140,9 +136,7 @@ class IssueAttachmentV2Endpoint(BaseAPIView):
|
||||
|
||||
@allow_permission([ROLE.ADMIN], creator=True, model=FileAsset)
|
||||
def delete(self, request, slug, project_id, issue_id, pk):
|
||||
issue_attachment = FileAsset.objects.get(
|
||||
pk=pk, workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
issue_attachment = FileAsset.objects.get(pk=pk, workspace__slug=slug, project_id=project_id)
|
||||
issue_attachment.is_deleted = True
|
||||
issue_attachment.deleted_at = timezone.now()
|
||||
issue_attachment.save()
|
||||
@ -165,9 +159,7 @@ class IssueAttachmentV2Endpoint(BaseAPIView):
|
||||
def get(self, request, slug, project_id, issue_id, pk=None):
|
||||
if pk:
|
||||
# Get the asset
|
||||
asset = FileAsset.objects.get(
|
||||
id=pk, workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
asset = FileAsset.objects.get(id=pk, workspace__slug=slug, project_id=project_id)
|
||||
|
||||
# Check if the asset is uploaded
|
||||
if not asset.is_uploaded:
|
||||
@ -198,9 +190,7 @@ class IssueAttachmentV2Endpoint(BaseAPIView):
|
||||
|
||||
@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
|
||||
)
|
||||
issue_attachment = FileAsset.objects.get(pk=pk, workspace__slug=slug, project_id=project_id)
|
||||
serializer = IssueAttachmentSerializer(issue_attachment)
|
||||
|
||||
# Send this activity only if the attachment is not uploaded before
|
||||
|
||||
@ -82,16 +82,12 @@ class IssueListEndpoint(BaseAPIView):
|
||||
issue_ids = request.GET.get("issues", False)
|
||||
|
||||
if not issue_ids:
|
||||
return Response(
|
||||
{"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
return Response({"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
issue_ids = [issue_id for issue_id in issue_ids.split(",") if issue_id != ""]
|
||||
|
||||
# Base queryset with basic filters
|
||||
queryset = Issue.issue_objects.filter(
|
||||
workspace__slug=slug, project_id=project_id, pk__in=issue_ids
|
||||
)
|
||||
queryset = Issue.issue_objects.filter(workspace__slug=slug, project_id=project_id, pk__in=issue_ids)
|
||||
|
||||
# Apply filtering from filterset
|
||||
queryset = self.filter_queryset(queryset)
|
||||
@ -102,17 +98,15 @@ class IssueListEndpoint(BaseAPIView):
|
||||
|
||||
# Add select_related, prefetch_related if fields or expand is not None
|
||||
if self.fields or self.expand:
|
||||
issue_queryset = issue_queryset.select_related(
|
||||
"workspace", "project", "state", "parent"
|
||||
).prefetch_related("assignees", "labels", "issue_module__module")
|
||||
issue_queryset = issue_queryset.select_related("workspace", "project", "state", "parent").prefetch_related(
|
||||
"assignees", "labels", "issue_module__module"
|
||||
)
|
||||
|
||||
# Add annotations
|
||||
issue_queryset = (
|
||||
issue_queryset.annotate(
|
||||
cycle_id=Subquery(
|
||||
CycleIssue.objects.filter(
|
||||
issue=OuterRef("id"), deleted_at__isnull=True
|
||||
).values("cycle_id")[:1]
|
||||
CycleIssue.objects.filter(issue=OuterRef("id"), deleted_at__isnull=True).values("cycle_id")[:1]
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
@ -141,18 +135,14 @@ class IssueListEndpoint(BaseAPIView):
|
||||
|
||||
order_by_param = request.GET.get("order_by", "-created_at")
|
||||
# Issue queryset
|
||||
issue_queryset, _ = order_issue_queryset(
|
||||
issue_queryset=issue_queryset, order_by_param=order_by_param
|
||||
)
|
||||
issue_queryset, _ = order_issue_queryset(issue_queryset=issue_queryset, order_by_param=order_by_param)
|
||||
|
||||
# Group by
|
||||
group_by = request.GET.get("group_by", False)
|
||||
sub_group_by = request.GET.get("sub_group_by", False)
|
||||
|
||||
# issue queryset
|
||||
issue_queryset = issue_queryset_grouper(
|
||||
queryset=issue_queryset, group_by=group_by, sub_group_by=sub_group_by
|
||||
)
|
||||
issue_queryset = issue_queryset_grouper(queryset=issue_queryset, group_by=group_by, sub_group_by=sub_group_by)
|
||||
|
||||
recent_visited_task.delay(
|
||||
slug=slug,
|
||||
@ -163,9 +153,7 @@ class IssueListEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
if self.fields or self.expand:
|
||||
issues = IssueSerializer(
|
||||
queryset, many=True, fields=self.fields, expand=self.expand
|
||||
).data
|
||||
issues = IssueSerializer(queryset, many=True, fields=self.fields, expand=self.expand).data
|
||||
else:
|
||||
issues = issue_queryset.values(
|
||||
"id",
|
||||
@ -196,9 +184,7 @@ class IssueListEndpoint(BaseAPIView):
|
||||
"deleted_at",
|
||||
)
|
||||
datetime_fields = ["created_at", "updated_at"]
|
||||
issues = user_timezone_converter(
|
||||
issues, datetime_fields, request.user.user_timezone
|
||||
)
|
||||
issues = user_timezone_converter(issues, datetime_fields, request.user.user_timezone)
|
||||
return Response(issues, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
@ -210,11 +196,7 @@ class IssueViewSet(BaseViewSet):
|
||||
filterset_class = IssueFilterSet
|
||||
|
||||
def get_serializer_class(self):
|
||||
return (
|
||||
IssueCreateSerializer
|
||||
if self.action in ["create", "update", "partial_update"]
|
||||
else IssueSerializer
|
||||
)
|
||||
return IssueCreateSerializer if self.action in ["create", "update", "partial_update"] else IssueSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
issues = Issue.issue_objects.filter(
|
||||
@ -228,9 +210,7 @@ class IssueViewSet(BaseViewSet):
|
||||
issues = (
|
||||
issues.annotate(
|
||||
cycle_id=Subquery(
|
||||
CycleIssue.objects.filter(
|
||||
issue=OuterRef("id"), deleted_at__isnull=True
|
||||
).values("cycle_id")[:1]
|
||||
CycleIssue.objects.filter(issue=OuterRef("id"), deleted_at__isnull=True).values("cycle_id")[:1]
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
@ -301,9 +281,7 @@ class IssueViewSet(BaseViewSet):
|
||||
sub_group_by = request.GET.get("sub_group_by", False)
|
||||
|
||||
# issue queryset
|
||||
issue_queryset = issue_queryset_grouper(
|
||||
queryset=issue_queryset, group_by=group_by, sub_group_by=sub_group_by
|
||||
)
|
||||
issue_queryset = issue_queryset_grouper(queryset=issue_queryset, group_by=group_by, sub_group_by=sub_group_by)
|
||||
|
||||
recent_visited_task.delay(
|
||||
slug=slug,
|
||||
@ -323,9 +301,7 @@ class IssueViewSet(BaseViewSet):
|
||||
and not project.guest_view_all_features
|
||||
):
|
||||
issue_queryset = issue_queryset.filter(created_by=request.user)
|
||||
filtered_issue_queryset = filtered_issue_queryset.filter(
|
||||
created_by=request.user
|
||||
)
|
||||
filtered_issue_queryset = filtered_issue_queryset.filter(created_by=request.user)
|
||||
|
||||
if group_by:
|
||||
if sub_group_by:
|
||||
@ -405,9 +381,7 @@ class IssueViewSet(BaseViewSet):
|
||||
request=request,
|
||||
queryset=issue_queryset,
|
||||
total_count_queryset=filtered_issue_queryset,
|
||||
on_results=lambda issues: issue_on_results(
|
||||
group_by=group_by, issues=issues, sub_group_by=sub_group_by
|
||||
),
|
||||
on_results=lambda issues: issue_on_results(group_by=group_by, issues=issues, sub_group_by=sub_group_by),
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
@ -477,9 +451,7 @@ class IssueViewSet(BaseViewSet):
|
||||
.first()
|
||||
)
|
||||
datetime_fields = ["created_at", "updated_at"]
|
||||
issue = user_timezone_converter(
|
||||
issue, datetime_fields, request.user.user_timezone
|
||||
)
|
||||
issue = user_timezone_converter(issue, datetime_fields, request.user.user_timezone)
|
||||
# Send the model activity
|
||||
model_activity.delay(
|
||||
model_name="issue",
|
||||
@ -500,9 +472,7 @@ class IssueViewSet(BaseViewSet):
|
||||
return Response(issue, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], creator=True, model=Issue
|
||||
)
|
||||
@allow_permission(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)
|
||||
|
||||
@ -513,13 +483,7 @@ class IssueViewSet(BaseViewSet):
|
||||
pk=pk,
|
||||
)
|
||||
.select_related("state")
|
||||
.annotate(
|
||||
cycle_id=Subquery(
|
||||
CycleIssue.objects.filter(issue=OuterRef("id")).values("cycle_id")[
|
||||
:1
|
||||
]
|
||||
)
|
||||
)
|
||||
.annotate(cycle_id=Subquery(CycleIssue.objects.filter(issue=OuterRef("id")).values("cycle_id")[:1]))
|
||||
.annotate(
|
||||
link_count=Subquery(
|
||||
IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
@ -643,9 +607,7 @@ class IssueViewSet(BaseViewSet):
|
||||
serializer = IssueDetailSerializer(issue, expand=self.expand)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], creator=True, model=Issue
|
||||
)
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], creator=True, model=Issue)
|
||||
def partial_update(self, request, slug, project_id, pk=None):
|
||||
queryset = self.get_queryset()
|
||||
queryset = self.apply_annotations(queryset)
|
||||
@ -655,10 +617,7 @@ class IssueViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(labels__id__isnull=True)
|
||||
& Q(label_issue__deleted_at__isnull=True)
|
||||
),
|
||||
filter=Q(~Q(labels__id__isnull=True) & Q(label_issue__deleted_at__isnull=True)),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@ -692,18 +651,12 @@ class IssueViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
if not issue:
|
||||
return Response(
|
||||
{"error": "Issue not found"}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
return Response({"error": "Issue not found"}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
current_instance = json.dumps(
|
||||
IssueDetailSerializer(issue).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
current_instance = json.dumps(IssueDetailSerializer(issue).data, cls=DjangoJSONEncoder)
|
||||
|
||||
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
|
||||
serializer = IssueCreateSerializer(
|
||||
issue, data=request.data, partial=True, context={"project_id": project_id}
|
||||
)
|
||||
serializer = IssueCreateSerializer(issue, data=request.data, partial=True, context={"project_id": project_id})
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
issue_activity.delay(
|
||||
@ -765,29 +718,19 @@ class IssueViewSet(BaseViewSet):
|
||||
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
|
||||
)
|
||||
issue_property = IssueUserProperty.objects.get(user=request.user, project_id=project_id)
|
||||
|
||||
issue_property.rich_filters = request.data.get(
|
||||
"rich_filters", issue_property.rich_filters
|
||||
)
|
||||
issue_property.rich_filters = request.data.get("rich_filters", issue_property.rich_filters)
|
||||
issue_property.filters = request.data.get("filters", issue_property.filters)
|
||||
issue_property.display_filters = request.data.get(
|
||||
"display_filters", issue_property.display_filters
|
||||
)
|
||||
issue_property.display_properties = request.data.get(
|
||||
"display_properties", issue_property.display_properties
|
||||
)
|
||||
issue_property.display_filters = request.data.get("display_filters", issue_property.display_filters)
|
||||
issue_property.display_properties = request.data.get("display_properties", issue_property.display_properties)
|
||||
issue_property.save()
|
||||
serializer = IssueUserPropertySerializer(issue_property)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
@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
|
||||
)
|
||||
issue_property, _ = IssueUserProperty.objects.get_or_create(user=request.user, project_id=project_id)
|
||||
serializer = IssueUserPropertySerializer(issue_property)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@ -798,13 +741,9 @@ class BulkDeleteIssuesEndpoint(BaseAPIView):
|
||||
issue_ids = request.data.get("issue_ids", [])
|
||||
|
||||
if not len(issue_ids):
|
||||
return Response(
|
||||
{"error": "Issue IDs are required"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
return Response({"error": "Issue IDs are required"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
issues = Issue.issue_objects.filter(
|
||||
workspace__slug=slug, project_id=project_id, pk__in=issue_ids
|
||||
)
|
||||
issues = Issue.issue_objects.filter(workspace__slug=slug, project_id=project_id, pk__in=issue_ids)
|
||||
|
||||
total_issues = len(issues)
|
||||
|
||||
@ -844,19 +783,11 @@ class IssuePaginatedViewSet(BaseViewSet):
|
||||
workspace_slug = self.kwargs.get("slug")
|
||||
project_id = self.kwargs.get("project_id")
|
||||
|
||||
issue_queryset = Issue.issue_objects.filter(
|
||||
workspace__slug=workspace_slug, project_id=project_id
|
||||
)
|
||||
issue_queryset = Issue.issue_objects.filter(workspace__slug=workspace_slug, project_id=project_id)
|
||||
|
||||
return (
|
||||
issue_queryset.select_related("state")
|
||||
.annotate(
|
||||
cycle_id=Subquery(
|
||||
CycleIssue.objects.filter(issue=OuterRef("id")).values("cycle_id")[
|
||||
:1
|
||||
]
|
||||
)
|
||||
)
|
||||
.annotate(cycle_id=Subquery(CycleIssue.objects.filter(issue=OuterRef("id")).values("cycle_id")[:1]))
|
||||
.annotate(
|
||||
link_count=Subquery(
|
||||
IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
@ -891,9 +822,7 @@ class IssuePaginatedViewSet(BaseViewSet):
|
||||
|
||||
# converting the datetime fields in paginated data
|
||||
datetime_fields = ["created_at", "updated_at"]
|
||||
paginated_data = user_timezone_converter(
|
||||
paginated_data, datetime_fields, timezone
|
||||
)
|
||||
paginated_data = user_timezone_converter(paginated_data, datetime_fields, timezone)
|
||||
|
||||
return paginated_data
|
||||
|
||||
@ -937,9 +866,7 @@ class IssuePaginatedViewSet(BaseViewSet):
|
||||
required_fields.append("description_html")
|
||||
|
||||
# querying issues
|
||||
base_queryset = Issue.issue_objects.filter(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
base_queryset = Issue.issue_objects.filter(workspace__slug=slug, project_id=project_id)
|
||||
|
||||
base_queryset = base_queryset.order_by("updated_at")
|
||||
queryset = self.get_queryset().order_by("updated_at")
|
||||
@ -1018,9 +945,7 @@ class IssueDetailEndpoint(BaseAPIView):
|
||||
return (
|
||||
issues.annotate(
|
||||
cycle_id=Subquery(
|
||||
CycleIssue.objects.filter(
|
||||
issue=OuterRef("id"), deleted_at__isnull=True
|
||||
).values("cycle_id")[:1]
|
||||
CycleIssue.objects.filter(issue=OuterRef("id"), deleted_at__isnull=True).values("cycle_id")[:1]
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
@ -1071,9 +996,7 @@ class IssueDetailEndpoint(BaseAPIView):
|
||||
# check for the project member role, if the role is 5 then check for the guest_view_all_features
|
||||
# if it is true then show all the issues else show only the issues created by the user
|
||||
permission_subquery = (
|
||||
Issue.issue_objects.filter(
|
||||
workspace__slug=slug, project_id=project_id, id=OuterRef("id")
|
||||
)
|
||||
Issue.issue_objects.filter(workspace__slug=slug, project_id=project_id, id=OuterRef("id"))
|
||||
.filter(
|
||||
Q(
|
||||
project__project_projectmember__member=self.request.user,
|
||||
@ -1097,9 +1020,9 @@ class IssueDetailEndpoint(BaseAPIView):
|
||||
.values("id")
|
||||
)
|
||||
# Main issue query
|
||||
issue = Issue.issue_objects.filter(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
).filter(Exists(permission_subquery))
|
||||
issue = Issue.issue_objects.filter(workspace__slug=slug, project_id=project_id).filter(
|
||||
Exists(permission_subquery)
|
||||
)
|
||||
|
||||
# Add additional prefetch based on expand parameter
|
||||
if self.expand:
|
||||
@ -1133,9 +1056,7 @@ class IssueDetailEndpoint(BaseAPIView):
|
||||
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
|
||||
)
|
||||
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,
|
||||
@ -1188,9 +1109,7 @@ class IssueBulkUpdateDateEndpoint(BaseAPIView):
|
||||
|
||||
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
|
||||
)
|
||||
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"},
|
||||
@ -1213,12 +1132,8 @@ class IssueBulkUpdateDateEndpoint(BaseAPIView):
|
||||
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)}
|
||||
),
|
||||
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),
|
||||
@ -1230,9 +1145,7 @@ class IssueBulkUpdateDateEndpoint(BaseAPIView):
|
||||
# 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
|
||||
)
|
||||
return Response({"message": "Issues updated successfully"}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class IssueMetaEndpoint(BaseAPIView):
|
||||
@ -1267,9 +1180,7 @@ class IssueDetailIdentifierEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
# Fetch the project
|
||||
project = Project.objects.get(
|
||||
identifier__iexact=project_identifier, workspace__slug=slug
|
||||
)
|
||||
project = Project.objects.get(identifier__iexact=project_identifier, workspace__slug=slug)
|
||||
|
||||
# Check if the user is a member of the project
|
||||
if not ProjectMember.objects.filter(
|
||||
@ -1289,13 +1200,7 @@ class IssueDetailIdentifierEndpoint(BaseAPIView):
|
||||
.filter(workspace__slug=slug)
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||
.annotate(
|
||||
cycle_id=Subquery(
|
||||
CycleIssue.objects.filter(issue=OuterRef("id")).values("cycle_id")[
|
||||
:1
|
||||
]
|
||||
)
|
||||
)
|
||||
.annotate(cycle_id=Subquery(CycleIssue.objects.filter(issue=OuterRef("id")).values("cycle_id")[:1]))
|
||||
.annotate(
|
||||
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
|
||||
.order_by()
|
||||
@ -1323,10 +1228,7 @@ class IssueDetailIdentifierEndpoint(BaseAPIView):
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(labels__id__isnull=True)
|
||||
& Q(label_issue__deleted_at__isnull=True)
|
||||
),
|
||||
filter=Q(~Q(labels__id__isnull=True) & Q(label_issue__deleted_at__isnull=True)),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
|
||||
@ -77,9 +77,7 @@ 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
|
||||
)
|
||||
serializer.save(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),
|
||||
@ -106,21 +104,12 @@ class IssueCommentViewSet(BaseViewSet):
|
||||
|
||||
@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
|
||||
)
|
||||
issue_comment = IssueComment.objects.get(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
|
||||
)
|
||||
serializer = IssueCommentSerializer(
|
||||
issue_comment, data=request.data, partial=True
|
||||
)
|
||||
current_instance = json.dumps(IssueCommentSerializer(issue_comment).data, cls=DjangoJSONEncoder)
|
||||
serializer = IssueCommentSerializer(issue_comment, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
if (
|
||||
"comment_html" in request.data
|
||||
and request.data["comment_html"] != issue_comment.comment_html
|
||||
):
|
||||
if "comment_html" in request.data and request.data["comment_html"] != issue_comment.comment_html:
|
||||
serializer.save(edited_at=timezone.now())
|
||||
else:
|
||||
serializer.save()
|
||||
@ -150,12 +139,8 @@ class IssueCommentViewSet(BaseViewSet):
|
||||
|
||||
@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
|
||||
)
|
||||
current_instance = json.dumps(
|
||||
IssueCommentSerializer(issue_comment).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
issue_comment = IssueComment.objects.get(workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk)
|
||||
current_instance = json.dumps(IssueCommentSerializer(issue_comment).data, cls=DjangoJSONEncoder)
|
||||
issue_comment.delete()
|
||||
issue_activity.delay(
|
||||
type="comment.activity.deleted",
|
||||
|
||||
@ -35,9 +35,7 @@ class LabelViewSet(BaseViewSet):
|
||||
.order_by("sort_order")
|
||||
)
|
||||
|
||||
@invalidate_cache(
|
||||
path="/api/workspaces/:slug/labels/", url_params=True, user=False, multiple=True
|
||||
)
|
||||
@invalidate_cache(path="/api/workspaces/:slug/labels/", url_params=True, user=False, multiple=True)
|
||||
@allow_permission([ROLE.ADMIN])
|
||||
def create(self, request, slug, project_id):
|
||||
try:
|
||||
@ -58,9 +56,7 @@ class LabelViewSet(BaseViewSet):
|
||||
# Check if the label name is unique within the project
|
||||
if (
|
||||
"name" in request.data
|
||||
and Label.objects.filter(
|
||||
project_id=kwargs["project_id"], name=request.data["name"]
|
||||
)
|
||||
and Label.objects.filter(project_id=kwargs["project_id"], name=request.data["name"])
|
||||
.exclude(pk=kwargs["pk"])
|
||||
.exists()
|
||||
):
|
||||
|
||||
@ -45,9 +45,7 @@ class IssueLinkViewSet(BaseViewSet):
|
||||
serializer = IssueLinkSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
serializer.save(project_id=project_id, issue_id=issue_id)
|
||||
crawl_work_item_link_title.delay(
|
||||
serializer.data.get("id"), serializer.data.get("url")
|
||||
)
|
||||
crawl_work_item_link_title.delay(serializer.data.get("id"), serializer.data.get("url"))
|
||||
issue_activity.delay(
|
||||
type="link.activity.created",
|
||||
requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder),
|
||||
@ -67,20 +65,14 @@ class IssueLinkViewSet(BaseViewSet):
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def partial_update(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
|
||||
)
|
||||
issue_link = IssueLink.objects.get(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
|
||||
)
|
||||
current_instance = json.dumps(IssueLinkSerializer(issue_link).data, cls=DjangoJSONEncoder)
|
||||
|
||||
serializer = IssueLinkSerializer(issue_link, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
crawl_work_item_link_title.delay(
|
||||
serializer.data.get("id"), serializer.data.get("url")
|
||||
)
|
||||
crawl_work_item_link_title.delay(serializer.data.get("id"), serializer.data.get("url"))
|
||||
|
||||
issue_activity.delay(
|
||||
type="link.activity.updated",
|
||||
@ -100,12 +92,8 @@ class IssueLinkViewSet(BaseViewSet):
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def destroy(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
|
||||
)
|
||||
current_instance = json.dumps(
|
||||
IssueLinkSerializer(issue_link).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
issue_link = IssueLink.objects.get(workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk)
|
||||
current_instance = json.dumps(IssueLinkSerializer(issue_link).data, cls=DjangoJSONEncoder)
|
||||
issue_activity.delay(
|
||||
type="link.activity.deleted",
|
||||
requested_data=json.dumps({"link_id": str(pk)}),
|
||||
|
||||
@ -42,9 +42,7 @@ class IssueReactionViewSet(BaseViewSet):
|
||||
def create(self, request, slug, project_id, issue_id):
|
||||
serializer = IssueReactionSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
serializer.save(
|
||||
issue_id=issue_id, project_id=project_id, actor=request.user
|
||||
)
|
||||
serializer.save(issue_id=issue_id, project_id=project_id, actor=request.user)
|
||||
issue_activity.delay(
|
||||
type="issue_reaction.activity.created",
|
||||
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
|
||||
@ -74,9 +72,7 @@ class IssueReactionViewSet(BaseViewSet):
|
||||
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(
|
||||
{"reaction": str(reaction_code), "identifier": str(issue_reaction.id)}
|
||||
),
|
||||
current_instance=json.dumps({"reaction": str(reaction_code), "identifier": str(issue_reaction.id)}),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
|
||||
@ -37,9 +37,7 @@ class IssueRelationViewSet(BaseViewSet):
|
||||
|
||||
def list(self, request, slug, project_id, issue_id):
|
||||
issue_relations = (
|
||||
IssueRelation.objects.filter(
|
||||
Q(issue_id=issue_id) | Q(related_issue=issue_id)
|
||||
)
|
||||
IssueRelation.objects.filter(Q(issue_id=issue_id) | Q(related_issue=issue_id))
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.select_related("project")
|
||||
.select_related("workspace")
|
||||
@ -48,19 +46,19 @@ class IssueRelationViewSet(BaseViewSet):
|
||||
.distinct()
|
||||
)
|
||||
# get all blocking issues
|
||||
blocking_issues = issue_relations.filter(
|
||||
relation_type="blocked_by", related_issue_id=issue_id
|
||||
).values_list("issue_id", flat=True)
|
||||
blocking_issues = issue_relations.filter(relation_type="blocked_by", related_issue_id=issue_id).values_list(
|
||||
"issue_id", flat=True
|
||||
)
|
||||
|
||||
# get all blocked by issues
|
||||
blocked_by_issues = issue_relations.filter(
|
||||
relation_type="blocked_by", issue_id=issue_id
|
||||
).values_list("related_issue_id", flat=True)
|
||||
blocked_by_issues = issue_relations.filter(relation_type="blocked_by", issue_id=issue_id).values_list(
|
||||
"related_issue_id", flat=True
|
||||
)
|
||||
|
||||
# get all duplicate issues
|
||||
duplicate_issues = issue_relations.filter(
|
||||
issue_id=issue_id, relation_type="duplicate"
|
||||
).values_list("related_issue_id", flat=True)
|
||||
duplicate_issues = issue_relations.filter(issue_id=issue_id, relation_type="duplicate").values_list(
|
||||
"related_issue_id", flat=True
|
||||
)
|
||||
|
||||
# get all relates to issues
|
||||
duplicate_issues_related = issue_relations.filter(
|
||||
@ -68,9 +66,9 @@ class IssueRelationViewSet(BaseViewSet):
|
||||
).values_list("issue_id", flat=True)
|
||||
|
||||
# get all relates to issues
|
||||
relates_to_issues = issue_relations.filter(
|
||||
issue_id=issue_id, relation_type="relates_to"
|
||||
).values_list("related_issue_id", flat=True)
|
||||
relates_to_issues = issue_relations.filter(issue_id=issue_id, relation_type="relates_to").values_list(
|
||||
"related_issue_id", flat=True
|
||||
)
|
||||
|
||||
# get all relates to issues
|
||||
relates_to_issues_related = issue_relations.filter(
|
||||
@ -83,9 +81,9 @@ class IssueRelationViewSet(BaseViewSet):
|
||||
).values_list("issue_id", flat=True)
|
||||
|
||||
# get all start_before issues
|
||||
start_before_issues = issue_relations.filter(
|
||||
relation_type="start_before", issue_id=issue_id
|
||||
).values_list("related_issue_id", flat=True)
|
||||
start_before_issues = issue_relations.filter(relation_type="start_before", issue_id=issue_id).values_list(
|
||||
"related_issue_id", flat=True
|
||||
)
|
||||
|
||||
# get all finish after issues
|
||||
finish_after_issues = issue_relations.filter(
|
||||
@ -93,9 +91,9 @@ class IssueRelationViewSet(BaseViewSet):
|
||||
).values_list("issue_id", flat=True)
|
||||
|
||||
# get all finish before issues
|
||||
finish_before_issues = issue_relations.filter(
|
||||
relation_type="finish_before", issue_id=issue_id
|
||||
).values_list("related_issue_id", flat=True)
|
||||
finish_before_issues = issue_relations.filter(relation_type="finish_before", issue_id=issue_id).values_list(
|
||||
"related_issue_id", flat=True
|
||||
)
|
||||
|
||||
queryset = (
|
||||
Issue.issue_objects.filter(workspace__slug=slug)
|
||||
@ -103,9 +101,7 @@ class IssueRelationViewSet(BaseViewSet):
|
||||
.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]
|
||||
CycleIssue.objects.filter(issue=OuterRef("id"), deleted_at__isnull=True).values("cycle_id")[:1]
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
@ -134,10 +130,7 @@ class IssueRelationViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(labels__id__isnull=True)
|
||||
& (Q(label_issue__deleted_at__isnull=True))
|
||||
),
|
||||
filter=Q(~Q(labels__id__isnull=True) & (Q(label_issue__deleted_at__isnull=True))),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@ -223,15 +216,9 @@ class IssueRelationViewSet(BaseViewSet):
|
||||
issue_relation = IssueRelation.objects.bulk_create(
|
||||
[
|
||||
IssueRelation(
|
||||
issue_id=(
|
||||
issue
|
||||
if relation_type in ["blocking", "start_after", "finish_after"]
|
||||
else issue_id
|
||||
),
|
||||
issue_id=(issue if relation_type in ["blocking", "start_after", "finish_after"] else issue_id),
|
||||
related_issue_id=(
|
||||
issue_id
|
||||
if relation_type in ["blocking", "start_after", "finish_after"]
|
||||
else issue
|
||||
issue_id if relation_type in ["blocking", "start_after", "finish_after"] else issue
|
||||
),
|
||||
relation_type=(get_actual_relation(relation_type)),
|
||||
project_id=project_id,
|
||||
@ -274,13 +261,10 @@ class IssueRelationViewSet(BaseViewSet):
|
||||
issue_relations = IssueRelation.objects.filter(
|
||||
workspace__slug=slug,
|
||||
).filter(
|
||||
Q(issue_id=related_issue, related_issue_id=issue_id)
|
||||
| Q(issue_id=issue_id, related_issue_id=related_issue)
|
||||
Q(issue_id=related_issue, related_issue_id=issue_id) | Q(issue_id=issue_id, related_issue_id=related_issue)
|
||||
)
|
||||
issue_relations = issue_relations.first()
|
||||
current_instance = json.dumps(
|
||||
IssueRelationSerializer(issue_relations).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
current_instance = json.dumps(IssueRelationSerializer(issue_relations).data, cls=DjangoJSONEncoder)
|
||||
issue_relations.delete()
|
||||
issue_activity.delay(
|
||||
type="issue_relation.activity.deleted",
|
||||
|
||||
@ -37,9 +37,7 @@ class SubIssuesEndpoint(BaseAPIView):
|
||||
.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]
|
||||
CycleIssue.objects.filter(issue=OuterRef("id"), deleted_at__isnull=True).values("cycle_id")[:1]
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
@ -68,10 +66,7 @@ class SubIssuesEndpoint(BaseAPIView):
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(labels__id__isnull=True)
|
||||
& Q(label_issue__deleted_at__isnull=True)
|
||||
),
|
||||
filter=Q(~Q(labels__id__isnull=True) & Q(label_issue__deleted_at__isnull=True)),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@ -109,9 +104,7 @@ class SubIssuesEndpoint(BaseAPIView):
|
||||
group_by = request.GET.get("group_by", False)
|
||||
|
||||
if order_by_param:
|
||||
sub_issues, order_by_param = order_issue_queryset(
|
||||
sub_issues, order_by_param
|
||||
)
|
||||
sub_issues, order_by_param = order_issue_queryset(sub_issues, order_by_param)
|
||||
|
||||
# create's a dict with state group name with their respective issue id's
|
||||
result = defaultdict(list)
|
||||
@ -146,9 +139,7 @@ class SubIssuesEndpoint(BaseAPIView):
|
||||
"archived_at",
|
||||
)
|
||||
datetime_fields = ["created_at", "updated_at"]
|
||||
sub_issues = user_timezone_converter(
|
||||
sub_issues, datetime_fields, request.user.user_timezone
|
||||
)
|
||||
sub_issues = user_timezone_converter(sub_issues, datetime_fields, request.user.user_timezone)
|
||||
# Grouping
|
||||
if group_by:
|
||||
result_dict = defaultdict(list)
|
||||
@ -192,9 +183,7 @@ class SubIssuesEndpoint(BaseAPIView):
|
||||
|
||||
_ = Issue.objects.bulk_update(sub_issues, ["parent"], batch_size=10)
|
||||
|
||||
updated_sub_issues = Issue.issue_objects.filter(id__in=sub_issue_ids).annotate(
|
||||
state_group=F("state__group")
|
||||
)
|
||||
updated_sub_issues = Issue.issue_objects.filter(id__in=sub_issue_ids).annotate(state_group=F("state__group"))
|
||||
|
||||
# Track the issue
|
||||
_ = [
|
||||
|
||||
@ -25,9 +25,7 @@ class IssueVersionEndpoint(BaseAPIView):
|
||||
paginated_data = results.values(*fields)
|
||||
|
||||
datetime_fields = ["created_at", "updated_at"]
|
||||
paginated_data = user_timezone_converter(
|
||||
paginated_data, datetime_fields, timezone
|
||||
)
|
||||
paginated_data = user_timezone_converter(paginated_data, datetime_fields, timezone)
|
||||
|
||||
return paginated_data
|
||||
|
||||
@ -77,18 +75,14 @@ class WorkItemDescriptionVersionEndpoint(BaseAPIView):
|
||||
paginated_data = results.values(*fields)
|
||||
|
||||
datetime_fields = ["created_at", "updated_at"]
|
||||
paginated_data = user_timezone_converter(
|
||||
paginated_data, datetime_fields, timezone
|
||||
)
|
||||
paginated_data = user_timezone_converter(paginated_data, datetime_fields, timezone)
|
||||
|
||||
return paginated_data
|
||||
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def get(self, request, slug, project_id, work_item_id, pk=None):
|
||||
project = Project.objects.get(pk=project_id)
|
||||
issue = Issue.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=work_item_id
|
||||
)
|
||||
issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=work_item_id)
|
||||
|
||||
if (
|
||||
ProjectMember.objects.filter(
|
||||
@ -114,9 +108,7 @@ class WorkItemDescriptionVersionEndpoint(BaseAPIView):
|
||||
pk=pk,
|
||||
)
|
||||
|
||||
serializer = IssueDescriptionVersionDetailSerializer(
|
||||
issue_description_version
|
||||
)
|
||||
serializer = IssueDescriptionVersionDetailSerializer(issue_description_version)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
cursor = request.GET.get("cursor", None)
|
||||
|
||||
@ -113,11 +113,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
issue_module__deleted_at__isnull=True,
|
||||
)
|
||||
.values("issue_module__module_id")
|
||||
.annotate(
|
||||
completed_estimate_points=Sum(
|
||||
Cast("estimate_point__value", FloatField())
|
||||
)
|
||||
)
|
||||
.annotate(completed_estimate_points=Sum(Cast("estimate_point__value", FloatField())))
|
||||
.values("completed_estimate_points")[:1]
|
||||
)
|
||||
|
||||
@ -128,9 +124,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
issue_module__deleted_at__isnull=True,
|
||||
)
|
||||
.values("issue_module__module_id")
|
||||
.annotate(
|
||||
total_estimate_points=Sum(Cast("estimate_point__value", FloatField()))
|
||||
)
|
||||
.annotate(total_estimate_points=Sum(Cast("estimate_point__value", FloatField())))
|
||||
.values("total_estimate_points")[:1]
|
||||
)
|
||||
backlog_estimate_point = (
|
||||
@ -141,9 +135,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
issue_module__deleted_at__isnull=True,
|
||||
)
|
||||
.values("issue_module__module_id")
|
||||
.annotate(
|
||||
backlog_estimate_point=Sum(Cast("estimate_point__value", FloatField()))
|
||||
)
|
||||
.annotate(backlog_estimate_point=Sum(Cast("estimate_point__value", FloatField())))
|
||||
.values("backlog_estimate_point")[:1]
|
||||
)
|
||||
unstarted_estimate_point = (
|
||||
@ -154,11 +146,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
issue_module__deleted_at__isnull=True,
|
||||
)
|
||||
.values("issue_module__module_id")
|
||||
.annotate(
|
||||
unstarted_estimate_point=Sum(
|
||||
Cast("estimate_point__value", FloatField())
|
||||
)
|
||||
)
|
||||
.annotate(unstarted_estimate_point=Sum(Cast("estimate_point__value", FloatField())))
|
||||
.values("unstarted_estimate_point")[:1]
|
||||
)
|
||||
started_estimate_point = (
|
||||
@ -169,9 +157,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
issue_module__deleted_at__isnull=True,
|
||||
)
|
||||
.values("issue_module__module_id")
|
||||
.annotate(
|
||||
started_estimate_point=Sum(Cast("estimate_point__value", FloatField()))
|
||||
)
|
||||
.annotate(started_estimate_point=Sum(Cast("estimate_point__value", FloatField())))
|
||||
.values("started_estimate_point")[:1]
|
||||
)
|
||||
cancelled_estimate_point = (
|
||||
@ -182,11 +168,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
issue_module__deleted_at__isnull=True,
|
||||
)
|
||||
.values("issue_module__module_id")
|
||||
.annotate(
|
||||
cancelled_estimate_point=Sum(
|
||||
Cast("estimate_point__value", FloatField())
|
||||
)
|
||||
)
|
||||
.annotate(cancelled_estimate_point=Sum(Cast("estimate_point__value", FloatField())))
|
||||
.values("cancelled_estimate_point")[:1]
|
||||
)
|
||||
return (
|
||||
@ -214,27 +196,15 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
Value(0, output_field=IntegerField()),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
started_issues=Coalesce(
|
||||
Subquery(started_issues[:1]), Value(0, output_field=IntegerField())
|
||||
)
|
||||
)
|
||||
.annotate(started_issues=Coalesce(Subquery(started_issues[:1]), Value(0, output_field=IntegerField())))
|
||||
.annotate(
|
||||
unstarted_issues=Coalesce(
|
||||
Subquery(unstarted_issues[:1]),
|
||||
Value(0, output_field=IntegerField()),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
backlog_issues=Coalesce(
|
||||
Subquery(backlog_issues[:1]), Value(0, output_field=IntegerField())
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
total_issues=Coalesce(
|
||||
Subquery(total_issues[:1]), Value(0, output_field=IntegerField())
|
||||
)
|
||||
)
|
||||
.annotate(backlog_issues=Coalesce(Subquery(backlog_issues[:1]), Value(0, output_field=IntegerField())))
|
||||
.annotate(total_issues=Coalesce(Subquery(total_issues[:1]), Value(0, output_field=IntegerField())))
|
||||
.annotate(
|
||||
backlog_estimate_points=Coalesce(
|
||||
Subquery(backlog_estimate_point),
|
||||
@ -266,9 +236,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
total_estimate_points=Coalesce(
|
||||
Subquery(total_estimate_point), Value(0, output_field=FloatField())
|
||||
)
|
||||
total_estimate_points=Coalesce(Subquery(total_estimate_point), Value(0, output_field=FloatField()))
|
||||
)
|
||||
.annotate(
|
||||
member_ids=Coalesce(
|
||||
@ -317,9 +285,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
"archived_at",
|
||||
)
|
||||
datetime_fields = ["created_at", "updated_at"]
|
||||
modules = user_timezone_converter(
|
||||
modules, datetime_fields, request.user.user_timezone
|
||||
)
|
||||
modules = user_timezone_converter(modules, datetime_fields, request.user.user_timezone)
|
||||
return Response(modules, status=status.HTTP_200_OK)
|
||||
else:
|
||||
queryset = (
|
||||
@ -389,9 +355,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
"avatar_url",
|
||||
"display_name",
|
||||
)
|
||||
.annotate(
|
||||
total_estimates=Sum(Cast("estimate_point__value", FloatField()))
|
||||
)
|
||||
.annotate(total_estimates=Sum(Cast("estimate_point__value", FloatField())))
|
||||
.annotate(
|
||||
completed_estimates=Sum(
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
@ -425,9 +389,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
.annotate(color=F("labels__color"))
|
||||
.annotate(label_id=F("labels__id"))
|
||||
.values("label_name", "color", "label_id")
|
||||
.annotate(
|
||||
total_estimates=Sum(Cast("estimate_point__value", FloatField()))
|
||||
)
|
||||
.annotate(total_estimates=Sum(Cast("estimate_point__value", FloatField())))
|
||||
.annotate(
|
||||
completed_estimates=Sum(
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
@ -500,11 +462,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
"avatar_url",
|
||||
"display_name",
|
||||
)
|
||||
.annotate(
|
||||
total_issues=Count(
|
||||
"id", filter=Q(archived_at__isnull=True, is_draft=False)
|
||||
)
|
||||
)
|
||||
.annotate(total_issues=Count("id", filter=Q(archived_at__isnull=True, is_draft=False)))
|
||||
.annotate(
|
||||
completed_issues=Count(
|
||||
"id",
|
||||
@ -539,11 +497,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
.annotate(color=F("labels__color"))
|
||||
.annotate(label_id=F("labels__id"))
|
||||
.values("label_name", "color", "label_id")
|
||||
.annotate(
|
||||
total_issues=Count(
|
||||
"id", filter=Q(archived_at__isnull=True, is_draft=False)
|
||||
)
|
||||
)
|
||||
.annotate(total_issues=Count("id", filter=Q(archived_at__isnull=True, is_draft=False)))
|
||||
.annotate(
|
||||
completed_issues=Count(
|
||||
"id",
|
||||
@ -584,9 +538,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
|
||||
def post(self, request, slug, project_id, module_id):
|
||||
module = Module.objects.get(
|
||||
pk=module_id, project_id=project_id, workspace__slug=slug
|
||||
)
|
||||
module = Module.objects.get(pk=module_id, 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"},
|
||||
@ -600,14 +552,10 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
).delete()
|
||||
return Response(
|
||||
{"archived_at": str(module.archived_at)}, status=status.HTTP_200_OK
|
||||
)
|
||||
return Response({"archived_at": str(module.archived_at)}, status=status.HTTP_200_OK)
|
||||
|
||||
def delete(self, request, slug, project_id, module_id):
|
||||
module = Module.objects.get(
|
||||
pk=module_id, project_id=project_id, workspace__slug=slug
|
||||
)
|
||||
module = Module.objects.get(pk=module_id, project_id=project_id, workspace__slug=slug)
|
||||
module.archived_at = None
|
||||
module.save()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@ -69,11 +69,7 @@ class ModuleViewSet(BaseViewSet):
|
||||
webhook_event = "module"
|
||||
|
||||
def get_serializer_class(self):
|
||||
return (
|
||||
ModuleWriteSerializer
|
||||
if self.action in ["create", "update", "partial_update"]
|
||||
else ModuleSerializer
|
||||
)
|
||||
return ModuleWriteSerializer if self.action in ["create", "update", "partial_update"] else ModuleSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
favorite_subquery = UserFavorite.objects.filter(
|
||||
@ -150,11 +146,7 @@ class ModuleViewSet(BaseViewSet):
|
||||
issue_module__deleted_at__isnull=True,
|
||||
)
|
||||
.values("issue_module__module_id")
|
||||
.annotate(
|
||||
completed_estimate_points=Sum(
|
||||
Cast("estimate_point__value", FloatField())
|
||||
)
|
||||
)
|
||||
.annotate(completed_estimate_points=Sum(Cast("estimate_point__value", FloatField())))
|
||||
.values("completed_estimate_points")[:1]
|
||||
)
|
||||
|
||||
@ -165,9 +157,7 @@ class ModuleViewSet(BaseViewSet):
|
||||
issue_module__deleted_at__isnull=True,
|
||||
)
|
||||
.values("issue_module__module_id")
|
||||
.annotate(
|
||||
total_estimate_points=Sum(Cast("estimate_point__value", FloatField()))
|
||||
)
|
||||
.annotate(total_estimate_points=Sum(Cast("estimate_point__value", FloatField())))
|
||||
.values("total_estimate_points")[:1]
|
||||
)
|
||||
backlog_estimate_point = (
|
||||
@ -178,9 +168,7 @@ class ModuleViewSet(BaseViewSet):
|
||||
issue_module__deleted_at__isnull=True,
|
||||
)
|
||||
.values("issue_module__module_id")
|
||||
.annotate(
|
||||
backlog_estimate_point=Sum(Cast("estimate_point__value", FloatField()))
|
||||
)
|
||||
.annotate(backlog_estimate_point=Sum(Cast("estimate_point__value", FloatField())))
|
||||
.values("backlog_estimate_point")[:1]
|
||||
)
|
||||
unstarted_estimate_point = (
|
||||
@ -191,11 +179,7 @@ class ModuleViewSet(BaseViewSet):
|
||||
issue_module__deleted_at__isnull=True,
|
||||
)
|
||||
.values("issue_module__module_id")
|
||||
.annotate(
|
||||
unstarted_estimate_point=Sum(
|
||||
Cast("estimate_point__value", FloatField())
|
||||
)
|
||||
)
|
||||
.annotate(unstarted_estimate_point=Sum(Cast("estimate_point__value", FloatField())))
|
||||
.values("unstarted_estimate_point")[:1]
|
||||
)
|
||||
started_estimate_point = (
|
||||
@ -206,9 +190,7 @@ class ModuleViewSet(BaseViewSet):
|
||||
issue_module__deleted_at__isnull=True,
|
||||
)
|
||||
.values("issue_module__module_id")
|
||||
.annotate(
|
||||
started_estimate_point=Sum(Cast("estimate_point__value", FloatField()))
|
||||
)
|
||||
.annotate(started_estimate_point=Sum(Cast("estimate_point__value", FloatField())))
|
||||
.values("started_estimate_point")[:1]
|
||||
)
|
||||
cancelled_estimate_point = (
|
||||
@ -219,11 +201,7 @@ class ModuleViewSet(BaseViewSet):
|
||||
issue_module__deleted_at__isnull=True,
|
||||
)
|
||||
.values("issue_module__module_id")
|
||||
.annotate(
|
||||
cancelled_estimate_point=Sum(
|
||||
Cast("estimate_point__value", FloatField())
|
||||
)
|
||||
)
|
||||
.annotate(cancelled_estimate_point=Sum(Cast("estimate_point__value", FloatField())))
|
||||
.values("cancelled_estimate_point")[:1]
|
||||
)
|
||||
return (
|
||||
@ -251,27 +229,15 @@ class ModuleViewSet(BaseViewSet):
|
||||
Value(0, output_field=IntegerField()),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
started_issues=Coalesce(
|
||||
Subquery(started_issues[:1]), Value(0, output_field=IntegerField())
|
||||
)
|
||||
)
|
||||
.annotate(started_issues=Coalesce(Subquery(started_issues[:1]), Value(0, output_field=IntegerField())))
|
||||
.annotate(
|
||||
unstarted_issues=Coalesce(
|
||||
Subquery(unstarted_issues[:1]),
|
||||
Value(0, output_field=IntegerField()),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
backlog_issues=Coalesce(
|
||||
Subquery(backlog_issues[:1]), Value(0, output_field=IntegerField())
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
total_issues=Coalesce(
|
||||
Subquery(total_issues[:1]), Value(0, output_field=IntegerField())
|
||||
)
|
||||
)
|
||||
.annotate(backlog_issues=Coalesce(Subquery(backlog_issues[:1]), Value(0, output_field=IntegerField())))
|
||||
.annotate(total_issues=Coalesce(Subquery(total_issues[:1]), Value(0, output_field=IntegerField())))
|
||||
.annotate(
|
||||
backlog_estimate_points=Coalesce(
|
||||
Subquery(backlog_estimate_point),
|
||||
@ -303,9 +269,7 @@ class ModuleViewSet(BaseViewSet):
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
total_estimate_points=Coalesce(
|
||||
Subquery(total_estimate_point), Value(0, output_field=FloatField())
|
||||
)
|
||||
total_estimate_points=Coalesce(Subquery(total_estimate_point), Value(0, output_field=FloatField()))
|
||||
)
|
||||
.annotate(
|
||||
member_ids=Coalesce(
|
||||
@ -326,9 +290,7 @@ class ModuleViewSet(BaseViewSet):
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def create(self, request, slug, project_id):
|
||||
project = Project.objects.get(workspace__slug=slug, pk=project_id)
|
||||
serializer = ModuleWriteSerializer(
|
||||
data=request.data, context={"project": project}
|
||||
)
|
||||
serializer = ModuleWriteSerializer(data=request.data, context={"project": project})
|
||||
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
@ -380,9 +342,7 @@ class ModuleViewSet(BaseViewSet):
|
||||
origin=base_host(request=request, is_app=True),
|
||||
)
|
||||
datetime_fields = ["created_at", "updated_at"]
|
||||
module = user_timezone_converter(
|
||||
module, datetime_fields, request.user.user_timezone
|
||||
)
|
||||
module = user_timezone_converter(module, datetime_fields, request.user.user_timezone)
|
||||
return Response(module, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@ -425,9 +385,7 @@ class ModuleViewSet(BaseViewSet):
|
||||
"updated_at",
|
||||
)
|
||||
datetime_fields = ["created_at", "updated_at"]
|
||||
modules = user_timezone_converter(
|
||||
modules, datetime_fields, request.user.user_timezone
|
||||
)
|
||||
modules = user_timezone_converter(modules, datetime_fields, request.user.user_timezone)
|
||||
return Response(modules, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
@ -450,9 +408,7 @@ class ModuleViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
if not queryset.exists():
|
||||
return Response(
|
||||
{"error": "Module not found"}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
return Response({"error": "Module not found"}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
estimate_type = Project.objects.filter(
|
||||
workspace__slug=slug,
|
||||
@ -505,9 +461,7 @@ class ModuleViewSet(BaseViewSet):
|
||||
"avatar_url",
|
||||
"display_name",
|
||||
)
|
||||
.annotate(
|
||||
total_estimates=Sum(Cast("estimate_point__value", FloatField()))
|
||||
)
|
||||
.annotate(total_estimates=Sum(Cast("estimate_point__value", FloatField())))
|
||||
.annotate(
|
||||
completed_estimates=Sum(
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
@ -542,9 +496,7 @@ class ModuleViewSet(BaseViewSet):
|
||||
.annotate(color=F("labels__color"))
|
||||
.annotate(label_id=F("labels__id"))
|
||||
.values("label_name", "color", "label_id")
|
||||
.annotate(
|
||||
total_estimates=Sum(Cast("estimate_point__value", FloatField()))
|
||||
)
|
||||
.annotate(total_estimates=Sum(Cast("estimate_point__value", FloatField())))
|
||||
.annotate(
|
||||
completed_estimates=Sum(
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
@ -602,21 +554,13 @@ class ModuleViewSet(BaseViewSet):
|
||||
),
|
||||
),
|
||||
# If `avatar_asset` is None, fall back to using `avatar` field directly
|
||||
When(
|
||||
assignees__avatar_asset__isnull=True, then="assignees__avatar"
|
||||
),
|
||||
When(assignees__avatar_asset__isnull=True, then="assignees__avatar"),
|
||||
default=Value(None),
|
||||
output_field=models.CharField(),
|
||||
)
|
||||
)
|
||||
.values(
|
||||
"first_name", "last_name", "assignee_id", "avatar_url", "display_name"
|
||||
)
|
||||
.annotate(
|
||||
total_issues=Count(
|
||||
"id", filter=Q(archived_at__isnull=True, is_draft=False)
|
||||
)
|
||||
)
|
||||
.values("first_name", "last_name", "assignee_id", "avatar_url", "display_name")
|
||||
.annotate(total_issues=Count("id", filter=Q(archived_at__isnull=True, is_draft=False)))
|
||||
.annotate(
|
||||
completed_issues=Count(
|
||||
"id",
|
||||
@ -651,11 +595,7 @@ class ModuleViewSet(BaseViewSet):
|
||||
.annotate(color=F("labels__color"))
|
||||
.annotate(label_id=F("labels__id"))
|
||||
.values("label_name", "color", "label_id")
|
||||
.annotate(
|
||||
total_issues=Count(
|
||||
"id", filter=Q(archived_at__isnull=True, is_draft=False)
|
||||
)
|
||||
)
|
||||
.annotate(total_issues=Count("id", filter=Q(archived_at__isnull=True, is_draft=False)))
|
||||
.annotate(
|
||||
completed_issues=Count(
|
||||
"id",
|
||||
@ -685,12 +625,7 @@ class ModuleViewSet(BaseViewSet):
|
||||
"completion_chart": {},
|
||||
}
|
||||
|
||||
if (
|
||||
modules
|
||||
and modules.start_date
|
||||
and modules.target_date
|
||||
and modules.total_issues > 0
|
||||
):
|
||||
if modules and modules.start_date and modules.target_date and modules.total_issues > 0:
|
||||
data["distribution"]["completion_chart"] = burndown_plot(
|
||||
queryset=modules,
|
||||
slug=slug,
|
||||
@ -726,12 +661,8 @@ class ModuleViewSet(BaseViewSet):
|
||||
{"error": "Archived module cannot be updated"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
current_instance = json.dumps(
|
||||
ModuleSerializer(current_module).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
serializer = ModuleWriteSerializer(
|
||||
current_module, data=request.data, partial=True
|
||||
)
|
||||
current_instance = json.dumps(ModuleSerializer(current_module).data, cls=DjangoJSONEncoder)
|
||||
serializer = ModuleWriteSerializer(current_module, data=request.data, partial=True)
|
||||
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
@ -781,9 +712,7 @@ class ModuleViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
datetime_fields = ["created_at", "updated_at"]
|
||||
module = user_timezone_converter(
|
||||
module, datetime_fields, request.user.user_timezone
|
||||
)
|
||||
module = user_timezone_converter(module, datetime_fields, request.user.user_timezone)
|
||||
return Response(module, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@ -791,9 +720,7 @@ class ModuleViewSet(BaseViewSet):
|
||||
def destroy(self, request, slug, project_id, pk):
|
||||
module = Module.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
|
||||
|
||||
module_issues = list(
|
||||
ModuleIssue.objects.filter(module_id=pk).values_list("issue", flat=True)
|
||||
)
|
||||
module_issues = list(ModuleIssue.objects.filter(module_id=pk).values_list("issue", flat=True))
|
||||
_ = [
|
||||
issue_activity.delay(
|
||||
type="module.activity.deleted",
|
||||
@ -901,15 +828,9 @@ class ModuleUserPropertiesEndpoint(BaseAPIView):
|
||||
workspace__slug=slug,
|
||||
)
|
||||
|
||||
module_properties.filters = request.data.get(
|
||||
"filters", module_properties.filters
|
||||
)
|
||||
module_properties.rich_filters = request.data.get(
|
||||
"rich_filters", module_properties.rich_filters
|
||||
)
|
||||
module_properties.display_filters = request.data.get(
|
||||
"display_filters", module_properties.display_filters
|
||||
)
|
||||
module_properties.filters = request.data.get("filters", module_properties.filters)
|
||||
module_properties.rich_filters = request.data.get("rich_filters", module_properties.rich_filters)
|
||||
module_properties.display_filters = request.data.get("display_filters", module_properties.display_filters)
|
||||
module_properties.display_properties = request.data.get(
|
||||
"display_properties", module_properties.display_properties
|
||||
)
|
||||
|
||||
@ -50,9 +50,7 @@ class ModuleIssueViewSet(BaseViewSet):
|
||||
return (
|
||||
issues.annotate(
|
||||
cycle_id=Subquery(
|
||||
CycleIssue.objects.filter(
|
||||
issue=OuterRef("id"), deleted_at__isnull=True
|
||||
).values("cycle_id")[:1]
|
||||
CycleIssue.objects.filter(issue=OuterRef("id"), deleted_at__isnull=True).values("cycle_id")[:1]
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
@ -119,18 +117,14 @@ class ModuleIssueViewSet(BaseViewSet):
|
||||
sub_group_by = request.GET.get("sub_group_by", False)
|
||||
|
||||
# issue queryset
|
||||
issue_queryset = issue_queryset_grouper(
|
||||
queryset=issue_queryset, group_by=group_by, sub_group_by=sub_group_by
|
||||
)
|
||||
issue_queryset = issue_queryset_grouper(queryset=issue_queryset, group_by=group_by, sub_group_by=sub_group_by)
|
||||
|
||||
if group_by:
|
||||
# Check group and sub group value paginate
|
||||
if sub_group_by:
|
||||
if group_by == sub_group_by:
|
||||
return Response(
|
||||
{
|
||||
"error": "Group by and sub group by cannot have same parameters"
|
||||
},
|
||||
{"error": "Group by and sub group by cannot have same parameters"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
else:
|
||||
@ -205,9 +199,7 @@ class ModuleIssueViewSet(BaseViewSet):
|
||||
request=request,
|
||||
queryset=issue_queryset,
|
||||
total_count_queryset=total_issue_queryset,
|
||||
on_results=lambda issues: issue_on_results(
|
||||
group_by=group_by, issues=issues, sub_group_by=sub_group_by
|
||||
),
|
||||
on_results=lambda issues: issue_on_results(group_by=group_by, issues=issues, sub_group_by=sub_group_by),
|
||||
)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
@ -215,9 +207,7 @@ class ModuleIssueViewSet(BaseViewSet):
|
||||
def create_module_issues(self, request, slug, project_id, module_id):
|
||||
issues = request.data.get("issues", [])
|
||||
if not issues:
|
||||
return Response(
|
||||
{"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
return Response({"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
project = Project.objects.get(pk=project_id)
|
||||
_ = ModuleIssue.objects.bulk_create(
|
||||
[
|
||||
@ -334,9 +324,7 @@ class ModuleIssueViewSet(BaseViewSet):
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(issue_id),
|
||||
project_id=str(project_id),
|
||||
current_instance=json.dumps(
|
||||
{"module_name": module_issue.first().module.name}
|
||||
),
|
||||
current_instance=json.dumps({"module_name": module_issue.first().module.name}),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
|
||||
@ -40,9 +40,7 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
||||
.select_related("workspace", "project", "triggered_by", "receiver")
|
||||
)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
|
||||
)
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def list(self, request, slug):
|
||||
# Get query parameters
|
||||
snoozed = request.GET.get("snoozed", "false")
|
||||
@ -59,9 +57,7 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
||||
)
|
||||
|
||||
notifications = (
|
||||
Notification.objects.filter(
|
||||
workspace__slug=slug, receiver_id=request.user.id
|
||||
)
|
||||
Notification.objects.filter(workspace__slug=slug, receiver_id=request.user.id)
|
||||
.filter(entity_name="issue")
|
||||
.annotate(is_inbox_issue=Exists(intake_issue))
|
||||
.annotate(is_intake_issue=Exists(intake_issue))
|
||||
@ -106,23 +102,9 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
||||
# Subscribed issues
|
||||
if "subscribed" in type:
|
||||
issue_ids = (
|
||||
IssueSubscriber.objects.filter(
|
||||
workspace__slug=slug, subscriber_id=request.user.id
|
||||
)
|
||||
.annotate(
|
||||
created=Exists(
|
||||
Issue.objects.filter(
|
||||
created_by=request.user, pk=OuterRef("issue_id")
|
||||
)
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
assigned=Exists(
|
||||
IssueAssignee.objects.filter(
|
||||
pk=OuterRef("issue_id"), assignee=request.user
|
||||
)
|
||||
)
|
||||
)
|
||||
IssueSubscriber.objects.filter(workspace__slug=slug, subscriber_id=request.user.id)
|
||||
.annotate(created=Exists(Issue.objects.filter(created_by=request.user, pk=OuterRef("issue_id"))))
|
||||
.annotate(assigned=Exists(IssueAssignee.objects.filter(pk=OuterRef("issue_id"), assignee=request.user)))
|
||||
.filter(created=False, assigned=False)
|
||||
.values_list("issue_id", flat=True)
|
||||
)
|
||||
@ -130,9 +112,9 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
||||
|
||||
# Assigned Issues
|
||||
if "assigned" in type:
|
||||
issue_ids = IssueAssignee.objects.filter(
|
||||
workspace__slug=slug, assignee_id=request.user.id
|
||||
).values_list("issue_id", flat=True)
|
||||
issue_ids = IssueAssignee.objects.filter(workspace__slug=slug, assignee_id=request.user.id).values_list(
|
||||
"issue_id", flat=True
|
||||
)
|
||||
q_filters |= Q(entity_identifier__in=issue_ids)
|
||||
|
||||
# Created issues
|
||||
@ -142,9 +124,9 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
||||
).exists():
|
||||
notifications = notifications.none()
|
||||
else:
|
||||
issue_ids = Issue.objects.filter(
|
||||
workspace__slug=slug, created_by=request.user
|
||||
).values_list("pk", flat=True)
|
||||
issue_ids = Issue.objects.filter(workspace__slug=slug, created_by=request.user).values_list(
|
||||
"pk", flat=True
|
||||
)
|
||||
q_filters |= Q(entity_identifier__in=issue_ids)
|
||||
|
||||
# Apply the combined Q object filters
|
||||
@ -156,75 +138,51 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
||||
order_by=request.GET.get("order_by", "-created_at"),
|
||||
request=request,
|
||||
queryset=(notifications),
|
||||
on_results=lambda notifications: NotificationSerializer(
|
||||
notifications, many=True
|
||||
).data,
|
||||
on_results=lambda notifications: NotificationSerializer(notifications, many=True).data,
|
||||
)
|
||||
|
||||
serializer = NotificationSerializer(notifications, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
|
||||
)
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def partial_update(self, request, slug, pk):
|
||||
notification = Notification.objects.get(
|
||||
workspace__slug=slug, pk=pk, receiver=request.user
|
||||
)
|
||||
notification = Notification.objects.get(workspace__slug=slug, pk=pk, receiver=request.user)
|
||||
# Only read_at and snoozed_till can be updated
|
||||
notification_data = {"snoozed_till": request.data.get("snoozed_till", None)}
|
||||
serializer = NotificationSerializer(
|
||||
notification, data=notification_data, partial=True
|
||||
)
|
||||
serializer = NotificationSerializer(notification, data=notification_data, partial=True)
|
||||
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
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, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
|
||||
)
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def mark_read(self, request, slug, pk):
|
||||
notification = Notification.objects.get(
|
||||
receiver=request.user, workspace__slug=slug, pk=pk
|
||||
)
|
||||
notification = Notification.objects.get(receiver=request.user, workspace__slug=slug, pk=pk)
|
||||
notification.read_at = timezone.now()
|
||||
notification.save()
|
||||
serializer = NotificationSerializer(notification)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
|
||||
)
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def mark_unread(self, request, slug, pk):
|
||||
notification = Notification.objects.get(
|
||||
receiver=request.user, workspace__slug=slug, pk=pk
|
||||
)
|
||||
notification = Notification.objects.get(receiver=request.user, workspace__slug=slug, pk=pk)
|
||||
notification.read_at = None
|
||||
notification.save()
|
||||
serializer = NotificationSerializer(notification)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
|
||||
)
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def archive(self, request, slug, pk):
|
||||
notification = Notification.objects.get(
|
||||
receiver=request.user, workspace__slug=slug, pk=pk
|
||||
)
|
||||
notification = Notification.objects.get(receiver=request.user, workspace__slug=slug, pk=pk)
|
||||
notification.archived_at = timezone.now()
|
||||
notification.save()
|
||||
serializer = NotificationSerializer(notification)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
|
||||
)
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def unarchive(self, request, slug, pk):
|
||||
notification = Notification.objects.get(
|
||||
receiver=request.user, workspace__slug=slug, pk=pk
|
||||
)
|
||||
notification = Notification.objects.get(receiver=request.user, workspace__slug=slug, pk=pk)
|
||||
notification.archived_at = None
|
||||
notification.save()
|
||||
serializer = NotificationSerializer(notification)
|
||||
@ -234,9 +192,7 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
|
||||
class UnreadNotificationEndpoint(BaseAPIView):
|
||||
use_read_replica = True
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
|
||||
)
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def get(self, request, slug):
|
||||
# Watching Issues Count
|
||||
unread_notifications_count = (
|
||||
@ -270,31 +226,23 @@ class UnreadNotificationEndpoint(BaseAPIView):
|
||||
|
||||
|
||||
class MarkAllReadNotificationViewSet(BaseViewSet):
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
|
||||
)
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def create(self, request, slug):
|
||||
snoozed = request.data.get("snoozed", False)
|
||||
archived = request.data.get("archived", False)
|
||||
type = request.data.get("type", "all")
|
||||
|
||||
notifications = (
|
||||
Notification.objects.filter(
|
||||
workspace__slug=slug, receiver_id=request.user.id, read_at__isnull=True
|
||||
)
|
||||
Notification.objects.filter(workspace__slug=slug, receiver_id=request.user.id, read_at__isnull=True)
|
||||
.select_related("workspace", "project", "triggered_by", "receiver")
|
||||
.order_by("snoozed_till", "-created_at")
|
||||
)
|
||||
|
||||
# Filter for snoozed notifications
|
||||
if snoozed:
|
||||
notifications = notifications.filter(
|
||||
Q(snoozed_till__lt=timezone.now()) | Q(snoozed_till__isnull=False)
|
||||
)
|
||||
notifications = notifications.filter(Q(snoozed_till__lt=timezone.now()) | Q(snoozed_till__isnull=False))
|
||||
else:
|
||||
notifications = notifications.filter(
|
||||
Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True)
|
||||
)
|
||||
notifications = notifications.filter(Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True))
|
||||
|
||||
# Filter for archived or unarchive
|
||||
if archived:
|
||||
@ -304,16 +252,16 @@ class MarkAllReadNotificationViewSet(BaseViewSet):
|
||||
|
||||
# Subscribed issues
|
||||
if type == "watching":
|
||||
issue_ids = IssueSubscriber.objects.filter(
|
||||
workspace__slug=slug, subscriber_id=request.user.id
|
||||
).values_list("issue_id", flat=True)
|
||||
issue_ids = IssueSubscriber.objects.filter(workspace__slug=slug, subscriber_id=request.user.id).values_list(
|
||||
"issue_id", flat=True
|
||||
)
|
||||
notifications = notifications.filter(entity_identifier__in=issue_ids)
|
||||
|
||||
# Assigned Issues
|
||||
if type == "assigned":
|
||||
issue_ids = IssueAssignee.objects.filter(
|
||||
workspace__slug=slug, assignee_id=request.user.id
|
||||
).values_list("issue_id", flat=True)
|
||||
issue_ids = IssueAssignee.objects.filter(workspace__slug=slug, assignee_id=request.user.id).values_list(
|
||||
"issue_id", flat=True
|
||||
)
|
||||
notifications = notifications.filter(entity_identifier__in=issue_ids)
|
||||
|
||||
# Created issues
|
||||
@ -323,18 +271,16 @@ class MarkAllReadNotificationViewSet(BaseViewSet):
|
||||
).exists():
|
||||
notifications = Notification.objects.none()
|
||||
else:
|
||||
issue_ids = Issue.objects.filter(
|
||||
workspace__slug=slug, created_by=request.user
|
||||
).values_list("pk", flat=True)
|
||||
issue_ids = Issue.objects.filter(workspace__slug=slug, created_by=request.user).values_list(
|
||||
"pk", flat=True
|
||||
)
|
||||
notifications = notifications.filter(entity_identifier__in=issue_ids)
|
||||
|
||||
updated_notifications = []
|
||||
for notification in notifications:
|
||||
notification.read_at = timezone.now()
|
||||
updated_notifications.append(notification)
|
||||
Notification.objects.bulk_update(
|
||||
updated_notifications, ["read_at"], batch_size=100
|
||||
)
|
||||
Notification.objects.bulk_update(updated_notifications, ["read_at"], batch_size=100)
|
||||
return Response({"message": "Successful"}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
@ -344,20 +290,14 @@ class UserNotificationPreferenceEndpoint(BaseAPIView):
|
||||
|
||||
# request the object
|
||||
def get(self, request):
|
||||
user_notification_preference = UserNotificationPreference.objects.get(
|
||||
user=request.user
|
||||
)
|
||||
user_notification_preference = UserNotificationPreference.objects.get(user=request.user)
|
||||
serializer = UserNotificationPreferenceSerializer(user_notification_preference)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
# update the object
|
||||
def patch(self, request):
|
||||
user_notification_preference = UserNotificationPreference.objects.get(
|
||||
user=request.user
|
||||
)
|
||||
serializer = UserNotificationPreferenceSerializer(
|
||||
user_notification_preference, data=request.data, partial=True
|
||||
)
|
||||
user_notification_preference = UserNotificationPreference.objects.get(user=request.user)
|
||||
serializer = UserNotificationPreferenceSerializer(user_notification_preference, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@ -101,9 +101,7 @@ class PageViewSet(BaseViewSet):
|
||||
.order_by("-is_favorite", "-created_at")
|
||||
.annotate(
|
||||
project=Exists(
|
||||
ProjectPage.objects.filter(
|
||||
page_id=OuterRef("id"), project_id=self.kwargs.get("project_id")
|
||||
)
|
||||
ProjectPage.objects.filter(page_id=OuterRef("id"), project_id=self.kwargs.get("project_id"))
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
@ -116,9 +114,7 @@ class PageViewSet(BaseViewSet):
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
project_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"projects__id", distinct=True, filter=~Q(projects__id=True)
|
||||
),
|
||||
ArrayAgg("projects__id", distinct=True, filter=~Q(projects__id=True)),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
)
|
||||
@ -149,30 +145,19 @@ class PageViewSet(BaseViewSet):
|
||||
|
||||
def partial_update(self, request, slug, project_id, page_id):
|
||||
try:
|
||||
page = Page.objects.get(
|
||||
pk=page_id, workspace__slug=slug, projects__id=project_id
|
||||
)
|
||||
page = Page.objects.get(pk=page_id, workspace__slug=slug, projects__id=project_id)
|
||||
|
||||
if page.is_locked:
|
||||
return Response(
|
||||
{"error": "Page is locked"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
return Response({"error": "Page is locked"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
parent = request.data.get("parent", None)
|
||||
if parent:
|
||||
_ = Page.objects.get(
|
||||
pk=parent, workspace__slug=slug, projects__id=project_id
|
||||
)
|
||||
_ = Page.objects.get(pk=parent, workspace__slug=slug, projects__id=project_id)
|
||||
|
||||
# Only update access if the page owner is the requesting user
|
||||
if (
|
||||
page.access != request.data.get("access", page.access)
|
||||
and page.owned_by_id != request.user.id
|
||||
):
|
||||
if page.access != request.data.get("access", page.access) and page.owned_by_id != request.user.id:
|
||||
return Response(
|
||||
{
|
||||
"error": "Access cannot be updated since this page is owned by someone else"
|
||||
},
|
||||
{"error": "Access cannot be updated since this page is owned by someone else"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@ -195,9 +180,7 @@ class PageViewSet(BaseViewSet):
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
except Page.DoesNotExist:
|
||||
return Response(
|
||||
{
|
||||
"error": "Access cannot be updated since this page is owned by someone else"
|
||||
},
|
||||
{"error": "Access cannot be updated since this page is owned by someone else"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@ -228,13 +211,11 @@ class PageViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
if page is None:
|
||||
return Response(
|
||||
{"error": "Page not found"}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
return Response({"error": "Page not found"}, status=status.HTTP_404_NOT_FOUND)
|
||||
else:
|
||||
issue_ids = PageLog.objects.filter(
|
||||
page_id=page_id, entity_name="issue"
|
||||
).values_list("entity_identifier", flat=True)
|
||||
issue_ids = PageLog.objects.filter(page_id=page_id, entity_name="issue").values_list(
|
||||
"entity_identifier", flat=True
|
||||
)
|
||||
data = PageDetailSerializer(page).data
|
||||
data["issue_ids"] = issue_ids
|
||||
if track_visit:
|
||||
@ -248,18 +229,14 @@ class PageViewSet(BaseViewSet):
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
|
||||
def lock(self, request, slug, project_id, page_id):
|
||||
page = Page.objects.filter(
|
||||
pk=page_id, workspace__slug=slug, projects__id=project_id
|
||||
).first()
|
||||
page = Page.objects.filter(pk=page_id, workspace__slug=slug, projects__id=project_id).first()
|
||||
|
||||
page.is_locked = True
|
||||
page.save()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
def unlock(self, request, slug, project_id, page_id):
|
||||
page = Page.objects.filter(
|
||||
pk=page_id, workspace__slug=slug, projects__id=project_id
|
||||
).first()
|
||||
page = Page.objects.filter(pk=page_id, workspace__slug=slug, projects__id=project_id).first()
|
||||
|
||||
page.is_locked = False
|
||||
page.save()
|
||||
@ -268,19 +245,12 @@ class PageViewSet(BaseViewSet):
|
||||
|
||||
def access(self, request, slug, project_id, page_id):
|
||||
access = request.data.get("access", 0)
|
||||
page = Page.objects.filter(
|
||||
pk=page_id, workspace__slug=slug, projects__id=project_id
|
||||
).first()
|
||||
page = Page.objects.filter(pk=page_id, workspace__slug=slug, projects__id=project_id).first()
|
||||
|
||||
# Only update access if the page owner is the requesting user
|
||||
if (
|
||||
page.access != request.data.get("access", page.access)
|
||||
and page.owned_by_id != request.user.id
|
||||
):
|
||||
if page.access != request.data.get("access", page.access) and page.owned_by_id != request.user.id:
|
||||
return Response(
|
||||
{
|
||||
"error": "Access cannot be updated since this page is owned by someone else"
|
||||
},
|
||||
{"error": "Access cannot be updated since this page is owned by someone else"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@ -306,9 +276,7 @@ class PageViewSet(BaseViewSet):
|
||||
return Response(pages, status=status.HTTP_200_OK)
|
||||
|
||||
def archive(self, request, slug, project_id, page_id):
|
||||
page = Page.objects.get(
|
||||
pk=page_id, workspace__slug=slug, projects__id=project_id
|
||||
)
|
||||
page = Page.objects.get(pk=page_id, workspace__slug=slug, projects__id=project_id)
|
||||
|
||||
# only the owner or admin can archive the page
|
||||
if (
|
||||
@ -334,9 +302,7 @@ class PageViewSet(BaseViewSet):
|
||||
return Response({"archived_at": str(datetime.now())}, status=status.HTTP_200_OK)
|
||||
|
||||
def unarchive(self, request, slug, project_id, page_id):
|
||||
page = Page.objects.get(
|
||||
pk=page_id, workspace__slug=slug, projects__id=project_id
|
||||
)
|
||||
page = Page.objects.get(pk=page_id, workspace__slug=slug, projects__id=project_id)
|
||||
|
||||
# only the owner or admin can un archive the page
|
||||
if (
|
||||
@ -360,9 +326,7 @@ class PageViewSet(BaseViewSet):
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
def destroy(self, request, slug, project_id, page_id):
|
||||
page = Page.objects.get(
|
||||
pk=page_id, workspace__slug=slug, projects__id=project_id
|
||||
)
|
||||
page = Page.objects.get(pk=page_id, workspace__slug=slug, projects__id=project_id)
|
||||
|
||||
if page.archived_at is None:
|
||||
return Response(
|
||||
@ -385,9 +349,7 @@ class PageViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
# remove parent from all the children
|
||||
_ = Page.objects.filter(
|
||||
parent_id=page_id, projects__id=project_id, workspace__slug=slug
|
||||
).update(parent=None)
|
||||
_ = Page.objects.filter(parent_id=page_id, projects__id=project_id, workspace__slug=slug).update(parent=None)
|
||||
|
||||
page.delete()
|
||||
# Delete the user favorite page
|
||||
@ -418,9 +380,7 @@ class PageViewSet(BaseViewSet):
|
||||
.filter(Q(owned_by=request.user) | Q(access=0))
|
||||
.annotate(
|
||||
project=Exists(
|
||||
ProjectPage.objects.filter(
|
||||
page_id=OuterRef("id"), project_id=self.kwargs.get("project_id")
|
||||
)
|
||||
ProjectPage.objects.filter(page_id=OuterRef("id"), project_id=self.kwargs.get("project_id"))
|
||||
)
|
||||
)
|
||||
.filter(project=True)
|
||||
@ -453,11 +413,7 @@ class PageViewSet(BaseViewSet):
|
||||
output_field=IntegerField(),
|
||||
)
|
||||
),
|
||||
archived_pages=Count(
|
||||
Case(
|
||||
When(archived_at__isnull=False, then=1), output_field=IntegerField()
|
||||
)
|
||||
),
|
||||
archived_pages=Count(Case(When(archived_at__isnull=False, then=1), output_field=IntegerField())),
|
||||
)
|
||||
|
||||
return Response(stats, status=status.HTTP_200_OK)
|
||||
@ -494,9 +450,7 @@ class PagesDescriptionViewSet(BaseViewSet):
|
||||
|
||||
def retrieve(self, request, slug, project_id, page_id):
|
||||
page = (
|
||||
Page.objects.filter(
|
||||
pk=page_id, workspace__slug=slug, projects__id=project_id
|
||||
)
|
||||
Page.objects.filter(pk=page_id, workspace__slug=slug, projects__id=project_id)
|
||||
.filter(Q(owned_by=self.request.user) | Q(access=0))
|
||||
.first()
|
||||
)
|
||||
@ -510,17 +464,13 @@ class PagesDescriptionViewSet(BaseViewSet):
|
||||
else:
|
||||
yield b""
|
||||
|
||||
response = StreamingHttpResponse(
|
||||
stream_data(), content_type="application/octet-stream"
|
||||
)
|
||||
response = StreamingHttpResponse(stream_data(), content_type="application/octet-stream")
|
||||
response["Content-Disposition"] = 'attachment; filename="page_description.bin"'
|
||||
return response
|
||||
|
||||
def partial_update(self, request, slug, project_id, page_id):
|
||||
page = (
|
||||
Page.objects.filter(
|
||||
pk=page_id, workspace__slug=slug, projects__id=project_id
|
||||
)
|
||||
Page.objects.filter(pk=page_id, workspace__slug=slug, projects__id=project_id)
|
||||
.filter(Q(owned_by=self.request.user) | Q(access=0))
|
||||
.first()
|
||||
)
|
||||
@ -547,18 +497,14 @@ class PagesDescriptionViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
# Serialize the existing instance
|
||||
existing_instance = json.dumps(
|
||||
{"description_html": page.description_html}, cls=DjangoJSONEncoder
|
||||
)
|
||||
existing_instance = json.dumps({"description_html": page.description_html}, cls=DjangoJSONEncoder)
|
||||
|
||||
# Use serializer for validation and update
|
||||
serializer = PageBinaryUpdateSerializer(page, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
# Capture the page transaction
|
||||
if request.data.get("description_html"):
|
||||
page_transaction.delay(
|
||||
new_value=request.data, old_value=existing_instance, page_id=page_id
|
||||
)
|
||||
page_transaction.delay(new_value=request.data, old_value=existing_instance, page_id=page_id)
|
||||
|
||||
# Update the page using serializer
|
||||
updated_page = serializer.save()
|
||||
@ -578,20 +524,14 @@ class PageDuplicateEndpoint(BaseAPIView):
|
||||
permission_classes = [ProjectPagePermission]
|
||||
|
||||
def post(self, request, slug, project_id, page_id):
|
||||
page = Page.objects.filter(
|
||||
pk=page_id, workspace__slug=slug, projects__id=project_id
|
||||
).first()
|
||||
page = Page.objects.filter(pk=page_id, workspace__slug=slug, projects__id=project_id).first()
|
||||
|
||||
# check for permission
|
||||
if page.access == Page.PRIVATE_ACCESS and page.owned_by_id != request.user.id:
|
||||
return Response(
|
||||
{"error": "Permission denied"}, status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
return Response({"error": "Permission denied"}, status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
# get all the project ids where page is present
|
||||
project_ids = ProjectPage.objects.filter(page_id=page_id).values_list(
|
||||
"project_id", flat=True
|
||||
)
|
||||
project_ids = ProjectPage.objects.filter(page_id=page_id).values_list("project_id", flat=True)
|
||||
|
||||
page.pk = None
|
||||
page.name = f"{page.name} (Copy)"
|
||||
@ -610,9 +550,7 @@ class PageDuplicateEndpoint(BaseAPIView):
|
||||
updated_by_id=page.updated_by_id,
|
||||
)
|
||||
|
||||
page_transaction.delay(
|
||||
{"description_html": page.description_html}, None, page.id
|
||||
)
|
||||
page_transaction.delay({"description_html": page.description_html}, None, page.id)
|
||||
|
||||
# Copy the s3 objects uploaded in the page
|
||||
copy_s3_objects_of_description_and_assets.delay(
|
||||
@ -627,9 +565,7 @@ class PageDuplicateEndpoint(BaseAPIView):
|
||||
Page.objects.filter(pk=page.id)
|
||||
.annotate(
|
||||
project_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"projects__id", distinct=True, filter=~Q(projects__id=True)
|
||||
),
|
||||
ArrayAgg("projects__id", distinct=True, filter=~Q(projects__id=True)),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
)
|
||||
)
|
||||
|
||||
@ -6,27 +6,22 @@ from rest_framework.response import Response
|
||||
from plane.db.models import PageVersion
|
||||
from ..base import BaseAPIView
|
||||
from plane.app.serializers import PageVersionSerializer, PageVersionDetailSerializer
|
||||
from plane.app.permissions import allow_permission, ROLE
|
||||
from plane.app.permissions import ProjectPagePermission
|
||||
|
||||
class PageVersionEndpoint(BaseAPIView):
|
||||
|
||||
class PageVersionEndpoint(BaseAPIView):
|
||||
permission_classes = [ProjectPagePermission]
|
||||
|
||||
def get(self, request, slug, project_id, page_id, pk=None):
|
||||
# Check if pk is provided
|
||||
if pk:
|
||||
# Return a single page version
|
||||
page_version = PageVersion.objects.get(
|
||||
workspace__slug=slug, page_id=page_id, pk=pk
|
||||
)
|
||||
page_version = PageVersion.objects.get(workspace__slug=slug, page_id=page_id, pk=pk)
|
||||
# Serialize the page version
|
||||
serializer = PageVersionDetailSerializer(page_version)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
# Return all page versions
|
||||
page_versions = PageVersion.objects.filter(
|
||||
workspace__slug=slug, page_id=page_id
|
||||
)
|
||||
page_versions = PageVersion.objects.filter(workspace__slug=slug, page_id=page_id)
|
||||
# Serialize the page versions
|
||||
serializer = PageVersionSerializer(page_versions, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@ -58,9 +58,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.select_related(
|
||||
"workspace", "workspace__owner", "default_assignee", "project_lead"
|
||||
)
|
||||
.select_related("workspace", "workspace__owner", "default_assignee", "project_lead")
|
||||
.annotate(
|
||||
is_favorite=Exists(
|
||||
UserFavorite.objects.filter(
|
||||
@ -98,9 +96,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
.distinct()
|
||||
)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
|
||||
)
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def list_detail(self, request, slug):
|
||||
fields = [field for field in request.GET.get("fields", "").split(",") if field]
|
||||
projects = self.get_queryset().order_by("sort_order", "name")
|
||||
@ -134,19 +130,13 @@ class ProjectViewSet(BaseViewSet):
|
||||
order_by=request.GET.get("order_by", "-created_at"),
|
||||
request=request,
|
||||
queryset=(projects),
|
||||
on_results=lambda projects: ProjectListSerializer(
|
||||
projects, many=True
|
||||
).data,
|
||||
on_results=lambda projects: ProjectListSerializer(projects, many=True).data,
|
||||
)
|
||||
|
||||
projects = ProjectListSerializer(
|
||||
projects, many=True, fields=fields if fields else None
|
||||
).data
|
||||
projects = ProjectListSerializer(projects, many=True, fields=fields if fields else None).data
|
||||
return Response(projects, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
|
||||
)
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def list(self, request, slug):
|
||||
sort_order = ProjectMember.objects.filter(
|
||||
member=self.request.user,
|
||||
@ -157,9 +147,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
|
||||
projects = (
|
||||
Project.objects.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.select_related(
|
||||
"workspace", "workspace__owner", "default_assignee", "project_lead"
|
||||
)
|
||||
.select_related("workspace", "workspace__owner", "default_assignee", "project_lead")
|
||||
.annotate(
|
||||
member_role=ProjectMember.objects.filter(
|
||||
project_id=OuterRef("pk"),
|
||||
@ -219,9 +207,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
)
|
||||
return Response(projects, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
|
||||
)
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def retrieve(self, request, slug, pk):
|
||||
project = (
|
||||
self.get_queryset()
|
||||
@ -234,9 +220,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
).first()
|
||||
|
||||
if project is None:
|
||||
return Response(
|
||||
{"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
return Response({"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
recent_visited_task.delay(
|
||||
slug=slug,
|
||||
@ -253,9 +237,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
def create(self, request, slug):
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
|
||||
serializer = ProjectSerializer(
|
||||
data={**request.data}, context={"workspace_id": workspace.id}
|
||||
)
|
||||
serializer = ProjectSerializer(data={**request.data}, context={"workspace_id": workspace.id})
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
|
||||
@ -266,13 +248,11 @@ class ProjectViewSet(BaseViewSet):
|
||||
role=ROLE.ADMIN.value,
|
||||
)
|
||||
# Also create the issue property for the user
|
||||
_ = IssueUserProperty.objects.create(
|
||||
project_id=serializer.data["id"], user=request.user
|
||||
)
|
||||
_ = IssueUserProperty.objects.create(project_id=serializer.data["id"], user=request.user)
|
||||
|
||||
if serializer.data["project_lead"] is not None and str(
|
||||
serializer.data["project_lead"]
|
||||
) != str(request.user.id):
|
||||
if serializer.data["project_lead"] is not None and str(serializer.data["project_lead"]) != str(
|
||||
request.user.id
|
||||
):
|
||||
ProjectMember.objects.create(
|
||||
project_id=serializer.data["id"],
|
||||
member_id=serializer.data["project_lead"],
|
||||
@ -380,9 +360,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
|
||||
project = Project.objects.get(pk=pk)
|
||||
intake_view = request.data.get("inbox_view", project.intake_view)
|
||||
current_instance = json.dumps(
|
||||
ProjectSerializer(project).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
current_instance = json.dumps(ProjectSerializer(project).data, cls=DjangoJSONEncoder)
|
||||
if project.archived_at:
|
||||
return Response(
|
||||
{"error": "Archived projects cannot be updated"},
|
||||
@ -474,9 +452,7 @@ class ProjectArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
project.archived_at = timezone.now()
|
||||
project.save()
|
||||
UserFavorite.objects.filter(workspace__slug=slug, project=project_id).delete()
|
||||
return Response(
|
||||
{"archived_at": str(project.archived_at)}, status=status.HTTP_200_OK
|
||||
)
|
||||
return Response({"archived_at": str(project.archived_at)}, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||
def delete(self, request, slug, project_id):
|
||||
@ -492,26 +468,18 @@ class ProjectIdentifierEndpoint(BaseAPIView):
|
||||
name = request.GET.get("name", "").strip().upper()
|
||||
|
||||
if name == "":
|
||||
return Response(
|
||||
{"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
return Response({"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
exists = ProjectIdentifier.objects.filter(
|
||||
name=name, workspace__slug=slug
|
||||
).values("id", "name", "project")
|
||||
exists = ProjectIdentifier.objects.filter(name=name, workspace__slug=slug).values("id", "name", "project")
|
||||
|
||||
return Response(
|
||||
{"exists": len(exists), "identifiers": exists}, status=status.HTTP_200_OK
|
||||
)
|
||||
return Response({"exists": len(exists), "identifiers": exists}, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
|
||||
def delete(self, request, slug):
|
||||
name = request.data.get("name", "").strip().upper()
|
||||
|
||||
if name == "":
|
||||
return Response(
|
||||
{"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
return Response({"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if Project.objects.filter(identifier=name, workspace__slug=slug).exists():
|
||||
return Response(
|
||||
@ -528,9 +496,7 @@ class ProjectUserViewsEndpoint(BaseAPIView):
|
||||
def post(self, request, slug, project_id):
|
||||
project = Project.objects.get(pk=project_id, workspace__slug=slug)
|
||||
|
||||
project_member = ProjectMember.objects.filter(
|
||||
member=request.user, project=project, is_active=True
|
||||
).first()
|
||||
project_member = ProjectMember.objects.filter(member=request.user, project=project, is_active=True).first()
|
||||
|
||||
if project_member is None:
|
||||
return Response({"error": "Forbidden"}, status=status.HTTP_403_FORBIDDEN)
|
||||
@ -559,9 +525,7 @@ class ProjectFavoritesViewSet(BaseViewSet):
|
||||
.get_queryset()
|
||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.filter(user=self.request.user)
|
||||
.select_related(
|
||||
"project", "project__project_lead", "project__default_assignee"
|
||||
)
|
||||
.select_related("project", "project__project_lead", "project__default_assignee")
|
||||
.select_related("workspace", "workspace__owner")
|
||||
)
|
||||
|
||||
|
||||
@ -52,9 +52,7 @@ class ProjectInvitationsViewset(BaseViewSet):
|
||||
|
||||
# Check if email is provided
|
||||
if not emails:
|
||||
return Response(
|
||||
{"error": "Emails are required"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
return Response({"error": "Emails are required"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
for email in emails:
|
||||
workspace_role = WorkspaceMember.objects.filter(
|
||||
@ -62,11 +60,7 @@ class ProjectInvitationsViewset(BaseViewSet):
|
||||
).role
|
||||
|
||||
if workspace_role in [5, 20] and workspace_role != email.get("role", 5):
|
||||
return Response(
|
||||
{
|
||||
"error": "You cannot invite a user with different role than workspace role"
|
||||
}
|
||||
)
|
||||
return Response({"error": "You cannot invite a user with different role than workspace role"})
|
||||
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
|
||||
@ -91,7 +85,7 @@ class ProjectInvitationsViewset(BaseViewSet):
|
||||
except ValidationError:
|
||||
return Response(
|
||||
{
|
||||
"error": f"Invalid email - {email} provided a valid email address is required to send the invite"
|
||||
"error": f"Invalid email - {email} provided a valid email address is required to send the invite" # noqa: E501
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
@ -112,9 +106,7 @@ class ProjectInvitationsViewset(BaseViewSet):
|
||||
request.user.email,
|
||||
)
|
||||
|
||||
return Response(
|
||||
{"message": "Email sent successfully"}, status=status.HTTP_200_OK
|
||||
)
|
||||
return Response({"message": "Email sent successfully"}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class UserProjectInvitationsViewset(BaseViewSet):
|
||||
@ -134,20 +126,13 @@ class UserProjectInvitationsViewset(BaseViewSet):
|
||||
project_ids = request.data.get("project_ids", [])
|
||||
|
||||
# Get the workspace user role
|
||||
workspace_member = WorkspaceMember.objects.get(
|
||||
member=request.user, workspace__slug=slug, is_active=True
|
||||
)
|
||||
workspace_member = WorkspaceMember.objects.get(member=request.user, workspace__slug=slug, is_active=True)
|
||||
|
||||
# Get all the projects
|
||||
projects = Project.objects.filter(
|
||||
id__in=project_ids, workspace__slug=slug
|
||||
).only("id", "network")
|
||||
projects = Project.objects.filter(id__in=project_ids, workspace__slug=slug).only("id", "network")
|
||||
# Check if user has permission to join each project
|
||||
for project in projects:
|
||||
if (
|
||||
project.network == ProjectNetwork.SECRET.value
|
||||
and workspace_member.role != ROLE.ADMIN.value
|
||||
):
|
||||
if project.network == ProjectNetwork.SECRET.value and workspace_member.role != ROLE.ADMIN.value:
|
||||
return Response(
|
||||
{"error": "Only workspace admins can join private project"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
@ -157,9 +142,9 @@ class UserProjectInvitationsViewset(BaseViewSet):
|
||||
workspace = workspace_member.workspace
|
||||
|
||||
# If the user was already part of workspace
|
||||
_ = ProjectMember.objects.filter(
|
||||
workspace__slug=slug, project_id__in=project_ids, member=request.user
|
||||
).update(is_active=True)
|
||||
_ = ProjectMember.objects.filter(workspace__slug=slug, project_id__in=project_ids, member=request.user).update(
|
||||
is_active=True
|
||||
)
|
||||
|
||||
ProjectMember.objects.bulk_create(
|
||||
[
|
||||
@ -188,18 +173,14 @@ class UserProjectInvitationsViewset(BaseViewSet):
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
return Response(
|
||||
{"message": "Projects joined successfully"}, status=status.HTTP_201_CREATED
|
||||
)
|
||||
return Response({"message": "Projects joined successfully"}, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
class ProjectJoinEndpoint(BaseAPIView):
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
def post(self, request, slug, project_id, pk):
|
||||
project_invite = ProjectMemberInvite.objects.get(
|
||||
pk=pk, project_id=project_id, workspace__slug=slug
|
||||
)
|
||||
project_invite = ProjectMemberInvite.objects.get(pk=pk, project_id=project_id, workspace__slug=slug)
|
||||
|
||||
email = request.data.get("email", "")
|
||||
|
||||
@ -219,9 +200,7 @@ class ProjectJoinEndpoint(BaseAPIView):
|
||||
user = User.objects.filter(email=email).first()
|
||||
|
||||
# Check if user is a part of workspace
|
||||
workspace_member = WorkspaceMember.objects.filter(
|
||||
workspace__slug=slug, member=user
|
||||
).first()
|
||||
workspace_member = WorkspaceMember.objects.filter(workspace__slug=slug, member=user).first()
|
||||
# Add him to workspace
|
||||
if workspace_member is None:
|
||||
_ = WorkspaceMember.objects.create(
|
||||
@ -266,8 +245,6 @@ class ProjectJoinEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
def get(self, request, slug, project_id, pk):
|
||||
project_invitation = ProjectMemberInvite.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
)
|
||||
project_invitation = ProjectMemberInvite.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
|
||||
serializer = ProjectMemberInviteSerializer(project_invitation)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@ -57,9 +57,7 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
bulk_issue_props = []
|
||||
|
||||
# Create a dictionary of the member_id and their roles
|
||||
member_roles = {
|
||||
member.get("member_id"): member.get("role") for member in members
|
||||
}
|
||||
member_roles = {member.get("member_id"): member.get("role") for member in members}
|
||||
|
||||
# check the workspace role of the new user
|
||||
for member in member_roles:
|
||||
@ -68,17 +66,13 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
).role
|
||||
if workspace_member_role in [20] and member_roles.get(member) in [5, 15]:
|
||||
return Response(
|
||||
{
|
||||
"error": "You cannot add a user with role lower than the workspace role"
|
||||
},
|
||||
{"error": "You cannot add a user with role lower than the workspace role"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if workspace_member_role in [5] and member_roles.get(member) in [15, 20]:
|
||||
return Response(
|
||||
{
|
||||
"error": "You cannot add a user with role higher than the workspace role"
|
||||
},
|
||||
{"error": "You cannot add a user with role higher than the workspace role"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@ -92,9 +86,7 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
bulk_project_members.append(project_member)
|
||||
|
||||
# Update the roles of the existing members
|
||||
ProjectMember.objects.bulk_update(
|
||||
bulk_project_members, ["is_active", "role"], batch_size=100
|
||||
)
|
||||
ProjectMember.objects.bulk_update(bulk_project_members, ["is_active", "role"], batch_size=100)
|
||||
|
||||
# Get the list of project members of the requested workspace with the given slug
|
||||
project_members = (
|
||||
@ -134,13 +126,9 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
# Bulk create the project members and issue properties
|
||||
project_members = ProjectMember.objects.bulk_create(
|
||||
bulk_project_members, batch_size=10, ignore_conflicts=True
|
||||
)
|
||||
project_members = ProjectMember.objects.bulk_create(bulk_project_members, batch_size=10, ignore_conflicts=True)
|
||||
|
||||
_ = IssueUserProperty.objects.bulk_create(
|
||||
bulk_issue_props, batch_size=10, ignore_conflicts=True
|
||||
)
|
||||
_ = IssueUserProperty.objects.bulk_create(bulk_issue_props, batch_size=10, ignore_conflicts=True)
|
||||
|
||||
project_members = ProjectMember.objects.filter(
|
||||
project_id=project_id,
|
||||
@ -172,16 +160,12 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
member__member_workspace__is_active=True,
|
||||
).select_related("project", "member", "workspace")
|
||||
|
||||
serializer = ProjectMemberRoleSerializer(
|
||||
project_members, fields=("id", "member", "role"), many=True
|
||||
)
|
||||
serializer = ProjectMemberRoleSerializer(project_members, fields=("id", "member", "role"), many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def partial_update(self, request, slug, project_id, pk):
|
||||
project_member = ProjectMember.objects.get(
|
||||
pk=pk, workspace__slug=slug, project_id=project_id, is_active=True
|
||||
)
|
||||
project_member = ProjectMember.objects.get(pk=pk, workspace__slug=slug, project_id=project_id, is_active=True)
|
||||
|
||||
# Fetch the workspace role of the project member
|
||||
workspace_role = WorkspaceMember.objects.get(
|
||||
@ -203,20 +187,15 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
if workspace_role in [5] and int(
|
||||
request.data.get("role", project_member.role)
|
||||
) in [15, 20]:
|
||||
if workspace_role in [5] and int(request.data.get("role", project_member.role)) in [15, 20]:
|
||||
return Response(
|
||||
{
|
||||
"error": "You cannot add a user with role higher than the workspace role"
|
||||
},
|
||||
{"error": "You cannot add a user with role higher than the workspace role"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if (
|
||||
"role" in request.data
|
||||
and int(request.data.get("role", project_member.role))
|
||||
> requested_project_member.role
|
||||
and int(request.data.get("role", project_member.role)) > requested_project_member.role
|
||||
and not is_workspace_admin
|
||||
):
|
||||
return Response(
|
||||
@ -224,9 +203,7 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
serializer = ProjectMemberSerializer(
|
||||
project_member, data=request.data, partial=True
|
||||
)
|
||||
serializer = ProjectMemberSerializer(project_member, data=request.data, partial=True)
|
||||
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
@ -252,9 +229,7 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
# User cannot remove himself
|
||||
if str(project_member.id) == str(requesting_project_member.id):
|
||||
return Response(
|
||||
{
|
||||
"error": "You cannot remove yourself from the workspace. Please use leave workspace"
|
||||
},
|
||||
{"error": "You cannot remove yourself from the workspace. Please use leave workspace"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
# User cannot deactivate higher role
|
||||
@ -287,7 +262,7 @@ class ProjectMemberViewSet(BaseViewSet):
|
||||
):
|
||||
return Response(
|
||||
{
|
||||
"error": "You cannot leave the project as your the only admin of the project you will have to either delete the project or create an another admin"
|
||||
"error": "You cannot leave the project as your the only admin of the project you will have to either delete the project or create an another admin" # noqa: E501
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
@ -323,7 +298,5 @@ class UserProjectRolesEndpoint(BaseAPIView):
|
||||
member__member_workspace__is_active=True,
|
||||
).values("project_id", "role")
|
||||
|
||||
project_members = {
|
||||
str(member["project_id"]): member["role"] for member in project_members
|
||||
}
|
||||
project_members = {str(member["project_id"]): member["role"] for member in project_members}
|
||||
return Response(project_members, status=status.HTTP_200_OK)
|
||||
|
||||
@ -120,9 +120,7 @@ class GlobalSearchEndpoint(BaseAPIView):
|
||||
if workspace_search == "false" and project_id:
|
||||
cycles = cycles.filter(project_id=project_id)
|
||||
|
||||
return cycles.distinct().values(
|
||||
"name", "id", "project_id", "project__identifier", "workspace__slug"
|
||||
)
|
||||
return cycles.distinct().values("name", "id", "project_id", "project__identifier", "workspace__slug")
|
||||
|
||||
def filter_modules(self, query, slug, project_id, workspace_search):
|
||||
fields = ["name"]
|
||||
@ -141,9 +139,7 @@ class GlobalSearchEndpoint(BaseAPIView):
|
||||
if workspace_search == "false" and project_id:
|
||||
modules = modules.filter(project_id=project_id)
|
||||
|
||||
return modules.distinct().values(
|
||||
"name", "id", "project_id", "project__identifier", "workspace__slug"
|
||||
)
|
||||
return modules.distinct().values("name", "id", "project_id", "project__identifier", "workspace__slug")
|
||||
|
||||
def filter_pages(self, query, slug, project_id, workspace_search):
|
||||
fields = ["name"]
|
||||
@ -161,9 +157,7 @@ class GlobalSearchEndpoint(BaseAPIView):
|
||||
)
|
||||
.annotate(
|
||||
project_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"projects__id", distinct=True, filter=~Q(projects__id=True)
|
||||
),
|
||||
ArrayAgg("projects__id", distinct=True, filter=~Q(projects__id=True)),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
)
|
||||
)
|
||||
@ -180,17 +174,13 @@ class GlobalSearchEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
if workspace_search == "false" and project_id:
|
||||
project_subquery = ProjectPage.objects.filter(
|
||||
page_id=OuterRef("id"), project_id=project_id
|
||||
).values_list("project_id", flat=True)[:1]
|
||||
project_subquery = ProjectPage.objects.filter(page_id=OuterRef("id"), project_id=project_id).values_list(
|
||||
"project_id", flat=True
|
||||
)[:1]
|
||||
|
||||
pages = pages.annotate(project_id=Subquery(project_subquery)).filter(
|
||||
project_id=project_id
|
||||
)
|
||||
pages = pages.annotate(project_id=Subquery(project_subquery)).filter(project_id=project_id)
|
||||
|
||||
return pages.distinct().values(
|
||||
"name", "id", "project_ids", "project_identifiers", "workspace__slug"
|
||||
)
|
||||
return pages.distinct().values("name", "id", "project_ids", "project_identifiers", "workspace__slug")
|
||||
|
||||
def filter_views(self, query, slug, project_id, workspace_search):
|
||||
fields = ["name"]
|
||||
@ -209,9 +199,7 @@ class GlobalSearchEndpoint(BaseAPIView):
|
||||
if workspace_search == "false" and project_id:
|
||||
issue_views = issue_views.filter(project_id=project_id)
|
||||
|
||||
return issue_views.distinct().values(
|
||||
"name", "id", "project_id", "project__identifier", "workspace__slug"
|
||||
)
|
||||
return issue_views.distinct().values("name", "id", "project_id", "project__identifier", "workspace__slug")
|
||||
|
||||
def get(self, request, slug):
|
||||
query = request.query_params.get("search", False)
|
||||
@ -308,9 +296,7 @@ class SearchEndpoint(BaseAPIView):
|
||||
|
||||
if issue_id:
|
||||
issue_created_by = (
|
||||
Issue.objects.filter(id=issue_id)
|
||||
.values_list("created_by_id", flat=True)
|
||||
.first()
|
||||
Issue.objects.filter(id=issue_id).values_list("created_by_id", flat=True).first()
|
||||
)
|
||||
users = (
|
||||
users.filter(Q(role__gt=10) | Q(member_id=issue_created_by))
|
||||
@ -344,15 +330,12 @@ class SearchEndpoint(BaseAPIView):
|
||||
projects = (
|
||||
Project.objects.filter(
|
||||
q,
|
||||
Q(project_projectmember__member=self.request.user)
|
||||
| Q(network=2),
|
||||
Q(project_projectmember__member=self.request.user) | Q(network=2),
|
||||
workspace__slug=slug,
|
||||
)
|
||||
.order_by("-created_at")
|
||||
.distinct()
|
||||
.values(
|
||||
"name", "id", "identifier", "logo_props", "workspace__slug"
|
||||
)[:count]
|
||||
.values("name", "id", "identifier", "logo_props", "workspace__slug")[:count]
|
||||
)
|
||||
response_data["project"] = list(projects)
|
||||
|
||||
@ -411,20 +394,16 @@ class SearchEndpoint(BaseAPIView):
|
||||
.annotate(
|
||||
status=Case(
|
||||
When(
|
||||
Q(start_date__lte=timezone.now())
|
||||
& Q(end_date__gte=timezone.now()),
|
||||
Q(start_date__lte=timezone.now()) & Q(end_date__gte=timezone.now()),
|
||||
then=Value("CURRENT"),
|
||||
),
|
||||
When(
|
||||
start_date__gt=timezone.now(),
|
||||
then=Value("UPCOMING"),
|
||||
),
|
||||
When(end_date__lt=timezone.now(), then=Value("COMPLETED")),
|
||||
When(
|
||||
end_date__lt=timezone.now(), then=Value("COMPLETED")
|
||||
),
|
||||
When(
|
||||
Q(start_date__isnull=True)
|
||||
& Q(end_date__isnull=True),
|
||||
Q(start_date__isnull=True) & Q(end_date__isnull=True),
|
||||
then=Value("DRAFT"),
|
||||
),
|
||||
default=Value("DRAFT"),
|
||||
@ -542,9 +521,7 @@ class SearchEndpoint(BaseAPIView):
|
||||
)
|
||||
)
|
||||
.order_by("-created_at")
|
||||
.values(
|
||||
"member__avatar_url", "member__display_name", "member__id"
|
||||
)[:count]
|
||||
.values("member__avatar_url", "member__display_name", "member__id")[:count]
|
||||
)
|
||||
response_data["user_mention"] = list(users)
|
||||
|
||||
@ -558,15 +535,12 @@ class SearchEndpoint(BaseAPIView):
|
||||
projects = (
|
||||
Project.objects.filter(
|
||||
q,
|
||||
Q(project_projectmember__member=self.request.user)
|
||||
| Q(network=2),
|
||||
Q(project_projectmember__member=self.request.user) | Q(network=2),
|
||||
workspace__slug=slug,
|
||||
)
|
||||
.order_by("-created_at")
|
||||
.distinct()
|
||||
.values(
|
||||
"name", "id", "identifier", "logo_props", "workspace__slug"
|
||||
)[:count]
|
||||
.values("name", "id", "identifier", "logo_props", "workspace__slug")[:count]
|
||||
)
|
||||
response_data["project"] = list(projects)
|
||||
|
||||
@ -623,20 +597,16 @@ class SearchEndpoint(BaseAPIView):
|
||||
.annotate(
|
||||
status=Case(
|
||||
When(
|
||||
Q(start_date__lte=timezone.now())
|
||||
& Q(end_date__gte=timezone.now()),
|
||||
Q(start_date__lte=timezone.now()) & Q(end_date__gte=timezone.now()),
|
||||
then=Value("CURRENT"),
|
||||
),
|
||||
When(
|
||||
start_date__gt=timezone.now(),
|
||||
then=Value("UPCOMING"),
|
||||
),
|
||||
When(end_date__lt=timezone.now(), then=Value("COMPLETED")),
|
||||
When(
|
||||
end_date__lt=timezone.now(), then=Value("COMPLETED")
|
||||
),
|
||||
When(
|
||||
Q(start_date__isnull=True)
|
||||
& Q(end_date__isnull=True),
|
||||
Q(start_date__isnull=True) & Q(end_date__isnull=True),
|
||||
then=Value("DRAFT"),
|
||||
),
|
||||
default=Value("DRAFT"),
|
||||
|
||||
@ -30,23 +30,17 @@ class IssueSearchEndpoint(BaseAPIView):
|
||||
|
||||
return issues
|
||||
|
||||
def search_issues_and_excluding_parent(
|
||||
self, issues: QuerySet, issue_id: str
|
||||
) -> QuerySet:
|
||||
def search_issues_and_excluding_parent(self, issues: QuerySet, issue_id: str) -> QuerySet:
|
||||
"""
|
||||
Search issues and epics by query excluding the parent
|
||||
"""
|
||||
|
||||
issue = Issue.issue_objects.filter(pk=issue_id).first()
|
||||
if issue:
|
||||
issues = issues.filter(
|
||||
~Q(pk=issue_id), ~Q(pk=issue.parent_id), ~Q(parent_id=issue_id)
|
||||
)
|
||||
issues = issues.filter(~Q(pk=issue_id), ~Q(pk=issue.parent_id), ~Q(parent_id=issue_id))
|
||||
return issues
|
||||
|
||||
def filter_issues_excluding_related_issues(
|
||||
self, issue_id: str, issues: QuerySet
|
||||
) -> QuerySet:
|
||||
def filter_issues_excluding_related_issues(self, issue_id: str, issues: QuerySet) -> QuerySet:
|
||||
"""
|
||||
Filter issues excluding related issues
|
||||
"""
|
||||
@ -81,18 +75,14 @@ class IssueSearchEndpoint(BaseAPIView):
|
||||
"""
|
||||
Exclude issues in cycles
|
||||
"""
|
||||
issues = issues.exclude(
|
||||
Q(issue_cycle__isnull=False) & Q(issue_cycle__deleted_at__isnull=True)
|
||||
)
|
||||
issues = issues.exclude(Q(issue_cycle__isnull=False) & Q(issue_cycle__deleted_at__isnull=True))
|
||||
return issues
|
||||
|
||||
def exclude_issues_in_module(self, issues: QuerySet, module: str) -> QuerySet:
|
||||
"""
|
||||
Exclude issues in a module
|
||||
"""
|
||||
issues = issues.exclude(
|
||||
Q(issue_module__module=module) & Q(issue_module__deleted_at__isnull=True)
|
||||
)
|
||||
issues = issues.exclude(Q(issue_module__module=module) & Q(issue_module__deleted_at__isnull=True))
|
||||
return issues
|
||||
|
||||
def filter_issues_without_target_date(self, issues: QuerySet) -> QuerySet:
|
||||
|
||||
@ -57,9 +57,7 @@ class StateViewSet(BaseViewSet):
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
def partial_update(self, request, slug, project_id, pk):
|
||||
try:
|
||||
state = State.objects.get(
|
||||
pk=pk, project_id=project_id, workspace__slug=slug
|
||||
)
|
||||
state = State.objects.get(pk=pk, project_id=project_id, workspace__slug=slug)
|
||||
serializer = StateSerializer(state, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
@ -103,20 +101,14 @@ class StateViewSet(BaseViewSet):
|
||||
@allow_permission([ROLE.ADMIN])
|
||||
def mark_as_default(self, request, slug, project_id, pk):
|
||||
# Select all the states which are marked as default
|
||||
_ = State.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id, default=True
|
||||
).update(default=False)
|
||||
_ = State.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id, pk=pk
|
||||
).update(default=True)
|
||||
_ = State.objects.filter(workspace__slug=slug, project_id=project_id, default=True).update(default=False)
|
||||
_ = State.objects.filter(workspace__slug=slug, project_id=project_id, pk=pk).update(default=True)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@invalidate_cache(path="workspaces/:slug/states/", url_params=True, user=False)
|
||||
@allow_permission([ROLE.ADMIN])
|
||||
def destroy(self, request, slug, project_id, pk):
|
||||
state = State.objects.get(
|
||||
is_triage=False, pk=pk, project_id=project_id, workspace__slug=slug
|
||||
)
|
||||
state = State.objects.get(is_triage=False, pk=pk, project_id=project_id, workspace__slug=slug)
|
||||
|
||||
if state.default:
|
||||
return Response(
|
||||
|
||||
@ -187,10 +187,7 @@ class TimezoneEndpoint(APIView):
|
||||
total_seconds = int(current_utc_offset.total_seconds())
|
||||
hours_offset = total_seconds // 3600
|
||||
minutes_offset = abs(total_seconds % 3600) // 60
|
||||
offset = (
|
||||
f"{'+' if hours_offset >= 0 else '-'}"
|
||||
f"{abs(hours_offset):02}:{minutes_offset:02}"
|
||||
)
|
||||
offset = f"{'+' if hours_offset >= 0 else '-'}{abs(hours_offset):02}:{minutes_offset:02}"
|
||||
|
||||
timezone_value = {
|
||||
"offset": int(current_offset),
|
||||
|
||||
@ -63,9 +63,7 @@ class UserEndpoint(BaseViewSet):
|
||||
|
||||
def retrieve_instance_admin(self, request):
|
||||
instance = Instance.objects.first()
|
||||
is_admin = InstanceAdmin.objects.filter(
|
||||
instance=instance, user=request.user
|
||||
).exists()
|
||||
is_admin = InstanceAdmin.objects.filter(instance=instance, user=request.user).exists()
|
||||
return Response({"is_instance_admin": is_admin}, status=status.HTTP_200_OK)
|
||||
|
||||
def partial_update(self, request, *args, **kwargs):
|
||||
@ -78,18 +76,14 @@ class UserEndpoint(BaseViewSet):
|
||||
# Instance admin check
|
||||
if InstanceAdmin.objects.filter(user=user).exists():
|
||||
return Response(
|
||||
{
|
||||
"error": "You cannot deactivate your account since you are an instance admin"
|
||||
},
|
||||
{"error": "You cannot deactivate your account since you are an instance admin"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
projects_to_deactivate = []
|
||||
workspaces_to_deactivate = []
|
||||
|
||||
projects = ProjectMember.objects.filter(
|
||||
member=request.user, is_active=True
|
||||
).annotate(
|
||||
projects = ProjectMember.objects.filter(member=request.user, is_active=True).annotate(
|
||||
other_admin_exists=Count(
|
||||
Case(
|
||||
When(Q(role=20, is_active=True) & ~Q(member=request.user), then=1),
|
||||
@ -106,15 +100,11 @@ class UserEndpoint(BaseViewSet):
|
||||
projects_to_deactivate.append(project)
|
||||
else:
|
||||
return Response(
|
||||
{
|
||||
"error": "You cannot deactivate account as you are the only admin in some projects."
|
||||
},
|
||||
{"error": "You cannot deactivate account as you are the only admin in some projects."},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
workspaces = WorkspaceMember.objects.filter(
|
||||
member=request.user, is_active=True
|
||||
).annotate(
|
||||
workspaces = WorkspaceMember.objects.filter(member=request.user, is_active=True).annotate(
|
||||
other_admin_exists=Count(
|
||||
Case(
|
||||
When(Q(role=20, is_active=True) & ~Q(member=request.user), then=1),
|
||||
@ -131,19 +121,13 @@ class UserEndpoint(BaseViewSet):
|
||||
workspaces_to_deactivate.append(workspace)
|
||||
else:
|
||||
return Response(
|
||||
{
|
||||
"error": "You cannot deactivate account as you are the only admin in some workspaces."
|
||||
},
|
||||
{"error": "You cannot deactivate account as you are the only admin in some workspaces."},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
ProjectMember.objects.bulk_update(
|
||||
projects_to_deactivate, ["is_active"], batch_size=100
|
||||
)
|
||||
ProjectMember.objects.bulk_update(projects_to_deactivate, ["is_active"], batch_size=100)
|
||||
|
||||
WorkspaceMember.objects.bulk_update(
|
||||
workspaces_to_deactivate, ["is_active"], batch_size=100
|
||||
)
|
||||
WorkspaceMember.objects.bulk_update(workspaces_to_deactivate, ["is_active"], batch_size=100)
|
||||
|
||||
# Delete all workspace invites
|
||||
WorkspaceMemberInvite.objects.filter(email=user.email).delete()
|
||||
@ -224,9 +208,7 @@ class UserActivityEndpoint(BaseAPIView, BasePaginator):
|
||||
order_by=request.GET.get("order_by", "-created_at"),
|
||||
request=request,
|
||||
queryset=queryset,
|
||||
on_results=lambda issue_activities: IssueActivitySerializer(
|
||||
issue_activities, many=True
|
||||
).data,
|
||||
on_results=lambda issue_activities: IssueActivitySerializer(issue_activities, many=True).data,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@ -64,34 +64,22 @@ class WorkspaceViewViewSet(BaseViewSet):
|
||||
.distinct()
|
||||
)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
|
||||
)
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def list(self, request, slug):
|
||||
queryset = self.get_queryset()
|
||||
fields = [field for field in request.GET.get("fields", "").split(",") if field]
|
||||
if WorkspaceMember.objects.filter(
|
||||
workspace__slug=slug, member=request.user, role=5, is_active=True
|
||||
).exists():
|
||||
if WorkspaceMember.objects.filter(workspace__slug=slug, member=request.user, role=5, is_active=True).exists():
|
||||
queryset = queryset.filter(owned_by=request.user)
|
||||
views = IssueViewSerializer(
|
||||
queryset, many=True, fields=fields if fields else None
|
||||
).data
|
||||
views = IssueViewSerializer(queryset, many=True, fields=fields if fields else None).data
|
||||
return Response(views, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[], level="WORKSPACE", creator=True, model=IssueView
|
||||
)
|
||||
@allow_permission(allowed_roles=[], level="WORKSPACE", creator=True, model=IssueView)
|
||||
def partial_update(self, request, slug, pk):
|
||||
with transaction.atomic():
|
||||
workspace_view = IssueView.objects.select_for_update().get(
|
||||
pk=pk, workspace__slug=slug
|
||||
)
|
||||
workspace_view = IssueView.objects.select_for_update().get(pk=pk, workspace__slug=slug)
|
||||
|
||||
if workspace_view.is_locked:
|
||||
return Response(
|
||||
{"error": "view is locked"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
return Response({"error": "view is locked"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Only update the view if owner is updating
|
||||
if workspace_view.owned_by_id != request.user.id:
|
||||
@ -100,9 +88,7 @@ class WorkspaceViewViewSet(BaseViewSet):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
serializer = IssueViewSerializer(
|
||||
workspace_view, data=request.data, partial=True
|
||||
)
|
||||
serializer = IssueViewSerializer(workspace_view, data=request.data, partial=True)
|
||||
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
@ -121,9 +107,7 @@ class WorkspaceViewViewSet(BaseViewSet):
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN], level="WORKSPACE", creator=True, model=IssueView
|
||||
)
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE", creator=True, model=IssueView)
|
||||
def destroy(self, request, slug, pk):
|
||||
workspace_view = IssueView.objects.get(pk=pk, workspace__slug=slug)
|
||||
|
||||
@ -177,9 +161,7 @@ class WorkspaceViewIssuesViewSet(BaseViewSet):
|
||||
return (
|
||||
issues.annotate(
|
||||
cycle_id=Subquery(
|
||||
CycleIssue.objects.filter(
|
||||
issue=OuterRef("id"), deleted_at__isnull=True
|
||||
).values("cycle_id")[:1]
|
||||
CycleIssue.objects.filter(issue=OuterRef("id"), deleted_at__isnull=True).values("cycle_id")[:1]
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
@ -227,9 +209,7 @@ class WorkspaceViewIssuesViewSet(BaseViewSet):
|
||||
return Issue.issue_objects.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
|
||||
@method_decorator(gzip_page)
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
|
||||
)
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def list(self, request, slug):
|
||||
issue_queryset = self.get_queryset()
|
||||
|
||||
@ -274,9 +254,7 @@ class IssueViewViewSet(BaseViewSet):
|
||||
model = IssueView
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(
|
||||
project_id=self.kwargs.get("project_id"), owned_by=self.request.user
|
||||
)
|
||||
serializer.save(project_id=self.kwargs.get("project_id"), owned_by=self.request.user)
|
||||
|
||||
def get_queryset(self):
|
||||
subquery = UserFavorite.objects.filter(
|
||||
@ -320,9 +298,7 @@ class IssueViewViewSet(BaseViewSet):
|
||||
):
|
||||
queryset = queryset.filter(owned_by=request.user)
|
||||
fields = [field for field in request.GET.get("fields", "").split(",") if field]
|
||||
views = IssueViewSerializer(
|
||||
queryset, many=True, fields=fields if fields else None
|
||||
).data
|
||||
views = IssueViewSerializer(queryset, many=True, fields=fields if fields else None).data
|
||||
return Response(views, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
@ -363,14 +339,10 @@ class IssueViewViewSet(BaseViewSet):
|
||||
@allow_permission(allowed_roles=[], creator=True, model=IssueView)
|
||||
def partial_update(self, request, slug, project_id, pk):
|
||||
with transaction.atomic():
|
||||
issue_view = IssueView.objects.select_for_update().get(
|
||||
pk=pk, workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
issue_view = IssueView.objects.select_for_update().get(pk=pk, workspace__slug=slug, project_id=project_id)
|
||||
|
||||
if issue_view.is_locked:
|
||||
return Response(
|
||||
{"error": "view is locked"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
return Response({"error": "view is locked"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Only update the view if owner is updating
|
||||
if issue_view.owned_by_id != request.user.id:
|
||||
@ -379,9 +351,7 @@ class IssueViewViewSet(BaseViewSet):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
serializer = IssueViewSerializer(
|
||||
issue_view, data=request.data, partial=True
|
||||
)
|
||||
serializer = IssueViewSerializer(issue_view, data=request.data, partial=True)
|
||||
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
@ -390,9 +360,7 @@ class IssueViewViewSet(BaseViewSet):
|
||||
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN], creator=True, model=IssueView)
|
||||
def destroy(self, request, slug, project_id, pk):
|
||||
project_view = IssueView.objects.get(
|
||||
pk=pk, project_id=project_id, workspace__slug=slug
|
||||
)
|
||||
project_view = IssueView.objects.get(pk=pk, project_id=project_id, workspace__slug=slug)
|
||||
if (
|
||||
ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
|
||||
@ -18,9 +18,7 @@ class WebhookEndpoint(BaseAPIView):
|
||||
def post(self, request, slug):
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
try:
|
||||
serializer = WebhookSerializer(
|
||||
data=request.data, context={"request": request}
|
||||
)
|
||||
serializer = WebhookSerializer(data=request.data, context={"request": request})
|
||||
if serializer.is_valid():
|
||||
serializer.save(workspace_id=workspace.id)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
@ -119,8 +117,6 @@ class WebhookSecretRegenerateEndpoint(BaseAPIView):
|
||||
class WebhookLogsEndpoint(BaseAPIView):
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE")
|
||||
def get(self, request, slug, webhook_id):
|
||||
webhook_logs = WebhookLog.objects.filter(
|
||||
workspace__slug=slug, webhook=webhook_id
|
||||
)
|
||||
webhook_logs = WebhookLog.objects.filter(workspace__slug=slug, webhook=webhook_id)
|
||||
serializer = WebhookLogSerializer(webhook_logs, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@ -57,9 +57,7 @@ class WorkSpaceViewSet(BaseViewSet):
|
||||
|
||||
def get_queryset(self):
|
||||
member_count = (
|
||||
WorkspaceMember.objects.filter(
|
||||
workspace=OuterRef("id"), member__is_bot=False, is_active=True
|
||||
)
|
||||
WorkspaceMember.objects.filter(workspace=OuterRef("id"), member__is_bot=False, is_active=True)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
@ -126,9 +124,7 @@ class WorkSpaceViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
# Get total members and role
|
||||
total_members = WorkspaceMember.objects.filter(
|
||||
workspace_id=serializer.data["id"]
|
||||
).count()
|
||||
total_members = WorkspaceMember.objects.filter(workspace_id=serializer.data["id"]).count()
|
||||
data = serializer.data
|
||||
data["total_members"] = total_members
|
||||
data["role"] = 20
|
||||
@ -179,31 +175,25 @@ class UserWorkSpacesEndpoint(BaseAPIView):
|
||||
def get(self, request):
|
||||
fields = [field for field in request.GET.get("fields", "").split(",") if field]
|
||||
member_count = (
|
||||
WorkspaceMember.objects.filter(
|
||||
workspace=OuterRef("id"), member__is_bot=False, is_active=True
|
||||
)
|
||||
WorkspaceMember.objects.filter(workspace=OuterRef("id"), member__is_bot=False, is_active=True)
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
|
||||
role = WorkspaceMember.objects.filter(
|
||||
workspace=OuterRef("id"), member=request.user, is_active=True
|
||||
).values("role")
|
||||
role = WorkspaceMember.objects.filter(workspace=OuterRef("id"), member=request.user, is_active=True).values(
|
||||
"role"
|
||||
)
|
||||
|
||||
workspace = (
|
||||
Workspace.objects.prefetch_related(
|
||||
Prefetch(
|
||||
"workspace_member",
|
||||
queryset=WorkspaceMember.objects.filter(
|
||||
member=request.user, is_active=True
|
||||
),
|
||||
queryset=WorkspaceMember.objects.filter(member=request.user, is_active=True),
|
||||
)
|
||||
)
|
||||
.annotate(role=role, total_members=member_count)
|
||||
.filter(
|
||||
workspace_member__member=request.user, workspace_member__is_active=True
|
||||
)
|
||||
.filter(workspace_member__member=request.user, workspace_member__is_active=True)
|
||||
.distinct()
|
||||
)
|
||||
|
||||
@ -226,10 +216,7 @@ class WorkSpaceAvailabilityCheckEndpoint(BaseAPIView):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
workspace = (
|
||||
Workspace.objects.filter(slug=slug).exists()
|
||||
or slug in RESTRICTED_WORKSPACE_SLUGS
|
||||
)
|
||||
workspace = Workspace.objects.filter(slug=slug).exists() or slug in RESTRICTED_WORKSPACE_SLUGS
|
||||
return Response({"status": not workspace}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
@ -268,9 +255,7 @@ class UserWorkspaceDashboardEndpoint(BaseAPIView):
|
||||
.order_by("week_in_month")
|
||||
)
|
||||
|
||||
assigned_issues = Issue.issue_objects.filter(
|
||||
workspace__slug=slug, assignees__in=[request.user]
|
||||
).count()
|
||||
assigned_issues = Issue.issue_objects.filter(workspace__slug=slug, assignees__in=[request.user]).count()
|
||||
|
||||
pending_issues_count = Issue.issue_objects.filter(
|
||||
~Q(state__group__in=["completed", "cancelled"]),
|
||||
@ -283,18 +268,14 @@ class UserWorkspaceDashboardEndpoint(BaseAPIView):
|
||||
).count()
|
||||
|
||||
issues_due_week = (
|
||||
Issue.issue_objects.filter(
|
||||
workspace__slug=slug, assignees__in=[request.user]
|
||||
)
|
||||
Issue.issue_objects.filter(workspace__slug=slug, assignees__in=[request.user])
|
||||
.annotate(target_week=ExtractWeek("target_date"))
|
||||
.filter(target_week=timezone.now().date().isocalendar()[1])
|
||||
.count()
|
||||
)
|
||||
|
||||
state_distribution = (
|
||||
Issue.issue_objects.filter(
|
||||
workspace__slug=slug, assignees__in=[request.user]
|
||||
)
|
||||
Issue.issue_objects.filter(workspace__slug=slug, assignees__in=[request.user])
|
||||
.annotate(state_group=F("state__group"))
|
||||
.values("state_group")
|
||||
.annotate(state_count=Count("state_group"))
|
||||
@ -363,9 +344,7 @@ class ExportWorkspaceUserActivityEndpoint(BaseAPIView):
|
||||
|
||||
def post(self, request, slug, user_id):
|
||||
if not request.data.get("date"):
|
||||
return Response(
|
||||
{"error": "Date is required"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
return Response({"error": "Date is required"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
user_activities = IssueActivity.objects.filter(
|
||||
~Q(field__in=["comment", "vote", "reaction", "draft"]),
|
||||
@ -403,7 +382,5 @@ class ExportWorkspaceUserActivityEndpoint(BaseAPIView):
|
||||
]
|
||||
csv_buffer = self.generate_csv_from_rows([header] + rows)
|
||||
response = HttpResponse(csv_buffer.getvalue(), content_type="text/csv")
|
||||
response["Content-Disposition"] = (
|
||||
'attachment; filename="workspace-user-activity.csv"'
|
||||
)
|
||||
response["Content-Disposition"] = 'attachment; filename="workspace-user-activity.csv"'
|
||||
return response
|
||||
|
||||
@ -49,9 +49,9 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
|
||||
.prefetch_related("assignees", "labels", "draft_issue_module__module")
|
||||
.annotate(
|
||||
cycle_id=Subquery(
|
||||
DraftIssueCycle.objects.filter(
|
||||
draft_issue=OuterRef("id"), deleted_at__isnull=True
|
||||
).values("cycle_id")[:1]
|
||||
DraftIssueCycle.objects.filter(draft_issue=OuterRef("id"), deleted_at__isnull=True).values(
|
||||
"cycle_id"
|
||||
)[:1]
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
@ -59,10 +59,7 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(labels__id__isnull=True)
|
||||
& (Q(draft_label_issue__deleted_at__isnull=True))
|
||||
),
|
||||
filter=Q(~Q(labels__id__isnull=True) & (Q(draft_label_issue__deleted_at__isnull=True))),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
@ -94,14 +91,10 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
|
||||
).distinct()
|
||||
|
||||
@method_decorator(gzip_page)
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
|
||||
)
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def list(self, request, slug):
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
issues = (
|
||||
self.get_queryset().filter(created_by=request.user).order_by("-created_at")
|
||||
)
|
||||
issues = self.get_queryset().filter(created_by=request.user).order_by("-created_at")
|
||||
|
||||
issues = issues.filter(**filters)
|
||||
# List Paginate
|
||||
@ -111,9 +104,7 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
|
||||
on_results=lambda issues: DraftIssueSerializer(issues, many=True).data,
|
||||
)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
|
||||
)
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def create(self, request, slug):
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
|
||||
@ -168,9 +159,7 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
|
||||
issue = self.get_queryset().filter(pk=pk, created_by=request.user).first()
|
||||
|
||||
if not issue:
|
||||
return Response(
|
||||
{"error": "Issue not found"}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
return Response({"error": "Issue not found"}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
project_id = request.data.get("project_id", issue.project_id)
|
||||
|
||||
@ -190,9 +179,7 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN], creator=True, model=Issue, level="WORKSPACE"
|
||||
)
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN], creator=True, model=Issue, level="WORKSPACE")
|
||||
def retrieve(self, request, slug, pk=None):
|
||||
issue = self.get_queryset().filter(pk=pk, created_by=request.user).first()
|
||||
|
||||
@ -205,9 +192,7 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
|
||||
serializer = DraftIssueDetailSerializer(issue)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN], creator=True, model=DraftIssue, level="WORKSPACE"
|
||||
)
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN], creator=True, model=DraftIssue, level="WORKSPACE")
|
||||
def destroy(self, request, slug, pk=None):
|
||||
draft_issue = DraftIssue.objects.get(workspace__slug=slug, pk=pk)
|
||||
draft_issue.delete()
|
||||
@ -266,9 +251,7 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
|
||||
current_instance=json.dumps(
|
||||
{
|
||||
"updated_cycle_issues": None,
|
||||
"created_cycle_issues": serializers.serialize(
|
||||
"json", [created_records]
|
||||
),
|
||||
"created_cycle_issues": serializers.serialize("json", [created_records]),
|
||||
}
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
|
||||
@ -16,9 +16,9 @@ class WorkspaceEstimatesEndpoint(BaseAPIView):
|
||||
|
||||
@cache_response(60 * 60 * 2)
|
||||
def get(self, request, slug):
|
||||
estimate_ids = Project.objects.filter(
|
||||
workspace__slug=slug, estimate__isnull=False
|
||||
).values_list("estimate_id", flat=True)
|
||||
estimate_ids = Project.objects.filter(workspace__slug=slug, estimate__isnull=False).values_list(
|
||||
"estimate_id", flat=True
|
||||
)
|
||||
estimates = (
|
||||
Estimate.objects.filter(pk__in=estimate_ids, workspace__slug=slug)
|
||||
.prefetch_related("points")
|
||||
|
||||
@ -19,9 +19,7 @@ class WorkspaceFavoriteEndpoint(BaseAPIView):
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
|
||||
def get(self, request, slug):
|
||||
# the second filter is to check if the user is a member of the project
|
||||
favorites = UserFavorite.objects.filter(
|
||||
user=request.user, workspace__slug=slug, parent__isnull=True
|
||||
).filter(
|
||||
favorites = UserFavorite.objects.filter(user=request.user, workspace__slug=slug, parent__isnull=True).filter(
|
||||
Q(project__isnull=True) & ~Q(entity_type="page")
|
||||
| (
|
||||
Q(project__isnull=False)
|
||||
@ -62,15 +60,11 @@ class WorkspaceFavoriteEndpoint(BaseAPIView):
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
except IntegrityError:
|
||||
return Response(
|
||||
{"error": "Favorite already exists"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
return Response({"error": "Favorite already exists"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
|
||||
def patch(self, request, slug, favorite_id):
|
||||
favorite = UserFavorite.objects.get(
|
||||
user=request.user, workspace__slug=slug, pk=favorite_id
|
||||
)
|
||||
favorite = UserFavorite.objects.get(user=request.user, workspace__slug=slug, pk=favorite_id)
|
||||
serializer = UserFavoriteSerializer(favorite, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
@ -79,9 +73,7 @@ class WorkspaceFavoriteEndpoint(BaseAPIView):
|
||||
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
|
||||
def delete(self, request, slug, favorite_id):
|
||||
favorite = UserFavorite.objects.get(
|
||||
user=request.user, workspace__slug=slug, pk=favorite_id
|
||||
)
|
||||
favorite = UserFavorite.objects.get(user=request.user, workspace__slug=slug, pk=favorite_id)
|
||||
favorite.delete(soft=False)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@ -89,9 +81,7 @@ class WorkspaceFavoriteEndpoint(BaseAPIView):
|
||||
class WorkspaceFavoriteGroupEndpoint(BaseAPIView):
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
|
||||
def get(self, request, slug, favorite_id):
|
||||
favorites = UserFavorite.objects.filter(
|
||||
user=request.user, workspace__slug=slug, parent_id=favorite_id
|
||||
).filter(
|
||||
favorites = UserFavorite.objects.filter(user=request.user, workspace__slug=slug, parent_id=favorite_id).filter(
|
||||
Q(project__isnull=True)
|
||||
| (
|
||||
Q(project__isnull=False)
|
||||
|
||||
@ -20,9 +20,7 @@ class WorkspaceHomePreferenceViewSet(BaseAPIView):
|
||||
def get(self, request, slug):
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
|
||||
get_preference = WorkspaceHomePreference.objects.filter(
|
||||
user=request.user, workspace_id=workspace.id
|
||||
)
|
||||
get_preference = WorkspaceHomePreference.objects.filter(user=request.user, workspace_id=workspace.id)
|
||||
|
||||
create_preference_keys = []
|
||||
|
||||
@ -55,9 +53,7 @@ class WorkspaceHomePreferenceViewSet(BaseAPIView):
|
||||
)
|
||||
sort_order_counter += 1
|
||||
|
||||
preference = WorkspaceHomePreference.objects.filter(
|
||||
user=request.user, workspace_id=workspace.id
|
||||
)
|
||||
preference = WorkspaceHomePreference.objects.filter(user=request.user, workspace_id=workspace.id)
|
||||
|
||||
return Response(
|
||||
preference.values("key", "is_enabled", "config", "sort_order"),
|
||||
@ -66,20 +62,14 @@ class WorkspaceHomePreferenceViewSet(BaseAPIView):
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def patch(self, request, slug, key):
|
||||
preference = WorkspaceHomePreference.objects.filter(
|
||||
key=key, workspace__slug=slug, user=request.user
|
||||
).first()
|
||||
preference = WorkspaceHomePreference.objects.filter(key=key, workspace__slug=slug, user=request.user).first()
|
||||
|
||||
if preference:
|
||||
serializer = WorkspaceHomePreferenceSerializer(
|
||||
preference, data=request.data, partial=True
|
||||
)
|
||||
serializer = WorkspaceHomePreferenceSerializer(preference, data=request.data, partial=True)
|
||||
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
return Response(
|
||||
{"detail": "Preference not found"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
return Response({"detail": "Preference not found"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@ -50,23 +50,13 @@ class WorkspaceInvitationsViewset(BaseViewSet):
|
||||
emails = request.data.get("emails", [])
|
||||
# Check if email is provided
|
||||
if not emails:
|
||||
return Response(
|
||||
{"error": "Emails are required"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
return Response({"error": "Emails are required"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# check for role level of the requesting user
|
||||
requesting_user = WorkspaceMember.objects.get(
|
||||
workspace__slug=slug, member=request.user, is_active=True
|
||||
)
|
||||
requesting_user = WorkspaceMember.objects.get(workspace__slug=slug, member=request.user, is_active=True)
|
||||
|
||||
# Check if any invited user has an higher role
|
||||
if len(
|
||||
[
|
||||
email
|
||||
for email in emails
|
||||
if int(email.get("role", 5)) > requesting_user.role
|
||||
]
|
||||
):
|
||||
if len([email for email in emails if int(email.get("role", 5)) > requesting_user.role]):
|
||||
return Response(
|
||||
{"error": "You cannot invite a user with higher role"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
@ -86,9 +76,7 @@ class WorkspaceInvitationsViewset(BaseViewSet):
|
||||
return Response(
|
||||
{
|
||||
"error": "Some users are already member of workspace",
|
||||
"workspace_users": WorkSpaceMemberSerializer(
|
||||
workspace_members, many=True
|
||||
).data,
|
||||
"workspace_users": WorkSpaceMemberSerializer(workspace_members, many=True).data,
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
@ -113,7 +101,7 @@ class WorkspaceInvitationsViewset(BaseViewSet):
|
||||
except ValidationError:
|
||||
return Response(
|
||||
{
|
||||
"error": f"Invalid email - {email} provided a valid email address is required to send the invite"
|
||||
"error": f"Invalid email - {email} provided a valid email address is required to send the invite" # noqa: E501
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
@ -134,14 +122,10 @@ class WorkspaceInvitationsViewset(BaseViewSet):
|
||||
request.user.email,
|
||||
)
|
||||
|
||||
return Response(
|
||||
{"message": "Emails sent successfully"}, status=status.HTTP_200_OK
|
||||
)
|
||||
return Response({"message": "Emails sent successfully"}, status=status.HTTP_200_OK)
|
||||
|
||||
def destroy(self, request, slug, pk):
|
||||
workspace_member_invite = WorkspaceMemberInvite.objects.get(
|
||||
pk=pk, workspace__slug=slug
|
||||
)
|
||||
workspace_member_invite = WorkspaceMemberInvite.objects.get(pk=pk, workspace__slug=slug)
|
||||
workspace_member_invite.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@ -160,9 +144,7 @@ class WorkspaceJoinEndpoint(BaseAPIView):
|
||||
)
|
||||
@invalidate_cache(path="/api/users/me/settings/", multiple=True)
|
||||
def post(self, request, slug, pk):
|
||||
workspace_invite = WorkspaceMemberInvite.objects.get(
|
||||
pk=pk, workspace__slug=slug
|
||||
)
|
||||
workspace_invite = WorkspaceMemberInvite.objects.get(pk=pk, workspace__slug=slug)
|
||||
|
||||
email = request.data.get("email", "")
|
||||
|
||||
@ -235,9 +217,7 @@ class WorkspaceJoinEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
def get(self, request, slug, pk):
|
||||
workspace_invitation = WorkspaceMemberInvite.objects.get(
|
||||
workspace__slug=slug, pk=pk
|
||||
)
|
||||
workspace_invitation = WorkspaceMemberInvite.objects.get(workspace__slug=slug, pk=pk)
|
||||
serializer = WorkSpaceMemberInviteSerializer(workspace_invitation)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@ -248,10 +228,7 @@ class UserWorkspaceInvitationsViewSet(BaseViewSet):
|
||||
|
||||
def get_queryset(self):
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(email=self.request.user.email)
|
||||
.select_related("workspace")
|
||||
super().get_queryset().filter(email=self.request.user.email).select_related("workspace")
|
||||
)
|
||||
|
||||
@invalidate_cache(path="/api/workspaces/", user=False)
|
||||
@ -271,9 +248,9 @@ class UserWorkspaceInvitationsViewSet(BaseViewSet):
|
||||
multiple=True,
|
||||
)
|
||||
# Update the WorkspaceMember for this specific invitation
|
||||
WorkspaceMember.objects.filter(
|
||||
workspace_id=invitation.workspace_id, member=request.user
|
||||
).update(is_active=True, role=invitation.role)
|
||||
WorkspaceMember.objects.filter(workspace_id=invitation.workspace_id, member=request.user).update(
|
||||
is_active=True, role=invitation.role
|
||||
)
|
||||
|
||||
# Bulk create the user for all the workspaces
|
||||
WorkspaceMember.objects.bulk_create(
|
||||
|
||||
@ -38,24 +38,16 @@ class WorkSpaceMemberViewSet(BaseViewSet):
|
||||
.select_related("member", "member__avatar_asset")
|
||||
)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
|
||||
)
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def list(self, request, slug):
|
||||
workspace_member = WorkspaceMember.objects.get(
|
||||
member=request.user, workspace__slug=slug, is_active=True
|
||||
)
|
||||
workspace_member = WorkspaceMember.objects.get(member=request.user, workspace__slug=slug, is_active=True)
|
||||
|
||||
# Get all active workspace members
|
||||
workspace_members = self.get_queryset()
|
||||
if workspace_member.role > 5:
|
||||
serializer = WorkspaceMemberAdminSerializer(
|
||||
workspace_members, fields=("id", "member", "role"), many=True
|
||||
)
|
||||
serializer = WorkspaceMemberAdminSerializer(workspace_members, fields=("id", "member", "role"), many=True)
|
||||
else:
|
||||
serializer = WorkSpaceMemberSerializer(
|
||||
workspace_members, fields=("id", "member", "role"), many=True
|
||||
)
|
||||
serializer = WorkSpaceMemberSerializer(workspace_members, fields=("id", "member", "role"), many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE")
|
||||
@ -71,13 +63,9 @@ class WorkSpaceMemberViewSet(BaseViewSet):
|
||||
|
||||
# If a user is moved to a guest role he can't have any other role in projects
|
||||
if "role" in request.data and int(request.data.get("role")) == 5:
|
||||
ProjectMember.objects.filter(
|
||||
workspace__slug=slug, member_id=workspace_member.member_id
|
||||
).update(role=5)
|
||||
ProjectMember.objects.filter(workspace__slug=slug, member_id=workspace_member.member_id).update(role=5)
|
||||
|
||||
serializer = WorkSpaceMemberSerializer(
|
||||
workspace_member, data=request.data, partial=True
|
||||
)
|
||||
serializer = WorkSpaceMemberSerializer(workspace_member, data=request.data, partial=True)
|
||||
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
@ -98,9 +86,7 @@ class WorkSpaceMemberViewSet(BaseViewSet):
|
||||
|
||||
if str(workspace_member.id) == str(requesting_workspace_member.id):
|
||||
return Response(
|
||||
{
|
||||
"error": "You cannot remove yourself from the workspace. Please use leave workspace"
|
||||
},
|
||||
{"error": "You cannot remove yourself from the workspace. Please use leave workspace"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@ -126,7 +112,7 @@ class WorkSpaceMemberViewSet(BaseViewSet):
|
||||
):
|
||||
return Response(
|
||||
{
|
||||
"error": "User is a part of some projects where they are the only admin, they should either leave that project or promote another user to admin."
|
||||
"error": "User is a part of some projects where they are the only admin, they should either leave that project or promote another user to admin." # noqa: E501
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
@ -148,25 +134,18 @@ class WorkSpaceMemberViewSet(BaseViewSet):
|
||||
)
|
||||
@invalidate_cache(path="/api/users/me/settings/")
|
||||
@invalidate_cache(path="api/users/me/workspaces/", user=False, multiple=True)
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
|
||||
)
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def leave(self, request, slug):
|
||||
workspace_member = WorkspaceMember.objects.get(
|
||||
workspace__slug=slug, member=request.user, is_active=True
|
||||
)
|
||||
workspace_member = WorkspaceMember.objects.get(workspace__slug=slug, member=request.user, is_active=True)
|
||||
|
||||
# Check if the leaving user is the only admin of the workspace
|
||||
if (
|
||||
workspace_member.role == 20
|
||||
and not WorkspaceMember.objects.filter(
|
||||
workspace__slug=slug, role=20, is_active=True
|
||||
).count()
|
||||
> 1
|
||||
and not WorkspaceMember.objects.filter(workspace__slug=slug, role=20, is_active=True).count() > 1
|
||||
):
|
||||
return Response(
|
||||
{
|
||||
"error": "You cannot leave the workspace as you are the only admin of the workspace you will have to either delete the workspace or promote another user to admin."
|
||||
"error": "You cannot leave the workspace as you are the only admin of the workspace you will have to either delete the workspace or promote another user to admin." # noqa: E501
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
@ -187,7 +166,7 @@ class WorkSpaceMemberViewSet(BaseViewSet):
|
||||
):
|
||||
return Response(
|
||||
{
|
||||
"error": "You are a part of some projects where you are the only admin, you should either leave the project or promote another user to admin."
|
||||
"error": "You are a part of some projects where you are the only admin, you should either leave the project or promote another user to admin." # noqa: E501
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
@ -205,9 +184,7 @@ class WorkSpaceMemberViewSet(BaseViewSet):
|
||||
|
||||
class WorkspaceMemberUserViewsEndpoint(BaseAPIView):
|
||||
def post(self, request, slug):
|
||||
workspace_member = WorkspaceMember.objects.get(
|
||||
workspace__slug=slug, member=request.user, is_active=True
|
||||
)
|
||||
workspace_member = WorkspaceMember.objects.get(workspace__slug=slug, member=request.user, is_active=True)
|
||||
workspace_member.view_props = request.data.get("view_props", {})
|
||||
workspace_member.save()
|
||||
|
||||
@ -219,23 +196,15 @@ class WorkspaceMemberUserEndpoint(BaseAPIView):
|
||||
|
||||
def get(self, request, slug):
|
||||
draft_issue_count = (
|
||||
DraftIssue.objects.filter(
|
||||
created_by=request.user, workspace_id=OuterRef("workspace_id")
|
||||
)
|
||||
DraftIssue.objects.filter(created_by=request.user, workspace_id=OuterRef("workspace_id"))
|
||||
.values("workspace_id")
|
||||
.annotate(count=Count("id"))
|
||||
.values("count")
|
||||
)
|
||||
|
||||
workspace_member = (
|
||||
WorkspaceMember.objects.filter(
|
||||
member=request.user, workspace__slug=slug, is_active=True
|
||||
)
|
||||
.annotate(
|
||||
draft_issue_count=Coalesce(
|
||||
Subquery(draft_issue_count, output_field=IntegerField()), 0
|
||||
)
|
||||
)
|
||||
WorkspaceMember.objects.filter(member=request.user, workspace__slug=slug, is_active=True)
|
||||
.annotate(draft_issue_count=Coalesce(Subquery(draft_issue_count, output_field=IntegerField()), 0))
|
||||
.first()
|
||||
)
|
||||
serializer = WorkspaceMemberMeSerializer(workspace_member)
|
||||
|
||||
@ -28,48 +28,34 @@ class QuickLinkViewSet(BaseViewSet):
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def partial_update(self, request, slug, pk):
|
||||
quick_link = WorkspaceUserLink.objects.filter(
|
||||
pk=pk, workspace__slug=slug, owner=request.user
|
||||
).first()
|
||||
quick_link = WorkspaceUserLink.objects.filter(pk=pk, workspace__slug=slug, owner=request.user).first()
|
||||
|
||||
if quick_link:
|
||||
serializer = WorkspaceUserLinkSerializer(
|
||||
quick_link, data=request.data, partial=True
|
||||
)
|
||||
serializer = WorkspaceUserLinkSerializer(quick_link, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response(
|
||||
{"detail": "Quick link not found."}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
return Response({"detail": "Quick link not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def retrieve(self, request, slug, pk):
|
||||
try:
|
||||
quick_link = WorkspaceUserLink.objects.get(
|
||||
pk=pk, workspace__slug=slug, owner=request.user
|
||||
)
|
||||
quick_link = WorkspaceUserLink.objects.get(pk=pk, workspace__slug=slug, owner=request.user)
|
||||
serializer = WorkspaceUserLinkSerializer(quick_link)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
except WorkspaceUserLink.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Quick link not found."}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
return Response({"error": "Quick link not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def destroy(self, request, slug, pk):
|
||||
quick_link = WorkspaceUserLink.objects.get(
|
||||
pk=pk, workspace__slug=slug, owner=request.user
|
||||
)
|
||||
quick_link = WorkspaceUserLink.objects.get(pk=pk, workspace__slug=slug, owner=request.user)
|
||||
quick_link.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def list(self, request, slug):
|
||||
quick_links = WorkspaceUserLink.objects.filter(
|
||||
workspace__slug=slug, owner=request.user
|
||||
)
|
||||
quick_links = WorkspaceUserLink.objects.filter(workspace__slug=slug, owner=request.user)
|
||||
|
||||
serializer = WorkspaceUserLinkSerializer(quick_links, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@ -19,18 +19,14 @@ class UserRecentVisitViewSet(BaseViewSet):
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def list(self, request, slug):
|
||||
user_recent_visits = UserRecentVisit.objects.filter(
|
||||
workspace__slug=slug, user=request.user
|
||||
)
|
||||
user_recent_visits = UserRecentVisit.objects.filter(workspace__slug=slug, user=request.user)
|
||||
|
||||
entity_name = request.query_params.get("entity_name")
|
||||
|
||||
if entity_name:
|
||||
user_recent_visits = user_recent_visits.filter(entity_name=entity_name)
|
||||
|
||||
user_recent_visits = user_recent_visits.filter(
|
||||
entity_name__in=["issue", "page", "project"]
|
||||
)
|
||||
user_recent_visits = user_recent_visits.filter(entity_name__in=["issue", "page", "project"])
|
||||
|
||||
serializer = WorkspaceRecentVisitSerializer(user_recent_visits[:20], many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@ -24,9 +24,7 @@ class WorkspaceStickyViewSet(BaseViewSet):
|
||||
.distinct()
|
||||
)
|
||||
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
|
||||
)
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def create(self, request, slug):
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
serializer = StickySerializer(data=request.data)
|
||||
@ -35,9 +33,7 @@ class WorkspaceStickyViewSet(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, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
|
||||
)
|
||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def list(self, request, slug):
|
||||
query = request.query_params.get("query", False)
|
||||
stickies = self.get_queryset().order_by("-sort_order")
|
||||
|
||||
@ -101,9 +101,7 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
|
||||
return (
|
||||
issues.annotate(
|
||||
cycle_id=Subquery(
|
||||
CycleIssue.objects.filter(
|
||||
issue=OuterRef("id"), deleted_at__isnull=True
|
||||
).values("cycle_id")[:1]
|
||||
CycleIssue.objects.filter(issue=OuterRef("id"), deleted_at__isnull=True).values("cycle_id")[:1]
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
@ -136,9 +134,7 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
|
||||
order_by_param = request.GET.get("order_by", "-created_at")
|
||||
issue_queryset = Issue.issue_objects.filter(
|
||||
id__in=Issue.issue_objects.filter(
|
||||
Q(assignees__in=[user_id])
|
||||
| Q(created_by_id=user_id)
|
||||
| Q(issue_subscribers__subscriber_id=user_id),
|
||||
Q(assignees__in=[user_id]) | Q(created_by_id=user_id) | Q(issue_subscribers__subscriber_id=user_id),
|
||||
workspace__slug=slug,
|
||||
).values_list("id", flat=True),
|
||||
workspace__slug=slug,
|
||||
@ -168,9 +164,7 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
|
||||
sub_group_by = request.GET.get("sub_group_by", False)
|
||||
|
||||
# issue queryset
|
||||
issue_queryset = issue_queryset_grouper(
|
||||
queryset=issue_queryset, group_by=group_by, sub_group_by=sub_group_by
|
||||
)
|
||||
issue_queryset = issue_queryset_grouper(queryset=issue_queryset, group_by=group_by, sub_group_by=sub_group_by)
|
||||
|
||||
if group_by:
|
||||
if sub_group_by:
|
||||
@ -247,9 +241,7 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
|
||||
request=request,
|
||||
queryset=issue_queryset,
|
||||
total_count_queryset=total_issue_queryset,
|
||||
on_results=lambda issues: issue_on_results(
|
||||
group_by=group_by, issues=issues, sub_group_by=sub_group_by
|
||||
),
|
||||
on_results=lambda issues: issue_on_results(group_by=group_by, issues=issues, sub_group_by=sub_group_by),
|
||||
)
|
||||
|
||||
|
||||
@ -257,19 +249,11 @@ class WorkspaceUserPropertiesEndpoint(BaseAPIView):
|
||||
permission_classes = [WorkspaceViewerPermission]
|
||||
|
||||
def patch(self, request, slug):
|
||||
workspace_properties = WorkspaceUserProperties.objects.get(
|
||||
user=request.user, workspace__slug=slug
|
||||
)
|
||||
workspace_properties = WorkspaceUserProperties.objects.get(user=request.user, workspace__slug=slug)
|
||||
|
||||
workspace_properties.filters = request.data.get(
|
||||
"filters", workspace_properties.filters
|
||||
)
|
||||
workspace_properties.rich_filters = request.data.get(
|
||||
"rich_filters", workspace_properties.rich_filters
|
||||
)
|
||||
workspace_properties.display_filters = request.data.get(
|
||||
"display_filters", workspace_properties.display_filters
|
||||
)
|
||||
workspace_properties.filters = request.data.get("filters", workspace_properties.filters)
|
||||
workspace_properties.rich_filters = request.data.get("rich_filters", workspace_properties.rich_filters)
|
||||
workspace_properties.display_filters = request.data.get("display_filters", workspace_properties.display_filters)
|
||||
workspace_properties.display_properties = request.data.get(
|
||||
"display_properties", workspace_properties.display_properties
|
||||
)
|
||||
@ -398,9 +382,7 @@ class WorkspaceUserActivityEndpoint(BaseAPIView):
|
||||
order_by=request.GET.get("order_by", "-created_at"),
|
||||
request=request,
|
||||
queryset=queryset,
|
||||
on_results=lambda issue_activities: IssueActivitySerializer(
|
||||
issue_activities, many=True
|
||||
).data,
|
||||
on_results=lambda issue_activities: IssueActivitySerializer(issue_activities, many=True).data,
|
||||
)
|
||||
|
||||
|
||||
@ -410,10 +392,7 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
|
||||
|
||||
state_distribution = (
|
||||
Issue.issue_objects.filter(
|
||||
(
|
||||
Q(assignees__in=[user_id])
|
||||
& Q(issue_assignee__deleted_at__isnull=True)
|
||||
),
|
||||
(Q(assignees__in=[user_id]) & Q(issue_assignee__deleted_at__isnull=True)),
|
||||
workspace__slug=slug,
|
||||
project__project_projectmember__member=request.user,
|
||||
project__project_projectmember__is_active=True,
|
||||
@ -429,10 +408,7 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
|
||||
|
||||
priority_distribution = (
|
||||
Issue.issue_objects.filter(
|
||||
(
|
||||
Q(assignees__in=[user_id])
|
||||
& Q(issue_assignee__deleted_at__isnull=True)
|
||||
),
|
||||
(Q(assignees__in=[user_id]) & Q(issue_assignee__deleted_at__isnull=True)),
|
||||
workspace__slug=slug,
|
||||
project__project_projectmember__member=request.user,
|
||||
project__project_projectmember__is_active=True,
|
||||
@ -443,10 +419,7 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
|
||||
.filter(priority_count__gte=1)
|
||||
.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)],
|
||||
default=Value(len(priority_order)),
|
||||
output_field=IntegerField(),
|
||||
)
|
||||
@ -467,10 +440,7 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
|
||||
|
||||
assigned_issues_count = (
|
||||
Issue.issue_objects.filter(
|
||||
(
|
||||
Q(assignees__in=[user_id])
|
||||
& Q(issue_assignee__deleted_at__isnull=True)
|
||||
),
|
||||
(Q(assignees__in=[user_id]) & Q(issue_assignee__deleted_at__isnull=True)),
|
||||
workspace__slug=slug,
|
||||
project__project_projectmember__member=request.user,
|
||||
project__project_projectmember__is_active=True,
|
||||
@ -482,10 +452,7 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
|
||||
pending_issues_count = (
|
||||
Issue.issue_objects.filter(
|
||||
~Q(state__group__in=["completed", "cancelled"]),
|
||||
(
|
||||
Q(assignees__in=[user_id])
|
||||
& Q(issue_assignee__deleted_at__isnull=True)
|
||||
),
|
||||
(Q(assignees__in=[user_id]) & Q(issue_assignee__deleted_at__isnull=True)),
|
||||
workspace__slug=slug,
|
||||
project__project_projectmember__member=request.user,
|
||||
project__project_projectmember__is_active=True,
|
||||
@ -496,10 +463,7 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
|
||||
|
||||
completed_issues_count = (
|
||||
Issue.issue_objects.filter(
|
||||
(
|
||||
Q(assignees__in=[user_id])
|
||||
& Q(issue_assignee__deleted_at__isnull=True)
|
||||
),
|
||||
(Q(assignees__in=[user_id]) & Q(issue_assignee__deleted_at__isnull=True)),
|
||||
workspace__slug=slug,
|
||||
state__group="completed",
|
||||
project__project_projectmember__member=request.user,
|
||||
|
||||
@ -22,9 +22,7 @@ class WorkspaceUserPreferenceViewSet(BaseAPIView):
|
||||
def get(self, request, slug):
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
|
||||
get_preference = WorkspaceUserPreference.objects.filter(
|
||||
user=request.user, workspace_id=workspace.id
|
||||
)
|
||||
get_preference = WorkspaceUserPreference.objects.filter(user=request.user, workspace_id=workspace.id)
|
||||
|
||||
create_preference_keys = []
|
||||
|
||||
@ -49,9 +47,7 @@ class WorkspaceUserPreferenceViewSet(BaseAPIView):
|
||||
)
|
||||
|
||||
preferences = (
|
||||
WorkspaceUserPreference.objects.filter(
|
||||
user=request.user, workspace_id=workspace.id
|
||||
)
|
||||
WorkspaceUserPreference.objects.filter(user=request.user, workspace_id=workspace.id)
|
||||
.order_by("sort_order")
|
||||
.values("key", "is_pinned", "sort_order")
|
||||
)
|
||||
@ -70,20 +66,14 @@ class WorkspaceUserPreferenceViewSet(BaseAPIView):
|
||||
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
|
||||
def patch(self, request, slug, key):
|
||||
preference = WorkspaceUserPreference.objects.filter(
|
||||
key=key, workspace__slug=slug, user=request.user
|
||||
).first()
|
||||
preference = WorkspaceUserPreference.objects.filter(key=key, workspace__slug=slug, user=request.user).first()
|
||||
|
||||
if preference:
|
||||
serializer = WorkspaceUserPreferenceSerializer(
|
||||
preference, data=request.data, partial=True
|
||||
)
|
||||
serializer = WorkspaceUserPreferenceSerializer(preference, data=request.data, partial=True)
|
||||
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
return Response(
|
||||
{"detail": "Preference not found"}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
return Response({"detail": "Preference not found"}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
@ -91,10 +91,7 @@ class Adapter:
|
||||
)
|
||||
|
||||
# Check if sign up is disabled and invite is present or not
|
||||
if (
|
||||
ENABLE_SIGNUP == "0"
|
||||
and not WorkspaceMemberInvite.objects.filter(email=email).exists()
|
||||
):
|
||||
if ENABLE_SIGNUP == "0" and not WorkspaceMemberInvite.objects.filter(email=email).exists():
|
||||
# Raise exception
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["SIGNUP_DISABLED"],
|
||||
|
||||
@ -73,9 +73,7 @@ class OauthAdapter(Adapter):
|
||||
return response.json()
|
||||
except requests.RequestException:
|
||||
code = self.authentication_error_code()
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[code], error_message=str(code)
|
||||
)
|
||||
raise AuthenticationException(error_code=AUTHENTICATION_ERROR_CODES[code], error_message=str(code))
|
||||
|
||||
def get_user_response(self):
|
||||
try:
|
||||
@ -85,9 +83,7 @@ class OauthAdapter(Adapter):
|
||||
return response.json()
|
||||
except requests.RequestException:
|
||||
code = self.authentication_error_code()
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[code], error_message=str(code)
|
||||
)
|
||||
raise AuthenticationException(error_code=AUTHENTICATION_ERROR_CODES[code], error_message=str(code))
|
||||
|
||||
def set_user_data(self, data):
|
||||
self.user_data = data
|
||||
@ -104,12 +100,8 @@ class OauthAdapter(Adapter):
|
||||
if account:
|
||||
account.access_token = self.token_data.get("access_token")
|
||||
account.refresh_token = self.token_data.get("refresh_token", None)
|
||||
account.access_token_expired_at = self.token_data.get(
|
||||
"access_token_expired_at"
|
||||
)
|
||||
account.refresh_token_expired_at = self.token_data.get(
|
||||
"refresh_token_expired_at"
|
||||
)
|
||||
account.access_token_expired_at = self.token_data.get("access_token_expired_at")
|
||||
account.refresh_token_expired_at = self.token_data.get("refresh_token_expired_at")
|
||||
account.last_connected_at = timezone.now()
|
||||
account.id_token = self.token_data.get("id_token", "")
|
||||
account.save()
|
||||
@ -118,17 +110,11 @@ class OauthAdapter(Adapter):
|
||||
Account.objects.create(
|
||||
user=user,
|
||||
provider=self.provider,
|
||||
provider_account_id=self.user_data.get("user", {}).get(
|
||||
"provider_id"
|
||||
),
|
||||
provider_account_id=self.user_data.get("user", {}).get("provider_id"),
|
||||
access_token=self.token_data.get("access_token"),
|
||||
refresh_token=self.token_data.get("refresh_token", None),
|
||||
access_token_expired_at=self.token_data.get(
|
||||
"access_token_expired_at"
|
||||
),
|
||||
refresh_token_expired_at=self.token_data.get(
|
||||
"refresh_token_expired_at"
|
||||
),
|
||||
access_token_expired_at=self.token_data.get("access_token_expired_at"),
|
||||
refresh_token_expired_at=self.token_data.get("refresh_token_expired_at"),
|
||||
last_connected_at=timezone.now(),
|
||||
id_token=self.token_data.get("id_token", ""),
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user