# 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", "
") return f"

{escaped_comment}

" 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 ), }, }, )