diff --git a/.github/workflows/publish-to-live-pypi.yml b/.github/workflows/publish-to-live-pypi.yml
index 1607f7724..928076bef 100644
--- a/.github/workflows/publish-to-live-pypi.yml
+++ b/.github/workflows/publish-to-live-pypi.yml
@@ -9,12 +9,17 @@ jobs:
build-n-publish:
name: Build and publish Python 🐍 distributions 📦 to pypi
runs-on: ubuntu-latest
+ environment:
+ name: pypi
+ url: https://pypi.org/p/django-filer
+ permissions:
+ id-token: write
steps:
- - uses: actions/checkout@master
- - name: Set up Python 3.9
- uses: actions/setup-python@v1
+ - uses: actions/checkout@v4
+ - name: Set up Python
+ uses: actions/setup-python@v4
with:
- python-version: 3.9
+ python-version: '3.12'
- name: Install pypa/build
run: >-
@@ -33,7 +38,4 @@ jobs:
- name: Publish distribution 📦 to PyPI
if: startsWith(github.ref, 'refs/tags')
- uses: pypa/gh-action-pypi-publish@master
- with:
- user: __token__
- password: ${{ secrets.PYPI_API_TOKEN }}
+ uses: pypa/gh-action-pypi-publish@release/v1
diff --git a/.github/workflows/publish-to-test-pypi.yml b/.github/workflows/publish-to-test-pypi.yml
index d590f480e..bf43b73e6 100644
--- a/.github/workflows/publish-to-test-pypi.yml
+++ b/.github/workflows/publish-to-test-pypi.yml
@@ -9,12 +9,17 @@ jobs:
build-n-publish:
name: Build and publish Python 🐍 distributions 📦 to TestPyPI
runs-on: ubuntu-latest
+ environment:
+ name: test
+ url: https://test.pypi.org/p/django-filer
+ permissions:
+ id-token: write
steps:
- - uses: actions/checkout@master
- - name: Set up Python 3.9
- uses: actions/setup-python@v1
+ - uses: actions/checkout@v4
+ - name: Set up Python
+ uses: actions/setup-python@v4
with:
- python-version: 3.9
+ python-version: '3.12'
- name: Install pypa/build
run: >-
@@ -32,9 +37,7 @@ jobs:
.
- name: Publish distribution 📦 to Test PyPI
- uses: pypa/gh-action-pypi-publish@master
+ uses: pypa/gh-action-pypi-publish@release/v1
with:
- user: __token__
- password: ${{ secrets.TEST_PYPI_API_TOKEN }}
- repository_url: https://test.pypi.org/legacy/
- skip_existing: true
+ repository-url: https://test.pypi.org/legacy/
+ skip-existing: true
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 89ba6d406..9fbfeca42 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -31,7 +31,7 @@ repos:
- id: yesqa
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.4.0
+ rev: v4.5.0
hooks:
- id: check-merge-conflict
- id: mixed-line-ending
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index ce4ec4140..cb2f3909e 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -2,6 +2,14 @@
CHANGELOG
=========
+3.1.1 (2023-11-18)
+==================
+
+* fix: Added compatibility code in aldryn_config go support setting THUMBNAIL_DEFAULT_STORAGE in django 4.2
+* fix: address failing gulp ci jobs
+* feat: Image dimensions update management command
+* ci: pre-commit autoupdate
+
3.1.0 (2023-10-01)
==================
diff --git a/aldryn_config.py b/aldryn_config.py
index 145fe1ded..3572d4ec2 100644
--- a/aldryn_config.py
+++ b/aldryn_config.py
@@ -47,7 +47,12 @@ def to_settings(self, data, settings):
# If the DEFAULT_FILE_STORAGE has been set to a value known by
# aldryn-django, then use that as THUMBNAIL_DEFAULT_STORAGE as well.
for storage_backend in storage.SCHEMES.values():
- if storage_backend == settings['DEFAULT_FILE_STORAGE']:
+ # Process before django 4.2
+ if storage_backend == settings.get('DEFAULT_FILE_STORAGE', None):
+ settings['THUMBNAIL_DEFAULT_STORAGE'] = storage_backend
+ break
+ # Process django 4.2 and after
+ if storage_backend == settings.get('STORAGES', {}).get('default', {}).get('BACKEND', None):
settings['THUMBNAIL_DEFAULT_STORAGE'] = storage_backend
break
return settings
diff --git a/filer/__init__.py b/filer/__init__.py
index 839815895..badb62cbd 100644
--- a/filer/__init__.py
+++ b/filer/__init__.py
@@ -13,4 +13,4 @@
8. Publish the release and it will automatically release to pypi
"""
-__version__ = '3.1.0'
+__version__ = '3.1.1'
diff --git a/filer/management/commands/filer_check.py b/filer/management/commands/filer_check.py
index 3a2de61b3..3eb77d12b 100644
--- a/filer/management/commands/filer_check.py
+++ b/filer/management/commands/filer_check.py
@@ -4,6 +4,8 @@
from django.core.management.base import BaseCommand
from django.utils.module_loading import import_string
+from PIL import UnidentifiedImageError
+
from filer import settings as filer_settings
@@ -41,6 +43,13 @@ def add_arguments(self, parser):
default=False,
help="Delete references in database if files are missing in media folder.",
)
+ parser.add_argument(
+ '--image-dimensions',
+ action='store_true',
+ dest='image_dimensions',
+ default=False,
+ help="Look for images without dimensions set, set them accordingly.",
+ )
parser.add_argument(
'--noinput',
'--no-input',
@@ -72,6 +81,8 @@ def handle(self, *args, **options):
self.stdout.write("Aborted: Delete orphaned files from storage.")
return
self.verify_storages(options)
+ if options['image_dimensions']:
+ self.image_dimensions(options)
def verify_references(self, options):
from filer.models.filemodels import File
@@ -112,3 +123,41 @@ def walk(prefix):
filer_public = filer_settings.FILER_STORAGES['public']['main']
storage = import_string(filer_public['ENGINE'])()
walk(filer_public['UPLOAD_TO_PREFIX'])
+
+ def image_dimensions(self, options):
+ from django.db.models import Q
+
+ import easy_thumbnails
+ from easy_thumbnails.VIL import Image as VILImage
+
+ from filer.models.imagemodels import Image
+ from filer.utils.compatibility import PILImage
+
+ no_dimensions = Image.objects.filter(
+ Q(_width=0) | Q(_width__isnull=True)
+ )
+ self.stdout.write(f"trying to set dimensions on {no_dimensions.count()} files")
+ for image in no_dimensions:
+ if image.file_ptr:
+ file_holder = image.file_ptr
+ else:
+ file_holder = image
+ try:
+ imgfile = file_holder.file
+ imgfile.seek(0)
+ except (FileNotFoundError):
+ pass
+ else:
+ if image.file.name.lower().endswith('.svg'):
+ with VILImage.load(imgfile) as vil_image:
+ # invalid svg doesnt throw errors
+ image._width, image._height = vil_image.size
+ else:
+ try:
+ with PILImage.open(imgfile) as pil_image:
+ image._width, image._height = pil_image.size
+ image._transparent = easy_thumbnails.utils.is_transparent(pil_image)
+ except UnidentifiedImageError:
+ continue
+ image.save()
+ return
diff --git a/filer/static/filer/js/addons/dropzone.init.js b/filer/static/filer/js/addons/dropzone.init.js
index d3c7ff67c..fefe90105 100644
--- a/filer/static/filer/js/addons/dropzone.init.js
+++ b/filer/static/filer/js/addons/dropzone.init.js
@@ -163,7 +163,8 @@ djQuery(function ($) {
// Handle initialization of the dropzone on dynamic formsets (i.e. Django admin inlines)
$(document).on('formset:added', function (ev, row) {
- if(ev.detail && ev.detail.formsetName) {
+ var dropzones, rowIdx, row_;
+ if (ev.detail && ev.detail.formsetName) {
/*
Django 4.1 changed the event type being fired when adding
a new formset from a jQuery to a vanilla JavaScript event.
@@ -172,16 +173,17 @@ djQuery(function ($) {
In this case we find the newly added row and initialize the
dropzone on any dropzoneSelector on that row.
*/
- let rowIdx = parseInt(
+
+ rowIdx = parseInt(
document.getElementById(
'id_' + event.detail.formsetName + '-TOTAL_FORMS'
).value, 10
) - 1;
- let row_ = document.getElementById(event.detail.formsetName + '-' + rowIdx);
- var dropzones = $(row_).find(dropzoneSelector)
+ row_ = document.getElementById(event.detail.formsetName + '-' + rowIdx);
+ dropzones = $(row_).find(dropzoneSelector);
} else {
- var dropzones = $(row).find(dropzoneSelector);
+ dropzones = $(row).find(dropzoneSelector);
}
dropzones.each(createDropzone);
diff --git a/tests/test_admin.py b/tests/test_admin.py
index 5647d098f..ce5a23ad1 100644
--- a/tests/test_admin.py
+++ b/tests/test_admin.py
@@ -348,7 +348,7 @@ def test_image_expand_view(self):
self.assertContains(
response,
- f""""""
+ f""""""
)
diff --git a/tests/test_filer_check.py b/tests/test_filer_check.py
index 01b951ca0..f18bd389c 100644
--- a/tests/test_filer_check.py
+++ b/tests/test_filer_check.py
@@ -1,6 +1,6 @@
import os
import shutil
-from io import StringIO
+from io import BytesIO, StringIO
from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.management import call_command
@@ -9,10 +9,21 @@
from filer import settings as filer_settings
from filer.models.filemodels import File
+from filer.models.imagemodels import Image
from tests.helpers import create_image
class FilerCheckTestCase(TestCase):
+
+ svg_file_string = """
+
+ """
+
def setUp(self):
# ensure that filer_public directory is empty from previous tests
storage = import_string(filer_settings.FILER_STORAGES['public']['main']['ENGINE'])()
@@ -67,3 +78,81 @@ def test_delete_orphans(self):
call_command('filer_check', delete_orphans=True, interactive=False, verbosity=0)
self.assertFalse(os.path.exists(orphan_file))
+
+ def test_image_dimensions_corrupted_file(self):
+ original_filename = 'testimage.jpg'
+ file_obj = SimpleUploadedFile(
+ name=original_filename,
+ # corrupted!
+ content=create_image().tobytes(),
+ content_type='image/jpeg')
+ self.filer_image = Image.objects.create(
+ file=file_obj,
+ original_filename=original_filename)
+
+ self.filer_image._width = 0
+ self.filer_image.save()
+ call_command('filer_check', image_dimensions=True)
+
+ def test_image_dimensions_file_not_found(self):
+ self.filer_image = Image.objects.create(
+ file="123.jpg",
+ original_filename="123.jpg")
+ call_command('filer_check', image_dimensions=True)
+ self.filer_image.refresh_from_db()
+
+ def test_image_dimensions(self):
+
+ original_filename = 'testimage.jpg'
+ with BytesIO() as jpg:
+ create_image().save(jpg, format='JPEG')
+ jpg.seek(0)
+ file_obj = SimpleUploadedFile(
+ name=original_filename,
+ content=jpg.read(),
+ content_type='image/jpeg')
+ self.filer_image = Image.objects.create(
+ file=file_obj,
+ original_filename=original_filename)
+
+ self.filer_image._width = 0
+ self.filer_image.save()
+
+ call_command('filer_check', image_dimensions=True)
+ self.filer_image.refresh_from_db()
+ self.assertGreater(self.filer_image._width, 0)
+
+ def test_image_dimensions_invalid_svg(self):
+
+ original_filename = 'test.svg'
+ svg_file = bytes("" + self.svg_file_string, "utf-8")
+ file_obj = SimpleUploadedFile(
+ name=original_filename,
+ content=svg_file,
+ content_type='image/svg+xml')
+ self.filer_image = Image.objects.create(
+ file=file_obj,
+ original_filename=original_filename)
+
+ self.filer_image._width = 0
+ self.filer_image.save()
+ call_command('filer_check', image_dimensions=True)
+
+ def test_image_dimensions_svg(self):
+
+ original_filename = 'test.svg'
+ svg_file = bytes(self.svg_file_string, "utf-8")
+ file_obj = SimpleUploadedFile(
+ name=original_filename,
+ content=svg_file,
+ content_type='image/svg+xml')
+ self.filer_image = Image.objects.create(
+ file=file_obj,
+ original_filename=original_filename)
+
+ self.filer_image._width = 0
+ self.filer_image.save()
+
+ call_command('filer_check', image_dimensions=True)
+ self.filer_image.refresh_from_db()
+ self.assertGreater(self.filer_image._width, 0)