diff options
-rw-r--r-- | askbot/conf/settings_wrapper.py | 18 | ||||
-rw-r--r-- | askbot/conf/sidebar_main.py | 16 | ||||
-rw-r--r-- | askbot/conf/sidebar_profile.py | 7 | ||||
-rw-r--r-- | askbot/conf/sidebar_question.py | 28 | ||||
-rw-r--r-- | askbot/conf/site_settings.py | 5 | ||||
-rw-r--r-- | askbot/deps/livesettings/forms.py | 20 | ||||
-rw-r--r-- | askbot/deps/livesettings/models.py | 1 | ||||
-rw-r--r-- | askbot/deps/livesettings/values.py | 182 | ||||
-rw-r--r-- | askbot/deps/livesettings/views.py | 4 | ||||
-rw-r--r-- | askbot/doc/source/changelog.rst | 1 | ||||
-rw-r--r-- | askbot/media/style/style.css | 3 | ||||
-rw-r--r-- | askbot/media/style/style.less | 3 | ||||
-rw-r--r-- | askbot/templates/livesettings/group_settings.html | 2 |
13 files changed, 214 insertions, 76 deletions
diff --git a/askbot/conf/settings_wrapper.py b/askbot/conf/settings_wrapper.py index 7fc9540d..d725f637 100644 --- a/askbot/conf/settings_wrapper.py +++ b/askbot/conf/settings_wrapper.py @@ -22,6 +22,7 @@ askbot.deps.livesettings is a module developed for satchmo project """ from django.conf import settings as django_settings from django.core.cache import cache +from django.utils.translation import get_language from askbot.deps.livesettings import SortedDotDict, config_register from askbot.deps.livesettings.functions import config_get from askbot.deps.livesettings import signals @@ -68,11 +69,22 @@ class ConfigSettings(object): def update(self, key, value): try: - setting = config_get(self.__group_map[key], key) - setting.update(value) + setting = config_get(self.__group_map[key], key) + if setting.localized: + lang = get_language() + else: + lang = None + setting.update(value, lang) + except: from askbot.deps.livesettings.models import Setting - setting = Setting.objects.get(key=key) + lang_postfix = '_' + get_language().upper() + #first try localized setting + try: + setting = Setting.objects.get(key=key + lang_postfix) + except Setting.DoesNotExist: + setting = Setting.objects.get(key=key) + setting.value = value setting.save() #self.prime_cache() diff --git a/askbot/conf/sidebar_main.py b/askbot/conf/sidebar_main.py index 97b89e37..77f9ede2 100644 --- a/askbot/conf/sidebar_main.py +++ b/askbot/conf/sidebar_main.py @@ -19,6 +19,7 @@ settings.register( 'SIDEBAR_MAIN_HEADER', description = _('Custom sidebar header'), default = '', + localized = True, help_text = _( 'Use this area to enter content at the TOP of the sidebar' 'in HTML format. When using this option ' @@ -34,7 +35,7 @@ settings.register( SIDEBAR_MAIN, 'SIDEBAR_MAIN_HEADER_ANON_ONLY', description=_('Show above only to anonymous users'), - default=False + default=False, ) ) @@ -65,12 +66,12 @@ settings.register( values.BooleanValue( SIDEBAR_MAIN, 'SIDEBAR_MAIN_SHOW_TAG_SELECTOR', - description = _('Show tag selector in sidebar'), - help_text = _( + description=_('Show tag selector in sidebar'), + help_text=_( 'Uncheck this if you want to hide the options ' 'for choosing interesting and ignored tags ' ), - default = True + default=True, ) ) @@ -91,9 +92,10 @@ settings.register( values.LongStringValue( SIDEBAR_MAIN, 'SIDEBAR_MAIN_FOOTER', - description = _('Custom sidebar footer'), - default = '', - help_text = _( + description=_('Custom sidebar footer'), + default='', + localized=True, + help_text=_( 'Use this area to enter content at the BOTTOM of the sidebar' 'in HTML format. When using this option ' '(as well as the sidebar header), please ' diff --git a/askbot/conf/sidebar_profile.py b/askbot/conf/sidebar_profile.py index a216de4b..5e04ca7d 100644 --- a/askbot/conf/sidebar_profile.py +++ b/askbot/conf/sidebar_profile.py @@ -17,9 +17,10 @@ settings.register( values.LongStringValue( SIDEBAR_PROFILE, 'SIDEBAR_PROFILE', - description = _('Custom sidebar'), - default = '', - help_text = _( + description=_('Custom sidebar'), + default='', + localized=True, + help_text=_( 'Use this area to enter content at the TOP of the sidebar' 'in HTML format. When using this option ' '(as well as the sidebar footer), please ' diff --git a/askbot/conf/sidebar_question.py b/askbot/conf/sidebar_question.py index 4bdedcee..85f17347 100644 --- a/askbot/conf/sidebar_question.py +++ b/askbot/conf/sidebar_question.py @@ -16,9 +16,10 @@ settings.register( values.LongStringValue( SIDEBAR_QUESTION, 'QUESTION_PAGE_TOP_BANNER', - description = _('Top banner'), - default = '', - help_text = _( + description=_('Top banner'), + default='', + localized=True, + help_text=_( 'When using this option, please ' 'use the HTML validation service to make sure that ' 'your input is valid and works well in all browsers.' @@ -39,9 +40,10 @@ settings.register( values.LongStringValue( SIDEBAR_QUESTION, 'QUESTION_PAGE_ANSWER_BANNER', - description = _('Answers banner'), - default = '', - help_text = _( + description=_('Answers banner'), + default='', + localized=True, + help_text=_( 'This banner will show under the first answer. ' 'When using this option, please ' 'use the HTML validation service to make sure that ' @@ -73,9 +75,10 @@ settings.register( values.LongStringValue( SIDEBAR_QUESTION, 'SIDEBAR_QUESTION_HEADER', - description = _('Custom sidebar header'), - default = '', - help_text = _( + description=_('Custom sidebar header'), + default='', + localized=True, + help_text=_( 'Use this area to enter content at the TOP of the sidebar ' 'in HTML format. When using this option ' '(as well as the sidebar footer), please ' @@ -138,9 +141,10 @@ settings.register( values.LongStringValue( SIDEBAR_QUESTION, 'SIDEBAR_QUESTION_FOOTER', - description = _('Custom sidebar footer'), - default = '', - help_text = _( + description=_('Custom sidebar footer'), + default='', + localized=True, + help_text=_( 'Use this area to enter content at the BOTTOM of the sidebar' 'in HTML format. When using this option ' '(as well as the sidebar header), please ' diff --git a/askbot/conf/site_settings.py b/askbot/conf/site_settings.py index 0ac5b081..c0eb9d89 100644 --- a/askbot/conf/site_settings.py +++ b/askbot/conf/site_settings.py @@ -22,7 +22,7 @@ settings.register( livesettings.StringValue( QA_SITE_SETTINGS, 'APP_TITLE', - default=u'Askbot: Open Source Q&A Forum', + default=_('My site'), description=_('Site title for the Q&A forum') ) ) @@ -112,7 +112,8 @@ settings.register( livesettings.StringValue( QA_SITE_SETTINGS, 'GREETING_FOR_ANONYMOUS_USER', - default='First time here? Check out the FAQ!', + default=_('First time here? Check out the FAQ!'), + localized=True, hidden=False, description=_( 'Text shown in the greeting message ' diff --git a/askbot/deps/livesettings/forms.py b/askbot/deps/livesettings/forms.py index 764247f3..afc8632b 100644 --- a/askbot/deps/livesettings/forms.py +++ b/askbot/deps/livesettings/forms.py @@ -1,4 +1,5 @@ from django import forms +from django.conf import settings as django_settings from askbot.deps.livesettings import ConfigurationGroup import logging @@ -24,16 +25,25 @@ class SettingsEditor(forms.Form): kw = {#todo: maybe move into the make_field call 'label': setting.description, 'help_text': setting.help_text, - # Provide current setting values for initializing the form - 'initial': setting.editor_value } - field = setting.make_field(**kw) + fields = setting.make_fields(**kw) - k = '%s__%s' % (setting.group.key, setting.key) + for field in fields: + k = '%s__%s__%s' % (setting.group.key, setting.key, field.language_code) + self.fields[k] = field - self.fields[k] = field if not setting.group in groups: groups.append(setting.group) #log.debug("Added field: %s = %s" % (k, str(field))) self.groups = groups + +class LocalizedChoiceField(forms.ChoiceField): + def __init__(self, *args, **kwargs): + self.language_code = kwargs.pop('language_code', django_settings.LANGUAGE_CODE) + super(LocalizedChoiceField, self).__init__(*args, **kwargs) + +class LocalizedMultipleChoiceField(forms.MultipleChoiceField): + def __init__(self, *args, **kwargs): + self.language_code = kwargs.pop('language_code', django_settings.LANGUAGE_CODE) + super(LocalizedMultipleChoiceField, self).__init__(*args, **kwargs) diff --git a/askbot/deps/livesettings/models.py b/askbot/deps/livesettings/models.py index 3f42b59d..fced320a 100644 --- a/askbot/deps/livesettings/models.py +++ b/askbot/deps/livesettings/models.py @@ -41,7 +41,6 @@ def find_setting(group, key, site=None): elif use_db: try: setting = cache_get(ck) - except NotCachedError, nce: if loading.app_cache_ready(): try: diff --git a/askbot/deps/livesettings/values.py b/askbot/deps/livesettings/values.py index 4ed2fe0e..f0db1845 100644 --- a/askbot/deps/livesettings/values.py +++ b/askbot/deps/livesettings/values.py @@ -11,8 +11,11 @@ from django.core.cache import cache from django.utils import simplejson from django.utils.datastructures import SortedDict from django.utils.encoding import force_unicode +from django.utils.safestring import mark_safe from django.utils.translation import ugettext as _ from django.utils.translation import ugettext_lazy +from django.utils.translation import get_language +from django.utils.translation import activate as activate_language from django.core.files import storage from askbot.deps.livesettings.models import find_setting, LongSetting, Setting, SettingNotSet from askbot.deps.livesettings.overrides import get_overrides @@ -165,6 +168,7 @@ class Value(object): self.choices = kwargs.get('choices',[]) self.ordering = kwargs.pop('ordering', 0) self.hidden = kwargs.pop('hidden', False) + self.localized = kwargs.pop('localized', False) self.update_callback = kwargs.pop('update_callback', None) self.requires = kwargs.pop('requires', None) self.clear_cache = kwargs.pop('clear_cache', False) @@ -222,7 +226,8 @@ class Value(object): def choice_field(self, **kwargs): if self.hidden: kwargs['widget'] = forms.MultipleHiddenInput() - return forms.ChoiceField(choices=self.choices, **kwargs) + from askbot.deps.livesettings.forms import LocalizedChoiceField + return LocalizedChoiceField(choices=self.choices, **kwargs) def _choice_values(self): choices = self.choices @@ -237,21 +242,16 @@ class Value(object): return new_value def _default_text(self): - if not self.use_default: + if not self.use_default or force_unicode(self.default) == '': note = "" + elif self.choices: + work = [] + for x in self.choices: + if x[0] in self.default: + work.append(force_unicode(x[1])) + note = _('Default value: ') + unicode(u", ".join(work)) else: - if self.default == "": - note = _('Default value: ""') - - elif self.choices: - work = [] - for x in self.choices: - if x[0] in self.default: - work.append(force_unicode(x[1])) - note = _('Default value: ') + unicode(u", ".join(work)) - - else: - note = _("Default value: %s") % force_unicode(self.default) + note = _("Default value: %s") % force_unicode(self.default) return note @@ -283,48 +283,103 @@ class Value(object): field = self.field(**kwargs) field.group = self.group - field.default_text = self.default_text + field.default_text = _('Default value: ') + \ + self.get_default_editor_value(kwargs.get('language_code')) return field - def make_setting(self, db_value): + def make_fields(self, **kwargs): + if self.localized: + langs_dict = SortedDict(django_settings.LANGUAGES) + default_code = django_settings.LANGUAGE_CODE + default_name = langs_dict[default_code] + langs_dict.insert(0, default_code, default_name) + langs = langs_dict.keys() + else: + langs = (django_settings.LANGUAGE_CODE,) + + fields = list() + for lang in langs: + kwargs['language_code'] = lang + fields.append(self.make_field(**kwargs)) + + #set initial values + for field in fields: + lang = field.language_code + field.initial = self.get_editor_value(lang) + + if self.localized and len(django_settings.LANGUAGES) > 1: + for field in fields: + lang_name = unicode(langs_dict[field.language_code]) + field.label += mark_safe(' <span class="lang">(%s)</span>' % lang_name) + + return fields + + def make_setting(self, db_value, language_code=None): log.debug('new setting %s.%s', self.group.key, self.key) - return Setting(group=self.group.key, key=self.key, value=db_value) + key = self.key + if self.localized and language_code: + key += '_' + language_code.upper() + return Setting(group=self.group.key, key=key, value=db_value) def _setting(self): - return find_setting(self.group.key, self.key) + key = self.key + if self.localized: + key += '_' + get_language().upper() + return find_setting(self.group.key, key) + #here we have duplicationg with get_setting function setting = property(fget = _setting) + def get_setting(self, language_code=None): + key = self.key + if self.localized and language_code: + key += '_' + language_code.upper() + return find_setting(self.group.key, key) + def _value(self): use_db, overrides = get_overrides() + lang = get_language() + + key = self.key + if self.localized: + key += '_' + lang.upper() + if not use_db: try: - val = overrides[self.group.key][self.key] + val = overrides[self.group.key][key] except KeyError: if self.use_default: val = self.default else: - raise SettingNotSet('%s.%s is not in your LIVESETTINGS_OPTIONS' % (self.group.key, self.key)) + raise SettingNotSet('%s.%s is not in your LIVESETTINGS_OPTIONS' % (self.group.key, key)) else: try: val = self.setting.value except SettingNotSet, sns: + + if self.localized and lang == django_settings.LANGUAGE_CODE: + try: + unlocalized_setting = find_setting(self.group.key, self.key) + return unlocalized_setting.value + except SettingNotSet: + pass + if self.use_default: val = self.default if overrides: # maybe override the default grp = overrides.get(self.group.key, {}) - if grp.has_key(self.key): + if grp.has_key(key): val = grp[self.key] else: val = NOTSET except AttributeError, ae: log.error("Attribute error: %s", ae) - log.error("%s: Could not get _value of %s", self.key, self.setting) + log.error("%s: Could not get _value of %s", key, self.setting) raise(ae) except Exception, e: @@ -332,21 +387,21 @@ class Value(object): log.error(e) if str(e).find("configuration_setting") > -1: if not _WARN.has_key('configuration_setting'): - log.warn('Error loading setting %s.%s from table, OK if you are in syncdb', self.group.key, self.key) + log.warn('Error loading setting %s.%s from table, OK if you are in syncdb', self.group.key, key) _WARN['configuration_setting'] = True if self.use_default: val = self.default else: - raise ImproperlyConfigured("All settings used in startup must have defaults, %s.%s does not", self.group.key, self.key) + raise ImproperlyConfigured("All settings used in startup must have defaults, %s.%s does not", self.group.key, key) else: import traceback traceback.print_exc() - log.warn("Problem finding settings %s.%s, %s", self.group.key, self.key, e) - raise SettingNotSet("Startup error, couldn't load %s.%s" %(self.group.key, self.key)) + log.warn("Problem finding settings %s.%s, %s", self.group.key, key, e) + raise SettingNotSet("Startup error, couldn't load %s.%s" %(self.group.key, key)) return val - def update(self, value): + def update(self, value, language_code=None): use_db, overrides = get_overrides() if use_db: @@ -360,11 +415,11 @@ class Value(object): db_value = self.get_db_prep_save(new_value) try: - s = self.setting + s = self.get_setting(language_code) s.value = db_value except SettingNotSet: - s = self.make_setting(db_value) + s = self.make_setting(db_value, language_code=language_code) if self.use_default and self.default == new_value: if s.id: @@ -374,6 +429,13 @@ class Value(object): log.info("Updated setting %s.%s = %s", self.group.key, self.key, value) s.save() + if self.localized: + try: + unlocalized_setting = find_setting(self.group.key, self.key) + unlocalized_setting.delete() + except SettingNotSet: + pass + signals.configuration_value_changed.send(self, old_value=current_value, new_value=new_value, setting=self) if self.clear_cache: @@ -395,6 +457,33 @@ class Value(object): val = self._value() return self.to_editor(val) + def get_default_editor_value(self, language_code): + if self.use_default: + if language_code and self.localized: + current_lang = get_language() + activate_language(language_code) + localized_value = unicode(self.default) + activate_language(current_lang) + return localized_value + elif self.use_default: + return unicode(self.default) + + return '' + + def get_editor_value(self, language_code): + try: + setting = self.get_setting(language_code) + return setting.value + except SettingNotSet: + if language_code == django_settings.LANGUAGE_CODE: + try: + return find_setting(self.group.key, self.key).value + except SettingNotSet: + pass + + return self.get_default_editor_value(language_code) + + # Subclasses should override the following methods where applicable def to_python(self, value): @@ -425,6 +514,7 @@ class BooleanValue(Value): def __init__(self, *args, **kwargs): kwargs['required'] = False + self.language_code = kwargs.pop('language_code', django_settings.LANGUAGE_CODE) forms.BooleanField.__init__(self, *args, **kwargs) def add_choice(self, choice): @@ -440,10 +530,10 @@ class BooleanValue(Value): class DecimalValue(Value): class field(forms.DecimalField): - - def __init__(self, *args, **kwargs): - kwargs['required'] = False - forms.DecimalField.__init__(self, *args, **kwargs) + def __init__(self, *args, **kwargs): + kwargs['required'] = False + self.language_code = kwargs.pop('language_code', django_settings.LANGUAGE_CODE) + forms.DecimalField.__init__(self, *args, **kwargs) def to_python(self, value): if value==NOTSET: @@ -498,6 +588,7 @@ class FloatValue(Value): def __init__(self, *args, **kwargs): kwargs['required'] = False + self.language_code = kwargs.pop('language_code', django_settings.LANGUAGE_CODE) forms.FloatField.__init__(self, *args, **kwargs) def to_python(self, value): @@ -513,9 +604,9 @@ class FloatValue(Value): class IntegerValue(Value): class field(forms.IntegerField): - def __init__(self, *args, **kwargs): kwargs['required'] = False + self.language_code = kwargs.pop('language_code', django_settings.LANGUAGE_CODE) forms.IntegerField.__init__(self, *args, **kwargs) def to_python(self, value): @@ -533,9 +624,9 @@ class IntegerValue(Value): class PercentValue(Value): class field(forms.DecimalField): - def __init__(self, *args, **kwargs): kwargs['required'] = False + self.language_code = kwargs.pop('language_code', django_settings.LANGUAGE_CODE) forms.DecimalField.__init__(self, 100, 0, 5, 2, *args, **kwargs) class widget(forms.TextInput): @@ -562,6 +653,7 @@ class PositiveIntegerValue(IntegerValue): def __init__(self, *args, **kwargs): kwargs['min_value'] = 0 + self.language_code = kwargs.pop('language_code', django_settings.LANGUAGE_CODE) forms.IntegerField.__init__(self, *args, **kwargs) @@ -570,6 +662,7 @@ class StringValue(Value): class field(forms.CharField): def __init__(self, *args, **kwargs): kwargs['required'] = False + self.language_code = kwargs.pop('language_code', django_settings.LANGUAGE_CODE) forms.CharField.__init__(self, *args, **kwargs) def to_python(self, value): @@ -585,6 +678,7 @@ class URLValue(Value): def __init__(self, *args, **kwargs): kwargs['required'] = False + self.language_code = kwargs.pop('language_code', django_settings.LANGUAGE_CODE) forms.URLField.__init__(self, *args, **kwargs) class LongStringValue(Value): @@ -593,11 +687,15 @@ class LongStringValue(Value): def __init__(self, *args, **kwargs): kwargs['required'] = False kwargs['widget'] = forms.Textarea() + self.language_code = kwargs.pop('language_code', django_settings.LANGUAGE_CODE) forms.CharField.__init__(self, *args, **kwargs) - def make_setting(self, db_value): + def make_setting(self, db_value, language_code=None): log.debug('new long setting %s.%s', self.group.key, self.key) - return LongSetting(group=self.group.key, key=self.key, value=db_value) + key = self.key + if self.localized and language_code: + key = self.key + '_' + language_code.upper() + return LongSetting(group=self.group.key, key=key, value=db_value) def to_python(self, value): if value == NOTSET: @@ -628,6 +726,7 @@ class ImageValue(StringValue): def __init__(self, *args, **kwargs): kwargs['required'] = False self.allowed_file_extensions = kwargs.pop('allowed_file_extensions') + self.language_code = kwargs.pop('language_code', django_settings.LANGUAGE_CODE) url_resolver = kwargs.pop('url_resolver') kwargs['widget'] = ImageInput(url_resolver = url_resolver) forms.FileField.__init__(self, *args, **kwargs) @@ -647,7 +746,7 @@ class ImageValue(StringValue): kwargs['allowed_file_extensions'] = self.allowed_file_extensions return super(StringValue, self).make_field(**kwargs) - def update(self, uploaded_file): + def update(self, uploaded_file, language_code=None): """uploaded_file is an instance of django UploadedFile object """ @@ -678,7 +777,7 @@ class ImageValue(StringValue): #saved file path is relative to the upload_directory #so that things could be easily relocated - super(ImageValue, self).update(url) + super(ImageValue, self).update(url, language_code=language_code) class MultipleStringValue(Value): @@ -686,11 +785,13 @@ class MultipleStringValue(Value): def __init__(self, *args, **kwargs): kwargs['required'] = False + self.language_code = kwargs.pop('language_code', django_settings.LANGUAGE_CODE) forms.CharField.__init__(self, *args, **kwargs) def choice_field(self, **kwargs): kwargs['required'] = False - return forms.MultipleChoiceField(choices=self.choices, **kwargs) + from askbot.deps.livesettings.forms import LocalizedMultipleChoiceField + return LocalizedMultipleChoiceField(choices=self.choices, **kwargs) def get_db_prep_save(self, value): if is_string_like(value): @@ -722,6 +823,7 @@ class ModuleValue(Value): def __init__(self, *args, **kwargs): kwargs['required'] = False + self.language_code = kwargs.pop('language_code', django_settings.LANGUAGE_CODE) forms.CharField.__init__(self, *args, **kwargs) def load_module(self, module): diff --git a/askbot/deps/livesettings/views.py b/askbot/deps/livesettings/views.py index 0c64d23d..077fa472 100644 --- a/askbot/deps/livesettings/views.py +++ b/askbot/deps/livesettings/views.py @@ -34,7 +34,7 @@ def group_settings(request, group, template='livesettings/group_settings.html'): form = forms.SettingsEditor(data, request.FILES, settings=settings) if form.is_valid(): for name, value in form.cleaned_data.items(): - group, key = name.split('__') + group, key, lang = name.split('__') cfg = mgr.get_config(group, key) if isinstance(cfg, ImageValue): @@ -44,7 +44,7 @@ def group_settings(request, group, template='livesettings/group_settings.html'): continue try: - cfg.update(value) + cfg.update(value, lang) #message='Updated %s on %s' % (cfg.key, cfg.group.key) #messages.success(request, message) #the else if for the settings that are not updated. diff --git a/askbot/doc/source/changelog.rst b/askbot/doc/source/changelog.rst index f160dc95..c0d21d1e 100644 --- a/askbot/doc/source/changelog.rst +++ b/askbot/doc/source/changelog.rst @@ -3,6 +3,7 @@ Changes in Askbot Development master branch (only on github) ------------------------------------------ +* Allowed localized site settings * Added management command `askbot_clear_moderation_queue` * Admins and Moderators can merge questions. * Improved moderation modes: flags, audit, premoderation. diff --git a/askbot/media/style/style.css b/askbot/media/style/style.css index 95b987d1..c2223d9c 100644 --- a/askbot/media/style/style.css +++ b/askbot/media/style/style.css @@ -4442,6 +4442,9 @@ table.settings td { td.setting-label { width: 30%; } +td.setting-label span.lang { + color: #ff4747; +} td.setting-input { padding-left: 10px; width: 70%; diff --git a/askbot/media/style/style.less b/askbot/media/style/style.less index f2a97df6..749d9ce4 100644 --- a/askbot/media/style/style.less +++ b/askbot/media/style/style.less @@ -4671,6 +4671,9 @@ table.settings { } td.setting-label { width: 30%; + span.lang { + color: rgb(273, 71, 71); + } } td.setting-input { padding-left: 10px; diff --git a/askbot/templates/livesettings/group_settings.html b/askbot/templates/livesettings/group_settings.html index 16fa3241..f4bb0a5e 100644 --- a/askbot/templates/livesettings/group_settings.html +++ b/askbot/templates/livesettings/group_settings.html @@ -26,7 +26,7 @@ {% endif %} <tr{% if field.errors %} class="error"{% endif %}> <td class="setting-label"> - {{ field.label_tag() }} + <label for="{{ field.name }}">{{ field.label }}</label> {% if field.help_text %} <p class="help">{{ field.help_text|safe }}</p> {% endif %} |