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)