mirror of
https://github.com/django/django.git
synced 2024-12-22 17:16:24 +00:00
Fixed #15667 -- Added template-based widget rendering.
Thanks Carl Meyer and Tim Graham for contributing to the patch.
This commit is contained in:
parent
51cde873d9
commit
b52c73008a
@ -216,6 +216,9 @@ INSTALLED_APPS = []
|
||||
|
||||
TEMPLATES = []
|
||||
|
||||
# Default form rendering class.
|
||||
FORM_RENDERER = 'django.forms.renderers.DjangoTemplates'
|
||||
|
||||
# Default email address to use for various automated correspondence from
|
||||
# the site managers.
|
||||
DEFAULT_FROM_EMAIL = 'webmaster@localhost'
|
||||
|
@ -0,0 +1,6 @@
|
||||
{% if is_initial %}<p class="file-upload">{{ initial_text }}: <a href="{{ widget.value.url }}">{{ widget.value }}</a>{% if not widget.required %}
|
||||
<span class="clearable-file-input">
|
||||
<input type="checkbox" name="{{ checkbox_name }}" id="{{ checkbox_id }}" />
|
||||
<label for="{{ checkbox_id }}">{{ clear_checkbox_label }}</label>{% endif %}</span><br />
|
||||
{{ input_text }}:{% endif %}
|
||||
<input type="{{ widget.type }}" name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %} />{% if is_initial %}</p>{% endif %}
|
@ -0,0 +1 @@
|
||||
{% include 'django/forms/widgets/input.html' %}{% if related_url %}<a href="{{ related_url }}" class="related-lookup" id="lookup_id_{{ widget.name }}" title="{{ link_title }}"></a>{% endif %}{% if link_label %} <strong>{% if link_url %}<a href="{{ link_url }}">{% endif %}{{ link_label }}{% if link_url %}</a>{% endif %}</strong>{% endif %}
|
@ -0,0 +1 @@
|
||||
{% include 'admin/widgets/foreign_key_raw_id.html' %}
|
1
django/contrib/admin/templates/admin/widgets/radio.html
Normal file
1
django/contrib/admin/templates/admin/widgets/radio.html
Normal file
@ -0,0 +1 @@
|
||||
{% include "django/forms/widgets/multiple_input.html" %}
|
@ -0,0 +1,27 @@
|
||||
{% load i18n static %}
|
||||
<div class="related-widget-wrapper">
|
||||
{% include widget.template_name %}
|
||||
{% block links %}
|
||||
{% if can_change_related %}
|
||||
<a class="related-widget-wrapper-link change-related" id="change_id_{{ widget.name }}"
|
||||
data-href-template="{{ change_related_template_url }}?{{ url_params }}"
|
||||
title="{% blocktrans %}Change selected {{ model }}{% endblocktrans %}">
|
||||
<img src="{% static 'admin/img/icon-changelink.svg' %}" width="10" height="10" alt="{% trans 'Change' %}"/>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if can_add_related %}
|
||||
<a class="related-widget-wrapper-link add-related" id="add_id_{{ widget.name }}"
|
||||
href="{{ add_related_url }}?{{ url_params }}"
|
||||
title="{% blocktrans %}Add another {{ model }}{% endblocktrans %}">
|
||||
<img src="{% static 'admin/img/icon-addlink.svg' %}" width="10" height="10" alt="{% trans 'Add' %}"/>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if can_delete_related %}
|
||||
<a class="related-widget-wrapper-link delete-related" id="delete_id_{{ widget.name }}"
|
||||
data-href-template="{{ delete_related_template_url }}?{{ url_params }}"
|
||||
title="{% blocktrans %}Delete selected {{ model }}{% endblocktrans %}">
|
||||
<img src="{% static 'admin/img/icon-deletelink.svg' %}" width="10" height="10" alt="{% trans 'Delete' %}"/>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</div>
|
@ -0,0 +1,4 @@
|
||||
<p class="datetime">
|
||||
{{ date_label }} {% with widget=widget.subwidgets.0 %}{% include widget.template_name %}{% endwith %}<br />
|
||||
{{ time_label }} {% with widget=widget.subwidgets.1 %}{% include widget.template_name %}{% endwith %}
|
||||
</p>
|
1
django/contrib/admin/templates/admin/widgets/url.html
Normal file
1
django/contrib/admin/templates/admin/widgets/url.html
Normal file
@ -0,0 +1 @@
|
||||
{% if widget.value %}<p class="url">{{ current_label }} <a href="{{ widget.href }}">{{ widget.value }}</a><br />{{ change_label }} {% endif %}{% include "django/forms/widgets/input.html" %}{% if widget.value %}</p>{% endif %}
|
@ -7,14 +7,11 @@ import copy
|
||||
|
||||
from django import forms
|
||||
from django.db.models.deletion import CASCADE
|
||||
from django.forms.utils import flatatt
|
||||
from django.forms.widgets import RadioFieldRenderer
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse
|
||||
from django.urls.exceptions import NoReverseMatch
|
||||
from django.utils import six
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.html import format_html, format_html_join, smart_urlquote
|
||||
from django.utils.html import smart_urlquote
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.text import Truncator
|
||||
from django.utils.translation import ugettext as _
|
||||
@ -37,17 +34,14 @@ class FilteredSelectMultiple(forms.SelectMultiple):
|
||||
self.is_stacked = is_stacked
|
||||
super(FilteredSelectMultiple, self).__init__(attrs, choices)
|
||||
|
||||
def render(self, name, value, attrs=None):
|
||||
if attrs is None:
|
||||
attrs = {}
|
||||
attrs['class'] = 'selectfilter'
|
||||
def get_context(self, name, value, attrs=None):
|
||||
context = super(FilteredSelectMultiple, self).get_context(name, value, attrs)
|
||||
context['widget']['attrs']['class'] = 'selectfilter'
|
||||
if self.is_stacked:
|
||||
attrs['class'] += 'stacked'
|
||||
|
||||
attrs['data-field-name'] = self.verbose_name
|
||||
attrs['data-is-stacked'] = int(self.is_stacked)
|
||||
output = super(FilteredSelectMultiple, self).render(name, value, attrs)
|
||||
return mark_safe(output)
|
||||
context['widget']['attrs']['class'] += 'stacked'
|
||||
context['widget']['attrs']['data-field-name'] = self.verbose_name
|
||||
context['widget']['attrs']['data-is-stacked'] = int(self.is_stacked)
|
||||
return context
|
||||
|
||||
|
||||
class AdminDateWidget(forms.DateInput):
|
||||
@ -80,38 +74,27 @@ class AdminSplitDateTime(forms.SplitDateTimeWidget):
|
||||
"""
|
||||
A SplitDateTime Widget that has some admin-specific styling.
|
||||
"""
|
||||
template_name = 'admin/widgets/split_datetime.html'
|
||||
|
||||
def __init__(self, attrs=None):
|
||||
widgets = [AdminDateWidget, AdminTimeWidget]
|
||||
# Note that we're calling MultiWidget, not SplitDateTimeWidget, because
|
||||
# we want to define widgets.
|
||||
forms.MultiWidget.__init__(self, widgets, attrs)
|
||||
|
||||
def format_output(self, rendered_widgets):
|
||||
return format_html('<p class="datetime">{} {}<br />{} {}</p>',
|
||||
_('Date:'), rendered_widgets[0],
|
||||
_('Time:'), rendered_widgets[1])
|
||||
|
||||
|
||||
class AdminRadioFieldRenderer(RadioFieldRenderer):
|
||||
def render(self):
|
||||
"""Outputs a <ul> for this set of radio fields."""
|
||||
return format_html('<ul{}>\n{}\n</ul>',
|
||||
flatatt(self.attrs),
|
||||
format_html_join('\n', '<li>{}</li>',
|
||||
((force_text(w),) for w in self)))
|
||||
def get_context(self, name, value, attrs):
|
||||
context = super(AdminSplitDateTime, self).get_context(name, value, attrs)
|
||||
context['date_label'] = _('Date:')
|
||||
context['time_label'] = _('Time:')
|
||||
return context
|
||||
|
||||
|
||||
class AdminRadioSelect(forms.RadioSelect):
|
||||
renderer = AdminRadioFieldRenderer
|
||||
template_name = 'admin/widgets/radio.html'
|
||||
|
||||
|
||||
class AdminFileWidget(forms.ClearableFileInput):
|
||||
template_with_initial = (
|
||||
'<p class="file-upload">%s</p>' % forms.ClearableFileInput.template_with_initial
|
||||
)
|
||||
template_with_clear = (
|
||||
'<span class="clearable-file-input">%s</span>' % forms.ClearableFileInput.template_with_clear
|
||||
)
|
||||
template_name = 'admin/widgets/clearable_file_input.html'
|
||||
|
||||
|
||||
def url_params_from_lookup_dict(lookups):
|
||||
@ -141,17 +124,17 @@ class ForeignKeyRawIdWidget(forms.TextInput):
|
||||
A Widget for displaying ForeignKeys in the "raw_id" interface rather than
|
||||
in a <select> box.
|
||||
"""
|
||||
template_name = 'admin/widgets/foreign_key_raw_id.html'
|
||||
|
||||
def __init__(self, rel, admin_site, attrs=None, using=None):
|
||||
self.rel = rel
|
||||
self.admin_site = admin_site
|
||||
self.db = using
|
||||
super(ForeignKeyRawIdWidget, self).__init__(attrs)
|
||||
|
||||
def render(self, name, value, attrs=None):
|
||||
def get_context(self, name, value, attrs=None):
|
||||
context = super(ForeignKeyRawIdWidget, self).get_context(name, value, attrs)
|
||||
rel_to = self.rel.model
|
||||
if attrs is None:
|
||||
attrs = {}
|
||||
extra = []
|
||||
if rel_to in self.admin_site._registry:
|
||||
# The related object is registered with the same AdminSite
|
||||
related_url = reverse(
|
||||
@ -164,21 +147,16 @@ class ForeignKeyRawIdWidget(forms.TextInput):
|
||||
|
||||
params = self.url_parameters()
|
||||
if params:
|
||||
url = '?' + '&'.join('%s=%s' % (k, v) for k, v in params.items())
|
||||
else:
|
||||
url = ''
|
||||
if "class" not in attrs:
|
||||
attrs['class'] = 'vForeignKeyRawIdAdminField' # The JavaScript code looks for this hook.
|
||||
# TODO: "lookup_id_" is hard-coded here. This should instead use
|
||||
# the correct API to determine the ID dynamically.
|
||||
extra.append(
|
||||
'<a href="%s%s" class="related-lookup" id="lookup_id_%s" title="%s"></a>'
|
||||
% (related_url, url, name, _('Lookup'))
|
||||
)
|
||||
output = [super(ForeignKeyRawIdWidget, self).render(name, value, attrs)] + extra
|
||||
if value:
|
||||
output.append(self.label_for_value(value))
|
||||
return mark_safe(''.join(output))
|
||||
related_url += '?' + '&'.join(
|
||||
'%s=%s' % (k, v) for k, v in params.items(),
|
||||
)
|
||||
context['related_url'] = mark_safe(related_url)
|
||||
context['link_title'] = _('Lookup')
|
||||
# The JavaScript code looks for this class.
|
||||
context['widget']['attrs'].setdefault('class', 'vForeignKeyRawIdAdminField')
|
||||
if context['widget']['value']:
|
||||
context['link_label'], context['link_url'] = self.label_and_url_for_value(value)
|
||||
return context
|
||||
|
||||
def base_url_parameters(self):
|
||||
limit_choices_to = self.rel.limit_choices_to
|
||||
@ -192,17 +170,15 @@ class ForeignKeyRawIdWidget(forms.TextInput):
|
||||
params.update({TO_FIELD_VAR: self.rel.get_related_field().name})
|
||||
return params
|
||||
|
||||
def label_for_value(self, value):
|
||||
def label_and_url_for_value(self, value):
|
||||
key = self.rel.get_related_field().name
|
||||
try:
|
||||
obj = self.rel.model._default_manager.using(self.db).get(**{key: value})
|
||||
except (ValueError, self.rel.model.DoesNotExist):
|
||||
return ''
|
||||
return '', ''
|
||||
|
||||
label = ' <strong>{}</strong>'
|
||||
text = Truncator(obj).words(14, truncate='...')
|
||||
try:
|
||||
change_url = reverse(
|
||||
url = reverse(
|
||||
'%s:%s_%s_change' % (
|
||||
self.admin_site.name,
|
||||
obj._meta.app_label,
|
||||
@ -211,11 +187,9 @@ class ForeignKeyRawIdWidget(forms.TextInput):
|
||||
args=(obj.pk,)
|
||||
)
|
||||
except NoReverseMatch:
|
||||
pass # Admin not registered for target model.
|
||||
else:
|
||||
text = format_html('<a href="{}">{}</a>', change_url, text)
|
||||
url = '' # Admin not registered for target model.
|
||||
|
||||
return format_html(label, text)
|
||||
return Truncator(obj).words(14, truncate='...'), url
|
||||
|
||||
|
||||
class ManyToManyRawIdWidget(ForeignKeyRawIdWidget):
|
||||
@ -223,36 +197,36 @@ class ManyToManyRawIdWidget(ForeignKeyRawIdWidget):
|
||||
A Widget for displaying ManyToMany ids in the "raw_id" interface rather than
|
||||
in a <select multiple> box.
|
||||
"""
|
||||
def render(self, name, value, attrs=None):
|
||||
if attrs is None:
|
||||
attrs = {}
|
||||
template_name = 'admin/widgets/many_to_many_raw_id.html'
|
||||
|
||||
def get_context(self, name, value, attrs=None):
|
||||
context = super(ManyToManyRawIdWidget, self).get_context(name, value, attrs)
|
||||
if self.rel.model in self.admin_site._registry:
|
||||
# The related object is registered with the same AdminSite
|
||||
attrs['class'] = 'vManyToManyRawIdAdminField'
|
||||
if value:
|
||||
value = ','.join(force_text(v) for v in value)
|
||||
else:
|
||||
value = ''
|
||||
return super(ManyToManyRawIdWidget, self).render(name, value, attrs)
|
||||
context['widget']['attrs']['class'] = 'vManyToManyRawIdAdminField'
|
||||
return context
|
||||
|
||||
def url_parameters(self):
|
||||
return self.base_url_parameters()
|
||||
|
||||
def label_for_value(self, value):
|
||||
return ''
|
||||
def label_and_url_for_value(self, value):
|
||||
return '', ''
|
||||
|
||||
def value_from_datadict(self, data, files, name):
|
||||
value = data.get(name)
|
||||
if value:
|
||||
return value.split(',')
|
||||
|
||||
def format_value(self, value):
|
||||
return ','.join(force_text(v) for v in value) if value else ''
|
||||
|
||||
|
||||
class RelatedFieldWidgetWrapper(forms.Widget):
|
||||
"""
|
||||
This class is a wrapper to a given widget to add the add icon for the
|
||||
admin interface.
|
||||
"""
|
||||
template = 'admin/related_widget_wrapper.html'
|
||||
template_name = 'admin/widgets/related_widget_wrapper.html'
|
||||
|
||||
def __init__(self, widget, rel, admin_site, can_add_related=None,
|
||||
can_change_related=False, can_delete_related=False):
|
||||
@ -294,21 +268,19 @@ class RelatedFieldWidgetWrapper(forms.Widget):
|
||||
return reverse("admin:%s_%s_%s" % (info + (action,)),
|
||||
current_app=self.admin_site.name, args=args)
|
||||
|
||||
def render(self, name, value, *args, **kwargs):
|
||||
def get_context(self, name, value, attrs=None):
|
||||
with self.widget.override_choices(self.choices):
|
||||
context = self.widget.get_context(name, value, attrs)
|
||||
|
||||
from django.contrib.admin.views.main import IS_POPUP_VAR, TO_FIELD_VAR
|
||||
rel_opts = self.rel.model._meta
|
||||
info = (rel_opts.app_label, rel_opts.model_name)
|
||||
self.widget.choices = self.choices
|
||||
url_params = '&'.join("%s=%s" % param for param in [
|
||||
(TO_FIELD_VAR, self.rel.get_related_field().name),
|
||||
(IS_POPUP_VAR, 1),
|
||||
])
|
||||
context = {
|
||||
'widget': self.widget.render(name, value, *args, **kwargs),
|
||||
'name': name,
|
||||
'url_params': url_params,
|
||||
'model': rel_opts.verbose_name,
|
||||
}
|
||||
context['url_params'] = url_params
|
||||
context['model'] = rel_opts.verbose_name
|
||||
if self.can_change_related:
|
||||
change_related_template_url = self.get_related_url(info, 'change', '__fk__')
|
||||
context.update(
|
||||
@ -327,12 +299,7 @@ class RelatedFieldWidgetWrapper(forms.Widget):
|
||||
can_delete_related=True,
|
||||
delete_related_template_url=delete_related_template_url,
|
||||
)
|
||||
return mark_safe(render_to_string(self.template, context))
|
||||
|
||||
def build_attrs(self, extra_attrs=None, **kwargs):
|
||||
"Helper function for building an attribute dictionary."
|
||||
self.attrs = self.widget.build_attrs(extra_attrs=None, **kwargs)
|
||||
return self.attrs
|
||||
return context
|
||||
|
||||
def value_from_datadict(self, data, files, name):
|
||||
return self.widget.value_from_datadict(data, files, name)
|
||||
@ -366,23 +333,24 @@ class AdminEmailInputWidget(forms.EmailInput):
|
||||
|
||||
|
||||
class AdminURLFieldWidget(forms.URLInput):
|
||||
template_name = 'admin/widgets/url.html'
|
||||
|
||||
def __init__(self, attrs=None):
|
||||
final_attrs = {'class': 'vURLField'}
|
||||
if attrs is not None:
|
||||
final_attrs.update(attrs)
|
||||
super(AdminURLFieldWidget, self).__init__(attrs=final_attrs)
|
||||
|
||||
def render(self, name, value, attrs=None):
|
||||
html = super(AdminURLFieldWidget, self).render(name, value, attrs)
|
||||
if value:
|
||||
value = force_text(self.format_value(value))
|
||||
final_attrs = {'href': smart_urlquote(value)}
|
||||
html = format_html(
|
||||
'<p class="url">{} <a{}>{}</a><br />{} {}</p>',
|
||||
_('Currently:'), flatatt(final_attrs), value,
|
||||
_('Change:'), html
|
||||
)
|
||||
return html
|
||||
def get_context(self, name, value, attrs):
|
||||
context = super(AdminURLFieldWidget, self).get_context(name, value, attrs)
|
||||
context['current_label'] = _('Currently:')
|
||||
context['change_label'] = _('Change:')
|
||||
context['widget']['href'] = smart_urlquote(context['widget']['value'])
|
||||
return context
|
||||
|
||||
def format_value(self, value):
|
||||
value = super(AdminURLFieldWidget, self).format_value(value)
|
||||
return force_text(value)
|
||||
|
||||
|
||||
class AdminIntegerFieldWidget(forms.NumberInput):
|
||||
|
@ -13,12 +13,9 @@ from django.contrib.auth.models import User
|
||||
from django.contrib.auth.tokens import default_token_generator
|
||||
from django.contrib.sites.shortcuts import get_current_site
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.forms.utils import flatatt
|
||||
from django.template import loader
|
||||
from django.utils.encoding import force_bytes
|
||||
from django.utils.html import format_html, format_html_join
|
||||
from django.utils.http import urlsafe_base64_encode
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.text import capfirst
|
||||
from django.utils.translation import ugettext, ugettext_lazy as _
|
||||
|
||||
@ -26,26 +23,23 @@ UserModel = get_user_model()
|
||||
|
||||
|
||||
class ReadOnlyPasswordHashWidget(forms.Widget):
|
||||
def render(self, name, value, attrs):
|
||||
encoded = value
|
||||
final_attrs = self.build_attrs(attrs)
|
||||
template_name = 'auth/widgets/read_only_password_hash.html'
|
||||
|
||||
if not encoded or encoded.startswith(UNUSABLE_PASSWORD_PREFIX):
|
||||
summary = mark_safe("<strong>%s</strong>" % ugettext("No password set."))
|
||||
def get_context(self, name, value, attrs):
|
||||
context = super(ReadOnlyPasswordHashWidget, self).get_context(name, value, attrs)
|
||||
summary = []
|
||||
if not value or value.startswith(UNUSABLE_PASSWORD_PREFIX):
|
||||
summary.append({'label': ugettext("No password set.")})
|
||||
else:
|
||||
try:
|
||||
hasher = identify_hasher(encoded)
|
||||
hasher = identify_hasher(value)
|
||||
except ValueError:
|
||||
summary = mark_safe("<strong>%s</strong>" % ugettext(
|
||||
"Invalid password format or unknown hashing algorithm."
|
||||
))
|
||||
summary.append({'label': ugettext("Invalid password format or unknown hashing algorithm.")})
|
||||
else:
|
||||
summary = format_html_join(
|
||||
'', '<strong>{}</strong>: {} ',
|
||||
((ugettext(key), value) for key, value in hasher.safe_summary(encoded).items())
|
||||
)
|
||||
|
||||
return format_html("<div{}>{}</div>", flatatt(final_attrs), summary)
|
||||
for key, value_ in hasher.safe_summary(value).items():
|
||||
summary.append({'label': ugettext(key), 'value': value_})
|
||||
context['summary'] = summary
|
||||
return context
|
||||
|
||||
|
||||
class ReadOnlyPasswordHashField(forms.Field):
|
||||
|
@ -0,0 +1,3 @@
|
||||
{% for entry in summary %}
|
||||
<div{% include 'django/forms/widgets/attrs.html' %}><strong>{{ entry.label }}</strong>{% if entry.value %}: {{ entry.value }}{% endif %}
|
||||
{% endfor %}
|
@ -80,7 +80,7 @@ class GeoModelAdmin(ModelAdmin):
|
||||
collection_type = 'None'
|
||||
|
||||
class OLMap(self.widget):
|
||||
template = self.map_template
|
||||
template_name = self.map_template
|
||||
geom_type = db_field.geom_type
|
||||
|
||||
wms_options = ''
|
||||
|
@ -3,7 +3,6 @@ import logging
|
||||
from django.contrib.gis.gdal import GDALException
|
||||
from django.contrib.gis.geos import GEOSException, GEOSGeometry
|
||||
from django.forms.widgets import Textarea
|
||||
from django.template import loader
|
||||
from django.utils import six, translation
|
||||
|
||||
# Creating a template context that contains Django settings
|
||||
@ -16,7 +15,7 @@ class OpenLayersWidget(Textarea):
|
||||
"""
|
||||
Renders an OpenLayers map using the WKT of the geometry.
|
||||
"""
|
||||
def render(self, name, value, attrs=None):
|
||||
def get_context(self, name, value, attrs=None):
|
||||
# Update the template parameters with any attributes passed in.
|
||||
if attrs:
|
||||
self.params.update(attrs)
|
||||
@ -77,7 +76,7 @@ class OpenLayersWidget(Textarea):
|
||||
self.params['wkt'] = wkt
|
||||
|
||||
self.params.update(geo_context)
|
||||
return loader.render_to_string(self.template, self.params)
|
||||
return self.params
|
||||
|
||||
def map_options(self):
|
||||
"Builds the map options hash for the OpenLayers template."
|
||||
|
@ -6,7 +6,6 @@ from django.conf import settings
|
||||
from django.contrib.gis import gdal
|
||||
from django.contrib.gis.geos import GEOSException, GEOSGeometry
|
||||
from django.forms.widgets import Widget
|
||||
from django.template import loader
|
||||
from django.utils import six, translation
|
||||
|
||||
logger = logging.getLogger('django.contrib.gis')
|
||||
@ -43,7 +42,7 @@ class BaseGeometryWidget(Widget):
|
||||
logger.error("Error creating geometry from value '%s' (%s)", value, err)
|
||||
return None
|
||||
|
||||
def render(self, name, value, attrs=None):
|
||||
def get_context(self, name, value, attrs=None):
|
||||
# If a string reaches here (via a validation error on another
|
||||
# field) then just reconstruct the Geometry.
|
||||
if value and isinstance(value, six.string_types):
|
||||
@ -62,16 +61,19 @@ class BaseGeometryWidget(Widget):
|
||||
value.srid, self.map_srid, err
|
||||
)
|
||||
|
||||
context = self.build_attrs(
|
||||
attrs,
|
||||
if attrs is None:
|
||||
attrs = {}
|
||||
|
||||
context = self.build_attrs(self.attrs, dict(
|
||||
name=name,
|
||||
module='geodjango_%s' % name.replace('-', '_'), # JS-safe
|
||||
serialized=self.serialize(value),
|
||||
geom_type=gdal.OGRGeomType(self.attrs['geom_type']),
|
||||
STATIC_URL=settings.STATIC_URL,
|
||||
LANGUAGE_BIDI=translation.get_language_bidi(),
|
||||
)
|
||||
return loader.render_to_string(self.template_name, context)
|
||||
**attrs
|
||||
))
|
||||
return context
|
||||
|
||||
|
||||
class OpenLayersWidget(BaseGeometryWidget):
|
||||
|
@ -117,7 +117,7 @@ class SplitArrayWidget(forms.Widget):
|
||||
id_ += '_0'
|
||||
return id_
|
||||
|
||||
def render(self, name, value, attrs=None):
|
||||
def render(self, name, value, attrs=None, renderer=None):
|
||||
if self.is_localized:
|
||||
self.widget.is_localized = self.is_localized
|
||||
value = value or []
|
||||
@ -131,7 +131,7 @@ class SplitArrayWidget(forms.Widget):
|
||||
widget_value = None
|
||||
if id_:
|
||||
final_attrs = dict(final_attrs, id='%s_%s' % (id_, i))
|
||||
output.append(self.widget.render(name + '_%s' % i, widget_value, final_attrs))
|
||||
output.append(self.widget.render(name + '_%s' % i, widget_value, final_attrs, renderer))
|
||||
return mark_safe(self.format_output(output))
|
||||
|
||||
def format_output(self, rendered_widgets):
|
||||
|
@ -1,13 +1,16 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import datetime
|
||||
import warnings
|
||||
|
||||
from django.forms.utils import flatatt, pretty_name
|
||||
from django.forms.widgets import Textarea, TextInput
|
||||
from django.utils import six
|
||||
from django.utils.deprecation import RemovedInDjango21Warning
|
||||
from django.utils.encoding import force_text, python_2_unicode_compatible
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.html import conditional_escape, format_html, html_safe
|
||||
from django.utils.inspect import func_supports_parameter
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
@ -49,7 +52,10 @@ class BoundField(object):
|
||||
id_ = self.field.widget.attrs.get('id') or self.auto_id
|
||||
attrs = {'id': id_} if id_ else {}
|
||||
attrs = self.build_widget_attrs(attrs)
|
||||
return list(self.field.widget.subwidgets(self.html_name, self.value(), attrs))
|
||||
return list(
|
||||
BoundWidget(self.field.widget, widget, self.form.renderer)
|
||||
for widget in self.field.widget.subwidgets(self.html_name, self.value(), attrs=attrs)
|
||||
)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.subwidgets)
|
||||
@ -97,7 +103,23 @@ class BoundField(object):
|
||||
name = self.html_name
|
||||
else:
|
||||
name = self.html_initial_name
|
||||
return force_text(widget.render(name, self.value(), attrs=attrs))
|
||||
|
||||
kwargs = {}
|
||||
if func_supports_parameter(widget.render, 'renderer'):
|
||||
kwargs['renderer'] = self.form.renderer
|
||||
else:
|
||||
warnings.warn(
|
||||
'Add the `renderer` argument to the render() method of %s. '
|
||||
'It will be mandatory in Django 2.1.' % widget.__class__,
|
||||
RemovedInDjango21Warning, stacklevel=2,
|
||||
)
|
||||
html = widget.render(
|
||||
name=name,
|
||||
value=self.value(),
|
||||
attrs=attrs,
|
||||
**kwargs
|
||||
)
|
||||
return force_text(html)
|
||||
|
||||
def as_text(self, attrs=None, **kwargs):
|
||||
"""
|
||||
@ -230,3 +252,45 @@ class BoundField(object):
|
||||
if self.field.disabled:
|
||||
attrs['disabled'] = True
|
||||
return attrs
|
||||
|
||||
|
||||
@html_safe
|
||||
@python_2_unicode_compatible
|
||||
class BoundWidget(object):
|
||||
"""
|
||||
A container class used for iterating over widgets. This is useful for
|
||||
widgets that have choices. For example, the following can be used in a
|
||||
template:
|
||||
|
||||
{% for radio in myform.beatles %}
|
||||
<label for="{{ radio.id_for_label }}">
|
||||
{{ radio.choice_label }}
|
||||
<span class="radio">{{ radio.tag }}</span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
"""
|
||||
def __init__(self, parent_widget, data, renderer):
|
||||
self.parent_widget = parent_widget
|
||||
self.data = data
|
||||
self.renderer = renderer
|
||||
|
||||
def __str__(self):
|
||||
return self.tag(wrap_label=True)
|
||||
|
||||
def tag(self, wrap_label=False):
|
||||
context = {'widget': self.data, 'wrap_label': wrap_label}
|
||||
return self.parent_widget._render(self.template_name, context, self.renderer)
|
||||
|
||||
@property
|
||||
def template_name(self):
|
||||
if 'template_name' in self.data:
|
||||
return self.data['template_name']
|
||||
return self.parent_widget.template_name
|
||||
|
||||
@property
|
||||
def id_for_label(self):
|
||||
return 'id_%s_%s' % (self.data['name'], self.data['index'])
|
||||
|
||||
@property
|
||||
def choice_label(self):
|
||||
return self.data['label']
|
||||
|
@ -21,6 +21,8 @@ from django.utils.html import conditional_escape, html_safe
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from .renderers import get_default_renderer
|
||||
|
||||
__all__ = ('BaseForm', 'Form')
|
||||
|
||||
|
||||
@ -65,13 +67,14 @@ class BaseForm(object):
|
||||
# class is different than Form. See the comments by the Form class for more
|
||||
# information. Any improvements to the form API should be made to *this*
|
||||
# class, not to the Form class.
|
||||
default_renderer = None
|
||||
field_order = None
|
||||
prefix = None
|
||||
use_required_attribute = True
|
||||
|
||||
def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
|
||||
initial=None, error_class=ErrorList, label_suffix=None,
|
||||
empty_permitted=False, field_order=None, use_required_attribute=None):
|
||||
empty_permitted=False, field_order=None, use_required_attribute=None, renderer=None):
|
||||
self.is_bound = data is not None or files is not None
|
||||
self.data = data or {}
|
||||
self.files = files or {}
|
||||
@ -97,6 +100,17 @@ class BaseForm(object):
|
||||
if use_required_attribute is not None:
|
||||
self.use_required_attribute = use_required_attribute
|
||||
|
||||
# Initialize form renderer. Use a global default if not specified
|
||||
# either as an argument or as self.default_renderer.
|
||||
if renderer is None:
|
||||
if self.default_renderer is None:
|
||||
renderer = get_default_renderer()
|
||||
else:
|
||||
renderer = self.default_renderer
|
||||
if isinstance(self.default_renderer, type):
|
||||
renderer = renderer()
|
||||
self.renderer = renderer
|
||||
|
||||
def order_fields(self, field_order):
|
||||
"""
|
||||
Rearranges the fields according to field_order.
|
||||
|
1
django/forms/jinja2/django/forms/widgets/attrs.html
Normal file
1
django/forms/jinja2/django/forms/widgets/attrs.html
Normal file
@ -0,0 +1 @@
|
||||
{% for name, value in widget.attrs.items() %} {{ name }}{% if not value is sameas True %}="{{ value }}"{% endif %}{% endfor %}
|
1
django/forms/jinja2/django/forms/widgets/checkbox.html
Normal file
1
django/forms/jinja2/django/forms/widgets/checkbox.html
Normal file
@ -0,0 +1 @@
|
||||
{% include "django/forms/widgets/input.html" %}
|
@ -0,0 +1 @@
|
||||
{% include "django/forms/widgets/input_option.html" %}
|
@ -0,0 +1 @@
|
||||
{% include "django/forms/widgets/multiple_input.html" %}
|
@ -0,0 +1,5 @@
|
||||
{% if is_initial %}{{ initial_text }}: <a href="{{ widget.value.url }}">{{ widget.value }}</a>{% if not widget.required %}
|
||||
<input type="checkbox" name="{{ checkbox_name }}" id="{{ checkbox_id }}" />
|
||||
<label for="{{ checkbox_id }}">{{ clear_checkbox_label }}</label>{% endif %}<br />
|
||||
{{ input_text }}:{% endif %}
|
||||
<input type="{{ widget.type }}" name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %} />
|
1
django/forms/jinja2/django/forms/widgets/date.html
Normal file
1
django/forms/jinja2/django/forms/widgets/date.html
Normal file
@ -0,0 +1 @@
|
||||
{% include "django/forms/widgets/input.html" %}
|
1
django/forms/jinja2/django/forms/widgets/datetime.html
Normal file
1
django/forms/jinja2/django/forms/widgets/datetime.html
Normal file
@ -0,0 +1 @@
|
||||
{% include "django/forms/widgets/input.html" %}
|
1
django/forms/jinja2/django/forms/widgets/email.html
Normal file
1
django/forms/jinja2/django/forms/widgets/email.html
Normal file
@ -0,0 +1 @@
|
||||
{% include "django/forms/widgets/input.html" %}
|
1
django/forms/jinja2/django/forms/widgets/file.html
Normal file
1
django/forms/jinja2/django/forms/widgets/file.html
Normal file
@ -0,0 +1 @@
|
||||
{% include "django/forms/widgets/input.html" %}
|
1
django/forms/jinja2/django/forms/widgets/hidden.html
Normal file
1
django/forms/jinja2/django/forms/widgets/hidden.html
Normal file
@ -0,0 +1 @@
|
||||
{% include "django/forms/widgets/input.html" %}
|
1
django/forms/jinja2/django/forms/widgets/input.html
Normal file
1
django/forms/jinja2/django/forms/widgets/input.html
Normal file
@ -0,0 +1 @@
|
||||
<input type="{{ widget.type }}" name="{{ widget.name }}"{% if widget.value != None and widget.value != "" %} value="{{ widget.value }}"{% endif %}{% include "django/forms/widgets/attrs.html" %} />
|
@ -0,0 +1 @@
|
||||
{% if wrap_label %}<label{% if widget.attrs.id %} for="{{ widget.attrs.id }}"{% endif %}>{% endif %}{% include "django/forms/widgets/input.html" %}{% if wrap_label %} {{ widget.label }}</label>{% endif %}
|
@ -0,0 +1 @@
|
||||
{% include "django/forms/widgets/multiwidget.html" %}
|
@ -0,0 +1,5 @@
|
||||
{% set id = widget.attrs.id %}<ul{% if id %} id="{{ id }}"{% endif %}>{% for group, options, index in widget.optgroups %}{% if group %}
|
||||
<li>{{ group }}<ul{% if id %} id="{{ id }}_{{ index }}{% endif %}">{% endif %}{% for widget in options %}
|
||||
<li>{% include widget.template_name %}</li>{% endfor %}{% if group %}
|
||||
</ul></li>{% endif %}{% endfor %}
|
||||
</ul>
|
@ -0,0 +1 @@
|
||||
{% for widget in widget.subwidgets %}{% include widget.template_name %}{% endfor %}
|
1
django/forms/jinja2/django/forms/widgets/number.html
Normal file
1
django/forms/jinja2/django/forms/widgets/number.html
Normal file
@ -0,0 +1 @@
|
||||
{% include "django/forms/widgets/input.html" %}
|
1
django/forms/jinja2/django/forms/widgets/password.html
Normal file
1
django/forms/jinja2/django/forms/widgets/password.html
Normal file
@ -0,0 +1 @@
|
||||
{% include "django/forms/widgets/input.html" %}
|
1
django/forms/jinja2/django/forms/widgets/radio.html
Normal file
1
django/forms/jinja2/django/forms/widgets/radio.html
Normal file
@ -0,0 +1 @@
|
||||
{% include "django/forms/widgets/multiple_input.html" %}
|
@ -0,0 +1 @@
|
||||
{% include "django/forms/widgets/input_option.html" %}
|
5
django/forms/jinja2/django/forms/widgets/select.html
Normal file
5
django/forms/jinja2/django/forms/widgets/select.html
Normal file
@ -0,0 +1,5 @@
|
||||
<select name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}>{% for group_name, group_choices, group_index in widget.optgroups %}{% if group_name %}
|
||||
<optgroup label="{{ group_name }}">{% endif %}{% for widget in group_choices %}
|
||||
{% include widget.template_name %}{% endfor %}{% if group_name %}
|
||||
</optgroup>{% endif %}{% endfor %}
|
||||
</select>
|
@ -0,0 +1 @@
|
||||
{% include 'django/forms/widgets/multiwidget.html' %}
|
@ -0,0 +1 @@
|
||||
<option value="{{ widget.value }}"{% include "django/forms/widgets/attrs.html" %}>{{ widget.label }}</option>
|
@ -0,0 +1 @@
|
||||
{% include 'django/forms/widgets/multiwidget.html' %}
|
@ -0,0 +1 @@
|
||||
{% include 'django/forms/widgets/multiwidget.html' %}
|
1
django/forms/jinja2/django/forms/widgets/text.html
Normal file
1
django/forms/jinja2/django/forms/widgets/text.html
Normal file
@ -0,0 +1 @@
|
||||
{% include "django/forms/widgets/input.html" %}
|
2
django/forms/jinja2/django/forms/widgets/textarea.html
Normal file
2
django/forms/jinja2/django/forms/widgets/textarea.html
Normal file
@ -0,0 +1,2 @@
|
||||
<textarea name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}>
|
||||
{% if widget.value %}{{ widget.value }}{% endif %}</textarea>
|
1
django/forms/jinja2/django/forms/widgets/time.html
Normal file
1
django/forms/jinja2/django/forms/widgets/time.html
Normal file
@ -0,0 +1 @@
|
||||
{% include "django/forms/widgets/input.html" %}
|
1
django/forms/jinja2/django/forms/widgets/url.html
Normal file
1
django/forms/jinja2/django/forms/widgets/url.html
Normal file
@ -0,0 +1 @@
|
||||
{% include "django/forms/widgets/input.html" %}
|
71
django/forms/renderers.py
Normal file
71
django/forms/renderers.py
Normal file
@ -0,0 +1,71 @@
|
||||
import os
|
||||
|
||||
from django.conf import settings
|
||||
from django.template.backends.django import DjangoTemplates
|
||||
from django.template.loader import get_template
|
||||
from django.utils import lru_cache
|
||||
from django.utils._os import upath
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.module_loading import import_string
|
||||
|
||||
try:
|
||||
from django.template.backends.jinja2 import Jinja2
|
||||
except ImportError:
|
||||
def Jinja2(params):
|
||||
raise ImportError("jinja2 isn't installed")
|
||||
|
||||
ROOT = upath(os.path.dirname(__file__))
|
||||
|
||||
|
||||
@lru_cache.lru_cache()
|
||||
def get_default_renderer():
|
||||
renderer_class = import_string(settings.FORM_RENDERER)
|
||||
return renderer_class()
|
||||
|
||||
|
||||
class BaseRenderer(object):
|
||||
def get_template(self, template_name):
|
||||
raise NotImplementedError('subclasses must implement get_template()')
|
||||
|
||||
def render(self, template_name, context, request=None):
|
||||
template = self.get_template(template_name)
|
||||
return template.render(context, request=request).strip()
|
||||
|
||||
|
||||
class EngineMixin(object):
|
||||
def get_template(self, template_name):
|
||||
return self.engine.get_template(template_name)
|
||||
|
||||
@cached_property
|
||||
def engine(self):
|
||||
return self.backend({
|
||||
'APP_DIRS': True,
|
||||
'DIRS': [os.path.join(ROOT, self.backend.app_dirname)],
|
||||
'NAME': 'djangoforms',
|
||||
'OPTIONS': {},
|
||||
})
|
||||
|
||||
|
||||
class DjangoTemplates(EngineMixin, BaseRenderer):
|
||||
"""
|
||||
Load Django templates from the built-in widget templates in
|
||||
django/forms/templates and from apps' 'templates' directory.
|
||||
"""
|
||||
backend = DjangoTemplates
|
||||
|
||||
|
||||
class Jinja2(EngineMixin, BaseRenderer):
|
||||
"""
|
||||
Load Jinja2 templates from the built-in widget templates in
|
||||
django/forms/jinja2 and from apps' 'jinja2' directory.
|
||||
"""
|
||||
backend = Jinja2
|
||||
|
||||
|
||||
class TemplatesSetting(BaseRenderer):
|
||||
"""
|
||||
Load templates using template.loader.get_template() which is configured
|
||||
based on settings.TEMPLATES.
|
||||
"""
|
||||
def get_template(self, template_name):
|
||||
return get_template(template_name)
|
1
django/forms/templates/django/forms/widgets/attrs.html
Normal file
1
django/forms/templates/django/forms/widgets/attrs.html
Normal file
@ -0,0 +1 @@
|
||||
{% for name, value in widget.attrs.items %} {{ name }}{% if not value is True %}="{{ value }}"{% endif %}{% endfor %}
|
@ -0,0 +1 @@
|
||||
{% include "django/forms/widgets/input.html" %}
|
@ -0,0 +1 @@
|
||||
{% include "django/forms/widgets/input_option.html" %}
|
@ -0,0 +1 @@
|
||||
{% include "django/forms/widgets/multiple_input.html" %}
|
@ -0,0 +1,5 @@
|
||||
{% if is_initial %}{{ initial_text }}: <a href="{{ widget.value.url }}">{{ widget.value }}</a>{% if not widget.required %}
|
||||
<input type="checkbox" name="{{ checkbox_name }}" id="{{ checkbox_id }}" />
|
||||
<label for="{{ checkbox_id }}">{{ clear_checkbox_label }}</label>{% endif %}<br />
|
||||
{{ input_text }}:{% endif %}
|
||||
<input type="{{ widget.type }}" name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %} />
|
1
django/forms/templates/django/forms/widgets/date.html
Normal file
1
django/forms/templates/django/forms/widgets/date.html
Normal file
@ -0,0 +1 @@
|
||||
{% include "django/forms/widgets/input.html" %}
|
@ -0,0 +1 @@
|
||||
{% include "django/forms/widgets/input.html" %}
|
1
django/forms/templates/django/forms/widgets/email.html
Normal file
1
django/forms/templates/django/forms/widgets/email.html
Normal file
@ -0,0 +1 @@
|
||||
{% include "django/forms/widgets/input.html" %}
|
1
django/forms/templates/django/forms/widgets/file.html
Normal file
1
django/forms/templates/django/forms/widgets/file.html
Normal file
@ -0,0 +1 @@
|
||||
{% include "django/forms/widgets/input.html" %}
|
1
django/forms/templates/django/forms/widgets/hidden.html
Normal file
1
django/forms/templates/django/forms/widgets/hidden.html
Normal file
@ -0,0 +1 @@
|
||||
{% include "django/forms/widgets/input.html" %}
|
1
django/forms/templates/django/forms/widgets/input.html
Normal file
1
django/forms/templates/django/forms/widgets/input.html
Normal file
@ -0,0 +1 @@
|
||||
<input type="{{ widget.type }}" name="{{ widget.name }}"{% if widget.value != None and widget.value != "" %} value="{{ widget.value }}"{% endif %}{% include "django/forms/widgets/attrs.html" %} />
|
@ -0,0 +1 @@
|
||||
{% if wrap_label %}<label{% if widget.attrs.id %} for="{{ widget.attrs.id }}"{% endif %}>{% endif %}{% include "django/forms/widgets/input.html" %}{% if wrap_label %} {{ widget.label }}</label>{% endif %}
|
@ -0,0 +1 @@
|
||||
{% include "django/forms/widgets/multiwidget.html" %}
|
@ -0,0 +1,5 @@
|
||||
{% with id=widget.attrs.id %}<ul{% if id %} id="{{ id }}"{% endif %}>{% for group, options, index in widget.optgroups %}{% if group %}
|
||||
<li>{{ group }}<ul{% if id %} id="{{ id }}_{{ index }}{% endif %}">{% endif %}{% for option in options %}
|
||||
<li>{% include option.template_name with widget=option %}</li>{% endfor %}{% if group %}
|
||||
</ul></li>{% endif %}{% endfor %}
|
||||
</ul>{% endwith %}
|
@ -0,0 +1 @@
|
||||
{% for widget in widget.subwidgets %}{% include widget.template_name %}{% endfor %}
|
1
django/forms/templates/django/forms/widgets/number.html
Normal file
1
django/forms/templates/django/forms/widgets/number.html
Normal file
@ -0,0 +1 @@
|
||||
{% include "django/forms/widgets/input.html" %}
|
@ -0,0 +1 @@
|
||||
{% include "django/forms/widgets/input.html" %}
|
1
django/forms/templates/django/forms/widgets/radio.html
Normal file
1
django/forms/templates/django/forms/widgets/radio.html
Normal file
@ -0,0 +1 @@
|
||||
{% include "django/forms/widgets/multiple_input.html" %}
|
@ -0,0 +1 @@
|
||||
{% include "django/forms/widgets/input_option.html" %}
|
5
django/forms/templates/django/forms/widgets/select.html
Normal file
5
django/forms/templates/django/forms/widgets/select.html
Normal file
@ -0,0 +1,5 @@
|
||||
<select name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}>{% for group_name, group_choices, group_index in widget.optgroups %}{% if group_name %}
|
||||
<optgroup label="{{ group_name }}">{% endif %}{% for option in group_choices %}
|
||||
{% include option.template_name with widget=option %}{% endfor %}{% if group_name %}
|
||||
</optgroup>{% endif %}{% endfor %}
|
||||
</select>
|
@ -0,0 +1 @@
|
||||
{% include 'django/forms/widgets/multiwidget.html' %}
|
@ -0,0 +1 @@
|
||||
<option value="{{ widget.value }}"{% include "django/forms/widgets/attrs.html" %}>{{ widget.label }}</option>
|
@ -0,0 +1 @@
|
||||
{% include 'django/forms/widgets/multiwidget.html' %}
|
@ -0,0 +1 @@
|
||||
{% include 'django/forms/widgets/multiwidget.html' %}
|
1
django/forms/templates/django/forms/widgets/text.html
Normal file
1
django/forms/templates/django/forms/widgets/text.html
Normal file
@ -0,0 +1 @@
|
||||
{% include "django/forms/widgets/input.html" %}
|
@ -0,0 +1,2 @@
|
||||
<textarea name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}>
|
||||
{% if widget.value %}{{ widget.value }}{% endif %}</textarea>
|
1
django/forms/templates/django/forms/widgets/time.html
Normal file
1
django/forms/templates/django/forms/widgets/time.html
Normal file
@ -0,0 +1 @@
|
||||
{% include "django/forms/widgets/input.html" %}
|
1
django/forms/templates/django/forms/widgets/url.html
Normal file
1
django/forms/templates/django/forms/widgets/url.html
Normal file
@ -0,0 +1 @@
|
||||
{% include "django/forms/widgets/input.html" %}
|
File diff suppressed because it is too large
Load Diff
@ -12,7 +12,6 @@ from django.utils.functional import cached_property
|
||||
from django.utils.module_loading import import_string
|
||||
|
||||
from .base import BaseEngine
|
||||
from .utils import csrf_input_lazy, csrf_token_lazy
|
||||
|
||||
|
||||
class Jinja2(BaseEngine):
|
||||
@ -70,6 +69,7 @@ class Template(object):
|
||||
)
|
||||
|
||||
def render(self, context=None, request=None):
|
||||
from .utils import csrf_input_lazy, csrf_token_lazy
|
||||
if context is None:
|
||||
context = {}
|
||||
if request is not None:
|
||||
|
@ -97,6 +97,8 @@ def reset_template_engines(**kwargs):
|
||||
engines._engines = {}
|
||||
from django.template.engine import Engine
|
||||
Engine.get_default.cache_clear()
|
||||
from django.forms.renderers import get_default_renderer
|
||||
get_default_renderer.cache_clear()
|
||||
|
||||
|
||||
@receiver(setting_changed)
|
||||
|
@ -51,6 +51,9 @@ details on these changes.
|
||||
* Support for regular expression groups with ``iLmsu#`` in ``url()`` will be
|
||||
removed.
|
||||
|
||||
* Support for ``Widget.render()`` methods without the ``renderer`` argument
|
||||
will be removed.
|
||||
|
||||
.. _deprecation-removed-in-2.0:
|
||||
|
||||
2.0
|
||||
|
@ -720,6 +720,29 @@ When set to ``True`` (the default), required form fields will have the
|
||||
``use_required_attribute=False`` to avoid incorrect browser validation when
|
||||
adding and deleting forms from a formset.
|
||||
|
||||
Configuring the rendering of a form's widgets
|
||||
---------------------------------------------
|
||||
|
||||
.. attribute:: Form.default_renderer
|
||||
|
||||
.. versionadded:: 1.11
|
||||
|
||||
Specifies the :doc:`renderer <renderers>` to use for the form. Defaults to
|
||||
``None`` which means to use the default renderer specified by the
|
||||
:setting:`FORM_RENDERER` setting.
|
||||
|
||||
You can set this as a class attribute when declaring your form or use the
|
||||
``renderer`` argument to ``Form.__init__()``. For example::
|
||||
|
||||
from django import forms
|
||||
|
||||
class MyForm(forms.Form):
|
||||
default_renderer = MyRenderer()
|
||||
|
||||
or::
|
||||
|
||||
form = MyForm(renderer=MyRenderer())
|
||||
|
||||
Notes on field ordering
|
||||
-----------------------
|
||||
|
||||
|
@ -12,5 +12,6 @@ Detailed form API reference. For introductory material, see the
|
||||
fields
|
||||
models
|
||||
formsets
|
||||
renderers
|
||||
widgets
|
||||
validation
|
||||
|
131
docs/ref/forms/renderers.txt
Normal file
131
docs/ref/forms/renderers.txt
Normal file
@ -0,0 +1,131 @@
|
||||
======================
|
||||
The form rendering API
|
||||
======================
|
||||
|
||||
.. module:: django.forms.renderers
|
||||
:synopsis: Built-in form renderers.
|
||||
|
||||
.. versionadded:: 1.11
|
||||
|
||||
In older versions, widgets are rendered using Python. All APIs described
|
||||
in this document are new.
|
||||
|
||||
Django's form widgets are rendered using Django's :doc:`template engines
|
||||
system </topics/templates>`.
|
||||
|
||||
The form rendering process can be customized at several levels:
|
||||
|
||||
* Widgets can specify custom template names.
|
||||
* Forms and widgets can specify custom renderer classes.
|
||||
* A widget's template can be overridden by a project. (Reusable applications
|
||||
typically shouldn't override built-in templates because they might conflict
|
||||
with a project's custom templates.)
|
||||
|
||||
.. _low-level-widget-render-api:
|
||||
|
||||
The low-level render API
|
||||
========================
|
||||
|
||||
The rendering of form templates is controlled by a customizable renderer class.
|
||||
A custom renderer can be specified by updating the :setting:`FORM_RENDERER`
|
||||
setting. It defaults to
|
||||
``'``:class:`django.forms.renderers.DjangoTemplates`\ ``'``.
|
||||
|
||||
You can also provide a custom renderer by setting the
|
||||
:attr:`.Form.default_renderer` attribute or by using the ``renderer`` argument
|
||||
of :meth:`.Widget.render`.
|
||||
|
||||
Use one of the :ref:`built-in template form renderers
|
||||
<built-in-template-form-renderers>` or implement your own. Custom renderers
|
||||
must implement a ``render(template_name, context, request=None)`` method. It
|
||||
should return a rendered templates (as a string) or raise
|
||||
:exc:`~django.template.TemplateDoesNotExist`.
|
||||
|
||||
.. _built-in-template-form-renderers:
|
||||
|
||||
Built-in-template form renderers
|
||||
================================
|
||||
|
||||
``DjangoTemplates``
|
||||
-------------------
|
||||
|
||||
.. class:: DjangoTemplates
|
||||
|
||||
This renderer uses a standalone
|
||||
:class:`~django.template.backends.django.DjangoTemplates`
|
||||
engine (unconnected to what you might have configured in the
|
||||
:setting:`TEMPLATES` setting). It loads templates first from the built-in form
|
||||
templates directory in ``django/forms/templates`` and then from the installed
|
||||
apps' templates directories using the :class:`app_directories
|
||||
<django.template.loaders.app_directories.Loader>` loader.
|
||||
|
||||
If you want to render templates with customizations from your
|
||||
:setting:`TEMPLATES` setting, such as context processors for example, use the
|
||||
:class:`TemplatesSetting` renderer.
|
||||
|
||||
``Jinja2``
|
||||
----------
|
||||
|
||||
.. class:: Jinja2
|
||||
|
||||
This renderer is the same as the :class:`DjangoTemplates` renderer except that
|
||||
it uses a :class:`~django.template.backends.jinja2.Jinja2` backend. Templates
|
||||
for the built-in widgets are located in ``django/forms/jinja2`` and installed
|
||||
apps can provide templates in a ``jinja2`` directory.
|
||||
|
||||
To use this backend, all the widgets in your project and its third-party apps
|
||||
must have Jinja2 templates. Unless you provide your own Jinja2 templates for
|
||||
widgets that don't have any, you can't use this renderer. For example,
|
||||
:mod:`django.contrib.admin` doesn't include Jinja2 templates for its widgets
|
||||
due to their usage of Django template tags.
|
||||
|
||||
``TemplatesSetting``
|
||||
--------------------
|
||||
|
||||
.. class:: TemplatesSetting
|
||||
|
||||
This renderer gives you complete control of how widget templates are sourced.
|
||||
It uses :func:`~django.template.loader.get_template` to find widget
|
||||
templates based on what's configured in the :setting:`TEMPLATES` setting.
|
||||
|
||||
Using this renderer along with the built-in widget templates requires either:
|
||||
|
||||
#. ``'django.forms'`` in :setting:`INSTALLED_APPS` and at least one engine
|
||||
with :setting:`APP_DIRS=True <TEMPLATES-APP_DIRS>`.
|
||||
|
||||
#. Adding the built-in widgets templates directory (``django/forms/templates``
|
||||
or ``django/forms/jinja2``) in :setting:`DIRS <TEMPLATES-DIRS>` of one of
|
||||
your template engines.
|
||||
|
||||
Using this renderer requires you to make sure the form templates your project
|
||||
needs can be located.
|
||||
|
||||
Context available in widget templates
|
||||
=====================================
|
||||
|
||||
Widget templates receive a context from :meth:`.Widget.get_context`. By
|
||||
default, widgets receive a single value in the context, ``widget``. This is a
|
||||
dictionary that contains values like:
|
||||
|
||||
* ``name``
|
||||
* ``value``
|
||||
* ``attrs``
|
||||
* ``is_hidden``
|
||||
* ``template_name``
|
||||
|
||||
Some widgets add further information to the context. For instance, all widgets
|
||||
that subclass ``Input`` defines ``widget['type']`` and :class:`.MultiWidget`
|
||||
defines ``widget['subwidgets']`` for looping purposes.
|
||||
|
||||
Overriding built-in widget templates
|
||||
====================================
|
||||
|
||||
Each widget has a ``template_name`` attribute with a value such as
|
||||
``input.html``. Built-in widget templates are stored in the
|
||||
``django/forms/widgets`` path. You can provide a custom template for
|
||||
``input.html`` by defining ``django/forms/widgets/input.html``, for example.
|
||||
See :ref:`built-in widgets` for the name of each widget's template.
|
||||
|
||||
If you use the :class:`TemplatesSetting` renderer, overriding widget templates
|
||||
works the same as overriding any other template in your project. You can't
|
||||
override built-in widget templates using the other built-in renderers.
|
@ -241,6 +241,28 @@ foundation for custom widgets.
|
||||
In older versions, this method is a private API named
|
||||
``_format_value()``. The old name will work until Django 2.0.
|
||||
|
||||
.. method:: get_context(name, value, attrs=None)
|
||||
|
||||
.. versionadded:: 1.11
|
||||
|
||||
Returns a dictionary of values to use when rendering the widget
|
||||
template. By default, the dictionary contains a single key,
|
||||
``'widget'``, which is a dictionary representation of the widget
|
||||
containing the following keys:
|
||||
|
||||
* ``'name'``: The name of the field from the ``name`` argument.
|
||||
* ``'is_hidden'``: A boolean indicating whether or not this widget is
|
||||
hidden.
|
||||
* ``'required'``: A boolean indicating whether or not the field for
|
||||
this widget is required.
|
||||
* ``'value'``: The value as returned by :meth:`format_value`.
|
||||
* ``'attrs'``: HTML attributes to be set on the rendered widget. The
|
||||
combination of the :attr:`attrs` attribute and the ``attrs`` argument.
|
||||
* ``'template_name'``: The value of ``self.template_name``.
|
||||
|
||||
``Widget`` subclasses can provide custom context values by overriding
|
||||
this method.
|
||||
|
||||
.. method:: id_for_label(self, id_)
|
||||
|
||||
Returns the HTML ID attribute of this widget for use by a ``<label>``,
|
||||
@ -251,14 +273,16 @@ foundation for custom widgets.
|
||||
return an ID value that corresponds to the first ID in the widget's
|
||||
tags.
|
||||
|
||||
.. method:: render(name, value, attrs=None)
|
||||
.. method:: render(name, value, attrs=None, renderer=None)
|
||||
|
||||
Returns HTML for the widget, as a Unicode string. This method must be
|
||||
implemented by the subclass, otherwise ``NotImplementedError`` will be
|
||||
raised.
|
||||
Renders a widget to HTML using the given renderer. If ``renderer`` is
|
||||
``None``, the renderer from the :setting:`FORM_RENDERER` setting is
|
||||
used.
|
||||
|
||||
The 'value' given is not guaranteed to be valid input, therefore
|
||||
subclass implementations should program defensively.
|
||||
.. versionchanged:: 1.11
|
||||
|
||||
The ``renderer`` argument was added. Support for subclasses that
|
||||
don't accept it will be removed in Django 2.1.
|
||||
|
||||
.. method:: value_from_datadict(data, files, name)
|
||||
|
||||
@ -360,40 +384,21 @@ foundation for custom widgets.
|
||||
with the opposite responsibility - to combine cleaned values of
|
||||
all member fields into one.
|
||||
|
||||
Other methods that may be useful to override include:
|
||||
It provides some custom context:
|
||||
|
||||
.. method:: render(name, value, attrs=None)
|
||||
.. method:: get_context(name, value, attrs=None)
|
||||
|
||||
Argument ``value`` is handled differently in this method from the
|
||||
subclasses of :class:`~Widget` because it has to figure out how to
|
||||
split a single value for display in multiple widgets.
|
||||
In addition to the ``'widget'`` key described in
|
||||
:meth:`Widget.get_context`, ``MultiValueWidget`` adds a
|
||||
``widget['subwidgets']`` key.
|
||||
|
||||
The ``value`` argument used when rendering can be one of two things:
|
||||
These can be looped over in the widget template:
|
||||
|
||||
* A ``list``.
|
||||
* A single value (e.g., a string) that is the "compressed" representation
|
||||
of a ``list`` of values.
|
||||
.. code-block:: html+django
|
||||
|
||||
If ``value`` is a list, the output of :meth:`~MultiWidget.render` will
|
||||
be a concatenation of rendered child widgets. If ``value`` is not a
|
||||
list, it will first be processed by the method
|
||||
:meth:`~MultiWidget.decompress()` to create the list and then rendered.
|
||||
|
||||
When ``render()`` executes its HTML rendering, each value in the list
|
||||
is rendered with the corresponding widget -- the first value is
|
||||
rendered in the first widget, the second value is rendered in the
|
||||
second widget, etc.
|
||||
|
||||
Unlike in the single value widgets, method :meth:`~MultiWidget.render`
|
||||
need not be implemented in the subclasses.
|
||||
|
||||
.. method:: format_output(rendered_widgets)
|
||||
|
||||
Given a list of rendered widgets (as strings), returns a Unicode string
|
||||
representing the HTML for the whole lot.
|
||||
|
||||
This hook allows you to format the HTML design of the widgets any way
|
||||
you'd like.
|
||||
{% for subwidget in widget.subwidgets %}
|
||||
{% include widget.template_name with widget=subwidget %}
|
||||
{% endfor %}
|
||||
|
||||
Here's an example widget which subclasses :class:`MultiWidget` to display
|
||||
a date with the day, month, and year in different select boxes. This widget
|
||||
@ -421,9 +426,6 @@ foundation for custom widgets.
|
||||
return [value.day, value.month, value.year]
|
||||
return [None, None, None]
|
||||
|
||||
def format_output(self, rendered_widgets):
|
||||
return ''.join(rendered_widgets)
|
||||
|
||||
def value_from_datadict(self, data, files, name):
|
||||
datelist = [
|
||||
widget.value_from_datadict(data, files, name + '_%s' % i)
|
||||
@ -442,11 +444,6 @@ foundation for custom widgets.
|
||||
The constructor creates several :class:`Select` widgets in a tuple. The
|
||||
``super`` class uses this tuple to setup the widget.
|
||||
|
||||
The :meth:`~MultiWidget.format_output` method is fairly vanilla here (in
|
||||
fact, it's the same as what's been implemented as the default for
|
||||
``MultiWidget``), but the idea is that you could add custom HTML between
|
||||
the widgets should you wish.
|
||||
|
||||
The required method :meth:`~MultiWidget.decompress` breaks up a
|
||||
``datetime.date`` value into the day, month, and year values corresponding
|
||||
to each widget. Note how the method handles the case where ``value`` is
|
||||
@ -485,14 +482,18 @@ These widgets make use of the HTML elements ``input`` and ``textarea``.
|
||||
|
||||
.. class:: TextInput
|
||||
|
||||
Text input: ``<input type="text" ...>``
|
||||
* ``input_type``: ``'text'``
|
||||
* ``template_name``: ``'django/forms/widgets/text.html'``
|
||||
* Renders as: ``<input type="text" ...>``
|
||||
|
||||
``NumberInput``
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
.. class:: NumberInput
|
||||
|
||||
Text input: ``<input type="number" ...>``
|
||||
* ``input_type``: ``'number'``
|
||||
* ``template_name``: ``'django/forms/widgets/number.html'``
|
||||
* Renders as: ``<input type="number" ...>``
|
||||
|
||||
Beware that not all browsers support entering localized numbers in
|
||||
``number`` input types. Django itself avoids using them for fields having
|
||||
@ -503,21 +504,27 @@ These widgets make use of the HTML elements ``input`` and ``textarea``.
|
||||
|
||||
.. class:: EmailInput
|
||||
|
||||
Text input: ``<input type="email" ...>``
|
||||
* ``input_type``: ``'email'``
|
||||
* ``template_name``: ``'django/forms/widgets/email.html'``
|
||||
* Renders as: ``<input type="email" ...>``
|
||||
|
||||
``URLInput``
|
||||
~~~~~~~~~~~~
|
||||
|
||||
.. class:: URLInput
|
||||
|
||||
Text input: ``<input type="url" ...>``
|
||||
* ``input_type``: ``'url'``
|
||||
* ``template_name``: ``'django/forms/widgets/url.html'``
|
||||
* Renders as: ``<input type="url" ...>``
|
||||
|
||||
``PasswordInput``
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. class:: PasswordInput
|
||||
|
||||
Password input: ``<input type='password' ...>``
|
||||
* ``input_type``: ``'password'``
|
||||
* ``template_name``: ``'django/forms/widgets/password.html'``
|
||||
* Renders as: ``<input type='password' ...>``
|
||||
|
||||
Takes one optional argument:
|
||||
|
||||
@ -531,7 +538,9 @@ These widgets make use of the HTML elements ``input`` and ``textarea``.
|
||||
|
||||
.. class:: HiddenInput
|
||||
|
||||
Hidden input: ``<input type='hidden' ...>``
|
||||
* ``input_type``: ``'hidden'``
|
||||
* ``template_name``: ``'django/forms/widgets/hidden.html'``
|
||||
* Renders as: ``<input type='hidden' ...>``
|
||||
|
||||
Note that there also is a :class:`MultipleHiddenInput` widget that
|
||||
encapsulates a set of hidden input elements.
|
||||
@ -541,7 +550,9 @@ These widgets make use of the HTML elements ``input`` and ``textarea``.
|
||||
|
||||
.. class:: DateInput
|
||||
|
||||
Date input as a simple text box: ``<input type='text' ...>``
|
||||
* ``input_type``: ``'text'``
|
||||
* ``template_name``: ``'django/forms/widgets/date.html'``
|
||||
* Renders as: ``<input type='text' ...>``
|
||||
|
||||
Takes same arguments as :class:`TextInput`, with one more optional argument:
|
||||
|
||||
@ -558,7 +569,9 @@ These widgets make use of the HTML elements ``input`` and ``textarea``.
|
||||
|
||||
.. class:: DateTimeInput
|
||||
|
||||
Date/time input as a simple text box: ``<input type='text' ...>``
|
||||
* ``input_type``: ``'text'``
|
||||
* ``template_name``: ``'django/forms/widgets/datetime.html'``
|
||||
* Renders as: ``<input type='text' ...>``
|
||||
|
||||
Takes same arguments as :class:`TextInput`, with one more optional argument:
|
||||
|
||||
@ -579,7 +592,9 @@ These widgets make use of the HTML elements ``input`` and ``textarea``.
|
||||
|
||||
.. class:: TimeInput
|
||||
|
||||
Time input as a simple text box: ``<input type='text' ...>``
|
||||
* ``input_type``: ``'text'``
|
||||
* ``template_name``: ``'django/forms/widgets/time.html'``
|
||||
* Renders as: ``<input type='text' ...>``
|
||||
|
||||
Takes same arguments as :class:`TextInput`, with one more optional argument:
|
||||
|
||||
@ -598,7 +613,8 @@ These widgets make use of the HTML elements ``input`` and ``textarea``.
|
||||
|
||||
.. class:: Textarea
|
||||
|
||||
Text area: ``<textarea>...</textarea>``
|
||||
* ``template_name``: ``'django/forms/widgets/textarea.html'``
|
||||
* Renders as: ``<textarea>...</textarea>``
|
||||
|
||||
.. _selector-widgets:
|
||||
|
||||
@ -610,7 +626,9 @@ Selector and checkbox widgets
|
||||
|
||||
.. class:: CheckboxInput
|
||||
|
||||
Checkbox: ``<input type='checkbox' ...>``
|
||||
* ``input_type``: ``'checkbox'``
|
||||
* ``template_name``: ``'django/forms/widgets/checkbox.html'``
|
||||
* Renders as: ``<input type='checkbox' ...>``
|
||||
|
||||
Takes one optional argument:
|
||||
|
||||
@ -624,7 +642,8 @@ Selector and checkbox widgets
|
||||
|
||||
.. class:: Select
|
||||
|
||||
Select widget: ``<select><option ...>...</select>``
|
||||
* ``template_name``: ``'django/forms/widgets/select.html'``
|
||||
* Renders as: ``<select><option ...>...</select>``
|
||||
|
||||
.. attribute:: Select.choices
|
||||
|
||||
@ -637,6 +656,8 @@ Selector and checkbox widgets
|
||||
|
||||
.. class:: NullBooleanSelect
|
||||
|
||||
* ``template_name``: ``'django/forms/widgets/select.html'``
|
||||
|
||||
Select widget with options 'Unknown', 'Yes' and 'No'
|
||||
|
||||
``SelectMultiple``
|
||||
@ -644,6 +665,8 @@ Selector and checkbox widgets
|
||||
|
||||
.. class:: SelectMultiple
|
||||
|
||||
* ``template_name``: ``'django/forms/widgets/select.html'``
|
||||
|
||||
Similar to :class:`Select`, but allows multiple selection:
|
||||
``<select multiple='multiple'>...</select>``
|
||||
|
||||
@ -652,6 +675,8 @@ Selector and checkbox widgets
|
||||
|
||||
.. class:: RadioSelect
|
||||
|
||||
* ``template_name``: ``'django/forms/widgets/radio.html'``
|
||||
|
||||
Similar to :class:`Select`, but rendered as a list of radio buttons within
|
||||
``<li>`` tags:
|
||||
|
||||
@ -744,6 +769,8 @@ Selector and checkbox widgets
|
||||
|
||||
.. class:: CheckboxSelectMultiple
|
||||
|
||||
* ``template_name``: ``'django/forms/widgets/checkbox_select.html'``
|
||||
|
||||
Similar to :class:`SelectMultiple`, but rendered as a list of check
|
||||
buttons:
|
||||
|
||||
@ -776,16 +803,18 @@ File upload widgets
|
||||
|
||||
.. class:: FileInput
|
||||
|
||||
File upload input: ``<input type='file' ...>``
|
||||
* ``template_name``: ``'django/forms/widgets/file.html'``
|
||||
* Renders as: ``<input type='file' ...>``
|
||||
|
||||
``ClearableFileInput``
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. class:: ClearableFileInput
|
||||
|
||||
File upload input: ``<input type='file' ...>``, with an additional checkbox
|
||||
input to clear the field's value, if the field is not required and has
|
||||
initial data.
|
||||
* ``template_name``: ``'django/forms/widgets/clearable_file_input.html'``
|
||||
* Renders as: ``<input type='file' ...>`` with an additional checkbox
|
||||
input to clear the field's value, if the field is not required and has
|
||||
initial data.
|
||||
|
||||
.. _composite-widgets:
|
||||
|
||||
@ -797,7 +826,8 @@ Composite widgets
|
||||
|
||||
.. class:: MultipleHiddenInput
|
||||
|
||||
Multiple ``<input type='hidden' ...>`` widgets.
|
||||
* ``template_name``: ``'django/forms/widgets/multiple_hidden.html'``
|
||||
* Renders as: multiple ``<input type='hidden' ...>`` tags
|
||||
|
||||
A widget that handles multiple hidden widgets for fields that have a list
|
||||
of values.
|
||||
@ -813,6 +843,8 @@ Composite widgets
|
||||
|
||||
.. class:: SplitDateTimeWidget
|
||||
|
||||
* ``template_name``: ``'django/forms/widgets/splitdatetime.html'``
|
||||
|
||||
Wrapper (using :class:`MultiWidget`) around two widgets: :class:`DateInput`
|
||||
for the date, and :class:`TimeInput` for the time. Must be used with
|
||||
:class:`SplitDateTimeField` rather than :class:`DateTimeField`.
|
||||
@ -832,6 +864,8 @@ Composite widgets
|
||||
|
||||
.. class:: SplitHiddenDateTimeWidget
|
||||
|
||||
* ``template_name``: ``'django/forms/widgets/splithiddendatetime.html'``
|
||||
|
||||
Similar to :class:`SplitDateTimeWidget`, but uses :class:`HiddenInput` for
|
||||
both date and time.
|
||||
|
||||
@ -840,6 +874,8 @@ Composite widgets
|
||||
|
||||
.. class:: SelectDateWidget
|
||||
|
||||
* ``template_name``: ``'django/forms/widgets/select_date.html'``
|
||||
|
||||
Wrapper around three :class:`~django.forms.Select` widgets: one each for
|
||||
month, day, and year.
|
||||
|
||||
|
@ -1517,6 +1517,18 @@ generate correct URLs when ``SCRIPT_NAME`` is not ``/``.
|
||||
|
||||
The setting's use in :func:`django.setup()` was added.
|
||||
|
||||
.. setting:: FORM_RENDERER
|
||||
|
||||
``FORM_RENDERER``
|
||||
-----------------
|
||||
|
||||
.. versionadded:: 1.11
|
||||
|
||||
Default: ``'``:class:`django.forms.renderers.DjangoTemplates`\ ``'``
|
||||
|
||||
The class that renders form widgets. It must implement :ref:`the low-level
|
||||
render API <low-level-widget-render-api>`.
|
||||
|
||||
.. setting:: FORMAT_MODULE_PATH
|
||||
|
||||
``FORMAT_MODULE_PATH``
|
||||
@ -3351,6 +3363,10 @@ File uploads
|
||||
* :setting:`MEDIA_ROOT`
|
||||
* :setting:`MEDIA_URL`
|
||||
|
||||
Forms
|
||||
-----
|
||||
* :setting:`FORM_RENDERER`
|
||||
|
||||
Globalization (``i18n``/``l10n``)
|
||||
---------------------------------
|
||||
* :setting:`DATE_FORMAT`
|
||||
|
@ -61,6 +61,15 @@ It can be subclassed to support different index types, such as
|
||||
:class:`~django.contrib.postgres.indexes.GinIndex`. It also allows defining the
|
||||
order (ASC/DESC) for the columns of the index.
|
||||
|
||||
Template-based widget rendering
|
||||
-------------------------------
|
||||
|
||||
To ease customizing widgets, form widget rendering is now done using the
|
||||
template system rather than in Python. See :doc:`/ref/forms/renderers`.
|
||||
|
||||
You may need to adjust any custom widgets that you've written for a few
|
||||
:ref:`backwards incompatible changes <template-widget-incompatibilities-1-11>`.
|
||||
|
||||
Minor features
|
||||
--------------
|
||||
|
||||
@ -551,6 +560,21 @@ inside help text.
|
||||
Read-only fields are wrapped in ``<div class="readonly">...</div>`` instead of
|
||||
``<p>...</p>`` to allow any kind of HTML as the field's content.
|
||||
|
||||
.. _template-widget-incompatibilities-1-11:
|
||||
|
||||
Changes due to the introduction of template-based widget rendering
|
||||
------------------------------------------------------------------
|
||||
|
||||
Some undocumented classes in ``django.forms.widgets`` are removed:
|
||||
|
||||
* ``SubWidget``
|
||||
* ``RendererMixin``, ``ChoiceFieldRenderer``, ``RadioFieldRenderer``,
|
||||
``CheckboxFieldRenderer``
|
||||
* ``ChoiceInput``, ``RadioChoiceInput``, ``CheckboxChoiceInput``
|
||||
|
||||
The ``Widget.format_output()`` method is removed. Use a custom widget template
|
||||
instead.
|
||||
|
||||
Miscellaneous
|
||||
-------------
|
||||
|
||||
@ -754,3 +778,7 @@ Miscellaneous
|
||||
entries for search engines, for example. An alternative solution could be to
|
||||
create a :data:`~django.conf.urls.handler404` that looks for uppercase
|
||||
characters in the URL and redirects to a lowercase equivalent.
|
||||
|
||||
* The ``renderer`` argument is added to the :meth:`Widget.render()
|
||||
<django.forms.Widget.render>` method. Methods that don't accept that argument
|
||||
will work through a deprecation period.
|
||||
|
@ -673,6 +673,7 @@ releasers
|
||||
reloader
|
||||
removetags
|
||||
renderer
|
||||
renderers
|
||||
repo
|
||||
reportable
|
||||
reprojection
|
||||
|
@ -184,9 +184,11 @@ class TestInline(TestDataMixin, TestCase):
|
||||
SomeChildModel.objects.create(name='c', position='1', parent=parent)
|
||||
response = self.client.get(reverse('admin:admin_inlines_someparentmodel_change', args=(parent.pk,)))
|
||||
self.assertNotContains(response, '<td class="field-position">')
|
||||
self.assertContains(response, (
|
||||
self.assertInHTML(
|
||||
'<input id="id_somechildmodel_set-1-position" '
|
||||
'name="somechildmodel_set-1-position" type="hidden" value="1" />'))
|
||||
'name="somechildmodel_set-1-position" type="hidden" value="1" />',
|
||||
response.rendered_content,
|
||||
)
|
||||
|
||||
def test_non_related_name_inline(self):
|
||||
"""
|
||||
@ -273,12 +275,12 @@ class TestInline(TestDataMixin, TestCase):
|
||||
'name="binarytree_set-TOTAL_FORMS" type="hidden" value="2" />'
|
||||
)
|
||||
response = self.client.get(reverse('admin:admin_inlines_binarytree_add'))
|
||||
self.assertContains(response, max_forms_input % 3)
|
||||
self.assertContains(response, total_forms_hidden)
|
||||
self.assertInHTML(max_forms_input % 3, response.rendered_content)
|
||||
self.assertInHTML(total_forms_hidden, response.rendered_content)
|
||||
|
||||
response = self.client.get(reverse('admin:admin_inlines_binarytree_change', args=(bt_head.id,)))
|
||||
self.assertContains(response, max_forms_input % 2)
|
||||
self.assertContains(response, total_forms_hidden)
|
||||
self.assertInHTML(max_forms_input % 2, response.rendered_content)
|
||||
self.assertInHTML(total_forms_hidden, response.rendered_content)
|
||||
|
||||
def test_min_num(self):
|
||||
"""
|
||||
@ -302,8 +304,8 @@ class TestInline(TestDataMixin, TestCase):
|
||||
request = self.factory.get(reverse('admin:admin_inlines_binarytree_add'))
|
||||
request.user = User(username='super', is_superuser=True)
|
||||
response = modeladmin.changeform_view(request)
|
||||
self.assertContains(response, min_forms)
|
||||
self.assertContains(response, total_forms)
|
||||
self.assertInHTML(min_forms, response.rendered_content)
|
||||
self.assertInHTML(total_forms, response.rendered_content)
|
||||
|
||||
def test_custom_min_num(self):
|
||||
bt_head = BinaryTree.objects.create(name="Tree Head")
|
||||
@ -331,14 +333,14 @@ class TestInline(TestDataMixin, TestCase):
|
||||
request = self.factory.get(reverse('admin:admin_inlines_binarytree_add'))
|
||||
request.user = User(username='super', is_superuser=True)
|
||||
response = modeladmin.changeform_view(request)
|
||||
self.assertContains(response, min_forms % 2)
|
||||
self.assertContains(response, total_forms % 5)
|
||||
self.assertInHTML(min_forms % 2, response.rendered_content)
|
||||
self.assertInHTML(total_forms % 5, response.rendered_content)
|
||||
|
||||
request = self.factory.get(reverse('admin:admin_inlines_binarytree_change', args=(bt_head.id,)))
|
||||
request.user = User(username='super', is_superuser=True)
|
||||
response = modeladmin.changeform_view(request, object_id=str(bt_head.id))
|
||||
self.assertContains(response, min_forms % 5)
|
||||
self.assertContains(response, total_forms % 8)
|
||||
self.assertInHTML(min_forms % 5, response.rendered_content)
|
||||
self.assertInHTML(total_forms % 8, response.rendered_content)
|
||||
|
||||
def test_inline_nonauto_noneditable_pk(self):
|
||||
response = self.client.get(reverse('admin:admin_inlines_author_add'))
|
||||
|
@ -5980,7 +5980,7 @@ class AdminViewOnSiteTests(TestCase):
|
||||
response, 'inline_admin_formset', 0, None,
|
||||
['Children must share a family name with their parents in this contrived test case']
|
||||
)
|
||||
msg = "The formset 'inline_admin_formset' in context 4 does not contain any non-form errors."
|
||||
msg = "The formset 'inline_admin_formset' in context 10 does not contain any non-form errors."
|
||||
with self.assertRaisesMessage(AssertionError, msg):
|
||||
self.assertFormsetError(response, 'inline_admin_formset', None, None, ['Error'])
|
||||
|
||||
|
@ -3,6 +3,7 @@ from __future__ import unicode_literals
|
||||
|
||||
import gettext
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
from importlib import import_module
|
||||
|
||||
@ -354,34 +355,53 @@ class AdminURLWidgetTest(SimpleTestCase):
|
||||
)
|
||||
|
||||
def test_render_quoting(self):
|
||||
# WARNING: Don't use assertHTMLEqual in that testcase!
|
||||
# assertHTMLEqual will get rid of some escapes which are tested here!
|
||||
"""
|
||||
WARNING: This test doesn't use assertHTMLEqual since it will get rid
|
||||
of some escapes which are tested here!
|
||||
"""
|
||||
HREF_RE = re.compile('href="([^"]+)"')
|
||||
VALUE_RE = re.compile('value="([^"]+)"')
|
||||
TEXT_RE = re.compile('<a[^>]+>([^>]+)</a>')
|
||||
w = widgets.AdminURLFieldWidget()
|
||||
output = w.render('test', 'http://example.com/<sometag>some text</sometag>')
|
||||
self.assertEqual(
|
||||
w.render('test', 'http://example.com/<sometag>some text</sometag>'),
|
||||
'<p class="url">Currently: '
|
||||
'<a href="http://example.com/%3Csometag%3Esome%20text%3C/sometag%3E">'
|
||||
'http://example.com/<sometag>some text</sometag></a><br />'
|
||||
'Change: <input class="vURLField" name="test" type="url" '
|
||||
'value="http://example.com/<sometag>some text</sometag>" /></p>'
|
||||
HREF_RE.search(output).groups()[0],
|
||||
'http://example.com/%3Csometag%3Esome%20text%3C/sometag%3E',
|
||||
)
|
||||
self.assertEqual(
|
||||
w.render('test', 'http://example-äüö.com/<sometag>some text</sometag>'),
|
||||
'<p class="url">Currently: '
|
||||
'<a href="http://xn--example--7za4pnc.com/%3Csometag%3Esome%20text%3C/sometag%3E">'
|
||||
'http://example-äüö.com/<sometag>some text</sometag></a><br />'
|
||||
'Change: <input class="vURLField" name="test" type="url" '
|
||||
'value="http://example-äüö.com/<sometag>some text</sometag>" /></p>'
|
||||
TEXT_RE.search(output).groups()[0],
|
||||
'http://example.com/<sometag>some text</sometag>',
|
||||
)
|
||||
self.assertEqual(
|
||||
w.render('test', 'http://www.example.com/%C3%A4"><script>alert("XSS!")</script>"'),
|
||||
'<p class="url">Currently: '
|
||||
'<a href="http://www.example.com/%C3%A4%22%3E%3Cscript%3Ealert(%22XSS!%22)%3C/script%3E%22">'
|
||||
VALUE_RE.search(output).groups()[0],
|
||||
'http://example.com/<sometag>some text</sometag>',
|
||||
)
|
||||
output = w.render('test', 'http://example-äüö.com/<sometag>some text</sometag>')
|
||||
self.assertEqual(
|
||||
HREF_RE.search(output).groups()[0],
|
||||
'http://xn--example--7za4pnc.com/%3Csometag%3Esome%20text%3C/sometag%3E',
|
||||
)
|
||||
self.assertEqual(
|
||||
TEXT_RE.search(output).groups()[0],
|
||||
'http://example-äüö.com/<sometag>some text</sometag>',
|
||||
)
|
||||
self.assertEqual(
|
||||
VALUE_RE.search(output).groups()[0],
|
||||
'http://example-äüö.com/<sometag>some text</sometag>',
|
||||
)
|
||||
output = w.render('test', 'http://www.example.com/%C3%A4"><script>alert("XSS!")</script>"')
|
||||
self.assertEqual(
|
||||
HREF_RE.search(output).groups()[0],
|
||||
'http://www.example.com/%C3%A4%22%3E%3Cscript%3Ealert(%22XSS!%22)%3C/script%3E%22',
|
||||
)
|
||||
self.assertEqual(
|
||||
TEXT_RE.search(output).groups()[0],
|
||||
'http://www.example.com/%C3%A4"><script>'
|
||||
'alert("XSS!")</script>"</a><br />'
|
||||
'Change: <input class="vURLField" name="test" type="url" '
|
||||
'value="http://www.example.com/%C3%A4"><script>'
|
||||
'alert("XSS!")</script>"" /></p>'
|
||||
'alert("XSS!")</script>"'
|
||||
)
|
||||
self.assertEqual(
|
||||
VALUE_RE.search(output).groups()[0],
|
||||
'http://www.example.com/%C3%A4"><script>alert("XSS!")</script>"',
|
||||
)
|
||||
|
||||
|
||||
|
@ -39,6 +39,7 @@ class FilePathFieldTest(SimpleTestCase):
|
||||
('/django/forms/forms.py', 'forms.py'),
|
||||
('/django/forms/formsets.py', 'formsets.py'),
|
||||
('/django/forms/models.py', 'models.py'),
|
||||
('/django/forms/renderers.py', 'renderers.py'),
|
||||
('/django/forms/utils.py', 'utils.py'),
|
||||
('/django/forms/widgets.py', 'widgets.py')
|
||||
]
|
||||
@ -62,6 +63,7 @@ class FilePathFieldTest(SimpleTestCase):
|
||||
('/django/forms/forms.py', 'forms.py'),
|
||||
('/django/forms/formsets.py', 'formsets.py'),
|
||||
('/django/forms/models.py', 'models.py'),
|
||||
('/django/forms/renderers.py', 'renderers.py'),
|
||||
('/django/forms/utils.py', 'utils.py'),
|
||||
('/django/forms/widgets.py', 'widgets.py')
|
||||
]
|
||||
@ -83,6 +85,7 @@ class FilePathFieldTest(SimpleTestCase):
|
||||
('/django/forms/forms.py', 'forms.py'),
|
||||
('/django/forms/formsets.py', 'formsets.py'),
|
||||
('/django/forms/models.py', 'models.py'),
|
||||
('/django/forms/renderers.py', 'renderers.py'),
|
||||
('/django/forms/utils.py', 'utils.py'),
|
||||
('/django/forms/widgets.py', 'widgets.py')
|
||||
]
|
||||
|
1
tests/forms_tests/jinja2/forms_tests/custom_widget.html
Normal file
1
tests/forms_tests/jinja2/forms_tests/custom_widget.html
Normal file
@ -0,0 +1 @@
|
||||
<input type="text" name="custom">
|
@ -0,0 +1 @@
|
||||
<input type="text" name="custom">
|
@ -17,6 +17,7 @@ from django.forms import (
|
||||
SplitDateTimeField, SplitHiddenDateTimeWidget, Textarea, TextInput,
|
||||
TimeField, ValidationError, forms,
|
||||
)
|
||||
from django.forms.renderers import DjangoTemplates, get_default_renderer
|
||||
from django.forms.utils import ErrorList
|
||||
from django.http import QueryDict
|
||||
from django.template import Context, Template
|
||||
@ -678,6 +679,50 @@ Java</label></li>
|
||||
<div><label><input type="radio" name="name" value="ringo" required /> Ringo</label></div>"""
|
||||
)
|
||||
|
||||
def test_form_with_iterable_boundfield_id(self):
|
||||
class BeatleForm(Form):
|
||||
name = ChoiceField(
|
||||
choices=[('john', 'John'), ('paul', 'Paul'), ('george', 'George'), ('ringo', 'Ringo')],
|
||||
widget=RadioSelect,
|
||||
)
|
||||
fields = list(BeatleForm()['name'])
|
||||
self.assertEqual(len(fields), 4)
|
||||
|
||||
self.assertEqual(fields[0].id_for_label, 'id_name_0')
|
||||
self.assertEqual(fields[0].choice_label, 'John')
|
||||
self.assertHTMLEqual(
|
||||
fields[0].tag(),
|
||||
'<input type="radio" name="name" value="john" id="id_name_0" required />'
|
||||
)
|
||||
self.assertHTMLEqual(
|
||||
str(fields[0]),
|
||||
'<label for="id_name_0"><input type="radio" name="name" '
|
||||
'value="john" id="id_name_0" required /> John</label>'
|
||||
)
|
||||
|
||||
self.assertEqual(fields[1].id_for_label, 'id_name_1')
|
||||
self.assertEqual(fields[1].choice_label, 'Paul')
|
||||
self.assertHTMLEqual(
|
||||
fields[1].tag(),
|
||||
'<input type="radio" name="name" value="paul" id="id_name_1" required />'
|
||||
)
|
||||
self.assertHTMLEqual(
|
||||
str(fields[1]),
|
||||
'<label for="id_name_1"><input type="radio" name="name" '
|
||||
'value="paul" id="id_name_1" required /> Paul</label>'
|
||||
)
|
||||
|
||||
def test_iterable_boundfield_select(self):
|
||||
class BeatleForm(Form):
|
||||
name = ChoiceField(choices=[('john', 'John'), ('paul', 'Paul'), ('george', 'George'), ('ringo', 'Ringo')])
|
||||
fields = list(BeatleForm(auto_id=False)['name'])
|
||||
self.assertEqual(len(fields), 4)
|
||||
|
||||
self.assertEqual(fields[0].id_for_label, 'id_name_0')
|
||||
self.assertEqual(fields[0].choice_label, 'John')
|
||||
self.assertHTMLEqual(fields[0].tag(), '<option value="john">John</option>')
|
||||
self.assertHTMLEqual(str(fields[0]), '<option value="john">John</option>')
|
||||
|
||||
def test_form_with_noniterable_boundfield(self):
|
||||
# You can iterate over any BoundField, not just those with widget=RadioSelect.
|
||||
class BeatleForm(Form):
|
||||
@ -1993,8 +2038,9 @@ Password: <input type="password" name="password" required /></li>
|
||||
doesn't lose it's safe string status (#22950).
|
||||
"""
|
||||
class CustomWidget(TextInput):
|
||||
def render(self, name, value, attrs=None):
|
||||
return format_html(str('<input{} required />'), ' id=custom')
|
||||
def render(self, name, value, attrs=None, choices=None,
|
||||
renderer=None, extra_context=None):
|
||||
return format_html(str('<input{} />'), ' id=custom')
|
||||
|
||||
class SampleForm(Form):
|
||||
name = CharField(widget=CustomWidget)
|
||||
@ -3573,3 +3619,46 @@ Good luck picking a username that doesn't already exist.</p>
|
||||
f = DataForm({'data': 'xyzzy'})
|
||||
self.assertTrue(f.is_valid())
|
||||
self.assertEqual(f.cleaned_data, {'data': 'xyzzy'})
|
||||
|
||||
|
||||
class CustomRenderer(DjangoTemplates):
|
||||
pass
|
||||
|
||||
|
||||
class RendererTests(SimpleTestCase):
|
||||
|
||||
def test_default(self):
|
||||
form = Form()
|
||||
self.assertEqual(form.renderer, get_default_renderer())
|
||||
|
||||
def test_kwarg_instance(self):
|
||||
custom = CustomRenderer()
|
||||
form = Form(renderer=custom)
|
||||
self.assertEqual(form.renderer, custom)
|
||||
|
||||
def test_kwarg_class(self):
|
||||
custom = CustomRenderer()
|
||||
form = Form(renderer=custom)
|
||||
self.assertEqual(form.renderer, custom)
|
||||
|
||||
def test_attribute_instance(self):
|
||||
class CustomForm(Form):
|
||||
default_renderer = DjangoTemplates()
|
||||
|
||||
form = CustomForm()
|
||||
self.assertEqual(form.renderer, CustomForm.default_renderer)
|
||||
|
||||
def test_attribute_class(self):
|
||||
class CustomForm(Form):
|
||||
default_renderer = CustomRenderer
|
||||
|
||||
form = CustomForm()
|
||||
self.assertTrue(isinstance(form.renderer, CustomForm.default_renderer))
|
||||
|
||||
def test_attribute_override(self):
|
||||
class CustomForm(Form):
|
||||
default_renderer = DjangoTemplates()
|
||||
|
||||
custom = CustomRenderer()
|
||||
form = CustomForm(renderer=custom)
|
||||
self.assertEqual(form.renderer, custom)
|
||||
|
52
tests/forms_tests/tests/test_renderers.py
Normal file
52
tests/forms_tests/tests/test_renderers.py
Normal file
@ -0,0 +1,52 @@
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from django.forms.renderers import (
|
||||
BaseRenderer, DjangoTemplates, Jinja2, TemplatesSetting,
|
||||
)
|
||||
from django.test import SimpleTestCase
|
||||
from django.utils._os import upath
|
||||
|
||||
try:
|
||||
import jinja2
|
||||
except ImportError:
|
||||
jinja2 = None
|
||||
|
||||
|
||||
class SharedTests(object):
|
||||
expected_widget_dir = 'templates'
|
||||
|
||||
def test_installed_apps_template_found(self):
|
||||
"""Can find a custom template in INSTALLED_APPS."""
|
||||
renderer = self.renderer()
|
||||
# Found because forms_tests is .
|
||||
tpl = renderer.get_template('forms_tests/custom_widget.html')
|
||||
expected_path = os.path.abspath(
|
||||
os.path.join(
|
||||
upath(os.path.dirname(__file__)),
|
||||
'..',
|
||||
self.expected_widget_dir + '/forms_tests/custom_widget.html',
|
||||
)
|
||||
)
|
||||
self.assertEqual(tpl.origin.name, expected_path)
|
||||
|
||||
|
||||
class BaseTemplateRendererTests(SimpleTestCase):
|
||||
|
||||
def test_get_renderer(self):
|
||||
with self.assertRaisesMessage(NotImplementedError, 'subclasses must implement get_template()'):
|
||||
BaseRenderer().get_template('')
|
||||
|
||||
|
||||
class DjangoTemplatesTests(SharedTests, SimpleTestCase):
|
||||
renderer = DjangoTemplates
|
||||
|
||||
|
||||
@unittest.skipIf(jinja2 is None, 'jinja2 required')
|
||||
class Jinja2Tests(SharedTests, SimpleTestCase):
|
||||
renderer = Jinja2
|
||||
expected_widget_dir = 'jinja2'
|
||||
|
||||
|
||||
class TemplatesSettingTests(SharedTests, SimpleTestCase):
|
||||
renderer = TemplatesSetting
|
@ -1,182 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.contrib.admin.tests import AdminSeleniumTestCase
|
||||
from django.forms import (
|
||||
CheckboxSelectMultiple, ClearableFileInput, RadioSelect, TextInput,
|
||||
)
|
||||
from django.forms.widgets import (
|
||||
ChoiceFieldRenderer, ChoiceInput, RadioFieldRenderer,
|
||||
)
|
||||
from django.test import SimpleTestCase, override_settings
|
||||
from django.test import override_settings
|
||||
from django.urls import reverse
|
||||
from django.utils import six
|
||||
from django.utils.encoding import force_text, python_2_unicode_compatible
|
||||
from django.utils.safestring import SafeData
|
||||
|
||||
from ..models import Article
|
||||
|
||||
|
||||
class FormsWidgetTests(SimpleTestCase):
|
||||
|
||||
def test_radiofieldrenderer(self):
|
||||
# RadioSelect uses a RadioFieldRenderer to render the individual radio inputs.
|
||||
# You can manipulate that object directly to customize the way the RadioSelect
|
||||
# is rendered.
|
||||
w = RadioSelect(choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo')))
|
||||
r = w.get_renderer('beatle', 'J')
|
||||
inp_set1 = []
|
||||
inp_set2 = []
|
||||
inp_set3 = []
|
||||
inp_set4 = []
|
||||
|
||||
for inp in r:
|
||||
inp_set1.append(str(inp))
|
||||
inp_set2.append('%s<br />' % inp)
|
||||
inp_set3.append('<p>%s %s</p>' % (inp.tag(), inp.choice_label))
|
||||
inp_set4.append(
|
||||
'%s %s %s %s %s' % (
|
||||
inp.name,
|
||||
inp.value,
|
||||
inp.choice_value,
|
||||
inp.choice_label,
|
||||
inp.is_checked(),
|
||||
)
|
||||
)
|
||||
|
||||
self.assertHTMLEqual('\n'.join(inp_set1), """<label><input checked type="radio" name="beatle" value="J" /> John</label>
|
||||
<label><input type="radio" name="beatle" value="P" /> Paul</label>
|
||||
<label><input type="radio" name="beatle" value="G" /> George</label>
|
||||
<label><input type="radio" name="beatle" value="R" /> Ringo</label>""")
|
||||
self.assertHTMLEqual('\n'.join(inp_set2), """<label><input checked type="radio" name="beatle" value="J" /> John</label><br />
|
||||
<label><input type="radio" name="beatle" value="P" /> Paul</label><br />
|
||||
<label><input type="radio" name="beatle" value="G" /> George</label><br />
|
||||
<label><input type="radio" name="beatle" value="R" /> Ringo</label><br />""")
|
||||
self.assertHTMLEqual('\n'.join(inp_set3), """<p><input checked type="radio" name="beatle" value="J" /> John</p>
|
||||
<p><input type="radio" name="beatle" value="P" /> Paul</p>
|
||||
<p><input type="radio" name="beatle" value="G" /> George</p>
|
||||
<p><input type="radio" name="beatle" value="R" /> Ringo</p>""")
|
||||
self.assertHTMLEqual('\n'.join(inp_set4), """beatle J J John True
|
||||
beatle J P Paul False
|
||||
beatle J G George False
|
||||
beatle J R Ringo False""")
|
||||
|
||||
# A RadioFieldRenderer object also allows index access to individual RadioChoiceInput
|
||||
w = RadioSelect(choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo')))
|
||||
r = w.get_renderer('beatle', 'J')
|
||||
self.assertHTMLEqual(str(r[1]), '<label><input type="radio" name="beatle" value="P" /> Paul</label>')
|
||||
self.assertHTMLEqual(
|
||||
str(r[0]),
|
||||
'<label><input checked type="radio" name="beatle" value="J" /> John</label>'
|
||||
)
|
||||
self.assertTrue(r[0].is_checked())
|
||||
self.assertFalse(r[1].is_checked())
|
||||
self.assertEqual((r[1].name, r[1].value, r[1].choice_value, r[1].choice_label), ('beatle', 'J', 'P', 'Paul'))
|
||||
|
||||
# These individual widgets can accept extra attributes if manually rendered.
|
||||
self.assertHTMLEqual(
|
||||
r[1].render(attrs={'extra': 'value'}),
|
||||
'<label><input type="radio" extra="value" name="beatle" value="P" /> Paul</label>'
|
||||
)
|
||||
|
||||
with self.assertRaises(IndexError):
|
||||
r[10]
|
||||
|
||||
# You can create your own custom renderers for RadioSelect to use.
|
||||
class MyRenderer(RadioFieldRenderer):
|
||||
def render(self):
|
||||
return '<br />\n'.join(six.text_type(choice) for choice in self)
|
||||
w = RadioSelect(choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo')), renderer=MyRenderer)
|
||||
self.assertHTMLEqual(
|
||||
w.render('beatle', 'G'),
|
||||
"""<label><input type="radio" name="beatle" value="J" /> John</label><br />
|
||||
<label><input type="radio" name="beatle" value="P" /> Paul</label><br />
|
||||
<label><input checked type="radio" name="beatle" value="G" /> George</label><br />
|
||||
<label><input type="radio" name="beatle" value="R" /> Ringo</label>"""
|
||||
)
|
||||
|
||||
# Or you can use custom RadioSelect fields that use your custom renderer.
|
||||
class CustomRadioSelect(RadioSelect):
|
||||
renderer = MyRenderer
|
||||
w = CustomRadioSelect(choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo')))
|
||||
self.assertHTMLEqual(
|
||||
w.render('beatle', 'G'),
|
||||
"""<label><input type="radio" name="beatle" value="J" /> John</label><br />
|
||||
<label><input type="radio" name="beatle" value="P" /> Paul</label><br />
|
||||
<label><input checked type="radio" name="beatle" value="G" /> George</label><br />
|
||||
<label><input type="radio" name="beatle" value="R" /> Ringo</label>"""
|
||||
)
|
||||
|
||||
# You can customize rendering with outer_html/inner_html renderer variables (#22950)
|
||||
class MyRenderer(RadioFieldRenderer):
|
||||
# str is just to test some Python 2 issue with bytestrings
|
||||
outer_html = str('<div{id_attr}>{content}</div>')
|
||||
inner_html = '<p>{choice_value}{sub_widgets}</p>'
|
||||
w = RadioSelect(choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo')), renderer=MyRenderer)
|
||||
output = w.render('beatle', 'J', attrs={'id': 'bar'})
|
||||
self.assertIsInstance(output, SafeData)
|
||||
self.assertHTMLEqual(
|
||||
output,
|
||||
"""<div id="bar">
|
||||
<p><label for="bar_0"><input checked type="radio" id="bar_0" value="J" name="beatle" /> John</label></p>
|
||||
<p><label for="bar_1"><input type="radio" id="bar_1" value="P" name="beatle" /> Paul</label></p>
|
||||
<p><label for="bar_2"><input type="radio" id="bar_2" value="G" name="beatle" /> George</label></p>
|
||||
<p><label for="bar_3"><input type="radio" id="bar_3" value="R" name="beatle" /> Ringo</label></p>
|
||||
</div>""")
|
||||
|
||||
def test_subwidget(self):
|
||||
# Each subwidget tag gets a separate ID when the widget has an ID specified
|
||||
self.assertHTMLEqual(
|
||||
"\n".join(
|
||||
c.tag() for c in CheckboxSelectMultiple(
|
||||
attrs={'id': 'abc'},
|
||||
choices=zip('abc', 'ABC')
|
||||
).subwidgets('letters', list('ac'))
|
||||
),
|
||||
"""<input checked type="checkbox" name="letters" value="a" id="abc_0" />
|
||||
<input type="checkbox" name="letters" value="b" id="abc_1" />
|
||||
<input checked type="checkbox" name="letters" value="c" id="abc_2" />""")
|
||||
|
||||
# Each subwidget tag does not get an ID if the widget does not have an ID specified
|
||||
self.assertHTMLEqual(
|
||||
"\n".join(c.tag() for c in CheckboxSelectMultiple(
|
||||
choices=zip('abc', 'ABC'),
|
||||
).subwidgets('letters', list('ac'))),
|
||||
"""<input checked type="checkbox" name="letters" value="a" />
|
||||
<input type="checkbox" name="letters" value="b" />
|
||||
<input checked type="checkbox" name="letters" value="c" />""")
|
||||
|
||||
# The id_for_label property of the subwidget should return the ID that is used on the subwidget's tag
|
||||
self.assertHTMLEqual(
|
||||
"\n".join(
|
||||
'<input type="checkbox" name="letters" value="%s" id="%s" />'
|
||||
% (c.choice_value, c.id_for_label) for c in CheckboxSelectMultiple(
|
||||
attrs={'id': 'abc'},
|
||||
choices=zip('abc', 'ABC'),
|
||||
).subwidgets('letters', [])
|
||||
),
|
||||
"""<input type="checkbox" name="letters" value="a" id="abc_0" />
|
||||
<input type="checkbox" name="letters" value="b" id="abc_1" />
|
||||
<input type="checkbox" name="letters" value="c" id="abc_2" />""")
|
||||
|
||||
def test_sub_widget_html_safe(self):
|
||||
widget = TextInput()
|
||||
subwidget = next(widget.subwidgets('username', 'John Doe'))
|
||||
self.assertTrue(hasattr(subwidget, '__html__'))
|
||||
self.assertEqual(force_text(subwidget), subwidget.__html__())
|
||||
|
||||
def test_choice_input_html_safe(self):
|
||||
widget = ChoiceInput('choices', 'CHOICE1', {}, ('CHOICE1', 'first choice'), 0)
|
||||
self.assertTrue(hasattr(ChoiceInput, '__html__'))
|
||||
self.assertEqual(force_text(widget), widget.__html__())
|
||||
|
||||
def test_choice_field_renderer_html_safe(self):
|
||||
renderer = ChoiceFieldRenderer('choices', 'CHOICE1', {}, [('CHOICE1', 'first_choice')])
|
||||
renderer.choice_input_class = lambda *args: args
|
||||
self.assertTrue(hasattr(ChoiceFieldRenderer, '__html__'))
|
||||
self.assertEqual(force_text(renderer), renderer.__html__())
|
||||
|
||||
|
||||
@override_settings(ROOT_URLCONF='forms_tests.urls')
|
||||
class LiveWidgetTests(AdminSeleniumTestCase):
|
||||
|
||||
@ -190,33 +20,4 @@ class LiveWidgetTests(AdminSeleniumTestCase):
|
||||
self.selenium.get(self.live_server_url + reverse('article_form', args=[article.pk]))
|
||||
self.selenium.find_element_by_id('submit').submit()
|
||||
article = Article.objects.get(pk=article.pk)
|
||||
# Should be "\nTst\n" after #19251 is fixed
|
||||
self.assertEqual(article.content, "\r\nTst\r\n")
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class FakeFieldFile(object):
|
||||
"""
|
||||
Quacks like a FieldFile (has a .url and unicode representation), but
|
||||
doesn't require us to care about storages etc.
|
||||
"""
|
||||
url = 'something'
|
||||
|
||||
def __str__(self):
|
||||
return self.url
|
||||
|
||||
|
||||
class ClearableFileInputTests(SimpleTestCase):
|
||||
|
||||
def test_render_custom_template(self):
|
||||
widget = ClearableFileInput()
|
||||
widget.template_with_initial = (
|
||||
'%(initial_text)s: <img src="%(initial_url)s" alt="%(initial)s" /> '
|
||||
'%(clear_template)s<br />%(input_text)s: %(input)s'
|
||||
)
|
||||
self.assertHTMLEqual(
|
||||
widget.render('myfile', FakeFieldFile()),
|
||||
'Currently: <img src="something" alt="something" /> '
|
||||
'<input type="checkbox" name="myfile-clear" id="myfile-clear_id" /> '
|
||||
'<label for="myfile-clear_id">Clear</label><br />Change: <input type="file" name="myfile" />'
|
||||
)
|
||||
|
@ -1,9 +1,27 @@
|
||||
from django.forms.renderers import DjangoTemplates, Jinja2
|
||||
from django.test import SimpleTestCase
|
||||
|
||||
try:
|
||||
import jinja2
|
||||
except ImportError:
|
||||
jinja2 = None
|
||||
|
||||
|
||||
class WidgetTest(SimpleTestCase):
|
||||
beatles = (('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo'))
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.django_renderer = DjangoTemplates()
|
||||
cls.jinja2_renderer = Jinja2() if jinja2 else None
|
||||
cls.renderers = [cls.django_renderer] + ([cls.jinja2_renderer] if cls.jinja2_renderer else [])
|
||||
super(WidgetTest, cls).setUpClass()
|
||||
|
||||
def check_html(self, widget, name, value, html='', attrs=None, **kwargs):
|
||||
output = widget.render(name, value, attrs=attrs, **kwargs)
|
||||
if self.jinja2_renderer:
|
||||
output = widget.render(name, value, attrs=attrs, renderer=self.jinja2_renderer, **kwargs)
|
||||
# Django escapes quotes with '"' while Jinja2 uses '"'.
|
||||
self.assertHTMLEqual(output.replace('"', '"'), html)
|
||||
|
||||
output = widget.render(name, value, attrs=attrs, renderer=self.django_renderer, **kwargs)
|
||||
self.assertHTMLEqual(output, html)
|
||||
|
@ -221,6 +221,68 @@ class SelectTest(WidgetTest):
|
||||
</select>"""
|
||||
))
|
||||
|
||||
def test_options(self):
|
||||
options = list(self.widget(choices=self.beatles).options(
|
||||
'name', ['J'], attrs={'class': 'super'},
|
||||
))
|
||||
self.assertEqual(len(options), 4)
|
||||
self.assertEqual(options[0]['name'], 'name')
|
||||
self.assertEqual(options[0]['value'], 'J')
|
||||
self.assertEqual(options[0]['label'], 'John')
|
||||
self.assertEqual(options[0]['index'], '0')
|
||||
self.assertEqual(options[0]['selected'], True)
|
||||
# Template-related attributes
|
||||
self.assertEqual(options[1]['name'], 'name')
|
||||
self.assertEqual(options[1]['value'], 'P')
|
||||
self.assertEqual(options[1]['label'], 'Paul')
|
||||
self.assertEqual(options[1]['index'], '1')
|
||||
self.assertEqual(options[1]['selected'], False)
|
||||
|
||||
def test_optgroups(self):
|
||||
choices = [
|
||||
('Audio', [
|
||||
('vinyl', 'Vinyl'),
|
||||
('cd', 'CD'),
|
||||
]),
|
||||
('Video', [
|
||||
('vhs', 'VHS Tape'),
|
||||
('dvd', 'DVD'),
|
||||
]),
|
||||
('unknown', 'Unknown'),
|
||||
]
|
||||
groups = list(self.widget(choices=choices).optgroups(
|
||||
'name', ['vhs'], attrs={'class': 'super'},
|
||||
))
|
||||
self.assertEqual(len(groups), 3)
|
||||
self.assertEqual(groups[0][0], None)
|
||||
self.assertEqual(groups[0][2], 0)
|
||||
self.assertEqual(len(groups[0][1]), 1)
|
||||
options = groups[0][1]
|
||||
self.assertEqual(options[0]['name'], 'name')
|
||||
self.assertEqual(options[0]['value'], 'unknown')
|
||||
self.assertEqual(options[0]['label'], 'Unknown')
|
||||
self.assertEqual(options[0]['index'], '0')
|
||||
self.assertEqual(options[0]['selected'], False)
|
||||
self.assertEqual(groups[1][0], 'Audio')
|
||||
self.assertEqual(groups[1][2], 1)
|
||||
self.assertEqual(len(groups[1][1]), 2)
|
||||
options = groups[1][1]
|
||||
self.assertEqual(options[0]['name'], 'name')
|
||||
self.assertEqual(options[0]['value'], 'vinyl')
|
||||
self.assertEqual(options[0]['label'], 'Vinyl')
|
||||
self.assertEqual(options[0]['index'], '1_0')
|
||||
self.assertEqual(options[1]['index'], '1_1')
|
||||
self.assertEqual(groups[2][0], 'Video')
|
||||
self.assertEqual(groups[2][2], 2)
|
||||
self.assertEqual(len(groups[2][1]), 2)
|
||||
options = groups[2][1]
|
||||
self.assertEqual(options[0]['name'], 'name')
|
||||
self.assertEqual(options[0]['value'], 'vhs')
|
||||
self.assertEqual(options[0]['label'], 'VHS Tape')
|
||||
self.assertEqual(options[0]['index'], '2_0')
|
||||
self.assertEqual(options[0]['selected'], True)
|
||||
self.assertEqual(options[1]['index'], '2_1')
|
||||
|
||||
def test_deepcopy(self):
|
||||
"""
|
||||
__deepcopy__() should copy all attributes properly (#25085).
|
||||
|
@ -1651,13 +1651,6 @@ class ModelChoiceFieldTests(TestCase):
|
||||
with self.assertNumQueries(1):
|
||||
template.render(Context({'field': field}))
|
||||
|
||||
def test_modelchoicefield_index_renderer(self):
|
||||
field = forms.ModelChoiceField(Category.objects.all(), widget=forms.RadioSelect)
|
||||
self.assertEqual(
|
||||
str(field.widget.get_renderer('foo', [])[0]),
|
||||
'<label><input name="foo" type="radio" value="" /> ---------</label>'
|
||||
)
|
||||
|
||||
def test_disabled_modelchoicefield(self):
|
||||
class ModelChoiceForm(forms.ModelForm):
|
||||
author = forms.ModelChoiceField(Author.objects.all(), disabled=True)
|
||||
@ -2115,7 +2108,7 @@ class FileAndImageFieldTests(TestCase):
|
||||
|
||||
doc = Document.objects.create()
|
||||
form = DocumentForm(instance=doc)
|
||||
self.assertEqual(
|
||||
self.assertHTMLEqual(
|
||||
str(form['myfile']),
|
||||
'<input id="id_myfile" name="myfile" type="file" />'
|
||||
)
|
||||
|
@ -169,6 +169,11 @@ def setup(verbosity, test_labels, parallel):
|
||||
'The GeoManager class is deprecated.',
|
||||
RemovedInDjango20Warning
|
||||
)
|
||||
warnings.filterwarnings(
|
||||
'ignore',
|
||||
'django.forms.extras is deprecated.',
|
||||
RemovedInDjango20Warning
|
||||
)
|
||||
|
||||
# Load all the ALWAYS_INSTALLED_APPS.
|
||||
django.setup()
|
||||
|
Loading…
Reference in New Issue
Block a user