From 4974aec2f7bbed81ddb7509215fc73d474d07185 Mon Sep 17 00:00:00 2001 From: Fabian Braun Date: Thu, 9 Oct 2025 11:43:12 +0200 Subject: [PATCH 01/10] feat: Add search page endpoint --- djangocms_rest/urls.py | 5 +++++ djangocms_rest/views.py | 11 +++++++++++ 2 files changed, 16 insertions(+) diff --git a/djangocms_rest/urls.py b/djangocms_rest/urls.py index 08cf348..0bb3dfe 100644 --- a/djangocms_rest/urls.py +++ b/djangocms_rest/urls.py @@ -30,6 +30,11 @@ views.PageDetailView.as_view(), name="page-detail", ), + path( + "/search_pages/", + views.PageSearchView.as_view(), + name="page-search", + ), path( "/placeholders////", views.PlaceholderDetailView.as_view(), diff --git a/djangocms_rest/views.py b/djangocms_rest/views.py index 46c1f58..32c0a46 100644 --- a/djangocms_rest/views.py +++ b/djangocms_rest/views.py @@ -135,6 +135,17 @@ def get_queryset(self): raise NotFound() +class PageSearchView(PageListView): + def get(self, request, language: str | None = None) -> Response: + self.search_term = request.GET.get("q", "") + self.language = language + return super().get(request) + + def get_queryset(self): + qs = Page.objects.search(self.search_term, language=self.language, current_site_only=False).on_site(self.site) + return PageContent.objects.filter(page__in=qs).distinct() + + class PageTreeListView(BaseAPIView): permission_classes = [IsAllowedPublicLanguage] serializer_class = PageMetaSerializer From 381497f28d1c509d106a94d4b06071c6b005df74 Mon Sep 17 00:00:00 2001 From: Fabian Braun Date: Thu, 9 Oct 2025 11:48:02 +0200 Subject: [PATCH 02/10] fix: Empty or missing search term returns empty result --- djangocms_rest/views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/djangocms_rest/views.py b/djangocms_rest/views.py index 32c0a46..791db4a 100644 --- a/djangocms_rest/views.py +++ b/djangocms_rest/views.py @@ -142,6 +142,8 @@ def get(self, request, language: str | None = None) -> Response: return super().get(request) def get_queryset(self): + if not self.search_term: + return PageContent.objects.none() qs = Page.objects.search(self.search_term, language=self.language, current_site_only=False).on_site(self.site) return PageContent.objects.filter(page__in=qs).distinct() From 6180204ab5c244738dd01b3156d3764d23d73d14 Mon Sep 17 00:00:00 2001 From: Fabian Braun Date: Thu, 9 Oct 2025 16:43:11 +0200 Subject: [PATCH 03/10] fix: Rename endpoint, add tests --- djangocms_rest/urls.py | 2 +- tests/endpoints/test_page_list.py | 32 ++++++++++++++++++++++--------- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/djangocms_rest/urls.py b/djangocms_rest/urls.py index 0bb3dfe..3bd53a9 100644 --- a/djangocms_rest/urls.py +++ b/djangocms_rest/urls.py @@ -31,7 +31,7 @@ name="page-detail", ), path( - "/search_pages/", + "/page_search/", views.PageSearchView.as_view(), name="page-search", ), diff --git a/tests/endpoints/test_page_list.py b/tests/endpoints/test_page_list.py index 5719b95..f7eda85 100644 --- a/tests/endpoints/test_page_list.py +++ b/tests/endpoints/test_page_list.py @@ -60,20 +60,34 @@ def test_get_paginated_list(self): self.assertEqual(response.status_code, 404) # GET PREVIEW - response = self.client.get( - reverse("page-list", kwargs={"language": "en"}) + "?preview" - ) + response = self.client.get(reverse("page-list", kwargs={"language": "en"}) + "?preview") self.assertEqual(response.status_code, 403) - response = self.client.get( - reverse("page-list", kwargs={"language": "xx"}) + "?preview" - ) + response = self.client.get(reverse("page-list", kwargs={"language": "xx"}) + "?preview") self.assertEqual(response.status_code, 403) # GET PREVIEW - Protected def test_get_protected(self): self.client.force_login(self.user) - response = self.client.get( - reverse("page-list", kwargs={"language": "en"}) + "?preview" - ) + response = self.client.get(reverse("page-list", kwargs={"language": "en"}) + "?preview") self.assertEqual(response.status_code, 200) + + def test_page_search(self): + for page in self.pages: + page_content = page.get_admin_content("en") + if hasattr(page_content, "versions"): + page_content.versions.first().publish(self.get_superuser()) + + # GET + response = self.client.get(reverse("page-search", kwargs={"language": "en"}) + "?q=1") + self.assertEqual(response.status_code, 200) + data = response.json() + results = data["results"] + + # Validate REST Pagination Attributes + self.assertIn("count", data) + self.assertIn("next", data) + self.assertIn("previous", data) + self.assertIn("results", data) + self.assertIsInstance(results, list) + self.assertEqual(data["count"], 4) From 46d99664e36dcfc55dd0cb552f9b2155d523a050 Mon Sep 17 00:00:00 2001 From: Fabian Braun Date: Thu, 9 Oct 2025 18:24:55 +0200 Subject: [PATCH 04/10] tests: add test for empty search --- tests/endpoints/test_page_list.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/endpoints/test_page_list.py b/tests/endpoints/test_page_list.py index f7eda85..dba676b 100644 --- a/tests/endpoints/test_page_list.py +++ b/tests/endpoints/test_page_list.py @@ -91,3 +91,23 @@ def test_page_search(self): self.assertIn("results", data) self.assertIsInstance(results, list) self.assertEqual(data["count"], 4) + + def test_empty_page_search(self): + for page in self.pages: + page_content = page.get_admin_content("en") + if hasattr(page_content, "versions"): + page_content.versions.first().publish(self.get_superuser()) + + # GET + response = self.client.get(reverse("page-search", kwargs={"language": "en"})) + self.assertEqual(response.status_code, 200) + data = response.json() + results = data["results"] + + # Validate REST Pagination Attributes + self.assertIn("count", data) + self.assertIn("next", data) + self.assertIn("previous", data) + self.assertIn("results", data) + self.assertIsInstance(results, list) + self.assertEqual(data["count"], 0) From 85aafe30bde7be317410b00019e34f8531c780e4 Mon Sep 17 00:00:00 2001 From: Fabian Braun Date: Thu, 6 Nov 2025 09:55:55 +0100 Subject: [PATCH 05/10] chore: Add tests for Django 6 and cms main repo --- .github/workflows/test.yml | 11 +++++++++-- tests/requirements/dj52_cmsmain.txt | 5 +++++ tests/requirements/dj60_cms50.txt | 4 ++++ 3 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 tests/requirements/dj52_cmsmain.txt create mode 100644 tests/requirements/dj60_cms50.txt diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 502041e..e227a4a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,17 +8,24 @@ jobs: strategy: fail-fast: false matrix: - python-version: [ "3.10", "3.11", "3.12"] + python-version: [ "3.10", "3.11", "3.12", "3.13"] requirements-file: [ dj42_cms41.txt, dj42_cms50.txt, dj50_cms50.txt, dj51_cms50.txt, - dj52_cms50.txt + dj52_cms50.txt, + dj60_cms50.txt, + dj52_cmsmain.txt ] os: [ ubuntu-latest, ] + exclude: + - python-version: "3.10" + requirements-file: dj60_cms50.txt + - python-version: "3.11" + requirements-file: dj60_cms50.txt steps: - uses: actions/checkout@v5 - name: Set up Python ${{ matrix.python-version }} diff --git a/tests/requirements/dj52_cmsmain.txt b/tests/requirements/dj52_cmsmain.txt new file mode 100644 index 0000000..327cc97 --- /dev/null +++ b/tests/requirements/dj52_cmsmain.txt @@ -0,0 +1,5 @@ +-r base.txt + +Django>=5.2,<5.3 +git+https://github.com/django-cms/django-cms.git@main + diff --git a/tests/requirements/dj60_cms50.txt b/tests/requirements/dj60_cms50.txt new file mode 100644 index 0000000..699b7c9 --- /dev/null +++ b/tests/requirements/dj60_cms50.txt @@ -0,0 +1,4 @@ +-r base.txt + +Django>=6.0a1,<6.1 +django-cms>=5.0,<5.1 From 3ee81b27cae0b912e8b68ce24560d526a60612bb Mon Sep 17 00:00:00 2001 From: Fabian Braun Date: Thu, 6 Nov 2025 10:04:47 +0100 Subject: [PATCH 06/10] chore: remove cairo from test srack --- .github/workflows/test.yml | 5 ----- tests/requirements/base.txt | 3 +++ 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 704cfb5..5015382 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -52,11 +52,6 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install uv run: curl -LsSf https://astral.sh/uv/install.sh | sh - - name: Install system deps (cairo stack) - run: | - sudo apt-get update - sudo apt-get install -y \ - build-essential libcairo2-dev pkg-config python3-dev - name: Install dependencies run: | uv venv diff --git a/tests/requirements/base.txt b/tests/requirements/base.txt index 6f1dd38..3fc5843 100644 --- a/tests/requirements/base.txt +++ b/tests/requirements/base.txt @@ -9,3 +9,6 @@ drf-spectacular # other requirements coverage tox + +# avoid having to install cairo stack for testing +svglib!=1.6.0 From ec1639391739724d5993dcebdf0203f6d45961bf Mon Sep 17 00:00:00 2001 From: Marc Widmer Date: Thu, 6 Nov 2025 14:27:09 +0100 Subject: [PATCH 07/10] feat: Add page search schema extension for exact match queries (#79) --- djangocms_rest/views.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/djangocms_rest/views.py b/djangocms_rest/views.py index 6ffa90e..2f14374 100644 --- a/djangocms_rest/views.py +++ b/djangocms_rest/views.py @@ -61,6 +61,18 @@ ] ) + extend_page_search_schema = extend_schema( + parameters=[ + OpenApiParameter( + name="q", + type=OpenApiTypes.STR, + location=OpenApiParameter.QUERY, + description="Search for an exact match of the search term to find pages by title, page_title, menu_title, or meta_description", + required=False, + ), + ] + ) + except ImportError: # pragma: no cover class OpenApiTypes: @@ -85,6 +97,9 @@ def _decorator(obj: T) -> T: def extend_placeholder_schema(func: Callable[P, T]) -> Callable[P, T]: return func + def extend_page_search_schema(func: Callable[P, T]) -> Callable[P, T]: + return func + # Generate the plugin definitions once at module load time # This avoids the need to import the plugin definitions in every view @@ -137,6 +152,7 @@ def get_queryset(self): class PageSearchView(PageListView): + @extend_page_search_schema def get(self, request, language: str | None = None) -> Response: self.search_term = request.GET.get("q", "") self.language = language From 6e458706231b84f665bb189d6f0bde0e1b883ee3 Mon Sep 17 00:00:00 2001 From: Fabian Braun Date: Sat, 8 Nov 2025 12:06:01 +0100 Subject: [PATCH 08/10] fix merge --- djangocms_rest/schemas.py | 12 ++++++++++++ djangocms_rest/views.py | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/djangocms_rest/schemas.py b/djangocms_rest/schemas.py index 2c5ac84..1cfdaad 100644 --- a/djangocms_rest/schemas.py +++ b/djangocms_rest/schemas.py @@ -69,6 +69,18 @@ def method_schema_decorator(method): ] ) + extend_page_search_schema = extend_schema( + parameters=[ + OpenApiParameter( + name="q", + type=OpenApiTypes.STR, + location=OpenApiParameter.QUERY, + description="Search for an exact match of the search term to find pages by title, page_title, menu_title, or meta_description", + required=False, + ), + ] + ) + except ImportError: def method_schema_decorator(method): diff --git a/djangocms_rest/views.py b/djangocms_rest/views.py index 7a9fd12..b44dd6e 100644 --- a/djangocms_rest/views.py +++ b/djangocms_rest/views.py @@ -32,7 +32,7 @@ get_site_filtered_queryset, ) from djangocms_rest.views_base import BaseAPIView, BaseListAPIView -from djangocms_rest.schemas import extend_placeholder_schema, menu_schema_class +from djangocms_rest.schemas import extend_placeholder_schema, extend_page_search_schema, menu_schema_class # Generate the plugin definitions once at module load time # This avoids the need to import the plugin definitions in every view From 23419e5d178531ab0b3496e0e338227e46eee8ca Mon Sep 17 00:00:00 2001 From: Fabian Braun Date: Mon, 10 Nov 2025 18:37:53 +0100 Subject: [PATCH 09/10] fix: Fallback for page search schema --- djangocms_rest/schemas.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/djangocms_rest/schemas.py b/djangocms_rest/schemas.py index 1cfdaad..5176c8d 100644 --- a/djangocms_rest/schemas.py +++ b/djangocms_rest/schemas.py @@ -75,7 +75,7 @@ def method_schema_decorator(method): name="q", type=OpenApiTypes.STR, location=OpenApiParameter.QUERY, - description="Search for an exact match of the search term to find pages by title, page_title, menu_title, or meta_description", + description="Search for an exact match of the search term to find pages", required=False, ), ] @@ -96,3 +96,7 @@ def create_view_with_url_name(view_class, url_name): def extend_placeholder_schema(func): """No-op when drf-spectacular is not available.""" return func + + def extend_page_search_schema(func): + """No-op when drf-spectacular is not available.""" + return func From a3e609e11f0728a2af0352c59fe58bdc470c0b5a Mon Sep 17 00:00:00 2001 From: Fabian Braun Date: Tue, 11 Nov 2025 08:37:25 +0100 Subject: [PATCH 10/10] fix: highlight fonts --- djangocms_rest/static/djangocms_rest/highlight.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/djangocms_rest/static/djangocms_rest/highlight.css b/djangocms_rest/static/djangocms_rest/highlight.css index d7b8b87..731c960 100644 --- a/djangocms_rest/static/djangocms_rest/highlight.css +++ b/djangocms_rest/static/djangocms_rest/highlight.css @@ -18,6 +18,12 @@ } } +section { + margin: 10px; + h1 { + font-family: Helvetica,Arial,sans-serif; + } +} .rest-placeholder { display: block;