-
Notifications
You must be signed in to change notification settings - Fork 5k
feat(api): add lite list endpoints for projects, members, cycles, and modules #9410
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: preview
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -318,5 +318,6 @@ class Meta: | |
| "emoji", | ||
| "description", | ||
| "cover_image_url", | ||
| "archived_at", | ||
| ] | ||
| read_only_fields = fields | ||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -314,6 +314,20 @@ def get(self, request, slug, project_id): | |||||||||
| Supports filtering, ordering, and field selection through query parameters. | ||||||||||
| """ | ||||||||||
|
|
||||||||||
| unsupported_filters = [param for param in ("pql", "filters") if request.GET.get(param)] | ||||||||||
| if unsupported_filters: | ||||||||||
|
Comment on lines
+317
to
+318
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Reject unsupported parameters by presence, not truthiness.
Proposed fix- unsupported_filters = [param for param in ("pql", "filters") if request.GET.get(param)]
+ unsupported_filters = [param for param in ("pql", "filters") if param in request.GET]📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||
| return Response( | ||||||||||
| { | ||||||||||
| "pql": ( | ||||||||||
| "PQL and structured filters are not supported on this Plane edition. " | ||||||||||
| "Remove the pql/filters parameter and filter results client-side, or use " | ||||||||||
| "a Plane edition that supports work item query filtering." | ||||||||||
| ), | ||||||||||
| "unsupported_parameters": unsupported_filters, | ||||||||||
| }, | ||||||||||
| status=status.HTTP_400_BAD_REQUEST, | ||||||||||
| ) | ||||||||||
|
|
||||||||||
| external_id = request.GET.get("external_id") | ||||||||||
| external_source = request.GET.get("external_source") | ||||||||||
|
|
||||||||||
|
|
||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,18 +13,26 @@ | |
|
|
||
| # Module imports | ||
| from .base import BaseAPIView | ||
| from plane.api.serializers import UserLiteSerializer, ProjectMemberSerializer | ||
| from plane.api.serializers import ( | ||
| UserLiteSerializer, | ||
| ProjectMemberSerializer, | ||
| WorkspaceMemberLiteAPISerializer, | ||
| ProjectMemberLiteAPISerializer, | ||
| ) | ||
| from plane.db.models import User, Workspace, WorkspaceMember, ProjectMember | ||
| from plane.utils.permissions import ProjectMemberPermission, WorkSpaceAdminPermission, ProjectAdminPermission | ||
| from plane.utils.openapi import ( | ||
| WORKSPACE_SLUG_PARAMETER, | ||
| PROJECT_ID_PARAMETER, | ||
| CURSOR_PARAMETER, | ||
| PER_PAGE_PARAMETER, | ||
| UNAUTHORIZED_RESPONSE, | ||
| FORBIDDEN_RESPONSE, | ||
| WORKSPACE_NOT_FOUND_RESPONSE, | ||
| PROJECT_NOT_FOUND_RESPONSE, | ||
| WORKSPACE_MEMBER_EXAMPLE, | ||
| PROJECT_MEMBER_EXAMPLE, | ||
| create_paginated_response, | ||
| ) | ||
|
|
||
|
|
||
|
|
@@ -220,3 +228,99 @@ def delete(self, request, slug, project_id, pk): | |
| project_member.is_active = False | ||
| project_member.save() | ||
| return Response(status=status.HTTP_204_NO_CONTENT) | ||
|
|
||
|
|
||
| class WorkspaceMemberLiteAPIEndpoint(BaseAPIView): | ||
| """Workspace members (lite) list endpoint.""" | ||
|
|
||
| permission_classes = [WorkSpaceAdminPermission] | ||
| use_read_replica = True | ||
|
|
||
| @extend_schema( | ||
| operation_id="get_workspace_members_lite", | ||
| summary="List workspace members (lite)", | ||
| description="Retrieve a paginated, lightweight list of workspace members for pickers and directories.", | ||
| tags=["Members"], | ||
| parameters=[WORKSPACE_SLUG_PARAMETER, CURSOR_PARAMETER, PER_PAGE_PARAMETER], | ||
| responses={ | ||
| 200: create_paginated_response( | ||
| WorkspaceMemberLiteAPISerializer, | ||
| "PaginatedWorkspaceMemberLite", | ||
| "Paginated list of workspace members with minimal fields", | ||
| "Paginated Workspace Members (Lite)", | ||
| ), | ||
| 401: UNAUTHORIZED_RESPONSE, | ||
| 403: FORBIDDEN_RESPONSE, | ||
| 404: WORKSPACE_NOT_FOUND_RESPONSE, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win OpenAPI schema documents 404 but code returns 400 for non-existent workspace. Both Also applies to: 301-301 🤖 Prompt for AI Agents |
||
| }, | ||
| ) | ||
| def get(self, request, slug): | ||
| """List workspace members (lite) | ||
|
|
||
| Retrieve a paginated, lightweight list of workspace members, optimized for | ||
| pickers and directories. | ||
| """ | ||
| # Check if the workspace exists | ||
| if not Workspace.objects.filter(slug=slug).exists(): | ||
| return Response( | ||
| {"error": "Provided workspace does not exist"}, | ||
| status=status.HTTP_400_BAD_REQUEST, | ||
| ) | ||
|
|
||
| workspace_members = ( | ||
| WorkspaceMember.objects.filter(workspace__slug=slug).select_related("member").order_by("-created_at") | ||
| ) | ||
| return self.paginate( | ||
| request=request, | ||
| queryset=workspace_members, | ||
| on_results=lambda members: WorkspaceMemberLiteAPISerializer(members, many=True).data, | ||
| ) | ||
|
|
||
|
|
||
| class ProjectMemberLiteAPIEndpoint(BaseAPIView): | ||
| """Project members (lite) list endpoint.""" | ||
|
|
||
| permission_classes = [ProjectMemberPermission] | ||
| use_read_replica = True | ||
|
|
||
| @extend_schema( | ||
| operation_id="get_project_members_lite", | ||
| summary="List project members (lite)", | ||
| description="Retrieve a paginated, lightweight list of project members for pickers and directories.", | ||
| tags=["Members"], | ||
| parameters=[WORKSPACE_SLUG_PARAMETER, PROJECT_ID_PARAMETER, CURSOR_PARAMETER, PER_PAGE_PARAMETER], | ||
| responses={ | ||
| 200: create_paginated_response( | ||
| ProjectMemberLiteAPISerializer, | ||
| "PaginatedProjectMemberLite", | ||
| "Paginated list of project members with minimal fields", | ||
| "Paginated Project Members (Lite)", | ||
| ), | ||
| 401: UNAUTHORIZED_RESPONSE, | ||
| 403: FORBIDDEN_RESPONSE, | ||
| 404: PROJECT_NOT_FOUND_RESPONSE, | ||
| }, | ||
| ) | ||
| def get(self, request, slug, project_id): | ||
| """List project members (lite) | ||
|
|
||
| Retrieve a paginated, lightweight list of project members, optimized for | ||
| pickers and directories. | ||
| """ | ||
| # Check if the workspace exists | ||
| if not Workspace.objects.filter(slug=slug).exists(): | ||
| return Response( | ||
| {"error": "Provided workspace does not exist"}, | ||
| status=status.HTTP_400_BAD_REQUEST, | ||
| ) | ||
|
|
||
| project_members = ( | ||
| ProjectMember.objects.filter(project_id=project_id, workspace__slug=slug) | ||
| .select_related("member") | ||
| .order_by("-created_at") | ||
| ) | ||
|
Comment on lines
+304
to
+321
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Check if ProjectMemberPermission validates project existence and what exception it raises.
rg -n "class ProjectMemberPermission" --type=py -A30 apps/api/plane/utils/permissions.pyRepository: makeplane/plane Length of output: 229 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Locate the permission class and the lite endpoint implementation.
git ls-files | rg 'member\.py$|permissions\.py$|project.*member|ProjectMember'
rg -n "class ProjectMemberPermission|ProjectMemberLiteAPIEndpoint|def get\(self, request, slug, project_id\)" apps/api -SRepository: makeplane/plane Length of output: 4263 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Inspect the lite endpoint and the matching permission class.
sed -n '280,340p' apps/api/plane/api/views/member.py
printf '\n---\n'
sed -n '1,180p' apps/api/plane/utils/permissions/project.py
printf '\n---\n'
sed -n '1,220p' apps/api/plane/app/permissions/project.pyRepository: makeplane/plane Length of output: 12256 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Check whether the lite endpoint uses ProjectMemberPermission and how exceptions are handled.
sed -n '240,320p' apps/api/plane/api/views/member.py
printf '\n---\n'
sed -n '1,220p' apps/api/plane/api/views/base.py
printf '\n---\n'
rg -n "DoesNotExist|handle_exception" apps/api/plane/api/views/base.py apps/api/plane/api/views -SRepository: makeplane/plane Length of output: 12844 Add an explicit project existence check here. 🤖 Prompt for AI Agents |
||
| return self.paginate( | ||
| request=request, | ||
| queryset=project_members, | ||
| on_results=lambda members: ProjectMemberLiteAPISerializer(members, many=True).data, | ||
| ) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: makeplane/plane
Length of output: 225
🏁 Script executed:
Repository: makeplane/plane
Length of output: 39733
🏁 Script executed:
Repository: makeplane/plane
Length of output: 659
🏁 Script executed:
Repository: makeplane/plane
Length of output: 18406
Validate
order_bybefore passing it to.order_by()request.GET.get("order_by", "-created_at")still reaches.order_by()directly here, so an invalid field can raiseFieldErrorand fall through the generic 500 path. Reusesanitize_order_by()here; the same raw pattern also appears in the module and project endpoints.🤖 Prompt for AI Agents