Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
134 changes: 134 additions & 0 deletions apps/api/plane/api/serializers/page.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# 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
if self.instance:
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")
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("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="pages",
),
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