Repository: django-crispy-forms/django-crispy-forms

Issue:
String replacement failure when using custom templates 
If the template kwarg is defined on a Field in a layout, Django1.8 throws a `not all arguments converted during string formatting` error.

This is because in the Field render function (layouts.py line 437), the script attempts to do string replacement on a variable which may no longer have `%s` in the template kwarg that was passed. This makes perfect sense that this string replacement is done on the default template variable `"%s_field.html"`, but I don't think it should be being done when a custom template is passed since users probably aren't defining a custom template/widget for all the available crispy template packs.

Cannot override template on dev branch
```
File "/home/vagrant/.virtualenvs/someproject/lib/python2.7/site-packages/crispy_forms/bootstrap.py" in render
  185.         template = self.template % template_pack

Exception Type: TypeError at /login/
Exception Value: not all arguments converted during string formatting
```

I get the above mentioned exception when overriding the template attribute on the helper.
It seems that the code always expect a template_pack placeholder to be present in the string which shouldn't always be the case, one should be able to completely override the template path.

Before submitting a PR I think its needed to have a design decision on what this should be doing, should we allow bare replacement of the template or should we document that the template_pack being used should be provided as a placeholder in the string...

anyone ? 



Discussion:
I noticed the same thing on 1.5.0 with my current project. I only notice it when I use Django form wizard.

Anyone want to give me a failing test case here?

I'm working on a small test case right now, my actual form is rather complex so I'm trying to isolate the issue.

@Remiz that's what I like to hear! :-)

I narrowed it down to a part where I'm using a custom template for a Field object, as @tayden says...

``` python
# In form definition
instant_alerts = forms.MultipleChoiceField(
    choices=CHOICES_ALERT,
    label='Instant alerts on',
    widget=forms.CheckboxSelectMultiple(),
    required=False,
)
...
# In init() self.helper.layout definition
Field(
    'instant_alerts',
    template='common/checkboxselectmultiple.html'
),
...
```

I kind of fixed my issue by no longer using a custom template for that field, but I'm sure other people may want to have customization working.

I also added a small test project to illustrate the issue: https://github.com/Remiz/dcf-bug

I can say I've hit this challenge just now myself. Am looking at the issue, though I am erroring out at `crispy_forms/layout.py in render, line 439` which is also `template = self.template % template_pack`

As I see it, the basic problem is that the render line assumes that the template value will still be the pre-set string with expects a string value to be inserted (i.e. is `template = "%s/field.html"`). When you specify your own template override, this breaks the assumption. 

My current workaround is to wrap the render string with:

```
        if '%s' in self.template:
            template = self.template % template_pack
        else:
            template = self.template
```

Just updated to crispy 1.5 from 1.4 and got bitten with this. Now I get to decide whether to roll back to 1.4 or try to temporarily fork 1.5 to patch the bug (so I have a reproducible deployment process) using @marshalc fix.

There are multiple instances of this problem in crispy_forms/layout.py.

@damienjones first off, if it works with 1.4 rollback to that — that one is easy.

> there are multiple instances...

That's not helpful. I need specifics. Preferably with test cases and maybe fixes in pull requests.

This has been in the Dev branch for 2 years. It's a shame no one ran it against their app to find the problem before now. However, let's get it fixed for a quick 1.5.1

@carltongibson I was not intending to be unhelpful; I'm actually very appreciative of crispy-forms. I was going to submit a PR but I'm not set up to run the test suite and can't really stop what I'm doing at the moment to address that, and I wasn't comfortable submitting a PR that I hadn't personally tested. On top of that, it was @marshalc who suggested the fix that worked for me. I just applied it to my installed code so I can keep working on my main project today. If I get time tonight I'll see about replicating (and testing) the fix and submitting a PR.

Good man! 👍🏽


Resolving patch:
diff --git a/crispy_forms/layout.py b/crispy_forms/layout.py
--- a/crispy_forms/layout.py
+++ b/crispy_forms/layout.py
@@ -5,8 +5,17 @@
 from crispy_forms.compatibility import string_types, text_type
 from crispy_forms.utils import render_field, flatatt, TEMPLATE_PACK, get_template_pack
 
+class TemplateNameMixin(object):
 
-class LayoutObject(object):
+    def get_template_name(self, template_pack):
+        if '%s' in self.template:
+            template = self.template % template_pack
+        else:
+            template = self.template
+
+        return template
+
+class LayoutObject(TemplateNameMixin):
     def __getitem__(self, slice):
         return self.fields[slice]
 
@@ -150,7 +159,7 @@ def __init__(self, *fields, **kwargs):
     def render(self, form, form_style, context, template_pack=TEMPLATE_PACK, **kwargs):
         html = self.get_rendered_fields(form, form_style, context, template_pack, **kwargs)
 
-        template = self.template % template_pack
+        template = self.get_template_name(template_pack)
         return render_to_string(
             template,
             {'buttonholder': self, 'fields_output': html},
@@ -158,7 +167,7 @@ def render(self, form, form_style, context, template_pack=TEMPLATE_PACK, **kwarg
         )
 
 
-class BaseInput(object):
+class BaseInput(TemplateNameMixin):
     """
     A base class to reduce the amount of code in the Input classes.
     """
@@ -182,7 +191,7 @@ def render(self, form, form_style, context, template_pack=TEMPLATE_PACK, **kwarg
         Input button value can be a variable in context.
         """
         self.value = Template(text_type(self.value)).render(context)
-        template = self.template % template_pack
+        template = self.get_template_name(template_pack)
         return render_to_string(template, {'input': self}, context)
 
 
@@ -275,7 +284,7 @@ def render(self, form, form_style, context, template_pack=TEMPLATE_PACK, **kwarg
         if self.legend:
             legend = u'%s' % Template(text_type(self.legend)).render(context)
 
-        template = self.template % template_pack
+        template = self.get_template_name(template_pack)
         return render_to_string(
             template,
             {'fieldset': self, 'legend': legend, 'fields': fields, 'form_style': form_style}
@@ -314,7 +323,7 @@ def render(self, form, form_style, context, template_pack=TEMPLATE_PACK, **kwarg
             'multifield': self,
             'fields_output': fields_output
         }
-        template = self.template % template_pack
+        template = self.get_template_name(template_pack)
         return render_to_string(template, extra_context, context)
 
 
@@ -343,7 +352,7 @@ def __init__(self, *fields, **kwargs):
     def render(self, form, form_style, context, template_pack=TEMPLATE_PACK, **kwargs):
         fields = self.get_rendered_fields(form, form_style, context, template_pack, **kwargs)
 
-        template = self.template % template_pack
+        template = self.get_template_name(template_pack)
         return render_to_string(template, {'div': self, 'fields': fields})
 
 
@@ -421,7 +430,8 @@ def render(self, form, form_style, context, template_pack=TEMPLATE_PACK, extra_c
         if hasattr(self, 'wrapper_class'):
             extra_context['wrapper_class'] = self.wrapper_class
 
-        template = self.template % template_pack
+        template = self.get_template_name(template_pack)
+
         return self.get_rendered_fields(
             form, form_style, context, template_pack,
             template=template, attrs=self.attrs, extra_context=extra_context,


Test patch:
diff --git a/crispy_forms/tests/test_layout_objects.py b/crispy_forms/tests/test_layout_objects.py
--- a/crispy_forms/tests/test_layout_objects.py
+++ b/crispy_forms/tests/test_layout_objects.py
@@ -25,6 +25,20 @@
 )
 from crispy_forms.utils import render_crispy_form
 
+def test_field_with_custom_template():
+    template = get_template_from_string(u"""
+        {% load crispy_forms_tags %}
+        {% crispy form %}
+    """)
+
+    test_form = TestForm()
+    test_form.helper = FormHelper()
+    test_form.helper.layout = Layout(
+        Field('email',template='custom_field_template.html')
+    )
+
+    html = render_crispy_form(test_form)
+    assert '<h1>Special custom field</h1>' in html
 
 def test_multiwidget_field():
     template = get_template_from_string(u"""