[SILO-1087] feat: add IssueRelations external API (#8763)
* add IssueRelations external API * update serializer methods and filter by slug
This commit is contained in:
parent
9851fe0b8f
commit
d7c80885fd
7 changed files with 451 additions and 12 deletions
|
|
@ -25,6 +25,10 @@ from .issue import (
|
|||
IssueCommentCreateSerializer,
|
||||
IssueLinkCreateSerializer,
|
||||
IssueLinkUpdateSerializer,
|
||||
IssueRelationCreateSerializer,
|
||||
IssueRelationResponseSerializer,
|
||||
IssueRelationSerializer,
|
||||
RelatedIssueSerializer,
|
||||
)
|
||||
from .state import StateLiteSerializer, StateSerializer
|
||||
from .cycle import (
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ from plane.db.models import (
|
|||
IssueComment,
|
||||
IssueLabel,
|
||||
IssueLink,
|
||||
IssueRelation,
|
||||
Label,
|
||||
ProjectMember,
|
||||
State,
|
||||
|
|
@ -479,6 +480,184 @@ class IssueLinkSerializer(BaseSerializer):
|
|||
]
|
||||
|
||||
|
||||
class IssueRelationResponseSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for issue relations response showing grouped relation types.
|
||||
|
||||
Returns issue IDs organized by relation type for efficient client-side processing.
|
||||
"""
|
||||
|
||||
blocking = serializers.ListField(
|
||||
child=serializers.UUIDField(),
|
||||
help_text="List of issue IDs that are blocking this issue",
|
||||
)
|
||||
blocked_by = serializers.ListField(
|
||||
child=serializers.UUIDField(),
|
||||
help_text="List of issue IDs that this issue is blocked by",
|
||||
)
|
||||
duplicate = serializers.ListField(
|
||||
child=serializers.UUIDField(),
|
||||
help_text="List of issue IDs that are duplicates of this issue",
|
||||
)
|
||||
relates_to = serializers.ListField(
|
||||
child=serializers.UUIDField(),
|
||||
help_text="List of issue IDs that relate to this issue",
|
||||
)
|
||||
start_after = serializers.ListField(
|
||||
child=serializers.UUIDField(),
|
||||
help_text="List of issue IDs that start after this issue",
|
||||
)
|
||||
start_before = serializers.ListField(
|
||||
child=serializers.UUIDField(),
|
||||
help_text="List of issue IDs that start before this issue",
|
||||
)
|
||||
finish_after = serializers.ListField(
|
||||
child=serializers.UUIDField(),
|
||||
help_text="List of issue IDs that finish after this issue",
|
||||
)
|
||||
finish_before = serializers.ListField(
|
||||
child=serializers.UUIDField(),
|
||||
help_text="List of issue IDs that finish before this issue",
|
||||
)
|
||||
|
||||
|
||||
class IssueRelationCreateSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for creating issue relations.
|
||||
|
||||
Creates issue relations with the specified relation type and issues.
|
||||
Validates relation types and ensures proper issue ID format.
|
||||
"""
|
||||
|
||||
RELATION_TYPE_CHOICES = [
|
||||
("blocking", "Blocking"),
|
||||
("blocked_by", "Blocked By"),
|
||||
("duplicate", "Duplicate"),
|
||||
("relates_to", "Relates To"),
|
||||
("start_before", "Start Before"),
|
||||
("start_after", "Start After"),
|
||||
("finish_before", "Finish Before"),
|
||||
("finish_after", "Finish After"),
|
||||
]
|
||||
|
||||
relation_type = serializers.ChoiceField(
|
||||
choices=RELATION_TYPE_CHOICES,
|
||||
required=True,
|
||||
help_text="Type of relationship between work items",
|
||||
)
|
||||
issues = serializers.ListField(
|
||||
child=serializers.UUIDField(),
|
||||
required=True,
|
||||
min_length=1,
|
||||
help_text="Array of work item IDs to create relations with",
|
||||
)
|
||||
|
||||
def validate_issues(self, value):
|
||||
"""Validate that issues list is not empty and contains valid UUIDs."""
|
||||
if not value:
|
||||
raise serializers.ValidationError("At least one issue ID is required.")
|
||||
return value
|
||||
|
||||
|
||||
class IssueRelationRemoveSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer for removing issue relations.
|
||||
|
||||
Removes existing relationships between work items by specifying
|
||||
the related issue ID.
|
||||
"""
|
||||
|
||||
related_issue = serializers.UUIDField(
|
||||
required=True, help_text="ID of the related work item to remove relation with"
|
||||
)
|
||||
|
||||
|
||||
class IssueRelationSerializer(BaseSerializer):
|
||||
"""
|
||||
Serializer for issue relationships showing related issue details.
|
||||
|
||||
Provides comprehensive information about related issues including
|
||||
project context, sequence ID, and relationship type.
|
||||
"""
|
||||
|
||||
id = serializers.UUIDField(source="related_issue.id", read_only=True)
|
||||
project_id = serializers.UUIDField(source="related_issue.project_id", read_only=True)
|
||||
sequence_id = serializers.IntegerField(source="related_issue.sequence_id", read_only=True)
|
||||
name = serializers.CharField(source="related_issue.name", read_only=True)
|
||||
relation_type = serializers.CharField(read_only=True)
|
||||
state_id = serializers.UUIDField(source="related_issue.state.id", read_only=True)
|
||||
priority = serializers.CharField(source="related_issue.priority", read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = IssueRelation
|
||||
fields = [
|
||||
"id",
|
||||
"project_id",
|
||||
"sequence_id",
|
||||
"relation_type",
|
||||
"name",
|
||||
"state_id",
|
||||
"priority",
|
||||
"created_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"updated_by",
|
||||
]
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
"created_by",
|
||||
"created_at",
|
||||
"updated_by",
|
||||
"updated_at",
|
||||
]
|
||||
|
||||
|
||||
class RelatedIssueSerializer(BaseSerializer):
|
||||
"""
|
||||
Serializer for reverse issue relationships showing issue details.
|
||||
|
||||
Provides comprehensive information about the source issue in a relationship
|
||||
including project context, sequence ID, and relationship type.
|
||||
"""
|
||||
|
||||
id = serializers.UUIDField(source="issue.id", read_only=True)
|
||||
project_id = serializers.PrimaryKeyRelatedField(source="issue.project_id", read_only=True)
|
||||
sequence_id = serializers.IntegerField(source="issue.sequence_id", read_only=True)
|
||||
name = serializers.CharField(source="issue.name", read_only=True)
|
||||
type_id = serializers.UUIDField(source="issue.type.id", read_only=True)
|
||||
relation_type = serializers.CharField(read_only=True)
|
||||
is_epic = serializers.BooleanField(source="issue.type.is_epic", read_only=True)
|
||||
state_id = serializers.UUIDField(source="issue.state.id", read_only=True)
|
||||
priority = serializers.CharField(source="issue.priority", read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = IssueRelation
|
||||
fields = [
|
||||
"id",
|
||||
"project_id",
|
||||
"sequence_id",
|
||||
"relation_type",
|
||||
"name",
|
||||
"type_id",
|
||||
"is_epic",
|
||||
"state_id",
|
||||
"priority",
|
||||
"created_by",
|
||||
"created_at",
|
||||
"updated_by",
|
||||
"updated_at",
|
||||
]
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
"created_by",
|
||||
"created_at",
|
||||
"updated_by",
|
||||
"updated_at",
|
||||
]
|
||||
|
||||
|
||||
class IssueAttachmentSerializer(BaseSerializer):
|
||||
"""
|
||||
Serializer for work item file attachments.
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ from plane.api.views import (
|
|||
IssueAttachmentDetailAPIEndpoint,
|
||||
WorkspaceIssueAPIEndpoint,
|
||||
IssueSearchEndpoint,
|
||||
IssueRelationListCreateAPIEndpoint,
|
||||
)
|
||||
|
||||
# Deprecated url patterns
|
||||
|
|
@ -145,6 +146,11 @@ new_url_patterns = [
|
|||
IssueAttachmentDetailAPIEndpoint.as_view(http_method_names=["get", "patch", "delete"]),
|
||||
name="work-item-attachment-detail",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/work-items/<uuid:issue_id>/relations/",
|
||||
IssueRelationListCreateAPIEndpoint.as_view(http_method_names=["get", "post"]),
|
||||
name="work-item-relation-list",
|
||||
),
|
||||
]
|
||||
|
||||
urlpatterns = old_url_patterns + new_url_patterns
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ from .issue import (
|
|||
IssueAttachmentListCreateAPIEndpoint,
|
||||
IssueAttachmentDetailAPIEndpoint,
|
||||
IssueSearchEndpoint,
|
||||
IssueRelationListCreateAPIEndpoint,
|
||||
)
|
||||
|
||||
from .cycle import (
|
||||
|
|
|
|||
|
|
@ -23,7 +23,11 @@ from django.db.models import (
|
|||
Value,
|
||||
When,
|
||||
Subquery,
|
||||
UUIDField,
|
||||
)
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.contrib.postgres.aggregates import ArrayAgg
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
|
||||
|
|
@ -45,6 +49,9 @@ from plane.api.serializers import (
|
|||
IssueActivitySerializer,
|
||||
IssueCommentSerializer,
|
||||
IssueLinkSerializer,
|
||||
IssueRelationCreateSerializer,
|
||||
IssueRelationResponseSerializer,
|
||||
IssueRelationSerializer,
|
||||
IssueSerializer,
|
||||
LabelSerializer,
|
||||
IssueAttachmentUploadSerializer,
|
||||
|
|
@ -53,6 +60,7 @@ from plane.api.serializers import (
|
|||
IssueLinkCreateSerializer,
|
||||
IssueLinkUpdateSerializer,
|
||||
LabelCreateUpdateSerializer,
|
||||
RelatedIssueSerializer,
|
||||
)
|
||||
from plane.app.permissions import (
|
||||
ProjectEntityPermission,
|
||||
|
|
@ -66,6 +74,7 @@ from plane.db.models import (
|
|||
FileAsset,
|
||||
IssueComment,
|
||||
IssueLink,
|
||||
IssueRelation,
|
||||
Label,
|
||||
Project,
|
||||
ProjectMember,
|
||||
|
|
@ -76,10 +85,12 @@ from plane.settings.storage import S3Storage
|
|||
from plane.bgtasks.storage_metadata_task import get_asset_object_metadata
|
||||
from .base import BaseAPIView
|
||||
from plane.utils.host import base_host
|
||||
from plane.utils.issue_relation_mapper import get_actual_relation
|
||||
from plane.bgtasks.webhook_task import model_activity
|
||||
from plane.app.permissions import ROLE
|
||||
from plane.utils.openapi import (
|
||||
work_item_docs,
|
||||
work_item_relation_docs,
|
||||
label_docs,
|
||||
issue_link_docs,
|
||||
issue_comment_docs,
|
||||
|
|
@ -1119,9 +1130,9 @@ class IssueLinkListCreateAPIEndpoint(BaseAPIView):
|
|||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(self.get_queryset()),
|
||||
on_results=lambda issue_links: IssueLinkSerializer(
|
||||
issue_links, many=True, fields=self.fields, expand=self.expand
|
||||
).data,
|
||||
on_results=lambda issue_links: (
|
||||
IssueLinkSerializer(issue_links, many=True, fields=self.fields, expand=self.expand).data
|
||||
),
|
||||
)
|
||||
|
||||
@issue_link_docs(
|
||||
|
|
@ -1226,9 +1237,9 @@ class IssueLinkDetailAPIEndpoint(BaseAPIView):
|
|||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(self.get_queryset()),
|
||||
on_results=lambda issue_links: IssueLinkSerializer(
|
||||
issue_links, many=True, fields=self.fields, expand=self.expand
|
||||
).data,
|
||||
on_results=lambda issue_links: (
|
||||
IssueLinkSerializer(issue_links, many=True, fields=self.fields, expand=self.expand).data
|
||||
),
|
||||
)
|
||||
issue_link = self.get_queryset().get(pk=pk)
|
||||
serializer = IssueLinkSerializer(issue_link, fields=self.fields, expand=self.expand)
|
||||
|
|
@ -1377,9 +1388,9 @@ class IssueCommentListCreateAPIEndpoint(BaseAPIView):
|
|||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(self.get_queryset()),
|
||||
on_results=lambda issue_comments: IssueCommentSerializer(
|
||||
issue_comments, many=True, fields=self.fields, expand=self.expand
|
||||
).data,
|
||||
on_results=lambda issue_comments: (
|
||||
IssueCommentSerializer(issue_comments, many=True, fields=self.fields, expand=self.expand).data
|
||||
),
|
||||
)
|
||||
|
||||
@issue_comment_docs(
|
||||
|
|
@ -1688,9 +1699,9 @@ class IssueActivityListAPIEndpoint(BaseAPIView):
|
|||
return self.paginate(
|
||||
request=request,
|
||||
queryset=(issue_activities),
|
||||
on_results=lambda issue_activity: IssueActivitySerializer(
|
||||
issue_activity, many=True, fields=self.fields, expand=self.expand
|
||||
).data,
|
||||
on_results=lambda issue_activity: (
|
||||
IssueActivitySerializer(issue_activity, many=True, fields=self.fields, expand=self.expand).data
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -2250,3 +2261,224 @@ class IssueSearchEndpoint(BaseAPIView):
|
|||
)[: int(limit)]
|
||||
|
||||
return Response({"issues": issue_results}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class IssueRelationListCreateAPIEndpoint(BaseAPIView):
|
||||
"""Issue Relation List and Create Endpoint"""
|
||||
|
||||
serializer_class = IssueRelationSerializer
|
||||
model = IssueRelation
|
||||
permission_classes = [ProjectEntityPermission]
|
||||
use_read_replica = True
|
||||
|
||||
@work_item_relation_docs(
|
||||
operation_id="list_work_item_relations",
|
||||
summary="List work item relations",
|
||||
description="Retrieve all relationships for a work item including blocking, blocked_by, duplicate, relates_to, start_before, start_after, finish_before, and finish_after relations.", # noqa E501
|
||||
parameters=[
|
||||
ISSUE_ID_PARAMETER,
|
||||
CURSOR_PARAMETER,
|
||||
PER_PAGE_PARAMETER,
|
||||
ORDER_BY_PARAMETER,
|
||||
FIELDS_PARAMETER,
|
||||
EXPAND_PARAMETER,
|
||||
],
|
||||
responses={
|
||||
200: OpenApiResponse(
|
||||
description="Work item relations grouped by relation type",
|
||||
response=IssueRelationResponseSerializer,
|
||||
examples=[
|
||||
OpenApiExample(
|
||||
name="Work Item Relations Response",
|
||||
value={
|
||||
"blocking": [
|
||||
"550e8400-e29b-41d4-a716-446655440000",
|
||||
"550e8400-e29b-41d4-a716-446655440001",
|
||||
],
|
||||
"blocked_by": ["550e8400-e29b-41d4-a716-446655440002"],
|
||||
"duplicate": [],
|
||||
"relates_to": ["550e8400-e29b-41d4-a716-446655440003"],
|
||||
"start_after": [],
|
||||
"start_before": ["550e8400-e29b-41d4-a716-446655440004"],
|
||||
"finish_after": [],
|
||||
"finish_before": [],
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
400: INVALID_REQUEST_RESPONSE,
|
||||
404: ISSUE_NOT_FOUND_RESPONSE,
|
||||
},
|
||||
)
|
||||
def get(self, request, slug, project_id, issue_id):
|
||||
"""List work item relations
|
||||
|
||||
Retrieve all relationships for a work item organized by relation type.
|
||||
Returns a structured response with relations grouped by type.
|
||||
"""
|
||||
empty_uuid_array = Value([], output_field=ArrayField(UUIDField()))
|
||||
|
||||
def _agg_ids(field, **filter_kwargs):
|
||||
return Coalesce(
|
||||
ArrayAgg(field, filter=Q(**filter_kwargs), distinct=True),
|
||||
empty_uuid_array,
|
||||
)
|
||||
|
||||
issue_relation_qs = IssueRelation.objects.filter(
|
||||
Q(issue_id=issue_id) | Q(related_issue_id=issue_id),
|
||||
workspace__slug=slug,
|
||||
)
|
||||
|
||||
relation_ids = issue_relation_qs.aggregate(
|
||||
blocking_ids=_agg_ids("issue_id", relation_type="blocked_by", related_issue_id=issue_id),
|
||||
blocked_by_ids=_agg_ids("related_issue_id", relation_type="blocked_by", issue_id=issue_id),
|
||||
duplicate_ids=_agg_ids("related_issue_id", relation_type="duplicate", issue_id=issue_id),
|
||||
duplicate_ids_related=_agg_ids("issue_id", relation_type="duplicate", related_issue_id=issue_id),
|
||||
relates_to_ids=_agg_ids("related_issue_id", relation_type="relates_to", issue_id=issue_id),
|
||||
relates_to_ids_related=_agg_ids("issue_id", relation_type="relates_to", related_issue_id=issue_id),
|
||||
start_after_ids=_agg_ids("issue_id", relation_type="start_before", related_issue_id=issue_id),
|
||||
start_before_ids=_agg_ids("related_issue_id", relation_type="start_before", issue_id=issue_id),
|
||||
finish_after_ids=_agg_ids("issue_id", relation_type="finish_before", related_issue_id=issue_id),
|
||||
finish_before_ids=_agg_ids("related_issue_id", relation_type="finish_before", issue_id=issue_id),
|
||||
)
|
||||
|
||||
response_data = {
|
||||
"blocking": relation_ids["blocking_ids"],
|
||||
"blocked_by": relation_ids["blocked_by_ids"],
|
||||
"duplicate": list(set(relation_ids["duplicate_ids"] + relation_ids["duplicate_ids_related"])),
|
||||
"relates_to": list(set(relation_ids["relates_to_ids"] + relation_ids["relates_to_ids_related"])),
|
||||
"start_after": relation_ids["start_after_ids"],
|
||||
"start_before": relation_ids["start_before_ids"],
|
||||
"finish_after": relation_ids["finish_after_ids"],
|
||||
"finish_before": relation_ids["finish_before_ids"],
|
||||
}
|
||||
|
||||
return Response(response_data, status=status.HTTP_200_OK)
|
||||
|
||||
@work_item_relation_docs(
|
||||
operation_id="create_work_item_relation",
|
||||
summary="Create work item relation",
|
||||
description="Create relationships between work items. Supports various relation types including blocking, blocked_by, duplicate, relates_to, start_before, start_after, finish_before, and finish_after.", # noqa E501
|
||||
parameters=[
|
||||
ISSUE_ID_PARAMETER,
|
||||
],
|
||||
request=OpenApiRequest(
|
||||
request=IssueRelationCreateSerializer,
|
||||
examples=[
|
||||
OpenApiExample(
|
||||
name="Create blocking relation",
|
||||
value={
|
||||
"relation_type": "blocking",
|
||||
"issues": [
|
||||
"550e8400-e29b-41d4-a716-446655440000",
|
||||
"550e8400-e29b-41d4-a716-446655440001",
|
||||
],
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
responses={
|
||||
201: OpenApiResponse(
|
||||
description="Work item relations created successfully",
|
||||
response=IssueRelationSerializer(many=True),
|
||||
examples=[
|
||||
OpenApiExample(
|
||||
name="Relations created",
|
||||
value=[
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "Fix authentication bug",
|
||||
"sequence_id": 42,
|
||||
"project_id": "550e8400-e29b-41d4-a716-446655440001",
|
||||
"relation_type": "blocked_by",
|
||||
"state_id": "550e8400-e29b-41d4-a716-446655440002",
|
||||
"priority": "high",
|
||||
"created_at": "2024-01-15T10:00:00Z",
|
||||
"updated_at": "2024-01-15T10:00:00Z",
|
||||
"created_by": "550e8400-e29b-41d4-a716-446655440004",
|
||||
"updated_by": "550e8400-e29b-41d4-a716-446655440004",
|
||||
}
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
400: INVALID_REQUEST_RESPONSE,
|
||||
404: ISSUE_NOT_FOUND_RESPONSE,
|
||||
},
|
||||
)
|
||||
def post(self, request, slug, project_id, issue_id):
|
||||
"""Create work item relation
|
||||
|
||||
Create relationships between work items with specified relation type.
|
||||
Automatically tracks relation creation activity.
|
||||
"""
|
||||
# Validate request data using serializer
|
||||
serializer = IssueRelationCreateSerializer(data=request.data)
|
||||
if not serializer.is_valid():
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
relation_type = serializer.validated_data["relation_type"]
|
||||
issues = serializer.validated_data["issues"]
|
||||
project = Project.objects.get(pk=project_id, workspace__slug=slug)
|
||||
|
||||
actual_relation = get_actual_relation(relation_type)
|
||||
is_reverse = relation_type in ["blocking", "start_after", "finish_after"]
|
||||
|
||||
IssueRelation.objects.bulk_create(
|
||||
[
|
||||
IssueRelation(
|
||||
issue_id=(issue if is_reverse else issue_id),
|
||||
related_issue_id=(issue_id if is_reverse else issue),
|
||||
relation_type=actual_relation,
|
||||
project_id=project_id,
|
||||
workspace_id=project.workspace_id,
|
||||
created_by=request.user,
|
||||
updated_by=request.user,
|
||||
)
|
||||
for issue in issues
|
||||
],
|
||||
batch_size=10,
|
||||
ignore_conflicts=True,
|
||||
)
|
||||
|
||||
issue_activity.delay(
|
||||
type="issue_relation.activity.created",
|
||||
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(issue_id),
|
||||
project_id=str(project_id),
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
)
|
||||
|
||||
# Re-fetch with select_related to avoid N+1 queries in serializers.
|
||||
# bulk_create with ignore_conflicts=True may not return PKs,
|
||||
# so query by the issue/related_issue pairs and relation type.
|
||||
if is_reverse:
|
||||
refetch_filter = Q(
|
||||
issue_id__in=issues,
|
||||
related_issue_id=issue_id,
|
||||
relation_type=actual_relation,
|
||||
)
|
||||
else:
|
||||
refetch_filter = Q(
|
||||
issue_id=issue_id,
|
||||
related_issue_id__in=issues,
|
||||
relation_type=actual_relation,
|
||||
)
|
||||
|
||||
refetched_relations = IssueRelation.objects.filter(
|
||||
refetch_filter,
|
||||
workspace__slug=slug,
|
||||
).select_related(
|
||||
"issue__state",
|
||||
"related_issue__state",
|
||||
)
|
||||
|
||||
serializer_class = RelatedIssueSerializer if is_reverse else IssueRelationSerializer
|
||||
return Response(
|
||||
serializer_class(refetched_relations, many=True).data,
|
||||
status=status.HTTP_201_CREATED,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -157,6 +157,7 @@ from .decorators import (
|
|||
user_docs,
|
||||
cycle_docs,
|
||||
work_item_docs,
|
||||
work_item_relation_docs,
|
||||
label_docs,
|
||||
issue_link_docs,
|
||||
issue_comment_docs,
|
||||
|
|
@ -307,6 +308,7 @@ __all__ = [
|
|||
"user_docs",
|
||||
"cycle_docs",
|
||||
"work_item_docs",
|
||||
"work_item_relation_docs",
|
||||
"label_docs",
|
||||
"issue_link_docs",
|
||||
"issue_comment_docs",
|
||||
|
|
|
|||
|
|
@ -223,6 +223,21 @@ def issue_attachment_docs(**kwargs):
|
|||
return extend_schema(**_merge_schema_options(defaults, kwargs))
|
||||
|
||||
|
||||
def work_item_relation_docs(**kwargs):
|
||||
"""Decorator for work item relation endpoints"""
|
||||
defaults = {
|
||||
"tags": ["Work Item Relations"],
|
||||
"parameters": [WORKSPACE_SLUG_PARAMETER, PROJECT_ID_PARAMETER],
|
||||
"responses": {
|
||||
401: UNAUTHORIZED_RESPONSE,
|
||||
403: FORBIDDEN_RESPONSE,
|
||||
404: NOT_FOUND_RESPONSE,
|
||||
},
|
||||
}
|
||||
|
||||
return extend_schema(**_merge_schema_options(defaults, kwargs))
|
||||
|
||||
|
||||
def module_docs(**kwargs):
|
||||
"""Decorator for module management endpoints"""
|
||||
defaults = {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue