forked from ungleich-public/dynamicweb
clone cms with menus, make menus standalone app
This commit is contained in:
parent
1ece5ce7c7
commit
afb096b7a3
1153 changed files with 318897 additions and 37 deletions
0
cms/forms/__init__.py
Normal file
0
cms/forms/__init__.py
Normal file
116
cms/forms/fields.py
Normal file
116
cms/forms/fields.py
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
from django import forms
|
||||
from django.contrib.admin.widgets import RelatedFieldWidgetWrapper
|
||||
from django.core.validators import EMPTY_VALUES
|
||||
from django.forms import ChoiceField
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from cms.forms.utils import get_page_choices, get_site_choices
|
||||
from cms.forms.validators import validate_url
|
||||
from cms.forms.widgets import PageSelectWidget, PageSmartLinkWidget
|
||||
from cms.models.pagemodel import Page
|
||||
|
||||
|
||||
class PageSelectFormField(forms.MultiValueField):
|
||||
"""
|
||||
Behaves like a :class:`django.forms.ModelChoiceField` field for the
|
||||
:class:`cms.models.pagemodel.Page` model, but displays itself as a split
|
||||
field with a select drop-down for the site and one for the page. It also
|
||||
indents the page names based on what level they're on, so that the page
|
||||
select drop-down is easier to use. This takes the same arguments as
|
||||
:class:`django.forms.ModelChoiceField`.
|
||||
"""
|
||||
widget = PageSelectWidget
|
||||
default_error_messages = {
|
||||
'invalid_site': _('Select a valid site'),
|
||||
'invalid_page': _('Select a valid page'),
|
||||
}
|
||||
|
||||
def __init__(self, queryset=None, empty_label="---------", cache_choices=False,
|
||||
required=True, widget=None, to_field_name=None, limit_choices_to=None, *args, **kwargs):
|
||||
errors = self.default_error_messages.copy()
|
||||
if 'error_messages' in kwargs:
|
||||
errors.update(kwargs['error_messages'])
|
||||
self.limit_choices_to = limit_choices_to
|
||||
kwargs['required'] = required
|
||||
fields = (
|
||||
ChoiceField(choices=get_site_choices, required=False, error_messages={'invalid': errors['invalid_site']}),
|
||||
ChoiceField(choices=get_page_choices, required=False, error_messages={'invalid': errors['invalid_page']}),
|
||||
)
|
||||
|
||||
# Remove the unexpected blank kwarg if it's supplied,
|
||||
# causes an error where the MultiValueField doesn't expect it
|
||||
# https://github.com/django/django/commit/da79ee472d803963dc3ea81ee67767dc06068aac
|
||||
if 'blank' in kwargs:
|
||||
del kwargs['blank']
|
||||
|
||||
super().__init__(fields, *args, **kwargs)
|
||||
|
||||
def compress(self, data_list):
|
||||
if data_list:
|
||||
page_id = data_list[1]
|
||||
|
||||
if page_id in EMPTY_VALUES:
|
||||
if not self.required:
|
||||
return None
|
||||
raise forms.ValidationError(self.error_messages['invalid_page'])
|
||||
return Page.objects.get(pk=page_id)
|
||||
return None
|
||||
|
||||
def has_changed(self, initial, data):
|
||||
is_empty = data and (len(data) >= 2 and data[1] in [None, ''])
|
||||
|
||||
if isinstance(self.widget, RelatedFieldWidgetWrapper):
|
||||
self.widget.decompress = self.widget.widget.decompress
|
||||
|
||||
if is_empty and initial is None:
|
||||
# when empty data will have [u'1', u'', u''] as value
|
||||
# this will cause django to always return True because of the '1'
|
||||
# so we simply follow django's default behavior when initial is None and data is "empty"
|
||||
data = ['' for x in range(0, len(data))]
|
||||
return super().has_changed(initial, data)
|
||||
|
||||
def _has_changed(self, initial, data):
|
||||
return self.has_changed(initial, data)
|
||||
|
||||
|
||||
class PageSmartLinkField(forms.CharField):
|
||||
"""
|
||||
A field making use of ``cms.forms.widgets.PageSmartLinkWidget``.
|
||||
This field will offer you a list of matching internal pages as you type.
|
||||
You can either pick one or enter an arbitrary URL to create a non-existing entry.
|
||||
Takes a `placeholder_text` argument to define the text displayed inside the
|
||||
input before you type.
|
||||
|
||||
The widget uses an ajax request to try to find pages match. It will try to find
|
||||
case-insensitive matches amongst public and published pages on the `title`, `path`,
|
||||
`page_title`, `menu_title` fields.
|
||||
|
||||
.. note::
|
||||
|
||||
``PageSmartLinkField`` stores its value as a plain URL string, so links
|
||||
do not follow pages that are later moved or renamed. For dynamically
|
||||
linking to pages, other CMS content, or external URLs, the preferred
|
||||
approach is the ``LinkField`` provided by
|
||||
`djangocms-link <https://github.com/django-cms/djangocms-link>`_, which
|
||||
keeps references stable across moves and supports a configurable set of
|
||||
link targets.
|
||||
"""
|
||||
widget = PageSmartLinkWidget
|
||||
default_validators = [validate_url]
|
||||
|
||||
def __init__(self, max_length=None, min_length=None, placeholder_text=None,
|
||||
ajax_view=None, *args, **kwargs):
|
||||
self.placeholder_text = placeholder_text
|
||||
widget = self.widget(ajax_view=ajax_view)
|
||||
super().__init__(
|
||||
max_length=max_length, min_length=min_length, widget=widget, *args, **kwargs
|
||||
)
|
||||
|
||||
def widget_attrs(self, widget):
|
||||
attrs = super().widget_attrs(widget)
|
||||
attrs.update({'placeholder_text': self.placeholder_text})
|
||||
return attrs
|
||||
|
||||
def clean(self, value):
|
||||
value = self.to_python(value).strip()
|
||||
return super().clean(value)
|
||||
15
cms/forms/login.py
Normal file
15
cms/forms/login.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
from django import forms
|
||||
from django.contrib.auth.forms import AuthenticationForm
|
||||
|
||||
|
||||
class CMSToolbarLoginForm(AuthenticationForm):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
kwargs['prefix'] = kwargs.get('prefix', 'cms')
|
||||
self.fields['username'].widget = forms.TextInput(
|
||||
attrs={'required': 'required'}
|
||||
)
|
||||
self.fields['password'].widget = forms.PasswordInput(
|
||||
attrs={'required': 'required'}
|
||||
)
|
||||
107
cms/forms/utils.py
Normal file
107
cms/forms/utils.py
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
from django.contrib.sites.models import Site
|
||||
from django.db.models import Prefetch
|
||||
from django.db.models.signals import post_delete, post_save
|
||||
from django.utils.html import escape
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from cms.cache.choices import (
|
||||
_page_cache_key,
|
||||
_site_cache_key,
|
||||
clean_page_choices_cache,
|
||||
clean_site_choices_cache,
|
||||
)
|
||||
from cms.models import Page
|
||||
from cms.utils import i18n
|
||||
|
||||
|
||||
def get_sites():
|
||||
sites = (
|
||||
Site
|
||||
.objects
|
||||
.filter(djangocms_pages__isnull=False)
|
||||
.order_by('name')
|
||||
.distinct()
|
||||
)
|
||||
return sites
|
||||
|
||||
|
||||
def get_page_choices_for_site(site, language):
|
||||
from cms.models import PageContent
|
||||
|
||||
fallbacks = i18n.get_fallback_languages(language, site_id=site.pk)
|
||||
languages = [language] + fallbacks
|
||||
translation_lookup = Prefetch(
|
||||
'pagecontent_set',
|
||||
to_attr='filtered_translations',
|
||||
queryset=PageContent.objects.filter(language__in=languages).only('pk', 'page', 'language', 'title')
|
||||
)
|
||||
pages = (
|
||||
Page
|
||||
.objects
|
||||
.on_site(site)
|
||||
.prefetch_related(translation_lookup)
|
||||
.order_by('path')
|
||||
.only('pk')
|
||||
)
|
||||
|
||||
for page in pages:
|
||||
translations = page.filtered_translations
|
||||
pagecontent_by_language = {trans.language: trans.title for trans in translations}
|
||||
|
||||
for lang in languages:
|
||||
# EmptyPageContent is used to prevent the cms from trying
|
||||
# to find a translation in the database
|
||||
if lang in pagecontent_by_language:
|
||||
title = pagecontent_by_language[lang]
|
||||
indent = " " * (page.depth - 1)
|
||||
label = mark_safe(f"{indent}{escape(title)}")
|
||||
yield (page.pk, label)
|
||||
break
|
||||
|
||||
|
||||
def update_site_and_page_choices(language=None):
|
||||
if language is None:
|
||||
language = i18n.get_current_language()
|
||||
|
||||
site_choices = []
|
||||
page_choices = [('', '----')]
|
||||
site_choices_key = _site_cache_key(language)
|
||||
page_choices_key = _page_cache_key(language)
|
||||
|
||||
for site in get_sites():
|
||||
_page_choices = list(get_page_choices_for_site(site, language))
|
||||
site_choices.append((site.pk, site.name))
|
||||
page_choices.append((site.name, _page_choices))
|
||||
|
||||
from django.core.cache import cache
|
||||
|
||||
# We set it to 1 day here because we actively invalidate this cache.
|
||||
cache.set(site_choices_key, site_choices, 86400)
|
||||
cache.set(page_choices_key, page_choices, 86400)
|
||||
return site_choices, page_choices
|
||||
|
||||
|
||||
def get_site_choices(lang=None):
|
||||
from django.core.cache import cache
|
||||
|
||||
lang = lang or i18n.get_current_language()
|
||||
site_choices = cache.get(_site_cache_key(lang))
|
||||
if site_choices is None:
|
||||
site_choices, _ = update_site_and_page_choices(lang)
|
||||
return site_choices
|
||||
|
||||
|
||||
def get_page_choices(lang=None):
|
||||
from django.core.cache import cache
|
||||
|
||||
lang = lang or i18n.get_current_language()
|
||||
page_choices = cache.get(_page_cache_key(lang))
|
||||
if page_choices is None:
|
||||
_, page_choices = update_site_and_page_choices(lang)
|
||||
return page_choices
|
||||
|
||||
|
||||
post_save.connect(clean_page_choices_cache, sender=Page)
|
||||
post_save.connect(clean_site_choices_cache, sender=Site)
|
||||
post_delete.connect(clean_page_choices_cache, sender=Page)
|
||||
post_delete.connect(clean_site_choices_cache, sender=Site)
|
||||
117
cms/forms/validators.py
Normal file
117
cms/forms/validators.py
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
from typing import TYPE_CHECKING, Optional
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import RegexValidator, URLValidator
|
||||
from django.utils.html import escape
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext
|
||||
|
||||
from cms.utils.urlutils import admin_reverse, path_segment_regex, relative_url_regex
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# Only needed for type hinting - avoid circular import
|
||||
from cms.models.pagemodel import Page
|
||||
|
||||
|
||||
def validate_relative_url(value):
|
||||
RegexValidator(regex=relative_url_regex)(value)
|
||||
|
||||
|
||||
def validate_url(value):
|
||||
if "<" in value or ">" in value:
|
||||
# ``relative_url_regex`` excludes angle brackets, but the absolute fallback below
|
||||
# is Django's ``URLValidator``, whose path pattern accepts them as long as the
|
||||
# value carries no space (``https://example.com/<script>alert(1)</script>``).
|
||||
# Reject them before either branch runs, so markup cannot enter through a scheme.
|
||||
raise ValidationError(
|
||||
gettext("Enter a valid relative or absolute URL."),
|
||||
code="invalid",
|
||||
)
|
||||
|
||||
try:
|
||||
# Validate relative urls first
|
||||
validate_relative_url(value)
|
||||
except ValidationError:
|
||||
# Fallback to absolute urls
|
||||
URLValidator()(value)
|
||||
else:
|
||||
# ``relative_url_regex`` only constrains the *characters* of a relative
|
||||
# path, not the part before the first slash. Values such as
|
||||
# ``javascript:alert(1)/x``, ``data:text/html,...`` or ``vbscript:.../x``
|
||||
# therefore match the relative branch while still carrying a scheme that
|
||||
# executes script when the value is later rendered into an ``href``
|
||||
# (CWE-79). A genuine relative URL has no scheme, so reject any value
|
||||
# that does. (Off-site targets such as ``https://example.com`` or
|
||||
# ``//example.com`` are intentionally permitted -- this field allows
|
||||
# absolute redirects -- and navigate rather than execute script.)
|
||||
if urlsplit(value).scheme:
|
||||
raise ValidationError(
|
||||
gettext("Enter a valid relative or absolute URL."),
|
||||
code="invalid",
|
||||
)
|
||||
|
||||
|
||||
def validate_path_segment(value):
|
||||
"""Validate a single path segment, i.e. a path that contains no "/"."""
|
||||
RegexValidator(
|
||||
regex=path_segment_regex,
|
||||
message=gettext("Enter a valid relative or absolute URL."),
|
||||
code="invalid",
|
||||
)(value)
|
||||
|
||||
|
||||
def validate_path(value):
|
||||
"""Validate a page path.
|
||||
|
||||
Unlike :func:`validate_url` this also accepts a single path segment such as ``contact``,
|
||||
which ``relative_url_regex`` -- and therefore :func:`validate_url` -- never matches.
|
||||
"""
|
||||
if "/" in value:
|
||||
validate_url(value)
|
||||
else:
|
||||
validate_path_segment(value)
|
||||
|
||||
|
||||
def validate_url_uniqueness(
|
||||
site, path: str, language: str, user_language: str | None = None, exclude_page: Optional["Page"] = None
|
||||
):
|
||||
"""Checks for conflicting urls"""
|
||||
from cms.models.pagemodel import PageUrl
|
||||
|
||||
# Single-segment paths used to skip validation entirely, so an "Overwrite URL" without a
|
||||
# "/" could hold arbitrary characters.
|
||||
validate_path(path)
|
||||
|
||||
path = path.strip("/")
|
||||
page_urls = PageUrl.objects.get_for_site(site, language=language).filter(path=path)
|
||||
|
||||
if exclude_page:
|
||||
page_urls = page_urls.exclude(page=exclude_page.pk)
|
||||
|
||||
try:
|
||||
conflict_page = page_urls[0].page
|
||||
except IndexError:
|
||||
return True
|
||||
|
||||
conflict_translation = conflict_page.get_content_obj(language, fallback=False)
|
||||
|
||||
if conflict_translation: # No empty page content
|
||||
change_url = admin_reverse("cms_pagecontent_change", args=[conflict_translation.pk])
|
||||
else:
|
||||
change_url = "" # Empty page has no slug
|
||||
if user_language:
|
||||
change_url += f"?language={user_language}"
|
||||
|
||||
conflict_url = f'<a href="{change_url}" target="_blank">{escape(str(conflict_translation.title))}</a>'
|
||||
|
||||
if exclude_page:
|
||||
message = gettext("Page %(conflict_page)s has the same url '%(url)s' as current page \"%(instance)s\".")
|
||||
else:
|
||||
message = gettext("Page %(conflict_page)s has the same url '%(url)s' as current page.")
|
||||
message = message % {
|
||||
"conflict_page": conflict_url,
|
||||
"url": escape(path),
|
||||
"instance": escape(exclude_page.get_title(language)) if exclude_page else "",
|
||||
}
|
||||
raise ValidationError(mark_safe(message))
|
||||
249
cms/forms/widgets.py
Normal file
249
cms/forms/widgets.py
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
from django.contrib.auth import get_permission_codename
|
||||
from django.contrib.sites.models import Site
|
||||
from django.forms.widgets import MultiWidget, Select, TextInput
|
||||
from django.templatetags.static import static
|
||||
from django.urls import NoReverseMatch, reverse_lazy
|
||||
from django.utils.encoding import force_str
|
||||
from django.utils.html import escape, format_html
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from cms.forms.utils import get_page_choices, get_site_choices
|
||||
from cms.models import Page, PageUser
|
||||
from cms.utils.urlutils import admin_reverse, static_with_version
|
||||
|
||||
|
||||
class PageSelectWidget(MultiWidget):
|
||||
"""A widget that allows selecting a page by first selecting a site and then
|
||||
a page on that site in a two-step process.
|
||||
"""
|
||||
template_name = 'cms/widgets/pageselectwidget.html'
|
||||
|
||||
class Media:
|
||||
js = (
|
||||
static_with_version('cms/js/dist/bundle.forms.pageselectwidget.min.js'),
|
||||
)
|
||||
|
||||
def __init__(self, site_choices=None, page_choices=None, attrs=None):
|
||||
if attrs is not None:
|
||||
self.attrs = attrs.copy()
|
||||
else:
|
||||
self.attrs = {}
|
||||
self.choices = []
|
||||
super().__init__((Select, Select, Select), attrs)
|
||||
|
||||
def decompress(self, value):
|
||||
"""
|
||||
receives a page_id in value and returns the site_id and page_id
|
||||
of that page or the current site_id and None if no page_id is given.
|
||||
"""
|
||||
if value:
|
||||
page = Page.objects.get(pk=value)
|
||||
return [page.site_id, page.pk, page.pk]
|
||||
site = Site.objects.first()
|
||||
return [site.pk, None, None]
|
||||
|
||||
def _has_changed(self, initial, data):
|
||||
# THIS IS A COPY OF django.forms.widgets.Widget._has_changed()
|
||||
# (except for the first if statement)
|
||||
|
||||
"""
|
||||
Return True if data differs from initial.
|
||||
"""
|
||||
# For purposes of seeing whether something has changed, None is
|
||||
# the same as an empty string, if the data or initial value we get
|
||||
# is None, replace it w/ ''.
|
||||
if data is None or (len(data) >= 2 and data[1] in [None, '']):
|
||||
data_value = ''
|
||||
else:
|
||||
data_value = data
|
||||
if initial is None:
|
||||
initial_value = ''
|
||||
else:
|
||||
initial_value = initial
|
||||
if force_str(initial_value) != force_str(data_value):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _build_widgets(self):
|
||||
site_choices = get_site_choices()
|
||||
page_choices = get_page_choices()
|
||||
self.site_choices = site_choices
|
||||
self.choices = page_choices
|
||||
self.widgets = (
|
||||
Select(choices=site_choices),
|
||||
Select(choices=[('', '----')]),
|
||||
Select(choices=self.choices, attrs={'style': "display:none;"}),
|
||||
)
|
||||
|
||||
def get_context(self, name, value, attrs):
|
||||
self._build_widgets()
|
||||
context = super().get_context(name, value, attrs)
|
||||
context['widget']['script_data'] = {"name": name}
|
||||
return context
|
||||
|
||||
def format_output(self, rendered_widgets):
|
||||
return ' '.join(rendered_widgets)
|
||||
|
||||
|
||||
class PageSmartLinkWidget(TextInput):
|
||||
"""Presents the user with a Select2 widget to select a page and returns the link to this page as a string."""
|
||||
template_name = 'cms/widgets/pagesmartlinkwidget.html'
|
||||
|
||||
class Media:
|
||||
css = {
|
||||
'screen': (
|
||||
'admin/css/vendor/select2/select2.css',
|
||||
'admin/css/autocomplete.css',
|
||||
),
|
||||
}
|
||||
js = (
|
||||
'admin/js/vendor/jquery/jquery.js',
|
||||
'admin/js/vendor/select2/select2.full.js',
|
||||
'admin/js/jquery.init.js',
|
||||
static_with_version('cms/js/dist/bundle.forms.pagesmartlinkwidget.min.js'),
|
||||
)
|
||||
|
||||
def __init__(self, attrs=None, ajax_view=None):
|
||||
super().__init__(attrs)
|
||||
self.ajax_url = self.get_ajax_url(ajax_view=ajax_view)
|
||||
|
||||
def get_ajax_url(self, ajax_view):
|
||||
try:
|
||||
return reverse_lazy(ajax_view)
|
||||
except NoReverseMatch:
|
||||
raise Exception(
|
||||
'You should provide an ajax_view argument that can be reversed to the PageSmartLinkWidget'
|
||||
)
|
||||
|
||||
def _build_script_data(self, name, value, attrs):
|
||||
return {
|
||||
"id": attrs.get('id', ''),
|
||||
"text": str(attrs.get('placeholder_text', '')),
|
||||
"lang": self.language,
|
||||
"url": force_str(self.ajax_url),
|
||||
}
|
||||
|
||||
def get_context(self, name, value, attrs):
|
||||
context = super().get_context(name, value, attrs)
|
||||
context['widget']['script_data'] = self._build_script_data(name, value, context['widget']['attrs'])
|
||||
return context
|
||||
|
||||
|
||||
class UserSelectAdminWidget(Select):
|
||||
"""Special widget used in page permission inlines, because we have to render
|
||||
an add user (plus) icon, but point it somewhere else - to special user creation
|
||||
view, which is accessible only if user haves "add user" permissions.
|
||||
|
||||
Current user should be assigned to widget in form constructor as an user
|
||||
attribute.
|
||||
"""
|
||||
def render(self, name, value, attrs=None, choices=(), renderer=None):
|
||||
output = [super().render(name, value, attrs, renderer=renderer)]
|
||||
if hasattr(self, 'user') and (
|
||||
self.user.is_superuser or self.user.has_perm(
|
||||
PageUser._meta.app_label + '.' + get_permission_codename('add', PageUser._meta))
|
||||
):
|
||||
# append + icon
|
||||
add_url = admin_reverse('cms_pageuser_add')
|
||||
output.append(format_html(
|
||||
'<a href="{}?_popup=1" class="related-widget-wrapper-link add-related"'
|
||||
' id="add_id_{}" data-popup="yes" title="{}">'
|
||||
'<img src="{}" alt="{}" width="20" height="20"></a>',
|
||||
add_url,
|
||||
name,
|
||||
_('Add another'),
|
||||
static('admin/img/icon-addlink.svg'),
|
||||
_('Add'),
|
||||
))
|
||||
return mark_safe(''.join(output))
|
||||
|
||||
|
||||
class AppHookSelect(Select):
|
||||
|
||||
"""Special widget used for the App Hook selector in the Advanced Settings
|
||||
of the Page Admin. It adds support for a data attribute per option and
|
||||
includes supporting JS into the page.
|
||||
"""
|
||||
|
||||
class Media:
|
||||
js = (
|
||||
static_with_version('cms/js/dist/bundle.forms.apphookselect.min.js'),
|
||||
)
|
||||
|
||||
def __init__(self, attrs=None, choices=(), app_namespaces={}):
|
||||
self.app_namespaces = app_namespaces
|
||||
super().__init__(attrs, choices)
|
||||
|
||||
def create_option(self, name, value, label, selected, index, subindex=None, attrs=None):
|
||||
option = super().create_option(name, value, label, selected, index, subindex, attrs)
|
||||
if value in self.app_namespaces:
|
||||
option['attrs']['data-namespace'] = escape(self.app_namespaces[value])
|
||||
return option
|
||||
|
||||
def _build_option(self, selected_choices, option_value, option_label):
|
||||
if option_value is None:
|
||||
option_value = ''
|
||||
option_value = force_str(option_value)
|
||||
if option_value in selected_choices:
|
||||
selected_html = mark_safe(' selected="selected"')
|
||||
if not self.allow_multiple_selected:
|
||||
# Only allow for a single selection.
|
||||
selected_choices.remove(option_value)
|
||||
else:
|
||||
selected_html = ''
|
||||
|
||||
if option_value in self.app_namespaces:
|
||||
data_html = mark_safe(' data-namespace="%s"' % escape(self.app_namespaces[option_value]))
|
||||
else:
|
||||
data_html = ''
|
||||
return option_value, selected_html, data_html, force_str(option_label)
|
||||
|
||||
def render_option(self, selected_choices, option_value, option_label):
|
||||
option_data = self._build_option(selected_choices, option_value, option_label)
|
||||
return '<option value="%s"%s%s>%s</option>' % option_data
|
||||
|
||||
|
||||
class ApplicationConfigSelect(Select):
|
||||
"""
|
||||
Special widget -populate by javascript- that shows application configurations
|
||||
depending on selected Apphooks.
|
||||
|
||||
Required data are injected in the page as JSON data that forms.apphookselect.js
|
||||
uses to create the appropriate data structure.
|
||||
|
||||
A stub 'addlink' link is created and filled in with the correct URL by the same
|
||||
javascript.
|
||||
"""
|
||||
template_name = 'cms/widgets/applicationconfigselect.html'
|
||||
|
||||
class Media:
|
||||
js = (
|
||||
static_with_version('cms/js/dist/bundle.forms.apphookselect.min.js'),
|
||||
)
|
||||
|
||||
def __init__(self, attrs=None, choices=(), app_configs={}):
|
||||
self.app_configs = app_configs
|
||||
super().__init__(attrs, choices)
|
||||
|
||||
def _build_script_data(self, name, value, attrs):
|
||||
configs = {
|
||||
str(application): [[str(config.pk), str(config)] for config in cms_app.get_configs()]
|
||||
for application, cms_app in self.app_configs.items()
|
||||
}
|
||||
urls = {
|
||||
str(application): cms_app.get_config_add_url()
|
||||
for application, cms_app in self.app_configs.items()
|
||||
}
|
||||
|
||||
return {
|
||||
"apphooks_configuration": configs,
|
||||
"apphooks_configuration_url": urls,
|
||||
"apphooks_configuration_value": str(value) if value is not None else "",
|
||||
# apphook_configuration_value needs to correspond to correspond to str(config.pk)
|
||||
}
|
||||
|
||||
def get_context(self, name, value, attrs):
|
||||
context = super().get_context(name, value, attrs)
|
||||
context['widget']['script_data'] = self._build_script_data(name, value, context['widget']['attrs'])
|
||||
return context
|
||||
176
cms/forms/wizards.py
Normal file
176
cms/forms/wizards.py
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from django.utils.html import strip_tags
|
||||
from django.utils.text import slugify
|
||||
from django.utils.translation import gettext, gettext_lazy as _
|
||||
|
||||
from cms.admin.forms import AddPageForm
|
||||
from cms.plugin_pool import plugin_pool
|
||||
from cms.utils import permissions
|
||||
from cms.utils.conf import get_cms_setting
|
||||
from cms.utils.page import get_available_slug
|
||||
from cms.utils.page_permissions import user_can_add_page, user_can_add_subpage
|
||||
|
||||
try:
|
||||
# djangocms_text is not guaranteed to be available
|
||||
from djangocms_text.widgets import TextEditorWidget
|
||||
text_widget = TextEditorWidget
|
||||
except ImportError:
|
||||
text_widget = forms.Textarea
|
||||
|
||||
|
||||
class CreateCMSPageForm(AddPageForm):
|
||||
sub_page_form = False
|
||||
|
||||
# Field overrides
|
||||
menu_title = None
|
||||
page_title = None
|
||||
meta_description = None
|
||||
|
||||
content = forms.CharField(
|
||||
label=_('Content'), widget=text_widget, required=False,
|
||||
help_text=_("Optional. If supplied, will be automatically added "
|
||||
"within a new text plugin.")
|
||||
)
|
||||
|
||||
class Media:
|
||||
js = (
|
||||
# This simply adds some JS for
|
||||
# hiding/showing the content field based on the selection of this select.
|
||||
'cms/js/widgets/wizard.pagetypeselect.js',
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['title'].help_text = _("Provide a title for the new page.")
|
||||
self.fields['slug'].required = False
|
||||
self.fields['slug'].help_text = _("Leave empty for automatic slug, or override as required.")
|
||||
|
||||
def get_placeholder(self, page_content, slot=None):
|
||||
"""
|
||||
Returns the named placeholder or, if no «slot» provided, the first
|
||||
editable, non-static placeholder or None.
|
||||
"""
|
||||
placeholders = page_content.get_placeholders()
|
||||
|
||||
if slot:
|
||||
placeholders = placeholders.filter(slot=slot)
|
||||
|
||||
for ph in placeholders:
|
||||
if not ph.is_static and ph.is_editable:
|
||||
return ph
|
||||
return None
|
||||
|
||||
@property
|
||||
def _language(self):
|
||||
return self.language_code
|
||||
|
||||
def clean(self):
|
||||
"""
|
||||
Validates that either the slug is provided, or that slugification from
|
||||
`title` produces a valid slug.
|
||||
:return:
|
||||
"""
|
||||
data = self.cleaned_data
|
||||
|
||||
if self._errors:
|
||||
return data
|
||||
|
||||
slug = slugify(data.get('slug')) or slugify(data['title'])
|
||||
if not slug:
|
||||
data["slug"] = ""
|
||||
forms.ValidationError({
|
||||
"slug": [_("Cannot automatically create slug. Please provide one manually.")],
|
||||
})
|
||||
|
||||
parent_page = data.get('parent_page')
|
||||
|
||||
if parent_page:
|
||||
base = parent_page.get_path(self._language)
|
||||
path = f'{base}/{slug}' if base else slug
|
||||
else:
|
||||
base = ''
|
||||
path = slug
|
||||
|
||||
data['slug'] = get_available_slug(self._site, path, self._language, suffix=None)
|
||||
data['path'] = '{}/{}'.format(base, data['slug']) if base else data['slug']
|
||||
|
||||
if not data['slug']:
|
||||
raise forms.ValidationError(_("Please provide a valid slug."))
|
||||
return data
|
||||
|
||||
def clean_content(self):
|
||||
# A rich-text editor submits an empty value as an empty paragraph
|
||||
# (e.g. ``<p></p>`` or ``<p> </p>``) rather than an empty string.
|
||||
# Treat any value without visible text as empty so the wizard does not
|
||||
# create a blank text plugin on the new page.
|
||||
content = self.cleaned_data.get("content")
|
||||
if content and not strip_tags(content).replace("\xa0", "").replace(" ", "").strip():
|
||||
return ""
|
||||
return content
|
||||
|
||||
def clean_parent_page(self):
|
||||
# Check to see if this user has permissions to make this page. We've
|
||||
# already checked this when producing a list of wizard entries, but this
|
||||
# is to prevent people from possible form-hacking.
|
||||
if self._page and self.sub_page_form:
|
||||
# User is adding a page which will be a direct
|
||||
# child of the current page.
|
||||
parent_page = self._page
|
||||
elif self._page and self._page.parent:
|
||||
# User is adding a page which will be a right
|
||||
# sibling to the current page.
|
||||
parent_page = self._page.parent
|
||||
else:
|
||||
parent_page = None
|
||||
|
||||
if parent_page:
|
||||
if parent_page.site_id != self._site.pk:
|
||||
raise ValidationError("Site doesn't match the parent's page site")
|
||||
has_perm = user_can_add_subpage(self._user, target=parent_page, site=self._site)
|
||||
else:
|
||||
has_perm = user_can_add_page(self._user, site=self._site)
|
||||
|
||||
if not has_perm:
|
||||
message = gettext('You don\'t have the permissions required to add a page.')
|
||||
raise ValidationError(message)
|
||||
return parent_page if parent_page else None
|
||||
|
||||
def get_template(self):
|
||||
return get_cms_setting('PAGE_WIZARD_DEFAULT_TEMPLATE')
|
||||
|
||||
@transaction.atomic
|
||||
def save(self, **kwargs):
|
||||
from cms.api import add_plugin
|
||||
|
||||
new_translation = super().save(**kwargs)
|
||||
new_page = new_translation.page
|
||||
|
||||
if self.cleaned_data.get("page_type"):
|
||||
return new_page
|
||||
|
||||
# If the user provided content, then use that instead.
|
||||
content = self.cleaned_data.get('content')
|
||||
plugin_type = get_cms_setting('PAGE_WIZARD_CONTENT_PLUGIN')
|
||||
plugin_body = get_cms_setting('PAGE_WIZARD_CONTENT_PLUGIN_BODY')
|
||||
slot = get_cms_setting('PAGE_WIZARD_CONTENT_PLACEHOLDER')
|
||||
|
||||
if plugin_type in plugin_pool.plugins and plugin_body:
|
||||
if content and permissions.has_plugin_permission(
|
||||
self._user, plugin_type, "add"):
|
||||
placeholder = self.get_placeholder(new_translation, slot=slot)
|
||||
if placeholder:
|
||||
opts = {
|
||||
'placeholder': placeholder,
|
||||
'plugin_type': plugin_type,
|
||||
'language': self.language_code,
|
||||
plugin_body: content,
|
||||
}
|
||||
add_plugin(**opts)
|
||||
return new_page
|
||||
|
||||
|
||||
class CreateCMSSubPageForm(CreateCMSPageForm):
|
||||
|
||||
sub_page_form = True
|
||||
Loading…
Add table
Add a link
Reference in a new issue