Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/api/plane/api/serializers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,4 @@
ProjectMemberLiteAPISerializer,
)
from .sticky import StickySerializer
from .page import PageSerializer, PageDetailSerializer
139 changes: 139 additions & 0 deletions apps/api/plane/api/serializers/page.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.

# Django imports
from django.db import transaction

# Third party imports
from rest_framework import serializers

# Module imports
from .base import BaseSerializer
from plane.db.models import Page, Project, ProjectPage
from plane.utils.content_validator import validate_html_content


class PageSerializer(BaseSerializer):
"""
Serializer for pages with metadata fields.

Handles page metadata (name, access, color, parent, lock and archive
state) for list responses. Page content is exposed through
PageDetailSerializer.
"""

class Meta:
model = Page
fields = [
"id",
"name",
"access",
"color",
"parent",
"is_locked",
"archived_at",
"workspace",
"view_props",
"logo_props",
"external_id",
"external_source",
"owned_by",
"created_at",
"updated_at",
"created_by",
"updated_by",
]
read_only_fields = [
"id",
"workspace",
"owned_by",
"archived_at",
"created_at",
"updated_at",
"created_by",
"updated_by",
]
extra_kwargs = {"name": {"required": True, "allow_blank": False}}
Comment thread
RemoYukoff marked this conversation as resolved.

def validate_parent(self, value):
if value is None:
return value

# Reject the page itself and any of its descendants to avoid cycles;
# track visited ids so a pre-existing cycle in the chain cannot loop forever
if self.instance:
visited = set()
ancestor = value
while ancestor is not None:
if ancestor.id == self.instance.id:
raise serializers.ValidationError("Parent page cannot be the page itself or one of its descendants")
if ancestor.id in visited:
raise serializers.ValidationError("Parent page hierarchy contains a cycle")
visited.add(ancestor.id)
ancestor = ancestor.parent

# The parent page must belong to the same project
project_id = self.context.get("project_id") or (
self.instance and self.instance.project_pages.values_list("project_id", flat=True).first()
)
if (
project_id
and not ProjectPage.objects.filter(
page_id=value.id, project_id=project_id, deleted_at__isnull=True
).exists()
):
raise serializers.ValidationError("Parent page must belong to the same project")
return value
Comment thread
coderabbitai[bot] marked this conversation as resolved.


class PageDetailSerializer(PageSerializer):
"""
Extended page serializer including HTML content.

Provides the full page representation with description_html for
create, retrieve and update operations.
"""

description_html = serializers.CharField(required=False, allow_blank=True)

class Meta(PageSerializer.Meta):
fields = PageSerializer.Meta.fields + ["description_html"]

def validate_description_html(self, value):
# Validate and sanitize the HTML content for security
if value:
is_valid, error_msg, sanitized_html = validate_html_content(value)
if not is_valid:
raise serializers.ValidationError(error_msg or "html content is not valid")
if sanitized_html is not None:
return sanitized_html
return value

def create(self, validated_data):
project_id = self.context["project_id"]
owned_by_id = self.context["owned_by_id"]

# Get the workspace id from the project
project = Project.objects.get(pk=project_id)

with transaction.atomic():
# Create the page
page = Page.objects.create(
**validated_data,
owned_by_id=owned_by_id,
created_by_id=owned_by_id,
updated_by_id=owned_by_id,
workspace_id=project.workspace_id,
)

# Create the project page
ProjectPage.objects.create(
workspace_id=page.workspace_id,
project_id=project_id,
page_id=page.id,
created_by_id=page.created_by_id,
updated_by_id=page.updated_by_id,
)

return page
Comment thread
coderabbitai[bot] marked this conversation as resolved.
2 changes: 2 additions & 0 deletions apps/api/plane/api/urls/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from .label import urlpatterns as label_patterns
from .member import urlpatterns as member_patterns
from .module import urlpatterns as module_patterns
from .page import urlpatterns as page_patterns
from .project import urlpatterns as project_patterns
from .state import urlpatterns as state_patterns
from .user import urlpatterns as user_patterns
Expand All @@ -22,6 +23,7 @@
*label_patterns,
*member_patterns,
*module_patterns,
*page_patterns,
*project_patterns,
*state_patterns,
*user_patterns,
Expand Down
29 changes: 29 additions & 0 deletions apps/api/plane/api/urls/page.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.

from django.urls import path

from plane.api.views.page import (
PageListCreateAPIEndpoint,
PageDetailAPIEndpoint,
PageArchiveUnarchiveAPIEndpoint,
)

urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/",
PageListCreateAPIEndpoint.as_view(http_method_names=["get", "post"]),
name="pages",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/",
PageDetailAPIEndpoint.as_view(http_method_names=["get", "patch", "delete"]),
name="page-detail",
),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/archive/",
PageArchiveUnarchiveAPIEndpoint.as_view(http_method_names=["post", "delete"]),
name="page-archive-unarchive",
),
]
6 changes: 6 additions & 0 deletions apps/api/plane/api/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,9 @@
from .invite import WorkspaceInvitationsViewset

from .sticky import StickyViewSet

from .page import (
PageListCreateAPIEndpoint,
PageDetailAPIEndpoint,
PageArchiveUnarchiveAPIEndpoint,
)
Loading