From 6f636a9f0bae5cc377dcd8b0b58220500a2a19f2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Apr 2022 21:06:47 +0000 Subject: [PATCH 01/11] Bump actions/upload-artifact from 2 to 3 Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 2 to 3. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c6569c7..3cdbc39 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,7 +57,7 @@ jobs: - uses: actions/checkout@v3 - run: python setup.py sdist bdist_wheel - run: python -m twine check dist/* - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v3 with: path: dist/* From c2aa355b7a0af30a0715062e12adb204a99119e1 Mon Sep 17 00:00:00 2001 From: J Harley <502818+julzhk@users.noreply.github.com> Date: Tue, 26 Apr 2022 15:51:21 -0300 Subject: [PATCH 02/11] minor type in readme Otherwise: spelling --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cc70313..2144030 100644 --- a/README.md +++ b/README.md @@ -191,5 +191,5 @@ python manage.py rendervariations 'app_name.model_name.field_name' [--replace] [ ``` The `replace` option will replace all existing files. The `ignore-missing` option will suspend missing source file errors and keep -rendering variations for other files. Othervise command will stop on first +rendering variations for other files. Otherwise command will stop on first missing file. From 39e2306f29863ab6cb22cb264d606afc8127cc88 Mon Sep 17 00:00:00 2001 From: J Harley <502818+julzhk@users.noreply.github.com> Date: Wed, 27 Apr 2022 10:58:09 -0300 Subject: [PATCH 03/11] Update Readme.md More extensive edit for clarity and readability --- README.md | 86 ++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 57 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 2144030..b006efc 100644 --- a/README.md +++ b/README.md @@ -4,19 +4,29 @@ # Django Standardized Image Field -Django Field that implement the following features: +## Why would I want this? -* Django-Storages compatible (S3) -* Resize images to different sizes +This is a drop-in replacement for the [Django ImageField](https://docs.djangoproject.com/en/1.8/ref/models/fields/#django.db.models.ImageField) that provides a standardized way to handle image uploads. +It is designed to be as easy to use as possible, and to provide a consistent interface for all image fields. +It allows images to be presented in various size variants (eg:thumbnails, mid, and hi-res versions) +and it provides a way to handle images that are too large with validators. + + +## Features + +Django Standardized Image Field implements the following features: + +* [Django-Storages](https://django-storages.readthedocs.io/en/latest/) compatible (eg: S3, Azure, Google Cloud Storage, etc) +* Resizes images to different sizes * Access thumbnails on model level, no template tags required -* Preserves original image -* Asynchronous rendering (Celery & Co) -* Restrict accepted image dimensions -* Rename files to a standardized name (using a callable upload_to) +* Preserves original images +* Can be rendered asynchronously (ie as a [Celery job](https://realpython.com/asynchronous-tasks-with-django-and-celery/)) +* Restricts acceptable image dimensions +* Renames file to a standardized name format (using a callable `upload_to` function, see below) ## Installation -Simply install the latest stable package using the command +Simply install the latest stable package using the following command ```bash pip install django-stdimage @@ -28,11 +38,13 @@ and add `'stdimage'` to `INSTALLED_APP`s in your settings.py, that's it! ## Usage +Now it's instally you can use either: `StdImageField` or `JPEGField`. + `StdImageField` works just like Django's own [ImageField](https://docs.djangoproject.com/en/dev/ref/models/fields/#imagefield) -except that you can specify different sized variations. +except that you can specify different size variations. -The `JPEGField` works similar to the `StdImageField` but all size variations are +The `JPEGField` is the same as the `StdImageField` but all images are converted to JPEGs, no matter what type the original file is. ### Variations @@ -58,7 +70,7 @@ class MyModel(models.Model): # is the same as dictionary-style call image = StdImageField(upload_to='path/to/img', variations={'thumbnail': (100, 75)}) - # variations are converted to JPEGs + # JPEGField variations are converted to JPEGs. jpeg = JPEGField( upload_to='path/to/img', variations={'full': (None, None), 'thumbnail': (100, 75)}, @@ -77,7 +89,7 @@ class MyModel(models.Model): }, delete_orphans=True) ``` -For using generated variations in templates use `myimagefield.variation_name`. +To use these variations in templates use `myimagefield.variation_name`. Example: @@ -85,15 +97,32 @@ Example: ``` -### Utils +### Upload to function + +You can use a function for the `upload_to` argument. Using [Django Dynamic Filenames][dynamic_filenames].[dynamic_filenames]: https://github.com/codingjoe/django-dynamic-filenames -Since version 4 the custom `upload_to` utils have been dropped in favor of -[Django Dynamic Filenames][dynamic_filenames]. +This allows images to be given unique paths and filenames based on the model instance. -[dynamic_filenames]: https://github.com/codingjoe/django-dynamic-filenames +Example + +```python +from django.db import models +from stdimage import StdImageField +from dynamic_filenames import FilePattern + +upload_to_pattern = FilePattern( + filename_pattern='my_model/{app_label:.25}/{model_name:.30}/{uuid:base32}{ext}', +) + + +class MyModel(models.Model): + # works just like django's ImageField + image = StdImageField(upload_to=upload_to_pattern) +``` ### Validators -The `StdImageField` doesn't implement any size validation. Validation can be specified using the validator attribute +The `StdImageField` doesn't implement any size validation out-of-the-box. +However, Validation can be specified using the validator attribute and using a set of validators shipped with this package. Validators can be used for both Forms and Models. @@ -120,9 +149,9 @@ Django [dropped support](https://docs.djangoproject.com/en/dev/releases/1.3/#del for automated deletions in version 1.3. Since version 5, this package supports a `delete_orphans` argument. It will delete -orphaned files, should a file be delete or replaced via Django form or and object with -a `StdImageField` be deleted. It will not be deleted if the field value is changed or -reassigned programatically. In those rare cases, you will need to handle proper deletion +orphaned files, should a file be deleted or replaced via a Django form and the object with +the `StdImageField` be deleted. It will not delete files if the field value is changed or +reassigned programatically. In these rare cases, you will need to handle proper deletion yourself. ```python @@ -141,10 +170,10 @@ class MyModel(models.Model): ### Async image processing Tools like celery allow to execute time-consuming tasks outside of the request. If you don't want -to wait for your variations to be rendered in request, StdImage provides your the option to pass a -async keyword and a util. -Note that the callback is not transaction save, but the file will be there. -This example is based on celery. +to wait for your variations to be rendered in request, StdImage provides you the option to pass an +async keyword and a 'render_variations' function that triggers the async task. +Note that the callback is not transaction save, but the file variations will be present. +The example below is based on celery. `tasks.py`: ```python @@ -177,19 +206,18 @@ def image_processor(file_name, variations, storage): class AsyncImageModel(models.Model): image = StdImageField( # above task definition can only handle one model object per image filename - upload_to='path/to/file/', + upload_to='path/to/file/', # or use a function render_variations=image_processor # pass boolean or callable ) processed = models.BooleanField(default=False) # flag that could be used for view querysets ``` ### Re-rendering variations -You might want to add new variations to a field. That means you need to render new variations for missing fields. +You might have added or changed variations to an existing field. That means you will need to render new variations. This can be accomplished using a management command. ```bash python manage.py rendervariations 'app_name.model_name.field_name' [--replace] [-i/--ignore-missing] ``` The `replace` option will replace all existing files. -The `ignore-missing` option will suspend missing source file errors and keep -rendering variations for other files. Otherwise command will stop on first -missing file. +The `ignore-missing` option will suspend 'missing source file' errors and keep +rendering variations for other files. Otherwise, the command will stop on first missing file. From bec1c82fde5958eba0743c6733ade5fafaacb0bc Mon Sep 17 00:00:00 2001 From: J Harley <502818+julzhk@users.noreply.github.com> Date: Tue, 3 May 2022 09:08:34 -0300 Subject: [PATCH 04/11] Update README.md Co-authored-by: Johannes Maron --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b006efc..e1d3f63 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Django Standardized Image Field implements the following features: * Preserves original images * Can be rendered asynchronously (ie as a [Celery job](https://realpython.com/asynchronous-tasks-with-django-and-celery/)) * Restricts acceptable image dimensions -* Renames file to a standardized name format (using a callable `upload_to` function, see below) +* Renames a file to a standardized name format (using a callable `upload_to` function, see below) ## Installation From bed5fed42062d14a43c55c1335cbbf3d3f05ed49 Mon Sep 17 00:00:00 2001 From: J Harley <502818+julzhk@users.noreply.github.com> Date: Tue, 3 May 2022 09:08:48 -0300 Subject: [PATCH 05/11] Update README.md Co-authored-by: Johannes Maron --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e1d3f63..cac50c5 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Django Standardized Image Field implements the following features: ## Installation -Simply install the latest stable package using the following command +Simply install the latest stable package using the following command: ```bash pip install django-stdimage From f6d953b0cd480a037964b259d379e8d1b2ab9b88 Mon Sep 17 00:00:00 2001 From: J Harley <502818+julzhk@users.noreply.github.com> Date: Tue, 3 May 2022 09:09:14 -0300 Subject: [PATCH 06/11] Update README.md Co-authored-by: Johannes Maron --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cac50c5..c602c24 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ Now it's instally you can use either: `StdImageField` or `JPEGField`. [ImageField](https://docs.djangoproject.com/en/dev/ref/models/fields/#imagefield) except that you can specify different size variations. -The `JPEGField` is the same as the `StdImageField` but all images are +The `JPEGField` is identical to the `StdImageField` but all images are converted to JPEGs, no matter what type the original file is. ### Variations From 7db39615f709dccf839f90503b2baa76cbd48896 Mon Sep 17 00:00:00 2001 From: Johannes Maron Date: Sat, 21 May 2022 11:53:17 +0200 Subject: [PATCH 07/11] Update CI pipline (#288) --- .github/workflows/ci.yml | 27 ++++++++++++--------------- .github/workflows/release.yml | 10 ++++++---- setup.cfg | 16 +++++++--------- 3 files changed, 25 insertions(+), 28 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3cdbc39..3070c08 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,16 +46,14 @@ jobs: - run: python -m pip install -r lint-requirements.txt - run: ${{ matrix.lint-command }} - dist: runs-on: ubuntu-latest steps: - - name: Install gettext - run: sudo apt install gettext -y - - uses: actions/setup-python@v3 - - run: python -m pip install --upgrade pip setuptools wheel twine readme-renderer - uses: actions/checkout@v3 - - run: python setup.py sdist bdist_wheel + - uses: actions/setup-python@v3 + - run: sudo apt install gettext -y + - run: python -m pip install --upgrade pip build wheel twine readme-renderer + - run: python -m build --sdist --wheel - run: python -m twine check dist/* - uses: actions/upload-artifact@v3 with: @@ -73,11 +71,11 @@ jobs: - "3.9" - "3.10" django-version: - - "3.2a" - - "4.0a" + - "3.2" + - "4.0" extra: - - "" - - "progressbar" + - "test" + - "test,progressbar" steps: - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v3 @@ -85,10 +83,9 @@ jobs: python-version: ${{ matrix.python-version }} - run: sudo apt install gettext -y - uses: actions/checkout@v3 - - run: python -m pip install --upgrade pip setuptools codecov wheel - - run: python -m pip install .[${{ matrix.extra }}] + - run: python -m pip install --upgrade pip codecov + - run: python -m pip install -e .[${{ matrix.extra }}] if: ${{ matrix.extra }} - - run: python -m pip install django~=${{ matrix.django-version }} - - name: Test with pytest - run: python setup.py test + - run: python -m pip install django~=${{ matrix.django-version }}a + - run: python -m pytest - run: codecov diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ff70343..19f0b4e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,10 +1,12 @@ -name: PyPi Release +name: Release -on: [release] +on: + release: + types: [published] jobs: - build: + PyPi: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 @@ -14,5 +16,5 @@ jobs: - run: python -m build --sdist --wheel - run: python -m twine upload dist/* env: - TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }} + TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} diff --git a/setup.cfg b/setup.cfg index 10e8e2b..e6adaf7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -38,11 +38,6 @@ install_requires = setup_requires = setuptools_scm - pytest-runner -tests_require = - pytest - pytest-cov - pytest-django [options.package_data] * = *.txt, *.rst, *.html, *.po @@ -52,20 +47,23 @@ exclude = tests [options.extras_require] +test = + pytest + pytest-cov + pytest-django progressbar = progressbar2>=3.0.0 [bdist_wheel] universal = 1 -[aliases] -test = pytest - [tool:pytest] +testpaths = + tests norecursedirs=venv env .eggs DJANGO_SETTINGS_MODULE=tests.settings addopts = --cov=stdimage --nomigrations --tb=short filterwarnings = - error + ignore:DeprecationWarning [coverage:run] source = . From 62dbd15f1e1e6b07199fbe2be552a81d73053842 Mon Sep 17 00:00:00 2001 From: Johannes Maron Date: Sat, 21 May 2022 11:55:00 +0200 Subject: [PATCH 08/11] Fix Pillow ANTIALIAS deprecation warning --- setup.cfg | 2 +- stdimage/models.py | 3 ++- tests/test_utils.py | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/setup.cfg b/setup.cfg index e6adaf7..d93790f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -63,7 +63,7 @@ norecursedirs=venv env .eggs DJANGO_SETTINGS_MODULE=tests.settings addopts = --cov=stdimage --nomigrations --tb=short filterwarnings = - ignore:DeprecationWarning + error [coverage:run] source = . diff --git a/stdimage/models.py b/stdimage/models.py index 97ff514..ff13cff 100644 --- a/stdimage/models.py +++ b/stdimage/models.py @@ -11,6 +11,7 @@ ImageFileDescriptor, ) from PIL import Image, ImageFile, ImageOps +from PIL.Image import Resampling from .validators import MinSizeValidator @@ -186,7 +187,7 @@ class StdImageField(ImageField): "width": None, "height": None, "crop": False, - "resample": Image.ANTIALIAS, + "resample": Resampling.LANCZOS, } def __init__( diff --git a/tests/test_utils.py b/tests/test_utils.py index 2ab2c68..7180204 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,7 +1,7 @@ import os import pytest -from PIL import Image +from PIL.Image import Resampling from stdimage.utils import render_variations from tests.models import ManualVariationsModel @@ -24,7 +24,7 @@ def test_render_variations(self, image_upload_file): "width": 150, "height": 150, "crop": True, - "resample": Image.ANTIALIAS, + "resample": Resampling.LANCZOS, } }, ) From c8c389f7a9b9b4bcb0521e9ec68a3893826c672b Mon Sep 17 00:00:00 2001 From: Johannes Maron Date: Sat, 21 May 2022 11:48:02 +0200 Subject: [PATCH 09/11] Add migrations to enable a smooth transition to django-pictures We to migrate from django-stimage to pictures, we need to include variations into the migration model state. This allows deleting obsolite files during the migration. --- stdimage/models.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/stdimage/models.py b/stdimage/models.py index ff13cff..528bc4c 100644 --- a/stdimage/models.py +++ b/stdimage/models.py @@ -307,6 +307,19 @@ def save_form_data(self, instance, data): file.delete(save=False) super().save_form_data(instance, data) + def deconstruct(self): + name, path, args, kwargs = super().deconstruct() + return ( + name, + path, + args, + { + **kwargs, + "variations": self._variations, + "force_min_size": self.force_min_size, + }, + ) + class JPEGFieldFile(StdImageFieldFile): @classmethod From 0730fccdb5223e93b225e112dbaada05c3ce8097 Mon Sep 17 00:00:00 2001 From: Johannes Maron Date: Sat, 21 May 2022 12:02:46 +0200 Subject: [PATCH 10/11] Set developent status classifiert to inactive --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index d93790f..54c817c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -9,7 +9,7 @@ url = https://github.com/codingjoe/django-stdimage license = MIT license_file = LICENSE classifier = - Development Status :: 5 - Production/Stable + Development Status :: 7 - Inactive Environment :: Web Environment Framework :: Django Topic :: Multimedia :: Graphics :: Graphics Conversion From 0c5b5e58e966e125d5c35d8fb2199983a94cdc10 Mon Sep 17 00:00:00 2001 From: Johannes Maron Date: Sat, 21 May 2022 12:23:40 +0200 Subject: [PATCH 11/11] Add deprecation warning and migration notes. --- README.md | 85 ++++++++++++++++++++++++++++++++++++++++++++-- setup.cfg | 2 +- stdimage/models.py | 9 +++++ 3 files changed, 93 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c602c24..77fe5f6 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,92 @@ # Django Standardized Image Field +This package has been deprecated in favor of [django-pictures][django-pictures]. + +## Migration Instructions + +First, make sure you understand the differences between the two packages and +how to serve images in a modern web application via the [picture][picture-tag]-Element. + +Next, follow the setup instructions for [django-pictures][django-pictures]. + +Once you are set up, change your models to use the new `PictureField` and provide the + `aspect_ratios` you'd like to serve. Do create migrations just yet. + +This step should be followed by changing your templates and frontend. +The new placeholders feature for local development should help you +to do this almost effortlessly. + +Finally, run `makemigrations` and replace the `AlterField` operation with +`AlterPictureField`. + +We highly recommend to use Django's `image_width` and `image_height` fields, to avoid +unnecessary IO. If you can add these fields to your model, you can use the following +snippet to populate them: + +```python +import django.core.files.storage +from django.db import migrations, models +import pictures.models +from pictures.migrations import AlterPictureField + +def forward(apps, schema_editor): + for obj in apps.get_model("my-app.MyModel").objects.all().iterator(): + obj.image_width = obj.logo.width + obj.image_height = obj.logo.height + obj.save(update_fields=["image_height", "image_width"]) + +def backward(apps, schema_editor): + apps.get_model("my-app.MyModel").objects.all().update( + image_width=None, + image_height=None, + ) + +class Migration(migrations.Migration): + dependencies = [ + ('my-app', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name="mymodel", + name="image_height", + field=models.PositiveIntegerField(editable=False, null=True), + ), + migrations.AddField( + model_name="mymodel", + name="image_width", + field=models.PositiveIntegerField(editable=False, null=True), + ), + migrations.RunPython(forward, backward), + AlterPictureField( + model_name="mymodel", + name="image", + field=pictures.models.PictureField( + aspect_ratios=["3/2", "3/1"], + breakpoints={"desktop": 1024, "mobile": 576}, + container_width=1200, + file_types=["WEBP"], + grid_columns=12, + height_field="image_height", + pixel_densities=[1, 2], + storage=django.core.files.storage.FileSystemStorage(), + upload_to="pictures/", + verbose_name="image", + width_field="image_width", + ), + ), + ] +``` + +[django-pictures]: https://github.com/codingjoe/django-pictures +[picture-tag]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/picture + ## Why would I want this? This is a drop-in replacement for the [Django ImageField](https://docs.djangoproject.com/en/1.8/ref/models/fields/#django.db.models.ImageField) that provides a standardized way to handle image uploads. It is designed to be as easy to use as possible, and to provide a consistent interface for all image fields. -It allows images to be presented in various size variants (eg:thumbnails, mid, and hi-res versions) +It allows images to be presented in various size variants (eg:thumbnails, mid, and hi-res versions) and it provides a way to handle images that are too large with validators. @@ -103,7 +184,7 @@ You can use a function for the `upload_to` argument. Using [Django Dynamic Filen This allows images to be given unique paths and filenames based on the model instance. -Example +Example ```python from django.db import models diff --git a/setup.cfg b/setup.cfg index 54c817c..5262f42 100644 --- a/setup.cfg +++ b/setup.cfg @@ -63,7 +63,7 @@ norecursedirs=venv env .eggs DJANGO_SETTINGS_MODULE=tests.settings addopts = --cov=stdimage --nomigrations --tb=short filterwarnings = - error + ignore::DeprecationWarning [coverage:run] source = . diff --git a/stdimage/models.py b/stdimage/models.py index 528bc4c..bed00d7 100644 --- a/stdimage/models.py +++ b/stdimage/models.py @@ -1,5 +1,6 @@ import logging import os +import warnings from io import BytesIO from django.core.files.base import ContentFile @@ -18,6 +19,14 @@ logger = logging.getLogger() +warnings.warn( + "The django-stdimage is deprecated in favor of django-pictures.\n" + "Migration instructions are available in the README:\n" + "https://github.com/codingjoe/django-stdimage#migration-instructions", + DeprecationWarning, +) + + class StdImageFileDescriptor(ImageFileDescriptor): """The variation property of the field is accessible in instance cases."""