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 `_, 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)