Skip to content

Conversation

@pablohashescobar
Copy link
Member

@pablohashescobar pablohashescobar commented Nov 3, 2025

Description

  • Introduced WorkspaceInviteSerializer for handling workspace invites.
  • Added WorkspaceInvitationsViewset to manage invites (list, create, retrieve, update, delete).
  • Created ProjectMemberSerializer for managing project members.
  • Updated ProjectMemberAPIEndpoint to support creating and updating project members with appropriate permissions.
  • Enhanced URL routing to include new invite and member management paths.

Type of Change

  • Improvement (change that would cause existing functionality to not work as expected)

References

WEB-5237

Summary by CodeRabbit

  • New Features

    • Workspace invitation management: list/create/update/delete invites with email/role validation and workspace scoping; invite endpoints mounted under workspace routes.
    • Project member management: list/create/update/soft-delete via dedicated list/detail endpoints and serializers.
  • Permissions

    • Unified role enum (Admin/Member/Guest), new project-admin permission, and expanded guest allowances for several actions.
    • New page-level permission rules for public/private pages and per-method access.
  • Backend

    • Centralized API base viewset with improved error handling and consistent request handling.

- Introduced `WorkspaceInviteSerializer` for handling workspace invites.
- Added `WorkspaceInvitationsViewset` to manage invites (list, create, retrieve, update, delete).
- Created `ProjectMemberSerializer` for managing project members.
- Updated `ProjectMemberAPIEndpoint` to support creating and updating project members with appropriate permissions.
- Enhanced URL routing to include new invite and member management paths.
Copilot AI review requested due to automatic review settings November 3, 2025 11:16
@makeplane
Copy link

makeplane bot commented Nov 3, 2025

Linked to Plane Work Item(s)

This comment was auto-generated by Plane

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 3, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds workspace invitation serializers, views, and routes; splits project-member endpoints into list/create and detail; introduces a BaseViewSet with enhanced error handling and helpers; implements a permission system (ROLE enum, allow_permission decorator, workspace/project/page permissions); and re-exports new serializers, viewset, and permissions.

Changes

Cohort / File(s) Summary
Serializers — Invitations & Members
apps/api/plane/api/serializers/invite.py, apps/api/plane/api/serializers/member.py, apps/api/plane/api/serializers/__init__.py
Added WorkspaceInviteSerializer (fields: id, email, role, created_at, updated_at, responded_at, accepted; validates email format, role enum, and per-workspace uniqueness) and ProjectMemberSerializer (member PK field, validates member existence and workspace membership). Both serializers exported via __init__.py.
Views — Base & Invitations
apps/api/plane/api/views/base.py, apps/api/plane/api/views/invite.py, apps/api/plane/api/views/__init__.py
Added BaseViewSet (Timezone & read-replica mixins, ModelViewSet, improved get_queryset, handle_exception, dispatch, and helpers: workspace_slug, project_id, fields, expand) and WorkspaceInvitationsViewset (scoped by workspace slug; list/retrieve/create/partial_update/destroy; uses WorkspaceInviteSerializer, permission WorkspaceOwnerPermission). Exported viewset in __init__.py.
Views — Project Members & Issues
apps/api/plane/api/views/member.py, apps/api/plane/api/views/issue.py
Replaced a single project-member endpoint with ProjectMemberListCreateAPIEndpoint (GET/POST) and ProjectMemberDetailAPIEndpoint (GET/PATCH/DELETE with soft-delete). Added per-method permission selection and OpenAPI annotations. Extended issue handlers to permit GUEST role for specified actions.
URL Routing
apps/api/plane/api/urls/invite.py, apps/api/plane/api/urls/member.py, apps/api/plane/api/urls/__init__.py
New router mounts WorkspaceInvitationsViewset under workspaces/<str:slug>/invitations/; invite patterns included in package urlpatterns. Project-member routes extended to support POST and member detail routes with PATCH/DELETE/GET.
Permissions — Base, Workspace, Project, Page, Exports
apps/api/plane/utils/permissions/base.py, apps/api/plane/utils/permissions/workspace.py, apps/api/plane/utils/permissions/project.py, apps/api/plane/utils/permissions/page.py, apps/api/plane/utils/permissions/__init__.py, apps/api/plane/app/permissions/__init__.py
Introduced ROLE enum (ADMIN, MEMBER, GUEST) and allow_permission decorator. Implemented workspace permissions (owner/admin/viewer/entity/user), project permissions (ProjectBasePermission, ProjectMemberPermission, ProjectEntityPermission, ProjectAdminPermission, ProjectLitePermission), and ProjectPagePermission. Re-exported permissions via package __init__ files and added ProjectAdminPermission to app-level exports.
Small public API exports
apps/api/plane/api/serializers/__init__.py, apps/api/plane/api/views/__init__.py, apps/api/plane/app/permissions/__init__.py
Exposed new serializers and WorkspaceInvitationsViewset, and re-exported ProjectAdminPermission in package __init__ modules.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant API as WorkspaceInvitationsViewset
    participant Perm as WorkspaceOwnerPermission
    participant Serializer as WorkspaceInviteSerializer
    participant DB as Database

    rect rgb(245,250,255)
    note over Client,API: Create invitation (POST /workspaces/{slug}/invitations/)
    Client->>API: POST { email, role }
    API->>Perm: has_permission(request, view)
    Perm-->>API: allowed / denied
    alt allowed
        API->>Serializer: validate_email(), validate_role(), validate()
        Serializer->>DB: query WorkspaceMemberInvite by (workspace_slug, email)
        alt exists
            Serializer-->>API: ValidationError (EMAIL_ALREADY_INVITED)
            API-->>Client: 400 Bad Request
        else not exists
            API->>DB: create WorkspaceMemberInvite(created_by=request.user, workspace=slug)
            DB-->>API: created invite
            API-->>Client: 201 Created
        end
    else denied
        API-->>Client: 403 Forbidden
    end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Pay attention to BaseViewSet.handle_exception mappings and dispatch logging side effects.
  • Inspect WorkspaceInvitationsViewset scoping, email immutability in partial_update, and destroy guards (responded/accepted).
  • Review serializer uniqueness validation for race conditions and proper queryset filtering.
  • Audit new permission classes and allow_permission decorator for consistent ROLE handling, anonymous/inactive user behavior, and overlaps between workspace/project checks.
  • Verify URL patterns to avoid routing conflicts with existing routes.

Poem

🐰
I nibble keys and patch the gate,
Invites hop in — new members wait.
BaseViewSet watches, tidy and bright,
Permissions guard both day and night.
Hooray — I thump and code takes flight!

Pre-merge checks and finishing touches

✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately captures the main features introduced: workspace invitation endpoints and project member management endpoints, matching the substantial changes made across serializers, views, and URL routing.
Description check ✅ Passed The description covers the main changes (serializers, viewset, endpoints, URL routing) and includes a type of change and reference link, but lacks test scenarios and any other supporting details requested in the template.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch member-invite-apis

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds functionality for managing project members and workspace invitations through new API endpoints. The main focus is enabling CRUD operations for project members with appropriate permission controls and creating a workspace invitation management system.

  • Added ProjectAdminPermission class to restrict certain operations to project admins
  • Created new POST, PATCH, and DELETE endpoints for managing project members
  • Implemented workspace invitation viewset with full CRUD operations
  • Extended guest role permissions to allow guests to perform certain issue operations

Reviewed Changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
apps/api/plane/app/permissions/project.py Added ProjectAdminPermission class for admin-only operations
apps/api/plane/app/permissions/init.py Exported new ProjectAdminPermission class
apps/api/plane/api/views/member.py Added POST, PATCH, DELETE methods to ProjectMemberAPIEndpoint with dynamic permissions
apps/api/plane/api/views/issue.py Extended allowed roles to include GUEST for issue operations
apps/api/plane/api/views/invite.py New viewset for workspace invitation management
apps/api/plane/api/views/base.py Added BaseViewSet class with common functionality and error handling
apps/api/plane/api/views/init.py Exported new WorkspaceInvitationsViewset
apps/api/plane/api/urls/member.py Added URL patterns for project member POST, PATCH, DELETE operations
apps/api/plane/api/urls/invite.py New URL patterns for workspace invitations
apps/api/plane/api/urls/init.py Included invitation URL patterns
apps/api/plane/api/serializers/member.py New serializer for project member operations
apps/api/plane/api/serializers/invite.py New serializer for workspace invitations
apps/api/plane/api/serializers/init.py Exported new serializers
Comments suppressed due to low confidence (1)

apps/api/plane/api/views/member.py:2

  • Import of 'c' is not used.
from calendar import c

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/api/plane/api/views/issue.py (1)

1963-1969: Fix incorrect parameter type in permission checks.

Lines 1964, 2037, and 2102 pass request.user (User object) to user_has_issue_permission(), but the function signature at line 146 expects user_id (integer) as the first parameter. This causes incorrect behavior:

  • Line 147: user_id == issue.created_by_id will always return False when comparing a User object to an integer
  • Line 152: member_id=user_id filter will fail or return incorrect results

Line 1807 demonstrates the correct usage: user_has_issue_permission(request.user.id, ...).

Apply this diff to fix the issue:

 if not user_has_issue_permission(
-    request.user,
+    request.user.id,
     project_id=project_id,
     issue=issue,
     allowed_roles=[ROLE.ADMIN.value, ROLE.MEMBER.value, ROLE.GUEST.value],
     allow_creator=True,
 ):

This same fix should be applied at lines 1964, 2037, and 2102.

Also applies to: 2036-2042, 2101-2107

apps/api/plane/api/views/base.py (1)

114-124: Critical bug: returning exception object instead of response.

Line 124 returns exc (the exception object) instead of response (the error response from handle_exception). This will cause the API to fail when exceptions occur, as Django expects a Response object, not an exception.

Apply this diff:

     def dispatch(self, request, *args, **kwargs):
         try:
             response = super().dispatch(request, *args, **kwargs)
             if settings.DEBUG:
                 from django.db import connection

                 print(f"{request.method} - {request.get_full_path()} of Queries: {len(connection.queries)}")
             return response
         except Exception as exc:
             response = self.handle_exception(exc)
-            return exc
+            return response
🧹 Nitpick comments (5)
apps/api/plane/api/serializers/invite.py (1)

49-56: Consider making uniqueness validation more defensive.

The validation correctly prevents duplicate email invitations within a workspace. However, it doesn't exclude the current instance during updates, relying instead on the viewset's partial_update method to block email changes. While this works given the current implementation, the serializer could be more defensive by checking if it's an update operation and excluding self.instance from the uniqueness check.

Example improvement:

 def validate(self, data):
     slug = self.context["slug"]
+    queryset = WorkspaceMemberInvite.objects.filter(email=data["email"], workspace__slug=slug)
+    if self.instance:
+        queryset = queryset.exclude(pk=self.instance.pk)
-    if (
-        data.get("email")
-        and WorkspaceMemberInvite.objects.filter(email=data["email"], workspace__slug=slug).exists()
-    ):
+    if data.get("email") and queryset.exists():
         raise serializers.ValidationError("Email already invited", code="EMAIL_ALREADY_INVITED")
     return data
apps/api/plane/api/views/member.py (2)

166-171: Consider more explicit error handling for object retrieval.

The ProjectMember.objects.get() call will raise DoesNotExist if the member isn't found, which is caught by handle_exception. While this works, using get_object_or_404 or a try/except block would make the error handling more explicit and improve code clarity.

Example:

+from django.shortcuts import get_object_or_404
+
 def patch(self, request, slug, project_id, pk):
-    project_member = ProjectMember.objects.get(project_id=project_id, workspace__slug=slug, pk=pk)
+    project_member = get_object_or_404(ProjectMember, project_id=project_id, workspace__slug=slug, pk=pk)
     serializer = ProjectMemberSerializer(project_member, data=request.data, partial=True, context={"slug": slug})
     serializer.is_valid(raise_exception=True)
     serializer.save()
     return Response(serializer.data, status=status.HTTP_200_OK)

181-185: Consider more explicit error handling (same as PATCH method).

Like the patch method, this uses ProjectMember.objects.get() without explicit error handling. The soft-delete approach (setting is_active=False) is good for preserving history.

Apply the same improvement as suggested for the patch method:

 def delete(self, request, slug, project_id, pk):
-    project_member = ProjectMember.objects.get(project_id=project_id, workspace__slug=slug, pk=pk)
+    project_member = get_object_or_404(ProjectMember, project_id=project_id, workspace__slug=slug, pk=pk)
     project_member.is_active = False
     project_member.save()
     return Response(status=status.HTTP_204_NO_CONTENT)
apps/api/plane/api/views/base.py (1)

165-277: Consider reducing code duplication between BaseAPIView and BaseViewSet.

There's significant duplication of the handle_exception, dispatch, and property methods (workspace_slug, project_id, fields, expand) between BaseAPIView and BaseViewSet. While some duplication is expected due to different base classes (GenericAPIView vs ModelViewSet), the common logic could potentially be extracted into a shared mixin.

Consider creating a shared mixin for common functionality:

class CommonAPIMethodsMixin:
    @property
    def workspace_slug(self):
        return self.kwargs.get("slug", None)
    
    @property
    def project_id(self):
        project_id = self.kwargs.get("project_id", None)
        if project_id:
            return project_id
        if resolve(self.request.path_info).url_name == "project":
            return self.kwargs.get("pk", None)
    
    @property
    def fields(self):
        fields = [field for field in self.request.GET.get("fields", "").split(",") if field]
        return fields if fields else None
    
    @property
    def expand(self):
        expand = [expand for expand in self.request.GET.get("expand", "").split(",") if expand]
        return expand if expand else None

Then both base classes could inherit from this mixin.

apps/api/plane/api/views/invite.py (1)

85-90: Consider more explicit error handling for workspace retrieval.

While WorkspaceOwnerPermission should ensure the workspace exists, using get_object_or_404 or explicit error handling would make the code more defensive and clearer.

+from django.shortcuts import get_object_or_404
+
 def create(self, request, slug):
-    workspace = Workspace.objects.get(slug=slug)
+    workspace = get_object_or_404(Workspace, slug=slug)
     serializer = WorkspaceInviteSerializer(data=request.data, context={"slug": slug})
     serializer.is_valid(raise_exception=True)
     serializer.save(workspace=workspace, created_by=request.user)
     return Response(serializer.data, status=status.HTTP_201_CREATED)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bfea2c8 and 65d8520.

📒 Files selected for processing (13)
  • apps/api/plane/api/serializers/__init__.py (1 hunks)
  • apps/api/plane/api/serializers/invite.py (1 hunks)
  • apps/api/plane/api/serializers/member.py (1 hunks)
  • apps/api/plane/api/urls/__init__.py (2 hunks)
  • apps/api/plane/api/urls/invite.py (1 hunks)
  • apps/api/plane/api/urls/member.py (1 hunks)
  • apps/api/plane/api/views/__init__.py (1 hunks)
  • apps/api/plane/api/views/base.py (2 hunks)
  • apps/api/plane/api/views/invite.py (1 hunks)
  • apps/api/plane/api/views/issue.py (3 hunks)
  • apps/api/plane/api/views/member.py (3 hunks)
  • apps/api/plane/app/permissions/__init__.py (1 hunks)
  • apps/api/plane/app/permissions/project.py (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: NarayanBavisetti
Repo: makeplane/plane PR: 7460
File: apps/api/plane/app/serializers/draft.py:112-122
Timestamp: 2025-07-23T18:18:06.875Z
Learning: In the Plane codebase serializers, workspace_id is not consistently passed in serializer context, so parent issue validation in DraftIssueCreateSerializer only checks project_id rather than both workspace_id and project_id. The existing project member authentication system already validates that users can only access projects they belong to, providing sufficient security without risking breaking functionality by adding workspace_id validation where the context might not be available.
📚 Learning: 2025-07-23T18:18:06.875Z
Learnt from: NarayanBavisetti
Repo: makeplane/plane PR: 7460
File: apps/api/plane/app/serializers/draft.py:112-122
Timestamp: 2025-07-23T18:18:06.875Z
Learning: In the Plane codebase serializers, workspace_id is not consistently passed in serializer context, so parent issue validation in DraftIssueCreateSerializer only checks project_id rather than both workspace_id and project_id. The existing project member authentication system already validates that users can only access projects they belong to, providing sufficient security without risking breaking functionality by adding workspace_id validation where the context might not be available.

Applied to files:

  • apps/api/plane/api/serializers/__init__.py
  • apps/api/plane/api/serializers/member.py
  • apps/api/plane/api/serializers/invite.py
🧬 Code graph analysis (12)
apps/api/plane/api/serializers/__init__.py (2)
apps/api/plane/api/serializers/invite.py (1)
  • WorkspaceInviteSerializer (12-56)
apps/api/plane/api/serializers/member.py (1)
  • ProjectMemberSerializer (10-35)
apps/api/plane/api/views/invite.py (3)
apps/api/plane/api/views/base.py (3)
  • BaseViewSet (165-277)
  • get_queryset (174-179)
  • filter_queryset (55-58)
apps/api/plane/db/models/workspace.py (2)
  • WorkspaceMemberInvite (227-251)
  • Workspace (115-178)
apps/api/plane/api/serializers/invite.py (1)
  • WorkspaceInviteSerializer (12-56)
apps/api/plane/api/serializers/member.py (4)
apps/api/plane/db/models/project.py (1)
  • ProjectMember (192-236)
apps/api/plane/db/models/workspace.py (1)
  • WorkspaceMember (194-224)
apps/api/plane/api/serializers/base.py (1)
  • BaseSerializer (5-114)
apps/api/plane/db/models/user.py (1)
  • User (38-164)
apps/api/plane/api/urls/invite.py (1)
apps/api/plane/api/views/invite.py (1)
  • WorkspaceInvitationsViewset (20-150)
apps/api/plane/api/serializers/invite.py (2)
apps/api/plane/db/models/workspace.py (1)
  • WorkspaceMemberInvite (227-251)
apps/api/plane/api/serializers/base.py (1)
  • BaseSerializer (5-114)
apps/api/plane/api/views/__init__.py (1)
apps/api/plane/api/views/invite.py (1)
  • WorkspaceInvitationsViewset (20-150)
apps/api/plane/api/views/member.py (4)
apps/api/plane/api/serializers/member.py (1)
  • ProjectMemberSerializer (10-35)
apps/api/plane/db/models/workspace.py (3)
  • Workspace (115-178)
  • WorkspaceMember (194-224)
  • save (188-191)
apps/api/plane/db/models/project.py (4)
  • ProjectMember (192-236)
  • save (157-159)
  • save (169-171)
  • save (208-218)
apps/api/plane/app/permissions/project.py (2)
  • ProjectMemberPermission (52-78)
  • ProjectAdminPermission (115-126)
apps/api/plane/api/views/base.py (4)
apps/api/plane/utils/exception_logger.py (1)
  • log_exception (9-20)
apps/api/plane/utils/paginator.py (1)
  • BasePaginator (631-729)
apps/api/plane/utils/core/mixins/view.py (1)
  • ReadReplicaControlMixin (6-20)
apps/api/plane/app/views/base.py (6)
  • BaseViewSet (44-142)
  • TimezoneMixin (30-41)
  • handle_exception (66-105)
  • handle_exception (163-200)
  • fields (135-137)
  • fields (225-227)
apps/api/plane/app/permissions/project.py (2)
apps/api/plane/db/models/project.py (2)
  • ProjectMember (192-236)
  • ROLE (21-24)
apps/api/plane/api/views/base.py (4)
  • workspace_slug (142-143)
  • workspace_slug (257-258)
  • project_id (146-152)
  • project_id (261-267)
apps/api/plane/api/views/issue.py (1)
apps/api/plane/db/models/project.py (1)
  • ROLE (21-24)
apps/api/plane/app/permissions/__init__.py (1)
apps/api/plane/app/permissions/project.py (1)
  • ProjectAdminPermission (115-126)
apps/api/plane/api/urls/member.py (1)
apps/api/plane/api/views/member.py (1)
  • ProjectMemberAPIEndpoint (92-185)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: CodeQL analysis (python)
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (17)
apps/api/plane/api/views/__init__.py (1)

56-57: LGTM!

The import correctly exposes WorkspaceInvitationsViewset for use in URL routing.

apps/api/plane/api/urls/__init__.py (1)

11-24: LGTM!

The invite patterns are correctly imported and included in the URL configuration, following the established pattern used for other URL modules.

apps/api/plane/api/serializers/__init__.py (1)

56-57: LGTM!

The new serializers are correctly exported, making them accessible as part of the package's public API.

apps/api/plane/app/permissions/project.py (1)

115-127: LGTM!

The ProjectAdminPermission class correctly enforces admin-level authorization by verifying that the user is an active project admin. The implementation follows the established patterns in the codebase.

apps/api/plane/app/permissions/__init__.py (1)

14-14: LGTM!

The ProjectAdminPermission is correctly exported, making it available for use in views that require admin-level project access control.

apps/api/plane/api/serializers/invite.py (2)

37-42: LGTM - Email validation is correctly implemented.

The validation uses Django's built-in validator and properly converts exceptions to DRF's ValidationError format with an appropriate error code.


44-47: LGTM - Role validation correctly restricts to valid workspace roles.

The validation ensures only permitted roles (ADMIN, MEMBER, GUEST) can be assigned, with a clear error code for invalid values.

apps/api/plane/api/views/member.py (2)

96-99: LGTM - Dynamic permission handling is well-designed.

The method appropriately restricts mutating operations (POST, PATCH, DELETE) to project admins while allowing any project member to list members via GET requests.


151-155: LGTM - Project member creation follows standard patterns.

The implementation correctly validates the member's workspace membership via the serializer and associates the member with the project.

apps/api/plane/api/views/base.py (3)

165-179: LGTM - BaseViewSet structure is well-designed.

The class correctly extends the necessary mixins and base classes for a full-featured viewset with timezone handling, read replica control, pagination, and standard ModelViewSet capabilities.


181-240: LGTM - Exception handling with structured logging is well-implemented.

The error handling provides appropriate HTTP status codes for different exception types and includes structured logging with error codes and messages. The use of different log levels (warning vs error) appropriately categorizes the severity of different error types.


242-254: LGTM - dispatch correctly returns response (unlike BaseAPIView).

The exception handling in this dispatch method correctly returns the response from handle_exception, unlike the bug in BaseAPIView.dispatch at line 124.

apps/api/plane/api/views/invite.py (5)

20-36: LGTM - Queryset filtering correctly scopes invitations to workspace.

The get_queryset method properly filters invitations by workspace slug and applies any configured filters. The get_object method retrieves individual invitations from this scoped queryset.


51-54: LGTM - Standard list implementation.

The method correctly retrieves and serializes workspace invitations using the filtered queryset.


71-74: LGTM - Standard retrieve implementation.

The method correctly retrieves and serializes a single workspace invitation.


108-120: LGTM - Excellent immutability enforcement for email field.

The method correctly prevents email changes after invite creation, which is the appropriate business logic. Allowing partial updates of other fields (like role) while blocking email changes is well-designed.


137-150: LGTM - Excellent validation prevents data integrity issues.

The method correctly prevents deletion of invitations that have been accepted or responded to, maintaining audit trail and preventing data loss. The business logic validation is well-designed.

- Replaced manual URL paths for workspace invitations with a DefaultRouter for cleaner routing.
- Updated imports for better organization and clarity.
…utilities

- Updated import paths for `WorkspaceOwnerPermission`, `ProjectMemberPermission`, and others to improve code organization.
- Added new permission utility classes in `utils/permissions` for better role management and access control.
- Implemented `allow_permission` decorator for streamlined permission checks across views.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/api/plane/api/views/member.py (1)

133-138: Soft-deleted members still leak into the GET response

delete() only flips is_active to False, but the GET endpoint keeps returning every ProjectMember without filtering by is_active. As soon as someone is “removed”, their record continues to appear in the list, so clients still see members who should have been deleted. Please filter the queryset by is_active=True before serializing.

-        project_members = ProjectMember.objects.filter(project_id=project_id, workspace__slug=slug).values_list(
+        project_members = ProjectMember.objects.filter(
+            project_id=project_id,
+            workspace__slug=slug,
+            is_active=True,
+        ).values_list(
             "member_id", flat=True
         )
🧹 Nitpick comments (1)
apps/api/plane/utils/permissions/page.py (1)

113-121: Rename misleading variable.

The variable project_member_exists suggests it's a boolean indicating membership, but it actually holds the result of the action access check (which is role-based permission validation).

Apply this diff for clarity:

     def _has_public_page_action_access(self, request, role):
         """
         Check if the user has permission to access a public page
         and can perform operations on the page.
         """
-        project_member_exists = self._check_project_action_access(request, role)
-        if not project_member_exists:
+        has_action_access = self._check_project_action_access(request, role)
+        if not has_action_access:
             return False
         return True
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5500e50 and c72688b.

📒 Files selected for processing (7)
  • apps/api/plane/api/views/invite.py (1 hunks)
  • apps/api/plane/api/views/member.py (3 hunks)
  • apps/api/plane/utils/permissions/__init__.py (1 hunks)
  • apps/api/plane/utils/permissions/base.py (1 hunks)
  • apps/api/plane/utils/permissions/page.py (1 hunks)
  • apps/api/plane/utils/permissions/project.py (1 hunks)
  • apps/api/plane/utils/permissions/workspace.py (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: NarayanBavisetti
Repo: makeplane/plane PR: 7460
File: apps/api/plane/app/serializers/draft.py:112-122
Timestamp: 2025-07-23T18:18:06.875Z
Learning: In the Plane codebase serializers, workspace_id is not consistently passed in serializer context, so parent issue validation in DraftIssueCreateSerializer only checks project_id rather than both workspace_id and project_id. The existing project member authentication system already validates that users can only access projects they belong to, providing sufficient security without risking breaking functionality by adding workspace_id validation where the context might not be available.
📚 Learning: 2025-07-23T18:18:06.875Z
Learnt from: NarayanBavisetti
Repo: makeplane/plane PR: 7460
File: apps/api/plane/app/serializers/draft.py:112-122
Timestamp: 2025-07-23T18:18:06.875Z
Learning: In the Plane codebase serializers, workspace_id is not consistently passed in serializer context, so parent issue validation in DraftIssueCreateSerializer only checks project_id rather than both workspace_id and project_id. The existing project member authentication system already validates that users can only access projects they belong to, providing sufficient security without risking breaking functionality by adding workspace_id validation where the context might not be available.

Applied to files:

  • apps/api/plane/utils/permissions/workspace.py
  • apps/api/plane/utils/permissions/page.py
🧬 Code graph analysis (7)
apps/api/plane/utils/permissions/base.py (2)
apps/api/plane/db/models/workspace.py (1)
  • WorkspaceMember (194-224)
apps/api/plane/db/models/project.py (1)
  • ProjectMember (192-236)
apps/api/plane/utils/permissions/workspace.py (1)
apps/api/plane/db/models/workspace.py (1)
  • WorkspaceMember (194-224)
apps/api/plane/api/views/invite.py (5)
apps/api/plane/api/views/base.py (3)
  • BaseViewSet (165-277)
  • get_queryset (174-179)
  • filter_queryset (55-58)
apps/api/plane/db/models/workspace.py (2)
  • WorkspaceMemberInvite (227-251)
  • Workspace (115-178)
apps/api/plane/api/serializers/invite.py (1)
  • WorkspaceInviteSerializer (12-56)
apps/api/plane/utils/permissions/workspace.py (1)
  • WorkspaceOwnerPermission (47-54)
apps/api/plane/app/views/workspace/invite.py (1)
  • WorkspaceInvitationsViewset (33-146)
apps/api/plane/utils/permissions/page.py (5)
apps/api/plane/db/models/project.py (1)
  • ProjectMember (192-236)
apps/api/plane/db/models/page.py (1)
  • Page (19-73)
apps/api/plane/utils/permissions/base.py (1)
  • ROLE (9-12)
apps/api/plane/utils/permissions/project.py (5)
  • has_permission (10-49)
  • has_permission (53-78)
  • has_permission (82-112)
  • has_permission (116-126)
  • has_permission (130-139)
apps/api/plane/app/views/page/base.py (1)
  • access (247-260)
apps/api/plane/utils/permissions/__init__.py (4)
apps/api/plane/utils/permissions/workspace.py (6)
  • WorkSpaceBasePermission (15-44)
  • WorkspaceOwnerPermission (47-54)
  • WorkSpaceAdminPermission (57-67)
  • WorkspaceEntityPermission (70-86)
  • WorkspaceViewerPermission (89-96)
  • WorkspaceUserPermission (99-106)
apps/api/plane/utils/permissions/project.py (5)
  • ProjectBasePermission (9-49)
  • ProjectEntityPermission (81-112)
  • ProjectMemberPermission (52-78)
  • ProjectLitePermission (129-139)
  • ProjectAdminPermission (115-126)
apps/api/plane/utils/permissions/base.py (2)
  • allow_permission (15-73)
  • ROLE (9-12)
apps/api/plane/utils/permissions/page.py (1)
  • ProjectPagePermission (14-121)
apps/api/plane/utils/permissions/project.py (4)
apps/api/plane/db/models/project.py (1)
  • ProjectMember (192-236)
apps/api/plane/db/models/workspace.py (1)
  • WorkspaceMember (194-224)
apps/api/plane/utils/permissions/base.py (1)
  • ROLE (9-12)
apps/api/plane/api/views/issue.py (1)
  • project_identifier (174-175)
apps/api/plane/api/views/member.py (4)
apps/api/plane/api/views/base.py (3)
  • BaseAPIView (48-162)
  • project_id (146-152)
  • project_id (261-267)
apps/api/plane/api/serializers/member.py (1)
  • ProjectMemberSerializer (10-35)
apps/api/plane/db/models/project.py (4)
  • ProjectMember (192-236)
  • save (157-159)
  • save (169-171)
  • save (208-218)
apps/api/plane/utils/permissions/project.py (2)
  • ProjectMemberPermission (52-78)
  • ProjectAdminPermission (115-126)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (3)
apps/api/plane/utils/permissions/project.py (2)

9-49: LGTM: Well-structured base permission logic.

The ProjectBasePermission class correctly implements a layered access control strategy:

  • SAFE_METHODS require workspace membership
  • POST requires workspace admin or member role
  • Other methods require project admin OR (project member AND workspace admin)

This provides a solid foundation for project-level access control.


81-112: LGTM: Appropriate dual-path handling for project identification.

The ProjectEntityPermission class correctly handles two scenarios:

  • Resources accessed via project_identifier (lines 87-94)
  • Resources accessed via project_id (lines 97-112)

This flexibility supports different API endpoint patterns while maintaining consistent security checks.

apps/api/plane/utils/permissions/page.py (1)

1-2: Keep import as-is; review comment is based on incomplete analysis.

The script reveals an import pattern across the codebase: all project.py files import ROLE from plane.db.models.project, while all page.py files import from plane.app.permissions. This naming-based split appears intentional, not an error. Although ROLE is defined identically in multiple locations (ADMIN=20, MEMBER=15, GUEST=5), the architectural separation by file type is consistent across ~40+ import statements in the codebase. The inconsistency flagged in the review exists but reflects an established design decision rather than a bug.

- Renamed `ProjectMemberAPIEndpoint` to `ProjectMemberListCreateAPIEndpoint` and introduced `ProjectMemberDetailAPIEndpoint` for better separation of concerns.
- Updated URL routing to reflect the new endpoint structure, allowing for distinct handling of member listing, creation, and detail retrieval.
- Enhanced the detail endpoint to support fetching and updating specific project members.
dheeru0198
dheeru0198 previously approved these changes Nov 3, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (3)
apps/api/plane/api/views/member.py (3)

2-2: Remove the unused import.

This accidental import is not used anywhere in the file.

Apply this diff:

-from calendar import c

175-192: Fix response serializer and add error handling.

Two issues here:

  1. Response type mismatch: The OpenAPI schema (line 168) declares ProjectMemberSerializer as the response, but the implementation returns UserLiteSerializer data. This loses the role information and doesn't match the API contract.

  2. Missing error handling: The .get() calls on lines 189-190 will raise exceptions if the records don't exist, resulting in 500 errors instead of proper 404 responses.

Apply this diff to fix both issues:

-        # Get the workspace members that are present inside the workspace
-        project_members = ProjectMember.objects.get(project_id=project_id, workspace__slug=slug, pk=pk)
-        user = User.objects.get(id=project_members.member_id)
-        user = UserLiteSerializer(user).data
-        return Response(user, status=status.HTTP_200_OK)
+        try:
+            project_member = ProjectMember.objects.get(project_id=project_id, workspace__slug=slug, pk=pk)
+        except ProjectMember.DoesNotExist:
+            return Response(
+                {"error": "Project member not found"},
+                status=status.HTTP_404_NOT_FOUND,
+            )
+        
+        serializer = ProjectMemberSerializer(project_member)
+        return Response(serializer.data, status=status.HTTP_200_OK)

218-223: Add error handling for missing records.

Both the PATCH and DELETE methods use .get() without error handling (lines 219, 234). If the project member doesn't exist, this will raise an exception and return a 500 error instead of a proper 404 response.

Apply this diff:

     def patch(self, request, slug, project_id, pk):
-        project_member = ProjectMember.objects.get(project_id=project_id, workspace__slug=slug, pk=pk)
+        try:
+            project_member = ProjectMember.objects.get(project_id=project_id, workspace__slug=slug, pk=pk)
+        except ProjectMember.DoesNotExist:
+            return Response(
+                {"error": "Project member not found"},
+                status=status.HTTP_404_NOT_FOUND,
+            )
         serializer = ProjectMemberSerializer(project_member, data=request.data, partial=True, context={"slug": slug})
         serializer.is_valid(raise_exception=True)
         serializer.save()
         return Response(serializer.data, status=status.HTTP_200_OK)
 
     @extend_schema(
         operation_id="delete_project_member",
         summary="Delete project member",
         description="Delete a project member",
         tags=["Members"],
         parameters=[WORKSPACE_SLUG_PARAMETER, PROJECT_ID_PARAMETER],
         responses={204: OpenApiResponse(description="Project member deleted")},
     )
     def delete(self, request, slug, project_id, pk):
-        project_member = ProjectMember.objects.get(project_id=project_id, workspace__slug=slug, pk=pk)
+        try:
+            project_member = ProjectMember.objects.get(project_id=project_id, workspace__slug=slug, pk=pk)
+        except ProjectMember.DoesNotExist:
+            return Response(
+                {"error": "Project member not found"},
+                status=status.HTTP_404_NOT_FOUND,
+            )
         project_member.is_active = False
         project_member.save()
         return Response(status=status.HTTP_204_NO_CONTENT)

Also applies to: 233-237

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c72688b and 78f8c33.

📒 Files selected for processing (3)
  • apps/api/plane/api/urls/member.py (1 hunks)
  • apps/api/plane/api/views/__init__.py (2 hunks)
  • apps/api/plane/api/views/member.py (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/api/plane/api/views/init.py
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: NarayanBavisetti
Repo: makeplane/plane PR: 7460
File: apps/api/plane/app/serializers/draft.py:112-122
Timestamp: 2025-07-23T18:18:06.875Z
Learning: In the Plane codebase serializers, workspace_id is not consistently passed in serializer context, so parent issue validation in DraftIssueCreateSerializer only checks project_id rather than both workspace_id and project_id. The existing project member authentication system already validates that users can only access projects they belong to, providing sufficient security without risking breaking functionality by adding workspace_id validation where the context might not be available.
🧬 Code graph analysis (2)
apps/api/plane/api/urls/member.py (1)
apps/api/plane/api/views/member.py (3)
  • ProjectMemberListCreateAPIEndpoint (91-153)
  • ProjectMemberDetailAPIEndpoint (157-237)
  • WorkspaceMemberAPIEndpoint (28-88)
apps/api/plane/api/views/member.py (4)
apps/api/plane/api/views/base.py (3)
  • BaseAPIView (48-162)
  • project_id (146-152)
  • project_id (261-267)
apps/api/plane/api/serializers/user.py (1)
  • UserLiteSerializer (9-34)
apps/api/plane/api/serializers/member.py (1)
  • ProjectMemberSerializer (10-35)
apps/api/plane/utils/permissions/project.py (2)
  • ProjectMemberPermission (52-78)
  • ProjectAdminPermission (115-126)
🔇 Additional comments (3)
apps/api/plane/api/urls/member.py (1)

3-15: LGTM! Previous feedback addressed.

The URL patterns now have unique names ("project-members" for list/create vs "project-member" for detail), and the GET method has been added to the detail endpoint as requested. The routing structure is clean and follows Django conventions.

apps/api/plane/api/views/member.py (2)

95-98: LGTM! Dynamic permission handling is correct.

The get_permissions method appropriately restricts POST requests to project admins while allowing project members to list members. This is a standard DRF pattern for per-method permission control.


140-153: LGTM! Project member creation is properly validated.

The POST endpoint correctly passes the workspace slug in the serializer context, which enables the validate_member method to verify that the user is a workspace member before adding them to the project.

…ers and views

- Updated `ProjectMemberSerializer` to include role validation, ensuring only valid roles are accepted.
- Improved error handling for missing slug in member validation.
- Refactored permission checks in `IssueAttachmentDetailAPIEndpoint` to use user ID instead of user object for consistency and clarity.
- Cleaned up unused imports in `member.py` for better code organization.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (6)
apps/api/plane/api/serializers/member.py (1)

25-26: Remove redundant None check.

The member field is defined with required=True (line 18), which already ensures the value cannot be None. This explicit check is unnecessary.

Apply this diff:

     def validate_member(self, value):
         slug = self.context.get("slug")
         if not slug:
             raise serializers.ValidationError("Slug is required", code="INVALID_SLUG")
-        if not value:
-            raise serializers.ValidationError("Member is required", code="INVALID_MEMBER")
         if not WorkspaceMember.objects.filter(workspace__slug=slug, member=value).exists():
             raise serializers.ValidationError("Member not found in workspace", code="INVALID_MEMBER")
         return value
apps/api/plane/api/views/member.py (5)

157-158: Remove unused get_object method.

This method is never called by any view method (all use ProjectMember.objects.get() directly). Additionally, it references self.get_queryset() which is not defined.

Apply this diff:

 class ProjectMemberDetailAPIEndpoint(ProjectMemberListCreateAPIEndpoint):
-    def get_object(self):
-        return self.get_queryset().get(pk=self.kwargs.get("pk"))
-

188-190: Missing error handling and inefficient query pattern.

Two issues:

  1. ProjectMember.objects.get() and User.objects.get() will raise exceptions if not found, resulting in 500 errors instead of proper 404 responses.
  2. Two separate queries are inefficient when one query with select_related("member") would suffice.

Apply this diff:

-        # Get the workspace members that are present inside the workspace
-        project_members = ProjectMember.objects.get(project_id=project_id, workspace__slug=slug, pk=pk)
-        user = User.objects.get(id=project_members.member_id)
-        user = UserLiteSerializer(user).data
+        # Get the project member with related user data
+        try:
+            project_member = ProjectMember.objects.select_related("member").get(
+                project_id=project_id, workspace__slug=slug, pk=pk
+            )
+        except ProjectMember.DoesNotExist:
+            return Response(
+                {"error": "Project member not found"},
+                status=status.HTTP_404_NOT_FOUND,
+            )
+        user = UserLiteSerializer(project_member.member).data
         return Response(user, status=status.HTTP_200_OK)

193-206: Remove duplicate POST method.

This POST method duplicates the parent class implementation (lines 139-152) and creates a duplicate OpenAPI operation_id="create_project_member". POST operations should not be defined on detail endpoints.

Apply this diff:

-    @extend_schema(
-        operation_id="create_project_member",
-        summary="Create project member",
-        description="Create a new project member",
-        tags=["Members"],
-        parameters=[WORKSPACE_SLUG_PARAMETER, PROJECT_ID_PARAMETER],
-        responses={201: OpenApiResponse(description="Project member created", response=ProjectMemberSerializer)},
-        request=OpenApiRequest(request=ProjectMemberSerializer),
-    )
-    def post(self, request, slug, project_id):
-        serializer = ProjectMemberSerializer(data=request.data, context={"slug": slug})
-        serializer.is_valid(raise_exception=True)
-        serializer.save(project_id=project_id)
-        return Response(serializer.data, status=status.HTTP_201_CREATED)
-

218-218: Missing error handling for ProjectMember.objects.get().

If the project member doesn't exist, this will raise ProjectMember.DoesNotExist, resulting in a 500 error instead of a proper 404 response.

Apply this diff:

     def patch(self, request, slug, project_id, pk):
-        project_member = ProjectMember.objects.get(project_id=project_id, workspace__slug=slug, pk=pk)
+        try:
+            project_member = ProjectMember.objects.get(project_id=project_id, workspace__slug=slug, pk=pk)
+        except ProjectMember.DoesNotExist:
+            return Response(
+                {"error": "Project member not found"},
+                status=status.HTTP_404_NOT_FOUND,
+            )
         serializer = ProjectMemberSerializer(project_member, data=request.data, partial=True, context={"slug": slug})

233-233: Missing error handling for ProjectMember.objects.get().

If the project member doesn't exist, this will raise ProjectMember.DoesNotExist, resulting in a 500 error instead of a proper 204 response for an idempotent delete or 404 for clarity.

Apply this diff:

     def delete(self, request, slug, project_id, pk):
-        project_member = ProjectMember.objects.get(project_id=project_id, workspace__slug=slug, pk=pk)
+        try:
+            project_member = ProjectMember.objects.get(project_id=project_id, workspace__slug=slug, pk=pk)
+        except ProjectMember.DoesNotExist:
+            return Response(
+                {"error": "Project member not found"},
+                status=status.HTTP_404_NOT_FOUND,
+            )
         project_member.is_active = False
         project_member.save()
🧹 Nitpick comments (1)
apps/api/plane/api/views/member.py (1)

91-91: Redundant permission_classes declaration.

Line 91 sets permission_classes but is immediately overridden by the get_permissions() method (lines 94-97). The declaration on line 91 has no effect.

Apply this diff:

 class ProjectMemberListCreateAPIEndpoint(BaseAPIView):
-    permission_classes = [ProjectMemberPermission]
     use_read_replica = True
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 78f8c33 and 5275e21.

📒 Files selected for processing (3)
  • apps/api/plane/api/serializers/member.py (1 hunks)
  • apps/api/plane/api/views/issue.py (4 hunks)
  • apps/api/plane/api/views/member.py (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/api/plane/api/views/issue.py
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: NarayanBavisetti
Repo: makeplane/plane PR: 7460
File: apps/api/plane/app/serializers/draft.py:112-122
Timestamp: 2025-07-23T18:18:06.875Z
Learning: In the Plane codebase serializers, workspace_id is not consistently passed in serializer context, so parent issue validation in DraftIssueCreateSerializer only checks project_id rather than both workspace_id and project_id. The existing project member authentication system already validates that users can only access projects they belong to, providing sufficient security without risking breaking functionality by adding workspace_id validation where the context might not be available.
📚 Learning: 2025-07-23T18:18:06.875Z
Learnt from: NarayanBavisetti
Repo: makeplane/plane PR: 7460
File: apps/api/plane/app/serializers/draft.py:112-122
Timestamp: 2025-07-23T18:18:06.875Z
Learning: In the Plane codebase serializers, workspace_id is not consistently passed in serializer context, so parent issue validation in DraftIssueCreateSerializer only checks project_id rather than both workspace_id and project_id. The existing project member authentication system already validates that users can only access projects they belong to, providing sufficient security without risking breaking functionality by adding workspace_id validation where the context might not be available.

Applied to files:

  • apps/api/plane/api/serializers/member.py
🧬 Code graph analysis (2)
apps/api/plane/api/serializers/member.py (5)
apps/api/plane/db/models/project.py (1)
  • ProjectMember (192-236)
apps/api/plane/db/models/workspace.py (1)
  • WorkspaceMember (194-224)
apps/api/plane/db/models/user.py (1)
  • User (38-164)
apps/api/plane/api/views/member.py (2)
  • get (65-87)
  • get (117-137)
apps/api/plane/api/serializers/invite.py (2)
  • validate_role (44-47)
  • Meta (17-35)
apps/api/plane/api/views/member.py (6)
apps/api/plane/api/serializers/user.py (1)
  • UserLiteSerializer (9-34)
apps/api/plane/api/serializers/member.py (1)
  • ProjectMemberSerializer (11-39)
apps/api/plane/db/models/workspace.py (3)
  • Workspace (115-178)
  • WorkspaceMember (194-224)
  • save (188-191)
apps/api/plane/db/models/project.py (4)
  • ProjectMember (192-236)
  • save (157-159)
  • save (169-171)
  • save (208-218)
apps/api/plane/utils/permissions/project.py (2)
  • ProjectMemberPermission (52-78)
  • ProjectAdminPermission (115-126)
apps/api/plane/utils/permissions/workspace.py (1)
  • WorkSpaceAdminPermission (57-67)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (2)
apps/api/plane/api/serializers/member.py (1)

31-39: LGTM!

The role validation correctly restricts values to valid ROLE constants, and the Meta configuration properly defines the serializer fields.

apps/api/plane/api/views/member.py (1)

139-152: LGTM!

The POST implementation correctly uses ProjectMemberSerializer with workspace slug in context for validation, and passes project_id to save() for proper association.

…dpoint

- Removed the `get_object` method from `ProjectMemberDetailAPIEndpoint` as it was redundant.
- Deleted the `post` method for creating project members, streamlining the endpoint to focus on retrieval and updates only.
- Updated the endpoint documentation to reflect the changes in functionality.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
apps/api/plane/api/views/member.py (2)

186-187: Verify error handling approach for .get() calls.

While BaseAPIView.handle_exception() automatically converts DoesNotExist exceptions to 404 responses, explicit error handling would make the code clearer and allow custom error messages.

Consider using get_object_or_404() or try-except blocks:

from django.shortcuts import get_object_or_404

# Option 1: Using get_object_or_404
project_member = get_object_or_404(
    ProjectMember, 
    project_id=project_id, 
    workspace__slug=slug, 
    pk=pk
)
user = get_object_or_404(User, id=project_member.member_id)

# Option 2: Explicit try-except
try:
    project_member = ProjectMember.objects.get(
        project_id=project_id, workspace__slug=slug, pk=pk
    )
    user = User.objects.get(id=project_member.member_id)
except ProjectMember.DoesNotExist:
    return Response(
        {"error": "Project member not found"},
        status=status.HTTP_404_NOT_FOUND,
    )
except User.DoesNotExist:
    return Response(
        {"error": "User not found"},
        status=status.HTTP_404_NOT_FOUND,
    )

165-165: Fix OpenAPI response schema mismatch.

The schema declares response=ProjectMemberSerializer but the implementation returns UserLiteSerializer data (lines 186-189). This creates incorrect API documentation for consumers.

Apply this diff:

-            200: OpenApiResponse(description="Project member", response=ProjectMemberSerializer),
+            200: OpenApiResponse(description="Project member", response=UserLiteSerializer),
🧹 Nitpick comments (3)
apps/api/plane/api/views/member.py (3)

156-156: Reconsider inheritance structure.

Inheriting from ProjectMemberListCreateAPIEndpoint causes the detail endpoint to inherit the post() method (lines 148-152), which is inappropriate for a detail endpoint that should only handle retrieve/update/delete operations.

Consider either:

  1. Extracting shared logic to a base class and having both list-create and detail endpoints inherit from it
  2. Having ProjectMemberDetailAPIEndpoint inherit from BaseAPIView and duplicate get_permissions()

179-183: Consider extracting workspace validation.

The workspace existence check is duplicated from the list GET method (lines 124-128) and missing from PATCH and DELETE methods. Consider extracting to a helper method for consistency.

Example:

def _validate_workspace(self, slug):
    """Validate workspace exists, return 400 if not."""
    if not Workspace.objects.filter(slug=slug).exists():
        return Response(
            {"error": "Provided workspace does not exist"},
            status=status.HTTP_400_BAD_REQUEST,
        )
    return None

def get(self, request, slug, project_id, pk):
    error_response = self._validate_workspace(slug)
    if error_response:
        return error_response
    # ... rest of method

200-205: Add workspace validation for consistency.

The PATCH method lacks the workspace existence check that's present in the GET method (lines 179-183). This creates inconsistent validation across endpoints.

Consider adding the same workspace check at the beginning of the method, or better yet, extract it to a helper method as suggested in the previous comment.

Also, the same error handling concerns from line 186-187 apply to line 201.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5275e21 and f0e9ac4.

📒 Files selected for processing (1)
  • apps/api/plane/api/views/member.py (3 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: NarayanBavisetti
Repo: makeplane/plane PR: 7460
File: apps/api/plane/app/serializers/draft.py:112-122
Timestamp: 2025-07-23T18:18:06.875Z
Learning: In the Plane codebase serializers, workspace_id is not consistently passed in serializer context, so parent issue validation in DraftIssueCreateSerializer only checks project_id rather than both workspace_id and project_id. The existing project member authentication system already validates that users can only access projects they belong to, providing sufficient security without risking breaking functionality by adding workspace_id validation where the context might not be available.
📚 Learning: 2025-07-23T18:18:06.875Z
Learnt from: NarayanBavisetti
Repo: makeplane/plane PR: 7460
File: apps/api/plane/app/serializers/draft.py:112-122
Timestamp: 2025-07-23T18:18:06.875Z
Learning: In the Plane codebase serializers, workspace_id is not consistently passed in serializer context, so parent issue validation in DraftIssueCreateSerializer only checks project_id rather than both workspace_id and project_id. The existing project member authentication system already validates that users can only access projects they belong to, providing sufficient security without risking breaking functionality by adding workspace_id validation where the context might not be available.

Applied to files:

  • apps/api/plane/api/views/member.py
🧬 Code graph analysis (1)
apps/api/plane/api/views/member.py (7)
apps/api/plane/api/views/base.py (3)
  • BaseAPIView (48-162)
  • project_id (146-152)
  • project_id (261-267)
apps/api/plane/api/serializers/user.py (1)
  • UserLiteSerializer (9-34)
apps/api/plane/api/serializers/member.py (1)
  • ProjectMemberSerializer (11-39)
apps/api/plane/db/models/workspace.py (3)
  • Workspace (115-178)
  • WorkspaceMember (194-224)
  • save (188-191)
apps/api/plane/db/models/project.py (4)
  • ProjectMember (192-236)
  • save (157-159)
  • save (169-171)
  • save (208-218)
apps/api/plane/utils/permissions/project.py (2)
  • ProjectMemberPermission (52-78)
  • ProjectAdminPermission (115-126)
apps/api/plane/utils/permissions/workspace.py (1)
  • WorkSpaceAdminPermission (57-67)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (3)
apps/api/plane/api/views/member.py (3)

7-7: LGTM: Import additions are correct.

The new imports (OpenApiRequest, ProjectMemberSerializer, ProjectAdminPermission) are appropriately used throughout the file for request schema documentation, member serialization, and permission enforcement.

Also applies to: 12-12, 14-14


94-97: LGTM: Proper method-level permission control.

The dynamic permission selection correctly restricts write operations to project admins while allowing project members to view the member list.


139-152: LGTM: Project member creation is properly implemented.

The endpoint correctly validates workspace membership via the serializer context and associates the new member with the specified project.

Comment on lines +216 to +219
project_member = ProjectMember.objects.get(project_id=project_id, workspace__slug=slug, pk=pk)
project_member.is_active = False
project_member.save()
return Response(status=status.HTTP_204_NO_CONTENT)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify soft delete implementation.

The code sets is_active = False but doesn't set the deleted_at field. Based on the ProjectMember model's unique constraint (condition=Q(deleted_at__isnull=True)), deleted_at is the primary soft-delete marker. Only setting is_active=False may prevent re-adding the same member later.

Consider setting deleted_at for proper soft delete:

from django.utils import timezone

def delete(self, request, slug, project_id, pk):
    project_member = ProjectMember.objects.get(project_id=project_id, workspace__slug=slug, pk=pk)
    project_member.is_active = False
    project_member.deleted_at = timezone.now()
    project_member.save()
    return Response(status=status.HTTP_204_NO_CONTENT)

Also, this method lacks the workspace validation present in the GET method (lines 179-183), and has the same error handling concerns as line 186.


I'll verify the concerns raised in this review comment by examining the ProjectMember model, checking soft delete patterns in the codebase, and validating the workspace validation issue.


---

🏁 Script executed:

```shell
#!/bin/bash
# Find ProjectMember model definition
find . -type f -name "*.py" | xargs grep -l "class ProjectMember" | head -5

Length of output: 212


🏁 Script executed:

#!/bin/bash
# Search for deleted_at field in ProjectMember or related base classes
rg -nA 20 "class ProjectMember" --type=py | head -60

Length of output: 4927


🏁 Script executed:

#!/bin/bash
# Check the member.py file to understand the GET method and error handling
rg -nB 5 -A 10 "def retrieve\(self, request" apps/api/plane/api/views/member.py

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look for other soft delete patterns in the codebase
rg -nP "\.delete_at\s*=" --type=py | head -20

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Find BaseModel to understand deleted_at field
find . -type f -name "*.py" -path "*/models/*" | xargs grep -l "class BaseModel" | head -5

Length of output: 34


🏁 Script executed:

#!/bin/bash
# Check the DELETE method in member.py with full context
rg -nB 15 -A 5 "def delete\(self, request, slug, project_id, pk\)" apps/api/plane/api/views/member.py

Length of output: 1118


🏁 Script executed:

#!/bin/bash
# Check BaseModel definition for deleted_at field
rg -nA 30 "class BaseModel" apps/api/plane/db/models/base.py | head -50

Length of output: 1275


🏁 Script executed:

#!/bin/bash
# Get the full ProjectMember model definition including constraints
rg -nA 50 "class ProjectMember\(ProjectBaseModel\)" apps/api/plane/db/models/project.py

Length of output: 2261


🏁 Script executed:

#!/bin/bash
# Look for unique constraint in ProjectMember Meta class
rg -nA 10 "class ProjectMember" apps/api/plane/db/models/project.py | grep -A 10 "class Meta"

Length of output: 377


🏁 Script executed:

#!/bin/bash
# Search for deleted_at references in project.py model
rg -n "deleted_at" apps/api/plane/db/models/project.py

Length of output: 1020


🏁 Script executed:

#!/bin/bash
# Check the full member.py view file to find GET/retrieve method
rg -nB 3 -A 15 "def get\(self, request, slug, project_id, pk\)" apps/api/plane/api/views/member.py

Length of output: 846


🏁 Script executed:

#!/bin/bash
# Look for patterns where is_active is used as primary soft delete marker
rg -nP "is_active\s*=\s*False" apps/api/plane/db/models/project.py | head -10

Length of output: 0



---

🏁 Script executed:

```shell
#!/bin/bash
# Check ProjectBaseModel definition
rg -nA 20 "class ProjectBaseModel" apps/api/plane/db/models/project.py | head -40

Length of output: 892


🏁 Script executed:

#!/bin/bash
# Check AuditModel for deleted_at field
rg -nA 20 "class AuditModel" apps/api/plane/db/models/base.py

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Verify soft delete patterns - check if other soft deletes set deleted_at
rg -nB 2 -A 2 "\.deleted_at\s*=" --type=py | head -40

Length of output: 3209


🏁 Script executed:

#!/bin/bash
# Check if there are other delete methods in views that show the pattern
rg -nB 2 -A 6 "def delete\(self, request" apps/api/plane/api/views/ | head -50

Length of output: 3532



---

🏁 Script executed:

```shell
#!/bin/bash
# Search for deleted_at field definition in the entire codebase
rg -n "deleted_at\s*=" apps/api/plane/db/models/base.py

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Get full AuditModel class from base.py
cat apps/api/plane/db/models/base.py | head -100

Length of output: 1350


🏁 Script executed:

#!/bin/bash
# Confirm the issue: Check if ProjectMember inherits deleted_at from parent
rg -nA 5 "class.*AuditModel" apps/api/plane/db/models/base.py

Length of output: 196


🏁 Script executed:

#!/bin/bash
# Search for any imports or field definitions around deleted_at
rg -nB 5 -A 5 "deleted_at" apps/api/plane/db/models/base.py | head -30

Length of output: 0



---

🏁 Script executed:

```shell
#!/bin/bash
# Find AuditModel in mixins
find . -type f -name "*.py" -path "*/mixins*" | xargs grep -l "class AuditModel" | head -5

Length of output: 29


🏁 Script executed:

#!/bin/bash
# Get AuditModel definition
rg -nA 15 "class AuditModel" --type=py | head -40

Length of output: 326


🏁 Script executed:

#!/bin/bash
# Double-check by looking for where deleted_at field is actually defined
rg -n "deleted_at.*models\." --type=py | head -10

Length of output: 678


Set deleted_at when soft-deleting project members.

The code sets only is_active = False but omits deleted_at, which is the primary soft-delete marker in this codebase. The ProjectMember model's unique constraint (condition=Q(deleted_at__isnull=True)) only prevents duplicates when deleted_at is null, so this incomplete soft delete prevents re-adding the same member later. This pattern is established throughout the codebase (see attachment.py:141, deletion_task.py:68, asset/v2.py:32 where deleted_at = timezone.now() is set).

Additionally, the DELETE method at line 216 lacks the workspace existence check present in the GET method (line 179), and both methods use .get() without error handling.

Suggested fix:

from django.utils import timezone

def delete(self, request, slug, project_id, pk):
    # Check if the workspace exists (consistent with GET method)
    if not Workspace.objects.filter(slug=slug).exists():
        return Response(
            {"error": "Provided workspace does not exist"},
            status=status.HTTP_400_BAD_REQUEST,
        )
    
    project_member = ProjectMember.objects.get(project_id=project_id, workspace__slug=slug, pk=pk)
    project_member.is_active = False
    project_member.deleted_at = timezone.now()
    project_member.save()
    return Response(status=status.HTTP_204_NO_CONTENT)

- Introduced new URL paths for managing project members, including a dedicated endpoint for listing and creating project members.
- Added a detail endpoint for specific project members to enhance retrieval and update capabilities.
- Updated existing URL patterns to maintain consistency and clarity in member management.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f0e9ac4 and f823a89.

📒 Files selected for processing (1)
  • apps/api/plane/api/urls/member.py (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: NarayanBavisetti
Repo: makeplane/plane PR: 7460
File: apps/api/plane/app/serializers/draft.py:112-122
Timestamp: 2025-07-23T18:18:06.875Z
Learning: In the Plane codebase serializers, workspace_id is not consistently passed in serializer context, so parent issue validation in DraftIssueCreateSerializer only checks project_id rather than both workspace_id and project_id. The existing project member authentication system already validates that users can only access projects they belong to, providing sufficient security without risking breaking functionality by adding workspace_id validation where the context might not be available.
🧬 Code graph analysis (1)
apps/api/plane/api/urls/member.py (1)
apps/api/plane/api/views/member.py (3)
  • ProjectMemberListCreateAPIEndpoint (90-152)
  • ProjectMemberDetailAPIEndpoint (156-219)
  • WorkspaceMemberAPIEndpoint (27-87)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (2)
apps/api/plane/api/urls/member.py (2)

3-3: LGTM: Import statement correctly updated.

The imports now properly reference the split endpoints (ProjectMemberListCreateAPIEndpoint and ProjectMemberDetailAPIEndpoint), which aligns with the endpoint separation in the URL patterns below.


7-16: LGTM: Past review feedback addressed.

The duplicate URL pattern name issue from the previous review has been correctly fixed—the collection route uses "project-members" (plural) and the detail route uses "project-member" (singular). The addition of the "get" method to the detail endpoint (line 14) also addresses the earlier request to retrieve a single record.

Comment on lines +17 to +26
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-members/",
ProjectMemberListCreateAPIEndpoint.as_view(http_method_names=["get", "post"]),
name="project-members",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-members/<uuid:pk>/",
ProjectMemberDetailAPIEndpoint.as_view(http_method_names=["patch", "delete", "get"]),
name="project-member",
),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Duplicate URL pattern names and redundant routes.

These routes introduce two critical issues:

  1. Duplicate URL pattern names: The names "project-members" (line 20) and "project-member" (line 25) conflict with the names at lines 10 and 15. Django's reverse() function will not be able to resolve these names unambiguously.

  2. Redundant API endpoints: The /project-members/ routes (lines 17-26) appear functionally identical to the /members/ routes (lines 7-16)—they use the same view classes, HTTP methods, and provide the same functionality. Having two different URL paths for the same resource violates RESTful API design principles and creates confusion for API consumers.

Questions:

  • Is there a specific reason to maintain both /members/ and /project-members/ endpoints?
  • If this is for backward compatibility during a transition period, it should be documented and one set should eventually be deprecated.
  • If only one endpoint is needed, which one should be retained?

If both endpoints are truly required, apply this diff to fix the naming conflict:

     path(
         "workspaces/<str:slug>/projects/<uuid:project_id>/project-members/",
         ProjectMemberListCreateAPIEndpoint.as_view(http_method_names=["get", "post"]),
-        name="project-members",
+        name="project-members-alt",
     ),
     path(
         "workspaces/<str:slug>/projects/<uuid:project_id>/project-members/<uuid:pk>/",
         ProjectMemberDetailAPIEndpoint.as_view(http_method_names=["patch", "delete", "get"]),
-        name="project-member",
+        name="project-member-alt",
     ),

However, the recommended solution is to remove the duplicate routes entirely unless there is a documented justification for maintaining both.


I'll verify the concerns in the review comment by examining the full URL configuration and checking for references throughout the codebase.


---

🏁 Script executed:

```shell
#!/bin/bash
# Display the complete member.py file to see full context
cat apps/api/plane/api/urls/member.py

Length of output: 1225


🏁 Script executed:

#!/bin/bash
# Search for reverse() calls using "project-members" or "project-member" names
rg -n "reverse\(" -A2 -B2 --type=py | grep -E "(project-members|project-member)" -A2 -B2

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search for any references to the URL names to understand usage
rg -nP '"project-members"|\'project-members\'|"project-member"|\'project-member\'' --type=py

Length of output: 68


Let me search more broadly for references and test usage:


---

🏁 Script executed:

```shell
#!/bin/bash
# Search for test files that might reference these project member endpoints
fd -e test.py -e tests.py --type f | head -20

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search for any reverse calls in Python files to see patterns
rg -n 'reverse\s*\(' --type=py -A1 | head -40

Length of output: 2834


🏁 Script executed:

#!/bin/bash
# Search for project-members string anywhere (broader search)
rg 'project-members' --type=py --type=json

Length of output: 634


🏁 Script executed:

#!/bin/bash
# Check for TODO/FIXME comments in the member.py context
rg -n 'TODO|FIXME' apps/api/plane/api/urls/member.py

Length of output: 0


Remove duplicate URL pattern names and resolve redundant routes.

The codebase has confirmed duplicate URL pattern names in member.py:

  • Lines 10 and 20: both define name="project-members"
  • Lines 15 and 25: both define name="project-member"

Django's reverse() function cannot resolve duplicate names unambiguously. A test file comment in test_project_app.py (line 19-20) explicitly acknowledges this problem: "unreliable due to duplicate 'name' values in URL patterns."

Additionally, both /members/ and /project-members/ route pairs use identical views and HTTP methods, creating redundant API endpoints for the same resource.

Fix required:
Either:

  1. Remove one set of routes entirely (recommended), or
  2. Rename the second pair to name="project-members-alt" and name="project-member-alt" as a temporary measure

Determine which path (/members/ or /project-members/) should be the canonical endpoint and remove or deprecate the other.

🤖 Prompt for AI Agents
In apps/api/plane/api/urls/member.py around lines 17 to 26 there are duplicate
URL pattern names and redundant routes for the same views; remove the duplicate
route pair that uses the /members/ paths (keep /project-members/ as the
canonical endpoints), delete the duplicate path entries and their imports/usages
so only one set of routes remains, and ensure the remaining patterns use unique
names ("project-members" and "project-member"); update any references
(reverse(), tests) to point to the canonical names if needed.

@pushya22 pushya22 merged commit 3c6f24d into preview Nov 4, 2025
6 of 7 checks passed
@pushya22 pushya22 deleted the member-invite-apis branch November 4, 2025 09:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants