Skip to content

Commit 045e4da

Browse files
authored
Merge branch 'main' into feat/search
2 parents 85aafe3 + 2a129da commit 045e4da

40 files changed

+4260
-46
lines changed

.github/workflows/test.yml

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ jobs:
88
strategy:
99
fail-fast: false
1010
matrix:
11-
python-version: [ "3.10", "3.11", "3.12", "3.13"]
11+
python-version: [ "3.10", "3.11", "3.12", "3.13", "3.14"]
1212
requirements-file: [
1313
dj42_cms41.txt,
1414
dj42_cms50.txt,
@@ -26,27 +26,53 @@ jobs:
2626
requirements-file: dj60_cms50.txt
2727
- python-version: "3.11"
2828
requirements-file: dj60_cms50.txt
29+
- python-version: "3.14"
30+
requirements-file: dj42_cms41.txt
31+
os: ubuntu-latest
32+
- python-version: "3.14"
33+
requirements-file: dj42_cms50.txt
34+
os: ubuntu-latest
35+
- python-version: "3.14"
36+
requirements-file: dj51_cms50.txt
37+
os: ubuntu-latest
38+
- python-version: "3.14"
39+
requirements-file: dj50_cms50.txt
40+
os: ubuntu-latest
41+
- python-version: "3.10"
42+
requirements-file: dj60_cms50.txt
43+
os: ubuntu-latest
44+
- python-version: "3.11"
45+
requirements-file: dj60_cms50.txt
46+
os: ubuntu-latest
2947
steps:
3048
- uses: actions/checkout@v5
3149
- name: Set up Python ${{ matrix.python-version }}
3250
uses: actions/setup-python@v6
3351
with:
3452
python-version: ${{ matrix.python-version }}
53+
- name: Install uv
54+
run: curl -LsSf https://astral.sh/uv/install.sh | sh
3555
- name: Install system deps (cairo stack)
3656
run: |
3757
sudo apt-get update
3858
sudo apt-get install -y \
3959
build-essential libcairo2-dev pkg-config python3-dev
4060
- name: Install dependencies
4161
run: |
42-
python -m pip install --upgrade pip
43-
pip install -r tests/requirements/${{ matrix.requirements-file }}
44-
pip install .
62+
uv venv
63+
source .venv/bin/activate
64+
uv pip install -r tests/requirements/${{ matrix.requirements-file }}
65+
uv pip install .
4566
4667
- name: Run coverage
47-
run: coverage run -m pytest
68+
run: |
69+
source .venv/bin/activate
70+
coverage run -m pytest
71+
coverage xml
4872
4973
- name: Upload Coverage to Codecov
5074
uses: codecov/[email protected]
5175
with:
5276
token: ${{ secrets.CODECOV_TOKEN }}
77+
files: ./coverage.xml
78+
fail_ci_if_error: false

.readthedocs.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Read the Docs configuration file
2+
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
3+
version: 2
4+
5+
build:
6+
os: ubuntu-24.04
7+
tools:
8+
python: "3.13"
9+
10+
sphinx:
11+
configuration: docs/source/conf.py
12+
fail_on_warning: false
13+
14+
formats:
15+
- epub
16+
- pdf
17+
18+
python:
19+
install:
20+
- requirements: docs/requirements.txt

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -411,4 +411,4 @@ like to change.
411411

412412
## License
413413

414-
[BSD-3](https://github.com/fsbraun/djangocms-rest/blob/main/LICENSE)
414+
[BSD-3](https://github.com/django-cms/djangocms-rest/blob/main/LICENSE)

djangocms_rest/cms_config.py

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,7 @@ def get_page_api_endpoint(page, language=None, fallback=True):
2828
if page.is_home:
2929
return reverse("page-root", kwargs={"language": language})
3030
path = page.get_path(language, fallback)
31-
return (
32-
reverse("page-detail", kwargs={"language": language, "path": path})
33-
if path
34-
else None
35-
)
31+
return reverse("page-detail", kwargs={"language": language, "path": path}) if path else None
3632
except NoReverseMatch:
3733
return None
3834

@@ -51,6 +47,10 @@ def inner(self, page_content: PageContent, *args, **kwargs):
5147
page_content.page,
5248
page_content.language,
5349
)
50+
# To save API calls, we add the page's path to the node attributes
51+
node.attr["path"] = page_content.page.get_path(
52+
page_content.language,
53+
)
5454
return node
5555

5656
return inner
@@ -59,9 +59,7 @@ def inner(self, page_content: PageContent, *args, **kwargs):
5959
def patch_page_menu(menu: type[CMSMenu]):
6060
"""Patch the CMSMenu to use the REST API endpoint for pages."""
6161
if hasattr(menu, "get_menu_node_for_page_content"):
62-
menu.get_menu_node_for_page_content = patch_get_menu_node_for_page_content(
63-
menu.get_menu_node_for_page_content
64-
)
62+
menu.get_menu_node_for_page_content = patch_get_menu_node_for_page_content(menu.get_menu_node_for_page_content)
6563

6664

6765
class NavigationNodeMixin:
@@ -101,9 +99,7 @@ class RESTToolbarMixin:
10199
def __init__(self, *args, **kwargs):
102100
super().__init__(*args, **kwargs)
103101

104-
if getattr(
105-
settings, "REST_JSON_RENDERING", not getattr(settings, "CMS_TEMPLATES", False)
106-
):
102+
if getattr(settings, "REST_JSON_RENDERING", not getattr(settings, "CMS_TEMPLATES", False)):
107103
try:
108104
from djangocms_text import settings
109105

djangocms_rest/middleware.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Callable
1+
from collections.abc import Callable
22

33
from django.contrib.sites.shortcuts import get_current_site
44
from django.contrib.sites.models import Site

djangocms_rest/plugin_rendering.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import json
2-
from typing import Any, Iterable, Optional, TypeVar
2+
from typing import Any, TypeVar
3+
from collections.abc import Iterable
34

45
from django.contrib.sites.shortcuts import get_current_site
56
from django.core.exceptions import ValidationError
@@ -50,8 +51,8 @@ def get_auto_model_serializer(model_class: type[ModelType]) -> type:
5051

5152

5253
def serialize_cms_plugin(
53-
instance: Optional[Any], context: dict[str, Any]
54-
) -> Optional[dict[str, Any]]:
54+
instance: Any | None, context: dict[str, Any]
55+
) -> dict[str, Any] | None:
5556
if not instance or not hasattr(instance, "get_plugin_instance"):
5657
return None
5758
plugin_instance, plugin = instance.get_plugin_instance()

djangocms_rest/serializers/menus.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ class NavigationNodeSerializer(serializers.Serializer):
99
namespace = serializers.CharField(allow_null=True)
1010
title = serializers.CharField()
1111
url = serializers.URLField(allow_null=True)
12-
path = serializers.CharField(allow_null=True)
1312
api_endpoint = serializers.URLField(allow_null=True)
1413
visible = serializers.BooleanField()
1514
selected = serializers.BooleanField()
@@ -28,12 +27,18 @@ def get_children(self, obj: NavigationNode) -> list[dict]:
2827

2928
def to_representation(self, obj: NavigationNode) -> dict:
3029
"""Customize the base representation of the NavigationNode."""
30+
path = getattr(obj, "api_endpoint", "")
31+
api_endpoint = get_absolute_frontend_url(self.request, path) if path else ""
32+
if self.request._preview_mode:
33+
if "?" in api_endpoint:
34+
api_endpoint += "&preview=1"
35+
else:
36+
api_endpoint += "?preview=1"
3137
return {
3238
"namespace": getattr(obj, "namespace", None),
3339
"title": obj.title,
3440
"url": get_absolute_frontend_url(self.request, obj.url) or "",
35-
"api_endpoint": get_absolute_frontend_url(self.request, getattr(obj, "api_endpoint", None)) or "",
36-
"path": getattr(obj, "api_endpoint", ""),
41+
"api_endpoint": api_endpoint,
3742
"visible": obj.visible,
3843
"selected": obj.selected or obj.attr.get("is_home", False) and getattr(self.request, "is_home", False),
3944
"attr": obj.attr,

djangocms_rest/serializers/pages.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from django.db import models
22

33
from cms.models import PageContent
4+
from cms.utils.placeholder import get_declared_placeholders_for_obj
45

56
from rest_framework import serializers
67

@@ -42,6 +43,11 @@ def get_base_representation(self, page_content: PageContent) -> dict:
4243
path = page_content.page.get_path(page_content.language)
4344
absolute_url = get_absolute_frontend_url(request, path)
4445
api_endpoint = get_absolute_frontend_url(request, page_content.page.get_api_endpoint(page_content.language))
46+
if self.is_preview:
47+
if "?" in api_endpoint:
48+
api_endpoint += "&preview=1"
49+
else:
50+
api_endpoint += "?preview=1"
4551
redirect = str(page_content.redirect or "")
4652
xframe_options = str(page_content.xframe_options or "")
4753
application_namespace = str(page_content.page.application_namespace or "")
@@ -132,7 +138,7 @@ def __init__(self, *args, **kwargs):
132138
self.request = self.context.get("request")
133139

134140
def to_representation(self, page_content: PageContent) -> dict:
135-
declared_slots = [placeholder.slot for placeholder in page_content.page.get_declared_placeholders()]
141+
declared_slots = [placeholder.slot for placeholder in get_declared_placeholders_for_obj(page_content)]
136142
placeholders = [
137143
placeholder for placeholder in page_content.placeholders.all() if placeholder.slot in declared_slots
138144
]

djangocms_rest/serializers/placeholders.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,7 @@ class PlaceholderSerializer(serializers.Serializer):
1111
slot = serializers.CharField()
1212
label = serializers.CharField()
1313
language = serializers.CharField()
14-
content = serializers.ListSerializer(
15-
child=serializers.JSONField(), allow_empty=True, required=False
16-
)
14+
content = serializers.ListSerializer(child=serializers.JSONField(), allow_empty=True, required=False)
1715
html = serializers.CharField(default="", required=False)
1816

1917
def __init__(self, *args, **kwargs):
@@ -64,7 +62,7 @@ def to_representation(self, instance):
6462
return super().to_representation(instance)
6563

6664
def get_details(self, instance):
67-
return get_absolute_frontend_url(
65+
api_endpoint = get_absolute_frontend_url(
6866
self.request,
6967
reverse(
7068
"placeholder-detail",
@@ -76,3 +74,9 @@ def get_details(self, instance):
7674
],
7775
),
7876
)
77+
if self.request._preview_mode:
78+
if "?" in api_endpoint:
79+
api_endpoint += "&preview=1"
80+
else:
81+
api_endpoint += "?preview=1"
82+
return api_endpoint

djangocms_rest/serializers/plugins.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any, Optional
1+
from typing import Any
22

33
from django.apps import apps
44
from django.db.models import Field, Model
@@ -17,7 +17,7 @@ def serialize_fk(
1717
request: HttpRequest,
1818
related_model: type[CMSPlugin],
1919
pk: Any,
20-
obj: Optional[Model] = None,
20+
obj: Model | None = None,
2121
) -> dict[str, Any]:
2222
"""
2323
Serializes a foreign key reference to a related model as a URL or identifier.
@@ -157,7 +157,7 @@ class PluginDefinitionSerializer(serializers.Serializer):
157157
properties = serializers.DictField(help_text="Property definitions")
158158

159159
@staticmethod
160-
def get_field_format(field: Field) -> Optional[str]:
160+
def get_field_format(field: Field) -> str | None:
161161
"""
162162
Get the format for specific field types.
163163

0 commit comments

Comments
 (0)