From 682731f1a2f7a488b22eb3a9a3cd4a4dbba1865a Mon Sep 17 00:00:00 2001 From: Fabian Braun Date: Thu, 6 Mar 2025 22:24:15 +0100 Subject: [PATCH] Upgrade project meta data and actions --- .github/workflows/lint.yml | 2 +- .pre-commit-config.yaml | 23 ++-- aldryn_config.py | 6 +- docs/conf.py | 2 +- filer/admin/patched/admin_utils.py | 4 +- filer/cache.py | 2 +- .../commands/generate_thumbnails.py | 6 +- filer/management/commands/import_files.py | 8 +- filer/migrations/0001_initial.py | 2 +- filer/server/backends/default.py | 2 +- filer/utils/files.py | 35 +++++- pyproject.toml | 105 ++++++++++++++++++ setup.cfg | 30 ----- setup.py | 75 +------------ tests/helpers.py | 2 +- tests/test_models.py | 2 +- tests/test_server_backends.py | 3 + tests/utils/test_app/admin.py | 4 +- tox.ini | 4 +- 19 files changed, 178 insertions(+), 139 deletions(-) create mode 100644 pyproject.toml delete mode 100644 setup.cfg diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 6dd355980..701eca22a 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -14,7 +14,7 @@ jobs: with: python-version: 3.9 - name: Install flake8 - run: pip install --upgrade flake8 + run: pip install --upgrade flake8 flake8-pyproject - name: Run flake8 uses: liskin/gh-problem-matcher-wrap@v1 with: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6e83a27bf..6a89b3a48 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,22 +8,23 @@ ci: autoupdate_schedule: monthly repos: -# - repo: https://github.com/asottile/pyupgrade -# rev: v2.37.3 -# hooks: -# - id: pyupgrade -# args: ["--py36-plus"] -# -# - repo: https://github.com/adamchainz/django-upgrade -# rev: '1.7.0' -# hooks: -# - id: django-upgrade -# args: [--target-version, "2.2"] + - repo: https://github.com/asottile/pyupgrade + rev: v2.37.3 + hooks: + - id: pyupgrade + args: ["--py39-plus"] + + - repo: https://github.com/adamchainz/django-upgrade + rev: '1.7.0' + hooks: + - id: django-upgrade + args: [--target-version, "3.2"] - repo: https://github.com/PyCQA/flake8 rev: 7.1.1 hooks: - id: flake8 + additional_dependencies: [flake8-pyproject] - repo: https://github.com/asottile/yesqa rev: v1.5.0 diff --git a/aldryn_config.py b/aldryn_config.py index 3572d4ec2..2747035ce 100644 --- a/aldryn_config.py +++ b/aldryn_config.py @@ -24,7 +24,7 @@ def to_settings(self, data, settings): settings.setdefault('MEDIA_HEADERS', []).insert(0, ( r'filer_public(?:_thumbnails)?/.*', { - 'Cache-Control': 'public, max-age={}'.format(86400 * 365), + 'Cache-Control': f'public, max-age={86400 * 365}', }, )) @@ -34,12 +34,12 @@ def to_settings(self, data, settings): settings['THUMBNAIL_CACHE_DIMENSIONS'] = True # Swap scale and crop for django-filer version - settings['THUMBNAIL_PROCESSORS'] = tuple([ + settings['THUMBNAIL_PROCESSORS'] = tuple( processor if processor != 'easy_thumbnails.processors.scale_and_crop' else 'filer.thumbnail_processors.scale_and_crop_with_subject_location' for processor in EasyThumbnailSettings.THUMBNAIL_PROCESSORS - ]) + ) # easy_thumbnails uses django's default storage backend (local file # system storage) by default, even if the DEFAULT_FILE_STORAGE setting diff --git a/docs/conf.py b/docs/conf.py index 6cfd2285e..8f8651e43 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -57,7 +57,7 @@ # General information about the project. project = 'django-filer' -copyright = '%s, Stefan Foulis' % (datetime.date.today().year,) +copyright = f'{datetime.date.today().year}, Stefan Foulis' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/filer/admin/patched/admin_utils.py b/filer/admin/patched/admin_utils.py index 84602d5e5..1beebf9a8 100644 --- a/filer/admin/patched/admin_utils.py +++ b/filer/admin/patched/admin_utils.py @@ -40,7 +40,7 @@ def format_callback(obj): has_admin = obj.__class__ in admin_site._registry opts = obj._meta - no_edit_link = '%s: %s' % (capfirst(opts.verbose_name), + no_edit_link = '{}: {}'.format(capfirst(opts.verbose_name), force_str(obj)) if has_admin: @@ -54,7 +54,7 @@ def format_callback(obj): # Change url doesn't exist -- don't display link to edit return no_edit_link - p = '%s.%s' % (opts.app_label, + p = '{}.{}'.format(opts.app_label, get_permission_codename('delete', opts)) if not user.has_perm(p): perms_needed.add(opts.verbose_name) diff --git a/filer/cache.py b/filer/cache.py index b5d4fc441..f4e0497f6 100644 --- a/filer/cache.py +++ b/filer/cache.py @@ -71,7 +71,7 @@ def clear_folder_permission_cache(user: UserModel, permission: typing.Optional[s cache.delete(get_folder_perm_cache_key(user, permission)) -def update_folder_permission_cache(user: UserModel, permission: str, id_list: typing.List[int]) -> None: +def update_folder_permission_cache(user: UserModel, permission: str, id_list: list[int]) -> None: """ Updates the cached folder permissions for a given user and permission. diff --git a/filer/management/commands/generate_thumbnails.py b/filer/management/commands/generate_thumbnails.py index 64bf3df2f..c63816e5c 100644 --- a/filer/management/commands/generate_thumbnails.py +++ b/filer/management/commands/generate_thumbnails.py @@ -17,12 +17,12 @@ def handle(self, *args, **options): image = None try: image = Image.objects.get(pk=pk) - self.stdout.write(u'Processing image {0} / {1} {2}'.format(idx + 1, total, image)) + self.stdout.write(f'Processing image {idx + 1} / {total} {image}') self.stdout.flush() image.thumbnails image.icons - except IOError as e: - self.stderr.write('Failed to generate thumbnails: {0}'.format(str(e))) + except OSError as e: + self.stderr.write(f'Failed to generate thumbnails: {str(e)}') self.stderr.flush() finally: del image diff --git a/filer/management/commands/import_files.py b/filer/management/commands/import_files.py index ff71705e1..098f63798 100644 --- a/filer/management/commands/import_files.py +++ b/filer/management/commands/import_files.py @@ -47,7 +47,7 @@ def import_file(self, file_obj, folder): if created: self.file_created += 1 if self.verbosity >= 2: - print("file_created #%s / image_created #%s -- file : %s -- created : %s" % (self.file_created, + print("file_created #{} / image_created #{} -- file : {} -- created : {}".format(self.file_created, self.image_created, obj, created)) return obj @@ -70,7 +70,7 @@ def get_or_create_folder(self, folder_names): if created: self.folder_created += 1 if self.verbosity >= 2: - print("folder_created #%s folder : %s -- created : %s" % (self.folder_created, current_parent, created)) + print(f"folder_created #{self.folder_created} folder : {current_parent} -- created : {created}") return current_parent def walker(self, path=None, base_folder=None): @@ -84,9 +84,9 @@ def walker(self, path=None, base_folder=None): path = os.path.normpath(path) if base_folder: base_folder = os.path.normpath(base_folder) - print("The directory structure will be imported in %s" % (base_folder,)) + print(f"The directory structure will be imported in {base_folder}") if self.verbosity >= 1: - print("Import the folders and files in %s" % (path,)) + print(f"Import the folders and files in {path}") root_folder_name = os.path.basename(path) for root, dirs, files in os.walk(path): rel_folders = root.partition(path)[2].strip(os.path.sep).split(os.path.sep) diff --git a/filer/migrations/0001_initial.py b/filer/migrations/0001_initial.py index 3a8e3a499..baebf9cc6 100644 --- a/filer/migrations/0001_initial.py +++ b/filer/migrations/0001_initial.py @@ -104,7 +104,7 @@ class Migration(migrations.Migration): ), migrations.AlterUniqueTogether( name='folder', - unique_together=set([('parent', 'name')]), + unique_together={('parent', 'name')}, ), migrations.AddField( model_name='file', diff --git a/filer/server/backends/default.py b/filer/server/backends/default.py index 2aed251d4..d6c6219c6 100644 --- a/filer/server/backends/default.py +++ b/filer/server/backends/default.py @@ -24,7 +24,7 @@ def serve(self, request, filer_file, **kwargs): # Respect the If-Modified-Since header. statobj = os.stat(fullpath) response_params = {'content_type': filer_file.mime_type} - if not was_modified_since(request.META.get('HTTP_IF_MODIFIED_SINCE'), + if not was_modified_since(request.headers.get('if-modified-since'), statobj[stat.ST_MTIME]): return HttpResponseNotModified(**response_params) response = HttpResponse(open(fullpath, 'rb').read(), **response_params) diff --git a/filer/utils/files.py b/filer/utils/files.py index c9732843d..837fe24d6 100644 --- a/filer/utils/files.py +++ b/filer/utils/files.py @@ -1,5 +1,6 @@ import mimetypes import os +import uuid from django.http.multipartparser import ChunkIter, SkipFile, StopFutureHandlers, StopUpload, exhaust from django.template.defaultfilters import slugify as slugify_django @@ -121,6 +122,33 @@ def slugify(string): return slugify_django(force_str(string)) +def _ensure_safe_length(filename, max_length=155, random_suffix_length=16): + """ + Ensures that the filename does not exceed the maximum allowed length. + If it does, the function truncates the filename and appends a random hexadecimal + suffix of length `random_suffix_length` to ensure uniqueness and compliance with + database constraints - even after markers for a thumbnail are added. + + Parameters: + filename (str): The filename to check. + max_length (int): The maximum allowed length for the filename. + random_suffix_length (int): The length of the random suffix to append. + + Returns: + str: The safe filename. + + + Reference issue: https://github.com/django-cms/django-filer/issues/1270 + """ + + if len(filename) <= max_length: + return filename + + keep_length = max_length - random_suffix_length + random_suffix = uuid.uuid4().hex[:random_suffix_length] + return filename[:keep_length] + random_suffix + + def get_valid_filename(s): """ like the regular get_valid_filename, but also slugifies away @@ -131,6 +159,9 @@ def get_valid_filename(s): filename = slugify(filename) ext = slugify(ext) if ext: - return "{}.{}".format(filename, ext) + valid_filename = f"{filename}.{ext}" else: - return "{}".format(filename) + valid_filename = f"{filename}" + + # Ensure the filename meets the maximum length requirements. + return _ensure_safe_length(valid_filename) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..4e4d2ed4d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,105 @@ +[build-system] +requires = ["setuptools>=42", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "django-filer" +description = "A file management application for django that makes handling of files and images a breeze." +requires-python = ">=3.8" +license = {text = "BSD-3-Clause"} +authors = [ + {name = "Divio AG", email = "info@divio.ch"}, +] +maintainers = [ + {name = "Django CMS Association and contributors", email = "info@django-cms.org"}, +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Framework :: Django", + "Framework :: Django :: 3.2", + "Framework :: Django :: 4.0", + "Framework :: Django :: 4.1", + "Framework :: Django :: 4.2", + "Framework :: Django :: 5.0", + "Framework :: Django :: 5.1", + "Framework :: Django CMS", + "Framework :: Django CMS :: 3.8", + "Framework :: Django CMS :: 3.9", + "Framework :: Django CMS :: 3.10", + "Framework :: Django CMS :: 3.11", + "Framework :: Django CMS :: 4.0", + "Framework :: Django CMS :: 4.1", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: Dynamic Content", + "Topic :: Software Development", + "Topic :: Software Development :: Libraries", +] +dynamic = ["version", "readme"] +dependencies = [ + "django>=3.2", + "django-polymorphic", + "easy-thumbnails[svg]", +] +[project.urls] +Homepage = "https://github.com/django-cms/django-filer" + + +[project.optional-dependencies] +heif = ["pillow-heif"] + +[tool.setuptools] +include-package-data = true +zip-safe = false + +[tool.setuptools.packages.find] +where = ["."] +include = ["filer*"] +exclude = ["tests*"] +namespaces = false + +[tool.setuptools.dynamic] +version = {attr = "filer.__version__"} +readme = {file = ["README.rst"]} + +[tool.flake8] +max-line-length = 119 +exclude = [ + "*.egg-info", + ".eggs", + ".env", + ".git", + ".settings", + ".tox", + ".venv", + "build", + "data", + "dist", + "docs", + "*migrations*", + "tmp", + "node_modules", +] +ignore = ["E251", "E128", "E501", "W503"] + +[tool.isort] +line_length = 119 +skip = ["manage.py", "*migrations*", ".tox", ".eggs", "data", ".env", ".venv"] +include_trailing_comma = true +multi_line_output = 5 +lines_after_imports = 2 +default_section = "THIRDPARTY" +sections = ["FUTURE", "STDLIB", "DJANGO", "CMS", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"] +known_first_party = ["filer"] +known_cms = ["cms", "menus"] +known_django = ["django"] \ No newline at end of file diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 6f03acdee..000000000 --- a/setup.cfg +++ /dev/null @@ -1,30 +0,0 @@ -[flake8] -max-line-length = 119 -exclude = - *.egg-info, - .eggs, - .env, - .git, - .settings, - .tox, - .venv, - build, - data, - dist, - docs, - *migrations*, - tmp, - node_modules -ignore = E251,E128,E501,W503 - -[isort] -line_length = 119 -skip = manage.py, *migrations*, .tox, .eggs, data, .env, .venv -include_trailing_comma = true -multi_line_output = 5 -lines_after_imports = 2 -default_section = THIRDPARTY -sections = FUTURE, STDLIB, DJANGO, CMS, THIRDPARTY, FIRSTPARTY, LOCALFOLDER -known_first_party = filer -known_cms = cms, menus -known_django = django diff --git a/setup.py b/setup.py index e4569a756..c82334553 100644 --- a/setup.py +++ b/setup.py @@ -1,75 +1,4 @@ #!/usr/bin/env python -from setuptools import find_packages, setup +from setuptools import setup -from filer import __version__ - - -REQUIREMENTS = [ - 'django>=3.2', - 'django-polymorphic', - 'easy-thumbnails[svg]', -] - - -EXTRA_REQUIREMENTS = { - "heif": [ - "pillow-heif", - ], -} - - -CLASSIFIERS = [ - 'Development Status :: 5 - Production/Stable', - 'Environment :: Web Environment', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3.12', - 'Framework :: Django', - 'Framework :: Django :: 3.2', - 'Framework :: Django :: 4.0', - 'Framework :: Django :: 4.1', - 'Framework :: Django :: 4.2', - 'Framework :: Django :: 5.0', - 'Framework :: Django :: 5.1', - 'Framework :: Django CMS', - 'Framework :: Django CMS :: 3.8', - 'Framework :: Django CMS :: 3.9', - 'Framework :: Django CMS :: 3.10', - 'Framework :: Django CMS :: 3.11', - 'Framework :: Django CMS :: 4.0', - 'Framework :: Django CMS :: 4.1', - 'Topic :: Internet :: WWW/HTTP', - 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', - 'Topic :: Software Development', - 'Topic :: Software Development :: Libraries', -] - - -setup( - name='django-filer', - version=__version__, - author='Divio AG', - author_email='info@divio.ch', - maintainer='Django CMS Association and contributors', - maintainer_email='info@django-cms.org', - url='https://github.com/django-cms/django-filer', - license='BSD-3-Clause', - description='A file management application for django that makes handling ' - 'of files and images a breeze.', - long_description=open('README.rst').read(), - packages=find_packages(), - include_package_data=True, - zip_safe=False, - install_requires=REQUIREMENTS, - extras_require=EXTRA_REQUIREMENTS, - python_requires='>=3.8', - classifiers=CLASSIFIERS, - test_suite='tests.settings.run', -) +setup() diff --git a/tests/helpers.py b/tests/helpers.py index 506a367de..0bcc5fab7 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -25,7 +25,7 @@ def create_folder_structure(depth=2, sibling=2, parent=None): depth_range.reverse() for d in depth_range: for s in range(1, sibling + 1): - name = "folder: {} -- {}".format(str(d), str(s)) + name = f"folder: {str(d)} -- {str(s)}" folder = Folder(name=name, parent=parent) folder.save() create_folder_structure(depth=d - 1, sibling=sibling, parent=folder) diff --git a/tests/test_models.py b/tests/test_models.py index 3d52e2502..966745fc1 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -87,7 +87,7 @@ def test_create_icons(self): self.assertEqual(len(icons), len(filer_settings.FILER_ADMIN_ICON_SIZES)) for size in filer_settings.FILER_ADMIN_ICON_SIZES: self.assertEqual(os.path.basename(icons[size]), - file_basename + '__{}x{}_q85_crop_subsampling-2_upscale.jpg'.format(size, size)) + file_basename + f'__{size}x{size}_q85_crop_subsampling-2_upscale.jpg') def test_access_icons_property(self): """Test IconsMixin that calls static on a non-existent file""" diff --git a/tests/test_server_backends.py b/tests/test_server_backends.py index 72e60f4e8..de05a2f9c 100644 --- a/tests/test_server_backends.py +++ b/tests/test_server_backends.py @@ -40,6 +40,7 @@ def test_normal(self): server = DefaultServer() request = Mock() request.META = {} + request.headers = {} response = server.serve(request, self.filer_file) self.assertTrue(response.has_header('Last-Modified')) @@ -47,6 +48,7 @@ def test_save_as(self): server = DefaultServer() request = Mock() request.META = {} + request.headers = {} response = server.serve(request, self.filer_file, save_as=True) self.assertEqual(response['Content-Disposition'], 'attachment; filename=testimage.jpg') @@ -60,6 +62,7 @@ def test_not_modified(self): server = DefaultServer() request = Mock() request.META = {'HTTP_IF_MODIFIED_SINCE': http_date(time.time())} + request.headers = {'if-modified-since': http_date(time.time())} response = server.serve(request, self.filer_file) self.assertTrue(isinstance(response, HttpResponseNotModified)) diff --git a/tests/utils/test_app/admin.py b/tests/utils/test_app/admin.py index f1c73ed51..2b6b6b44d 100644 --- a/tests/utils/test_app/admin.py +++ b/tests/utils/test_app/admin.py @@ -3,8 +3,6 @@ from .models import MyModel +@admin.register(MyModel) class MyModelAdmin(admin.ModelAdmin): model = MyModel - - -admin.site.register(MyModel, MyModelAdmin) diff --git a/tox.ini b/tox.ini index d04a03bd2..edb49ea10 100644 --- a/tox.ini +++ b/tox.ini @@ -31,7 +31,9 @@ setenv = swap: CUSTOM_IMAGE=custom_image.Image [testenv:flake8] -deps = flake8 +deps = + flake8 + flake8-pyproject commands = flake8 [testenv:isort]