Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Aaron Merriam committed Jul 24, 2013
0 parents commit 1df4215
Show file tree
Hide file tree
Showing 36 changed files with 2,057 additions and 0 deletions.
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
*.pyc
*.egg-info/
docs/_build/
tests/sqlite_database
tests/.coverage
tests/htmlcov/
dist/
13 changes: 13 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
language: python
python:
- "2.7"
- "2.6"
env:
- DJANGO_PACKAGE=Django==1.3.7
- DJANGO_PACKAGE=Django==1.4.5
- DJANGO_PACKAGE=Django==1.5.1
- DJANGO_PACKAGE=https://github.com/django/django/archive/master.zip
install:
- pip install -q $DJANGO_PACKAGE --use-mirrors
- python setup.py install
script: make test
Empty file added CHANGES
Empty file.
22 changes: 22 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
Copyright (c) 2013, Fusionbox, Inc.
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

- Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
18 changes: 18 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
TESTS=tests betterforms
SETTINGS=tests.sqlite_test_settings
COVERAGE_COMMAND=


test: test-builtin

test-builtin:
cd tests && DJANGO_SETTINGS_MODULE=$(SETTINGS) $(COVERAGE_COMMAND) ./manage.py test --traceback $(TESTS) --verbosity=2

coverage:
+make test COVERAGE_COMMAND='coverage run --source=betterforms --branch --parallel-mode'
cd tests && coverage combine && coverage html

docs:
cd docs && $(MAKE) html

.PHONY: test test-builtin coverage docs
18 changes: 18 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
django-betterforms
------------------

.. image:: https://travis-ci.org/fusionbox/django-betterforms.png-
:target: http://travis-ci.org/fusionbox/django-betterforms
:alt: Build Status

`django-betterforms` builds on the build in django forms.


Installation
============

1. Install the package::

$ pip install django-betterforms

2. Add ``betterforms`` to your ``INSTALLED_APPS``.
Empty file added betterforms/__init__.py
Empty file.
230 changes: 230 additions & 0 deletions betterforms/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
import collections

from django import forms
from django.forms.util import ErrorList, ErrorDict
from django.template.loader import render_to_string
from django.utils.datastructures import SortedDict


class CSSClassMixin(object):
"""
Sane defaults for error and css classes.
"""
error_css_class = 'error'
required_css_class = 'required'


class NonBraindamagedErrorMixin(object):
"""
Form mixin for easier field based error messages.
"""
def field_error(self, name, error):
self._errors = self._errors or ErrorDict()
self._errors.setdefault(name, ErrorList())
self._errors[name].append(error)


def process_fieldset_row(fields, fieldset_class, base_name):
for index, row in enumerate(fields):
if not isinstance(row, (basestring, Fieldset)):
if len(row) == 2 and isinstance(row[0], basestring) and isinstance(row[1], dict):
row = fieldset_class(row[0], **row[1])
else:
row = fieldset_class("{0}_{1}".format(base_name, index), fields=row)
yield row


def flatten(elements):
"""
Flattens a mixed list of strings and iterables of strings into a single
iterable of strings.
"""
for element in elements:
if isinstance(element, collections.Iterable) and not isinstance(element, basestring):
for sub_element in flatten(element):
yield sub_element
else:
yield element

flatten_to_tuple = lambda x: tuple(flatten(x))


class Fieldset(CSSClassMixin):
FIELDSET_CSS_CLASS = 'formFieldset'
css_classes = None
template_name = None

def __init__(self, name, fields=[], **kwargs):
self.name = name
self.base_fields = tuple(process_fieldset_row(fields, type(self), name))

# Check for duplicate names.
names = (str(thing) for thing in self.base_fields)
duplicates = [x for x, y in collections.Counter(names).items() if y > 1]
if duplicates:
raise AttributeError('Name Conflict in fieldset `{0}`. The name(s) `{1}` appear multiple times.'.format(self.name, duplicates))
for key, value in kwargs.iteritems():
setattr(self, key, value)

def __iter__(self):
return iter(self.base_fields)

def __nonzero__(self):
return bool(self.base_fields)

def __unicode__(self):
return unicode(self.name)

# These methods need to be implemented to render the fieldsets and fields
# in a similar structure as `BaseForm`
def as_table(self):
raise NotImplementedError('To be implemented')

def as_ul(self):
raise NotImplementedError('To be implemented')

def as_p(self):
raise NotImplementedError('To be implemented')

@property
def fields(self):
return flatten_to_tuple(self)


class BoundFieldset(object):
is_fieldset = True

def __init__(self, form, fieldset, name):
self.form = form
self.name = name
self.fieldset = fieldset
self.rows = SortedDict()
for row in fieldset:
self.rows[unicode(row)] = row

def __getitem__(self, key):
"""
>>> fieldset[1]
# returns the item at index-1 in the fieldset
>>> fieldset['name']
# returns the item in the fieldset under the key 'name'
"""
if isinstance(key, int) and not key in self.rows:
return self[self.rows.keyOrder[key]]
value = self.rows[key]
if isinstance(value, basestring):
return self.form[value]
else:
return type(self)(self.form, value, key)

def __str__(self):
env = {
'fieldset': self,
'form': self.form,
'fieldset_template_name': 'partials/fieldset_as_divs.html',
}
# TODO: don't hardcode the default template name.
return render_to_string(self.template_name or 'partials/fieldset_as_divs.html', env)

def __iter__(self):
for name in self.rows.keys():
yield self[name]

@property
def template_name(self):
return self.fieldset.template_name

@property
def errors(self):
return self.form.errors.get(self.name, self.form.error_class())

@property
def css_classes(self):
css_classes = set((self.fieldset.FIELDSET_CSS_CLASS, self.name))
css_classes.update(self.fieldset.css_classes or [])
if self.errors:
css_classes.add(self.fieldset.error_css_class)
return ' '.join(css_classes)


class FieldsetMixin(NonBraindamagedErrorMixin):
template_name = None
fieldset_class = Fieldset
bound_fieldset_class = BoundFieldset
base_fieldsets = tuple()

@property
def fieldsets(self):
return self.bound_fieldset_class(self, self.base_fieldsets, self.base_fieldsets.name)

def __getitem__(self, key):
try:
return super(FieldsetMixin, self).__getitem__(key)
except KeyError:
return self.fieldsets[key]

def __iter__(self):
return iter(self.fieldsets)

def __str__(self):
if not self.fieldsets:
return super(FieldsetMixin, self).__str__()
return '\n'.join(str(fieldset) for fieldset in self.fieldsets)


def get_fieldsets(bases, attrs):
try:
return attrs['Meta'].fieldsets
except (KeyError, AttributeError):
for base in bases:
fieldsets = getattr(base, 'base_fieldsets', None)
if fieldsets is not None:
return fieldsets or []
return []


def get_fieldset_class(bases, attrs):
if 'fieldset_class' in attrs:
return attrs['fieldset_class']
else:
for base in bases:
try:
return base.fieldset_class
except AttributeError:
continue
return Fieldset


class BetterModelFormMetaclass(forms.models.ModelFormMetaclass):
def __new__(cls, name, bases, attrs):
base_fieldsets = get_fieldsets(bases, attrs)
if base_fieldsets:
FieldsetClass = get_fieldset_class(bases, attrs)
base_fieldsets = FieldsetClass('__base_fieldset__', fields=base_fieldsets)
attrs['base_fieldsets'] = base_fieldsets
Meta = attrs.get('Meta')
if Meta and Meta.__dict__.get('fields') is None and Meta.__dict__.get('exclude') is None:
attrs['Meta'].fields = flatten_to_tuple(base_fieldsets)
attrs['base_fieldsets'] = base_fieldsets
return super(BetterModelFormMetaclass, cls).__new__(cls, name, bases, attrs)


class BetterModelForm(FieldsetMixin, CSSClassMixin, forms.ModelForm):
__metaclass__ = BetterModelFormMetaclass
pass


class BetterFormMetaClass(forms.forms.DeclarativeFieldsMetaclass):
def __new__(cls, name, bases, attrs):
FieldsetClass = get_fieldset_class(bases, attrs)
base_fieldsets = get_fieldsets(bases, attrs)
attrs['base_fieldsets'] = FieldsetClass('__base_fieldset__', fields=base_fieldsets)
return super(BetterFormMetaClass, cls).__new__(cls, name, bases, attrs)


class BetterForm(FieldsetMixin, CSSClassMixin, forms.forms.BaseForm):
"""
A 'Better' base Form class.
"""
__metaclass__ = BetterFormMetaClass
pass
Empty file added betterforms/models.py
Empty file.
20 changes: 20 additions & 0 deletions betterforms/templates/partials/field_as_div.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{% load betterforms_tags %}
{% if field.is_hidden %}
{{ field }}
{% else %}
<div class="{{ field.css_classes }} {{ field.html_name }}{% if required %} required{% endif %} formField">
{% if not field|is_checkbox %}
{{ field.label_tag }}
{% endif %}

{% if field.help_text %}
<p class="help_text">{{ field.help_text }}</p>
{% endif %}

{{ field }}
{% if field|is_checkbox %}
{{ field.label_tag }}
{% endif %}
{{ field.errors }}
</div>
{% endif %}
14 changes: 14 additions & 0 deletions betterforms/templates/partials/fieldset_as_divs.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{% if fieldset.template_name %}
{% include fieldset.template_name %}
{% else %}
<fieldset class="{{ fieldset.css_classes }}">
{{ fieldset.errors }}
{% for row in fieldset %}
{% if row.is_fieldset %}
{% include fieldset_template_name with fieldset=row %}
{% else %}
{% include "partials/field_as_div.html" with field=row %}
{% endif %}
{% endfor %}
</fieldset>
{% endif %}
27 changes: 27 additions & 0 deletions betterforms/templates/partials/form_as_fieldsets.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{% block form_head %}
{% if not no_head %}
{% csrf_token %}
{% if next %}
<input type="hidden" name="next" value="{{ next }}">
{% endif %}
{{ form.media }}
{% endif %}
{% endblock %}

{% block form_body %}
{{ form.non_field_errors }}
{% if form.fieldsets %}
{% with fieldset_template_name="partials/fieldset_as_divs.html" %}
{% for fieldset in form.fieldsets %}
{# Hack to allow recursive template inclusion #}
{% include fieldset_template_name %}
{% endfor %}
{% endwith %}
{% else %}
<fieldset>
{% for field in form %}
{% include "partials/field_as_div.html" %}
{% endfor %}
</fieldset>
{% endif %}
{% endblock %}
Empty file.
13 changes: 13 additions & 0 deletions betterforms/templatetags/betterforms_tags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from django import forms
from django import template

register = template.Library()


@register.filter(name='is_checkbox')
def is_checkbox(field):
"""
Boolean filter for form fields to determine if a field is using a checkbox
widget.
"""
return isinstance(field.field.widget, forms.CheckboxInput)
Empty file added betterforms/tests.py
Empty file.
Loading

0 comments on commit 1df4215

Please sign in to comment.