Skip to content

Commit e9078be

Browse files
fsbraunmetaforx
andauthored
feat: Add page search endpoint (#64)
* feat: Add search page endpoint * fix: Empty or missing search term returns empty result * fix: Rename endpoint, add tests * tests: add test for empty search * chore: Add tests for Django 6 and cms main repo * chore: remove cairo from test srack * feat: Add page search schema extension for exact match queries (#79) * fix merge * fix: Fallback for page search schema * fix: highlight fonts --------- Co-authored-by: Marc Widmer <[email protected]>
1 parent b78b5a0 commit e9078be

File tree

9 files changed

+99
-17
lines changed

9 files changed

+99
-17
lines changed

.github/workflows/test.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,16 @@ jobs:
1616
dj51_cms50.txt,
1717
dj52_cms50.txt,
1818
dj60_cms50.txt,
19+
dj52_cmsmain.txt
1920
]
2021
os: [
2122
ubuntu-latest,
2223
]
2324
exclude:
25+
- python-version: "3.10"
26+
requirements-file: dj60_cms50.txt
27+
- python-version: "3.11"
28+
requirements-file: dj60_cms50.txt
2429
- python-version: "3.14"
2530
requirements-file: dj42_cms41.txt
2631
os: ubuntu-latest
@@ -47,11 +52,6 @@ jobs:
4752
python-version: ${{ matrix.python-version }}
4853
- name: Install uv
4954
run: curl -LsSf https://astral.sh/uv/install.sh | sh
50-
- name: Install system deps (cairo stack)
51-
run: |
52-
sudo apt-get update
53-
sudo apt-get install -y \
54-
build-essential libcairo2-dev pkg-config python3-dev
5555
- name: Install dependencies
5656
run: |
5757
uv venv

djangocms_rest/schemas.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,18 @@ def method_schema_decorator(method):
6969
]
7070
)
7171

72+
extend_page_search_schema = extend_schema(
73+
parameters=[
74+
OpenApiParameter(
75+
name="q",
76+
type=OpenApiTypes.STR,
77+
location=OpenApiParameter.QUERY,
78+
description="Search for an exact match of the search term to find pages",
79+
required=False,
80+
),
81+
]
82+
)
83+
7284
except ImportError:
7385

7486
def method_schema_decorator(method):
@@ -84,3 +96,7 @@ def create_view_with_url_name(view_class, url_name):
8496
def extend_placeholder_schema(func):
8597
"""No-op when drf-spectacular is not available."""
8698
return func
99+
100+
def extend_page_search_schema(func):
101+
"""No-op when drf-spectacular is not available."""
102+
return func

djangocms_rest/static/djangocms_rest/highlight.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@
1818
}
1919
}
2020

21+
section {
22+
margin: 10px;
23+
h1 {
24+
font-family: Helvetica,Arial,sans-serif;
25+
}
26+
}
2127

2228
.rest-placeholder {
2329
display: block;

djangocms_rest/urls.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@
3131
views.PageDetailView.as_view(),
3232
name="page-detail",
3333
),
34+
path(
35+
"<slug:language>/page_search/",
36+
views.PageSearchView.as_view(),
37+
name="page-search",
38+
),
3439
path(
3540
"<slug:language>/placeholders/<int:content_type_id>/<int:object_id>/<str:slot>/",
3641
views.PlaceholderDetailView.as_view(),

djangocms_rest/views.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,7 @@
3232
get_site_filtered_queryset,
3333
)
3434
from djangocms_rest.views_base import BaseAPIView, BaseListAPIView
35-
from djangocms_rest.schemas import extend_placeholder_schema, menu_schema_class
36-
35+
from djangocms_rest.schemas import extend_placeholder_schema, extend_page_search_schema, menu_schema_class
3736

3837
# Generate the plugin definitions once at module load time
3938
# This avoids the need to import the plugin definitions in every view
@@ -85,6 +84,20 @@ def get_queryset(self):
8584
raise NotFound()
8685

8786

87+
class PageSearchView(PageListView):
88+
@extend_page_search_schema
89+
def get(self, request, language: str | None = None) -> Response:
90+
self.search_term = request.GET.get("q", "")
91+
self.language = language
92+
return super().get(request)
93+
94+
def get_queryset(self):
95+
if not self.search_term:
96+
return PageContent.objects.none()
97+
qs = Page.objects.search(self.search_term, language=self.language, current_site_only=False).on_site(self.site)
98+
return PageContent.objects.filter(page__in=qs).distinct()
99+
100+
88101
class PageTreeListView(BaseAPIView):
89102
permission_classes = [IsAllowedPublicLanguage]
90103
serializer_class = PageMetaSerializer

tests/endpoints/test_page_list.py

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -60,20 +60,54 @@ def test_get_paginated_list(self):
6060
self.assertEqual(response.status_code, 404)
6161

6262
# GET PREVIEW
63-
response = self.client.get(
64-
reverse("page-list", kwargs={"language": "en"}) + "?preview"
65-
)
63+
response = self.client.get(reverse("page-list", kwargs={"language": "en"}) + "?preview")
6664
self.assertEqual(response.status_code, 403)
6765

68-
response = self.client.get(
69-
reverse("page-list", kwargs={"language": "xx"}) + "?preview"
70-
)
66+
response = self.client.get(reverse("page-list", kwargs={"language": "xx"}) + "?preview")
7167
self.assertEqual(response.status_code, 403)
7268

7369
# GET PREVIEW - Protected
7470
def test_get_protected(self):
7571
self.client.force_login(self.user)
76-
response = self.client.get(
77-
reverse("page-list", kwargs={"language": "en"}) + "?preview"
78-
)
72+
response = self.client.get(reverse("page-list", kwargs={"language": "en"}) + "?preview")
7973
self.assertEqual(response.status_code, 200)
74+
75+
def test_page_search(self):
76+
for page in self.pages:
77+
page_content = page.get_admin_content("en")
78+
if hasattr(page_content, "versions"):
79+
page_content.versions.first().publish(self.get_superuser())
80+
81+
# GET
82+
response = self.client.get(reverse("page-search", kwargs={"language": "en"}) + "?q=1")
83+
self.assertEqual(response.status_code, 200)
84+
data = response.json()
85+
results = data["results"]
86+
87+
# Validate REST Pagination Attributes
88+
self.assertIn("count", data)
89+
self.assertIn("next", data)
90+
self.assertIn("previous", data)
91+
self.assertIn("results", data)
92+
self.assertIsInstance(results, list)
93+
self.assertEqual(data["count"], 4)
94+
95+
def test_empty_page_search(self):
96+
for page in self.pages:
97+
page_content = page.get_admin_content("en")
98+
if hasattr(page_content, "versions"):
99+
page_content.versions.first().publish(self.get_superuser())
100+
101+
# GET
102+
response = self.client.get(reverse("page-search", kwargs={"language": "en"}))
103+
self.assertEqual(response.status_code, 200)
104+
data = response.json()
105+
results = data["results"]
106+
107+
# Validate REST Pagination Attributes
108+
self.assertIn("count", data)
109+
self.assertIn("next", data)
110+
self.assertIn("previous", data)
111+
self.assertIn("results", data)
112+
self.assertIsInstance(results, list)
113+
self.assertEqual(data["count"], 0)

tests/requirements/base.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,6 @@ drf-spectacular
99
# other requirements
1010
coverage
1111
tox
12+
13+
# avoid having to install cairo stack for testing
14+
svglib!=1.6.0
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-r base.txt
2+
3+
Django>=5.2,<5.3
4+
git+https://github.com/django-cms/django-cms.git@main
5+

tests/requirements/dj60_cms50.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
-r base.txt
22

33
Django>=6.0a1,<6.1
4-
django-cms>=5.0.0a1,<5.1
4+
django-cms>=5.0,<5.1

0 commit comments

Comments
 (0)