Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Improve multivalue support #976

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 5 additions & 1 deletion crispy_forms/templates/bootstrap4/field.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,12 @@
{% if field|is_radioselect %}
{% include 'bootstrap4/layout/radioselect.html' %}
{% endif %}

{% if field|is_multivalue %}
{% include 'bootstrap4/layout/multivalue.html' %}
{% endif %}

{% if not field|is_checkboxselectmultiple and not field|is_radioselect %}
{% if not field|is_checkboxselectmultiple and not field|is_radioselect and not field|is_multivalue %}
{% if field|is_checkbox and form_show_labels %}
{%if use_custom_control%}
{% crispy_field field 'class' 'custom-control-input' %}
Expand Down
41 changes: 41 additions & 0 deletions crispy_forms/templates/bootstrap4/layout/multivalue.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{% load crispy_forms_field %}
{% load crispy_forms_tags %}

{% if field.is_hidden %}
{{ field }}
{% else %}

<div class="form-row">
{% for subfield in field.field.fields %}
<div class="
{% if 'col' in field_class %}{{ field_class }}{% else %} col-md{% endif %} {% if not forloop.last %}form-group {% endif %} ">
{% if subfield|is_checkbox %}
<div class="form-check">
{% render_multi_field field forloop.counter0 'class' 'form-check-input' %}
{% if subfield.label %}
<label for="{{ subfield.id_for_label }}"
class="{{ label_class }}{% if subfield.required %} requiredField{% endif %}">
{{ subfield.label|safe }}{% endif %}
</label>
</div>
{% elif subfield.errors %}
{% render_multi_field field forloop.counter0 'class' 'form-control is-invalid' %}
{% else %}
{% render_multi_field field forloop.counter0 'class' 'form-control' %}
{% endif %}
{% if error_text_inline %}
{% include 'bootstrap4/layout/field_errors.html' %}
{% else %}
{% include 'bootstrap4/layout/field_errors_block.html' %}
{% endif %}
</div>

{% endfor %}

</div>
{% include 'bootstrap4/layout/help_text.html' %}
{% endif %}




6 changes: 5 additions & 1 deletion crispy_forms/templatetags/crispy_forms_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@

@register.filter
def is_checkbox(field):
return isinstance(field.field.widget, forms.CheckboxInput)
try:
return isinstance(field.widget, forms.CheckboxInput)
except AttributeError:
return isinstance(field.field.widget, forms.CheckboxInput)



@register.filter
Expand Down
115 changes: 115 additions & 0 deletions crispy_forms/templatetags/crispy_forms_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from crispy_forms.compatibility import lru_cache
from crispy_forms.helper import FormHelper
from crispy_forms.utils import TEMPLATE_PACK, get_template_pack
from crispy_forms.templatetags.crispy_forms_field import pairwise

register = template.Library()

Expand Down Expand Up @@ -272,3 +273,117 @@ def do_uni_form(parser, token):
)

return CrispyFormNode(form, helper, template_pack=template_pack)


@register.tag
def render_multi_field(parser, token):
"""
Takes form field as first argument, field number as second argument, and
list of attribute-value pairs for all other arguments.
Attribute-value pairs should be in the form of 'attribute' 'value'
"""
error_msg = ('%r tag requires a form field and index followed by a list '
'of attributes and values in the form "attr" "value"'
% token.split_contents()[0])
try:
bits = token.split_contents()
form_field = bits[1]
field_index = bits[2]
attr_list = bits[3:]
except ValueError:
raise template.TemplateSyntaxError(error_msg)

attrs = {}
for attribute_name, value in pairwise(attr_list):
attrs[attribute_name] = value

return MultiFieldAttributeNode(form_field, attrs, index=field_index)


class MultiFieldAttributeNode(template.Node):
def __init__(self, field, attrs, index):
self.field = field
self.attrs = attrs
self.html5_required = 'html5_required'
self.index = index

def render(self, context):
# Nodes are not threadsafe so we must store and look up our instance
# variables in the current rendering context first
if self not in context.render_context:
context.render_context[self] = (
template.Variable(self.field),
self.attrs,
template.Variable(self.html5_required),
template.Variable(self.index)
)

field, attrs, html5_required, field_index = context.render_context[self]
field_index = field_index.resolve(context)
bounded_field = field.resolve(context)

try:
html5_required = html5_required.resolve(context)
except template.VariableDoesNotExist:
html5_required = False

field = bounded_field.field.fields[field_index]

if not bounded_field.form.is_bound:
data = bounded_field.field.initial

if callable(data):
data = data()
data = bounded_field.field.widget.decompress(data)[field_index]
else:
data = bounded_field.data[field_index]

# If template pack has been overridden in FormHelper we can pick it from context
template_pack = context.get('template_pack', TEMPLATE_PACK)

# There are special django widgets that wrap actual widgets,
# such as forms.widgets.MultiWidget, admin.widgets.RelatedFieldWidgetWrapper
widgets = getattr(bounded_field.field.widget, 'widgets',
[getattr(bounded_field.field.widget, 'widget', bounded_field.field.widget)])

if isinstance(attrs, dict):
attrs = [attrs] * len(widgets)

converters = {
'textinput': 'textinput textInput',
'fileinput': 'fileinput fileUpload',
'passwordinput': 'textinput textInput',
}
converters.update(getattr(settings, 'CRISPY_CLASS_CONVERTERS', {}))

for widget, attr in zip(widgets, attrs):
class_name = widget.__class__.__name__.lower()
class_name = converters.get(class_name, class_name)
css_class = widget.attrs.get('class', '')
if css_class:
if css_class.find(class_name) == -1:
css_class += " %s" % class_name
else:
css_class = class_name

if template_pack == 'bootstrap4':
if bounded_field.errors:
css_class += ' is-invalid'

widget.attrs['class'] = css_class

# HTML5 required attribute
if html5_required and bounded_field.field.required and 'required' not in widget.attrs:
if field.widget.__class__.__name__ != 'RadioSelect':
widget.attrs['required'] = True

for attribute_name, attribute in attr.items():
attribute_name = template.Variable(attribute_name).resolve(context)

if attribute_name in widget.attrs:
widget.attrs[attribute_name] += " " + template.Variable(attribute).resolve(context)
else:
widget.attrs[attribute_name] = template.Variable(attribute).resolve(context)

return widgets[field_index].render('%s_%d' % (bounded_field.html_name, field_index),
data, widgets[field_index].attrs)
10 changes: 5 additions & 5 deletions crispy_forms/tests/test_form_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def test_inputs(settings):
assert 'btn btn-primary' in html
assert 'btn btn-inverse' in html
if settings.CRISPY_TEMPLATE_PACK == 'bootstrap4':
assert len(re.findall(r'<input[^>]+> <', html)) == 9
assert len(re.findall(r'<input[^>]+> <', html)) == 10
else:
assert len(re.findall(r'<input[^>]+> <', html)) == 8

Expand Down Expand Up @@ -865,7 +865,7 @@ def test_label_class_and_field_class_bs4():

assert '<div class="form-group">' in html
assert '<div class="col-lg-8">' in html
assert html.count('col-lg-8') == 7
assert html.count('col-lg-8') == 8
assert 'offset' not in html

form.helper.label_class = 'col-sm-3 col-md-4'
Expand All @@ -874,7 +874,7 @@ def test_label_class_and_field_class_bs4():

assert '<div class="form-group">' in html
assert '<div class="col-sm-8 col-md-6">' in html
assert html.count('col-sm-8') == 7
assert html.count('col-sm-8') == 8
assert 'offset' not in html


Expand All @@ -889,15 +889,15 @@ def test_label_class_and_field_class_bs4_offset_when_horizontal():

assert '<div class="form-group row">' in html
assert '<div class="offset-lg-2 col-lg-8">' in html
assert html.count('col-lg-8') == 7
assert html.count('col-lg-8') == 8

form.helper.label_class = 'col-sm-3 col-md-4'
form.helper.field_class = 'col-sm-8 col-md-6'
html = render_crispy_form(form)

assert '<div class="form-group row">' in html
assert '<div class="offset-sm-3 offset-md-4 col-sm-8 col-md-6">' in html
assert html.count('col-sm-8') == 7
assert html.count('col-sm-8') == 8


@only_bootstrap4
Expand Down
2 changes: 1 addition & 1 deletion crispy_forms/tests/test_layout_objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ def test_field_type_hidden():
'test_form': test_form,
})
html = template.render(c)

# Check form parameters

assert html.count('data-test="12"') == 1
assert html.count('name="email"') == 1
assert html.count('class="dateinput') == 1
Expand Down