ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: realtime внешних контуров и действия исходящих карточек
This commit is contained in:
@@ -45,7 +45,7 @@ urlpatterns = [
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/external-contours/<uuid:request_id>/",
|
||||
ExternalContourDetailEndpoint.as_view(http_method_names=["get", "patch"]),
|
||||
ExternalContourDetailEndpoint.as_view(http_method_names=["get", "patch", "delete"]),
|
||||
name="external-contour-detail",
|
||||
),
|
||||
path(
|
||||
|
||||
@@ -21,6 +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.realtime.issue_events import publish_external_contour_event_on_commit, publish_issue_event_on_commit
|
||||
from plane.app.views.base import BaseAPIView
|
||||
from plane.db.models import FileAsset, Intake, IntakeIssue, Label, Notification, Project, ProjectMember, State, StateGroup
|
||||
from plane.db.models.intake import IntakeIssueStatus, SourceType
|
||||
@@ -171,6 +172,29 @@ class ExternalContourListCreateEndpoint(BaseAPIView):
|
||||
issue.state = target_default_state
|
||||
issue.save()
|
||||
|
||||
publish_issue_event_on_commit(
|
||||
"issue.created",
|
||||
issue,
|
||||
actor_id=request.user.id,
|
||||
changed_fields=[
|
||||
"name",
|
||||
"description_html",
|
||||
"priority",
|
||||
"assignees",
|
||||
"labels",
|
||||
"target_date",
|
||||
"state",
|
||||
"external_contour",
|
||||
],
|
||||
publish_external_bridge=False,
|
||||
)
|
||||
publish_external_contour_event_on_commit(
|
||||
"external_contour.created",
|
||||
intake_issue,
|
||||
actor_id=request.user.id,
|
||||
changed_fields=["issue", "state", "external_contour"],
|
||||
)
|
||||
|
||||
response_serializer = ExternalContourRequestSerializer(
|
||||
IntakeIssue.objects.select_related(
|
||||
"issue",
|
||||
@@ -331,6 +355,7 @@ class ExternalContourDetailEndpoint(BaseAPIView):
|
||||
serializer = ExternalContourRequestUpdateSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
issue_update_data = serializer.validated_data.copy()
|
||||
changed_fields = list(issue_update_data.keys())
|
||||
assignee_ids = issue_update_data.pop("assignee_ids", None)
|
||||
label_ids = issue_update_data.pop("label_ids", None)
|
||||
if assignee_ids is not None:
|
||||
@@ -348,11 +373,24 @@ class ExternalContourDetailEndpoint(BaseAPIView):
|
||||
},
|
||||
)
|
||||
issue_serializer.is_valid(raise_exception=True)
|
||||
issue_serializer.save()
|
||||
updated_issue = issue_serializer.save()
|
||||
publish_issue_event_on_commit(
|
||||
"issue.updated",
|
||||
updated_issue,
|
||||
actor_id=request.user.id,
|
||||
changed_fields=changed_fields,
|
||||
publish_external_bridge=False,
|
||||
)
|
||||
|
||||
contour_request.updated_at = timezone.now()
|
||||
contour_request.save(update_fields=["updated_at"])
|
||||
contour_request.refresh_from_db()
|
||||
publish_external_contour_event_on_commit(
|
||||
"external_contour.updated",
|
||||
contour_request,
|
||||
actor_id=request.user.id,
|
||||
changed_fields=changed_fields,
|
||||
)
|
||||
|
||||
response_serializer = ExternalContourRequestSerializer(
|
||||
contour_request,
|
||||
@@ -365,6 +403,37 @@ class ExternalContourDetailEndpoint(BaseAPIView):
|
||||
)
|
||||
return Response(response_serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
def delete(self, request, slug, project_id, request_id):
|
||||
contour_request = get_object_or_404(self.get_queryset())
|
||||
issue = contour_request.issue
|
||||
|
||||
if not issue:
|
||||
contour_request.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
requested_by_id = contour_request.extra.get("requested_by_id") or (
|
||||
str(issue.created_by_id) if issue.created_by_id else None
|
||||
)
|
||||
if str(request.user.id) != str(requested_by_id):
|
||||
return Response({"error": "Only the sender can delete this request"}, status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
publish_external_contour_event_on_commit(
|
||||
"external_contour.deleted",
|
||||
contour_request,
|
||||
actor_id=request.user.id,
|
||||
changed_fields=["deleted_at"],
|
||||
)
|
||||
publish_issue_event_on_commit(
|
||||
"issue.deleted",
|
||||
issue,
|
||||
actor_id=request.user.id,
|
||||
changed_fields=["deleted_at", "external_contour"],
|
||||
publish_external_bridge=False,
|
||||
)
|
||||
contour_request.delete()
|
||||
issue.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class ExternalContourDecisionEndpoint(BaseAPIView):
|
||||
permission_classes = [ProjectLitePermission]
|
||||
@@ -434,6 +503,13 @@ class ExternalContourDecisionEndpoint(BaseAPIView):
|
||||
|
||||
issue.state = target_default_state
|
||||
issue.save(update_fields=["state", "updated_at"])
|
||||
publish_issue_event_on_commit(
|
||||
"issue.updated",
|
||||
issue,
|
||||
actor_id=request.user.id,
|
||||
changed_fields=["state", "external_contour_reopen"],
|
||||
publish_external_bridge=False,
|
||||
)
|
||||
|
||||
extra = dict(contour_request.extra or {})
|
||||
extra.pop("source_decision", None)
|
||||
@@ -445,6 +521,13 @@ class ExternalContourDecisionEndpoint(BaseAPIView):
|
||||
contour_request.extra = extra
|
||||
contour_request.save(update_fields=["extra", "updated_at"])
|
||||
|
||||
publish_external_contour_event_on_commit(
|
||||
"external_contour.updated",
|
||||
contour_request,
|
||||
actor_id=request.user.id,
|
||||
changed_fields=["source_decision", "state"],
|
||||
)
|
||||
|
||||
contour_request.refresh_from_db()
|
||||
Notification.objects.filter(
|
||||
receiver_id=request.user.id,
|
||||
|
||||
@@ -21,7 +21,98 @@ def issue_event_channel(project_id):
|
||||
return f"{ISSUE_EVENT_CHANNEL_PREFIX}:{project_id}"
|
||||
|
||||
|
||||
def publish_issue_event_on_commit(event_type, issue, actor_id=None, changed_fields=None):
|
||||
def _publish_payload(project_id, payload):
|
||||
next_payload = {
|
||||
**payload,
|
||||
"project_id": str(project_id),
|
||||
}
|
||||
redis_instance().publish(
|
||||
issue_event_channel(project_id),
|
||||
json.dumps(next_payload, cls=DjangoJSONEncoder),
|
||||
)
|
||||
|
||||
|
||||
def _external_contour_project_ids(contour_request):
|
||||
extra = contour_request.extra or {}
|
||||
project_ids = {
|
||||
str(project_id)
|
||||
for project_id in [
|
||||
extra.get("source_project_id"),
|
||||
extra.get("target_project_id"),
|
||||
contour_request.project_id,
|
||||
getattr(contour_request.issue, "project_id", None),
|
||||
]
|
||||
if project_id
|
||||
}
|
||||
|
||||
return sorted(project_ids)
|
||||
|
||||
|
||||
def publish_external_contour_event_on_commit(event_type, contour_request, actor_id=None, changed_fields=None):
|
||||
issue = contour_request.issue
|
||||
extra = contour_request.extra or {}
|
||||
payload = {
|
||||
"event_id": str(uuid4()),
|
||||
"type": event_type,
|
||||
"workspace_id": str(contour_request.workspace_id),
|
||||
"workspace_slug": contour_request.workspace.slug if getattr(contour_request, "workspace", None) else None,
|
||||
"request_id": str(contour_request.id),
|
||||
"issue_id": str(issue.id) if issue else None,
|
||||
"sequence_id": issue.sequence_id if issue else None,
|
||||
"source_project_id": str(extra.get("source_project_id")) if extra.get("source_project_id") else None,
|
||||
"target_project_id": str(extra.get("target_project_id") or contour_request.project_id),
|
||||
"updated_at": contour_request.updated_at or timezone.now(),
|
||||
"actor_id": str(actor_id) if actor_id else None,
|
||||
"changed_fields": sorted(set(changed_fields or [])),
|
||||
}
|
||||
|
||||
def _publish():
|
||||
try:
|
||||
for project_id in _external_contour_project_ids(contour_request):
|
||||
_publish_payload(project_id, payload)
|
||||
except Exception:
|
||||
logger.exception("Failed to publish external contour realtime event")
|
||||
|
||||
transaction.on_commit(_publish)
|
||||
|
||||
|
||||
def publish_external_contour_issue_event_on_commit(event_type, issue, actor_id=None, changed_fields=None):
|
||||
def _publish():
|
||||
try:
|
||||
from plane.db.models import IntakeIssue
|
||||
|
||||
contour_requests = (
|
||||
IntakeIssue.objects.filter(issue_id=issue.id, extra__bridge="external-contours")
|
||||
.select_related("issue", "workspace")
|
||||
.only("id", "workspace_id", "workspace__slug", "project_id", "issue_id", "extra", "updated_at")
|
||||
)
|
||||
for contour_request in contour_requests:
|
||||
event_name = "external_contour.deleted" if event_type == "issue.deleted" else "external_contour.updated"
|
||||
payload = {
|
||||
"event_id": str(uuid4()),
|
||||
"type": event_name,
|
||||
"workspace_id": str(contour_request.workspace_id),
|
||||
"workspace_slug": contour_request.workspace.slug if getattr(contour_request, "workspace", None) else None,
|
||||
"request_id": str(contour_request.id),
|
||||
"issue_id": str(issue.id),
|
||||
"sequence_id": issue.sequence_id,
|
||||
"source_project_id": str(contour_request.extra.get("source_project_id"))
|
||||
if contour_request.extra.get("source_project_id")
|
||||
else None,
|
||||
"target_project_id": str(contour_request.extra.get("target_project_id") or contour_request.project_id),
|
||||
"updated_at": issue.updated_at or timezone.now(),
|
||||
"actor_id": str(actor_id) if actor_id else None,
|
||||
"changed_fields": sorted(set(changed_fields or [])),
|
||||
}
|
||||
for project_id in _external_contour_project_ids(contour_request):
|
||||
_publish_payload(project_id, payload)
|
||||
except Exception:
|
||||
logger.exception("Failed to publish external contour bridge event")
|
||||
|
||||
transaction.on_commit(_publish)
|
||||
|
||||
|
||||
def publish_issue_event_on_commit(event_type, issue, actor_id=None, changed_fields=None, publish_external_bridge=True):
|
||||
payload = {
|
||||
"event_id": str(uuid4()),
|
||||
"type": event_type,
|
||||
@@ -37,11 +128,10 @@ def publish_issue_event_on_commit(event_type, issue, actor_id=None, changed_fiel
|
||||
|
||||
def _publish():
|
||||
try:
|
||||
redis_instance().publish(
|
||||
issue_event_channel(issue.project_id),
|
||||
json.dumps(payload, cls=DjangoJSONEncoder),
|
||||
)
|
||||
_publish_payload(issue.project_id, payload)
|
||||
except Exception:
|
||||
logger.exception("Failed to publish issue realtime event")
|
||||
|
||||
transaction.on_commit(_publish)
|
||||
if publish_external_bridge:
|
||||
publish_external_contour_issue_event_on_commit(event_type, issue, actor_id=actor_id, changed_fields=changed_fields)
|
||||
|
||||
@@ -45,7 +45,7 @@ urlpatterns = [
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/external-contours/<uuid:request_id>/",
|
||||
ExternalContourDetailEndpoint.as_view(http_method_names=["get", "patch"]),
|
||||
ExternalContourDetailEndpoint.as_view(http_method_names=["get", "patch", "delete"]),
|
||||
name="external-contour-detail",
|
||||
),
|
||||
path(
|
||||
|
||||
@@ -23,6 +23,7 @@ from plane.api.serializers import (
|
||||
)
|
||||
from plane.api.serializers.issue import IssueSerializer as IssueCreateSerializer
|
||||
from plane.app.permissions import ProjectLitePermission
|
||||
from plane.app.realtime.issue_events import publish_external_contour_event_on_commit, publish_issue_event_on_commit
|
||||
from plane.app.views.base import BaseAPIView
|
||||
from plane.db.models import FileAsset, Intake, IntakeIssue, Label, Notification, Project, ProjectMember, State, StateGroup
|
||||
from plane.db.models.intake import IntakeIssueStatus, SourceType
|
||||
@@ -173,6 +174,29 @@ class ExternalContourListCreateEndpoint(BaseAPIView):
|
||||
issue.state = target_default_state
|
||||
issue.save()
|
||||
|
||||
publish_issue_event_on_commit(
|
||||
"issue.created",
|
||||
issue,
|
||||
actor_id=request.user.id,
|
||||
changed_fields=[
|
||||
"name",
|
||||
"description_html",
|
||||
"priority",
|
||||
"assignees",
|
||||
"labels",
|
||||
"target_date",
|
||||
"state",
|
||||
"external_contour",
|
||||
],
|
||||
publish_external_bridge=False,
|
||||
)
|
||||
publish_external_contour_event_on_commit(
|
||||
"external_contour.created",
|
||||
intake_issue,
|
||||
actor_id=request.user.id,
|
||||
changed_fields=["issue", "state", "external_contour"],
|
||||
)
|
||||
|
||||
response_serializer = ExternalContourRequestSerializer(
|
||||
IntakeIssue.objects.select_related(
|
||||
"issue",
|
||||
@@ -650,6 +674,7 @@ class ExternalContourDetailEndpoint(BaseAPIView):
|
||||
serializer = ExternalContourRequestUpdateSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
issue_update_data = serializer.validated_data.copy()
|
||||
changed_fields = list(issue_update_data.keys())
|
||||
assignee_ids = issue_update_data.pop("assignee_ids", None)
|
||||
label_ids = issue_update_data.pop("label_ids", None)
|
||||
if assignee_ids is not None:
|
||||
@@ -667,11 +692,24 @@ class ExternalContourDetailEndpoint(BaseAPIView):
|
||||
},
|
||||
)
|
||||
issue_serializer.is_valid(raise_exception=True)
|
||||
issue_serializer.save()
|
||||
updated_issue = issue_serializer.save()
|
||||
|
||||
contour_request.updated_at = timezone.now()
|
||||
contour_request.save(update_fields=["updated_at"])
|
||||
contour_request.refresh_from_db()
|
||||
publish_issue_event_on_commit(
|
||||
"issue.updated",
|
||||
updated_issue,
|
||||
actor_id=request.user.id,
|
||||
changed_fields=changed_fields,
|
||||
publish_external_bridge=False,
|
||||
)
|
||||
publish_external_contour_event_on_commit(
|
||||
"external_contour.updated",
|
||||
contour_request,
|
||||
actor_id=request.user.id,
|
||||
changed_fields=changed_fields,
|
||||
)
|
||||
|
||||
response_serializer = ExternalContourRequestSerializer(
|
||||
contour_request,
|
||||
@@ -684,6 +722,37 @@ class ExternalContourDetailEndpoint(BaseAPIView):
|
||||
)
|
||||
return Response(response_serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
def delete(self, request, slug, project_id, request_id):
|
||||
contour_request = get_object_or_404(self.get_queryset())
|
||||
issue = contour_request.issue
|
||||
|
||||
if not issue:
|
||||
contour_request.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
requested_by_id = contour_request.extra.get("requested_by_id") or (
|
||||
str(issue.created_by_id) if issue.created_by_id else None
|
||||
)
|
||||
if str(request.user.id) != str(requested_by_id):
|
||||
return Response({"error": "Only the sender can delete this request"}, status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
publish_external_contour_event_on_commit(
|
||||
"external_contour.deleted",
|
||||
contour_request,
|
||||
actor_id=request.user.id,
|
||||
changed_fields=["deleted_at"],
|
||||
)
|
||||
publish_issue_event_on_commit(
|
||||
"issue.deleted",
|
||||
issue,
|
||||
actor_id=request.user.id,
|
||||
changed_fields=["deleted_at", "external_contour"],
|
||||
publish_external_bridge=False,
|
||||
)
|
||||
contour_request.delete()
|
||||
issue.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class ExternalContourDecisionEndpoint(BaseAPIView):
|
||||
permission_classes = [ProjectLitePermission]
|
||||
@@ -753,6 +822,13 @@ class ExternalContourDecisionEndpoint(BaseAPIView):
|
||||
|
||||
issue.state = target_default_state
|
||||
issue.save(update_fields=["state", "updated_at"])
|
||||
publish_issue_event_on_commit(
|
||||
"issue.updated",
|
||||
issue,
|
||||
actor_id=request.user.id,
|
||||
changed_fields=["state", "external_contour_reopen"],
|
||||
publish_external_bridge=False,
|
||||
)
|
||||
|
||||
extra = dict(contour_request.extra or {})
|
||||
extra.pop("source_decision", None)
|
||||
@@ -764,6 +840,13 @@ class ExternalContourDecisionEndpoint(BaseAPIView):
|
||||
contour_request.extra = extra
|
||||
contour_request.save(update_fields=["extra", "updated_at"])
|
||||
|
||||
publish_external_contour_event_on_commit(
|
||||
"external_contour.updated",
|
||||
contour_request,
|
||||
actor_id=request.user.id,
|
||||
changed_fields=["source_decision", "state"],
|
||||
)
|
||||
|
||||
contour_request.refresh_from_db()
|
||||
Notification.objects.filter(
|
||||
receiver_id=request.user.id,
|
||||
@@ -831,6 +914,12 @@ 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()
|
||||
publish_external_contour_event_on_commit(
|
||||
"external_contour.updated",
|
||||
contour_request,
|
||||
actor_id=request.user.id,
|
||||
changed_fields=["comments"],
|
||||
)
|
||||
Notification.objects.filter(
|
||||
receiver_id=request.user.id,
|
||||
sender__startswith="in_app:external_contours:",
|
||||
|
||||
Reference in New Issue
Block a user