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)