8000 feat: Headless readiness (#7850) · django-cms/django-cms@d0a25c0 · GitHub
[go: up one dir, main page]

Skip to content 8000

Commit d0a25c0

Browse files
fsbraunVinit Kumar
andauthored
feat: Headless readiness (#7850)
* Feat: Move data bridge data to script tags for easier extraction. * Fix linting * Move json script before script reading it to avoid race conditions * lean template * Move wizard urls to admin namespace * Fix linting errors * Update appresolver and fix page middleware (though no need to use in headless mode) * fix: use admin_reverse instead of reverse('admin: ... * Update preview condition for toolbar * Self-contained structure mode css * Stay in 4.1.x * Alles structure endpoint for read-only objects * fix: linting * more linting * feat: Allow running without templates * Add unformatted preview for headless mode * Fix: Advanced placeholder config * Fix: tests * Fix {% render_placeholder %} tag * Add docs * Fix typos * fix typos * Update docs for better clarity * Fix: Use user language for headless plugin list * Fix docs typo * Update docs after review --------- Co-authored-by: Vinit Kumar <vinit.kumar@kidskonnect.nl>
1 parent e10be3d commit d0a25c0

35 files changed

+647
-210
lines changed

.editorconfig

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,6 @@ indent_size = 2
3131

3232
[*.yml]
3333
indent_size = 2
34+
35+
[*.html]
36+
insert_final_newline = false

cms/admin/pageadmin.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1194,8 +1194,13 @@ def change_template(self, request, object_id):
11941194

11951195
to_template = request.POST.get("template", None)
11961196

1197-
if to_template not in dict(get_cms_setting('TEMPLATES')):
1198-
return HttpResponseBadRequest(_("Template not valid"))
1197+
if get_cms_setting('TEMPLATES'):
1198+
if to_template not in dict(get_cms_setting('TEMPLATES')):
1199+
return HttpResponseBadRequest(_("Template not valid"))
1200+
else:
1201+
if to_template not in (placeholder_set[0] for placeholder_set in get_cms_setting('PLACEHOLDERS')):
1202+
return HttpResponseBadRequest(_("Placeholder selection not valid"))
1203+
11991204

12001205
page_content.template = to_template
12011206
page_content.save()

cms/admin/placeholderadmin.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
import uuid
23
import warnings
34
from urllib.parse import parse_qsl, urlparse
@@ -17,7 +18,7 @@
1718
)
1819
from django.shortcuts import get_list_or_404, get_object_or_404, render
1920
from django.template.response import TemplateResponse
20-
from django.urls import re_path
21+
from django.urls import include, re_path
2122
from django.utils.decorators import method_decorator
2223
from django.utils.encoding import force_str
2324
from django.utils.html import conditional_escape
@@ -34,7 +35,7 @@
3435
from cms.models.pluginmodel import CMSPlugin
3536
from cms.plugin_pool import plugin_pool
3637
from cms.signals import post_placeholder_operation, pre_placeholder_operation
37-
from cms.toolbar.utils import get_plugin_tree_as_json
38+
from cms.toolbar.utils import get_plugin_tree
3839
from cms.utils import get_current_site
3940
from cms.utils.compat.warnings import RemovedInDjangoCMS50Warning
4041
from cms.utils.conf import get_cms_setting
@@ -222,6 +223,7 @@ def get_urls(self):
222223
def pat(regex, fn):
223224
return re_path(regex, self.admin_site.admin_view(fn), name=f"{info}_{fn.__name__}")
224225
url_patterns = [
226+
re_path(r'^cms_wizard/', include('cms.wizards.urls')),
225227
pat(r'^copy-plugins/$', self.copy_plugins),
226228
pat(r'^add-plugin/$', self.add_plugin),
227229
pat(r'^edit-plugin/([0-9]+)/$', self.edit_plugin),
@@ -452,8 +454,8 @@ def copy_plugins(self, request):
452454
source_placeholder,
453455
target_placeholder,
454456
)
455-
data = get_plugin_tree_as_json(request, new_plugins)
456-
return HttpResponse(data, content_type='application/json')
457+
data = get_plugin_tree(request, new_plugins)
458+
return HttpResponse(json.dumps(data), content_type='application/json')
457459

458460
def _copy_plugin_to_clipboard(self, request, target_placeholder):
459461
source_language = request.POST['source_language']
@@ -735,8 +737,8 @@ def move_plugin(self, request):
735737
if new_plugin and fetch_tree:
736738
root = (new_plugin.parent or new_plugin)
737739
new_plugins = [root] + list(root.get_descendants())
738-
data = get_plugin_tree_as_json(request, new_plugins)
739-
return HttpResponse(data, content_type='application/json')
740+
data = get_plugin_tree(request, new_plugins)
741+
return HttpResponse(json.dumps(data), content_type='application/json')
740742

741743
def _paste_plugin(self, request, plugin, target_language,
742744
target_placeholder, target_position, target_parent=None):

cms/appresolver.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
from django.core.exceptions import ImproperlyConfigured
55
from django.db import OperationalError, ProgrammingError
6-
from django.urls import Resolver404, URLResolver, reverse
6+
from django.urls import NoReverseMatch, Resolver404, URLResolver, reverse
77
from django.urls.resolvers import RegexPattern, URLPattern
88
from django.utils.translation import get_language, override
99

@@ -26,7 +26,10 @@ def applications_page_check(request):
2626
"""
2727
# We should get in this branch only if an apphook is active on /
2828
# This removes the non-CMS part of the URL.
29-
path = request.path_info.replace(reverse('pages-root'), '', 1)
29+
try:
30+
path = request.path_info.replace(reverse('pages-root'), '', 1)
31+
except NoReverseMatch:
32+
path = request.path_info
3033

3134
# check if application resolver can resolve this
3235
for lang in get_language_list():

cms/cms_toolbars.py

Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ def add_wizard_button(self):
9696
disabled = True
9797

9898
url = '{url}?page={page}&language={lang}&edit'.format(
99-
url=reverse("cms_wizard_create"),
99+
url=admin_reverse("cms_wizard_create"),
100100
page=page_pk,
101101
lang=self.toolbar.site_language,
102102
)
@@ -115,7 +115,8 @@ def render_object_editable_buttons(self):
115115
if self.toolbar.content_mode_active and self._can_add_button():
116116
self.add_edit_button()
117117
# Preview button
118-
if self.toolbar.edit_mode_active and self._can_add_button():
118+
if not self.toolbar.preview_mode_active and get_cms_setting('TEMPLATES') and self._can_add_button():
119+
# Only add preview button if there are templates available for previewing
119120
self.add_preview_button()
120121
# Structure mode
121122
if self._can_add_structure_mode():
@@ -184,14 +185,15 @@ def add_edit_button(self):
184185

185186
def add_preview_button(self):
186187
url = get_object_preview_url(self.toolbar.obj, language=self.toolbar.request_language)
187-
item = ButtonList(side=self.toolbar.RIGHT)
188-
item.add_button(
189-
_('Preview'),
190-
url=url,
191-
disabled=False,
192-
extra_classes=['cms-btn', 'cms-btn-switch-save'],
193-
)
194-
self.toolbar.add_item(item)
188+
if url:
189+
item = ButtonList(side=self.toolbar.RIGHT)
190+
item.add_button(
191+
_('Preview'),
192+
url=url,
193+
disabled=False,
194+
extra_classes=['cms-btn', 'cms-btn-switch-save'],
195+
)
196+
self.toolbar.add_item(item)
195197

196198
def add_structure_mode(self, extra_classes=('cms-toolbar-item-cms-mode-switcher',)):
197199
structure_active = self.toolbar.structure_mode_active
@@ -435,7 +437,10 @@ def get_on_delete_redirect_url(self):
435437

436438
# else redirect to root, do not redirect to Page.objects.get_home() because user could have deleted the last
437439
# page, if DEBUG == False this could cause a 404
438-
return reverse('pages-root')
440+
try:
441+
return reverse('pages-root')
442+
except NoReverseMatch:
443+
return admin_reverse("cms_pagecontent_changelist")
439444

440445
@property
441446
def title(self):
@@ -651,18 +656,25 @@ def add_page_menu(self):
651656
action = admin_reverse('cms_pagecontent_change_template', args=(self.page_content.pk,))
652657

653658
if can_change_advanced:
654-
templates_menu = current_page_menu.get_or_create_menu(
655-
'templates',
656-
_('Templates'),
657-
disabled=not can_change,
658-
)
659-
660-
for path, name in get_cms_setting('TEMPLATES'):
661-
active = self.page_content.template == path
662-
if path == TEMPLATE_INHERITANCE_MAGIC:
663-
templates_menu.add_break(TEMPLATE_MENU_BREAK)
664-
templates_menu.add_ajax_item(name, action=action, data={'template': path}, active=active,
665-
on_success=refresh)
659+
if get_cms_setting('TEMPLATES'):
660+
options = get_cms_setting('TEMPLATES')
661+
template_menu = _('Templates')
662+
else:
663+
options = [(placeholders[0], placeholders[2]) for placeholders in get_cms_setting('PLACEHOLDERS')]
664+
template_menu = _('Placeholders')
665+
if options:
666+
templates_menu = current_page_menu.get_or_create_menu(
667+
'templates',
668+
template_menu,
669+
disabled=not can_change,
670+
)
671+
672+
for path, name in options:
673+
active = self.page_content.template == path
674+
if path == TEMPLATE_INHERITANCE_MAGIC:
675+
templates_menu.add_break(TEMPLATE_MENU_BREAK)
676+
templates_menu.add_ajax_item(name, action=action, data={'template': path}, active=active,
677+
on_success=refresh)
666678

667679
# navigation toggle
668680
in_navigation = self.page_content.in_navigation

cms/models/contentmodels.py

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from django.conf import settings
12
from django.db import models
23
from django.utils import timezone
34
from django.utils.translation import gettext_lazy as _
@@ -16,7 +17,7 @@ class PageContent(models.Model):
1617
(constants.VISIBILITY_ANONYMOUS, _('for anonymous users only')),
1718
)
1819
TEMPLATE_DEFAULT = constants.TEMPLATE_INHERITANCE_MAGIC if get_cms_setting(
19-
'TEMPLATE_INHERITANCE') else get_cms_setting('TEMPLATES')[0][0]
20+
'TEMPLATE_INHERITANCE') else (get_cms_setting('TEMPLATES')[0][0] if get_cms_setting('TEMPLATES') else "")
2021

2122
X_FRAME_OPTIONS_CHOICES = (
2223
(constants.X_FRAME_OPTIONS_INHERIT, _('Inherit from parent page')),
@@ -198,6 +199,39 @@ def get_ancestor_titles(self):
198199
language=self.language,
199200
)
200201

202+
def get_placeholder_slots(self):
203+
"""
204+
Returns a list of placeholder slots for this page content object.
205+
"""
206+
if not get_cms_setting('PLACEHOLDERS'):
207+
return []
208+
if not hasattr(self, "_placeholder_slot_cache"):
209+
if self.template == constants.TEMPLATE_INHERITANCE_MAGIC:
210+
templates = (
211+
self
212+
.get_ancestor_titles()
213+
.exclude(template=constants.TEMPLATE_INHERITANCE_MAGIC)
214+
.order_by('-page__node__path')
215+
.values_list('template', flat=True)
216+
)
217+
if templates:
218+
placeholder_set = templates[0]
219+
else:
220+
placeholder_set = get_cms_setting('PLACEHOLDERS')[0][0]
221+
else:
222+
placeholder_set = self.template or get_cms_setting('PLACEHOLDERS')[0][0]
223+
224+
for key, value, _ in get_cms_setting("PLACEHOLDERS"):
225+
if key == placeholder_set or key == "": # NOQA: PLR1714 - Empty string matches always
226+
self._placeholder_slot_cache = value
227+
break
228+
else: # No matching placeholder list found
229+
self._placeholder_slot_cache = get_cms_setting('PLACEHOLDERS')[0][1]
230+
if isinstance(self._placeholder_slot_cache, str):
231+
# Accidentally a strong not a tuple? Make it a 1-element tuple
232+
self._placeholder_slot_cache = (self._placeholder_slot_cache,)
233+
return self._placeholder_slot_cache
234+
201235
def get_template(self):
202236
"""
203237
get the template of this page if defined or if closer parent if
@@ -206,6 +240,9 @@ def get_template(self):
206240
if hasattr(self, '_template_cache'):
207241
return self._template_cache
208242

243+
if not get_cms_setting("TEMPLATES"):
244+
return ""
245+
209246
if self.template != constants.TEMPLATE_INHERITANCE_MAGIC:
210247
self._template_cache = self.template or get_cms_setting('TEMPLATES')[0][0]
211248
return self._template_cache
@@ -221,7 +258,7 @@ def get_template(self):
221258
try:
222259
self._template_cache = templates[0]
223260
except IndexError:
224-
self._template_cache = get_cms_setting('TEMPLATES')[0][0]
261+
self._template_cache = get_cms_setting('TEMPLATES')[0][0] if get_cms_setting('TEMPLATES') else ""
225262
return self._template_cache
226263

227264
def get_template_name(self):
@@ -289,13 +326,17 @@ class EmptyPageContent:
289326
menu_title = ""
290327
page_title = ""
291328
xframe_options = None
292-
template = get_cms_setting('TEMPLATES')[0][0]
329+
template = None
293330
soft_root = False
294331
in_navigation = False
295332

296333
def __init__(self, language, page=None):
297334
self.language = language
298335
self.page = page
336+
if get_cms_setting("TEMPLATES"):
337+
self.template = get_cms_setting("TEMPLATES")[0][0]
338+
else:
339+
self.template = ""
299340

300341
def __bool__(self):
301342
return False

cms/models/pagemodel.py

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,10 @@
99
from django.db.models.base import ModelState
1010
from django.db.models.functions import Concat
1111
from django.forms import model_to_dict
12-
from django.urls import reverse
12+
from django.urls import NoReverseMatch, reverse
1313
from django.utils.encoding import force_str
1414
from django.utils.timezone import now
15-
from django.utils.translation import (
16-
get_language,
17-
gettext_lazy as _,
18-
override as force_language,
19-
)
15+
from django.utils.translation import get_language, gettext_lazy as _, override as force_language
2016
from treebeard.mp_tree import MP_Node
2117

2218
from cms import constants
@@ -367,10 +363,13 @@ def get_absolute_url(self, language=None, fallback=True):
367363
language = get_current_language()
368364

369365
with force_language(language):
370-
if self.is_home:
371-
return reverse('pages-root')
372-
path = self.get_path(language, fallback) or self.get_slug(language, fallback) # TODO: Disallow get_slug
373-
return reverse('pages-details-by-slug', kwargs={"slug": path}) if path else None
366+
try:
367+
if self.is_home:
368+
return reverse('pages-root')
369+
path = self.get_path(language, fallback) or self.get_slug(language, fallback) # TODO: Disallow get_slug
370+
return reverse('pages-details-by-slug', kwargs={"slug": path}) if path else None
371+
except NoReverseMatch:
372+
return None
374373

375374
def set_tree_node(self, site, target=None, position='first-child'):
376375
warnings.warn(
@@ -923,7 +922,7 @@ def get_template(self, language=None, fallback=True, force_reload=False):
923922
content = self.get_content_obj(language, fallback, force_reload)
924923
if content:
925924
return content.get_template()
926-
return get_cms_setting('TEMPLATES')[0][0]
925+
return get_cms_setting('TEMPLATES')[0][0] if get_cms_setting('TEMPLATES') else ""
927926

928927
def get_template_name(self):
929928
"""
@@ -978,9 +977,7 @@ def has_publish_permission(self, user):
978977
return user_can_publish_page(user, page=self)
979978

980979
def has_advanced_settings_permission(self, user):
981-
from cms.utils.page_permissions import (
982-
user_can_change_page_advanced_settings,
983-
)
980+
from cms.utils.page_permissions import user_can_change_page_advanced_settings
984981
return user_can_change_page_advanced_settings(user, page=self)
985982

986983
def has_change_permissions_permission(self, user):
@@ -1021,16 +1018,18 @@ def reload(self):
10211018
def rescan_placeholders(self, language):
10221019
return self.get_content_obj(language=language).rescan_placeholders()
10231020

1024-
def get_declared_placeholders(self):
1025-
# inline import to prevent circular imports
1026-
from cms.utils.placeholder import get_placeholders
1021+
def get_declared_placeholders(self, language=None, fallback=True, force_reload=False):
1022+
from cms.utils.placeholder import get_declared_placeholders_for_obj
10271023

1028-
return get_placeholders(self.get_template())
1024+
content = self.get_content_obj(language, fallback, force_reload)
1025+
if content:
1026+
return get_declared_placeholders_for_obj(content)
1027+
return []
10291028

10301029
def get_xframe_options(self, language=None, fallback=True, force_reload=False):
1031-
title = self.get_content_obj(language, fallback, force_reload)
1032-
if title:
1033-
return title.get_xframe_options()
1030+
content = self.get_content_obj(language, fallback, force_reload)
1031+
if content:
1032+
return content.get_xframe_options()
10341033

10351034
def get_soft_root(self, language=None, fallback=True, force_reload=False):
10361035
return self.get_page_content_obj_attribute("soft_root", language, fallback, force_reload)
@@ -1067,9 +1066,12 @@ def get_absolute_url(self, language=None, fallback=True):
10671066
language = get_current_language()
10681067

10691068
with force_language(language):
1070-
if self.path == '':
1071-
return reverse('pages-root')
1072-
return reverse('pages-details-by-slug', kwargs={"slug": self.path})
1069+
try:
1070+
if self.path == '':
1071+
return reverse('pages-root')
1072+
return reverse('pages-details-by-slug', kwargs={"slug": self.path})
1073+
except NoReverseMatch:
1074+
return None
10731075

10741076
def get_path_for_base(self, base_path=''):
10751077
old_base, sep, slug = self.path.rpartition('/')

0 commit comments

Comments
 (0)
0