Skip to content

Commit

Permalink
Drop upload_to utils in favor of django-dynamic-filenames (#201)
Browse files Browse the repository at this point in the history
  • Loading branch information
codingjoe committed Sep 7, 2018
1 parent b660a02 commit 1ff71ac
Show file tree
Hide file tree
Showing 6 changed files with 43 additions and 194 deletions.
54 changes: 17 additions & 37 deletions README.md
Expand Up @@ -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


Expand All @@ -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),
Expand All @@ -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
Expand All @@ -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)])
```
Expand All @@ -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.
Expand All @@ -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

Expand All @@ -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()
```
Expand All @@ -175,18 +156,17 @@ 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

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
Expand Down
65 changes: 0 additions & 65 deletions 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):
Expand Down
2 changes: 1 addition & 1 deletion tests/conftest.py
Expand Up @@ -18,6 +18,6 @@ def imagedata():
@pytest.fixture
def image_upload_file(imagedata):
return SimpleUploadedFile(
'testfile.jpg',
'image.jpg',
imagedata.getvalue()
)
38 changes: 14 additions & 24 deletions tests/models.py
Expand Up @@ -9,29 +9,30 @@
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
)


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),
Expand All @@ -42,54 +43,43 @@ 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)}
)


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)}
)


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)]
)


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
Expand All @@ -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
)
Expand All @@ -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(),
)
Expand All @@ -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
)
Expand Down Expand Up @@ -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,
)
Expand Down

0 comments on commit 1ff71ac

Please sign in to comment.