diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 24b9992..5015382 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,11 +16,16 @@ jobs: dj51_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 - python-version: "3.14" requirements-file: dj42_cms41.txt os: ubuntu-latest @@ -47,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/djangocms_rest/schemas.py b/djangocms_rest/schemas.py index 2c5ac84..5176c8d 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", + required=False, + ), + ] + ) + except ImportError: def method_schema_decorator(method): @@ -84,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 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; diff --git a/djangocms_rest/urls.py b/djangocms_rest/urls.py index 15134e4..5284aef 100644 --- a/djangocms_rest/urls.py +++ b/djangocms_rest/urls.py @@ -31,6 +31,11 @@ views.PageDetailView.as_view(), name="page-detail", ), + path( + "/page_search/", + 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 e96195b..b44dd6e 100644 --- a/djangocms_rest/views.py +++ b/djangocms_rest/views.py @@ -32,8 +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 @@ -85,6 +84,20 @@ def get_queryset(self): raise NotFound() +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 + 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() + + class PageTreeListView(BaseAPIView): permission_classes = [IsAllowedPublicLanguage] serializer_class = PageMetaSerializer diff --git a/tests/endpoints/test_page_list.py b/tests/endpoints/test_page_list.py index 5719b95..dba676b 100644 --- a/tests/endpoints/test_page_list.py +++ b/tests/endpoints/test_page_list.py @@ -60,20 +60,54 @@ 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) + + 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) 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 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 index a6959c1..699b7c9 100644 --- a/tests/requirements/dj60_cms50.txt +++ b/tests/requirements/dj60_cms50.txt @@ -1,4 +1,4 @@ -r base.txt Django>=6.0a1,<6.1 -django-cms>=5.0.0a1,<5.1 +django-cms>=5.0,<5.1