diff --git a/README.md b/README.md index a2c698a..c544f02 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ A variation can be defined both as a tuple or a dictionary. Example: ```python +from django.db import models from stdimage.models import StdImageField @@ -59,7 +60,7 @@ class MyModel(models.Model): }) ## Full ammo here. Please note all the definitions below are equal - image = StdImageField(upload_to=upload_to, blank=True, variations={ + image = StdImageField(upload_to='path/to/img', blank=True, variations={ 'large': (600, 400), 'thumbnail': (100, 100, True), 'medium': (300, 200), @@ -74,36 +75,11 @@ Example: ``` ### Utils -By default StdImageField stores images without modifying the file name. -If you want to use more consistent file names you can use the build in upload callables. - -Example: -```python -from stdimage.utils import UploadToUUID, UploadToClassNameDir, UploadToAutoSlug, \ - UploadToAutoSlugClassNameDir - - -class MyClass(models.Model): - title = models.CharField(max_length=50) - - # Gets saved to MEDIA_ROOT/myclass/#FILENAME#.#EXT# - image1 = StdImageField(upload_to=UploadToClassNameDir()) - - # Gets saved to MEDIA_ROOT/myclass/pic.#EXT# - image2 = StdImageField(upload_to=UploadToClassNameDir(name='pic')) - # Gets saved to MEDIA_ROOT/images/#UUID#.#EXT# - image3 = StdImageField(upload_to=UploadToUUID(path='images')) +Since version 4 the custom `upload_to` utils have been dropped in favor of +[Django Dynamic Filenames][dynamic_filenames]. - # Gets saved to MEDIA_ROOT/myclass/#UUID#.#EXT# - image4 = StdImageField(upload_to=UploadToClassNameDirUUID()) - - # Gets save to MEDIA_ROOT/images/#SLUG#.#EXT# - image5 = StdImageField(upload_to=UploadToAutoSlug(populate_from='title')) - - # Gets save to MEDIA_ROOT/myclass/#SLUG#.#EXT# - image6 = StdImageField(upload_to=UploadToAutoSlugClassNameDir(populate_from='title')) -``` +[dynamic_filenames]: https://github.com/codingjoe/django-dynamic-filenames ### Validators The `StdImageField` doesn't implement any size validation. Validation can be specified using the validator attribute @@ -112,10 +88,12 @@ Validators can be used for both Forms and Models. Example ```python +from django.db import models from stdimage.validators import MinSizeValidator, MaxSizeValidator +from stdimage.models import StdImageField -class MyClass(models.Model) +class MyClass(models.Model): image1 = StdImageField(validators=[MinSizeValidator(800, 600)]) image2 = StdImageField(validators=[MaxSizeValidator(1028, 768)]) ``` @@ -133,11 +111,14 @@ Clearing the field if blank is true, does not delete the file. This can also be This packages contains two signal callback methods that handle file deletion for all SdtImageFields of a model. ```python +from django.db.models.signals import pre_delete, pre_save from stdimage.utils import pre_delete_delete_callback, pre_save_delete_callback +from . import models -post_delete.connect(pre_delete_delete_callback, sender=MyModel) -pre_save.connect(pre_save_delete_callback, sender=MyModel) + +pre_delete.connect(pre_delete_delete_callback, sender=models.MyModel) +pre_save.connect(pre_save_delete_callback, sender=models.MyModel) ``` **Warning:** You should not use the signal callbacks in production. They may result in data loss. @@ -156,7 +137,7 @@ try: from django.apps import apps get_model = apps.get_model except ImportError: - from django.db.models.loading import get_model + from django.apps import apps from celery import shared_task @@ -166,7 +147,7 @@ from stdimage.utils import render_variations @shared_task def process_photo_image(file_name, variations, storage): render_variations(file_name, variations, replace=True, storage=storage) - obj = get_model('myapp', 'Photo').objects.get(image=file_name) + obj = apps.get_model('myapp', 'Photo').objects.get(image=file_name) obj.processed = True obj.save() ``` @@ -175,7 +156,6 @@ def process_photo_image(file_name, variations, storage): ```python from django.db import models from stdimage.models import StdImageField -from stdimage.utils import UploadToClassNameDir from tasks import process_photo_image @@ -183,10 +163,10 @@ def image_processor(file_name, variations, storage): process_photo_image.delay(file_name, variations, storage) return False # prevent default rendering -class AsyncImageModel(models.Model) +class AsyncImageModel(models.Model): image = StdImageField( # above task definition can only handle one model object per image filename - upload_to=UploadToClassNameDir(), + upload_to='path/to/file/', render_variations=image_processor # pass boolean or callable ) processed = models.BooleanField(default=False) # flag that could be used for view querysets diff --git a/stdimage/utils.py b/stdimage/utils.py index 9d18158..b28e0dc 100644 --- a/stdimage/utils.py +++ b/stdimage/utils.py @@ -1,73 +1,8 @@ -import os -import uuid - from django.core.files.storage import default_storage -from django.utils.text import slugify from .models import StdImageField, StdImageFieldFile -class UploadTo: - file_pattern = "%(name)s%(ext)s" - path_pattern = "%(path)s" - - def __call__(self, instance, filename): - path, ext = os.path.splitext(filename) - path, name = os.path.split(path) - defaults = { - 'ext': ext, - 'name': name, - 'path': path, - 'class_name': instance.__class__.__name__, - } - defaults.update(self.kwargs) - return os.path.join(self.path_pattern % defaults, - self.file_pattern % defaults).lower() - - def __init__(self, *args, **kwargs): - self.kwargs = kwargs - self.args = args - - def deconstruct(self): - path = "%s.%s" % (self.__class__.__module__, self.__class__.__name__) - return path, self.args, self.kwargs - - -class UploadToUUID(UploadTo): - - def __call__(self, instance, filename): - self.kwargs.update({ - 'name': uuid.uuid4().hex, - }) - return super().__call__(instance, filename) - - -class UploadToClassNameDir(UploadTo): - path_pattern = '%(class_name)s' - - -class UploadToClassNameDirUUID(UploadToClassNameDir, UploadToUUID): - pass - - -class UploadToAutoSlug(UploadTo): - - def __init__(self, populate_from, **kwargs): - self.populate_from = populate_from - super().__init__(populate_from, **kwargs) - - def __call__(self, instance, filename): - field_value = getattr(instance, self.populate_from) - self.kwargs.update({ - 'name': slugify(field_value), - }) - return super().__call__(instance, filename) - - -class UploadToAutoSlugClassNameDir(UploadToClassNameDir, UploadToAutoSlug): - pass - - def pre_delete_delete_callback(sender, instance, **kwargs): for field in instance._meta.fields: if isinstance(field, StdImageField): diff --git a/tests/conftest.py b/tests/conftest.py index bfa9dc3..33ebae0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,6 +18,6 @@ def imagedata(): @pytest.fixture def image_upload_file(imagedata): return SimpleUploadedFile( - 'testfile.jpg', + 'image.jpg', imagedata.getvalue() ) diff --git a/tests/models.py b/tests/models.py index ee0f6ac..ac3376f 100644 --- a/tests/models.py +++ b/tests/models.py @@ -9,21 +9,22 @@ from stdimage import StdImageField from stdimage.models import StdImageFieldFile from stdimage.utils import ( - UploadTo, UploadToAutoSlugClassNameDir, UploadToUUID, pre_delete_delete_callback, pre_save_delete_callback, render_variations ) from stdimage.validators import MaxSizeValidator, MinSizeValidator +upload_to = 'img/' + class SimpleModel(models.Model): """works as ImageField""" - image = StdImageField(upload_to='img/') + image = StdImageField(upload_to=upload_to) class AdminDeleteModel(models.Model): """can be deleted through admin""" image = StdImageField( - upload_to=UploadTo(name='image', path='img'), + upload_to=upload_to, blank=True ) @@ -31,7 +32,7 @@ class AdminDeleteModel(models.Model): class ResizeModel(models.Model): """resizes image to maximum size to fit a 640x480 area""" image = StdImageField( - upload_to=UploadTo(name='image', path='img'), + upload_to=upload_to, variations={ 'medium': {'width': 400, 'height': 400}, 'thumbnail': (100, 75), @@ -42,7 +43,7 @@ class ResizeModel(models.Model): class ResizeCropModel(models.Model): """resizes image to 640x480 cropping if necessary""" image = StdImageField( - upload_to=UploadTo(name='image', path='img'), + upload_to=upload_to, variations={'thumbnail': (150, 150, True)} ) @@ -50,7 +51,7 @@ class ResizeCropModel(models.Model): class ThumbnailModel(models.Model): """creates a thumbnail resized to maximum size to fit a 100x75 area""" image = StdImageField( - upload_to=UploadTo(name='image', path='img'), + upload_to=upload_to, blank=True, variations={'thumbnail': (100, 75)} ) @@ -58,14 +59,14 @@ class ThumbnailModel(models.Model): class MaxSizeModel(models.Model): image = StdImageField( - upload_to=UploadTo(name='image', path='img'), + upload_to=upload_to, validators=[MaxSizeValidator(16, 16)] ) class MinSizeModel(models.Model): image = StdImageField( - upload_to=UploadTo(name='image', path='img'), + upload_to=upload_to, validators=[MinSizeValidator(200, 200)] ) @@ -73,23 +74,12 @@ class MinSizeModel(models.Model): class ForceMinSizeModel(models.Model): """creates a thumbnail resized to maximum size to fit a 100x75 area""" image = StdImageField( - upload_to=UploadTo(name='image', path='img'), + upload_to=upload_to, force_min_size=True, variations={'thumbnail': (600, 600)} ) -class AutoSlugClassNameDirModel(models.Model): - name = models.CharField(max_length=50) - image = StdImageField( - upload_to=UploadToAutoSlugClassNameDir(populate_from='name') - ) - - -class UUIDModel(models.Model): - image = StdImageField(upload_to=UploadToUUID(path='img')) - - class CustomManager(models.Manager): """Just like Django's default, but a different class.""" pass @@ -105,7 +95,7 @@ class Meta: class ManualVariationsModel(CustomManagerModel): """delays creation of 150x150 thumbnails until it is called manually""" image = StdImageField( - upload_to=UploadTo(name='image', path='img'), + upload_to=upload_to, variations={'thumbnail': (150, 150, True)}, render_variations=False ) @@ -114,7 +104,7 @@ class ManualVariationsModel(CustomManagerModel): class MyStorageModel(CustomManagerModel): """delays creation of 150x150 thumbnails until it is called manually""" image = StdImageField( - upload_to=UploadTo(name='image', path='img'), + upload_to=upload_to, variations={'thumbnail': (150, 150, True)}, storage=FileSystemStorage(), ) @@ -128,7 +118,7 @@ def render_job(**kwargs): class UtilVariationsModel(models.Model): """delays creation of 150x150 thumbnails until it is called manually""" image = StdImageField( - upload_to=UploadTo(name='image', path='img'), + upload_to=upload_to, variations={'thumbnail': (150, 150, True)}, render_variations=render_job ) @@ -169,7 +159,7 @@ class CustomRenderVariationsModel(models.Model): """Use custom render_variations.""" image = StdImageField( - upload_to=UploadTo(name='image', path='img'), + upload_to=upload_to, variations={'thumbnail': (150, 150)}, render_variations=custom_render_variations, ) diff --git a/tests/test_models.py b/tests/test_models.py index 21b30d5..6669e80 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -23,8 +23,7 @@ class UUID4Monkey: from .models import ( SimpleModel, ResizeModel, AdminDeleteModel, - ThumbnailModel, ResizeCropModel, AutoSlugClassNameDirModel, - UUIDModel, + ThumbnailModel, ResizeCropModel, UtilVariationsModel, ThumbnailWithoutDirectoryModel, CustomRenderVariationsModel) # NoQA @@ -85,27 +84,27 @@ def test_variations(self, db): source_file = self.fixtures['600x400.jpg'] - assert os.path.exists(os.path.join(IMG_DIR, 'image.jpg')) + assert os.path.exists(os.path.join(IMG_DIR, '600x400.jpg')) assert instance.image.width == 600 assert instance.image.height == 400 - path = os.path.join(IMG_DIR, 'image.jpg') + path = os.path.join(IMG_DIR, '600x400.jpg') with open(path, 'rb') as f: source_file.seek(0) assert source_file.read() == f.read() - path = os.path.join(IMG_DIR, 'image.medium.jpg') + path = os.path.join(IMG_DIR, '600x400.medium.jpg') assert os.path.exists(path) assert instance.image.medium.width == 400 assert instance.image.medium.height <= 400 - with open(os.path.join(IMG_DIR, 'image.medium.jpg'), 'rb') as f: + with open(os.path.join(IMG_DIR, '600x400.medium.jpg'), 'rb') as f: source_file.seek(0) assert source_file.read() != f.read() - assert os.path.exists(os.path.join(IMG_DIR, 'image.thumbnail.jpg')) + assert os.path.exists(os.path.join(IMG_DIR, '600x400.thumbnail.jpg')) assert instance.image.thumbnail.width == 100 assert instance.image.thumbnail.height <= 75 - with open(os.path.join(IMG_DIR, 'image.thumbnail.jpg'), 'rb') as f: + with open(os.path.join(IMG_DIR, '600x400.thumbnail.jpg'), 'rb') as f: source_file.seek(0) assert source_file.read() != f.read() @@ -207,31 +206,11 @@ def test_pre_save_delete_callback_new(self, admin_client): }) assert not os.path.exists(os.path.join(IMG_DIR, 'image.gif')) - def test_upload_to_auto_slug_class_name_dir(self, db): - AutoSlugClassNameDirModel.objects.create( - name='foo bar', - image=self.fixtures['100.gif'] - ) - file_path = os.path.join( - settings.MEDIA_ROOT, - 'autoslugclassnamedirmodel', - 'foo-bar.gif' - ) - assert os.path.exists(file_path) - - def test_upload_to_uuid(self, db): - UUIDModel.objects.create(image=self.fixtures['100.gif']) - file_path = os.path.join( - IMG_DIR, - '653d1c6863404b9689b75fa930c9d0a0.gif' - ) - assert os.path.exists(file_path) - def test_render_variations_callback(self, db): UtilVariationsModel.objects.create(image=self.fixtures['100.gif']) file_path = os.path.join( IMG_DIR, - 'image.thumbnail.gif' + '100.thumbnail.gif' ) assert os.path.exists(file_path) @@ -241,10 +220,10 @@ def test_max_size_validator(self, admin_client): admin_client.post('/admin/tests/maxsizemodel/add/', { 'image': self.fixtures['600x400.jpg'], }) - assert not os.path.exists(os.path.join(IMG_DIR, 'image.jpg')) + assert not os.path.exists(os.path.join(IMG_DIR, '800x600.jpg')) def test_min_size_validator(self, admin_client): admin_client.post('/admin/tests/minsizemodel/add/', { 'image': self.fixtures['100.gif'], }) - assert not os.path.exists(os.path.join(IMG_DIR, 'image.gif')) + assert not os.path.exists(os.path.join(IMG_DIR, '100.gif')) diff --git a/tests/test_utils.py b/tests/test_utils.py index 38c1672..047a651 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -3,7 +3,7 @@ import pytest from PIL import Image -from stdimage.utils import UploadTo, render_variations +from stdimage.utils import render_variations from tests.models import ManualVariationsModel from tests.test_models import IMG_DIR @@ -29,38 +29,3 @@ def test_render_variations(self, image_upload_file): } ) assert os.path.exists(path) - - -class TestUploadTo: - def test_file_name(self): - file_name = UploadTo()(object(), '/path/to/file.jpeg') - assert file_name == '/path/to/file.jpeg' - - def test_file_name_lower(self): - file_name = UploadTo()(object(), '/path/To/File.JPEG') - assert file_name == '/path/to/file.jpeg' - - def test_file_name_no_ext(self): - file_name = UploadTo()(object(), '/path/to/file') - assert file_name == '/path/to/file' - - def test_file_name_kwargs(self): - file_name = UploadTo(path='/foo', name='bar', ext='.BAZ')( - object(), '/path/to/file') - assert file_name == '/foo/bar.baz' - - def test_path_pattern(self): - class U2(UploadTo): - path_pattern = '/foo' - - file_name = U2()( - object(), '/path/to/file.jpeg') - assert file_name == '/foo/file.jpeg' - - def test_name_pattern(self): - class U2(UploadTo): - file_pattern = ".%(name)s%(ext)s" - - file_name = U2()( - object(), '/path/to/file.jpeg') - assert file_name == '/path/to/.file.jpeg'