Skip to content

Commit 54e1e23

Browse files
authored
feat: frontend URL handling with absolute URL generation. (#36)
* feat: frontend URL handling with absolute URL generation. * refactor: use request context to handle frontend protocol or http as fallback * refactor: simplify get_absolute_frontend_url path handling * test: add unit tests for `get_absolute_frontend_url` utility * fix: remove unused imports from utils module * fix: remove unused imports from serializers
1 parent b5bf099 commit 54e1e23

File tree

7 files changed

+101
-12
lines changed

7 files changed

+101
-12
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,12 @@ INSTALLED_APPS = [
180180
]
181181
```
182182

183+
Set up frontend protocol and caching in your `settings.py`:
184+
```python
185+
# Set frontend protocol
186+
FRONTEND_PROTOCOL = 'http' # or 'https', needed for absolute URLs in API responses
187+
```
188+
183189
```python
184190
# Enabled Caching
185191
CONTENT_CACHE_DURATION = 60 # Overwrites default from django CMS

djangocms_rest/serializers/pages.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from rest_framework import serializers
66

77
from djangocms_rest.serializers.placeholders import PlaceholderRelationSerializer
8+
from djangocms_rest.utils import get_absolute_frontend_url
89

910

1011
class BasePageSerializer(serializers.Serializer):
@@ -38,30 +39,33 @@ class PreviewMixin:
3839

3940
class BasePageContentMixin:
4041
def get_base_representation(self, page_content: PageContent) -> Dict:
41-
relative_url = page_content.page.get_path(page_content.language)
42-
absolute_url = page_content.page.get_absolute_url(page_content.language) or ""
42+
request = getattr(self, "request", None)
43+
path = page_content.page.get_path(page_content.language)
44+
absolute_url = get_absolute_frontend_url(request,path)
45+
redirect = str(page_content.redirect or "")
4346
xframe_options = str(page_content.xframe_options or "")
47+
application_namespace = str(page_content.page.application_namespace or "")
4448
limit_visibility_in_menu = bool(page_content.limit_visibility_in_menu)
4549

4650
return {
4751
"title": page_content.title,
4852
"page_title": page_content.page_title or page_content.title,
4953
"menu_title": page_content.menu_title or page_content.title,
5054
"meta_description": page_content.meta_description,
51-
"redirect": page_content.redirect,
55+
"redirect": redirect,
5256
"in_navigation": page_content.in_navigation,
5357
"soft_root": page_content.soft_root,
5458
"template": page_content.template,
5559
"xframe_options": xframe_options,
5660
"limit_visibility_in_menu": limit_visibility_in_menu,
5761
"language": page_content.language,
58-
"path": relative_url,
62+
"path": path,
5963
"absolute_url": absolute_url,
6064
"is_home": page_content.page.is_home,
6165
"login_required": page_content.page.login_required,
6266
"languages": page_content.page.get_languages(),
6367
"is_preview": getattr(self, "is_preview", False),
64-
"application_namespace": page_content.page.application_namespace,
68+
"application_namespace": application_namespace,
6569
"creation_date": page_content.creation_date,
6670
"changed_date": page_content.changed_date,
6771
}
@@ -78,9 +82,7 @@ def tree_to_representation(self, item: PageContent) -> Dict:
7882
serialized_data = self.child.to_representation(item)
7983
serialized_data["children"] = []
8084
if item.page in self.tree:
81-
serialized_data["children"] = [
82-
self.tree_to_representation(child) for child in self.tree[item.page]
83-
]
85+
serialized_data["children"] = [self.tree_to_representation(child) for child in self.tree[item.page]]
8486
return serialized_data
8587

8688
def to_representation(self, data: Dict) -> list[Dict]:
@@ -152,6 +154,7 @@ def to_representation(self, page_content: PageContent) -> Dict:
152154

153155
class PreviewPageContentSerializer(PageContentSerializer, PreviewMixin):
154156
"""Serializer specifically for preview/draft page content"""
157+
155158
placeholders = PlaceholderRelationSerializer(many=True, required=False)
156159

157160
def to_representation(self, page_content: PageContent) -> Dict:

djangocms_rest/utils.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from cms.models import Page, PageUrl
22
from django.contrib.sites.models import Site
3+
from django.contrib.sites.shortcuts import get_current_site
34
from django.http import Http404
5+
from rest_framework.request import Request
46

57

68
def get_object(site: Site, path: str) -> Page:
@@ -15,3 +17,25 @@ def get_object(site: Site, path: str) -> Page:
1517
else:
1618
page.urls_cache = {url.language: url for url in page_urls}
1719
return page
20+
21+
22+
def get_absolute_frontend_url(request: Request, path: str) -> str:
23+
"""
24+
Creates an absolute URL for a given relative path using the current site's domain and protocol.
25+
26+
Args:
27+
request: The HTTP request object
28+
path: The relative path to the page
29+
30+
Returns:
31+
An absolute URL formatted as a string.
32+
"""
33+
34+
if path.startswith('/'):
35+
raise ValueError(f"Path should not start with '/': {path}")
36+
37+
site = get_current_site(request) if request else Site.objects.get(id=1)
38+
domain = site.domain.rstrip('/')
39+
protocol = getattr(request, "scheme", "http")
40+
41+
return f"{protocol}://{domain}/{path}"

djangocms_rest/views.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ def get(self, request, language):
115115
except PageContent.DoesNotExist:
116116
raise NotFound()
117117

118-
serializer = self.serializer_class(pages, many=True, read_only=True)
118+
serializer = self.serializer_class(pages, many=True, read_only=True, context={"request": request})
119119
return Response(serializer.data)
120120

121121

@@ -134,7 +134,7 @@ def get(self, request: Request, language: str, path: str = "") -> Response:
134134
page_content = page.get_content_obj(language, fallback=True)
135135
if page_content is None:
136136
raise PageContent.DoesNotExist()
137-
serializer = self.serializer_class(page_content, read_only=True)
137+
serializer = self.serializer_class(page_content, read_only=True, context={"request": request})
138138
return Response(serializer.data)
139139
except PageContent.DoesNotExist:
140140
raise NotFound()
@@ -250,7 +250,7 @@ def get(self, request: Request, language: str, path: str = "") -> Response:
250250
except PageContent.DoesNotExist:
251251
raise NotFound()
252252

253-
serializer = self.serializer_class(page_content, read_only=True)
253+
serializer = self.serializer_class(page_content, read_only=True, context={"request": request})
254254
return Response(serializer.data)
255255

256256
#NOTE: This is working, but might need refactoring
@@ -279,7 +279,7 @@ def get(self, request, language):
279279
except PageContent.DoesNotExist:
280280
raise NotFound()
281281

282-
serializer = self.serializer_class(pages, many=True, read_only=True)
282+
serializer = self.serializer_class(pages, many=True, read_only=True, context={"request": request})
283283
return Response(serializer.data)
284284

285285
class PreviewPageListView(BaseListAPIView):

tests/core/__init__.py

Whitespace-only changes.

tests/core/test_utils.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
from django.contrib.sites.models import Site
2+
from rest_framework.test import APIRequestFactory
3+
4+
from djangocms_rest.utils import get_absolute_frontend_url
5+
from tests.base import BaseCMSRestTestCase
6+
7+
8+
class UrlUtilsTestCase(BaseCMSRestTestCase):
9+
"""
10+
Test the get_absolute_frontend_url utility function.
11+
12+
Verifies:
13+
- Function correctly builds absolute URLs for frontend paths
14+
- Function raises proper ValueError for invalid paths
15+
- Function works correctly with both a request object and None
16+
- All URLs use the correct domain from a Site object
17+
"""
18+
19+
def setUp(self):
20+
super().setUp()
21+
self.factory = APIRequestFactory()
22+
23+
def test_get_absolute_frontend_url_valid_path(self):
24+
"""Test that get_absolute_frontend_url works with valid paths."""
25+
26+
request = self.factory.get("/dummy")
27+
site = Site.objects.get_current()
28+
result = get_absolute_frontend_url(request, "valid/path")
29+
30+
# Validation
31+
expected_url = f"http://{site.domain}/valid/path"
32+
self.assertEqual(result, expected_url)
33+
34+
def test_get_absolute_frontend_url_with_leading_slash(self):
35+
"""Test that get_absolute_frontend_url raises ValueError with paths starting with /."""
36+
request = self.factory.get("/dummy")
37+
38+
# Function execution and validation
39+
with self.assertRaises(ValueError) as context:
40+
get_absolute_frontend_url(request, "/invalid/path")
41+
42+
# Error message validation
43+
error_message = str(context.exception)
44+
self.assertIn("Path should not start with '/'", error_message)
45+
self.assertIn("/invalid/path", error_message)
46+
47+
def test_get_absolute_frontend_url_without_request(self):
48+
"""Test that get_absolute_frontend_url works with request=None."""
49+
50+
result = get_absolute_frontend_url(None, "valid/path")
51+
52+
# Validation
53+
site = Site.objects.get(id=1)
54+
expected_url = f"http://{site.domain}/valid/path"
55+
self.assertEqual(result, expected_url)

tests/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,3 +181,4 @@ def __getitem__(self, item):
181181
}
182182

183183
USE_TZ = True
184+

0 commit comments

Comments
 (0)