From c9d2efc27108a1138149c0ac72b8f7b58c15f814 Mon Sep 17 00:00:00 2001 From: Jorge Pintor Date: Thu, 18 Jan 2024 15:42:58 -0600 Subject: [PATCH 1/9] Update to support Django 4.2 --- .travis.yml | 23 +- CHANGELOG.rst | 6 + setup.py | 9 +- suit/__init__.py | 2 +- .../templates/admin/cms/page/change_form.html | 2 +- .../templates/admin/cms/page/change_list.html | 2 +- .../admin/filer/folder/directory_table.html | 6 +- suit/templates/admin/pagination.html | 4 +- suit/templatetags/suit_list.py | 200 ++++--- suit/tests/__init__.py | 31 +- suit/tests/models.py | 41 +- suit/tests/settings.py | 102 ++-- suit/tests/templates/form_tabs.py | 36 +- suit/tests/templatetags/suit_list.py | 121 +++-- suit/tests/templatetags/suit_menu.py | 507 +++++++++--------- suit/tests/templatetags/suit_tags.py | 85 +-- suit/tests/urls/__init__.py | 24 +- suit/tests/urls/admin_at_root.py | 8 +- suit/tests/urls/admin_custom.py | 8 +- suit/tests/urls/admin_i18n.py | 7 +- suit/tests/widgets.py | 196 ++++--- suit/widgets.py | 43 +- 22 files changed, 752 insertions(+), 711 deletions(-) diff --git a/.travis.yml b/.travis.yml index 91ff8b90..96250ed7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,15 +1,12 @@ language: python python: - - 2.7 - - 3.4 - - pypy - - pypy3 + - 3.8 + - 3.9 + - 3.10 env: - - DJANGO=1.8.18 - - DJANGO=1.9.13 - - DJANGO=1.10.8 - - DJANGO=1.11.10 - - DJANGO=2.0.2 + - DJANGO=3.2 + - DJANGO=4.2 + - DJANGO=5.0 before_install: - export DJANGO_SETTINGS_MODULE=suit.tests.settings install: @@ -20,7 +17,7 @@ script: matrix: exclude: # Django doesn't support following combinations - - python: 2.7 - env: DJANGO=2.0.2 - - python: pypy - env: DJANGO=2.0.2 + - python: 3.8 + env: DJANGO=5.0 + - python: 3.9 + env: DJANGO=5.0 diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8ec1ccaa..da8d34bd 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,12 @@ Changelog Only important changes are mentioned below. See `commit log `_, `closed issues `_ and `closed pull requests `_ for full changes. +v.0.3.0 (2024-01-18) +-------------------- +* Add support for Django >=4.0 +* Remove support for Django < 3.2 + + v.0.2.29 (2021-04-28) -------------------- * This is a 'maintenance fork' which contains some fixes for compatibility with the latest Django releases. diff --git a/setup.py b/setup.py index 491cb5b5..9e9401d4 100644 --- a/setup.py +++ b/setup.py @@ -23,11 +23,10 @@ 'Intended Audience :: System Administrators', 'Operating System :: OS Independent', 'Programming Language :: Python', - 'Programming Language :: Python :: 2.5', - 'Programming Language :: Python :: 2.6', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.2', - 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', 'Environment :: Web Environment', 'Topic :: Software Development', 'Topic :: Software Development :: User Interfaces', diff --git a/suit/__init__.py b/suit/__init__.py index 5ec7de59..d325e712 100644 --- a/suit/__init__.py +++ b/suit/__init__.py @@ -1 +1 @@ -VERSION = '0.2.29' +VERSION = '0.3.0' diff --git a/suit/templates/admin/cms/page/change_form.html b/suit/templates/admin/cms/page/change_form.html index 107f1cce..8afe0f14 100644 --- a/suit/templates/admin/cms/page/change_form.html +++ b/suit/templates/admin/cms/page/change_form.html @@ -121,7 +121,7 @@

{% trans 'actions'|capfirst %}

{% if show_language_tabs %} {% endif %} diff --git a/suit/templates/admin/cms/page/change_list.html b/suit/templates/admin/cms/page/change_list.html index 48b03328..105e3f0a 100644 --- a/suit/templates/admin/cms/page/change_list.html +++ b/suit/templates/admin/cms/page/change_list.html @@ -124,7 +124,7 @@ {% if cl.has_access_to_multiple_sites %}
{% trans "Pages on:" %}
{% else %} diff --git a/suit/templates/admin/filer/folder/directory_table.html b/suit/templates/admin/filer/folder/directory_table.html index 49cc66ee..8cbcce1b 100644 --- a/suit/templates/admin/filer/folder/directory_table.html +++ b/suit/templates/admin/filer/folder/directory_table.html @@ -42,7 +42,7 @@ {% trans "Delete" %} {% trans "Change" %}
{{ file.label }}
-
{{ file.size|filesizeformat }}{% ifequal file.file_type "Image" %}, {{ file.width }}x{{ file.height }} px{% endifequal %}
{% trans "Owner" %}: {{ file.owner|default:"n/a" }}{% if enable_permissions %}. {% trans "Permissions" %}: {% if file.is_public %}{% trans "disabled" %}{% else %}{% trans "enabled" %}{% endif %}{% endif %}
+
{{ file.size|filesizeformat }}{% if file.file_type == "Image" %}, {{ file.width }}x{{ file.height }} px{% endif %}
{% trans "Owner" %}: {{ file.owner|default:"n/a" }}{% if enable_permissions %}. {% trans "Permissions" %}: {% if file.is_public %}{% trans "disabled" %}{% else %}{% trans "enabled" %}{% endif %}{% endif %}
{# {% if file.has_all_mandatory_data %}
0 {% trans "has all mandatory metadata" %}
{% else %}
1 {% trans "missing metadata!" %}
{% endif %} #} @@ -51,12 +51,12 @@ {% endwith %}{% endif %} {% endfor %} - {% if not folder.is_root %}{% ifequal folder.item_count 0 %} + {% if not folder.is_root %}{% if folder.item_count == 0 %} {% trans "there are no files or subfolders" %} - {% endifequal %}{% endif %} + {% endif %}{% endif %}
diff --git a/suit/templates/admin/pagination.html b/suit/templates/admin/pagination.html index a928d141..d964024c 100644 --- a/suit/templates/admin/pagination.html +++ b/suit/templates/admin/pagination.html @@ -20,8 +20,8 @@ {% paginator_info cl %}   /   {{ cl.result_count }} - {% ifequal cl.result_count 1 %}{{ cl.opts.verbose_name }}{% else %} - {{ cl.opts.verbose_name_plural }}{% endifequal %} + {% if cl.result_count == 1 %}{{ cl.opts.verbose_name }}{% else %} + {{ cl.opts.verbose_name_plural }}{% endif %} {% if show_all_url %}   {% trans 'Show all' %}{% endif %}
diff --git a/suit/templatetags/suit_list.py b/suit/templatetags/suit_list.py index 028ec784..dab52fdd 100644 --- a/suit/templatetags/suit_list.py +++ b/suit/templatetags/suit_list.py @@ -1,41 +1,29 @@ from copy import copy from inspect import getargspec +from urllib.parse import parse_qs import django - from django import template -from django.template.loader import get_template -from django.utils.safestring import mark_safe from django.contrib.admin.templatetags.admin_list import result_list from django.contrib.admin.views.main import ALL_VAR, PAGE_VAR -from django.utils.html import escape -from django.utils.html import format_html +from django.template.loader import get_template +from django.utils.html import escape, format_html +from django.utils.safestring import mark_safe + from suit.compat import tpl_context_class # Starting with Django 3.2, the pagination is 1-based instead of 0-based. # I've taken the implementations (latest at the time of release 3.2) from https://github.com/django/django/blob/main/django/contrib/admin/templatetags/admin_list.py # to use in the implementations below. Older versions of django will use the old implementation and will keep working. # There are corresponding CSS changes in suit/static/suit/less/ui/pagination.less to fix the pagination, as the code below generates different html objects. -USE_NEW_DJANGO_ADMIN_PAGINATION = django.get_version() >= '3.2' - -try: - # Python 3. - from urllib.parse import parse_qs -except ImportError: - # Python 2.5+ - from urlparse import urlparse - - try: - # Python 2.6+ - from urlparse import parse_qs - except ImportError: - # Python <=2.5 - from cgi import parse_qs +print(django.get_version()) +USE_NEW_DJANGO_ADMIN_PAGINATION = django.get_version() >= "3.2" + register = template.Library() -DOT = '.' +DOT = "." @register.simple_tag @@ -46,16 +34,20 @@ def paginator_number(cl, i): if not USE_NEW_DJANGO_ADMIN_PAGINATION: if i == DOT: return mark_safe( - '
  • ..' - '.
  • ') + '
  • ..' + ".
  • " + ) elif i == cl.page_num: - return mark_safe( - '
  • %d
  • ' % (i + 1)) + return mark_safe('
  • %d
  • ' % (i + 1)) else: - return mark_safe('
  • %d
  • ' % ( - escape(cl.get_query_string({PAGE_VAR: i})), - (i == cl.paginator.num_pages - 1 and ' class="end"' or ''), - i + 1)) + return mark_safe( + '
  • %d
  • ' + % ( + escape(cl.get_query_string({PAGE_VAR: i})), + (i == cl.paginator.num_pages - 1 and ' class="end"' or ""), + i + 1, + ) + ) if i == cl.paginator.ELLIPSIS: return format_html('{} ', cl.paginator.ELLIPSIS) @@ -65,7 +57,7 @@ def paginator_number(cl, i): return format_html( '{} ', cl.get_query_string({PAGE_VAR: i}), - mark_safe(' class="end"' if i == cl.paginator.num_pages else ''), + mark_safe(' class="end"' if i == cl.paginator.num_pages else ""), i, ) @@ -81,12 +73,13 @@ def paginator_info(cl): entries_to = paginator.count else: entries_from = ( - (paginator.per_page * cl.page_num) + 1) if paginator.count > 0 else 0 + ((paginator.per_page * cl.page_num) + 1) if paginator.count > 0 else 0 + ) entries_to = entries_from - 1 + paginator.per_page if paginator.count < entries_to: entries_to = paginator.count - return '%s - %s' % (entries_from, entries_to) + return "%s - %s" % (entries_from, entries_to) paginator = cl.paginator @@ -95,15 +88,17 @@ def paginator_info(cl): entries_from = 1 if paginator.count > 0 else 0 entries_to = paginator.count else: - entries_from = ((paginator.per_page * (cl.page_num - 1)) + 1) if paginator.count > 0 else 0 + entries_from = ( + ((paginator.per_page * (cl.page_num - 1)) + 1) if paginator.count > 0 else 0 + ) entries_to = entries_from - 1 + paginator.per_page if paginator.count < entries_to: entries_to = paginator.count - return '%s - %s' % (entries_from, entries_to) + return "%s - %s" % (entries_from, entries_to) -@register.inclusion_tag('admin/pagination.html') +@register.inclusion_tag("admin/pagination.html") def pagination(cl): """ Generate the series of links to the pages in a paginated list. @@ -111,8 +106,7 @@ def pagination(cl): if not USE_NEW_DJANGO_ADMIN_PAGINATION: paginator, page_num = cl.paginator, cl.page_num - pagination_required = (not cl.show_all or not cl.can_show_all) \ - and cl.multi_page + pagination_required = (not cl.show_all or not cl.can_show_all) and cl.multi_page if not pagination_required: page_range = [] else: @@ -135,35 +129,36 @@ def pagination(cl): else: page_range.extend(range(0, page_num + 1)) if page_num < (paginator.num_pages - ON_EACH_SIDE - ON_ENDS - 1): - page_range.extend( - range(page_num + 1, page_num + ON_EACH_SIDE + 1)) + page_range.extend(range(page_num + 1, page_num + ON_EACH_SIDE + 1)) page_range.append(DOT) page_range.extend( - range(paginator.num_pages - ON_ENDS, paginator.num_pages)) + range(paginator.num_pages - ON_ENDS, paginator.num_pages) + ) else: page_range.extend(range(page_num + 1, paginator.num_pages)) need_show_all_link = cl.can_show_all and not cl.show_all and cl.multi_page return { - 'cl': cl, - 'pagination_required': pagination_required, - 'show_all_url': need_show_all_link and cl.get_query_string( - {ALL_VAR: ''}), - 'page_range': page_range, - 'ALL_VAR': ALL_VAR, - '1': 1, + "cl": cl, + "pagination_required": pagination_required, + "show_all_url": need_show_all_link and cl.get_query_string({ALL_VAR: ""}), + "page_range": page_range, + "ALL_VAR": ALL_VAR, + "1": 1, } pagination_required = (not cl.show_all or not cl.can_show_all) and cl.multi_page - page_range = cl.paginator.get_elided_page_range(cl.page_num) if pagination_required else [] + page_range = ( + cl.paginator.get_elided_page_range(cl.page_num) if pagination_required else [] + ) need_show_all_link = cl.can_show_all and not cl.show_all and cl.multi_page return { - 'cl': cl, - 'pagination_required': pagination_required, - 'show_all_url': need_show_all_link and cl.get_query_string({ALL_VAR: ''}), - 'page_range': page_range, - 'ALL_VAR': ALL_VAR, - '1': 1, + "cl": cl, + "pagination_required": pagination_required, + "show_all_url": need_show_all_link and cl.get_query_string({ALL_VAR: ""}), + "page_range": page_range, + "ALL_VAR": ALL_VAR, + "1": 1, } @@ -171,21 +166,19 @@ def pagination(cl): def suit_list_filter_select(cl, spec): tpl = get_template(spec.template) choices = list(spec.choices(cl)) - field_key = spec.field_path if hasattr(spec, 'field_path') else \ - spec.parameter_name + field_key = spec.field_path if hasattr(spec, "field_path") else spec.parameter_name matched_key = field_key for choice in choices: - query_string = choice['query_string'][1:] + query_string = choice["query_string"][1:] query_parts = parse_qs(query_string) - value = '' + value = "" matches = {} for key in query_parts.keys(): if key == field_key: value = query_parts[key][0] matched_key = key - elif key.startswith( - field_key + '__') or '__' + field_key + '__' in key: + elif key.startswith(field_key + "__") or "__" + field_key + "__" in key: value = query_parts[key][0] matched_key = key @@ -196,18 +189,22 @@ def suit_list_filter_select(cl, spec): i = 0 for key, value in matches.items(): if i == 0: - choice['name'] = key - choice['val'] = value + choice["name"] = key + choice["val"] = value else: - choice['additional'] = '%s=%s' % (key, value) + choice["additional"] = "%s=%s" % (key, value) i += 1 - return tpl.render(tpl_context_class({ - 'field_name': field_key, - 'title': spec.title, - 'choices': choices, - 'spec': spec, - })) + return tpl.render( + tpl_context_class( + { + "field_name": field_key, + "title": spec.title, + "choices": choices, + "spec": spec, + } + ) + ) @register.filter @@ -216,29 +213,29 @@ def headers_handler(result_headers, cl): Adds field name to css class, so we can style specific columns """ # field = cl.list_display.get() - attrib_key = 'class_attrib' + attrib_key = "class_attrib" for i, header in enumerate(result_headers): field_name = cl.list_display[i] - if field_name == 'action_checkbox': + if field_name == "action_checkbox": continue if not attrib_key in header: header[attrib_key] = mark_safe(' class=""') pattern = 'class="' if pattern in header[attrib_key]: - replacement = '%s%s-column ' % (pattern, field_name) + replacement = "%s%s-column " % (pattern, field_name) header[attrib_key] = mark_safe( - header[attrib_key].replace(pattern, replacement)) + header[attrib_key].replace(pattern, replacement) + ) return result_headers def dict_to_attrs(attrs): - return mark_safe(' ' + ' '.join(['%s="%s"' % (k, v) - for k, v in attrs.items()])) + return mark_safe(" " + " ".join(['%s="%s"' % (k, v) for k, v in attrs.items()])) -@register.inclusion_tag('admin/change_list_results.html', takes_context=True) +@register.inclusion_tag("admin/change_list_results.html", takes_context=True) def result_list_with_context(context, cl): """ Wraps Djangos default result_list to ammend the context with the request. @@ -246,7 +243,7 @@ def result_list_with_context(context, cl): This gives us access to the request in change_list_results. """ res = result_list(cl) - res['request'] = context['request'] + res["request"] = context["request"] return res @@ -256,10 +253,8 @@ def result_row_attrs(context, cl, row_index): Returns row attributes based on object instance """ row_index -= 1 - attrs = { - 'class': 'row1' if row_index % 2 == 0 else 'row2' - } - suit_row_attributes = getattr(cl.model_admin, 'suit_row_attributes', None) + attrs = {"class": "row1" if row_index % 2 == 0 else "row2"} + suit_row_attributes = getattr(cl.model_admin, "suit_row_attributes", None) if not suit_row_attributes: return dict_to_attrs(attrs) @@ -267,8 +262,8 @@ def result_row_attrs(context, cl, row_index): # Backwards compatibility for suit_row_attributes without request argument args = getargspec(suit_row_attributes) - if 'request' in args[0]: - new_attrs = suit_row_attributes(instance, context['request']) + if "request" in args[0]: + new_attrs = suit_row_attributes(instance, context["request"]) else: new_attrs = suit_row_attributes(instance) @@ -277,12 +272,14 @@ def result_row_attrs(context, cl, row_index): # Validate if not isinstance(new_attrs, dict): - raise TypeError('"suit_row_attributes" must return dict. Got: %s: %s' % - (new_attrs.__class__.__name__, new_attrs)) + raise TypeError( + '"suit_row_attributes" must return dict. Got: %s: %s' + % (new_attrs.__class__.__name__, new_attrs) + ) # Merge 'class' attribute - if 'class' in new_attrs: - attrs['class'] += ' ' + new_attrs.pop('class') + if "class" in new_attrs: + attrs["class"] += " " + new_attrs.pop("class") attrs.update(new_attrs) return dict_to_attrs(attrs) @@ -293,13 +290,13 @@ def cells_handler(results, cl): """ Changes result cell attributes based on object instance and field name """ - suit_cell_attributes = getattr(cl.model_admin, 'suit_cell_attributes', None) + suit_cell_attributes = getattr(cl.model_admin, "suit_cell_attributes", None) if not suit_cell_attributes: return results class_pattern = 'class="' - td_pattern = '')[0] and 'class' in attrs: - css_class = attrs.pop('class') - replacement = '%s%s ' % (class_pattern, css_class) - result[col] = mark_safe( - item.replace(class_pattern, replacement)) + if class_pattern in item.split(">")[0] and "class" in attrs: + css_class = attrs.pop("class") + replacement = "%s%s " % (class_pattern, css_class) + result[col] = mark_safe(item.replace(class_pattern, replacement)) # Add rest of attributes if any left if attrs: - cell_pattern = td_pattern if item.startswith( - td_pattern) else th_pattern + cell_pattern = td_pattern if item.startswith(td_pattern) else th_pattern result[col] = mark_safe( - result[col].replace(cell_pattern, - td_pattern + dict_to_attrs(attrs))) + result[col].replace(cell_pattern, td_pattern + dict_to_attrs(attrs)) + ) return results diff --git a/suit/tests/__init__.py b/suit/tests/__init__.py index 8c9a9bfc..449326ff 100644 --- a/suit/tests/__init__.py +++ b/suit/tests/__init__.py @@ -1,25 +1,20 @@ import django -try: - # Django 1.9+ - django.setup() -except Exception: - pass +from django.test.runner import DiscoverRunner as DjangoTestSuiteRunner + +django.setup() -from suit.tests.templatetags.suit_menu import SuitMenuTestCase, \ - SuitMenuAdminRootURLTestCase, SuitMenuAdminI18NURLTestCase, \ - SuitMenuAdminCustomURLTestCase -from suit.tests.templatetags.suit_tags import SuitTagsTestCase -from suit.tests.templatetags.suit_list import SuitListTestCase -from suit.tests.templates.form_tabs import FormTabsTestCase from suit.tests.config import ConfigTestCase, ConfigWithModelsTestCase -from suit.tests.widgets import WidgetsTestCase +from suit.tests.templates.form_tabs import FormTabsTestCase +from suit.tests.templatetags.suit_list import SuitListTestCase +from suit.tests.templatetags.suit_menu import ( + SuitMenuAdminCustomURLTestCase, + SuitMenuAdminI18NURLTestCase, + SuitMenuAdminRootURLTestCase, + SuitMenuTestCase, +) +from suit.tests.templatetags.suit_tags import SuitTagsTestCase from suit.tests.utils import UtilsTestCase - -try: - # Django 1.7+ - from django.test.runner import DiscoverRunner as DjangoTestSuiteRunner -except ImportError: - from django.test.simple import DjangoTestSuiteRunner +from suit.tests.widgets import WidgetsTestCase class NoDbTestRunner(DjangoTestSuiteRunner): diff --git a/suit/tests/models.py b/suit/tests/models.py index e6ff885d..aebcb1d9 100644 --- a/suit/tests/models.py +++ b/suit/tests/models.py @@ -1,5 +1,5 @@ -from django.db import models from django.contrib import admin +from django.db import models def test_app_label(): @@ -10,7 +10,7 @@ def test_app_label(): try: return Book._meta.app_label except: - return 'tests' + return "tests" class Book(models.Model): @@ -20,7 +20,7 @@ def __unicode__(self): return self.name class Meta: - ordering = ('-id',) + ordering = ("-id",) class Album(models.Model): @@ -30,34 +30,47 @@ def __unicode__(self): return self.name +@admin.register(Book) class BookAdmin(admin.ModelAdmin): - list_filter = ('id', 'name',) - list_display = ('id', 'name',) + list_filter = ( + "id", + "name", + ) + list_display = ( + "id", + "name", + ) def suit_row_attributes(self, obj, request): - return {'class': 'suit_row_attr_class-%s' % obj.name, - 'data': obj.pk, - 'data-request': request} + return { + "class": "suit_row_attr_class-%s" % obj.name, + "data": obj.pk, + "data-request": request, + } def suit_cell_attributes(self, obj, column): - return {'class': 'suit_cell_attr_class-%s-%s' % (column, obj.name), - 'data': obj.pk} + return { + "class": "suit_cell_attr_class-%s-%s" % (column, obj.name), + "data": obj.pk, + } +@admin.register(Album) class AlbumAdmin(admin.ModelAdmin): def suit_row_attributes(self, obj): """No request defined to test backward-compatibility""" - return {'class': 'suit_row_album_attr_class-%s' % obj.name, - 'data-album': obj.pk} + return { + "class": "suit_row_album_attr_class-%s" % obj.name, + "data-album": obj.pk, + } class User(models.Model): """ Class to test menu marking as active if two apps have model with same name """ + name = models.CharField(max_length=64) -admin.site.register(Book, BookAdmin) -admin.site.register(Album, AlbumAdmin) admin.site.register(User) diff --git a/suit/tests/settings.py b/suit/tests/settings.py index 12b8a470..454182db 100644 --- a/suit/tests/settings.py +++ b/suit/tests/settings.py @@ -9,83 +9,59 @@ MANAGERS = ADMINS DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', + "default": { + "ENGINE": "django.db.backends.sqlite3", } } +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" -TIME_ZONE = 'Europe/Riga' +TIME_ZONE = "Europe/Riga" SITE_ID = 1 USE_I18N = True -USE_L10N = True -MEDIA_ROOT = '' -MEDIA_URL = '' -SECRET_KEY = 'vaO4Y.w(w~q_b=|1`?90KxA%UB!63' +MEDIA_ROOT = "" +MEDIA_URL = "" +SECRET_KEY = "vaO4Y.w(w~q_b=|1`?90KxA%UB!63" MIDDLEWARE = [ - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.security.SecurityMiddleware", ] -if django.VERSION < (1, 10): - MIDDLEWARE_CLASSES = MIDDLEWARE + [ - 'django.contrib.auth.middleware.AuthenticationMiddleware', - ] -else: - MIDDLEWARE += [ - 'django.middleware.security.SecurityMiddleware', - ] - -ROOT_URLCONF = 'suit.tests.urls' +ROOT_URLCONF = "suit.tests.urls" INSTALLED_APPS = ( - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.sites', - 'django.contrib.messages', - - 'suit', - 'suit.tests.templatetags', - 'django.contrib.admin', + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.sites", + "django.contrib.messages", + "suit", + "suit.tests.templatetags", + "django.contrib.admin", ) -STATIC_URL = '/static/' - -try: - from django.conf.global_settings import TEMPLATE_CONTEXT_PROCESSORS as TCP - - TEMPLATE_DEBUG = DEBUG - - TEMPLATE_LOADERS = ( - 'django.template.loaders.filesystem.Loader', - 'django.template.loaders.app_directories.Loader', - ) - - TEMPLATE_DIRS = () - - TEMPLATE_CONTEXT_PROCESSORS = list(TCP) + [ - 'django.core.context_processors.request', - ] -except ImportError: # Django 1.9+ - TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], - }, +STATIC_URL = "/static/" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], }, - ] + }, +] SUIT_CONFIG = {} -TEST_RUNNER = 'suit.tests.SuitTestRunner' +TEST_RUNNER = "suit.tests.SuitTestRunner" diff --git a/suit/tests/templates/form_tabs.py b/suit/tests/templates/form_tabs.py index 6bd9cde5..115bbfe1 100644 --- a/suit/tests/templates/form_tabs.py +++ b/suit/tests/templates/form_tabs.py @@ -1,5 +1,5 @@ from django.contrib import admin -from django.utils.translation import ugettext +from django.utils.translation import gettext from suit.tests.mixins import ModelsTestCaseMixin, UserTestCaseMixin from suit.tests.models import Book, BookAdmin, test_app_label @@ -14,8 +14,11 @@ class TabbedBookAdmin(BookAdmin): - list_filter = ('id', 'name',) - suit_form_tabs = (('tab1', 'Tab1'), ('tab2', ugettext('Tab2'))) + list_filter = ( + "id", + "name", + ) + suit_form_tabs = (("tab1", "Tab1"), ("tab2", gettext("Tab2"))) suit_form_includes = None @@ -26,24 +29,25 @@ class TabbedBookAdmin(BookAdmin): class FormTabsTestCase(ModelsTestCaseMixin, UserTestCaseMixin): def setUp(self): self.login_superuser() - self.url = reverse('admin:%s_book_add' % app_label) + self.url = reverse("admin:%s_book_add" % app_label) self.get_response(self.url) def test_tabs_appearance(self): for x in range(0, 2): - vars = (TabbedBookAdmin.suit_form_tabs[x][0], - TabbedBookAdmin.suit_form_tabs[x][1]) - self.assertContains(self.response, '
  • %s
  • ' % - vars) + vars = ( + TabbedBookAdmin.suit_form_tabs[x][0], + TabbedBookAdmin.suit_form_tabs[x][1], + ) + self.assertContains(self.response, '
  • %s
  • ' % vars) def test_template_includes(self): - suit_form_include = 'admin/date_hierarchy.html' - TabbedBookAdmin.suit_form_includes = ( - (suit_form_include, 'top', 'tab1'), - ) + suit_form_include = "admin/date_hierarchy.html" + TabbedBookAdmin.suit_form_includes = ((suit_form_include, "top", "tab1"),) self.get_response(self.url) - self.assertTemplateUsed(self.response, - 'suit/includes/change_form_includes.html') + self.assertTemplateUsed( + self.response, "suit/includes/change_form_includes.html" + ) self.assertTemplateUsed(self.response, suit_form_include) - self.assertContains(self.response, - '
    ') + self.assertContains( + self.response, '
    ' + ) diff --git a/suit/tests/templatetags/suit_list.py b/suit/tests/templatetags/suit_list.py index 8b82366b..92782749 100644 --- a/suit/tests/templatetags/suit_list.py +++ b/suit/tests/templatetags/suit_list.py @@ -1,32 +1,35 @@ from django.contrib.admin import ModelAdmin from django.contrib.admin.templatetags.admin_list import result_list -from suit.templatetags.suit_list import paginator_number, paginator_info, \ - pagination, suit_list_filter_select, headers_handler, dict_to_attrs, \ - result_row_attrs, cells_handler -from suit.tests.mixins import UserTestCaseMixin, ModelsTestCaseMixin +from django.urls import reverse + +from suit.templatetags.suit_list import ( + cells_handler, + dict_to_attrs, + headers_handler, + pagination, + paginator_info, + paginator_number, + result_row_attrs, + suit_list_filter_select, +) +from suit.tests.mixins import ModelsTestCaseMixin, UserTestCaseMixin from suit.tests.models import Album, Book, test_app_label -try: - from django.core.urlresolvers import reverse -except ImportError: - # For Django >= 2.0 - from django.urls import reverse - app_label = test_app_label() class ModelAdminMock(object): def suit_row_attributes(self, obj): - return {'class': obj.name, 'data': obj.pk} + return {"class": obj.name, "data": obj.pk} def suit_cell_attributes(self, obj, column): - return {'class': 'col-' + column, 'data': obj.pk} + return {"class": "col-" + column, "data": obj.pk} class ChangeListMock(object): - list_display = ('action_checkbox', 'name', 'order', 'status') + list_display = ("action_checkbox", "name", "order", "status") model_admin = ModelAdminMock() - result_list = [Book(pk=1, name='beach'), Book(pk=2, name='sky')] + result_list = [Book(pk=1, name="beach"), Book(pk=2, name="sky")] class SuitListTestCase(UserTestCaseMixin, ModelsTestCaseMixin): @@ -34,48 +37,48 @@ class SuitListTestCase(UserTestCaseMixin, ModelsTestCaseMixin): book = None def get_changelist(self): - self.get_response(reverse('admin:%s_book_changelist' % app_label)) - self.changelist = self.response.context_data['cl'] + self.get_response(reverse("admin:%s_book_changelist" % app_label)) + self.changelist = self.response.context_data["cl"] def setUp(self): self.login_superuser() - self.book = Book(name='Test') + self.book = Book(name="Test") self.book.save() self.get_changelist() def test_paginator_number(self): output = paginator_number(self.changelist, 100) - self.assertTrue('100' in output) + self.assertTrue("100" in output) - output = paginator_number(self.changelist, '.') - self.assertTrue('...' in output) + output = paginator_number(self.changelist, "...") + self.assertTrue("..." in output) - output = paginator_number(self.changelist, 0) - self.assertTrue('active' in output) + output = paginator_number(self.changelist, 1) + self.assertTrue("this-page" in output) def test_paginator_info(self): output = paginator_info(self.changelist) - self.assertEqual('1 - 1', output) + self.assertEqual("1 - 1", output) def test_pagination_one_page(self): pg = pagination(self.changelist) - self.assertEqual(pg['cl'], self.changelist) - self.assertEqual(pg['page_range'], []) - self.assertEqual(pg['pagination_required'], False) + self.assertEqual(pg["cl"], self.changelist) + self.assertEqual(pg["page_range"], []) + self.assertEqual(pg["pagination_required"], False) def test_pagination_many_pages(self): per_page_original = ModelAdmin.list_per_page ModelAdmin.list_per_page = 20 for x in range(25): - book = Book(name='Test %d' % x) + book = Book(name="Test %d" % x) book.save() self.get_changelist() pg = pagination(self.changelist) ModelAdmin.list_per_page = per_page_original - self.assertEqual(pg['cl'], self.changelist) - self.assertEqual(len(pg['page_range']), 2) - self.assertEqual(pg['pagination_required'], True) + self.assertEqual(pg["cl"], self.changelist) + self.assertEqual(len(list(pg["page_range"])), 2) + self.assertEqual(pg["pagination_required"], True) def test_suit_list_filter_select(self): filter_matches = (self.book.pk, self.book.name) @@ -85,21 +88,23 @@ def test_suit_list_filter_select(self): self.assertTrue('value="%s"' % filter_matches[i] in filter_output) def test_suit_list_headers_handler(self): - result_headers = [{'class_attrib': ' class="test"'}, {}] - result = [{'class_attrib': ' class="test"'}, - {'class_attrib': ' class="name-column "'}] + result_headers = [{"class_attrib": ' class="test"'}, {}] + result = [ + {"class_attrib": ' class="test"'}, + {"class_attrib": ' class="name-column "'}, + ] cl = ChangeListMock() self.assertEqual(headers_handler(result_headers, cl), result) def test_suit_list_dict_to_attrs(self): - attrs = {'class': 'test', 'data': 123} + attrs = {"class": "test", "data": 123} result = dict_to_attrs(attrs) self.assertTrue('data="123"' in result) self.assertTrue('class="test"' in result) def test_suit_list_result_row_attrs(self): cl = ChangeListMock() - context = {'request': 'dummy'} + context = {"request": "dummy"} result = result_row_attrs(context, cl, 1) self.assertTrue('data="1"' in result) self.assertTrue('class="row1 beach"' in result) @@ -110,10 +115,10 @@ def test_suit_list_result_row_attrs(self): def test_suit_list_result_row_attrs_by_response(self): Book.objects.all().delete() for x in range(2): - book = Book(pk=x, name='sky-%s' % x) + book = Book(pk=x, name="sky-%s" % x) book.save() - context = {'request': 'dummy'} + context = {"request": "dummy"} self.get_changelist() result = result_row_attrs(context, self.changelist, 1) self.assertTrue('data="1"' in result) @@ -127,27 +132,31 @@ def test_suit_list_result_row_attrs_backwards_compatible(self): # A bit more verbose and manual, as this is the only test against album # changelist - self.get_response(reverse('admin:%s_album_changelist' % app_label)) - changelist = self.response.context_data['cl'] + self.get_response(reverse("admin:%s_album_changelist" % app_label)) + changelist = self.response.context_data["cl"] - context = {'request': 'dummy'} + context = {"request": "dummy"} result = result_row_attrs(context, changelist, 1) self.assertTrue('data-album="1"' in result) self.assertTrue('class="row1 suit_row_album_attr_class-foo"' in result) def test_suit_list_cells_handler(self): results = [ - ['', '', - ''], - ['', '', - ''], + ["", '', ''], + ["", '', ''], + ] + result = [ + [ + '', + '', + '', + ], + [ + '', + '', + '', + ], ] - result = [['', - '', - ''], - ['', - '', - '']] cl = ChangeListMock() result = cells_handler(results, cl) self.assertTrue('data="1"' in result[0][0]) @@ -155,20 +164,20 @@ def test_suit_list_cells_handler(self): self.assertTrue('data="1"' in result[0][2]) self.assertTrue('class="col-action_checkbox"' in result[0][0]) # Django 1.6 adds col-NAME class automatically - self.assertTrue('class="test"' in result[0][1] - or 'class="col-name test"' in result[0][1]) + self.assertTrue( + 'class="test"' in result[0][1] or 'class="col-name test"' in result[0][1] + ) self.assertTrue('class="col-order"' in result[0][2]) def test_suit_list_cells_handler_by_response(self): Book.objects.all().delete() for x in range(2): - book = Book(pk=x, name='sky-%s' % x) + book = Book(pk=x, name="sky-%s" % x) book.save() self.get_changelist() cl = self.changelist - results = result_list(cl)['results'] + results = result_list(cl)["results"] result_cells = cells_handler(results, cl) - self.assertTrue( - 'class="suit_cell_attr_class-name-sky-1' in result_cells[0][-1]) + self.assertTrue('class="suit_cell_attr_class-name-sky-1' in result_cells[0][-1]) self.assertTrue(' data="1"' in result_cells[0][-1]) diff --git a/suit/tests/templatetags/suit_menu.py b/suit/tests/templatetags/suit_menu.py index 544e68e7..918a7c42 100644 --- a/suit/tests/templatetags/suit_menu.py +++ b/suit/tests/templatetags/suit_menu.py @@ -1,22 +1,12 @@ from django.conf import settings from django.contrib.auth.models import Permission +from django.urls import reverse +from django.utils.encoding import force_str as force_unicode + from suit.templatetags.suit_menu import get_menu from suit.tests.mixins import ModelsTestCaseMixin, UserTestCaseMixin from suit.tests.models import test_app_label -try: - from django.core.urlresolvers import reverse -except ImportError: - # For Django >= 2.0 - from django.urls import reverse - - -# conditional import, force_unicode was renamed in Django 1.5 -try: - from django.utils.encoding import force_unicode -except ImportError: - from django.utils.encoding import force_text as force_unicode - app_label = test_app_label() @@ -26,234 +16,272 @@ def setUp(self): self.login_superuser() def setUpConfig(self): - settings.SUIT_CONFIG = getattr(settings, 'SUIT_CONFIG', {}) - settings.SUIT_CONFIG.update({ - 'MENU_OPEN_FIRST_CHILD': True, - 'MENU_ICONS': { - 'auth': 'icon-auth-assert', - }, - 'MENU_EXCLUDE': [], - 'MENU': [ - app_label, - {'app': app_label}, - {'app': app_label, 'label': 'Custom'}, - {'app': app_label, 'icon': 'icon-test-assert'}, - {'app': app_label, 'icon': ''}, - {'app': app_label, 'icon': None}, - {'app': 'auth'}, - '-', - {'label': 'Custom', 'url': '/custom/'}, - {'label': 'Custom2', 'url': '/custom2/', 'permissions': 'x'}, - {'label': 'Custom3', 'url': '/custom3/', 'permissions': ('y',)}, - {'label': 'Custom4', 'url': '/custom4/', 'blank': True}, - {'label': 'C4', 'url': '/c/4', 'models': ('book',)}, - {'label': 'C5', 'url': '/c/5', 'models': - ('%s.book' % app_label,)}, - {'label': 'C6', 'url': 'admin:index', 'models': - ({'label': 'mx', 'url': 'admin:index'},)}, - {'label': 'C7', 'url': '%s.book' % app_label}, - {'app': app_label, 'models': []}, - {'app': app_label, 'models': ['book', 'album']}, - {'app': app_label, 'models': ['%s.book' % app_label, - '%s.album' % app_label]}, - {'app': app_label, 'models': [ - 'book', '%s.book' % app_label, + settings.SUIT_CONFIG = getattr(settings, "SUIT_CONFIG", {}) + settings.SUIT_CONFIG.update( + { + "MENU_OPEN_FIRST_CHILD": True, + "MENU_ICONS": { + "auth": "icon-auth-assert", + }, + "MENU_EXCLUDE": [], + "MENU": [ + app_label, + {"app": app_label}, + {"app": app_label, "label": "Custom"}, + {"app": app_label, "icon": "icon-test-assert"}, + {"app": app_label, "icon": ""}, + {"app": app_label, "icon": None}, + {"app": "auth"}, + "-", + {"label": "Custom", "url": "/custom/"}, + {"label": "Custom2", "url": "/custom2/", "permissions": "x"}, + {"label": "Custom3", "url": "/custom3/", "permissions": ("y",)}, + {"label": "Custom4", "url": "/custom4/", "blank": True}, + {"label": "C4", "url": "/c/4", "models": ("book",)}, + {"label": "C5", "url": "/c/5", "models": ("%s.book" % app_label,)}, + { + "label": "C6", + "url": "admin:index", + "models": ({"label": "mx", "url": "admin:index"},), + }, + {"label": "C7", "url": "%s.book" % app_label}, + {"app": app_label, "models": []}, + {"app": app_label, "models": ["book", "album"]}, { - 'model': '%s.album' % app_label, - 'label': 'Albumzzz', - 'url': '/albumzzz/', - }, { - 'label': 'CustModel', - 'url': '/cust-mod/', - 'permissions': 'z' - }]}, - ] - }) + "app": app_label, + "models": ["%s.book" % app_label, "%s.album" % app_label], + }, + { + "app": app_label, + "models": [ + "book", + "%s.book" % app_label, + { + "model": "%s.album" % app_label, + "label": "Albumzzz", + "url": "/albumzzz/", + }, + { + "label": "CustModel", + "url": "/cust-mod/", + "permissions": "z", + }, + ], + }, + ], + } + ) def setUpOldConfig(self): - settings.SUIT_CONFIG.update({ - 'MENU_OPEN_FIRST_CHILD': False, - 'MENU_ICONS': { - app_label: 'icon-fire icon-test-against-keyword', - }, - 'MENU_ORDER': ( - (app_label, ('book',)), - (('Custom app name', '/custom-url-test/', 'icon-custom-app'), ( - ('Custom link', '/admin/custom/', - '%s.add_book' % app_label), - ('Check out error 404', '/admin/non-existant/', - ('mega-perms',)), - '%s.album' % app_label - )), - (('Custom app no models', '/custom-app-no-models', - '', 'mega-rights'),), - (('Custom app no models tuple perms', '/custom-app-tuple-perms', - '', ('mega-rights',)),), - ), - 'MENU_EXCLUDE': [] - }) - del settings.SUIT_CONFIG['MENU'] + settings.SUIT_CONFIG.update( + { + "MENU_OPEN_FIRST_CHILD": False, + "MENU_ICONS": { + app_label: "icon-fire icon-test-against-keyword", + }, + "MENU_ORDER": ( + (app_label, ("book",)), + ( + ("Custom app name", "/custom-url-test/", "icon-custom-app"), + ( + ( + "Custom link", + "/admin/custom/", + "%s.add_book" % app_label, + ), + ( + "Check out error 404", + "/admin/non-existant/", + ("mega-perms",), + ), + "%s.album" % app_label, + ), + ), + ( + ( + "Custom app no models", + "/custom-app-no-models", + "", + "mega-rights", + ), + ), + ( + ( + "Custom app no models tuple perms", + "/custom-app-tuple-perms", + "", + ("mega-rights",), + ), + ), + ), + "MENU_EXCLUDE": [], + } + ) + del settings.SUIT_CONFIG["MENU"] def make_menu_from_response(self): return get_menu(self.response.context[-1], self.response._request) def test_menu_search_url_formats(self): # Test named url as defined in setUp config - settings.SUIT_CONFIG['SEARCH_URL'] = 'admin:%s_book_changelist' \ - % app_label - admin_root = reverse('admin:index') + settings.SUIT_CONFIG["SEARCH_URL"] = "admin:%s_book_changelist" % app_label + admin_root = reverse("admin:index") self.get_response() - self.assertContains(self.response, - 'action="%s%s/book/"' % (admin_root, app_label)) + self.assertContains( + self.response, 'action="%s%s/book/"' % (admin_root, app_label) + ) # Test absolute url - absolute_search_url = '/absolute/search/url' - settings.SUIT_CONFIG['SEARCH_URL'] = absolute_search_url + absolute_search_url = "/absolute/search/url" + settings.SUIT_CONFIG["SEARCH_URL"] = absolute_search_url self.get_response() self.assertContains(self.response, absolute_search_url) def test_menu(self): - mc = settings.SUIT_CONFIG['MENU'] + mc = settings.SUIT_CONFIG["MENU"] self.get_response() menu = self.make_menu_from_response() self.assertEqual(len(menu), len(mc)) # as string i = 0 - first_model_url = reverse('admin:%s_album_changelist' % app_label) - self.assertEqual(menu[i]['url'], first_model_url) - self.assertEqual(len(menu[i]['models']), 3) - self.assertEqual(menu[i]['name'], mc[i]) - self.assertEqual(menu[i]['label'], app_label.title()) - self.assertEqual(menu[i]['icon'], None) - self.assertEqual(menu[i]['models'][0]['url'], first_model_url) - self.assertEqual(force_unicode(menu[0]['models'][0]['label']), 'Albums') - - i += 1 # as dict - self.assertEqual(menu[i]['url'], first_model_url) - self.assertEqual(len(menu[i]['models']), 3) - - i += 1 # with label - self.assertEqual(menu[i]['label'], mc[i]['label']) - - i += 1 # with icon - self.assertEqual(menu[i]['icon'], mc[i]['icon']) - - i += 1 # with icon='' - self.assertEqual(menu[i]['icon'], 'icon-') - - i += 1 # with is is None - self.assertEqual(menu[i]['icon'], 'icon-') - - i += 1 # icon from SUIT_ICONS - self.assertEqual(menu[i]['icon'], 'icon-auth-assert') - - i += 1 # separator - self.assertEqual(menu[i]['separator'], True) - - i += 1 # custom app - self.assertEqual(menu[i]['label'], mc[i]['label']) - self.assertEqual(menu[i]['url'], mc[i]['url']) - - i += 1 # custom app, with perms as string - self.assertEqual(menu[i]['label'], mc[i]['label']) - - i += 1 # custom app, with perms as tuple - self.assertEqual(menu[i]['label'], mc[i]['label']) - - i += 1 # custom app, with perms as tuple - self.assertEqual(menu[i]['blank'], True) - - i += 1 # custom app with wrong model - self.assertEqual(menu[i]['label'], mc[i]['label']) - self.assertEqual(menu[i]['models'], []) - self.assertEqual(menu[i]['url'], mc[i]['url']) - - i += 1 # custom app with correct model - first_model_url = reverse('admin:%s_book_changelist' % app_label) - self.assertEqual(menu[i]['label'], mc[i]['label']) - self.assertEqual(len(menu[i]['models']), 1) - self.assertEqual(menu[i]['url'], first_model_url) - - i += 1 # custom app and model with named urls - expected_url = reverse('admin:index') - self.assertEqual(menu[i]['url'], expected_url) - self.assertEqual(menu[i]['models'][0]['url'], expected_url) - - i += 1 # with url by model - books_url = reverse('admin:%s_book_changelist' % app_label) - self.assertEqual(menu[i]['url'], books_url) - - i += 1 # with empty models - self.assertEqual(menu[i]['models'], []) - self.assertEqual(menu[i]['url'], - reverse('admin:app_list', args=[mc[i]['app']])) - - i += 1 # with ordered models - first_model_url = reverse('admin:%s_book_changelist' % app_label) - self.assertEqual(menu[i]['models'][0]['url'], first_model_url) - self.assertEqual(len(menu[i]['models']), 2) - - i += 1 # with prefixed models - first_model_url = reverse('admin:%s_book_changelist' % app_label) - self.assertEqual(menu[i]['models'][0]['url'], first_model_url) - self.assertEqual(len(menu[i]['models']), 2) - - i += 1 # with dict models - first_model_url = reverse('admin:%s_book_changelist' % app_label) - self.assertEqual(menu[i]['models'][0]['url'], first_model_url) - self.assertEqual(len(menu[i]['models']), 4) - self.assertEqual(force_unicode(menu[i]['models'][2]['label']), - mc[i]['models'][2]['label']) - self.assertEqual(force_unicode(menu[i]['models'][2]['url']), - mc[i]['models'][2]['url']) - self.assertEqual(force_unicode(menu[i]['models'][3]['label']), - mc[i]['models'][3]['label']) - self.assertEqual(force_unicode(menu[i]['models'][3]['url']), - mc[i]['models'][3]['url']) - + first_model_url = reverse("admin:%s_album_changelist" % app_label) + self.assertEqual(menu[i]["url"], first_model_url) + self.assertEqual(len(menu[i]["models"]), 3) + self.assertEqual(menu[i]["name"], mc[i]) + self.assertEqual(menu[i]["label"], app_label.title()) + self.assertEqual(menu[i]["icon"], None) + self.assertEqual(menu[i]["models"][0]["url"], first_model_url) + self.assertEqual(force_unicode(menu[0]["models"][0]["label"]), "Albums") + + i += 1 # as dict + self.assertEqual(menu[i]["url"], first_model_url) + self.assertEqual(len(menu[i]["models"]), 3) + + i += 1 # with label + self.assertEqual(menu[i]["label"], mc[i]["label"]) + + i += 1 # with icon + self.assertEqual(menu[i]["icon"], mc[i]["icon"]) + + i += 1 # with icon='' + self.assertEqual(menu[i]["icon"], "icon-") + + i += 1 # with is is None + self.assertEqual(menu[i]["icon"], "icon-") + + i += 1 # icon from SUIT_ICONS + self.assertEqual(menu[i]["icon"], "icon-auth-assert") + + i += 1 # separator + self.assertEqual(menu[i]["separator"], True) + + i += 1 # custom app + self.assertEqual(menu[i]["label"], mc[i]["label"]) + self.assertEqual(menu[i]["url"], mc[i]["url"]) + + i += 1 # custom app, with perms as string + self.assertEqual(menu[i]["label"], mc[i]["label"]) + + i += 1 # custom app, with perms as tuple + self.assertEqual(menu[i]["label"], mc[i]["label"]) + + i += 1 # custom app, with perms as tuple + self.assertEqual(menu[i]["blank"], True) + + i += 1 # custom app with wrong model + self.assertEqual(menu[i]["label"], mc[i]["label"]) + self.assertEqual(menu[i]["models"], []) + self.assertEqual(menu[i]["url"], mc[i]["url"]) + + i += 1 # custom app with correct model + first_model_url = reverse("admin:%s_book_changelist" % app_label) + self.assertEqual(menu[i]["label"], mc[i]["label"]) + self.assertEqual(len(menu[i]["models"]), 1) + self.assertEqual(menu[i]["url"], first_model_url) + + i += 1 # custom app and model with named urls + expected_url = reverse("admin:index") + self.assertEqual(menu[i]["url"], expected_url) + self.assertEqual(menu[i]["models"][0]["url"], expected_url) + + i += 1 # with url by model + books_url = reverse("admin:%s_book_changelist" % app_label) + self.assertEqual(menu[i]["url"], books_url) + + i += 1 # with empty models + self.assertEqual(menu[i]["models"], []) + self.assertEqual(menu[i]["url"], reverse("admin:app_list", args=[mc[i]["app"]])) + + i += 1 # with ordered models + first_model_url = reverse("admin:%s_book_changelist" % app_label) + self.assertEqual(menu[i]["models"][0]["url"], first_model_url) + self.assertEqual(len(menu[i]["models"]), 2) + + i += 1 # with prefixed models + first_model_url = reverse("admin:%s_book_changelist" % app_label) + self.assertEqual(menu[i]["models"][0]["url"], first_model_url) + self.assertEqual(len(menu[i]["models"]), 2) + + i += 1 # with dict models + first_model_url = reverse("admin:%s_book_changelist" % app_label) + self.assertEqual(menu[i]["models"][0]["url"], first_model_url) + self.assertEqual(len(menu[i]["models"]), 4) + self.assertEqual( + force_unicode(menu[i]["models"][2]["label"]), mc[i]["models"][2]["label"] + ) + self.assertEqual( + force_unicode(menu[i]["models"][2]["url"]), mc[i]["models"][2]["url"] + ) + self.assertEqual( + force_unicode(menu[i]["models"][3]["label"]), mc[i]["models"][3]["label"] + ) + self.assertEqual( + force_unicode(menu[i]["models"][3]["url"]), mc[i]["models"][3]["url"] + ) def test_menu_app_exclude(self): - settings.SUIT_CONFIG['MENU'] = ({'app': app_label, 'models': ['book']}, - {'app': 'auth'}, 'auth') - settings.SUIT_CONFIG['MENU_EXCLUDE'] = ('auth', '%s.book' % app_label) + settings.SUIT_CONFIG["MENU"] = ( + {"app": app_label, "models": ["book"]}, + {"app": "auth"}, + "auth", + ) + settings.SUIT_CONFIG["MENU_EXCLUDE"] = ("auth", "%s.book" % app_label) self.get_response() menu = self.make_menu_from_response() self.assertEqual(len(menu), 1) - self.assertEqual(menu[0]['models'], []) + self.assertEqual(menu[0]["models"], []) def test_menu_model_exclude_with_string_app(self): - settings.SUIT_CONFIG['MENU'] = (app_label,) - settings.SUIT_CONFIG['MENU_EXCLUDE'] = ('%s.book' % app_label,) + settings.SUIT_CONFIG["MENU"] = (app_label,) + settings.SUIT_CONFIG["MENU_EXCLUDE"] = ("%s.book" % app_label,) self.get_response() menu = self.make_menu_from_response() self.assertEqual(len(menu), 1) - self.assertEqual(len(menu[0]['models']), 2) + self.assertEqual(len(menu[0]["models"]), 2) def test_menu_custom_app(self): - label = 'custom' - icon = 'icon-custom' - settings.SUIT_CONFIG['MENU'] = ({'label': label, 'icon': icon},) + label = "custom" + icon = "icon-custom" + settings.SUIT_CONFIG["MENU"] = ({"label": label, "icon": icon},) self.get_response() menu = self.make_menu_from_response() self.assertEqual(len(menu), 1) - self.assertEqual(menu[0]['label'], label) - self.assertEqual(menu[0]['icon'], icon) + self.assertEqual(menu[0]["label"], label) + self.assertEqual(menu[0]["icon"], icon) def test_menu_custom_app_permissions(self): - settings.SUIT_CONFIG['MENU'] = ({'label': 'a', - 'permissions': 'secure-perms'}, - {'label': 'b', - 'permissions': ('secure-perms',)}, - {'label': 'c', 'models': [ - {'label': 'model1', - 'permissions': 'x'}]},) + settings.SUIT_CONFIG["MENU"] = ( + {"label": "a", "permissions": "secure-perms"}, + {"label": "b", "permissions": ("secure-perms",)}, + {"label": "c", "models": [{"label": "model1", "permissions": "x"}]}, + ) self.client.logout() self.login_user() self.get_response() menu = self.make_menu_from_response() self.assertEqual(len(menu), 1) - self.assertEqual(len(menu[0]['models']), 0) + self.assertEqual(len(menu[0]["models"]), 0) # Now do the same with super user self.client.logout() @@ -261,59 +289,60 @@ def test_menu_custom_app_permissions(self): self.get_response() menu = self.make_menu_from_response() self.assertEqual(len(menu), 3) - self.assertEqual(len(menu[2]['models']), 1) + self.assertEqual(len(menu[2]["models"]), 1) def test_menu_app_marked_as_active(self): - self.get_response(reverse('admin:app_list', args=[app_label])) + self.get_response(reverse("admin:app_list", args=[app_label])) self.assertContains(self.response, '
  • ') menu = self.make_menu_from_response() - self.assertTrue(menu[0]['is_active']) + self.assertTrue(menu[0]["is_active"]) def test_menu_app_marked_as_active_model_link(self): - settings.SUIT_CONFIG['MENU'] = ( - {'label': '%s-user' % app_label, 'url': '%s.user' % app_label}, - {'label': 'auth-user', 'url': 'auth.user'}, + settings.SUIT_CONFIG["MENU"] = ( + {"label": "%s-user" % app_label, "url": "%s.user" % app_label}, + {"label": "auth-user", "url": "auth.user"}, ) - self.get_response(reverse('admin:auth_user_add')) + self.get_response(reverse("admin:auth_user_add")) self.assertContains(self.response, '
  • ') # Test if right user model is activated, when models have identical name menu = self.make_menu_from_response() - self.assertFalse(menu[0]['is_active']) - self.assertTrue(menu[1]['is_active']) + self.assertFalse(menu[0]["is_active"]) + self.assertTrue(menu[1]["is_active"]) def test_menu_model_marked_as_active(self): - self.get_response(reverse('admin:%s_album_changelist' % app_label)) + self.get_response(reverse("admin:%s_album_changelist" % app_label)) menu = self.make_menu_from_response() - self.assertTrue(menu[0]['is_active']) - self.assertTrue(menu[0]['models'][0]['is_active']) + self.assertTrue(menu[0]["is_active"]) + self.assertTrue(menu[0]["models"][0]["is_active"]) def test_only_native_apps(self): - del settings.SUIT_CONFIG['MENU'] - if 'MENU_ORDER' in settings.SUIT_CONFIG: - del settings.SUIT_CONFIG['MENU_ORDER'] - icon = 'icon-auth-assert' - settings.SUIT_CONFIG['MENU_ICONS'] = {'auth': icon} + del settings.SUIT_CONFIG["MENU"] + if "MENU_ORDER" in settings.SUIT_CONFIG: + del settings.SUIT_CONFIG["MENU_ORDER"] + icon = "icon-auth-assert" + settings.SUIT_CONFIG["MENU_ICONS"] = {"auth": icon} self.get_response() menu = self.make_menu_from_response() self.assertEqual(len(menu), 4) - self.assertEqual(menu[0]['icon'], icon) + self.assertEqual(menu[0]["icon"], icon) def test_user_with_add_but_not_change(self): - settings.SUIT_CONFIG['MENU'] = ({'app': app_label, - 'models': ['book']}, - {'app': 'auth'}, 'auth') - settings.SUIT_CONFIG['MENU_EXCLUDE'] = () + settings.SUIT_CONFIG["MENU"] = ( + {"app": app_label, "models": ["book"]}, + {"app": "auth"}, + "auth", + ) + settings.SUIT_CONFIG["MENU_EXCLUDE"] = () self.client.logout() self.login_user() - self.user.user_permissions.add( - Permission.objects.get(codename='add_book')) + self.user.user_permissions.add(Permission.objects.get(codename="add_book")) self.user.save() self.get_response() menu = self.make_menu_from_response() - add_book_url = reverse('admin:%s_book_add' % app_label) - self.assertEqual(menu[0]['url'], add_book_url) - self.assertEqual(menu[0]['models'][0]['url'], add_book_url) + add_book_url = reverse("admin:%s_book_add" % app_label) + self.assertEqual(menu[0]["url"], add_book_url) + self.assertEqual(menu[0]["models"][0]["url"], add_book_url) # # Tests for old menu config format @@ -324,10 +353,10 @@ def test_old_menu_init(self): self.login_superuser() self.setUpOldConfig() self.get_response() - self.assertTemplateUsed(self.response, 'suit/menu.html') - self.assertContains(self.response, 'left-nav') - self.assertContains(self.response, 'icon-test-against-keyword') - app_list = self.response.context_data['app_list'] + self.assertTemplateUsed(self.response, "suit/menu.html") + self.assertContains(self.response, "left-nav") + self.assertContains(self.response, "icon-test-against-keyword") + app_list = self.response.context_data["app_list"] pass # print self.response.content @@ -335,7 +364,7 @@ def test_old_menu_custom_app_and_models(self): # Test custom app name, url and icon self.setUpOldConfig() self.get_response() - menu_order = settings.SUIT_CONFIG['MENU_ORDER'] + menu_order = settings.SUIT_CONFIG["MENU_ORDER"] self.assertContains(self.response, menu_order[1][0][0]) self.assertContains(self.response, menu_order[1][0][1]) self.assertContains(self.response, menu_order[1][0][2]) @@ -346,14 +375,14 @@ def test_old_menu_custom_app_and_models(self): # Test custom app when perms defined but is allowed self.assertContains(self.response, menu_order[2][0][0]) # Test cross-linked app - self.assertContains(self.response, '%s/album' % app_label) + self.assertContains(self.response, "%s/album" % app_label) def test_old_menu_when_open_first_child_is_true(self): # Test custom app name, url and icon self.setUpOldConfig() - settings.SUIT_CONFIG['MENU_OPEN_FIRST_CHILD'] = True + settings.SUIT_CONFIG["MENU_OPEN_FIRST_CHILD"] = True self.get_response() - menu_order = settings.SUIT_CONFIG['MENU_ORDER'] + menu_order = settings.SUIT_CONFIG["MENU_ORDER"] self.assertNotContains(self.response, menu_order[1][0][1]) def test_old_custom_menu_permissions(self): @@ -362,9 +391,9 @@ def test_old_custom_menu_permissions(self): self.setUpOldConfig() self.get_response() # Test for menu at all for simple user - self.assertTemplateUsed(self.response, 'suit/menu.html') - self.assertContains(self.response, 'left-nav') - menu_order = settings.SUIT_CONFIG['MENU_ORDER'] + self.assertTemplateUsed(self.response, "suit/menu.html") + self.assertContains(self.response, "left-nav") + menu_order = settings.SUIT_CONFIG["MENU_ORDER"] # Test custom model when perms defined as string self.assertNotContains(self.response, menu_order[1][1][0][0]) # Test custom model when perms defined as tuple @@ -376,17 +405,17 @@ def test_old_custom_menu_permissions(self): def test_old_menu_marked_as_active(self): self.setUpOldConfig() - self.get_response(reverse('admin:app_list', args=[app_label])) + self.get_response(reverse("admin:app_list", args=[app_label])) self.assertContains(self.response, '
  • ') class SuitMenuAdminRootURLTestCase(SuitMenuTestCase): - urls = 'suit.tests.urls.admin_at_root' + urls = "suit.tests.urls.admin_at_root" class SuitMenuAdminI18NURLTestCase(SuitMenuTestCase): - urls = 'suit.tests.urls.admin_i18n' + urls = "suit.tests.urls.admin_i18n" class SuitMenuAdminCustomURLTestCase(SuitMenuTestCase): - urls = 'suit.tests.urls.admin_custom' + urls = "suit.tests.urls.admin_custom" diff --git a/suit/tests/templatetags/suit_tags.py b/suit/tests/templatetags/suit_tags.py index 737b79b2..7aae3a62 100644 --- a/suit/tests/templatetags/suit_tags.py +++ b/suit/tests/templatetags/suit_tags.py @@ -1,16 +1,23 @@ import datetime + from django.conf import settings -from django.test import TestCase -from django.utils.encoding import python_2_unicode_compatible -from suit import utils -from suit.templatetags.suit_tags import suit_conf, suit_date, suit_time, \ - admin_url, field_contents_foreign_linked, suit_bc, suit_bc_value -from django.db import models from django.contrib import admin from django.contrib.admin.helpers import AdminReadonlyField +from django.db import models +from django.test import TestCase + +from suit import utils +from suit.templatetags.suit_tags import ( + admin_url, + field_contents_foreign_linked, + suit_bc, + suit_bc_value, + suit_conf, + suit_date, + suit_time, +) -@python_2_unicode_compatible class Country(models.Model): name = models.CharField(max_length=64) @@ -18,7 +25,6 @@ def __str__(self): return self.name -@python_2_unicode_compatible class City(models.Model): name = models.CharField(max_length=64) country = models.ForeignKey(Country, on_delete=models.CASCADE) @@ -27,13 +33,14 @@ def __str__(self): return self.name +@admin.register(City) class CityAdmin(admin.ModelAdmin): - readonly_fields = ('country',) - pass + model = City + list_display = ("name", "country") + readonly_fields = ("country",) admin.site.register(Country) -admin.site.register(City, CityAdmin) class SuitTagsTestCase(TestCase): @@ -42,61 +49,57 @@ class SuitTagsTestCase(TestCase): """ def test_suit_config_string(self): - admin_name = 'Custom Name' - settings.SUIT_CONFIG = { - 'ADMIN_NAME': admin_name - } - value = suit_conf('ADMIN_NAME') + admin_name = "Custom Name" + settings.SUIT_CONFIG = {"ADMIN_NAME": admin_name} + value = suit_conf("ADMIN_NAME") self.assertEqual(value, admin_name) - self.assertTrue('Safe' in value.__class__.__name__) + self.assertTrue("Safe" in value.__class__.__name__) def test_suit_config_mark_safe(self): list = (1, 2, 3) - settings.SUIT_CONFIG = { - 'SOME_LIST': list - } - value = suit_conf('SOME_LIST') + settings.SUIT_CONFIG = {"SOME_LIST": list} + value = suit_conf("SOME_LIST") self.assertEqual(value, list) - self.assertEqual(value.__class__.__name__, 'tuple') + self.assertEqual(value.__class__.__name__, "tuple") def test_suit_date_and_time(self): settings.SUIT_CONFIG = { - 'HEADER_DATE_FORMAT': 'Y-m-d', - 'HEADER_TIME_FORMAT': 'H:i', + "HEADER_DATE_FORMAT": "Y-m-d", + "HEADER_TIME_FORMAT": "H:i", } - self.assertEqual(datetime.datetime.now().strftime('%Y-%m-%d'), - suit_date({}, {}).render({})) - self.assertEqual(datetime.datetime.now().strftime('%H:%M'), - suit_time({}, {}).render({})) + self.assertEqual( + datetime.datetime.now().strftime("%Y-%m-%d"), suit_date({}, {}).render({}) + ) + self.assertEqual( + datetime.datetime.now().strftime("%H:%M"), suit_time({}, {}).render({}) + ) def test_admin_url(self): - country = Country(pk=1, name='USA') - assert '/country/1' in admin_url(country) + country = Country(pk=1, name="USA") + assert "/country/1" in admin_url(country) pass def test_field_contents_foreign_linked(self): - country = Country(pk=1, name='France') - city = City(pk=1, name='Paris', country=country) + country = Country(pk=1, name="France") + city = City(pk=1, name="Paris", country=country) ma = CityAdmin(City, admin.site) # Create form request = None - form = ma.get_form(request, city) - form.instance = city - ro_field = AdminReadonlyField(form, 'country', True, ma) + form = ma.get_form(request, fields=None)(instance=city) + ro_field = AdminReadonlyField(form, "country", True, ma) - self.assertEqual(country.name, - field_contents_foreign_linked(ro_field)) + self.assertInHTML(country.name, field_contents_foreign_linked(ro_field)) # Now it should return as link - ro_field.model_admin.linked_readonly_fields = ('country',) - assert admin_url(country) in field_contents_foreign_linked(ro_field) + ro_field.model_admin.linked_readonly_fields = ("country",) + self.assertIn(admin_url(country), field_contents_foreign_linked(ro_field)) def test_suit_bc(self): - args = [utils.django_major_version(), 'a'] + args = [utils.django_major_version(), "a"] self.assertEqual(utils.value_by_version(args), suit_bc(*args)) def test_suit_bc_value(self): - args = [utils.django_major_version(), 'a'] + args = [utils.django_major_version(), "a"] self.assertEqual(utils.value_by_version(args), suit_bc_value(*args)) diff --git a/suit/tests/urls/__init__.py b/suit/tests/urls/__init__.py index 1cf95b07..625f5cb3 100644 --- a/suit/tests/urls/__init__.py +++ b/suit/tests/urls/__init__.py @@ -1,25 +1,9 @@ import django -from django.conf.urls import include, url from django.contrib import admin +from django.urls import re_path admin.autodiscover() -try: - # Django 2.0+ - from django.urls import re_path - - urlpatterns = [ - re_path(r'^admin/', admin.site.urls), - ] -except ImportError: - try: - from django.conf.urls import patterns - urlpatterns = patterns( - '', - # Examples for custom menu - url(r'^admin/', include(admin.site.urls)), - ) - except ImportError: # Django 1.10+ - urlpatterns = [ - url(r'^admin/', include(admin.site.urls)), - ] +urlpatterns = [ + re_path(r"^admin/", admin.site.urls), +] diff --git a/suit/tests/urls/admin_at_root.py b/suit/tests/urls/admin_at_root.py index e05a555b..f91d7974 100644 --- a/suit/tests/urls/admin_at_root.py +++ b/suit/tests/urls/admin_at_root.py @@ -1,9 +1,11 @@ -from django.conf.urls import patterns, include, url +from django.conf.urls import patterns from django.contrib import admin +from django.urls import include, path admin.autodiscover() -urlpatterns = patterns('', +urlpatterns = patterns( + "", # Examples for custom menu - url(r'^', include(admin.site.urls)), + path("", include(admin.site.urls)), ) diff --git a/suit/tests/urls/admin_custom.py b/suit/tests/urls/admin_custom.py index 21f4f627..0e477026 100644 --- a/suit/tests/urls/admin_custom.py +++ b/suit/tests/urls/admin_custom.py @@ -1,9 +1,11 @@ -from django.conf.urls import patterns, include, url +from django.conf.urls import patterns from django.contrib import admin +from django.urls import include, path admin.autodiscover() -urlpatterns = patterns('', +urlpatterns = patterns( + "", # Examples for custom menu - url(r'^foo/bar/', include(admin.site.urls)), + path("foo/bar/", include(admin.site.urls)), ) diff --git a/suit/tests/urls/admin_i18n.py b/suit/tests/urls/admin_i18n.py index 402699b0..f349f349 100644 --- a/suit/tests/urls/admin_i18n.py +++ b/suit/tests/urls/admin_i18n.py @@ -1,10 +1,11 @@ -from django.conf.urls import include, url from django.conf.urls.i18n import i18n_patterns from django.contrib import admin +from django.urls import include, path admin.autodiscover() -urlpatterns = i18n_patterns('', +urlpatterns = i18n_patterns( + "", # Examples for custom menu - url(r'^admin/', include(admin.site.urls)), + path("admin/", include(admin.site.urls)), ) diff --git a/suit/tests/widgets.py b/suit/tests/widgets.py index e901105b..d3b96ec7 100644 --- a/suit/tests/widgets.py +++ b/suit/tests/widgets.py @@ -1,10 +1,18 @@ -from django.test import TestCase -from suit.widgets import LinkedSelect, HTML5Input, EnclosedInput, \ - NumberInput, SuitDateWidget, SuitTimeWidget, SuitSplitDateTimeWidget, \ - AutosizedTextarea -from django.utils.translation import ugettext as _ from django.templatetags.static import static +from django.test import TestCase +from django.utils.translation import gettext as _ + from suit import utils +from suit.widgets import ( + AutosizedTextarea, + EnclosedInput, + HTML5Input, + LinkedSelect, + NumberInput, + SuitDateWidget, + SuitSplitDateTimeWidget, + SuitTimeWidget, +) django_version = utils.django_major_version() @@ -12,156 +20,168 @@ class WidgetsTestCase(TestCase): def test_NumberInput(self): inp = NumberInput() - self.assertEqual('number', inp.input_type) + self.assertEqual("number", inp.input_type) def test_HTML5Input(self): - input_type = 'calendar' + input_type = "calendar" inp = HTML5Input(input_type=input_type) self.assertEqual(input_type, inp.input_type) def test_LinkedSelect(self): ls = LinkedSelect() - self.assertTrue('linked-select' in ls.attrs['class']) + self.assertTrue("linked-select" in ls.attrs["class"]) def test_LinkedSelect_with_existing_attr(self): - ls = LinkedSelect(attrs={'class': 'custom-class', 'custom': 123}) - self.assertEqual('linked-select custom-class', ls.attrs['class']) - self.assertEqual(ls.attrs['custom'], 123) + ls = LinkedSelect(attrs={"class": "custom-class", "custom": 123}) + self.assertEqual("linked-select custom-class", ls.attrs["class"]) + self.assertEqual(ls.attrs["custom"], 123) def render_enclosed_widget(self, enclosed_widget): - return enclosed_widget.render('enc', 123) + return enclosed_widget.render("enc", 123) def get_enclosed_widget_html(self, values): - return '
    %s%s
    ' % values + return ( + '
    %s%s
    ' % values + ) def test_EnclosedInput_as_text(self): - inp = EnclosedInput(prepend='p', append='a') + inp = EnclosedInput(prepend="p", append="a") output = self.render_enclosed_widget(inp) - result = ('p', - 'a') + result = ('p', 'a') self.assertHTMLEqual(output, self.get_enclosed_widget_html(result)) def test_EnclosedInput_as_icon(self): - inp = EnclosedInput(prepend='icon-fire', append='icon-leaf') + inp = EnclosedInput(prepend="icon-fire", append="icon-leaf") output = self.render_enclosed_widget(inp) - result = ('', - '') + result = ( + '', + '', + ) self.assertHTMLEqual(output, self.get_enclosed_widget_html(result)) def test_EnclosedInput_as_html(self): - inp = EnclosedInput(prepend='p', append='a') + inp = EnclosedInput(prepend="p", append="a") output = self.render_enclosed_widget(inp) - result = ('p', 'a') + result = ("p", "a") self.assertHTMLEqual(output, self.get_enclosed_widget_html(result)) def test_SuitDateWidget(self): sdw = SuitDateWidget() - self.assertTrue('vDateField' in sdw.attrs['class']) + self.assertTrue("vDateField" in sdw.attrs["class"]) def test_SuitDateWidget_with_existing_class_attr(self): - sdw = SuitDateWidget(attrs={'class': 'custom-class'}) - self.assertTrue('vDateField ' in sdw.attrs['class']) - self.assertTrue(' custom-class' in sdw.attrs['class']) - self.assertEqual(_('Date:')[:-1], sdw.attrs['placeholder']) + sdw = SuitDateWidget(attrs={"class": "custom-class"}) + self.assertTrue("vDateField " in sdw.attrs["class"]) + self.assertTrue(" custom-class" in sdw.attrs["class"]) + self.assertEqual(_("Date:")[:-1], sdw.attrs["placeholder"]) def test_SuitDateWidget_with_existing_placeholder_attr(self): - sdw = SuitDateWidget(attrs={'class': 'custom-cls', 'placeholder': 'p'}) - self.assertTrue('vDateField ' in sdw.attrs['class']) - self.assertTrue(' custom-cls' in sdw.attrs['class']) - self.assertEqual('p', sdw.attrs['placeholder']) + sdw = SuitDateWidget(attrs={"class": "custom-cls", "placeholder": "p"}) + self.assertTrue("vDateField " in sdw.attrs["class"]) + self.assertTrue(" custom-cls" in sdw.attrs["class"]) + self.assertEqual("p", sdw.attrs["placeholder"]) def get_SuitDateWidget_output(self): if django_version < (1, 11): - return '
    ' + return ( + '
    ' + ) else: - return '
    ' \ - '
    ' + return ( + '
    ' + '
    ' + ) def test_SuitDateWidget_output(self): - sdw = SuitDateWidget(attrs={'placeholder': 'Date'}) - output = sdw.render('sdw', '') - self.assertHTMLEqual( - self.get_SuitDateWidget_output(), output) + sdw = SuitDateWidget(attrs={"placeholder": "Date"}) + output = sdw.render("sdw", "") + self.assertHTMLEqual(self.get_SuitDateWidget_output(), output) def test_SuitTimeWidget(self): sdw = SuitTimeWidget() - self.assertTrue('vTimeField' in sdw.attrs['class']) + self.assertTrue("vTimeField" in sdw.attrs["class"]) def test_SuitTimeWidget_with_existing_class_attr(self): - sdw = SuitTimeWidget(attrs={'class': 'custom-class'}) - self.assertTrue('vTimeField ' in sdw.attrs['class']) - self.assertTrue(' custom-class' in sdw.attrs['class']) - self.assertEqual(_('Time:')[:-1], sdw.attrs['placeholder']) + sdw = SuitTimeWidget(attrs={"class": "custom-class"}) + self.assertTrue("vTimeField " in sdw.attrs["class"]) + self.assertTrue(" custom-class" in sdw.attrs["class"]) + self.assertEqual(_("Time:")[:-1], sdw.attrs["placeholder"]) def test_SuitTimeWidget_with_existing_placeholder_attr(self): - sdw = SuitTimeWidget(attrs={'class': 'custom-cls', 'placeholder': 'p'}) - self.assertTrue('vTimeField ' in sdw.attrs['class']) - self.assertTrue(' custom-cls' in sdw.attrs['class']) - self.assertEqual('p', sdw.attrs['placeholder']) + sdw = SuitTimeWidget(attrs={"class": "custom-cls", "placeholder": "p"}) + self.assertTrue("vTimeField " in sdw.attrs["class"]) + self.assertTrue(" custom-cls" in sdw.attrs["class"]) + self.assertEqual("p", sdw.attrs["placeholder"]) def get_SuitTimeWidget_output(self): if django_version < (1, 11): - return '
    ' + return ( + '
    ' + ) else: - return '
    ' \ - '
    ' + return ( + '
    ' + '
    ' + ) def test_SuitTimeWidget_output(self): - sdw = SuitTimeWidget(attrs={'placeholder': 'Time'}) - output = sdw.render('sdw', '') - self.assertHTMLEqual( - self.get_SuitTimeWidget_output(), - output) + sdw = SuitTimeWidget(attrs={"placeholder": "Time"}) + output = sdw.render("sdw", "") + self.assertHTMLEqual(self.get_SuitTimeWidget_output(), output) def get_SuitSplitDateTimeWidget_output(self): if django_version < (1, 11): - dwo = self.get_SuitDateWidget_output().replace('sdw', 'sdw_0') - two = self.get_SuitTimeWidget_output().replace('sdw', 'sdw_1') + dwo = self.get_SuitDateWidget_output().replace("sdw", "sdw_0") + two = self.get_SuitTimeWidget_output().replace("sdw", "sdw_1") return '
    %s %s
    ' % (dwo, two) else: - return '
    ' + return ( + '
    ' + ) def test_SuitSplitDateTimeWidget(self): ssdtw = SuitSplitDateTimeWidget() - output = ssdtw.render('sdw', '') - self.assertHTMLEqual( - self.get_SuitSplitDateTimeWidget_output(), - output) + output = ssdtw.render("sdw", "") + self.assertHTMLEqual(self.get_SuitSplitDateTimeWidget_output(), output) def test_AutosizedTextarea(self): txt = AutosizedTextarea() - self.assertTrue('autosize' in txt.attrs['class']) - self.assertEqual(2, txt.attrs['rows']) + self.assertTrue("autosize" in txt.attrs["class"]) + self.assertEqual(2, txt.attrs["rows"]) def test_AutosizedTextarea_with_existing_attrs(self): - txt = AutosizedTextarea(attrs={'class': 'custom-class', 'rows': 3}) - self.assertTrue('autosize ' in txt.attrs['class']) - self.assertTrue(' custom-class' in txt.attrs['class']) - self.assertEqual(txt.attrs['rows'], 3) + txt = AutosizedTextarea(attrs={"class": "custom-class", "rows": 3}) + self.assertTrue("autosize " in txt.attrs["class"]) + self.assertTrue(" custom-class" in txt.attrs["class"]) + self.assertEqual(txt.attrs["rows"], 3) def test_AutosizedTextarea_output(self): txt = AutosizedTextarea() - self.assertHTMLEqual(txt.render('txt', ''), ( - '')) + self.assertHTMLEqual( + txt.render("txt", ""), + ( + '" + ), + ) def test_AutosizedTextarea_media(self): txt = AutosizedTextarea() - js_url = static('suit/js/jquery.autosize-min.js') - self.assertHTMLEqual(str(txt.media), - '' - % js_url) + js_url = static("suit/js/jquery.autosize-min.js") + self.assertHTMLEqual( + str(txt.media), '' % js_url + ) diff --git a/suit/widgets.py b/suit/widgets.py index 8ca602b3..8adc5f79 100644 --- a/suit/widgets.py +++ b/suit/widgets.py @@ -1,9 +1,9 @@ -from django.contrib.admin.widgets import AdminTimeWidget, AdminDateWidget -from django.forms import TextInput, Select, Textarea -from django.utils.safestring import mark_safe from django import forms -from django.utils.translation import ugettext as _ +from django.contrib.admin.widgets import AdminDateWidget, AdminTimeWidget +from django.forms import Select, Textarea, TextInput from django.templatetags.static import static +from django.utils.safestring import mark_safe +from django.utils.translation import gettext as _ from suit import utils @@ -15,7 +15,8 @@ class NumberInput(TextInput): HTML5 Number input Left for backwards compatibility """ - input_type = 'number' + + input_type = "number" class HTML5Input(TextInput): @@ -71,16 +72,15 @@ def render(self, name, value, attrs=None, renderer=None): div_classes = [] if self.prepend: - div_classes.append('input-prepend') + div_classes.append("input-prepend") self.prepend = self.enclose_value(self.prepend) - output = ''.join((self.prepend, output)) + output = "".join((self.prepend, output)) if self.append: - div_classes.append('input-append') + div_classes.append("input-append") self.append = self.enclose_value(self.append) - output = ''.join((output, self.append)) + output = "".join((output, self.append)) - return mark_safe( - '
    %s
    ' % (' '.join(div_classes), output)) + return mark_safe('
    %s
    ' % (" ".join(div_classes), output)) class AutosizedTextarea(Textarea): @@ -104,7 +104,8 @@ def render(self, name, value, attrs=None, renderer=None): output += mark_safe( "" - % name) + % name + ) return output @@ -113,7 +114,7 @@ def render(self, name, value, attrs=None, renderer=None): # class SuitDateWidget(AdminDateWidget): def __init__(self, attrs=None, format=None): - defaults = {'placeholder': _('Date:')[:-1]} + defaults = {"placeholder": _("Date:")[:-1]} new_attrs = _make_attrs(attrs, defaults, "vDateField input-small") super(SuitDateWidget, self).__init__(attrs=new_attrs, format=format) @@ -124,13 +125,13 @@ def render(self, name, value, attrs=None, renderer=None): output = super(SuitDateWidget, self).render(name, value, attrs, renderer) return mark_safe( '
    %s
    ' % - output) + 'class="add-on">
  • ' % output + ) class SuitTimeWidget(AdminTimeWidget): def __init__(self, attrs=None, format=None): - defaults = {'placeholder': _('Time:')[:-1]} + defaults = {"placeholder": _("Time:")[:-1]} new_attrs = _make_attrs(attrs, defaults, "vTimeField input-small") super(SuitTimeWidget, self).__init__(attrs=new_attrs, format=format) @@ -141,8 +142,8 @@ def render(self, name, value, attrs=None, renderer=None): output = super(SuitTimeWidget, self).render(name, value, attrs, renderer) return mark_safe( '
    %s
    ' % - output) + 'class="add-on">
    ' % output + ) class SuitSplitDateTimeWidget(forms.SplitDateTimeWidget): @@ -155,12 +156,16 @@ def __init__(self, attrs=None): forms.MultiWidget.__init__(self, widgets, attrs) if django_version < (1, 11): + def format_output(self, rendered_widgets): out_tpl = '
    %s %s
    ' return mark_safe(out_tpl % (rendered_widgets[0], rendered_widgets[1])) else: + def render(self, name, value, attrs=None, renderer=None): - output = super(SuitSplitDateTimeWidget, self).render(name, value, attrs, renderer) + output = super(SuitSplitDateTimeWidget, self).render( + name, value, attrs, renderer + ) return mark_safe('
    %s
    ' % output) From 8cae72c36b3655f05656e1073eb98db163c5f74b Mon Sep 17 00:00:00 2001 From: Jorge Pintor Date: Wed, 14 Feb 2024 15:32:33 -0600 Subject: [PATCH 2/9] Upadates comments and remove prints --- setup.py | 1 - suit/templatetags/suit_list.py | 7 ++++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 9e9401d4..5409ddd3 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,6 @@ 'Intended Audience :: System Administrators', 'Operating System :: OS Independent', 'Programming Language :: Python', - 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', diff --git a/suit/templatetags/suit_list.py b/suit/templatetags/suit_list.py index dab52fdd..ea7884bb 100644 --- a/suit/templatetags/suit_list.py +++ b/suit/templatetags/suit_list.py @@ -13,10 +13,11 @@ from suit.compat import tpl_context_class # Starting with Django 3.2, the pagination is 1-based instead of 0-based. -# I've taken the implementations (latest at the time of release 3.2) from https://github.com/django/django/blob/main/django/contrib/admin/templatetags/admin_list.py +# I've taken the implementations (latest at the time of release 3.2) from +# https://github.com/django/django/blob/main/django/contrib/admin/templatetags/admin_list.py # to use in the implementations below. Older versions of django will use the old implementation and will keep working. -# There are corresponding CSS changes in suit/static/suit/less/ui/pagination.less to fix the pagination, as the code below generates different html objects. -print(django.get_version()) +# There are corresponding CSS changes in suit/static/suit/less/ui/pagination.less to fix the pagination, as the code +# below generates different html objects. USE_NEW_DJANGO_ADMIN_PAGINATION = django.get_version() >= "3.2" From 045e51e9c79fea76c1f047dfcb29001249d27bb8 Mon Sep 17 00:00:00 2001 From: Jorge Pintor Date: Wed, 14 Feb 2024 15:52:39 -0600 Subject: [PATCH 3/9] Add pre-commit --- .pre-commit-config.yaml | 13 +++++++++++++ dev_requirements.txt | 6 ++++++ requirements.txt | 2 ++ 3 files changed, 21 insertions(+) create mode 100644 .pre-commit-config.yaml create mode 100644 dev_requirements.txt create mode 100644 requirements.txt diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..f3e0a58e --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,13 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.2.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-added-large-files + - id: check-ast + +- repo: https://github.com/psf/black + rev: 22.10.0 + hooks: + - id: black diff --git a/dev_requirements.txt b/dev_requirements.txt new file mode 100644 index 00000000..8f74f08d --- /dev/null +++ b/dev_requirements.txt @@ -0,0 +1,6 @@ +-c requirements.txt +# Packages needed if you'd like to test / develop on django-autocompleter +coverage +pre-commit +twine +build diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..fe46db81 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +suit +suit.templatetags From 2f4012b64898b764ba745ba8bc87657062cd696e Mon Sep 17 00:00:00 2001 From: Jorge Pintor Date: Wed, 14 Feb 2024 15:55:29 -0600 Subject: [PATCH 4/9] Add github actions --- .github/ISSUE_TEMPLATE.md | 6 ++--- .github/workflows/main.yml | 52 ++++++++++++++++++++++++++++++++++++++ CHANGELOG.rst | 4 +-- LICENSE | 4 +-- setup.py | 42 +++++++++++++++--------------- 5 files changed, 80 insertions(+), 28 deletions(-) create mode 100644 .github/workflows/main.yml diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index c6a0439b..523c3edd 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,7 +1,7 @@ If this is a bug please specify versions you're using first. -Django version: -Django Suit version: -Python version: +Django version: +Django Suit version: +Python version: Issue: diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..d6de20c7 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,52 @@ +name: Test Suite + +# Controls when the action will run. +on: + # Triggers the workflow on push for the master branch. + push: + branches: [ master ] + # Triggers the workflow on pull request for any branch. + pull_request: + # Allows you to run this workflow manually from the Actions tab. + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [ "3.8", "3.9", "3.10"] + django-version: [ "3.2" , "4.0", "4.1", "4.2"] + include: + - python-version: "3.10" + django-version: "5.0" + steps: + # Checks-out the repository. + - uses: actions/checkout@v3 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + cache-dependency-path: | + requirements.txt + dev_requirements.txt + + - name: Install Python dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install -r dev_requirements.txt + pip install -q Django==${{ matrix.django-version }}.* + + - name: Install Package + run: pip install -e . + + - name: Run tests + run: python manage.py test suit + working-directory: ./test_project diff --git a/CHANGELOG.rst b/CHANGELOG.rst index da8d34bd..ebbd8af8 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,7 +6,7 @@ requests =4.0 +* Add support for Django >=4.0 * Remove support for Django < 3.2 @@ -248,7 +248,7 @@ v0.1.8 (2013-03-20) * [Feature] `Full-width fieldsets `_ * [Feature] Introduced two related wysiwyg apps `suit-redactor `_ and `suit-ckeditor `_ * [CSS] New "multi-fields in row" look and behaviour. -* [CSS] Support for fieldset "wide" class +* [CSS] Support for fieldset "wide" class * [Refactor] Major fieldset refactoring to support multi-line labels * [Fix] Many CSS/Templating fixes and tweaks. See commit log for full changes diff --git a/LICENSE b/LICENSE index 85dd25b4..64aa7bd8 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Django Suit +Django Suit ----------- Django Suit by is licensed under a @@ -7,7 +7,7 @@ Creative Commons Attribution-NonCommercial 3.0 Unported License See online version of this license here: http://creativecommons.org/licenses/by-nc/3.0/ -License +License ------- THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE diff --git a/setup.py b/setup.py index 5409ddd3..a90a7e4a 100644 --- a/setup.py +++ b/setup.py @@ -4,30 +4,30 @@ long_description = fh.read() setup( - name='django-suit', - version=__import__('suit').VERSION, - description='Modern theme for Django admin interface.', + name="django-suit", + version=__import__("suit").VERSION, + description="Modern theme for Django admin interface.", long_description=long_description, long_description_content_type="text/x-rst", - author='Kaspars Sprogis (darklow)', - author_email='info@djangosuit.com', - url='http://djangosuit.com', - packages=['suit', 'suit.templatetags'], + author="Kaspars Sprogis (darklow)", + author_email="info@djangosuit.com", + url="http://djangosuit.com", + packages=["suit", "suit.templatetags"], zip_safe=False, include_package_data=True, classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Framework :: Django', - 'License :: Free for non-commercial use', - 'Intended Audience :: Developers', - 'Intended Audience :: System Administrators', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Environment :: Web Environment', - 'Topic :: Software Development', - 'Topic :: Software Development :: User Interfaces', - ] + "Development Status :: 5 - Production/Stable", + "Framework :: Django", + "License :: Free for non-commercial use", + "Intended Audience :: Developers", + "Intended Audience :: System Administrators", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Environment :: Web Environment", + "Topic :: Software Development", + "Topic :: Software Development :: User Interfaces", + ], ) From 8d26de68b96a72196408ba7776e84c0d7b4af688 Mon Sep 17 00:00:00 2001 From: Jorge Pintor Date: Wed, 14 Feb 2024 15:58:33 -0600 Subject: [PATCH 5/9] Fix github actions --- .github/workflows/main.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d6de20c7..1a55ec81 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -40,13 +40,10 @@ jobs: - name: Install Python dependencies run: | python -m pip install --upgrade pip - pip install -r requirements.txt - pip install -r dev_requirements.txt pip install -q Django==${{ matrix.django-version }}.* - name: Install Package - run: pip install -e . + run: pip install . - name: Run tests run: python manage.py test suit - working-directory: ./test_project From 804d90811f70570ca74810146b191c6e3909f6ca Mon Sep 17 00:00:00 2001 From: Jorge Pintor Date: Wed, 14 Feb 2024 16:01:10 -0600 Subject: [PATCH 6/9] Remove travis.yml --- .travis.yml | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 96250ed7..00000000 --- a/.travis.yml +++ /dev/null @@ -1,23 +0,0 @@ -language: python -python: - - 3.8 - - 3.9 - - 3.10 -env: - - DJANGO=3.2 - - DJANGO=4.2 - - DJANGO=5.0 -before_install: - - export DJANGO_SETTINGS_MODULE=suit.tests.settings -install: - - pip install -q Django==$DJANGO - - pip install . -script: - - python manage.py test suit -matrix: - exclude: - # Django doesn't support following combinations - - python: 3.8 - env: DJANGO=5.0 - - python: 3.9 - env: DJANGO=5.0 From 131737b99cb2d2e7e642ff00bd21d013293fe075 Mon Sep 17 00:00:00 2001 From: Jorge Pintor Date: Wed, 14 Feb 2024 16:31:53 -0600 Subject: [PATCH 7/9] Fix failing tests --- suit/tests/widgets.py | 112 ++++++++++++++++++++++-------------------- 1 file changed, 59 insertions(+), 53 deletions(-) diff --git a/suit/tests/widgets.py b/suit/tests/widgets.py index d3b96ec7..35482f6d 100644 --- a/suit/tests/widgets.py +++ b/suit/tests/widgets.py @@ -18,6 +18,65 @@ class WidgetsTestCase(TestCase): + def get_SuitDateWidget_output(self): + if django_version < (4, 2): + return ( + '
    ' + '' + '' + '
    ' + ) + else: + return ( + '
    ' + '

    ' + '' + '

    ' + '' + '
    ' + ) + + def get_SuitTimeWidget_output(self): + if django_version < (4, 2): + return ( + '
    ' + '' + '' + '
    ' + ) + else: + return ( + '
    ' + '

    ' + '' + '

    ' + '
    ' + ) + + def get_enclosed_widget_html(self, values): + return ( + '
    %s%s
    ' % values + ) + + def get_SuitSplitDateTimeWidget_output(self): + if django_version < (4, 2): + return ( + '
    ' + '
    ' + ) + else: + return ( + '

    ' + '' + '

    ' + '' + '

    ' + ) + + def render_enclosed_widget(self, enclosed_widget): + return enclosed_widget.render("enc", 123) + def test_NumberInput(self): inp = NumberInput() self.assertEqual("number", inp.input_type) @@ -36,15 +95,6 @@ def test_LinkedSelect_with_existing_attr(self): self.assertEqual("linked-select custom-class", ls.attrs["class"]) self.assertEqual(ls.attrs["custom"], 123) - def render_enclosed_widget(self, enclosed_widget): - return enclosed_widget.render("enc", 123) - - def get_enclosed_widget_html(self, values): - return ( - '
    %s%s
    ' % values - ) - def test_EnclosedInput_as_text(self): inp = EnclosedInput(prepend="p", append="a") output = self.render_enclosed_widget(inp) @@ -82,21 +132,6 @@ def test_SuitDateWidget_with_existing_placeholder_attr(self): self.assertTrue(" custom-cls" in sdw.attrs["class"]) self.assertEqual("p", sdw.attrs["placeholder"]) - def get_SuitDateWidget_output(self): - if django_version < (1, 11): - return ( - '
    ' - ) - else: - return ( - '
    ' - '
    ' - ) - def test_SuitDateWidget_output(self): sdw = SuitDateWidget(attrs={"placeholder": "Date"}) output = sdw.render("sdw", "") @@ -118,40 +153,11 @@ def test_SuitTimeWidget_with_existing_placeholder_attr(self): self.assertTrue(" custom-cls" in sdw.attrs["class"]) self.assertEqual("p", sdw.attrs["placeholder"]) - def get_SuitTimeWidget_output(self): - if django_version < (1, 11): - return ( - '
    ' - ) - else: - return ( - '
    ' - '
    ' - ) - def test_SuitTimeWidget_output(self): sdw = SuitTimeWidget(attrs={"placeholder": "Time"}) output = sdw.render("sdw", "") self.assertHTMLEqual(self.get_SuitTimeWidget_output(), output) - def get_SuitSplitDateTimeWidget_output(self): - if django_version < (1, 11): - dwo = self.get_SuitDateWidget_output().replace("sdw", "sdw_0") - two = self.get_SuitTimeWidget_output().replace("sdw", "sdw_1") - return '
    %s %s
    ' % (dwo, two) - else: - return ( - '
    ' - ) - def test_SuitSplitDateTimeWidget(self): ssdtw = SuitSplitDateTimeWidget() output = ssdtw.render("sdw", "") From 134667609dea5100bb9698b4c23d9b7833572b02 Mon Sep 17 00:00:00 2001 From: Jorge Pintor Date: Thu, 15 Feb 2024 09:07:03 -0600 Subject: [PATCH 8/9] beautify code --- .pre-commit-config.yaml | 8 ++ docs/conf.py | 103 +++++++++++++------------- suit/admin.py | 36 ++++----- suit/compat.py | 24 +----- suit/config.py | 8 +- suit/templatetags/suit_list.py | 37 +++------ suit/templatetags/suit_menu.py | 59 +++++---------- suit/templatetags/suit_tags.py | 20 +++-- suit/tests/config.py | 26 ++----- suit/tests/migrations/0001_initial.py | 3 +- suit/tests/mixins.py | 11 +-- suit/tests/templates/form_tabs.py | 8 +- suit/tests/templatetags/suit_list.py | 4 +- suit/tests/templatetags/suit_menu.py | 20 ++--- suit/tests/templatetags/suit_tags.py | 8 +- suit/tests/widgets.py | 7 +- suit/utils.py | 3 +- suit/watch_less.py | 4 +- suit/widgets.py | 10 +-- 19 files changed, 145 insertions(+), 254 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f3e0a58e..98335dab 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,7 +7,15 @@ repos: - id: check-added-large-files - id: check-ast +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.1.1 + hooks: + - id: ruff + # enable auto-fixing of common errors + args: [--exit-non-zero-on-fix] + - repo: https://github.com/psf/black rev: 22.10.0 hooks: - id: black + args: [--line-length=120, --skip-string-normalization] diff --git a/docs/conf.py b/docs/conf.py index b0346b51..2c778a03 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -11,8 +11,6 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, os - try: import sphinx_rtd_theme @@ -24,12 +22,12 @@ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +# sys.path.insert(0, os.path.abspath('.')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. @@ -42,7 +40,7 @@ source_suffix = '.rst' # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' @@ -62,37 +60,37 @@ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- @@ -104,27 +102,27 @@ # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, @@ -133,44 +131,44 @@ # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'DjangoSuitdoc' @@ -181,10 +179,8 @@ latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. #'preamble': '', } @@ -192,42 +188,38 @@ # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'DjangoSuit.tex', u'Django Suit Documentation', - u'Kaspars Sprogis (darklow)', 'manual'), + ('index', 'DjangoSuit.tex', u'Django Suit Documentation', u'Kaspars Sprogis (darklow)', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - ('index', 'djangosuit', u'Django Suit Documentation', - [u'Kaspars Sprogis (darklow)'], 1) -] +man_pages = [('index', 'djangosuit', u'Django Suit Documentation', [u'Kaspars Sprogis (darklow)'], 1)] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ @@ -236,17 +228,22 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'DjangoSuit', u'Django Suit Documentation', - u'Kaspars Sprogis (darklow)', 'DjangoSuit', - 'One line description of project.', - 'Miscellaneous'), + ( + 'index', + 'DjangoSuit', + u'Django Suit Documentation', + u'Kaspars Sprogis (darklow)', + 'DjangoSuit', + 'One line description of project.', + 'Miscellaneous', + ), ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' diff --git a/suit/admin.py b/suit/admin.py index d9f67b5c..9d71fe1b 100644 --- a/suit/admin.py +++ b/suit/admin.py @@ -13,6 +13,7 @@ class SortableModelAdminBase(object): """ Base class for SortableTabularInline and SortableModelAdmin """ + sortable = 'order' class Media: @@ -25,10 +26,7 @@ class SortableListForm(ModelForm): """ class Meta: - widgets = { - 'order': NumberInput( - attrs={'class': 'hide input-mini suit-sortable'}) - } + widgets = {'order': NumberInput(attrs={'class': 'hide input-mini suit-sortable'})} class SortableChangeList(ChangeList): @@ -56,16 +54,14 @@ def __init__(self, *args, **kwargs): def formfield_for_dbfield(self, db_field, **kwargs): if db_field.name == self.sortable: kwargs['widget'] = SortableListForm.Meta.widgets['order'] - return super(SortableTabularInlineBase, self).formfield_for_dbfield( - db_field, **kwargs) + return super(SortableTabularInlineBase, self).formfield_for_dbfield(db_field, **kwargs) class SortableTabularInline(SortableTabularInlineBase, admin.TabularInline): pass -class SortableGenericTabularInline(SortableTabularInlineBase, - ct_admin.GenericTabularInline): +class SortableGenericTabularInline(SortableTabularInlineBase, ct_admin.GenericTabularInline): pass @@ -73,6 +69,7 @@ class SortableStackedInlineBase(SortableModelAdminBase): """ Sortable stacked inline """ + def __init__(self, *args, **kwargs): super(SortableStackedInlineBase, self).__init__(*args, **kwargs) self.ordering = (self.sortable,) @@ -83,8 +80,7 @@ def get_fieldsets(self, *args, **kwargs): Remove sortable from every other fieldset, if by some reason someone has added it """ - fieldsets = super(SortableStackedInlineBase, self).get_fieldsets( - *args, **kwargs) + fieldsets = super(SortableStackedInlineBase, self).get_fieldsets(*args, **kwargs) sortable_added = False for fieldset in fieldsets: @@ -115,20 +111,17 @@ def get_fieldsets(self, *args, **kwargs): def formfield_for_dbfield(self, db_field, **kwargs): if db_field.name == self.sortable: - kwargs['widget'] = copy.deepcopy( - SortableListForm.Meta.widgets['order']) + kwargs['widget'] = copy.deepcopy(SortableListForm.Meta.widgets['order']) kwargs['widget'].attrs['class'] += ' suit-sortable-stacked' kwargs['widget'].attrs['rowclass'] = ' suit-sortable-stacked-row' - return super(SortableStackedInlineBase, self).formfield_for_dbfield( - db_field, **kwargs) + return super(SortableStackedInlineBase, self).formfield_for_dbfield(db_field, **kwargs) class SortableStackedInline(SortableStackedInlineBase, admin.StackedInline): pass -class SortableGenericStackedInline(SortableStackedInlineBase, - ct_admin.GenericStackedInline): +class SortableGenericStackedInline(SortableStackedInlineBase, ct_admin.GenericStackedInline): pass @@ -136,6 +129,7 @@ class SortableModelAdmin(SortableModelAdminBase, ModelAdmin): """ Sortable tabular inline """ + list_per_page = 500 def __init__(self, *args, **kwargs): @@ -161,12 +155,10 @@ def merge_form_meta(self, form): form.Meta = SortableListForm.Meta if not getattr(form.Meta, 'widgets', None): form.Meta.widgets = {} - form.Meta.widgets[self.sortable] = SortableListForm.Meta.widgets[ - 'order'] + form.Meta.widgets[self.sortable] = SortableListForm.Meta.widgets['order'] def get_changelist_form(self, request, **kwargs): - form = super(SortableModelAdmin, self).get_changelist_form(request, - **kwargs) + form = super(SortableModelAdmin, self).get_changelist_form(request, **kwargs) self.merge_form_meta(form) return form @@ -175,8 +167,7 @@ def get_changelist(self, request, **kwargs): def save_model(self, request, obj, form, change): if not obj.pk: - max_order = obj.__class__.objects.aggregate( - models.Max(self.sortable)) + max_order = obj.__class__.objects.aggregate(models.Max(self.sortable)) try: next_order = max_order['%s__max' % self.sortable] + 1 except TypeError: @@ -197,4 +188,3 @@ def save_model(self, request, obj, form, change): } except ImportError: pass - diff --git a/suit/compat.py b/suit/compat.py index 1e94d141..5563d140 100644 --- a/suit/compat.py +++ b/suit/compat.py @@ -7,25 +7,7 @@ """ import django +from django.template.defaulttags import url +from django.contrib.contenttypes import admin as ct_admin -try: - from importlib import import_module -except ImportError: # python = 2.6 - from django.utils.importlib import import_module # Django < 1.9 - -if django.VERSION[:2] < (1, 5): - from django.templatetags.future import url -else: - from django.template.defaulttags import url - -try: - from django.contrib.contenttypes import admin as ct_admin -except ImportError: - from django.contrib.contenttypes import generic as ct_admin # Django < 1.8 - -if django.VERSION[:2] < (1, 8): - from django.template import Context - - tpl_context_class = Context -else: - tpl_context_class = dict +tpl_context_class = dict diff --git a/suit/config.py b/suit/config.py index 0cdd6d47..bde2c4f7 100644 --- a/suit/config.py +++ b/suit/config.py @@ -6,16 +6,13 @@ def default_config(): return { 'VERSION': VERSION, - # configurable 'ADMIN_NAME': 'Django Suit', 'HEADER_DATE_FORMAT': 'l, jS F Y', 'HEADER_TIME_FORMAT': 'H:i', - # form 'SHOW_REQUIRED_ASTERISK': True, 'CONFIRM_UNSAVED_CHANGES': True, - # menu 'SEARCH_URL': '/admin/auth/user/', 'MENU_OPEN_FIRST_CHILD': True, @@ -30,9 +27,8 @@ def default_config(): # {'label': 'Settings', 'icon':'icon-cog', 'models': ('auth.user', 'auth.group')}, # {'label': 'Support', 'icon':'icon-question-sign', 'url': '/support/'}, # ), - # misc - 'LIST_PER_PAGE': 20 + 'LIST_PER_PAGE': 20, } @@ -49,6 +45,7 @@ def get_config(param=None): return value return config + # Reverse default actions position ModelAdmin.actions_on_top = False ModelAdmin.actions_on_bottom = True @@ -56,6 +53,7 @@ def get_config(param=None): # Set global list_per_page ModelAdmin.list_per_page = get_config('LIST_PER_PAGE') + def setup_filer(): from suit.widgets import AutosizedTextarea from filer.admin.imageadmin import ImageAdminForm diff --git a/suit/templatetags/suit_list.py b/suit/templatetags/suit_list.py index ea7884bb..15d96b13 100644 --- a/suit/templatetags/suit_list.py +++ b/suit/templatetags/suit_list.py @@ -34,10 +34,7 @@ def paginator_number(cl, i): """ if not USE_NEW_DJANGO_ADMIN_PAGINATION: if i == DOT: - return mark_safe( - '
  • ..' - ".
  • " - ) + return mark_safe('
  • ..' ".
  • ") elif i == cl.page_num: return mark_safe('
  • %d
  • ' % (i + 1)) else: @@ -73,9 +70,7 @@ def paginator_info(cl): entries_from = 1 if paginator.count > 0 else 0 entries_to = paginator.count else: - entries_from = ( - ((paginator.per_page * cl.page_num) + 1) if paginator.count > 0 else 0 - ) + entries_from = ((paginator.per_page * cl.page_num) + 1) if paginator.count > 0 else 0 entries_to = entries_from - 1 + paginator.per_page if paginator.count < entries_to: entries_to = paginator.count @@ -89,9 +84,7 @@ def paginator_info(cl): entries_from = 1 if paginator.count > 0 else 0 entries_to = paginator.count else: - entries_from = ( - ((paginator.per_page * (cl.page_num - 1)) + 1) if paginator.count > 0 else 0 - ) + entries_from = ((paginator.per_page * (cl.page_num - 1)) + 1) if paginator.count > 0 else 0 entries_to = entries_from - 1 + paginator.per_page if paginator.count < entries_to: entries_to = paginator.count @@ -132,9 +125,7 @@ def pagination(cl): if page_num < (paginator.num_pages - ON_EACH_SIDE - ON_ENDS - 1): page_range.extend(range(page_num + 1, page_num + ON_EACH_SIDE + 1)) page_range.append(DOT) - page_range.extend( - range(paginator.num_pages - ON_ENDS, paginator.num_pages) - ) + page_range.extend(range(paginator.num_pages - ON_ENDS, paginator.num_pages)) else: page_range.extend(range(page_num + 1, paginator.num_pages)) @@ -149,9 +140,7 @@ def pagination(cl): } pagination_required = (not cl.show_all or not cl.can_show_all) and cl.multi_page - page_range = ( - cl.paginator.get_elided_page_range(cl.page_num) if pagination_required else [] - ) + page_range = cl.paginator.get_elided_page_range(cl.page_num) if pagination_required else [] need_show_all_link = cl.can_show_all and not cl.show_all and cl.multi_page return { "cl": cl, @@ -219,15 +208,13 @@ def headers_handler(result_headers, cl): field_name = cl.list_display[i] if field_name == "action_checkbox": continue - if not attrib_key in header: + if attrib_key not in header: header[attrib_key] = mark_safe(' class=""') pattern = 'class="' if pattern in header[attrib_key]: replacement = "%s%s-column " % (pattern, field_name) - header[attrib_key] = mark_safe( - header[attrib_key].replace(pattern, replacement) - ) + header[attrib_key] = mark_safe(header[attrib_key].replace(pattern, replacement)) return result_headers @@ -274,8 +261,7 @@ def result_row_attrs(context, cl, row_index): # Validate if not isinstance(new_attrs, dict): raise TypeError( - '"suit_row_attributes" must return dict. Got: %s: %s' - % (new_attrs.__class__.__name__, new_attrs) + '"suit_row_attributes" must return dict. Got: %s: %s' % (new_attrs.__class__.__name__, new_attrs) ) # Merge 'class' attribute @@ -309,8 +295,7 @@ def cells_handler(results, cl): # Validate if not isinstance(attrs, dict): raise TypeError( - '"suit_cell_attributes" must return dict. ' - "Got: %s: %s" % (attrs.__class__.__name__, attrs) + '"suit_cell_attributes" must return dict. ' "Got: %s: %s" % (attrs.__class__.__name__, attrs) ) # Merge 'class' attribute @@ -323,8 +308,6 @@ def cells_handler(results, cl): if attrs: cell_pattern = td_pattern if item.startswith(td_pattern) else th_pattern - result[col] = mark_safe( - result[col].replace(cell_pattern, td_pattern + dict_to_attrs(attrs)) - ) + result[col] = mark_safe(result[col].replace(cell_pattern, td_pattern + dict_to_attrs(attrs))) return results diff --git a/suit/templatetags/suit_menu.py b/suit/templatetags/suit_menu.py index d101585f..225c9869 100644 --- a/suit/templatetags/suit_menu.py +++ b/suit/templatetags/suit_menu.py @@ -1,7 +1,6 @@ import re import warnings -import django from django import template from django.contrib import admin from django.contrib.admin import AdminSite @@ -33,7 +32,7 @@ def get_menu(context, request): else: try: template_response = get_admin_site(context.current_app).index(request) - # Django 1.10 removed the current_app parameter for some classes and functions. + # Django 1.10 removed the current_app parameter for some classes and functions. # Check the release notes. except AttributeError: template_response = get_admin_site(context.request.resolver_match.namespace).index(request) @@ -119,8 +118,7 @@ def get_app_list(self): def make_menu(self, config): menu = [] if not isinstance(config, (tuple, list)): - raise TypeError('Django Suit MENU config parameter must be ' - 'tuple or list. Got %s' % repr(config)) + raise TypeError('Django Suit MENU config parameter must be ' 'tuple or list. Got %s' % repr(config)) for app in config: app = self.make_app(app) if app: @@ -137,8 +135,7 @@ def make_app(self, app_def): else: app = self.make_app_from_native(app_def) else: - raise TypeError('MENU list item must be string or dict. Got %s' - % repr(app_def)) + raise TypeError('MENU list item must be string or dict. Got %s' % repr(app_def)) if app: return self.process_app(app) @@ -178,10 +175,8 @@ def process_app(self, app): return app - def app_is_forbidden(self, app): - return app['permissions'] and \ - not self.user_has_permission(app['permissions']) + return app['permissions'] and not self.user_has_permission(app['permissions']) def app_is_excluded(self, app): return self.conf_exclude and app['name'] in self.conf_exclude @@ -193,8 +188,7 @@ def process_icons(self, app): """ if 'icon' in app: app['icon'] = app['icon'] or 'icon-' - elif self.conf_icons and 'name' in app and \ - app['name'] in self.conf_icons: + elif self.conf_icons and 'name' in app and app['name'] in self.conf_icons: app['icon'] = self.conf_icons[app['name']] def process_semi_native_app(self, app): @@ -246,17 +240,10 @@ def convert_native_app(self, native_app, app_name): if not models: return - return { - 'label': native_app['name'], - 'url': native_app['app_url'], - 'models': models, - 'name': app_name - } + return {'label': native_app['name'], 'url': native_app['app_url'], 'models': models, 'name': app_name} def make_separator(self): - return { - 'separator': True - } + return {'separator': True} def process_models(self, app): models = [] @@ -276,11 +263,11 @@ def make_models(self, model_def, app_name): model = self.make_model(model_def, app_name) return [model] if model else [] prefix = match.group(1) - prefix = self.get_model_name(app_name,prefix) + prefix = self.get_model_name(app_name, prefix) return [ m for m in [ - self.convert_native_model(native_model,app_name) + self.convert_native_model(native_model, app_name) for native_model in self.all_models if self.get_native_model_name(native_model).startswith(prefix) ] @@ -293,8 +280,7 @@ def make_model(self, model_def, app_name): elif isinstance(model_def, str): model = self.make_model_from_native(model_def, app_name) else: - raise TypeError('MENU list item must be string or dict. Got %s' - % repr(model_def)) + raise TypeError('MENU list item must be string or dict. Got %s' % repr(model_def)) if model: return self.process_model(model, app_name) @@ -327,7 +313,7 @@ def get_native_model_name(self, model): """ url_parts = self.get_native_model_url(model).rstrip('/').split('/') root_url_parts = reverse('admin:index').rstrip('/').split('/') - return '.'.join(url_parts[len(root_url_parts):][:2]) + return '.'.join(url_parts[len(root_url_parts) :][:2]) def convert_native_model(self, model, app_name): return { @@ -335,8 +321,8 @@ def convert_native_model(self, model, app_name): 'url': self.get_native_model_url(model), 'name': self.get_native_model_name(model), 'app': app_name, - 'perms': model.get('perms',None), - 'add_url': model.get('add_url',None), + 'perms': model.get('perms', None), + 'add_url': model.get('add_url', None), } def get_native_model_url(self, model): @@ -365,23 +351,20 @@ def process_model(self, model, app_name): return model def model_is_forbidden(self, model): - return model['permissions'] and \ - not self.user_has_permission(model['permissions']) + return model['permissions'] and not self.user_has_permission(model['permissions']) def process_semi_native_model(self, model, app_name): """ Process app defined as { model: 'model' } """ - model_from_native = self.make_model_from_native(model['model'], - app_name) + model_from_native = self.make_model_from_native(model['model'], app_name) if model_from_native: del model['model'] model_from_native.update(model) return model_from_native def ensure_app_keys(self, app): - keys = ['label', 'url', 'icon', 'permissions', 'name', 'is_active', - 'blank'] + keys = ['label', 'url', 'icon', 'permissions', 'name', 'is_active', 'blank'] self.fill_keys(app, keys) def ensure_model_keys(self, model): @@ -409,9 +392,7 @@ def activate_menu(self, menu): self.activate_models(app) # Mark as active by url match - if not self.app_activated \ - and (self.request.path == app['url'] - or self.request.path == app.get('orig_url')): + if not self.app_activated and (self.request.path == app['url'] or self.request.path == app.get('orig_url')): app['is_active'] = self.app_activated = True if not self.app_activated: @@ -428,8 +409,7 @@ def activate_models(self, app, match_by_name=False): # Mark as active by url or model plural name match model['is_active'] = self.request.path == model['url'] else: - model['is_active'] = self.ctx_model_plural == model[ - 'label'].lower() + model['is_active'] = self.ctx_model_plural == model['label'].lower() # Mark parent as active too if model['is_active'] and not self.app_activated: @@ -489,7 +469,8 @@ def make_menu_from_old_format(self, conf_order): warnings.warn( 'Django Suit "MENU_ORDER" setting is deprecated. Use new "MENU"' ' key instead, see Documentation for new syntax.', - DeprecationWarning) + DeprecationWarning, + ) new_conf = [] for order in conf_order: diff --git a/suit/templatetags/suit_tags.py b/suit/templatetags/suit_tags.py index 7e55e398..34779731 100644 --- a/suit/templatetags/suit_tags.py +++ b/suit/templatetags/suit_tags.py @@ -57,15 +57,14 @@ def field_contents_foreign_linked(admin_field): displayed = admin_field.contents() obj = admin_field.form.instance - if not hasattr(admin_field.model_admin, - 'linked_readonly_fields') or fieldname not in admin_field \ - .model_admin \ - .linked_readonly_fields: + if ( + not hasattr(admin_field.model_admin, 'linked_readonly_fields') + or fieldname not in admin_field.model_admin.linked_readonly_fields + ): return displayed try: - fieldtype, attr, value = lookup_field(fieldname, obj, - admin_field.model_admin) + fieldtype, attr, value = lookup_field(fieldname, obj, admin_field.model_admin) except ObjectDoesNotExist: fieldtype = None @@ -97,10 +96,9 @@ def suit_bc_value(*args): @simple_tag def admin_extra_filters(cl): - """ Return the dict of used filters which is not included - in list_filters form """ - used_parameters = list(itertools.chain(*(s.used_parameters.keys() - for s in cl.filter_specs))) + """Return the dict of used filters which is not included + in list_filters form""" + used_parameters = list(itertools.chain(*(s.used_parameters.keys() for s in cl.filter_specs))) return dict((k, v) for k, v in cl.params.items() if k not in used_parameters) @@ -139,11 +137,11 @@ def str_to_version(string): def add_preserved_filters(*args, **kwargs): pass + if django_version < (1, 5): # Add admin_urlquote filter to support Django 1.4 from django.contrib.admin.util import quote - @register.filter def admin_urlquote(value): return quote(value) diff --git a/suit/tests/config.py b/suit/tests/config.py index b8ec7d51..0b7a7077 100644 --- a/suit/tests/config.py +++ b/suit/tests/config.py @@ -18,36 +18,25 @@ def test_suit_config_when_not_defined(self): except AttributeError: pass default_suit_config = default_config() - self.assertEqual(get_config('ADMIN_NAME'), - default_suit_config['ADMIN_NAME']) + self.assertEqual(get_config('ADMIN_NAME'), default_suit_config['ADMIN_NAME']) # Defined as None, should also use fallback admin_name = None - settings.SUIT_CONFIG = { - 'ADMIN_NAME': admin_name - } - self.assertEqual(get_config('ADMIN_NAME'), - default_suit_config['ADMIN_NAME']) + settings.SUIT_CONFIG = {'ADMIN_NAME': admin_name} + self.assertEqual(get_config('ADMIN_NAME'), default_suit_config['ADMIN_NAME']) def test_suit_config_when_defined_but_no_key(self): - settings.SUIT_CONFIG = { - 'RANDOM_KEY': 123 - } + settings.SUIT_CONFIG = {'RANDOM_KEY': 123} default_suit_config = default_config() - self.assertEqual(get_config('ADMIN_NAME'), - default_suit_config['ADMIN_NAME']) + self.assertEqual(get_config('ADMIN_NAME'), default_suit_config['ADMIN_NAME']) # Defined as empty, should stay empty admin_name = '' - settings.SUIT_CONFIG = { - 'ADMIN_NAME': admin_name - } + settings.SUIT_CONFIG = {'ADMIN_NAME': admin_name} self.assertEqual(get_config('ADMIN_NAME'), admin_name) def test_suit_config_when_defined(self): admin_name = 'Custom Name' - settings.SUIT_CONFIG = { - 'ADMIN_NAME': admin_name - } + settings.SUIT_CONFIG = {'ADMIN_NAME': admin_name} self.assertEqual(get_config('ADMIN_NAME'), admin_name) def test_django_modeladmin_overrides(self): @@ -57,7 +46,6 @@ def test_django_modeladmin_overrides(self): class ConfigWithModelsTestCase(ModelsTestCaseMixin, UserTestCaseMixin): - def create_book(self): book = Book(pk=2, name='Some book') book.save() diff --git a/suit/tests/migrations/0001_initial.py b/suit/tests/migrations/0001_initial.py index adf3ad2d..753ccb21 100644 --- a/suit/tests/migrations/0001_initial.py +++ b/suit/tests/migrations/0001_initial.py @@ -5,8 +5,7 @@ class Migration(migrations.Migration): initial = True - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( diff --git a/suit/tests/mixins.py b/suit/tests/mixins.py index f488615b..d862e7d5 100644 --- a/suit/tests/mixins.py +++ b/suit/tests/mixins.py @@ -30,12 +30,10 @@ def login_superuser(self): self.client.login(username=self.superuser.username, password='password') def create_superuser(self): - return User.objects.create_superuser('admin-%s' % str(randint(1, 9999)), - 'test@test.com', 'password') + return User.objects.create_superuser('admin-%s' % str(randint(1, 9999)), 'test@test.com', 'password') def create_user(self): - user = User.objects.create_user('user-%s' % str(randint(1, 9999)), - 'test2@test2.com', 'password') + user = User.objects.create_user('user-%s' % str(randint(1, 9999)), 'test2@test2.com', 'password') user.is_staff = True user.save() return user @@ -55,14 +53,13 @@ def _pre_setup(self): self.saved_INSTALLED_APPS = settings.INSTALLED_APPS self.saved_DEBUG = settings.DEBUG test_app = 'suit.tests' - settings.INSTALLED_APPS = tuple( - list(self.saved_INSTALLED_APPS) + [test_app] - ) + settings.INSTALLED_APPS = tuple(list(self.saved_INSTALLED_APPS) + [test_app]) settings.DEBUG = True # Legacy Django < 1.9: load our fake application and syncdb try: from django.db.models.loading import load_app + load_app(test_app) except ImportError: pass diff --git a/suit/tests/templates/form_tabs.py b/suit/tests/templates/form_tabs.py index 115bbfe1..7cd0b0c6 100644 --- a/suit/tests/templates/form_tabs.py +++ b/suit/tests/templates/form_tabs.py @@ -44,10 +44,6 @@ def test_template_includes(self): suit_form_include = "admin/date_hierarchy.html" TabbedBookAdmin.suit_form_includes = ((suit_form_include, "top", "tab1"),) self.get_response(self.url) - self.assertTemplateUsed( - self.response, "suit/includes/change_form_includes.html" - ) + self.assertTemplateUsed(self.response, "suit/includes/change_form_includes.html") self.assertTemplateUsed(self.response, suit_form_include) - self.assertContains( - self.response, '
    ' - ) + self.assertContains(self.response, '
    ') diff --git a/suit/tests/templatetags/suit_list.py b/suit/tests/templatetags/suit_list.py index 92782749..0288703a 100644 --- a/suit/tests/templatetags/suit_list.py +++ b/suit/tests/templatetags/suit_list.py @@ -164,9 +164,7 @@ def test_suit_list_cells_handler(self): self.assertTrue('data="1"' in result[0][2]) self.assertTrue('class="col-action_checkbox"' in result[0][0]) # Django 1.6 adds col-NAME class automatically - self.assertTrue( - 'class="test"' in result[0][1] or 'class="col-name test"' in result[0][1] - ) + self.assertTrue('class="test"' in result[0][1] or 'class="col-name test"' in result[0][1]) self.assertTrue('class="col-order"' in result[0][2]) def test_suit_list_cells_handler_by_response(self): diff --git a/suit/tests/templatetags/suit_menu.py b/suit/tests/templatetags/suit_menu.py index 918a7c42..e1b33e7e 100644 --- a/suit/tests/templatetags/suit_menu.py +++ b/suit/tests/templatetags/suit_menu.py @@ -127,9 +127,7 @@ def test_menu_search_url_formats(self): settings.SUIT_CONFIG["SEARCH_URL"] = "admin:%s_book_changelist" % app_label admin_root = reverse("admin:index") self.get_response() - self.assertContains( - self.response, 'action="%s%s/book/"' % (admin_root, app_label) - ) + self.assertContains(self.response, 'action="%s%s/book/"' % (admin_root, app_label)) # Test absolute url absolute_search_url = "/absolute/search/url" @@ -227,18 +225,10 @@ def test_menu(self): first_model_url = reverse("admin:%s_book_changelist" % app_label) self.assertEqual(menu[i]["models"][0]["url"], first_model_url) self.assertEqual(len(menu[i]["models"]), 4) - self.assertEqual( - force_unicode(menu[i]["models"][2]["label"]), mc[i]["models"][2]["label"] - ) - self.assertEqual( - force_unicode(menu[i]["models"][2]["url"]), mc[i]["models"][2]["url"] - ) - self.assertEqual( - force_unicode(menu[i]["models"][3]["label"]), mc[i]["models"][3]["label"] - ) - self.assertEqual( - force_unicode(menu[i]["models"][3]["url"]), mc[i]["models"][3]["url"] - ) + self.assertEqual(force_unicode(menu[i]["models"][2]["label"]), mc[i]["models"][2]["label"]) + self.assertEqual(force_unicode(menu[i]["models"][2]["url"]), mc[i]["models"][2]["url"]) + self.assertEqual(force_unicode(menu[i]["models"][3]["label"]), mc[i]["models"][3]["label"]) + self.assertEqual(force_unicode(menu[i]["models"][3]["url"]), mc[i]["models"][3]["url"]) def test_menu_app_exclude(self): settings.SUIT_CONFIG["MENU"] = ( diff --git a/suit/tests/templatetags/suit_tags.py b/suit/tests/templatetags/suit_tags.py index 7aae3a62..5bf7d534 100644 --- a/suit/tests/templatetags/suit_tags.py +++ b/suit/tests/templatetags/suit_tags.py @@ -67,12 +67,8 @@ def test_suit_date_and_time(self): "HEADER_DATE_FORMAT": "Y-m-d", "HEADER_TIME_FORMAT": "H:i", } - self.assertEqual( - datetime.datetime.now().strftime("%Y-%m-%d"), suit_date({}, {}).render({}) - ) - self.assertEqual( - datetime.datetime.now().strftime("%H:%M"), suit_time({}, {}).render({}) - ) + self.assertEqual(datetime.datetime.now().strftime("%Y-%m-%d"), suit_date({}, {}).render({})) + self.assertEqual(datetime.datetime.now().strftime("%H:%M"), suit_time({}, {}).render({})) def test_admin_url(self): country = Country(pk=1, name="USA") diff --git a/suit/tests/widgets.py b/suit/tests/widgets.py index 35482f6d..b8992d42 100644 --- a/suit/tests/widgets.py +++ b/suit/tests/widgets.py @@ -55,8 +55,7 @@ def get_SuitTimeWidget_output(self): def get_enclosed_widget_html(self, values): return ( - '
    %s%s
    ' % values + '
    %s%s
    ' % values ) def get_SuitSplitDateTimeWidget_output(self): @@ -188,6 +187,4 @@ def test_AutosizedTextarea_output(self): def test_AutosizedTextarea_media(self): txt = AutosizedTextarea() js_url = static("suit/js/jquery.autosize-min.js") - self.assertHTMLEqual( - str(txt.media), '' % js_url - ) + self.assertHTMLEqual(str(txt.media), '' % js_url) diff --git a/suit/utils.py b/suit/utils.py index 944af587..382eb28a 100644 --- a/suit/utils.py +++ b/suit/utils.py @@ -12,8 +12,7 @@ def value_by_version(args): """ version_map = args_to_dict(args) major_version = '.'.join(str(v) for v in django_major_version()) - return version_map.get(major_version, - list(version_map.values())[-1]) + return version_map.get(major_version, list(version_map.values())[-1]) def args_to_dict(args): diff --git a/suit/watch_less.py b/suit/watch_less.py index efcb1119..e5f2ec96 100644 --- a/suit/watch_less.py +++ b/suit/watch_less.py @@ -7,7 +7,6 @@ class LessCompiler(FileSystemEventHandler): - def __init__(self, source): self.source = source FileSystemEventHandler.__init__(self) @@ -29,8 +28,7 @@ def on_any_event(self, event): if __name__ == "__main__": if len(sys.argv) < 2: - sys.stderr.write( - 'Usage: %s source [destination=../css/$1.css]\n' % sys.argv[0]) + sys.stderr.write('Usage: %s source [destination=../css/$1.css]\n' % sys.argv[0]) sys.exit(1) source = os.path.abspath(sys.argv[1]) diff --git a/suit/widgets.py b/suit/widgets.py index 8adc5f79..8a4db524 100644 --- a/suit/widgets.py +++ b/suit/widgets.py @@ -102,10 +102,7 @@ def render(self, name, value, attrs=None, renderer=None): else: output = super(AutosizedTextarea, self).render(name, value, attrs, renderer) - output += mark_safe( - "" - % name - ) + output += mark_safe("" % name) return output @@ -160,12 +157,11 @@ def __init__(self, attrs=None): def format_output(self, rendered_widgets): out_tpl = '
    %s %s
    ' return mark_safe(out_tpl % (rendered_widgets[0], rendered_widgets[1])) + else: def render(self, name, value, attrs=None, renderer=None): - output = super(SuitSplitDateTimeWidget, self).render( - name, value, attrs, renderer - ) + output = super(SuitSplitDateTimeWidget, self).render(name, value, attrs, renderer) return mark_safe('
    %s
    ' % output) From 6f629163059c79871a985686befdaf1f89a9ddc1 Mon Sep 17 00:00:00 2001 From: Jorge Pintor Date: Thu, 15 Feb 2024 09:23:23 -0600 Subject: [PATCH 9/9] More code imporvements --- docs/conf.py | 12 +++++------ pyproject.toml | 34 +++++++++++++++++++++++++++++++ suit/admin.py | 8 +++++--- suit/compat.py | 2 +- suit/config.py | 10 +++++---- suit/templatetags/suit_compat.py | 1 + suit/templatetags/suit_tags.py | 4 +++- suit/tests/config.py | 5 +++-- suit/tests/mixins.py | 6 +++--- suit/tests/templates/form_tabs.py | 1 + suit/tests/test.py | 3 --- suit/tests/utils.py | 2 +- suit/watch_less.py | 5 ++--- 13 files changed, 66 insertions(+), 27 deletions(-) create mode 100644 pyproject.toml diff --git a/docs/conf.py b/docs/conf.py index 2c778a03..fddc0233 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -46,8 +46,8 @@ master_doc = 'index' # General information about the project. -project = u'Django Suit' -copyright = u'2013, Kaspars Sprogis (darklow)' +project = 'Django Suit' +copyright = '2013, Kaspars Sprogis (darklow)' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -188,7 +188,7 @@ # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'DjangoSuit.tex', u'Django Suit Documentation', u'Kaspars Sprogis (darklow)', 'manual'), + ('index', 'DjangoSuit.tex', 'Django Suit Documentation', 'Kaspars Sprogis (darklow)', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -216,7 +216,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [('index', 'djangosuit', u'Django Suit Documentation', [u'Kaspars Sprogis (darklow)'], 1)] +man_pages = [('index', 'djangosuit', 'Django Suit Documentation', ['Kaspars Sprogis (darklow)'], 1)] # If true, show URL addresses after external links. # man_show_urls = False @@ -231,8 +231,8 @@ ( 'index', 'DjangoSuit', - u'Django Suit Documentation', - u'Kaspars Sprogis (darklow)', + 'Django Suit Documentation', + 'Kaspars Sprogis (darklow)', 'DjangoSuit', 'One line description of project.', 'Miscellaneous', diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..ed81710b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,34 @@ +[tool.ruff] +# https://docs.astral.sh/ruff/configuration/#using-pyprojecttoml + +fix = true + +ignore = [ + "E203", # Whitespace before '{symbol}' + "F401", # {name} imported but unused +] + +line-length = 120 + +# https://docs.astral.sh/ruff/rules/ +select = [ + # First two lines represent default flake8 configuration + "E", "W", # PyCodestyle + "F", # PyFlakes + "T10", # flake8-debugger: catch debug statements + "T20", # flake8-print: catch print statements + "UP025", # unicode-kind-prefix: Prevent unicode prefix in strings + "I", # isort: Check import order and add missing required imports +] + +show-fixes = true + +[tool.ruff.isort] +# https://docs.astral.sh/ruff/settings/#isort +# Combine aliased imports into the same import statement. +combine-as-imports = true +# With imports including at least one alias, force each imported member to its own line. +force-wrap-aliases = true +# Do not sort imports by type (constant, class, function, etc.). +# Instead, sort them alphabetically and case-insensitively. +order-by-type = false diff --git a/suit/admin.py b/suit/admin.py index 9d71fe1b..1acc0a1f 100644 --- a/suit/admin.py +++ b/suit/admin.py @@ -1,12 +1,14 @@ import copy + from django.conf import settings +from django.contrib import admin from django.contrib.admin import ModelAdmin from django.contrib.admin.views.main import ChangeList -from django.forms import ModelForm -from django.contrib import admin from django.db import models -from suit.widgets import NumberInput, SuitSplitDateTimeWidget +from django.forms import ModelForm + from suit.compat import ct_admin +from suit.widgets import NumberInput, SuitSplitDateTimeWidget class SortableModelAdminBase(object): diff --git a/suit/compat.py b/suit/compat.py index 5563d140..f1ebf4d2 100644 --- a/suit/compat.py +++ b/suit/compat.py @@ -7,7 +7,7 @@ """ import django -from django.template.defaulttags import url from django.contrib.contenttypes import admin as ct_admin +from django.template.defaulttags import url tpl_context_class = dict diff --git a/suit/config.py b/suit/config.py index bde2c4f7..d2662e96 100644 --- a/suit/config.py +++ b/suit/config.py @@ -1,5 +1,6 @@ -from django.contrib.admin import ModelAdmin from django.conf import settings +from django.contrib.admin import ModelAdmin + from . import VERSION @@ -55,10 +56,11 @@ def get_config(param=None): def setup_filer(): - from suit.widgets import AutosizedTextarea - from filer.admin.imageadmin import ImageAdminForm - from filer.admin.fileadmin import FileAdminChangeFrom from filer.admin import FolderAdmin + from filer.admin.fileadmin import FileAdminChangeFrom + from filer.admin.imageadmin import ImageAdminForm + + from suit.widgets import AutosizedTextarea def ensure_meta_widgets(meta_cls): if not hasattr(meta_cls, 'widgets'): diff --git a/suit/templatetags/suit_compat.py b/suit/templatetags/suit_compat.py index bf3f26a5..8fbc8b0c 100644 --- a/suit/templatetags/suit_compat.py +++ b/suit/templatetags/suit_compat.py @@ -1,4 +1,5 @@ from django import template + from ..compat import url as url_compat register = template.Library() diff --git a/suit/templatetags/suit_tags.py b/suit/templatetags/suit_tags.py index 34779731..ad3e7910 100644 --- a/suit/templatetags/suit_tags.py +++ b/suit/templatetags/suit_tags.py @@ -1,11 +1,13 @@ import itertools + from django import template from django.core.exceptions import ObjectDoesNotExist from django.db.models import ForeignKey from django.template.defaulttags import NowNode from django.utils.safestring import mark_safe -from suit.config import get_config + from suit import utils +from suit.config import get_config try: from django.core.urlresolvers import NoReverseMatch, reverse diff --git a/suit/tests/config.py b/suit/tests/config.py index 0b7a7077..0d7bba73 100644 --- a/suit/tests/config.py +++ b/suit/tests/config.py @@ -1,10 +1,11 @@ -from django.contrib.admin import ModelAdmin from django.conf import settings +from django.contrib.admin import ModelAdmin + from suit import VERSION from suit.config import default_config, get_config from suit.templatetags.suit_tags import admin_url +from suit.tests.mixins import ModelsTestCaseMixin, UserTestCaseMixin from suit.tests.models import Book -from suit.tests.mixins import UserTestCaseMixin, ModelsTestCaseMixin class ConfigTestCase(UserTestCaseMixin): diff --git a/suit/tests/mixins.py b/suit/tests/mixins.py index d862e7d5..8c8e4b84 100644 --- a/suit/tests/mixins.py +++ b/suit/tests/mixins.py @@ -1,9 +1,9 @@ +from random import randint + from django.conf import settings from django.contrib.auth.models import User -from django.core.management import CommandError -from django.core.management import call_command +from django.core.management import call_command, CommandError from django.test import TestCase -from random import randint # Django 1.7 compatiblity try: diff --git a/suit/tests/templates/form_tabs.py b/suit/tests/templates/form_tabs.py index 7cd0b0c6..e31c3ce7 100644 --- a/suit/tests/templates/form_tabs.py +++ b/suit/tests/templates/form_tabs.py @@ -1,5 +1,6 @@ from django.contrib import admin from django.utils.translation import gettext + from suit.tests.mixins import ModelsTestCaseMixin, UserTestCaseMixin from suit.tests.models import Book, BookAdmin, test_app_label diff --git a/suit/tests/test.py b/suit/tests/test.py index 7bfced62..e69de29b 100644 --- a/suit/tests/test.py +++ b/suit/tests/test.py @@ -1,3 +0,0 @@ -from .templatetags import * -from .templates import * -from . import * diff --git a/suit/tests/utils.py b/suit/tests/utils.py index 9b3d1cb3..75d4c9c2 100644 --- a/suit/tests/utils.py +++ b/suit/tests/utils.py @@ -1,7 +1,7 @@ from django import get_version -from suit import utils from django.test import TestCase +from suit import utils from suit.templatetags.suit_tags import str_to_version diff --git a/suit/watch_less.py b/suit/watch_less.py index e5f2ec96..d955d8eb 100644 --- a/suit/watch_less.py +++ b/suit/watch_less.py @@ -1,9 +1,9 @@ -import sys import os +import sys import time +from watchdog.events import FileModifiedEvent, FileSystemEventHandler from watchdog.observers import Observer -from watchdog.events import FileSystemEventHandler, FileModifiedEvent class LessCompiler(FileSystemEventHandler): @@ -17,7 +17,6 @@ def compile_css(self): else: destination = sys.argv[2] cmd = 'lessc %s > %s -x' % (source, os.path.abspath(destination)) - print(cmd) os.system(cmd) def on_any_event(self, event):