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::
@@ -64,6 +70,32 @@ It is also possible to create a horizontal form. The form class and template tag
+Custom Layout +~~~~~~~~~~~~~~~~~ + +For custom layout - use {% bootstrap %} tag - each line in it represent bootstrap .row with fields separted by space:: + +
+ Form Title + {% csrf_token %} + {% bootstrap form %} + char_field choice_field + radio_choice multiple_choice multiple_checkbox + password_field file_fied + textarea + boolean_field + {% endbootstrap %} +
+
+ +
+
+
+ +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