ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: source-side принятие и возврат внешнего запроса
This commit is contained in:
@@ -55,6 +55,7 @@ from .intake import (
|
||||
)
|
||||
from .external_contours import (
|
||||
ExternalContourRequestCreateSerializer,
|
||||
ExternalContourRequestDecisionSerializer,
|
||||
ExternalContourRequestSerializer,
|
||||
ExternalContourTargetOptionsSerializer,
|
||||
ExternalContourTargetProjectSerializer,
|
||||
|
||||
@@ -27,6 +27,10 @@ class ExternalContourRequestCreateSerializer(serializers.Serializer):
|
||||
issue = ExternalContourIssuePayloadSerializer()
|
||||
|
||||
|
||||
class ExternalContourRequestDecisionSerializer(serializers.Serializer):
|
||||
action = serializers.ChoiceField(choices=["accept", "decline"])
|
||||
|
||||
|
||||
class ExternalContourTargetProjectSerializer(BaseSerializer):
|
||||
inbox_view = serializers.BooleanField(read_only=True, source="intake_view")
|
||||
|
||||
@@ -95,6 +99,9 @@ class ExternalContourRequestSerializer(BaseSerializer):
|
||||
issue = ExternalContourIssueSerializer(read_only=True)
|
||||
source_project_id = serializers.SerializerMethodField()
|
||||
source_project_name = serializers.SerializerMethodField()
|
||||
source_decision = serializers.SerializerMethodField()
|
||||
source_decision_at = serializers.SerializerMethodField()
|
||||
source_decision_by_name = serializers.SerializerMethodField()
|
||||
target_project_id = serializers.SerializerMethodField()
|
||||
target_project_name = serializers.SerializerMethodField()
|
||||
requested_by_id = serializers.SerializerMethodField()
|
||||
@@ -112,6 +119,9 @@ class ExternalContourRequestSerializer(BaseSerializer):
|
||||
"issue",
|
||||
"source_project_id",
|
||||
"source_project_name",
|
||||
"source_decision",
|
||||
"source_decision_at",
|
||||
"source_decision_by_name",
|
||||
"target_project_id",
|
||||
"target_project_name",
|
||||
"requested_by_id",
|
||||
@@ -127,6 +137,15 @@ class ExternalContourRequestSerializer(BaseSerializer):
|
||||
def get_source_project_name(self, obj):
|
||||
return obj.extra.get("source_project_name")
|
||||
|
||||
def get_source_decision(self, obj):
|
||||
return obj.extra.get("source_decision")
|
||||
|
||||
def get_source_decision_at(self, obj):
|
||||
return obj.extra.get("source_decision_at")
|
||||
|
||||
def get_source_decision_by_name(self, obj):
|
||||
return obj.extra.get("source_decision_by_name")
|
||||
|
||||
def get_target_project_id(self, obj):
|
||||
target_project_id = obj.extra.get("target_project_id")
|
||||
if target_project_id:
|
||||
|
||||
@@ -6,6 +6,7 @@ from django.urls import path
|
||||
|
||||
from plane.api.views import (
|
||||
ExternalContourDetailAPIEndpoint,
|
||||
ExternalContourDecisionAPIEndpoint,
|
||||
ExternalContourListCreateAPIEndpoint,
|
||||
ExternalContourTargetOptionsAPIEndpoint,
|
||||
ExternalContourTargetProjectListAPIEndpoint,
|
||||
@@ -32,4 +33,9 @@ urlpatterns = [
|
||||
ExternalContourDetailAPIEndpoint.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"]),
|
||||
name="external-contour-decision",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -56,10 +56,11 @@ from .intake import (
|
||||
IntakeIssueDetailAPIEndpoint,
|
||||
)
|
||||
from .external_contours import (
|
||||
ExternalContourListCreateAPIEndpoint,
|
||||
ExternalContourDetailAPIEndpoint,
|
||||
ExternalContourTargetProjectListAPIEndpoint,
|
||||
ExternalContourTargetOptionsAPIEndpoint,
|
||||
ExternalContourListCreateEndpoint as ExternalContourListCreateAPIEndpoint,
|
||||
ExternalContourDetailEndpoint as ExternalContourDetailAPIEndpoint,
|
||||
ExternalContourDecisionEndpoint as ExternalContourDecisionAPIEndpoint,
|
||||
ExternalContourTargetProjectListEndpoint as ExternalContourTargetProjectListAPIEndpoint,
|
||||
ExternalContourTargetOptionsEndpoint as ExternalContourTargetOptionsAPIEndpoint,
|
||||
)
|
||||
|
||||
from .asset import UserAssetEndpoint, UserServerAssetEndpoint, GenericAssetEndpoint
|
||||
|
||||
@@ -3,29 +3,40 @@
|
||||
# See the LICENSE file for details.
|
||||
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils import timezone
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
|
||||
from plane.api.serializers import (
|
||||
ExternalContourRequestCreateSerializer,
|
||||
ExternalContourRequestDecisionSerializer,
|
||||
ExternalContourRequestSerializer,
|
||||
ExternalContourTargetOptionsSerializer,
|
||||
ExternalContourTargetProjectSerializer,
|
||||
)
|
||||
from plane.app.permissions import ProjectLitePermission
|
||||
from plane.db.models import Intake, IntakeIssue, Label, Project, ProjectMember, State, StateGroup
|
||||
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.intake import IntakeIssueStatus, SourceType
|
||||
|
||||
|
||||
class ExternalContourListCreateAPIEndpoint(BaseAPIView):
|
||||
class ExternalContourListCreateEndpoint(BaseAPIView):
|
||||
permission_classes = [ProjectLitePermission]
|
||||
serializer_class = ExternalContourRequestSerializer
|
||||
|
||||
def get_source_project(self, slug, project_id):
|
||||
return get_object_or_404(Project, workspace__slug=slug, pk=project_id)
|
||||
|
||||
def get_default_target_state(self, target_project):
|
||||
return (
|
||||
State.objects.filter(project=target_project, default=True)
|
||||
.exclude(group=StateGroup.TRIAGE.value)
|
||||
.first()
|
||||
) or State.objects.filter(project=target_project).exclude(group=StateGroup.TRIAGE.value).order_by(
|
||||
"sequence", "created_at"
|
||||
).first()
|
||||
|
||||
def get_queryset(self):
|
||||
return (
|
||||
IntakeIssue.objects.filter(
|
||||
@@ -93,11 +104,7 @@ class ExternalContourListCreateAPIEndpoint(BaseAPIView):
|
||||
default=False,
|
||||
)
|
||||
|
||||
target_default_state = (
|
||||
State.objects.filter(project=target_project, default=True)
|
||||
.exclude(group=StateGroup.TRIAGE.value)
|
||||
.first()
|
||||
) or State.objects.filter(project=target_project).exclude(group=StateGroup.TRIAGE.value).order_by("sequence", "created_at").first()
|
||||
target_default_state = self.get_default_target_state(target_project)
|
||||
|
||||
if not target_default_state:
|
||||
return Response({"error": "Target project has no available workflow state"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -166,7 +173,7 @@ class ExternalContourListCreateAPIEndpoint(BaseAPIView):
|
||||
return Response(response_serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
class ExternalContourTargetProjectListAPIEndpoint(BaseAPIView):
|
||||
class ExternalContourTargetProjectListEndpoint(BaseAPIView):
|
||||
permission_classes = [ProjectLitePermission]
|
||||
serializer_class = ExternalContourTargetProjectSerializer
|
||||
|
||||
@@ -190,7 +197,7 @@ class ExternalContourTargetProjectListAPIEndpoint(BaseAPIView):
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class ExternalContourTargetOptionsAPIEndpoint(BaseAPIView):
|
||||
class ExternalContourTargetOptionsEndpoint(BaseAPIView):
|
||||
permission_classes = [ProjectLitePermission]
|
||||
serializer_class = ExternalContourTargetOptionsSerializer
|
||||
|
||||
@@ -239,7 +246,7 @@ class ExternalContourTargetOptionsAPIEndpoint(BaseAPIView):
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class ExternalContourDetailAPIEndpoint(BaseAPIView):
|
||||
class ExternalContourDetailEndpoint(BaseAPIView):
|
||||
permission_classes = [ProjectLitePermission]
|
||||
serializer_class = ExternalContourRequestSerializer
|
||||
|
||||
@@ -264,3 +271,84 @@ class ExternalContourDetailAPIEndpoint(BaseAPIView):
|
||||
contour_request = get_object_or_404(self.get_queryset())
|
||||
serializer = ExternalContourRequestSerializer(contour_request)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class ExternalContourDecisionEndpoint(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 = ExternalContourRequestDecisionSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
action = serializer.validated_data["action"]
|
||||
issue = contour_request.issue
|
||||
|
||||
if not issue or not issue.state or issue.state.group not in [StateGroup.COMPLETED.value, StateGroup.CANCELLED.value]:
|
||||
return Response(
|
||||
{"error": "Source decision is available only after the target contour finishes processing"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if action == "accept":
|
||||
contour_request.extra = {
|
||||
**contour_request.extra,
|
||||
"source_decision": "accepted",
|
||||
"source_decision_at": timezone.now().isoformat(),
|
||||
"source_decision_by_name": request.user.display_name,
|
||||
}
|
||||
contour_request.save(update_fields=["extra", "updated_at"])
|
||||
else:
|
||||
target_default_state = (
|
||||
State.objects.filter(project=issue.project, default=True)
|
||||
.exclude(group=StateGroup.TRIAGE.value)
|
||||
.first()
|
||||
) or State.objects.filter(project=issue.project).exclude(group=StateGroup.TRIAGE.value).order_by(
|
||||
"sequence", "created_at"
|
||||
).first()
|
||||
|
||||
if not target_default_state:
|
||||
return Response({"error": "Target project has no available workflow state"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
issue.state = target_default_state
|
||||
issue.save(update_fields=["state", "updated_at"])
|
||||
|
||||
extra = dict(contour_request.extra or {})
|
||||
extra.pop("source_decision", None)
|
||||
extra.pop("source_decision_at", None)
|
||||
extra.pop("source_decision_by_name", None)
|
||||
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
|
||||
contour_request.save(update_fields=["extra", "updated_at"])
|
||||
|
||||
contour_request.refresh_from_db()
|
||||
serializer = ExternalContourRequestSerializer(
|
||||
IntakeIssue.objects.select_related(
|
||||
"issue",
|
||||
"issue__state",
|
||||
"issue__project",
|
||||
"issue__created_by",
|
||||
)
|
||||
.prefetch_related("issue__issue_assignee__assignee", "issue__label_issue__label")
|
||||
.get(pk=contour_request.id)
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -6,6 +6,7 @@ from django.urls import path
|
||||
|
||||
from plane.app.views import (
|
||||
ExternalContourDetailEndpoint,
|
||||
ExternalContourDecisionEndpoint,
|
||||
ExternalContourListCreateEndpoint,
|
||||
ExternalContourTargetOptionsEndpoint,
|
||||
ExternalContourTargetProjectListEndpoint,
|
||||
@@ -33,4 +34,9 @@ urlpatterns = [
|
||||
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/",
|
||||
ExternalContourDecisionEndpoint.as_view(http_method_names=["post"]),
|
||||
name="external-contour-decision",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -227,6 +227,7 @@ from .exporter.base import ExportIssuesEndpoint
|
||||
from .external_contours import (
|
||||
ExternalContourListCreateEndpoint,
|
||||
ExternalContourDetailEndpoint,
|
||||
ExternalContourDecisionEndpoint,
|
||||
ExternalContourTargetProjectListEndpoint,
|
||||
ExternalContourTargetOptionsEndpoint,
|
||||
)
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
# See the LICENSE file for details.
|
||||
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils import timezone
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
|
||||
from plane.api.serializers import (
|
||||
ExternalContourRequestCreateSerializer,
|
||||
ExternalContourRequestDecisionSerializer,
|
||||
ExternalContourRequestSerializer,
|
||||
ExternalContourTargetOptionsSerializer,
|
||||
ExternalContourTargetProjectSerializer,
|
||||
@@ -26,6 +28,15 @@ class ExternalContourListCreateEndpoint(BaseAPIView):
|
||||
def get_source_project(self, slug, project_id):
|
||||
return get_object_or_404(Project, workspace__slug=slug, pk=project_id)
|
||||
|
||||
def get_default_target_state(self, target_project):
|
||||
return (
|
||||
State.objects.filter(project=target_project, default=True)
|
||||
.exclude(group=StateGroup.TRIAGE.value)
|
||||
.first()
|
||||
) or State.objects.filter(project=target_project).exclude(group=StateGroup.TRIAGE.value).order_by(
|
||||
"sequence", "created_at"
|
||||
).first()
|
||||
|
||||
def get_queryset(self):
|
||||
return (
|
||||
IntakeIssue.objects.filter(
|
||||
@@ -93,11 +104,7 @@ class ExternalContourListCreateEndpoint(BaseAPIView):
|
||||
default=False,
|
||||
)
|
||||
|
||||
target_default_state = (
|
||||
State.objects.filter(project=target_project, default=True)
|
||||
.exclude(group=StateGroup.TRIAGE.value)
|
||||
.first()
|
||||
) or State.objects.filter(project=target_project).exclude(group=StateGroup.TRIAGE.value).order_by("sequence", "created_at").first()
|
||||
target_default_state = self.get_default_target_state(target_project)
|
||||
|
||||
if not target_default_state:
|
||||
return Response({"error": "Target project has no available workflow state"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -264,3 +271,84 @@ class ExternalContourDetailEndpoint(BaseAPIView):
|
||||
contour_request = get_object_or_404(self.get_queryset())
|
||||
serializer = ExternalContourRequestSerializer(contour_request)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class ExternalContourDecisionEndpoint(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 = ExternalContourRequestDecisionSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
action = serializer.validated_data["action"]
|
||||
issue = contour_request.issue
|
||||
|
||||
if not issue or not issue.state or issue.state.group not in [StateGroup.COMPLETED.value, StateGroup.CANCELLED.value]:
|
||||
return Response(
|
||||
{"error": "Source decision is available only after the target contour finishes processing"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if action == "accept":
|
||||
contour_request.extra = {
|
||||
**contour_request.extra,
|
||||
"source_decision": "accepted",
|
||||
"source_decision_at": timezone.now().isoformat(),
|
||||
"source_decision_by_name": request.user.display_name,
|
||||
}
|
||||
contour_request.save(update_fields=["extra", "updated_at"])
|
||||
else:
|
||||
target_default_state = (
|
||||
State.objects.filter(project=issue.project, default=True)
|
||||
.exclude(group=StateGroup.TRIAGE.value)
|
||||
.first()
|
||||
) or State.objects.filter(project=issue.project).exclude(group=StateGroup.TRIAGE.value).order_by(
|
||||
"sequence", "created_at"
|
||||
).first()
|
||||
|
||||
if not target_default_state:
|
||||
return Response({"error": "Target project has no available workflow state"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
issue.state = target_default_state
|
||||
issue.save(update_fields=["state", "updated_at"])
|
||||
|
||||
extra = dict(contour_request.extra or {})
|
||||
extra.pop("source_decision", None)
|
||||
extra.pop("source_decision_at", None)
|
||||
extra.pop("source_decision_by_name", None)
|
||||
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
|
||||
contour_request.save(update_fields=["extra", "updated_at"])
|
||||
|
||||
contour_request.refresh_from_db()
|
||||
serializer = ExternalContourRequestSerializer(
|
||||
IntakeIssue.objects.select_related(
|
||||
"issue",
|
||||
"issue__state",
|
||||
"issue__project",
|
||||
"issue__created_by",
|
||||
)
|
||||
.prefetch_related("issue__issue_assignee__assignee", "issue__label_issue__label")
|
||||
.get(pk=contour_request.id)
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
Reference in New Issue
Block a user