ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: зеркалирование комментариев, вложений и активности внешнего контура
This commit is contained in:
@@ -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 IntakeIssue, Issue, Label, Project
|
||||
from plane.db.models import FileAsset, IntakeIssue, Issue, IssueActivity, IssueComment, Label, Project
|
||||
|
||||
|
||||
class ExternalContourIssuePayloadSerializer(serializers.Serializer):
|
||||
@@ -95,8 +95,54 @@ class ExternalContourIssueSerializer(BaseSerializer):
|
||||
]
|
||||
|
||||
|
||||
class ExternalContourMirroredAttachmentSerializer(BaseSerializer):
|
||||
uploaded_by = serializers.SerializerMethodField()
|
||||
download_url = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = FileAsset
|
||||
fields = ["id", "attributes", "asset_url", "download_url", "updated_at", "uploaded_by"]
|
||||
read_only_fields = fields
|
||||
|
||||
def get_uploaded_by(self, obj):
|
||||
user = obj.updated_by or obj.created_by
|
||||
return getattr(user, "display_name", None)
|
||||
|
||||
def get_download_url(self, obj):
|
||||
workspace_slug = self.context.get("workspace_slug")
|
||||
source_project_id = self.context.get("source_project_id")
|
||||
request_id = self.context.get("request_id")
|
||||
if not workspace_slug or not source_project_id or not request_id:
|
||||
return None
|
||||
return (
|
||||
f"/api/workspaces/{workspace_slug}/projects/{source_project_id}/external-contours/"
|
||||
f"{request_id}/attachments/{obj.id}/"
|
||||
)
|
||||
|
||||
|
||||
class ExternalContourMirroredCommentSerializer(BaseSerializer):
|
||||
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
||||
|
||||
class Meta:
|
||||
model = IssueComment
|
||||
fields = ["id", "comment_html", "created_at", "updated_at", "edited_at", "parent_id", "actor_detail"]
|
||||
read_only_fields = fields
|
||||
|
||||
|
||||
class ExternalContourMirroredActivitySerializer(BaseSerializer):
|
||||
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
||||
|
||||
class Meta:
|
||||
model = IssueActivity
|
||||
fields = ["id", "verb", "field", "old_value", "new_value", "comment", "created_at", "actor_detail"]
|
||||
read_only_fields = fields
|
||||
|
||||
|
||||
class ExternalContourRequestSerializer(BaseSerializer):
|
||||
issue = ExternalContourIssueSerializer(read_only=True)
|
||||
mirrored_activity = serializers.SerializerMethodField()
|
||||
mirrored_attachments = serializers.SerializerMethodField()
|
||||
mirrored_comments = serializers.SerializerMethodField()
|
||||
source_project_id = serializers.SerializerMethodField()
|
||||
source_project_name = serializers.SerializerMethodField()
|
||||
source_decision = serializers.SerializerMethodField()
|
||||
@@ -117,6 +163,9 @@ class ExternalContourRequestSerializer(BaseSerializer):
|
||||
"updated_at",
|
||||
"created_by",
|
||||
"issue",
|
||||
"mirrored_activity",
|
||||
"mirrored_attachments",
|
||||
"mirrored_comments",
|
||||
"source_project_id",
|
||||
"source_project_name",
|
||||
"source_decision",
|
||||
@@ -134,6 +183,53 @@ class ExternalContourRequestSerializer(BaseSerializer):
|
||||
def get_source_project_id(self, obj):
|
||||
return obj.extra.get("source_project_id")
|
||||
|
||||
def get_mirrored_activity(self, obj):
|
||||
if not self.context.get("include_mirror_data") or not obj.issue_id:
|
||||
return []
|
||||
|
||||
activity = (
|
||||
IssueActivity.objects.filter(issue_id=obj.issue_id)
|
||||
.exclude(field__in=["comment", "vote", "reaction", "draft"])
|
||||
.select_related("actor")
|
||||
.order_by("-created_at")[:50]
|
||||
)
|
||||
return ExternalContourMirroredActivitySerializer(activity, many=True).data
|
||||
|
||||
def get_mirrored_attachments(self, obj):
|
||||
if not self.context.get("include_mirror_data") or not obj.issue_id:
|
||||
return []
|
||||
|
||||
attachments = (
|
||||
FileAsset.objects.filter(
|
||||
issue_id=obj.issue_id,
|
||||
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
|
||||
is_uploaded=True,
|
||||
is_deleted=False,
|
||||
)
|
||||
.select_related("created_by", "updated_by")
|
||||
.order_by("-updated_at")
|
||||
)
|
||||
return ExternalContourMirroredAttachmentSerializer(
|
||||
attachments,
|
||||
many=True,
|
||||
context={
|
||||
"workspace_slug": self.context.get("workspace_slug"),
|
||||
"source_project_id": self.context.get("source_project_id"),
|
||||
"request_id": str(obj.id),
|
||||
},
|
||||
).data
|
||||
|
||||
def get_mirrored_comments(self, obj):
|
||||
if not self.context.get("include_mirror_data") or not obj.issue_id:
|
||||
return []
|
||||
|
||||
comments = (
|
||||
IssueComment.objects.filter(issue_id=obj.issue_id)
|
||||
.select_related("actor")
|
||||
.order_by("created_at")
|
||||
)
|
||||
return ExternalContourMirroredCommentSerializer(comments, many=True).data
|
||||
|
||||
def get_source_project_name(self, obj):
|
||||
return obj.extra.get("source_project_name")
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
from django.urls import path
|
||||
|
||||
from plane.api.views import (
|
||||
ExternalContourAttachmentDownloadAPIEndpoint,
|
||||
ExternalContourDetailAPIEndpoint,
|
||||
ExternalContourDecisionAPIEndpoint,
|
||||
ExternalContourListCreateAPIEndpoint,
|
||||
@@ -38,4 +39,9 @@ urlpatterns = [
|
||||
ExternalContourDecisionAPIEndpoint.as_view(http_method_names=["post"]),
|
||||
name="external-contour-decision",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/external-contours/<uuid:request_id>/attachments/<uuid:attachment_id>/",
|
||||
ExternalContourAttachmentDownloadAPIEndpoint.as_view(http_method_names=["get"]),
|
||||
name="external-contour-attachment-download",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -56,6 +56,7 @@ from .intake import (
|
||||
IntakeIssueDetailAPIEndpoint,
|
||||
)
|
||||
from .external_contours import (
|
||||
ExternalContourAttachmentDownloadEndpoint as ExternalContourAttachmentDownloadAPIEndpoint,
|
||||
ExternalContourListCreateEndpoint as ExternalContourListCreateAPIEndpoint,
|
||||
ExternalContourDetailEndpoint as ExternalContourDetailAPIEndpoint,
|
||||
ExternalContourDecisionEndpoint as ExternalContourDecisionAPIEndpoint,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# See the LICENSE file for details.
|
||||
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils import timezone
|
||||
from rest_framework import status
|
||||
@@ -17,8 +18,9 @@ from plane.api.serializers import (
|
||||
from plane.api.serializers.issue import IssueSerializer as IssueCreateSerializer
|
||||
from plane.app.permissions import ProjectLitePermission
|
||||
from .base import BaseAPIView
|
||||
from plane.db.models import Intake, IntakeIssue, Label, Project, ProjectMember, State, StateGroup
|
||||
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
|
||||
|
||||
|
||||
class ExternalContourListCreateEndpoint(BaseAPIView):
|
||||
@@ -269,7 +271,14 @@ class ExternalContourDetailEndpoint(BaseAPIView):
|
||||
|
||||
def get(self, request, slug, project_id, request_id):
|
||||
contour_request = get_object_or_404(self.get_queryset())
|
||||
serializer = ExternalContourRequestSerializer(contour_request)
|
||||
serializer = ExternalContourRequestSerializer(
|
||||
contour_request,
|
||||
context={
|
||||
"include_mirror_data": True,
|
||||
"workspace_slug": slug,
|
||||
"source_project_id": str(project_id),
|
||||
},
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
@@ -349,6 +358,42 @@ class ExternalContourDecisionEndpoint(BaseAPIView):
|
||||
"issue__created_by",
|
||||
)
|
||||
.prefetch_related("issue__issue_assignee__assignee", "issue__label_issue__label")
|
||||
.get(pk=contour_request.id)
|
||||
.get(pk=contour_request.id),
|
||||
context={
|
||||
"include_mirror_data": True,
|
||||
"workspace_slug": slug,
|
||||
"source_project_id": str(project_id),
|
||||
},
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class ExternalContourAttachmentDownloadEndpoint(BaseAPIView):
|
||||
permission_classes = [ProjectLitePermission]
|
||||
|
||||
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__project", "workspace")
|
||||
|
||||
def get(self, request, slug, project_id, request_id, attachment_id):
|
||||
contour_request = get_object_or_404(self.get_queryset())
|
||||
attachment = get_object_or_404(
|
||||
FileAsset,
|
||||
pk=attachment_id,
|
||||
issue_id=contour_request.issue_id,
|
||||
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
|
||||
is_uploaded=True,
|
||||
is_deleted=False,
|
||||
)
|
||||
|
||||
storage = S3Storage(request=request)
|
||||
presigned_url = storage.generate_presigned_url(
|
||||
object_name=attachment.asset.name,
|
||||
disposition="attachment",
|
||||
filename=attachment.attributes.get("name"),
|
||||
)
|
||||
return HttpResponseRedirect(presigned_url)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
from django.urls import path
|
||||
|
||||
from plane.app.views import (
|
||||
ExternalContourAttachmentDownloadEndpoint,
|
||||
ExternalContourDetailEndpoint,
|
||||
ExternalContourDecisionEndpoint,
|
||||
ExternalContourListCreateEndpoint,
|
||||
@@ -39,4 +40,9 @@ 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>/attachments/<uuid:attachment_id>/",
|
||||
ExternalContourAttachmentDownloadEndpoint.as_view(http_method_names=["get"]),
|
||||
name="external-contour-attachment-download",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -225,6 +225,7 @@ from .notification.base import (
|
||||
|
||||
from .exporter.base import ExportIssuesEndpoint
|
||||
from .external_contours import (
|
||||
ExternalContourAttachmentDownloadEndpoint,
|
||||
ExternalContourListCreateEndpoint,
|
||||
ExternalContourDetailEndpoint,
|
||||
ExternalContourDecisionEndpoint,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# See the LICENSE file for details.
|
||||
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils import timezone
|
||||
from rest_framework import status
|
||||
@@ -17,8 +18,9 @@ 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 Intake, IntakeIssue, Label, Project, ProjectMember, State, StateGroup
|
||||
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
|
||||
|
||||
|
||||
class ExternalContourListCreateEndpoint(BaseAPIView):
|
||||
@@ -269,7 +271,14 @@ class ExternalContourDetailEndpoint(BaseAPIView):
|
||||
|
||||
def get(self, request, slug, project_id, request_id):
|
||||
contour_request = get_object_or_404(self.get_queryset())
|
||||
serializer = ExternalContourRequestSerializer(contour_request)
|
||||
serializer = ExternalContourRequestSerializer(
|
||||
contour_request,
|
||||
context={
|
||||
"include_mirror_data": True,
|
||||
"workspace_slug": slug,
|
||||
"source_project_id": str(project_id),
|
||||
},
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
@@ -349,6 +358,42 @@ class ExternalContourDecisionEndpoint(BaseAPIView):
|
||||
"issue__created_by",
|
||||
)
|
||||
.prefetch_related("issue__issue_assignee__assignee", "issue__label_issue__label")
|
||||
.get(pk=contour_request.id)
|
||||
.get(pk=contour_request.id),
|
||||
context={
|
||||
"include_mirror_data": True,
|
||||
"workspace_slug": slug,
|
||||
"source_project_id": str(project_id),
|
||||
},
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class ExternalContourAttachmentDownloadEndpoint(BaseAPIView):
|
||||
permission_classes = [ProjectLitePermission]
|
||||
|
||||
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__project", "workspace")
|
||||
|
||||
def get(self, request, slug, project_id, request_id, attachment_id):
|
||||
contour_request = get_object_or_404(self.get_queryset())
|
||||
attachment = get_object_or_404(
|
||||
FileAsset,
|
||||
pk=attachment_id,
|
||||
issue_id=contour_request.issue_id,
|
||||
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
|
||||
is_uploaded=True,
|
||||
is_deleted=False,
|
||||
)
|
||||
|
||||
storage = S3Storage(request=request)
|
||||
presigned_url = storage.generate_presigned_url(
|
||||
object_name=attachment.asset.name,
|
||||
disposition="attachment",
|
||||
filename=attachment.attributes.get("name"),
|
||||
)
|
||||
return HttpResponseRedirect(presigned_url)
|
||||
|
||||
Reference in New Issue
Block a user