diff --git a/django/forms/forms.py b/django/forms/forms.py
index 94eb22d..e8031fb 100644
--- a/django/forms/forms.py
+++ b/django/forms/forms.py
@@ -54,19 +54,34 @@ def get_declared_fields(bases, attrs, with_base_fields=True):
 
     return SortedDict(fields)
 
-class DeclarativeFieldsMetaclass(type):
-    """
-    Metaclass that converts Field attributes to a dictionary called
-    'base_fields', taking into account parent class 'base_fields' as well.
-    """
+class BaseFormOptions(object):
+    def __init__(self, options=None):
+        self.fieldsets = getattr(options, 'fieldsets', None)
+
+class BaseFormMetaclass(type):
     def __new__(cls, name, bases, attrs):
-        attrs['base_fields'] = get_declared_fields(bases, attrs)
-        new_class = super(DeclarativeFieldsMetaclass,
-                     cls).__new__(cls, name, bases, attrs)
+        try:
+            parents = [b for b in bases if issubclass(b, BaseForm)]
+        except NameError:
+            # We are defining Form itself.
+            parents = None
+        new_class = super(BaseFormMetaclass, cls).__new__(cls, name, bases, attrs)
+        if not parents:
+            return new_class
         if 'media' not in attrs:
             new_class.media = media_property(new_class)
+        new_class._meta = BaseFormOptions(getattr(new_class, 'Meta', None))
         return new_class
 
+class DeclarativeFieldsMetaclass(BaseFormMetaclass):
+     """
+     Metaclass that converts Field attributes to a dictionary called
+     'base_fields', taking into account parent class 'base_fields' as well.
+     """
+     def __new__(cls, name, bases, attrs):
+         attrs['base_fields'] = get_declared_fields(bases, attrs)
+         return super(DeclarativeFieldsMetaclass, cls).__new__(cls, name, bases, attrs)
+
 class BaseForm(StrAndUnicode):
     # This is the main implementation of all the Form logic. Note that this
     # class is different than Form. See the comments by the Form class for more
@@ -138,104 +153,50 @@ class BaseForm(StrAndUnicode):
         """
         return u'initial-%s' % self.add_prefix(field_name)
 
-    def _html_output(self, normal_row, error_row, row_ender, help_text_html, errors_on_separate_row):
+    def _html_output(self, fieldset_method, error_row, before_fieldset=u'', after_fieldset=u''):
         "Helper function for outputting HTML. Used by as_table(), as_ul(), as_p()."
         top_errors = self.non_field_errors() # Errors that should be displayed above all fields.
-        output, hidden_fields = [], []
-
-        for name, field in self.fields.items():
-            html_class_attr = ''
-            bf = self[name]
-            bf_errors = self.error_class([conditional_escape(error) for error in bf.errors]) # Escape and cache in local variable.
-            if bf.is_hidden:
-                if bf_errors:
-                    top_errors.extend([u'(Hidden field %s) %s' % (name, force_unicode(e)) for e in bf_errors])
-                hidden_fields.append(unicode(bf))
-            else:
-                # Create a 'class="..."' atribute if the row should have any
-                # CSS classes applied.
-                css_classes = bf.css_classes()
-                if css_classes:
-                    html_class_attr = ' class="%s"' % css_classes
-
-                if errors_on_separate_row and bf_errors:
-                    output.append(error_row % force_unicode(bf_errors))
-
-                if bf.label:
-                    label = conditional_escape(force_unicode(bf.label))
-                    # Only add the suffix if the label does not end in
-                    # punctuation.
-                    if self.label_suffix:
-                        if label[-1] not in ':?.!':
-                            label += self.label_suffix
-                    label = bf.label_tag(label) or ''
-                else:
-                    label = ''
-
-                if field.help_text:
-                    help_text = help_text_html % force_unicode(field.help_text)
-                else:
-                    help_text = u''
-
-                output.append(normal_row % {
-                    'errors': force_unicode(bf_errors),
-                    'label': force_unicode(label),
-                    'field': unicode(bf),
-                    'help_text': help_text,
-                    'html_class_attr': html_class_attr
-                })
+        output = []
+
+        for fieldset in self.fieldsets:
+            fieldset_html = [getattr(fieldset, fieldset_method)()]
+            if not fieldset.dummy:
+                fieldset_html.insert(0, u'<fieldset>')
+                fieldset_html.insert(1, before_fieldset)
+                fieldset_html.append(after_fieldset)
+                fieldset_html.append(u'</fieldset>')
+                if fieldset.legend:
+                    fieldset_html.insert(1, fieldset.legend_tag())
+                if top_errors:
+                    output.insert(0, force_unicode(top_errors))
+            output.extend(fieldset_html)
 
         if top_errors:
             output.insert(0, error_row % force_unicode(top_errors))
 
-        if hidden_fields: # Insert any hidden fields in the last row.
-            str_hidden = u''.join(hidden_fields)
-            if output:
-                last_row = output[-1]
-                # Chop off the trailing row_ender (e.g. '</td></tr>') and
-                # insert the hidden fields.
-                if not last_row.endswith(row_ender):
-                    # This can happen in the as_p() case (and possibly others
-                    # that users write): if there are only top errors, we may
-                    # not be able to conscript the last row for our purposes,
-                    # so insert a new, empty row.
-                    last_row = (normal_row % {'errors': '', 'label': '',
-                                              'field': '', 'help_text':'',
-                                              'html_class_attr': html_class_attr})
-                    output.append(last_row)
-                output[-1] = last_row[:-len(row_ender)] + str_hidden + row_ender
-            else:
-                # If there aren't any rows in the output, just append the
-                # hidden fields.
-                output.append(str_hidden)
         return mark_safe(u'\n'.join(output))
 
     def as_table(self):
         "Returns this form rendered as HTML <tr>s -- excluding the <table></table>."
         return self._html_output(
-            normal_row = u'<tr%(html_class_attr)s><th>%(label)s</th><td>%(errors)s%(field)s%(help_text)s</td></tr>',
+            fieldset_method='as_table',
             error_row = u'<tr><td colspan="2">%s</td></tr>',
-            row_ender = u'</td></tr>',
-            help_text_html = u'<br /><span class="helptext">%s</span>',
-            errors_on_separate_row = False)
+            before_fieldset=u'<table>',
+            after_fieldset=u'</table>')
 
     def as_ul(self):
         "Returns this form rendered as HTML <li>s -- excluding the <ul></ul>."
         return self._html_output(
-            normal_row = u'<li%(html_class_attr)s>%(errors)s%(label)s %(field)s%(help_text)s</li>',
+            fieldset_method='as_ul',
             error_row = u'<li>%s</li>',
-            row_ender = '</li>',
-            help_text_html = u' <span class="helptext">%s</span>',
-            errors_on_separate_row = False)
+            before_fieldset=u'<ul>',
+            after_fieldset=u'</ul>')
 
     def as_p(self):
         "Returns this form rendered as HTML <p>s."
         return self._html_output(
-            normal_row = u'<p%(html_class_attr)s>%(label)s %(field)s%(help_text)s</p>',
-            error_row = u'%s',
-            row_ender = '</p>',
-            help_text_html = u' <span class="helptext">%s</span>',
-            errors_on_separate_row = True)
+            fieldset_method='as_p',
+            error_row = u'%s')
 
     def non_field_errors(self):
         """
@@ -380,6 +341,19 @@ class BaseForm(StrAndUnicode):
         """
         return [field for field in self if not field.is_hidden]
 
+    def _fieldsets(self):
+        """
+        Returns a list of Fieldset objects for each fieldset
+        defined in Form's Meta options. If no fieldsets were defined,
+        returns a list containing single, 'dummy' Fieldset with
+        all form fields.
+        """
+        if self._meta.fieldsets:
+            return [Fieldset(self, legend, attrs.get('fields', tuple()))
+                    for legend, attrs in self._meta.fieldsets]
+        return [Fieldset(self, None, self.fields.keys(), dummy=True)]
+    fieldsets = property(_fieldsets)
+
 class Form(BaseForm):
     "A collection of Fields, plus their associated data."
     # This is a separate class from BaseForm in order to abstract the way
@@ -389,6 +363,167 @@ class Form(BaseForm):
     # BaseForm itself has no way of designating self.fields.
     __metaclass__ = DeclarativeFieldsMetaclass
 
+class Fieldset(StrAndUnicode):
+
+    def __init__(self, form, legend, fields, dummy=False):
+        """
+        Arguments:
+        form   -- form this fieldset belongs to
+        legend -- fieldset's legend (used in <legend> tag)
+        fields -- list containing names of fields in this fieldset
+
+        Keyword arguments:
+        dummy  -- flag informing that the fieldset was created automatically
+                  from all fields of form, because user has not defined
+                  custom fieldsets
+        """
+        self.form = form
+        self.legend = legend
+        self.fields = fields
+        self.dummy = dummy
+
+    def __unicode__(self):
+        return self.as_table()
+
+    def __iter__(self):
+        for name in self.fields:
+            yield BoundField(self.form, self.form.fields[name], name)
+
+    def __getitem__(self, name):
+        "Returns a BoundField with the given name."
+        if not name in self.fields:
+            raise KeyError('Key %r not found in Fieldset' % name)
+        return self.form[name]
+
+    def _html_output(self, normal_row, error_row, row_ender, help_text_html, errors_on_separate_row):
+        "Helper function for outputting HTML. Used by as_table(), as_ul(), as_p()."
+        output, hidden_fields = [], []
+        top_errors = self.form.error_class()
+
+        for name in self.fields:
+            field = self.form.fields[name]
+            html_class_attr = ''
+            bf = self.form[name]
+            bf_errors = self.form.error_class([conditional_escape(error) for error in bf.errors]) # Escape and cache in local variable.
+            if bf.is_hidden:
+                if bf_errors:
+                    top_errors.extend([u'(Hidden field %s) %s' % (name, force_unicode(e)) for e in bf_errors])
+                hidden_fields.append(unicode(bf))
+            else:
+                # Create a 'class="..."' atribute if the row should have any
+                # CSS classes applied.
+                css_classes = bf.css_classes()
+                if css_classes:
+                    html_class_attr = ' class="%s"' % css_classes
+
+                if errors_on_separate_row and bf_errors:
+                    output.append(error_row % force_unicode(bf_errors))
+
+                if bf.label:
+                    label = conditional_escape(force_unicode(bf.label))
+                    # Only add the suffix if the label does not end in
+                    # punctuation.
+                    if self.form.label_suffix:
+                        if label[-1] not in ':?.!':
+                            label += self.form.label_suffix
+                    label = bf.label_tag(label) or ''
+                else:
+                    label = ''
+
+                if field.help_text:
+                    help_text = help_text_html % force_unicode(field.help_text)
+                else:
+                    help_text = u''
+
+                output.append(normal_row % {
+                    'errors': force_unicode(bf_errors),
+                    'label': force_unicode(label),
+                    'field': unicode(bf),
+                    'help_text': help_text,
+                    'html_class_attr': html_class_attr
+                })
+
+        if top_errors:
+            output.insert(0, error_row % force_unicode(top_errors))
+
+        if hidden_fields: # Insert any hidden fields in the last row.
+            str_hidden = u''.join(hidden_fields)
+            if output:
+                last_row = output[-1]
+                # Chop off the trailing row_ender (e.g. '</td></tr>') and
+                # insert the hidden fields.
+                if not last_row.endswith(row_ender):
+                    # This can happen in the as_p() case (and possibly others
+                    # that users write): if there are only top errors, we may
+                    # not be able to conscript the last row for our purposes,
+                    # so insert a new, empty row.
+                    last_row = (normal_row % {'errors': '', 'label': '',
+                                              'field': '', 'help_text':'',
+                                              'html_class_attr': html_class_attr})
+                    output.append(last_row)
+                output[-1] = last_row[:-len(row_ender)] + str_hidden + row_ender
+            else:
+                # If there aren't any rows in the output, just append the
+                # hidden fields.
+                output.append(str_hidden)
+
+        return mark_safe(u'\n'.join(output))
+
+    def as_table(self):
+        "Returns this fieldset rendered as HTML <tr>s -- excluding the <table>, <fieldset> and <legend> tags."
+        return self._html_output(
+            normal_row = u'<tr%(html_class_attr)s><th>%(label)s</th><td>%(errors)s%(field)s%(help_text)s</td></tr>',
+            error_row = u'<tr><td colspan="2">%s</td></tr>',
+            row_ender = u'</td></tr>',
+            help_text_html = u'<br /><span class="helptext">%s</span>',
+            errors_on_separate_row = False)
+
+    def as_ul(self):
+        "Returns this fieldset rendered as HTML <li>s -- excluding the <ul>, <fieldset> and <legend> tags."
+        return self._html_output(
+            normal_row = u'<li%(html_class_attr)s>%(errors)s%(label)s %(field)s%(help_text)s</li>',
+            error_row = u'<li>%s</li>',
+            row_ender = '</li>',
+            help_text_html = u' <span class="helptext">%s</span>',
+            errors_on_separate_row = False)
+
+    def as_p(self):
+        "Returns this fieldset rendered as HTML <p>s -- excluding the <fieldset> and <legend> tags."
+        return self._html_output(
+            normal_row = u'<p%(html_class_attr)s>%(label)s %(field)s%(help_text)s</p>',
+            error_row = u'%s',
+            row_ender = '</p>',
+            help_text_html = u' <span class="helptext">%s</span>',
+            errors_on_separate_row = True)
+
+    def legend_tag(self, contents=None, attrs=None):
+        """
+        Wraps the given contents in a <legend>. Does not HTML-escape the contents.
+        If contents aren't given, uses the fieldset's HTML-escaped legend.
+
+        If attrs are given, they're used as HTML attributes on the <legend> tag.
+        """
+        if contents is None and not self.legend is None:
+            contents = conditional_escape(self.legend)
+        attrs = attrs and flatatt(attrs) or ''
+        if not contents is None:
+            return mark_safe(u'<legend%s>%s</legend>' % (attrs, force_unicode(self.legend)))
+        return None
+
+    def hidden_fields(self):
+        """
+        Returns a list of all the BoundField objects that are hidden fields.
+        Useful for manual form layout in templates.
+        """
+        return [field for field in self if field.is_hidden]
+
+    def visible_fields(self):
+        """
+        Returns a list of BoundField objects that aren't hidden fields.
+        The opposite of the hidden_fields() method.
+        """
+        return [field for field in self if not field.is_hidden]
+
 class BoundField(StrAndUnicode):
     "A Field plus data"
     def __init__(self, form, field, name):
diff --git a/django/forms/models.py b/django/forms/models.py
index cd8f027..d3d3d2e 100644
--- a/django/forms/models.py
+++ b/django/forms/models.py
@@ -8,11 +8,12 @@ from __future__ import absolute_import
 from django.core.exceptions import ValidationError, NON_FIELD_ERRORS, FieldError
 from django.core.validators import EMPTY_VALUES
 from django.forms.fields import Field, ChoiceField
-from django.forms.forms import BaseForm, get_declared_fields
+from django.forms.forms import (BaseForm, BaseFormOptions, BaseFormMetaclass,
+    get_declared_fields)
 from django.forms.formsets import BaseFormSet, formset_factory
 from django.forms.util import ErrorList
 from django.forms.widgets import (SelectMultiple, HiddenInput,
-    MultipleHiddenInput, media_property)
+    MultipleHiddenInput)
 from django.utils.encoding import smart_unicode, force_unicode
 from django.utils.datastructures import SortedDict
 from django.utils.text import get_text_list, capfirst
@@ -175,19 +176,19 @@ def fields_for_model(model, fields=None, exclude=None, widgets=None, formfield_c
         )
     return field_dict
 
-class ModelFormOptions(object):
+class ModelFormOptions(BaseFormOptions):
     def __init__(self, options=None):
+        super(ModelFormOptions, self).__init__(options)
         self.model = getattr(options, 'model', None)
         self.fields = getattr(options, 'fields', None)
         self.exclude = getattr(options, 'exclude', None)
         self.widgets = getattr(options, 'widgets', None)
 
-
-class ModelFormMetaclass(type):
+class ModelFormMetaclass(BaseFormMetaclass):
     def __new__(cls, name, bases, attrs):
         formfield_callback = attrs.pop('formfield_callback', None)
         try:
-            parents = [b for b in bases if issubclass(b, ModelForm)]
+            parents = [b for b in bases if issubclass(b, BaseModelForm)]
         except NameError:
             # We are defining ModelForm itself.
             parents = None
@@ -196,9 +197,10 @@ class ModelFormMetaclass(type):
                 attrs)
         if not parents:
             return new_class
-
-        if 'media' not in attrs:
-            new_class.media = media_property(new_class)
+        # Override BaseFormOptions with ModelFormOptions (which is actually
+        # BaseFormOptions' subclass). This obviously causes BaseFormOptions.__init__()
+        # being called twice through the form class definition, but it's a price we can
+        # pay for the less redundant code.
         opts = new_class._meta = ModelFormOptions(getattr(new_class, 'Meta', None))
         if opts.model:
             # If a model is defined, extract form fields from it.
@@ -305,7 +307,7 @@ class BaseModelForm(BaseForm):
 
     def _post_clean(self):
         opts = self._meta
-        # Update the model instance with self.cleaned_data.
+        # Update the model instance with self.cleaned_data.ModelForm
         self.instance = construct_instance(self, self.instance, opts.fields, opts.exclude)
 
         exclude = self._get_validation_exclusions()
diff --git a/docs/topics/forms/index.txt b/docs/topics/forms/index.txt
index 18e55f5..bd47732 100644
--- a/docs/topics/forms/index.txt
+++ b/docs/topics/forms/index.txt
@@ -388,6 +388,116 @@ tag::
 If you find yourself doing this often, you might consider creating a custom
 :ref:`inclusion tag<howto-custom-template-tags-inclusion-tags>`.
 
+Fieldsets
+---------
+
+.. versionadded:: development
+
+Having a complex form you may want to organize its fields in logical groups.
+In Django you can do that using fieldsets. Fieldsets allow you to iterate
+through their fields and are autmatically rendered by form using ``<fieldset>``
+HTML tag and its subtag ``<legend>``.
+
+
+Fieldsets are defined using ``Meta`` options class. If you're familiar
+with :attr:`~django.contrib.admin.ModelAdmin.fieldsets` from Django admin
+options, you alreadyknow the syntax:
+
+.. code-block:: python
+
+    class PersonForm(forms.Form):
+        home_phone = CharField()
+        cell_phone = CharField()
+        first_name = CharField()
+        last_name = CharField()
+
+        class Meta:
+            fieldsets = (
+                (None, {
+                    'fields': ('first_name', 'last_name',),
+                }),
+                ("Phone numbers", {
+                    'fields': ('cell_phone', 'home_phone',),
+                }),
+            )
+
+Having above example form you may render it in a template just like a normal form::
+
+    <form action="" method="post">
+        {{ form.as_table }}
+        <input type="submit" value="Send" />
+    </form>
+
+Except now instead of one ``<table>`` element, ``as_table`` method will
+print two tables wrapped up in ``<fieldset>`` tags::
+
+    <form action="" method="post">
+        <fieldset>
+            <table>
+                <tr>
+                    <th><label for="id_first_name">First name:</label></th>
+                    <td><input type="text" name="first_name" id="id_first_name" /></td>
+                </tr>
+                <tr>
+                    <th><label for="id_last_name">Last name:</label></th>
+                    <td><input type="text" name="last_name" id="id_last_name" /></td>
+                </tr>
+            </table>
+        </fieldset>
+        <fieldset>
+            <legend>Phone numbers</legend>
+            <table>
+                <tr>
+                    <th><label for="id_cell_phone">Cell phone:</label></th>
+                    <td><input type="text" name="cell_phone" id="id_cell_phone" /></td>
+                </tr>
+                <tr>
+                    <th><label for="id_home_phone">Home phone:</label></th>
+                    <td><input type="text" name="home_phone" id="id_home_phone" /></td>
+                </tr>
+            </table>
+        </fieldset>
+        <input type="submit" value="Send" />
+    </form>
+
+You can also customize your output looping through form's fieldsets and using
+their methods -- ``as_table``, ``as_ul`` and ``as_p`` -- which behave just like
+their equivalents from ``Form`` class and using a ``legend_tag`` method::
+
+    <form action="" method="post">
+        {% for fieldset in form.fieldsets %}
+            <fieldset>
+                {{ fieldset.legend_tag }}
+                <table>
+                    {{ fieldset.as_table }}
+                </table>
+            </fieldset>
+        {% endfor %}
+        <input type="submit" value="Send" />
+    </form>
+
+You can be even more specific and loop through all fields of all fieldsets::
+
+    <form action="" method="post">
+        {% for fieldset in form.fieldsets %}
+            <fieldset>
+                {{ fieldset.legend_tag }}
+                <ul>
+                    {% for field in fieldset %}
+                        <li>
+                            {{ field.label_tag }}
+                            {{ field }}
+                        </li>
+                    {% endfor %}
+                </ul>
+            </fieldset>
+        {% endfor %}
+        <input type="submit" value="Send" />
+    </form>
+
+You can also loop though fieldset ``hidden_fields`` and ``visible_fields`` just
+line in a form class.
+
 Further topics
 ==============
 
diff --git a/tests/regressiontests/forms/tests/__init__.py b/tests/regressiontests/forms/tests/__init__.py
index 8e2150c..8c3d0fb 100644
--- a/tests/regressiontests/forms/tests/__init__.py
+++ b/tests/regressiontests/forms/tests/__init__.py
@@ -19,3 +19,4 @@ from .util import FormsUtilTestCase
 from .validators import TestFieldWithValidators
 from .widgets import (FormsWidgetTestCase, FormsI18NWidgetsTestCase,
     WidgetTests, ClearableFileInputTests)
+from .fieldsets import FieldsetsTestCase
diff --git a/tests/regressiontests/forms/tests/fieldsets.py b/tests/regressiontests/forms/tests/fieldsets.py
new file mode 100644
index 0000000..79ccabb
--- /dev/null
+++ b/tests/regressiontests/forms/tests/fieldsets.py
@@ -0,0 +1,128 @@
+# -*- coding: utf-8 -*-
+import datetime
+
+from django.core.files.uploadedfile import SimpleUploadedFile
+from django.forms import *
+from django import forms
+from django.http import QueryDict
+from django.template import Template, Context
+from django.utils.datastructures import MultiValueDict, MergeDict
+from django.utils.safestring import mark_safe
+from django.utils.unittest import TestCase
+
+
+class PersonWithoutFormfields(Form):
+    first_name = CharField()
+    last_name = CharField()
+    birthday = DateField()
+    band = CharField()
+    secret = CharField(widget=HiddenInput)
+
+class Person(PersonWithoutFormfields):
+    class Meta:
+        fieldsets = (
+            (None, {
+                'fields': ('first_name', 'last_name', 'birthday'),
+            }),
+            ("Additional fields", {
+                'fields': ('band', 'secret'),
+            }),
+        )
+
+class FieldsetsTestCase(TestCase):
+
+    some_data = {
+        'first_name': u'John',
+        'last_name': u'Lennon',
+        'birthday': u'1940-10-9',
+        'band': u'The Beatles', 
+        'secret': u'he didnt say',
+    }
+
+    def test_simple_rendering(self):
+        # Pass a dictionary to a Form's __init__().
+        p = Person(self.some_data)
+        # as_table
+        self.assertEqual(str(p), """<fieldset>
+<table>
+<tr><th><label for="id_first_name">First name:</label></th><td><input type="text" name="first_name" value="John" id="id_first_name" /></td></tr>
+<tr><th><label for="id_last_name">Last name:</label></th><td><input type="text" name="last_name" value="Lennon" id="id_last_name" /></td></tr>
+<tr><th><label for="id_birthday">Birthday:</label></th><td><input type="text" name="birthday" value="1940-10-9" id="id_birthday" /></td></tr>
+</table>
+</fieldset>
+<fieldset>
+<legend>Additional fields</legend>
+<table>
+<tr><th><label for="id_band">Band:</label></th><td><input type="text" name="band" value="The Beatles" id="id_band" /><input type="hidden" name="secret" value="he didnt say" id="id_secret" /></td></tr>
+</table>
+</fieldset>""")
+        self.assertEqual(str(p), unicode(p))
+        self.assertEqual(str(p), p.as_table())
+        # as_ul
+        self.assertEqual(p.as_ul(), """<fieldset>
+<ul>
+<li><label for="id_first_name">First name:</label> <input type="text" name="first_name" value="John" id="id_first_name" /></li>
+<li><label for="id_last_name">Last name:</label> <input type="text" name="last_name" value="Lennon" id="id_last_name" /></li>
+<li><label for="id_birthday">Birthday:</label> <input type="text" name="birthday" value="1940-10-9" id="id_birthday" /></li>
+</ul>
+</fieldset>
+<fieldset>
+<legend>Additional fields</legend>
+<ul>
+<li><label for="id_band">Band:</label> <input type="text" name="band" value="The Beatles" id="id_band" /><input type="hidden" name="secret" value="he didnt say" id="id_secret" /></li>
+</ul>
+</fieldset>""")
+        # as_p
+        self.assertEqual(p.as_p(), """<fieldset>
+
+<p><label for="id_first_name">First name:</label> <input type="text" name="first_name" value="John" id="id_first_name" /></p>
+<p><label for="id_last_name">Last name:</label> <input type="text" name="last_name" value="Lennon" id="id_last_name" /></p>
+<p><label for="id_birthday">Birthday:</label> <input type="text" name="birthday" value="1940-10-9" id="id_birthday" /></p>
+
+</fieldset>
+<fieldset>
+<legend>Additional fields</legend>
+
+<p><label for="id_band">Band:</label> <input type="text" name="band" value="The Beatles" id="id_band" /><input type="hidden" name="secret" value="he didnt say" id="id_secret" /></p>
+
+</fieldset>""") # Additional blank lines are ok
+
+    def test_single_fieldset_rendering(self):
+        # Pass a dictionary to a Form's __init__().
+        p = Person(self.some_data)
+        # as_table
+        self.assertEqual(str(p.fieldsets[0]), """<tr><th><label for="id_first_name">First name:</label></th><td><input type="text" name="first_name" value="John" id="id_first_name" /></td></tr>
+<tr><th><label for="id_last_name">Last name:</label></th><td><input type="text" name="last_name" value="Lennon" id="id_last_name" /></td></tr>
+<tr><th><label for="id_birthday">Birthday:</label></th><td><input type="text" name="birthday" value="1940-10-9" id="id_birthday" /></td></tr>""")
+        self.assertEqual(str(p.fieldsets[0]), unicode(p.fieldsets[0]))
+        self.assertEqual(str(p.fieldsets[0]), p.fieldsets[0].as_table())
+        self.assertEqual(str(p.fieldsets[1]), """<tr><th><label for="id_band">Band:</label></th><td><input type="text" name="band" value="The Beatles" id="id_band" /><input type="hidden" name="secret" value="he didnt say" id="id_secret" /></td></tr>""")
+        self.assertEqual(str(p.fieldsets[1]), p.fieldsets[1].as_table())
+        # as_ul
+        self.assertEqual(p.fieldsets[0].as_ul(), """<li><label for="id_first_name">First name:</label> <input type="text" name="first_name" value="John" id="id_first_name" /></li>
+<li><label for="id_last_name">Last name:</label> <input type="text" name="last_name" value="Lennon" id="id_last_name" /></li>
+<li><label for="id_birthday">Birthday:</label> <input type="text" name="birthday" value="1940-10-9" id="id_birthday" /></li>""")
+        self.assertEqual(p.fieldsets[1].as_ul(), """<li><label for="id_band">Band:</label> <input type="text" name="band" value="The Beatles" id="id_band" /><input type="hidden" name="secret" value="he didnt say" id="id_secret" /></li>""")
+        # as_p
+        self.assertEqual(p.fieldsets[0].as_p(), """<p><label for="id_first_name">First name:</label> <input type="text" name="first_name" value="John" id="id_first_name" /></p>
+<p><label for="id_last_name">Last name:</label> <input type="text" name="last_name" value="Lennon" id="id_last_name" /></p>
+<p><label for="id_birthday">Birthday:</label> <input type="text" name="birthday" value="1940-10-9" id="id_birthday" /></p>""")
+        self.assertEqual(p.fieldsets[1].as_p(), """<p><label for="id_band">Band:</label> <input type="text" name="band" value="The Beatles" id="id_band" /><input type="hidden" name="secret" value="he didnt say" id="id_secret" /></p>""")
+
+    def test_fieldset_fields_iteration(self):
+        # Pass a dictionary to a Form's __init__().
+        p = Person(self.some_data)
+        for fieldset in p.fieldsets:
+            for field in fieldset:
+                pass
+        self.assertEqual(set([field.name for field in p.fieldsets[0].visible_fields()]), set(['first_name', 'last_name', 'birthday']))
+        self.assertEqual(len(p.fieldsets[0].hidden_fields()), 0)
+        self.assertEqual(set([field.name for field in p.fieldsets[1].visible_fields()]), set(['band']))
+        self.assertEqual(set([field.name for field in p.fieldsets[1].hidden_fields()]), set(['secret']))
+
+    def test_legend_tag(self):
+        # Pass a dictionary to a Form's __init__().
+        p = Person(self.some_data)
+        self.assertIsNone(p.fieldsets[0].legend_tag())
+        self.assertEqual(p.fieldsets[1].legend_tag(), """<legend>Additional fields</legend>""")
+
diff --git a/tests/regressiontests/forms/tests/forms.py b/tests/regressiontests/forms/tests/forms.py
index 3f529f2..4b0faa6 100644
--- a/tests/regressiontests/forms/tests/forms.py
+++ b/tests/regressiontests/forms/tests/forms.py
@@ -1807,3 +1807,48 @@ class FormsTestCase(TestCase):
         form = NameForm(data={'name' : ['fname', 'lname']})
         self.assertTrue(form.is_valid())
         self.assertEqual(form.cleaned_data, {'name' : 'fname lname'})
+
+    def test_meta_options(self):
+        class MetaOptionsForm(Form):
+            class Meta:
+                fieldsets = 0xDEADBEEF
+                some_nonexising_option = True
+        class MetaOptionsDerivantForm(MetaOptionsForm):
+            pass
+        # Test classes
+        self.assertEqual(MetaOptionsForm._meta.fieldsets, 0xDEADBEEF)
+        self.assertEqual(MetaOptionsDerivantForm._meta.fieldsets, 0xDEADBEEF)
+        self.assertFalse(hasattr(MetaOptionsForm._meta, 'some_nonexising_option'))
+        self.assertFalse(hasattr(MetaOptionsDerivantForm._meta, 'some_nonexising_option'))
+        # Test instances
+        meta_options_form = MetaOptionsForm()
+        meta_options_derivant_form = MetaOptionsDerivantForm()
+        self.assertEqual(meta_options_form._meta.fieldsets, 0xDEADBEEF)
+        self.assertEqual(meta_options_derivant_form._meta.fieldsets, 0xDEADBEEF)
+        self.assertFalse(hasattr(meta_options_form._meta, 'some_nonexising_option'))
+        self.assertFalse(hasattr(meta_options_derivant_form._meta, 'some_nonexising_option'))
+
+    def test_meta_options_override(self):
+        class MetaOptionsForm(Form):
+            class Meta:
+                fieldsets = 0xDEADBEEF
+        class MetaOptionsDerivantForm(MetaOptionsForm):
+            class Meta:
+                fieldsets = 0xCAFEBABE
+        # Test classes
+        self.assertEqual(MetaOptionsForm._meta.fieldsets, 0xDEADBEEF)
+        self.assertEqual(MetaOptionsDerivantForm._meta.fieldsets, 0xCAFEBABE)
+        # Test instances
+        meta_options_form = MetaOptionsForm()
+        meta_options_derivant_form = MetaOptionsDerivantForm()
+        self.assertEqual(meta_options_form._meta.fieldsets, 0xDEADBEEF)
+        self.assertEqual(meta_options_derivant_form._meta.fieldsets, 0xCAFEBABE)
+
+    def test_meta_option_defaults(self):
+        class MetaOptionsForm(Form):
+            pass
+        # Test classes
+        self.assertIsNone(MetaOptionsForm._meta.fieldsets)
+        # Test instance
+        meta_options_form = MetaOptionsForm()
+        self.assertIsNone(meta_options_form._meta.fieldsets)
