ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: причина отклонения, reply и in-app уведомления
This commit is contained in:
@@ -56,6 +56,7 @@ from .intake import (
|
||||
from .external_contours import (
|
||||
ExternalContourRequestCreateSerializer,
|
||||
ExternalContourRequestDecisionSerializer,
|
||||
ExternalContourRequestReplySerializer,
|
||||
ExternalContourRequestSerializer,
|
||||
ExternalContourTargetOptionsSerializer,
|
||||
ExternalContourTargetProjectSerializer,
|
||||
|
||||
@@ -29,6 +29,16 @@ class ExternalContourRequestCreateSerializer(serializers.Serializer):
|
||||
|
||||
class ExternalContourRequestDecisionSerializer(serializers.Serializer):
|
||||
action = serializers.ChoiceField(choices=["accept", "decline"])
|
||||
comment = serializers.CharField(required=False, allow_blank=True)
|
||||
|
||||
def validate(self, data):
|
||||
if data.get("action") == "decline" and not (data.get("comment") or "").strip():
|
||||
raise serializers.ValidationError({"comment": "Decline reason is required"})
|
||||
return data
|
||||
|
||||
|
||||
class ExternalContourRequestReplySerializer(serializers.Serializer):
|
||||
comment = serializers.CharField()
|
||||
|
||||
|
||||
class ExternalContourTargetProjectSerializer(BaseSerializer):
|
||||
|
||||
@@ -4,44 +4,51 @@
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from plane.api.views import (
|
||||
ExternalContourAttachmentDownloadAPIEndpoint,
|
||||
ExternalContourDetailAPIEndpoint,
|
||||
ExternalContourDecisionAPIEndpoint,
|
||||
ExternalContourListCreateAPIEndpoint,
|
||||
ExternalContourTargetOptionsAPIEndpoint,
|
||||
ExternalContourTargetProjectListAPIEndpoint,
|
||||
from plane.app.views import (
|
||||
ExternalContourAttachmentDownloadEndpoint,
|
||||
ExternalContourDetailEndpoint,
|
||||
ExternalContourDecisionEndpoint,
|
||||
ExternalContourListCreateEndpoint,
|
||||
ExternalContourReplyEndpoint,
|
||||
ExternalContourTargetOptionsEndpoint,
|
||||
ExternalContourTargetProjectListEndpoint,
|
||||
)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/external-contours/",
|
||||
ExternalContourListCreateAPIEndpoint.as_view(http_method_names=["get", "post"]),
|
||||
ExternalContourListCreateEndpoint.as_view(http_method_names=["get", "post"]),
|
||||
name="external-contours",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/external-contours/targets/",
|
||||
ExternalContourTargetProjectListAPIEndpoint.as_view(http_method_names=["get"]),
|
||||
ExternalContourTargetProjectListEndpoint.as_view(http_method_names=["get"]),
|
||||
name="external-contour-targets",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/external-contours/targets/<uuid:target_project_id>/options/",
|
||||
ExternalContourTargetOptionsAPIEndpoint.as_view(http_method_names=["get"]),
|
||||
ExternalContourTargetOptionsEndpoint.as_view(http_method_names=["get"]),
|
||||
name="external-contour-target-options",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/external-contours/<uuid:request_id>/",
|
||||
ExternalContourDetailAPIEndpoint.as_view(http_method_names=["get"]),
|
||||
ExternalContourDetailEndpoint.as_view(http_method_names=["get"]),
|
||||
name="external-contour-detail",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/external-contours/<uuid:request_id>/decision/",
|
||||
ExternalContourDecisionAPIEndpoint.as_view(http_method_names=["post"]),
|
||||
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>/",
|
||||
ExternalContourAttachmentDownloadAPIEndpoint.as_view(http_method_names=["get"]),
|
||||
ExternalContourAttachmentDownloadEndpoint.as_view(http_method_names=["get"]),
|
||||
name="external-contour-attachment-download",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -60,6 +60,7 @@ from .external_contours import (
|
||||
ExternalContourListCreateEndpoint as ExternalContourListCreateAPIEndpoint,
|
||||
ExternalContourDetailEndpoint as ExternalContourDetailAPIEndpoint,
|
||||
ExternalContourDecisionEndpoint as ExternalContourDecisionAPIEndpoint,
|
||||
ExternalContourReplyEndpoint as ExternalContourReplyAPIEndpoint,
|
||||
ExternalContourTargetProjectListEndpoint as ExternalContourTargetProjectListAPIEndpoint,
|
||||
ExternalContourTargetOptionsEndpoint as ExternalContourTargetOptionsAPIEndpoint,
|
||||
)
|
||||
|
||||
@@ -5,22 +5,26 @@
|
||||
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,
|
||||
)
|
||||
from plane.api.serializers.issue import IssueSerializer as IssueCreateSerializer
|
||||
from plane.app.permissions import ProjectLitePermission
|
||||
from .base import BaseAPIView
|
||||
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]
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -23,8 +23,10 @@ from plane.db.models import (
|
||||
IssueActivity,
|
||||
UserNotificationPreference,
|
||||
ProjectMember,
|
||||
IntakeIssue,
|
||||
)
|
||||
from django.db.models import Subquery
|
||||
from plane.utils.external_contours import create_external_contour_notification
|
||||
|
||||
# Third Party imports
|
||||
from celery import shared_task
|
||||
@@ -99,6 +101,10 @@ def extract_mentions_as_subscribers(project_id, issue_id, mentions):
|
||||
and ProjectMember.objects.filter(project_id=project_id, member_id=mention_id, is_active=True).exists()
|
||||
):
|
||||
project = Project.objects.get(pk=project_id)
|
||||
external_contour_request = IntakeIssue.objects.filter(
|
||||
issue_id=issue_id,
|
||||
extra__bridge="external-contours",
|
||||
).first()
|
||||
|
||||
bulk_mention_subscribers.append(
|
||||
IssueSubscriber(
|
||||
@@ -288,6 +294,10 @@ def notifications(
|
||||
)
|
||||
|
||||
issue = Issue.objects.filter(pk=issue_id).first()
|
||||
external_contour_request = IntakeIssue.objects.filter(
|
||||
issue_id=issue_id,
|
||||
extra__bridge="external-contours",
|
||||
).first()
|
||||
|
||||
if subscriber:
|
||||
# add the user to issue subscriber
|
||||
@@ -517,7 +527,19 @@ def notifications(
|
||||
},
|
||||
)
|
||||
)
|
||||
bulk_notifications.append(notification)
|
||||
|
||||
if external_contour_request is not None:
|
||||
for issue_activity in issue_activities_created:
|
||||
if issue_activity.get("issue_detail").get("id") != issue_id:
|
||||
continue
|
||||
|
||||
external_contour_notification = create_external_contour_notification(
|
||||
contour_request=external_contour_request,
|
||||
issue=issue,
|
||||
issue_activity_data=issue_activity,
|
||||
)
|
||||
if external_contour_notification is not None:
|
||||
bulk_notifications.append(external_contour_notification)
|
||||
|
||||
for mention_id in new_mentions:
|
||||
if mention_id != actor_id:
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
# Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# See the LICENSE file for details.
|
||||
|
||||
import json
|
||||
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.utils import timezone
|
||||
from django.utils.html import escape
|
||||
|
||||
from plane.app.serializers import IssueCommentSerializer
|
||||
from plane.db.models import IssueComment, Notification, Project
|
||||
|
||||
|
||||
EXTERNAL_CONTOUR_NOTIFICATION_FIELDS = {"state", "comment", "attachment", "description"}
|
||||
|
||||
|
||||
def build_external_contour_comment_html(comment: str) -> str:
|
||||
escaped_comment = escape((comment or "").strip())
|
||||
escaped_comment = escaped_comment.replace("\n", "<br />")
|
||||
return f"<p>{escaped_comment}</p>"
|
||||
|
||||
|
||||
def create_external_contour_issue_comment(*, issue, actor, comment: str, origin: str):
|
||||
from plane.bgtasks.issue_activities_task import issue_activity
|
||||
|
||||
comment_html = build_external_contour_comment_html(comment)
|
||||
serializer = IssueCommentSerializer(data={"comment_html": comment_html})
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save(project_id=issue.project_id, issue_id=issue.id, actor=actor)
|
||||
issue_activity.delay(
|
||||
type="comment.activity.created",
|
||||
requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder),
|
||||
actor_id=str(actor.id),
|
||||
issue_id=str(issue.id),
|
||||
project_id=str(issue.project_id),
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=origin,
|
||||
)
|
||||
return serializer.data
|
||||
|
||||
|
||||
def create_external_contour_notification(*, contour_request, issue, issue_activity_data):
|
||||
extra = contour_request.extra or {}
|
||||
receiver_id = extra.get("requested_by_id")
|
||||
source_project_id = extra.get("source_project_id")
|
||||
field = issue_activity_data.get("field")
|
||||
actor_id = issue_activity_data.get("actor_id") or (issue_activity_data.get("actor_detail") or {}).get("id")
|
||||
|
||||
if not receiver_id or not source_project_id or not field:
|
||||
return None
|
||||
|
||||
if field not in EXTERNAL_CONTOUR_NOTIFICATION_FIELDS:
|
||||
return None
|
||||
|
||||
if str(receiver_id) == str(actor_id):
|
||||
return None
|
||||
|
||||
source_project = Project.objects.filter(
|
||||
pk=source_project_id,
|
||||
workspace_id=issue.workspace_id,
|
||||
archived_at__isnull=True,
|
||||
).first()
|
||||
|
||||
if not source_project:
|
||||
return None
|
||||
|
||||
issue_comment = (
|
||||
IssueComment.objects.filter(
|
||||
id=issue_activity_data.get("issue_comment"),
|
||||
issue_id=issue.id,
|
||||
project_id=issue.project_id,
|
||||
workspace_id=issue.workspace_id,
|
||||
).first()
|
||||
if issue_activity_data.get("issue_comment")
|
||||
else None
|
||||
)
|
||||
|
||||
return Notification(
|
||||
workspace=source_project.workspace,
|
||||
project=source_project,
|
||||
sender=f"in_app:external_contours:{field}",
|
||||
triggered_by_id=actor_id,
|
||||
receiver_id=receiver_id,
|
||||
entity_identifier=issue.id,
|
||||
entity_name="issue",
|
||||
title=issue_activity_data.get("comment") or "External contour updated",
|
||||
data={
|
||||
"issue": {
|
||||
"id": str(contour_request.id),
|
||||
"name": str(issue.name),
|
||||
"identifier": str(issue.project.identifier),
|
||||
"sequence_id": issue.sequence_id,
|
||||
"state_name": issue.state.name if issue.state else None,
|
||||
"state_group": issue.state.group if issue.state else None,
|
||||
"project_id": str(source_project.id),
|
||||
"workspace_slug": str(source_project.workspace.slug),
|
||||
"external_contour_request_id": str(contour_request.id),
|
||||
"target_issue_id": str(issue.id),
|
||||
"target_project_id": str(issue.project_id),
|
||||
"target_project_name": str(issue.project.name),
|
||||
"source_project_id": str(source_project.id),
|
||||
"source_project_name": str(source_project.name),
|
||||
},
|
||||
"issue_activity": {
|
||||
"id": str(issue_activity_data.get("id")),
|
||||
"verb": str(issue_activity_data.get("verb")),
|
||||
"field": str(field),
|
||||
"actor": str(actor_id),
|
||||
"new_value": str(issue_activity_data.get("new_value")),
|
||||
"old_value": str(issue_activity_data.get("old_value")),
|
||||
"issue_comment": str(issue_comment.comment_stripped if issue_comment is not None else ""),
|
||||
"old_identifier": (
|
||||
str(issue_activity_data.get("old_identifier")) if issue_activity_data.get("old_identifier") else None
|
||||
),
|
||||
"new_identifier": (
|
||||
str(issue_activity_data.get("new_identifier")) if issue_activity_data.get("new_identifier") else None
|
||||
),
|
||||
},
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user