From fa9b67f6571690b9c25604e395d4d1e71fd2d204 Mon Sep 17 00:00:00 2001 From: Denis K Date: Thu, 8 Mar 2018 18:22:58 +0300 Subject: [PATCH 1/5] Initial commit --- .gitignore | 7 ++ CHANGELOG.rst | 7 ++ LICENSE | 20 +++ MANIFEST.in | 4 + README.rst | 77 ++++++++++++ setup.py | 54 ++++++++ translatable_fields/__init__.py | 1 + translatable_fields/models.py | 26 ++++ .../translatable_fields.css | 47 +++++++ .../translatable_fields.js | 37 ++++++ .../translatable_fields/multiwidget.html | 17 +++ translatable_fields/value.py | 18 +++ translatable_fields/widgets.py | 117 ++++++++++++++++++ 13 files changed, 432 insertions(+) create mode 100644 .gitignore create mode 100644 CHANGELOG.rst create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 README.rst create mode 100644 setup.py create mode 100644 translatable_fields/__init__.py create mode 100644 translatable_fields/models.py create mode 100755 translatable_fields/static/translatable_fields/translatable_fields.css create mode 100755 translatable_fields/static/translatable_fields/translatable_fields.js create mode 100755 translatable_fields/templates/translatable_fields/multiwidget.html create mode 100644 translatable_fields/value.py create mode 100644 translatable_fields/widgets.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6c02314 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +*.pyc +*.DS_Store +*.egg* +/dist/ +/.idea +/docs/_build/ +/node_modules/ diff --git a/CHANGELOG.rst b/CHANGELOG.rst new file mode 100644 index 0000000..126a25d --- /dev/null +++ b/CHANGELOG.rst @@ -0,0 +1,7 @@ +Changelog +========= + +0.0.1 +----- + +* Initial release diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..67ec320 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2012 Selwin Ong + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..b2ae275 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,4 @@ +include README.rst +include LICENSE +recursive-include translatable_fields/static * +recursive-include translatable_fields/templates * diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..a1dc73c --- /dev/null +++ b/README.rst @@ -0,0 +1,77 @@ +========================== +Django Translatable Fields +========================== + +Translatable model fields for Django with admin integration. Uses PostgreSQL JSONField. + +Installation +============ + + +* Add application + +.. code:: python + + INSTALLED_APPS = ( + ... + 'translatable_fields', + ... + ) + +* Add `TranslatableField` model fields + +.. code:: python + + from django.db import models + from django.utils.translation import ugettext_lazy as _ + + from translatable_fields.models import TranslatableField + + + class Position(models.Model): + ... + title = TranslatableField( + verbose_name=_('title') + ) + description = TranslatableField( + verbose_name=_('description') + ) + ... + +* Create custom model admin form + +.. code:: python + + from django import forms + from django.contrib.postgres.forms import JSONField + from ckeditor_uploader.widgets import CKEditorUploadingWidget + + from careers.models.position import Position + from translatable_fields.widgets import TranslatableWidget + + + class PositionAdminForm(forms.ModelForm): + title = JSONField(widget=TranslatableWidget(widget=forms.TextInput)) + description = JSONField(widget=TranslatableWidget(widget=CKEditorUploadingWidget)) + + class Meta: + model = Position + fields = ( + ... + 'title', + 'description', + ... + ) + +* Create custom model admin with custom form + +.. code:: python + + from django.contrib import admin + + from careers.forms.admin.position import PositionAdminForm + + + class PositionAdmin(admin.ModelAdmin): + form = PositionAdminForm + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..c92799f --- /dev/null +++ b/setup.py @@ -0,0 +1,54 @@ +import os +from setuptools import setup, find_packages + + +def read(fname): + path = os.path.join(os.path.dirname(__file__), fname) + try: + file = open(path, encoding='utf-8') + except TypeError: + file = open(path) + return file.read() + + +def get_install_requires(): + install_requires = ['Django'] + + return install_requires + +setup( + name='django-translatable-fields', + version=__import__('translatable_fields').VERSION, + description='Translatable model fields for Django with admin integration', + long_description=read('README.rst'), + author='Denis Kildishev', + author_email='d.kildishev@geex-arts.com', + url='https://github.com/geex-arts/django-translatable-fields', + packages=find_packages(), + license='MIT', + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Framework :: Django', + 'License :: Free for non-commercial use', + 'License :: OSI Approved :: MIT License', + 'Intended Audience :: Developers', + 'Intended Audience :: System Administrators', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.2', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Environment :: Web Environment', + 'Topic :: Software Development', + 'Topic :: Software Development :: User Interfaces', + ], + zip_safe=False, + include_package_data=True, + install_requires=get_install_requires() +) diff --git a/translatable_fields/__init__.py b/translatable_fields/__init__.py new file mode 100644 index 0000000..a4e55ec --- /dev/null +++ b/translatable_fields/__init__.py @@ -0,0 +1 @@ +VERSION = '0.0.1' diff --git a/translatable_fields/models.py b/translatable_fields/models.py new file mode 100644 index 0000000..33c3bc2 --- /dev/null +++ b/translatable_fields/models.py @@ -0,0 +1,26 @@ +from django.contrib.postgres.fields import JSONField + +from translatable_fields.value import TranslatableValue + + +class TranslatableField(JSONField): + def from_db_value(self, value, *args, **kwargs): + if value is None: + return value + + instance = TranslatableValue() + instance.update(value) + + return instance + + def to_python(self, value): + if isinstance(value, TranslatableValue): + return value + + if value is None: + return value + + instance = TranslatableValue() + instance.update(value) + + return instance diff --git a/translatable_fields/static/translatable_fields/translatable_fields.css b/translatable_fields/static/translatable_fields/translatable_fields.css new file mode 100755 index 0000000..4575037 --- /dev/null +++ b/translatable_fields/static/translatable_fields/translatable_fields.css @@ -0,0 +1,47 @@ +.translatable_field { + display: inline-block; +} + +.translatable_field__tabs { + display: block; + margin: 0 0 6px 0; +} + +.translatable_field__tabs-item { + display: inline-block; + margin-left: 5px; + border-radius: 4px; + padding: 0 10px; + background: #79aec8; + color: #fff; + font-weight: 400; + opacity: 0.5; +} + +.translatable_field__tabs-item:first-child { + margin-left: 0; +} + +.translatable_field__tabs-item:hover { + background: #417690; + opacity: 1; +} + +.translatable_field__tabs-item label, .inline-group .translatable_field__tabs-item label { + width: auto; + font-size: 11px; + padding: 2px 0 1px 0; + text-decoration: none; + color: #fff; +} + +.translatable_field__tabs-item_active, +.translatable_field__tabs-item_active:hover { + background: #79aec8; + border-color: #79aec8; + opacity: 1; +} + +.translatable_field p.file-upload { + margin-left: 0; +} diff --git a/translatable_fields/static/translatable_fields/translatable_fields.js b/translatable_fields/static/translatable_fields/translatable_fields.js new file mode 100755 index 0000000..e922f34 --- /dev/null +++ b/translatable_fields/static/translatable_fields/translatable_fields.js @@ -0,0 +1,37 @@ +(function($) { + var syncTabs = function($container, lang) { + $container.find('.translatable_field__tabs-item label:contains("'+lang+'")').each(function(){ + $(this).parents('.translatable_field').find('.translatable_field__tabs-item').removeClass('translatable_field__tabs-item_active'); + $(this).parents('.translatable_field__tabs-item').addClass('translatable_field__tabs-item_active'); + $(this).parents('.translatable_field').find('.translatable_field__widgets-item').hide(); + $('#'+$(this).attr('for')).parents('.translatable_field__widgets-item').show(); + }); + }; + + $(function (){ + $('.translatable_field__widgets-item').hide(); + // set first tab as active + $('.translatable_field').each(function () { + $(this).find('.translatable_field__tabs-item:first').addClass('translatable_field__tabs-item_active'); + syncTabs($(this), $(this).find('.translatable_field__tabs-item:first label').text()); + }); + // try set active last selected tab + if (window.sessionStorage) { + var lang = window.sessionStorage.getItem('translatable-field-lang'); + if (lang) { + $('.translatable_field').each(function () { + syncTabs($(this), lang); + }); + } + } + + $('.translatable_field__tabs-item label').click(function(event) { + event.preventDefault(); + syncTabs($(this).parents('.translatable_field'), $(this).text()); + if (window.sessionStorage) { + window.sessionStorage.setItem('translatable-field-lang', $(this).text()); + } + return false; + }); + }); +})(django.jQuery); diff --git a/translatable_fields/templates/translatable_fields/multiwidget.html b/translatable_fields/templates/translatable_fields/multiwidget.html new file mode 100755 index 0000000..cc80bc6 --- /dev/null +++ b/translatable_fields/templates/translatable_fields/multiwidget.html @@ -0,0 +1,17 @@ +
+
+ {% for widget in widget.subwidgets %} +
+ +
+ {% endfor %} +
+
+ {% for widget in widget.subwidgets %} +
+{# {% include widget.template_name %}#} + {{ widget.html }} +
+ {% endfor %} +
+
diff --git a/translatable_fields/value.py b/translatable_fields/value.py new file mode 100644 index 0000000..7fe5aaa --- /dev/null +++ b/translatable_fields/value.py @@ -0,0 +1,18 @@ +from django.conf import settings +from django.utils import translation + + +class TranslatableValue(dict): + def __str__(self): + language = translation.get_language() or settings.LANGUAGE_CODE + languages = [language] + + if len(self): + languages.append(list(self.keys())[0]) + + for lang_code in languages: + value = self.get(lang_code) + if value: + return value or '' + + return '' diff --git a/translatable_fields/widgets.py b/translatable_fields/widgets.py new file mode 100644 index 0000000..f2c8232 --- /dev/null +++ b/translatable_fields/widgets.py @@ -0,0 +1,117 @@ +import json + +from django import forms +from django.conf import settings + + +class TranslatableWidget(forms.MultiWidget): + template_name = 'translatable_fields/multiwidget.html' + widget = forms.Textarea + + class Media: + js = ( + settings.STATIC_URL + 'translatable_fields/translatable_fields.js', + ) + css = { + 'all': ( + settings.STATIC_URL + 'translatable_fields/translatable_fields.css', + ) + } + + def __init__(self, widget=None, *args, **kwargs): + if widget: + self.widget = widget + + initial_widgets = [ + self.widget + for _ in settings.LANGUAGES + ] + + super().__init__(initial_widgets, *args, **kwargs) + + for ((lang_code, lang_name), sub_widget) in zip(settings.LANGUAGES, self.widgets): + sub_widget.attrs['lang'] = lang_code + sub_widget.lang_code = lang_code + sub_widget.lang_name = lang_name + + def decompress(self, value): + """ + Returns a list of decompressed values for the given compressed value. + The given value can be assumed to be valid, but not necessarily + non-empty. + """ + + if isinstance(value, str): + try: + value = json.loads(value) + except ValueError: + value = {} + + result = [] + for lang_code, _ in settings.LANGUAGES: + if value: + result.append(value.get(lang_code)) + else: + result.append(None) + + return result + + def value_from_datadict(self, data, files, name): + result = dict([ + (widget.lang_code, widget.value_from_datadict(data, files, name + '_%s' % i)) + for i, widget in enumerate(self.widgets) + ]) + if all(map(lambda x: x == '', result.values())): + return '' + return result + + def get_context(self, name, value, attrs): + context = super(forms.MultiWidget, self).get_context(name, value, attrs) + if self.is_localized: + for widget in self.widgets: + widget.is_localized = self.is_localized + # value is a list of values, each corresponding to a widget + # in self.widgets. + if not isinstance(value, list): + value = self.decompress(value) + + final_attrs = context['widget']['attrs'] + input_type = final_attrs.pop('type', None) + id_ = final_attrs.get('id') + subwidgets = [] + + for i, widget in enumerate(self.widgets): + if input_type is not None: + widget.input_type = input_type + widget_name = '%s_%s' % (name, i) + try: + widget_value = value[i] + except IndexError: + widget_value = None + if id_: + widget_attrs = final_attrs.copy() + widget_attrs['id'] = '%s_%s' % (id_, i) + else: + widget_attrs = final_attrs + widget_attrs = self.build_widget_attrs(widget, widget_value, widget_attrs) + widget_context = widget.get_context(widget_name, widget_value, widget_attrs)['widget'] + widget_context.update(dict( + lang_code=widget.lang_code, + lang_name=widget.lang_name + )) + + widget_context['html'] = widget.render(widget_name, widget_value, widget_attrs) + subwidgets.append(widget_context) + context['widget']['subwidgets'] = subwidgets + + return context + + @staticmethod + def build_widget_attrs(widget, value, attrs): + attrs = dict(attrs) # Copy attrs to avoid modifying the argument. + + if (not widget.use_required_attribute(value) or not widget.is_required) \ + and 'required' in attrs: + del attrs['required'] + + return attrs From 4fd09bf2deb936fc0d8012bde1a0bf7f3e0672a6 Mon Sep 17 00:00:00 2001 From: Denis Kildishev Date: Thu, 8 Mar 2018 18:29:22 +0300 Subject: [PATCH 2/5] Update README.rst --- README.rst | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index a1dc73c..9998a5a 100644 --- a/README.rst +++ b/README.rst @@ -8,7 +8,7 @@ Installation ============ -* Add application +* Add application in `settings.py` .. code:: python @@ -17,6 +17,19 @@ Installation 'translatable_fields', ... ) + +* Specify languages in `settings.py` + +.. code:: python + + # Internationalization + + LANGUAGE_CODE = 'en' + LANGUAGES = ( + ('en', 'English'), + ('ru', 'Русский') + ) + * Add `TranslatableField` model fields From de9362ff4cf56a44b8401d6fa40519757e35df5f Mon Sep 17 00:00:00 2001 From: Jakob Klepp Date: Sun, 16 Feb 2020 13:12:26 +0100 Subject: [PATCH 3/5] Update .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 6c02314..066a37d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ /.idea /docs/_build/ /node_modules/ +result +*.sqlite3 From 0240564eb50715105c7a9a4b5d4faa00d8c4ba7f Mon Sep 17 00:00:00 2001 From: Jakob Klepp Date: Sun, 16 Feb 2020 13:14:28 +0100 Subject: [PATCH 4/5] Move project related files to app --- MANIFEST.in | 4 -- setup.py | 54 ------------------- .../CHANGELOG.rst | 0 LICENSE => translatable_fields/LICENSE | 0 README.rst => translatable_fields/README.rst | 0 5 files changed, 58 deletions(-) delete mode 100644 MANIFEST.in delete mode 100644 setup.py rename CHANGELOG.rst => translatable_fields/CHANGELOG.rst (100%) rename LICENSE => translatable_fields/LICENSE (100%) rename README.rst => translatable_fields/README.rst (100%) diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index b2ae275..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,4 +0,0 @@ -include README.rst -include LICENSE -recursive-include translatable_fields/static * -recursive-include translatable_fields/templates * diff --git a/setup.py b/setup.py deleted file mode 100644 index c92799f..0000000 --- a/setup.py +++ /dev/null @@ -1,54 +0,0 @@ -import os -from setuptools import setup, find_packages - - -def read(fname): - path = os.path.join(os.path.dirname(__file__), fname) - try: - file = open(path, encoding='utf-8') - except TypeError: - file = open(path) - return file.read() - - -def get_install_requires(): - install_requires = ['Django'] - - return install_requires - -setup( - name='django-translatable-fields', - version=__import__('translatable_fields').VERSION, - description='Translatable model fields for Django with admin integration', - long_description=read('README.rst'), - author='Denis Kildishev', - author_email='d.kildishev@geex-arts.com', - url='https://github.com/geex-arts/django-translatable-fields', - packages=find_packages(), - license='MIT', - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Framework :: Django', - 'License :: Free for non-commercial use', - 'License :: OSI Approved :: MIT License', - 'Intended Audience :: Developers', - 'Intended Audience :: System Administrators', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.6', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.2', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Environment :: Web Environment', - 'Topic :: Software Development', - 'Topic :: Software Development :: User Interfaces', - ], - zip_safe=False, - include_package_data=True, - install_requires=get_install_requires() -) diff --git a/CHANGELOG.rst b/translatable_fields/CHANGELOG.rst similarity index 100% rename from CHANGELOG.rst rename to translatable_fields/CHANGELOG.rst diff --git a/LICENSE b/translatable_fields/LICENSE similarity index 100% rename from LICENSE rename to translatable_fields/LICENSE diff --git a/README.rst b/translatable_fields/README.rst similarity index 100% rename from README.rst rename to translatable_fields/README.rst From 4b861162d0acaa1f70adf3e2451718117b4ef1e1 Mon Sep 17 00:00:00 2001 From: Jakob Klepp Date: Sun, 16 Feb 2020 13:15:17 +0100 Subject: [PATCH 5/5] Format code with black --- translatable_fields/__init__.py | 2 +- translatable_fields/value.py | 4 +- translatable_fields/widgets.py | 72 ++++++++++++++++++--------------- 3 files changed, 42 insertions(+), 36 deletions(-) diff --git a/translatable_fields/__init__.py b/translatable_fields/__init__.py index a4e55ec..901e511 100644 --- a/translatable_fields/__init__.py +++ b/translatable_fields/__init__.py @@ -1 +1 @@ -VERSION = '0.0.1' +VERSION = "0.0.1" diff --git a/translatable_fields/value.py b/translatable_fields/value.py index 7fe5aaa..141d6e2 100644 --- a/translatable_fields/value.py +++ b/translatable_fields/value.py @@ -13,6 +13,6 @@ class TranslatableValue(dict): for lang_code in languages: value = self.get(lang_code) if value: - return value or '' + return value or "" - return '' + return "" diff --git a/translatable_fields/widgets.py b/translatable_fields/widgets.py index f2c8232..6461d28 100644 --- a/translatable_fields/widgets.py +++ b/translatable_fields/widgets.py @@ -5,16 +5,14 @@ from django.conf import settings class TranslatableWidget(forms.MultiWidget): - template_name = 'translatable_fields/multiwidget.html' + template_name = "translatable_fields/multiwidget.html" widget = forms.Textarea class Media: - js = ( - settings.STATIC_URL + 'translatable_fields/translatable_fields.js', - ) + js = (settings.STATIC_URL + "translatable_fields/translatable_fields.js",) css = { - 'all': ( - settings.STATIC_URL + 'translatable_fields/translatable_fields.css', + "all": ( + settings.STATIC_URL + "translatable_fields/translatable_fields.css", ) } @@ -22,15 +20,14 @@ class TranslatableWidget(forms.MultiWidget): if widget: self.widget = widget - initial_widgets = [ - self.widget - for _ in settings.LANGUAGES - ] + initial_widgets = [self.widget for _ in settings.LANGUAGES] super().__init__(initial_widgets, *args, **kwargs) - for ((lang_code, lang_name), sub_widget) in zip(settings.LANGUAGES, self.widgets): - sub_widget.attrs['lang'] = lang_code + for ((lang_code, lang_name), sub_widget) in zip( + settings.LANGUAGES, self.widgets + ): + sub_widget.attrs["lang"] = lang_code sub_widget.lang_code = lang_code sub_widget.lang_name = lang_name @@ -57,12 +54,17 @@ class TranslatableWidget(forms.MultiWidget): return result def value_from_datadict(self, data, files, name): - result = dict([ - (widget.lang_code, widget.value_from_datadict(data, files, name + '_%s' % i)) - for i, widget in enumerate(self.widgets) - ]) - if all(map(lambda x: x == '', result.values())): - return '' + result = dict( + [ + ( + widget.lang_code, + widget.value_from_datadict(data, files, name + "_%s" % i), + ) + for i, widget in enumerate(self.widgets) + ] + ) + if all(map(lambda x: x == "", result.values())): + return "" return result def get_context(self, name, value, attrs): @@ -75,34 +77,37 @@ class TranslatableWidget(forms.MultiWidget): if not isinstance(value, list): value = self.decompress(value) - final_attrs = context['widget']['attrs'] - input_type = final_attrs.pop('type', None) - id_ = final_attrs.get('id') + final_attrs = context["widget"]["attrs"] + input_type = final_attrs.pop("type", None) + id_ = final_attrs.get("id") subwidgets = [] for i, widget in enumerate(self.widgets): if input_type is not None: widget.input_type = input_type - widget_name = '%s_%s' % (name, i) + widget_name = "%s_%s" % (name, i) try: widget_value = value[i] except IndexError: widget_value = None if id_: widget_attrs = final_attrs.copy() - widget_attrs['id'] = '%s_%s' % (id_, i) + widget_attrs["id"] = "%s_%s" % (id_, i) else: widget_attrs = final_attrs widget_attrs = self.build_widget_attrs(widget, widget_value, widget_attrs) - widget_context = widget.get_context(widget_name, widget_value, widget_attrs)['widget'] - widget_context.update(dict( - lang_code=widget.lang_code, - lang_name=widget.lang_name - )) + widget_context = widget.get_context( + widget_name, widget_value, widget_attrs + )["widget"] + widget_context.update( + dict(lang_code=widget.lang_code, lang_name=widget.lang_name) + ) - widget_context['html'] = widget.render(widget_name, widget_value, widget_attrs) + widget_context["html"] = widget.render( + widget_name, widget_value, widget_attrs + ) subwidgets.append(widget_context) - context['widget']['subwidgets'] = subwidgets + context["widget"]["subwidgets"] = subwidgets return context @@ -110,8 +115,9 @@ class TranslatableWidget(forms.MultiWidget): def build_widget_attrs(widget, value, attrs): attrs = dict(attrs) # Copy attrs to avoid modifying the argument. - if (not widget.use_required_attribute(value) or not widget.is_required) \ - and 'required' in attrs: - del attrs['required'] + if ( + not widget.use_required_attribute(value) or not widget.is_required + ) and "required" in attrs: + del attrs["required"] return attrs