[WEB-5044] fix: ruff lint and format errors (#7868)
* fix: lint errors * fix: file formatting * fix: code refactor
This commit is contained in:
parent
1fb22bd252
commit
9237f568dd
261 changed files with 2199 additions and 6378 deletions
|
|
@ -158,9 +158,7 @@ class UserAssetEndpoint(BaseAPIView):
|
|||
# Get the presigned URL
|
||||
storage = S3Storage(request=request)
|
||||
# Generate a presigned URL to share an S3 object
|
||||
presigned_url = storage.generate_presigned_post(
|
||||
object_name=asset_key, file_type=type, file_size=size_limit
|
||||
)
|
||||
presigned_url = storage.generate_presigned_post(object_name=asset_key, file_type=type, file_size=size_limit)
|
||||
# Return the presigned URL
|
||||
return Response(
|
||||
{
|
||||
|
|
@ -236,9 +234,7 @@ class UserAssetEndpoint(BaseAPIView):
|
|||
asset.is_deleted = True
|
||||
asset.deleted_at = timezone.now()
|
||||
# get the entity and save the asset id for the request field
|
||||
self.entity_asset_delete(
|
||||
entity_type=asset.entity_type, asset=asset, request=request
|
||||
)
|
||||
self.entity_asset_delete(entity_type=asset.entity_type, asset=asset, request=request)
|
||||
asset.save(update_fields=["is_deleted", "deleted_at"])
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
|
@ -335,9 +331,7 @@ class UserServerAssetEndpoint(BaseAPIView):
|
|||
# Get the presigned URL
|
||||
storage = S3Storage(request=request, is_server=True)
|
||||
# Generate a presigned URL to share an S3 object
|
||||
presigned_url = storage.generate_presigned_post(
|
||||
object_name=asset_key, file_type=type, file_size=size_limit
|
||||
)
|
||||
presigned_url = storage.generate_presigned_post(object_name=asset_key, file_type=type, file_size=size_limit)
|
||||
# Return the presigned URL
|
||||
return Response(
|
||||
{
|
||||
|
|
@ -389,16 +383,15 @@ class UserServerAssetEndpoint(BaseAPIView):
|
|||
def delete(self, request, asset_id):
|
||||
"""Delete user server asset.
|
||||
|
||||
Delete a user profile asset (avatar or cover image) using server credentials and remove its reference from the user profile.
|
||||
This performs a soft delete by marking the asset as deleted and updating the user's profile.
|
||||
Delete a user profile asset (avatar or cover image) using server credentials and
|
||||
remove its reference from the user profile. This performs a soft delete by marking the
|
||||
asset as deleted and updating the user's profile.
|
||||
"""
|
||||
asset = FileAsset.objects.get(id=asset_id, user_id=request.user.id)
|
||||
asset.is_deleted = True
|
||||
asset.deleted_at = timezone.now()
|
||||
# get the entity and save the asset id for the request field
|
||||
self.entity_asset_delete(
|
||||
entity_type=asset.entity_type, asset=asset, request=request
|
||||
)
|
||||
self.entity_asset_delete(entity_type=asset.entity_type, asset=asset, request=request)
|
||||
asset.save(update_fields=["is_deleted", "deleted_at"])
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
|
@ -430,9 +423,7 @@ class GenericAssetEndpoint(BaseAPIView):
|
|||
workspace = Workspace.objects.get(slug=slug)
|
||||
|
||||
# Get the asset
|
||||
asset = FileAsset.objects.get(
|
||||
id=asset_id, workspace_id=workspace.id, is_deleted=False
|
||||
)
|
||||
asset = FileAsset.objects.get(id=asset_id, workspace_id=workspace.id, is_deleted=False)
|
||||
|
||||
# Check if the asset exists and is uploaded
|
||||
if not asset.is_uploaded:
|
||||
|
|
@ -458,13 +449,9 @@ class GenericAssetEndpoint(BaseAPIView):
|
|||
)
|
||||
|
||||
except Workspace.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Workspace not found"}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
return Response({"error": "Workspace not found"}, status=status.HTTP_404_NOT_FOUND)
|
||||
except FileAsset.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Asset not found"}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
return Response({"error": "Asset not found"}, status=status.HTTP_404_NOT_FOUND)
|
||||
except Exception as e:
|
||||
log_exception(e)
|
||||
return Response(
|
||||
|
|
@ -566,14 +553,12 @@ class GenericAssetEndpoint(BaseAPIView):
|
|||
created_by=request.user,
|
||||
external_id=external_id,
|
||||
external_source=external_source,
|
||||
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT, # Using ISSUE_ATTACHMENT since we'll bind it to issues
|
||||
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT, # Using ISSUE_ATTACHMENT since we'll bind it to issues # noqa: E501
|
||||
)
|
||||
|
||||
# Get the presigned URL
|
||||
storage = S3Storage(request=request, is_server=True)
|
||||
presigned_url = storage.generate_presigned_post(
|
||||
object_name=asset_key, file_type=type, file_size=size_limit
|
||||
)
|
||||
presigned_url = storage.generate_presigned_post(object_name=asset_key, file_type=type, file_size=size_limit)
|
||||
|
||||
return Response(
|
||||
{
|
||||
|
|
@ -612,9 +597,7 @@ class GenericAssetEndpoint(BaseAPIView):
|
|||
and trigger metadata extraction.
|
||||
"""
|
||||
try:
|
||||
asset = FileAsset.objects.get(
|
||||
id=asset_id, workspace__slug=slug, is_deleted=False
|
||||
)
|
||||
asset = FileAsset.objects.get(id=asset_id, workspace__slug=slug, is_deleted=False)
|
||||
|
||||
# Update is_uploaded status
|
||||
asset.is_uploaded = request.data.get("is_uploaded", asset.is_uploaded)
|
||||
|
|
@ -627,6 +610,4 @@ class GenericAssetEndpoint(BaseAPIView):
|
|||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
except FileAsset.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Asset not found"}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
return Response({"error": "Asset not found"}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
|
|
|||
|
|
@ -37,9 +37,7 @@ class TimezoneMixin:
|
|||
timezone.deactivate()
|
||||
|
||||
|
||||
class BaseAPIView(
|
||||
TimezoneMixin, GenericAPIView, ReadReplicaControlMixin, BasePaginator
|
||||
):
|
||||
class BaseAPIView(TimezoneMixin, GenericAPIView, ReadReplicaControlMixin, BasePaginator):
|
||||
authentication_classes = [APIKeyAuthentication]
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
|
@ -56,9 +54,7 @@ class BaseAPIView(
|
|||
api_key = self.request.headers.get("X-Api-Key")
|
||||
|
||||
if api_key:
|
||||
service_token = APIToken.objects.filter(
|
||||
token=api_key, is_service=True
|
||||
).first()
|
||||
service_token = APIToken.objects.filter(token=api_key, is_service=True).first()
|
||||
|
||||
if service_token:
|
||||
throttle_classes.append(ServiceTokenRateThrottle())
|
||||
|
|
@ -113,9 +109,7 @@ class BaseAPIView(
|
|||
if settings.DEBUG:
|
||||
from django.db import connection
|
||||
|
||||
print(
|
||||
f"{request.method} - {request.get_full_path()} of Queries: {len(connection.queries)}"
|
||||
)
|
||||
print(f"{request.method} - {request.get_full_path()} of Queries: {len(connection.queries)}")
|
||||
return response
|
||||
except Exception as exc:
|
||||
response = self.handle_exception(exc)
|
||||
|
|
@ -151,14 +145,10 @@ class BaseAPIView(
|
|||
|
||||
@property
|
||||
def fields(self):
|
||||
fields = [
|
||||
field for field in self.request.GET.get("fields", "").split(",") if field
|
||||
]
|
||||
fields = [field for field in self.request.GET.get("fields", "").split(",") if field]
|
||||
return fields if fields else None
|
||||
|
||||
@property
|
||||
def expand(self):
|
||||
expand = [
|
||||
expand for expand in self.request.GET.get("expand", "").split(",") if expand
|
||||
]
|
||||
expand = [expand for expand in self.request.GET.get("expand", "").split(",") if expand]
|
||||
return expand if expand else None
|
||||
|
|
|
|||
|
|
@ -171,7 +171,7 @@ class CycleListCreateAPIEndpoint(BaseAPIView):
|
|||
@cycle_docs(
|
||||
operation_id="list_cycles",
|
||||
summary="List cycles",
|
||||
description="Retrieve all cycles in a project. Supports filtering by cycle status like current, upcoming, completed, or draft.",
|
||||
description="Retrieve all cycles in a project. Supports filtering by cycle status like current, upcoming, completed, or draft.", # noqa: E501
|
||||
parameters=[
|
||||
CURSOR_PARAMETER,
|
||||
PER_PAGE_PARAMETER,
|
||||
|
|
@ -201,9 +201,7 @@ class CycleListCreateAPIEndpoint(BaseAPIView):
|
|||
|
||||
# Current Cycle
|
||||
if cycle_view == "current":
|
||||
queryset = queryset.filter(
|
||||
start_date__lte=timezone.now(), end_date__gte=timezone.now()
|
||||
)
|
||||
queryset = queryset.filter(start_date__lte=timezone.now(), end_date__gte=timezone.now())
|
||||
data = CycleSerializer(
|
||||
queryset,
|
||||
many=True,
|
||||
|
|
@ -260,9 +258,7 @@ class CycleListCreateAPIEndpoint(BaseAPIView):
|
|||
|
||||
# Incomplete Cycles
|
||||
if cycle_view == "incomplete":
|
||||
queryset = queryset.filter(
|
||||
Q(end_date__gte=timezone.now()) | Q(end_date__isnull=True)
|
||||
)
|
||||
queryset = queryset.filter(Q(end_date__gte=timezone.now()) | Q(end_date__isnull=True))
|
||||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(queryset),
|
||||
|
|
@ -289,7 +285,7 @@ class CycleListCreateAPIEndpoint(BaseAPIView):
|
|||
@cycle_docs(
|
||||
operation_id="create_cycle",
|
||||
summary="Create cycle",
|
||||
description="Create a new development cycle with specified name, description, and date range. Supports external ID tracking for integration purposes.",
|
||||
description="Create a new development cycle with specified name, description, and date range. Supports external ID tracking for integration purposes.", # noqa: E501
|
||||
request=OpenApiRequest(
|
||||
request=CycleCreateSerializer,
|
||||
examples=[CYCLE_CREATE_EXAMPLE],
|
||||
|
|
@ -308,12 +304,8 @@ class CycleListCreateAPIEndpoint(BaseAPIView):
|
|||
Create a new development cycle with specified name, description, and date range.
|
||||
Supports external ID tracking for integration purposes.
|
||||
"""
|
||||
if (
|
||||
request.data.get("start_date", None) is None
|
||||
and request.data.get("end_date", None) is None
|
||||
) or (
|
||||
request.data.get("start_date", None) is not None
|
||||
and request.data.get("end_date", None) is not None
|
||||
if (request.data.get("start_date", None) is None and request.data.get("end_date", None) is None) or (
|
||||
request.data.get("start_date", None) is not None and request.data.get("end_date", None) is not None
|
||||
):
|
||||
serializer = CycleCreateSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
|
|
@ -358,9 +350,7 @@ class CycleListCreateAPIEndpoint(BaseAPIView):
|
|||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
else:
|
||||
return Response(
|
||||
{
|
||||
"error": "Both start date and end date are either required or are to be null"
|
||||
},
|
||||
{"error": "Both start date and end date are either required or are to be null"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
|
@ -487,7 +477,7 @@ class CycleDetailAPIEndpoint(BaseAPIView):
|
|||
@cycle_docs(
|
||||
operation_id="update_cycle",
|
||||
summary="Update cycle",
|
||||
description="Modify an existing cycle's properties like name, description, or date range. Completed cycles can only have their sort order changed.",
|
||||
description="Modify an existing cycle's properties like name, description, or date range. Completed cycles can only have their sort order changed.", # noqa: E501
|
||||
request=OpenApiRequest(
|
||||
request=CycleUpdateSerializer,
|
||||
examples=[CYCLE_UPDATE_EXAMPLE],
|
||||
|
|
@ -508,9 +498,7 @@ class CycleDetailAPIEndpoint(BaseAPIView):
|
|||
"""
|
||||
cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
|
||||
|
||||
current_instance = json.dumps(
|
||||
CycleSerializer(cycle).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
current_instance = json.dumps(CycleSerializer(cycle).data, cls=DjangoJSONEncoder)
|
||||
|
||||
if cycle.archived_at:
|
||||
return Response(
|
||||
|
|
@ -523,14 +511,10 @@ class CycleDetailAPIEndpoint(BaseAPIView):
|
|||
if cycle.end_date is not None and cycle.end_date < timezone.now():
|
||||
if "sort_order" in request_data:
|
||||
# Can only change sort order
|
||||
request_data = {
|
||||
"sort_order": request_data.get("sort_order", cycle.sort_order)
|
||||
}
|
||||
request_data = {"sort_order": request_data.get("sort_order", cycle.sort_order)}
|
||||
else:
|
||||
return Response(
|
||||
{
|
||||
"error": "The Cycle has already been completed so it cannot be edited"
|
||||
},
|
||||
{"error": "The Cycle has already been completed so it cannot be edited"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
|
@ -542,9 +526,7 @@ class CycleDetailAPIEndpoint(BaseAPIView):
|
|||
and Cycle.objects.filter(
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
external_source=request.data.get(
|
||||
"external_source", cycle.external_source
|
||||
),
|
||||
external_source=request.data.get("external_source", cycle.external_source),
|
||||
external_id=request.data.get("external_id"),
|
||||
).exists()
|
||||
):
|
||||
|
|
@ -601,11 +583,7 @@ class CycleDetailAPIEndpoint(BaseAPIView):
|
|||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
cycle_issues = list(
|
||||
CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list(
|
||||
"issue", flat=True
|
||||
)
|
||||
)
|
||||
cycle_issues = list(CycleIssue.objects.filter(cycle_id=self.kwargs.get("pk")).values_list("issue", flat=True))
|
||||
|
||||
issue_activity.delay(
|
||||
type="cycle.activity.deleted",
|
||||
|
|
@ -625,9 +603,7 @@ class CycleDetailAPIEndpoint(BaseAPIView):
|
|||
# Delete the cycle
|
||||
cycle.delete()
|
||||
# Delete the user favorite cycle
|
||||
UserFavorite.objects.filter(
|
||||
entity_type="cycle", entity_identifier=pk, project_id=project_id
|
||||
).delete()
|
||||
UserFavorite.objects.filter(entity_type="cycle", entity_identifier=pk, project_id=project_id).delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
|
|
@ -765,15 +741,13 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
|
|||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(self.get_queryset()),
|
||||
on_results=lambda cycles: CycleSerializer(
|
||||
cycles, many=True, fields=self.fields, expand=self.expand
|
||||
).data,
|
||||
on_results=lambda cycles: CycleSerializer(cycles, many=True, fields=self.fields, expand=self.expand).data,
|
||||
)
|
||||
|
||||
@cycle_docs(
|
||||
operation_id="archive_cycle",
|
||||
summary="Archive cycle",
|
||||
description="Move a completed cycle to archived status for historical tracking. Only cycles that have ended can be archived.",
|
||||
description="Move a completed cycle to archived status for historical tracking. Only cycles that have ended can be archived.", # noqa: E501
|
||||
request={},
|
||||
responses={
|
||||
204: ARCHIVED_RESPONSE,
|
||||
|
|
@ -786,9 +760,7 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
|
|||
Move a completed cycle to archived status for historical tracking.
|
||||
Only cycles that have ended can be archived.
|
||||
"""
|
||||
cycle = Cycle.objects.get(
|
||||
pk=cycle_id, project_id=project_id, workspace__slug=slug
|
||||
)
|
||||
cycle = Cycle.objects.get(pk=cycle_id, project_id=project_id, workspace__slug=slug)
|
||||
if cycle.end_date >= timezone.now():
|
||||
return Response(
|
||||
{"error": "Only completed cycles can be archived"},
|
||||
|
|
@ -819,9 +791,7 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
|
|||
Restore an archived cycle to active status, making it available for regular use.
|
||||
The cycle will reappear in active cycle lists.
|
||||
"""
|
||||
cycle = Cycle.objects.get(
|
||||
pk=cycle_id, project_id=project_id, workspace__slug=slug
|
||||
)
|
||||
cycle = Cycle.objects.get(pk=cycle_id, project_id=project_id, workspace__slug=slug)
|
||||
cycle.archived_at = None
|
||||
cycle.save()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
|
@ -884,9 +854,7 @@ class CycleIssueListCreateAPIEndpoint(BaseAPIView):
|
|||
# List
|
||||
order_by = request.GET.get("order_by", "created_at")
|
||||
issues = (
|
||||
Issue.issue_objects.filter(
|
||||
issue_cycle__cycle_id=cycle_id, issue_cycle__deleted_at__isnull=True
|
||||
)
|
||||
Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id, issue_cycle__deleted_at__isnull=True)
|
||||
.annotate(
|
||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
||||
.order_by()
|
||||
|
|
@ -923,15 +891,13 @@ class CycleIssueListCreateAPIEndpoint(BaseAPIView):
|
|||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(issues),
|
||||
on_results=lambda issues: IssueSerializer(
|
||||
issues, many=True, fields=self.fields, expand=self.expand
|
||||
).data,
|
||||
on_results=lambda issues: IssueSerializer(issues, many=True, fields=self.fields, expand=self.expand).data,
|
||||
)
|
||||
|
||||
@cycle_docs(
|
||||
operation_id="add_cycle_work_items",
|
||||
summary="Add Work Items to Cycle",
|
||||
description="Assign multiple work items to a cycle. Automatically handles bulk creation and updates with activity tracking.",
|
||||
description="Assign multiple work items to a cycle. Automatically handles bulk creation and updates with activity tracking.", # noqa: E501
|
||||
request=OpenApiRequest(
|
||||
request=CycleIssueRequestSerializer,
|
||||
examples=[CYCLE_ISSUE_REQUEST_EXAMPLE],
|
||||
|
|
@ -955,22 +921,24 @@ class CycleIssueListCreateAPIEndpoint(BaseAPIView):
|
|||
|
||||
if not issues:
|
||||
return Response(
|
||||
{"error": "Work items are required"}, status=status.HTTP_400_BAD_REQUEST
|
||||
{"error": "Work items are required", "code": "MISSING_WORK_ITEMS"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
cycle = Cycle.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=cycle_id
|
||||
)
|
||||
cycle = Cycle.objects.get(workspace__slug=slug, project_id=project_id, pk=cycle_id)
|
||||
|
||||
if cycle.end_date is not None and cycle.end_date < timezone.now():
|
||||
return Response(
|
||||
{
|
||||
"code": "CYCLE_COMPLETED",
|
||||
"message": "The Cycle has already been completed so no new issues can be added",
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Get all CycleWorkItems already created
|
||||
cycle_issues = list(
|
||||
CycleIssue.objects.filter(~Q(cycle_id=cycle_id), issue_id__in=issues)
|
||||
)
|
||||
|
||||
cycle_issues = list(CycleIssue.objects.filter(~Q(cycle_id=cycle_id), issue_id__in=issues))
|
||||
existing_issues = [
|
||||
str(cycle_issue.issue_id)
|
||||
for cycle_issue in cycle_issues
|
||||
if str(cycle_issue.issue_id) in issues
|
||||
str(cycle_issue.issue_id) for cycle_issue in cycle_issues if str(cycle_issue.issue_id) in issues
|
||||
]
|
||||
new_issues = list(set(issues) - set(existing_issues))
|
||||
|
||||
|
|
@ -1021,9 +989,7 @@ class CycleIssueListCreateAPIEndpoint(BaseAPIView):
|
|||
current_instance=json.dumps(
|
||||
{
|
||||
"updated_cycle_issues": update_cycle_issue_activity,
|
||||
"created_cycle_issues": serializers.serialize(
|
||||
"json", created_records
|
||||
),
|
||||
"created_cycle_issues": serializers.serialize("json", created_records),
|
||||
}
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
|
|
@ -1099,9 +1065,7 @@ class CycleIssueDetailAPIEndpoint(BaseAPIView):
|
|||
cycle_id=cycle_id,
|
||||
issue_id=issue_id,
|
||||
)
|
||||
serializer = CycleIssueSerializer(
|
||||
cycle_issue, fields=self.fields, expand=self.expand
|
||||
)
|
||||
serializer = CycleIssueSerializer(cycle_issue, fields=self.fields, expand=self.expand)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@cycle_docs(
|
||||
|
|
@ -1154,7 +1118,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
|||
@cycle_docs(
|
||||
operation_id="transfer_cycle_work_items",
|
||||
summary="Transfer cycle work items",
|
||||
description="Move incomplete work items from the current cycle to a new target cycle. Captures progress snapshot and transfers only unfinished work items.",
|
||||
description="Move incomplete work items from the current cycle to a new target cycle. Captures progress snapshot and transfers only unfinished work items.", # noqa: E501
|
||||
request=OpenApiRequest(
|
||||
request=TransferCycleIssueRequestSerializer,
|
||||
examples=[TRANSFER_CYCLE_ISSUE_EXAMPLE],
|
||||
|
|
@ -1207,14 +1171,10 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
|||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
new_cycle = Cycle.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id, pk=new_cycle_id
|
||||
).first()
|
||||
new_cycle = Cycle.objects.filter(workspace__slug=slug, project_id=project_id, pk=new_cycle_id).first()
|
||||
|
||||
old_cycle = (
|
||||
Cycle.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id, pk=cycle_id
|
||||
)
|
||||
Cycle.objects.filter(workspace__slug=slug, project_id=project_id, pk=cycle_id)
|
||||
.annotate(
|
||||
total_issues=Count(
|
||||
"issue_cycle",
|
||||
|
|
@ -1324,9 +1284,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
|||
)
|
||||
)
|
||||
.values("display_name", "assignee_id", "avatar", "avatar_url")
|
||||
.annotate(
|
||||
total_estimates=Sum(Cast("estimate_point__value", FloatField()))
|
||||
)
|
||||
.annotate(total_estimates=Sum(Cast("estimate_point__value", FloatField())))
|
||||
.annotate(
|
||||
completed_estimates=Sum(
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
|
|
@ -1353,9 +1311,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
|||
assignee_estimate_distribution = [
|
||||
{
|
||||
"display_name": item["display_name"],
|
||||
"assignee_id": (
|
||||
str(item["assignee_id"]) if item["assignee_id"] else None
|
||||
),
|
||||
"assignee_id": (str(item["assignee_id"]) if item["assignee_id"] else None),
|
||||
"avatar": item.get("avatar", None),
|
||||
"avatar_url": item.get("avatar_url", None),
|
||||
"total_estimates": item["total_estimates"],
|
||||
|
|
@ -1376,9 +1332,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
|||
.annotate(color=F("labels__color"))
|
||||
.annotate(label_id=F("labels__id"))
|
||||
.values("label_name", "color", "label_id")
|
||||
.annotate(
|
||||
total_estimates=Sum(Cast("estimate_point__value", FloatField()))
|
||||
)
|
||||
.annotate(total_estimates=Sum(Cast("estimate_point__value", FloatField())))
|
||||
.annotate(
|
||||
completed_estimates=Sum(
|
||||
Cast("estimate_point__value", FloatField()),
|
||||
|
|
@ -1445,19 +1399,13 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
|||
),
|
||||
),
|
||||
# If `avatar_asset` is None, fall back to using `avatar` field directly
|
||||
When(
|
||||
assignees__avatar_asset__isnull=True, then="assignees__avatar"
|
||||
),
|
||||
When(assignees__avatar_asset__isnull=True, then="assignees__avatar"),
|
||||
default=Value(None),
|
||||
output_field=models.CharField(),
|
||||
)
|
||||
)
|
||||
.values("display_name", "assignee_id", "avatar_url")
|
||||
.annotate(
|
||||
total_issues=Count(
|
||||
"id", filter=Q(archived_at__isnull=True, is_draft=False)
|
||||
)
|
||||
)
|
||||
.annotate(total_issues=Count("id", filter=Q(archived_at__isnull=True, is_draft=False)))
|
||||
.annotate(
|
||||
completed_issues=Count(
|
||||
"id",
|
||||
|
|
@ -1484,9 +1432,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
|||
assignee_distribution_data = [
|
||||
{
|
||||
"display_name": item["display_name"],
|
||||
"assignee_id": (
|
||||
str(item["assignee_id"]) if item["assignee_id"] else None
|
||||
),
|
||||
"assignee_id": (str(item["assignee_id"]) if item["assignee_id"] else None),
|
||||
"avatar": item.get("avatar", None),
|
||||
"avatar_url": item.get("avatar_url", None),
|
||||
"total_issues": item["total_issues"],
|
||||
|
|
@ -1508,11 +1454,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
|||
.annotate(color=F("labels__color"))
|
||||
.annotate(label_id=F("labels__id"))
|
||||
.values("label_name", "color", "label_id")
|
||||
.annotate(
|
||||
total_issues=Count(
|
||||
"id", filter=Q(archived_at__isnull=True, is_draft=False)
|
||||
)
|
||||
)
|
||||
.annotate(total_issues=Count("id", filter=Q(archived_at__isnull=True, is_draft=False)))
|
||||
.annotate(
|
||||
completed_issues=Count(
|
||||
"id",
|
||||
|
|
@ -1558,9 +1500,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
|||
cycle_id=cycle_id,
|
||||
)
|
||||
|
||||
current_cycle = Cycle.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id, pk=cycle_id
|
||||
).first()
|
||||
current_cycle = Cycle.objects.filter(workspace__slug=slug, project_id=project_id, pk=cycle_id).first()
|
||||
|
||||
current_cycle.progress_snapshot = {
|
||||
"total_issues": old_cycle.total_issues,
|
||||
|
|
@ -1588,9 +1528,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
|||
|
||||
if new_cycle.end_date is not None and new_cycle.end_date < timezone.now():
|
||||
return Response(
|
||||
{
|
||||
"error": "The cycle where the issues are transferred is already completed"
|
||||
},
|
||||
{"error": "The cycle where the issues are transferred is already completed"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
|
@ -1614,9 +1552,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
|||
}
|
||||
)
|
||||
|
||||
cycle_issues = CycleIssue.objects.bulk_update(
|
||||
updated_cycles, ["cycle_id"], batch_size=100
|
||||
)
|
||||
cycle_issues = CycleIssue.objects.bulk_update(updated_cycles, ["cycle_id"], batch_size=100)
|
||||
|
||||
# Capture Issue Activity
|
||||
issue_activity.delay(
|
||||
|
|
|
|||
|
|
@ -62,9 +62,7 @@ class IntakeIssueListCreateAPIEndpoint(BaseAPIView):
|
|||
project_id=self.kwargs.get("project_id"),
|
||||
).first()
|
||||
|
||||
project = Project.objects.get(
|
||||
workspace__slug=self.kwargs.get("slug"), pk=self.kwargs.get("project_id")
|
||||
)
|
||||
project = Project.objects.get(workspace__slug=self.kwargs.get("slug"), pk=self.kwargs.get("project_id"))
|
||||
|
||||
if intake is None or not project.intake_view:
|
||||
return IntakeIssue.objects.none()
|
||||
|
|
@ -83,7 +81,7 @@ class IntakeIssueListCreateAPIEndpoint(BaseAPIView):
|
|||
@intake_docs(
|
||||
operation_id="get_intake_work_items_list",
|
||||
summary="List intake work items",
|
||||
description="Retrieve all work items in the project's intake queue. Returns paginated results when listing all intake work items.",
|
||||
description="Retrieve all work items in the project's intake queue. Returns paginated results when listing all intake work items.", # noqa: E501
|
||||
parameters=[
|
||||
WORKSPACE_SLUG_PARAMETER,
|
||||
PROJECT_ID_PARAMETER,
|
||||
|
|
@ -119,7 +117,7 @@ class IntakeIssueListCreateAPIEndpoint(BaseAPIView):
|
|||
@intake_docs(
|
||||
operation_id="create_intake_work_item",
|
||||
summary="Create intake work item",
|
||||
description="Submit a new work item to the project's intake queue for review and triage. Automatically creates the work item with default triage state and tracks activity.",
|
||||
description="Submit a new work item to the project's intake queue for review and triage. Automatically creates the work item with default triage state and tracks activity.", # noqa: E501
|
||||
parameters=[
|
||||
WORKSPACE_SLUG_PARAMETER,
|
||||
PROJECT_ID_PARAMETER,
|
||||
|
|
@ -144,22 +142,16 @@ class IntakeIssueListCreateAPIEndpoint(BaseAPIView):
|
|||
Automatically creates the work item with default triage state and tracks activity.
|
||||
"""
|
||||
if not request.data.get("issue", {}).get("name", False):
|
||||
return Response(
|
||||
{"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
return Response({"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
intake = Intake.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
).first()
|
||||
intake = Intake.objects.filter(workspace__slug=slug, project_id=project_id).first()
|
||||
|
||||
project = Project.objects.get(workspace__slug=slug, pk=project_id)
|
||||
|
||||
# Intake view
|
||||
if intake is None and not project.intake_view:
|
||||
return Response(
|
||||
{
|
||||
"error": "Intake is not enabled for this project enable it through the project's api"
|
||||
},
|
||||
{"error": "Intake is not enabled for this project enable it through the project's api"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
|
@ -171,17 +163,13 @@ class IntakeIssueListCreateAPIEndpoint(BaseAPIView):
|
|||
"urgent",
|
||||
"none",
|
||||
]:
|
||||
return Response(
|
||||
{"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
return Response({"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# create an issue
|
||||
issue = Issue.objects.create(
|
||||
name=request.data.get("issue", {}).get("name"),
|
||||
description=request.data.get("issue", {}).get("description", {}),
|
||||
description_html=request.data.get("issue", {}).get(
|
||||
"description_html", "<p></p>"
|
||||
),
|
||||
description_html=request.data.get("issue", {}).get("description_html", "<p></p>"),
|
||||
priority=request.data.get("issue", {}).get("priority", "none"),
|
||||
project_id=project_id,
|
||||
)
|
||||
|
|
@ -226,9 +214,7 @@ class IntakeIssueDetailAPIEndpoint(BaseAPIView):
|
|||
project_id=self.kwargs.get("project_id"),
|
||||
).first()
|
||||
|
||||
project = Project.objects.get(
|
||||
workspace__slug=self.kwargs.get("slug"), pk=self.kwargs.get("project_id")
|
||||
)
|
||||
project = Project.objects.get(workspace__slug=self.kwargs.get("slug"), pk=self.kwargs.get("project_id"))
|
||||
|
||||
if intake is None or not project.intake_view:
|
||||
return IntakeIssue.objects.none()
|
||||
|
|
@ -267,15 +253,13 @@ class IntakeIssueDetailAPIEndpoint(BaseAPIView):
|
|||
Retrieve details of a specific intake work item.
|
||||
"""
|
||||
intake_issue_queryset = self.get_queryset().get(issue_id=issue_id)
|
||||
intake_issue_data = IntakeIssueSerializer(
|
||||
intake_issue_queryset, fields=self.fields, expand=self.expand
|
||||
).data
|
||||
intake_issue_data = IntakeIssueSerializer(intake_issue_queryset, fields=self.fields, expand=self.expand).data
|
||||
return Response(intake_issue_data, status=status.HTTP_200_OK)
|
||||
|
||||
@intake_docs(
|
||||
operation_id="update_intake_work_item",
|
||||
summary="Update intake work item",
|
||||
description="Modify an existing intake work item's properties or status for triage processing. Supports status changes like accept, reject, or mark as duplicate.",
|
||||
description="Modify an existing intake work item's properties or status for triage processing. Supports status changes like accept, reject, or mark as duplicate.", # noqa: E501
|
||||
parameters=[
|
||||
WORKSPACE_SLUG_PARAMETER,
|
||||
PROJECT_ID_PARAMETER,
|
||||
|
|
@ -300,18 +284,14 @@ class IntakeIssueDetailAPIEndpoint(BaseAPIView):
|
|||
Modify an existing intake work item's properties or status for triage processing.
|
||||
Supports status changes like accept, reject, or mark as duplicate.
|
||||
"""
|
||||
intake = Intake.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
).first()
|
||||
intake = Intake.objects.filter(workspace__slug=slug, project_id=project_id).first()
|
||||
|
||||
project = Project.objects.get(workspace__slug=slug, pk=project_id)
|
||||
|
||||
# Intake view
|
||||
if intake is None and not project.intake_view:
|
||||
return Response(
|
||||
{
|
||||
"error": "Intake is not enabled for this project enable it through the project's api"
|
||||
},
|
||||
{"error": "Intake is not enabled for this project enable it through the project's api"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
|
@ -332,9 +312,7 @@ class IntakeIssueDetailAPIEndpoint(BaseAPIView):
|
|||
)
|
||||
|
||||
# Only project members admins and created_by users can access this endpoint
|
||||
if project_member.role <= 5 and str(intake_issue.created_by_id) != str(
|
||||
request.user.id
|
||||
):
|
||||
if project_member.role <= 5 and str(intake_issue.created_by_id) != str(request.user.id):
|
||||
return Response(
|
||||
{"error": "You cannot edit intake work items"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
|
|
@ -349,10 +327,7 @@ class IntakeIssueDetailAPIEndpoint(BaseAPIView):
|
|||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=Q(
|
||||
~Q(labels__id__isnull=True)
|
||||
& Q(label_issue__deleted_at__isnull=True)
|
||||
),
|
||||
filter=Q(~Q(labels__id__isnull=True) & Q(label_issue__deleted_at__isnull=True)),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
|
|
@ -373,9 +348,7 @@ class IntakeIssueDetailAPIEndpoint(BaseAPIView):
|
|||
if project_member.role <= 5:
|
||||
issue_data = {
|
||||
"name": issue_data.get("name", issue.name),
|
||||
"description_html": issue_data.get(
|
||||
"description_html", issue.description_html
|
||||
),
|
||||
"description_html": issue_data.get("description_html", issue.description_html),
|
||||
"description": issue_data.get("description", issue.description),
|
||||
}
|
||||
|
||||
|
|
@ -401,45 +374,31 @@ class IntakeIssueDetailAPIEndpoint(BaseAPIView):
|
|||
)
|
||||
issue_serializer.save()
|
||||
else:
|
||||
return Response(
|
||||
issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
return Response(issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Only project admins and members can edit intake issue attributes
|
||||
if project_member.role > 15:
|
||||
serializer = IntakeIssueUpdateSerializer(
|
||||
intake_issue, data=request.data, partial=True
|
||||
)
|
||||
current_instance = json.dumps(
|
||||
IntakeIssueSerializer(intake_issue).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
serializer = IntakeIssueUpdateSerializer(intake_issue, data=request.data, partial=True)
|
||||
current_instance = json.dumps(IntakeIssueSerializer(intake_issue).data, cls=DjangoJSONEncoder)
|
||||
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
# Update the issue state if the issue is rejected or marked as duplicate
|
||||
if serializer.data["status"] in [-1, 2]:
|
||||
issue = Issue.objects.get(
|
||||
pk=issue_id, workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
state = State.objects.filter(
|
||||
group="cancelled", workspace__slug=slug, project_id=project_id
|
||||
).first()
|
||||
issue = Issue.objects.get(pk=issue_id, workspace__slug=slug, project_id=project_id)
|
||||
state = State.objects.filter(group="cancelled", workspace__slug=slug, project_id=project_id).first()
|
||||
if state is not None:
|
||||
issue.state = state
|
||||
issue.save()
|
||||
|
||||
# Update the issue state if it is accepted
|
||||
if serializer.data["status"] in [1]:
|
||||
issue = Issue.objects.get(
|
||||
pk=issue_id, workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
issue = Issue.objects.get(pk=issue_id, workspace__slug=slug, project_id=project_id)
|
||||
|
||||
# Update the issue state only if it is in triage state
|
||||
if issue.state.is_triage:
|
||||
# Move to default state
|
||||
state = State.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id, default=True
|
||||
).first()
|
||||
state = State.objects.filter(workspace__slug=slug, project_id=project_id, default=True).first()
|
||||
if state is not None:
|
||||
issue.state = state
|
||||
issue.save()
|
||||
|
|
@ -461,14 +420,12 @@ class IntakeIssueDetailAPIEndpoint(BaseAPIView):
|
|||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
else:
|
||||
return Response(
|
||||
IntakeIssueSerializer(intake_issue).data, status=status.HTTP_200_OK
|
||||
)
|
||||
return Response(IntakeIssueSerializer(intake_issue).data, status=status.HTTP_200_OK)
|
||||
|
||||
@intake_docs(
|
||||
operation_id="delete_intake_work_item",
|
||||
summary="Delete intake work item",
|
||||
description="Permanently remove an intake work item from the triage queue. Also deletes the underlying work item if it hasn't been accepted yet.",
|
||||
description="Permanently remove an intake work item from the triage queue. Also deletes the underlying work item if it hasn't been accepted yet.", # noqa: E501
|
||||
parameters=[
|
||||
WORKSPACE_SLUG_PARAMETER,
|
||||
PROJECT_ID_PARAMETER,
|
||||
|
|
@ -484,18 +441,14 @@ class IntakeIssueDetailAPIEndpoint(BaseAPIView):
|
|||
Permanently remove an intake work item from the triage queue.
|
||||
Also deletes the underlying work item if it hasn't been accepted yet.
|
||||
"""
|
||||
intake = Intake.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
).first()
|
||||
intake = Intake.objects.filter(workspace__slug=slug, project_id=project_id).first()
|
||||
|
||||
project = Project.objects.get(workspace__slug=slug, pk=project_id)
|
||||
|
||||
# Intake view
|
||||
if intake is None and not project.intake_view:
|
||||
return Response(
|
||||
{
|
||||
"error": "Intake is not enabled for this project enable it through the project's api"
|
||||
},
|
||||
{"error": "Intake is not enabled for this project enable it through the project's api"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
|
@ -510,9 +463,7 @@ class IntakeIssueDetailAPIEndpoint(BaseAPIView):
|
|||
# Check the issue status
|
||||
if intake_issue.status in [-2, -1, 0, 2]:
|
||||
# Delete the issue also
|
||||
issue = Issue.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id, pk=issue_id
|
||||
).first()
|
||||
issue = Issue.objects.filter(workspace__slug=slug, project_id=project_id, pk=issue_id).first()
|
||||
if issue.created_by_id != request.user.id and (
|
||||
not ProjectMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
|
|
|
|||
|
|
@ -142,9 +142,8 @@ from plane.utils.openapi import (
|
|||
)
|
||||
from plane.bgtasks.work_item_link_task import crawl_work_item_link_title
|
||||
|
||||
def user_has_issue_permission(
|
||||
user_id, project_id, issue=None, allowed_roles=None, allow_creator=True
|
||||
):
|
||||
|
||||
def user_has_issue_permission(user_id, project_id, issue=None, allowed_roles=None, allow_creator=True):
|
||||
if allow_creator and issue is not None and user_id == issue.created_by_id:
|
||||
return True
|
||||
|
||||
|
|
@ -269,7 +268,7 @@ class IssueListCreateAPIEndpoint(BaseAPIView):
|
|||
@work_item_docs(
|
||||
operation_id="list_work_items",
|
||||
summary="List work items",
|
||||
description="Retrieve a paginated list of all work items in a project. Supports filtering, ordering, and field selection through query parameters.",
|
||||
description="Retrieve a paginated list of all work items in a project. Supports filtering, ordering, and field selection through query parameters.", # noqa: E501
|
||||
parameters=[
|
||||
CURSOR_PARAMETER,
|
||||
PER_PAGE_PARAMETER,
|
||||
|
|
@ -322,9 +321,7 @@ class IssueListCreateAPIEndpoint(BaseAPIView):
|
|||
self.get_queryset()
|
||||
.annotate(
|
||||
cycle_id=Subquery(
|
||||
CycleIssue.objects.filter(
|
||||
issue=OuterRef("id"), deleted_at__isnull=True
|
||||
).values("cycle_id")[:1]
|
||||
CycleIssue.objects.filter(issue=OuterRef("id"), deleted_at__isnull=True).values("cycle_id")[:1]
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
|
|
@ -344,21 +341,14 @@ class IssueListCreateAPIEndpoint(BaseAPIView):
|
|||
)
|
||||
)
|
||||
|
||||
total_issue_queryset = Issue.issue_objects.filter(
|
||||
project_id=project_id, workspace__slug=slug
|
||||
)
|
||||
total_issue_queryset = Issue.issue_objects.filter(project_id=project_id, workspace__slug=slug)
|
||||
|
||||
# Priority Ordering
|
||||
if order_by_param == "priority" or order_by_param == "-priority":
|
||||
priority_order = (
|
||||
priority_order if order_by_param == "priority" else priority_order[::-1]
|
||||
)
|
||||
priority_order = priority_order if order_by_param == "priority" else priority_order[::-1]
|
||||
issue_queryset = issue_queryset.annotate(
|
||||
priority_order=Case(
|
||||
*[
|
||||
When(priority=p, then=Value(i))
|
||||
for i, p in enumerate(priority_order)
|
||||
],
|
||||
*[When(priority=p, then=Value(i)) for i, p in enumerate(priority_order)],
|
||||
output_field=CharField(),
|
||||
)
|
||||
).order_by("priority_order")
|
||||
|
|
@ -370,17 +360,10 @@ class IssueListCreateAPIEndpoint(BaseAPIView):
|
|||
"-state__name",
|
||||
"-state__group",
|
||||
]:
|
||||
state_order = (
|
||||
state_order
|
||||
if order_by_param in ["state__name", "state__group"]
|
||||
else state_order[::-1]
|
||||
)
|
||||
state_order = state_order if order_by_param in ["state__name", "state__group"] else state_order[::-1]
|
||||
issue_queryset = issue_queryset.annotate(
|
||||
state_order=Case(
|
||||
*[
|
||||
When(state__group=state_group, then=Value(i))
|
||||
for i, state_group in enumerate(state_order)
|
||||
],
|
||||
*[When(state__group=state_group, then=Value(i)) for i, state_group in enumerate(state_order)],
|
||||
default=Value(len(state_order)),
|
||||
output_field=CharField(),
|
||||
)
|
||||
|
|
@ -393,14 +376,8 @@ class IssueListCreateAPIEndpoint(BaseAPIView):
|
|||
"-assignees__first_name",
|
||||
]:
|
||||
issue_queryset = issue_queryset.annotate(
|
||||
max_values=Max(
|
||||
order_by_param[1::]
|
||||
if order_by_param.startswith("-")
|
||||
else order_by_param
|
||||
)
|
||||
).order_by(
|
||||
"-max_values" if order_by_param.startswith("-") else "max_values"
|
||||
)
|
||||
max_values=Max(order_by_param[1::] if order_by_param.startswith("-") else order_by_param)
|
||||
).order_by("-max_values" if order_by_param.startswith("-") else "max_values")
|
||||
else:
|
||||
issue_queryset = issue_queryset.order_by(order_by_param)
|
||||
|
||||
|
|
@ -408,9 +385,7 @@ class IssueListCreateAPIEndpoint(BaseAPIView):
|
|||
request=request,
|
||||
queryset=(issue_queryset),
|
||||
total_count_queryset=total_issue_queryset,
|
||||
on_results=lambda issues: IssueSerializer(
|
||||
issues, many=True, fields=self.fields, expand=self.expand
|
||||
).data,
|
||||
on_results=lambda issues: IssueSerializer(issues, many=True, fields=self.fields, expand=self.expand).data,
|
||||
)
|
||||
|
||||
@work_item_docs(
|
||||
|
|
@ -476,9 +451,7 @@ class IssueListCreateAPIEndpoint(BaseAPIView):
|
|||
|
||||
serializer.save()
|
||||
# Refetch the issue
|
||||
issue = Issue.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id, pk=serializer.data["id"]
|
||||
).first()
|
||||
issue = Issue.objects.filter(workspace__slug=slug, project_id=project_id, pk=serializer.data["id"]).first()
|
||||
issue.created_at = request.data.get("created_at", timezone.now())
|
||||
issue.created_by_id = request.data.get("created_by", request.user.id)
|
||||
issue.save(update_fields=["created_at", "created_by"])
|
||||
|
|
@ -579,7 +552,7 @@ class IssueDetailAPIEndpoint(BaseAPIView):
|
|||
@work_item_docs(
|
||||
operation_id="put_work_item",
|
||||
summary="Update or create work item",
|
||||
description="Update an existing work item identified by external ID and source, or create a new one if it doesn't exist. Requires external_id and external_source parameters for identification.",
|
||||
description="Update an existing work item identified by external ID and source, or create a new one if it doesn't exist. Requires external_id and external_source parameters for identification.", # noqa: E501
|
||||
request=OpenApiRequest(
|
||||
request=IssueSerializer,
|
||||
examples=[ISSUE_UPSERT_EXAMPLE],
|
||||
|
|
@ -625,9 +598,7 @@ class IssueDetailAPIEndpoint(BaseAPIView):
|
|||
|
||||
# Get the current instance of the issue in order to track
|
||||
# changes and dispatch the issue activity
|
||||
current_instance = json.dumps(
|
||||
IssueSerializer(issue).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
current_instance = json.dumps(IssueSerializer(issue).data, cls=DjangoJSONEncoder)
|
||||
|
||||
# Get the requested data, encode it as django object and pass it
|
||||
# to serializer to validation
|
||||
|
|
@ -690,16 +661,12 @@ class IssueDetailAPIEndpoint(BaseAPIView):
|
|||
# the issue with the provided data, else return with the
|
||||
# default states given.
|
||||
issue.created_at = request.data.get("created_at", timezone.now())
|
||||
issue.created_by_id = request.data.get(
|
||||
"created_by", request.user.id
|
||||
)
|
||||
issue.created_by_id = request.data.get("created_by", request.user.id)
|
||||
issue.save(update_fields=["created_at", "created_by"])
|
||||
|
||||
issue_activity.delay(
|
||||
type="issue.activity.created",
|
||||
requested_data=json.dumps(
|
||||
self.request.data, cls=DjangoJSONEncoder
|
||||
),
|
||||
requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(serializer.data.get("id", None)),
|
||||
project_id=str(project_id),
|
||||
|
|
@ -717,7 +684,7 @@ class IssueDetailAPIEndpoint(BaseAPIView):
|
|||
@work_item_docs(
|
||||
operation_id="update_work_item",
|
||||
summary="Partially update work item",
|
||||
description="Partially update an existing work item with the provided fields. Supports external ID validation to prevent conflicts.",
|
||||
description="Partially update an existing work item with the provided fields. Supports external ID validation to prevent conflicts.", # noqa: E501
|
||||
parameters=[
|
||||
PROJECT_ID_PARAMETER,
|
||||
],
|
||||
|
|
@ -744,9 +711,7 @@ class IssueDetailAPIEndpoint(BaseAPIView):
|
|||
"""
|
||||
issue = Issue.objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
|
||||
project = Project.objects.get(pk=project_id)
|
||||
current_instance = json.dumps(
|
||||
IssueSerializer(issue).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
current_instance = json.dumps(IssueSerializer(issue).data, cls=DjangoJSONEncoder)
|
||||
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
|
||||
serializer = IssueSerializer(
|
||||
issue,
|
||||
|
|
@ -761,9 +726,7 @@ class IssueDetailAPIEndpoint(BaseAPIView):
|
|||
and Issue.objects.filter(
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
external_source=request.data.get(
|
||||
"external_source", issue.external_source
|
||||
),
|
||||
external_source=request.data.get("external_source", issue.external_source),
|
||||
external_id=request.data.get("external_id"),
|
||||
).exists()
|
||||
):
|
||||
|
|
@ -791,7 +754,7 @@ class IssueDetailAPIEndpoint(BaseAPIView):
|
|||
@work_item_docs(
|
||||
operation_id="delete_work_item",
|
||||
summary="Delete work item",
|
||||
description="Permanently delete an existing work item from the project. Only admins or the item creator can perform this action.",
|
||||
description="Permanently delete an existing work item from the project. Only admins or the item creator can perform this action.", # noqa: E501
|
||||
parameters=[
|
||||
PROJECT_ID_PARAMETER,
|
||||
],
|
||||
|
|
@ -821,9 +784,7 @@ class IssueDetailAPIEndpoint(BaseAPIView):
|
|||
{"error": "Only admin or creator can delete the work item"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
current_instance = json.dumps(
|
||||
IssueSerializer(issue).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
current_instance = json.dumps(IssueSerializer(issue).data, cls=DjangoJSONEncoder)
|
||||
issue.delete()
|
||||
issue_activity.delay(
|
||||
type="issue.activity.deleted",
|
||||
|
|
@ -959,9 +920,7 @@ class LabelListCreateAPIEndpoint(BaseAPIView):
|
|||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(self.get_queryset()),
|
||||
on_results=lambda labels: LabelSerializer(
|
||||
labels, many=True, fields=self.fields, expand=self.expand
|
||||
).data,
|
||||
on_results=lambda labels: LabelSerializer(labels, many=True, fields=self.fields, expand=self.expand).data,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -1033,9 +992,7 @@ class LabelDetailAPIEndpoint(LabelListCreateAPIEndpoint):
|
|||
and Label.objects.filter(
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
external_source=request.data.get(
|
||||
"external_source", label.external_source
|
||||
),
|
||||
external_source=request.data.get("external_source", label.external_source),
|
||||
external_id=request.data.get("external_id"),
|
||||
)
|
||||
.exclude(id=pk)
|
||||
|
|
@ -1162,9 +1119,7 @@ class IssueLinkListCreateAPIEndpoint(BaseAPIView):
|
|||
serializer = IssueLinkCreateSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
serializer.save(project_id=project_id, issue_id=issue_id)
|
||||
crawl_work_item_link_title.delay(
|
||||
serializer.instance.id, serializer.instance.url
|
||||
)
|
||||
crawl_work_item_link_title.delay(serializer.instance.id, serializer.instance.url)
|
||||
link = IssueLink.objects.get(pk=serializer.instance.id)
|
||||
link.created_by_id = request.data.get("created_by", request.user.id)
|
||||
link.save(update_fields=["created_by"])
|
||||
|
|
@ -1233,9 +1188,7 @@ class IssueLinkDetailAPIEndpoint(BaseAPIView):
|
|||
"""
|
||||
if pk is None:
|
||||
issue_links = self.get_queryset()
|
||||
serializer = IssueLinkSerializer(
|
||||
issue_links, fields=self.fields, expand=self.expand
|
||||
)
|
||||
serializer = IssueLinkSerializer(issue_links, fields=self.fields, expand=self.expand)
|
||||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(self.get_queryset()),
|
||||
|
|
@ -1244,9 +1197,7 @@ class IssueLinkDetailAPIEndpoint(BaseAPIView):
|
|||
).data,
|
||||
)
|
||||
issue_link = self.get_queryset().get(pk=pk)
|
||||
serializer = IssueLinkSerializer(
|
||||
issue_link, fields=self.fields, expand=self.expand
|
||||
)
|
||||
serializer = IssueLinkSerializer(issue_link, fields=self.fields, expand=self.expand)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@issue_link_docs(
|
||||
|
|
@ -1276,19 +1227,13 @@ class IssueLinkDetailAPIEndpoint(BaseAPIView):
|
|||
Modify the URL, title, or metadata of an existing issue link.
|
||||
Tracks all changes in issue activity logs.
|
||||
"""
|
||||
issue_link = IssueLink.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
|
||||
)
|
||||
issue_link = IssueLink.objects.get(workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk)
|
||||
requested_data = json.dumps(request.data, cls=DjangoJSONEncoder)
|
||||
current_instance = json.dumps(
|
||||
IssueLinkSerializer(issue_link).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
current_instance = json.dumps(IssueLinkSerializer(issue_link).data, cls=DjangoJSONEncoder)
|
||||
serializer = IssueLinkSerializer(issue_link, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
crawl_work_item_link_title.delay(
|
||||
serializer.data.get("id"), serializer.data.get("url")
|
||||
)
|
||||
crawl_work_item_link_title.delay(serializer.data.get("id"), serializer.data.get("url"))
|
||||
issue_activity.delay(
|
||||
type="link.activity.updated",
|
||||
requested_data=requested_data,
|
||||
|
|
@ -1320,12 +1265,8 @@ class IssueLinkDetailAPIEndpoint(BaseAPIView):
|
|||
Permanently remove an external link from a work item.
|
||||
Records deletion activity for audit purposes.
|
||||
"""
|
||||
issue_link = IssueLink.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
|
||||
)
|
||||
current_instance = json.dumps(
|
||||
IssueLinkSerializer(issue_link).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
issue_link = IssueLink.objects.get(workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk)
|
||||
current_instance = json.dumps(IssueLinkSerializer(issue_link).data, cls=DjangoJSONEncoder)
|
||||
issue_activity.delay(
|
||||
type="link.activity.deleted",
|
||||
requested_data=json.dumps({"link_id": str(pk)}),
|
||||
|
|
@ -1461,15 +1402,12 @@ class IssueCommentListCreateAPIEndpoint(BaseAPIView):
|
|||
|
||||
serializer = IssueCommentCreateSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
serializer.save(
|
||||
project_id=project_id, issue_id=issue_id, actor=request.user
|
||||
)
|
||||
serializer.save(project_id=project_id, issue_id=issue_id, actor=request.user)
|
||||
issue_comment = IssueComment.objects.get(pk=serializer.instance.id)
|
||||
# Update the created_at and the created_by and save the comment
|
||||
issue_comment.created_at = request.data.get("created_at", timezone.now())
|
||||
issue_comment.created_by_id = request.data.get(
|
||||
"created_by", request.user.id
|
||||
)
|
||||
issue_comment.created_by_id = request.data.get("created_by", request.user.id)
|
||||
issue_comment.actor_id = request.data.get("created_by", request.user.id)
|
||||
issue_comment.save(update_fields=["created_at", "created_by"])
|
||||
|
||||
issue_activity.delay(
|
||||
|
|
@ -1555,9 +1493,7 @@ class IssueCommentDetailAPIEndpoint(BaseAPIView):
|
|||
Retrieve details of a specific comment.
|
||||
"""
|
||||
issue_comment = self.get_queryset().get(pk=pk)
|
||||
serializer = IssueCommentSerializer(
|
||||
issue_comment, fields=self.fields, expand=self.expand
|
||||
)
|
||||
serializer = IssueCommentSerializer(issue_comment, fields=self.fields, expand=self.expand)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@issue_comment_docs(
|
||||
|
|
@ -1588,13 +1524,9 @@ class IssueCommentDetailAPIEndpoint(BaseAPIView):
|
|||
Modify the content of an existing comment on a work item.
|
||||
Validates external ID uniqueness if provided.
|
||||
"""
|
||||
issue_comment = IssueComment.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
|
||||
)
|
||||
issue_comment = IssueComment.objects.get(workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk)
|
||||
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
|
||||
current_instance = json.dumps(
|
||||
IssueCommentSerializer(issue_comment).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
current_instance = json.dumps(IssueCommentSerializer(issue_comment).data, cls=DjangoJSONEncoder)
|
||||
|
||||
# Validation check if the issue already exists
|
||||
if (
|
||||
|
|
@ -1603,9 +1535,7 @@ class IssueCommentDetailAPIEndpoint(BaseAPIView):
|
|||
and IssueComment.objects.filter(
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
external_source=request.data.get(
|
||||
"external_source", issue_comment.external_source
|
||||
),
|
||||
external_source=request.data.get("external_source", issue_comment.external_source),
|
||||
external_id=request.data.get("external_id"),
|
||||
).exists()
|
||||
):
|
||||
|
|
@ -1617,9 +1547,7 @@ class IssueCommentDetailAPIEndpoint(BaseAPIView):
|
|||
status=status.HTTP_409_CONFLICT,
|
||||
)
|
||||
|
||||
serializer = IssueCommentCreateSerializer(
|
||||
issue_comment, data=request.data, partial=True
|
||||
)
|
||||
serializer = IssueCommentCreateSerializer(issue_comment, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
issue_activity.delay(
|
||||
|
|
@ -1665,12 +1593,8 @@ class IssueCommentDetailAPIEndpoint(BaseAPIView):
|
|||
Permanently remove a comment from a work item.
|
||||
Records deletion activity for audit purposes.
|
||||
"""
|
||||
issue_comment = IssueComment.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk
|
||||
)
|
||||
current_instance = json.dumps(
|
||||
IssueCommentSerializer(issue_comment).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
issue_comment = IssueComment.objects.get(workspace__slug=slug, project_id=project_id, issue_id=issue_id, pk=pk)
|
||||
current_instance = json.dumps(IssueCommentSerializer(issue_comment).data, cls=DjangoJSONEncoder)
|
||||
issue_comment.delete()
|
||||
issue_activity.delay(
|
||||
type="comment.activity.deleted",
|
||||
|
|
@ -1717,9 +1641,7 @@ class IssueActivityListAPIEndpoint(BaseAPIView):
|
|||
Excludes comment, vote, reaction, and draft activities.
|
||||
"""
|
||||
issue_activities = (
|
||||
IssueActivity.objects.filter(
|
||||
issue_id=issue_id, workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
IssueActivity.objects.filter(issue_id=issue_id, workspace__slug=slug, project_id=project_id)
|
||||
.filter(
|
||||
~Q(field__in=["comment", "vote", "reaction", "draft"]),
|
||||
project__project_projectmember__member=self.request.user,
|
||||
|
|
@ -1774,9 +1696,7 @@ class IssueActivityDetailAPIEndpoint(BaseAPIView):
|
|||
Excludes comment, vote, reaction, and draft activities.
|
||||
"""
|
||||
issue_activities = (
|
||||
IssueActivity.objects.filter(
|
||||
issue_id=issue_id, workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
IssueActivity.objects.filter(issue_id=issue_id, workspace__slug=slug, project_id=project_id)
|
||||
.filter(
|
||||
~Q(field__in=["comment", "vote", "reaction", "draft"]),
|
||||
project__project_projectmember__member=self.request.user,
|
||||
|
|
@ -1866,12 +1786,8 @@ class IssueAttachmentListCreateAPIEndpoint(BaseAPIView):
|
|||
name="Workspace not found",
|
||||
value={"error": "Workspace not found"},
|
||||
),
|
||||
OpenApiExample(
|
||||
name="Project not found", value={"error": "Project not found"}
|
||||
),
|
||||
OpenApiExample(
|
||||
name="Issue not found", value={"error": "Issue not found"}
|
||||
),
|
||||
OpenApiExample(name="Project not found", value={"error": "Project not found"}),
|
||||
OpenApiExample(name="Issue not found", value={"error": "Issue not found"}),
|
||||
],
|
||||
),
|
||||
},
|
||||
|
|
@ -1882,9 +1798,7 @@ class IssueAttachmentListCreateAPIEndpoint(BaseAPIView):
|
|||
Generate presigned URL for uploading file attachments to a work item.
|
||||
Validates file type and size before creating the attachment record.
|
||||
"""
|
||||
issue = Issue.objects.get(
|
||||
pk=issue_id, workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
issue = Issue.objects.get(pk=issue_id, workspace__slug=slug, project_id=project_id)
|
||||
# if the user is creator or admin,member then allow the upload
|
||||
if not user_has_issue_permission(
|
||||
request.user.id,
|
||||
|
|
@ -1970,9 +1884,7 @@ class IssueAttachmentListCreateAPIEndpoint(BaseAPIView):
|
|||
# Get the presigned URL
|
||||
storage = S3Storage(request=request)
|
||||
# Generate a presigned URL to share an S3 object
|
||||
presigned_url = storage.generate_presigned_post(
|
||||
object_name=asset_key, file_type=type, file_size=size_limit
|
||||
)
|
||||
presigned_url = storage.generate_presigned_post(object_name=asset_key, file_type=type, file_size=size_limit)
|
||||
# Return the presigned URL
|
||||
return Response(
|
||||
{
|
||||
|
|
@ -2032,9 +1944,7 @@ class IssueAttachmentDetailAPIEndpoint(BaseAPIView):
|
|||
ATTACHMENT_ID_PARAMETER,
|
||||
],
|
||||
responses={
|
||||
204: OpenApiResponse(
|
||||
description="Work item attachment deleted successfully"
|
||||
),
|
||||
204: OpenApiResponse(description="Work item attachment deleted successfully"),
|
||||
404: ATTACHMENT_NOT_FOUND_RESPONSE,
|
||||
},
|
||||
)
|
||||
|
|
@ -2044,9 +1954,7 @@ class IssueAttachmentDetailAPIEndpoint(BaseAPIView):
|
|||
Soft delete an attachment from a work item by marking it as deleted.
|
||||
Records deletion activity and triggers metadata cleanup.
|
||||
"""
|
||||
issue = Issue.objects.get(
|
||||
pk=issue_id, workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
issue = Issue.objects.get(pk=issue_id, workspace__slug=slug, project_id=project_id)
|
||||
# if the request user is creator or admin then delete the attachment
|
||||
if not user_has_issue_permission(
|
||||
request.user,
|
||||
|
|
@ -2060,9 +1968,7 @@ class IssueAttachmentDetailAPIEndpoint(BaseAPIView):
|
|||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
issue_attachment = FileAsset.objects.get(
|
||||
pk=pk, workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
issue_attachment = FileAsset.objects.get(pk=pk, workspace__slug=slug, project_id=project_id)
|
||||
issue_attachment.is_deleted = True
|
||||
issue_attachment.deleted_at = timezone.now()
|
||||
issue_attachment.save()
|
||||
|
|
@ -2136,9 +2042,7 @@ class IssueAttachmentDetailAPIEndpoint(BaseAPIView):
|
|||
)
|
||||
|
||||
# Get the asset
|
||||
asset = FileAsset.objects.get(
|
||||
id=pk, workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
asset = FileAsset.objects.get(id=pk, workspace__slug=slug, project_id=project_id)
|
||||
|
||||
# Check if the asset is uploaded
|
||||
if not asset.is_uploaded:
|
||||
|
|
@ -2176,9 +2080,7 @@ class IssueAttachmentDetailAPIEndpoint(BaseAPIView):
|
|||
examples=[ATTACHMENT_UPLOAD_CONFIRM_EXAMPLE],
|
||||
),
|
||||
responses={
|
||||
204: OpenApiResponse(
|
||||
description="Work item attachment uploaded successfully"
|
||||
),
|
||||
204: OpenApiResponse(description="Work item attachment uploaded successfully"),
|
||||
400: INVALID_REQUEST_RESPONSE,
|
||||
404: ATTACHMENT_NOT_FOUND_RESPONSE,
|
||||
},
|
||||
|
|
@ -2190,9 +2092,7 @@ class IssueAttachmentDetailAPIEndpoint(BaseAPIView):
|
|||
Triggers activity logging and metadata extraction.
|
||||
"""
|
||||
|
||||
issue = Issue.objects.get(
|
||||
pk=issue_id, workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
issue = Issue.objects.get(pk=issue_id, workspace__slug=slug, project_id=project_id)
|
||||
# if the user is creator or admin then allow the upload
|
||||
if not user_has_issue_permission(
|
||||
request.user,
|
||||
|
|
@ -2206,9 +2106,7 @@ class IssueAttachmentDetailAPIEndpoint(BaseAPIView):
|
|||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
issue_attachment = FileAsset.objects.get(
|
||||
pk=pk, workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
issue_attachment = FileAsset.objects.get(pk=pk, workspace__slug=slug, project_id=project_id)
|
||||
serializer = IssueAttachmentSerializer(issue_attachment)
|
||||
|
||||
# Send this activity only if the attachment is not uploaded before
|
||||
|
|
|
|||
|
|
@ -74,9 +74,7 @@ class WorkspaceMemberAPIEndpoint(BaseAPIView):
|
|||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
workspace_members = WorkspaceMember.objects.filter(
|
||||
workspace__slug=slug
|
||||
).select_related("member")
|
||||
workspace_members = WorkspaceMember.objects.filter(workspace__slug=slug).select_related("member")
|
||||
|
||||
# Get all the users with their roles
|
||||
users_with_roles = []
|
||||
|
|
@ -125,13 +123,11 @@ class ProjectMemberAPIEndpoint(BaseAPIView):
|
|||
)
|
||||
|
||||
# Get the workspace members that are present inside the workspace
|
||||
project_members = ProjectMember.objects.filter(
|
||||
project_id=project_id, workspace__slug=slug
|
||||
).values_list("member_id", flat=True)
|
||||
project_members = ProjectMember.objects.filter(project_id=project_id, workspace__slug=slug).values_list(
|
||||
"member_id", flat=True
|
||||
)
|
||||
|
||||
# Get all the users that are present inside the workspace
|
||||
users = UserLiteSerializer(
|
||||
User.objects.filter(id__in=project_members), many=True
|
||||
).data
|
||||
users = UserLiteSerializer(User.objects.filter(id__in=project_members), many=True).data
|
||||
|
||||
return Response(users, status=status.HTTP_200_OK)
|
||||
|
|
|
|||
|
|
@ -394,9 +394,7 @@ class ModuleDetailAPIEndpoint(BaseAPIView):
|
|||
examples=[MODULE_UPDATE_EXAMPLE],
|
||||
),
|
||||
404: OpenApiResponse(description="Module not found"),
|
||||
409: OpenApiResponse(
|
||||
description="Module with same external ID already exists"
|
||||
),
|
||||
409: OpenApiResponse(description="Module with same external ID already exists"),
|
||||
},
|
||||
)
|
||||
def patch(self, request, slug, project_id, pk):
|
||||
|
|
@ -407,18 +405,14 @@ class ModuleDetailAPIEndpoint(BaseAPIView):
|
|||
"""
|
||||
module = Module.objects.get(pk=pk, project_id=project_id, workspace__slug=slug)
|
||||
|
||||
current_instance = json.dumps(
|
||||
ModuleSerializer(module).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
current_instance = json.dumps(ModuleSerializer(module).data, cls=DjangoJSONEncoder)
|
||||
|
||||
if module.archived_at:
|
||||
return Response(
|
||||
{"error": "Archived module cannot be edited"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
serializer = ModuleSerializer(
|
||||
module, data=request.data, context={"project_id": project_id}, partial=True
|
||||
)
|
||||
serializer = ModuleSerializer(module, data=request.data, context={"project_id": project_id}, partial=True)
|
||||
if serializer.is_valid():
|
||||
if (
|
||||
request.data.get("external_id")
|
||||
|
|
@ -426,9 +420,7 @@ class ModuleDetailAPIEndpoint(BaseAPIView):
|
|||
and Module.objects.filter(
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
external_source=request.data.get(
|
||||
"external_source", module.external_source
|
||||
),
|
||||
external_source=request.data.get("external_source", module.external_source),
|
||||
external_id=request.data.get("external_id"),
|
||||
).exists()
|
||||
):
|
||||
|
|
@ -514,9 +506,7 @@ class ModuleDetailAPIEndpoint(BaseAPIView):
|
|||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
module_issues = list(
|
||||
ModuleIssue.objects.filter(module_id=pk).values_list("issue", flat=True)
|
||||
)
|
||||
module_issues = list(ModuleIssue.objects.filter(module_id=pk).values_list("issue", flat=True))
|
||||
issue_activity.delay(
|
||||
type="module.activity.deleted",
|
||||
requested_data=json.dumps(
|
||||
|
|
@ -537,9 +527,7 @@ class ModuleDetailAPIEndpoint(BaseAPIView):
|
|||
# Delete the module issues
|
||||
ModuleIssue.objects.filter(module=pk, project_id=project_id).delete()
|
||||
# Delete the user favorite module
|
||||
UserFavorite.objects.filter(
|
||||
entity_type="module", entity_identifier=pk, project_id=project_id
|
||||
).delete()
|
||||
UserFavorite.objects.filter(entity_type="module", entity_identifier=pk, project_id=project_id).delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
|
|
@ -609,9 +597,7 @@ class ModuleIssueListCreateAPIEndpoint(BaseAPIView):
|
|||
"""
|
||||
order_by = request.GET.get("order_by", "created_at")
|
||||
issues = (
|
||||
Issue.issue_objects.filter(
|
||||
issue_module__module_id=module_id, issue_module__deleted_at__isnull=True
|
||||
)
|
||||
Issue.issue_objects.filter(issue_module__module_id=module_id, issue_module__deleted_at__isnull=True)
|
||||
.annotate(
|
||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
||||
.order_by()
|
||||
|
|
@ -647,15 +633,13 @@ class ModuleIssueListCreateAPIEndpoint(BaseAPIView):
|
|||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(issues),
|
||||
on_results=lambda issues: IssueSerializer(
|
||||
issues, many=True, fields=self.fields, expand=self.expand
|
||||
).data,
|
||||
on_results=lambda issues: IssueSerializer(issues, many=True, fields=self.fields, expand=self.expand).data,
|
||||
)
|
||||
|
||||
@module_issue_docs(
|
||||
operation_id="add_module_work_items",
|
||||
summary="Add Work Items to Module",
|
||||
description="Assign multiple work items to a module or move them from another module. Automatically handles bulk creation and updates with activity tracking.",
|
||||
description="Assign multiple work items to a module or move them from another module. Automatically handles bulk creation and updates with activity tracking.", # noqa: E501
|
||||
parameters=[
|
||||
MODULE_ID_PARAMETER,
|
||||
],
|
||||
|
|
@ -681,16 +665,12 @@ class ModuleIssueListCreateAPIEndpoint(BaseAPIView):
|
|||
"""
|
||||
issues = request.data.get("issues", [])
|
||||
if not len(issues):
|
||||
return Response(
|
||||
{"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
module = Module.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=module_id
|
||||
)
|
||||
return Response({"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
module = Module.objects.get(workspace__slug=slug, project_id=project_id, pk=module_id)
|
||||
|
||||
issues = Issue.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id, pk__in=issues
|
||||
).values_list("id", flat=True)
|
||||
issues = Issue.objects.filter(workspace__slug=slug, project_id=project_id, pk__in=issues).values_list(
|
||||
"id", flat=True
|
||||
)
|
||||
|
||||
module_issues = list(ModuleIssue.objects.filter(issue_id__in=issues))
|
||||
|
||||
|
|
@ -699,11 +679,7 @@ class ModuleIssueListCreateAPIEndpoint(BaseAPIView):
|
|||
record_to_create = []
|
||||
|
||||
for issue in issues:
|
||||
module_issue = [
|
||||
module_issue
|
||||
for module_issue in module_issues
|
||||
if str(module_issue.issue_id) in issues
|
||||
]
|
||||
module_issue = [module_issue for module_issue in module_issues if str(module_issue.issue_id) in issues]
|
||||
|
||||
if len(module_issue):
|
||||
if module_issue[0].module_id != module_id:
|
||||
|
|
@ -728,9 +704,7 @@ class ModuleIssueListCreateAPIEndpoint(BaseAPIView):
|
|||
)
|
||||
)
|
||||
|
||||
ModuleIssue.objects.bulk_create(
|
||||
record_to_create, batch_size=10, ignore_conflicts=True
|
||||
)
|
||||
ModuleIssue.objects.bulk_create(record_to_create, batch_size=10, ignore_conflicts=True)
|
||||
|
||||
ModuleIssue.objects.bulk_update(records_to_update, ["module"], batch_size=10)
|
||||
|
||||
|
|
@ -744,9 +718,7 @@ class ModuleIssueListCreateAPIEndpoint(BaseAPIView):
|
|||
current_instance=json.dumps(
|
||||
{
|
||||
"updated_module_issues": update_module_issue_activity,
|
||||
"created_module_issues": serializers.serialize(
|
||||
"json", record_to_create
|
||||
),
|
||||
"created_module_issues": serializers.serialize("json", record_to_create),
|
||||
}
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
|
|
@ -871,9 +843,7 @@ class ModuleIssueDetailAPIEndpoint(BaseAPIView):
|
|||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(issues),
|
||||
on_results=lambda issues: IssueSerializer(
|
||||
issues, many=True, fields=self.fields, expand=self.expand
|
||||
).data,
|
||||
on_results=lambda issues: IssueSerializer(issues, many=True, fields=self.fields, expand=self.expand).data,
|
||||
)
|
||||
|
||||
@module_issue_docs(
|
||||
|
|
@ -904,9 +874,7 @@ class ModuleIssueDetailAPIEndpoint(BaseAPIView):
|
|||
module_issue.delete()
|
||||
issue_activity.delay(
|
||||
type="module.activity.deleted",
|
||||
requested_data=json.dumps(
|
||||
{"module_id": str(module_id), "issues": [str(module_issue.issue_id)]}
|
||||
),
|
||||
requested_data=json.dumps({"module_id": str(module_id), "issues": [str(module_issue.issue_id)]}),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(issue_id),
|
||||
project_id=str(project_id),
|
||||
|
|
|
|||
|
|
@ -79,9 +79,7 @@ class ProjectListCreateAPIEndpoint(BaseAPIView):
|
|||
)
|
||||
| Q(network=2)
|
||||
)
|
||||
.select_related(
|
||||
"workspace", "workspace__owner", "default_assignee", "project_lead"
|
||||
)
|
||||
.select_related("workspace", "workspace__owner", "default_assignee", "project_lead")
|
||||
.annotate(
|
||||
is_member=Exists(
|
||||
ProjectMember.objects.filter(
|
||||
|
|
@ -170,9 +168,9 @@ class ProjectListCreateAPIEndpoint(BaseAPIView):
|
|||
.prefetch_related(
|
||||
Prefetch(
|
||||
"project_projectmember",
|
||||
queryset=ProjectMember.objects.filter(
|
||||
workspace__slug=slug, is_active=True
|
||||
).select_related("member"),
|
||||
queryset=ProjectMember.objects.filter(workspace__slug=slug, is_active=True).select_related(
|
||||
"member"
|
||||
),
|
||||
)
|
||||
)
|
||||
.order_by(request.GET.get("order_by", "sort_order"))
|
||||
|
|
@ -211,24 +209,18 @@ class ProjectListCreateAPIEndpoint(BaseAPIView):
|
|||
"""
|
||||
try:
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
serializer = ProjectCreateSerializer(
|
||||
data={**request.data}, context={"workspace_id": workspace.id}
|
||||
)
|
||||
serializer = ProjectCreateSerializer(data={**request.data}, context={"workspace_id": workspace.id})
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
|
||||
# Add the user as Administrator to the project
|
||||
_ = ProjectMember.objects.create(
|
||||
project_id=serializer.instance.id, member=request.user, role=20
|
||||
)
|
||||
_ = ProjectMember.objects.create(project_id=serializer.instance.id, member=request.user, role=20)
|
||||
# Also create the issue property for the user
|
||||
_ = IssueUserProperty.objects.create(
|
||||
project_id=serializer.instance.id, user=request.user
|
||||
)
|
||||
_ = IssueUserProperty.objects.create(project_id=serializer.instance.id, user=request.user)
|
||||
|
||||
if serializer.instance.project_lead is not None and str(
|
||||
serializer.instance.project_lead
|
||||
) != str(request.user.id):
|
||||
if serializer.instance.project_lead is not None and str(serializer.instance.project_lead) != str(
|
||||
request.user.id
|
||||
):
|
||||
ProjectMember.objects.create(
|
||||
project_id=serializer.instance.id,
|
||||
member_id=serializer.instance.project_lead,
|
||||
|
|
@ -314,9 +306,7 @@ class ProjectListCreateAPIEndpoint(BaseAPIView):
|
|||
status=status.HTTP_409_CONFLICT,
|
||||
)
|
||||
except Workspace.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Workspace does not exist"}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
return Response({"error": "Workspace does not exist"}, status=status.HTTP_404_NOT_FOUND)
|
||||
except ValidationError:
|
||||
return Response(
|
||||
{"identifier": "The project identifier is already taken"},
|
||||
|
|
@ -344,9 +334,7 @@ class ProjectDetailAPIEndpoint(BaseAPIView):
|
|||
)
|
||||
| Q(network=2)
|
||||
)
|
||||
.select_related(
|
||||
"workspace", "workspace__owner", "default_assignee", "project_lead"
|
||||
)
|
||||
.select_related("workspace", "workspace__owner", "default_assignee", "project_lead")
|
||||
.annotate(
|
||||
is_member=Exists(
|
||||
ProjectMember.objects.filter(
|
||||
|
|
@ -451,9 +439,7 @@ class ProjectDetailAPIEndpoint(BaseAPIView):
|
|||
try:
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
project = Project.objects.get(pk=pk)
|
||||
current_instance = json.dumps(
|
||||
ProjectSerializer(project).data, cls=DjangoJSONEncoder
|
||||
)
|
||||
current_instance = json.dumps(ProjectSerializer(project).data, cls=DjangoJSONEncoder)
|
||||
|
||||
intake_view = request.data.get("intake_view", project.intake_view)
|
||||
|
||||
|
|
@ -473,9 +459,7 @@ class ProjectDetailAPIEndpoint(BaseAPIView):
|
|||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
if serializer.data["intake_view"]:
|
||||
intake = Intake.objects.filter(
|
||||
project=project, is_default=True
|
||||
).first()
|
||||
intake = Intake.objects.filter(project=project, is_default=True).first()
|
||||
if not intake:
|
||||
Intake.objects.create(
|
||||
name=f"{project.name} Intake",
|
||||
|
|
@ -505,9 +489,7 @@ class ProjectDetailAPIEndpoint(BaseAPIView):
|
|||
status=status.HTTP_409_CONFLICT,
|
||||
)
|
||||
except (Project.DoesNotExist, Workspace.DoesNotExist):
|
||||
return Response(
|
||||
{"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
return Response({"error": "Project does not exist"}, status=status.HTTP_404_NOT_FOUND)
|
||||
except ValidationError:
|
||||
return Response(
|
||||
{"identifier": "The project identifier is already taken"},
|
||||
|
|
@ -533,9 +515,7 @@ class ProjectDetailAPIEndpoint(BaseAPIView):
|
|||
"""
|
||||
project = Project.objects.get(pk=pk, workspace__slug=slug)
|
||||
# Delete the user favorite cycle
|
||||
UserFavorite.objects.filter(
|
||||
entity_type="project", entity_identifier=pk, project_id=pk
|
||||
).delete()
|
||||
UserFavorite.objects.filter(entity_type="project", entity_identifier=pk, project_id=pk).delete()
|
||||
project.delete()
|
||||
webhook_activity.delay(
|
||||
event="project",
|
||||
|
|
|
|||
|
|
@ -80,9 +80,7 @@ class StateListCreateAPIEndpoint(BaseAPIView):
|
|||
Supports external ID tracking for integration purposes.
|
||||
"""
|
||||
try:
|
||||
serializer = StateSerializer(
|
||||
data=request.data, context={"project_id": project_id}
|
||||
)
|
||||
serializer = StateSerializer(data=request.data, context={"project_id": project_id})
|
||||
if serializer.is_valid():
|
||||
if (
|
||||
request.data.get("external_id")
|
||||
|
|
@ -153,9 +151,7 @@ class StateListCreateAPIEndpoint(BaseAPIView):
|
|||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(self.get_queryset()),
|
||||
on_results=lambda states: StateSerializer(
|
||||
states, many=True, fields=self.fields, expand=self.expand
|
||||
).data,
|
||||
on_results=lambda states: StateSerializer(states, many=True, fields=self.fields, expand=self.expand).data,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -213,7 +209,7 @@ class StateDetailAPIEndpoint(BaseAPIView):
|
|||
@state_docs(
|
||||
operation_id="delete_state",
|
||||
summary="Delete state",
|
||||
description="Permanently remove a workflow state from a project. Default states and states with existing work items cannot be deleted.",
|
||||
description="Permanently remove a workflow state from a project. Default states and states with existing work items cannot be deleted.", # noqa: E501
|
||||
parameters=[
|
||||
STATE_ID_PARAMETER,
|
||||
],
|
||||
|
|
@ -228,9 +224,7 @@ class StateDetailAPIEndpoint(BaseAPIView):
|
|||
Permanently remove a workflow state from a project.
|
||||
Default states and states with existing work items cannot be deleted.
|
||||
"""
|
||||
state = State.objects.get(
|
||||
is_triage=False, pk=state_id, project_id=project_id, workspace__slug=slug
|
||||
)
|
||||
state = State.objects.get(is_triage=False, pk=state_id, project_id=project_id, workspace__slug=slug)
|
||||
|
||||
if state.default:
|
||||
return Response(
|
||||
|
|
@ -277,9 +271,7 @@ class StateDetailAPIEndpoint(BaseAPIView):
|
|||
Partially update an existing workflow state's properties like name, color, or group.
|
||||
Validates external ID uniqueness if provided.
|
||||
"""
|
||||
state = State.objects.get(
|
||||
workspace__slug=slug, project_id=project_id, pk=state_id
|
||||
)
|
||||
state = State.objects.get(workspace__slug=slug, project_id=project_id, pk=state_id)
|
||||
serializer = StateSerializer(state, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
if (
|
||||
|
|
@ -288,9 +280,7 @@ class StateDetailAPIEndpoint(BaseAPIView):
|
|||
and State.objects.filter(
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
external_source=request.data.get(
|
||||
"external_source", state.external_source
|
||||
),
|
||||
external_source=request.data.get("external_source", state.external_source),
|
||||
external_id=request.data.get("external_id"),
|
||||
).exists()
|
||||
):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue