[WEB-5044] fix: ruff lint and format errors (#7868)

* fix: lint errors

* fix: file formatting

* fix: code refactor
This commit is contained in:
sriram veeraghanta 2025-09-29 19:15:32 +05:30 committed by GitHub
parent 1fb22bd252
commit 9237f568dd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
261 changed files with 2199 additions and 6378 deletions

View file

@ -46,9 +46,7 @@ class AssetUpdateSerializer(serializers.Serializer):
and upload confirmation for S3-based file storage workflows. and upload confirmation for S3-based file storage workflows.
""" """
attributes = serializers.JSONField( attributes = serializers.JSONField(required=False, help_text="Additional attributes to update for the asset")
required=False, help_text="Additional attributes to update for the asset"
)
class GenericAssetUploadSerializer(serializers.Serializer): class GenericAssetUploadSerializer(serializers.Serializer):
@ -85,9 +83,7 @@ class GenericAssetUpdateSerializer(serializers.Serializer):
upload completion marking and metadata finalization. upload completion marking and metadata finalization.
""" """
is_uploaded = serializers.BooleanField( is_uploaded = serializers.BooleanField(default=True, help_text="Whether the asset has been successfully uploaded")
default=True, help_text="Whether the asset has been successfully uploaded"
)
class FileAssetSerializer(BaseSerializer): class FileAssetSerializer(BaseSerializer):

View file

@ -103,13 +103,9 @@ class BaseSerializer(serializers.ModelSerializer):
# Check if field in expansion then expand the field # Check if field in expansion then expand the field
if expand in expansion: if expand in expansion:
if isinstance(response.get(expand), list): if isinstance(response.get(expand), list):
exp_serializer = expansion[expand]( exp_serializer = expansion[expand](getattr(instance, expand), many=True)
getattr(instance, expand), many=True
)
else: else:
exp_serializer = expansion[expand]( exp_serializer = expansion[expand](getattr(instance, expand))
getattr(instance, expand)
)
response[expand] = exp_serializer.data response[expand] = exp_serializer.data
else: else:
# You might need to handle this case differently # You might need to handle this case differently

View file

@ -55,14 +55,9 @@ class CycleCreateSerializer(BaseSerializer):
): ):
raise serializers.ValidationError("Start date cannot exceed end date") raise serializers.ValidationError("Start date cannot exceed end date")
if ( if data.get("start_date", None) is not None and data.get("end_date", None) is not None:
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 ( project_id = self.initial_data.get("project_id") or (
self.instance.project_id self.instance.project_id if self.instance and hasattr(self.instance, "project_id") else None
if self.instance and hasattr(self.instance, "project_id")
else None
) )
if not project_id: if not project_id:
@ -166,9 +161,7 @@ class CycleIssueRequestSerializer(serializers.Serializer):
cycle assignment and sprint planning workflows. cycle assignment and sprint planning workflows.
""" """
issues = serializers.ListField( issues = serializers.ListField(child=serializers.UUIDField(), help_text="List of issue IDs to add to the cycle")
child=serializers.UUIDField(), help_text="List of issue IDs to add to the cycle"
)
class TransferCycleIssueRequestSerializer(serializers.Serializer): class TransferCycleIssueRequestSerializer(serializers.Serializer):
@ -179,6 +172,4 @@ class TransferCycleIssueRequestSerializer(serializers.Serializer):
and relationship updates for sprint reallocation workflows. and relationship updates for sprint reallocation workflows.
""" """
new_cycle_id = serializers.UUIDField( new_cycle_id = serializers.UUIDField(help_text="ID of the target cycle to transfer issues to")
help_text="ID of the target cycle to transfer issues to"
)

View file

@ -98,9 +98,7 @@ class IntakeIssueUpdateSerializer(BaseSerializer):
and embedded issue updates for issue queue processing workflows. and embedded issue updates for issue queue processing workflows.
""" """
issue = IssueForIntakeSerializer( issue = IssueForIntakeSerializer(required=False, help_text="Issue data to update in the intake issue")
required=False, help_text="Issue data to update in the intake issue"
)
class Meta: class Meta:
model = IntakeIssue model = IntakeIssue
@ -132,9 +130,5 @@ class IssueDataSerializer(serializers.Serializer):
""" """
name = serializers.CharField(max_length=255, help_text="Issue name") name = serializers.CharField(max_length=255, help_text="Issue name")
description_html = serializers.CharField( description_html = serializers.CharField(required=False, allow_null=True, help_text="Issue description HTML")
required=False, allow_null=True, help_text="Issue description HTML" priority = serializers.ChoiceField(choices=Issue.PRIORITY_CHOICES, default="none", help_text="Issue priority")
)
priority = serializers.ChoiceField(
choices=Issue.PRIORITY_CHOICES, default="none", help_text="Issue priority"
)

View file

@ -48,17 +48,13 @@ class IssueSerializer(BaseSerializer):
""" """
assignees = serializers.ListField( assignees = serializers.ListField(
child=serializers.PrimaryKeyRelatedField( child=serializers.PrimaryKeyRelatedField(queryset=User.objects.values_list("id", flat=True)),
queryset=User.objects.values_list("id", flat=True)
),
write_only=True, write_only=True,
required=False, required=False,
) )
labels = serializers.ListField( labels = serializers.ListField(
child=serializers.PrimaryKeyRelatedField( child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.values_list("id", flat=True)),
queryset=Label.objects.values_list("id", flat=True)
),
write_only=True, write_only=True,
required=False, required=False,
) )
@ -90,13 +86,9 @@ class IssueSerializer(BaseSerializer):
# Validate description content for security # Validate description content for security
if data.get("description_html"): if data.get("description_html"):
is_valid, error_msg, sanitized_html = validate_html_content( is_valid, error_msg, sanitized_html = validate_html_content(data["description_html"])
data["description_html"]
)
if not is_valid: if not is_valid:
raise serializers.ValidationError( raise serializers.ValidationError({"error": "html content is not valid"})
{"error": "html content is not valid"}
)
# Update the data with sanitized HTML if available # Update the data with sanitized HTML if available
if sanitized_html is not None: if sanitized_html is not None:
data["description_html"] = sanitized_html data["description_html"] = sanitized_html
@ -104,9 +96,7 @@ class IssueSerializer(BaseSerializer):
if data.get("description_binary"): if data.get("description_binary"):
is_valid, error_msg = validate_binary_data(data["description_binary"]) is_valid, error_msg = validate_binary_data(data["description_binary"])
if not is_valid: if not is_valid:
raise serializers.ValidationError( raise serializers.ValidationError({"description_binary": "Invalid binary data"})
{"description_binary": "Invalid binary data"}
)
# Validate assignees are from project # Validate assignees are from project
if data.get("assignees", []): if data.get("assignees", []):
@ -126,13 +116,9 @@ class IssueSerializer(BaseSerializer):
# Check state is from the project only else raise validation error # Check state is from the project only else raise validation error
if ( if (
data.get("state") data.get("state")
and not State.objects.filter( and not State.objects.filter(project_id=self.context.get("project_id"), pk=data.get("state").id).exists()
project_id=self.context.get("project_id"), pk=data.get("state").id
).exists()
): ):
raise serializers.ValidationError( raise serializers.ValidationError("State is not valid please pass a valid state_id")
"State is not valid please pass a valid state_id"
)
# Check parent issue is from workspace as it can be cross workspace # Check parent issue is from workspace as it can be cross workspace
if ( if (
@ -143,9 +129,7 @@ class IssueSerializer(BaseSerializer):
pk=data.get("parent").id, pk=data.get("parent").id,
).exists() ).exists()
): ):
raise serializers.ValidationError( raise serializers.ValidationError("Parent is not valid issue_id please pass a valid issue_id")
"Parent is not valid issue_id please pass a valid issue_id"
)
if ( if (
data.get("estimate_point") data.get("estimate_point")
@ -155,9 +139,7 @@ class IssueSerializer(BaseSerializer):
pk=data.get("estimate_point").id, pk=data.get("estimate_point").id,
).exists() ).exists()
): ):
raise serializers.ValidationError( raise serializers.ValidationError("Estimate point is not valid please pass a valid estimate_point_id")
"Estimate point is not valid please pass a valid estimate_point_id"
)
return data return data
@ -173,14 +155,10 @@ class IssueSerializer(BaseSerializer):
if not issue_type: if not issue_type:
# Get default issue type # Get default issue type
issue_type = IssueType.objects.filter( issue_type = IssueType.objects.filter(project_issue_types__project_id=project_id, is_default=True).first()
project_issue_types__project_id=project_id, is_default=True
).first()
issue_type = issue_type issue_type = issue_type
issue = Issue.objects.create( issue = Issue.objects.create(**validated_data, project_id=project_id, type=issue_type)
**validated_data, project_id=project_id, type=issue_type
)
# Issue Audit Users # Issue Audit Users
created_by_id = issue.created_by_id created_by_id = issue.created_by_id
@ -312,35 +290,26 @@ class IssueSerializer(BaseSerializer):
data["assignees"] = UserLiteSerializer( data["assignees"] = UserLiteSerializer(
User.objects.filter( User.objects.filter(
pk__in=IssueAssignee.objects.filter(issue=instance).values_list( pk__in=IssueAssignee.objects.filter(issue=instance).values_list("assignee_id", flat=True)
"assignee_id", flat=True
)
), ),
many=True, many=True,
).data ).data
else: else:
data["assignees"] = [ data["assignees"] = [
str(assignee) str(assignee)
for assignee in IssueAssignee.objects.filter( for assignee in IssueAssignee.objects.filter(issue=instance).values_list("assignee_id", flat=True)
issue=instance
).values_list("assignee_id", flat=True)
] ]
if "labels" in self.fields: if "labels" in self.fields:
if "labels" in self.expand: if "labels" in self.expand:
data["labels"] = LabelSerializer( data["labels"] = LabelSerializer(
Label.objects.filter( Label.objects.filter(
pk__in=IssueLabel.objects.filter(issue=instance).values_list( pk__in=IssueLabel.objects.filter(issue=instance).values_list("label_id", flat=True)
"label_id", flat=True
)
), ),
many=True, many=True,
).data ).data
else: else:
data["labels"] = [ data["labels"] = [
str(label) str(label) for label in IssueLabel.objects.filter(issue=instance).values_list("label_id", flat=True)
for label in IssueLabel.objects.filter(issue=instance).values_list(
"label_id", flat=True
)
] ]
return data return data
@ -452,12 +421,8 @@ class IssueLinkCreateSerializer(BaseSerializer):
# Validation if url already exists # Validation if url already exists
def create(self, validated_data): def create(self, validated_data):
if IssueLink.objects.filter( if IssueLink.objects.filter(url=validated_data.get("url"), issue_id=validated_data.get("issue_id")).exists():
url=validated_data.get("url"), issue_id=validated_data.get("issue_id") raise serializers.ValidationError({"error": "URL already exists for this Issue"})
).exists():
raise serializers.ValidationError(
{"error": "URL already exists for this Issue"}
)
return IssueLink.objects.create(**validated_data) return IssueLink.objects.create(**validated_data)
@ -478,15 +443,11 @@ class IssueLinkUpdateSerializer(IssueLinkCreateSerializer):
def update(self, instance, validated_data): def update(self, instance, validated_data):
if ( if (
IssueLink.objects.filter( IssueLink.objects.filter(url=validated_data.get("url"), issue_id=instance.issue_id)
url=validated_data.get("url"), issue_id=instance.issue_id
)
.exclude(pk=instance.id) .exclude(pk=instance.id)
.exists() .exists()
): ):
raise serializers.ValidationError( raise serializers.ValidationError({"error": "URL already exists for this Issue"})
{"error": "URL already exists for this Issue"}
)
return super().update(instance, validated_data) return super().update(instance, validated_data)
@ -677,17 +638,13 @@ class IssueExpandSerializer(BaseSerializer):
expand = self.context.get("expand", []) expand = self.context.get("expand", [])
if "labels" in expand: if "labels" in expand:
# Use prefetched data # Use prefetched data
return LabelLiteSerializer( return LabelLiteSerializer([il.label for il in obj.label_issue.all()], many=True).data
[il.label for il in obj.label_issue.all()], many=True
).data
return [il.label_id for il in obj.label_issue.all()] return [il.label_id for il in obj.label_issue.all()]
def get_assignees(self, obj): def get_assignees(self, obj):
expand = self.context.get("expand", []) expand = self.context.get("expand", [])
if "assignees" in expand: if "assignees" in expand:
return UserLiteSerializer( return UserLiteSerializer([ia.assignee for ia in obj.issue_assignee.all()], many=True).data
[ia.assignee for ia in obj.issue_assignee.all()], many=True
).data
return [ia.assignee_id for ia in obj.issue_assignee.all()] return [ia.assignee_id for ia in obj.issue_assignee.all()]
class Meta: class Meta:
@ -735,8 +692,6 @@ class IssueSearchSerializer(serializers.Serializer):
id = serializers.CharField(required=True, help_text="Issue ID") id = serializers.CharField(required=True, help_text="Issue ID")
name = serializers.CharField(required=True, help_text="Issue name") name = serializers.CharField(required=True, help_text="Issue name")
sequence_id = serializers.CharField(required=True, help_text="Issue sequence ID") sequence_id = serializers.CharField(required=True, help_text="Issue sequence ID")
project__identifier = serializers.CharField( project__identifier = serializers.CharField(required=True, help_text="Project identifier")
required=True, help_text="Project identifier"
)
project_id = serializers.CharField(required=True, help_text="Project ID") project_id = serializers.CharField(required=True, help_text="Project ID")
workspace__slug = serializers.CharField(required=True, help_text="Workspace slug") workspace__slug = serializers.CharField(required=True, help_text="Workspace slug")

View file

@ -76,9 +76,15 @@ class ModuleCreateSerializer(BaseSerializer):
module_name = validated_data.get("name") module_name = validated_data.get("name")
if module_name: if module_name:
# Lookup for the module name in the module table for that project # 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( 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) module = Module.objects.create(**validated_data, project_id=project_id)
@ -123,14 +129,8 @@ class ModuleUpdateSerializer(ModuleCreateSerializer):
module_name = validated_data.get("name") module_name = validated_data.get("name")
if module_name: if module_name:
# Lookup for the module name in the module table for that project # Lookup for the module name in the module table for that project
if ( if Module.objects.filter(name=module_name, project=instance.project).exclude(id=instance.id).exists():
Module.objects.filter(name=module_name, project=instance.project) raise serializers.ValidationError({"error": "Module with this name already exists"})
.exclude(id=instance.id)
.exists()
):
raise serializers.ValidationError(
{"error": "Module with this name already exists"}
)
if members is not None: if members is not None:
ModuleMember.objects.filter(module=instance).delete() ModuleMember.objects.filter(module=instance).delete()
@ -240,12 +240,8 @@ class ModuleLinkSerializer(BaseSerializer):
# Validation if url already exists # Validation if url already exists
def create(self, validated_data): def create(self, validated_data):
if ModuleLink.objects.filter( if ModuleLink.objects.filter(url=validated_data.get("url"), module_id=validated_data.get("module_id")).exists():
url=validated_data.get("url"), module_id=validated_data.get("module_id") raise serializers.ValidationError({"error": "URL already exists for this Issue"})
).exists():
raise serializers.ValidationError(
{"error": "URL already exists for this Issue"}
)
return ModuleLink.objects.create(**validated_data) return ModuleLink.objects.create(**validated_data)

View file

@ -66,9 +66,7 @@ class ProjectCreateSerializer(BaseSerializer):
workspace_id=self.context["workspace_id"], workspace_id=self.context["workspace_id"],
member_id=data.get("project_lead"), member_id=data.get("project_lead"),
).exists(): ).exists():
raise serializers.ValidationError( raise serializers.ValidationError("Project lead should be a user in the workspace")
"Project lead should be a user in the workspace"
)
if data.get("default_assignee", None) is not None: if data.get("default_assignee", None) is not None:
# Check if the default assignee is a member of the workspace # Check if the default assignee is a member of the workspace
@ -76,9 +74,7 @@ class ProjectCreateSerializer(BaseSerializer):
workspace_id=self.context["workspace_id"], workspace_id=self.context["workspace_id"],
member_id=data.get("default_assignee"), member_id=data.get("default_assignee"),
).exists(): ).exists():
raise serializers.ValidationError( raise serializers.ValidationError("Default assignee should be a user in the workspace")
"Default assignee should be a user in the workspace"
)
return data return data
@ -87,14 +83,10 @@ class ProjectCreateSerializer(BaseSerializer):
if identifier == "": if identifier == "":
raise serializers.ValidationError(detail="Project Identifier is required") raise serializers.ValidationError(detail="Project Identifier is required")
if ProjectIdentifier.objects.filter( if ProjectIdentifier.objects.filter(name=identifier, workspace_id=self.context["workspace_id"]).exists():
name=identifier, workspace_id=self.context["workspace_id"]
).exists():
raise serializers.ValidationError(detail="Project Identifier is taken") raise serializers.ValidationError(detail="Project Identifier is taken")
project = Project.objects.create( project = Project.objects.create(**validated_data, workspace_id=self.context["workspace_id"])
**validated_data, workspace_id=self.context["workspace_id"]
)
return project return project
@ -119,25 +111,17 @@ class ProjectUpdateSerializer(ProjectCreateSerializer):
"""Update a project""" """Update a project"""
if ( if (
validated_data.get("default_state", None) is not None validated_data.get("default_state", None) is not None
and not State.objects.filter( and not State.objects.filter(project=instance, id=validated_data.get("default_state")).exists()
project=instance, id=validated_data.get("default_state")
).exists()
): ):
# Check if the default state is a state in the project # Check if the default state is a state in the project
raise serializers.ValidationError( raise serializers.ValidationError("Default state should be a state in the project")
"Default state should be a state in the project"
)
if ( if (
validated_data.get("estimate", None) is not None validated_data.get("estimate", None) is not None
and not Estimate.objects.filter( and not Estimate.objects.filter(project=instance, id=validated_data.get("estimate")).exists()
project=instance, id=validated_data.get("estimate")
).exists()
): ):
# Check if the estimate is a estimate in the project # Check if the estimate is a estimate in the project
raise serializers.ValidationError( raise serializers.ValidationError("Estimate should be a estimate in the project")
"Estimate should be a estimate in the project"
)
return super().update(instance, validated_data) return super().update(instance, validated_data)
@ -182,9 +166,7 @@ class ProjectSerializer(BaseSerializer):
member_id=data.get("project_lead"), member_id=data.get("project_lead"),
).exists() ).exists()
): ):
raise serializers.ValidationError( raise serializers.ValidationError("Project lead should be a user in the workspace")
"Project lead should be a user in the workspace"
)
# Check default assignee should be a member of the workspace # Check default assignee should be a member of the workspace
if ( if (
@ -194,23 +176,17 @@ class ProjectSerializer(BaseSerializer):
member_id=data.get("default_assignee"), member_id=data.get("default_assignee"),
).exists() ).exists()
): ):
raise serializers.ValidationError( raise serializers.ValidationError("Default assignee should be a user in the workspace")
"Default assignee should be a user in the workspace"
)
# Validate description content for security # Validate description content for security
if "description_html" in data and data["description_html"]: if "description_html" in data and data["description_html"]:
if isinstance(data["description_html"], dict): if isinstance(data["description_html"], dict):
is_valid, error_msg, sanitized_html = validate_html_content( is_valid, error_msg, sanitized_html = validate_html_content(str(data["description_html"]))
str(data["description_html"])
)
# Update the data with sanitized HTML if available # Update the data with sanitized HTML if available
if sanitized_html is not None: if sanitized_html is not None:
data["description_html"] = sanitized_html data["description_html"] = sanitized_html
if not is_valid: if not is_valid:
raise serializers.ValidationError( raise serializers.ValidationError({"error": "html content is not valid"})
{"error": "html content is not valid"}
)
return data return data
@ -219,14 +195,10 @@ class ProjectSerializer(BaseSerializer):
if identifier == "": if identifier == "":
raise serializers.ValidationError(detail="Project Identifier is required") raise serializers.ValidationError(detail="Project Identifier is required")
if ProjectIdentifier.objects.filter( if ProjectIdentifier.objects.filter(name=identifier, workspace_id=self.context["workspace_id"]).exists():
name=identifier, workspace_id=self.context["workspace_id"]
).exists():
raise serializers.ValidationError(detail="Project Identifier is taken") raise serializers.ValidationError(detail="Project Identifier is taken")
project = Project.objects.create( project = Project.objects.create(**validated_data, workspace_id=self.context["workspace_id"])
**validated_data, workspace_id=self.context["workspace_id"]
)
_ = ProjectIdentifier.objects.create( _ = ProjectIdentifier.objects.create(
name=project.identifier, name=project.identifier,
project=project, project=project,

View file

@ -14,9 +14,7 @@ class StateSerializer(BaseSerializer):
def validate(self, data): def validate(self, data):
# If the default is being provided then make all other states default False # If the default is being provided then make all other states default False
if data.get("default", False): if data.get("default", False):
State.objects.filter(project_id=self.context.get("project_id")).update( State.objects.filter(project_id=self.context.get("project_id")).update(default=False)
default=False
)
return data return data
class Meta: class Meta:

View file

@ -14,9 +14,7 @@ urlpatterns = [
), ),
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/intake-issues/<uuid:issue_id>/", "workspaces/<str:slug>/projects/<uuid:project_id>/intake-issues/<uuid:issue_id>/",
IntakeIssueDetailAPIEndpoint.as_view( IntakeIssueDetailAPIEndpoint.as_view(http_method_names=["get", "patch", "delete"]),
http_method_names=["get", "patch", "delete"]
),
name="intake-issue", name="intake-issue",
), ),
] ]

View file

@ -55,9 +55,7 @@ urlpatterns = [
), ),
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/links/<uuid:pk>/", "workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/links/<uuid:pk>/",
IssueLinkDetailAPIEndpoint.as_view( IssueLinkDetailAPIEndpoint.as_view(http_method_names=["get", "patch", "delete"]),
http_method_names=["get", "patch", "delete"]
),
name="link", name="link",
), ),
path( path(
@ -67,9 +65,7 @@ urlpatterns = [
), ),
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/comments/<uuid:pk>/", "workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/comments/<uuid:pk>/",
IssueCommentDetailAPIEndpoint.as_view( IssueCommentDetailAPIEndpoint.as_view(http_method_names=["get", "patch", "delete"]),
http_method_names=["get", "patch", "delete"]
),
name="comment", name="comment",
), ),
path( path(
@ -89,9 +85,7 @@ urlpatterns = [
), ),
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-attachments/<uuid:pk>/", "workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-attachments/<uuid:pk>/",
IssueAttachmentDetailAPIEndpoint.as_view( IssueAttachmentDetailAPIEndpoint.as_view(http_method_names=["get", "patch", "delete"]),
http_method_names=["get", "patch", "delete"]
),
name="issue-attachment", name="issue-attachment",
), ),
] ]

View file

@ -19,9 +19,7 @@ urlpatterns = [
), ),
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/archive/", "workspaces/<str:slug>/projects/<uuid:project_id>/archive/",
ProjectArchiveUnarchiveAPIEndpoint.as_view( ProjectArchiveUnarchiveAPIEndpoint.as_view(http_method_names=["post", "delete"]),
http_method_names=["post", "delete"]
),
name="project-archive-unarchive", name="project-archive-unarchive",
), ),
] ]

View file

@ -158,9 +158,7 @@ class UserAssetEndpoint(BaseAPIView):
# Get the presigned URL # Get the presigned URL
storage = S3Storage(request=request) storage = S3Storage(request=request)
# Generate a presigned URL to share an S3 object # Generate a presigned URL to share an S3 object
presigned_url = storage.generate_presigned_post( presigned_url = storage.generate_presigned_post(object_name=asset_key, file_type=type, file_size=size_limit)
object_name=asset_key, file_type=type, file_size=size_limit
)
# Return the presigned URL # Return the presigned URL
return Response( return Response(
{ {
@ -236,9 +234,7 @@ class UserAssetEndpoint(BaseAPIView):
asset.is_deleted = True asset.is_deleted = True
asset.deleted_at = timezone.now() asset.deleted_at = timezone.now()
# get the entity and save the asset id for the request field # get the entity and save the asset id for the request field
self.entity_asset_delete( self.entity_asset_delete(entity_type=asset.entity_type, asset=asset, request=request)
entity_type=asset.entity_type, asset=asset, request=request
)
asset.save(update_fields=["is_deleted", "deleted_at"]) asset.save(update_fields=["is_deleted", "deleted_at"])
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
@ -335,9 +331,7 @@ class UserServerAssetEndpoint(BaseAPIView):
# Get the presigned URL # Get the presigned URL
storage = S3Storage(request=request, is_server=True) storage = S3Storage(request=request, is_server=True)
# Generate a presigned URL to share an S3 object # Generate a presigned URL to share an S3 object
presigned_url = storage.generate_presigned_post( presigned_url = storage.generate_presigned_post(object_name=asset_key, file_type=type, file_size=size_limit)
object_name=asset_key, file_type=type, file_size=size_limit
)
# Return the presigned URL # Return the presigned URL
return Response( return Response(
{ {
@ -389,16 +383,15 @@ class UserServerAssetEndpoint(BaseAPIView):
def delete(self, request, asset_id): def delete(self, request, asset_id):
"""Delete user server asset. """Delete user server asset.
Delete a user profile asset (avatar or cover image) using server credentials and remove its reference from the user profile. Delete a user profile asset (avatar or cover image) using server credentials and
This performs a soft delete by marking the asset as deleted and updating the user's profile. 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 = FileAsset.objects.get(id=asset_id, user_id=request.user.id)
asset.is_deleted = True asset.is_deleted = True
asset.deleted_at = timezone.now() asset.deleted_at = timezone.now()
# get the entity and save the asset id for the request field # get the entity and save the asset id for the request field
self.entity_asset_delete( self.entity_asset_delete(entity_type=asset.entity_type, asset=asset, request=request)
entity_type=asset.entity_type, asset=asset, request=request
)
asset.save(update_fields=["is_deleted", "deleted_at"]) asset.save(update_fields=["is_deleted", "deleted_at"])
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
@ -430,9 +423,7 @@ class GenericAssetEndpoint(BaseAPIView):
workspace = Workspace.objects.get(slug=slug) workspace = Workspace.objects.get(slug=slug)
# Get the asset # Get the asset
asset = FileAsset.objects.get( asset = FileAsset.objects.get(id=asset_id, workspace_id=workspace.id, is_deleted=False)
id=asset_id, workspace_id=workspace.id, is_deleted=False
)
# Check if the asset exists and is uploaded # Check if the asset exists and is uploaded
if not asset.is_uploaded: if not asset.is_uploaded:
@ -458,13 +449,9 @@ class GenericAssetEndpoint(BaseAPIView):
) )
except Workspace.DoesNotExist: except Workspace.DoesNotExist:
return Response( return Response({"error": "Workspace not found"}, status=status.HTTP_404_NOT_FOUND)
{"error": "Workspace not found"}, status=status.HTTP_404_NOT_FOUND
)
except FileAsset.DoesNotExist: except FileAsset.DoesNotExist:
return Response( return Response({"error": "Asset not found"}, status=status.HTTP_404_NOT_FOUND)
{"error": "Asset not found"}, status=status.HTTP_404_NOT_FOUND
)
except Exception as e: except Exception as e:
log_exception(e) log_exception(e)
return Response( return Response(
@ -566,14 +553,12 @@ class GenericAssetEndpoint(BaseAPIView):
created_by=request.user, created_by=request.user,
external_id=external_id, external_id=external_id,
external_source=external_source, 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 # Get the presigned URL
storage = S3Storage(request=request, is_server=True) storage = S3Storage(request=request, is_server=True)
presigned_url = storage.generate_presigned_post( presigned_url = storage.generate_presigned_post(object_name=asset_key, file_type=type, file_size=size_limit)
object_name=asset_key, file_type=type, file_size=size_limit
)
return Response( return Response(
{ {
@ -612,9 +597,7 @@ class GenericAssetEndpoint(BaseAPIView):
and trigger metadata extraction. and trigger metadata extraction.
""" """
try: try:
asset = FileAsset.objects.get( asset = FileAsset.objects.get(id=asset_id, workspace__slug=slug, is_deleted=False)
id=asset_id, workspace__slug=slug, is_deleted=False
)
# Update is_uploaded status # Update is_uploaded status
asset.is_uploaded = request.data.get("is_uploaded", asset.is_uploaded) 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) return Response(status=status.HTTP_204_NO_CONTENT)
except FileAsset.DoesNotExist: except FileAsset.DoesNotExist:
return Response( return Response({"error": "Asset not found"}, status=status.HTTP_404_NOT_FOUND)
{"error": "Asset not found"}, status=status.HTTP_404_NOT_FOUND
)

View file

@ -37,9 +37,7 @@ class TimezoneMixin:
timezone.deactivate() timezone.deactivate()
class BaseAPIView( class BaseAPIView(TimezoneMixin, GenericAPIView, ReadReplicaControlMixin, BasePaginator):
TimezoneMixin, GenericAPIView, ReadReplicaControlMixin, BasePaginator
):
authentication_classes = [APIKeyAuthentication] authentication_classes = [APIKeyAuthentication]
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
@ -56,9 +54,7 @@ class BaseAPIView(
api_key = self.request.headers.get("X-Api-Key") api_key = self.request.headers.get("X-Api-Key")
if api_key: if api_key:
service_token = APIToken.objects.filter( service_token = APIToken.objects.filter(token=api_key, is_service=True).first()
token=api_key, is_service=True
).first()
if service_token: if service_token:
throttle_classes.append(ServiceTokenRateThrottle()) throttle_classes.append(ServiceTokenRateThrottle())
@ -113,9 +109,7 @@ class BaseAPIView(
if settings.DEBUG: if settings.DEBUG:
from django.db import connection from django.db import connection
print( print(f"{request.method} - {request.get_full_path()} of Queries: {len(connection.queries)}")
f"{request.method} - {request.get_full_path()} of Queries: {len(connection.queries)}"
)
return response return response
except Exception as exc: except Exception as exc:
response = self.handle_exception(exc) response = self.handle_exception(exc)
@ -151,14 +145,10 @@ class BaseAPIView(
@property @property
def fields(self): def fields(self):
fields = [ fields = [field for field in self.request.GET.get("fields", "").split(",") if field]
field for field in self.request.GET.get("fields", "").split(",") if field
]
return fields if fields else None return fields if fields else None
@property @property
def expand(self): def expand(self):
expand = [ expand = [expand for expand in self.request.GET.get("expand", "").split(",") if expand]
expand for expand in self.request.GET.get("expand", "").split(",") if expand
]
return expand if expand else None return expand if expand else None

View file

@ -171,7 +171,7 @@ class CycleListCreateAPIEndpoint(BaseAPIView):
@cycle_docs( @cycle_docs(
operation_id="list_cycles", operation_id="list_cycles",
summary="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=[ parameters=[
CURSOR_PARAMETER, CURSOR_PARAMETER,
PER_PAGE_PARAMETER, PER_PAGE_PARAMETER,
@ -201,9 +201,7 @@ class CycleListCreateAPIEndpoint(BaseAPIView):
# Current Cycle # Current Cycle
if cycle_view == "current": if cycle_view == "current":
queryset = queryset.filter( queryset = queryset.filter(start_date__lte=timezone.now(), end_date__gte=timezone.now())
start_date__lte=timezone.now(), end_date__gte=timezone.now()
)
data = CycleSerializer( data = CycleSerializer(
queryset, queryset,
many=True, many=True,
@ -260,9 +258,7 @@ class CycleListCreateAPIEndpoint(BaseAPIView):
# Incomplete Cycles # Incomplete Cycles
if cycle_view == "incomplete": if cycle_view == "incomplete":
queryset = queryset.filter( queryset = queryset.filter(Q(end_date__gte=timezone.now()) | Q(end_date__isnull=True))
Q(end_date__gte=timezone.now()) | Q(end_date__isnull=True)
)
return self.paginate( return self.paginate(
request=request, request=request,
queryset=(queryset), queryset=(queryset),
@ -289,7 +285,7 @@ class CycleListCreateAPIEndpoint(BaseAPIView):
@cycle_docs( @cycle_docs(
operation_id="create_cycle", operation_id="create_cycle",
summary="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=OpenApiRequest(
request=CycleCreateSerializer, request=CycleCreateSerializer,
examples=[CYCLE_CREATE_EXAMPLE], examples=[CYCLE_CREATE_EXAMPLE],
@ -308,12 +304,8 @@ class CycleListCreateAPIEndpoint(BaseAPIView):
Create a new development cycle with specified name, description, and date range. Create a new development cycle with specified name, description, and date range.
Supports external ID tracking for integration purposes. Supports external ID tracking for integration purposes.
""" """
if ( 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 None request.data.get("start_date", None) is not None and request.data.get("end_date", None) is not 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) serializer = CycleCreateSerializer(data=request.data)
if serializer.is_valid(): if serializer.is_valid():
@ -358,9 +350,7 @@ class CycleListCreateAPIEndpoint(BaseAPIView):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
else: else:
return Response( 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, status=status.HTTP_400_BAD_REQUEST,
) )
@ -487,7 +477,7 @@ class CycleDetailAPIEndpoint(BaseAPIView):
@cycle_docs( @cycle_docs(
operation_id="update_cycle", operation_id="update_cycle",
summary="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=OpenApiRequest(
request=CycleUpdateSerializer, request=CycleUpdateSerializer,
examples=[CYCLE_UPDATE_EXAMPLE], examples=[CYCLE_UPDATE_EXAMPLE],
@ -508,9 +498,7 @@ class CycleDetailAPIEndpoint(BaseAPIView):
""" """
cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk) cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
current_instance = json.dumps( current_instance = json.dumps(CycleSerializer(cycle).data, cls=DjangoJSONEncoder)
CycleSerializer(cycle).data, cls=DjangoJSONEncoder
)
if cycle.archived_at: if cycle.archived_at:
return Response( return Response(
@ -523,14 +511,10 @@ class CycleDetailAPIEndpoint(BaseAPIView):
if cycle.end_date is not None and cycle.end_date < timezone.now(): if cycle.end_date is not None and cycle.end_date < timezone.now():
if "sort_order" in request_data: if "sort_order" in request_data:
# Can only change sort order # Can only change sort order
request_data = { request_data = {"sort_order": request_data.get("sort_order", cycle.sort_order)}
"sort_order": request_data.get("sort_order", cycle.sort_order)
}
else: else:
return Response( 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, status=status.HTTP_400_BAD_REQUEST,
) )
@ -542,9 +526,7 @@ class CycleDetailAPIEndpoint(BaseAPIView):
and Cycle.objects.filter( and Cycle.objects.filter(
project_id=project_id, project_id=project_id,
workspace__slug=slug, workspace__slug=slug,
external_source=request.data.get( external_source=request.data.get("external_source", cycle.external_source),
"external_source", cycle.external_source
),
external_id=request.data.get("external_id"), external_id=request.data.get("external_id"),
).exists() ).exists()
): ):
@ -601,11 +583,7 @@ class CycleDetailAPIEndpoint(BaseAPIView):
status=status.HTTP_403_FORBIDDEN, status=status.HTTP_403_FORBIDDEN,
) )
cycle_issues = list( cycle_issues = list(CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list("issue", flat=True))
CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list(
"issue", flat=True
)
)
issue_activity.delay( issue_activity.delay(
type="cycle.activity.deleted", type="cycle.activity.deleted",
@ -625,9 +603,7 @@ class CycleDetailAPIEndpoint(BaseAPIView):
# Delete the cycle # Delete the cycle
cycle.delete() cycle.delete()
# Delete the user favorite cycle # Delete the user favorite cycle
UserFavorite.objects.filter( UserFavorite.objects.filter(entity_type="cycle", entity_identifier=pk, project_id=project_id).delete()
entity_type="cycle", entity_identifier=pk, project_id=project_id
).delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
@ -765,15 +741,13 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
return self.paginate( return self.paginate(
request=request, request=request,
queryset=(self.get_queryset()), queryset=(self.get_queryset()),
on_results=lambda cycles: CycleSerializer( on_results=lambda cycles: CycleSerializer(cycles, many=True, fields=self.fields, expand=self.expand).data,
cycles, many=True, fields=self.fields, expand=self.expand
).data,
) )
@cycle_docs( @cycle_docs(
operation_id="archive_cycle", operation_id="archive_cycle",
summary="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={}, request={},
responses={ responses={
204: ARCHIVED_RESPONSE, 204: ARCHIVED_RESPONSE,
@ -786,9 +760,7 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
Move a completed cycle to archived status for historical tracking. Move a completed cycle to archived status for historical tracking.
Only cycles that have ended can be archived. Only cycles that have ended can be archived.
""" """
cycle = Cycle.objects.get( cycle = Cycle.objects.get(pk=cycle_id, project_id=project_id, workspace__slug=slug)
pk=cycle_id, project_id=project_id, workspace__slug=slug
)
if cycle.end_date >= timezone.now(): if cycle.end_date >= timezone.now():
return Response( return Response(
{"error": "Only completed cycles can be archived"}, {"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. Restore an archived cycle to active status, making it available for regular use.
The cycle will reappear in active cycle lists. The cycle will reappear in active cycle lists.
""" """
cycle = Cycle.objects.get( cycle = Cycle.objects.get(pk=cycle_id, project_id=project_id, workspace__slug=slug)
pk=cycle_id, project_id=project_id, workspace__slug=slug
)
cycle.archived_at = None cycle.archived_at = None
cycle.save() cycle.save()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
@ -884,9 +854,7 @@ class CycleIssueListCreateAPIEndpoint(BaseAPIView):
# List # List
order_by = request.GET.get("order_by", "created_at") order_by = request.GET.get("order_by", "created_at")
issues = ( issues = (
Issue.issue_objects.filter( Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id, issue_cycle__deleted_at__isnull=True)
issue_cycle__cycle_id=cycle_id, issue_cycle__deleted_at__isnull=True
)
.annotate( .annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by() .order_by()
@ -923,15 +891,13 @@ class CycleIssueListCreateAPIEndpoint(BaseAPIView):
return self.paginate( return self.paginate(
request=request, request=request,
queryset=(issues), queryset=(issues),
on_results=lambda issues: IssueSerializer( on_results=lambda issues: IssueSerializer(issues, many=True, fields=self.fields, expand=self.expand).data,
issues, many=True, fields=self.fields, expand=self.expand
).data,
) )
@cycle_docs( @cycle_docs(
operation_id="add_cycle_work_items", operation_id="add_cycle_work_items",
summary="Add Work Items to Cycle", 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=OpenApiRequest(
request=CycleIssueRequestSerializer, request=CycleIssueRequestSerializer,
examples=[CYCLE_ISSUE_REQUEST_EXAMPLE], examples=[CYCLE_ISSUE_REQUEST_EXAMPLE],
@ -955,22 +921,24 @@ class CycleIssueListCreateAPIEndpoint(BaseAPIView):
if not issues: if not issues:
return Response( 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( cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=cycle_id)
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 # Get all CycleWorkItems already created
cycle_issues = list( cycle_issues = list(CycleIssue.objects.filter(~Q(cycle_id=cycle_id), issue_id__in=issues))
CycleIssue.objects.filter(~Q(cycle_id=cycle_id), issue_id__in=issues)
)
existing_issues = [ existing_issues = [
str(cycle_issue.issue_id) str(cycle_issue.issue_id) for cycle_issue in cycle_issues if str(cycle_issue.issue_id) in issues
for cycle_issue in cycle_issues
if str(cycle_issue.issue_id) in issues
] ]
new_issues = list(set(issues) - set(existing_issues)) new_issues = list(set(issues) - set(existing_issues))
@ -1021,9 +989,7 @@ class CycleIssueListCreateAPIEndpoint(BaseAPIView):
current_instance=json.dumps( current_instance=json.dumps(
{ {
"updated_cycle_issues": update_cycle_issue_activity, "updated_cycle_issues": update_cycle_issue_activity,
"created_cycle_issues": serializers.serialize( "created_cycle_issues": serializers.serialize("json", created_records),
"json", created_records
),
} }
), ),
epoch=int(timezone.now().timestamp()), epoch=int(timezone.now().timestamp()),
@ -1099,9 +1065,7 @@ class CycleIssueDetailAPIEndpoint(BaseAPIView):
cycle_id=cycle_id, cycle_id=cycle_id,
issue_id=issue_id, issue_id=issue_id,
) )
serializer = CycleIssueSerializer( serializer = CycleIssueSerializer(cycle_issue, fields=self.fields, expand=self.expand)
cycle_issue, fields=self.fields, expand=self.expand
)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
@cycle_docs( @cycle_docs(
@ -1154,7 +1118,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
@cycle_docs( @cycle_docs(
operation_id="transfer_cycle_work_items", operation_id="transfer_cycle_work_items",
summary="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=OpenApiRequest(
request=TransferCycleIssueRequestSerializer, request=TransferCycleIssueRequestSerializer,
examples=[TRANSFER_CYCLE_ISSUE_EXAMPLE], examples=[TRANSFER_CYCLE_ISSUE_EXAMPLE],
@ -1207,14 +1171,10 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
new_cycle = Cycle.objects.filter( new_cycle = Cycle.objects.filter(workspace__slug=slug, project_id=project_id, pk=new_cycle_id).first()
workspace__slug=slug, project_id=project_id, pk=new_cycle_id
).first()
old_cycle = ( old_cycle = (
Cycle.objects.filter( Cycle.objects.filter(workspace__slug=slug, project_id=project_id, pk=cycle_id)
workspace__slug=slug, project_id=project_id, pk=cycle_id
)
.annotate( .annotate(
total_issues=Count( total_issues=Count(
"issue_cycle", "issue_cycle",
@ -1324,9 +1284,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
) )
) )
.values("display_name", "assignee_id", "avatar", "avatar_url") .values("display_name", "assignee_id", "avatar", "avatar_url")
.annotate( .annotate(total_estimates=Sum(Cast("estimate_point__value", FloatField())))
total_estimates=Sum(Cast("estimate_point__value", FloatField()))
)
.annotate( .annotate(
completed_estimates=Sum( completed_estimates=Sum(
Cast("estimate_point__value", FloatField()), Cast("estimate_point__value", FloatField()),
@ -1353,9 +1311,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
assignee_estimate_distribution = [ assignee_estimate_distribution = [
{ {
"display_name": item["display_name"], "display_name": item["display_name"],
"assignee_id": ( "assignee_id": (str(item["assignee_id"]) if item["assignee_id"] else None),
str(item["assignee_id"]) if item["assignee_id"] else None
),
"avatar": item.get("avatar", None), "avatar": item.get("avatar", None),
"avatar_url": item.get("avatar_url", None), "avatar_url": item.get("avatar_url", None),
"total_estimates": item["total_estimates"], "total_estimates": item["total_estimates"],
@ -1376,9 +1332,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
.annotate(color=F("labels__color")) .annotate(color=F("labels__color"))
.annotate(label_id=F("labels__id")) .annotate(label_id=F("labels__id"))
.values("label_name", "color", "label_id") .values("label_name", "color", "label_id")
.annotate( .annotate(total_estimates=Sum(Cast("estimate_point__value", FloatField())))
total_estimates=Sum(Cast("estimate_point__value", FloatField()))
)
.annotate( .annotate(
completed_estimates=Sum( completed_estimates=Sum(
Cast("estimate_point__value", FloatField()), Cast("estimate_point__value", FloatField()),
@ -1445,19 +1399,13 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
), ),
), ),
# If `avatar_asset` is None, fall back to using `avatar` field directly # If `avatar_asset` is None, fall back to using `avatar` field directly
When( When(assignees__avatar_asset__isnull=True, then="assignees__avatar"),
assignees__avatar_asset__isnull=True, then="assignees__avatar"
),
default=Value(None), default=Value(None),
output_field=models.CharField(), output_field=models.CharField(),
) )
) )
.values("display_name", "assignee_id", "avatar_url") .values("display_name", "assignee_id", "avatar_url")
.annotate( .annotate(total_issues=Count("id", filter=Q(archived_at__isnull=True, is_draft=False)))
total_issues=Count(
"id", filter=Q(archived_at__isnull=True, is_draft=False)
)
)
.annotate( .annotate(
completed_issues=Count( completed_issues=Count(
"id", "id",
@ -1484,9 +1432,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
assignee_distribution_data = [ assignee_distribution_data = [
{ {
"display_name": item["display_name"], "display_name": item["display_name"],
"assignee_id": ( "assignee_id": (str(item["assignee_id"]) if item["assignee_id"] else None),
str(item["assignee_id"]) if item["assignee_id"] else None
),
"avatar": item.get("avatar", None), "avatar": item.get("avatar", None),
"avatar_url": item.get("avatar_url", None), "avatar_url": item.get("avatar_url", None),
"total_issues": item["total_issues"], "total_issues": item["total_issues"],
@ -1508,11 +1454,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
.annotate(color=F("labels__color")) .annotate(color=F("labels__color"))
.annotate(label_id=F("labels__id")) .annotate(label_id=F("labels__id"))
.values("label_name", "color", "label_id") .values("label_name", "color", "label_id")
.annotate( .annotate(total_issues=Count("id", filter=Q(archived_at__isnull=True, is_draft=False)))
total_issues=Count(
"id", filter=Q(archived_at__isnull=True, is_draft=False)
)
)
.annotate( .annotate(
completed_issues=Count( completed_issues=Count(
"id", "id",
@ -1558,9 +1500,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
cycle_id=cycle_id, cycle_id=cycle_id,
) )
current_cycle = Cycle.objects.filter( current_cycle = Cycle.objects.filter(workspace__slug=slug, project_id=project_id, pk=cycle_id).first()
workspace__slug=slug, project_id=project_id, pk=cycle_id
).first()
current_cycle.progress_snapshot = { current_cycle.progress_snapshot = {
"total_issues": old_cycle.total_issues, "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(): if new_cycle.end_date is not None and new_cycle.end_date < timezone.now():
return Response( 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, status=status.HTTP_400_BAD_REQUEST,
) )
@ -1614,9 +1552,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
} }
) )
cycle_issues = CycleIssue.objects.bulk_update( cycle_issues = CycleIssue.objects.bulk_update(updated_cycles, ["cycle_id"], batch_size=100)
updated_cycles, ["cycle_id"], batch_size=100
)
# Capture Issue Activity # Capture Issue Activity
issue_activity.delay( issue_activity.delay(

View file

@ -62,9 +62,7 @@ class IntakeIssueListCreateAPIEndpoint(BaseAPIView):
project_id=self.kwargs.get("project_id"), project_id=self.kwargs.get("project_id"),
).first() ).first()
project = Project.objects.get( project = Project.objects.get(workspace__slug=self.kwargs.get("slug"), pk=self.kwargs.get("project_id"))
workspace__slug=self.kwargs.get("slug"), pk=self.kwargs.get("project_id")
)
if intake is None or not project.intake_view: if intake is None or not project.intake_view:
return IntakeIssue.objects.none() return IntakeIssue.objects.none()
@ -83,7 +81,7 @@ class IntakeIssueListCreateAPIEndpoint(BaseAPIView):
@intake_docs( @intake_docs(
operation_id="get_intake_work_items_list", operation_id="get_intake_work_items_list",
summary="List intake work items", 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=[ parameters=[
WORKSPACE_SLUG_PARAMETER, WORKSPACE_SLUG_PARAMETER,
PROJECT_ID_PARAMETER, PROJECT_ID_PARAMETER,
@ -119,7 +117,7 @@ class IntakeIssueListCreateAPIEndpoint(BaseAPIView):
@intake_docs( @intake_docs(
operation_id="create_intake_work_item", operation_id="create_intake_work_item",
summary="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=[ parameters=[
WORKSPACE_SLUG_PARAMETER, WORKSPACE_SLUG_PARAMETER,
PROJECT_ID_PARAMETER, PROJECT_ID_PARAMETER,
@ -144,22 +142,16 @@ class IntakeIssueListCreateAPIEndpoint(BaseAPIView):
Automatically creates the work item with default triage state and tracks activity. Automatically creates the work item with default triage state and tracks activity.
""" """
if not request.data.get("issue", {}).get("name", False): if not request.data.get("issue", {}).get("name", False):
return Response( return Response({"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST)
{"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
)
intake = Intake.objects.filter( intake = Intake.objects.filter(workspace__slug=slug, project_id=project_id).first()
workspace__slug=slug, project_id=project_id
).first()
project = Project.objects.get(workspace__slug=slug, pk=project_id) project = Project.objects.get(workspace__slug=slug, pk=project_id)
# Intake view # Intake view
if intake is None and not project.intake_view: if intake is None and not project.intake_view:
return Response( 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, status=status.HTTP_400_BAD_REQUEST,
) )
@ -171,17 +163,13 @@ class IntakeIssueListCreateAPIEndpoint(BaseAPIView):
"urgent", "urgent",
"none", "none",
]: ]:
return Response( return Response({"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST)
{"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
)
# create an issue # create an issue
issue = Issue.objects.create( issue = Issue.objects.create(
name=request.data.get("issue", {}).get("name"), name=request.data.get("issue", {}).get("name"),
description=request.data.get("issue", {}).get("description", {}), description=request.data.get("issue", {}).get("description", {}),
description_html=request.data.get("issue", {}).get( description_html=request.data.get("issue", {}).get("description_html", "<p></p>"),
"description_html", "<p></p>"
),
priority=request.data.get("issue", {}).get("priority", "none"), priority=request.data.get("issue", {}).get("priority", "none"),
project_id=project_id, project_id=project_id,
) )
@ -226,9 +214,7 @@ class IntakeIssueDetailAPIEndpoint(BaseAPIView):
project_id=self.kwargs.get("project_id"), project_id=self.kwargs.get("project_id"),
).first() ).first()
project = Project.objects.get( project = Project.objects.get(workspace__slug=self.kwargs.get("slug"), pk=self.kwargs.get("project_id"))
workspace__slug=self.kwargs.get("slug"), pk=self.kwargs.get("project_id")
)
if intake is None or not project.intake_view: if intake is None or not project.intake_view:
return IntakeIssue.objects.none() return IntakeIssue.objects.none()
@ -267,15 +253,13 @@ class IntakeIssueDetailAPIEndpoint(BaseAPIView):
Retrieve details of a specific intake work item. Retrieve details of a specific intake work item.
""" """
intake_issue_queryset = self.get_queryset().get(issue_id=issue_id) intake_issue_queryset = self.get_queryset().get(issue_id=issue_id)
intake_issue_data = IntakeIssueSerializer( intake_issue_data = IntakeIssueSerializer(intake_issue_queryset, fields=self.fields, expand=self.expand).data
intake_issue_queryset, fields=self.fields, expand=self.expand
).data
return Response(intake_issue_data, status=status.HTTP_200_OK) return Response(intake_issue_data, status=status.HTTP_200_OK)
@intake_docs( @intake_docs(
operation_id="update_intake_work_item", operation_id="update_intake_work_item",
summary="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=[ parameters=[
WORKSPACE_SLUG_PARAMETER, WORKSPACE_SLUG_PARAMETER,
PROJECT_ID_PARAMETER, PROJECT_ID_PARAMETER,
@ -300,18 +284,14 @@ class IntakeIssueDetailAPIEndpoint(BaseAPIView):
Modify an existing intake work item's properties or status for triage processing. Modify an existing intake work item's properties or status for triage processing.
Supports status changes like accept, reject, or mark as duplicate. Supports status changes like accept, reject, or mark as duplicate.
""" """
intake = Intake.objects.filter( intake = Intake.objects.filter(workspace__slug=slug, project_id=project_id).first()
workspace__slug=slug, project_id=project_id
).first()
project = Project.objects.get(workspace__slug=slug, pk=project_id) project = Project.objects.get(workspace__slug=slug, pk=project_id)
# Intake view # Intake view
if intake is None and not project.intake_view: if intake is None and not project.intake_view:
return Response( 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, 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 # Only project members admins and created_by users can access this endpoint
if project_member.role <= 5 and str(intake_issue.created_by_id) != str( if project_member.role <= 5 and str(intake_issue.created_by_id) != str(request.user.id):
request.user.id
):
return Response( return Response(
{"error": "You cannot edit intake work items"}, {"error": "You cannot edit intake work items"},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
@ -349,10 +327,7 @@ class IntakeIssueDetailAPIEndpoint(BaseAPIView):
ArrayAgg( ArrayAgg(
"labels__id", "labels__id",
distinct=True, distinct=True,
filter=Q( filter=Q(~Q(labels__id__isnull=True) & Q(label_issue__deleted_at__isnull=True)),
~Q(labels__id__isnull=True)
& Q(label_issue__deleted_at__isnull=True)
),
), ),
Value([], output_field=ArrayField(UUIDField())), Value([], output_field=ArrayField(UUIDField())),
), ),
@ -373,9 +348,7 @@ class IntakeIssueDetailAPIEndpoint(BaseAPIView):
if project_member.role <= 5: if project_member.role <= 5:
issue_data = { issue_data = {
"name": issue_data.get("name", issue.name), "name": issue_data.get("name", issue.name),
"description_html": issue_data.get( "description_html": issue_data.get("description_html", issue.description_html),
"description_html", issue.description_html
),
"description": issue_data.get("description", issue.description), "description": issue_data.get("description", issue.description),
} }
@ -401,45 +374,31 @@ class IntakeIssueDetailAPIEndpoint(BaseAPIView):
) )
issue_serializer.save() issue_serializer.save()
else: else:
return Response( return Response(issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
# Only project admins and members can edit intake issue attributes # Only project admins and members can edit intake issue attributes
if project_member.role > 15: if project_member.role > 15:
serializer = IntakeIssueUpdateSerializer( serializer = IntakeIssueUpdateSerializer(intake_issue, data=request.data, partial=True)
intake_issue, data=request.data, partial=True current_instance = json.dumps(IntakeIssueSerializer(intake_issue).data, cls=DjangoJSONEncoder)
)
current_instance = json.dumps(
IntakeIssueSerializer(intake_issue).data, cls=DjangoJSONEncoder
)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
# Update the issue state if the issue is rejected or marked as duplicate # Update the issue state if the issue is rejected or marked as duplicate
if serializer.data["status"] in [-1, 2]: if serializer.data["status"] in [-1, 2]:
issue = Issue.objects.get( issue = Issue.objects.get(pk=issue_id, workspace__slug=slug, project_id=project_id)
pk=issue_id, workspace__slug=slug, project_id=project_id state = State.objects.filter(group="cancelled", workspace__slug=slug, project_id=project_id).first()
)
state = State.objects.filter(
group="cancelled", workspace__slug=slug, project_id=project_id
).first()
if state is not None: if state is not None:
issue.state = state issue.state = state
issue.save() issue.save()
# Update the issue state if it is accepted # Update the issue state if it is accepted
if serializer.data["status"] in [1]: if serializer.data["status"] in [1]:
issue = Issue.objects.get( issue = Issue.objects.get(pk=issue_id, workspace__slug=slug, project_id=project_id)
pk=issue_id, workspace__slug=slug, project_id=project_id
)
# Update the issue state only if it is in triage state # Update the issue state only if it is in triage state
if issue.state.is_triage: if issue.state.is_triage:
# Move to default state # Move to default state
state = State.objects.filter( state = State.objects.filter(workspace__slug=slug, project_id=project_id, default=True).first()
workspace__slug=slug, project_id=project_id, default=True
).first()
if state is not None: if state is not None:
issue.state = state issue.state = state
issue.save() issue.save()
@ -461,14 +420,12 @@ class IntakeIssueDetailAPIEndpoint(BaseAPIView):
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
else: else:
return Response( return Response(IntakeIssueSerializer(intake_issue).data, status=status.HTTP_200_OK)
IntakeIssueSerializer(intake_issue).data, status=status.HTTP_200_OK
)
@intake_docs( @intake_docs(
operation_id="delete_intake_work_item", operation_id="delete_intake_work_item",
summary="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=[ parameters=[
WORKSPACE_SLUG_PARAMETER, WORKSPACE_SLUG_PARAMETER,
PROJECT_ID_PARAMETER, PROJECT_ID_PARAMETER,
@ -484,18 +441,14 @@ class IntakeIssueDetailAPIEndpoint(BaseAPIView):
Permanently remove an intake work item from the triage queue. Permanently remove an intake work item from the triage queue.
Also deletes the underlying work item if it hasn't been accepted yet. Also deletes the underlying work item if it hasn't been accepted yet.
""" """
intake = Intake.objects.filter( intake = Intake.objects.filter(workspace__slug=slug, project_id=project_id).first()
workspace__slug=slug, project_id=project_id
).first()
project = Project.objects.get(workspace__slug=slug, pk=project_id) project = Project.objects.get(workspace__slug=slug, pk=project_id)
# Intake view # Intake view
if intake is None and not project.intake_view: if intake is None and not project.intake_view:
return Response( 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, status=status.HTTP_400_BAD_REQUEST,
) )
@ -510,9 +463,7 @@ class IntakeIssueDetailAPIEndpoint(BaseAPIView):
# Check the issue status # Check the issue status
if intake_issue.status in [-2, -1, 0, 2]: if intake_issue.status in [-2, -1, 0, 2]:
# Delete the issue also # Delete the issue also
issue = Issue.objects.filter( issue = Issue.objects.filter(workspace__slug=slug, project_id=project_id, pk=issue_id).first()
workspace__slug=slug, project_id=project_id, pk=issue_id
).first()
if issue.created_by_id != request.user.id and ( if issue.created_by_id != request.user.id and (
not ProjectMember.objects.filter( not ProjectMember.objects.filter(
workspace__slug=slug, workspace__slug=slug,

View file

@ -142,9 +142,8 @@ from plane.utils.openapi import (
) )
from plane.bgtasks.work_item_link_task import crawl_work_item_link_title 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: if allow_creator and issue is not None and user_id == issue.created_by_id:
return True return True
@ -269,7 +268,7 @@ class IssueListCreateAPIEndpoint(BaseAPIView):
@work_item_docs( @work_item_docs(
operation_id="list_work_items", operation_id="list_work_items",
summary="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=[ parameters=[
CURSOR_PARAMETER, CURSOR_PARAMETER,
PER_PAGE_PARAMETER, PER_PAGE_PARAMETER,
@ -322,9 +321,7 @@ class IssueListCreateAPIEndpoint(BaseAPIView):
self.get_queryset() self.get_queryset()
.annotate( .annotate(
cycle_id=Subquery( cycle_id=Subquery(
CycleIssue.objects.filter( CycleIssue.objects.filter(issue=OuterRef("id"), deleted_at__isnull=True).values("cycle_id")[:1]
issue=OuterRef("id"), deleted_at__isnull=True
).values("cycle_id")[:1]
) )
) )
.annotate( .annotate(
@ -344,21 +341,14 @@ class IssueListCreateAPIEndpoint(BaseAPIView):
) )
) )
total_issue_queryset = Issue.issue_objects.filter( total_issue_queryset = Issue.issue_objects.filter(project_id=project_id, workspace__slug=slug)
project_id=project_id, workspace__slug=slug
)
# Priority Ordering # Priority Ordering
if order_by_param == "priority" or order_by_param == "-priority": if order_by_param == "priority" or order_by_param == "-priority":
priority_order = ( priority_order = priority_order if order_by_param == "priority" else priority_order[::-1]
priority_order if order_by_param == "priority" else priority_order[::-1]
)
issue_queryset = issue_queryset.annotate( issue_queryset = issue_queryset.annotate(
priority_order=Case( 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(), output_field=CharField(),
) )
).order_by("priority_order") ).order_by("priority_order")
@ -370,17 +360,10 @@ class IssueListCreateAPIEndpoint(BaseAPIView):
"-state__name", "-state__name",
"-state__group", "-state__group",
]: ]:
state_order = ( state_order = state_order if order_by_param in ["state__name", "state__group"] else state_order[::-1]
state_order
if order_by_param in ["state__name", "state__group"]
else state_order[::-1]
)
issue_queryset = issue_queryset.annotate( issue_queryset = issue_queryset.annotate(
state_order=Case( 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)), default=Value(len(state_order)),
output_field=CharField(), output_field=CharField(),
) )
@ -393,14 +376,8 @@ class IssueListCreateAPIEndpoint(BaseAPIView):
"-assignees__first_name", "-assignees__first_name",
]: ]:
issue_queryset = issue_queryset.annotate( issue_queryset = issue_queryset.annotate(
max_values=Max( max_values=Max(order_by_param[1::] if order_by_param.startswith("-") else order_by_param)
order_by_param[1::] ).order_by("-max_values" if order_by_param.startswith("-") else "max_values")
if order_by_param.startswith("-")
else order_by_param
)
).order_by(
"-max_values" if order_by_param.startswith("-") else "max_values"
)
else: else:
issue_queryset = issue_queryset.order_by(order_by_param) issue_queryset = issue_queryset.order_by(order_by_param)
@ -408,9 +385,7 @@ class IssueListCreateAPIEndpoint(BaseAPIView):
request=request, request=request,
queryset=(issue_queryset), queryset=(issue_queryset),
total_count_queryset=total_issue_queryset, total_count_queryset=total_issue_queryset,
on_results=lambda issues: IssueSerializer( on_results=lambda issues: IssueSerializer(issues, many=True, fields=self.fields, expand=self.expand).data,
issues, many=True, fields=self.fields, expand=self.expand
).data,
) )
@work_item_docs( @work_item_docs(
@ -476,9 +451,7 @@ class IssueListCreateAPIEndpoint(BaseAPIView):
serializer.save() serializer.save()
# Refetch the issue # Refetch the issue
issue = Issue.objects.filter( issue = Issue.objects.filter(workspace__slug=slug, project_id=project_id, pk=serializer.data["id"]).first()
workspace__slug=slug, project_id=project_id, pk=serializer.data["id"]
).first()
issue.created_at = request.data.get("created_at", timezone.now()) issue.created_at = request.data.get("created_at", timezone.now())
issue.created_by_id = request.data.get("created_by", request.user.id) issue.created_by_id = request.data.get("created_by", request.user.id)
issue.save(update_fields=["created_at", "created_by"]) issue.save(update_fields=["created_at", "created_by"])
@ -579,7 +552,7 @@ class IssueDetailAPIEndpoint(BaseAPIView):
@work_item_docs( @work_item_docs(
operation_id="put_work_item", operation_id="put_work_item",
summary="Update or create 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=OpenApiRequest(
request=IssueSerializer, request=IssueSerializer,
examples=[ISSUE_UPSERT_EXAMPLE], examples=[ISSUE_UPSERT_EXAMPLE],
@ -625,9 +598,7 @@ class IssueDetailAPIEndpoint(BaseAPIView):
# Get the current instance of the issue in order to track # Get the current instance of the issue in order to track
# changes and dispatch the issue activity # changes and dispatch the issue activity
current_instance = json.dumps( current_instance = json.dumps(IssueSerializer(issue).data, cls=DjangoJSONEncoder)
IssueSerializer(issue).data, cls=DjangoJSONEncoder
)
# Get the requested data, encode it as django object and pass it # Get the requested data, encode it as django object and pass it
# to serializer to validation # to serializer to validation
@ -690,16 +661,12 @@ class IssueDetailAPIEndpoint(BaseAPIView):
# the issue with the provided data, else return with the # the issue with the provided data, else return with the
# default states given. # default states given.
issue.created_at = request.data.get("created_at", timezone.now()) issue.created_at = request.data.get("created_at", timezone.now())
issue.created_by_id = request.data.get( issue.created_by_id = request.data.get("created_by", request.user.id)
"created_by", request.user.id
)
issue.save(update_fields=["created_at", "created_by"]) issue.save(update_fields=["created_at", "created_by"])
issue_activity.delay( issue_activity.delay(
type="issue.activity.created", type="issue.activity.created",
requested_data=json.dumps( requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder),
self.request.data, cls=DjangoJSONEncoder
),
actor_id=str(request.user.id), actor_id=str(request.user.id),
issue_id=str(serializer.data.get("id", None)), issue_id=str(serializer.data.get("id", None)),
project_id=str(project_id), project_id=str(project_id),
@ -717,7 +684,7 @@ class IssueDetailAPIEndpoint(BaseAPIView):
@work_item_docs( @work_item_docs(
operation_id="update_work_item", operation_id="update_work_item",
summary="Partially 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=[ parameters=[
PROJECT_ID_PARAMETER, PROJECT_ID_PARAMETER,
], ],
@ -744,9 +711,7 @@ class IssueDetailAPIEndpoint(BaseAPIView):
""" """
issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk) issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
project = Project.objects.get(pk=project_id) project = Project.objects.get(pk=project_id)
current_instance = json.dumps( current_instance = json.dumps(IssueSerializer(issue).data, cls=DjangoJSONEncoder)
IssueSerializer(issue).data, cls=DjangoJSONEncoder
)
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder) requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
serializer = IssueSerializer( serializer = IssueSerializer(
issue, issue,
@ -761,9 +726,7 @@ class IssueDetailAPIEndpoint(BaseAPIView):
and Issue.objects.filter( and Issue.objects.filter(
project_id=project_id, project_id=project_id,
workspace__slug=slug, workspace__slug=slug,
external_source=request.data.get( external_source=request.data.get("external_source", issue.external_source),
"external_source", issue.external_source
),
external_id=request.data.get("external_id"), external_id=request.data.get("external_id"),
).exists() ).exists()
): ):
@ -791,7 +754,7 @@ class IssueDetailAPIEndpoint(BaseAPIView):
@work_item_docs( @work_item_docs(
operation_id="delete_work_item", operation_id="delete_work_item",
summary="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=[ parameters=[
PROJECT_ID_PARAMETER, PROJECT_ID_PARAMETER,
], ],
@ -821,9 +784,7 @@ class IssueDetailAPIEndpoint(BaseAPIView):
{"error": "Only admin or creator can delete the work item"}, {"error": "Only admin or creator can delete the work item"},
status=status.HTTP_403_FORBIDDEN, status=status.HTTP_403_FORBIDDEN,
) )
current_instance = json.dumps( current_instance = json.dumps(IssueSerializer(issue).data, cls=DjangoJSONEncoder)
IssueSerializer(issue).data, cls=DjangoJSONEncoder
)
issue.delete() issue.delete()
issue_activity.delay( issue_activity.delay(
type="issue.activity.deleted", type="issue.activity.deleted",
@ -959,9 +920,7 @@ class LabelListCreateAPIEndpoint(BaseAPIView):
return self.paginate( return self.paginate(
request=request, request=request,
queryset=(self.get_queryset()), queryset=(self.get_queryset()),
on_results=lambda labels: LabelSerializer( on_results=lambda labels: LabelSerializer(labels, many=True, fields=self.fields, expand=self.expand).data,
labels, many=True, fields=self.fields, expand=self.expand
).data,
) )
@ -1033,9 +992,7 @@ class LabelDetailAPIEndpoint(LabelListCreateAPIEndpoint):
and Label.objects.filter( and Label.objects.filter(
project_id=project_id, project_id=project_id,
workspace__slug=slug, workspace__slug=slug,
external_source=request.data.get( external_source=request.data.get("external_source", label.external_source),
"external_source", label.external_source
),
external_id=request.data.get("external_id"), external_id=request.data.get("external_id"),
) )
.exclude(id=pk) .exclude(id=pk)
@ -1162,9 +1119,7 @@ class IssueLinkListCreateAPIEndpoint(BaseAPIView):
serializer = IssueLinkCreateSerializer(data=request.data) serializer = IssueLinkCreateSerializer(data=request.data)
if serializer.is_valid(): if serializer.is_valid():
serializer.save(project_id=project_id, issue_id=issue_id) serializer.save(project_id=project_id, issue_id=issue_id)
crawl_work_item_link_title.delay( crawl_work_item_link_title.delay(serializer.instance.id, serializer.instance.url)
serializer.instance.id, serializer.instance.url
)
link = IssueLink.objects.get(pk=serializer.instance.id) link = IssueLink.objects.get(pk=serializer.instance.id)
link.created_by_id = request.data.get("created_by", request.user.id) link.created_by_id = request.data.get("created_by", request.user.id)
link.save(update_fields=["created_by"]) link.save(update_fields=["created_by"])
@ -1233,9 +1188,7 @@ class IssueLinkDetailAPIEndpoint(BaseAPIView):
""" """
if pk is None: if pk is None:
issue_links = self.get_queryset() issue_links = self.get_queryset()
serializer = IssueLinkSerializer( serializer = IssueLinkSerializer(issue_links, fields=self.fields, expand=self.expand)
issue_links, fields=self.fields, expand=self.expand
)
return self.paginate( return self.paginate(
request=request, request=request,
queryset=(self.get_queryset()), queryset=(self.get_queryset()),
@ -1244,9 +1197,7 @@ class IssueLinkDetailAPIEndpoint(BaseAPIView):
).data, ).data,
) )
issue_link = self.get_queryset().get(pk=pk) issue_link = self.get_queryset().get(pk=pk)
serializer = IssueLinkSerializer( serializer = IssueLinkSerializer(issue_link, fields=self.fields, expand=self.expand)
issue_link, fields=self.fields, expand=self.expand
)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
@issue_link_docs( @issue_link_docs(
@ -1276,19 +1227,13 @@ class IssueLinkDetailAPIEndpoint(BaseAPIView):
Modify the URL, title, or metadata of an existing issue link. Modify the URL, title, or metadata of an existing issue link.
Tracks all changes in issue activity logs. Tracks all changes in issue activity logs.
""" """
issue_link = IssueLink.objects.get( issue_link = IssueLink.objects.get(workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk)
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
)
requested_data = json.dumps(request.data, cls=DjangoJSONEncoder) requested_data = json.dumps(request.data, cls=DjangoJSONEncoder)
current_instance = json.dumps( current_instance = json.dumps(IssueLinkSerializer(issue_link).data, cls=DjangoJSONEncoder)
IssueLinkSerializer(issue_link).data, cls=DjangoJSONEncoder
)
serializer = IssueLinkSerializer(issue_link, data=request.data, partial=True) serializer = IssueLinkSerializer(issue_link, data=request.data, partial=True)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
crawl_work_item_link_title.delay( crawl_work_item_link_title.delay(serializer.data.get("id"), serializer.data.get("url"))
serializer.data.get("id"), serializer.data.get("url")
)
issue_activity.delay( issue_activity.delay(
type="link.activity.updated", type="link.activity.updated",
requested_data=requested_data, requested_data=requested_data,
@ -1320,12 +1265,8 @@ class IssueLinkDetailAPIEndpoint(BaseAPIView):
Permanently remove an external link from a work item. Permanently remove an external link from a work item.
Records deletion activity for audit purposes. Records deletion activity for audit purposes.
""" """
issue_link = IssueLink.objects.get( issue_link = IssueLink.objects.get(workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk)
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk current_instance = json.dumps(IssueLinkSerializer(issue_link).data, cls=DjangoJSONEncoder)
)
current_instance = json.dumps(
IssueLinkSerializer(issue_link).data, cls=DjangoJSONEncoder
)
issue_activity.delay( issue_activity.delay(
type="link.activity.deleted", type="link.activity.deleted",
requested_data=json.dumps({"link_id": str(pk)}), requested_data=json.dumps({"link_id": str(pk)}),
@ -1461,15 +1402,12 @@ class IssueCommentListCreateAPIEndpoint(BaseAPIView):
serializer = IssueCommentCreateSerializer(data=request.data) serializer = IssueCommentCreateSerializer(data=request.data)
if serializer.is_valid(): if serializer.is_valid():
serializer.save( serializer.save(project_id=project_id, issue_id=issue_id, actor=request.user)
project_id=project_id, issue_id=issue_id, actor=request.user
)
issue_comment = IssueComment.objects.get(pk=serializer.instance.id) issue_comment = IssueComment.objects.get(pk=serializer.instance.id)
# Update the created_at and the created_by and save the comment # Update the created_at and the created_by and save the comment
issue_comment.created_at = request.data.get("created_at", timezone.now()) issue_comment.created_at = request.data.get("created_at", timezone.now())
issue_comment.created_by_id = request.data.get( issue_comment.created_by_id = request.data.get("created_by", request.user.id)
"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_comment.save(update_fields=["created_at", "created_by"])
issue_activity.delay( issue_activity.delay(
@ -1555,9 +1493,7 @@ class IssueCommentDetailAPIEndpoint(BaseAPIView):
Retrieve details of a specific comment. Retrieve details of a specific comment.
""" """
issue_comment = self.get_queryset().get(pk=pk) issue_comment = self.get_queryset().get(pk=pk)
serializer = IssueCommentSerializer( serializer = IssueCommentSerializer(issue_comment, fields=self.fields, expand=self.expand)
issue_comment, fields=self.fields, expand=self.expand
)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
@issue_comment_docs( @issue_comment_docs(
@ -1588,13 +1524,9 @@ class IssueCommentDetailAPIEndpoint(BaseAPIView):
Modify the content of an existing comment on a work item. Modify the content of an existing comment on a work item.
Validates external ID uniqueness if provided. Validates external ID uniqueness if provided.
""" """
issue_comment = IssueComment.objects.get( issue_comment = IssueComment.objects.get(workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk)
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
)
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder) requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
current_instance = json.dumps( current_instance = json.dumps(IssueCommentSerializer(issue_comment).data, cls=DjangoJSONEncoder)
IssueCommentSerializer(issue_comment).data, cls=DjangoJSONEncoder
)
# Validation check if the issue already exists # Validation check if the issue already exists
if ( if (
@ -1603,9 +1535,7 @@ class IssueCommentDetailAPIEndpoint(BaseAPIView):
and IssueComment.objects.filter( and IssueComment.objects.filter(
project_id=project_id, project_id=project_id,
workspace__slug=slug, workspace__slug=slug,
external_source=request.data.get( external_source=request.data.get("external_source", issue_comment.external_source),
"external_source", issue_comment.external_source
),
external_id=request.data.get("external_id"), external_id=request.data.get("external_id"),
).exists() ).exists()
): ):
@ -1617,9 +1547,7 @@ class IssueCommentDetailAPIEndpoint(BaseAPIView):
status=status.HTTP_409_CONFLICT, status=status.HTTP_409_CONFLICT,
) )
serializer = IssueCommentCreateSerializer( serializer = IssueCommentCreateSerializer(issue_comment, data=request.data, partial=True)
issue_comment, data=request.data, partial=True
)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
issue_activity.delay( issue_activity.delay(
@ -1665,12 +1593,8 @@ class IssueCommentDetailAPIEndpoint(BaseAPIView):
Permanently remove a comment from a work item. Permanently remove a comment from a work item.
Records deletion activity for audit purposes. Records deletion activity for audit purposes.
""" """
issue_comment = IssueComment.objects.get( issue_comment = IssueComment.objects.get(workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk)
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk current_instance = json.dumps(IssueCommentSerializer(issue_comment).data, cls=DjangoJSONEncoder)
)
current_instance = json.dumps(
IssueCommentSerializer(issue_comment).data, cls=DjangoJSONEncoder
)
issue_comment.delete() issue_comment.delete()
issue_activity.delay( issue_activity.delay(
type="comment.activity.deleted", type="comment.activity.deleted",
@ -1717,9 +1641,7 @@ class IssueActivityListAPIEndpoint(BaseAPIView):
Excludes comment, vote, reaction, and draft activities. Excludes comment, vote, reaction, and draft activities.
""" """
issue_activities = ( issue_activities = (
IssueActivity.objects.filter( IssueActivity.objects.filter(issue_id=issue_id, workspace__slug=slug, project_id=project_id)
issue_id=issue_id, workspace__slug=slug, project_id=project_id
)
.filter( .filter(
~Q(field__in=["comment", "vote", "reaction", "draft"]), ~Q(field__in=["comment", "vote", "reaction", "draft"]),
project__project_projectmember__member=self.request.user, project__project_projectmember__member=self.request.user,
@ -1774,9 +1696,7 @@ class IssueActivityDetailAPIEndpoint(BaseAPIView):
Excludes comment, vote, reaction, and draft activities. Excludes comment, vote, reaction, and draft activities.
""" """
issue_activities = ( issue_activities = (
IssueActivity.objects.filter( IssueActivity.objects.filter(issue_id=issue_id, workspace__slug=slug, project_id=project_id)
issue_id=issue_id, workspace__slug=slug, project_id=project_id
)
.filter( .filter(
~Q(field__in=["comment", "vote", "reaction", "draft"]), ~Q(field__in=["comment", "vote", "reaction", "draft"]),
project__project_projectmember__member=self.request.user, project__project_projectmember__member=self.request.user,
@ -1866,12 +1786,8 @@ class IssueAttachmentListCreateAPIEndpoint(BaseAPIView):
name="Workspace not found", name="Workspace not found",
value={"error": "Workspace not found"}, value={"error": "Workspace not found"},
), ),
OpenApiExample( OpenApiExample(name="Project not found", value={"error": "Project not found"}),
name="Project not found", value={"error": "Project not found"} OpenApiExample(name="Issue not found", value={"error": "Issue 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. Generate presigned URL for uploading file attachments to a work item.
Validates file type and size before creating the attachment record. Validates file type and size before creating the attachment record.
""" """
issue = Issue.objects.get( issue = Issue.objects.get(pk=issue_id, workspace__slug=slug, project_id=project_id)
pk=issue_id, workspace__slug=slug, project_id=project_id
)
# if the user is creator or admin,member then allow the upload # if the user is creator or admin,member then allow the upload
if not user_has_issue_permission( if not user_has_issue_permission(
request.user.id, request.user.id,
@ -1970,9 +1884,7 @@ class IssueAttachmentListCreateAPIEndpoint(BaseAPIView):
# Get the presigned URL # Get the presigned URL
storage = S3Storage(request=request) storage = S3Storage(request=request)
# Generate a presigned URL to share an S3 object # Generate a presigned URL to share an S3 object
presigned_url = storage.generate_presigned_post( presigned_url = storage.generate_presigned_post(object_name=asset_key, file_type=type, file_size=size_limit)
object_name=asset_key, file_type=type, file_size=size_limit
)
# Return the presigned URL # Return the presigned URL
return Response( return Response(
{ {
@ -2032,9 +1944,7 @@ class IssueAttachmentDetailAPIEndpoint(BaseAPIView):
ATTACHMENT_ID_PARAMETER, ATTACHMENT_ID_PARAMETER,
], ],
responses={ responses={
204: OpenApiResponse( 204: OpenApiResponse(description="Work item attachment deleted successfully"),
description="Work item attachment deleted successfully"
),
404: ATTACHMENT_NOT_FOUND_RESPONSE, 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. Soft delete an attachment from a work item by marking it as deleted.
Records deletion activity and triggers metadata cleanup. Records deletion activity and triggers metadata cleanup.
""" """
issue = Issue.objects.get( issue = Issue.objects.get(pk=issue_id, workspace__slug=slug, project_id=project_id)
pk=issue_id, workspace__slug=slug, project_id=project_id
)
# if the request user is creator or admin then delete the attachment # if the request user is creator or admin then delete the attachment
if not user_has_issue_permission( if not user_has_issue_permission(
request.user, request.user,
@ -2060,9 +1968,7 @@ class IssueAttachmentDetailAPIEndpoint(BaseAPIView):
status=status.HTTP_403_FORBIDDEN, status=status.HTTP_403_FORBIDDEN,
) )
issue_attachment = FileAsset.objects.get( issue_attachment = FileAsset.objects.get(pk=pk, workspace__slug=slug, project_id=project_id)
pk=pk, workspace__slug=slug, project_id=project_id
)
issue_attachment.is_deleted = True issue_attachment.is_deleted = True
issue_attachment.deleted_at = timezone.now() issue_attachment.deleted_at = timezone.now()
issue_attachment.save() issue_attachment.save()
@ -2136,9 +2042,7 @@ class IssueAttachmentDetailAPIEndpoint(BaseAPIView):
) )
# Get the asset # Get the asset
asset = FileAsset.objects.get( asset = FileAsset.objects.get(id=pk, workspace__slug=slug, project_id=project_id)
id=pk, workspace__slug=slug, project_id=project_id
)
# Check if the asset is uploaded # Check if the asset is uploaded
if not asset.is_uploaded: if not asset.is_uploaded:
@ -2176,9 +2080,7 @@ class IssueAttachmentDetailAPIEndpoint(BaseAPIView):
examples=[ATTACHMENT_UPLOAD_CONFIRM_EXAMPLE], examples=[ATTACHMENT_UPLOAD_CONFIRM_EXAMPLE],
), ),
responses={ responses={
204: OpenApiResponse( 204: OpenApiResponse(description="Work item attachment uploaded successfully"),
description="Work item attachment uploaded successfully"
),
400: INVALID_REQUEST_RESPONSE, 400: INVALID_REQUEST_RESPONSE,
404: ATTACHMENT_NOT_FOUND_RESPONSE, 404: ATTACHMENT_NOT_FOUND_RESPONSE,
}, },
@ -2190,9 +2092,7 @@ class IssueAttachmentDetailAPIEndpoint(BaseAPIView):
Triggers activity logging and metadata extraction. Triggers activity logging and metadata extraction.
""" """
issue = Issue.objects.get( issue = Issue.objects.get(pk=issue_id, workspace__slug=slug, project_id=project_id)
pk=issue_id, workspace__slug=slug, project_id=project_id
)
# if the user is creator or admin then allow the upload # if the user is creator or admin then allow the upload
if not user_has_issue_permission( if not user_has_issue_permission(
request.user, request.user,
@ -2206,9 +2106,7 @@ class IssueAttachmentDetailAPIEndpoint(BaseAPIView):
status=status.HTTP_403_FORBIDDEN, status=status.HTTP_403_FORBIDDEN,
) )
issue_attachment = FileAsset.objects.get( issue_attachment = FileAsset.objects.get(pk=pk, workspace__slug=slug, project_id=project_id)
pk=pk, workspace__slug=slug, project_id=project_id
)
serializer = IssueAttachmentSerializer(issue_attachment) serializer = IssueAttachmentSerializer(issue_attachment)
# Send this activity only if the attachment is not uploaded before # Send this activity only if the attachment is not uploaded before

View file

@ -74,9 +74,7 @@ class WorkspaceMemberAPIEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
workspace_members = WorkspaceMember.objects.filter( workspace_members = WorkspaceMember.objects.filter(workspace__slug=slug).select_related("member")
workspace__slug=slug
).select_related("member")
# Get all the users with their roles # Get all the users with their roles
users_with_roles = [] users_with_roles = []
@ -125,13 +123,11 @@ class ProjectMemberAPIEndpoint(BaseAPIView):
) )
# Get the workspace members that are present inside the workspace # Get the workspace members that are present inside the workspace
project_members = ProjectMember.objects.filter( project_members = ProjectMember.objects.filter(project_id=project_id, workspace__slug=slug).values_list(
project_id=project_id, workspace__slug=slug "member_id", flat=True
).values_list("member_id", flat=True) )
# Get all the users that are present inside the workspace # Get all the users that are present inside the workspace
users = UserLiteSerializer( users = UserLiteSerializer(User.objects.filter(id__in=project_members), many=True).data
User.objects.filter(id__in=project_members), many=True
).data
return Response(users, status=status.HTTP_200_OK) return Response(users, status=status.HTTP_200_OK)

View file

@ -394,9 +394,7 @@ class ModuleDetailAPIEndpoint(BaseAPIView):
examples=[MODULE_UPDATE_EXAMPLE], examples=[MODULE_UPDATE_EXAMPLE],
), ),
404: OpenApiResponse(description="Module not found"), 404: OpenApiResponse(description="Module not found"),
409: OpenApiResponse( 409: OpenApiResponse(description="Module with same external ID already exists"),
description="Module with same external ID already exists"
),
}, },
) )
def patch(self, request, slug, project_id, pk): 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) module = Module.objects.get(pk=pk, project_id=project_id, workspace__slug=slug)
current_instance = json.dumps( current_instance = json.dumps(ModuleSerializer(module).data, cls=DjangoJSONEncoder)
ModuleSerializer(module).data, cls=DjangoJSONEncoder
)
if module.archived_at: if module.archived_at:
return Response( return Response(
{"error": "Archived module cannot be edited"}, {"error": "Archived module cannot be edited"},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
serializer = ModuleSerializer( serializer = ModuleSerializer(module, data=request.data, context={"project_id": project_id}, partial=True)
module, data=request.data, context={"project_id": project_id}, partial=True
)
if serializer.is_valid(): if serializer.is_valid():
if ( if (
request.data.get("external_id") request.data.get("external_id")
@ -426,9 +420,7 @@ class ModuleDetailAPIEndpoint(BaseAPIView):
and Module.objects.filter( and Module.objects.filter(
project_id=project_id, project_id=project_id,
workspace__slug=slug, workspace__slug=slug,
external_source=request.data.get( external_source=request.data.get("external_source", module.external_source),
"external_source", module.external_source
),
external_id=request.data.get("external_id"), external_id=request.data.get("external_id"),
).exists() ).exists()
): ):
@ -514,9 +506,7 @@ class ModuleDetailAPIEndpoint(BaseAPIView):
status=status.HTTP_403_FORBIDDEN, status=status.HTTP_403_FORBIDDEN,
) )
module_issues = list( module_issues = list(ModuleIssue.objects.filter(module_id=pk).values_list("issue", flat=True))
ModuleIssue.objects.filter(module_id=pk).values_list("issue", flat=True)
)
issue_activity.delay( issue_activity.delay(
type="module.activity.deleted", type="module.activity.deleted",
requested_data=json.dumps( requested_data=json.dumps(
@ -537,9 +527,7 @@ class ModuleDetailAPIEndpoint(BaseAPIView):
# Delete the module issues # Delete the module issues
ModuleIssue.objects.filter(module=pk, project_id=project_id).delete() ModuleIssue.objects.filter(module=pk, project_id=project_id).delete()
# Delete the user favorite module # Delete the user favorite module
UserFavorite.objects.filter( UserFavorite.objects.filter(entity_type="module", entity_identifier=pk, project_id=project_id).delete()
entity_type="module", entity_identifier=pk, project_id=project_id
).delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
@ -609,9 +597,7 @@ class ModuleIssueListCreateAPIEndpoint(BaseAPIView):
""" """
order_by = request.GET.get("order_by", "created_at") order_by = request.GET.get("order_by", "created_at")
issues = ( issues = (
Issue.issue_objects.filter( Issue.issue_objects.filter(issue_module__module_id=module_id, issue_module__deleted_at__isnull=True)
issue_module__module_id=module_id, issue_module__deleted_at__isnull=True
)
.annotate( .annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by() .order_by()
@ -647,15 +633,13 @@ class ModuleIssueListCreateAPIEndpoint(BaseAPIView):
return self.paginate( return self.paginate(
request=request, request=request,
queryset=(issues), queryset=(issues),
on_results=lambda issues: IssueSerializer( on_results=lambda issues: IssueSerializer(issues, many=True, fields=self.fields, expand=self.expand).data,
issues, many=True, fields=self.fields, expand=self.expand
).data,
) )
@module_issue_docs( @module_issue_docs(
operation_id="add_module_work_items", operation_id="add_module_work_items",
summary="Add Work Items to Module", 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=[ parameters=[
MODULE_ID_PARAMETER, MODULE_ID_PARAMETER,
], ],
@ -681,16 +665,12 @@ class ModuleIssueListCreateAPIEndpoint(BaseAPIView):
""" """
issues = request.data.get("issues", []) issues = request.data.get("issues", [])
if not len(issues): if not len(issues):
return Response( return Response({"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST)
{"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST module = Module.objects.get(workspace__slug=slug, project_id=project_id, pk=module_id)
)
module = Module.objects.get(
workspace__slug=slug, project_id=project_id, pk=module_id
)
issues = Issue.objects.filter( issues = Issue.objects.filter(workspace__slug=slug, project_id=project_id, pk__in=issues).values_list(
workspace__slug=slug, project_id=project_id, pk__in=issues "id", flat=True
).values_list("id", flat=True) )
module_issues = list(ModuleIssue.objects.filter(issue_id__in=issues)) module_issues = list(ModuleIssue.objects.filter(issue_id__in=issues))
@ -699,11 +679,7 @@ class ModuleIssueListCreateAPIEndpoint(BaseAPIView):
record_to_create = [] record_to_create = []
for issue in issues: for issue in issues:
module_issue = [ module_issue = [module_issue for module_issue in module_issues if str(module_issue.issue_id) in issues]
module_issue
for module_issue in module_issues
if str(module_issue.issue_id) in issues
]
if len(module_issue): if len(module_issue):
if module_issue[0].module_id != module_id: if module_issue[0].module_id != module_id:
@ -728,9 +704,7 @@ class ModuleIssueListCreateAPIEndpoint(BaseAPIView):
) )
) )
ModuleIssue.objects.bulk_create( ModuleIssue.objects.bulk_create(record_to_create, batch_size=10, ignore_conflicts=True)
record_to_create, batch_size=10, ignore_conflicts=True
)
ModuleIssue.objects.bulk_update(records_to_update, ["module"], batch_size=10) ModuleIssue.objects.bulk_update(records_to_update, ["module"], batch_size=10)
@ -744,9 +718,7 @@ class ModuleIssueListCreateAPIEndpoint(BaseAPIView):
current_instance=json.dumps( current_instance=json.dumps(
{ {
"updated_module_issues": update_module_issue_activity, "updated_module_issues": update_module_issue_activity,
"created_module_issues": serializers.serialize( "created_module_issues": serializers.serialize("json", record_to_create),
"json", record_to_create
),
} }
), ),
epoch=int(timezone.now().timestamp()), epoch=int(timezone.now().timestamp()),
@ -871,9 +843,7 @@ class ModuleIssueDetailAPIEndpoint(BaseAPIView):
return self.paginate( return self.paginate(
request=request, request=request,
queryset=(issues), queryset=(issues),
on_results=lambda issues: IssueSerializer( on_results=lambda issues: IssueSerializer(issues, many=True, fields=self.fields, expand=self.expand).data,
issues, many=True, fields=self.fields, expand=self.expand
).data,
) )
@module_issue_docs( @module_issue_docs(
@ -904,9 +874,7 @@ class ModuleIssueDetailAPIEndpoint(BaseAPIView):
module_issue.delete() module_issue.delete()
issue_activity.delay( issue_activity.delay(
type="module.activity.deleted", type="module.activity.deleted",
requested_data=json.dumps( requested_data=json.dumps({"module_id": str(module_id), "issues": [str(module_issue.issue_id)]}),
{"module_id": str(module_id), "issues": [str(module_issue.issue_id)]}
),
actor_id=str(request.user.id), actor_id=str(request.user.id),
issue_id=str(issue_id), issue_id=str(issue_id),
project_id=str(project_id), project_id=str(project_id),

View file

@ -79,9 +79,7 @@ class ProjectListCreateAPIEndpoint(BaseAPIView):
) )
| Q(network=2) | Q(network=2)
) )
.select_related( .select_related("workspace", "workspace__owner", "default_assignee", "project_lead")
"workspace", "workspace__owner", "default_assignee", "project_lead"
)
.annotate( .annotate(
is_member=Exists( is_member=Exists(
ProjectMember.objects.filter( ProjectMember.objects.filter(
@ -170,9 +168,9 @@ class ProjectListCreateAPIEndpoint(BaseAPIView):
.prefetch_related( .prefetch_related(
Prefetch( Prefetch(
"project_projectmember", "project_projectmember",
queryset=ProjectMember.objects.filter( queryset=ProjectMember.objects.filter(workspace__slug=slug, is_active=True).select_related(
workspace__slug=slug, is_active=True "member"
).select_related("member"), ),
) )
) )
.order_by(request.GET.get("order_by", "sort_order")) .order_by(request.GET.get("order_by", "sort_order"))
@ -211,24 +209,18 @@ class ProjectListCreateAPIEndpoint(BaseAPIView):
""" """
try: try:
workspace = Workspace.objects.get(slug=slug) workspace = Workspace.objects.get(slug=slug)
serializer = ProjectCreateSerializer( serializer = ProjectCreateSerializer(data={**request.data}, context={"workspace_id": workspace.id})
data={**request.data}, context={"workspace_id": workspace.id}
)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
# Add the user as Administrator to the project # Add the user as Administrator to the project
_ = ProjectMember.objects.create( _ = ProjectMember.objects.create(project_id=serializer.instance.id, member=request.user, role=20)
project_id=serializer.instance.id, member=request.user, role=20
)
# Also create the issue property for the user # Also create the issue property for the user
_ = IssueUserProperty.objects.create( _ = IssueUserProperty.objects.create(project_id=serializer.instance.id, user=request.user)
project_id=serializer.instance.id, user=request.user
)
if serializer.instance.project_lead is not None and str( if serializer.instance.project_lead is not None and str(serializer.instance.project_lead) != str(
serializer.instance.project_lead request.user.id
) != str(request.user.id): ):
ProjectMember.objects.create( ProjectMember.objects.create(
project_id=serializer.instance.id, project_id=serializer.instance.id,
member_id=serializer.instance.project_lead, member_id=serializer.instance.project_lead,
@ -314,9 +306,7 @@ class ProjectListCreateAPIEndpoint(BaseAPIView):
status=status.HTTP_409_CONFLICT, status=status.HTTP_409_CONFLICT,
) )
except Workspace.DoesNotExist: except Workspace.DoesNotExist:
return Response( return Response({"error": "Workspace does not exist"}, status=status.HTTP_404_NOT_FOUND)
{"error": "Workspace does not exist"}, status=status.HTTP_404_NOT_FOUND
)
except ValidationError: except ValidationError:
return Response( return Response(
{"identifier": "The project identifier is already taken"}, {"identifier": "The project identifier is already taken"},
@ -344,9 +334,7 @@ class ProjectDetailAPIEndpoint(BaseAPIView):
) )
| Q(network=2) | Q(network=2)
) )
.select_related( .select_related("workspace", "workspace__owner", "default_assignee", "project_lead")
"workspace", "workspace__owner", "default_assignee", "project_lead"
)
.annotate( .annotate(
is_member=Exists( is_member=Exists(
ProjectMember.objects.filter( ProjectMember.objects.filter(
@ -451,9 +439,7 @@ class ProjectDetailAPIEndpoint(BaseAPIView):
try: try:
workspace = Workspace.objects.get(slug=slug) workspace = Workspace.objects.get(slug=slug)
project = Project.objects.get(pk=pk) project = Project.objects.get(pk=pk)
current_instance = json.dumps( current_instance = json.dumps(ProjectSerializer(project).data, cls=DjangoJSONEncoder)
ProjectSerializer(project).data, cls=DjangoJSONEncoder
)
intake_view = request.data.get("intake_view", project.intake_view) intake_view = request.data.get("intake_view", project.intake_view)
@ -473,9 +459,7 @@ class ProjectDetailAPIEndpoint(BaseAPIView):
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
if serializer.data["intake_view"]: if serializer.data["intake_view"]:
intake = Intake.objects.filter( intake = Intake.objects.filter(project=project, is_default=True).first()
project=project, is_default=True
).first()
if not intake: if not intake:
Intake.objects.create( Intake.objects.create(
name=f"{project.name} Intake", name=f"{project.name} Intake",
@ -505,9 +489,7 @@ class ProjectDetailAPIEndpoint(BaseAPIView):
status=status.HTTP_409_CONFLICT, status=status.HTTP_409_CONFLICT,
) )
except (Project.DoesNotExist, Workspace.DoesNotExist): except (Project.DoesNotExist, Workspace.DoesNotExist):
return Response( return Response({"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND)
{"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND
)
except ValidationError: except ValidationError:
return Response( return Response(
{"identifier": "The project identifier is already taken"}, {"identifier": "The project identifier is already taken"},
@ -533,9 +515,7 @@ class ProjectDetailAPIEndpoint(BaseAPIView):
""" """
project = Project.objects.get(pk=pk, workspace__slug=slug) project = Project.objects.get(pk=pk, workspace__slug=slug)
# Delete the user favorite cycle # Delete the user favorite cycle
UserFavorite.objects.filter( UserFavorite.objects.filter(entity_type="project", entity_identifier=pk, project_id=pk).delete()
entity_type="project", entity_identifier=pk, project_id=pk
).delete()
project.delete() project.delete()
webhook_activity.delay( webhook_activity.delay(
event="project", event="project",

View file

@ -80,9 +80,7 @@ class StateListCreateAPIEndpoint(BaseAPIView):
Supports external ID tracking for integration purposes. Supports external ID tracking for integration purposes.
""" """
try: try:
serializer = StateSerializer( serializer = StateSerializer(data=request.data, context={"project_id": project_id})
data=request.data, context={"project_id": project_id}
)
if serializer.is_valid(): if serializer.is_valid():
if ( if (
request.data.get("external_id") request.data.get("external_id")
@ -153,9 +151,7 @@ class StateListCreateAPIEndpoint(BaseAPIView):
return self.paginate( return self.paginate(
request=request, request=request,
queryset=(self.get_queryset()), queryset=(self.get_queryset()),
on_results=lambda states: StateSerializer( on_results=lambda states: StateSerializer(states, many=True, fields=self.fields, expand=self.expand).data,
states, many=True, fields=self.fields, expand=self.expand
).data,
) )
@ -213,7 +209,7 @@ class StateDetailAPIEndpoint(BaseAPIView):
@state_docs( @state_docs(
operation_id="delete_state", operation_id="delete_state",
summary="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=[ parameters=[
STATE_ID_PARAMETER, STATE_ID_PARAMETER,
], ],
@ -228,9 +224,7 @@ class StateDetailAPIEndpoint(BaseAPIView):
Permanently remove a workflow state from a project. Permanently remove a workflow state from a project.
Default states and states with existing work items cannot be deleted. Default states and states with existing work items cannot be deleted.
""" """
state = State.objects.get( state = State.objects.get(is_triage=False, pk=state_id, project_id=project_id, workspace__slug=slug)
is_triage=False, pk=state_id, project_id=project_id, workspace__slug=slug
)
if state.default: if state.default:
return Response( return Response(
@ -277,9 +271,7 @@ class StateDetailAPIEndpoint(BaseAPIView):
Partially update an existing workflow state's properties like name, color, or group. Partially update an existing workflow state's properties like name, color, or group.
Validates external ID uniqueness if provided. Validates external ID uniqueness if provided.
""" """
state = State.objects.get( state = State.objects.get(workspace__slug=slug, project_id=project_id, pk=state_id)
workspace__slug=slug, project_id=project_id, pk=state_id
)
serializer = StateSerializer(state, data=request.data, partial=True) serializer = StateSerializer(state, data=request.data, partial=True)
if serializer.is_valid(): if serializer.is_valid():
if ( if (
@ -288,9 +280,7 @@ class StateDetailAPIEndpoint(BaseAPIView):
and State.objects.filter( and State.objects.filter(
project_id=project_id, project_id=project_id,
workspace__slug=slug, workspace__slug=slug,
external_source=request.data.get( external_source=request.data.get("external_source", state.external_source),
"external_source", state.external_source
),
external_id=request.data.get("external_id"), external_id=request.data.get("external_id"),
).exists() ).exists()
): ):

View file

@ -18,16 +18,12 @@ def allow_permission(allowed_roles, level="PROJECT", creator=False, model=None):
def _wrapped_view(instance, request, *args, **kwargs): def _wrapped_view(instance, request, *args, **kwargs):
# Check for creator if required # Check for creator if required
if creator and model: if creator and model:
obj = model.objects.filter( obj = model.objects.filter(id=kwargs["pk"], created_by=request.user).exists()
id=kwargs["pk"], created_by=request.user
).exists()
if obj: if obj:
return view_func(instance, request, *args, **kwargs) return view_func(instance, request, *args, **kwargs)
# Convert allowed_roles to their values if they are enum members # Convert allowed_roles to their values if they are enum members
allowed_role_values = [ allowed_role_values = [role.value if isinstance(role, ROLE) else role for role in allowed_roles]
role.value if isinstance(role, ROLE) else role for role in allowed_roles
]
# Check role permissions # Check role permissions
if level == "WORKSPACE": if level == "WORKSPACE":
@ -47,7 +43,7 @@ def allow_permission(allowed_roles, level="PROJECT", creator=False, model=None):
is_active=True, is_active=True,
).exists() ).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: if is_user_has_allowed_role:
return view_func(instance, request, *args, **kwargs) return view_func(instance, request, *args, **kwargs)
elif ( elif (

View file

@ -30,9 +30,7 @@ class ProjectPagePermission(BasePermission):
project_id = view.kwargs.get("project_id") project_id = view.kwargs.get("project_id")
# Hook for extended validation # Hook for extended validation
extended_access, role = self._check_access_and_get_role( extended_access, role = self._check_access_and_get_role(request, slug, project_id)
request, slug, project_id
)
if extended_access is False: if extended_access is False:
return False return False
@ -45,9 +43,7 @@ class ProjectPagePermission(BasePermission):
# Handle private page access # Handle private page access
if page.access == Page.PRIVATE_ACCESS: if page.access == Page.PRIVATE_ACCESS:
return self._has_private_page_action_access( return self._has_private_page_action_access(request, slug, page, project_id)
request, slug, page, project_id
)
# Handle public page access # Handle public page access
return self._has_public_page_action_access(request, role) return self._has_public_page_action_access(request, role)

View file

@ -168,13 +168,9 @@ class DynamicBaseSerializer(BaseSerializer):
# Check if field in expansion then expand the field # Check if field in expansion then expand the field
if expand in expansion: if expand in expansion:
if isinstance(response.get(expand), list): if isinstance(response.get(expand), list):
exp_serializer = expansion[expand]( exp_serializer = expansion[expand](getattr(instance, expand), many=True)
getattr(instance, expand), many=True
)
else: else:
exp_serializer = expansion[expand]( exp_serializer = expansion[expand](getattr(instance, expand))
getattr(instance, expand)
)
response[expand] = exp_serializer.data response[expand] = exp_serializer.data
else: else:
# You might need to handle this case differently # You might need to handle this case differently
@ -194,9 +190,7 @@ class DynamicBaseSerializer(BaseSerializer):
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT, entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
) )
# Serialize issue_attachments and add them to the response # Serialize issue_attachments and add them to the response
response["issue_attachments"] = IssueAttachmentLiteSerializer( response["issue_attachments"] = IssueAttachmentLiteSerializer(issue_attachments, many=True).data
issue_attachments, many=True
).data
else: else:
response["issue_attachments"] = [] response["issue_attachments"] = []

View file

@ -16,10 +16,7 @@ class CycleWriteSerializer(BaseSerializer):
and data.get("start_date", None) > data.get("end_date", None) and data.get("start_date", None) > data.get("end_date", None)
): ):
raise serializers.ValidationError("Start date cannot exceed end date") raise serializers.ValidationError("Start date cannot exceed end date")
if ( if data.get("start_date", None) is not None and data.get("end_date", None) is not None:
data.get("start_date", None) is not None
and data.get("end_date", None) is not None
):
project_id = ( project_id = (
self.initial_data.get("project_id", None) self.initial_data.get("project_id", None)
or (self.instance and self.instance.project_id) or (self.instance and self.instance.project_id)

View file

@ -1,4 +1,3 @@
# Django imports # Django imports
from django.utils import timezone from django.utils import timezone
@ -75,13 +74,9 @@ class DraftIssueCreateSerializer(BaseSerializer):
# Validate description content for security # Validate description content for security
if "description_html" in attrs and attrs["description_html"]: if "description_html" in attrs and attrs["description_html"]:
is_valid, error_msg, sanitized_html = validate_html_content( is_valid, error_msg, sanitized_html = validate_html_content(attrs["description_html"])
attrs["description_html"]
)
if not is_valid: if not is_valid:
raise serializers.ValidationError( raise serializers.ValidationError({"error": "html content is not valid"})
{"error": "html content is not valid"}
)
# Update the attrs with sanitized HTML if available # Update the attrs with sanitized HTML if available
if sanitized_html is not None: if sanitized_html is not None:
attrs["description_html"] = sanitized_html attrs["description_html"] = sanitized_html
@ -89,9 +84,7 @@ class DraftIssueCreateSerializer(BaseSerializer):
if "description_binary" in attrs and attrs["description_binary"]: if "description_binary" in attrs and attrs["description_binary"]:
is_valid, error_msg = validate_binary_data(attrs["description_binary"]) is_valid, error_msg = validate_binary_data(attrs["description_binary"])
if not is_valid: if not is_valid:
raise serializers.ValidationError( raise serializers.ValidationError({"description_binary": "Invalid binary data"})
{"description_binary": "Invalid binary data"}
)
# Validate assignees are from project # Validate assignees are from project
if attrs.get("assignee_ids", []): if attrs.get("assignee_ids", []):
@ -106,9 +99,9 @@ class DraftIssueCreateSerializer(BaseSerializer):
if attrs.get("label_ids"): if attrs.get("label_ids"):
label_ids = [label.id for label in attrs["label_ids"]] label_ids = [label.id for label in attrs["label_ids"]]
attrs["label_ids"] = list( attrs["label_ids"] = list(
Label.objects.filter( Label.objects.filter(project_id=self.context.get("project_id"), id__in=label_ids).values_list(
project_id=self.context.get("project_id"), id__in=label_ids "id", flat=True
).values_list("id", flat=True) )
) )
# # Check state is from the project only else raise validation error # # Check state is from the project only else raise validation error
@ -119,9 +112,7 @@ class DraftIssueCreateSerializer(BaseSerializer):
pk=attrs.get("state").id, pk=attrs.get("state").id,
).exists() ).exists()
): ):
raise serializers.ValidationError( raise serializers.ValidationError("State is not valid please pass a valid state_id")
"State is not valid please pass a valid state_id"
)
# # Check parent issue is from workspace as it can be cross workspace # # Check parent issue is from workspace as it can be cross workspace
if ( if (
@ -131,9 +122,7 @@ class DraftIssueCreateSerializer(BaseSerializer):
pk=attrs.get("parent").id, pk=attrs.get("parent").id,
).exists() ).exists()
): ):
raise serializers.ValidationError( raise serializers.ValidationError("Parent is not valid issue_id please pass a valid issue_id")
"Parent is not valid issue_id please pass a valid issue_id"
)
if ( if (
attrs.get("estimate_point") attrs.get("estimate_point")
@ -142,9 +131,7 @@ class DraftIssueCreateSerializer(BaseSerializer):
pk=attrs.get("estimate_point").id, pk=attrs.get("estimate_point").id,
).exists() ).exists()
): ):
raise serializers.ValidationError( raise serializers.ValidationError("Estimate point is not valid please pass a valid estimate_point_id")
"Estimate point is not valid please pass a valid estimate_point_id"
)
return attrs return attrs
@ -159,9 +146,7 @@ class DraftIssueCreateSerializer(BaseSerializer):
project_id = self.context["project_id"] project_id = self.context["project_id"]
# Create Issue # Create Issue
issue = DraftIssue.objects.create( issue = DraftIssue.objects.create(**validated_data, workspace_id=workspace_id, project_id=project_id)
**validated_data, workspace_id=workspace_id, project_id=project_id
)
# Issue Audit Users # Issue Audit Users
created_by_id = issue.created_by_id created_by_id = issue.created_by_id

View file

@ -17,9 +17,7 @@ class PageFavoriteLiteSerializer(serializers.ModelSerializer):
fields = ["id", "name", "logo_props", "project_id"] fields = ["id", "name", "logo_props", "project_id"]
def get_project_id(self, obj): def get_project_id(self, obj):
project = ( project = obj.projects.first() # This gets the first project related to the Page
obj.projects.first()
) # This gets the first project related to the Page
return project.id if project else None return project.id if project else None

View file

@ -45,9 +45,7 @@ class IntakeIssueSerializer(BaseSerializer):
class IntakeIssueDetailSerializer(BaseSerializer): class IntakeIssueDetailSerializer(BaseSerializer):
issue = IssueDetailSerializer(read_only=True) issue = IssueDetailSerializer(read_only=True)
duplicate_issue_detail = IssueIntakeSerializer( duplicate_issue_detail = IssueIntakeSerializer(read_only=True, source="duplicate_to")
read_only=True, source="duplicate_to"
)
class Meta: class Meta:
model = IntakeIssue model = IntakeIssue

View file

@ -1,4 +1,3 @@
# Django imports # Django imports
from django.utils import timezone from django.utils import timezone
from django.core.validators import URLValidator from django.core.validators import URLValidator
@ -127,13 +126,9 @@ class IssueCreateSerializer(BaseSerializer):
# Validate description content for security # Validate description content for security
if "description_html" in attrs and attrs["description_html"]: if "description_html" in attrs and attrs["description_html"]:
is_valid, error_msg, sanitized_html = validate_html_content( is_valid, error_msg, sanitized_html = validate_html_content(attrs["description_html"])
attrs["description_html"]
)
if not is_valid: if not is_valid:
raise serializers.ValidationError( raise serializers.ValidationError({"error": "html content is not valid"})
{"error": "html content is not valid"}
)
# Update the attrs with sanitized HTML if available # Update the attrs with sanitized HTML if available
if sanitized_html is not None: if sanitized_html is not None:
attrs["description_html"] = sanitized_html attrs["description_html"] = sanitized_html
@ -141,9 +136,7 @@ class IssueCreateSerializer(BaseSerializer):
if "description_binary" in attrs and attrs["description_binary"]: if "description_binary" in attrs and attrs["description_binary"]:
is_valid, error_msg = validate_binary_data(attrs["description_binary"]) is_valid, error_msg = validate_binary_data(attrs["description_binary"])
if not is_valid: if not is_valid:
raise serializers.ValidationError( raise serializers.ValidationError({"description_binary": "Invalid binary data"})
{"description_binary": "Invalid binary data"}
)
# Validate assignees are from project # Validate assignees are from project
if attrs.get("assignee_ids", []): if attrs.get("assignee_ids", []):
@ -172,9 +165,7 @@ class IssueCreateSerializer(BaseSerializer):
pk=attrs.get("state").id, pk=attrs.get("state").id,
).exists() ).exists()
): ):
raise serializers.ValidationError( raise serializers.ValidationError("State is not valid please pass a valid state_id")
"State is not valid please pass a valid state_id"
)
# Check parent issue is from workspace as it can be cross workspace # Check parent issue is from workspace as it can be cross workspace
if ( if (
@ -184,9 +175,7 @@ class IssueCreateSerializer(BaseSerializer):
pk=attrs.get("parent").id, pk=attrs.get("parent").id,
).exists() ).exists()
): ):
raise serializers.ValidationError( raise serializers.ValidationError("Parent is not valid issue_id please pass a valid issue_id")
"Parent is not valid issue_id please pass a valid issue_id"
)
if ( if (
attrs.get("estimate_point") attrs.get("estimate_point")
@ -195,9 +184,7 @@ class IssueCreateSerializer(BaseSerializer):
pk=attrs.get("estimate_point").id, pk=attrs.get("estimate_point").id,
).exists() ).exists()
): ):
raise serializers.ValidationError( raise serializers.ValidationError("Estimate point is not valid please pass a valid estimate_point_id")
"Estimate point is not valid please pass a valid estimate_point_id"
)
return attrs return attrs
@ -343,11 +330,7 @@ class IssueActivitySerializer(BaseSerializer):
source_data = serializers.SerializerMethodField() source_data = serializers.SerializerMethodField()
def get_source_data(self, obj): def get_source_data(self, obj):
if ( if hasattr(obj, "issue") and hasattr(obj.issue, "source_data") and obj.issue.source_data:
hasattr(obj, "issue")
and hasattr(obj.issue, "source_data")
and obj.issue.source_data
):
return { return {
"source": obj.issue.source_data[0].source, "source": obj.issue.source_data[0].source,
"source_email": obj.issue.source_data[0].source_email, "source_email": obj.issue.source_data[0].source_email,
@ -397,12 +380,8 @@ class IssueLabelSerializer(BaseSerializer):
class IssueRelationSerializer(BaseSerializer): class IssueRelationSerializer(BaseSerializer):
id = serializers.UUIDField(source="related_issue.id", read_only=True) id = serializers.UUIDField(source="related_issue.id", read_only=True)
project_id = serializers.PrimaryKeyRelatedField( project_id = serializers.PrimaryKeyRelatedField(source="related_issue.project_id", read_only=True)
source="related_issue.project_id", read_only=True sequence_id = serializers.IntegerField(source="related_issue.sequence_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) name = serializers.CharField(source="related_issue.name", read_only=True)
relation_type = serializers.CharField(read_only=True) relation_type = serializers.CharField(read_only=True)
state_id = serializers.UUIDField(source="related_issue.state.id", 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): class RelatedIssueSerializer(BaseSerializer):
id = serializers.UUIDField(source="issue.id", read_only=True) id = serializers.UUIDField(source="issue.id", read_only=True)
project_id = serializers.PrimaryKeyRelatedField( project_id = serializers.PrimaryKeyRelatedField(source="issue.project_id", read_only=True)
source="issue.project_id", read_only=True
)
sequence_id = serializers.IntegerField(source="issue.sequence_id", read_only=True) sequence_id = serializers.IntegerField(source="issue.sequence_id", read_only=True)
name = serializers.CharField(source="issue.name", read_only=True) name = serializers.CharField(source="issue.name", read_only=True)
relation_type = serializers.CharField(read_only=True) relation_type = serializers.CharField(read_only=True)
@ -585,25 +562,17 @@ class IssueLinkSerializer(BaseSerializer):
# Validation if url already exists # Validation if url already exists
def create(self, validated_data): def create(self, validated_data):
if IssueLink.objects.filter( if IssueLink.objects.filter(url=validated_data.get("url"), issue_id=validated_data.get("issue_id")).exists():
url=validated_data.get("url"), issue_id=validated_data.get("issue_id") raise serializers.ValidationError({"error": "URL already exists for this Issue"})
).exists():
raise serializers.ValidationError(
{"error": "URL already exists for this Issue"}
)
return IssueLink.objects.create(**validated_data) return IssueLink.objects.create(**validated_data)
def update(self, instance, validated_data): def update(self, instance, validated_data):
if ( if (
IssueLink.objects.filter( IssueLink.objects.filter(url=validated_data.get("url"), issue_id=instance.issue_id)
url=validated_data.get("url"), issue_id=instance.issue_id
)
.exclude(pk=instance.id) .exclude(pk=instance.id)
.exists() .exists()
): ):
raise serializers.ValidationError( raise serializers.ValidationError({"error": "URL already exists for this Issue"})
{"error": "URL already exists for this Issue"}
)
return super().update(instance, validated_data) return super().update(instance, validated_data)
@ -941,9 +910,7 @@ class IssueDetailSerializer(IssueSerializer):
class IssuePublicSerializer(BaseSerializer): class IssuePublicSerializer(BaseSerializer):
project_detail = ProjectLiteSerializer(read_only=True, source="project") project_detail = ProjectLiteSerializer(read_only=True, source="project")
state_detail = StateLiteSerializer(read_only=True, source="state") state_detail = StateLiteSerializer(read_only=True, source="state")
reactions = IssueReactionSerializer( reactions = IssueReactionSerializer(read_only=True, many=True, source="issue_reactions")
read_only=True, many=True, source="issue_reactions"
)
votes = IssueVoteSerializer(read_only=True, many=True) votes = IssueVoteSerializer(read_only=True, many=True)
class Meta: class Meta:

View file

@ -65,9 +65,7 @@ class ModuleWriteSerializer(BaseSerializer):
if module_name: if module_name:
# Lookup for the module name in the module table for that project # Lookup for the module name in the module table for that project
if Module.objects.filter(name=module_name, project=project).exists(): if Module.objects.filter(name=module_name, project=project).exists():
raise serializers.ValidationError( raise serializers.ValidationError({"error": "Module with this name already exists"})
{"error": "Module with this name already exists"}
)
module = Module.objects.create(**validated_data, project=project) module = Module.objects.create(**validated_data, project=project)
if members is not None: if members is not None:
@ -94,14 +92,8 @@ class ModuleWriteSerializer(BaseSerializer):
module_name = validated_data.get("name") module_name = validated_data.get("name")
if module_name: if module_name:
# Lookup for the module name in the module table for that project # Lookup for the module name in the module table for that project
if ( if Module.objects.filter(name=module_name, project=instance.project).exclude(id=instance.id).exists():
Module.objects.filter(name=module_name, project=instance.project) raise serializers.ValidationError({"error": "Module with this name already exists"})
.exclude(id=instance.id)
.exists()
):
raise serializers.ValidationError(
{"error": "Module with this name already exists"}
)
if members is not None: if members is not None:
ModuleMember.objects.filter(module=instance).delete() ModuleMember.objects.filter(module=instance).delete()
@ -191,32 +183,24 @@ class ModuleLinkSerializer(BaseSerializer):
def create(self, validated_data): def create(self, validated_data):
validated_data["url"] = self.validate_url(validated_data.get("url")) validated_data["url"] = self.validate_url(validated_data.get("url"))
if ModuleLink.objects.filter( if ModuleLink.objects.filter(url=validated_data.get("url"), module_id=validated_data.get("module_id")).exists():
url=validated_data.get("url"), module_id=validated_data.get("module_id")
).exists():
raise serializers.ValidationError({"error": "URL already exists."}) raise serializers.ValidationError({"error": "URL already exists."})
return super().create(validated_data) return super().create(validated_data)
def update(self, instance, validated_data): def update(self, instance, validated_data):
validated_data["url"] = self.validate_url(validated_data.get("url")) validated_data["url"] = self.validate_url(validated_data.get("url"))
if ( if (
ModuleLink.objects.filter( ModuleLink.objects.filter(url=validated_data.get("url"), module_id=instance.module_id)
url=validated_data.get("url"), module_id=instance.module_id
)
.exclude(pk=instance.id) .exclude(pk=instance.id)
.exists() .exists()
): ):
raise serializers.ValidationError( raise serializers.ValidationError({"error": "URL already exists for this Issue"})
{"error": "URL already exists for this Issue"}
)
return super().update(instance, validated_data) return super().update(instance, validated_data)
class ModuleSerializer(DynamicBaseSerializer): class ModuleSerializer(DynamicBaseSerializer):
member_ids = serializers.ListField( member_ids = serializers.ListField(child=serializers.UUIDField(), required=False, allow_null=True)
child=serializers.UUIDField(), required=False, allow_null=True
)
is_favorite = serializers.BooleanField(read_only=True) is_favorite = serializers.BooleanField(read_only=True)
total_issues = serializers.IntegerField(read_only=True) total_issues = serializers.IntegerField(read_only=True)
cancelled_issues = serializers.IntegerField(read_only=True) cancelled_issues = serializers.IntegerField(read_only=True)

View file

@ -10,7 +10,6 @@ from plane.utils.content_validator import (
) )
from plane.db.models import ( from plane.db.models import (
Page, Page,
PageLog,
PageLabel, PageLabel,
Label, Label,
ProjectPage, ProjectPage,
@ -186,9 +185,7 @@ class PageBinaryUpdateSerializer(serializers.Serializer):
# Validate the binary data # Validate the binary data
is_valid, error_message = validate_binary_data(binary_data) is_valid, error_message = validate_binary_data(binary_data)
if not is_valid: if not is_valid:
raise serializers.ValidationError( raise serializers.ValidationError(f"Invalid binary data: {error_message}")
f"Invalid binary data: {error_message}"
)
return binary_data return binary_data
except Exception as e: except Exception as e:
@ -209,7 +206,6 @@ class PageBinaryUpdateSerializer(serializers.Serializer):
# Return sanitized HTML if available, otherwise return original # Return sanitized HTML if available, otherwise return original
return sanitized_html if sanitized_html is not None else value return sanitized_html if sanitized_html is not None else value
def update(self, instance, validated_data): def update(self, instance, validated_data):
"""Update the page instance with validated data""" """Update the page instance with validated data"""
if "description_binary" in validated_data: if "description_binary" in validated_data:

View file

@ -47,9 +47,7 @@ class ProjectSerializer(BaseSerializer):
project_id = self.instance.id if self.instance else None project_id = self.instance.id if self.instance else None
workspace_id = self.context["workspace_id"] workspace_id = self.context["workspace_id"]
project = Project.objects.filter( project = Project.objects.filter(identifier=identifier, workspace_id=workspace_id)
identifier=identifier, workspace_id=workspace_id
)
if project_id: if project_id:
project = project.exclude(id=project_id) project = project.exclude(id=project_id)
@ -64,17 +62,13 @@ class ProjectSerializer(BaseSerializer):
def validate(self, data): def validate(self, data):
# Validate description content for security # Validate description content for security
if "description_html" in data and data["description_html"]: if "description_html" in data and data["description_html"]:
is_valid, error_msg, sanitized_html = validate_html_content( is_valid, error_msg, sanitized_html = validate_html_content(str(data["description_html"]))
str(data["description_html"])
)
# Update the data with sanitized HTML if available # Update the data with sanitized HTML if available
if sanitized_html is not None: if sanitized_html is not None:
data["description_html"] = sanitized_html data["description_html"] = sanitized_html
if not is_valid: if not is_valid:
raise serializers.ValidationError( raise serializers.ValidationError({"error": "html content is not valid"})
{"error": "html content is not valid"}
)
return data return data
@ -83,9 +77,7 @@ class ProjectSerializer(BaseSerializer):
project = Project.objects.create(**validated_data, workspace_id=workspace_id) project = Project.objects.create(**validated_data, workspace_id=workspace_id)
ProjectIdentifier.objects.create( ProjectIdentifier.objects.create(name=project.identifier, project=project, workspace_id=workspace_id)
name=project.identifier, project=project, workspace_id=workspace_id
)
return project return project
@ -118,11 +110,7 @@ class ProjectListSerializer(DynamicBaseSerializer):
project_members = getattr(obj, "members_list", None) project_members = getattr(obj, "members_list", None)
if project_members is not None: if project_members is not None:
# Filter members by the project ID # Filter members by the project ID
return [ return [member.member_id for member in project_members if member.is_active and not member.member.is_bot]
member.member_id
for member in project_members
if member.is_active and not member.member.is_bot
]
return [] return []
class Meta: class Meta:

View file

@ -91,9 +91,7 @@ class UserMeSettingsSerializer(BaseSerializer):
read_only_fields = fields read_only_fields = fields
def get_workspace(self, obj): def get_workspace(self, obj):
workspace_invites = WorkspaceMemberInvite.objects.filter( workspace_invites = WorkspaceMemberInvite.objects.filter(email=obj.email).count()
email=obj.email
).count()
# profile # profile
profile = Profile.objects.get(user=obj) profile = Profile.objects.get(user=obj)
@ -110,43 +108,27 @@ class UserMeSettingsSerializer(BaseSerializer):
workspace_member__member=obj.id, workspace_member__member=obj.id,
workspace_member__is_active=True, workspace_member__is_active=True,
).first() ).first()
logo_asset_url = ( logo_asset_url = workspace.logo_asset.asset_url if workspace.logo_asset is not None else ""
workspace.logo_asset.asset_url
if workspace.logo_asset is not None
else ""
)
return { return {
"last_workspace_id": profile.last_workspace_id, "last_workspace_id": profile.last_workspace_id,
"last_workspace_slug": ( "last_workspace_slug": (workspace.slug if workspace is not None else ""),
workspace.slug if workspace is not None else "" "last_workspace_name": (workspace.name if workspace is not None else ""),
),
"last_workspace_name": (
workspace.name if workspace is not None else ""
),
"last_workspace_logo": (logo_asset_url), "last_workspace_logo": (logo_asset_url),
"fallback_workspace_id": profile.last_workspace_id, "fallback_workspace_id": profile.last_workspace_id,
"fallback_workspace_slug": ( "fallback_workspace_slug": (workspace.slug if workspace is not None else ""),
workspace.slug if workspace is not None else ""
),
"invites": workspace_invites, "invites": workspace_invites,
} }
else: else:
fallback_workspace = ( fallback_workspace = (
Workspace.objects.filter( Workspace.objects.filter(workspace_member__member_id=obj.id, workspace_member__is_active=True)
workspace_member__member_id=obj.id, workspace_member__is_active=True
)
.order_by("created_at") .order_by("created_at")
.first() .first()
) )
return { return {
"last_workspace_id": None, "last_workspace_id": None,
"last_workspace_slug": None, "last_workspace_slug": None,
"fallback_workspace_id": ( "fallback_workspace_id": (fallback_workspace.id if fallback_workspace is not None else None),
fallback_workspace.id if fallback_workspace is not None else None "fallback_workspace_slug": (fallback_workspace.slug if fallback_workspace is not None else None),
),
"fallback_workspace_slug": (
fallback_workspace.slug if fallback_workspace is not None else None
),
"invites": workspace_invites, "invites": workspace_invites,
} }
@ -195,14 +177,10 @@ class ChangePasswordSerializer(serializers.Serializer):
def validate(self, data): def validate(self, data):
if data.get("old_password") == data.get("new_password"): if data.get("old_password") == data.get("new_password"):
raise serializers.ValidationError( raise serializers.ValidationError({"error": "New password cannot be same as old password."})
{"error": "New password cannot be same as old password."}
)
if data.get("new_password") != data.get("confirm_password"): if data.get("new_password") != data.get("confirm_password"):
raise serializers.ValidationError( raise serializers.ValidationError({"error": "Confirm password should be same as the new password."})
{"error": "Confirm password should be same as the new password."}
)
return data return data

View file

@ -21,29 +21,21 @@ class WebhookSerializer(DynamicBaseSerializer):
# Extract the hostname from the URL # Extract the hostname from the URL
hostname = urlparse(url).hostname hostname = urlparse(url).hostname
if not hostname: if not hostname:
raise serializers.ValidationError( raise serializers.ValidationError({"url": "Invalid URL: No hostname found."})
{"url": "Invalid URL: No hostname found."}
)
# Resolve the hostname to IP addresses # Resolve the hostname to IP addresses
try: try:
ip_addresses = socket.getaddrinfo(hostname, None) ip_addresses = socket.getaddrinfo(hostname, None)
except socket.gaierror: except socket.gaierror:
raise serializers.ValidationError( raise serializers.ValidationError({"url": "Hostname could not be resolved."})
{"url": "Hostname could not be resolved."}
)
if not ip_addresses: if not ip_addresses:
raise serializers.ValidationError( raise serializers.ValidationError({"url": "No IP addresses found for the hostname."})
{"url": "No IP addresses found for the hostname."}
)
for addr in ip_addresses: for addr in ip_addresses:
ip = ipaddress.ip_address(addr[4][0]) ip = ipaddress.ip_address(addr[4][0])
if ip.is_loopback: if ip.is_loopback:
raise serializers.ValidationError( raise serializers.ValidationError({"url": "URL resolves to a blocked IP address."})
{"url": "URL resolves to a blocked IP address."}
)
# Additional validation for multiple request domains and their subdomains # Additional validation for multiple request domains and their subdomains
request = self.context.get("request") request = self.context.get("request")
@ -53,13 +45,8 @@ class WebhookSerializer(DynamicBaseSerializer):
disallowed_domains.append(request_host) disallowed_domains.append(request_host)
# Check if hostname is a subdomain or exact match of any disallowed domain # Check if hostname is a subdomain or exact match of any disallowed domain
if any( if any(hostname == domain or hostname.endswith("." + domain) for domain in disallowed_domains):
hostname == domain or hostname.endswith("." + domain) raise serializers.ValidationError({"url": "URL domain or its subdomain is not allowed."})
for domain in disallowed_domains
):
raise serializers.ValidationError(
{"url": "URL domain or its subdomain is not allowed."}
)
return Webhook.objects.create(**validated_data) return Webhook.objects.create(**validated_data)
@ -69,47 +56,32 @@ class WebhookSerializer(DynamicBaseSerializer):
# Extract the hostname from the URL # Extract the hostname from the URL
hostname = urlparse(url).hostname hostname = urlparse(url).hostname
if not hostname: if not hostname:
raise serializers.ValidationError( raise serializers.ValidationError({"url": "Invalid URL: No hostname found."})
{"url": "Invalid URL: No hostname found."}
)
# Resolve the hostname to IP addresses # Resolve the hostname to IP addresses
try: try:
ip_addresses = socket.getaddrinfo(hostname, None) ip_addresses = socket.getaddrinfo(hostname, None)
except socket.gaierror: except socket.gaierror:
raise serializers.ValidationError( raise serializers.ValidationError({"url": "Hostname could not be resolved."})
{"url": "Hostname could not be resolved."}
)
if not ip_addresses: if not ip_addresses:
raise serializers.ValidationError( raise serializers.ValidationError({"url": "No IP addresses found for the hostname."})
{"url": "No IP addresses found for the hostname."}
)
for addr in ip_addresses: for addr in ip_addresses:
ip = ipaddress.ip_address(addr[4][0]) ip = ipaddress.ip_address(addr[4][0])
if ip.is_loopback: if ip.is_loopback:
raise serializers.ValidationError( raise serializers.ValidationError({"url": "URL resolves to a blocked IP address."})
{"url": "URL resolves to a blocked IP address."}
)
# Additional validation for multiple request domains and their subdomains # Additional validation for multiple request domains and their subdomains
request = self.context.get("request") request = self.context.get("request")
disallowed_domains = ["plane.so"] # Add your disallowed domains here disallowed_domains = ["plane.so"] # Add your disallowed domains here
if request: if request:
request_host = request.get_host().split(":")[ request_host = request.get_host().split(":")[0] # Remove port if present
0
] # Remove port if present
disallowed_domains.append(request_host) disallowed_domains.append(request_host)
# Check if hostname is a subdomain or exact match of any disallowed domain # Check if hostname is a subdomain or exact match of any disallowed domain
if any( if any(hostname == domain or hostname.endswith("." + domain) for domain in disallowed_domains):
hostname == domain or hostname.endswith("." + domain) raise serializers.ValidationError({"url": "URL domain or its subdomain is not allowed."})
for domain in disallowed_domains
):
raise serializers.ValidationError(
{"url": "URL domain or its subdomain is not allowed."}
)
return super().update(instance, validated_data) return super().update(instance, validated_data)

View file

@ -173,9 +173,7 @@ class WorkspaceUserLinkSerializer(BaseSerializer):
) )
if workspace_user_link.exists(): if workspace_user_link.exists():
raise serializers.ValidationError( raise serializers.ValidationError({"error": "URL already exists for this workspace and owner"})
{"error": "URL already exists for this workspace and owner"}
)
return super().create(validated_data) return super().create(validated_data)
@ -189,9 +187,7 @@ class WorkspaceUserLinkSerializer(BaseSerializer):
) )
if workspace_user_link.exclude(pk=instance.id).exists(): if workspace_user_link.exclude(pk=instance.id).exists():
raise serializers.ValidationError( raise serializers.ValidationError({"error": "URL already exists for this workspace and owner"})
{"error": "URL already exists for this workspace and owner"}
)
return super().update(instance, validated_data) return super().update(instance, validated_data)
@ -219,11 +215,7 @@ class IssueRecentVisitSerializer(serializers.ModelSerializer):
return project.identifier if project else None return project.identifier if project else None
def get_assignees(self, obj): def get_assignees(self, obj):
return list( return list(obj.assignees.filter(issue_assignee__deleted_at__isnull=True).values_list("id", flat=True))
obj.assignees.filter(issue_assignee__deleted_at__isnull=True).values_list(
"id", flat=True
)
)
class ProjectRecentVisitSerializer(serializers.ModelSerializer): class ProjectRecentVisitSerializer(serializers.ModelSerializer):
@ -234,9 +226,9 @@ class ProjectRecentVisitSerializer(serializers.ModelSerializer):
fields = ["id", "name", "logo_props", "project_members", "identifier"] fields = ["id", "name", "logo_props", "project_members", "identifier"]
def get_project_members(self, obj): def get_project_members(self, obj):
members = ProjectMember.objects.filter( members = ProjectMember.objects.filter(project_id=obj.id, member__is_bot=False, is_active=True).values_list(
project_id=obj.id, member__is_bot=False, is_active=True "member", flat=True
).values_list("member", flat=True) )
return members return members
@ -257,11 +249,7 @@ class PageRecentVisitSerializer(serializers.ModelSerializer):
] ]
def get_project_id(self, obj): def get_project_id(self, obj):
return ( return obj.project_id if hasattr(obj, "project_id") else obj.projects.values_list("id", flat=True).first()
obj.project_id
if hasattr(obj, "project_id")
else obj.projects.values_list("id", flat=True).first()
)
def get_project_identifier(self, obj): def get_project_identifier(self, obj):
project = obj.projects.first() project = obj.projects.first()
@ -319,13 +307,9 @@ class StickySerializer(BaseSerializer):
def validate(self, data): def validate(self, data):
# Validate description content for security # Validate description content for security
if "description_html" in data and data["description_html"]: if "description_html" in data and data["description_html"]:
is_valid, error_msg, sanitized_html = validate_html_content( is_valid, error_msg, sanitized_html = validate_html_content(data["description_html"])
data["description_html"]
)
if not is_valid: if not is_valid:
raise serializers.ValidationError( raise serializers.ValidationError({"error": "html content is not valid"})
{"error": "html content is not valid"}
)
# Update the data with sanitized HTML if available # Update the data with sanitized HTML if available
if sanitized_html is not None: if sanitized_html is not None:
data["description_html"] = sanitized_html data["description_html"] = sanitized_html
@ -333,9 +317,7 @@ class StickySerializer(BaseSerializer):
if "description_binary" in data and data["description_binary"]: if "description_binary" in data and data["description_binary"]:
is_valid, error_msg = validate_binary_data(data["description_binary"]) is_valid, error_msg = validate_binary_data(data["description_binary"])
if not is_valid: if not is_valid:
raise serializers.ValidationError( raise serializers.ValidationError({"description_binary": "Invalid binary data"})
{"description_binary": "Invalid binary data"}
)
return data return data

View file

@ -30,9 +30,7 @@ urlpatterns = [
), ),
path( path(
"workspaces/<str:slug>/analytic-view/<uuid:pk>/", "workspaces/<str:slug>/analytic-view/<uuid:pk>/",
AnalyticViewViewset.as_view( AnalyticViewViewset.as_view({"get": "retrieve", "patch": "partial_update", "delete": "destroy"}),
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
),
name="analytic-view", name="analytic-view",
), ),
path( path(

View file

@ -21,9 +21,7 @@ urlpatterns = [
), ),
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:estimate_id>/", "workspaces/<str:slug>/projects/<uuid:project_id>/estimates/<uuid:estimate_id>/",
BulkEstimatePointEndpoint.as_view( BulkEstimatePointEndpoint.as_view({"get": "retrieve", "patch": "partial_update", "delete": "destroy"}),
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
),
name="bulk-create-estimate-points", name="bulk-create-estimate-points",
), ),
path( path(

View file

@ -16,9 +16,7 @@ urlpatterns = [
), ),
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/intakes/<uuid:pk>/", "workspaces/<str:slug>/projects/<uuid:project_id>/intakes/<uuid:pk>/",
IntakeViewSet.as_view( IntakeViewSet.as_view({"get": "retrieve", "patch": "partial_update", "delete": "destroy"}),
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
),
name="intake", name="intake",
), ),
path( path(
@ -28,9 +26,7 @@ urlpatterns = [
), ),
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/intake-issues/<uuid:pk>/", "workspaces/<str:slug>/projects/<uuid:project_id>/intake-issues/<uuid:pk>/",
IntakeIssueViewSet.as_view( IntakeIssueViewSet.as_view({"get": "retrieve", "patch": "partial_update", "delete": "destroy"}),
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
),
name="intake-issue", name="intake-issue",
), ),
path( path(
@ -40,9 +36,7 @@ urlpatterns = [
), ),
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/<uuid:pk>/", "workspaces/<str:slug>/projects/<uuid:project_id>/inboxes/<uuid:pk>/",
IntakeViewSet.as_view( IntakeViewSet.as_view({"get": "retrieve", "patch": "partial_update", "delete": "destroy"}),
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
),
name="inbox", name="inbox",
), ),
path( path(
@ -52,9 +46,7 @@ urlpatterns = [
), ),
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/<uuid:pk>/", "workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/<uuid:pk>/",
IntakeIssueViewSet.as_view( IntakeIssueViewSet.as_view({"get": "retrieve", "patch": "partial_update", "delete": "destroy"}),
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
),
name="inbox-issue", name="inbox-issue",
), ),
path( path(

View file

@ -187,9 +187,7 @@ urlpatterns = [
), ),
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/subscribe/", "workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/subscribe/",
IssueSubscriberViewSet.as_view( IssueSubscriberViewSet.as_view({"get": "subscription_status", "post": "subscribe", "delete": "unsubscribe"}),
{"get": "subscription_status", "post": "subscribe", "delete": "unsubscribe"}
),
name="project-issue-subscribers", name="project-issue-subscribers",
), ),
## End Issue Subscribers ## End Issue Subscribers
@ -232,9 +230,7 @@ urlpatterns = [
), ),
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:pk>/archive/", "workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:pk>/archive/",
IssueArchiveViewSet.as_view( IssueArchiveViewSet.as_view({"get": "retrieve", "post": "archive", "delete": "unarchive"}),
{"get": "retrieve", "post": "archive", "delete": "unarchive"}
),
name="project-issue-archive-unarchive", name="project-issue-archive-unarchive",
), ),
## End Issue Archives ## End Issue Archives

View file

@ -17,9 +17,7 @@ urlpatterns = [
), ),
path( path(
"workspaces/<str:slug>/users/notifications/<uuid:pk>/", "workspaces/<str:slug>/users/notifications/<uuid:pk>/",
NotificationViewSet.as_view( NotificationViewSet.as_view({"get": "retrieve", "patch": "partial_update", "delete": "destroy"}),
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
),
name="notifications", name="notifications",
), ),
path( path(

View file

@ -22,9 +22,7 @@ urlpatterns = [
), ),
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/", "workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/",
PageViewSet.as_view( PageViewSet.as_view({"get": "retrieve", "patch": "partial_update", "delete": "destroy"}),
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
),
name="project-pages", name="project-pages",
), ),
# favorite pages # favorite pages

View file

@ -77,9 +77,7 @@ urlpatterns = [
), ),
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/members/<uuid:pk>/", "workspaces/<str:slug>/projects/<uuid:project_id>/members/<uuid:pk>/",
ProjectMemberViewSet.as_view( ProjectMemberViewSet.as_view({"get": "retrieve", "patch": "partial_update", "delete": "destroy"}),
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
),
name="project-member", name="project-member",
), ),
path( path(
@ -119,9 +117,7 @@ urlpatterns = [
), ),
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/<uuid:pk>/", "workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/<uuid:pk>/",
DeployBoardViewSet.as_view( DeployBoardViewSet.as_view({"get": "retrieve", "patch": "partial_update", "delete": "destroy"}),
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
),
name="project-deploy-board", name="project-deploy-board",
), ),
path( path(

View file

@ -12,9 +12,7 @@ urlpatterns = [
), ),
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/states/<uuid:pk>/", "workspaces/<str:slug>/projects/<uuid:project_id>/states/<uuid:pk>/",
StateViewSet.as_view( StateViewSet.as_view({"get": "retrieve", "patch": "partial_update", "delete": "destroy"}),
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
),
name="project-state", name="project-state",
), ),
path( path(

View file

@ -21,9 +21,7 @@ urlpatterns = [
# User Profile # User Profile
path( path(
"users/me/", "users/me/",
UserEndpoint.as_view( UserEndpoint.as_view({"get": "retrieve", "patch": "partial_update", "delete": "deactivate"}),
{"get": "retrieve", "patch": "partial_update", "delete": "deactivate"}
),
name="users", name="users",
), ),
path("users/session/", UserSessionEndpoint.as_view(), name="user-session"), path("users/session/", UserSessionEndpoint.as_view(), name="user-session"),
@ -44,21 +42,15 @@ urlpatterns = [
UserEndpoint.as_view({"get": "retrieve_instance_admin"}), UserEndpoint.as_view({"get": "retrieve_instance_admin"}),
name="users", name="users",
), ),
path( path("users/me/onboard/", UpdateUserOnBoardedEndpoint.as_view(), name="user-onboard"),
"users/me/onboard/", UpdateUserOnBoardedEndpoint.as_view(), name="user-onboard"
),
path( path(
"users/me/tour-completed/", "users/me/tour-completed/",
UpdateUserTourCompletedEndpoint.as_view(), UpdateUserTourCompletedEndpoint.as_view(),
name="user-tour", name="user-tour",
), ),
path( path("users/me/activities/", UserActivityEndpoint.as_view(), name="user-activities"),
"users/me/activities/", UserActivityEndpoint.as_view(), name="user-activities"
),
# user workspaces # user workspaces
path( path("users/me/workspaces/", UserWorkSpacesEndpoint.as_view(), name="user-workspace"),
"users/me/workspaces/", UserWorkSpacesEndpoint.as_view(), name="user-workspace"
),
# User Graphs # User Graphs
path( path(
"users/me/workspaces/<str:slug>/activity-graph/", "users/me/workspaces/<str:slug>/activity-graph/",

View file

@ -65,9 +65,7 @@ urlpatterns = [
), ),
path( path(
"workspaces/<str:slug>/invitations/<uuid:pk>/", "workspaces/<str:slug>/invitations/<uuid:pk>/",
WorkspaceInvitationsViewset.as_view( WorkspaceInvitationsViewset.as_view({"delete": "destroy", "get": "retrieve", "patch": "partial_update"}),
{"delete": "destroy", "get": "retrieve", "patch": "partial_update"}
),
name="workspace-invitations", name="workspace-invitations",
), ),
# user workspace invitations # user workspace invitations
@ -94,9 +92,7 @@ urlpatterns = [
), ),
path( path(
"workspaces/<str:slug>/members/<uuid:pk>/", "workspaces/<str:slug>/members/<uuid:pk>/",
WorkSpaceMemberViewSet.as_view( WorkSpaceMemberViewSet.as_view({"patch": "partial_update", "delete": "destroy", "get": "retrieve"}),
{"patch": "partial_update", "delete": "destroy", "get": "retrieve"}
),
name="workspace-member", name="workspace-member",
), ),
path( path(
@ -126,9 +122,7 @@ urlpatterns = [
), ),
path( path(
"workspaces/<str:slug>/workspace-themes/<uuid:pk>/", "workspaces/<str:slug>/workspace-themes/<uuid:pk>/",
WorkspaceThemeViewSet.as_view( WorkspaceThemeViewSet.as_view({"get": "retrieve", "patch": "partial_update", "delete": "destroy"}),
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
),
name="workspace-themes", name="workspace-themes",
), ),
path( path(
@ -208,9 +202,7 @@ urlpatterns = [
), ),
path( path(
"workspaces/<str:slug>/draft-issues/<uuid:pk>/", "workspaces/<str:slug>/draft-issues/<uuid:pk>/",
WorkspaceDraftIssueViewSet.as_view( WorkspaceDraftIssueViewSet.as_view({"get": "retrieve", "patch": "partial_update", "delete": "destroy"}),
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
),
name="workspace-drafts-issues", name="workspace-drafts-issues",
), ),
path( path(
@ -226,9 +218,7 @@ urlpatterns = [
), ),
path( path(
"workspaces/<str:slug>/quick-links/<uuid:pk>/", "workspaces/<str:slug>/quick-links/<uuid:pk>/",
QuickLinkViewSet.as_view( QuickLinkViewSet.as_view({"get": "retrieve", "patch": "partial_update", "delete": "destroy"}),
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
),
name="workspace-quick-links", name="workspace-quick-links",
), ),
# Widgets # Widgets
@ -254,9 +244,7 @@ urlpatterns = [
), ),
path( path(
"workspaces/<str:slug>/stickies/<uuid:pk>/", "workspaces/<str:slug>/stickies/<uuid:pk>/",
WorkspaceStickyViewSet.as_view( WorkspaceStickyViewSet.as_view({"get": "retrieve", "patch": "partial_update", "delete": "destroy"}),
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
),
name="workspace-sticky", name="workspace-sticky",
), ),
# User Preference # User Preference

View file

@ -41,26 +41,16 @@ class AdvanceAnalyticsEndpoint(AdvanceAnalyticsBaseView):
def get_filtered_count() -> int: def get_filtered_count() -> int:
if self.filters["analytics_date_range"]: if self.filters["analytics_date_range"]:
return queryset.filter( return queryset.filter(
created_at__gte=self.filters["analytics_date_range"]["current"][ created_at__gte=self.filters["analytics_date_range"]["current"]["gte"],
"gte" created_at__lte=self.filters["analytics_date_range"]["current"]["lte"],
],
created_at__lte=self.filters["analytics_date_range"]["current"][
"lte"
],
).count() ).count()
return queryset.count() return queryset.count()
def get_previous_count() -> int: def get_previous_count() -> int:
if self.filters["analytics_date_range"] and self.filters[ if self.filters["analytics_date_range"] and self.filters["analytics_date_range"].get("previous"):
"analytics_date_range"
].get("previous"):
return queryset.filter( return queryset.filter(
created_at__gte=self.filters["analytics_date_range"]["previous"][ created_at__gte=self.filters["analytics_date_range"]["previous"]["gte"],
"gte" created_at__lte=self.filters["analytics_date_range"]["previous"]["lte"],
],
created_at__lte=self.filters["analytics_date_range"]["previous"][
"lte"
],
).count() ).count()
return 0 return 0
@ -71,39 +61,27 @@ class AdvanceAnalyticsEndpoint(AdvanceAnalyticsBaseView):
def get_overview_data(self) -> Dict[str, Dict[str, int]]: def get_overview_data(self) -> Dict[str, Dict[str, int]]:
members_query = WorkspaceMember.objects.filter( 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): if self.request.GET.get("project_ids", None):
project_ids = 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(",")] project_ids = [str(project_id) for project_id in project_ids.split(",")]
members_query = ProjectMember.objects.filter( 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 { return {
"total_users": self.get_filtered_counts(members_query), "total_users": self.get_filtered_counts(members_query),
"total_admins": self.get_filtered_counts( "total_admins": self.get_filtered_counts(members_query.filter(role=ROLE.ADMIN.value)),
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_members": self.get_filtered_counts( "total_projects": self.get_filtered_counts(Project.objects.filter(**self.filters["project_filters"])),
members_query.filter(role=ROLE.MEMBER.value) "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_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( "total_intake": self.get_filtered_counts(
Issue.objects.filter(**self.filters["base_filters"]).filter( 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 { return {
"total_work_items": self.get_filtered_counts(base_queryset), "total_work_items": self.get_filtered_counts(base_queryset),
"started_work_items": self.get_filtered_counts( "started_work_items": self.get_filtered_counts(base_queryset.filter(state__group="started")),
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")),
"backlog_work_items": self.get_filtered_counts( "completed_work_items": self.get_filtered_counts(base_queryset.filter(state__group="completed")),
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") @allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
@ -153,9 +123,7 @@ class AdvanceAnalyticsStatsEndpoint(AdvanceAnalyticsBaseView):
# Apply date range filter if available # Apply date range filter if available
if self.filters["chart_period_range"]: if self.filters["chart_period_range"]:
start_date, end_date = self.filters["chart_period_range"] start_date, end_date = self.filters["chart_period_range"]
base_queryset = base_queryset.filter( base_queryset = base_queryset.filter(created_at__date__gte=start_date, created_at__date__lte=end_date)
created_at__date__gte=start_date, created_at__date__lte=end_date
)
return ( return (
base_queryset.values("project_id", "project__name") base_queryset.values("project_id", "project__name")
@ -212,24 +180,16 @@ class AdvanceAnalyticsChartEndpoint(AdvanceAnalyticsBaseView):
} }
total_work_items = base_queryset.filter(**date_filter).count() total_work_items = base_queryset.filter(**date_filter).count()
total_cycles = Cycle.objects.filter( total_cycles = Cycle.objects.filter(**self.filters["base_filters"], **date_filter).count()
**self.filters["base_filters"], **date_filter total_modules = Module.objects.filter(**self.filters["base_filters"], **date_filter).count()
).count()
total_modules = Module.objects.filter(
**self.filters["base_filters"], **date_filter
).count()
total_intake = Issue.objects.filter( total_intake = Issue.objects.filter(
issue_intake__isnull=False, **self.filters["base_filters"], **date_filter issue_intake__isnull=False, **self.filters["base_filters"], **date_filter
).count() ).count()
total_members = WorkspaceMember.objects.filter( total_members = WorkspaceMember.objects.filter(
workspace__slug=self._workspace_slug, is_active=True, **date_filter workspace__slug=self._workspace_slug, is_active=True, **date_filter
).count() ).count()
total_pages = ProjectPage.objects.filter( total_pages = ProjectPage.objects.filter(**self.filters["base_filters"], **date_filter).count()
**self.filters["base_filters"], **date_filter total_views = IssueView.objects.filter(**self.filters["base_filters"], **date_filter).count()
).count()
total_views = IssueView.objects.filter(
**self.filters["base_filters"], **date_filter
).count()
data = { data = {
"work_items": total_work_items, "work_items": total_work_items,
@ -255,9 +215,7 @@ class AdvanceAnalyticsChartEndpoint(AdvanceAnalyticsBaseView):
queryset = ( queryset = (
Issue.issue_objects.filter(**self.filters["base_filters"]) Issue.issue_objects.filter(**self.filters["base_filters"])
.select_related("workspace", "state", "parent") .select_related("workspace", "state", "parent")
.prefetch_related( .prefetch_related("assignees", "labels", "issue_module__module", "issue_cycle__cycle")
"assignees", "labels", "issue_module__module", "issue_cycle__cycle"
)
) )
workspace = Workspace.objects.get(slug=self._workspace_slug) workspace = Workspace.objects.get(slug=self._workspace_slug)
@ -266,9 +224,7 @@ class AdvanceAnalyticsChartEndpoint(AdvanceAnalyticsBaseView):
# Apply date range filter if available # Apply date range filter if available
if self.filters["chart_period_range"]: if self.filters["chart_period_range"]:
start_date, end_date = self.filters["chart_period_range"] start_date, end_date = self.filters["chart_period_range"]
queryset = queryset.filter( queryset = queryset.filter(created_at__date__gte=start_date, created_at__date__lte=end_date)
created_at__date__gte=start_date, created_at__date__lte=end_date
)
# Annotate by month and count # Annotate by month and count
monthly_stats = ( monthly_stats = (
@ -311,9 +267,7 @@ class AdvanceAnalyticsChartEndpoint(AdvanceAnalyticsBaseView):
) )
# Move to next month # Move to next month
if current_month.month == 12: if current_month.month == 12:
current_month = current_month.replace( current_month = current_month.replace(year=current_month.year + 1, month=1)
year=current_month.year + 1, month=1
)
else: else:
current_month = current_month.replace(month=current_month.month + 1) current_month = current_month.replace(month=current_month.month + 1)
@ -338,17 +292,13 @@ class AdvanceAnalyticsChartEndpoint(AdvanceAnalyticsBaseView):
queryset = ( queryset = (
Issue.issue_objects.filter(**self.filters["base_filters"]) Issue.issue_objects.filter(**self.filters["base_filters"])
.select_related("workspace", "state", "parent") .select_related("workspace", "state", "parent")
.prefetch_related( .prefetch_related("assignees", "labels", "issue_module__module", "issue_cycle__cycle")
"assignees", "labels", "issue_module__module", "issue_cycle__cycle"
)
) )
# Apply date range filter if available # Apply date range filter if available
if self.filters["chart_period_range"]: if self.filters["chart_period_range"]:
start_date, end_date = self.filters["chart_period_range"] start_date, end_date = self.filters["chart_period_range"]
queryset = queryset.filter( queryset = queryset.filter(created_at__date__gte=start_date, created_at__date__lte=end_date)
created_at__date__gte=start_date, created_at__date__lte=end_date
)
return Response( return Response(
build_analytics_chart(queryset, x_axis, group_by), build_analytics_chart(queryset, x_axis, group_by),

View file

@ -55,25 +55,16 @@ class AnalyticsEndpoint(BaseAPIView):
valid_yaxis = ["issue_count", "estimate"] valid_yaxis = ["issue_count", "estimate"]
# Check for x-axis and y-axis as thery are required parameters # Check for x-axis and y-axis as thery are required parameters
if ( if not x_axis or not y_axis or x_axis not in valid_xaxis_segment or y_axis not in valid_yaxis:
not x_axis
or not y_axis
or x_axis not in valid_xaxis_segment
or y_axis not in valid_yaxis
):
return Response( 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, status=status.HTTP_400_BAD_REQUEST,
) )
# If segment is present it cannot be same as x-axis # If segment is present it cannot be same as x-axis
if segment and (segment not in valid_xaxis_segment or x_axis == segment): if segment and (segment not in valid_xaxis_segment or x_axis == segment):
return Response( 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, status=status.HTTP_400_BAD_REQUEST,
) )
@ -87,9 +78,7 @@ class AnalyticsEndpoint(BaseAPIView):
total_issues = queryset.count() total_issues = queryset.count()
# Build the graph payload # Build the graph payload
distribution = build_graph_plot( distribution = build_graph_plot(queryset=queryset, x_axis=x_axis, y_axis=y_axis, segment=segment)
queryset=queryset, x_axis=x_axis, y_axis=y_axis, segment=segment
)
state_details = {} state_details = {}
if x_axis in ["state_id"] or segment in ["state_id"]: 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"]: if x_axis in ["assignees__id"] or segment in ["assignees__id"]:
assignee_details = ( assignee_details = (
Issue.issue_objects.filter( Issue.issue_objects.filter(
Q( Q(Q(assignees__avatar__isnull=False) | Q(assignees__avatar_asset__isnull=False)),
Q(assignees__avatar__isnull=False)
| Q(assignees__avatar_asset__isnull=False)
),
workspace__slug=slug, workspace__slug=slug,
**filters, **filters,
) )
@ -171,9 +157,7 @@ class AnalyticsEndpoint(BaseAPIView):
) )
module_details = {} module_details = {}
if x_axis in ["issue_module__module_id"] or segment in [ if x_axis in ["issue_module__module_id"] or segment in ["issue_module__module_id"]:
"issue_module__module_id"
]:
module_details = ( module_details = (
Issue.issue_objects.filter( Issue.issue_objects.filter(
workspace__slug=slug, workspace__slug=slug,
@ -212,9 +196,7 @@ class AnalyticViewViewset(BaseViewSet):
serializer.save(workspace_id=workspace.id) serializer.save(workspace_id=workspace.id)
def get_queryset(self): def get_queryset(self):
return self.filter_queryset( return self.filter_queryset(super().get_queryset().filter(workspace__slug=self.kwargs.get("slug")))
super().get_queryset().filter(workspace__slug=self.kwargs.get("slug"))
)
class SavedAnalyticEndpoint(BaseAPIView): class SavedAnalyticEndpoint(BaseAPIView):
@ -235,9 +217,7 @@ class SavedAnalyticEndpoint(BaseAPIView):
) )
segment = request.GET.get("segment", False) segment = request.GET.get("segment", False)
distribution = build_graph_plot( distribution = build_graph_plot(queryset=queryset, x_axis=x_axis, y_axis=y_axis, segment=segment)
queryset=queryset, x_axis=x_axis, y_axis=y_axis, segment=segment
)
total_issues = queryset.count() total_issues = queryset.count()
return Response( return Response(
{"total": total_issues, "distribution": distribution}, {"total": total_issues, "distribution": distribution},
@ -270,36 +250,23 @@ class ExportAnalyticsEndpoint(BaseAPIView):
valid_yaxis = ["issue_count", "estimate"] valid_yaxis = ["issue_count", "estimate"]
# Check for x-axis and y-axis as thery are required parameters # Check for x-axis and y-axis as thery are required parameters
if ( if not x_axis or not y_axis or x_axis not in valid_xaxis_segment or y_axis not in valid_yaxis:
not x_axis
or not y_axis
or x_axis not in valid_xaxis_segment
or y_axis not in valid_yaxis
):
return Response( 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, status=status.HTTP_400_BAD_REQUEST,
) )
# If segment is present it cannot be same as x-axis # If segment is present it cannot be same as x-axis
if segment and (segment not in valid_xaxis_segment or x_axis == segment): if segment and (segment not in valid_xaxis_segment or x_axis == segment):
return Response( 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, status=status.HTTP_400_BAD_REQUEST,
) )
analytic_export_task.delay( analytic_export_task.delay(email=request.user.email, data=request.data, slug=slug)
email=request.user.email, data=request.data, slug=slug
)
return Response( 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, status=status.HTTP_200_OK,
) )
@ -315,9 +282,7 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
state_groups = base_issues.annotate(state_group=F("state__group")) state_groups = base_issues.annotate(state_group=F("state__group"))
total_issues_classified = ( total_issues_classified = (
state_groups.values("state_group") state_groups.values("state_group").annotate(state_count=Count("state_group")).order_by("state_group")
.annotate(state_count=Count("state_group"))
.order_by("state_group")
) )
open_issues_groups = ["backlog", "unstarted", "started"] 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 # If `avatar_asset` is None, fall back to using `avatar` field directly
When( When(created_by__avatar_asset__isnull=True, then="created_by__avatar"),
created_by__avatar_asset__isnull=True, then="created_by__avatar"
),
default=Value(None), default=Value(None),
output_field=models.CharField(), output_field=models.CharField(),
) )
@ -395,9 +358,7 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
), ),
), ),
# If `avatar_asset` is None, fall back to using `avatar` field directly # If `avatar_asset` is None, fall back to using `avatar` field directly
When( When(assignees__avatar_asset__isnull=True, then="assignees__avatar"),
assignees__avatar_asset__isnull=True, then="assignees__avatar"
),
default=Value(None), default=Value(None),
output_field=models.CharField(), output_field=models.CharField(),
) )
@ -422,9 +383,7 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
), ),
), ),
# If `avatar_asset` is None, fall back to using `avatar` field directly # If `avatar_asset` is None, fall back to using `avatar` field directly
When( When(assignees__avatar_asset__isnull=True, then="assignees__avatar"),
assignees__avatar_asset__isnull=True, then="assignees__avatar"
),
default=Value(None), default=Value(None),
output_field=models.CharField(), output_field=models.CharField(),
) )
@ -485,9 +444,7 @@ class ProjectStatsEndpoint(BaseAPIView):
if "completed_issues" in requested_fields: if "completed_issues" in requested_fields:
annotations["completed_issues"] = ( annotations["completed_issues"] = (
Issue.issue_objects.filter( Issue.issue_objects.filter(project_id=OuterRef("pk"), state__group__in=["completed", "cancelled"])
project_id=OuterRef("pk"), state__group="completed"
)
.order_by() .order_by()
.annotate(count=Func(F("id"), function="Count")) .annotate(count=Func(F("id"), function="Count"))
.values("count") .values("count")
@ -511,9 +468,7 @@ class ProjectStatsEndpoint(BaseAPIView):
if "total_members" in requested_fields: if "total_members" in requested_fields:
annotations["total_members"] = ( annotations["total_members"] = (
ProjectMember.objects.filter( ProjectMember.objects.filter(project_id=OuterRef("id"), member__is_bot=False, is_active=True)
project_id=OuterRef("id"), member__is_bot=False, is_active=True
)
.order_by() .order_by()
.annotate(count=Func(F("id"), function="Count")) .annotate(count=Func(F("id"), function="Count"))
.values("count") .values("count")

View file

@ -42,12 +42,8 @@ class ProjectAdvanceAnalyticsEndpoint(ProjectAdvanceAnalyticsBaseView):
def get_filtered_count() -> int: def get_filtered_count() -> int:
if self.filters["analytics_date_range"]: if self.filters["analytics_date_range"]:
return queryset.filter( return queryset.filter(
created_at__gte=self.filters["analytics_date_range"]["current"][ created_at__gte=self.filters["analytics_date_range"]["current"]["gte"],
"gte" created_at__lte=self.filters["analytics_date_range"]["current"]["lte"],
],
created_at__lte=self.filters["analytics_date_range"]["current"][
"lte"
],
).count() ).count()
return queryset.count() return queryset.count()
@ -55,42 +51,30 @@ class ProjectAdvanceAnalyticsEndpoint(ProjectAdvanceAnalyticsBaseView):
"count": get_filtered_count(), "count": get_filtered_count(),
} }
def get_work_items_stats( def get_work_items_stats(self, project_id, cycle_id=None, module_id=None) -> Dict[str, Dict[str, int]]:
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. Returns work item stats for the workspace, or filtered by cycle_id or module_id if provided.
""" """
base_queryset = None base_queryset = None
if cycle_id is not None: if cycle_id is not None:
cycle_issues = CycleIssue.objects.filter( cycle_issues = CycleIssue.objects.filter(**self.filters["base_filters"], cycle_id=cycle_id).values_list(
**self.filters["base_filters"], cycle_id=cycle_id "issue_id", flat=True
).values_list("issue_id", flat=True) )
base_queryset = Issue.issue_objects.filter(id__in=cycle_issues) base_queryset = Issue.issue_objects.filter(id__in=cycle_issues)
elif module_id is not None: elif module_id is not None:
module_issues = ModuleIssue.objects.filter( module_issues = ModuleIssue.objects.filter(**self.filters["base_filters"], module_id=module_id).values_list(
**self.filters["base_filters"], module_id=module_id "issue_id", flat=True
).values_list("issue_id", flat=True) )
base_queryset = Issue.issue_objects.filter(id__in=module_issues) base_queryset = Issue.issue_objects.filter(id__in=module_issues)
else: else:
base_queryset = Issue.issue_objects.filter( base_queryset = Issue.issue_objects.filter(**self.filters["base_filters"], project_id=project_id)
**self.filters["base_filters"], project_id=project_id
)
return { return {
"total_work_items": self.get_filtered_counts(base_queryset), "total_work_items": self.get_filtered_counts(base_queryset),
"started_work_items": self.get_filtered_counts( "started_work_items": self.get_filtered_counts(base_queryset.filter(state__group="started")),
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")),
"backlog_work_items": self.get_filtered_counts( "completed_work_items": self.get_filtered_counts(base_queryset.filter(state__group="completed")),
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]) @allow_permission([ROLE.ADMIN, ROLE.MEMBER])
@ -101,9 +85,7 @@ class ProjectAdvanceAnalyticsEndpoint(ProjectAdvanceAnalyticsBaseView):
cycle_id = request.GET.get("cycle_id", None) cycle_id = request.GET.get("cycle_id", None)
module_id = request.GET.get("module_id", None) module_id = request.GET.get("module_id", None)
return Response( return Response(
self.get_work_items_stats( self.get_work_items_stats(cycle_id=cycle_id, module_id=module_id, project_id=project_id),
cycle_id=cycle_id, module_id=module_id, project_id=project_id
),
status=status.HTTP_200_OK, status=status.HTTP_200_OK,
) )
@ -116,9 +98,7 @@ class ProjectAdvanceAnalyticsStatsEndpoint(ProjectAdvanceAnalyticsBaseView):
# Apply date range filter if available # Apply date range filter if available
if self.filters["chart_period_range"]: if self.filters["chart_period_range"]:
start_date, end_date = self.filters["chart_period_range"] start_date, end_date = self.filters["chart_period_range"]
base_queryset = base_queryset.filter( base_queryset = base_queryset.filter(created_at__date__gte=start_date, created_at__date__lte=end_date)
created_at__date__gte=start_date, created_at__date__lte=end_date
)
return ( return (
base_queryset.values("project_id", "project__name") base_queryset.values("project_id", "project__name")
@ -132,24 +112,20 @@ class ProjectAdvanceAnalyticsStatsEndpoint(ProjectAdvanceAnalyticsBaseView):
.order_by("project_id") .order_by("project_id")
) )
def get_work_items_stats( def get_work_items_stats(self, project_id, cycle_id=None, module_id=None) -> Dict[str, Dict[str, int]]:
self, project_id, cycle_id=None, module_id=None
) -> Dict[str, Dict[str, int]]:
base_queryset = None base_queryset = None
if cycle_id is not None: if cycle_id is not None:
cycle_issues = CycleIssue.objects.filter( cycle_issues = CycleIssue.objects.filter(**self.filters["base_filters"], cycle_id=cycle_id).values_list(
**self.filters["base_filters"], cycle_id=cycle_id "issue_id", flat=True
).values_list("issue_id", flat=True) )
base_queryset = Issue.issue_objects.filter(id__in=cycle_issues) base_queryset = Issue.issue_objects.filter(id__in=cycle_issues)
elif module_id is not None: elif module_id is not None:
module_issues = ModuleIssue.objects.filter( module_issues = ModuleIssue.objects.filter(**self.filters["base_filters"], module_id=module_id).values_list(
**self.filters["base_filters"], module_id=module_id "issue_id", flat=True
).values_list("issue_id", flat=True) )
base_queryset = Issue.issue_objects.filter(id__in=module_issues) base_queryset = Issue.issue_objects.filter(id__in=module_issues)
else: else:
base_queryset = Issue.issue_objects.filter( base_queryset = Issue.issue_objects.filter(**self.filters["base_filters"], project_id=project_id)
**self.filters["base_filters"], project_id=project_id
)
return ( return (
base_queryset.annotate(display_name=F("assignees__display_name")) base_queryset.annotate(display_name=F("assignees__display_name"))
.annotate(assignee_id=F("assignees__id")) .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 # If `avatar_asset` is None, fall back to using `avatar` field directly
When( When(assignees__avatar_asset__isnull=True, then="assignees__avatar"),
assignees__avatar_asset__isnull=True, then="assignees__avatar"
),
default=Value(None), default=Value(None),
output_field=models.CharField(), output_field=models.CharField(),
) )
) )
.values("display_name", "assignee_id", "avatar_url") .values("display_name", "assignee_id", "avatar_url")
.annotate( .annotate(
cancelled_work_items=Count( cancelled_work_items=Count("id", filter=Q(state__group="cancelled"), distinct=True),
"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),
completed_work_items=Count( un_started_work_items=Count("id", filter=Q(state__group="unstarted"), distinct=True),
"id", filter=Q(state__group="completed"), distinct=True started_work_items=Count("id", filter=Q(state__group="started"), 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") .order_by("display_name")
) )
@ -204,9 +168,7 @@ class ProjectAdvanceAnalyticsStatsEndpoint(ProjectAdvanceAnalyticsBaseView):
cycle_id = request.GET.get("cycle_id", None) cycle_id = request.GET.get("cycle_id", None)
module_id = request.GET.get("module_id", None) module_id = request.GET.get("module_id", None)
return Response( return Response(
self.get_work_items_stats( self.get_work_items_stats(project_id=project_id, cycle_id=cycle_id, module_id=module_id),
project_id=project_id, cycle_id=cycle_id, module_id=module_id
),
status=status.HTTP_200_OK, status=status.HTTP_200_OK,
) )
@ -214,23 +176,19 @@ class ProjectAdvanceAnalyticsStatsEndpoint(ProjectAdvanceAnalyticsBaseView):
class ProjectAdvanceAnalyticsChartEndpoint(ProjectAdvanceAnalyticsBaseView): class ProjectAdvanceAnalyticsChartEndpoint(ProjectAdvanceAnalyticsBaseView):
def work_item_completion_chart( def work_item_completion_chart(self, project_id, cycle_id=None, module_id=None) -> Dict[str, Any]:
self, project_id, cycle_id=None, module_id=None
) -> Dict[str, Any]:
# Get the base queryset # Get the base queryset
queryset = ( queryset = (
Issue.issue_objects.filter(**self.filters["base_filters"]) Issue.issue_objects.filter(**self.filters["base_filters"])
.filter(project_id=project_id) .filter(project_id=project_id)
.select_related("workspace", "state", "parent") .select_related("workspace", "state", "parent")
.prefetch_related( .prefetch_related("assignees", "labels", "issue_module__module", "issue_cycle__cycle")
"assignees", "labels", "issue_module__module", "issue_cycle__cycle"
)
) )
if cycle_id is not None: if cycle_id is not None:
cycle_issues = CycleIssue.objects.filter( cycle_issues = CycleIssue.objects.filter(**self.filters["base_filters"], cycle_id=cycle_id).values_list(
**self.filters["base_filters"], cycle_id=cycle_id "issue_id", flat=True
).values_list("issue_id", flat=True) )
cycle = Cycle.objects.filter(id=cycle_id).first() cycle = Cycle.objects.filter(id=cycle_id).first()
if cycle and cycle.start_date: if cycle and cycle.start_date:
start_date = cycle.start_date.date() start_date = cycle.start_date.date()
@ -240,9 +198,9 @@ class ProjectAdvanceAnalyticsChartEndpoint(ProjectAdvanceAnalyticsBaseView):
queryset = cycle_issues queryset = cycle_issues
elif module_id is not None: elif module_id is not None:
module_issues = ModuleIssue.objects.filter( module_issues = ModuleIssue.objects.filter(**self.filters["base_filters"], module_id=module_id).values_list(
**self.filters["base_filters"], module_id=module_id "issue_id", flat=True
).values_list("issue_id", flat=True) )
module = Module.objects.filter(id=module_id).first() module = Module.objects.filter(id=module_id).first()
if module and module.start_date: if module and module.start_date:
start_date = module.start_date start_date = module.start_date
@ -264,9 +222,7 @@ class ProjectAdvanceAnalyticsChartEndpoint(ProjectAdvanceAnalyticsBaseView):
queryset.values("created_at__date") queryset.values("created_at__date")
.annotate( .annotate(
created_count=Count("id"), created_count=Count("id"),
completed_count=Count( completed_count=Count("id", filter=Q(issue__state__group="completed")),
"id", filter=Q(issue__state__group="completed")
),
) )
.order_by("created_at__date") .order_by("created_at__date")
) )
@ -285,9 +241,7 @@ class ProjectAdvanceAnalyticsChartEndpoint(ProjectAdvanceAnalyticsBaseView):
current_date = start_date current_date = start_date
while current_date <= end_date: while current_date <= end_date:
date_str = current_date.strftime("%Y-%m-%d") date_str = current_date.strftime("%Y-%m-%d")
stats = stats_dict.get( stats = stats_dict.get(date_str, {"created_count": 0, "completed_count": 0})
date_str, {"created_count": 0, "completed_count": 0}
)
data.append( data.append(
{ {
"key": date_str, "key": date_str,
@ -302,9 +256,7 @@ class ProjectAdvanceAnalyticsChartEndpoint(ProjectAdvanceAnalyticsBaseView):
# Apply date range filter if available # Apply date range filter if available
if self.filters["chart_period_range"]: if self.filters["chart_period_range"]:
start_date, end_date = self.filters["chart_period_range"] start_date, end_date = self.filters["chart_period_range"]
queryset = queryset.filter( queryset = queryset.filter(created_at__date__gte=start_date, created_at__date__lte=end_date)
created_at__date__gte=start_date, created_at__date__lte=end_date
)
# Annotate by month and count # Annotate by month and count
monthly_stats = ( monthly_stats = (
@ -335,9 +287,7 @@ class ProjectAdvanceAnalyticsChartEndpoint(ProjectAdvanceAnalyticsBaseView):
while current_month <= last_month: while current_month <= last_month:
date_str = current_month.strftime("%Y-%m-%d") date_str = current_month.strftime("%Y-%m-%d")
stats = stats_dict.get( stats = stats_dict.get(date_str, {"created_count": 0, "completed_count": 0})
date_str, {"created_count": 0, "completed_count": 0}
)
data.append( data.append(
{ {
"key": date_str, "key": date_str,
@ -349,9 +299,7 @@ class ProjectAdvanceAnalyticsChartEndpoint(ProjectAdvanceAnalyticsBaseView):
) )
# Move to next month # Move to next month
if current_month.month == 12: if current_month.month == 12:
current_month = current_month.replace( current_month = current_month.replace(year=current_month.year + 1, month=1)
year=current_month.year + 1, month=1
)
else: else:
current_month = current_month.replace(month=current_month.month + 1) 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"]) Issue.issue_objects.filter(**self.filters["base_filters"])
.filter(project_id=project_id) .filter(project_id=project_id)
.select_related("workspace", "state", "parent") .select_related("workspace", "state", "parent")
.prefetch_related( .prefetch_related("assignees", "labels", "issue_module__module", "issue_cycle__cycle")
"assignees", "labels", "issue_module__module", "issue_cycle__cycle"
)
) )
# Apply cycle/module filters if present # Apply cycle/module filters if present
if cycle_id is not None: if cycle_id is not None:
cycle_issues = CycleIssue.objects.filter( cycle_issues = CycleIssue.objects.filter(**self.filters["base_filters"], cycle_id=cycle_id).values_list(
**self.filters["base_filters"], cycle_id=cycle_id "issue_id", flat=True
).values_list("issue_id", flat=True) )
queryset = queryset.filter(id__in=cycle_issues) queryset = queryset.filter(id__in=cycle_issues)
elif module_id is not None: elif module_id is not None:
@ -397,9 +343,7 @@ class ProjectAdvanceAnalyticsChartEndpoint(ProjectAdvanceAnalyticsBaseView):
# Apply date range filter if available # Apply date range filter if available
if self.filters["chart_period_range"]: if self.filters["chart_period_range"]:
start_date, end_date = self.filters["chart_period_range"] start_date, end_date = self.filters["chart_period_range"]
queryset = queryset.filter( queryset = queryset.filter(created_at__date__gte=start_date, created_at__date__lte=end_date)
created_at__date__gte=start_date, created_at__date__lte=end_date
)
return Response( return Response(
build_analytics_chart(queryset, x_axis, group_by), build_analytics_chart(queryset, x_axis, group_by),
@ -412,9 +356,7 @@ class ProjectAdvanceAnalyticsChartEndpoint(ProjectAdvanceAnalyticsBaseView):
module_id = request.GET.get("module_id", None) module_id = request.GET.get("module_id", None)
return Response( return Response(
self.work_item_completion_chart( self.work_item_completion_chart(project_id=project_id, cycle_id=cycle_id, module_id=module_id),
project_id=project_id, cycle_id=cycle_id, module_id=module_id
),
status=status.HTTP_200_OK, status=status.HTTP_200_OK,
) )

View file

@ -65,9 +65,7 @@ class ServiceApiTokenEndpoint(BaseAPIView):
def post(self, request: Request, slug: str) -> Response: def post(self, request: Request, slug: str) -> Response:
workspace = Workspace.objects.get(slug=slug) workspace = Workspace.objects.get(slug=slug)
api_token = APIToken.objects.filter( api_token = APIToken.objects.filter(workspace=workspace, is_service=True).first()
workspace=workspace, is_service=True
).first()
if api_token: if api_token:
return Response({"token": str(api_token.token)}, status=status.HTTP_200_OK) return Response({"token": str(api_token.token)}, status=status.HTTP_200_OK)
@ -83,6 +81,4 @@ class ServiceApiTokenEndpoint(BaseAPIView):
user_type=user_type, user_type=user_type,
is_service=True, is_service=True,
) )
return Response( return Response({"token": str(api_token.token)}, status=status.HTTP_201_CREATED)
{"token": str(api_token.token)}, status=status.HTTP_201_CREATED
)

View file

@ -20,12 +20,8 @@ class FileAssetEndpoint(BaseAPIView):
asset_key = str(workspace_id) + "/" + asset_key asset_key = str(workspace_id) + "/" + asset_key
files = FileAsset.objects.filter(asset=asset_key) files = FileAsset.objects.filter(asset=asset_key)
if files.exists(): if files.exists():
serializer = FileAssetSerializer( serializer = FileAssetSerializer(files, context={"request": request}, many=True)
files, context={"request": request}, many=True 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: else:
return Response( return Response(
{"error": "Asset key does not exist", "status": False}, {"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) files = FileAsset.objects.filter(asset=asset_key, created_by=request.user)
if files.exists(): if files.exists():
serializer = FileAssetSerializer(files, context={"request": request}) serializer = FileAssetSerializer(files, context={"request": request})
return Response( return Response({"data": serializer.data, "status": True}, status=status.HTTP_200_OK)
{"data": serializer.data, "status": True}, status=status.HTTP_200_OK
)
else: else:
return Response( return Response(
{"error": "Asset key does not exist", "status": False}, {"error": "Asset key does not exist", "status": False},

View file

@ -44,9 +44,7 @@ class UserAssetsV2Endpoint(BaseAPIView):
# Save the new avatar # Save the new avatar
user.avatar_asset_id = asset_id user.avatar_asset_id = asset_id
user.save() user.save()
invalidate_cache_directly( invalidate_cache_directly(path="/api/users/me/", url_params=False, user=True, request=request)
path="/api/users/me/", url_params=False, user=True, request=request
)
invalidate_cache_directly( invalidate_cache_directly(
path="/api/users/me/settings/", path="/api/users/me/settings/",
url_params=False, url_params=False,
@ -64,9 +62,7 @@ class UserAssetsV2Endpoint(BaseAPIView):
# Save the new cover image # Save the new cover image
user.cover_image_asset_id = asset_id user.cover_image_asset_id = asset_id
user.save() user.save()
invalidate_cache_directly( invalidate_cache_directly(path="/api/users/me/", url_params=False, user=True, request=request)
path="/api/users/me/", url_params=False, user=True, request=request
)
invalidate_cache_directly( invalidate_cache_directly(
path="/api/users/me/settings/", path="/api/users/me/settings/",
url_params=False, url_params=False,
@ -82,9 +78,7 @@ class UserAssetsV2Endpoint(BaseAPIView):
user = User.objects.get(id=asset.user_id) user = User.objects.get(id=asset.user_id)
user.avatar_asset_id = None user.avatar_asset_id = None
user.save() user.save()
invalidate_cache_directly( invalidate_cache_directly(path="/api/users/me/", url_params=False, user=True, request=request)
path="/api/users/me/", url_params=False, user=True, request=request
)
invalidate_cache_directly( invalidate_cache_directly(
path="/api/users/me/settings/", path="/api/users/me/settings/",
url_params=False, url_params=False,
@ -97,9 +91,7 @@ class UserAssetsV2Endpoint(BaseAPIView):
user = User.objects.get(id=asset.user_id) user = User.objects.get(id=asset.user_id)
user.cover_image_asset_id = None user.cover_image_asset_id = None
user.save() user.save()
invalidate_cache_directly( invalidate_cache_directly(path="/api/users/me/", url_params=False, user=True, request=request)
path="/api/users/me/", url_params=False, user=True, request=request
)
invalidate_cache_directly( invalidate_cache_directly(
path="/api/users/me/settings/", path="/api/users/me/settings/",
url_params=False, url_params=False,
@ -159,9 +151,7 @@ class UserAssetsV2Endpoint(BaseAPIView):
# Get the presigned URL # Get the presigned URL
storage = S3Storage(request=request) storage = S3Storage(request=request)
# Generate a presigned URL to share an S3 object # Generate a presigned URL to share an S3 object
presigned_url = storage.generate_presigned_post( presigned_url = storage.generate_presigned_post(object_name=asset_key, file_type=type, file_size=size_limit)
object_name=asset_key, file_type=type, file_size=size_limit
)
# Return the presigned URL # Return the presigned URL
return Response( return Response(
{ {
@ -198,9 +188,7 @@ class UserAssetsV2Endpoint(BaseAPIView):
asset.is_deleted = True asset.is_deleted = True
asset.deleted_at = timezone.now() asset.deleted_at = timezone.now()
# get the entity and save the asset id for the request field # get the entity and save the asset id for the request field
self.entity_asset_delete( self.entity_asset_delete(entity_type=asset.entity_type, asset=asset, request=request)
entity_type=asset.entity_type, asset=asset, request=request
)
asset.save(update_fields=["is_deleted", "deleted_at"]) asset.save(update_fields=["is_deleted", "deleted_at"])
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
@ -264,18 +252,14 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
workspace.logo = "" workspace.logo = ""
workspace.logo_asset_id = asset_id workspace.logo_asset_id = asset_id
workspace.save() workspace.save()
invalidate_cache_directly( invalidate_cache_directly(path="/api/workspaces/", url_params=False, user=False, request=request)
path="/api/workspaces/", url_params=False, user=False, request=request
)
invalidate_cache_directly( invalidate_cache_directly(
path="/api/users/me/workspaces/", path="/api/users/me/workspaces/",
url_params=False, url_params=False,
user=True, user=True,
request=request, request=request,
) )
invalidate_cache_directly( invalidate_cache_directly(path="/api/instances/", url_params=False, user=False, request=request)
path="/api/instances/", url_params=False, user=False, request=request
)
return return
# Project Cover # Project Cover
@ -302,18 +286,14 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
return return
workspace.logo_asset_id = None workspace.logo_asset_id = None
workspace.save() workspace.save()
invalidate_cache_directly( invalidate_cache_directly(path="/api/workspaces/", url_params=False, user=False, request=request)
path="/api/workspaces/", url_params=False, user=False, request=request
)
invalidate_cache_directly( invalidate_cache_directly(
path="/api/users/me/workspaces/", path="/api/users/me/workspaces/",
url_params=False, url_params=False,
user=True, user=True,
request=request, request=request,
) )
invalidate_cache_directly( invalidate_cache_directly(path="/api/instances/", url_params=False, user=False, request=request)
path="/api/instances/", url_params=False, user=False, request=request
)
return return
# Project Cover # Project Cover
elif entity_type == FileAsset.EntityTypeContext.PROJECT_COVER: elif entity_type == FileAsset.EntityTypeContext.PROJECT_COVER:
@ -374,17 +354,13 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
workspace=workspace, workspace=workspace,
created_by=request.user, created_by=request.user,
entity_type=entity_type, entity_type=entity_type,
**self.get_entity_id_field( **self.get_entity_id_field(entity_type=entity_type, entity_id=entity_identifier),
entity_type=entity_type, entity_id=entity_identifier
),
) )
# Get the presigned URL # Get the presigned URL
storage = S3Storage(request=request) storage = S3Storage(request=request)
# Generate a presigned URL to share an S3 object # Generate a presigned URL to share an S3 object
presigned_url = storage.generate_presigned_post( presigned_url = storage.generate_presigned_post(object_name=asset_key, file_type=type, file_size=size_limit)
object_name=asset_key, file_type=type, file_size=size_limit
)
# Return the presigned URL # Return the presigned URL
return Response( return Response(
{ {
@ -421,9 +397,7 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
asset.is_deleted = True asset.is_deleted = True
asset.deleted_at = timezone.now() asset.deleted_at = timezone.now()
# get the entity and save the asset id for the request field # get the entity and save the asset id for the request field
self.entity_asset_delete( self.entity_asset_delete(entity_type=asset.entity_type, asset=asset, request=request)
entity_type=asset.entity_type, asset=asset, request=request
)
asset.save(update_fields=["is_deleted", "deleted_at"]) asset.save(update_fields=["is_deleted", "deleted_at"])
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
@ -586,9 +560,7 @@ class ProjectAssetEndpoint(BaseAPIView):
# Get the presigned URL # Get the presigned URL
storage = S3Storage(request=request) storage = S3Storage(request=request)
# Generate a presigned URL to share an S3 object # Generate a presigned URL to share an S3 object
presigned_url = storage.generate_presigned_post( presigned_url = storage.generate_presigned_post(object_name=asset_key, file_type=type, file_size=size_limit)
object_name=asset_key, file_type=type, file_size=size_limit
)
# Return the presigned URL # Return the presigned URL
return Response( return Response(
{ {
@ -618,9 +590,7 @@ class ProjectAssetEndpoint(BaseAPIView):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def delete(self, request, slug, project_id, pk): def delete(self, request, slug, project_id, pk):
# Get the asset # Get the asset
asset = FileAsset.objects.get( asset = FileAsset.objects.get(id=pk, workspace__slug=slug, project_id=project_id)
id=pk, workspace__slug=slug, project_id=project_id
)
# Check deleted assets # Check deleted assets
asset.is_deleted = True asset.is_deleted = True
asset.deleted_at = timezone.now() asset.deleted_at = timezone.now()
@ -631,9 +601,7 @@ class ProjectAssetEndpoint(BaseAPIView):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def get(self, request, slug, project_id, pk): def get(self, request, slug, project_id, pk):
# get the asset id # get the asset id
asset = FileAsset.objects.get( asset = FileAsset.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
workspace__slug=slug, project_id=project_id, pk=pk
)
# Check if the asset is uploaded # Check if the asset is uploaded
if not asset.is_uploaded: if not asset.is_uploaded:
@ -666,9 +634,7 @@ class ProjectBulkAssetEndpoint(BaseAPIView):
# Check if the asset ids are provided # Check if the asset ids are provided
if not asset_ids: if not asset_ids:
return Response( return Response({"error": "No asset ids provided."}, status=status.HTTP_400_BAD_REQUEST)
{"error": "No asset ids provided."}, status=status.HTTP_400_BAD_REQUEST
)
# get the asset id # get the asset id
assets = FileAsset.objects.filter(id__in=asset_ids, workspace__slug=slug) assets = FileAsset.objects.filter(id__in=asset_ids, workspace__slug=slug)
@ -722,9 +688,7 @@ class AssetCheckEndpoint(BaseAPIView):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE") @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
def get(self, request, slug, asset_id): def get(self, request, slug, asset_id):
asset = FileAsset.all_objects.filter( asset = FileAsset.all_objects.filter(id=asset_id, workspace__slug=slug, deleted_at__isnull=True).exists()
id=asset_id, workspace__slug=slug, deleted_at__isnull=True
).exists()
return Response({"exists": asset}, status=status.HTTP_200_OK) return Response({"exists": asset}, status=status.HTTP_200_OK)

View file

@ -72,11 +72,7 @@ class BaseViewSet(TimezoneMixin, ReadReplicaControlMixin, ModelViewSet, BasePagi
response = super().handle_exception(exc) response = super().handle_exception(exc)
return response return response
except Exception as e: 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): if isinstance(e, IntegrityError):
return Response( return Response(
{"error": "The payload is not valid"}, {"error": "The payload is not valid"},
@ -115,9 +111,7 @@ class BaseViewSet(TimezoneMixin, ReadReplicaControlMixin, ModelViewSet, BasePagi
if settings.DEBUG: if settings.DEBUG:
from django.db import connection from django.db import connection
print( print(f"{request.method} - {request.get_full_path()} of Queries: {len(connection.queries)}")
f"{request.method} - {request.get_full_path()} of Queries: {len(connection.queries)}"
)
return response return response
except Exception as exc: except Exception as exc:
@ -139,16 +133,12 @@ class BaseViewSet(TimezoneMixin, ReadReplicaControlMixin, ModelViewSet, BasePagi
@property @property
def fields(self): def fields(self):
fields = [ fields = [field for field in self.request.GET.get("fields", "").split(",") if field]
field for field in self.request.GET.get("fields", "").split(",") if field
]
return fields if fields else None return fields if fields else None
@property @property
def expand(self): def expand(self):
expand = [ expand = [expand for expand in self.request.GET.get("expand", "").split(",") if expand]
expand for expand in self.request.GET.get("expand", "").split(",") if expand
]
return expand if expand else None return expand if expand else None
@ -216,9 +206,7 @@ class BaseAPIView(TimezoneMixin, ReadReplicaControlMixin, APIView, BasePaginator
if settings.DEBUG: if settings.DEBUG:
from django.db import connection from django.db import connection
print( print(f"{request.method} - {request.get_full_path()} of Queries: {len(connection.queries)}")
f"{request.method} - {request.get_full_path()} of Queries: {len(connection.queries)}"
)
return response return response
except Exception as exc: except Exception as exc:
@ -235,14 +223,10 @@ class BaseAPIView(TimezoneMixin, ReadReplicaControlMixin, APIView, BasePaginator
@property @property
def fields(self): def fields(self):
fields = [ fields = [field for field in self.request.GET.get("fields", "").split(",") if field]
field for field in self.request.GET.get("fields", "").split(",") if field
]
return fields if fields else None return fields if fields else None
@property @property
def expand(self): def expand(self):
expand = [ expand = [expand for expand in self.request.GET.get("expand", "").split(",") if expand]
expand for expand in self.request.GET.get("expand", "").split(",") if expand
]
return expand if expand else None return expand if expand else None

View file

@ -50,9 +50,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
issue_cycle__deleted_at__isnull=True, issue_cycle__deleted_at__isnull=True,
) )
.values("issue_cycle__cycle_id") .values("issue_cycle__cycle_id")
.annotate( .annotate(backlog_estimate_point=Sum(Cast("estimate_point__value", FloatField())))
backlog_estimate_point=Sum(Cast("estimate_point__value", FloatField()))
)
.values("backlog_estimate_point")[:1] .values("backlog_estimate_point")[:1]
) )
unstarted_estimate_point = ( unstarted_estimate_point = (
@ -63,11 +61,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
issue_cycle__deleted_at__isnull=True, issue_cycle__deleted_at__isnull=True,
) )
.values("issue_cycle__cycle_id") .values("issue_cycle__cycle_id")
.annotate( .annotate(unstarted_estimate_point=Sum(Cast("estimate_point__value", FloatField())))
unstarted_estimate_point=Sum(
Cast("estimate_point__value", FloatField())
)
)
.values("unstarted_estimate_point")[:1] .values("unstarted_estimate_point")[:1]
) )
started_estimate_point = ( started_estimate_point = (
@ -78,9 +72,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
issue_cycle__deleted_at__isnull=True, issue_cycle__deleted_at__isnull=True,
) )
.values("issue_cycle__cycle_id") .values("issue_cycle__cycle_id")
.annotate( .annotate(started_estimate_point=Sum(Cast("estimate_point__value", FloatField())))
started_estimate_point=Sum(Cast("estimate_point__value", FloatField()))
)
.values("started_estimate_point")[:1] .values("started_estimate_point")[:1]
) )
cancelled_estimate_point = ( cancelled_estimate_point = (
@ -91,11 +83,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
issue_cycle__deleted_at__isnull=True, issue_cycle__deleted_at__isnull=True,
) )
.values("issue_cycle__cycle_id") .values("issue_cycle__cycle_id")
.annotate( .annotate(cancelled_estimate_point=Sum(Cast("estimate_point__value", FloatField())))
cancelled_estimate_point=Sum(
Cast("estimate_point__value", FloatField())
)
)
.values("cancelled_estimate_point")[:1] .values("cancelled_estimate_point")[:1]
) )
completed_estimate_point = ( completed_estimate_point = (
@ -106,11 +94,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
issue_cycle__deleted_at__isnull=True, issue_cycle__deleted_at__isnull=True,
) )
.values("issue_cycle__cycle_id") .values("issue_cycle__cycle_id")
.annotate( .annotate(completed_estimate_points=Sum(Cast("estimate_point__value", FloatField())))
completed_estimate_points=Sum(
Cast("estimate_point__value", FloatField())
)
)
.values("completed_estimate_points")[:1] .values("completed_estimate_points")[:1]
) )
total_estimate_point = ( total_estimate_point = (
@ -120,9 +104,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
issue_cycle__deleted_at__isnull=True, issue_cycle__deleted_at__isnull=True,
) )
.values("issue_cycle__cycle_id") .values("issue_cycle__cycle_id")
.annotate( .annotate(total_estimate_points=Sum(Cast("estimate_point__value", FloatField())))
total_estimate_points=Sum(Cast("estimate_point__value", FloatField()))
)
.values("total_estimate_points")[:1] .values("total_estimate_points")[:1]
) )
return ( return (
@ -138,9 +120,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
.prefetch_related( .prefetch_related(
Prefetch( Prefetch(
"issue_cycle__issue__assignees", "issue_cycle__issue__assignees",
queryset=User.objects.only( queryset=User.objects.only("avatar_asset", "first_name", "id").distinct(),
"avatar_asset", "first_name", "id"
).distinct(),
) )
) )
.prefetch_related( .prefetch_related(
@ -224,8 +204,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
.annotate( .annotate(
status=Case( status=Case(
When( When(
Q(start_date__lte=timezone.now()) Q(start_date__lte=timezone.now()) & Q(end_date__gte=timezone.now()),
& Q(end_date__gte=timezone.now()),
then=Value("CURRENT"), then=Value("CURRENT"),
), ),
When(start_date__gt=timezone.now(), then=Value("UPCOMING")), When(start_date__gt=timezone.now(), then=Value("UPCOMING")),
@ -279,9 +258,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
) )
) )
.annotate( .annotate(
total_estimate_points=Coalesce( total_estimate_points=Coalesce(Subquery(total_estimate_point), Value(0, output_field=FloatField()))
Subquery(total_estimate_point), Value(0, output_field=FloatField())
)
) )
.order_by("-is_favorite", "name") .order_by("-is_favorite", "name")
.distinct() .distinct()
@ -322,9 +299,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
).order_by("-is_favorite", "-created_at") ).order_by("-is_favorite", "-created_at")
return Response(queryset, status=status.HTTP_200_OK) return Response(queryset, status=status.HTTP_200_OK)
else: else:
queryset = ( queryset = self.get_queryset().filter(archived_at__isnull=False).filter(pk=pk)
self.get_queryset().filter(archived_at__isnull=False).filter(pk=pk)
)
data = ( data = (
self.get_queryset() self.get_queryset()
.filter(pk=pk) .filter(pk=pk)
@ -415,9 +390,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
) )
) )
.values("display_name", "assignee_id", "avatar_url") .values("display_name", "assignee_id", "avatar_url")
.annotate( .annotate(total_estimates=Sum(Cast("estimate_point__value", FloatField())))
total_estimates=Sum(Cast("estimate_point__value", FloatField()))
)
.annotate( .annotate(
completed_estimates=Sum( completed_estimates=Sum(
Cast("estimate_point__value", FloatField()), Cast("estimate_point__value", FloatField()),
@ -452,9 +425,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
.annotate(color=F("labels__color")) .annotate(color=F("labels__color"))
.annotate(label_id=F("labels__id")) .annotate(label_id=F("labels__id"))
.values("label_name", "color", "label_id") .values("label_name", "color", "label_id")
.annotate( .annotate(total_estimates=Sum(Cast("estimate_point__value", FloatField())))
total_estimates=Sum(Cast("estimate_point__value", FloatField()))
)
.annotate( .annotate(
completed_estimates=Sum( completed_estimates=Sum(
Cast("estimate_point__value", FloatField()), Cast("estimate_point__value", FloatField()),
@ -531,11 +502,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
"avatar_url", "avatar_url",
"display_name", "display_name",
) )
.annotate( .annotate(total_issues=Count("id", filter=Q(archived_at__isnull=True, is_draft=False)))
total_issues=Count(
"id", filter=Q(archived_at__isnull=True, is_draft=False)
)
)
.annotate( .annotate(
completed_issues=Count( completed_issues=Count(
"id", "id",
@ -571,11 +538,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
.annotate(color=F("labels__color")) .annotate(color=F("labels__color"))
.annotate(label_id=F("labels__id")) .annotate(label_id=F("labels__id"))
.values("label_name", "color", "label_id") .values("label_name", "color", "label_id")
.annotate( .annotate(total_issues=Count("id", filter=Q(archived_at__isnull=True, is_draft=False)))
total_issues=Count(
"id", filter=Q(archived_at__isnull=True, is_draft=False)
)
)
.annotate( .annotate(
completed_issues=Count( completed_issues=Count(
"id", "id",
@ -618,9 +581,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER]) @allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def post(self, request, slug, project_id, cycle_id): def post(self, request, slug, project_id, cycle_id):
cycle = Cycle.objects.get( cycle = Cycle.objects.get(pk=cycle_id, project_id=project_id, workspace__slug=slug)
pk=cycle_id, project_id=project_id, workspace__slug=slug
)
if cycle.end_date >= timezone.now(): if cycle.end_date >= timezone.now():
return Response( return Response(
@ -636,15 +597,11 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
project_id=project_id, project_id=project_id,
workspace__slug=slug, workspace__slug=slug,
).delete() ).delete()
return Response( return Response({"archived_at": str(cycle.archived_at)}, status=status.HTTP_200_OK)
{"archived_at": str(cycle.archived_at)}, status=status.HTTP_200_OK
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER]) @allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def delete(self, request, slug, project_id, cycle_id): def delete(self, request, slug, project_id, cycle_id):
cycle = Cycle.objects.get( cycle = Cycle.objects.get(pk=cycle_id, project_id=project_id, workspace__slug=slug)
pk=cycle_id, project_id=project_id, workspace__slug=slug
)
cycle.archived_at = None cycle.archived_at = None
cycle.save() cycle.save()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)

View file

@ -46,7 +46,6 @@ from plane.db.models import (
Label, Label,
User, User,
Project, Project,
ProjectMember,
UserRecentVisit, UserRecentVisit,
) )
from plane.utils.analytics_plot import burndown_plot from plane.utils.analytics_plot import burndown_plot
@ -97,9 +96,7 @@ class CycleViewSet(BaseViewSet):
.prefetch_related( .prefetch_related(
Prefetch( Prefetch(
"issue_cycle__issue__assignees", "issue_cycle__issue__assignees",
queryset=User.objects.only( queryset=User.objects.only("avatar_asset", "first_name", "id").distinct(),
"avatar_asset", "first_name", "id"
).distinct(),
) )
) )
.prefetch_related( .prefetch_related(
@ -150,8 +147,7 @@ class CycleViewSet(BaseViewSet):
.annotate( .annotate(
status=Case( status=Case(
When( When(
Q(start_date__lte=current_time_in_utc) Q(start_date__lte=current_time_in_utc) & Q(end_date__gte=current_time_in_utc),
& Q(end_date__gte=current_time_in_utc),
then=Value("CURRENT"), then=Value("CURRENT"),
), ),
When(start_date__gt=current_time_in_utc, then=Value("UPCOMING")), When(start_date__gt=current_time_in_utc, then=Value("UPCOMING")),
@ -170,11 +166,7 @@ class CycleViewSet(BaseViewSet):
"issue_cycle__issue__assignees__id", "issue_cycle__issue__assignees__id",
distinct=True, distinct=True,
filter=~Q(issue_cycle__issue__assignees__id__isnull=True) filter=~Q(issue_cycle__issue__assignees__id__isnull=True)
& ( & (Q(issue_cycle__issue__issue_assignee__deleted_at__isnull=True)),
Q(
issue_cycle__issue__issue_assignee__deleted_at__isnull=True
)
),
), ),
Value([], output_field=ArrayField(UUIDField())), Value([], output_field=ArrayField(UUIDField())),
) )
@ -205,9 +197,7 @@ class CycleViewSet(BaseViewSet):
# Current Cycle # Current Cycle
if cycle_view == "current": if cycle_view == "current":
queryset = queryset.filter( queryset = queryset.filter(start_date__lte=current_time_in_utc, end_date__gte=current_time_in_utc)
start_date__lte=current_time_in_utc, end_date__gte=current_time_in_utc
)
data = queryset.values( data = queryset.values(
# necessary fields # necessary fields
@ -274,16 +264,10 @@ class CycleViewSet(BaseViewSet):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER]) @allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def create(self, request, slug, project_id): def create(self, request, slug, project_id):
if ( 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 None request.data.get("start_date", None) is not None and request.data.get("end_date", None) is not 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( serializer = CycleWriteSerializer(data=request.data, context={"project_id": project_id})
data=request.data, context={"project_id": project_id}
)
if serializer.is_valid(): if serializer.is_valid():
serializer.save(project_id=project_id, owned_by=request.user) serializer.save(project_id=project_id, owned_by=request.user)
cycle = ( cycle = (
@ -323,9 +307,7 @@ class CycleViewSet(BaseViewSet):
project_timezone = project.timezone project_timezone = project.timezone
datetime_fields = ["start_date", "end_date"] datetime_fields = ["start_date", "end_date"]
cycle = user_timezone_converter( cycle = user_timezone_converter(cycle, datetime_fields, project_timezone)
cycle, datetime_fields, project_timezone
)
# Send the model activity # Send the model activity
model_activity.delay( model_activity.delay(
@ -341,17 +323,13 @@ class CycleViewSet(BaseViewSet):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
else: else:
return Response( 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, status=status.HTTP_400_BAD_REQUEST,
) )
@allow_permission([ROLE.ADMIN, ROLE.MEMBER]) @allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def partial_update(self, request, slug, project_id, pk): def partial_update(self, request, slug, project_id, pk):
queryset = self.get_queryset().filter( queryset = self.get_queryset().filter(workspace__slug=slug, project_id=project_id, pk=pk)
workspace__slug=slug, project_id=project_id, pk=pk
)
cycle = queryset.first() cycle = queryset.first()
if cycle.archived_at: if cycle.archived_at:
return Response( return Response(
@ -359,29 +337,21 @@ class CycleViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
current_instance = json.dumps( current_instance = json.dumps(CycleSerializer(cycle).data, cls=DjangoJSONEncoder)
CycleSerializer(cycle).data, cls=DjangoJSONEncoder
)
request_data = request.data request_data = request.data
if cycle.end_date is not None and cycle.end_date < timezone.now(): if cycle.end_date is not None and cycle.end_date < timezone.now():
if "sort_order" in request_data: if "sort_order" in request_data:
# Can only change sort order for a completed cycle`` # Can only change sort order for a completed cycle``
request_data = { request_data = {"sort_order": request_data.get("sort_order", cycle.sort_order)}
"sort_order": request_data.get("sort_order", cycle.sort_order)
}
else: else:
return Response( 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, status=status.HTTP_400_BAD_REQUEST,
) )
serializer = CycleWriteSerializer( serializer = CycleWriteSerializer(cycle, data=request.data, partial=True, context={"project_id": project_id})
cycle, data=request.data, partial=True, context={"project_id": project_id}
)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
cycle = queryset.values( cycle = queryset.values(
@ -481,9 +451,7 @@ class CycleViewSet(BaseViewSet):
) )
if data is None: if data is None:
return Response( return Response({"error": "Cycle not found"}, status=status.HTTP_404_NOT_FOUND)
{"error": "Cycle not found"}, status=status.HTTP_404_NOT_FOUND
)
queryset = queryset.first() queryset = queryset.first()
# Fetch the project timezone # Fetch the project timezone
@ -505,11 +473,7 @@ class CycleViewSet(BaseViewSet):
def destroy(self, request, slug, project_id, pk): def destroy(self, request, slug, project_id, pk):
cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk) cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
cycle_issues = list( cycle_issues = list(CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list("issue", flat=True))
CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list(
"issue", flat=True
)
)
issue_activity.delay( issue_activity.delay(
type="cycle.activity.deleted", type="cycle.activity.deleted",
@ -560,9 +524,7 @@ class CycleDateCheckEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
start_date = convert_to_utc( start_date = convert_to_utc(date=str(start_date), project_id=project_id, is_start_date=True)
date=str(start_date), project_id=project_id, is_start_date=True
)
end_date = convert_to_utc( end_date = convert_to_utc(
date=str(end_date), date=str(end_date),
project_id=project_id, project_id=project_id,
@ -581,7 +543,7 @@ class CycleDateCheckEndpoint(BaseAPIView):
if cycles.exists(): if cycles.exists():
return Response( 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, "status": False,
} }
) )
@ -635,14 +597,10 @@ class TransferCycleIssueEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
new_cycle = Cycle.objects.filter( new_cycle = Cycle.objects.filter(workspace__slug=slug, project_id=project_id, pk=new_cycle_id).first()
workspace__slug=slug, project_id=project_id, pk=new_cycle_id
).first()
old_cycle = ( old_cycle = (
Cycle.objects.filter( Cycle.objects.filter(workspace__slug=slug, project_id=project_id, pk=cycle_id)
workspace__slug=slug, project_id=project_id, pk=cycle_id
)
.annotate( .annotate(
total_issues=Count( total_issues=Count(
"issue_cycle", "issue_cycle",
@ -755,9 +713,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
) )
) )
.values("display_name", "assignee_id", "avatar_url") .values("display_name", "assignee_id", "avatar_url")
.annotate( .annotate(total_estimates=Sum(Cast("estimate_point__value", FloatField())))
total_estimates=Sum(Cast("estimate_point__value", FloatField()))
)
.annotate( .annotate(
completed_estimates=Sum( completed_estimates=Sum(
Cast("estimate_point__value", FloatField()), Cast("estimate_point__value", FloatField()),
@ -784,9 +740,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
assignee_estimate_distribution = [ assignee_estimate_distribution = [
{ {
"display_name": item["display_name"], "display_name": item["display_name"],
"assignee_id": ( "assignee_id": (str(item["assignee_id"]) if item["assignee_id"] else None),
str(item["assignee_id"]) if item["assignee_id"] else None
),
"avatar": item.get("avatar"), "avatar": item.get("avatar"),
"avatar_url": item.get("avatar_url"), "avatar_url": item.get("avatar_url"),
"total_estimates": item["total_estimates"], "total_estimates": item["total_estimates"],
@ -807,9 +761,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
.annotate(color=F("labels__color")) .annotate(color=F("labels__color"))
.annotate(label_id=F("labels__id")) .annotate(label_id=F("labels__id"))
.values("label_name", "color", "label_id") .values("label_name", "color", "label_id")
.annotate( .annotate(total_estimates=Sum(Cast("estimate_point__value", FloatField())))
total_estimates=Sum(Cast("estimate_point__value", FloatField()))
)
.annotate( .annotate(
completed_estimates=Sum( completed_estimates=Sum(
Cast("estimate_point__value", FloatField()), Cast("estimate_point__value", FloatField()),
@ -875,19 +827,13 @@ class TransferCycleIssueEndpoint(BaseAPIView):
), ),
), ),
# If `avatar_asset` is None, fall back to using `avatar` field directly # If `avatar_asset` is None, fall back to using `avatar` field directly
When( When(assignees__avatar_asset__isnull=True, then="assignees__avatar"),
assignees__avatar_asset__isnull=True, then="assignees__avatar"
),
default=Value(None), default=Value(None),
output_field=models.CharField(), output_field=models.CharField(),
) )
) )
.values("display_name", "assignee_id", "avatar_url") .values("display_name", "assignee_id", "avatar_url")
.annotate( .annotate(total_issues=Count("id", filter=Q(archived_at__isnull=True, is_draft=False)))
total_issues=Count(
"id", filter=Q(archived_at__isnull=True, is_draft=False)
)
)
.annotate( .annotate(
completed_issues=Count( completed_issues=Count(
"id", "id",
@ -914,9 +860,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
assignee_distribution_data = [ assignee_distribution_data = [
{ {
"display_name": item["display_name"], "display_name": item["display_name"],
"assignee_id": ( "assignee_id": (str(item["assignee_id"]) if item["assignee_id"] else None),
str(item["assignee_id"]) if item["assignee_id"] else None
),
"avatar": item.get("avatar"), "avatar": item.get("avatar"),
"avatar_url": item.get("avatar_url"), "avatar_url": item.get("avatar_url"),
"total_issues": item["total_issues"], "total_issues": item["total_issues"],
@ -938,11 +882,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
.annotate(color=F("labels__color")) .annotate(color=F("labels__color"))
.annotate(label_id=F("labels__id")) .annotate(label_id=F("labels__id"))
.values("label_name", "color", "label_id") .values("label_name", "color", "label_id")
.annotate( .annotate(total_issues=Count("id", filter=Q(archived_at__isnull=True, is_draft=False)))
total_issues=Count(
"id", filter=Q(archived_at__isnull=True, is_draft=False)
)
)
.annotate( .annotate(
completed_issues=Count( completed_issues=Count(
"id", "id",
@ -988,9 +928,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
cycle_id=cycle_id, cycle_id=cycle_id,
) )
current_cycle = Cycle.objects.filter( current_cycle = Cycle.objects.filter(workspace__slug=slug, project_id=project_id, pk=cycle_id).first()
workspace__slug=slug, project_id=project_id, pk=cycle_id
).first()
current_cycle.progress_snapshot = { current_cycle.progress_snapshot = {
"total_issues": old_cycle.total_issues, "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(): if new_cycle.end_date is not None and new_cycle.end_date < timezone.now():
return Response( 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, status=status.HTTP_400_BAD_REQUEST,
) )
@ -1044,9 +980,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
} }
) )
cycle_issues = CycleIssue.objects.bulk_update( cycle_issues = CycleIssue.objects.bulk_update(updated_cycles, ["cycle_id"], batch_size=100)
updated_cycles, ["cycle_id"], batch_size=100
)
# Capture Issue Activity # Capture Issue Activity
issue_activity.delay( issue_activity.delay(
@ -1080,12 +1014,8 @@ class CycleUserPropertiesEndpoint(BaseAPIView):
) )
cycle_properties.filters = request.data.get("filters", cycle_properties.filters) cycle_properties.filters = request.data.get("filters", cycle_properties.filters)
cycle_properties.rich_filters = request.data.get( cycle_properties.rich_filters = request.data.get("rich_filters", cycle_properties.rich_filters)
"rich_filters", cycle_properties.rich_filters cycle_properties.display_filters = request.data.get("display_filters", cycle_properties.display_filters)
)
cycle_properties.display_filters = request.data.get(
"display_filters", cycle_properties.display_filters
)
cycle_properties.display_properties = request.data.get( cycle_properties.display_properties = request.data.get(
"display_properties", cycle_properties.display_properties "display_properties", cycle_properties.display_properties
) )
@ -1109,13 +1039,9 @@ class CycleUserPropertiesEndpoint(BaseAPIView):
class CycleProgressEndpoint(BaseAPIView): class CycleProgressEndpoint(BaseAPIView):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def get(self, request, slug, project_id, cycle_id): def get(self, request, slug, project_id, cycle_id):
cycle = Cycle.objects.filter( cycle = Cycle.objects.filter(workspace__slug=slug, project_id=project_id, id=cycle_id).first()
workspace__slug=slug, project_id=project_id, id=cycle_id
).first()
if not cycle: if not cycle:
return Response( return Response({"error": "Cycle not found"}, status=status.HTTP_404_NOT_FOUND)
{"error": "Cycle not found"}, status=status.HTTP_404_NOT_FOUND
)
aggregate_estimates = ( aggregate_estimates = (
Issue.issue_objects.filter( Issue.issue_objects.filter(
estimate_point__estimate__type="points", estimate_point__estimate__type="points",
@ -1161,9 +1087,7 @@ class CycleProgressEndpoint(BaseAPIView):
output_field=FloatField(), output_field=FloatField(),
) )
), ),
total_estimate_points=Sum( total_estimate_points=Sum("value_as_float", default=Value(0), output_field=FloatField()),
"value_as_float", default=Value(0), output_field=FloatField()
),
) )
) )
if cycle.progress_snapshot: if cycle.progress_snapshot:
@ -1223,22 +1147,11 @@ class CycleProgressEndpoint(BaseAPIView):
return Response( return Response(
{ {
"backlog_estimate_points": aggregate_estimates["backlog_estimate_point"] "backlog_estimate_points": aggregate_estimates["backlog_estimate_point"] or 0,
or 0, "unstarted_estimate_points": aggregate_estimates["unstarted_estimate_point"] or 0,
"unstarted_estimate_points": aggregate_estimates[ "started_estimate_points": aggregate_estimates["started_estimate_point"] or 0,
"unstarted_estimate_point" "cancelled_estimate_points": aggregate_estimates["cancelled_estimate_point"] or 0,
] "completed_estimate_points": aggregate_estimates["completed_estimate_points"] or 0,
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"], "total_estimate_points": aggregate_estimates["total_estimate_points"],
"backlog_issues": backlog_issues, "backlog_issues": backlog_issues,
"total_issues": total_issues, "total_issues": total_issues,
@ -1256,9 +1169,7 @@ class CycleAnalyticsEndpoint(BaseAPIView):
def get(self, request, slug, project_id, cycle_id): def get(self, request, slug, project_id, cycle_id):
analytic_type = request.GET.get("type", "issues") analytic_type = request.GET.get("type", "issues")
cycle = ( cycle = (
Cycle.objects.filter( Cycle.objects.filter(workspace__slug=slug, project_id=project_id, id=cycle_id)
workspace__slug=slug, project_id=project_id, id=cycle_id
)
.annotate( .annotate(
total_issues=Count( total_issues=Count(
"issue_cycle__issue__id", "issue_cycle__issue__id",
@ -1341,9 +1252,7 @@ class CycleAnalyticsEndpoint(BaseAPIView):
) )
) )
.values("display_name", "assignee_id", "avatar_url") .values("display_name", "assignee_id", "avatar_url")
.annotate( .annotate(total_estimates=Sum(Cast("estimate_point__value", FloatField())))
total_estimates=Sum(Cast("estimate_point__value", FloatField()))
)
.annotate( .annotate(
completed_estimates=Sum( completed_estimates=Sum(
Cast("estimate_point__value", FloatField()), Cast("estimate_point__value", FloatField()),
@ -1378,9 +1287,7 @@ class CycleAnalyticsEndpoint(BaseAPIView):
.annotate(color=F("labels__color")) .annotate(color=F("labels__color"))
.annotate(label_id=F("labels__id")) .annotate(label_id=F("labels__id"))
.values("label_name", "color", "label_id") .values("label_name", "color", "label_id")
.annotate( .annotate(total_estimates=Sum(Cast("estimate_point__value", FloatField())))
total_estimates=Sum(Cast("estimate_point__value", FloatField()))
)
.annotate( .annotate(
completed_estimates=Sum( completed_estimates=Sum(
Cast("estimate_point__value", FloatField()), Cast("estimate_point__value", FloatField()),
@ -1482,11 +1389,7 @@ class CycleAnalyticsEndpoint(BaseAPIView):
.annotate(color=F("labels__color")) .annotate(color=F("labels__color"))
.annotate(label_id=F("labels__id")) .annotate(label_id=F("labels__id"))
.values("label_name", "color", "label_id") .values("label_name", "color", "label_id")
.annotate( .annotate(total_issues=Count("label_id", filter=Q(archived_at__isnull=True, is_draft=False)))
total_issues=Count(
"label_id", filter=Q(archived_at__isnull=True, is_draft=False)
)
)
.annotate( .annotate(
completed_issues=Count( completed_issues=Count(
"label_id", "label_id",

View file

@ -74,9 +74,7 @@ class CycleIssueViewSet(BaseViewSet):
return ( return (
issues.annotate( issues.annotate(
cycle_id=Subquery( cycle_id=Subquery(
CycleIssue.objects.filter( CycleIssue.objects.filter(issue=OuterRef("id"), deleted_at__isnull=True).values("cycle_id")[:1]
issue=OuterRef("id"), deleted_at__isnull=True
).values("cycle_id")[:1]
) )
) )
.annotate( .annotate(
@ -100,9 +98,7 @@ class CycleIssueViewSet(BaseViewSet):
.annotate(count=Func(F("id"), function="Count")) .annotate(count=Func(F("id"), function="Count"))
.values("count") .values("count")
) )
.prefetch_related( .prefetch_related("assignees", "labels", "issue_module__module", "issue_cycle__cycle")
"assignees", "labels", "issue_module__module", "issue_cycle__cycle"
)
) )
@method_decorator(gzip_page) @method_decorator(gzip_page)
@ -110,9 +106,7 @@ class CycleIssueViewSet(BaseViewSet):
def list(self, request, slug, project_id, cycle_id): def list(self, request, slug, project_id, cycle_id):
filters = issue_filters(request.query_params, "GET") filters = issue_filters(request.query_params, "GET")
issue_queryset = ( issue_queryset = (
Issue.issue_objects.filter( Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id, issue_cycle__deleted_at__isnull=True)
issue_cycle__cycle_id=cycle_id, issue_cycle__deleted_at__isnull=True
)
.filter(project_id=project_id) .filter(project_id=project_id)
.filter(workspace__slug=slug) .filter(workspace__slug=slug)
) )
@ -140,18 +134,14 @@ class CycleIssueViewSet(BaseViewSet):
sub_group_by = request.GET.get("sub_group_by", False) sub_group_by = request.GET.get("sub_group_by", False)
# issue queryset # issue queryset
issue_queryset = issue_queryset_grouper( issue_queryset = issue_queryset_grouper(queryset=issue_queryset, group_by=group_by, sub_group_by=sub_group_by)
queryset=issue_queryset, group_by=group_by, sub_group_by=sub_group_by
)
if group_by: if group_by:
# Check group and sub group value paginate # Check group and sub group value paginate
if sub_group_by: if sub_group_by:
if group_by == sub_group_by: if group_by == sub_group_by:
return Response( 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, status=status.HTTP_400_BAD_REQUEST,
) )
else: else:
@ -223,9 +213,7 @@ class CycleIssueViewSet(BaseViewSet):
request=request, request=request,
queryset=issue_queryset, queryset=issue_queryset,
total_count_queryset=total_issue_queryset, total_count_queryset=total_issue_queryset,
on_results=lambda issues: issue_on_results( on_results=lambda issues: issue_on_results(group_by=group_by, issues=issues, sub_group_by=sub_group_by),
group_by=group_by, issues=issues, sub_group_by=sub_group_by
),
) )
@allow_permission([ROLE.ADMIN, ROLE.MEMBER]) @allow_permission([ROLE.ADMIN, ROLE.MEMBER])
@ -233,26 +221,18 @@ class CycleIssueViewSet(BaseViewSet):
issues = request.data.get("issues", []) issues = request.data.get("issues", [])
if not issues: if not issues:
return Response( return Response({"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST)
{"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST
)
cycle = Cycle.objects.get( cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=cycle_id)
workspace__slug=slug, project_id=project_id, pk=cycle_id
)
if cycle.end_date is not None and cycle.end_date < timezone.now(): if cycle.end_date is not None and cycle.end_date < timezone.now():
return Response( 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, status=status.HTTP_400_BAD_REQUEST,
) )
# Get all CycleIssues already created # Get all CycleIssues already created
cycle_issues = list( cycle_issues = list(CycleIssue.objects.filter(~Q(cycle_id=cycle_id), issue_id__in=issues))
CycleIssue.objects.filter(~Q(cycle_id=cycle_id), issue_id__in=issues)
)
existing_issues = [str(cycle_issue.issue_id) for cycle_issue in cycle_issues] existing_issues = [str(cycle_issue.issue_id) for cycle_issue in cycle_issues]
new_issues = list(set(issues) - set(existing_issues)) new_issues = list(set(issues) - set(existing_issues))
@ -303,9 +283,7 @@ class CycleIssueViewSet(BaseViewSet):
current_instance=json.dumps( current_instance=json.dumps(
{ {
"updated_cycle_issues": update_cycle_issue_activity, "updated_cycle_issues": update_cycle_issue_activity,
"created_cycle_issues": serializers.serialize( "created_cycle_issues": serializers.serialize("json", created_records),
"json", created_records
),
} }
), ),
epoch=int(timezone.now().timestamp()), epoch=int(timezone.now().timestamp()),

View file

@ -56,9 +56,7 @@ class BulkEstimatePointEndpoint(BaseViewSet):
serializer = EstimateReadSerializer(estimates, many=True) serializer = EstimateReadSerializer(estimates, many=True)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
@invalidate_cache( @invalidate_cache(path="/api/workspaces/:slug/estimates/", url_params=True, user=False)
path="/api/workspaces/:slug/estimates/", url_params=True, user=False
)
def create(self, request, slug, project_id): def create(self, request, slug, project_id):
estimate = request.data.get("estimate") estimate = request.data.get("estimate")
estimate_name = estimate.get("name", generate_random_name()) estimate_name = estimate.get("name", generate_random_name())
@ -73,9 +71,7 @@ class BulkEstimatePointEndpoint(BaseViewSet):
estimate_points = request.data.get("estimate_points", []) estimate_points = request.data.get("estimate_points", [])
serializer = EstimatePointSerializer( serializer = EstimatePointSerializer(data=request.data.get("estimate_points"), many=True)
data=request.data.get("estimate_points"), many=True
)
if not serializer.is_valid(): if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@ -101,15 +97,11 @@ class BulkEstimatePointEndpoint(BaseViewSet):
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
def retrieve(self, request, slug, project_id, estimate_id): def retrieve(self, request, slug, project_id, estimate_id):
estimate = Estimate.objects.get( estimate = Estimate.objects.get(pk=estimate_id, workspace__slug=slug, project_id=project_id)
pk=estimate_id, workspace__slug=slug, project_id=project_id
)
serializer = EstimateReadSerializer(estimate) serializer = EstimateReadSerializer(estimate)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
@invalidate_cache( @invalidate_cache(path="/api/workspaces/:slug/estimates/", url_params=True, user=False)
path="/api/workspaces/:slug/estimates/", url_params=True, user=False
)
def partial_update(self, request, slug, project_id, estimate_id): def partial_update(self, request, slug, project_id, estimate_id):
if not len(request.data.get("estimate_points", [])): if not len(request.data.get("estimate_points", [])):
return Response( return Response(
@ -127,9 +119,7 @@ class BulkEstimatePointEndpoint(BaseViewSet):
estimate_points_data = request.data.get("estimate_points", []) estimate_points_data = request.data.get("estimate_points", [])
estimate_points = EstimatePoint.objects.filter( estimate_points = EstimatePoint.objects.filter(
pk__in=[ pk__in=[estimate_point.get("id") for estimate_point in estimate_points_data],
estimate_point.get("id") for estimate_point in estimate_points_data
],
workspace__slug=slug, workspace__slug=slug,
project_id=project_id, project_id=project_id,
estimate_id=estimate_id, estimate_id=estimate_id,
@ -138,34 +128,20 @@ class BulkEstimatePointEndpoint(BaseViewSet):
updated_estimate_points = [] updated_estimate_points = []
for estimate_point in estimate_points: for estimate_point in estimate_points:
# Find the data for that estimate point # Find the data for that estimate point
estimate_point_data = [ estimate_point_data = [point for point in estimate_points_data if point.get("id") == str(estimate_point.id)]
point
for point in estimate_points_data
if point.get("id") == str(estimate_point.id)
]
if len(estimate_point_data): if len(estimate_point_data):
estimate_point.value = estimate_point_data[0].get( estimate_point.value = estimate_point_data[0].get("value", estimate_point.value)
"value", estimate_point.value estimate_point.key = estimate_point_data[0].get("key", estimate_point.key)
)
estimate_point.key = estimate_point_data[0].get(
"key", estimate_point.key
)
updated_estimate_points.append(estimate_point) updated_estimate_points.append(estimate_point)
EstimatePoint.objects.bulk_update( EstimatePoint.objects.bulk_update(updated_estimate_points, ["key", "value"], batch_size=10)
updated_estimate_points, ["key", "value"], batch_size=10
)
estimate_serializer = EstimateReadSerializer(estimate) estimate_serializer = EstimateReadSerializer(estimate)
return Response(estimate_serializer.data, status=status.HTTP_200_OK) return Response(estimate_serializer.data, status=status.HTTP_200_OK)
@invalidate_cache( @invalidate_cache(path="/api/workspaces/:slug/estimates/", url_params=True, user=False)
path="/api/workspaces/:slug/estimates/", url_params=True, user=False
)
def destroy(self, request, slug, project_id, estimate_id): def destroy(self, request, slug, project_id, estimate_id):
estimate = Estimate.objects.get( estimate = Estimate.objects.get(pk=estimate_id, workspace__slug=slug, project_id=project_id)
pk=estimate_id, workspace__slug=slug, project_id=project_id
)
estimate.delete() estimate.delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
@ -196,9 +172,7 @@ class EstimatePointEndpoint(BaseViewSet):
project_id=project_id, project_id=project_id,
workspace__slug=slug, workspace__slug=slug,
) )
serializer = EstimatePointSerializer( serializer = EstimatePointSerializer(estimate_point, data=request.data, partial=True)
estimate_point, data=request.data, partial=True
)
if not serializer.is_valid(): if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
serializer.save() serializer.save()
@ -220,24 +194,12 @@ class EstimatePointEndpoint(BaseViewSet):
for issue in issues: for issue in issues:
issue_activity.delay( issue_activity.delay(
type="issue.activity.updated", type="issue.activity.updated",
requested_data=json.dumps( requested_data=json.dumps({"estimate_point": (str(new_estimate_id) if new_estimate_id else None)}),
{
"estimate_point": (
str(new_estimate_id) if new_estimate_id else None
)
}
),
actor_id=str(request.user.id), actor_id=str(request.user.id),
issue_id=issue.id, issue_id=issue.id,
project_id=str(project_id), project_id=str(project_id),
current_instance=json.dumps( 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()), epoch=int(timezone.now().timestamp()),
) )
@ -256,13 +218,7 @@ class EstimatePointEndpoint(BaseViewSet):
issue_id=issue.id, issue_id=issue.id,
project_id=str(project_id), project_id=str(project_id),
current_instance=json.dumps( 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()), epoch=int(timezone.now().timestamp()),
) )
@ -277,9 +233,7 @@ class EstimatePointEndpoint(BaseViewSet):
estimate_point.key -= 1 estimate_point.key -= 1
updated_estimate_points.append(estimate_point) updated_estimate_points.append(estimate_point)
EstimatePoint.objects.bulk_update( EstimatePoint.objects.bulk_update(updated_estimate_points, ["key"], batch_size=10)
updated_estimate_points, ["key"], batch_size=10
)
old_estimate_point.delete() old_estimate_point.delete()

View file

@ -62,18 +62,16 @@ class ExportIssuesEndpoint(BaseAPIView):
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE") @allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
def get(self, request, slug): def get(self, request, slug):
exporter_history = ExporterHistory.objects.filter( exporter_history = ExporterHistory.objects.filter(workspace__slug=slug, type="issue_exports").select_related(
workspace__slug=slug, type="issue_exports" "workspace", "initiated_by"
).select_related("workspace", "initiated_by") )
if request.GET.get("per_page", False) and request.GET.get("cursor", False): if request.GET.get("per_page", False) and request.GET.get("cursor", False):
return self.paginate( return self.paginate(
order_by=request.GET.get("order_by", "-created_at"), order_by=request.GET.get("order_by", "-created_at"),
request=request, request=request,
queryset=exporter_history, queryset=exporter_history,
on_results=lambda exporter_history: ExporterHistorySerializer( on_results=lambda exporter_history: ExporterHistorySerializer(exporter_history, many=True).data,
exporter_history, many=True
).data,
) )
else: else:
return Response( return Response(

View file

@ -108,8 +108,7 @@ def get_llm_config() -> Tuple[str | None, str | None, str | None]:
if model not in provider.models: if model not in provider.models:
log_exception( log_exception(
ValueError( ValueError(
f"Model {model} not supported by {provider.name}. " f"Model {model} not supported by {provider.name}. Supported models: {', '.join(provider.models)}"
f"Supported models: {', '.join(provider.models)}"
) )
) )
return None, None, None 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 return api_key, model, provider_key
def get_llm_response( def get_llm_response(task, prompt, api_key: str, model: str, provider: str) -> Tuple[str | None, str | None]:
task, prompt, api_key: str, model: str, provider: str
) -> Tuple[str | None, str | None]:
"""Helper to get LLM completion response""" """Helper to get LLM completion response"""
final_text = task + "\n" + prompt final_text = task + "\n" + prompt
try: try:
@ -157,13 +154,9 @@ class GPTIntegrationEndpoint(BaseAPIView):
task = request.data.get("task", False) task = request.data.get("task", False)
if not task: if not task:
return Response( return Response({"error": "Task is required"}, status=status.HTTP_400_BAD_REQUEST)
{"error": "Task is required"}, status=status.HTTP_400_BAD_REQUEST
)
text, error = get_llm_response( text, error = get_llm_response(task, request.data.get("prompt", False), api_key, model, provider)
task, request.data.get("prompt", False), api_key, model, provider
)
if not text and error: if not text and error:
return Response( return Response(
{"error": "An internal error has occurred."}, {"error": "An internal error has occurred."},
@ -197,13 +190,9 @@ class WorkspaceGPTIntegrationEndpoint(BaseAPIView):
task = request.data.get("task", False) task = request.data.get("task", False)
if not task: if not task:
return Response( return Response({"error": "Task is required"}, status=status.HTTP_400_BAD_REQUEST)
{"error": "Task is required"}, status=status.HTTP_400_BAD_REQUEST
)
text, error = get_llm_response( text, error = get_llm_response(task, request.data.get("prompt", False), api_key, model, provider)
task, request.data.get("prompt", False), api_key, model, provider
)
if not text and error: if not text and error:
return Response( return Response(
{"error": "An internal error has occurred."}, {"error": "An internal error has occurred."},

View file

@ -60,11 +60,7 @@ class IntakeViewSet(BaseViewSet):
workspace__slug=self.kwargs.get("slug"), workspace__slug=self.kwargs.get("slug"),
project_id=self.kwargs.get("project_id"), project_id=self.kwargs.get("project_id"),
) )
.annotate( .annotate(pending_issue_count=Count("issue_intake", filter=Q(issue_intake__status=-2)))
pending_issue_count=Count(
"issue_intake", filter=Q(issue_intake__status=-2)
)
)
.select_related("workspace", "project") .select_related("workspace", "project")
) )
@ -79,9 +75,7 @@ class IntakeViewSet(BaseViewSet):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER]) @allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def destroy(self, request, slug, project_id, pk): def destroy(self, request, slug, project_id, pk):
intake = Intake.objects.filter( intake = Intake.objects.filter(workspace__slug=slug, project_id=project_id, pk=pk).first()
workspace__slug=slug, project_id=project_id, pk=pk
).first()
# Handle default intake delete # Handle default intake delete
if intake.is_default: if intake.is_default:
return Response( return Response(
@ -109,16 +103,12 @@ class IntakeIssueViewSet(BaseViewSet):
.prefetch_related( .prefetch_related(
Prefetch( Prefetch(
"issue_intake", "issue_intake",
queryset=IntakeIssue.objects.only( queryset=IntakeIssue.objects.only("status", "duplicate_to", "snoozed_till", "source"),
"status", "duplicate_to", "snoozed_till", "source"
),
) )
) )
.annotate( .annotate(
cycle_id=Subquery( cycle_id=Subquery(
CycleIssue.objects.filter( CycleIssue.objects.filter(issue=OuterRef("id"), deleted_at__isnull=True).values("cycle_id")[:1]
issue=OuterRef("id"), deleted_at__isnull=True
).values("cycle_id")[:1]
) )
) )
.annotate( .annotate(
@ -147,10 +137,7 @@ class IntakeIssueViewSet(BaseViewSet):
ArrayAgg( ArrayAgg(
"labels__id", "labels__id",
distinct=True, distinct=True,
filter=Q( filter=Q(~Q(labels__id__isnull=True) & Q(label_issue__deleted_at__isnull=True)),
~Q(labels__id__isnull=True)
& Q(label_issue__deleted_at__isnull=True)
),
), ),
Value([], output_field=ArrayField(UUIDField())), Value([], output_field=ArrayField(UUIDField())),
), ),
@ -183,20 +170,14 @@ class IntakeIssueViewSet(BaseViewSet):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def list(self, request, slug, project_id): def list(self, request, slug, project_id):
intake = Intake.objects.filter( intake = Intake.objects.filter(workspace__slug=slug, project_id=project_id).first()
workspace__slug=slug, project_id=project_id
).first()
if not intake: if not intake:
return Response( return Response({"error": "Intake not found"}, status=status.HTTP_404_NOT_FOUND)
{"error": "Intake not found"}, status=status.HTTP_404_NOT_FOUND
)
project = Project.objects.get(pk=project_id) project = Project.objects.get(pk=project_id)
filters = issue_filters(request.GET, "GET", "issue__") filters = issue_filters(request.GET, "GET", "issue__")
intake_issue = ( intake_issue = (
IntakeIssue.objects.filter( IntakeIssue.objects.filter(intake_id=intake.id, project_id=project_id, **filters)
intake_id=intake.id, project_id=project_id, **filters
)
.select_related("issue") .select_related("issue")
.prefetch_related("issue__labels") .prefetch_related("issue__labels")
.annotate( .annotate(
@ -204,21 +185,14 @@ class IntakeIssueViewSet(BaseViewSet):
ArrayAgg( ArrayAgg(
"issue__labels__id", "issue__labels__id",
distinct=True, distinct=True,
filter=Q( 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())), Value([], output_field=ArrayField(UUIDField())),
) )
) )
).order_by(request.GET.get("order_by", "-issue__created_at")) ).order_by(request.GET.get("order_by", "-issue__created_at"))
# Intake status filter # Intake status filter
intake_status = [ intake_status = [item for item in request.GET.get("status", "-2").split(",") if item != "null"]
item
for item in request.GET.get("status", "-2").split(",")
if item != "null"
]
if intake_status: if intake_status:
intake_issue = intake_issue.filter(status__in=intake_status) intake_issue = intake_issue.filter(status__in=intake_status)
@ -236,17 +210,13 @@ class IntakeIssueViewSet(BaseViewSet):
return self.paginate( return self.paginate(
request=request, request=request,
queryset=(intake_issue), queryset=(intake_issue),
on_results=lambda intake_issues: IntakeIssueSerializer( on_results=lambda intake_issues: IntakeIssueSerializer(intake_issues, many=True).data,
intake_issues, many=True
).data,
) )
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def create(self, request, slug, project_id): def create(self, request, slug, project_id):
if not request.data.get("issue", {}).get("name", False): if not request.data.get("issue", {}).get("name", False):
return Response( return Response({"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST)
{"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
)
# Check for valid priority # Check for valid priority
if request.data.get("issue", {}).get("priority", "none") not in [ if request.data.get("issue", {}).get("priority", "none") not in [
@ -256,9 +226,7 @@ class IntakeIssueViewSet(BaseViewSet):
"urgent", "urgent",
"none", "none",
]: ]:
return Response( return Response({"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST)
{"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
)
# create an issue # create an issue
project = Project.objects.get(pk=project_id) project = Project.objects.get(pk=project_id)
@ -272,9 +240,7 @@ class IntakeIssueViewSet(BaseViewSet):
) )
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
intake_id = Intake.objects.filter( intake_id = Intake.objects.filter(workspace__slug=slug, project_id=project_id).first()
workspace__slug=slug, project_id=project_id
).first()
# create an intake issue # create an intake issue
intake_issue = IntakeIssue.objects.create( intake_issue = IntakeIssue.objects.create(
intake_id=intake_id.id, intake_id=intake_id.id,
@ -311,8 +277,7 @@ class IntakeIssueViewSet(BaseViewSet):
"issue__labels__id", "issue__labels__id",
distinct=True, distinct=True,
filter=Q( filter=Q(
~Q(issue__labels__id__isnull=True) ~Q(issue__labels__id__isnull=True) & Q(issue__label_issue__deleted_at__isnull=True)
& Q(issue__label_issue__deleted_at__isnull=True)
), ),
), ),
Value([], output_field=ArrayField(UUIDField())), Value([], output_field=ArrayField(UUIDField())),
@ -340,9 +305,7 @@ class IntakeIssueViewSet(BaseViewSet):
@allow_permission(allowed_roles=[ROLE.ADMIN], creator=True, model=Issue) @allow_permission(allowed_roles=[ROLE.ADMIN], creator=True, model=Issue)
def partial_update(self, request, slug, project_id, pk): def partial_update(self, request, slug, project_id, pk):
intake_id = Intake.objects.filter( intake_id = Intake.objects.filter(workspace__slug=slug, project_id=project_id).first()
workspace__slug=slug, project_id=project_id
).first()
intake_issue = IntakeIssue.objects.get( intake_issue = IntakeIssue.objects.get(
issue_id=pk, issue_id=pk,
workspace__slug=slug, workspace__slug=slug,
@ -371,10 +334,9 @@ class IntakeIssueViewSet(BaseViewSet):
) )
# Only project members admins and created_by users can access this endpoint # Only project members admins and created_by users can access this endpoint
if ( if ((project_member and project_member.role <= ROLE.GUEST.value) and not is_workspace_admin) and str(
(project_member and project_member.role <= ROLE.GUEST.value) intake_issue.created_by_id
and not is_workspace_admin ) != str(request.user.id):
) and str(intake_issue.created_by_id) != str(request.user.id):
return Response( return Response(
{"error": "You cannot edit intake issues"}, {"error": "You cannot edit intake issues"},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
@ -388,10 +350,7 @@ class IntakeIssueViewSet(BaseViewSet):
ArrayAgg( ArrayAgg(
"labels__id", "labels__id",
distinct=True, distinct=True,
filter=Q( filter=Q(~Q(labels__id__isnull=True) & Q(label_issue__deleted_at__isnull=True)),
~Q(labels__id__isnull=True)
& Q(label_issue__deleted_at__isnull=True)
),
), ),
Value([], output_field=ArrayField(UUIDField())), Value([], output_field=ArrayField(UUIDField())),
), ),
@ -399,10 +358,7 @@ class IntakeIssueViewSet(BaseViewSet):
ArrayAgg( ArrayAgg(
"assignees__id", "assignees__id",
distinct=True, distinct=True,
filter=Q( filter=Q(~Q(assignees__id__isnull=True) & Q(issue_assignee__deleted_at__isnull=True)),
~Q(assignees__id__isnull=True)
& Q(issue_assignee__deleted_at__isnull=True)
),
), ),
Value([], output_field=ArrayField(UUIDField())), Value([], output_field=ArrayField(UUIDField())),
), ),
@ -411,15 +367,11 @@ class IntakeIssueViewSet(BaseViewSet):
if project_member and project_member.role <= ROLE.GUEST.value: if project_member and project_member.role <= ROLE.GUEST.value:
issue_data = { issue_data = {
"name": issue_data.get("name", issue.name), "name": issue_data.get("name", issue.name),
"description_html": issue_data.get( "description_html": issue_data.get("description_html", issue.description_html),
"description_html", issue.description_html
),
"description": issue_data.get("description", issue.description), "description": issue_data.get("description", issue.description),
} }
current_instance = json.dumps( current_instance = json.dumps(IssueDetailSerializer(issue).data, cls=DjangoJSONEncoder)
IssueDetailSerializer(issue).data, cls=DjangoJSONEncoder
)
issue_serializer = IssueCreateSerializer( issue_serializer = IssueCreateSerializer(
issue, data=issue_data, partial=True, context={"project_id": project_id} issue, data=issue_data, partial=True, context={"project_id": project_id}
@ -449,20 +401,12 @@ class IntakeIssueViewSet(BaseViewSet):
) )
issue_serializer.save() issue_serializer.save()
else: else:
return Response( return Response(issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
# Only project admins can edit intake issue attributes # Only project admins can edit intake issue attributes
if ( if (project_member and project_member.role > ROLE.MEMBER.value) or is_workspace_admin:
project_member and project_member.role > ROLE.MEMBER.value serializer = IntakeIssueSerializer(intake_issue, data=request.data, partial=True)
) or is_workspace_admin: current_instance = json.dumps(IntakeIssueSerializer(intake_issue).data, cls=DjangoJSONEncoder)
serializer = IntakeIssueSerializer(
intake_issue, data=request.data, partial=True
)
current_instance = json.dumps(
IntakeIssueSerializer(intake_issue).data, cls=DjangoJSONEncoder
)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
# Update the issue state if the issue is rejected or marked as duplicate # Update the issue state if the issue is rejected or marked as duplicate
@ -472,9 +416,7 @@ class IntakeIssueViewSet(BaseViewSet):
workspace__slug=slug, workspace__slug=slug,
project_id=project_id, project_id=project_id,
) )
state = State.objects.filter( state = State.objects.filter(group="cancelled", workspace__slug=slug, project_id=project_id).first()
group="cancelled", workspace__slug=slug, project_id=project_id
).first()
if state is not None: if state is not None:
issue.state = state issue.state = state
issue.save() issue.save()
@ -490,9 +432,7 @@ class IntakeIssueViewSet(BaseViewSet):
# Update the issue state only if it is in triage state # Update the issue state only if it is in triage state
if issue.state.is_triage: if issue.state.is_triage:
# Move to default state # Move to default state
state = State.objects.filter( state = State.objects.filter(workspace__slug=slug, project_id=project_id, default=True).first()
workspace__slug=slug, project_id=project_id, default=True
).first()
if state is not None: if state is not None:
issue.state = state issue.state = state
issue.save() issue.save()
@ -519,8 +459,7 @@ class IntakeIssueViewSet(BaseViewSet):
"issue__labels__id", "issue__labels__id",
distinct=True, distinct=True,
filter=Q( filter=Q(
~Q(issue__labels__id__isnull=True) ~Q(issue__labels__id__isnull=True) & Q(issue__label_issue__deleted_at__isnull=True)
& Q(issue__label_issue__deleted_at__isnull=True)
), ),
), ),
Value([], output_field=ArrayField(UUIDField())), Value([], output_field=ArrayField(UUIDField())),
@ -546,13 +485,9 @@ class IntakeIssueViewSet(BaseViewSet):
serializer = IntakeIssueDetailSerializer(intake_issue).data serializer = IntakeIssueDetailSerializer(intake_issue).data
return Response(serializer, status=status.HTTP_200_OK) return Response(serializer, status=status.HTTP_200_OK)
@allow_permission( @allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], creator=True, model=Issue)
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], creator=True, model=Issue
)
def retrieve(self, request, slug, project_id, pk): def retrieve(self, request, slug, project_id, pk):
intake_id = Intake.objects.filter( intake_id = Intake.objects.filter(workspace__slug=slug, project_id=project_id).first()
workspace__slug=slug, project_id=project_id
).first()
project = Project.objects.get(pk=project_id) project = Project.objects.get(pk=project_id)
intake_issue = ( intake_issue = (
IntakeIssue.objects.select_related("issue") IntakeIssue.objects.select_related("issue")
@ -562,10 +497,7 @@ class IntakeIssueViewSet(BaseViewSet):
ArrayAgg( ArrayAgg(
"issue__labels__id", "issue__labels__id",
distinct=True, distinct=True,
filter=Q( 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())), Value([], output_field=ArrayField(UUIDField())),
), ),
@ -574,8 +506,7 @@ class IntakeIssueViewSet(BaseViewSet):
"issue__assignees__id", "issue__assignees__id",
distinct=True, distinct=True,
filter=Q( filter=Q(
~Q(issue__assignees__id__isnull=True) ~Q(issue__assignees__id__isnull=True) & Q(issue__issue_assignee__deleted_at__isnull=True)
& Q(issue__issue_assignee__deleted_at__isnull=True)
), ),
), ),
Value([], output_field=ArrayField(UUIDField())), Value([], output_field=ArrayField(UUIDField())),
@ -603,9 +534,7 @@ class IntakeIssueViewSet(BaseViewSet):
@allow_permission(allowed_roles=[ROLE.ADMIN], creator=True, model=Issue) @allow_permission(allowed_roles=[ROLE.ADMIN], creator=True, model=Issue)
def destroy(self, request, slug, project_id, pk): def destroy(self, request, slug, project_id, pk):
intake_id = Intake.objects.filter( intake_id = Intake.objects.filter(workspace__slug=slug, project_id=project_id).first()
workspace__slug=slug, project_id=project_id
).first()
intake_issue = IntakeIssue.objects.get( intake_issue = IntakeIssue.objects.get(
issue_id=pk, issue_id=pk,
workspace__slug=slug, workspace__slug=slug,
@ -616,9 +545,7 @@ class IntakeIssueViewSet(BaseViewSet):
# Check the issue status # Check the issue status
if intake_issue.status in [-2, -1, 0, 2]: if intake_issue.status in [-2, -1, 0, 2]:
# Delete the issue also # Delete the issue also
issue = Issue.objects.filter( issue = Issue.objects.filter(workspace__slug=slug, project_id=project_id, pk=pk).first()
workspace__slug=slug, project_id=project_id, pk=pk
).first()
issue.delete() issue.delete()
intake_issue.delete() intake_issue.delete()
@ -630,18 +557,14 @@ class IntakeWorkItemDescriptionVersionEndpoint(BaseAPIView):
paginated_data = results.values(*fields) paginated_data = results.values(*fields)
datetime_fields = ["created_at", "updated_at"] datetime_fields = ["created_at", "updated_at"]
paginated_data = user_timezone_converter( paginated_data = user_timezone_converter(paginated_data, datetime_fields, timezone)
paginated_data, datetime_fields, timezone
)
return paginated_data return paginated_data
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) @allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def get(self, request, slug, project_id, work_item_id, pk=None): def get(self, request, slug, project_id, work_item_id, pk=None):
project = Project.objects.get(pk=project_id) project = Project.objects.get(pk=project_id)
issue = Issue.objects.get( issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=work_item_id)
workspace__slug=slug, project_id=project_id, pk=work_item_id
)
if ( if (
ProjectMember.objects.filter( ProjectMember.objects.filter(
@ -667,9 +590,7 @@ class IntakeWorkItemDescriptionVersionEndpoint(BaseAPIView):
pk=pk, pk=pk,
) )
serializer = IssueDescriptionVersionDetailSerializer( serializer = IssueDescriptionVersionDetailSerializer(issue_description_version)
issue_description_version
)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
cursor = request.GET.get("cursor", None) cursor = request.GET.get("cursor", None)

View file

@ -63,9 +63,7 @@ class IssueActivityEndpoint(BaseAPIView):
issue_activities = issue_activities.prefetch_related( issue_activities = issue_activities.prefetch_related(
Prefetch( Prefetch(
"issue__issue_intake", "issue__issue_intake",
queryset=IntakeIssue.objects.only( queryset=IntakeIssue.objects.only("source_email", "source", "extra"),
"source_email", "source", "extra"
),
to_attr="source_data", to_attr="source_data",
) )
) )

View file

@ -4,7 +4,7 @@ import json
# Django imports # Django imports
from django.core.serializers.json import DjangoJSONEncoder 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 import timezone
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views.decorators.gzip import gzip_page from django.views.decorators.gzip import gzip_page
@ -57,9 +57,7 @@ class IssueArchiveViewSet(BaseViewSet):
return ( return (
issues.annotate( issues.annotate(
cycle_id=Subquery( cycle_id=Subquery(
CycleIssue.objects.filter( CycleIssue.objects.filter(issue=OuterRef("id"), deleted_at__isnull=True).values("cycle_id")[:1]
issue=OuterRef("id"), deleted_at__isnull=True
).values("cycle_id")[:1]
) )
) )
.annotate( .annotate(
@ -110,11 +108,7 @@ class IssueArchiveViewSet(BaseViewSet):
issue_queryset = self.get_queryset() issue_queryset = self.get_queryset()
issue_queryset = ( issue_queryset = issue_queryset if show_sub_issues == "true" else issue_queryset.filter(parent__isnull=True)
issue_queryset
if show_sub_issues == "true"
else issue_queryset.filter(parent__isnull=True)
)
# Apply filtering from filterset # Apply filtering from filterset
issue_queryset = self.filter_queryset(issue_queryset) issue_queryset = self.filter_queryset(issue_queryset)
@ -137,18 +131,14 @@ class IssueArchiveViewSet(BaseViewSet):
sub_group_by = request.GET.get("sub_group_by", False) sub_group_by = request.GET.get("sub_group_by", False)
# issue queryset # issue queryset
issue_queryset = issue_queryset_grouper( issue_queryset = issue_queryset_grouper(queryset=issue_queryset, group_by=group_by, sub_group_by=sub_group_by)
queryset=issue_queryset, group_by=group_by, sub_group_by=sub_group_by
)
if group_by: if group_by:
# Check group and sub group value paginate # Check group and sub group value paginate
if sub_group_by: if sub_group_by:
if group_by == sub_group_by: if group_by == sub_group_by:
return Response( 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, status=status.HTTP_400_BAD_REQUEST,
) )
else: else:
@ -220,9 +210,7 @@ class IssueArchiveViewSet(BaseViewSet):
request=request, request=request,
queryset=issue_queryset, queryset=issue_queryset,
total_count_queryset=total_issue_queryset, total_count_queryset=total_issue_queryset,
on_results=lambda issues: issue_on_results( on_results=lambda issues: issue_on_results(group_by=group_by, issues=issues, sub_group_by=sub_group_by),
group_by=group_by, issues=issues, sub_group_by=sub_group_by
),
) )
@allow_permission([ROLE.ADMIN, ROLE.MEMBER]) @allow_permission([ROLE.ADMIN, ROLE.MEMBER])
@ -263,9 +251,7 @@ class IssueArchiveViewSet(BaseViewSet):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER]) @allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def archive(self, request, slug, project_id, pk=None): def archive(self, request, slug, project_id, pk=None):
issue = Issue.issue_objects.get( issue = Issue.issue_objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
workspace__slug=slug, project_id=project_id, pk=pk
)
if issue.state.group not in ["completed", "cancelled"]: if issue.state.group not in ["completed", "cancelled"]:
return Response( return Response(
{"error": "Can only archive completed or cancelled state group issue"}, {"error": "Can only archive completed or cancelled state group issue"},
@ -273,15 +259,11 @@ class IssueArchiveViewSet(BaseViewSet):
) )
issue_activity.delay( issue_activity.delay(
type="issue.activity.updated", type="issue.activity.updated",
requested_data=json.dumps( requested_data=json.dumps({"archived_at": str(timezone.now().date()), "automation": False}),
{"archived_at": str(timezone.now().date()), "automation": False}
),
actor_id=str(request.user.id), actor_id=str(request.user.id),
issue_id=str(issue.id), issue_id=str(issue.id),
project_id=str(project_id), project_id=str(project_id),
current_instance=json.dumps( current_instance=json.dumps(IssueSerializer(issue).data, cls=DjangoJSONEncoder),
IssueSerializer(issue).data, cls=DjangoJSONEncoder
),
epoch=int(timezone.now().timestamp()), epoch=int(timezone.now().timestamp()),
notification=True, notification=True,
origin=base_host(request=request, is_app=True), origin=base_host(request=request, is_app=True),
@ -289,9 +271,7 @@ class IssueArchiveViewSet(BaseViewSet):
issue.archived_at = timezone.now().date() issue.archived_at = timezone.now().date()
issue.save() issue.save()
return Response( return Response({"archived_at": str(issue.archived_at)}, status=status.HTTP_200_OK)
{"archived_at": str(issue.archived_at)}, status=status.HTTP_200_OK
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER]) @allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def unarchive(self, request, slug, project_id, pk=None): def unarchive(self, request, slug, project_id, pk=None):
@ -307,9 +287,7 @@ class IssueArchiveViewSet(BaseViewSet):
actor_id=str(request.user.id), actor_id=str(request.user.id),
issue_id=str(issue.id), issue_id=str(issue.id),
project_id=str(project_id), project_id=str(project_id),
current_instance=json.dumps( current_instance=json.dumps(IssueSerializer(issue).data, cls=DjangoJSONEncoder),
IssueSerializer(issue).data, cls=DjangoJSONEncoder
),
epoch=int(timezone.now().timestamp()), epoch=int(timezone.now().timestamp()),
notification=True, notification=True,
origin=base_host(request=request, is_app=True), origin=base_host(request=request, is_app=True),
@ -328,13 +306,11 @@ class BulkArchiveIssuesEndpoint(BaseAPIView):
issue_ids = request.data.get("issue_ids", []) issue_ids = request.data.get("issue_ids", [])
if not len(issue_ids): if not len(issue_ids):
return Response( return Response({"error": "Issue IDs are required"}, status=status.HTTP_400_BAD_REQUEST)
{"error": "Issue IDs are required"}, status=status.HTTP_400_BAD_REQUEST
)
issues = Issue.objects.filter( issues = Issue.objects.filter(workspace__slug=slug, project_id=project_id, pk__in=issue_ids).select_related(
workspace__slug=slug, project_id=project_id, pk__in=issue_ids "state"
).select_related("state") )
bulk_archive_issues = [] bulk_archive_issues = []
for issue in issues: for issue in issues:
if issue.state.group not in ["completed", "cancelled"]: if issue.state.group not in ["completed", "cancelled"]:
@ -347,15 +323,11 @@ class BulkArchiveIssuesEndpoint(BaseAPIView):
) )
issue_activity.delay( issue_activity.delay(
type="issue.activity.updated", type="issue.activity.updated",
requested_data=json.dumps( requested_data=json.dumps({"archived_at": str(timezone.now().date()), "automation": False}),
{"archived_at": str(timezone.now().date()), "automation": False}
),
actor_id=str(request.user.id), actor_id=str(request.user.id),
issue_id=str(issue.id), issue_id=str(issue.id),
project_id=str(project_id), project_id=str(project_id),
current_instance=json.dumps( current_instance=json.dumps(IssueSerializer(issue).data, cls=DjangoJSONEncoder),
IssueSerializer(issue).data, cls=DjangoJSONEncoder
),
epoch=int(timezone.now().timestamp()), epoch=int(timezone.now().timestamp()),
notification=True, notification=True,
origin=base_host(request=request, is_app=True), origin=base_host(request=request, is_app=True),
@ -364,6 +336,4 @@ class BulkArchiveIssuesEndpoint(BaseAPIView):
bulk_archive_issues.append(issue) bulk_archive_issues.append(issue)
Issue.objects.bulk_update(bulk_archive_issues, ["archived_at"]) Issue.objects.bulk_update(bulk_archive_issues, ["archived_at"])
return Response( return Response({"archived_at": str(timezone.now().date())}, status=status.HTTP_200_OK)
{"archived_at": str(timezone.now().date())}, status=status.HTTP_200_OK
)

View file

@ -75,9 +75,7 @@ class IssueAttachmentEndpoint(BaseAPIView):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def get(self, request, slug, project_id, issue_id): def get(self, request, slug, project_id, issue_id):
issue_attachments = FileAsset.objects.filter( issue_attachments = FileAsset.objects.filter(issue_id=issue_id, workspace__slug=slug, project_id=project_id)
issue_id=issue_id, workspace__slug=slug, project_id=project_id
)
serializer = IssueAttachmentSerializer(issue_attachments, many=True) serializer = IssueAttachmentSerializer(issue_attachments, many=True)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
@ -123,9 +121,7 @@ class IssueAttachmentV2Endpoint(BaseAPIView):
storage = S3Storage(request=request) storage = S3Storage(request=request)
# Generate a presigned URL to share an S3 object # Generate a presigned URL to share an S3 object
presigned_url = storage.generate_presigned_post( presigned_url = storage.generate_presigned_post(object_name=asset_key, file_type=type, file_size=size_limit)
object_name=asset_key, file_type=type, file_size=size_limit
)
# Return the presigned URL # Return the presigned URL
return Response( return Response(
@ -140,9 +136,7 @@ class IssueAttachmentV2Endpoint(BaseAPIView):
@allow_permission([ROLE.ADMIN], creator=True, model=FileAsset) @allow_permission([ROLE.ADMIN], creator=True, model=FileAsset)
def delete(self, request, slug, project_id, issue_id, pk): def delete(self, request, slug, project_id, issue_id, pk):
issue_attachment = FileAsset.objects.get( issue_attachment = FileAsset.objects.get(pk=pk, workspace__slug=slug, project_id=project_id)
pk=pk, workspace__slug=slug, project_id=project_id
)
issue_attachment.is_deleted = True issue_attachment.is_deleted = True
issue_attachment.deleted_at = timezone.now() issue_attachment.deleted_at = timezone.now()
issue_attachment.save() issue_attachment.save()
@ -165,9 +159,7 @@ class IssueAttachmentV2Endpoint(BaseAPIView):
def get(self, request, slug, project_id, issue_id, pk=None): def get(self, request, slug, project_id, issue_id, pk=None):
if pk: if pk:
# Get the asset # Get the asset
asset = FileAsset.objects.get( asset = FileAsset.objects.get(id=pk, workspace__slug=slug, project_id=project_id)
id=pk, workspace__slug=slug, project_id=project_id
)
# Check if the asset is uploaded # Check if the asset is uploaded
if not asset.is_uploaded: if not asset.is_uploaded:
@ -198,9 +190,7 @@ class IssueAttachmentV2Endpoint(BaseAPIView):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def patch(self, request, slug, project_id, issue_id, pk): def patch(self, request, slug, project_id, issue_id, pk):
issue_attachment = FileAsset.objects.get( issue_attachment = FileAsset.objects.get(pk=pk, workspace__slug=slug, project_id=project_id)
pk=pk, workspace__slug=slug, project_id=project_id
)
serializer = IssueAttachmentSerializer(issue_attachment) serializer = IssueAttachmentSerializer(issue_attachment)
# Send this activity only if the attachment is not uploaded before # Send this activity only if the attachment is not uploaded before

View file

@ -82,16 +82,12 @@ class IssueListEndpoint(BaseAPIView):
issue_ids = request.GET.get("issues", False) issue_ids = request.GET.get("issues", False)
if not issue_ids: if not issue_ids:
return Response( return Response({"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST)
{"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST
)
issue_ids = [issue_id for issue_id in issue_ids.split(",") if issue_id != ""] issue_ids = [issue_id for issue_id in issue_ids.split(",") if issue_id != ""]
# Base queryset with basic filters # Base queryset with basic filters
queryset = Issue.issue_objects.filter( queryset = Issue.issue_objects.filter(workspace__slug=slug, project_id=project_id, pk__in=issue_ids)
workspace__slug=slug, project_id=project_id, pk__in=issue_ids
)
# Apply filtering from filterset # Apply filtering from filterset
queryset = self.filter_queryset(queryset) queryset = self.filter_queryset(queryset)
@ -102,17 +98,15 @@ class IssueListEndpoint(BaseAPIView):
# Add select_related, prefetch_related if fields or expand is not None # Add select_related, prefetch_related if fields or expand is not None
if self.fields or self.expand: if self.fields or self.expand:
issue_queryset = issue_queryset.select_related( issue_queryset = issue_queryset.select_related("workspace", "project", "state", "parent").prefetch_related(
"workspace", "project", "state", "parent" "assignees", "labels", "issue_module__module"
).prefetch_related("assignees", "labels", "issue_module__module") )
# Add annotations # Add annotations
issue_queryset = ( issue_queryset = (
issue_queryset.annotate( issue_queryset.annotate(
cycle_id=Subquery( cycle_id=Subquery(
CycleIssue.objects.filter( CycleIssue.objects.filter(issue=OuterRef("id"), deleted_at__isnull=True).values("cycle_id")[:1]
issue=OuterRef("id"), deleted_at__isnull=True
).values("cycle_id")[:1]
) )
) )
.annotate( .annotate(
@ -141,18 +135,14 @@ class IssueListEndpoint(BaseAPIView):
order_by_param = request.GET.get("order_by", "-created_at") order_by_param = request.GET.get("order_by", "-created_at")
# Issue queryset # Issue queryset
issue_queryset, _ = order_issue_queryset( issue_queryset, _ = order_issue_queryset(issue_queryset=issue_queryset, order_by_param=order_by_param)
issue_queryset=issue_queryset, order_by_param=order_by_param
)
# Group by # Group by
group_by = request.GET.get("group_by", False) group_by = request.GET.get("group_by", False)
sub_group_by = request.GET.get("sub_group_by", False) sub_group_by = request.GET.get("sub_group_by", False)
# issue queryset # issue queryset
issue_queryset = issue_queryset_grouper( issue_queryset = issue_queryset_grouper(queryset=issue_queryset, group_by=group_by, sub_group_by=sub_group_by)
queryset=issue_queryset, group_by=group_by, sub_group_by=sub_group_by
)
recent_visited_task.delay( recent_visited_task.delay(
slug=slug, slug=slug,
@ -163,9 +153,7 @@ class IssueListEndpoint(BaseAPIView):
) )
if self.fields or self.expand: if self.fields or self.expand:
issues = IssueSerializer( issues = IssueSerializer(queryset, many=True, fields=self.fields, expand=self.expand).data
queryset, many=True, fields=self.fields, expand=self.expand
).data
else: else:
issues = issue_queryset.values( issues = issue_queryset.values(
"id", "id",
@ -196,9 +184,7 @@ class IssueListEndpoint(BaseAPIView):
"deleted_at", "deleted_at",
) )
datetime_fields = ["created_at", "updated_at"] datetime_fields = ["created_at", "updated_at"]
issues = user_timezone_converter( issues = user_timezone_converter(issues, datetime_fields, request.user.user_timezone)
issues, datetime_fields, request.user.user_timezone
)
return Response(issues, status=status.HTTP_200_OK) return Response(issues, status=status.HTTP_200_OK)
@ -210,11 +196,7 @@ class IssueViewSet(BaseViewSet):
filterset_class = IssueFilterSet filterset_class = IssueFilterSet
def get_serializer_class(self): def get_serializer_class(self):
return ( return IssueCreateSerializer if self.action in ["create", "update", "partial_update"] else IssueSerializer
IssueCreateSerializer
if self.action in ["create", "update", "partial_update"]
else IssueSerializer
)
def get_queryset(self): def get_queryset(self):
issues = Issue.issue_objects.filter( issues = Issue.issue_objects.filter(
@ -228,9 +210,7 @@ class IssueViewSet(BaseViewSet):
issues = ( issues = (
issues.annotate( issues.annotate(
cycle_id=Subquery( cycle_id=Subquery(
CycleIssue.objects.filter( CycleIssue.objects.filter(issue=OuterRef("id"), deleted_at__isnull=True).values("cycle_id")[:1]
issue=OuterRef("id"), deleted_at__isnull=True
).values("cycle_id")[:1]
) )
) )
.annotate( .annotate(
@ -301,9 +281,7 @@ class IssueViewSet(BaseViewSet):
sub_group_by = request.GET.get("sub_group_by", False) sub_group_by = request.GET.get("sub_group_by", False)
# issue queryset # issue queryset
issue_queryset = issue_queryset_grouper( issue_queryset = issue_queryset_grouper(queryset=issue_queryset, group_by=group_by, sub_group_by=sub_group_by)
queryset=issue_queryset, group_by=group_by, sub_group_by=sub_group_by
)
recent_visited_task.delay( recent_visited_task.delay(
slug=slug, slug=slug,
@ -323,9 +301,7 @@ class IssueViewSet(BaseViewSet):
and not project.guest_view_all_features and not project.guest_view_all_features
): ):
issue_queryset = issue_queryset.filter(created_by=request.user) issue_queryset = issue_queryset.filter(created_by=request.user)
filtered_issue_queryset = filtered_issue_queryset.filter( filtered_issue_queryset = filtered_issue_queryset.filter(created_by=request.user)
created_by=request.user
)
if group_by: if group_by:
if sub_group_by: if sub_group_by:
@ -405,9 +381,7 @@ class IssueViewSet(BaseViewSet):
request=request, request=request,
queryset=issue_queryset, queryset=issue_queryset,
total_count_queryset=filtered_issue_queryset, total_count_queryset=filtered_issue_queryset,
on_results=lambda issues: issue_on_results( on_results=lambda issues: issue_on_results(group_by=group_by, issues=issues, sub_group_by=sub_group_by),
group_by=group_by, issues=issues, sub_group_by=sub_group_by
),
) )
@allow_permission([ROLE.ADMIN, ROLE.MEMBER]) @allow_permission([ROLE.ADMIN, ROLE.MEMBER])
@ -477,9 +451,7 @@ class IssueViewSet(BaseViewSet):
.first() .first()
) )
datetime_fields = ["created_at", "updated_at"] datetime_fields = ["created_at", "updated_at"]
issue = user_timezone_converter( issue = user_timezone_converter(issue, datetime_fields, request.user.user_timezone)
issue, datetime_fields, request.user.user_timezone
)
# Send the model activity # Send the model activity
model_activity.delay( model_activity.delay(
model_name="issue", model_name="issue",
@ -500,9 +472,7 @@ class IssueViewSet(BaseViewSet):
return Response(issue, status=status.HTTP_201_CREATED) return Response(issue, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@allow_permission( @allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], creator=True, model=Issue)
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], creator=True, model=Issue
)
def retrieve(self, request, slug, project_id, pk=None): def retrieve(self, request, slug, project_id, pk=None):
project = Project.objects.get(pk=project_id, workspace__slug=slug) project = Project.objects.get(pk=project_id, workspace__slug=slug)
@ -513,13 +483,7 @@ class IssueViewSet(BaseViewSet):
pk=pk, pk=pk,
) )
.select_related("state") .select_related("state")
.annotate( .annotate(cycle_id=Subquery(CycleIssue.objects.filter(issue=OuterRef("id")).values("cycle_id")[:1]))
cycle_id=Subquery(
CycleIssue.objects.filter(issue=OuterRef("id")).values("cycle_id")[
:1
]
)
)
.annotate( .annotate(
link_count=Subquery( link_count=Subquery(
IssueLink.objects.filter(issue=OuterRef("id")) IssueLink.objects.filter(issue=OuterRef("id"))
@ -643,9 +607,7 @@ class IssueViewSet(BaseViewSet):
serializer = IssueDetailSerializer(issue, expand=self.expand) serializer = IssueDetailSerializer(issue, expand=self.expand)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
@allow_permission( @allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], creator=True, model=Issue)
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], creator=True, model=Issue
)
def partial_update(self, request, slug, project_id, pk=None): def partial_update(self, request, slug, project_id, pk=None):
queryset = self.get_queryset() queryset = self.get_queryset()
queryset = self.apply_annotations(queryset) queryset = self.apply_annotations(queryset)
@ -655,10 +617,7 @@ class IssueViewSet(BaseViewSet):
ArrayAgg( ArrayAgg(
"labels__id", "labels__id",
distinct=True, distinct=True,
filter=Q( filter=Q(~Q(labels__id__isnull=True) & Q(label_issue__deleted_at__isnull=True)),
~Q(labels__id__isnull=True)
& Q(label_issue__deleted_at__isnull=True)
),
), ),
Value([], output_field=ArrayField(UUIDField())), Value([], output_field=ArrayField(UUIDField())),
), ),
@ -692,18 +651,12 @@ class IssueViewSet(BaseViewSet):
) )
if not issue: if not issue:
return Response( return Response({"error": "Issue not found"}, status=status.HTTP_404_NOT_FOUND)
{"error": "Issue not found"}, status=status.HTTP_404_NOT_FOUND
)
current_instance = json.dumps( current_instance = json.dumps(IssueDetailSerializer(issue).data, cls=DjangoJSONEncoder)
IssueDetailSerializer(issue).data, cls=DjangoJSONEncoder
)
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder) requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
serializer = IssueCreateSerializer( serializer = IssueCreateSerializer(issue, data=request.data, partial=True, context={"project_id": project_id})
issue, data=request.data, partial=True, context={"project_id": project_id}
)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
issue_activity.delay( issue_activity.delay(
@ -765,29 +718,19 @@ class IssueViewSet(BaseViewSet):
class IssueUserDisplayPropertyEndpoint(BaseAPIView): class IssueUserDisplayPropertyEndpoint(BaseAPIView):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def patch(self, request, slug, project_id): def patch(self, request, slug, project_id):
issue_property = IssueUserProperty.objects.get( issue_property = IssueUserProperty.objects.get(user=request.user, project_id=project_id)
user=request.user, project_id=project_id
)
issue_property.rich_filters = request.data.get( issue_property.rich_filters = request.data.get("rich_filters", issue_property.rich_filters)
"rich_filters", issue_property.rich_filters
)
issue_property.filters = request.data.get("filters", issue_property.filters) issue_property.filters = request.data.get("filters", issue_property.filters)
issue_property.display_filters = request.data.get( issue_property.display_filters = request.data.get("display_filters", issue_property.display_filters)
"display_filters", issue_property.display_filters issue_property.display_properties = request.data.get("display_properties", issue_property.display_properties)
)
issue_property.display_properties = request.data.get(
"display_properties", issue_property.display_properties
)
issue_property.save() issue_property.save()
serializer = IssueUserPropertySerializer(issue_property) serializer = IssueUserPropertySerializer(issue_property)
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def get(self, request, slug, project_id): def get(self, request, slug, project_id):
issue_property, _ = IssueUserProperty.objects.get_or_create( issue_property, _ = IssueUserProperty.objects.get_or_create(user=request.user, project_id=project_id)
user=request.user, project_id=project_id
)
serializer = IssueUserPropertySerializer(issue_property) serializer = IssueUserPropertySerializer(issue_property)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
@ -798,13 +741,9 @@ class BulkDeleteIssuesEndpoint(BaseAPIView):
issue_ids = request.data.get("issue_ids", []) issue_ids = request.data.get("issue_ids", [])
if not len(issue_ids): if not len(issue_ids):
return Response( return Response({"error": "Issue IDs are required"}, status=status.HTTP_400_BAD_REQUEST)
{"error": "Issue IDs are required"}, status=status.HTTP_400_BAD_REQUEST
)
issues = Issue.issue_objects.filter( issues = Issue.issue_objects.filter(workspace__slug=slug, project_id=project_id, pk__in=issue_ids)
workspace__slug=slug, project_id=project_id, pk__in=issue_ids
)
total_issues = len(issues) total_issues = len(issues)
@ -844,19 +783,11 @@ class IssuePaginatedViewSet(BaseViewSet):
workspace_slug = self.kwargs.get("slug") workspace_slug = self.kwargs.get("slug")
project_id = self.kwargs.get("project_id") project_id = self.kwargs.get("project_id")
issue_queryset = Issue.issue_objects.filter( issue_queryset = Issue.issue_objects.filter(workspace__slug=workspace_slug, project_id=project_id)
workspace__slug=workspace_slug, project_id=project_id
)
return ( return (
issue_queryset.select_related("state") issue_queryset.select_related("state")
.annotate( .annotate(cycle_id=Subquery(CycleIssue.objects.filter(issue=OuterRef("id")).values("cycle_id")[:1]))
cycle_id=Subquery(
CycleIssue.objects.filter(issue=OuterRef("id")).values("cycle_id")[
:1
]
)
)
.annotate( .annotate(
link_count=Subquery( link_count=Subquery(
IssueLink.objects.filter(issue=OuterRef("id")) IssueLink.objects.filter(issue=OuterRef("id"))
@ -891,9 +822,7 @@ class IssuePaginatedViewSet(BaseViewSet):
# converting the datetime fields in paginated data # converting the datetime fields in paginated data
datetime_fields = ["created_at", "updated_at"] datetime_fields = ["created_at", "updated_at"]
paginated_data = user_timezone_converter( paginated_data = user_timezone_converter(paginated_data, datetime_fields, timezone)
paginated_data, datetime_fields, timezone
)
return paginated_data return paginated_data
@ -937,9 +866,7 @@ class IssuePaginatedViewSet(BaseViewSet):
required_fields.append("description_html") required_fields.append("description_html")
# querying issues # querying issues
base_queryset = Issue.issue_objects.filter( base_queryset = Issue.issue_objects.filter(workspace__slug=slug, project_id=project_id)
workspace__slug=slug, project_id=project_id
)
base_queryset = base_queryset.order_by("updated_at") base_queryset = base_queryset.order_by("updated_at")
queryset = self.get_queryset().order_by("updated_at") queryset = self.get_queryset().order_by("updated_at")
@ -1018,9 +945,7 @@ class IssueDetailEndpoint(BaseAPIView):
return ( return (
issues.annotate( issues.annotate(
cycle_id=Subquery( cycle_id=Subquery(
CycleIssue.objects.filter( CycleIssue.objects.filter(issue=OuterRef("id"), deleted_at__isnull=True).values("cycle_id")[:1]
issue=OuterRef("id"), deleted_at__isnull=True
).values("cycle_id")[:1]
) )
) )
.annotate( .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 # 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 # if it is true then show all the issues else show only the issues created by the user
permission_subquery = ( permission_subquery = (
Issue.issue_objects.filter( Issue.issue_objects.filter(workspace__slug=slug, project_id=project_id, id=OuterRef("id"))
workspace__slug=slug, project_id=project_id, id=OuterRef("id")
)
.filter( .filter(
Q( Q(
project__project_projectmember__member=self.request.user, project__project_projectmember__member=self.request.user,
@ -1097,9 +1020,9 @@ class IssueDetailEndpoint(BaseAPIView):
.values("id") .values("id")
) )
# Main issue query # Main issue query
issue = Issue.issue_objects.filter( issue = Issue.issue_objects.filter(workspace__slug=slug, project_id=project_id).filter(
workspace__slug=slug, project_id=project_id Exists(permission_subquery)
).filter(Exists(permission_subquery)) )
# Add additional prefetch based on expand parameter # Add additional prefetch based on expand parameter
if self.expand: if self.expand:
@ -1133,9 +1056,7 @@ class IssueDetailEndpoint(BaseAPIView):
order_by_param = request.GET.get("order_by", "-created_at") order_by_param = request.GET.get("order_by", "-created_at")
# Issue queryset # Issue queryset
issue, order_by_param = order_issue_queryset( issue, order_by_param = order_issue_queryset(issue_queryset=issue, order_by_param=order_by_param)
issue_queryset=issue, order_by_param=order_by_param
)
return self.paginate( return self.paginate(
request=request, request=request,
order_by=order_by_param, order_by=order_by_param,
@ -1188,9 +1109,7 @@ class IssueBulkUpdateDateEndpoint(BaseAPIView):
start_date = update.get("start_date") start_date = update.get("start_date")
target_date = update.get("target_date") target_date = update.get("target_date")
validate_dates = self.validate_dates( validate_dates = self.validate_dates(issue.start_date, issue.target_date, start_date, target_date)
issue.start_date, issue.target_date, start_date, target_date
)
if not validate_dates: if not validate_dates:
return Response( return Response(
{"message": "Start date cannot exceed target date"}, {"message": "Start date cannot exceed target date"},
@ -1213,12 +1132,8 @@ class IssueBulkUpdateDateEndpoint(BaseAPIView):
if target_date: if target_date:
issue_activity.delay( issue_activity.delay(
type="issue.activity.updated", type="issue.activity.updated",
requested_data=json.dumps( requested_data=json.dumps({"target_date": update.get("target_date")}),
{"target_date": update.get("target_date")} current_instance=json.dumps({"target_date": str(issue.target_date)}),
),
current_instance=json.dumps(
{"target_date": str(issue.target_date)}
),
issue_id=str(issue_id), issue_id=str(issue_id),
actor_id=str(request.user.id), actor_id=str(request.user.id),
project_id=str(project_id), project_id=str(project_id),
@ -1230,9 +1145,7 @@ class IssueBulkUpdateDateEndpoint(BaseAPIView):
# Bulk update issues # Bulk update issues
Issue.objects.bulk_update(issues_to_update, ["start_date", "target_date"]) Issue.objects.bulk_update(issues_to_update, ["start_date", "target_date"])
return Response( return Response({"message": "Issues updated successfully"}, status=status.HTTP_200_OK)
{"message": "Issues updated successfully"}, status=status.HTTP_200_OK
)
class IssueMetaEndpoint(BaseAPIView): class IssueMetaEndpoint(BaseAPIView):
@ -1267,9 +1180,7 @@ class IssueDetailIdentifierEndpoint(BaseAPIView):
) )
# Fetch the project # Fetch the project
project = Project.objects.get( project = Project.objects.get(identifier__iexact=project_identifier, workspace__slug=slug)
identifier__iexact=project_identifier, workspace__slug=slug
)
# Check if the user is a member of the project # Check if the user is a member of the project
if not ProjectMember.objects.filter( if not ProjectMember.objects.filter(
@ -1289,13 +1200,7 @@ class IssueDetailIdentifierEndpoint(BaseAPIView):
.filter(workspace__slug=slug) .filter(workspace__slug=slug)
.select_related("workspace", "project", "state", "parent") .select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels", "issue_module__module") .prefetch_related("assignees", "labels", "issue_module__module")
.annotate( .annotate(cycle_id=Subquery(CycleIssue.objects.filter(issue=OuterRef("id")).values("cycle_id")[:1]))
cycle_id=Subquery(
CycleIssue.objects.filter(issue=OuterRef("id")).values("cycle_id")[
:1
]
)
)
.annotate( .annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id")) link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by() .order_by()
@ -1323,10 +1228,7 @@ class IssueDetailIdentifierEndpoint(BaseAPIView):
ArrayAgg( ArrayAgg(
"labels__id", "labels__id",
distinct=True, distinct=True,
filter=Q( filter=Q(~Q(labels__id__isnull=True) & Q(label_issue__deleted_at__isnull=True)),
~Q(labels__id__isnull=True)
& Q(label_issue__deleted_at__isnull=True)
),
), ),
Value([], output_field=ArrayField(UUIDField())), Value([], output_field=ArrayField(UUIDField())),
), ),

View file

@ -77,9 +77,7 @@ class IssueCommentViewSet(BaseViewSet):
) )
serializer = IssueCommentSerializer(data=request.data) serializer = IssueCommentSerializer(data=request.data)
if serializer.is_valid(): if serializer.is_valid():
serializer.save( serializer.save(project_id=project_id, issue_id=issue_id, actor=request.user)
project_id=project_id, issue_id=issue_id, actor=request.user
)
issue_activity.delay( issue_activity.delay(
type="comment.activity.created", type="comment.activity.created",
requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder), 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) @allow_permission(allowed_roles=[ROLE.ADMIN], creator=True, model=IssueComment)
def partial_update(self, request, slug, project_id, issue_id, pk): def partial_update(self, request, slug, project_id, issue_id, pk):
issue_comment = IssueComment.objects.get( issue_comment = IssueComment.objects.get(workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk)
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
)
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder) requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
current_instance = json.dumps( current_instance = json.dumps(IssueCommentSerializer(issue_comment).data, cls=DjangoJSONEncoder)
IssueCommentSerializer(issue_comment).data, cls=DjangoJSONEncoder serializer = IssueCommentSerializer(issue_comment, data=request.data, partial=True)
)
serializer = IssueCommentSerializer(
issue_comment, data=request.data, partial=True
)
if serializer.is_valid(): if serializer.is_valid():
if ( if "comment_html" in request.data and request.data["comment_html"] != issue_comment.comment_html:
"comment_html" in request.data
and request.data["comment_html"] != issue_comment.comment_html
):
serializer.save(edited_at=timezone.now()) serializer.save(edited_at=timezone.now())
else: else:
serializer.save() serializer.save()
@ -150,12 +139,8 @@ class IssueCommentViewSet(BaseViewSet):
@allow_permission(allowed_roles=[ROLE.ADMIN], creator=True, model=IssueComment) @allow_permission(allowed_roles=[ROLE.ADMIN], creator=True, model=IssueComment)
def destroy(self, request, slug, project_id, issue_id, pk): def destroy(self, request, slug, project_id, issue_id, pk):
issue_comment = IssueComment.objects.get( issue_comment = IssueComment.objects.get(workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk)
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk current_instance = json.dumps(IssueCommentSerializer(issue_comment).data, cls=DjangoJSONEncoder)
)
current_instance = json.dumps(
IssueCommentSerializer(issue_comment).data, cls=DjangoJSONEncoder
)
issue_comment.delete() issue_comment.delete()
issue_activity.delay( issue_activity.delay(
type="comment.activity.deleted", type="comment.activity.deleted",

View file

@ -35,9 +35,7 @@ class LabelViewSet(BaseViewSet):
.order_by("sort_order") .order_by("sort_order")
) )
@invalidate_cache( @invalidate_cache(path="/api/workspaces/:slug/labels/", url_params=True, user=False, multiple=True)
path="/api/workspaces/:slug/labels/", url_params=True, user=False, multiple=True
)
@allow_permission([ROLE.ADMIN]) @allow_permission([ROLE.ADMIN])
def create(self, request, slug, project_id): def create(self, request, slug, project_id):
try: try:
@ -58,9 +56,7 @@ class LabelViewSet(BaseViewSet):
# Check if the label name is unique within the project # Check if the label name is unique within the project
if ( if (
"name" in request.data "name" in request.data
and Label.objects.filter( and Label.objects.filter(project_id=kwargs["project_id"], name=request.data["name"])
project_id=kwargs["project_id"], name=request.data["name"]
)
.exclude(pk=kwargs["pk"]) .exclude(pk=kwargs["pk"])
.exists() .exists()
): ):

View file

@ -45,9 +45,7 @@ class IssueLinkViewSet(BaseViewSet):
serializer = IssueLinkSerializer(data=request.data) serializer = IssueLinkSerializer(data=request.data)
if serializer.is_valid(): if serializer.is_valid():
serializer.save(project_id=project_id, issue_id=issue_id) serializer.save(project_id=project_id, issue_id=issue_id)
crawl_work_item_link_title.delay( crawl_work_item_link_title.delay(serializer.data.get("id"), serializer.data.get("url"))
serializer.data.get("id"), serializer.data.get("url")
)
issue_activity.delay( issue_activity.delay(
type="link.activity.created", type="link.activity.created",
requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder), 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) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def partial_update(self, request, slug, project_id, issue_id, pk): def partial_update(self, request, slug, project_id, issue_id, pk):
issue_link = IssueLink.objects.get( issue_link = IssueLink.objects.get(workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk)
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
)
requested_data = json.dumps(request.data, cls=DjangoJSONEncoder) requested_data = json.dumps(request.data, cls=DjangoJSONEncoder)
current_instance = json.dumps( current_instance = json.dumps(IssueLinkSerializer(issue_link).data, cls=DjangoJSONEncoder)
IssueLinkSerializer(issue_link).data, cls=DjangoJSONEncoder
)
serializer = IssueLinkSerializer(issue_link, data=request.data, partial=True) serializer = IssueLinkSerializer(issue_link, data=request.data, partial=True)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
crawl_work_item_link_title.delay( crawl_work_item_link_title.delay(serializer.data.get("id"), serializer.data.get("url"))
serializer.data.get("id"), serializer.data.get("url")
)
issue_activity.delay( issue_activity.delay(
type="link.activity.updated", type="link.activity.updated",
@ -100,12 +92,8 @@ class IssueLinkViewSet(BaseViewSet):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def destroy(self, request, slug, project_id, issue_id, pk): def destroy(self, request, slug, project_id, issue_id, pk):
issue_link = IssueLink.objects.get( issue_link = IssueLink.objects.get(workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk)
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk current_instance = json.dumps(IssueLinkSerializer(issue_link).data, cls=DjangoJSONEncoder)
)
current_instance = json.dumps(
IssueLinkSerializer(issue_link).data, cls=DjangoJSONEncoder
)
issue_activity.delay( issue_activity.delay(
type="link.activity.deleted", type="link.activity.deleted",
requested_data=json.dumps({"link_id": str(pk)}), requested_data=json.dumps({"link_id": str(pk)}),

View file

@ -42,9 +42,7 @@ class IssueReactionViewSet(BaseViewSet):
def create(self, request, slug, project_id, issue_id): def create(self, request, slug, project_id, issue_id):
serializer = IssueReactionSerializer(data=request.data) serializer = IssueReactionSerializer(data=request.data)
if serializer.is_valid(): if serializer.is_valid():
serializer.save( serializer.save(issue_id=issue_id, project_id=project_id, actor=request.user)
issue_id=issue_id, project_id=project_id, actor=request.user
)
issue_activity.delay( issue_activity.delay(
type="issue_reaction.activity.created", type="issue_reaction.activity.created",
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
@ -74,9 +72,7 @@ class IssueReactionViewSet(BaseViewSet):
actor_id=str(self.request.user.id), actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("issue_id", None)), issue_id=str(self.kwargs.get("issue_id", None)),
project_id=str(self.kwargs.get("project_id", None)), project_id=str(self.kwargs.get("project_id", None)),
current_instance=json.dumps( current_instance=json.dumps({"reaction": str(reaction_code), "identifier": str(issue_reaction.id)}),
{"reaction": str(reaction_code), "identifier": str(issue_reaction.id)}
),
epoch=int(timezone.now().timestamp()), epoch=int(timezone.now().timestamp()),
notification=True, notification=True,
origin=base_host(request=request, is_app=True), origin=base_host(request=request, is_app=True),

View file

@ -37,9 +37,7 @@ class IssueRelationViewSet(BaseViewSet):
def list(self, request, slug, project_id, issue_id): def list(self, request, slug, project_id, issue_id):
issue_relations = ( issue_relations = (
IssueRelation.objects.filter( IssueRelation.objects.filter(Q(issue_id=issue_id) | Q(related_issue=issue_id))
Q(issue_id=issue_id) | Q(related_issue=issue_id)
)
.filter(workspace__slug=self.kwargs.get("slug")) .filter(workspace__slug=self.kwargs.get("slug"))
.select_related("project") .select_related("project")
.select_related("workspace") .select_related("workspace")
@ -48,19 +46,19 @@ class IssueRelationViewSet(BaseViewSet):
.distinct() .distinct()
) )
# get all blocking issues # get all blocking issues
blocking_issues = issue_relations.filter( blocking_issues = issue_relations.filter(relation_type="blocked_by", related_issue_id=issue_id).values_list(
relation_type="blocked_by", related_issue_id=issue_id "issue_id", flat=True
).values_list("issue_id", flat=True) )
# get all blocked by issues # get all blocked by issues
blocked_by_issues = issue_relations.filter( blocked_by_issues = issue_relations.filter(relation_type="blocked_by", issue_id=issue_id).values_list(
relation_type="blocked_by", issue_id=issue_id "related_issue_id", flat=True
).values_list("related_issue_id", flat=True) )
# get all duplicate issues # get all duplicate issues
duplicate_issues = issue_relations.filter( duplicate_issues = issue_relations.filter(issue_id=issue_id, relation_type="duplicate").values_list(
issue_id=issue_id, relation_type="duplicate" "related_issue_id", flat=True
).values_list("related_issue_id", flat=True) )
# get all relates to issues # get all relates to issues
duplicate_issues_related = issue_relations.filter( duplicate_issues_related = issue_relations.filter(
@ -68,9 +66,9 @@ class IssueRelationViewSet(BaseViewSet):
).values_list("issue_id", flat=True) ).values_list("issue_id", flat=True)
# get all relates to issues # get all relates to issues
relates_to_issues = issue_relations.filter( relates_to_issues = issue_relations.filter(issue_id=issue_id, relation_type="relates_to").values_list(
issue_id=issue_id, relation_type="relates_to" "related_issue_id", flat=True
).values_list("related_issue_id", flat=True) )
# get all relates to issues # get all relates to issues
relates_to_issues_related = issue_relations.filter( relates_to_issues_related = issue_relations.filter(
@ -83,9 +81,9 @@ class IssueRelationViewSet(BaseViewSet):
).values_list("issue_id", flat=True) ).values_list("issue_id", flat=True)
# get all start_before issues # get all start_before issues
start_before_issues = issue_relations.filter( start_before_issues = issue_relations.filter(relation_type="start_before", issue_id=issue_id).values_list(
relation_type="start_before", issue_id=issue_id "related_issue_id", flat=True
).values_list("related_issue_id", flat=True) )
# get all finish after issues # get all finish after issues
finish_after_issues = issue_relations.filter( finish_after_issues = issue_relations.filter(
@ -93,9 +91,9 @@ class IssueRelationViewSet(BaseViewSet):
).values_list("issue_id", flat=True) ).values_list("issue_id", flat=True)
# get all finish before issues # get all finish before issues
finish_before_issues = issue_relations.filter( finish_before_issues = issue_relations.filter(relation_type="finish_before", issue_id=issue_id).values_list(
relation_type="finish_before", issue_id=issue_id "related_issue_id", flat=True
).values_list("related_issue_id", flat=True) )
queryset = ( queryset = (
Issue.issue_objects.filter(workspace__slug=slug) Issue.issue_objects.filter(workspace__slug=slug)
@ -103,9 +101,7 @@ class IssueRelationViewSet(BaseViewSet):
.prefetch_related("assignees", "labels", "issue_module__module") .prefetch_related("assignees", "labels", "issue_module__module")
.annotate( .annotate(
cycle_id=Subquery( cycle_id=Subquery(
CycleIssue.objects.filter( CycleIssue.objects.filter(issue=OuterRef("id"), deleted_at__isnull=True).values("cycle_id")[:1]
issue=OuterRef("id"), deleted_at__isnull=True
).values("cycle_id")[:1]
) )
) )
.annotate( .annotate(
@ -134,10 +130,7 @@ class IssueRelationViewSet(BaseViewSet):
ArrayAgg( ArrayAgg(
"labels__id", "labels__id",
distinct=True, distinct=True,
filter=Q( filter=Q(~Q(labels__id__isnull=True) & (Q(label_issue__deleted_at__isnull=True))),
~Q(labels__id__isnull=True)
& (Q(label_issue__deleted_at__isnull=True))
),
), ),
Value([], output_field=ArrayField(UUIDField())), Value([], output_field=ArrayField(UUIDField())),
), ),
@ -223,15 +216,9 @@ class IssueRelationViewSet(BaseViewSet):
issue_relation = IssueRelation.objects.bulk_create( issue_relation = IssueRelation.objects.bulk_create(
[ [
IssueRelation( IssueRelation(
issue_id=( issue_id=(issue if relation_type in ["blocking", "start_after", "finish_after"] else issue_id),
issue
if relation_type in ["blocking", "start_after", "finish_after"]
else issue_id
),
related_issue_id=( related_issue_id=(
issue_id issue_id if relation_type in ["blocking", "start_after", "finish_after"] else issue
if relation_type in ["blocking", "start_after", "finish_after"]
else issue
), ),
relation_type=(get_actual_relation(relation_type)), relation_type=(get_actual_relation(relation_type)),
project_id=project_id, project_id=project_id,
@ -274,13 +261,10 @@ class IssueRelationViewSet(BaseViewSet):
issue_relations = IssueRelation.objects.filter( issue_relations = IssueRelation.objects.filter(
workspace__slug=slug, workspace__slug=slug,
).filter( ).filter(
Q(issue_id=related_issue, related_issue_id=issue_id) Q(issue_id=related_issue, related_issue_id=issue_id) | Q(issue_id=issue_id, related_issue_id=related_issue)
| Q(issue_id=issue_id, related_issue_id=related_issue)
) )
issue_relations = issue_relations.first() issue_relations = issue_relations.first()
current_instance = json.dumps( current_instance = json.dumps(IssueRelationSerializer(issue_relations).data, cls=DjangoJSONEncoder)
IssueRelationSerializer(issue_relations).data, cls=DjangoJSONEncoder
)
issue_relations.delete() issue_relations.delete()
issue_activity.delay( issue_activity.delay(
type="issue_relation.activity.deleted", type="issue_relation.activity.deleted",

View file

@ -37,9 +37,7 @@ class SubIssuesEndpoint(BaseAPIView):
.prefetch_related("assignees", "labels", "issue_module__module") .prefetch_related("assignees", "labels", "issue_module__module")
.annotate( .annotate(
cycle_id=Subquery( cycle_id=Subquery(
CycleIssue.objects.filter( CycleIssue.objects.filter(issue=OuterRef("id"), deleted_at__isnull=True).values("cycle_id")[:1]
issue=OuterRef("id"), deleted_at__isnull=True
).values("cycle_id")[:1]
) )
) )
.annotate( .annotate(
@ -68,10 +66,7 @@ class SubIssuesEndpoint(BaseAPIView):
ArrayAgg( ArrayAgg(
"labels__id", "labels__id",
distinct=True, distinct=True,
filter=Q( filter=Q(~Q(labels__id__isnull=True) & Q(label_issue__deleted_at__isnull=True)),
~Q(labels__id__isnull=True)
& Q(label_issue__deleted_at__isnull=True)
),
), ),
Value([], output_field=ArrayField(UUIDField())), Value([], output_field=ArrayField(UUIDField())),
), ),
@ -109,9 +104,7 @@ class SubIssuesEndpoint(BaseAPIView):
group_by = request.GET.get("group_by", False) group_by = request.GET.get("group_by", False)
if order_by_param: if order_by_param:
sub_issues, order_by_param = order_issue_queryset( sub_issues, order_by_param = order_issue_queryset(sub_issues, order_by_param)
sub_issues, order_by_param
)
# create's a dict with state group name with their respective issue id's # create's a dict with state group name with their respective issue id's
result = defaultdict(list) result = defaultdict(list)
@ -146,9 +139,7 @@ class SubIssuesEndpoint(BaseAPIView):
"archived_at", "archived_at",
) )
datetime_fields = ["created_at", "updated_at"] datetime_fields = ["created_at", "updated_at"]
sub_issues = user_timezone_converter( sub_issues = user_timezone_converter(sub_issues, datetime_fields, request.user.user_timezone)
sub_issues, datetime_fields, request.user.user_timezone
)
# Grouping # Grouping
if group_by: if group_by:
result_dict = defaultdict(list) result_dict = defaultdict(list)
@ -192,9 +183,7 @@ class SubIssuesEndpoint(BaseAPIView):
_ = Issue.objects.bulk_update(sub_issues, ["parent"], batch_size=10) _ = Issue.objects.bulk_update(sub_issues, ["parent"], batch_size=10)
updated_sub_issues = Issue.issue_objects.filter(id__in=sub_issue_ids).annotate( updated_sub_issues = Issue.issue_objects.filter(id__in=sub_issue_ids).annotate(state_group=F("state__group"))
state_group=F("state__group")
)
# Track the issue # Track the issue
_ = [ _ = [

View file

@ -25,9 +25,7 @@ class IssueVersionEndpoint(BaseAPIView):
paginated_data = results.values(*fields) paginated_data = results.values(*fields)
datetime_fields = ["created_at", "updated_at"] datetime_fields = ["created_at", "updated_at"]
paginated_data = user_timezone_converter( paginated_data = user_timezone_converter(paginated_data, datetime_fields, timezone)
paginated_data, datetime_fields, timezone
)
return paginated_data return paginated_data
@ -77,18 +75,14 @@ class WorkItemDescriptionVersionEndpoint(BaseAPIView):
paginated_data = results.values(*fields) paginated_data = results.values(*fields)
datetime_fields = ["created_at", "updated_at"] datetime_fields = ["created_at", "updated_at"]
paginated_data = user_timezone_converter( paginated_data = user_timezone_converter(paginated_data, datetime_fields, timezone)
paginated_data, datetime_fields, timezone
)
return paginated_data return paginated_data
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) @allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def get(self, request, slug, project_id, work_item_id, pk=None): def get(self, request, slug, project_id, work_item_id, pk=None):
project = Project.objects.get(pk=project_id) project = Project.objects.get(pk=project_id)
issue = Issue.objects.get( issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=work_item_id)
workspace__slug=slug, project_id=project_id, pk=work_item_id
)
if ( if (
ProjectMember.objects.filter( ProjectMember.objects.filter(
@ -114,9 +108,7 @@ class WorkItemDescriptionVersionEndpoint(BaseAPIView):
pk=pk, pk=pk,
) )
serializer = IssueDescriptionVersionDetailSerializer( serializer = IssueDescriptionVersionDetailSerializer(issue_description_version)
issue_description_version
)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
cursor = request.GET.get("cursor", None) cursor = request.GET.get("cursor", None)

View file

@ -113,11 +113,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
issue_module__deleted_at__isnull=True, issue_module__deleted_at__isnull=True,
) )
.values("issue_module__module_id") .values("issue_module__module_id")
.annotate( .annotate(completed_estimate_points=Sum(Cast("estimate_point__value", FloatField())))
completed_estimate_points=Sum(
Cast("estimate_point__value", FloatField())
)
)
.values("completed_estimate_points")[:1] .values("completed_estimate_points")[:1]
) )
@ -128,9 +124,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
issue_module__deleted_at__isnull=True, issue_module__deleted_at__isnull=True,
) )
.values("issue_module__module_id") .values("issue_module__module_id")
.annotate( .annotate(total_estimate_points=Sum(Cast("estimate_point__value", FloatField())))
total_estimate_points=Sum(Cast("estimate_point__value", FloatField()))
)
.values("total_estimate_points")[:1] .values("total_estimate_points")[:1]
) )
backlog_estimate_point = ( backlog_estimate_point = (
@ -141,9 +135,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
issue_module__deleted_at__isnull=True, issue_module__deleted_at__isnull=True,
) )
.values("issue_module__module_id") .values("issue_module__module_id")
.annotate( .annotate(backlog_estimate_point=Sum(Cast("estimate_point__value", FloatField())))
backlog_estimate_point=Sum(Cast("estimate_point__value", FloatField()))
)
.values("backlog_estimate_point")[:1] .values("backlog_estimate_point")[:1]
) )
unstarted_estimate_point = ( unstarted_estimate_point = (
@ -154,11 +146,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
issue_module__deleted_at__isnull=True, issue_module__deleted_at__isnull=True,
) )
.values("issue_module__module_id") .values("issue_module__module_id")
.annotate( .annotate(unstarted_estimate_point=Sum(Cast("estimate_point__value", FloatField())))
unstarted_estimate_point=Sum(
Cast("estimate_point__value", FloatField())
)
)
.values("unstarted_estimate_point")[:1] .values("unstarted_estimate_point")[:1]
) )
started_estimate_point = ( started_estimate_point = (
@ -169,9 +157,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
issue_module__deleted_at__isnull=True, issue_module__deleted_at__isnull=True,
) )
.values("issue_module__module_id") .values("issue_module__module_id")
.annotate( .annotate(started_estimate_point=Sum(Cast("estimate_point__value", FloatField())))
started_estimate_point=Sum(Cast("estimate_point__value", FloatField()))
)
.values("started_estimate_point")[:1] .values("started_estimate_point")[:1]
) )
cancelled_estimate_point = ( cancelled_estimate_point = (
@ -182,11 +168,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
issue_module__deleted_at__isnull=True, issue_module__deleted_at__isnull=True,
) )
.values("issue_module__module_id") .values("issue_module__module_id")
.annotate( .annotate(cancelled_estimate_point=Sum(Cast("estimate_point__value", FloatField())))
cancelled_estimate_point=Sum(
Cast("estimate_point__value", FloatField())
)
)
.values("cancelled_estimate_point")[:1] .values("cancelled_estimate_point")[:1]
) )
return ( return (
@ -214,27 +196,15 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
Value(0, output_field=IntegerField()), Value(0, output_field=IntegerField()),
) )
) )
.annotate( .annotate(started_issues=Coalesce(Subquery(started_issues[:1]), Value(0, output_field=IntegerField())))
started_issues=Coalesce(
Subquery(started_issues[:1]), Value(0, output_field=IntegerField())
)
)
.annotate( .annotate(
unstarted_issues=Coalesce( unstarted_issues=Coalesce(
Subquery(unstarted_issues[:1]), Subquery(unstarted_issues[:1]),
Value(0, output_field=IntegerField()), Value(0, output_field=IntegerField()),
) )
) )
.annotate( .annotate(backlog_issues=Coalesce(Subquery(backlog_issues[:1]), Value(0, output_field=IntegerField())))
backlog_issues=Coalesce( .annotate(total_issues=Coalesce(Subquery(total_issues[:1]), Value(0, output_field=IntegerField())))
Subquery(backlog_issues[:1]), Value(0, output_field=IntegerField())
)
)
.annotate(
total_issues=Coalesce(
Subquery(total_issues[:1]), Value(0, output_field=IntegerField())
)
)
.annotate( .annotate(
backlog_estimate_points=Coalesce( backlog_estimate_points=Coalesce(
Subquery(backlog_estimate_point), Subquery(backlog_estimate_point),
@ -266,9 +236,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
) )
) )
.annotate( .annotate(
total_estimate_points=Coalesce( total_estimate_points=Coalesce(Subquery(total_estimate_point), Value(0, output_field=FloatField()))
Subquery(total_estimate_point), Value(0, output_field=FloatField())
)
) )
.annotate( .annotate(
member_ids=Coalesce( member_ids=Coalesce(
@ -317,9 +285,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
"archived_at", "archived_at",
) )
datetime_fields = ["created_at", "updated_at"] datetime_fields = ["created_at", "updated_at"]
modules = user_timezone_converter( modules = user_timezone_converter(modules, datetime_fields, request.user.user_timezone)
modules, datetime_fields, request.user.user_timezone
)
return Response(modules, status=status.HTTP_200_OK) return Response(modules, status=status.HTTP_200_OK)
else: else:
queryset = ( queryset = (
@ -389,9 +355,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
"avatar_url", "avatar_url",
"display_name", "display_name",
) )
.annotate( .annotate(total_estimates=Sum(Cast("estimate_point__value", FloatField())))
total_estimates=Sum(Cast("estimate_point__value", FloatField()))
)
.annotate( .annotate(
completed_estimates=Sum( completed_estimates=Sum(
Cast("estimate_point__value", FloatField()), Cast("estimate_point__value", FloatField()),
@ -425,9 +389,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
.annotate(color=F("labels__color")) .annotate(color=F("labels__color"))
.annotate(label_id=F("labels__id")) .annotate(label_id=F("labels__id"))
.values("label_name", "color", "label_id") .values("label_name", "color", "label_id")
.annotate( .annotate(total_estimates=Sum(Cast("estimate_point__value", FloatField())))
total_estimates=Sum(Cast("estimate_point__value", FloatField()))
)
.annotate( .annotate(
completed_estimates=Sum( completed_estimates=Sum(
Cast("estimate_point__value", FloatField()), Cast("estimate_point__value", FloatField()),
@ -500,11 +462,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
"avatar_url", "avatar_url",
"display_name", "display_name",
) )
.annotate( .annotate(total_issues=Count("id", filter=Q(archived_at__isnull=True, is_draft=False)))
total_issues=Count(
"id", filter=Q(archived_at__isnull=True, is_draft=False)
)
)
.annotate( .annotate(
completed_issues=Count( completed_issues=Count(
"id", "id",
@ -539,11 +497,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
.annotate(color=F("labels__color")) .annotate(color=F("labels__color"))
.annotate(label_id=F("labels__id")) .annotate(label_id=F("labels__id"))
.values("label_name", "color", "label_id") .values("label_name", "color", "label_id")
.annotate( .annotate(total_issues=Count("id", filter=Q(archived_at__isnull=True, is_draft=False)))
total_issues=Count(
"id", filter=Q(archived_at__isnull=True, is_draft=False)
)
)
.annotate( .annotate(
completed_issues=Count( completed_issues=Count(
"id", "id",
@ -584,9 +538,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
return Response(data, status=status.HTTP_200_OK) return Response(data, status=status.HTTP_200_OK)
def post(self, request, slug, project_id, module_id): def post(self, request, slug, project_id, module_id):
module = Module.objects.get( module = Module.objects.get(pk=module_id, project_id=project_id, workspace__slug=slug)
pk=module_id, project_id=project_id, workspace__slug=slug
)
if module.status not in ["completed", "cancelled"]: if module.status not in ["completed", "cancelled"]:
return Response( return Response(
{"error": "Only completed or cancelled modules can be archived"}, {"error": "Only completed or cancelled modules can be archived"},
@ -600,14 +552,10 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
project_id=project_id, project_id=project_id,
workspace__slug=slug, workspace__slug=slug,
).delete() ).delete()
return Response( return Response({"archived_at": str(module.archived_at)}, status=status.HTTP_200_OK)
{"archived_at": str(module.archived_at)}, status=status.HTTP_200_OK
)
def delete(self, request, slug, project_id, module_id): def delete(self, request, slug, project_id, module_id):
module = Module.objects.get( module = Module.objects.get(pk=module_id, project_id=project_id, workspace__slug=slug)
pk=module_id, project_id=project_id, workspace__slug=slug
)
module.archived_at = None module.archived_at = None
module.save() module.save()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)

View file

@ -69,11 +69,7 @@ class ModuleViewSet(BaseViewSet):
webhook_event = "module" webhook_event = "module"
def get_serializer_class(self): def get_serializer_class(self):
return ( return ModuleWriteSerializer if self.action in ["create", "update", "partial_update"] else ModuleSerializer
ModuleWriteSerializer
if self.action in ["create", "update", "partial_update"]
else ModuleSerializer
)
def get_queryset(self): def get_queryset(self):
favorite_subquery = UserFavorite.objects.filter( favorite_subquery = UserFavorite.objects.filter(
@ -150,11 +146,7 @@ class ModuleViewSet(BaseViewSet):
issue_module__deleted_at__isnull=True, issue_module__deleted_at__isnull=True,
) )
.values("issue_module__module_id") .values("issue_module__module_id")
.annotate( .annotate(completed_estimate_points=Sum(Cast("estimate_point__value", FloatField())))
completed_estimate_points=Sum(
Cast("estimate_point__value", FloatField())
)
)
.values("completed_estimate_points")[:1] .values("completed_estimate_points")[:1]
) )
@ -165,9 +157,7 @@ class ModuleViewSet(BaseViewSet):
issue_module__deleted_at__isnull=True, issue_module__deleted_at__isnull=True,
) )
.values("issue_module__module_id") .values("issue_module__module_id")
.annotate( .annotate(total_estimate_points=Sum(Cast("estimate_point__value", FloatField())))
total_estimate_points=Sum(Cast("estimate_point__value", FloatField()))
)
.values("total_estimate_points")[:1] .values("total_estimate_points")[:1]
) )
backlog_estimate_point = ( backlog_estimate_point = (
@ -178,9 +168,7 @@ class ModuleViewSet(BaseViewSet):
issue_module__deleted_at__isnull=True, issue_module__deleted_at__isnull=True,
) )
.values("issue_module__module_id") .values("issue_module__module_id")
.annotate( .annotate(backlog_estimate_point=Sum(Cast("estimate_point__value", FloatField())))
backlog_estimate_point=Sum(Cast("estimate_point__value", FloatField()))
)
.values("backlog_estimate_point")[:1] .values("backlog_estimate_point")[:1]
) )
unstarted_estimate_point = ( unstarted_estimate_point = (
@ -191,11 +179,7 @@ class ModuleViewSet(BaseViewSet):
issue_module__deleted_at__isnull=True, issue_module__deleted_at__isnull=True,
) )
.values("issue_module__module_id") .values("issue_module__module_id")
.annotate( .annotate(unstarted_estimate_point=Sum(Cast("estimate_point__value", FloatField())))
unstarted_estimate_point=Sum(
Cast("estimate_point__value", FloatField())
)
)
.values("unstarted_estimate_point")[:1] .values("unstarted_estimate_point")[:1]
) )
started_estimate_point = ( started_estimate_point = (
@ -206,9 +190,7 @@ class ModuleViewSet(BaseViewSet):
issue_module__deleted_at__isnull=True, issue_module__deleted_at__isnull=True,
) )
.values("issue_module__module_id") .values("issue_module__module_id")
.annotate( .annotate(started_estimate_point=Sum(Cast("estimate_point__value", FloatField())))
started_estimate_point=Sum(Cast("estimate_point__value", FloatField()))
)
.values("started_estimate_point")[:1] .values("started_estimate_point")[:1]
) )
cancelled_estimate_point = ( cancelled_estimate_point = (
@ -219,11 +201,7 @@ class ModuleViewSet(BaseViewSet):
issue_module__deleted_at__isnull=True, issue_module__deleted_at__isnull=True,
) )
.values("issue_module__module_id") .values("issue_module__module_id")
.annotate( .annotate(cancelled_estimate_point=Sum(Cast("estimate_point__value", FloatField())))
cancelled_estimate_point=Sum(
Cast("estimate_point__value", FloatField())
)
)
.values("cancelled_estimate_point")[:1] .values("cancelled_estimate_point")[:1]
) )
return ( return (
@ -251,27 +229,15 @@ class ModuleViewSet(BaseViewSet):
Value(0, output_field=IntegerField()), Value(0, output_field=IntegerField()),
) )
) )
.annotate( .annotate(started_issues=Coalesce(Subquery(started_issues[:1]), Value(0, output_field=IntegerField())))
started_issues=Coalesce(
Subquery(started_issues[:1]), Value(0, output_field=IntegerField())
)
)
.annotate( .annotate(
unstarted_issues=Coalesce( unstarted_issues=Coalesce(
Subquery(unstarted_issues[:1]), Subquery(unstarted_issues[:1]),
Value(0, output_field=IntegerField()), Value(0, output_field=IntegerField()),
) )
) )
.annotate( .annotate(backlog_issues=Coalesce(Subquery(backlog_issues[:1]), Value(0, output_field=IntegerField())))
backlog_issues=Coalesce( .annotate(total_issues=Coalesce(Subquery(total_issues[:1]), Value(0, output_field=IntegerField())))
Subquery(backlog_issues[:1]), Value(0, output_field=IntegerField())
)
)
.annotate(
total_issues=Coalesce(
Subquery(total_issues[:1]), Value(0, output_field=IntegerField())
)
)
.annotate( .annotate(
backlog_estimate_points=Coalesce( backlog_estimate_points=Coalesce(
Subquery(backlog_estimate_point), Subquery(backlog_estimate_point),
@ -303,9 +269,7 @@ class ModuleViewSet(BaseViewSet):
) )
) )
.annotate( .annotate(
total_estimate_points=Coalesce( total_estimate_points=Coalesce(Subquery(total_estimate_point), Value(0, output_field=FloatField()))
Subquery(total_estimate_point), Value(0, output_field=FloatField())
)
) )
.annotate( .annotate(
member_ids=Coalesce( member_ids=Coalesce(
@ -326,9 +290,7 @@ class ModuleViewSet(BaseViewSet):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER]) @allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def create(self, request, slug, project_id): def create(self, request, slug, project_id):
project = Project.objects.get(workspace__slug=slug, pk=project_id) project = Project.objects.get(workspace__slug=slug, pk=project_id)
serializer = ModuleWriteSerializer( serializer = ModuleWriteSerializer(data=request.data, context={"project": project})
data=request.data, context={"project": project}
)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
@ -380,9 +342,7 @@ class ModuleViewSet(BaseViewSet):
origin=base_host(request=request, is_app=True), origin=base_host(request=request, is_app=True),
) )
datetime_fields = ["created_at", "updated_at"] datetime_fields = ["created_at", "updated_at"]
module = user_timezone_converter( module = user_timezone_converter(module, datetime_fields, request.user.user_timezone)
module, datetime_fields, request.user.user_timezone
)
return Response(module, status=status.HTTP_201_CREATED) return Response(module, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@ -425,9 +385,7 @@ class ModuleViewSet(BaseViewSet):
"updated_at", "updated_at",
) )
datetime_fields = ["created_at", "updated_at"] datetime_fields = ["created_at", "updated_at"]
modules = user_timezone_converter( modules = user_timezone_converter(modules, datetime_fields, request.user.user_timezone)
modules, datetime_fields, request.user.user_timezone
)
return Response(modules, status=status.HTTP_200_OK) return Response(modules, status=status.HTTP_200_OK)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER]) @allow_permission([ROLE.ADMIN, ROLE.MEMBER])
@ -450,9 +408,7 @@ class ModuleViewSet(BaseViewSet):
) )
if not queryset.exists(): if not queryset.exists():
return Response( return Response({"error": "Module not found"}, status=status.HTTP_404_NOT_FOUND)
{"error": "Module not found"}, status=status.HTTP_404_NOT_FOUND
)
estimate_type = Project.objects.filter( estimate_type = Project.objects.filter(
workspace__slug=slug, workspace__slug=slug,
@ -505,9 +461,7 @@ class ModuleViewSet(BaseViewSet):
"avatar_url", "avatar_url",
"display_name", "display_name",
) )
.annotate( .annotate(total_estimates=Sum(Cast("estimate_point__value", FloatField())))
total_estimates=Sum(Cast("estimate_point__value", FloatField()))
)
.annotate( .annotate(
completed_estimates=Sum( completed_estimates=Sum(
Cast("estimate_point__value", FloatField()), Cast("estimate_point__value", FloatField()),
@ -542,9 +496,7 @@ class ModuleViewSet(BaseViewSet):
.annotate(color=F("labels__color")) .annotate(color=F("labels__color"))
.annotate(label_id=F("labels__id")) .annotate(label_id=F("labels__id"))
.values("label_name", "color", "label_id") .values("label_name", "color", "label_id")
.annotate( .annotate(total_estimates=Sum(Cast("estimate_point__value", FloatField())))
total_estimates=Sum(Cast("estimate_point__value", FloatField()))
)
.annotate( .annotate(
completed_estimates=Sum( completed_estimates=Sum(
Cast("estimate_point__value", FloatField()), Cast("estimate_point__value", FloatField()),
@ -602,21 +554,13 @@ class ModuleViewSet(BaseViewSet):
), ),
), ),
# If `avatar_asset` is None, fall back to using `avatar` field directly # If `avatar_asset` is None, fall back to using `avatar` field directly
When( When(assignees__avatar_asset__isnull=True, then="assignees__avatar"),
assignees__avatar_asset__isnull=True, then="assignees__avatar"
),
default=Value(None), default=Value(None),
output_field=models.CharField(), output_field=models.CharField(),
) )
) )
.values( .values("first_name", "last_name", "assignee_id", "avatar_url", "display_name")
"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(
total_issues=Count(
"id", filter=Q(archived_at__isnull=True, is_draft=False)
)
)
.annotate( .annotate(
completed_issues=Count( completed_issues=Count(
"id", "id",
@ -651,11 +595,7 @@ class ModuleViewSet(BaseViewSet):
.annotate(color=F("labels__color")) .annotate(color=F("labels__color"))
.annotate(label_id=F("labels__id")) .annotate(label_id=F("labels__id"))
.values("label_name", "color", "label_id") .values("label_name", "color", "label_id")
.annotate( .annotate(total_issues=Count("id", filter=Q(archived_at__isnull=True, is_draft=False)))
total_issues=Count(
"id", filter=Q(archived_at__isnull=True, is_draft=False)
)
)
.annotate( .annotate(
completed_issues=Count( completed_issues=Count(
"id", "id",
@ -685,12 +625,7 @@ class ModuleViewSet(BaseViewSet):
"completion_chart": {}, "completion_chart": {},
} }
if ( if modules and modules.start_date and modules.target_date and modules.total_issues > 0:
modules
and modules.start_date
and modules.target_date
and modules.total_issues > 0
):
data["distribution"]["completion_chart"] = burndown_plot( data["distribution"]["completion_chart"] = burndown_plot(
queryset=modules, queryset=modules,
slug=slug, slug=slug,
@ -726,12 +661,8 @@ class ModuleViewSet(BaseViewSet):
{"error": "Archived module cannot be updated"}, {"error": "Archived module cannot be updated"},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
current_instance = json.dumps( current_instance = json.dumps(ModuleSerializer(current_module).data, cls=DjangoJSONEncoder)
ModuleSerializer(current_module).data, cls=DjangoJSONEncoder serializer = ModuleWriteSerializer(current_module, data=request.data, partial=True)
)
serializer = ModuleWriteSerializer(
current_module, data=request.data, partial=True
)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
@ -781,9 +712,7 @@ class ModuleViewSet(BaseViewSet):
) )
datetime_fields = ["created_at", "updated_at"] datetime_fields = ["created_at", "updated_at"]
module = user_timezone_converter( module = user_timezone_converter(module, datetime_fields, request.user.user_timezone)
module, datetime_fields, request.user.user_timezone
)
return Response(module, status=status.HTTP_200_OK) return Response(module, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@ -791,9 +720,7 @@ class ModuleViewSet(BaseViewSet):
def destroy(self, request, slug, project_id, pk): def destroy(self, request, slug, project_id, pk):
module = Module.objects.get(workspace__slug=slug, project_id=project_id, pk=pk) module = Module.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
module_issues = list( module_issues = list(ModuleIssue.objects.filter(module_id=pk).values_list("issue", flat=True))
ModuleIssue.objects.filter(module_id=pk).values_list("issue", flat=True)
)
_ = [ _ = [
issue_activity.delay( issue_activity.delay(
type="module.activity.deleted", type="module.activity.deleted",
@ -901,15 +828,9 @@ class ModuleUserPropertiesEndpoint(BaseAPIView):
workspace__slug=slug, workspace__slug=slug,
) )
module_properties.filters = request.data.get( module_properties.filters = request.data.get("filters", module_properties.filters)
"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.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( module_properties.display_properties = request.data.get(
"display_properties", module_properties.display_properties "display_properties", module_properties.display_properties
) )

View file

@ -50,9 +50,7 @@ class ModuleIssueViewSet(BaseViewSet):
return ( return (
issues.annotate( issues.annotate(
cycle_id=Subquery( cycle_id=Subquery(
CycleIssue.objects.filter( CycleIssue.objects.filter(issue=OuterRef("id"), deleted_at__isnull=True).values("cycle_id")[:1]
issue=OuterRef("id"), deleted_at__isnull=True
).values("cycle_id")[:1]
) )
) )
.annotate( .annotate(
@ -119,18 +117,14 @@ class ModuleIssueViewSet(BaseViewSet):
sub_group_by = request.GET.get("sub_group_by", False) sub_group_by = request.GET.get("sub_group_by", False)
# issue queryset # issue queryset
issue_queryset = issue_queryset_grouper( issue_queryset = issue_queryset_grouper(queryset=issue_queryset, group_by=group_by, sub_group_by=sub_group_by)
queryset=issue_queryset, group_by=group_by, sub_group_by=sub_group_by
)
if group_by: if group_by:
# Check group and sub group value paginate # Check group and sub group value paginate
if sub_group_by: if sub_group_by:
if group_by == sub_group_by: if group_by == sub_group_by:
return Response( 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, status=status.HTTP_400_BAD_REQUEST,
) )
else: else:
@ -205,9 +199,7 @@ class ModuleIssueViewSet(BaseViewSet):
request=request, request=request,
queryset=issue_queryset, queryset=issue_queryset,
total_count_queryset=total_issue_queryset, total_count_queryset=total_issue_queryset,
on_results=lambda issues: issue_on_results( on_results=lambda issues: issue_on_results(group_by=group_by, issues=issues, sub_group_by=sub_group_by),
group_by=group_by, issues=issues, sub_group_by=sub_group_by
),
) )
@allow_permission([ROLE.ADMIN, ROLE.MEMBER]) @allow_permission([ROLE.ADMIN, ROLE.MEMBER])
@ -215,9 +207,7 @@ class ModuleIssueViewSet(BaseViewSet):
def create_module_issues(self, request, slug, project_id, module_id): def create_module_issues(self, request, slug, project_id, module_id):
issues = request.data.get("issues", []) issues = request.data.get("issues", [])
if not issues: if not issues:
return Response( return Response({"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST)
{"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST
)
project = Project.objects.get(pk=project_id) project = Project.objects.get(pk=project_id)
_ = ModuleIssue.objects.bulk_create( _ = ModuleIssue.objects.bulk_create(
[ [
@ -334,9 +324,7 @@ class ModuleIssueViewSet(BaseViewSet):
actor_id=str(request.user.id), actor_id=str(request.user.id),
issue_id=str(issue_id), issue_id=str(issue_id),
project_id=str(project_id), project_id=str(project_id),
current_instance=json.dumps( current_instance=json.dumps({"module_name": module_issue.first().module.name}),
{"module_name": module_issue.first().module.name}
),
epoch=int(timezone.now().timestamp()), epoch=int(timezone.now().timestamp()),
notification=True, notification=True,
origin=base_host(request=request, is_app=True), origin=base_host(request=request, is_app=True),

View file

@ -40,9 +40,7 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
.select_related("workspace", "project", "triggered_by", "receiver") .select_related("workspace", "project", "triggered_by", "receiver")
) )
@allow_permission( @allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
)
def list(self, request, slug): def list(self, request, slug):
# Get query parameters # Get query parameters
snoozed = request.GET.get("snoozed", "false") snoozed = request.GET.get("snoozed", "false")
@ -59,9 +57,7 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
) )
notifications = ( notifications = (
Notification.objects.filter( Notification.objects.filter(workspace__slug=slug, receiver_id=request.user.id)
workspace__slug=slug, receiver_id=request.user.id
)
.filter(entity_name="issue") .filter(entity_name="issue")
.annotate(is_inbox_issue=Exists(intake_issue)) .annotate(is_inbox_issue=Exists(intake_issue))
.annotate(is_intake_issue=Exists(intake_issue)) .annotate(is_intake_issue=Exists(intake_issue))
@ -106,23 +102,9 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
# Subscribed issues # Subscribed issues
if "subscribed" in type: if "subscribed" in type:
issue_ids = ( issue_ids = (
IssueSubscriber.objects.filter( IssueSubscriber.objects.filter(workspace__slug=slug, subscriber_id=request.user.id)
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)))
.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) .filter(created=False, assigned=False)
.values_list("issue_id", flat=True) .values_list("issue_id", flat=True)
) )
@ -130,9 +112,9 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
# Assigned Issues # Assigned Issues
if "assigned" in type: if "assigned" in type:
issue_ids = IssueAssignee.objects.filter( issue_ids = IssueAssignee.objects.filter(workspace__slug=slug, assignee_id=request.user.id).values_list(
workspace__slug=slug, assignee_id=request.user.id "issue_id", flat=True
).values_list("issue_id", flat=True) )
q_filters |= Q(entity_identifier__in=issue_ids) q_filters |= Q(entity_identifier__in=issue_ids)
# Created issues # Created issues
@ -142,9 +124,9 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
).exists(): ).exists():
notifications = notifications.none() notifications = notifications.none()
else: else:
issue_ids = Issue.objects.filter( issue_ids = Issue.objects.filter(workspace__slug=slug, created_by=request.user).values_list(
workspace__slug=slug, created_by=request.user "pk", flat=True
).values_list("pk", flat=True) )
q_filters |= Q(entity_identifier__in=issue_ids) q_filters |= Q(entity_identifier__in=issue_ids)
# Apply the combined Q object filters # Apply the combined Q object filters
@ -156,75 +138,51 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
order_by=request.GET.get("order_by", "-created_at"), order_by=request.GET.get("order_by", "-created_at"),
request=request, request=request,
queryset=(notifications), queryset=(notifications),
on_results=lambda notifications: NotificationSerializer( on_results=lambda notifications: NotificationSerializer(notifications, many=True).data,
notifications, many=True
).data,
) )
serializer = NotificationSerializer(notifications, many=True) serializer = NotificationSerializer(notifications, many=True)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
@allow_permission( @allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
)
def partial_update(self, request, slug, pk): def partial_update(self, request, slug, pk):
notification = Notification.objects.get( notification = Notification.objects.get(workspace__slug=slug, pk=pk, receiver=request.user)
workspace__slug=slug, pk=pk, receiver=request.user
)
# Only read_at and snoozed_till can be updated # Only read_at and snoozed_till can be updated
notification_data = {"snoozed_till": request.data.get("snoozed_till", None)} notification_data = {"snoozed_till": request.data.get("snoozed_till", None)}
serializer = NotificationSerializer( serializer = NotificationSerializer(notification, data=notification_data, partial=True)
notification, data=notification_data, partial=True
)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@allow_permission( @allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
)
def mark_read(self, request, slug, pk): def mark_read(self, request, slug, pk):
notification = Notification.objects.get( notification = Notification.objects.get(receiver=request.user, workspace__slug=slug, pk=pk)
receiver=request.user, workspace__slug=slug, pk=pk
)
notification.read_at = timezone.now() notification.read_at = timezone.now()
notification.save() notification.save()
serializer = NotificationSerializer(notification) serializer = NotificationSerializer(notification)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
@allow_permission( @allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
)
def mark_unread(self, request, slug, pk): def mark_unread(self, request, slug, pk):
notification = Notification.objects.get( notification = Notification.objects.get(receiver=request.user, workspace__slug=slug, pk=pk)
receiver=request.user, workspace__slug=slug, pk=pk
)
notification.read_at = None notification.read_at = None
notification.save() notification.save()
serializer = NotificationSerializer(notification) serializer = NotificationSerializer(notification)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
@allow_permission( @allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
)
def archive(self, request, slug, pk): def archive(self, request, slug, pk):
notification = Notification.objects.get( notification = Notification.objects.get(receiver=request.user, workspace__slug=slug, pk=pk)
receiver=request.user, workspace__slug=slug, pk=pk
)
notification.archived_at = timezone.now() notification.archived_at = timezone.now()
notification.save() notification.save()
serializer = NotificationSerializer(notification) serializer = NotificationSerializer(notification)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
@allow_permission( @allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
)
def unarchive(self, request, slug, pk): def unarchive(self, request, slug, pk):
notification = Notification.objects.get( notification = Notification.objects.get(receiver=request.user, workspace__slug=slug, pk=pk)
receiver=request.user, workspace__slug=slug, pk=pk
)
notification.archived_at = None notification.archived_at = None
notification.save() notification.save()
serializer = NotificationSerializer(notification) serializer = NotificationSerializer(notification)
@ -234,9 +192,7 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
class UnreadNotificationEndpoint(BaseAPIView): class UnreadNotificationEndpoint(BaseAPIView):
use_read_replica = True use_read_replica = True
@allow_permission( @allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
)
def get(self, request, slug): def get(self, request, slug):
# Watching Issues Count # Watching Issues Count
unread_notifications_count = ( unread_notifications_count = (
@ -270,31 +226,23 @@ class UnreadNotificationEndpoint(BaseAPIView):
class MarkAllReadNotificationViewSet(BaseViewSet): class MarkAllReadNotificationViewSet(BaseViewSet):
@allow_permission( @allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
)
def create(self, request, slug): def create(self, request, slug):
snoozed = request.data.get("snoozed", False) snoozed = request.data.get("snoozed", False)
archived = request.data.get("archived", False) archived = request.data.get("archived", False)
type = request.data.get("type", "all") type = request.data.get("type", "all")
notifications = ( notifications = (
Notification.objects.filter( Notification.objects.filter(workspace__slug=slug, receiver_id=request.user.id, read_at__isnull=True)
workspace__slug=slug, receiver_id=request.user.id, read_at__isnull=True
)
.select_related("workspace", "project", "triggered_by", "receiver") .select_related("workspace", "project", "triggered_by", "receiver")
.order_by("snoozed_till", "-created_at") .order_by("snoozed_till", "-created_at")
) )
# Filter for snoozed notifications # Filter for snoozed notifications
if snoozed: if snoozed:
notifications = notifications.filter( notifications = notifications.filter(Q(snoozed_till__lt=timezone.now()) | Q(snoozed_till__isnull=False))
Q(snoozed_till__lt=timezone.now()) | Q(snoozed_till__isnull=False)
)
else: else:
notifications = notifications.filter( notifications = notifications.filter(Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True))
Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True)
)
# Filter for archived or unarchive # Filter for archived or unarchive
if archived: if archived:
@ -304,16 +252,16 @@ class MarkAllReadNotificationViewSet(BaseViewSet):
# Subscribed issues # Subscribed issues
if type == "watching": if type == "watching":
issue_ids = IssueSubscriber.objects.filter( issue_ids = IssueSubscriber.objects.filter(workspace__slug=slug, subscriber_id=request.user.id).values_list(
workspace__slug=slug, subscriber_id=request.user.id "issue_id", flat=True
).values_list("issue_id", flat=True) )
notifications = notifications.filter(entity_identifier__in=issue_ids) notifications = notifications.filter(entity_identifier__in=issue_ids)
# Assigned Issues # Assigned Issues
if type == "assigned": if type == "assigned":
issue_ids = IssueAssignee.objects.filter( issue_ids = IssueAssignee.objects.filter(workspace__slug=slug, assignee_id=request.user.id).values_list(
workspace__slug=slug, assignee_id=request.user.id "issue_id", flat=True
).values_list("issue_id", flat=True) )
notifications = notifications.filter(entity_identifier__in=issue_ids) notifications = notifications.filter(entity_identifier__in=issue_ids)
# Created issues # Created issues
@ -323,18 +271,16 @@ class MarkAllReadNotificationViewSet(BaseViewSet):
).exists(): ).exists():
notifications = Notification.objects.none() notifications = Notification.objects.none()
else: else:
issue_ids = Issue.objects.filter( issue_ids = Issue.objects.filter(workspace__slug=slug, created_by=request.user).values_list(
workspace__slug=slug, created_by=request.user "pk", flat=True
).values_list("pk", flat=True) )
notifications = notifications.filter(entity_identifier__in=issue_ids) notifications = notifications.filter(entity_identifier__in=issue_ids)
updated_notifications = [] updated_notifications = []
for notification in notifications: for notification in notifications:
notification.read_at = timezone.now() notification.read_at = timezone.now()
updated_notifications.append(notification) updated_notifications.append(notification)
Notification.objects.bulk_update( Notification.objects.bulk_update(updated_notifications, ["read_at"], batch_size=100)
updated_notifications, ["read_at"], batch_size=100
)
return Response({"message": "Successful"}, status=status.HTTP_200_OK) return Response({"message": "Successful"}, status=status.HTTP_200_OK)
@ -344,20 +290,14 @@ class UserNotificationPreferenceEndpoint(BaseAPIView):
# request the object # request the object
def get(self, request): def get(self, request):
user_notification_preference = UserNotificationPreference.objects.get( user_notification_preference = UserNotificationPreference.objects.get(user=request.user)
user=request.user
)
serializer = UserNotificationPreferenceSerializer(user_notification_preference) serializer = UserNotificationPreferenceSerializer(user_notification_preference)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
# update the object # update the object
def patch(self, request): def patch(self, request):
user_notification_preference = UserNotificationPreference.objects.get( user_notification_preference = UserNotificationPreference.objects.get(user=request.user)
user=request.user serializer = UserNotificationPreferenceSerializer(user_notification_preference, data=request.data, partial=True)
)
serializer = UserNotificationPreferenceSerializer(
user_notification_preference, data=request.data, partial=True
)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)

View file

@ -101,9 +101,7 @@ class PageViewSet(BaseViewSet):
.order_by("-is_favorite", "-created_at") .order_by("-is_favorite", "-created_at")
.annotate( .annotate(
project=Exists( project=Exists(
ProjectPage.objects.filter( ProjectPage.objects.filter(page_id=OuterRef("id"), project_id=self.kwargs.get("project_id"))
page_id=OuterRef("id"), project_id=self.kwargs.get("project_id")
)
) )
) )
.annotate( .annotate(
@ -116,9 +114,7 @@ class PageViewSet(BaseViewSet):
Value([], output_field=ArrayField(UUIDField())), Value([], output_field=ArrayField(UUIDField())),
), ),
project_ids=Coalesce( project_ids=Coalesce(
ArrayAgg( ArrayAgg("projects__id", distinct=True, filter=~Q(projects__id=True)),
"projects__id", distinct=True, filter=~Q(projects__id=True)
),
Value([], output_field=ArrayField(UUIDField())), Value([], output_field=ArrayField(UUIDField())),
), ),
) )
@ -149,30 +145,19 @@ class PageViewSet(BaseViewSet):
def partial_update(self, request, slug, project_id, page_id): def partial_update(self, request, slug, project_id, page_id):
try: try:
page = Page.objects.get( page = Page.objects.get(pk=page_id, workspace__slug=slug, projects__id=project_id)
pk=page_id, workspace__slug=slug, projects__id=project_id
)
if page.is_locked: if page.is_locked:
return Response( return Response({"error": "Page is locked"}, status=status.HTTP_400_BAD_REQUEST)
{"error": "Page is locked"}, status=status.HTTP_400_BAD_REQUEST
)
parent = request.data.get("parent", None) parent = request.data.get("parent", None)
if parent: if parent:
_ = Page.objects.get( _ = Page.objects.get(pk=parent, workspace__slug=slug, projects__id=project_id)
pk=parent, workspace__slug=slug, projects__id=project_id
)
# Only update access if the page owner is the requesting user # Only update access if the page owner is the requesting user
if ( if page.access != request.data.get("access", page.access) and page.owned_by_id != request.user.id:
page.access != request.data.get("access", page.access)
and page.owned_by_id != request.user.id
):
return Response( 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, status=status.HTTP_400_BAD_REQUEST,
) )
@ -195,9 +180,7 @@ class PageViewSet(BaseViewSet):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except Page.DoesNotExist: except Page.DoesNotExist:
return Response( 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, status=status.HTTP_400_BAD_REQUEST,
) )
@ -228,13 +211,11 @@ class PageViewSet(BaseViewSet):
) )
if page is None: if page is None:
return Response( return Response({"error": "Page not found"}, status=status.HTTP_404_NOT_FOUND)
{"error": "Page not found"}, status=status.HTTP_404_NOT_FOUND
)
else: else:
issue_ids = PageLog.objects.filter( issue_ids = PageLog.objects.filter(page_id=page_id, entity_name="issue").values_list(
page_id=page_id, entity_name="issue" "entity_identifier", flat=True
).values_list("entity_identifier", flat=True) )
data = PageDetailSerializer(page).data data = PageDetailSerializer(page).data
data["issue_ids"] = issue_ids data["issue_ids"] = issue_ids
if track_visit: if track_visit:
@ -248,18 +229,14 @@ class PageViewSet(BaseViewSet):
return Response(data, status=status.HTTP_200_OK) return Response(data, status=status.HTTP_200_OK)
def lock(self, request, slug, project_id, page_id): def lock(self, request, slug, project_id, page_id):
page = Page.objects.filter( page = Page.objects.filter(pk=page_id, workspace__slug=slug, projects__id=project_id).first()
pk=page_id, workspace__slug=slug, projects__id=project_id
).first()
page.is_locked = True page.is_locked = True
page.save() page.save()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
def unlock(self, request, slug, project_id, page_id): def unlock(self, request, slug, project_id, page_id):
page = Page.objects.filter( page = Page.objects.filter(pk=page_id, workspace__slug=slug, projects__id=project_id).first()
pk=page_id, workspace__slug=slug, projects__id=project_id
).first()
page.is_locked = False page.is_locked = False
page.save() page.save()
@ -268,19 +245,12 @@ class PageViewSet(BaseViewSet):
def access(self, request, slug, project_id, page_id): def access(self, request, slug, project_id, page_id):
access = request.data.get("access", 0) access = request.data.get("access", 0)
page = Page.objects.filter( page = Page.objects.filter(pk=page_id, workspace__slug=slug, projects__id=project_id).first()
pk=page_id, workspace__slug=slug, projects__id=project_id
).first()
# Only update access if the page owner is the requesting user # Only update access if the page owner is the requesting user
if ( if page.access != request.data.get("access", page.access) and page.owned_by_id != request.user.id:
page.access != request.data.get("access", page.access)
and page.owned_by_id != request.user.id
):
return Response( 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, status=status.HTTP_400_BAD_REQUEST,
) )
@ -306,9 +276,7 @@ class PageViewSet(BaseViewSet):
return Response(pages, status=status.HTTP_200_OK) return Response(pages, status=status.HTTP_200_OK)
def archive(self, request, slug, project_id, page_id): def archive(self, request, slug, project_id, page_id):
page = Page.objects.get( page = Page.objects.get(pk=page_id, workspace__slug=slug, projects__id=project_id)
pk=page_id, workspace__slug=slug, projects__id=project_id
)
# only the owner or admin can archive the page # only the owner or admin can archive the page
if ( if (
@ -334,9 +302,7 @@ class PageViewSet(BaseViewSet):
return Response({"archived_at": str(datetime.now())}, status=status.HTTP_200_OK) return Response({"archived_at": str(datetime.now())}, status=status.HTTP_200_OK)
def unarchive(self, request, slug, project_id, page_id): def unarchive(self, request, slug, project_id, page_id):
page = Page.objects.get( page = Page.objects.get(pk=page_id, workspace__slug=slug, projects__id=project_id)
pk=page_id, workspace__slug=slug, projects__id=project_id
)
# only the owner or admin can un archive the page # only the owner or admin can un archive the page
if ( if (
@ -360,9 +326,7 @@ class PageViewSet(BaseViewSet):
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
def destroy(self, request, slug, project_id, page_id): def destroy(self, request, slug, project_id, page_id):
page = Page.objects.get( page = Page.objects.get(pk=page_id, workspace__slug=slug, projects__id=project_id)
pk=page_id, workspace__slug=slug, projects__id=project_id
)
if page.archived_at is None: if page.archived_at is None:
return Response( return Response(
@ -385,9 +349,7 @@ class PageViewSet(BaseViewSet):
) )
# remove parent from all the children # remove parent from all the children
_ = Page.objects.filter( _ = Page.objects.filter(parent_id=page_id, projects__id=project_id, workspace__slug=slug).update(parent=None)
parent_id=page_id, projects__id=project_id, workspace__slug=slug
).update(parent=None)
page.delete() page.delete()
# Delete the user favorite page # Delete the user favorite page
@ -418,9 +380,7 @@ class PageViewSet(BaseViewSet):
.filter(Q(owned_by=request.user) | Q(access=0)) .filter(Q(owned_by=request.user) | Q(access=0))
.annotate( .annotate(
project=Exists( project=Exists(
ProjectPage.objects.filter( ProjectPage.objects.filter(page_id=OuterRef("id"), project_id=self.kwargs.get("project_id"))
page_id=OuterRef("id"), project_id=self.kwargs.get("project_id")
)
) )
) )
.filter(project=True) .filter(project=True)
@ -453,11 +413,7 @@ class PageViewSet(BaseViewSet):
output_field=IntegerField(), output_field=IntegerField(),
) )
), ),
archived_pages=Count( archived_pages=Count(Case(When(archived_at__isnull=False, then=1), output_field=IntegerField())),
Case(
When(archived_at__isnull=False, then=1), output_field=IntegerField()
)
),
) )
return Response(stats, status=status.HTTP_200_OK) return Response(stats, status=status.HTTP_200_OK)
@ -494,9 +450,7 @@ class PagesDescriptionViewSet(BaseViewSet):
def retrieve(self, request, slug, project_id, page_id): def retrieve(self, request, slug, project_id, page_id):
page = ( page = (
Page.objects.filter( Page.objects.filter(pk=page_id, workspace__slug=slug, projects__id=project_id)
pk=page_id, workspace__slug=slug, projects__id=project_id
)
.filter(Q(owned_by=self.request.user) | Q(access=0)) .filter(Q(owned_by=self.request.user) | Q(access=0))
.first() .first()
) )
@ -510,17 +464,13 @@ class PagesDescriptionViewSet(BaseViewSet):
else: else:
yield b"" yield b""
response = StreamingHttpResponse( response = StreamingHttpResponse(stream_data(), content_type="application/octet-stream")
stream_data(), content_type="application/octet-stream"
)
response["Content-Disposition"] = 'attachment; filename="page_description.bin"' response["Content-Disposition"] = 'attachment; filename="page_description.bin"'
return response return response
def partial_update(self, request, slug, project_id, page_id): def partial_update(self, request, slug, project_id, page_id):
page = ( page = (
Page.objects.filter( Page.objects.filter(pk=page_id, workspace__slug=slug, projects__id=project_id)
pk=page_id, workspace__slug=slug, projects__id=project_id
)
.filter(Q(owned_by=self.request.user) | Q(access=0)) .filter(Q(owned_by=self.request.user) | Q(access=0))
.first() .first()
) )
@ -547,18 +497,14 @@ class PagesDescriptionViewSet(BaseViewSet):
) )
# Serialize the existing instance # Serialize the existing instance
existing_instance = json.dumps( existing_instance = json.dumps({"description_html": page.description_html}, cls=DjangoJSONEncoder)
{"description_html": page.description_html}, cls=DjangoJSONEncoder
)
# Use serializer for validation and update # Use serializer for validation and update
serializer = PageBinaryUpdateSerializer(page, data=request.data, partial=True) serializer = PageBinaryUpdateSerializer(page, data=request.data, partial=True)
if serializer.is_valid(): if serializer.is_valid():
# Capture the page transaction # Capture the page transaction
if request.data.get("description_html"): if request.data.get("description_html"):
page_transaction.delay( page_transaction.delay(new_value=request.data, old_value=existing_instance, page_id=page_id)
new_value=request.data, old_value=existing_instance, page_id=page_id
)
# Update the page using serializer # Update the page using serializer
updated_page = serializer.save() updated_page = serializer.save()
@ -578,20 +524,14 @@ class PageDuplicateEndpoint(BaseAPIView):
permission_classes = [ProjectPagePermission] permission_classes = [ProjectPagePermission]
def post(self, request, slug, project_id, page_id): def post(self, request, slug, project_id, page_id):
page = Page.objects.filter( page = Page.objects.filter(pk=page_id, workspace__slug=slug, projects__id=project_id).first()
pk=page_id, workspace__slug=slug, projects__id=project_id
).first()
# check for permission # check for permission
if page.access == Page.PRIVATE_ACCESS and page.owned_by_id != request.user.id: if page.access == Page.PRIVATE_ACCESS and page.owned_by_id != request.user.id:
return Response( return Response({"error": "Permission denied"}, status=status.HTTP_403_FORBIDDEN)
{"error": "Permission denied"}, status=status.HTTP_403_FORBIDDEN
)
# get all the project ids where page is present # get all the project ids where page is present
project_ids = ProjectPage.objects.filter(page_id=page_id).values_list( project_ids = ProjectPage.objects.filter(page_id=page_id).values_list("project_id", flat=True)
"project_id", flat=True
)
page.pk = None page.pk = None
page.name = f"{page.name} (Copy)" page.name = f"{page.name} (Copy)"
@ -610,9 +550,7 @@ class PageDuplicateEndpoint(BaseAPIView):
updated_by_id=page.updated_by_id, updated_by_id=page.updated_by_id,
) )
page_transaction.delay( page_transaction.delay({"description_html": page.description_html}, None, page.id)
{"description_html": page.description_html}, None, page.id
)
# Copy the s3 objects uploaded in the page # Copy the s3 objects uploaded in the page
copy_s3_objects_of_description_and_assets.delay( copy_s3_objects_of_description_and_assets.delay(
@ -627,9 +565,7 @@ class PageDuplicateEndpoint(BaseAPIView):
Page.objects.filter(pk=page.id) Page.objects.filter(pk=page.id)
.annotate( .annotate(
project_ids=Coalesce( project_ids=Coalesce(
ArrayAgg( ArrayAgg("projects__id", distinct=True, filter=~Q(projects__id=True)),
"projects__id", distinct=True, filter=~Q(projects__id=True)
),
Value([], output_field=ArrayField(UUIDField())), Value([], output_field=ArrayField(UUIDField())),
) )
) )

View file

@ -6,27 +6,22 @@ from rest_framework.response import Response
from plane.db.models import PageVersion from plane.db.models import PageVersion
from ..base import BaseAPIView from ..base import BaseAPIView
from plane.app.serializers import PageVersionSerializer, PageVersionDetailSerializer from plane.app.serializers import PageVersionSerializer, PageVersionDetailSerializer
from plane.app.permissions import allow_permission, ROLE
from plane.app.permissions import ProjectPagePermission from plane.app.permissions import ProjectPagePermission
class PageVersionEndpoint(BaseAPIView):
class PageVersionEndpoint(BaseAPIView):
permission_classes = [ProjectPagePermission] permission_classes = [ProjectPagePermission]
def get(self, request, slug, project_id, page_id, pk=None): def get(self, request, slug, project_id, page_id, pk=None):
# Check if pk is provided # Check if pk is provided
if pk: if pk:
# Return a single page version # Return a single page version
page_version = PageVersion.objects.get( page_version = PageVersion.objects.get(workspace__slug=slug, page_id=page_id, pk=pk)
workspace__slug=slug, page_id=page_id, pk=pk
)
# Serialize the page version # Serialize the page version
serializer = PageVersionDetailSerializer(page_version) serializer = PageVersionDetailSerializer(page_version)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
# Return all page versions # Return all page versions
page_versions = PageVersion.objects.filter( page_versions = PageVersion.objects.filter(workspace__slug=slug, page_id=page_id)
workspace__slug=slug, page_id=page_id
)
# Serialize the page versions # Serialize the page versions
serializer = PageVersionSerializer(page_versions, many=True) serializer = PageVersionSerializer(page_versions, many=True)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)

View file

@ -58,9 +58,7 @@ class ProjectViewSet(BaseViewSet):
super() super()
.get_queryset() .get_queryset()
.filter(workspace__slug=self.kwargs.get("slug")) .filter(workspace__slug=self.kwargs.get("slug"))
.select_related( .select_related("workspace", "workspace__owner", "default_assignee", "project_lead")
"workspace", "workspace__owner", "default_assignee", "project_lead"
)
.annotate( .annotate(
is_favorite=Exists( is_favorite=Exists(
UserFavorite.objects.filter( UserFavorite.objects.filter(
@ -98,9 +96,7 @@ class ProjectViewSet(BaseViewSet):
.distinct() .distinct()
) )
@allow_permission( @allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
)
def list_detail(self, request, slug): def list_detail(self, request, slug):
fields = [field for field in request.GET.get("fields", "").split(",") if field] fields = [field for field in request.GET.get("fields", "").split(",") if field]
projects = self.get_queryset().order_by("sort_order", "name") 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"), order_by=request.GET.get("order_by", "-created_at"),
request=request, request=request,
queryset=(projects), queryset=(projects),
on_results=lambda projects: ProjectListSerializer( on_results=lambda projects: ProjectListSerializer(projects, many=True).data,
projects, many=True
).data,
) )
projects = ProjectListSerializer( projects = ProjectListSerializer(projects, many=True, fields=fields if fields else None).data
projects, many=True, fields=fields if fields else None
).data
return Response(projects, status=status.HTTP_200_OK) return Response(projects, status=status.HTTP_200_OK)
@allow_permission( @allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
)
def list(self, request, slug): def list(self, request, slug):
sort_order = ProjectMember.objects.filter( sort_order = ProjectMember.objects.filter(
member=self.request.user, member=self.request.user,
@ -157,9 +147,7 @@ class ProjectViewSet(BaseViewSet):
projects = ( projects = (
Project.objects.filter(workspace__slug=self.kwargs.get("slug")) Project.objects.filter(workspace__slug=self.kwargs.get("slug"))
.select_related( .select_related("workspace", "workspace__owner", "default_assignee", "project_lead")
"workspace", "workspace__owner", "default_assignee", "project_lead"
)
.annotate( .annotate(
member_role=ProjectMember.objects.filter( member_role=ProjectMember.objects.filter(
project_id=OuterRef("pk"), project_id=OuterRef("pk"),
@ -219,9 +207,7 @@ class ProjectViewSet(BaseViewSet):
) )
return Response(projects, status=status.HTTP_200_OK) return Response(projects, status=status.HTTP_200_OK)
@allow_permission( @allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
)
def retrieve(self, request, slug, pk): def retrieve(self, request, slug, pk):
project = ( project = (
self.get_queryset() self.get_queryset()
@ -234,9 +220,7 @@ class ProjectViewSet(BaseViewSet):
).first() ).first()
if project is None: if project is None:
return Response( return Response({"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND)
{"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND
)
recent_visited_task.delay( recent_visited_task.delay(
slug=slug, slug=slug,
@ -253,9 +237,7 @@ class ProjectViewSet(BaseViewSet):
def create(self, request, slug): def create(self, request, slug):
workspace = Workspace.objects.get(slug=slug) workspace = Workspace.objects.get(slug=slug)
serializer = ProjectSerializer( serializer = ProjectSerializer(data={**request.data}, context={"workspace_id": workspace.id})
data={**request.data}, context={"workspace_id": workspace.id}
)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
@ -266,13 +248,11 @@ class ProjectViewSet(BaseViewSet):
role=ROLE.ADMIN.value, role=ROLE.ADMIN.value,
) )
# Also create the issue property for the user # Also create the issue property for the user
_ = IssueUserProperty.objects.create( _ = IssueUserProperty.objects.create(project_id=serializer.data["id"], user=request.user)
project_id=serializer.data["id"], user=request.user
)
if serializer.data["project_lead"] is not None and str( if serializer.data["project_lead"] is not None and str(serializer.data["project_lead"]) != str(
serializer.data["project_lead"] request.user.id
) != str(request.user.id): ):
ProjectMember.objects.create( ProjectMember.objects.create(
project_id=serializer.data["id"], project_id=serializer.data["id"],
member_id=serializer.data["project_lead"], member_id=serializer.data["project_lead"],
@ -380,9 +360,7 @@ class ProjectViewSet(BaseViewSet):
project = Project.objects.get(pk=pk) project = Project.objects.get(pk=pk)
intake_view = request.data.get("inbox_view", project.intake_view) intake_view = request.data.get("inbox_view", project.intake_view)
current_instance = json.dumps( current_instance = json.dumps(ProjectSerializer(project).data, cls=DjangoJSONEncoder)
ProjectSerializer(project).data, cls=DjangoJSONEncoder
)
if project.archived_at: if project.archived_at:
return Response( return Response(
{"error": "Archived projects cannot be updated"}, {"error": "Archived projects cannot be updated"},
@ -474,9 +452,7 @@ class ProjectArchiveUnarchiveEndpoint(BaseAPIView):
project.archived_at = timezone.now() project.archived_at = timezone.now()
project.save() project.save()
UserFavorite.objects.filter(workspace__slug=slug, project=project_id).delete() UserFavorite.objects.filter(workspace__slug=slug, project=project_id).delete()
return Response( return Response({"archived_at": str(project.archived_at)}, status=status.HTTP_200_OK)
{"archived_at": str(project.archived_at)}, status=status.HTTP_200_OK
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER]) @allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def delete(self, request, slug, project_id): def delete(self, request, slug, project_id):
@ -492,26 +468,18 @@ class ProjectIdentifierEndpoint(BaseAPIView):
name = request.GET.get("name", "").strip().upper() name = request.GET.get("name", "").strip().upper()
if name == "": if name == "":
return Response( return Response({"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST)
{"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
)
exists = ProjectIdentifier.objects.filter( exists = ProjectIdentifier.objects.filter(name=name, workspace__slug=slug).values("id", "name", "project")
name=name, workspace__slug=slug
).values("id", "name", "project")
return Response( return Response({"exists": len(exists), "identifiers": exists}, status=status.HTTP_200_OK)
{"exists": len(exists), "identifiers": exists}, status=status.HTTP_200_OK
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE") @allow_permission([ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
def delete(self, request, slug): def delete(self, request, slug):
name = request.data.get("name", "").strip().upper() name = request.data.get("name", "").strip().upper()
if name == "": if name == "":
return Response( return Response({"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST)
{"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
)
if Project.objects.filter(identifier=name, workspace__slug=slug).exists(): if Project.objects.filter(identifier=name, workspace__slug=slug).exists():
return Response( return Response(
@ -528,9 +496,7 @@ class ProjectUserViewsEndpoint(BaseAPIView):
def post(self, request, slug, project_id): def post(self, request, slug, project_id):
project = Project.objects.get(pk=project_id, workspace__slug=slug) project = Project.objects.get(pk=project_id, workspace__slug=slug)
project_member = ProjectMember.objects.filter( project_member = ProjectMember.objects.filter(member=request.user, project=project, is_active=True).first()
member=request.user, project=project, is_active=True
).first()
if project_member is None: if project_member is None:
return Response({"error": "Forbidden"}, status=status.HTTP_403_FORBIDDEN) return Response({"error": "Forbidden"}, status=status.HTTP_403_FORBIDDEN)
@ -559,9 +525,7 @@ class ProjectFavoritesViewSet(BaseViewSet):
.get_queryset() .get_queryset()
.filter(workspace__slug=self.kwargs.get("slug")) .filter(workspace__slug=self.kwargs.get("slug"))
.filter(user=self.request.user) .filter(user=self.request.user)
.select_related( .select_related("project", "project__project_lead", "project__default_assignee")
"project", "project__project_lead", "project__default_assignee"
)
.select_related("workspace", "workspace__owner") .select_related("workspace", "workspace__owner")
) )

View file

@ -52,9 +52,7 @@ class ProjectInvitationsViewset(BaseViewSet):
# Check if email is provided # Check if email is provided
if not emails: if not emails:
return Response( return Response({"error": "Emails are required"}, status=status.HTTP_400_BAD_REQUEST)
{"error": "Emails are required"}, status=status.HTTP_400_BAD_REQUEST
)
for email in emails: for email in emails:
workspace_role = WorkspaceMember.objects.filter( workspace_role = WorkspaceMember.objects.filter(
@ -62,11 +60,7 @@ class ProjectInvitationsViewset(BaseViewSet):
).role ).role
if workspace_role in [5, 20] and workspace_role != email.get("role", 5): if workspace_role in [5, 20] and workspace_role != email.get("role", 5):
return Response( return Response({"error": "You cannot invite a user with different role than workspace role"})
{
"error": "You cannot invite a user with different role than workspace role"
}
)
workspace = Workspace.objects.get(slug=slug) workspace = Workspace.objects.get(slug=slug)
@ -91,7 +85,7 @@ class ProjectInvitationsViewset(BaseViewSet):
except ValidationError: except ValidationError:
return Response( 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, status=status.HTTP_400_BAD_REQUEST,
) )
@ -112,9 +106,7 @@ class ProjectInvitationsViewset(BaseViewSet):
request.user.email, request.user.email,
) )
return Response( return Response({"message": "Email sent successfully"}, status=status.HTTP_200_OK)
{"message": "Email sent successfully"}, status=status.HTTP_200_OK
)
class UserProjectInvitationsViewset(BaseViewSet): class UserProjectInvitationsViewset(BaseViewSet):
@ -134,20 +126,13 @@ class UserProjectInvitationsViewset(BaseViewSet):
project_ids = request.data.get("project_ids", []) project_ids = request.data.get("project_ids", [])
# Get the workspace user role # Get the workspace user role
workspace_member = WorkspaceMember.objects.get( workspace_member = WorkspaceMember.objects.get(member=request.user, workspace__slug=slug, is_active=True)
member=request.user, workspace__slug=slug, is_active=True
)
# Get all the projects # Get all the projects
projects = Project.objects.filter( projects = Project.objects.filter(id__in=project_ids, workspace__slug=slug).only("id", "network")
id__in=project_ids, workspace__slug=slug
).only("id", "network")
# Check if user has permission to join each project # Check if user has permission to join each project
for project in projects: for project in projects:
if ( if project.network == ProjectNetwork.SECRET.value and workspace_member.role != ROLE.ADMIN.value:
project.network == ProjectNetwork.SECRET.value
and workspace_member.role != ROLE.ADMIN.value
):
return Response( return Response(
{"error": "Only workspace admins can join private project"}, {"error": "Only workspace admins can join private project"},
status=status.HTTP_403_FORBIDDEN, status=status.HTTP_403_FORBIDDEN,
@ -157,9 +142,9 @@ class UserProjectInvitationsViewset(BaseViewSet):
workspace = workspace_member.workspace workspace = workspace_member.workspace
# If the user was already part of workspace # If the user was already part of workspace
_ = ProjectMember.objects.filter( _ = ProjectMember.objects.filter(workspace__slug=slug, project_id__in=project_ids, member=request.user).update(
workspace__slug=slug, project_id__in=project_ids, member=request.user is_active=True
).update(is_active=True) )
ProjectMember.objects.bulk_create( ProjectMember.objects.bulk_create(
[ [
@ -188,18 +173,14 @@ class UserProjectInvitationsViewset(BaseViewSet):
ignore_conflicts=True, ignore_conflicts=True,
) )
return Response( return Response({"message": "Projects joined successfully"}, status=status.HTTP_201_CREATED)
{"message": "Projects joined successfully"}, status=status.HTTP_201_CREATED
)
class ProjectJoinEndpoint(BaseAPIView): class ProjectJoinEndpoint(BaseAPIView):
permission_classes = [AllowAny] permission_classes = [AllowAny]
def post(self, request, slug, project_id, pk): def post(self, request, slug, project_id, pk):
project_invite = ProjectMemberInvite.objects.get( project_invite = ProjectMemberInvite.objects.get(pk=pk, project_id=project_id, workspace__slug=slug)
pk=pk, project_id=project_id, workspace__slug=slug
)
email = request.data.get("email", "") email = request.data.get("email", "")
@ -219,9 +200,7 @@ class ProjectJoinEndpoint(BaseAPIView):
user = User.objects.filter(email=email).first() user = User.objects.filter(email=email).first()
# Check if user is a part of workspace # Check if user is a part of workspace
workspace_member = WorkspaceMember.objects.filter( workspace_member = WorkspaceMember.objects.filter(workspace__slug=slug, member=user).first()
workspace__slug=slug, member=user
).first()
# Add him to workspace # Add him to workspace
if workspace_member is None: if workspace_member is None:
_ = WorkspaceMember.objects.create( _ = WorkspaceMember.objects.create(
@ -266,8 +245,6 @@ class ProjectJoinEndpoint(BaseAPIView):
) )
def get(self, request, slug, project_id, pk): def get(self, request, slug, project_id, pk):
project_invitation = ProjectMemberInvite.objects.get( project_invitation = ProjectMemberInvite.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
workspace__slug=slug, project_id=project_id, pk=pk
)
serializer = ProjectMemberInviteSerializer(project_invitation) serializer = ProjectMemberInviteSerializer(project_invitation)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)

View file

@ -57,9 +57,7 @@ class ProjectMemberViewSet(BaseViewSet):
bulk_issue_props = [] bulk_issue_props = []
# Create a dictionary of the member_id and their roles # Create a dictionary of the member_id and their roles
member_roles = { member_roles = {member.get("member_id"): member.get("role") for member in members}
member.get("member_id"): member.get("role") for member in members
}
# check the workspace role of the new user # check the workspace role of the new user
for member in member_roles: for member in member_roles:
@ -68,17 +66,13 @@ class ProjectMemberViewSet(BaseViewSet):
).role ).role
if workspace_member_role in [20] and member_roles.get(member) in [5, 15]: if workspace_member_role in [20] and member_roles.get(member) in [5, 15]:
return Response( 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, status=status.HTTP_400_BAD_REQUEST,
) )
if workspace_member_role in [5] and member_roles.get(member) in [15, 20]: if workspace_member_role in [5] and member_roles.get(member) in [15, 20]:
return Response( 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, status=status.HTTP_400_BAD_REQUEST,
) )
@ -92,9 +86,7 @@ class ProjectMemberViewSet(BaseViewSet):
bulk_project_members.append(project_member) bulk_project_members.append(project_member)
# Update the roles of the existing members # Update the roles of the existing members
ProjectMember.objects.bulk_update( ProjectMember.objects.bulk_update(bulk_project_members, ["is_active", "role"], batch_size=100)
bulk_project_members, ["is_active", "role"], batch_size=100
)
# Get the list of project members of the requested workspace with the given slug # Get the list of project members of the requested workspace with the given slug
project_members = ( project_members = (
@ -134,13 +126,9 @@ class ProjectMemberViewSet(BaseViewSet):
) )
# Bulk create the project members and issue properties # Bulk create the project members and issue properties
project_members = ProjectMember.objects.bulk_create( project_members = ProjectMember.objects.bulk_create(bulk_project_members, batch_size=10, ignore_conflicts=True)
bulk_project_members, batch_size=10, ignore_conflicts=True
)
_ = IssueUserProperty.objects.bulk_create( _ = IssueUserProperty.objects.bulk_create(bulk_issue_props, batch_size=10, ignore_conflicts=True)
bulk_issue_props, batch_size=10, ignore_conflicts=True
)
project_members = ProjectMember.objects.filter( project_members = ProjectMember.objects.filter(
project_id=project_id, project_id=project_id,
@ -172,16 +160,12 @@ class ProjectMemberViewSet(BaseViewSet):
member__member_workspace__is_active=True, member__member_workspace__is_active=True,
).select_related("project", "member", "workspace") ).select_related("project", "member", "workspace")
serializer = ProjectMemberRoleSerializer( serializer = ProjectMemberRoleSerializer(project_members, fields=("id", "member", "role"), many=True)
project_members, fields=("id", "member", "role"), many=True
)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def partial_update(self, request, slug, project_id, pk): def partial_update(self, request, slug, project_id, pk):
project_member = ProjectMember.objects.get( project_member = ProjectMember.objects.get(pk=pk, workspace__slug=slug, project_id=project_id, is_active=True)
pk=pk, workspace__slug=slug, project_id=project_id, is_active=True
)
# Fetch the workspace role of the project member # Fetch the workspace role of the project member
workspace_role = WorkspaceMember.objects.get( workspace_role = WorkspaceMember.objects.get(
@ -203,20 +187,15 @@ class ProjectMemberViewSet(BaseViewSet):
is_active=True, is_active=True,
) )
if workspace_role in [5] and int( if workspace_role in [5] and int(request.data.get("role", project_member.role)) in [15, 20]:
request.data.get("role", project_member.role)
) in [15, 20]:
return Response( 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, status=status.HTTP_400_BAD_REQUEST,
) )
if ( if (
"role" in request.data "role" in request.data
and int(request.data.get("role", project_member.role)) and int(request.data.get("role", project_member.role)) > requested_project_member.role
> requested_project_member.role
and not is_workspace_admin and not is_workspace_admin
): ):
return Response( return Response(
@ -224,9 +203,7 @@ class ProjectMemberViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
serializer = ProjectMemberSerializer( serializer = ProjectMemberSerializer(project_member, data=request.data, partial=True)
project_member, data=request.data, partial=True
)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
@ -252,9 +229,7 @@ class ProjectMemberViewSet(BaseViewSet):
# User cannot remove himself # User cannot remove himself
if str(project_member.id) == str(requesting_project_member.id): if str(project_member.id) == str(requesting_project_member.id):
return Response( 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, status=status.HTTP_400_BAD_REQUEST,
) )
# User cannot deactivate higher role # User cannot deactivate higher role
@ -287,7 +262,7 @@ class ProjectMemberViewSet(BaseViewSet):
): ):
return Response( 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, status=status.HTTP_400_BAD_REQUEST,
) )
@ -323,7 +298,5 @@ class UserProjectRolesEndpoint(BaseAPIView):
member__member_workspace__is_active=True, member__member_workspace__is_active=True,
).values("project_id", "role") ).values("project_id", "role")
project_members = { project_members = {str(member["project_id"]): member["role"] for member in project_members}
str(member["project_id"]): member["role"] for member in project_members
}
return Response(project_members, status=status.HTTP_200_OK) return Response(project_members, status=status.HTTP_200_OK)

View file

@ -120,9 +120,7 @@ class GlobalSearchEndpoint(BaseAPIView):
if workspace_search == "false" and project_id: if workspace_search == "false" and project_id:
cycles = cycles.filter(project_id=project_id) cycles = cycles.filter(project_id=project_id)
return cycles.distinct().values( return cycles.distinct().values("name", "id", "project_id", "project__identifier", "workspace__slug")
"name", "id", "project_id", "project__identifier", "workspace__slug"
)
def filter_modules(self, query, slug, project_id, workspace_search): def filter_modules(self, query, slug, project_id, workspace_search):
fields = ["name"] fields = ["name"]
@ -141,9 +139,7 @@ class GlobalSearchEndpoint(BaseAPIView):
if workspace_search == "false" and project_id: if workspace_search == "false" and project_id:
modules = modules.filter(project_id=project_id) modules = modules.filter(project_id=project_id)
return modules.distinct().values( return modules.distinct().values("name", "id", "project_id", "project__identifier", "workspace__slug")
"name", "id", "project_id", "project__identifier", "workspace__slug"
)
def filter_pages(self, query, slug, project_id, workspace_search): def filter_pages(self, query, slug, project_id, workspace_search):
fields = ["name"] fields = ["name"]
@ -161,9 +157,7 @@ class GlobalSearchEndpoint(BaseAPIView):
) )
.annotate( .annotate(
project_ids=Coalesce( project_ids=Coalesce(
ArrayAgg( ArrayAgg("projects__id", distinct=True, filter=~Q(projects__id=True)),
"projects__id", distinct=True, filter=~Q(projects__id=True)
),
Value([], output_field=ArrayField(UUIDField())), Value([], output_field=ArrayField(UUIDField())),
) )
) )
@ -180,17 +174,13 @@ class GlobalSearchEndpoint(BaseAPIView):
) )
if workspace_search == "false" and project_id: if workspace_search == "false" and project_id:
project_subquery = ProjectPage.objects.filter( project_subquery = ProjectPage.objects.filter(page_id=OuterRef("id"), project_id=project_id).values_list(
page_id=OuterRef("id"), project_id=project_id "project_id", flat=True
).values_list("project_id", flat=True)[:1] )[:1]
pages = pages.annotate(project_id=Subquery(project_subquery)).filter( pages = pages.annotate(project_id=Subquery(project_subquery)).filter(project_id=project_id)
project_id=project_id
)
return pages.distinct().values( return pages.distinct().values("name", "id", "project_ids", "project_identifiers", "workspace__slug")
"name", "id", "project_ids", "project_identifiers", "workspace__slug"
)
def filter_views(self, query, slug, project_id, workspace_search): def filter_views(self, query, slug, project_id, workspace_search):
fields = ["name"] fields = ["name"]
@ -209,9 +199,7 @@ class GlobalSearchEndpoint(BaseAPIView):
if workspace_search == "false" and project_id: if workspace_search == "false" and project_id:
issue_views = issue_views.filter(project_id=project_id) issue_views = issue_views.filter(project_id=project_id)
return issue_views.distinct().values( return issue_views.distinct().values("name", "id", "project_id", "project__identifier", "workspace__slug")
"name", "id", "project_id", "project__identifier", "workspace__slug"
)
def get(self, request, slug): def get(self, request, slug):
query = request.query_params.get("search", False) query = request.query_params.get("search", False)
@ -308,9 +296,7 @@ class SearchEndpoint(BaseAPIView):
if issue_id: if issue_id:
issue_created_by = ( issue_created_by = (
Issue.objects.filter(id=issue_id) Issue.objects.filter(id=issue_id).values_list("created_by_id", flat=True).first()
.values_list("created_by_id", flat=True)
.first()
) )
users = ( users = (
users.filter(Q(role__gt=10) | Q(member_id=issue_created_by)) users.filter(Q(role__gt=10) | Q(member_id=issue_created_by))
@ -344,15 +330,12 @@ class SearchEndpoint(BaseAPIView):
projects = ( projects = (
Project.objects.filter( Project.objects.filter(
q, q,
Q(project_projectmember__member=self.request.user) Q(project_projectmember__member=self.request.user) | Q(network=2),
| Q(network=2),
workspace__slug=slug, workspace__slug=slug,
) )
.order_by("-created_at") .order_by("-created_at")
.distinct() .distinct()
.values( .values("name", "id", "identifier", "logo_props", "workspace__slug")[:count]
"name", "id", "identifier", "logo_props", "workspace__slug"
)[:count]
) )
response_data["project"] = list(projects) response_data["project"] = list(projects)
@ -411,20 +394,16 @@ class SearchEndpoint(BaseAPIView):
.annotate( .annotate(
status=Case( status=Case(
When( When(
Q(start_date__lte=timezone.now()) Q(start_date__lte=timezone.now()) & Q(end_date__gte=timezone.now()),
& Q(end_date__gte=timezone.now()),
then=Value("CURRENT"), then=Value("CURRENT"),
), ),
When( When(
start_date__gt=timezone.now(), start_date__gt=timezone.now(),
then=Value("UPCOMING"), then=Value("UPCOMING"),
), ),
When(end_date__lt=timezone.now(), then=Value("COMPLETED")),
When( When(
end_date__lt=timezone.now(), then=Value("COMPLETED") Q(start_date__isnull=True) & Q(end_date__isnull=True),
),
When(
Q(start_date__isnull=True)
& Q(end_date__isnull=True),
then=Value("DRAFT"), then=Value("DRAFT"),
), ),
default=Value("DRAFT"), default=Value("DRAFT"),
@ -542,9 +521,7 @@ class SearchEndpoint(BaseAPIView):
) )
) )
.order_by("-created_at") .order_by("-created_at")
.values( .values("member__avatar_url", "member__display_name", "member__id")[:count]
"member__avatar_url", "member__display_name", "member__id"
)[:count]
) )
response_data["user_mention"] = list(users) response_data["user_mention"] = list(users)
@ -558,15 +535,12 @@ class SearchEndpoint(BaseAPIView):
projects = ( projects = (
Project.objects.filter( Project.objects.filter(
q, q,
Q(project_projectmember__member=self.request.user) Q(project_projectmember__member=self.request.user) | Q(network=2),
| Q(network=2),
workspace__slug=slug, workspace__slug=slug,
) )
.order_by("-created_at") .order_by("-created_at")
.distinct() .distinct()
.values( .values("name", "id", "identifier", "logo_props", "workspace__slug")[:count]
"name", "id", "identifier", "logo_props", "workspace__slug"
)[:count]
) )
response_data["project"] = list(projects) response_data["project"] = list(projects)
@ -623,20 +597,16 @@ class SearchEndpoint(BaseAPIView):
.annotate( .annotate(
status=Case( status=Case(
When( When(
Q(start_date__lte=timezone.now()) Q(start_date__lte=timezone.now()) & Q(end_date__gte=timezone.now()),
& Q(end_date__gte=timezone.now()),
then=Value("CURRENT"), then=Value("CURRENT"),
), ),
When( When(
start_date__gt=timezone.now(), start_date__gt=timezone.now(),
then=Value("UPCOMING"), then=Value("UPCOMING"),
), ),
When(end_date__lt=timezone.now(), then=Value("COMPLETED")),
When( When(
end_date__lt=timezone.now(), then=Value("COMPLETED") Q(start_date__isnull=True) & Q(end_date__isnull=True),
),
When(
Q(start_date__isnull=True)
& Q(end_date__isnull=True),
then=Value("DRAFT"), then=Value("DRAFT"),
), ),
default=Value("DRAFT"), default=Value("DRAFT"),

View file

@ -30,23 +30,17 @@ class IssueSearchEndpoint(BaseAPIView):
return issues return issues
def search_issues_and_excluding_parent( def search_issues_and_excluding_parent(self, issues: QuerySet, issue_id: str) -> QuerySet:
self, issues: QuerySet, issue_id: str
) -> QuerySet:
""" """
Search issues and epics by query excluding the parent Search issues and epics by query excluding the parent
""" """
issue = Issue.issue_objects.filter(pk=issue_id).first() issue = Issue.issue_objects.filter(pk=issue_id).first()
if issue: if issue:
issues = issues.filter( issues = issues.filter(~Q(pk=issue_id), ~Q(pk=issue.parent_id), ~Q(parent_id=issue_id))
~Q(pk=issue_id), ~Q(pk=issue.parent_id), ~Q(parent_id=issue_id)
)
return issues return issues
def filter_issues_excluding_related_issues( def filter_issues_excluding_related_issues(self, issue_id: str, issues: QuerySet) -> QuerySet:
self, issue_id: str, issues: QuerySet
) -> QuerySet:
""" """
Filter issues excluding related issues Filter issues excluding related issues
""" """
@ -81,18 +75,14 @@ class IssueSearchEndpoint(BaseAPIView):
""" """
Exclude issues in cycles Exclude issues in cycles
""" """
issues = issues.exclude( issues = issues.exclude(Q(issue_cycle__isnull=False) & Q(issue_cycle__deleted_at__isnull=True))
Q(issue_cycle__isnull=False) & Q(issue_cycle__deleted_at__isnull=True)
)
return issues return issues
def exclude_issues_in_module(self, issues: QuerySet, module: str) -> QuerySet: def exclude_issues_in_module(self, issues: QuerySet, module: str) -> QuerySet:
""" """
Exclude issues in a module Exclude issues in a module
""" """
issues = issues.exclude( issues = issues.exclude(Q(issue_module__module=module) & Q(issue_module__deleted_at__isnull=True))
Q(issue_module__module=module) & Q(issue_module__deleted_at__isnull=True)
)
return issues return issues
def filter_issues_without_target_date(self, issues: QuerySet) -> QuerySet: def filter_issues_without_target_date(self, issues: QuerySet) -> QuerySet:

View file

@ -57,9 +57,7 @@ class StateViewSet(BaseViewSet):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def partial_update(self, request, slug, project_id, pk): def partial_update(self, request, slug, project_id, pk):
try: try:
state = State.objects.get( state = State.objects.get(pk=pk, project_id=project_id, workspace__slug=slug)
pk=pk, project_id=project_id, workspace__slug=slug
)
serializer = StateSerializer(state, data=request.data, partial=True) serializer = StateSerializer(state, data=request.data, partial=True)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
@ -103,20 +101,14 @@ class StateViewSet(BaseViewSet):
@allow_permission([ROLE.ADMIN]) @allow_permission([ROLE.ADMIN])
def mark_as_default(self, request, slug, project_id, pk): def mark_as_default(self, request, slug, project_id, pk):
# Select all the states which are marked as default # Select all the states which are marked as default
_ = State.objects.filter( _ = State.objects.filter(workspace__slug=slug, project_id=project_id, default=True).update(default=False)
workspace__slug=slug, project_id=project_id, default=True _ = State.objects.filter(workspace__slug=slug, project_id=project_id, pk=pk).update(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) return Response(status=status.HTTP_204_NO_CONTENT)
@invalidate_cache(path="workspaces/:slug/states/", url_params=True, user=False) @invalidate_cache(path="workspaces/:slug/states/", url_params=True, user=False)
@allow_permission([ROLE.ADMIN]) @allow_permission([ROLE.ADMIN])
def destroy(self, request, slug, project_id, pk): def destroy(self, request, slug, project_id, pk):
state = State.objects.get( state = State.objects.get(is_triage=False, pk=pk, project_id=project_id, workspace__slug=slug)
is_triage=False, pk=pk, project_id=project_id, workspace__slug=slug
)
if state.default: if state.default:
return Response( return Response(

View file

@ -187,10 +187,7 @@ class TimezoneEndpoint(APIView):
total_seconds = int(current_utc_offset.total_seconds()) total_seconds = int(current_utc_offset.total_seconds())
hours_offset = total_seconds // 3600 hours_offset = total_seconds // 3600
minutes_offset = abs(total_seconds % 3600) // 60 minutes_offset = abs(total_seconds % 3600) // 60
offset = ( offset = f"{'+' if hours_offset >= 0 else '-'}{abs(hours_offset):02}:{minutes_offset:02}"
f"{'+' if hours_offset >= 0 else '-'}"
f"{abs(hours_offset):02}:{minutes_offset:02}"
)
timezone_value = { timezone_value = {
"offset": int(current_offset), "offset": int(current_offset),

View file

@ -63,9 +63,7 @@ class UserEndpoint(BaseViewSet):
def retrieve_instance_admin(self, request): def retrieve_instance_admin(self, request):
instance = Instance.objects.first() instance = Instance.objects.first()
is_admin = InstanceAdmin.objects.filter( is_admin = InstanceAdmin.objects.filter(instance=instance, user=request.user).exists()
instance=instance, user=request.user
).exists()
return Response({"is_instance_admin": is_admin}, status=status.HTTP_200_OK) return Response({"is_instance_admin": is_admin}, status=status.HTTP_200_OK)
def partial_update(self, request, *args, **kwargs): def partial_update(self, request, *args, **kwargs):
@ -78,18 +76,14 @@ class UserEndpoint(BaseViewSet):
# Instance admin check # Instance admin check
if InstanceAdmin.objects.filter(user=user).exists(): if InstanceAdmin.objects.filter(user=user).exists():
return Response( 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, status=status.HTTP_400_BAD_REQUEST,
) )
projects_to_deactivate = [] projects_to_deactivate = []
workspaces_to_deactivate = [] workspaces_to_deactivate = []
projects = ProjectMember.objects.filter( projects = ProjectMember.objects.filter(member=request.user, is_active=True).annotate(
member=request.user, is_active=True
).annotate(
other_admin_exists=Count( other_admin_exists=Count(
Case( Case(
When(Q(role=20, is_active=True) & ~Q(member=request.user), then=1), 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) projects_to_deactivate.append(project)
else: else:
return Response( 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, status=status.HTTP_400_BAD_REQUEST,
) )
workspaces = WorkspaceMember.objects.filter( workspaces = WorkspaceMember.objects.filter(member=request.user, is_active=True).annotate(
member=request.user, is_active=True
).annotate(
other_admin_exists=Count( other_admin_exists=Count(
Case( Case(
When(Q(role=20, is_active=True) & ~Q(member=request.user), then=1), 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) workspaces_to_deactivate.append(workspace)
else: else:
return Response( 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, status=status.HTTP_400_BAD_REQUEST,
) )
ProjectMember.objects.bulk_update( ProjectMember.objects.bulk_update(projects_to_deactivate, ["is_active"], batch_size=100)
projects_to_deactivate, ["is_active"], batch_size=100
)
WorkspaceMember.objects.bulk_update( WorkspaceMember.objects.bulk_update(workspaces_to_deactivate, ["is_active"], batch_size=100)
workspaces_to_deactivate, ["is_active"], batch_size=100
)
# Delete all workspace invites # Delete all workspace invites
WorkspaceMemberInvite.objects.filter(email=user.email).delete() WorkspaceMemberInvite.objects.filter(email=user.email).delete()
@ -224,9 +208,7 @@ class UserActivityEndpoint(BaseAPIView, BasePaginator):
order_by=request.GET.get("order_by", "-created_at"), order_by=request.GET.get("order_by", "-created_at"),
request=request, request=request,
queryset=queryset, queryset=queryset,
on_results=lambda issue_activities: IssueActivitySerializer( on_results=lambda issue_activities: IssueActivitySerializer(issue_activities, many=True).data,
issue_activities, many=True
).data,
) )

View file

@ -64,34 +64,22 @@ class WorkspaceViewViewSet(BaseViewSet):
.distinct() .distinct()
) )
@allow_permission( @allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
)
def list(self, request, slug): def list(self, request, slug):
queryset = self.get_queryset() queryset = self.get_queryset()
fields = [field for field in request.GET.get("fields", "").split(",") if field] fields = [field for field in request.GET.get("fields", "").split(",") if field]
if WorkspaceMember.objects.filter( if WorkspaceMember.objects.filter(workspace__slug=slug, member=request.user, role=5, is_active=True).exists():
workspace__slug=slug, member=request.user, role=5, is_active=True
).exists():
queryset = queryset.filter(owned_by=request.user) queryset = queryset.filter(owned_by=request.user)
views = IssueViewSerializer( views = IssueViewSerializer(queryset, many=True, fields=fields if fields else None).data
queryset, many=True, fields=fields if fields else None
).data
return Response(views, status=status.HTTP_200_OK) return Response(views, status=status.HTTP_200_OK)
@allow_permission( @allow_permission(allowed_roles=[], level="WORKSPACE", creator=True, model=IssueView)
allowed_roles=[], level="WORKSPACE", creator=True, model=IssueView
)
def partial_update(self, request, slug, pk): def partial_update(self, request, slug, pk):
with transaction.atomic(): with transaction.atomic():
workspace_view = IssueView.objects.select_for_update().get( workspace_view = IssueView.objects.select_for_update().get(pk=pk, workspace__slug=slug)
pk=pk, workspace__slug=slug
)
if workspace_view.is_locked: if workspace_view.is_locked:
return Response( return Response({"error": "view is locked"}, status=status.HTTP_400_BAD_REQUEST)
{"error": "view is locked"}, status=status.HTTP_400_BAD_REQUEST
)
# Only update the view if owner is updating # Only update the view if owner is updating
if workspace_view.owned_by_id != request.user.id: if workspace_view.owned_by_id != request.user.id:
@ -100,9 +88,7 @@ class WorkspaceViewViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
serializer = IssueViewSerializer( serializer = IssueViewSerializer(workspace_view, data=request.data, partial=True)
workspace_view, data=request.data, partial=True
)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
@ -121,9 +107,7 @@ class WorkspaceViewViewSet(BaseViewSet):
) )
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
@allow_permission( @allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE", creator=True, model=IssueView)
allowed_roles=[ROLE.ADMIN], level="WORKSPACE", creator=True, model=IssueView
)
def destroy(self, request, slug, pk): def destroy(self, request, slug, pk):
workspace_view = IssueView.objects.get(pk=pk, workspace__slug=slug) workspace_view = IssueView.objects.get(pk=pk, workspace__slug=slug)
@ -177,9 +161,7 @@ class WorkspaceViewIssuesViewSet(BaseViewSet):
return ( return (
issues.annotate( issues.annotate(
cycle_id=Subquery( cycle_id=Subquery(
CycleIssue.objects.filter( CycleIssue.objects.filter(issue=OuterRef("id"), deleted_at__isnull=True).values("cycle_id")[:1]
issue=OuterRef("id"), deleted_at__isnull=True
).values("cycle_id")[:1]
) )
) )
.annotate( .annotate(
@ -227,9 +209,7 @@ class WorkspaceViewIssuesViewSet(BaseViewSet):
return Issue.issue_objects.filter(workspace__slug=self.kwargs.get("slug")) return Issue.issue_objects.filter(workspace__slug=self.kwargs.get("slug"))
@method_decorator(gzip_page) @method_decorator(gzip_page)
@allow_permission( @allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
)
def list(self, request, slug): def list(self, request, slug):
issue_queryset = self.get_queryset() issue_queryset = self.get_queryset()
@ -274,9 +254,7 @@ class IssueViewViewSet(BaseViewSet):
model = IssueView model = IssueView
def perform_create(self, serializer): def perform_create(self, serializer):
serializer.save( serializer.save(project_id=self.kwargs.get("project_id"), owned_by=self.request.user)
project_id=self.kwargs.get("project_id"), owned_by=self.request.user
)
def get_queryset(self): def get_queryset(self):
subquery = UserFavorite.objects.filter( subquery = UserFavorite.objects.filter(
@ -320,9 +298,7 @@ class IssueViewViewSet(BaseViewSet):
): ):
queryset = queryset.filter(owned_by=request.user) queryset = queryset.filter(owned_by=request.user)
fields = [field for field in request.GET.get("fields", "").split(",") if field] fields = [field for field in request.GET.get("fields", "").split(",") if field]
views = IssueViewSerializer( views = IssueViewSerializer(queryset, many=True, fields=fields if fields else None).data
queryset, many=True, fields=fields if fields else None
).data
return Response(views, status=status.HTTP_200_OK) return Response(views, status=status.HTTP_200_OK)
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) @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) @allow_permission(allowed_roles=[], creator=True, model=IssueView)
def partial_update(self, request, slug, project_id, pk): def partial_update(self, request, slug, project_id, pk):
with transaction.atomic(): with transaction.atomic():
issue_view = IssueView.objects.select_for_update().get( issue_view = IssueView.objects.select_for_update().get(pk=pk, workspace__slug=slug, project_id=project_id)
pk=pk, workspace__slug=slug, project_id=project_id
)
if issue_view.is_locked: if issue_view.is_locked:
return Response( return Response({"error": "view is locked"}, status=status.HTTP_400_BAD_REQUEST)
{"error": "view is locked"}, status=status.HTTP_400_BAD_REQUEST
)
# Only update the view if owner is updating # Only update the view if owner is updating
if issue_view.owned_by_id != request.user.id: if issue_view.owned_by_id != request.user.id:
@ -379,9 +351,7 @@ class IssueViewViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
serializer = IssueViewSerializer( serializer = IssueViewSerializer(issue_view, data=request.data, partial=True)
issue_view, data=request.data, partial=True
)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
@ -390,9 +360,7 @@ class IssueViewViewSet(BaseViewSet):
@allow_permission(allowed_roles=[ROLE.ADMIN], creator=True, model=IssueView) @allow_permission(allowed_roles=[ROLE.ADMIN], creator=True, model=IssueView)
def destroy(self, request, slug, project_id, pk): def destroy(self, request, slug, project_id, pk):
project_view = IssueView.objects.get( project_view = IssueView.objects.get(pk=pk, project_id=project_id, workspace__slug=slug)
pk=pk, project_id=project_id, workspace__slug=slug
)
if ( if (
ProjectMember.objects.filter( ProjectMember.objects.filter(
workspace__slug=slug, workspace__slug=slug,

View file

@ -18,9 +18,7 @@ class WebhookEndpoint(BaseAPIView):
def post(self, request, slug): def post(self, request, slug):
workspace = Workspace.objects.get(slug=slug) workspace = Workspace.objects.get(slug=slug)
try: try:
serializer = WebhookSerializer( serializer = WebhookSerializer(data=request.data, context={"request": request})
data=request.data, context={"request": request}
)
if serializer.is_valid(): if serializer.is_valid():
serializer.save(workspace_id=workspace.id) serializer.save(workspace_id=workspace.id)
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
@ -119,8 +117,6 @@ class WebhookSecretRegenerateEndpoint(BaseAPIView):
class WebhookLogsEndpoint(BaseAPIView): class WebhookLogsEndpoint(BaseAPIView):
@allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE") @allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE")
def get(self, request, slug, webhook_id): def get(self, request, slug, webhook_id):
webhook_logs = WebhookLog.objects.filter( webhook_logs = WebhookLog.objects.filter(workspace__slug=slug, webhook=webhook_id)
workspace__slug=slug, webhook=webhook_id
)
serializer = WebhookLogSerializer(webhook_logs, many=True) serializer = WebhookLogSerializer(webhook_logs, many=True)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)

View file

@ -57,9 +57,7 @@ class WorkSpaceViewSet(BaseViewSet):
def get_queryset(self): def get_queryset(self):
member_count = ( member_count = (
WorkspaceMember.objects.filter( WorkspaceMember.objects.filter(workspace=OuterRef("id"), member__is_bot=False, is_active=True)
workspace=OuterRef("id"), member__is_bot=False, is_active=True
)
.order_by() .order_by()
.annotate(count=Func(F("id"), function="Count")) .annotate(count=Func(F("id"), function="Count"))
.values("count") .values("count")
@ -126,9 +124,7 @@ class WorkSpaceViewSet(BaseViewSet):
) )
# Get total members and role # Get total members and role
total_members = WorkspaceMember.objects.filter( total_members = WorkspaceMember.objects.filter(workspace_id=serializer.data["id"]).count()
workspace_id=serializer.data["id"]
).count()
data = serializer.data data = serializer.data
data["total_members"] = total_members data["total_members"] = total_members
data["role"] = 20 data["role"] = 20
@ -179,31 +175,25 @@ class UserWorkSpacesEndpoint(BaseAPIView):
def get(self, request): def get(self, request):
fields = [field for field in request.GET.get("fields", "").split(",") if field] fields = [field for field in request.GET.get("fields", "").split(",") if field]
member_count = ( member_count = (
WorkspaceMember.objects.filter( WorkspaceMember.objects.filter(workspace=OuterRef("id"), member__is_bot=False, is_active=True)
workspace=OuterRef("id"), member__is_bot=False, is_active=True
)
.order_by() .order_by()
.annotate(count=Func(F("id"), function="Count")) .annotate(count=Func(F("id"), function="Count"))
.values("count") .values("count")
) )
role = WorkspaceMember.objects.filter( role = WorkspaceMember.objects.filter(workspace=OuterRef("id"), member=request.user, is_active=True).values(
workspace=OuterRef("id"), member=request.user, is_active=True "role"
).values("role") )
workspace = ( workspace = (
Workspace.objects.prefetch_related( Workspace.objects.prefetch_related(
Prefetch( Prefetch(
"workspace_member", "workspace_member",
queryset=WorkspaceMember.objects.filter( queryset=WorkspaceMember.objects.filter(member=request.user, is_active=True),
member=request.user, is_active=True
),
) )
) )
.annotate(role=role, total_members=member_count) .annotate(role=role, total_members=member_count)
.filter( .filter(workspace_member__member=request.user, workspace_member__is_active=True)
workspace_member__member=request.user, workspace_member__is_active=True
)
.distinct() .distinct()
) )
@ -226,10 +216,7 @@ class WorkSpaceAvailabilityCheckEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
workspace = ( workspace = Workspace.objects.filter(slug=slug).exists() or slug in RESTRICTED_WORKSPACE_SLUGS
Workspace.objects.filter(slug=slug).exists()
or slug in RESTRICTED_WORKSPACE_SLUGS
)
return Response({"status": not workspace}, status=status.HTTP_200_OK) return Response({"status": not workspace}, status=status.HTTP_200_OK)
@ -268,9 +255,7 @@ class UserWorkspaceDashboardEndpoint(BaseAPIView):
.order_by("week_in_month") .order_by("week_in_month")
) )
assigned_issues = Issue.issue_objects.filter( assigned_issues = Issue.issue_objects.filter(workspace__slug=slug, assignees__in=[request.user]).count()
workspace__slug=slug, assignees__in=[request.user]
).count()
pending_issues_count = Issue.issue_objects.filter( pending_issues_count = Issue.issue_objects.filter(
~Q(state__group__in=["completed", "cancelled"]), ~Q(state__group__in=["completed", "cancelled"]),
@ -283,18 +268,14 @@ class UserWorkspaceDashboardEndpoint(BaseAPIView):
).count() ).count()
issues_due_week = ( issues_due_week = (
Issue.issue_objects.filter( Issue.issue_objects.filter(workspace__slug=slug, assignees__in=[request.user])
workspace__slug=slug, assignees__in=[request.user]
)
.annotate(target_week=ExtractWeek("target_date")) .annotate(target_week=ExtractWeek("target_date"))
.filter(target_week=timezone.now().date().isocalendar()[1]) .filter(target_week=timezone.now().date().isocalendar()[1])
.count() .count()
) )
state_distribution = ( state_distribution = (
Issue.issue_objects.filter( Issue.issue_objects.filter(workspace__slug=slug, assignees__in=[request.user])
workspace__slug=slug, assignees__in=[request.user]
)
.annotate(state_group=F("state__group")) .annotate(state_group=F("state__group"))
.values("state_group") .values("state_group")
.annotate(state_count=Count("state_group")) .annotate(state_count=Count("state_group"))
@ -363,9 +344,7 @@ class ExportWorkspaceUserActivityEndpoint(BaseAPIView):
def post(self, request, slug, user_id): def post(self, request, slug, user_id):
if not request.data.get("date"): if not request.data.get("date"):
return Response( return Response({"error": "Date is required"}, status=status.HTTP_400_BAD_REQUEST)
{"error": "Date is required"}, status=status.HTTP_400_BAD_REQUEST
)
user_activities = IssueActivity.objects.filter( user_activities = IssueActivity.objects.filter(
~Q(field__in=["comment", "vote", "reaction", "draft"]), ~Q(field__in=["comment", "vote", "reaction", "draft"]),
@ -403,7 +382,5 @@ class ExportWorkspaceUserActivityEndpoint(BaseAPIView):
] ]
csv_buffer = self.generate_csv_from_rows([header] + rows) csv_buffer = self.generate_csv_from_rows([header] + rows)
response = HttpResponse(csv_buffer.getvalue(), content_type="text/csv") response = HttpResponse(csv_buffer.getvalue(), content_type="text/csv")
response["Content-Disposition"] = ( response["Content-Disposition"] = 'attachment; filename="workspace-user-activity.csv"'
'attachment; filename="workspace-user-activity.csv"'
)
return response return response

View file

@ -49,9 +49,9 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
.prefetch_related("assignees", "labels", "draft_issue_module__module") .prefetch_related("assignees", "labels", "draft_issue_module__module")
.annotate( .annotate(
cycle_id=Subquery( cycle_id=Subquery(
DraftIssueCycle.objects.filter( DraftIssueCycle.objects.filter(draft_issue=OuterRef("id"), deleted_at__isnull=True).values(
draft_issue=OuterRef("id"), deleted_at__isnull=True "cycle_id"
).values("cycle_id")[:1] )[:1]
) )
) )
.annotate( .annotate(
@ -59,10 +59,7 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
ArrayAgg( ArrayAgg(
"labels__id", "labels__id",
distinct=True, distinct=True,
filter=Q( filter=Q(~Q(labels__id__isnull=True) & (Q(draft_label_issue__deleted_at__isnull=True))),
~Q(labels__id__isnull=True)
& (Q(draft_label_issue__deleted_at__isnull=True))
),
), ),
Value([], output_field=ArrayField(UUIDField())), Value([], output_field=ArrayField(UUIDField())),
), ),
@ -94,14 +91,10 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
).distinct() ).distinct()
@method_decorator(gzip_page) @method_decorator(gzip_page)
@allow_permission( @allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
)
def list(self, request, slug): def list(self, request, slug):
filters = issue_filters(request.query_params, "GET") filters = issue_filters(request.query_params, "GET")
issues = ( issues = self.get_queryset().filter(created_by=request.user).order_by("-created_at")
self.get_queryset().filter(created_by=request.user).order_by("-created_at")
)
issues = issues.filter(**filters) issues = issues.filter(**filters)
# List Paginate # List Paginate
@ -111,9 +104,7 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
on_results=lambda issues: DraftIssueSerializer(issues, many=True).data, on_results=lambda issues: DraftIssueSerializer(issues, many=True).data,
) )
@allow_permission( @allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
)
def create(self, request, slug): def create(self, request, slug):
workspace = Workspace.objects.get(slug=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() issue = self.get_queryset().filter(pk=pk, created_by=request.user).first()
if not issue: if not issue:
return Response( return Response({"error": "Issue not found"}, status=status.HTTP_404_NOT_FOUND)
{"error": "Issue not found"}, status=status.HTTP_404_NOT_FOUND
)
project_id = request.data.get("project_id", issue.project_id) 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(status=status.HTTP_204_NO_CONTENT)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@allow_permission( @allow_permission(allowed_roles=[ROLE.ADMIN], creator=True, model=Issue, level="WORKSPACE")
allowed_roles=[ROLE.ADMIN], creator=True, model=Issue, level="WORKSPACE"
)
def retrieve(self, request, slug, pk=None): def retrieve(self, request, slug, pk=None):
issue = self.get_queryset().filter(pk=pk, created_by=request.user).first() issue = self.get_queryset().filter(pk=pk, created_by=request.user).first()
@ -205,9 +192,7 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
serializer = DraftIssueDetailSerializer(issue) serializer = DraftIssueDetailSerializer(issue)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
@allow_permission( @allow_permission(allowed_roles=[ROLE.ADMIN], creator=True, model=DraftIssue, level="WORKSPACE")
allowed_roles=[ROLE.ADMIN], creator=True, model=DraftIssue, level="WORKSPACE"
)
def destroy(self, request, slug, pk=None): def destroy(self, request, slug, pk=None):
draft_issue = DraftIssue.objects.get(workspace__slug=slug, pk=pk) draft_issue = DraftIssue.objects.get(workspace__slug=slug, pk=pk)
draft_issue.delete() draft_issue.delete()
@ -266,9 +251,7 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
current_instance=json.dumps( current_instance=json.dumps(
{ {
"updated_cycle_issues": None, "updated_cycle_issues": None,
"created_cycle_issues": serializers.serialize( "created_cycle_issues": serializers.serialize("json", [created_records]),
"json", [created_records]
),
} }
), ),
epoch=int(timezone.now().timestamp()), epoch=int(timezone.now().timestamp()),

View file

@ -16,9 +16,9 @@ class WorkspaceEstimatesEndpoint(BaseAPIView):
@cache_response(60 * 60 * 2) @cache_response(60 * 60 * 2)
def get(self, request, slug): def get(self, request, slug):
estimate_ids = Project.objects.filter( estimate_ids = Project.objects.filter(workspace__slug=slug, estimate__isnull=False).values_list(
workspace__slug=slug, estimate__isnull=False "estimate_id", flat=True
).values_list("estimate_id", flat=True) )
estimates = ( estimates = (
Estimate.objects.filter(pk__in=estimate_ids, workspace__slug=slug) Estimate.objects.filter(pk__in=estimate_ids, workspace__slug=slug)
.prefetch_related("points") .prefetch_related("points")

View file

@ -19,9 +19,7 @@ class WorkspaceFavoriteEndpoint(BaseAPIView):
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE") @allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
def get(self, request, slug): def get(self, request, slug):
# the second filter is to check if the user is a member of the project # the second filter is to check if the user is a member of the project
favorites = UserFavorite.objects.filter( favorites = UserFavorite.objects.filter(user=request.user, workspace__slug=slug, parent__isnull=True).filter(
user=request.user, workspace__slug=slug, parent__isnull=True
).filter(
Q(project__isnull=True) & ~Q(entity_type="page") Q(project__isnull=True) & ~Q(entity_type="page")
| ( | (
Q(project__isnull=False) Q(project__isnull=False)
@ -62,15 +60,11 @@ class WorkspaceFavoriteEndpoint(BaseAPIView):
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except IntegrityError: except IntegrityError:
return Response( return Response({"error": "Favorite already exists"}, status=status.HTTP_400_BAD_REQUEST)
{"error": "Favorite already exists"}, status=status.HTTP_400_BAD_REQUEST
)
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE") @allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
def patch(self, request, slug, favorite_id): def patch(self, request, slug, favorite_id):
favorite = UserFavorite.objects.get( favorite = UserFavorite.objects.get(user=request.user, workspace__slug=slug, pk=favorite_id)
user=request.user, workspace__slug=slug, pk=favorite_id
)
serializer = UserFavoriteSerializer(favorite, data=request.data, partial=True) serializer = UserFavoriteSerializer(favorite, data=request.data, partial=True)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
@ -79,9 +73,7 @@ class WorkspaceFavoriteEndpoint(BaseAPIView):
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE") @allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
def delete(self, request, slug, favorite_id): def delete(self, request, slug, favorite_id):
favorite = UserFavorite.objects.get( favorite = UserFavorite.objects.get(user=request.user, workspace__slug=slug, pk=favorite_id)
user=request.user, workspace__slug=slug, pk=favorite_id
)
favorite.delete(soft=False) favorite.delete(soft=False)
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
@ -89,9 +81,7 @@ class WorkspaceFavoriteEndpoint(BaseAPIView):
class WorkspaceFavoriteGroupEndpoint(BaseAPIView): class WorkspaceFavoriteGroupEndpoint(BaseAPIView):
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE") @allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
def get(self, request, slug, favorite_id): def get(self, request, slug, favorite_id):
favorites = UserFavorite.objects.filter( favorites = UserFavorite.objects.filter(user=request.user, workspace__slug=slug, parent_id=favorite_id).filter(
user=request.user, workspace__slug=slug, parent_id=favorite_id
).filter(
Q(project__isnull=True) Q(project__isnull=True)
| ( | (
Q(project__isnull=False) Q(project__isnull=False)

View file

@ -20,9 +20,7 @@ class WorkspaceHomePreferenceViewSet(BaseAPIView):
def get(self, request, slug): def get(self, request, slug):
workspace = Workspace.objects.get(slug=slug) workspace = Workspace.objects.get(slug=slug)
get_preference = WorkspaceHomePreference.objects.filter( get_preference = WorkspaceHomePreference.objects.filter(user=request.user, workspace_id=workspace.id)
user=request.user, workspace_id=workspace.id
)
create_preference_keys = [] create_preference_keys = []
@ -55,9 +53,7 @@ class WorkspaceHomePreferenceViewSet(BaseAPIView):
) )
sort_order_counter += 1 sort_order_counter += 1
preference = WorkspaceHomePreference.objects.filter( preference = WorkspaceHomePreference.objects.filter(user=request.user, workspace_id=workspace.id)
user=request.user, workspace_id=workspace.id
)
return Response( return Response(
preference.values("key", "is_enabled", "config", "sort_order"), 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") @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
def patch(self, request, slug, key): def patch(self, request, slug, key):
preference = WorkspaceHomePreference.objects.filter( preference = WorkspaceHomePreference.objects.filter(key=key, workspace__slug=slug, user=request.user).first()
key=key, workspace__slug=slug, user=request.user
).first()
if preference: if preference:
serializer = WorkspaceHomePreferenceSerializer( serializer = WorkspaceHomePreferenceSerializer(preference, data=request.data, partial=True)
preference, data=request.data, partial=True
)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
return Response( return Response({"detail": "Preference not found"}, status=status.HTTP_400_BAD_REQUEST)
{"detail": "Preference not found"}, status=status.HTTP_400_BAD_REQUEST
)

View file

@ -50,23 +50,13 @@ class WorkspaceInvitationsViewset(BaseViewSet):
emails = request.data.get("emails", []) emails = request.data.get("emails", [])
# Check if email is provided # Check if email is provided
if not emails: if not emails:
return Response( return Response({"error": "Emails are required"}, status=status.HTTP_400_BAD_REQUEST)
{"error": "Emails are required"}, status=status.HTTP_400_BAD_REQUEST
)
# check for role level of the requesting user # check for role level of the requesting user
requesting_user = WorkspaceMember.objects.get( requesting_user = WorkspaceMember.objects.get(workspace__slug=slug, member=request.user, is_active=True)
workspace__slug=slug, member=request.user, is_active=True
)
# Check if any invited user has an higher role # Check if any invited user has an higher role
if len( if len([email for email in emails if int(email.get("role", 5)) > requesting_user.role]):
[
email
for email in emails
if int(email.get("role", 5)) > requesting_user.role
]
):
return Response( return Response(
{"error": "You cannot invite a user with higher role"}, {"error": "You cannot invite a user with higher role"},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
@ -86,9 +76,7 @@ class WorkspaceInvitationsViewset(BaseViewSet):
return Response( return Response(
{ {
"error": "Some users are already member of workspace", "error": "Some users are already member of workspace",
"workspace_users": WorkSpaceMemberSerializer( "workspace_users": WorkSpaceMemberSerializer(workspace_members, many=True).data,
workspace_members, many=True
).data,
}, },
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
@ -113,7 +101,7 @@ class WorkspaceInvitationsViewset(BaseViewSet):
except ValidationError: except ValidationError:
return Response( 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, status=status.HTTP_400_BAD_REQUEST,
) )
@ -134,14 +122,10 @@ class WorkspaceInvitationsViewset(BaseViewSet):
request.user.email, request.user.email,
) )
return Response( return Response({"message": "Emails sent successfully"}, status=status.HTTP_200_OK)
{"message": "Emails sent successfully"}, status=status.HTTP_200_OK
)
def destroy(self, request, slug, pk): def destroy(self, request, slug, pk):
workspace_member_invite = WorkspaceMemberInvite.objects.get( workspace_member_invite = WorkspaceMemberInvite.objects.get(pk=pk, workspace__slug=slug)
pk=pk, workspace__slug=slug
)
workspace_member_invite.delete() workspace_member_invite.delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
@ -160,9 +144,7 @@ class WorkspaceJoinEndpoint(BaseAPIView):
) )
@invalidate_cache(path="/api/users/me/settings/", multiple=True) @invalidate_cache(path="/api/users/me/settings/", multiple=True)
def post(self, request, slug, pk): def post(self, request, slug, pk):
workspace_invite = WorkspaceMemberInvite.objects.get( workspace_invite = WorkspaceMemberInvite.objects.get(pk=pk, workspace__slug=slug)
pk=pk, workspace__slug=slug
)
email = request.data.get("email", "") email = request.data.get("email", "")
@ -235,9 +217,7 @@ class WorkspaceJoinEndpoint(BaseAPIView):
) )
def get(self, request, slug, pk): def get(self, request, slug, pk):
workspace_invitation = WorkspaceMemberInvite.objects.get( workspace_invitation = WorkspaceMemberInvite.objects.get(workspace__slug=slug, pk=pk)
workspace__slug=slug, pk=pk
)
serializer = WorkSpaceMemberInviteSerializer(workspace_invitation) serializer = WorkSpaceMemberInviteSerializer(workspace_invitation)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
@ -248,10 +228,7 @@ class UserWorkspaceInvitationsViewSet(BaseViewSet):
def get_queryset(self): def get_queryset(self):
return self.filter_queryset( return self.filter_queryset(
super() super().get_queryset().filter(email=self.request.user.email).select_related("workspace")
.get_queryset()
.filter(email=self.request.user.email)
.select_related("workspace")
) )
@invalidate_cache(path="/api/workspaces/", user=False) @invalidate_cache(path="/api/workspaces/", user=False)
@ -271,9 +248,9 @@ class UserWorkspaceInvitationsViewSet(BaseViewSet):
multiple=True, multiple=True,
) )
# Update the WorkspaceMember for this specific invitation # Update the WorkspaceMember for this specific invitation
WorkspaceMember.objects.filter( WorkspaceMember.objects.filter(workspace_id=invitation.workspace_id, member=request.user).update(
workspace_id=invitation.workspace_id, member=request.user is_active=True, role=invitation.role
).update(is_active=True, role=invitation.role) )
# Bulk create the user for all the workspaces # Bulk create the user for all the workspaces
WorkspaceMember.objects.bulk_create( WorkspaceMember.objects.bulk_create(

View file

@ -38,24 +38,16 @@ class WorkSpaceMemberViewSet(BaseViewSet):
.select_related("member", "member__avatar_asset") .select_related("member", "member__avatar_asset")
) )
@allow_permission( @allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
)
def list(self, request, slug): def list(self, request, slug):
workspace_member = WorkspaceMember.objects.get( workspace_member = WorkspaceMember.objects.get(member=request.user, workspace__slug=slug, is_active=True)
member=request.user, workspace__slug=slug, is_active=True
)
# Get all active workspace members # Get all active workspace members
workspace_members = self.get_queryset() workspace_members = self.get_queryset()
if workspace_member.role > 5: if workspace_member.role > 5:
serializer = WorkspaceMemberAdminSerializer( serializer = WorkspaceMemberAdminSerializer(workspace_members, fields=("id", "member", "role"), many=True)
workspace_members, fields=("id", "member", "role"), many=True
)
else: else:
serializer = WorkSpaceMemberSerializer( serializer = WorkSpaceMemberSerializer(workspace_members, fields=("id", "member", "role"), many=True)
workspace_members, fields=("id", "member", "role"), many=True
)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
@allow_permission(allowed_roles=[ROLE.ADMIN], level="WORKSPACE") @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 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: if "role" in request.data and int(request.data.get("role")) == 5:
ProjectMember.objects.filter( ProjectMember.objects.filter(workspace__slug=slug, member_id=workspace_member.member_id).update(role=5)
workspace__slug=slug, member_id=workspace_member.member_id
).update(role=5)
serializer = WorkSpaceMemberSerializer( serializer = WorkSpaceMemberSerializer(workspace_member, data=request.data, partial=True)
workspace_member, data=request.data, partial=True
)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
@ -98,9 +86,7 @@ class WorkSpaceMemberViewSet(BaseViewSet):
if str(workspace_member.id) == str(requesting_workspace_member.id): if str(workspace_member.id) == str(requesting_workspace_member.id):
return Response( 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, status=status.HTTP_400_BAD_REQUEST,
) )
@ -126,7 +112,7 @@ class WorkSpaceMemberViewSet(BaseViewSet):
): ):
return Response( 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, 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/settings/")
@invalidate_cache(path="api/users/me/workspaces/", user=False, multiple=True) @invalidate_cache(path="api/users/me/workspaces/", user=False, multiple=True)
@allow_permission( @allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
)
def leave(self, request, slug): def leave(self, request, slug):
workspace_member = WorkspaceMember.objects.get( workspace_member = WorkspaceMember.objects.get(workspace__slug=slug, member=request.user, is_active=True)
workspace__slug=slug, member=request.user, is_active=True
)
# Check if the leaving user is the only admin of the workspace # Check if the leaving user is the only admin of the workspace
if ( if (
workspace_member.role == 20 workspace_member.role == 20
and not WorkspaceMember.objects.filter( and not WorkspaceMember.objects.filter(workspace__slug=slug, role=20, is_active=True).count() > 1
workspace__slug=slug, role=20, is_active=True
).count()
> 1
): ):
return Response( 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, status=status.HTTP_400_BAD_REQUEST,
) )
@ -187,7 +166,7 @@ class WorkSpaceMemberViewSet(BaseViewSet):
): ):
return Response( 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, status=status.HTTP_400_BAD_REQUEST,
) )
@ -205,9 +184,7 @@ class WorkSpaceMemberViewSet(BaseViewSet):
class WorkspaceMemberUserViewsEndpoint(BaseAPIView): class WorkspaceMemberUserViewsEndpoint(BaseAPIView):
def post(self, request, slug): def post(self, request, slug):
workspace_member = WorkspaceMember.objects.get( workspace_member = WorkspaceMember.objects.get(workspace__slug=slug, member=request.user, is_active=True)
workspace__slug=slug, member=request.user, is_active=True
)
workspace_member.view_props = request.data.get("view_props", {}) workspace_member.view_props = request.data.get("view_props", {})
workspace_member.save() workspace_member.save()
@ -219,23 +196,15 @@ class WorkspaceMemberUserEndpoint(BaseAPIView):
def get(self, request, slug): def get(self, request, slug):
draft_issue_count = ( draft_issue_count = (
DraftIssue.objects.filter( DraftIssue.objects.filter(created_by=request.user, workspace_id=OuterRef("workspace_id"))
created_by=request.user, workspace_id=OuterRef("workspace_id")
)
.values("workspace_id") .values("workspace_id")
.annotate(count=Count("id")) .annotate(count=Count("id"))
.values("count") .values("count")
) )
workspace_member = ( workspace_member = (
WorkspaceMember.objects.filter( WorkspaceMember.objects.filter(member=request.user, workspace__slug=slug, is_active=True)
member=request.user, workspace__slug=slug, is_active=True .annotate(draft_issue_count=Coalesce(Subquery(draft_issue_count, output_field=IntegerField()), 0))
)
.annotate(
draft_issue_count=Coalesce(
Subquery(draft_issue_count, output_field=IntegerField()), 0
)
)
.first() .first()
) )
serializer = WorkspaceMemberMeSerializer(workspace_member) serializer = WorkspaceMemberMeSerializer(workspace_member)

View file

@ -28,48 +28,34 @@ class QuickLinkViewSet(BaseViewSet):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE") @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
def partial_update(self, request, slug, pk): def partial_update(self, request, slug, pk):
quick_link = WorkspaceUserLink.objects.filter( quick_link = WorkspaceUserLink.objects.filter(pk=pk, workspace__slug=slug, owner=request.user).first()
pk=pk, workspace__slug=slug, owner=request.user
).first()
if quick_link: if quick_link:
serializer = WorkspaceUserLinkSerializer( serializer = WorkspaceUserLinkSerializer(quick_link, data=request.data, partial=True)
quick_link, data=request.data, partial=True
)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
return Response( return Response({"detail": "Quick link not found."}, status=status.HTTP_404_NOT_FOUND)
{"detail": "Quick link not found."}, status=status.HTTP_404_NOT_FOUND
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE") @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
def retrieve(self, request, slug, pk): def retrieve(self, request, slug, pk):
try: try:
quick_link = WorkspaceUserLink.objects.get( quick_link = WorkspaceUserLink.objects.get(pk=pk, workspace__slug=slug, owner=request.user)
pk=pk, workspace__slug=slug, owner=request.user
)
serializer = WorkspaceUserLinkSerializer(quick_link) serializer = WorkspaceUserLinkSerializer(quick_link)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
except WorkspaceUserLink.DoesNotExist: except WorkspaceUserLink.DoesNotExist:
return Response( return Response({"error": "Quick link not found."}, status=status.HTTP_404_NOT_FOUND)
{"error": "Quick link not found."}, status=status.HTTP_404_NOT_FOUND
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE") @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
def destroy(self, request, slug, pk): def destroy(self, request, slug, pk):
quick_link = WorkspaceUserLink.objects.get( quick_link = WorkspaceUserLink.objects.get(pk=pk, workspace__slug=slug, owner=request.user)
pk=pk, workspace__slug=slug, owner=request.user
)
quick_link.delete() quick_link.delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE") @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
def list(self, request, slug): def list(self, request, slug):
quick_links = WorkspaceUserLink.objects.filter( quick_links = WorkspaceUserLink.objects.filter(workspace__slug=slug, owner=request.user)
workspace__slug=slug, owner=request.user
)
serializer = WorkspaceUserLinkSerializer(quick_links, many=True) serializer = WorkspaceUserLinkSerializer(quick_links, many=True)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)

View file

@ -19,18 +19,14 @@ class UserRecentVisitViewSet(BaseViewSet):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE") @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
def list(self, request, slug): def list(self, request, slug):
user_recent_visits = UserRecentVisit.objects.filter( user_recent_visits = UserRecentVisit.objects.filter(workspace__slug=slug, user=request.user)
workspace__slug=slug, user=request.user
)
entity_name = request.query_params.get("entity_name") entity_name = request.query_params.get("entity_name")
if 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=entity_name)
user_recent_visits = user_recent_visits.filter( user_recent_visits = user_recent_visits.filter(entity_name__in=["issue", "page", "project"])
entity_name__in=["issue", "page", "project"]
)
serializer = WorkspaceRecentVisitSerializer(user_recent_visits[:20], many=True) serializer = WorkspaceRecentVisitSerializer(user_recent_visits[:20], many=True)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)

View file

@ -24,9 +24,7 @@ class WorkspaceStickyViewSet(BaseViewSet):
.distinct() .distinct()
) )
@allow_permission( @allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
)
def create(self, request, slug): def create(self, request, slug):
workspace = Workspace.objects.get(slug=slug) workspace = Workspace.objects.get(slug=slug)
serializer = StickySerializer(data=request.data) serializer = StickySerializer(data=request.data)
@ -35,9 +33,7 @@ class WorkspaceStickyViewSet(BaseViewSet):
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@allow_permission( @allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
)
def list(self, request, slug): def list(self, request, slug):
query = request.query_params.get("query", False) query = request.query_params.get("query", False)
stickies = self.get_queryset().order_by("-sort_order") stickies = self.get_queryset().order_by("-sort_order")

View file

@ -101,9 +101,7 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
return ( return (
issues.annotate( issues.annotate(
cycle_id=Subquery( cycle_id=Subquery(
CycleIssue.objects.filter( CycleIssue.objects.filter(issue=OuterRef("id"), deleted_at__isnull=True).values("cycle_id")[:1]
issue=OuterRef("id"), deleted_at__isnull=True
).values("cycle_id")[:1]
) )
) )
.annotate( .annotate(
@ -136,9 +134,7 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
order_by_param = request.GET.get("order_by", "-created_at") order_by_param = request.GET.get("order_by", "-created_at")
issue_queryset = Issue.issue_objects.filter( issue_queryset = Issue.issue_objects.filter(
id__in=Issue.issue_objects.filter( id__in=Issue.issue_objects.filter(
Q(assignees__in=[user_id]) Q(assignees__in=[user_id]) | Q(created_by_id=user_id) | Q(issue_subscribers__subscriber_id=user_id),
| Q(created_by_id=user_id)
| Q(issue_subscribers__subscriber_id=user_id),
workspace__slug=slug, workspace__slug=slug,
).values_list("id", flat=True), ).values_list("id", flat=True),
workspace__slug=slug, workspace__slug=slug,
@ -168,9 +164,7 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
sub_group_by = request.GET.get("sub_group_by", False) sub_group_by = request.GET.get("sub_group_by", False)
# issue queryset # issue queryset
issue_queryset = issue_queryset_grouper( issue_queryset = issue_queryset_grouper(queryset=issue_queryset, group_by=group_by, sub_group_by=sub_group_by)
queryset=issue_queryset, group_by=group_by, sub_group_by=sub_group_by
)
if group_by: if group_by:
if sub_group_by: if sub_group_by:
@ -247,9 +241,7 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
request=request, request=request,
queryset=issue_queryset, queryset=issue_queryset,
total_count_queryset=total_issue_queryset, total_count_queryset=total_issue_queryset,
on_results=lambda issues: issue_on_results( on_results=lambda issues: issue_on_results(group_by=group_by, issues=issues, sub_group_by=sub_group_by),
group_by=group_by, issues=issues, sub_group_by=sub_group_by
),
) )
@ -257,19 +249,11 @@ class WorkspaceUserPropertiesEndpoint(BaseAPIView):
permission_classes = [WorkspaceViewerPermission] permission_classes = [WorkspaceViewerPermission]
def patch(self, request, slug): def patch(self, request, slug):
workspace_properties = WorkspaceUserProperties.objects.get( workspace_properties = WorkspaceUserProperties.objects.get(user=request.user, workspace__slug=slug)
user=request.user, workspace__slug=slug
)
workspace_properties.filters = request.data.get( workspace_properties.filters = request.data.get("filters", workspace_properties.filters)
"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.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( workspace_properties.display_properties = request.data.get(
"display_properties", workspace_properties.display_properties "display_properties", workspace_properties.display_properties
) )
@ -398,9 +382,7 @@ class WorkspaceUserActivityEndpoint(BaseAPIView):
order_by=request.GET.get("order_by", "-created_at"), order_by=request.GET.get("order_by", "-created_at"),
request=request, request=request,
queryset=queryset, queryset=queryset,
on_results=lambda issue_activities: IssueActivitySerializer( on_results=lambda issue_activities: IssueActivitySerializer(issue_activities, many=True).data,
issue_activities, many=True
).data,
) )
@ -410,10 +392,7 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
state_distribution = ( state_distribution = (
Issue.issue_objects.filter( 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, workspace__slug=slug,
project__project_projectmember__member=request.user, project__project_projectmember__member=request.user,
project__project_projectmember__is_active=True, project__project_projectmember__is_active=True,
@ -429,10 +408,7 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
priority_distribution = ( priority_distribution = (
Issue.issue_objects.filter( 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, workspace__slug=slug,
project__project_projectmember__member=request.user, project__project_projectmember__member=request.user,
project__project_projectmember__is_active=True, project__project_projectmember__is_active=True,
@ -443,10 +419,7 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
.filter(priority_count__gte=1) .filter(priority_count__gte=1)
.annotate( .annotate(
priority_order=Case( 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)), default=Value(len(priority_order)),
output_field=IntegerField(), output_field=IntegerField(),
) )
@ -467,10 +440,7 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
assigned_issues_count = ( assigned_issues_count = (
Issue.issue_objects.filter( 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, workspace__slug=slug,
project__project_projectmember__member=request.user, project__project_projectmember__member=request.user,
project__project_projectmember__is_active=True, project__project_projectmember__is_active=True,
@ -482,10 +452,7 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
pending_issues_count = ( pending_issues_count = (
Issue.issue_objects.filter( Issue.issue_objects.filter(
~Q(state__group__in=["completed", "cancelled"]), ~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, workspace__slug=slug,
project__project_projectmember__member=request.user, project__project_projectmember__member=request.user,
project__project_projectmember__is_active=True, project__project_projectmember__is_active=True,
@ -496,10 +463,7 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
completed_issues_count = ( completed_issues_count = (
Issue.issue_objects.filter( 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, workspace__slug=slug,
state__group="completed", state__group="completed",
project__project_projectmember__member=request.user, project__project_projectmember__member=request.user,

View file

@ -22,9 +22,7 @@ class WorkspaceUserPreferenceViewSet(BaseAPIView):
def get(self, request, slug): def get(self, request, slug):
workspace = Workspace.objects.get(slug=slug) workspace = Workspace.objects.get(slug=slug)
get_preference = WorkspaceUserPreference.objects.filter( get_preference = WorkspaceUserPreference.objects.filter(user=request.user, workspace_id=workspace.id)
user=request.user, workspace_id=workspace.id
)
create_preference_keys = [] create_preference_keys = []
@ -49,9 +47,7 @@ class WorkspaceUserPreferenceViewSet(BaseAPIView):
) )
preferences = ( preferences = (
WorkspaceUserPreference.objects.filter( WorkspaceUserPreference.objects.filter(user=request.user, workspace_id=workspace.id)
user=request.user, workspace_id=workspace.id
)
.order_by("sort_order") .order_by("sort_order")
.values("key", "is_pinned", "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") @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
def patch(self, request, slug, key): def patch(self, request, slug, key):
preference = WorkspaceUserPreference.objects.filter( preference = WorkspaceUserPreference.objects.filter(key=key, workspace__slug=slug, user=request.user).first()
key=key, workspace__slug=slug, user=request.user
).first()
if preference: if preference:
serializer = WorkspaceUserPreferenceSerializer( serializer = WorkspaceUserPreferenceSerializer(preference, data=request.data, partial=True)
preference, data=request.data, partial=True
)
if serializer.is_valid(): if serializer.is_valid():
serializer.save() serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
return Response( return Response({"detail": "Preference not found"}, status=status.HTTP_404_NOT_FOUND)
{"detail": "Preference not found"}, status=status.HTTP_404_NOT_FOUND
)

View file

@ -91,10 +91,7 @@ class Adapter:
) )
# Check if sign up is disabled and invite is present or not # Check if sign up is disabled and invite is present or not
if ( if ENABLE_SIGNUP == "0" and not WorkspaceMemberInvite.objects.filter(email=email).exists():
ENABLE_SIGNUP == "0"
and not WorkspaceMemberInvite.objects.filter(email=email).exists()
):
# Raise exception # Raise exception
raise AuthenticationException( raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["SIGNUP_DISABLED"], error_code=AUTHENTICATION_ERROR_CODES["SIGNUP_DISABLED"],

View file

@ -73,9 +73,7 @@ class OauthAdapter(Adapter):
return response.json() return response.json()
except requests.RequestException: except requests.RequestException:
code = self.authentication_error_code() code = self.authentication_error_code()
raise AuthenticationException( raise AuthenticationException(error_code=AUTHENTICATION_ERROR_CODES[code], error_message=str(code))
error_code=AUTHENTICATION_ERROR_CODES[code], error_message=str(code)
)
def get_user_response(self): def get_user_response(self):
try: try:
@ -85,9 +83,7 @@ class OauthAdapter(Adapter):
return response.json() return response.json()
except requests.RequestException: except requests.RequestException:
code = self.authentication_error_code() code = self.authentication_error_code()
raise AuthenticationException( raise AuthenticationException(error_code=AUTHENTICATION_ERROR_CODES[code], error_message=str(code))
error_code=AUTHENTICATION_ERROR_CODES[code], error_message=str(code)
)
def set_user_data(self, data): def set_user_data(self, data):
self.user_data = data self.user_data = data
@ -104,12 +100,8 @@ class OauthAdapter(Adapter):
if account: if account:
account.access_token = self.token_data.get("access_token") account.access_token = self.token_data.get("access_token")
account.refresh_token = self.token_data.get("refresh_token", None) account.refresh_token = self.token_data.get("refresh_token", None)
account.access_token_expired_at = self.token_data.get( account.access_token_expired_at = self.token_data.get("access_token_expired_at")
"access_token_expired_at" account.refresh_token_expired_at = self.token_data.get("refresh_token_expired_at")
)
account.refresh_token_expired_at = self.token_data.get(
"refresh_token_expired_at"
)
account.last_connected_at = timezone.now() account.last_connected_at = timezone.now()
account.id_token = self.token_data.get("id_token", "") account.id_token = self.token_data.get("id_token", "")
account.save() account.save()
@ -118,17 +110,11 @@ class OauthAdapter(Adapter):
Account.objects.create( Account.objects.create(
user=user, user=user,
provider=self.provider, provider=self.provider,
provider_account_id=self.user_data.get("user", {}).get( provider_account_id=self.user_data.get("user", {}).get("provider_id"),
"provider_id"
),
access_token=self.token_data.get("access_token"), access_token=self.token_data.get("access_token"),
refresh_token=self.token_data.get("refresh_token", None), refresh_token=self.token_data.get("refresh_token", None),
access_token_expired_at=self.token_data.get( access_token_expired_at=self.token_data.get("access_token_expired_at"),
"access_token_expired_at" refresh_token_expired_at=self.token_data.get("refresh_token_expired_at"),
),
refresh_token_expired_at=self.token_data.get(
"refresh_token_expired_at"
),
last_connected_at=timezone.now(), last_connected_at=timezone.now(),
id_token=self.token_data.get("id_token", ""), id_token=self.token_data.get("id_token", ""),
) )

View file

@ -37,11 +37,7 @@ class SessionMiddleware(MiddlewareMixin):
# First check if we need to delete this cookie. # First check if we need to delete this cookie.
# The session should be deleted only if the session is entirely empty. # The session should be deleted only if the session is entirely empty.
is_admin_path = "instances" in request.path is_admin_path = "instances" in request.path
cookie_name = ( cookie_name = settings.ADMIN_SESSION_COOKIE_NAME if is_admin_path else settings.SESSION_COOKIE_NAME
settings.ADMIN_SESSION_COOKIE_NAME
if is_admin_path
else settings.SESSION_COOKIE_NAME
)
if cookie_name in request.COOKIES and empty: if cookie_name in request.COOKIES and empty:
response.delete_cookie( response.delete_cookie(

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