Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Headless readiness #7850

Open
wants to merge 34 commits into
base: develop-4
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
cf26a48
Feat: Move data bridge data to script tags for easier extraction.
fsbraun Mar 21, 2024
600f85a
Fix linting
fsbraun Mar 21, 2024
d0574ed
Move json script before script reading it to avoid race conditions
fsbraun Mar 21, 2024
798829e
lean template
fsbraun Mar 21, 2024
29b7e74
Merge branch 'develop-4' into feat/data-bridge
fsbraun Mar 28, 2024
3c01d6a
Merge branch 'develop-4' into feat/data-bridge
fsbraun Apr 5, 2024
50680e9
Move wizard urls to admin namespace
fsbraun May 7, 2024
73cad7b
Merge branch 'develop-4' into feat/data-bridge
fsbraun May 7, 2024
eaa4edd
Fix linting errors
fsbraun May 7, 2024
c73dc50
Update appresolver and fix page middleware (though no need to use in…
fsbraun May 7, 2024
d733f59
fix: use admin_reverse instead of reverse('admin: ...
fsbraun May 7, 2024
fd2e9be
Update preview condition for toolbar
fsbraun May 8, 2024
6c60e12
Self-contained structure mode css
fsbraun May 8, 2024
23c4d9c
Stay in 4.1.x
fsbraun May 8, 2024
e0edb6f
Merge branch 'develop-4' into feat/data-bridge
fsbraun May 9, 2024
baff66e
Alles structure endpoint for read-only objects
fsbraun May 10, 2024
2872478
fix: linting
fsbraun May 10, 2024
6c65df1
more linting
fsbraun May 10, 2024
851796f
Merge branch 'develop-4' into feat/data-bridge
vinitkumar May 20, 2024
700e537
feat: Allow running without templates
fsbraun May 22, 2024
0cb5fed
Merge branch 'feat/data-bridge' of github.com:fsbraun/django-cms into…
fsbraun May 22, 2024
c50e1a4
Merge branch 'develop-4' into feat/data-bridge
fsbraun May 22, 2024
d5170a4
Add unformatted preview for headless mode
fsbraun May 23, 2024
b73febb
Merge branch 'feat/data-bridge' of github.com:fsbraun/django-cms into…
fsbraun May 23, 2024
3ebffaa
Fix: Advanced placeholder config
fsbraun May 23, 2024
5dbd7ef
Fix: tests
fsbraun May 23, 2024
deff31c
Fix {% render_placeholder %} tag
fsbraun May 23, 2024
46d3da1
Merge branch 'develop-4' into feat/data-bridge
fsbraun May 25, 2024
fc6bee8
Merge branch 'develop-4' into feat/data-bridge
fsbraun May 30, 2024
f35a9d3
Merge branch 'develop-4' into feat/data-bridge
fsbraun Jun 11, 2024
1c61cb7
Add docs
fsbraun Jun 15, 2024
5721225
Merge branch 'feat/data-bridge' of github.com:fsbraun/django-cms into…
fsbraun Jun 15, 2024
cbdae33
Fix typos
fsbraun Jun 15, 2024
f6d8854
Merge branch 'develop-4' into feat/data-bridge
fsbraun Jun 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,6 @@ indent_size = 2

[*.yml]
indent_size = 2

[*.html]
insert_final_newline = false
9 changes: 7 additions & 2 deletions cms/admin/pageadmin.py
Original file line number Diff line number Diff line change
Expand Up @@ -1194,8 +1194,13 @@ def change_template(self, request, object_id):

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

if to_template not in dict(get_cms_setting('TEMPLATES')):
return HttpResponseBadRequest(_("Template not valid"))
if get_cms_setting('TEMPLATES'):
if to_template not in dict(get_cms_setting('TEMPLATES')):
return HttpResponseBadRequest(_("Template not valid"))
else:
if to_template not in (placeholder_set[0] for placeholder_set in get_cms_setting('PLACEHOLDERS')):
return HttpResponseBadRequest(_("Placeholder selection not valid"))


page_content.template = to_template
page_content.save()
Expand Down
14 changes: 8 additions & 6 deletions cms/admin/placeholderadmin.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
import uuid
import warnings
from urllib.parse import parse_qsl, urlparse
Expand All @@ -17,7 +18,7 @@
)
from django.shortcuts import get_list_or_404, get_object_or_404, render
from django.template.response import TemplateResponse
from django.urls import re_path
from django.urls import include, re_path
from django.utils.decorators import method_decorator
from django.utils.encoding import force_str
from django.utils.html import conditional_escape
Expand All @@ -34,7 +35,7 @@
from cms.models.pluginmodel import CMSPlugin
from cms.plugin_pool import plugin_pool
from cms.signals import post_placeholder_operation, pre_placeholder_operation
from cms.toolbar.utils import get_plugin_tree_as_json
from cms.toolbar.utils import get_plugin_tree
from cms.utils import get_current_site
from cms.utils.compat.warnings import RemovedInDjangoCMS50Warning
from cms.utils.conf import get_cms_setting
Expand Down Expand Up @@ -221,6 +222,7 @@ def get_urls(self):
def pat(regex, fn):
return re_path(regex, self.admin_site.admin_view(fn), name="%s_%s" % (info, fn.__name__))
url_patterns = [
re_path(r'^cms_wizard/', include('cms.wizards.urls')),
pat(r'^copy-plugins/$', self.copy_plugins),
pat(r'^add-plugin/$', self.add_plugin),
pat(r'^edit-plugin/([0-9]+)/$', self.edit_plugin),
Expand Down Expand Up @@ -451,8 +453,8 @@ def copy_plugins(self, request):
source_placeholder,
target_placeholder,
)
data = get_plugin_tree_as_json(request, new_plugins)
return HttpResponse(data, content_type='application/json')
data = get_plugin_tree(request, new_plugins)
return HttpResponse(json.dumps(data), content_type='application/json')

def _copy_plugin_to_clipboard(self, request, target_placeholder):
source_language = request.POST['source_language']
Expand Down Expand Up @@ -734,8 +736,8 @@ def move_plugin(self, request):
if new_plugin and fetch_tree:
root = (new_plugin.parent or new_plugin)
new_plugins = [root] + list(root.get_descendants())
data = get_plugin_tree_as_json(request, new_plugins)
return HttpResponse(data, content_type='application/json')
data = get_plugin_tree(request, new_plugins)
return HttpResponse(json.dumps(data), content_type='application/json')

def _paste_plugin(self, request, plugin, target_language,
target_placeholder, target_position, target_parent=None):
Expand Down
7 changes: 5 additions & 2 deletions cms/appresolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from django.core.exceptions import ImproperlyConfigured
from django.db import OperationalError, ProgrammingError
from django.urls import Resolver404, URLResolver, reverse
from django.urls import NoReverseMatch, Resolver404, URLResolver, reverse
from django.urls.resolvers import RegexPattern, URLPattern
from django.utils.translation import get_language, override

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

# check if application resolver can resolve this
for lang in get_language_list():
Expand Down
58 changes: 35 additions & 23 deletions cms/cms_toolbars.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ def add_wizard_button(self):
disabled = True

url = '{url}?page={page}&language={lang}&edit'.format(
url=reverse("cms_wizard_create"),
url=admin_reverse("cms_wizard_create"),
page=page_pk,
lang=self.toolbar.site_language,
)
Expand All @@ -115,7 +115,8 @@ def render_object_editable_buttons(self):
if self.toolbar.content_mode_active and self._can_add_button():
self.add_edit_button()
# Preview button
if self.toolbar.edit_mode_active and self._can_add_button():
if not self.toolbar.preview_mode_active and get_cms_setting('TEMPLATES') and self._can_add_button():
# Only add preview button if there are templates available for previewing
self.add_preview_button()
# Structure mode
if self._can_add_structure_mode():
Expand Down Expand Up @@ -184,14 +185,15 @@ def add_edit_button(self):

def add_preview_button(self):
url = get_object_preview_url(self.toolbar.obj, language=self.toolbar.request_language)
item = ButtonList(side=self.toolbar.RIGHT)
item.add_button(
_('Preview'),
url=url,
disabled=False,
extra_classes=['cms-btn', 'cms-btn-switch-save'],
)
self.toolbar.add_item(item)
if url:
item = ButtonList(side=self.toolbar.RIGHT)
item.add_button(
_('Preview'),
url=url,
disabled=False,
extra_classes=['cms-btn', 'cms-btn-switch-save'],
)
self.toolbar.add_item(item)

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

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

@property
def title(self):
Expand Down Expand Up @@ -651,18 +656,25 @@ def add_page_menu(self):
action = admin_reverse('cms_pagecontent_change_template', args=(self.page_content.pk,))

if can_change_advanced:
templates_menu = current_page_menu.get_or_create_menu(
'templates',
_('Templates'),
disabled=not can_change,
)

for path, name in get_cms_setting('TEMPLATES'):
active = self.page_content.template == path
if path == TEMPLATE_INHERITANCE_MAGIC:
templates_menu.add_break(TEMPLATE_MENU_BREAK)
templates_menu.add_ajax_item(name, action=action, data={'template': path}, active=active,
on_success=refresh)
if get_cms_setting('TEMPLATES'):
options = get_cms_setting('TEMPLATES')
template_menu = _('Templates')
else:
options = [(placeholders[0], placeholders[2]) for placeholders in get_cms_setting('PLACEHOLDERS')]
template_menu = _('Placeholders')
if options:
templates_menu = current_page_menu.get_or_create_menu(
'templates',
template_menu,
disabled=not can_change,
)

for path, name in options:
active = self.page_content.template == path
if path == TEMPLATE_INHERITANCE_MAGIC:
templates_menu.add_break(TEMPLATE_MENU_BREAK)
templates_menu.add_ajax_item(name, action=action, data={'template': path}, active=active,
on_success=refresh)

# navigation toggle
in_navigation = self.page_content.in_navigation
Expand Down
44 changes: 41 additions & 3 deletions cms/models/contentmodels.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from django.conf import settings
from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
Expand All @@ -16,7 +17,7 @@ class PageContent(models.Model):
(constants.VISIBILITY_ANONYMOUS, _('for anonymous users only')),
)
TEMPLATE_DEFAULT = constants.TEMPLATE_INHERITANCE_MAGIC if get_cms_setting(
'TEMPLATE_INHERITANCE') else get_cms_setting('TEMPLATES')[0][0]
'TEMPLATE_INHERITANCE') else (get_cms_setting('TEMPLATES')[0][0] if get_cms_setting('TEMPLATES') else "")

X_FRAME_OPTIONS_CHOICES = (
(constants.X_FRAME_OPTIONS_INHERIT, _('Inherit from parent page')),
Expand Down Expand Up @@ -194,6 +195,36 @@ def get_ancestor_titles(self):
language=self.language,
)

def get_placeholder_slots(self):
"""
Returns a list of placeholder slots for this page content object.
"""
if not get_cms_setting('PLACEHOLDERS'):
return []
if not hasattr(self, "_placeholder_slot_cache"):
if self.template == constants.TEMPLATE_INHERITANCE_MAGIC:
templates = (
self
.get_ancestor_titles()
.exclude(template=constants.TEMPLATE_INHERITANCE_MAGIC)
.order_by('-page__node__path')
.values_list('template', flat=True)
)
if templates:
placeholder_set = templates[0]
else:
placeholder_set = get_cms_setting('PLACEHOLDERS')[0][0]
else:
placeholder_set = self.template or get_cms_setting('PLACEHOLDERS')[0][0]

for key, value, _ in get_cms_setting("PLACEHOLDERS"):
if key == placeholder_set or key == "": # NOQA: PLR1714 - Empty string matches always
self._placeholder_slot_cache = value
break
else: # No matching placeholder list found
self._placeholder_slot_cache = get_cms_setting('PLACEHOLDERS')[0][1]
return self._placeholder_slot_cache

def get_template(self):
"""
get the template of this page if defined or if closer parent if
Expand All @@ -202,6 +233,9 @@ def get_template(self):
if hasattr(self, '_template_cache'):
return self._template_cache

if not get_cms_setting("TEMPLATES"):
return ""

if self.template != constants.TEMPLATE_INHERITANCE_MAGIC:
self._template_cache = self.template or get_cms_setting('TEMPLATES')[0][0]
return self._template_cache
Expand All @@ -217,7 +251,7 @@ def get_template(self):
try:
self._template_cache = templates[0]
except IndexError:
self._template_cache = get_cms_setting('TEMPLATES')[0][0]
self._template_cache = get_cms_setting('TEMPLATES')[0][0] if get_cms_setting('TEMPLATES') else ""
return self._template_cache

def get_template_name(self):
Expand Down Expand Up @@ -285,13 +319,17 @@ class EmptyPageContent:
menu_title = ""
page_title = ""
xframe_options = None
template = get_cms_setting('TEMPLATES')[0][0]
template = None
soft_root = False
in_navigation = False

def __init__(self, language, page=None):
self.language = language
self.page = page
if get_cms_setting("TEMPLATES"):
self.template = get_cms_setting("TEMPLATES")[0][0]
else:
self.template = ""

def __bool__(self):
return False
Expand Down
50 changes: 26 additions & 24 deletions cms/models/pagemodel.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,11 @@
from django.db.models.base import ModelState
from django.db.models.functions import Concat
from django.forms import model_to_dict
from django.urls import reverse
from django.urls import NoReverseMatch, reverse
from django.utils.encoding import force_str
from django.utils.functional import cached_property
from django.utils.timezone import now
from django.utils.translation import (
get_language,
gettext_lazy as _,
override as force_language,
)
from django.utils.translation import get_language, gettext_lazy as _, override as force_language
from treebeard.mp_tree import MP_Node

from cms import constants
Expand Down Expand Up @@ -361,10 +357,13 @@ def get_absolute_url(self, language=None, fallback=True):
language = get_current_language()

with force_language(language):
if self.is_home:
return reverse('pages-root')
path = self.get_path(language, fallback) or self.get_slug(language, fallback) # TODO: Disallow get_slug
return reverse('pages-details-by-slug', kwargs={"slug": path}) if path else None
try:
if self.is_home:
return reverse('pages-root')
path = self.get_path(language, fallback) or self.get_slug(language, fallback) # TODO: Disallow get_slug
return reverse('pages-details-by-slug', kwargs={"slug": path}) if path else None
except NoReverseMatch:
return None

def set_tree_node(self, site, target=None, position='first-child'):
assert position in ('last-child', 'first-child', 'left', 'right')
Expand Down Expand Up @@ -946,7 +945,7 @@ def get_template(self, language=None, fallback=True, force_reload=False):
content = self.get_content_obj(language, fallback, force_reload)
if content:
return content.get_template()
return get_cms_setting('TEMPLATES')[0][0]
return get_cms_setting('TEMPLATES')[0][0] if get_cms_setting('TEMPLATES') else ""

def get_template_name(self):
"""
Expand Down Expand Up @@ -1001,9 +1000,7 @@ def has_publish_permission(self, user):
return user_can_publish_page(user, page=self)

def has_advanced_settings_permission(self, user):
from cms.utils.page_permissions import (
user_can_change_page_advanced_settings,
)
from cms.utils.page_permissions import user_can_change_page_advanced_settings
return user_can_change_page_advanced_settings(user, page=self)

def has_change_permissions_permission(self, user):
Expand Down Expand Up @@ -1044,16 +1041,18 @@ def reload(self):
def rescan_placeholders(self, language):
return self.get_content_obj(language=language).rescan_placeholders()

def get_declared_placeholders(self):
# inline import to prevent circular imports
from cms.utils.placeholder import get_placeholders
def get_declared_placeholders(self, language=None, fallback=True, force_reload=False):
from cms.utils.placeholder import get_declared_placeholders_for_obj

return get_placeholders(self.get_template())
content = self.get_content_obj(language, fallback, force_reload)
if content:
return get_declared_placeholders_for_obj(content)
return []

def get_xframe_options(self, language=None, fallback=True, force_reload=False):
title = self.get_content_obj(language, fallback, force_reload)
if title:
return title.get_xframe_options()
content = self.get_content_obj(language, fallback, force_reload)
if content:
return content.get_xframe_options()

def get_soft_root(self, language=None, fallback=True, force_reload=False):
return self.get_page_content_obj_attribute("soft_root", language, fallback, force_reload)
Expand Down Expand Up @@ -1090,9 +1089,12 @@ def get_absolute_url(self, language=None, fallback=True):
language = get_current_language()

with force_language(language):
if self.path == '':
return reverse('pages-root')
return reverse('pages-details-by-slug', kwargs={"slug": self.path})
try:
if self.path == '':
return reverse('pages-root')
return reverse('pages-details-by-slug', kwargs={"slug": self.path})
except NoReverseMatch:
return None

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