ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: причина отклонения, reply и in-app уведомления

This commit is contained in:
DCCONSTRUCTIONS
2026-04-19 09:22:15 +03:00
parent 61a8625a5c
commit 0a584abf26
25 changed files with 623 additions and 32 deletions
@@ -15,6 +15,7 @@ class NotificationSerializer(BaseSerializer):
triggered_by_details = UserLiteSerializer(read_only=True, source="triggered_by")
is_inbox_issue = serializers.BooleanField(read_only=True)
is_intake_issue = serializers.BooleanField(read_only=True)
is_external_contour = serializers.BooleanField(read_only=True)
is_mentioned_notification = serializers.BooleanField(read_only=True)
class Meta:
@@ -9,6 +9,7 @@ from plane.app.views import (
ExternalContourDetailEndpoint,
ExternalContourDecisionEndpoint,
ExternalContourListCreateEndpoint,
ExternalContourReplyEndpoint,
ExternalContourTargetOptionsEndpoint,
ExternalContourTargetProjectListEndpoint,
)
@@ -40,6 +41,11 @@ urlpatterns = [
ExternalContourDecisionEndpoint.as_view(http_method_names=["post"]),
name="external-contour-decision",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/external-contours/<uuid:request_id>/reply/",
ExternalContourReplyEndpoint.as_view(http_method_names=["post"]),
name="external-contour-reply",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/external-contours/<uuid:request_id>/attachments/<uuid:attachment_id>/",
ExternalContourAttachmentDownloadEndpoint.as_view(http_method_names=["get"]),
@@ -229,6 +229,7 @@ from .external_contours import (
ExternalContourListCreateEndpoint,
ExternalContourDetailEndpoint,
ExternalContourDecisionEndpoint,
ExternalContourReplyEndpoint,
ExternalContourTargetProjectListEndpoint,
ExternalContourTargetOptionsEndpoint,
)
@@ -5,12 +5,15 @@
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.utils import timezone
from rest_framework.exceptions import ValidationError
from rest_framework import status
from rest_framework.response import Response
from plane.utils.host import base_host
from plane.api.serializers import (
ExternalContourRequestCreateSerializer,
ExternalContourRequestDecisionSerializer,
ExternalContourRequestReplySerializer,
ExternalContourRequestSerializer,
ExternalContourTargetOptionsSerializer,
ExternalContourTargetProjectSerializer,
@@ -21,6 +24,7 @@ from plane.app.views.base import BaseAPIView
from plane.db.models import FileAsset, Intake, IntakeIssue, Label, Project, ProjectMember, State, StateGroup
from plane.db.models.intake import IntakeIssueStatus, SourceType
from plane.settings.storage import S3Storage
from plane.utils.external_contours import create_external_contour_issue_comment
class ExternalContourListCreateEndpoint(BaseAPIView):
@@ -309,6 +313,7 @@ class ExternalContourDecisionEndpoint(BaseAPIView):
serializer.is_valid(raise_exception=True)
action = serializer.validated_data["action"]
comment = (serializer.validated_data.get("comment") or "").strip()
issue = contour_request.issue
if not issue or not issue.state or issue.state.group not in [StateGroup.COMPLETED.value, StateGroup.CANCELLED.value]:
@@ -337,6 +342,16 @@ class ExternalContourDecisionEndpoint(BaseAPIView):
if not target_default_state:
return Response({"error": "Target project has no available workflow state"}, status=status.HTTP_400_BAD_REQUEST)
try:
create_external_contour_issue_comment(
issue=issue,
actor=request.user,
comment=comment,
origin=base_host(request=request, is_app=True),
)
except ValidationError as exc:
return Response({"error": exc.message_dict if hasattr(exc, "message_dict") else str(exc)}, status=status.HTTP_400_BAD_REQUEST)
issue.state = target_default_state
issue.save(update_fields=["state", "updated_at"])
@@ -344,6 +359,7 @@ class ExternalContourDecisionEndpoint(BaseAPIView):
extra.pop("source_decision", None)
extra.pop("source_decision_at", None)
extra.pop("source_decision_by_name", None)
extra["last_decline_comment"] = comment
extra["last_reopened_at"] = issue.updated_at.isoformat() if issue.updated_at else None
extra["last_reopened_by_name"] = request.user.display_name
contour_request.extra = extra
@@ -368,6 +384,58 @@ class ExternalContourDecisionEndpoint(BaseAPIView):
return Response(serializer.data, status=status.HTTP_200_OK)
class ExternalContourReplyEndpoint(BaseAPIView):
permission_classes = [ProjectLitePermission]
serializer_class = ExternalContourRequestSerializer
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__state",
"issue__project",
"issue__created_by",
)
.prefetch_related("issue__issue_assignee__assignee", "issue__label_issue__label")
)
def post(self, request, slug, project_id, request_id):
contour_request = get_object_or_404(self.get_queryset())
serializer = ExternalContourRequestReplySerializer(data=request.data)
serializer.is_valid(raise_exception=True)
issue = contour_request.issue
if not issue:
return Response({"error": "Target issue was not found"}, status=status.HTTP_404_NOT_FOUND)
try:
create_external_contour_issue_comment(
issue=issue,
actor=request.user,
comment=serializer.validated_data["comment"],
origin=base_host(request=request, is_app=True),
)
except ValidationError as exc:
return Response({"error": exc.message_dict if hasattr(exc, "message_dict") else str(exc)}, status=status.HTTP_400_BAD_REQUEST)
contour_request.refresh_from_db()
response_serializer = ExternalContourRequestSerializer(
contour_request,
context={
"include_mirror_data": True,
"workspace_slug": slug,
"source_project_id": str(project_id),
},
)
return Response(response_serializer.data, status=status.HTTP_200_OK)
class ExternalContourAttachmentDownloadEndpoint(BaseAPIView):
permission_classes = [ProjectLitePermission]
@@ -65,6 +65,13 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
.filter(entity_name="issue")
.annotate(is_inbox_issue=Exists(intake_issue))
.annotate(is_intake_issue=Exists(intake_issue))
.annotate(
is_external_contour=Case(
When(sender__startswith="in_app:external_contours:", then=True),
default=False,
output_field=BooleanField(),
)
)
.annotate(
is_mentioned_notification=Case(
When(sender__icontains="mentioned", then=True),