ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: additive read-layer двусторонней доски внешних контуров
This commit is contained in:
@@ -6,6 +6,8 @@ from django.urls import path
|
||||
|
||||
from plane.app.views import (
|
||||
ExternalContourAttachmentDownloadEndpoint,
|
||||
ExternalContourBoardEndpoint,
|
||||
ExternalContourBoardItemDetailEndpoint,
|
||||
ExternalContourDetailEndpoint,
|
||||
ExternalContourDecisionEndpoint,
|
||||
ExternalContourListCreateEndpoint,
|
||||
@@ -16,6 +18,16 @@ from plane.app.views import (
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/external-contours/board/",
|
||||
ExternalContourBoardEndpoint.as_view(http_method_names=["get"]),
|
||||
name="external-contour-board",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/external-contours/board-items/<uuid:request_id>/",
|
||||
ExternalContourBoardItemDetailEndpoint.as_view(http_method_names=["get"]),
|
||||
name="external-contour-board-item-detail",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/external-contours/",
|
||||
ExternalContourListCreateEndpoint.as_view(http_method_names=["get", "post"]),
|
||||
|
||||
@@ -226,6 +226,8 @@ from .notification.base import (
|
||||
from .exporter.base import ExportIssuesEndpoint
|
||||
from .external_contours import (
|
||||
ExternalContourAttachmentDownloadEndpoint,
|
||||
ExternalContourBoardEndpoint,
|
||||
ExternalContourBoardItemDetailEndpoint,
|
||||
ExternalContourListCreateEndpoint,
|
||||
ExternalContourDetailEndpoint,
|
||||
ExternalContourDecisionEndpoint,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
# See the LICENSE file for details.
|
||||
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils import timezone
|
||||
from rest_framework.exceptions import ValidationError
|
||||
@@ -11,6 +12,7 @@ from rest_framework.response import Response
|
||||
from plane.utils.host import base_host
|
||||
|
||||
from plane.api.serializers import (
|
||||
ExternalContourBoardItemSerializer,
|
||||
ExternalContourRequestCreateSerializer,
|
||||
ExternalContourRequestDecisionSerializer,
|
||||
ExternalContourRequestReplySerializer,
|
||||
@@ -258,6 +260,302 @@ class ExternalContourTargetOptionsEndpoint(BaseAPIView):
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class ExternalContourReadMixin:
|
||||
CLOSED_STATE_GROUPS = (StateGroup.COMPLETED.value, StateGroup.CANCELLED.value)
|
||||
BOARD_ORDERING_MAP = {
|
||||
"requested_at": "created_at",
|
||||
"updated_at": "updated_at",
|
||||
"issue__sequence_id": "issue__sequence_id",
|
||||
"target_date": "issue__target_date",
|
||||
}
|
||||
|
||||
def get_base_queryset(self):
|
||||
return (
|
||||
IntakeIssue.objects.filter(
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
extra__bridge="external-contours",
|
||||
)
|
||||
.select_related(
|
||||
"issue",
|
||||
"issue__state",
|
||||
"issue__project",
|
||||
"issue__created_by",
|
||||
)
|
||||
.prefetch_related("issue__issue_assignee__assignee", "issue__label_issue__label")
|
||||
)
|
||||
|
||||
def get_outgoing_queryset(self):
|
||||
return self.get_base_queryset().filter(extra__source_project_id=str(self.kwargs.get("project_id")))
|
||||
|
||||
def get_incoming_queryset(self):
|
||||
return self.get_base_queryset().filter(project_id=self.kwargs.get("project_id"))
|
||||
|
||||
def get_board_item_queryset(self):
|
||||
return self.get_base_queryset().filter(
|
||||
Q(extra__source_project_id=str(self.kwargs.get("project_id"))) | Q(project_id=self.kwargs.get("project_id"))
|
||||
)
|
||||
|
||||
def parse_csv_param(self, request, key):
|
||||
value = (request.query_params.get(key) or "").strip()
|
||||
if not value:
|
||||
return []
|
||||
return [item.strip() for item in value.split(",") if item.strip()]
|
||||
|
||||
def parse_bool_param(self, request, key):
|
||||
value = (request.query_params.get(key) or "").strip().lower()
|
||||
if value in ["true", "1", "yes"]:
|
||||
return True
|
||||
if value in ["false", "0", "no"]:
|
||||
return False
|
||||
return None
|
||||
|
||||
def get_requested_directions(self, request):
|
||||
directions = set(self.parse_csv_param(request, "direction"))
|
||||
valid_directions = {"outgoing", "incoming"}
|
||||
return directions.intersection(valid_directions) or valid_directions
|
||||
|
||||
def get_applied_filters(self, request):
|
||||
filters = {}
|
||||
for key in [
|
||||
"direction",
|
||||
"status",
|
||||
"state_ids",
|
||||
"priority",
|
||||
"assignee_ids",
|
||||
"created_by_ids",
|
||||
"requested_by_ids",
|
||||
"source_project_ids",
|
||||
"target_project_ids",
|
||||
"label_ids",
|
||||
]:
|
||||
filters[key] = self.parse_csv_param(request, key)
|
||||
|
||||
filters["has_unread_updates"] = self.parse_bool_param(request, "has_unread_updates")
|
||||
filters["search"] = (request.query_params.get("search") or "").strip()
|
||||
return filters
|
||||
|
||||
def get_sorting(self, request):
|
||||
order_by = (request.query_params.get("order_by") or "updated_at").strip()
|
||||
sort_by = (request.query_params.get("sort_by") or "desc").strip().lower()
|
||||
|
||||
if order_by not in self.BOARD_ORDERING_MAP:
|
||||
order_by = "updated_at"
|
||||
if sort_by not in ["asc", "desc"]:
|
||||
sort_by = "desc"
|
||||
|
||||
return {
|
||||
"order_by": order_by,
|
||||
"sort_by": sort_by,
|
||||
}
|
||||
|
||||
def get_unread_request_ids(self, user, request_ids=None):
|
||||
user_id = getattr(user, "id", None)
|
||||
if not user_id:
|
||||
return set()
|
||||
|
||||
queryset = Notification.objects.filter(
|
||||
receiver_id=user_id,
|
||||
sender__startswith="in_app:external_contours:",
|
||||
read_at__isnull=True,
|
||||
)
|
||||
if request_ids:
|
||||
queryset = queryset.filter(data__issue__id__in=[str(request_id) for request_id in request_ids])
|
||||
|
||||
return {
|
||||
str(request_id)
|
||||
for request_id in queryset.values_list("data__issue__id", flat=True)
|
||||
if request_id
|
||||
}
|
||||
|
||||
def apply_status_filter(self, queryset, statuses):
|
||||
normalized_statuses = {status_value for status_value in statuses if status_value in ["open", "closed"]}
|
||||
if not normalized_statuses or normalized_statuses == {"open", "closed"}:
|
||||
return queryset
|
||||
if normalized_statuses == {"open"}:
|
||||
return queryset.exclude(issue__state__group__in=self.CLOSED_STATE_GROUPS)
|
||||
if normalized_statuses == {"closed"}:
|
||||
return queryset.filter(issue__state__group__in=self.CLOSED_STATE_GROUPS)
|
||||
return queryset
|
||||
|
||||
def apply_board_filters(self, queryset, request):
|
||||
filters = self.get_applied_filters(request)
|
||||
|
||||
if filters["search"]:
|
||||
queryset = queryset.filter(
|
||||
Q(issue__name__icontains=filters["search"]) | Q(issue__description_html__icontains=filters["search"])
|
||||
)
|
||||
|
||||
queryset = self.apply_status_filter(queryset, filters["status"])
|
||||
|
||||
if filters["state_ids"]:
|
||||
queryset = queryset.filter(issue__state_id__in=filters["state_ids"])
|
||||
if filters["priority"]:
|
||||
queryset = queryset.filter(issue__priority__in=filters["priority"])
|
||||
if filters["assignee_ids"]:
|
||||
queryset = queryset.filter(issue__issue_assignee__assignee_id__in=filters["assignee_ids"])
|
||||
if filters["created_by_ids"]:
|
||||
queryset = queryset.filter(issue__created_by_id__in=filters["created_by_ids"])
|
||||
if filters["requested_by_ids"]:
|
||||
queryset = queryset.filter(extra__requested_by_id__in=filters["requested_by_ids"])
|
||||
if filters["source_project_ids"]:
|
||||
queryset = queryset.filter(extra__source_project_id__in=filters["source_project_ids"])
|
||||
if filters["target_project_ids"]:
|
||||
queryset = queryset.filter(
|
||||
Q(extra__target_project_id__in=filters["target_project_ids"])
|
||||
| Q(issue__project_id__in=filters["target_project_ids"])
|
||||
)
|
||||
if filters["label_ids"]:
|
||||
queryset = queryset.filter(issue__label_issue__label_id__in=filters["label_ids"])
|
||||
if filters["has_unread_updates"] is not None:
|
||||
unread_request_ids = self.get_unread_request_ids(request.user)
|
||||
if filters["has_unread_updates"]:
|
||||
queryset = queryset.filter(pk__in=unread_request_ids or ["00000000-0000-0000-0000-000000000000"])
|
||||
else:
|
||||
queryset = queryset.exclude(pk__in=unread_request_ids)
|
||||
|
||||
sorting = self.get_sorting(request)
|
||||
ordering = self.BOARD_ORDERING_MAP[sorting["order_by"]]
|
||||
if sorting["sort_by"] == "desc":
|
||||
ordering = f"-{ordering}"
|
||||
|
||||
return queryset.order_by(ordering).distinct()
|
||||
|
||||
def build_project_map(self, contour_requests):
|
||||
project_ids = {
|
||||
str(project_id)
|
||||
for contour_request in contour_requests
|
||||
for project_id in [
|
||||
contour_request.extra.get("source_project_id"),
|
||||
contour_request.extra.get("target_project_id") or (str(contour_request.issue.project_id) if contour_request.issue_id else None),
|
||||
]
|
||||
if project_id
|
||||
}
|
||||
|
||||
if not project_ids:
|
||||
return {}
|
||||
|
||||
return {
|
||||
str(project.id): project
|
||||
for project in Project.objects.filter(pk__in=project_ids)
|
||||
}
|
||||
|
||||
def build_member_project_ids(self, request, contour_requests):
|
||||
issue_project_ids = {
|
||||
str(contour_request.issue.project_id)
|
||||
for contour_request in contour_requests
|
||||
if contour_request.issue_id and contour_request.issue and contour_request.issue.project_id
|
||||
}
|
||||
|
||||
if not issue_project_ids:
|
||||
return set()
|
||||
|
||||
return {
|
||||
str(project_id)
|
||||
for project_id in ProjectMember.objects.filter(
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
member=request.user,
|
||||
project_id__in=issue_project_ids,
|
||||
is_active=True,
|
||||
).values_list("project_id", flat=True)
|
||||
}
|
||||
|
||||
def build_serializer_context(self, request, contour_requests, current_project_id, include_mirror_data=False):
|
||||
contour_requests = list(contour_requests)
|
||||
unread_request_ids = self.get_unread_request_ids(request.user, request_ids=[contour_request.id for contour_request in contour_requests])
|
||||
return {
|
||||
"include_mirror_data": include_mirror_data,
|
||||
"workspace_slug": self.kwargs.get("slug"),
|
||||
"source_project_id": str(current_project_id),
|
||||
"current_project_id": str(current_project_id),
|
||||
"project_map": self.build_project_map(contour_requests),
|
||||
"member_project_ids": self.build_member_project_ids(request, contour_requests),
|
||||
"unread_request_ids": unread_request_ids,
|
||||
"request": request,
|
||||
}
|
||||
|
||||
def mark_request_notifications_read(self, user, contour_request):
|
||||
user_id = getattr(user, "id", None)
|
||||
if not user_id:
|
||||
return
|
||||
|
||||
Notification.objects.filter(
|
||||
receiver_id=user_id,
|
||||
sender__startswith="in_app:external_contours:",
|
||||
read_at__isnull=True,
|
||||
data__issue__id=str(contour_request.id),
|
||||
).update(read_at=timezone.now())
|
||||
|
||||
|
||||
class ExternalContourBoardEndpoint(ExternalContourReadMixin, BaseAPIView):
|
||||
permission_classes = [ProjectLitePermission]
|
||||
serializer_class = ExternalContourBoardItemSerializer
|
||||
|
||||
def get(self, request, slug, project_id):
|
||||
requested_directions = self.get_requested_directions(request)
|
||||
current_project_id = str(project_id)
|
||||
|
||||
outgoing_queryset = self.apply_board_filters(self.get_outgoing_queryset(), request)
|
||||
incoming_queryset = self.apply_board_filters(self.get_incoming_queryset(), request)
|
||||
|
||||
outgoing_requests = list(outgoing_queryset) if "outgoing" in requested_directions else []
|
||||
incoming_requests = list(incoming_queryset) if "incoming" in requested_directions else []
|
||||
|
||||
outgoing_serializer = ExternalContourBoardItemSerializer(
|
||||
outgoing_requests,
|
||||
many=True,
|
||||
context=self.build_serializer_context(request, outgoing_requests, current_project_id=current_project_id),
|
||||
)
|
||||
incoming_serializer = ExternalContourBoardItemSerializer(
|
||||
incoming_requests,
|
||||
many=True,
|
||||
context=self.build_serializer_context(request, incoming_requests, current_project_id=current_project_id),
|
||||
)
|
||||
|
||||
return Response(
|
||||
{
|
||||
"filters": self.get_applied_filters(request),
|
||||
"sorting": self.get_sorting(request),
|
||||
"columns": [
|
||||
{
|
||||
"key": "outgoing",
|
||||
"title": "Исходящие",
|
||||
"total_count": len(outgoing_requests),
|
||||
"next_cursor": "",
|
||||
"results": outgoing_serializer.data,
|
||||
},
|
||||
{
|
||||
"key": "incoming",
|
||||
"title": "Входящие",
|
||||
"total_count": len(incoming_requests),
|
||||
"next_cursor": "",
|
||||
"results": incoming_serializer.data,
|
||||
},
|
||||
],
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
|
||||
class ExternalContourBoardItemDetailEndpoint(ExternalContourReadMixin, BaseAPIView):
|
||||
permission_classes = [ProjectLitePermission]
|
||||
serializer_class = ExternalContourBoardItemSerializer
|
||||
|
||||
def get(self, request, slug, project_id, request_id):
|
||||
contour_request = get_object_or_404(self.get_board_item_queryset(), pk=request_id)
|
||||
self.mark_request_notifications_read(request.user, contour_request)
|
||||
|
||||
serializer = ExternalContourBoardItemSerializer(
|
||||
contour_request,
|
||||
context=self.build_serializer_context(
|
||||
request,
|
||||
[contour_request],
|
||||
current_project_id=str(project_id),
|
||||
include_mirror_data=True,
|
||||
),
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class ExternalContourDetailEndpoint(BaseAPIView):
|
||||
permission_classes = [ProjectLitePermission]
|
||||
serializer_class = ExternalContourRequestSerializer
|
||||
@@ -519,16 +817,13 @@ class ExternalContourReplyEndpoint(BaseAPIView):
|
||||
return Response(response_serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class ExternalContourAttachmentDownloadEndpoint(BaseAPIView):
|
||||
class ExternalContourAttachmentDownloadEndpoint(ExternalContourReadMixin, BaseAPIView):
|
||||
permission_classes = [ProjectLitePermission]
|
||||
|
||||
def get_queryset(self):
|
||||
return IntakeIssue.objects.filter(
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
extra__bridge="external-contours",
|
||||
extra__source_project_id=str(self.kwargs.get("project_id")),
|
||||
pk=self.kwargs.get("request_id"),
|
||||
).select_related("issue", "issue__project", "workspace")
|
||||
return self.get_board_item_queryset().filter(pk=self.kwargs.get("request_id")).select_related(
|
||||
"issue", "issue__project", "workspace"
|
||||
)
|
||||
|
||||
def get(self, request, slug, project_id, request_id, attachment_id):
|
||||
contour_request = get_object_or_404(self.get_queryset())
|
||||
|
||||
Reference in New Issue
Block a user