diff --git a/README.md b/README.md
index acb47f3..d5eb73f 100644
--- a/README.md
+++ b/README.md
@@ -304,7 +304,7 @@ disallow/limit public access, or at least implement proper caching.
|:----------------------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `/api/languages/` | Fetch available languages. |
| `/api/plugins/` | Fetch types for all installed plugins. Used for automatic type checks with frontend frameworks. |
-| `/api/{language}/pages-root/` | Fetch the root page for a given language. |
+| `/api/{language}/pages/` | Fetch the root page for a given language. |
| `/api/{language}/pages-tree/` | Fetch the complete page tree of all published documents for a given language. Suitable for smaller projects for automatic navigation generation. For large page sets, use the `pages-list` endpoint instead. |
| `/api/{language}/pages-list/` | Fetch a paginated list. Supports `limit` and `offset` parameters for frontend structure building. |
| `/api/{language}/pages/{path}/` | Fetch page details by path for a given language. Path and language information is available via `pages-list` and `pages-tree` endpoints. |
@@ -317,14 +317,7 @@ preview content.
To determine permissions `user_can_view_page()` from djangocms is used, usually editors with
`is_staff` are allowed to view draft content.
-| Private Endpoints | Description |
-|:-----------------------------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------|
-| `/api/preview/{language}/pages-root` | Fetch the latest draft content for the root page. |
-| `/api/preview/{language}/pages-tree` | Fetch the page tree including unpublished pages. |
-| `/api/preview/{language}/pages-list` | Fetch a paginated list including unpublished pages. |
-| `/api/preview/{language}/pages/{path}` | Fetch the latest draft content from a published or unpublished page, including latest unpublished content objects. |
-| `/api/preview/{language}/placeholders/`
`{content_type_id}/{object_id}/{slot}` | Fetch the latest draft content objects for the given language. |
-| |
+Just add the `?preview` GET parameter to the above page, page-tree, or page-list endpoints.
### Sample API-Response: api/{en}/pages/{sub}/
diff --git a/djangocms_rest/cms_config.py b/djangocms_rest/cms_config.py
index bdd3b25..74529d5 100644
--- a/djangocms_rest/cms_config.py
+++ b/djangocms_rest/cms_config.py
@@ -4,8 +4,10 @@
from django.urls import NoReverseMatch, reverse
from cms.app_base import CMSAppConfig
-from cms.models import Page
+from cms.cms_menus import CMSMenu
+from cms.models import Page, PageContent
from cms.utils.i18n import force_language, get_current_language
+from menus import base
try:
@@ -42,6 +44,55 @@ def get_file_api_endpoint(file):
return file.url if file.is_public else None
+def patch_get_menu_node_for_page_content(method: callable) -> callable:
+ def inner(self, page_content: PageContent, *args, **kwargs):
+ node = method(self, page_content, *args, **kwargs)
+ node.api_endpoint = get_page_api_endpoint(
+ page_content.page,
+ page_content.language,
+ )
+ return node
+
+ return inner
+
+
+def patch_page_menu(menu: type[CMSMenu]):
+ """Patch the CMSMenu to use the REST API endpoint for pages."""
+ if hasattr(menu, "get_menu_node_for_page_content"):
+ menu.get_menu_node_for_page_content = patch_get_menu_node_for_page_content(
+ menu.get_menu_node_for_page_content
+ )
+
+
+class NavigationNodeMixin:
+ """Mixin to add API endpoint and selection logic to NavigationNode."""
+
+ def get_api_endpoint(self):
+ """Get the API endpoint for the navigation node."""
+ return self.api_endpoint
+
+ def is_selected(self, request):
+ """Check if the navigation node is selected."""
+ return (
+ self.api_endpoint == request.api_endpoint
+ if hasattr(request, "api_endpoint")
+ else super().is_selected(request)
+ )
+
+
+class NavigationNodeWithAPI(NavigationNodeMixin, base.NavigationNode):
+ # NavigationNodeWithAPI must be defined statically at the module level
+ # to allow it being pickled for cache
+ pass
+
+
+def add_api_endpoint(navigation_node: type[base.NavigationNode]):
+ """Add an API endpoint to the CMSNavigationNode."""
+ if not issubclass(navigation_node, NavigationNodeMixin):
+ navigation_node = NavigationNodeWithAPI
+ return navigation_node
+
+
class RESTToolbarMixin:
"""
Mixin to add REST rendering capabilities to the CMS toolbar.
@@ -73,3 +124,6 @@ class RESTCMSConfig(CMSAppConfig):
Page.add_to_class("get_api_endpoint", get_page_api_endpoint)
File.add_to_class("get_api_endpoint", get_file_api_endpoint) if File else None
+
+ base.NavigationNode = add_api_endpoint(base.NavigationNode)
+ patch_page_menu(CMSMenu)
diff --git a/djangocms_rest/serializers/menus.py b/djangocms_rest/serializers/menus.py
new file mode 100644
index 0000000..e8dcc1d
--- /dev/null
+++ b/djangocms_rest/serializers/menus.py
@@ -0,0 +1,49 @@
+from rest_framework import serializers
+
+from menus.base import NavigationNode
+
+from djangocms_rest.utils import get_absolute_frontend_url
+
+
+class NavigationNodeSerializer(serializers.Serializer):
+ namespace = serializers.CharField(allow_null=True)
+ title = serializers.CharField()
+ url = serializers.URLField(allow_null=True)
+ api_endpoint = serializers.URLField(allow_null=True)
+ visible = serializers.BooleanField()
+ selected = serializers.BooleanField()
+ attr = serializers.DictField(allow_null=True)
+ level = serializers.IntegerField(allow_null=True)
+ children = serializers.SerializerMethodField()
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.request = self.context.get("request")
+
+ def get_children(self, obj: NavigationNode) -> list[dict]:
+ # Assuming obj.children is a list of NavigationNode-like objects
+ serializer = NavigationNodeSerializer(
+ obj.children or [], many=True, context=self.context
+ )
+ return serializer.data
+
+ def to_representation(self, obj: NavigationNode) -> dict:
+ """Customize the base representation of the NavigationNode."""
+ return {
+ "title": obj.title,
+ "url": get_absolute_frontend_url(self.request, obj.url),
+ "api_endpoint": get_absolute_frontend_url(
+ self.request, getattr(obj, "api_endpoint", None)
+ ),
+ "visible": obj.visible,
+ "selected": obj.selected
+ or obj.attr.get("is_home", False)
+ and getattr(self.request, "is_home", False),
+ "attr": obj.attr,
+ "level": obj.level,
+ "children": self.get_children(obj),
+ }
+
+
+class NavigationNodeListSerializer(serializers.ListSerializer):
+ child = NavigationNodeSerializer()
diff --git a/djangocms_rest/serializers/pages.py b/djangocms_rest/serializers/pages.py
index 19d405c..f616e47 100644
--- a/djangocms_rest/serializers/pages.py
+++ b/djangocms_rest/serializers/pages.py
@@ -34,13 +34,13 @@ class BasePageSerializer(serializers.Serializer):
changed_date = serializers.DateTimeField()
-class PreviewMixin:
- """Mixin to mark content as preview"""
-
- is_preview = True
-
-
class BasePageContentMixin:
+ @property
+ def is_preview(self):
+ return "preview" in self.request.GET and self.request.GET.get(
+ "preview", ""
+ ).lower() not in ("0", "false")
+
def get_base_representation(self, page_content: PageContent) -> dict:
request = getattr(self, "request", None)
path = page_content.page.get_path(page_content.language)
@@ -150,7 +150,7 @@ def to_representation(self, page_content: PageContent) -> dict:
]
placeholders = [
placeholder
- for placeholder in page_content.page.get_placeholders(page_content.language)
+ for placeholder in page_content.placeholders.all()
if placeholder.slot in declared_slots
]
@@ -173,35 +173,6 @@ def to_representation(self, page_content: PageContent) -> dict:
return data
-class PreviewPageContentSerializer(PageContentSerializer, PreviewMixin):
- """Serializer specifically for preview/draft page content"""
-
- placeholders = PlaceholderRelationSerializer(many=True, required=False)
-
- def to_representation(self, page_content: PageContent) -> dict:
- # Get placeholders directly from the page_content
- # This avoids the extra query to get_declared_placeholders
- placeholders = page_content.placeholders.all()
-
- placeholders_data = [
- {
- "content_type_id": placeholder.content_type_id,
- "object_id": placeholder.object_id,
- "slot": placeholder.slot,
- }
- for placeholder in placeholders
- ]
-
- data = self.get_base_representation(page_content)
- data["placeholders"] = PlaceholderRelationSerializer(
- placeholders_data,
- language=page_content.language,
- context={"request": self.request},
- many=True,
- ).data
- return data
-
-
class PageListSerializer(BasePageSerializer, BasePageContentMixin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
diff --git a/djangocms_rest/urls.py b/djangocms_rest/urls.py
index cba7bdc..c0bf82e 100644
--- a/djangocms_rest/urls.py
+++ b/djangocms_rest/urls.py
@@ -21,7 +21,7 @@
name="page-list",
),
path(
- "/pages-root/",
+ "/pages/",
views.PageDetailView.as_view(),
name="page-root",
),
@@ -36,30 +36,81 @@
name="placeholder-detail",
),
path("plugins/", views.PluginDefinitionView.as_view(), name="plugin-list"),
- # Preview content endpoints
+ # Menu endpoints
+ path("/menu/", views.MenuView.as_view(), name="menu"),
path(
- "preview//pages-root/",
- views.PreviewPageView.as_view(),
- name="preview-page-root",
+ "/menu/////",
+ views.MenuView.as_view(),
+ name="menu",
),
path(
- "preview//pages-tree/",
- views.PreviewPageTreeListView.as_view(),
- name="preview-page-tree-list",
+ "/menu//////",
+ views.MenuView.as_view(),
+ name="menu",
),
path(
- "preview//pages-list/",
- views.PreviewPageListView.as_view(),
- name="preview-page-list",
+ "/menu///////",
+ views.MenuView.as_view(),
+ name="menu",
),
path(
- "preview//pages//",
- views.PreviewPageView.as_view(),
- name="preview-page",
+ "/submenu/////",
+ views.SubMenuView.as_view(),
+ name="submenu",
),
path(
- "preview//placeholders////",
- views.PreviewPlaceholderDetailView.as_view(),
- name="preview-placeholder-detail",
+ "/submenu////",
+ views.SubMenuView.as_view(),
+ name="submenu",
+ ),
+ path(
+ "/submenu////",
+ views.SubMenuView.as_view(),
+ name="submenu",
+ ),
+ path(
+ "/submenu///",
+ views.SubMenuView.as_view(),
+ name="submenu",
+ ),
+ path(
+ "/submenu///",
+ views.SubMenuView.as_view(),
+ name="submenu",
+ ),
+ path(
+ "/submenu//",
+ views.SubMenuView.as_view(),
+ name="submenu",
+ ),
+ path(
+ "/submenu//",
+ views.SubMenuView.as_view(),
+ name="submenu",
+ ),
+ path(
+ "/submenu/",
+ views.SubMenuView.as_view(),
+ name="submenu",
+ ),
+ path(
+ "/breadcrumbs///",
+ views.BreadcrumbView.as_view(),
+ name="breadcrumbs",
+ ),
+ path(
+ "/breadcrumbs//",
+ views.BreadcrumbView.as_view(),
+ name="breadcrumbs",
+ ),
+ path(
+ "/breadcrumbs//",
+ views.BreadcrumbView.as_view(),
+ name="breadcrumbs",
+ ),
+ path(
+ "/breadcrumbs/",
+ views.BreadcrumbView.as_view(),
+ name="breadcrumbs",
),
]
diff --git a/djangocms_rest/utils.py b/djangocms_rest/utils.py
index f2d6aef..0c8ff2f 100644
--- a/djangocms_rest/utils.py
+++ b/djangocms_rest/utils.py
@@ -45,8 +45,12 @@ def get_absolute_frontend_url(request: Request, path: str) -> str:
Returns:
An absolute URL formatted as a string.
"""
+ if path is None:
+ return None
protocol = getattr(request, "scheme", "http")
- domain = getattr(request, "get_host", lambda: Site.objects.get_current().domain)()
+ domain = getattr(
+ request, "get_host", lambda: Site.objects.get_current(request).domain
+ )()
if not path.startswith("/"):
path = f"/{path}"
return f"{protocol}://{domain}{path}"
diff --git a/djangocms_rest/views.py b/djangocms_rest/views.py
index 3763307..b3f54c0 100644
--- a/djangocms_rest/views.py
+++ b/djangocms_rest/views.py
@@ -1,27 +1,35 @@
+from __future__ import annotations
+
+from typing import Any
from django.contrib.sites.shortcuts import get_current_site
+from django.urls import reverse
from django.utils.functional import lazy
from cms.models import Page, PageContent, Placeholder
from cms.utils.conf import get_languages
from cms.utils.page_permissions import user_can_view_page
+from menus.templatetags.menu_tags import ShowBreadcrumb, ShowMenu, ShowSubMenu
+
from rest_framework.exceptions import NotFound
from rest_framework.pagination import LimitOffsetPagination
-from rest_framework.permissions import IsAdminUser
from rest_framework.request import Request
from rest_framework.response import Response
from djangocms_rest.permissions import CanViewPage, IsAllowedPublicLanguage
from djangocms_rest.serializers.languages import LanguageSerializer
+from djangocms_rest.serializers.menus import NavigationNodeSerializer
from djangocms_rest.serializers.pages import (
PageContentSerializer,
PageListSerializer,
PageMetaSerializer,
- PreviewPageContentSerializer,
)
from djangocms_rest.serializers.placeholders import PlaceholderSerializer
from djangocms_rest.serializers.plugins import PluginDefinitionSerializer
-from djangocms_rest.utils import get_object, get_site_filtered_queryset
+from djangocms_rest.utils import (
+ get_object,
+ get_site_filtered_queryset,
+)
from djangocms_rest.views_base import BaseAPIView, BaseListAPIView
@@ -76,7 +84,6 @@ class PageListView(BaseListAPIView):
permission_classes = [IsAllowedPublicLanguage]
serializer_class = PageListSerializer
pagination_class = LimitOffsetPagination
- content_getter = "get_content_obj"
def get_queryset(self):
"""Get queryset of pages for the given language."""
@@ -103,7 +110,6 @@ def get_queryset(self):
class PageTreeListView(BaseAPIView):
permission_classes = [IsAllowedPublicLanguage]
serializer_class = PageMetaSerializer
- content_getter = "get_content_obj"
def get(self, request, language):
"""List of all pages on this site for a given language."""
@@ -135,7 +141,6 @@ def get(self, request, language):
class PageDetailView(BaseAPIView):
permission_classes = [IsAllowedPublicLanguage, CanViewPage]
serializer_class = PageContentSerializer
- content_getter = "get_content_obj"
def get(self, request: Request, language: str, path: str = "") -> Response:
"""Retrieve a page instance. The page instance includes the placeholders and
@@ -159,7 +164,6 @@ def get(self, request: Request, language: str, path: str = "") -> Response:
class PlaceholderDetailView(BaseAPIView):
permission_classes = [IsAllowedPublicLanguage]
serializer_class = PlaceholderSerializer
- content_manager = "objects"
@extend_placeholder_schema
def get(
@@ -190,8 +194,9 @@ def get(
raise NotFound()
source_model = placeholder.content_type.model_class()
+ content_manager = "admin_manager" if self._preview_requested() else "content"
source = (
- getattr(source_model, self.content_manager, source_model.objects)
+ getattr(source_model, content_manager, source_model.objects)
.filter(pk=placeholder.object_id)
.first()
)
@@ -214,11 +219,6 @@ def get(
return Response(serializer.data)
-class PreviewPlaceholderDetailView(PlaceholderDetailView):
- content_manager = "admin_manager"
- permission_classes = [IsAdminUser]
-
-
class PluginDefinitionView(BaseAPIView):
"""
API view for retrieving plugin definitions
@@ -241,19 +241,89 @@ def get(self, request: Request) -> Response:
return Response(definitions)
-class PreviewPageView(PageDetailView):
- content_getter = "get_admin_content"
- serializer_class = PreviewPageContentSerializer
- permission_classes = [IsAdminUser, CanViewPage]
+class MenuView(BaseAPIView):
+ permission_classes = [IsAllowedPublicLanguage]
+ serializer_class = NavigationNodeSerializer
+ tag = ShowMenu
+ return_key = "children"
-class PreviewPageTreeListView(PageTreeListView):
- content_getter = "get_admin_content"
- serializer_class = PageMetaSerializer
- permission_classes = [IsAdminUser, CanViewPage]
+ def get(
+ self,
+ request: Request,
+ language: str,
+ path: str = "", # for menu-root endpoint
+ **kwargs: dict[str, Any],
+ ) -> Response:
+ """Get the menu structure for a specific language and path."""
+ self.populate_defaults(kwargs)
+ menu = self.get_menu_structure(request, language, path, **kwargs)
+ serializer = self.serializer_class(
+ menu, many=True, context={"request": request}
+ )
+ return Response(serializer.data)
+
+ def populate_defaults(self, kwargs: dict[str, Any]) -> None:
+ """Set default values for menu view parameters."""
+ kwargs.setdefault("from_level", 0)
+ kwargs.setdefault("to_level", 100)
+ kwargs.setdefault("extra_inactive", 0)
+ kwargs.setdefault("extra_active", 1000)
+ kwargs.setdefault("root_id", None)
+ kwargs.setdefault("namespace", None)
+ kwargs.setdefault("next_page", None)
+
+ def get_menu_structure(
+ self,
+ request: Request,
+ language: str,
+ path: str,
+ **kwargs: dict[str, Any],
+ ) -> list[dict[str, Any]]:
+ """Get the menu structure for a specific language and path."""
+ # Implement the logic to retrieve the menu structure
+ # Create tag instance without calling __init__
+ tag_instance = self.tag.__new__(self.tag)
-class PreviewPageListView(PageListView):
- content_getter = "get_admin_content"
- serializer_class = PageMetaSerializer
- permission_classes = [IsAdminUser, CanViewPage]
+ # Initialize minimal necessary attributes
+ tag_instance.kwargs = {}
+ tag_instance.blocks = {}
+
+ if path == "":
+ api_endpoint = reverse("page-root", kwargs={"language": language})
+ else:
+ api_endpoint = reverse(
+ "page-detail", kwargs={"language": language, "path": path}
+ )
+
+ request.api_endpoint = api_endpoint
+ request.LANGUAGE_CODE = language
+ request.current_page = get_object(self.site, path)
+ self.check_object_permissions(request, request.current_page)
+ context = {"request": request}
+
+ context = tag_instance.get_context(
+ context=context,
+ **kwargs,
+ template=None,
+ )
+ return context.get(self.return_key, [])
+
+
+class SubMenuView(MenuView):
+ tag = ShowSubMenu
+
+ def populate_defaults(self, kwargs: dict[str, Any]) -> None:
+ kwargs.setdefault("levels", 100)
+ kwargs.setdefault("root_level", None)
+ kwargs.setdefault("nephews", 100)
+
+
+class BreadcrumbView(MenuView):
+ tag = ShowBreadcrumb
+ return_key = "ancestors"
+
+ def populate_defaults(self, kwargs: dict[str, Any]) -> None:
+ kwargs.setdefault("start_level", 0)
+ kwargs.setdefault("only_visible", True)
diff --git a/djangocms_rest/views_base.py b/djangocms_rest/views_base.py
index 30bf512..b86e706 100644
--- a/djangocms_rest/views_base.py
+++ b/djangocms_rest/views_base.py
@@ -1,12 +1,13 @@
from django.contrib.sites.shortcuts import get_current_site
from django.utils.functional import cached_property
from rest_framework.generics import ListAPIView
+from rest_framework.permissions import IsAdminUser
from rest_framework.views import APIView
-class BaseAPIView(APIView):
+class BaseAPIMixin:
"""
- This is a base class for all API views. It sets the allowed methods to GET and OPTIONS.
+ This mixin provides common functionality for all API views.
"""
http_method_names = ("get", "options")
@@ -19,16 +20,36 @@ def site(self):
site = getattr(self.request, "site", None)
return site if site is not None else get_current_site(self.request)
+ def _preview_requested(self):
+ return "preview" in self.request.GET and self.request.GET.get(
+ "preview", ""
+ ).lower() not in ("0", "false")
+
+ @property
+ def content_getter(self):
+ if self._preview_requested():
+ return "get_admin_content"
+ return "get_content_obj"
+
+ def get_permissions(self):
+ permissions = super().get_permissions()
+ if self._preview_requested():
+ # Require admin access for preview as first check
+ permissions.insert(0, IsAdminUser())
+ return permissions
-class BaseListAPIView(ListAPIView):
+
+class BaseAPIView(BaseAPIMixin, APIView):
+ """
+ This is a base class for all API views. It sets the allowed methods to GET and OPTIONS.
+ """
+
+ pass
+
+
+class BaseListAPIView(BaseAPIMixin, ListAPIView):
"""
This is a base class for all list API views. It supports default pagination and sets the allowed methods to GET and OPTIONS.
"""
- @cached_property
- def site(self):
- """
- Fetch and cache the current site and make it available to all views.
- """
- site = getattr(self.request, "site", None)
- return site if site is not None else get_current_site(self.request)
+ pass
diff --git a/pytest.ini b/pytest.ini
index b3df23a..81635c7 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -1,3 +1,4 @@
[pytest]
DJANGO_SETTINGS_MODULE = tests.settings
python_files = tests/test_*.py tests/*/test_*.py
+pythonpath = .
diff --git a/tests/base.py b/tests/base.py
index f20f87d..cc718aa 100644
--- a/tests/base.py
+++ b/tests/base.py
@@ -21,7 +21,13 @@ def _create_pages(
is_first: bool = True,
):
new_pages = [
- create_page(f"page {i}", language="en", template="INHERIT", parent=parent)
+ create_page(
+ f"page {i}",
+ language="en",
+ template="INHERIT",
+ parent=parent,
+ in_navigation=True,
+ )
for i in range(page_list if isinstance(page_list, int) else len(page_list))
]
diff --git a/tests/endpoints/test_menu.py b/tests/endpoints/test_menu.py
new file mode 100644
index 0000000..624dfd5
--- /dev/null
+++ b/tests/endpoints/test_menu.py
@@ -0,0 +1,147 @@
+from django.contrib.sites.models import Site
+from rest_framework.reverse import reverse
+
+from tests.base import BaseCMSRestTestCase
+
+from cms.models import Page
+
+
+class PageListAPITestCase(BaseCMSRestTestCase):
+ def test_get_menu_no_children(self):
+ """
+ Test the menu endpoint (/api/{language}/menu/).
+
+ Verifies:
+ - Endpoint returns correct HTTP status code
+ - Response contains paginated structure
+ - All pages contain required fields
+ - All fields have correct data types
+ - Pagination metadata is present
+ - Invalid language code returns 404
+ """
+
+ # Get current site
+ site = Site.objects.get_current()
+ expected_length = Page.objects.filter(site=site, parent=None).count()
+
+ # GET
+ url = reverse(
+ "menu",
+ kwargs={
+ "language": "en",
+ "from_level": 0,
+ "to_level": 0,
+ "extra_inactive": 0,
+ "extra_active": 100,
+ },
+ )
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 200)
+ results = response.json()
+
+ # Number of results:
+ self.assertEqual(len(results), expected_length)
+
+ # Page titles:
+ self.assertEqual(results[0]["title"], "page 0")
+ self.assertEqual(results[1]["title"], "page 1")
+ self.assertEqual(results[2]["title"], "page 2")
+
+ # No children:
+ self.assertEqual(results[0]["children"], [])
+ self.assertEqual(results[1]["children"], [])
+ self.assertEqual(results[2]["children"], [])
+
+ # Selected: Root page
+ self.assertTrue(results[0]["selected"])
+ self.assertFalse(results[1]["selected"])
+ self.assertFalse(results[2]["selected"])
+
+ def test_get_menu_with_children(self):
+ """
+ Test the menu endpoint (/api/{language}/menu/{path}/) with child pages.
+
+ Verifies:
+ - Child pages are included in the response
+ - Child pages have correct titles and structure
+ """
+
+ # GET
+ url = reverse(
+ "menu",
+ kwargs={
+ "language": "en",
+ "path": "page-2",
+ "from_level": 0,
+ "to_level": 100,
+ "extra_inactive": 0,
+ "extra_active": 100,
+ },
+ )
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 200)
+
+ results = response.json()
+
+ # Check if the child page is included
+ self.assertIn("children", results[0])
+ self.assertEqual(len(results[2]["children"]), 2)
+ self.assertEqual(results[2]["children"][0]["title"], "page 0")
+
+ def test_default_levels(self):
+ url1 = reverse(
+ "menu",
+ kwargs={"language": "en"},
+ )
+ url2 = reverse(
+ "menu",
+ kwargs={
+ "language": "en",
+ "from_level": 0,
+ "to_level": 100,
+ "extra_inactive": 0,
+ "extra_active": 1000,
+ },
+ )
+ results1 = self.client.get(url1).json()
+ results2 = self.client.get(url2).json()
+
+ self.assertNotEqual(url1, url2)
+ self.assertEqual(results1, results2)
+
+ def test_submenu(self):
+ # GET
+ url = reverse(
+ "submenu",
+ kwargs={
+ "language": "en",
+ "path": "page-2",
+ },
+ )
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 200)
+ results = response.json()
+
+ self.assertEqual(len(results), 2)
+
+ self.assertEqual(results[0]["title"], "page 0")
+ self.assertEqual(results[1]["title"], "page 1")
+
+ def test_breadcrumbs(self):
+ # GET
+ url = reverse(
+ "breadcrumbs",
+ kwargs={
+ "language": "en",
+ "path": "page-2/page-0",
+ },
+ )
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 200)
+
+ results = response.json()
+ self.assertEqual(len(results), 3)
+
+ self.assertEqual(results[0]["title"], "page 0")
+ self.assertEqual(results[1]["title"], "page 2")
+ self.assertEqual(results[2]["title"], "page 0")
diff --git a/tests/endpoints/test_page_list.py b/tests/endpoints/test_page_list.py
index 54a9d18..5719b95 100644
--- a/tests/endpoints/test_page_list.py
+++ b/tests/endpoints/test_page_list.py
@@ -61,12 +61,12 @@ def test_get_paginated_list(self):
# GET PREVIEW
response = self.client.get(
- reverse("preview-page-list", kwargs={"language": "en"})
+ reverse("page-list", kwargs={"language": "en"}) + "?preview"
)
self.assertEqual(response.status_code, 403)
response = self.client.get(
- reverse("preview-page-list", kwargs={"language": "xx"})
+ reverse("page-list", kwargs={"language": "xx"}) + "?preview"
)
self.assertEqual(response.status_code, 403)
@@ -74,6 +74,6 @@ def test_get_paginated_list(self):
def test_get_protected(self):
self.client.force_login(self.user)
response = self.client.get(
- reverse("preview-page-list", kwargs={"language": "en"})
+ reverse("page-list", kwargs={"language": "en"}) + "?preview"
)
self.assertEqual(response.status_code, 200)
diff --git a/tests/endpoints/test_page_root.py b/tests/endpoints/test_page_root.py
index 5f92fb0..a507a1f 100644
--- a/tests/endpoints/test_page_root.py
+++ b/tests/endpoints/test_page_root.py
@@ -8,7 +8,7 @@
class PageRootAPITestCase(BaseCMSRestTestCase):
def test_get(self):
"""
- Test the page root endpoint ('/api/{language}/pages-root/').
+ Test the page root endpoint ('/api/{language}/pages/').
Verifies:
- Endpoint returns correct HTTP status code
@@ -26,6 +26,15 @@ def test_get(self):
response = self.client.get(reverse("page-root", kwargs={"language": "en"}))
self.assertEqual(response.status_code, 200)
page = response.json()
+ self.assertFalse(response.json().get("is_preview"))
+
+ # GET with ?preview=false
+ response = self.client.get(
+ reverse("page-root", kwargs={"language": "en"}) + "?preview=false"
+ )
+ self.assertEqual(response.status_code, 200)
+ page = response.json()
+ self.assertFalse(response.json().get("is_preview"))
# Data & Type Validation
for field, expected_type in type_checks.items():
@@ -45,12 +54,12 @@ def test_get(self):
# GET PREVIEW
response = self.client.get(
- reverse("preview-page-root", kwargs={"language": "en"})
+ reverse("page-root", kwargs={"language": "en"}) + "?preview"
)
self.assertEqual(response.status_code, 403)
response = self.client.get(
- reverse("preview-page-root", kwargs={"language": "xx"})
+ reverse("page-root", kwargs={"language": "xx"}) + "?preview"
)
self.assertEqual(response.status_code, 403)
@@ -58,6 +67,7 @@ def test_get(self):
def test_get_protected(self):
self.client.force_login(self.user)
response = self.client.get(
- reverse("preview-page-root", kwargs={"language": "en"})
+ reverse("page-root", kwargs={"language": "en"}) + "?preview"
)
self.assertEqual(response.status_code, 200)
+ self.assertTrue(response.json().get("is_preview"))
diff --git a/tests/endpoints/test_page_tree_list.py b/tests/endpoints/test_page_tree_list.py
index 7abb716..903453d 100644
--- a/tests/endpoints/test_page_tree_list.py
+++ b/tests/endpoints/test_page_tree_list.py
@@ -53,12 +53,12 @@ def test_get(self):
# GET PREVIEW
response = self.client.get(
- reverse("preview-page-tree-list", kwargs={"language": "en"})
+ reverse("page-tree-list", kwargs={"language": "en"}) + "?preview"
)
self.assertEqual(response.status_code, 403)
response = self.client.get(
- reverse("preview-page-tree-list", kwargs={"language": "xx"})
+ reverse("page-tree-list", kwargs={"language": "xx"}) + "?preview"
)
self.assertEqual(response.status_code, 403)
@@ -66,7 +66,7 @@ def test_get(self):
def test_get_protected(self):
self.client.force_login(self.user)
response = self.client.get(
- reverse("preview-page-tree-list", kwargs={"language": "en"})
+ reverse("page-tree-list", kwargs={"language": "en"}) + "?preview"
)
self.assertEqual(response.status_code, 200)
diff --git a/tests/endpoints/test_placeholders.py b/tests/endpoints/test_placeholders.py
index a1c6197..ef71851 100644
--- a/tests/endpoints/test_placeholders.py
+++ b/tests/endpoints/test_placeholders.py
@@ -171,7 +171,7 @@ def test_get(self):
# GET PREVIEW
response = self.client.get(
reverse(
- "preview-placeholder-detail",
+ "placeholder-detail",
kwargs={
"language": "en",
"content_type_id": self.page_content_type.id,
@@ -179,6 +179,7 @@ def test_get(self):
"slot": "content",
},
)
+ + "?preview=true"
)
self.assertEqual(response.status_code, 403)
@@ -187,7 +188,7 @@ def test_get_protected(self):
self.client.force_login(self.user)
response = self.client.get(
reverse(
- "preview-placeholder-detail",
+ "placeholder-detail",
kwargs={
"language": "en",
"content_type_id": self.page_content_type.id,
@@ -195,6 +196,7 @@ def test_get_protected(self):
"slot": "content",
},
)
+ + "?preview"
)
self.assertEqual(response.status_code, 200)
diff --git a/tests/test_plugin_renderer.py b/tests/test_plugin_renderer.py
index fa60999..0a17cd2 100644
--- a/tests/test_plugin_renderer.py
+++ b/tests/test_plugin_renderer.py
@@ -178,7 +178,7 @@ def test_edit_endpoint(self):
# Test link plugin resolves link to page API endpoint
self.assertContains(
response,
- '"page": "http://testserver/api/en/pages-root/"',
+ '"page": "http://testserver/api/en/pages/"',
)
# Test image plugin resolves image URL
diff --git a/tests/test_utils.py b/tests/test_utils.py
new file mode 100644
index 0000000..00167af
--- /dev/null
+++ b/tests/test_utils.py
@@ -0,0 +1,16 @@
+from django.test import TestCase
+from django.test import RequestFactory
+
+from djangocms_rest.utils import get_absolute_frontend_url
+
+
+class UtilityTestCase(TestCase):
+ def test_get_absolute_frontend_url_adds_site(self):
+ request = RequestFactory().get("http://testserver/")
+ url = get_absolute_frontend_url(request, "/some/path/")
+ self.assertEqual(url, "http://testserver/some/path/")
+
+ def test_get_absolute_frontend_url_keeps_none(self):
+ request = RequestFactory().get("http://testserver/")
+ url = get_absolute_frontend_url(request, None)
+ self.assertIsNone(url)