diff --git a/CHANGES.txt b/CHANGES.txt
index 43df05b..761a5c6 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,3 +1,6 @@
+- 2018-01-12:
+ - {% bootstrap %} tag by @vital1k
+
- 2015-03-09:
- Fix unit test fail with Django 1.7 @nikolas
diff --git a/README.rst b/README.rst
index 516ee0e..410d954 100644
--- a/README.rst
+++ b/README.rst
@@ -34,6 +34,9 @@ Add "bootstrapform" to your INSTALLED_APPS.
At the top of your template load in our template tags::
{% load bootstrap %}
+
+Vertical
+~~~~~~~~~~~~~~~~~
Then to render your form::
@@ -50,6 +53,9 @@ You can also set class="form-vertical" on the form element.
To use class="form-inline" on the form element, also change the "|boostrap" template tag to "|bootstrap_inline".
+Horizontal
+~~~~~~~~~~~~~~~~~
+
It is also possible to create a horizontal form. The form class and template tag are both changed, and you will also need slightly different CSS around the submit button::
+Custom Layout
+~~~~~~~~~~~~~~~~~
+
+For custom layout - use {% bootstrap %} tag - each line in it represent bootstrap .row with fields separted by space::
+
+
+
+Will result layout like this
+
+.. image:: docs/_static/bootstrap_tag.png
+
Demo
=====
diff --git a/bootstrapform/fixtures/bootstrap_tag.html b/bootstrapform/fixtures/bootstrap_tag.html
new file mode 100644
index 0000000..1c8d0ea
--- /dev/null
+++ b/bootstrapform/fixtures/bootstrap_tag.html
@@ -0,0 +1,191 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/bootstrapform/fixtures/bootstrap_tag_dj16.html b/bootstrapform/fixtures/bootstrap_tag_dj16.html
new file mode 100644
index 0000000..4125f7e
--- /dev/null
+++ b/bootstrapform/fixtures/bootstrap_tag_dj16.html
@@ -0,0 +1,190 @@
+
\ No newline at end of file
diff --git a/bootstrapform/fixtures/bootstrap_tag_old.html b/bootstrapform/fixtures/bootstrap_tag_old.html
new file mode 100644
index 0000000..c3f4981
--- /dev/null
+++ b/bootstrapform/fixtures/bootstrap_tag_old.html
@@ -0,0 +1,190 @@
+
\ No newline at end of file
diff --git a/bootstrapform/templatetags/bootstrap.py b/bootstrapform/templatetags/bootstrap.py
index b0dcc75..277bac0 100644
--- a/bootstrapform/templatetags/bootstrap.py
+++ b/bootstrapform/templatetags/bootstrap.py
@@ -1,3 +1,4 @@
+import re
import django
from django import forms, VERSION as django_version
from django.template import Context
@@ -8,6 +9,7 @@
register = template.Library()
+
@register.filter
def bootstrap(element):
markup_classes = {'label': '', 'value': '', 'single_value': ''}
@@ -46,6 +48,7 @@ def bootstrap_horizontal(element, label_cols='col-sm-2 col-lg-2'):
return render(element, markup_classes)
+
@register.filter
def add_input_classes(field):
if not is_checkbox(field) and not is_multiple_checkbox(field) \
@@ -78,7 +81,6 @@ def render(element, markup_classes):
template = get_template("bootstrapform/form.html")
context = {'form': element, 'classes': markup_classes}
-
if django_version < (1, 8):
context = Context(context)
@@ -103,3 +105,67 @@ def is_radio(field):
@register.filter
def is_file(field):
return isinstance(field.field.widget, forms.FileInput)
+
+
+# {% bootstrap %} tag
+
+@register.tag(name='bootstrap')
+def bootstrap_tag(parser, token):
+ nodelist = parser.parse(('endbootstrap',))
+ try:
+ # Splitting by None == splitting by spaces.
+ tag_name, form = token.contents.split(None, 1)
+ except ValueError:
+ raise template.TemplateSyntaxError(
+ "%r tag requires a form arguments" % token.contents.split()[0]
+ )
+ parser.delete_first_token()
+ return Bootstrap(nodelist, form)
+
+
+class Bootstrap(template.Node):
+ def __init__(self, nodelist, form):
+ self.nodelist = nodelist
+ self.form_variable = form
+
+ def render(self, context):
+ tag_contents = self.nodelist.render(context)
+ self.form = context[self.form_variable]
+ return ''.join(self._get_rows(tag_contents))
+
+ def _get_rows(self, tag_contents):
+ for row in self._parse_fields(tag_contents):
+ output = [
+ '',
+ ''.join(self._get_fields(row)),
+ '
',
+ ]
+ yield ''.join(output)
+
+ def _get_fields(self, row):
+ for f, size in row:
+ col_class = 'col'
+ if size:
+ col_class += '-md-' + size
+ try:
+ f = self.form[f]
+ except KeyError as e:
+ raise Exception('Failed to process line\n{}\n{}'.format(row, e))
+ yield '{}
'.format(col_class, bootstrap(f))
+
+ def _parse_fields(self, tag_contents):
+ result = []
+ for line in tag_contents.splitlines():
+ line = line.strip()
+ if not line:
+ continue
+ field_names = [i.strip() for i in line.split(' ') if i.strip()]
+ row = []
+ for name in field_names:
+ if '(' in name:
+ name, col_size = re.findall(r'^(.*?)\((\d+)\)$', name)[0]
+ else:
+ col_size = None
+ row.append((name, col_size))
+ result.append(row)
+ return result
diff --git a/bootstrapform/tests.py b/bootstrapform/tests.py
index f56f39b..da3d47b 100644
--- a/bootstrapform/tests.py
+++ b/bootstrapform/tests.py
@@ -12,8 +12,8 @@
CHOICES = (
- (0, 'Zero'),
- (1, 'One'),
+ (0, 'Zero'),
+ (1, 'One'),
(2, 'Two'),
)
@@ -23,6 +23,7 @@
except:
pass
+
class ExampleForm(forms.Form):
char_field = forms.CharField(required=False)
choice_field = forms.ChoiceField(choices=CHOICES, required=False)
@@ -43,7 +44,6 @@ def test_basic_form(self):
html = Template("{% load bootstrap %}{{ form|bootstrap }}").render(Context({'form': form}))
-
if StrictVersion(django.get_version()) >= StrictVersion('1.7'):
fixture = 'basic.html'
elif StrictVersion(django.get_version()) >= StrictVersion('1.6'):
@@ -80,3 +80,31 @@ def test_bound_field(self):
self.assertTrue(form.is_bound)
rendered_template = bootstrap.bootstrap(form['char_field'])
+
+ def test_bootstrap_tag(self):
+ form = ExampleForm()
+
+ tpl_str = """
+ {% load bootstrap %}
+ {% bootstrap form %}
+ char_field choice_field radio_choice
+ multiple_choice multiple_checkbox
+ file_fied password_field
+ textarea
+ boolean_field
+ {% endbootstrap %}
+ """
+ html = Template(tpl_str).render(Context({'form': form}))
+
+ if StrictVersion(django.get_version()) >= StrictVersion('1.7'):
+ fixture = 'bootstrap_tag.html'
+ elif StrictVersion(django.get_version()) >= StrictVersion('1.6'):
+ fixture = 'bootstrap_tag_dj16.html'
+ else:
+ fixture = 'bootstrap_tag_old.html'
+
+ tpl = os.path.join('fixtures', fixture)
+ with open(os.path.join(TEST_DIR, tpl)) as f:
+ content = f.read()
+
+ self.assertHTMLEqual(html, content)
diff --git a/docs/_static/bootstrap_tag.png b/docs/_static/bootstrap_tag.png
new file mode 100644
index 0000000..2bc65fa
Binary files /dev/null and b/docs/_static/bootstrap_tag.png differ