Skip to content

feat(api): add Pages CRUD endpoints to v1 API#8800

Open
dvdcastro wants to merge 2 commits intomakeplane:previewfrom
dvdcastro:feature/pages-api-v1-endpoints
Open

feat(api): add Pages CRUD endpoints to v1 API#8800
dvdcastro wants to merge 2 commits intomakeplane:previewfrom
dvdcastro:feature/pages-api-v1-endpoints

Conversation

@dvdcastro
Copy link

@dvdcastro dvdcastro commented Mar 26, 2026

Summary

Adds GET, POST, PATCH, and DELETE endpoints for project pages to the public v1 API.

Endpoints added

Method Endpoint Description
GET /api/v1/workspaces/{slug}/projects/{project_id}/pages/ List project pages
POST /api/v1/workspaces/{slug}/projects/{project_id}/pages/ Create a page
GET /api/v1/workspaces/{slug}/projects/{project_id}/pages/{page_id}/ Get a page
PATCH /api/v1/workspaces/{slug}/projects/{project_id}/pages/{page_id}/ Update a page
DELETE /api/v1/workspaces/{slug}/projects/{project_id}/pages/{page_id}/ Delete a page

Context

The page management logic already existed in plane.app; this PR exposes it through the public v1 API with proper permissions (ProjectPagePermission), serialization (PageAPISerializer), and drf-spectacular documentation annotations.

Locked pages return 400 on PATCH. Only the page owner can DELETE.

Tests

Unit tests added in apps/api/plane/tests/contract/api/test_pages.py covering all endpoints, authentication, locked page guard, and owner-only delete.

Closes #7319

Summary by CodeRabbit

  • New Features

    • Pages API: create, list, view, update, and delete project pages inside a workspace; includes ownership, access control, archival, locking, timestamps, and view/logo properties.
  • Tests

    • Added contract tests covering Pages API: authentication, creation/validation, listing, detail, update (including locked-page rejection), and deletion behaviors.

Adds GET, POST, PATCH, DELETE endpoints for project pages under:
  /api/v1/workspaces/{slug}/projects/{project_id}/pages/
  /api/v1/workspaces/{slug}/projects/{project_id}/pages/{page_id}/

The page management logic already existed in plane.app; this PR
exposes it through the public v1 API with proper permissions,
serialization, and drf-spectacular documentation.

Closes makeplane#7319
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 26, 2026

📝 Walkthrough

Walkthrough

Adds Page CRUD API: a new PageAPISerializer, two view endpoints (PageListCreateAPIEndpoint, PageDetailAPIEndpoint) with permission checks and project scoping, URL routes for list/detail, and contract tests covering list, create, retrieve, update (with lock validation), and delete behaviors.

Changes

Cohort / File(s) Summary
Serializers
apps/api/plane/api/serializers/page.py, apps/api/plane/api/serializers/__init__.py
New PageAPISerializer (requires name, binds to Page, exposes page attributes, marks workspace/owner/timestamps as read-only). Export added in package init.
Views
apps/api/plane/api/views/page.py, apps/api/plane/api/views/__init__.py
New class-based endpoints: PageListCreateAPIEndpoint (GET list with access/favorite annotations, POST creates Page + ProjectPage) and PageDetailAPIEndpoint (GET/PATCH with lock validation/partial updates, DELETE owner-only). Views re-exported in package init.
URLs
apps/api/plane/api/urls/page.py, apps/api/plane/api/urls/__init__.py
New URL patterns for pages: list/create at workspaces/<str:slug>/projects/<uuid:project_id>/pages/ and detail at .../pages/<uuid:page_id>/; integrated into top-level urlpatterns.
Tests
apps/api/plane/tests/contract/api/test_pages.py
New contract tests with fixtures for project, page, locked page. Coverage for unauthenticated/authenticated list, create (validation and non-existent project), retrieve (404), patch (including locked rejection), and delete (owner checks).

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Endpoint as PageListCreateAPIEndpoint
    participant DetailEP as PageDetailAPIEndpoint
    participant Serializer as PageAPISerializer
    participant ORM as ORM/Models
    participant DB as Database

    rect rgba(100,150,200,0.5)
    Note over Client,DB: POST - Create Page
    Client->>Endpoint: POST /workspaces/{slug}/projects/{project_id}/pages/ {name, description_html}
    Endpoint->>ORM: lookup Project by project_id & workspace
    ORM-->>Endpoint: Project found / 404
    Endpoint->>Serializer: validate input
    Serializer-->>Endpoint: validated data
    Endpoint->>ORM: create Page (description_binary=None)
    ORM->>DB: INSERT Page
    ORM-->>Endpoint: Page instance
    Endpoint->>ORM: create ProjectPage linkage (created_by/updated_by=user)
    ORM->>DB: INSERT ProjectPage
    Endpoint-->>Client: 201 Created {page}
    end

    rect rgba(150,100,200,0.5)
    Note over Client,DB: GET - List Pages
    Client->>Endpoint: GET /workspaces/{slug}/projects/{project_id}/pages/
    Endpoint->>ORM: query Pages (workspace, project mapping Exists, access/owned_by filters, not archived)
    ORM->>ORM: annotate is_favorite (Exists subquery)
    ORM->>DB: SELECT with prefetch/select_related
    DB-->>ORM: Page rows
    ORM-->>Endpoint: queryset
    Endpoint-->>Client: 200 OK [pages...]
    end

    rect rgba(200,150,100,0.5)
    Note over Client,DB: PATCH / DELETE - Detail Operations
    Client->>DetailEP: PATCH/DELETE /workspaces/{slug}/projects/{project_id}/pages/{page_id}/
    DetailEP->>ORM: lookup Page by id with access filters
    ORM->>DB: SELECT Page
    DB-->>ORM: Page or 404
    alt PATCH and Page.is_locked
        DetailEP-->>Client: 400 Bad Request
    else PATCH valid
        DetailEP->>Serializer: validate partial data
        Serializer-->>DetailEP: validated data
        DetailEP->>ORM: update Page (reset description_binary if description_html present) with updated_by=user
        ORM->>DB: UPDATE Page
        DetailEP-->>Client: 200 OK {page}
    else DELETE and owner
        DetailEP->>ORM: delete Page
        ORM->>DB: DELETE
        DetailEP-->>Client: 204 No Content
    end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~35 minutes

Poem

🐰 I hopped through code to stitch a trail,

Pages now answer when you send a mail.
Create, patch, fetch, and let old pages fall—
I nibble bugs and guard the call. ✨📚

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and concisely describes the main change: adding CRUD endpoints for Pages to the v1 API, which is the primary objective of this changeset.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering all key sections including summary, endpoints table, context, implementation details, tests, and issue closure reference.
Linked Issues check ✅ Passed All coding requirements from issue #7319 are fully met: POST endpoint for creating pages, PATCH for updates, GET endpoints for listing/retrieving, and DELETE for removal, with proper permissions and serialization.
Out of Scope Changes check ✅ Passed All code changes are directly scoped to adding Pages CRUD API endpoints as specified in the linked issue; no unrelated modifications were introduced.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@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: 4

🧹 Nitpick comments (2)
apps/api/plane/tests/contract/api/test_pages.py (2)

104-117: Assert the ProjectPage link on create.

This endpoint is project-scoped, but the test only proves that a Page row exists. If the through record is skipped or attached to the wrong project, this contract still passes.

Suggested assertion
         assert response.status_code == status.HTTP_201_CREATED
         assert response.data["name"] == "My New Page"
         assert Page.objects.filter(name="My New Page").exists()
+        assert ProjectPage.objects.filter(
+            project=project,
+            page__name="My New Page",
+        ).exists()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/plane/tests/contract/api/test_pages.py` around lines 104 - 117, The
test_create_page currently only asserts that a Page row exists; ensure it also
verifies the project-scoped link by asserting a ProjectPage through-record
exists and is associated with the created Page and the correct Project: after
calling api_key_client.post(self.get_url(workspace.slug, project.id), ...) and
checking response.data and Page, query ProjectPage (or the through model used to
link Page to Project) for a record with page_id equal to the created page id (or
response.data["id"]) and project_id equal to project.id, and assert it exists
and/or has expected fields to guarantee the page was attached to the intended
project.

187-203: Add the authenticated non-owner delete case.

Owner-only delete is part of the contract, but the suite only covers owner success and anonymous 401. A logged-in second user with project access should assert the 403 path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/plane/tests/contract/api/test_pages.py` around lines 187 - 203, Add
a new test that asserts an authenticated non-owner with project access gets 403
when deleting a page: create a second user (or use an existing test fixture for
a non-owner client), ensure this user has access to the workspace/project (but
is not the page owner), call the same delete endpoint used in
test_delete_page_by_owner using that non-owner authenticated client, assert
response.status_code == status.HTTP_403_FORBIDDEN and that
Page.objects.filter(id=page.id).exists() remains True; name the test something
like test_delete_page_by_non_owner_returns_403 to match the existing naming.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/api/plane/api/serializers/page.py`:
- Around line 33-50: The serializer currently exposes "is_locked" as writable
which lets non-owners lock other users' public pages; update the page serializer
in apps/api/plane/api/serializers/page.py (the serializer that defines
read_only_fields for pages) to make "is_locked" read-only by adding "is_locked"
to the read_only_fields list (or otherwise remove it from the writable fields),
ensuring lock state can only be changed via owner/admin-only flows (e.g., the
PageDetailAPIEndpoint or a dedicated lock/unlock endpoint).

In `@apps/api/plane/api/views/page.py`:
- Around line 95-113: The Page and ProjectPage creations must be done atomically
to avoid orphan Pages if the ProjectPage insert fails; wrap the
Page.objects.create and ProjectPage.objects.create calls inside a single
transaction (use django.db.transaction.atomic) so both are committed or both
rolled back, and keep references to the same page instance (created by
Page.objects.create) when creating the ProjectPage; update the view function
that calls Page.objects.create and ProjectPage.objects.create to use
transaction.atomic() and ensure any exceptions propagate so the transaction will
roll back.
- Around line 48-49: The query currently filters out nested pages by calling
.filter(parent__isnull=True); remove that filter so the view's queryset no
longer restricts results to root pages (keep the permission filters that use
Q(owned_by=self.request.user) | Q(access=0) intact) so GET /pages/ returns all
pages within the scope instead of only top-level ones; if you need project
scoping, ensure a project filter (e.g., parent__project or project field) is
applied elsewhere rather than parent__isnull.
- Around line 95-103: The Page.objects.create(...) call is passing a
non-existent field "description" which will raise a FieldError; remove the
description=serializer.validated_data.get("description", {}) entry from the
Page.objects.create invocation (located where Page.objects.create is called in
page view) so the create call only includes valid model fields (name,
description_html, description_stripped, access, color, view_props, logo_props,
etc.).

---

Nitpick comments:
In `@apps/api/plane/tests/contract/api/test_pages.py`:
- Around line 104-117: The test_create_page currently only asserts that a Page
row exists; ensure it also verifies the project-scoped link by asserting a
ProjectPage through-record exists and is associated with the created Page and
the correct Project: after calling
api_key_client.post(self.get_url(workspace.slug, project.id), ...) and checking
response.data and Page, query ProjectPage (or the through model used to link
Page to Project) for a record with page_id equal to the created page id (or
response.data["id"]) and project_id equal to project.id, and assert it exists
and/or has expected fields to guarantee the page was attached to the intended
project.
- Around line 187-203: Add a new test that asserts an authenticated non-owner
with project access gets 403 when deleting a page: create a second user (or use
an existing test fixture for a non-owner client), ensure this user has access to
the workspace/project (but is not the page owner), call the same delete endpoint
used in test_delete_page_by_owner using that non-owner authenticated client,
assert response.status_code == status.HTTP_403_FORBIDDEN and that
Page.objects.filter(id=page.id).exists() remains True; name the test something
like test_delete_page_by_non_owner_returns_403 to match the existing naming.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 360a9fd9-d62c-4e09-8a67-36e6f72b6bf0

📥 Commits

Reviewing files that changed from the base of the PR and between d94a269 and 009b65c.

📒 Files selected for processing (7)
  • apps/api/plane/api/serializers/__init__.py
  • apps/api/plane/api/serializers/page.py
  • apps/api/plane/api/urls/__init__.py
  • apps/api/plane/api/urls/page.py
  • apps/api/plane/api/views/__init__.py
  • apps/api/plane/api/views/page.py
  • apps/api/plane/tests/contract/api/test_pages.py

Comment on lines +33 to +50
"is_locked",
"is_global",
"archived_at",
"workspace",
"created_at",
"updated_at",
"created_by",
"updated_by",
"view_props",
"logo_props",
]
read_only_fields = [
"workspace",
"owned_by",
"created_by",
"updated_by",
"archived_at",
]
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 | 🟠 Major

is_locked is too permissive on this PATCH surface.

ProjectPagePermission allows members to PATCH public pages, and PageDetailAPIEndpoint.patch() only blocks edits when the page is already locked. Leaving is_locked writable here lets one member lock another user's public page and freeze future edits through this API. Please make is_locked read-only here or move lock toggles behind a stricter owner/admin-only flow.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/plane/api/serializers/page.py` around lines 33 - 50, The serializer
currently exposes "is_locked" as writable which lets non-owners lock other
users' public pages; update the page serializer in
apps/api/plane/api/serializers/page.py (the serializer that defines
read_only_fields for pages) to make "is_locked" read-only by adding "is_locked"
to the read_only_fields list (or otherwise remove it from the writable fields),
ensuring lock state can only be changed via owner/admin-only flows (e.g., the
PageDetailAPIEndpoint or a dedicated lock/unlock endpoint).

Comment on lines +48 to +49
.filter(parent__isnull=True)
.filter(Q(owned_by=self.request.user) | Q(access=0))
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 | 🟠 Major

GET /pages/ currently hides child pages.

The parent__isnull=True filter on Line 48 makes this route return only root pages, even though the endpoint/docs describe a project pages listing. Nested pages become undiscoverable unless the caller already knows their IDs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/plane/api/views/page.py` around lines 48 - 49, The query currently
filters out nested pages by calling .filter(parent__isnull=True); remove that
filter so the view's queryset no longer restricts results to root pages (keep
the permission filters that use Q(owned_by=self.request.user) | Q(access=0)
intact) so GET /pages/ returns all pages within the scope instead of only
top-level ones; if you need project scoping, ensure a project filter (e.g.,
parent__project or project field) is applied elsewhere rather than
parent__isnull.

Comment on lines +95 to +113
page = Page.objects.create(
name=serializer.validated_data.get("name", ""),
description_html=serializer.validated_data.get("description_html", ""),
description_stripped=serializer.validated_data.get("description_stripped", ""),
description=serializer.validated_data.get("description", {}),
access=serializer.validated_data.get("access", 0),
color=serializer.validated_data.get("color", ""),
view_props=serializer.validated_data.get("view_props", {}),
logo_props=serializer.validated_data.get("logo_props", {}),
owned_by=request.user,
workspace_id=project.workspace_id,
)
ProjectPage.objects.create(
workspace_id=page.workspace_id,
project_id=project_id,
page_id=page.id,
created_by=request.user,
updated_by=request.user,
)
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 | 🟠 Major

Create the page and its project link atomically.

Page and ProjectPage are both required for a successful POST, but they are written independently here. If the second insert fails, this leaves behind an orphaned Page that the project-scoped API cannot reach anymore.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/plane/api/views/page.py` around lines 95 - 113, The Page and
ProjectPage creations must be done atomically to avoid orphan Pages if the
ProjectPage insert fails; wrap the Page.objects.create and
ProjectPage.objects.create calls inside a single transaction (use
django.db.transaction.atomic) so both are committed or both rolled back, and
keep references to the same page instance (created by Page.objects.create) when
creating the ProjectPage; update the view function that calls
Page.objects.create and ProjectPage.objects.create to use transaction.atomic()
and ensure any exceptions propagate so the transaction will roll back.

Comment on lines +95 to +103
page = Page.objects.create(
name=serializer.validated_data.get("name", ""),
description_html=serializer.validated_data.get("description_html", ""),
description_stripped=serializer.validated_data.get("description_stripped", ""),
description=serializer.validated_data.get("description", {}),
access=serializer.validated_data.get("access", 0),
color=serializer.validated_data.get("color", ""),
view_props=serializer.validated_data.get("view_props", {}),
logo_props=serializer.validated_data.get("logo_props", {}),
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

🏁 Script executed:

# First, let's find and examine the PageAPISerializer
fd -t f "serializer" apps/api/plane/api | grep -i page | head -20

Repository: makeplane/plane

Length of output: 41


🏁 Script executed:

# Also check the Page model directly
fd -t f page.py apps/api/plane/db/models/ -x cat {}

Repository: makeplane/plane

Length of output: 7324


🏁 Script executed:

# Let's search for PageAPISerializer definition
rg "class PageAPISerializer" -A 30 apps/api/

Repository: makeplane/plane

Length of output: 2081


🏁 Script executed:

# Get the page.py view file and check lines 95-103
rg "class.*PageAPI" apps/api/plane/api/views/page.py -A 100 | head -120

Repository: makeplane/plane

Length of output: 4573


🏁 Script executed:

# Alternative: just get the file and read around line 95
cat -n apps/api/plane/api/views/page.py | sed -n '90,110p'

Repository: makeplane/plane

Length of output: 1237


Remove description parameter from Page.objects.create() call.

Line 99 passes description=serializer.validated_data.get("description", {}) to Page.objects.create(), but the Page model has no "description" field. The PageAPISerializer also does not validate a "description" field. This will raise a FieldError at runtime instead of returning a 201 response.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/plane/api/views/page.py` around lines 95 - 103, The
Page.objects.create(...) call is passing a non-existent field "description"
which will raise a FieldError; remove the
description=serializer.validated_data.get("description", {}) entry from the
Page.objects.create invocation (located where Page.objects.create is called in
page view) so the create call only includes valid model fields (name,
description_html, description_stripped, access, color, view_props, logo_props,
etc.).

When pages are created or updated via the v1 API, description_binary
was left untouched, causing the Tiptap/Yjs collaborative editor to
serve stale binary state instead of the new description_html content.

Fix: explicitly set description_binary=None whenever description_html
is written, so the collab server (plane-live) reloads the document
from the updated HTML on the next editor session.

Affects: PageListCreateAPIEndpoint.post, PageDetailAPIEndpoint.patch
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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/api/plane/api/views/page.py`:
- Around line 78-85: The POST handler currently fetches Project via
Project.objects.get(pk=project_id, workspace__slug=slug) but doesn't prevent
creating pages on archived projects; after retrieving the Project in post,
verify the archive flag (e.g., project.archived or project.is_archived) or
change the query to exclude archived projects (e.g., add archived=False) and
return an appropriate Response (403 Forbidden with an error message like
"Project is archived") when the project is archived so page creation is
rejected.
- Around line 125-134: get_page currently returns pages without excluding those
in archived projects, unlike get_queryset which filters with
projects__archived_at__isnull=True; update the Page query in get_page to include
the same archived-project filter (add projects__archived_at__isnull=True) so
GET/PATCH/DELETE respect archived status, keeping the existing Q(owned_by=...) |
Q(access=0) logic and other filters in get_page.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5e621226-aa3f-4b95-96e8-e97a0af1e622

📥 Commits

Reviewing files that changed from the base of the PR and between 009b65c and fa95826.

📒 Files selected for processing (1)
  • apps/api/plane/api/views/page.py

Comment on lines +78 to +85
def post(self, request, slug, project_id):
try:
project = Project.objects.get(pk=project_id, workspace__slug=slug)
except Project.DoesNotExist:
return Response(
{"error": "Project not found."},
status=status.HTTP_404_NOT_FOUND,
)
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 | 🟠 Major

POST allows page creation in archived projects.

The project lookup on line 80 doesn't verify that the project isn't archived. Users could create pages in archived projects, which contradicts the archive semantics.

🛡️ Proposed fix to reject archived projects
     def post(self, request, slug, project_id):
         try:
-            project = Project.objects.get(pk=project_id, workspace__slug=slug)
+            project = Project.objects.get(
+                pk=project_id,
+                workspace__slug=slug,
+                archived_at__isnull=True,
+            )
         except Project.DoesNotExist:
             return Response(
                 {"error": "Project not found."},
                 status=status.HTTP_404_NOT_FOUND,
             )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/plane/api/views/page.py` around lines 78 - 85, The POST handler
currently fetches Project via Project.objects.get(pk=project_id,
workspace__slug=slug) but doesn't prevent creating pages on archived projects;
after retrieving the Project in post, verify the archive flag (e.g.,
project.archived or project.is_archived) or change the query to exclude archived
projects (e.g., add archived=False) and return an appropriate Response (403
Forbidden with an error message like "Project is archived") when the project is
archived so page creation is rejected.

Comment on lines +125 to +134
def get_page(self, slug, project_id, page_id):
return (
Page.objects.filter(
workspace__slug=slug,
projects__id=project_id,
pk=page_id,
)
.filter(Q(owned_by=self.request.user) | Q(access=0))
.first()
)
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 | 🟠 Major

Detail operations can access pages in archived projects.

Unlike get_queryset() which filters by projects__archived_at__isnull=True (line 42), get_page() omits this check. This allows GET, PATCH, and DELETE operations on pages belonging to archived projects, which is inconsistent with the list endpoint behavior.

🛡️ Proposed fix to add archived project filter
     def get_page(self, slug, project_id, page_id):
         return (
             Page.objects.filter(
                 workspace__slug=slug,
                 projects__id=project_id,
                 pk=page_id,
             )
+            .filter(projects__archived_at__isnull=True)
             .filter(Q(owned_by=self.request.user) | Q(access=0))
             .first()
         )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/plane/api/views/page.py` around lines 125 - 134, get_page currently
returns pages without excluding those in archived projects, unlike get_queryset
which filters with projects__archived_at__isnull=True; update the Page query in
get_page to include the same archived-project filter (add
projects__archived_at__isnull=True) so GET/PATCH/DELETE respect archived status,
keeping the existing Q(owned_by=...) | Q(access=0) logic and other filters in
get_page.

@CLAassistant
Copy link

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

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.

[feature]: Add API Endpoints for Creating and Editing Pages

2 participants