UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: индикатор новых изменений во внешних контурах

This commit is contained in:
DCCONSTRUCTIONS
2026-04-19 09:52:27 +03:00
parent 0a584abf26
commit 4dff521f7f
8 changed files with 107 additions and 16 deletions
@@ -10,7 +10,7 @@ from .project import ProjectLiteSerializer
from .state import StateLiteSerializer
from .user import UserLiteSerializer
from plane.app.serializers.issue import LabelSerializer
from plane.db.models import FileAsset, IntakeIssue, Issue, IssueActivity, IssueComment, Label, Project
from plane.db.models import FileAsset, IntakeIssue, Issue, IssueActivity, IssueComment, Label, Notification, Project
class ExternalContourIssuePayloadSerializer(serializers.Serializer):
@@ -149,6 +149,7 @@ class ExternalContourMirroredActivitySerializer(BaseSerializer):
class ExternalContourRequestSerializer(BaseSerializer):
has_unread_updates = serializers.SerializerMethodField()
issue = ExternalContourIssueSerializer(read_only=True)
mirrored_activity = serializers.SerializerMethodField()
mirrored_attachments = serializers.SerializerMethodField()
@@ -172,6 +173,7 @@ class ExternalContourRequestSerializer(BaseSerializer):
"created_at",
"updated_at",
"created_by",
"has_unread_updates",
"issue",
"mirrored_activity",
"mirrored_attachments",
@@ -193,6 +195,20 @@ class ExternalContourRequestSerializer(BaseSerializer):
def get_source_project_id(self, obj):
return obj.extra.get("source_project_id")
def get_has_unread_updates(self, obj):
request = self.context.get("request")
user = getattr(request, "user", None)
user_id = getattr(user, "id", None)
if not user_id:
return False
return Notification.objects.filter(
receiver_id=user_id,
sender__startswith="in_app:external_contours:",
read_at__isnull=True,
data__issue__id=str(obj.id),
).exists()
def get_mirrored_activity(self, obj):
if not self.context.get("include_mirror_data") or not obj.issue_id:
return []
@@ -21,7 +21,7 @@ from plane.api.serializers import (
from plane.api.serializers.issue import IssueSerializer as IssueCreateSerializer
from plane.app.permissions import ProjectLitePermission
from plane.app.views.base import BaseAPIView
from plane.db.models import FileAsset, Intake, IntakeIssue, Label, Project, ProjectMember, State, StateGroup
from plane.db.models import FileAsset, Intake, IntakeIssue, Label, Notification, 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
@@ -61,7 +61,11 @@ class ExternalContourListCreateEndpoint(BaseAPIView):
)
def get(self, request, slug, project_id):
serializer = ExternalContourRequestSerializer(self.get_queryset(), many=True)
serializer = ExternalContourRequestSerializer(
self.get_queryset(),
many=True,
context={"request": request},
)
return Response(
{
"results": serializer.data,
@@ -174,7 +178,8 @@ class ExternalContourListCreateEndpoint(BaseAPIView):
"issue__created_by",
)
.prefetch_related("issue__issue_assignee__assignee", "issue__label_issue__label")
.get(pk=intake_issue.id)
.get(pk=intake_issue.id),
context={"request": request},
)
return Response(response_serializer.data, status=status.HTTP_201_CREATED)
@@ -273,14 +278,28 @@ class ExternalContourDetailEndpoint(BaseAPIView):
.prefetch_related("issue__issue_assignee__assignee", "issue__label_issue__label")
)
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())
def get(self, request, slug, project_id, request_id):
contour_request = get_object_or_404(self.get_queryset())
self.mark_request_notifications_read(request.user, contour_request)
serializer = ExternalContourRequestSerializer(
contour_request,
context={
"include_mirror_data": True,
"workspace_slug": slug,
"source_project_id": str(project_id),
"request": request,
},
)
return Response(serializer.data, status=status.HTTP_200_OK)
@@ -366,6 +385,12 @@ class ExternalContourDecisionEndpoint(BaseAPIView):
contour_request.save(update_fields=["extra", "updated_at"])
contour_request.refresh_from_db()
Notification.objects.filter(
receiver_id=request.user.id,
sender__startswith="in_app:external_contours:",
read_at__isnull=True,
data__issue__id=str(contour_request.id),
).update(read_at=timezone.now())
serializer = ExternalContourRequestSerializer(
IntakeIssue.objects.select_related(
"issue",
@@ -379,6 +404,7 @@ class ExternalContourDecisionEndpoint(BaseAPIView):
"include_mirror_data": True,
"workspace_slug": slug,
"source_project_id": str(project_id),
"request": request,
},
)
return Response(serializer.data, status=status.HTTP_200_OK)
@@ -425,12 +451,19 @@ class ExternalContourReplyEndpoint(BaseAPIView):
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()
Notification.objects.filter(
receiver_id=request.user.id,
sender__startswith="in_app:external_contours:",
read_at__isnull=True,
data__issue__id=str(contour_request.id),
).update(read_at=timezone.now())
response_serializer = ExternalContourRequestSerializer(
contour_request,
context={
"include_mirror_data": True,
"workspace_slug": slug,
"source_project_id": str(project_id),
"request": request,
},
)
return Response(response_serializer.data, status=status.HTTP_200_OK)
@@ -21,7 +21,7 @@ from plane.api.serializers import (
from plane.api.serializers.issue import IssueSerializer as IssueCreateSerializer
from plane.app.permissions import ProjectLitePermission
from plane.app.views.base import BaseAPIView
from plane.db.models import FileAsset, Intake, IntakeIssue, Label, Project, ProjectMember, State, StateGroup
from plane.db.models import FileAsset, Intake, IntakeIssue, Label, Notification, 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
@@ -61,7 +61,11 @@ class ExternalContourListCreateEndpoint(BaseAPIView):
)
def get(self, request, slug, project_id):
serializer = ExternalContourRequestSerializer(self.get_queryset(), many=True)
serializer = ExternalContourRequestSerializer(
self.get_queryset(),
many=True,
context={"request": request},
)
return Response(
{
"results": serializer.data,
@@ -174,7 +178,8 @@ class ExternalContourListCreateEndpoint(BaseAPIView):
"issue__created_by",
)
.prefetch_related("issue__issue_assignee__assignee", "issue__label_issue__label")
.get(pk=intake_issue.id)
.get(pk=intake_issue.id),
context={"request": request},
)
return Response(response_serializer.data, status=status.HTTP_201_CREATED)
@@ -273,14 +278,28 @@ class ExternalContourDetailEndpoint(BaseAPIView):
.prefetch_related("issue__issue_assignee__assignee", "issue__label_issue__label")
)
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())
def get(self, request, slug, project_id, request_id):
contour_request = get_object_or_404(self.get_queryset())
self.mark_request_notifications_read(request.user, contour_request)
serializer = ExternalContourRequestSerializer(
contour_request,
context={
"include_mirror_data": True,
"workspace_slug": slug,
"source_project_id": str(project_id),
"request": request,
},
)
return Response(serializer.data, status=status.HTTP_200_OK)
@@ -366,6 +385,12 @@ class ExternalContourDecisionEndpoint(BaseAPIView):
contour_request.save(update_fields=["extra", "updated_at"])
contour_request.refresh_from_db()
Notification.objects.filter(
receiver_id=request.user.id,
sender__startswith="in_app:external_contours:",
read_at__isnull=True,
data__issue__id=str(contour_request.id),
).update(read_at=timezone.now())
serializer = ExternalContourRequestSerializer(
IntakeIssue.objects.select_related(
"issue",
@@ -379,6 +404,7 @@ class ExternalContourDecisionEndpoint(BaseAPIView):
"include_mirror_data": True,
"workspace_slug": slug,
"source_project_id": str(project_id),
"request": request,
},
)
return Response(serializer.data, status=status.HTTP_200_OK)
@@ -425,12 +451,19 @@ class ExternalContourReplyEndpoint(BaseAPIView):
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()
Notification.objects.filter(
receiver_id=request.user.id,
sender__startswith="in_app:external_contours:",
read_at__isnull=True,
data__issue__id=str(contour_request.id),
).update(read_at=timezone.now())
response_serializer = ExternalContourRequestSerializer(
contour_request,
context={
"include_mirror_data": True,
"workspace_slug": slug,
"source_project_id": str(project_id),
"request": request,
},
)
return Response(response_serializer.data, status=status.HTTP_200_OK)