Review Board

beta

Add the new siteconfig app for Djblets for dynamic site-wide configuration

Updated 5 months, 1 week ago

Christian Hammond Reviewers
trunk reviewboard
None Navi
Django's settings file is nice for customizing site settings, so long as the values don't need to be changed often. It's less useful when you want to give users the ability to quickly change things or experiment with the site configuration, since it requires logging into the server, hand-editting a file, and restarting.

The new djblets.siteconfig app provides a means to fix this. Settings data is stored in a SiteConfiguration model linked to the active Site. The setting values can be of any type supported by Django's simplejson, meaning dictionary values, arrays, and other such things work fine.

To ease customization of settings, a special Form object is provided that settings pages can subclass. Each field in the Form can be a stored setting and Djblets will handle the loading/saving automatically. These Forms support fieldsets for grouping objects, descriptions of fieldsets, and disabled fields (with reasons why they're disabled).

Mapping tables and utility functions exist to transition to and from many of the built-in Django settings. Not all settings are provided, but the ones sites would most likely want to customize are. Sites are meant to add these mapping tables as defaults, which will ensure that accessing the siteconfig version of the setting would return the value saved in the settings.py file, and forms that change these settings can call a function in their save handler to write the values back to the in-memory Settings object, meaning that the database will always take precedence but that apps accessing the Settings object instead of the SiteConfiguration will receive the correct value.
Tested with my upcoming change for Review Board. Settings of various types are loaded and saved as expected. The Django Settings compatibility appears to work perfectly fine.
/trunk/djblets/djblets/siteconfig/admin.py
New File
1
#
2
# djblets/siteconfig/admin.py
3
#
4
# Copyright (c) 2008  Christian Hammond
5
#
6
# Permission is hereby granted, free of charge, to any person obtaining
7
# a copy of this software and associated documentation files (the
8
# "Software"), to deal in the Software without restriction, including
9
# without limitation the rights to use, copy, modify, merge, publish,
10
# distribute, sublicense, and/or sell copies of the Software, and to
11
# permit persons to whom the Software is furnished to do so, subject to
12
# the following conditions:
13
#
14
# The above copyright notice and this permission notice shall be included
15
# in all copies or substantial portions of the Software.
16
#
17
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
20
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
21
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
22
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
23
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
#
25
26
from django.contrib import admin
27
28
from djblets.siteconfig.models import SiteConfiguration
29
30
31
class SiteConfigurationAdmin(admin.ModelAdmin):
32
    list_display = ('site', 'version')
33
34
35
admin.site.register(SiteConfiguration, SiteConfigurationAdmin)
/trunk/djblets/djblets/siteconfig/context_processors.py
New File
1
#
2
# djblets/siteconfig/context_processors.py
3
#
4
# Copyright (c) 2008  Christian Hammond
5
#
6
# Permission is hereby granted, free of charge, to any person obtaining
7
# a copy of this software and associated documentation files (the
8
# "Software"), to deal in the Software without restriction, including
9
# without limitation the rights to use, copy, modify, merge, publish,
10
# distribute, sublicense, and/or sell copies of the Software, and to
11
# permit persons to whom the Software is furnished to do so, subject to
12
# the following conditions:
13
#
14
# The above copyright notice and this permission notice shall be included
15
# in all copies or substantial portions of the Software.
16
#
17
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
20
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
21
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
22
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
23
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
#
25
26
from djblets.siteconfig.models import SiteConfiguration
27
28
29
def siteconfig(request):
30
    """
31
    Exposes the site configuration as a siteconfig variable in templates.
32
    """
33
    return {'siteconfig': SiteConfiguration.objects.get_current()}
/trunk/djblets/djblets/siteconfig/django_settings.py
New File
1
#
2
# djblets/siteconfig/django_settings.py
3
#
4
# Copyright (c) 2008  Christian Hammond
5
#
6
# Permission is hereby granted, free of charge, to any person obtaining
7
# a copy of this software and associated documentation files (the
8
# "Software"), to deal in the Software without restriction, including
9
# without limitation the rights to use, copy, modify, merge, publish,
10
# distribute, sublicense, and/or sell copies of the Software, and to
11
# permit persons to whom the Software is furnished to do so, subject to
12
# the following conditions:
13
#
14
# The above copyright notice and this permission notice shall be included
15
# in all copies or substantial portions of the Software.
16
#
17
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
20
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
21
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
22
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
23
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
#
25
26
from django.conf import settings
27
28
29
locale_settings_map = {
30
    'locale_timezone':             'TIME_ZONE',
31
    'locale_language_code':        'LANGUAGE_CODE',
32
    'locale_date_format':          'DATE_FORMAT',
33
    'locale_datetime_format':      'DATETIME_FORMAT',
34
    'locale_default_charset':      'DEFAULT_CHARSET',
35
    'locale_language_code':        'LANGUAGE_CODE',
36
    'locale_month_day_format':     'MONTH_DAY_FORMAT',
37
    'locale_time_format':          'TIME_FORMAT',
38
    'locale_time_zone':            'TIME_ZONE',
39
}
40
41
mail_settings_map = {
42
    'locale_year_month_format':    'YEAR_MONTH_FORMAT',
43
    'mail_server_address':         'SERVER_EMAIL',
44
    'mail_default_from':           'DEFAULT_FROM_EMAIL',
45
    'mail_host':                   'EMAIL_HOST',
46
    'mail_port':                   'EMAIL_PORT',
47
    'mail_host_user':              'EMAIL_HOST_USER',
48
    'mail_host_password':          'EMAIL_HOST_PASSWORD',
49
    'mail_use_tls':                'EMAIL_USE_TLS',
50
}
51
52
site_settings_map = {
53
    'site_media_root':             'MEDIA_ROOT',
54
    'site_media_url':              'MEDIA_URL',
55
    'site_prepend_www':            'PREPEND_WWW',
56
    'site_upload_temp_dir':        'FILE_UPLOAD_TEMP_DIR',
57
    'site_upload_max_memory_size': 'FILE_UPLOAD_MAX_MEMORY_SIZE',
58
}
59
60
cache_settings_map = {
61
    'cache_backend':               'CACHE_BACKEND',
62
    'cache_expiration_time':       'CACHE_EXPIRATION_TIME',
63
}
64
65
66
# Don't build unless we need it.
67
_django_settings_map = {}
68
69
70
def get_django_settings_map():
71
    """
72
    Returns the settings map for all Django settings that users may need
73
    to customize.
74
    """
75
    if not _django_settings_map:
76
        _django_settings_map.update(locale_settings_map)
77
        _django_settings_map.update(mail_settings_map)
78
        _django_settings_map.update(site_settings_map)
79
        _django_settings_map.update(cache_settings_map)
80
81
    return _django_settings_map
82
83
84
def _generate_defaults(settings_map):
85
    """
86
    Utility function to generate a defaults mapping.
87
    """
88
    defaults = {}
89
90
    for siteconfig_key, settings_key in settings_map.iteritems():
91
        if hasattr(settings, settings_key):
92
            defaults[siteconfig_key] = getattr(settings, settings_key)
93
94
    return defaults
95
96
97
def get_locale_defaults():
98
    """
99
    Returns the locale-related Django defaults that projects may want to
100
    let users customize.
101
    """
102
    return _generate_defaults(locale_settings_map)
103
104
105
def get_mail_defaults():
106
    """
107
    Returns the mail-related Django defaults that projects may want to
108
    let users customize.
109
    """
110
    return _generate_defaults(mail_settings_map)
111
112
113
def get_site_defaults():
114
    """
115
    Returns the site-related Django defaults that projects may want to
116
    let users customize.
117
    """
118
    return _generate_defaults(site_settings_map)
119
120
121
def get_cache_defaults():
122
    """
123
    Returns the cache-related Django defaults that projects may want to
124
    let users customize.
125
    """
126
    return _generate_defaults(cache_settings_map)
127
128
129
def get_django_defaults():
130
    """
131
    Returns all Django defaults that projects may want to let users customize.
132
    """
133
    return _generate_defaults(get_django_settings_map())
134
135
136
def apply_django_settings(siteconfig, settings_map=None):
137
    """
138
    Applies all settings from the site configuration to the Django settings
139
    object.
140
    """
141
    if settings_map is None:
142
        settings_map = get_django_settings_map()
143
144
    for key, value in settings_map.iteritems():
145
        if key in siteconfig.settings:
146
            setattr(settings, key, siteconfig.get(key))
/trunk/djblets/djblets/siteconfig/forms.py
New File
1
#
2
# djblets/siteconfig/forms.py
3
#
4
# Copyright (c) 2008  Christian Hammond
5
#
6
# Permission is hereby granted, free of charge, to any person obtaining
7
# a copy of this software and associated documentation files (the
8
# "Software"), to deal in the Software without restriction, including
9
# without limitation the rights to use, copy, modify, merge, publish,
10
# distribute, sublicense, and/or sell copies of the Software, and to
11
# permit persons to whom the Software is furnished to do so, subject to
12
# the following conditions:
13
#
14
# The above copyright notice and this permission notice shall be included
15
# in all copies or substantial portions of the Software.
16
#
17
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
20
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
21
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
22
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
23
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
#
25
26
from django import forms
27
28
29
class SiteSettingsForm(forms.Form):
30
    """
31
    A base form for loading/saving settings for a SiteConfiguration. This is
32
    meant to be subclassed for different settings pages. Any fields defined
33
    by the form will be loaded/saved automatically.
34
    """
35
    def __init__(self, siteconfig, *args, **kwargs):
36
        forms.Form.__init__(self, *args, **kwargs)
37
        self.siteconfig = siteconfig
38
        self.disabled_fields = {}
39
        self.disabled_reasons = {}
40
41
        self.load()
42
43
    def load(self):
44
        """
45
        Loads settings from the ```SiteConfiguration''' into this form.
46
        The default values in the form will be the values in the settings.
47
48
        This also handles setting disabled fields based on the
49
        ```disabled_fields''' and ```disabled_reasons''' variables set on
50
        this form.
51
        """
52
        if hasattr(self, "Meta"):
53
            save_blacklist = getattr(self.Meta, "save_blacklist", [])
54
55
        for field in self.fields:
56
            value = self.siteconfig.get(field)
57
58
            if isinstance(value, bool) or value:
59
                self.fields[field].initial = value
60
61
            if field in self.disabled_fields:
62
                self.fields[field].widget.attrs['disabled'] = 'disabled'
63
64
    def save(self):
65
        """
66
        Saves settings from the form back into the ```SiteConfiguration'''.
67
        """
68
        if not self.errors:
69
            if hasattr(self, "Meta"):
70
                save_blacklist = getattr(self.Meta, "save_blacklist", [])
71
72
            for key, value in self.cleaned_data.iteritems():
73
                if key not in save_blacklist:
74
                    self.siteconfig.settings[key] = value
75
76
            self.siteconfig.save()
/trunk/djblets/djblets/siteconfig/managers.py
New File
1
#
2
# djblets/siteconfig/managers.py
3
#
4
# Copyright (c) 2008  Christian Hammond
5
#
6
# Permission is hereby granted, free of charge, to any person obtaining
7
# a copy of this software and associated documentation files (the
8
# "Software"), to deal in the Software without restriction, including
9
# without limitation the rights to use, copy, modify, merge, publish,
10
# distribute, sublicense, and/or sell copies of the Software, and to
11
# permit persons to whom the Software is furnished to do so, subject to
12
# the following conditions:
13
#
14
# The above copyright notice and this permission notice shall be included
15
# in all copies or substantial portions of the Software.
16
#
17
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
20
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
21
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
22
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
23
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
#
25
26
from django.conf import settings
27
from django.contrib.sites.models import Site
28
from django.db import models
29
30
31
_SITECONFIG_CACHE = {}
32
33
34
class SiteConfigurationManager(models.Manager):
35
    """
36
    A Manager that provides a get_current function for retrieving the
37
    SiteConfiguration for this particular running site.
38
    """
39
    def get_current(self):
40
        """
41
        Returns the site configuration on the active site.
42
        """
43
        # This will handle raising a ImproperlyConfigured if not set up
44
        # properly.
45
        site = Site.objects.get_current()
46
47
        if site.id not in _SITECONFIG_CACHE:
48
            _SITECONFIG_CACHE[site.id] = site.config.get()
49
50
        return _SITECONFIG_CACHE[site.id]
/trunk/djblets/djblets/siteconfig/models.py
New File
1
#
2
# djblets/siteconfig/models.py
3
#
4
# Copyright (c) 2008  Christian Hammond
5
#
6
# Permission is hereby granted, free of charge, to any person obtaining
7
# a copy of this software and associated documentation files (the
8
# "Software"), to deal in the Software without restriction, including
9
# without limitation the rights to use, copy, modify, merge, publish,
10
# distribute, sublicense, and/or sell copies of the Software, and to
11
# permit persons to whom the Software is furnished to do so, subject to
12
# the following conditions:
13
#
14
# The above copyright notice and this permission notice shall be included
15
# in all copies or substantial portions of the Software.
16
#
17
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
20
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
21
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
22
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
23
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
#
25
26
from django.contrib.sites.models import Site
27
from django.db import models
28
29
from djblets.siteconfig.managers import SiteConfigurationManager
30
from djblets.util.fields import JSONField
31
32
33
_DEFAULTS = {}
34
35
36
class SiteConfiguration(models.Model):
37
    """
38
    Configuration data for a site. The version and all persistent settings
39
    are stored here.
40
41
    The usual way to retrieve a SiteConfiguration is to do:
42
43
    >>> siteconfig = SiteConfiguration.objects.get_current()
44
    """
45
    site = models.ForeignKey(Site, related_name="config")
46
    version = models.CharField(max_length=20)
47
    settings = JSONField()
48
49
    objects = SiteConfigurationManager()
50
51
    def get(self, key, default=None):
52
        """
53
        Retrieves a setting. If the setting is not found, the default value
54
        will be returned. This is represented by the default parameter, if
55
        passed in, or a global default if set.
56
        """
57
        if default is None and self.id in _DEFAULTS:
58
            default = _DEFAULTS[self.id].get(key, None)
59
60
        return self.settings.get(key, default)
61
62
    def set(self, key, value):
63
        """
64
        Sets a setting. The key should be a string, but the value can be
65
        any native Python object.
66
        """
67
        self.settings[key] = value
68
69
    def add_defaults(self, defaults_dict):
70
        """
71
        Adds a dictionary of defaults to this SiteConfiguration. These
72
        defaults will be used when calling ```get''', if that setting wasn't
73
        saved in the database.
74
        """
75
        if self.id not in _DEFAULTS:
76
            _DEFAULTS[self.id] = {}
77
78
        _DEFAULTS[self.id].update(defaults_dict)
79
80
    def add_default(self, key, default_value):
81
        """
82
        Adds a single default setting.
83
        """
84
        self.add_defaults({key: default_value})
85
86
    def get_defaults(self):
87
        """
88
        Returns all default settings registered with this SiteConfiguration.
89
        """
90
        if self.id not in _DEFAULTS:
91
            _DEFAULTS[self.id] = {}
92
93
        return _DEFAULTS[self.id]
94
95
    def __unicode__(self):
96
        return "%s (version %s)" % (unicode(self.site), self.version)
/trunk/djblets/djblets/siteconfig/views.py
New File
1
#
2
# djblets/siteconfig/views.py
3
#
4
# Copyright (c) 2008  Christian Hammond
5
#
6
# Permission is hereby granted, free of charge, to any person obtaining
7
# a copy of this software and associated documentation files (the
8
# "Software"), to deal in the Software without restriction, including
9
# without limitation the rights to use, copy, modify, merge, publish,
10
# distribute, sublicense, and/or sell copies of the Software, and to
11
# permit persons to whom the Software is furnished to do so, subject to
12
# the following conditions:
13
#
14
# The above copyright notice and this permission notice shall be included
15
# in all copies or substantial portions of the Software.
16
#
17
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
20
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
21
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
22
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
23
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
#
25
26
from django.contrib.admin.views.decorators import staff_member_required
27
from django.contrib.sites.models import Site
28
from django.http import HttpResponseRedirect
29
from django.shortcuts import render_to_response
30
from django.template.context import RequestContext
31
32
from djblets.siteconfig.models import SiteConfiguration
33
34
35
@staff_member_required
36
def site_settings(request, form_class,
37
                  template_name="siteconfig/settings.html"):
38
    """
39
    Provides a front-end for customizing Review Board settings.
40
    """
41
    siteconfig = SiteConfiguration.objects.get_current()
42
43
    if request.method == "POST":
44
        form = form_class(siteconfig, request.POST, request.FILES)
45
46
        if form.is_valid():
47
            form.save()
48
            return HttpResponseRedirect(".?saved=1")
49
    else:
50
        form = form_class(siteconfig)
51
52
    return render_to_response(template_name, RequestContext(request, {
53
        'form': form,
54
        'saved': request.GET.get('saved', 0)
55
    }))
/trunk/djblets/djblets/siteconfig/templates/siteconfig/settings.html
New File
1
{% extends "admin/base_site.html" %}
2
{% load adminmedia admin_list djblets_utils i18n %}
3
{% block stylesheet %}{% admin_media_prefix %}css/forms.css{% endblock %}
4
5
{% block title %}{{form.Meta.title}} {{block.super}}{% endblock %}
6
7
{% block content %}
8
<div id="content-main">
9
10
{%  if form.error_dict %}
11
 <p class="errornote">
12
  {% blocktrans count form.error_dict.items|length as counter %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %}
13
 </p>
14
{%  endif %}
15
16
{% if saved %}
17
 <ul class="messagelist">
18
  <li>{% trans "The settings have been saved." %}</li>
19
 </ul>
20
{% endif %}
21
22
 <h1>{{form.Meta.title}}</h1>
23
24
 <form action="." method="post"{% if form.is_multipart %} enctype="multipart/form-data"{% endif %}>
25
{% if form.Meta.fieldsets %}
26
{%  for fieldset in form.Meta.fieldsets %}
27
  <fieldset class="module aligned{% if fieldset.classes %}{% for class in fieldset.classes %} {{class}}{% endfor %}{% endif %}"{% if fieldset.id %} id="fieldset_{{fieldset.id}}"{% endif %}>
28
{% if fieldset.title %}<h2>{{fieldset.title}}</h2>{% endif %}
29
{%   if fieldset.description %}
30
   <div class="description">
31
    {{fieldset.description|paragraphs}}
32
   </div>
33
{% endif %}
34
{%   for fieldname in fieldset.fields %}
35
{%    with form|getitem:fieldname as field %}
36
{%     include "siteconfig/settings_field.html" %}
37
{%    endwith %}
38
{%   endfor %}
39
  </fieldset>
40
{%  endfor %}
41
{% else %}
42
  <fieldset class="module aligned">
43
{%   for field in form %}
44
{%    include "siteconfig/settings_field.html" %}
45
{%   endfor %}
46
  </fieldset>
47
{% endif %}
48
  <div class="submit-row">
49
   <input type="submit" value="{% trans "Save" %}" class="default" />
50
  </div>
51
 </form>
52
</div>
53
{% endblock %}
/trunk/djblets/djblets/siteconfig/templates/siteconfig/settings_field.html
New File
1
{% load djblets_forms %}
2
{% load djblets_utils %}
3
{%    if field.is_hidden %}
4
   {{field}}
5
{%    else %}
6
{%     with field|form_field_has_label_first as label_first %}
7
   <div class="form-row{% if not label_first %} checkbox-row{% endif%}{% if field.errors %} error{% endif %}">
8
    {{field.errors}}
9
    {% if not label_first %}{{field}}{% endif %}
10
    {% label_tag field %}
11
    {% if label_first %}{{field}}{% endif %}
12
    {% if field.help_text %}<p class="help">{{field.help_text}}</p>{% endif %}
13
    {% if form.disabled_reasons|contains:fieldname %}<p class="disabled-reason">{{form.disabled_reasons|getitem:fieldname|safe}}</p>{% endif %}
14
   </div>
15
{%     endwith %}
16
{%    endif %}
/trunk/djblets/djblets/util/templatetags/djblets_forms.py
New File
1
#
2
# djblets_forms.py -- Form-related template tags
3
#
4
# Copyright (c) 2008  Christian Hammond
5
#
6
# Permission is hereby granted, free of charge, to any person obtaining
7
# a copy of this software and associated documentation files (the
8
# "Software"), to deal in the Software without restriction, including
9
# without limitation the rights to use, copy, modify, merge, publish,
10
# distribute, sublicense, and/or sell copies of the Software, and to
11
# permit persons to whom the Software is furnished to do so, subject to
12
# the following conditions:
13
#
14
# The above copyright notice and this permission notice shall be included
15
# in all copies or substantial portions of the Software.
16
#
17
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,