chore: format files in API server (#8292)
This commit is contained in:
parent
647813a6ab
commit
97e21ba21c
20 changed files with 173 additions and 373 deletions
|
|
@ -17,7 +17,7 @@ from plane.utils.content_validator import (
|
||||||
from .base import BaseSerializer
|
from .base import BaseSerializer
|
||||||
|
|
||||||
|
|
||||||
class ProjectCreateSerializer(BaseSerializer):
|
class ProjectCreateSerializer(BaseSerializer):
|
||||||
"""
|
"""
|
||||||
Serializer for creating projects with workspace validation.
|
Serializer for creating projects with workspace validation.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -195,9 +195,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,
|
||||||
|
|
@ -254,9 +252,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),
|
||||||
|
|
@ -302,17 +298,10 @@ 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, context={"request": request})
|
||||||
serializer = CycleCreateSerializer(
|
|
||||||
data=request.data, context={"request": request}
|
|
||||||
)
|
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
if (
|
if (
|
||||||
request.data.get("external_id")
|
request.data.get("external_id")
|
||||||
|
|
@ -355,9 +344,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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -505,9 +492,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(
|
||||||
|
|
@ -520,20 +505,14 @@ 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,
|
||||||
)
|
)
|
||||||
|
|
||||||
serializer = CycleUpdateSerializer(
|
serializer = CycleUpdateSerializer(cycle, data=request.data, partial=True, context={"request": request})
|
||||||
cycle, data=request.data, partial=True, context={"request": request}
|
|
||||||
)
|
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
if (
|
if (
|
||||||
request.data.get("external_id")
|
request.data.get("external_id")
|
||||||
|
|
@ -541,9 +520,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()
|
||||||
):
|
):
|
||||||
|
|
@ -600,11 +577,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",
|
||||||
|
|
@ -624,9 +597,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)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -764,9 +735,7 @@ 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(
|
||||||
|
|
@ -785,9 +754,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"},
|
||||||
|
|
@ -818,9 +785,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)
|
||||||
|
|
@ -883,9 +848,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()
|
||||||
|
|
@ -922,9 +885,7 @@ 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(
|
||||||
|
|
@ -958,9 +919,7 @@ class CycleIssueListCreateAPIEndpoint(BaseAPIView):
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
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(
|
||||||
|
|
@ -972,13 +931,9 @@ class CycleIssueListCreateAPIEndpoint(BaseAPIView):
|
||||||
)
|
)
|
||||||
|
|
||||||
# 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))
|
||||||
|
|
||||||
|
|
@ -1029,9 +984,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()),
|
||||||
|
|
@ -1107,9 +1060,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(
|
||||||
|
|
|
||||||
|
|
@ -154,7 +154,6 @@ class ProjectMemberListCreateAPIEndpoint(BaseAPIView):
|
||||||
|
|
||||||
# API endpoint to get and update a project member
|
# API endpoint to get and update a project member
|
||||||
class ProjectMemberDetailAPIEndpoint(ProjectMemberListCreateAPIEndpoint):
|
class ProjectMemberDetailAPIEndpoint(ProjectMemberListCreateAPIEndpoint):
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
operation_id="get_project_member",
|
operation_id="get_project_member",
|
||||||
summary="Get project member",
|
summary="Get project member",
|
||||||
|
|
|
||||||
|
|
@ -45,9 +45,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,
|
||||||
|
|
@ -65,9 +63,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,
|
||||||
|
|
@ -83,9 +79,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,
|
||||||
|
|
@ -98,9 +92,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,
|
||||||
|
|
@ -160,9 +152,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(
|
||||||
{
|
{
|
||||||
|
|
@ -199,9 +189,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)
|
||||||
|
|
||||||
|
|
@ -265,18 +253,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
|
||||||
|
|
@ -303,18 +287,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:
|
||||||
|
|
@ -375,17 +355,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(
|
||||||
{
|
{
|
||||||
|
|
@ -422,9 +398,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)
|
||||||
|
|
||||||
|
|
@ -587,9 +561,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(
|
||||||
{
|
{
|
||||||
|
|
@ -619,9 +591,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()
|
||||||
|
|
@ -632,9 +602,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:
|
||||||
|
|
@ -667,9 +635,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)
|
||||||
|
|
@ -723,14 +689,11 @@ 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)
|
||||||
|
|
||||||
|
|
||||||
class DuplicateAssetEndpoint(BaseAPIView):
|
class DuplicateAssetEndpoint(BaseAPIView):
|
||||||
|
|
||||||
throttle_classes = [AssetRateThrottle]
|
throttle_classes = [AssetRateThrottle]
|
||||||
|
|
||||||
def get_entity_id_field(self, entity_type, entity_id):
|
def get_entity_id_field(self, entity_type, entity_id):
|
||||||
|
|
@ -772,11 +735,7 @@ class DuplicateAssetEndpoint(BaseAPIView):
|
||||||
entity_id = request.data.get("entity_id", None)
|
entity_id = request.data.get("entity_id", None)
|
||||||
entity_type = request.data.get("entity_type", None)
|
entity_type = request.data.get("entity_type", None)
|
||||||
|
|
||||||
|
if not entity_type or entity_type not in FileAsset.EntityTypeContext.values:
|
||||||
if (
|
|
||||||
not entity_type
|
|
||||||
or entity_type not in FileAsset.EntityTypeContext.values
|
|
||||||
):
|
|
||||||
return Response(
|
return Response(
|
||||||
{"error": "Invalid entity type or entity id"},
|
{"error": "Invalid entity type or entity id"},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
|
@ -786,23 +745,15 @@ class DuplicateAssetEndpoint(BaseAPIView):
|
||||||
if project_id:
|
if project_id:
|
||||||
# check if project exists in the workspace
|
# check if project exists in the workspace
|
||||||
if not Project.objects.filter(id=project_id, workspace=workspace).exists():
|
if not Project.objects.filter(id=project_id, workspace=workspace).exists():
|
||||||
return Response(
|
return Response({"error": "Project not found"}, status=status.HTTP_404_NOT_FOUND)
|
||||||
{"error": "Project not found"}, status=status.HTTP_404_NOT_FOUND
|
|
||||||
)
|
|
||||||
|
|
||||||
storage = S3Storage(request=request)
|
storage = S3Storage(request=request)
|
||||||
original_asset = FileAsset.objects.filter(
|
original_asset = FileAsset.objects.filter(id=asset_id, is_uploaded=True).first()
|
||||||
id=asset_id, is_uploaded=True
|
|
||||||
).first()
|
|
||||||
|
|
||||||
if not original_asset:
|
if not original_asset:
|
||||||
return Response(
|
return Response({"error": "Asset not found"}, status=status.HTTP_404_NOT_FOUND)
|
||||||
{"error": "Asset not found"}, status=status.HTTP_404_NOT_FOUND
|
|
||||||
)
|
|
||||||
|
|
||||||
destination_key = (
|
destination_key = f"{workspace.id}/{uuid.uuid4().hex}-{original_asset.attributes.get('name')}"
|
||||||
f"{workspace.id}/{uuid.uuid4().hex}-{original_asset.attributes.get('name')}"
|
|
||||||
)
|
|
||||||
duplicated_asset = FileAsset.objects.create(
|
duplicated_asset = FileAsset.objects.create(
|
||||||
attributes={
|
attributes={
|
||||||
"name": original_asset.attributes.get("name"),
|
"name": original_asset.attributes.get("name"),
|
||||||
|
|
@ -822,9 +773,7 @@ class DuplicateAssetEndpoint(BaseAPIView):
|
||||||
# Update the is_uploaded field for all newly created assets
|
# Update the is_uploaded field for all newly created assets
|
||||||
FileAsset.objects.filter(id=duplicated_asset.id).update(is_uploaded=True)
|
FileAsset.objects.filter(id=duplicated_asset.id).update(is_uploaded=True)
|
||||||
|
|
||||||
return Response(
|
return Response({"asset_id": str(duplicated_asset.id)}, status=status.HTTP_200_OK)
|
||||||
{"asset_id": str(duplicated_asset.id)}, status=status.HTTP_200_OK
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class WorkspaceAssetDownloadEndpoint(BaseAPIView):
|
class WorkspaceAssetDownloadEndpoint(BaseAPIView):
|
||||||
|
|
|
||||||
|
|
@ -97,9 +97,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 +148,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 +167,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 +198,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 +265,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 +308,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 +324,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 +338,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 +452,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 +474,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 +525,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,
|
||||||
|
|
@ -666,12 +629,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
|
||||||
)
|
)
|
||||||
|
|
@ -695,13 +654,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",
|
||||||
|
|
@ -747,9 +702,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:
|
||||||
|
|
@ -809,22 +762,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,
|
||||||
|
|
@ -842,9 +784,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",
|
||||||
|
|
@ -927,9 +867,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()),
|
||||||
|
|
@ -964,9 +902,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()),
|
||||||
|
|
@ -1068,11 +1004,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",
|
||||||
|
|
|
||||||
|
|
@ -39,9 +39,7 @@ class LabelViewSet(BaseViewSet):
|
||||||
@allow_permission([ROLE.ADMIN])
|
@allow_permission([ROLE.ADMIN])
|
||||||
def create(self, request, slug, project_id):
|
def create(self, request, slug, project_id):
|
||||||
try:
|
try:
|
||||||
serializer = LabelSerializer(
|
serializer = LabelSerializer(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)
|
serializer.save(project_id=project_id)
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
|
|
||||||
|
|
@ -495,14 +495,12 @@ class PagesDescriptionViewSet(BaseViewSet):
|
||||||
permission_classes = [ProjectPagePermission]
|
permission_classes = [ProjectPagePermission]
|
||||||
|
|
||||||
def retrieve(self, request, slug, project_id, page_id):
|
def retrieve(self, request, slug, project_id, page_id):
|
||||||
page = (
|
page = Page.objects.get(
|
||||||
Page.objects.get(
|
Q(owned_by=self.request.user) | Q(access=0),
|
||||||
Q(owned_by=self.request.user) | Q(access=0),
|
pk=page_id,
|
||||||
pk=page_id,
|
workspace__slug=slug,
|
||||||
workspace__slug=slug,
|
projects__id=project_id,
|
||||||
projects__id=project_id,
|
project_pages__deleted_at__isnull=True,
|
||||||
project_pages__deleted_at__isnull=True,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
binary_data = page.description_binary
|
binary_data = page.description_binary
|
||||||
|
|
||||||
|
|
@ -517,14 +515,12 @@ class PagesDescriptionViewSet(BaseViewSet):
|
||||||
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.get(
|
||||||
Page.objects.get(
|
Q(owned_by=self.request.user) | Q(access=0),
|
||||||
Q(owned_by=self.request.user) | Q(access=0),
|
pk=page_id,
|
||||||
pk=page_id,
|
workspace__slug=slug,
|
||||||
workspace__slug=slug,
|
projects__id=project_id,
|
||||||
projects__id=project_id,
|
project_pages__deleted_at__isnull=True,
|
||||||
project_pages__deleted_at__isnull=True,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if page.is_locked:
|
if page.is_locked:
|
||||||
|
|
|
||||||
|
|
@ -90,9 +90,9 @@ class Adapter:
|
||||||
"""Check if sign up is enabled or not and raise exception if not enabled"""
|
"""Check if sign up is enabled or not and raise exception if not enabled"""
|
||||||
|
|
||||||
# Get configuration value
|
# Get configuration value
|
||||||
(ENABLE_SIGNUP,) = get_configuration_value([
|
(ENABLE_SIGNUP,) = get_configuration_value(
|
||||||
{"key": "ENABLE_SIGNUP", "default": os.environ.get("ENABLE_SIGNUP", "1")}
|
[{"key": "ENABLE_SIGNUP", "default": os.environ.get("ENABLE_SIGNUP", "1")}]
|
||||||
])
|
)
|
||||||
|
|
||||||
# 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 ENABLE_SIGNUP == "0" and not WorkspaceMemberInvite.objects.filter(email=email).exists():
|
if ENABLE_SIGNUP == "0" and not WorkspaceMemberInvite.objects.filter(email=email).exists():
|
||||||
|
|
|
||||||
|
|
@ -101,9 +101,7 @@ class GiteaOAuthProvider(OauthAdapter):
|
||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
"refresh_token_expired_at": (
|
"refresh_token_expired_at": (
|
||||||
datetime.fromtimestamp(
|
datetime.fromtimestamp(token_response.get("refresh_token_expired_at"), tz=pytz.utc)
|
||||||
token_response.get("refresh_token_expired_at"), tz=pytz.utc
|
|
||||||
)
|
|
||||||
if token_response.get("refresh_token_expired_at")
|
if token_response.get("refresh_token_expired_at")
|
||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -37,9 +37,7 @@ class GiteaOauthInitiateEndpoint(View):
|
||||||
params = exc.get_error_dict()
|
params = exc.get_error_dict()
|
||||||
if next_path:
|
if next_path:
|
||||||
params["next_path"] = str(validate_next_path(next_path))
|
params["next_path"] = str(validate_next_path(next_path))
|
||||||
url = urljoin(
|
url = urljoin(base_host(request=request, is_app=True), "?" + urlencode(params))
|
||||||
base_host(request=request, is_app=True), "?" + urlencode(params)
|
|
||||||
)
|
|
||||||
return HttpResponseRedirect(url)
|
return HttpResponseRedirect(url)
|
||||||
try:
|
try:
|
||||||
state = uuid.uuid4().hex
|
state = uuid.uuid4().hex
|
||||||
|
|
@ -51,9 +49,7 @@ class GiteaOauthInitiateEndpoint(View):
|
||||||
params = e.get_error_dict()
|
params = e.get_error_dict()
|
||||||
if next_path:
|
if next_path:
|
||||||
params["next_path"] = str(validate_next_path(next_path))
|
params["next_path"] = str(validate_next_path(next_path))
|
||||||
url = urljoin(
|
url = urljoin(base_host(request=request, is_app=True), "?" + urlencode(params))
|
||||||
base_host(request=request, is_app=True), "?" + urlencode(params)
|
|
||||||
)
|
|
||||||
return HttpResponseRedirect(url)
|
return HttpResponseRedirect(url)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -87,9 +83,7 @@ class GiteaCallbackEndpoint(View):
|
||||||
return HttpResponseRedirect(url)
|
return HttpResponseRedirect(url)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
provider = GiteaOAuthProvider(
|
provider = GiteaOAuthProvider(request=request, code=code, callback=post_user_auth_workflow)
|
||||||
request=request, code=code, callback=post_user_auth_workflow
|
|
||||||
)
|
|
||||||
user = provider.authenticate()
|
user = provider.authenticate()
|
||||||
# Login the user and record his device info
|
# Login the user and record his device info
|
||||||
user_login(request=request, user=user, is_app=True)
|
user_login(request=request, user=user, is_app=True)
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,6 @@ def page_transaction(new_description_html, old_description_html, page_id):
|
||||||
|
|
||||||
has_existing_logs = PageLog.objects.filter(page_id=page_id).exists()
|
has_existing_logs = PageLog.objects.filter(page_id=page_id).exists()
|
||||||
|
|
||||||
|
|
||||||
# Extract all components in a single pass (optimized)
|
# Extract all components in a single pass (optimized)
|
||||||
old_components = extract_all_components(old_description_html)
|
old_components = extract_all_components(old_description_html)
|
||||||
new_components = extract_all_components(new_description_html)
|
new_components = extract_all_components(new_description_html)
|
||||||
|
|
@ -125,12 +124,9 @@ def page_transaction(new_description_html, old_description_html, page_id):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# Bulk insert and cleanup
|
# Bulk insert and cleanup
|
||||||
if new_transactions:
|
if new_transactions:
|
||||||
PageLog.objects.bulk_create(
|
PageLog.objects.bulk_create(new_transactions, batch_size=50, ignore_conflicts=True)
|
||||||
new_transactions, batch_size=50, ignore_conflicts=True
|
|
||||||
)
|
|
||||||
|
|
||||||
if deleted_transaction_ids:
|
if deleted_transaction_ids:
|
||||||
PageLog.objects.filter(transaction__in=deleted_transaction_ids).delete()
|
PageLog.objects.filter(transaction__in=deleted_transaction_ids).delete()
|
||||||
|
|
|
||||||
|
|
@ -107,56 +107,60 @@ def create_project_and_member(workspace: Workspace, bot_user: User) -> Dict[int,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create project members
|
# Create project members
|
||||||
ProjectMember.objects.bulk_create([
|
ProjectMember.objects.bulk_create(
|
||||||
ProjectMember(
|
[
|
||||||
project=project,
|
ProjectMember(
|
||||||
member_id=workspace_member["member_id"],
|
project=project,
|
||||||
role=workspace_member["role"],
|
member_id=workspace_member["member_id"],
|
||||||
workspace_id=workspace.id,
|
role=workspace_member["role"],
|
||||||
created_by_id=bot_user.id,
|
workspace_id=workspace.id,
|
||||||
)
|
created_by_id=bot_user.id,
|
||||||
for workspace_member in workspace_members
|
)
|
||||||
])
|
for workspace_member in workspace_members
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
# Create issue user properties
|
# Create issue user properties
|
||||||
IssueUserProperty.objects.bulk_create([
|
IssueUserProperty.objects.bulk_create(
|
||||||
IssueUserProperty(
|
[
|
||||||
project=project,
|
IssueUserProperty(
|
||||||
user_id=workspace_member["member_id"],
|
project=project,
|
||||||
workspace_id=workspace.id,
|
user_id=workspace_member["member_id"],
|
||||||
display_filters={
|
workspace_id=workspace.id,
|
||||||
"layout": "list",
|
display_filters={
|
||||||
"calendar": {"layout": "month", "show_weekends": False},
|
"layout": "list",
|
||||||
"group_by": "state",
|
"calendar": {"layout": "month", "show_weekends": False},
|
||||||
"order_by": "sort_order",
|
"group_by": "state",
|
||||||
"sub_issue": True,
|
"order_by": "sort_order",
|
||||||
"sub_group_by": None,
|
"sub_issue": True,
|
||||||
"show_empty_groups": True,
|
"sub_group_by": None,
|
||||||
},
|
"show_empty_groups": True,
|
||||||
display_properties={
|
},
|
||||||
"key": True,
|
display_properties={
|
||||||
"link": True,
|
"key": True,
|
||||||
"cycle": False,
|
"link": True,
|
||||||
"state": True,
|
"cycle": False,
|
||||||
"labels": False,
|
"state": True,
|
||||||
"modules": False,
|
"labels": False,
|
||||||
"assignee": True,
|
"modules": False,
|
||||||
"due_date": False,
|
"assignee": True,
|
||||||
"estimate": True,
|
"due_date": False,
|
||||||
"priority": True,
|
"estimate": True,
|
||||||
"created_on": True,
|
"priority": True,
|
||||||
"issue_type": True,
|
"created_on": True,
|
||||||
"start_date": False,
|
"issue_type": True,
|
||||||
"updated_on": True,
|
"start_date": False,
|
||||||
"customer_count": True,
|
"updated_on": True,
|
||||||
"sub_issue_count": False,
|
"customer_count": True,
|
||||||
"attachment_count": False,
|
"sub_issue_count": False,
|
||||||
"customer_request_count": True,
|
"attachment_count": False,
|
||||||
},
|
"customer_request_count": True,
|
||||||
created_by_id=bot_user.id,
|
},
|
||||||
)
|
created_by_id=bot_user.id,
|
||||||
for workspace_member in workspace_members
|
)
|
||||||
])
|
for workspace_member in workspace_members
|
||||||
|
]
|
||||||
|
)
|
||||||
# update map
|
# update map
|
||||||
projects_map[project_id] = project.id
|
projects_map[project_id] = project.id
|
||||||
logger.info(f"Task: workspace_seed_task -> Project {project_id} created")
|
logger.info(f"Task: workspace_seed_task -> Project {project_id} created")
|
||||||
|
|
|
||||||
|
|
@ -134,8 +134,10 @@ class InstanceAdminSignUpEndpoint(View):
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
url = urljoin(
|
url = urljoin(
|
||||||
base_host(request=request, is_admin=True, ),
|
base_host(
|
||||||
|
request=request,
|
||||||
|
is_admin=True,
|
||||||
|
),
|
||||||
"?" + urlencode(exc.get_error_dict()),
|
"?" + urlencode(exc.get_error_dict()),
|
||||||
)
|
)
|
||||||
return HttpResponseRedirect(url)
|
return HttpResponseRedirect(url)
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,7 @@ class TestLabelSerializer:
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_label_serializer_create_valid_data(self, db, workspace):
|
def test_label_serializer_create_valid_data(self, db, workspace):
|
||||||
"""Test creating a label with valid data"""
|
"""Test creating a label with valid data"""
|
||||||
project = Project.objects.create(
|
project = Project.objects.create(name="Test Project", identifier="TEST", workspace=workspace)
|
||||||
name="Test Project", identifier="TEST", workspace=workspace
|
|
||||||
)
|
|
||||||
|
|
||||||
serializer = LabelSerializer(
|
serializer = LabelSerializer(
|
||||||
data={"name": "Test Label"},
|
data={"name": "Test Label"},
|
||||||
|
|
@ -30,14 +28,10 @@ class TestLabelSerializer:
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_label_serializer_create_duplicate_name(self, db, workspace):
|
def test_label_serializer_create_duplicate_name(self, db, workspace):
|
||||||
"""Test creating a label with a duplicate name"""
|
"""Test creating a label with a duplicate name"""
|
||||||
project = Project.objects.create(
|
project = Project.objects.create(name="Test Project", identifier="TEST", workspace=workspace)
|
||||||
name="Test Project", identifier="TEST", workspace=workspace
|
|
||||||
)
|
|
||||||
|
|
||||||
Label.objects.create(name="Test Label", project=project)
|
Label.objects.create(name="Test Label", project=project)
|
||||||
|
|
||||||
serializer = LabelSerializer(
|
serializer = LabelSerializer(data={"name": "Test Label"}, context={"project_id": project.id})
|
||||||
data={"name": "Test Label"}, context={"project_id": project.id}
|
|
||||||
)
|
|
||||||
assert not serializer.is_valid()
|
assert not serializer.is_valid()
|
||||||
assert serializer.errors == {"name": ["LABEL_NAME_ALREADY_EXISTS"]}
|
assert serializer.errors == {"name": ["LABEL_NAME_ALREADY_EXISTS"]}
|
||||||
|
|
|
||||||
|
|
@ -56,9 +56,7 @@ def validate_binary_data(data):
|
||||||
# Check for suspicious text patterns (HTML/JS)
|
# Check for suspicious text patterns (HTML/JS)
|
||||||
try:
|
try:
|
||||||
decoded_text = binary_data.decode("utf-8", errors="ignore")[:200]
|
decoded_text = binary_data.decode("utf-8", errors="ignore")[:200]
|
||||||
if any(
|
if any(pattern in decoded_text.lower() for pattern in SUSPICIOUS_BINARY_PATTERNS):
|
||||||
pattern in decoded_text.lower() for pattern in SUSPICIOUS_BINARY_PATTERNS
|
|
||||||
):
|
|
||||||
return False, "Binary data contains suspicious content patterns"
|
return False, "Binary data contains suspicious content patterns"
|
||||||
except Exception:
|
except Exception:
|
||||||
pass # Binary data might not be decodable as text, which is fine
|
pass # Binary data might not be decodable as text, which is fine
|
||||||
|
|
|
||||||
|
|
@ -51,9 +51,7 @@ def transfer_cycle_issues(
|
||||||
dict: Response data with success or error message
|
dict: Response data with success or error message
|
||||||
"""
|
"""
|
||||||
# Get the new cycle
|
# Get the new cycle
|
||||||
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()
|
|
||||||
|
|
||||||
# Check if new cycle is already completed
|
# Check if new cycle is already completed
|
||||||
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():
|
||||||
|
|
@ -216,9 +214,7 @@ def transfer_cycle_issues(
|
||||||
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_url": item.get("avatar_url"),
|
"avatar_url": item.get("avatar_url"),
|
||||||
"total_estimates": item["total_estimates"],
|
"total_estimates": item["total_estimates"],
|
||||||
"completed_estimates": item["completed_estimates"],
|
"completed_estimates": item["completed_estimates"],
|
||||||
|
|
@ -310,9 +306,7 @@ def transfer_cycle_issues(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.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",
|
||||||
|
|
@ -360,9 +354,7 @@ def transfer_cycle_issues(
|
||||||
.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",
|
||||||
|
|
@ -409,9 +401,7 @@ def transfer_cycle_issues(
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get the current cycle and save progress snapshot
|
# Get the current cycle and save progress snapshot
|
||||||
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,
|
||||||
|
|
@ -461,9 +451,7 @@ def transfer_cycle_issues(
|
||||||
)
|
)
|
||||||
|
|
||||||
# Bulk update cycle issues
|
# Bulk update cycle issues
|
||||||
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(
|
||||||
|
|
|
||||||
|
|
@ -263,6 +263,7 @@ def state_docs(**kwargs):
|
||||||
|
|
||||||
return extend_schema(**_merge_schema_options(defaults, kwargs))
|
return extend_schema(**_merge_schema_options(defaults, kwargs))
|
||||||
|
|
||||||
|
|
||||||
def sticky_docs(**kwargs):
|
def sticky_docs(**kwargs):
|
||||||
"""Decorator for sticky management endpoints"""
|
"""Decorator for sticky management endpoints"""
|
||||||
defaults = {
|
defaults = {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue