Skip to content

Commit 7cef741

Browse files
feat(api): add lite list endpoints for projects, members, cycles, and modules (#9410)
* feat: add lite list endpoints for projects, members, cycles, and modules * refactor: enhance order_by sanitization for cycle and module endpoints, update error handling for non-existent projects and workspaces
1 parent af1be50 commit 7cef741

18 files changed

Lines changed: 763 additions & 7 deletions

apps/api/plane/api/serializers/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,5 +62,9 @@
6262
FileAssetSerializer,
6363
)
6464
from .invite import WorkspaceInviteSerializer
65-
from .member import ProjectMemberSerializer
65+
from .member import (
66+
ProjectMemberSerializer,
67+
WorkspaceMemberLiteAPISerializer,
68+
ProjectMemberLiteAPISerializer,
69+
)
6670
from .sticky import StickySerializer

apps/api/plane/api/serializers/member.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,45 @@ class Meta:
4141
model = ProjectMember
4242
fields = ["id", "member", "role"]
4343
read_only_fields = ["id"]
44+
45+
46+
class BaseMemberLiteAPISerializer(BaseSerializer):
47+
"""Common flattened member representation for paginated member pickers/directories."""
48+
49+
id = serializers.UUIDField(source="member.id", read_only=True)
50+
first_name = serializers.CharField(source="member.first_name", read_only=True)
51+
last_name = serializers.CharField(source="member.last_name", read_only=True)
52+
email = serializers.EmailField(source="member.email", read_only=True)
53+
avatar = serializers.CharField(source="member.avatar", read_only=True, allow_null=True)
54+
avatar_url = serializers.CharField(source="member.avatar_url", read_only=True, allow_null=True)
55+
display_name = serializers.CharField(source="member.display_name", read_only=True)
56+
is_bot = serializers.BooleanField(source="member.is_bot", read_only=True)
57+
58+
class Meta:
59+
fields = [
60+
"id",
61+
"first_name",
62+
"last_name",
63+
"email",
64+
"avatar",
65+
"avatar_url",
66+
"display_name",
67+
"role",
68+
"is_active",
69+
"is_bot",
70+
]
71+
read_only_fields = fields
72+
73+
74+
class WorkspaceMemberLiteAPISerializer(BaseMemberLiteAPISerializer):
75+
"""Minimal WorkspaceMember representation for paginated member pickers/directories."""
76+
77+
class Meta(BaseMemberLiteAPISerializer.Meta):
78+
model = WorkspaceMember
79+
80+
81+
class ProjectMemberLiteAPISerializer(BaseMemberLiteAPISerializer):
82+
"""Minimal ProjectMember representation for paginated member pickers/directories."""
83+
84+
class Meta(BaseMemberLiteAPISerializer.Meta):
85+
model = ProjectMember

apps/api/plane/api/serializers/project.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,5 +318,6 @@ class Meta:
318318
"emoji",
319319
"description",
320320
"cover_image_url",
321+
"archived_at",
321322
]
322323
read_only_fields = fields

apps/api/plane/api/urls/cycle.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from plane.api.views.cycle import (
88
CycleListCreateAPIEndpoint,
9+
CycleListLiteAPIEndpoint,
910
CycleDetailAPIEndpoint,
1011
CycleIssueListCreateAPIEndpoint,
1112
CycleIssueDetailAPIEndpoint,
@@ -19,6 +20,11 @@
1920
CycleListCreateAPIEndpoint.as_view(http_method_names=["get", "post"]),
2021
name="cycles",
2122
),
23+
path(
24+
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles-lite/",
25+
CycleListLiteAPIEndpoint.as_view(http_method_names=["get"]),
26+
name="cycles-lite",
27+
),
2228
path(
2329
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:pk>/",
2430
CycleDetailAPIEndpoint.as_view(http_method_names=["get", "patch", "delete"]),

apps/api/plane/api/urls/member.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
from plane.api.views import (
88
ProjectMemberListCreateAPIEndpoint,
99
ProjectMemberDetailAPIEndpoint,
10+
ProjectMemberLiteAPIEndpoint,
1011
WorkspaceMemberAPIEndpoint,
12+
WorkspaceMemberLiteAPIEndpoint,
1113
)
1214

1315
urlpatterns = [
@@ -27,6 +29,11 @@
2729
ProjectMemberListCreateAPIEndpoint.as_view(http_method_names=["get", "post"]),
2830
name="project-members",
2931
),
32+
path(
33+
"workspaces/<str:slug>/projects/<uuid:project_id>/project-members-lite/",
34+
ProjectMemberLiteAPIEndpoint.as_view(http_method_names=["get"]),
35+
name="project-members-lite",
36+
),
3037
path(
3138
"workspaces/<str:slug>/projects/<uuid:project_id>/project-members/<uuid:pk>/",
3239
ProjectMemberDetailAPIEndpoint.as_view(http_method_names=["patch", "delete", "get"]),
@@ -37,4 +44,9 @@
3744
WorkspaceMemberAPIEndpoint.as_view(http_method_names=["get"]),
3845
name="workspace-members",
3946
),
47+
path(
48+
"workspaces/<str:slug>/members-lite/",
49+
WorkspaceMemberLiteAPIEndpoint.as_view(http_method_names=["get"]),
50+
name="workspace-members-lite",
51+
),
4052
]

apps/api/plane/api/urls/module.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from plane.api.views import (
88
ModuleListCreateAPIEndpoint,
9+
ModuleListLiteAPIEndpoint,
910
ModuleDetailAPIEndpoint,
1011
ModuleIssueListCreateAPIEndpoint,
1112
ModuleIssueDetailAPIEndpoint,
@@ -18,6 +19,11 @@
1819
ModuleListCreateAPIEndpoint.as_view(http_method_names=["get", "post"]),
1920
name="modules",
2021
),
22+
path(
23+
"workspaces/<str:slug>/projects/<uuid:project_id>/modules-lite/",
24+
ModuleListLiteAPIEndpoint.as_view(http_method_names=["get"]),
25+
name="modules-lite",
26+
),
2127
path(
2228
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:pk>/",
2329
ModuleDetailAPIEndpoint.as_view(http_method_names=["get", "patch", "delete"]),

apps/api/plane/api/urls/project.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from plane.api.views import (
88
ProjectListCreateAPIEndpoint,
9+
ProjectListLiteAPIEndpoint,
910
ProjectDetailAPIEndpoint,
1011
ProjectArchiveUnarchiveAPIEndpoint,
1112
ProjectSummaryAPIEndpoint,
@@ -17,6 +18,11 @@
1718
ProjectListCreateAPIEndpoint.as_view(http_method_names=["get", "post"]),
1819
name="project",
1920
),
21+
path(
22+
"workspaces/<str:slug>/projects-lite/",
23+
ProjectListLiteAPIEndpoint.as_view(http_method_names=["get"]),
24+
name="project-lite",
25+
),
2026
path(
2127
"workspaces/<str:slug>/projects/<uuid:pk>/",
2228
ProjectDetailAPIEndpoint.as_view(http_method_names=["get", "patch", "delete"]),

apps/api/plane/api/views/__init__.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from .project import (
66
ProjectListCreateAPIEndpoint,
7+
ProjectListLiteAPIEndpoint,
78
ProjectDetailAPIEndpoint,
89
ProjectArchiveUnarchiveAPIEndpoint,
910
ProjectSummaryAPIEndpoint,
@@ -34,6 +35,7 @@
3435

3536
from .cycle import (
3637
CycleListCreateAPIEndpoint,
38+
CycleListLiteAPIEndpoint,
3739
CycleDetailAPIEndpoint,
3840
CycleIssueListCreateAPIEndpoint,
3941
CycleIssueDetailAPIEndpoint,
@@ -43,13 +45,20 @@
4345

4446
from .module import (
4547
ModuleListCreateAPIEndpoint,
48+
ModuleListLiteAPIEndpoint,
4649
ModuleDetailAPIEndpoint,
4750
ModuleIssueListCreateAPIEndpoint,
4851
ModuleIssueDetailAPIEndpoint,
4952
ModuleArchiveUnarchiveAPIEndpoint,
5053
)
5154

52-
from .member import ProjectMemberListCreateAPIEndpoint, ProjectMemberDetailAPIEndpoint, WorkspaceMemberAPIEndpoint
55+
from .member import (
56+
ProjectMemberListCreateAPIEndpoint,
57+
ProjectMemberDetailAPIEndpoint,
58+
ProjectMemberLiteAPIEndpoint,
59+
WorkspaceMemberAPIEndpoint,
60+
WorkspaceMemberLiteAPIEndpoint,
61+
)
5362

5463
from .intake import (
5564
IntakeIssueListCreateAPIEndpoint,

apps/api/plane/api/views/cycle.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from plane.api.serializers import (
2828
CycleIssueSerializer,
2929
CycleSerializer,
30+
CycleLiteSerializer,
3031
CycleIssueRequestSerializer,
3132
TransferCycleIssueRequestSerializer,
3233
CycleCreateSerializer,
@@ -46,7 +47,7 @@
4647
UserFavorite,
4748
)
4849
from plane.utils.cycle_transfer_issues import transfer_cycle_issues
49-
from plane.utils.order_queryset import ISSUE_ORDER_BY_ALLOWLIST, sanitize_order_by
50+
from plane.utils.order_queryset import CYCLE_ORDER_BY_ALLOWLIST, ISSUE_ORDER_BY_ALLOWLIST, sanitize_order_by
5051
from plane.utils.host import base_host
5152
from .base import BaseAPIView
5253
from plane.bgtasks.webhook_task import model_activity
@@ -356,6 +357,54 @@ def post(self, request, slug, project_id):
356357
)
357358

358359

360+
class CycleListLiteAPIEndpoint(BaseAPIView):
361+
"""Cycle Lite List Endpoint"""
362+
363+
serializer_class = CycleLiteSerializer
364+
model = Cycle
365+
permission_classes = [ProjectEntityPermission]
366+
use_read_replica = True
367+
368+
@cycle_docs(
369+
operation_id="list_cycles_lite",
370+
summary="List cycles (lite)",
371+
description="Retrieve a paginated, lightweight list of cycles in a project for pickers and references.",
372+
parameters=[CURSOR_PARAMETER, PER_PAGE_PARAMETER, ORDER_BY_PARAMETER],
373+
responses={
374+
200: create_paginated_response(
375+
CycleLiteSerializer,
376+
"PaginatedCycleLiteResponse",
377+
"Paginated list of cycles with minimal fields",
378+
"Paginated Cycles (Lite)",
379+
),
380+
},
381+
)
382+
def get(self, request, slug, project_id):
383+
"""List cycles (lite)
384+
385+
Retrieve a paginated, lightweight list of non-archived cycles in a project,
386+
optimized for pickers and references.
387+
"""
388+
cycles = (
389+
Cycle.objects.filter(workspace__slug=slug, project_id=project_id)
390+
.filter(archived_at__isnull=True)
391+
.select_related("project", "workspace", "owned_by")
392+
.order_by(
393+
sanitize_order_by(
394+
request.GET.get("order_by", "-created_at"),
395+
CYCLE_ORDER_BY_ALLOWLIST,
396+
default="-created_at",
397+
)
398+
)
399+
.distinct()
400+
)
401+
return self.paginate(
402+
request=request,
403+
queryset=cycles,
404+
on_results=lambda cycles: CycleLiteSerializer(cycles, many=True).data,
405+
)
406+
407+
359408
class CycleDetailAPIEndpoint(BaseAPIView):
360409
"""
361410
This viewset automatically provides `retrieve`, `update` and `destroy` actions related to cycle.

apps/api/plane/api/views/issue.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,20 @@ def get(self, request, slug, project_id):
314314
Supports filtering, ordering, and field selection through query parameters.
315315
"""
316316

317+
unsupported_filters = [param for param in ("pql", "filters") if request.GET.get(param)]
318+
if unsupported_filters:
319+
return Response(
320+
{
321+
"pql": (
322+
"PQL and structured filters are not supported on this Plane edition. "
323+
"Remove the pql/filters parameter and filter results client-side, or use "
324+
"a Plane edition that supports work item query filtering."
325+
),
326+
"unsupported_parameters": unsupported_filters,
327+
},
328+
status=status.HTTP_400_BAD_REQUEST,
329+
)
330+
317331
external_id = request.GET.get("external_id")
318332
external_source = request.GET.get("external_source")
319333

0 commit comments

Comments
 (0)