diff --git a/djblets/siteconfig/templates/siteconfig/settings.html b/djblets/siteconfig/templates/siteconfig/settings.html
index 51f71c536fe35667f7ea33c4243e8ed7679b946e..27fe3b9880b2f5fbc9759b2515deb1319441bb31 100644
--- a/djblets/siteconfig/templates/siteconfig/settings.html
+++ b/djblets/siteconfig/templates/siteconfig/settings.html
@@ -1,5 +1,5 @@
 {% extends "admin/base_site.html" %}
-{% load admin_list compressed djblets_utils i18n staticfiles %}
+{% load admin_list compressed i18n staticfiles %}
 
 {% block title %}
 {% block page_title %}{{form.Meta.title}}{% endblock %} {{block.super}}
@@ -38,29 +38,7 @@
  <form action="." method="post"{% if form.is_multipart %} enctype="multipart/form-data"{% endif %}>
   {% csrf_token %}
 {% block form_content %}
-{% if form.Meta.fieldsets %}
-{%  for fieldset in form.Meta.fieldsets %}
-  <fieldset class="module aligned{% if fieldset.classes %}{% for class in fieldset.classes %} {{class}}{% endfor %}{% endif %}"{% if fieldset.id %} id="fieldset_{{fieldset.id}}"{% endif %}>
-{%   if fieldset.title %}<h2>{{fieldset.title}}</h2>{% endif %}
-{%   if fieldset.description %}
-   <div class="description">
-    {{fieldset.description|paragraphs}}
-   </div>
-{%   endif %}
-{%   for fieldname in fieldset.fields %}
-{%    with form|getitem:fieldname as field %}
-{%     include "siteconfig/settings_field.html" %}
-{%    endwith %}
-{%   endfor %}
-  </fieldset>
-{%  endfor %}
-{% else %}
-  <fieldset class="module aligned">
-{%   for field in form %}
-{%    include "siteconfig/settings_field.html" %}
-{%   endfor %}
-  </fieldset>
-{% endif %}
+{%  include "siteconfig/settings_fieldsets.html" %}
 {% endblock %}
 
 {% block submit_row %}
diff --git a/djblets/siteconfig/templates/siteconfig/settings_fieldsets.html b/djblets/siteconfig/templates/siteconfig/settings_fieldsets.html
new file mode 100644
index 0000000000000000000000000000000000000000..0e6830f9bc5f2dad5a285b4f52542075afb58712
--- /dev/null
+++ b/djblets/siteconfig/templates/siteconfig/settings_fieldsets.html
@@ -0,0 +1,23 @@
+{% load djblets_forms djblets_utils %}
+
+{% for fieldset_title, fieldset in form|get_fieldsets %}
+  <fieldset class="module aligned{% if fieldset.classes %}{% for class in fieldset.classes %} {{class}}{% endfor %}{% endif %}"{% if fieldset.id %} id="fieldset_{{fieldset.id}}"{% endif %}>
+{%  if fieldset_title %}<h2>{{fieldset_title}}</h2>{% endif %}
+{%  if fieldset.description %}
+   <div class="description">
+    {{fieldset.description|paragraphs}}
+   </div>
+{%  endif %}
+{%  for fieldname in fieldset.fields %}
+{%   with form|getitem:fieldname as field %}
+{%    include "siteconfig/settings_field.html" %}
+{%   endwith %}
+{%  endfor %}
+  </fieldset>
+{% empty %}
+  <fieldset class="module aligned">
+{%   for field in form %}
+{%    include "siteconfig/settings_field.html" %}
+{%   endfor %}
+  </fieldset>
+{% endfor %}
diff --git a/djblets/util/templatetags/djblets_forms.py b/djblets/util/templatetags/djblets_forms.py
index ab292eeef75cd3e380a750fac9f8edc1b0325289..9df36174e791384dd19e234b356dce47c341345c 100644
--- a/djblets/util/templatetags/djblets_forms.py
+++ b/djblets/util/templatetags/djblets_forms.py
@@ -107,3 +107,44 @@ def form_field_has_label_first(field):
     widget. This is the case in all fields except checkboxes.
     """
     return not is_field_checkbox(field)
+
+
+@register.filter
+def get_fieldsets(form):
+    """Normalize and iterate over fieldsets in a form.
+
+    This will loop through the fieldsets on a given form, converting either
+    standard Django style or legay Djblets style fieldset data into a standard
+    form and returning it to the template.
+
+    Args:
+        form (django.forms.Form):
+            The form containing the fieldsets.
+
+    Yields:
+        tuple:
+        A tuple of (fieldset_title, fieldset_info).
+
+    Example:
+        .. code-block:: html+django
+
+           {% for fieldset_title, fieldset in form|get_fieldsets %}
+           ...
+           {% endfor %}
+    """
+    try:
+        fieldsets = form.Meta.fieldsets
+    except AttributeError:
+        fieldsets = []
+
+    for fieldset in fieldsets:
+        if isinstance(fieldset, tuple):
+            # This is a standard Django-style fieldset entry. It's a tuple
+            # of (title, info).
+            yield fieldset
+        elif isinstance(fieldset, dict):
+            # This is a legacy Djblets-style fieldset entry. It's a dictionary
+            # that may contain the title as a "title" key.
+            yield fieldset.get('title'), fieldset
+        else:
+            raise ValueError('Invalid fieldset value: %r' % fieldset)
diff --git a/djblets/util/templatetags/tests.py b/djblets/util/templatetags/tests.py
index e5281f9735da7ee4af6887ed281c20a4655a5136..f4b9707ee54633464ab69fab088bedbf79edd75e 100644
--- a/djblets/util/templatetags/tests.py
+++ b/djblets/util/templatetags/tests.py
@@ -1,5 +1,8 @@
 from __future__ import unicode_literals
 
+from django.forms import Form
+from django.template import Context, Template
+
 from djblets.testing.testcases import TestCase
 from djblets.util.templatetags.djblets_js import json_dumps
 
@@ -17,3 +20,78 @@ class JSTagTests(TestCase):
             json_dumps(obj),
             '{"xss": "\\u003C/script\\u003E\\u003Cscript\\u003E'
             'alert(1);\\u003C/script\\u003E"}')
+
+
+class FormsTests(TestCase):
+    """Unit tests for the djblets_forms template tags."""
+
+    def test_get_fieldsets_modern(self):
+        """Testing the get_fieldsets template filter with modern fieldsets"""
+        class MyForm(Form):
+            class Meta:
+                fieldsets = (
+                    ('Test 1', {
+                        'description': 'This is test 1',
+                        'fields': ('field_1', 'field_2'),
+                    }),
+                    (None, {
+                        'description': 'This is test 2',
+                        'fields': ('field_3', 'field_4'),
+                    }),
+                )
+
+        t = Template(
+            '{% load djblets_forms %}'
+            '{% for title, fieldset in form|get_fieldsets %}'
+            'Title: {{title}}\n'
+            'Description: {{fieldset.description}}\n'
+            'Fields: {{fieldset.fields|join:","}}\n'
+            '{% endfor %}'
+        )
+
+        self.assertEqual(
+            t.render(Context({
+                'form': MyForm(),
+            })),
+            'Title: Test 1\n'
+            'Description: This is test 1\n'
+            'Fields: field_1,field_2\n'
+            'Title: None\n'
+            'Description: This is test 2\n'
+            'Fields: field_3,field_4\n')
+
+    def test_get_fieldsets_legacy(self):
+        """Testing the get_fieldsets template filter with legacy fieldsets"""
+        class MyForm(Form):
+            class Meta:
+                fieldsets = (
+                    {
+                        'title': 'Test 1',
+                        'description': 'This is test 1',
+                        'fields': ('field_1', 'field_2'),
+                    },
+                    {
+                        'description': 'This is test 2',
+                        'fields': ('field_3', 'field_4'),
+                    }
+                )
+
+        t = Template(
+            '{% load djblets_forms %}'
+            '{% for title, fieldset in form|get_fieldsets %}'
+            'Title: {{title}}\n'
+            'Description: {{fieldset.description}}\n'
+            'Fields: {{fieldset.fields|join:","}}\n'
+            '{% endfor %}'
+        )
+
+        self.assertEqual(
+            t.render(Context({
+                'form': MyForm(),
+            })),
+            'Title: Test 1\n'
+            'Description: This is test 1\n'
+            'Fields: field_1,field_2\n'
+            'Title: None\n'
+            'Description: This is test 2\n'
+            'Fields: field_3,field_4\n')
