Bundle trimed down alternative to photologue
This commit is contained in:
parent
2da3419b8d
commit
5368a51a76
40 changed files with 2652 additions and 70 deletions
659
photologue/models.py
Normal file
659
photologue/models.py
Normal file
|
|
@ -0,0 +1,659 @@
|
|||
# Based on https://github.com/richardbarran/django-photologue/
|
||||
# by Richard Barran, BSD-3 licensed
|
||||
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import unicodedata
|
||||
from datetime import datetime
|
||||
from functools import partial
|
||||
from importlib import import_module
|
||||
from inspect import isclass
|
||||
from io import BytesIO
|
||||
|
||||
import exifread
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.files.storage import default_storage
|
||||
from django.core.validators import RegexValidator
|
||||
from django.db import models
|
||||
from django.template.defaultfilters import slugify
|
||||
from django.urls import reverse
|
||||
from django.utils.encoding import filepath_to_uri, force_str, smart_str
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from PIL import Image, ImageFile, ImageFilter
|
||||
|
||||
logger = logging.getLogger('photologue.models')
|
||||
|
||||
# Default limit for gallery.latest
|
||||
LATEST_LIMIT = getattr(settings, 'PHOTOLOGUE_GALLERY_LATEST_LIMIT', None)
|
||||
|
||||
# max_length setting for the ImageModel ImageField
|
||||
IMAGE_FIELD_MAX_LENGTH = getattr(settings, 'PHOTOLOGUE_IMAGE_FIELD_MAX_LENGTH', 100)
|
||||
|
||||
# Modify image file buffer size.
|
||||
ImageFile.MAXBLOCK = getattr(settings, 'PHOTOLOGUE_MAXBLOCK', 256 * 2 ** 10)
|
||||
|
||||
# Look for user function to define file paths
|
||||
PHOTOLOGUE_PATH = getattr(settings, 'PHOTOLOGUE_PATH', None)
|
||||
if PHOTOLOGUE_PATH is not None:
|
||||
if callable(PHOTOLOGUE_PATH):
|
||||
get_storage_path = PHOTOLOGUE_PATH
|
||||
else:
|
||||
parts = PHOTOLOGUE_PATH.split('.')
|
||||
module_name = '.'.join(parts[:-1])
|
||||
module = import_module(module_name)
|
||||
get_storage_path = getattr(module, parts[-1])
|
||||
else:
|
||||
def get_storage_path(instance, filename):
|
||||
fn = unicodedata.normalize('NFKD', force_str(filename)).encode('ascii', 'ignore').decode('ascii')
|
||||
return os.path.join('photos', fn)
|
||||
|
||||
# Support CACHEDIR.TAG spec for backups for ignoring cache dir.
|
||||
# See http://www.brynosaurus.com/cachedir/spec.html
|
||||
PHOTOLOGUE_CACHEDIRTAG = os.path.join("photos", "cache", "CACHEDIR.TAG")
|
||||
if not default_storage.exists(PHOTOLOGUE_CACHEDIRTAG):
|
||||
default_storage.save(PHOTOLOGUE_CACHEDIRTAG, ContentFile(
|
||||
b"Signature: 8a477f597d28d172789f06886806bc55"))
|
||||
|
||||
# Exif Orientation values
|
||||
# Value 0thRow 0thColumn
|
||||
# 1 top left
|
||||
# 2 top right
|
||||
# 3 bottom right
|
||||
# 4 bottom left
|
||||
# 5 left top
|
||||
# 6 right top
|
||||
# 7 right bottom
|
||||
# 8 left bottom
|
||||
|
||||
# Image Orientations (according to EXIF informations) that needs to be
|
||||
# transposed and appropriate action
|
||||
IMAGE_EXIF_ORIENTATION_MAP = {
|
||||
2: Image.FLIP_LEFT_RIGHT,
|
||||
3: Image.ROTATE_180,
|
||||
6: Image.ROTATE_270,
|
||||
8: Image.ROTATE_90,
|
||||
}
|
||||
|
||||
# Quality options for JPEG images
|
||||
JPEG_QUALITY_CHOICES = (
|
||||
(30, _('Very Low')),
|
||||
(40, _('Low')),
|
||||
(50, _('Medium-Low')),
|
||||
(60, _('Medium')),
|
||||
(70, _('Medium-High')),
|
||||
(80, _('High')),
|
||||
(90, _('Very High')),
|
||||
)
|
||||
|
||||
# choices for new crop_anchor field in Photo
|
||||
CROP_ANCHOR_CHOICES = (
|
||||
('top', _('Top')),
|
||||
('right', _('Right')),
|
||||
('bottom', _('Bottom')),
|
||||
('left', _('Left')),
|
||||
('center', _('Center (Default)')),
|
||||
)
|
||||
|
||||
IMAGE_TRANSPOSE_CHOICES = (
|
||||
('FLIP_LEFT_RIGHT', _('Flip left to right')),
|
||||
('FLIP_TOP_BOTTOM', _('Flip top to bottom')),
|
||||
('ROTATE_90', _('Rotate 90 degrees counter-clockwise')),
|
||||
('ROTATE_270', _('Rotate 90 degrees clockwise')),
|
||||
('ROTATE_180', _('Rotate 180 degrees')),
|
||||
)
|
||||
|
||||
# Prepare a list of image filters
|
||||
filter_names = []
|
||||
for n in dir(ImageFilter):
|
||||
klass = getattr(ImageFilter, n)
|
||||
if isclass(klass) and issubclass(klass, ImageFilter.BuiltinFilter) and \
|
||||
hasattr(klass, 'name'):
|
||||
filter_names.append(klass.__name__)
|
||||
IMAGE_FILTERS_HELP_TEXT = _('Chain multiple filters using the following pattern "FILTER_ONE->FILTER_TWO->FILTER_THREE"'
|
||||
'. Image filters will be applied in order. The following filters are available: %s.'
|
||||
% (', '.join(filter_names)))
|
||||
|
||||
size_method_map = {}
|
||||
|
||||
|
||||
class TagField(models.CharField):
|
||||
"""Tags have been removed from Photologue, but the migrations still refer to them so this
|
||||
Tagfield definition is left here.
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
default_kwargs = {'max_length': 255, 'blank': True}
|
||||
default_kwargs.update(kwargs)
|
||||
super().__init__(**default_kwargs)
|
||||
|
||||
def get_internal_type(self):
|
||||
return 'CharField'
|
||||
|
||||
|
||||
class Gallery(models.Model):
|
||||
date_added = models.DateTimeField(_('date published'),
|
||||
default=now)
|
||||
title = models.CharField(_('title'),
|
||||
max_length=250,
|
||||
unique=True)
|
||||
slug = models.SlugField(_('title slug'),
|
||||
unique=True,
|
||||
max_length=250,
|
||||
help_text=_('A "slug" is a unique URL-friendly title for an object.'))
|
||||
description = models.TextField(_('description'),
|
||||
blank=True)
|
||||
is_public = models.BooleanField(_('is public'),
|
||||
default=True,
|
||||
help_text=_('Public galleries will be displayed '
|
||||
'in the default views.'))
|
||||
photos = models.ManyToManyField('photologue.Photo',
|
||||
related_name='galleries',
|
||||
verbose_name=_('photos'),
|
||||
blank=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-date_added']
|
||||
get_latest_by = 'date_added'
|
||||
verbose_name = _('gallery')
|
||||
verbose_name_plural = _('galleries')
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('photologue:pl-gallery', args=[self.slug])
|
||||
|
||||
def latest(self, limit=LATEST_LIMIT, public=True):
|
||||
if not limit:
|
||||
limit = self.photo_count()
|
||||
if public:
|
||||
return self.public()[:limit]
|
||||
else:
|
||||
return self.photos[:limit]
|
||||
|
||||
def sample(self, count=None, public=True):
|
||||
"""Return a sample of photos, ordered at random."""
|
||||
if not count:
|
||||
count = 1
|
||||
if count > self.photo_count():
|
||||
count = self.photo_count()
|
||||
if public:
|
||||
photo_set = self.public()
|
||||
else:
|
||||
photo_set = self.photos
|
||||
return random.sample(set(photo_set), count)
|
||||
|
||||
def photo_count(self, public=True):
|
||||
"""Return a count of all the photos in this gallery."""
|
||||
if public:
|
||||
return self.public().count()
|
||||
else:
|
||||
return self.photos.count()
|
||||
|
||||
photo_count.short_description = _('count')
|
||||
|
||||
def public(self):
|
||||
"""Return a queryset of all the public photos in this gallery."""
|
||||
return self.photos.filter(is_public=True)
|
||||
|
||||
|
||||
class ImageModel(models.Model):
|
||||
image = models.ImageField(_('image'),
|
||||
max_length=IMAGE_FIELD_MAX_LENGTH,
|
||||
upload_to=get_storage_path)
|
||||
date_taken = models.DateTimeField(_('date taken'),
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text=_('Date image was taken; is obtained from the image EXIF data.'))
|
||||
view_count = models.PositiveIntegerField(_('view count'),
|
||||
default=0,
|
||||
editable=False)
|
||||
crop_from = models.CharField(_('crop from'),
|
||||
blank=True,
|
||||
max_length=10,
|
||||
default='center',
|
||||
choices=CROP_ANCHOR_CHOICES)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def exif(self, file=None):
|
||||
try:
|
||||
if file:
|
||||
tags = exifread.process_file(file)
|
||||
else:
|
||||
with self.image.storage.open(self.image.name, 'rb') as file:
|
||||
tags = exifread.process_file(file, details=False)
|
||||
return tags
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
def admin_thumbnail(self):
|
||||
func = getattr(self, 'get_admin_thumbnail_url', None)
|
||||
if func is None:
|
||||
return _('An "admin_thumbnail" photo size has not been defined.')
|
||||
else:
|
||||
if hasattr(self, 'get_absolute_url'):
|
||||
return mark_safe('<a href="{}"><img src="{}"></a>'.format(self.get_absolute_url(), func()))
|
||||
else:
|
||||
return mark_safe('<a href="{}"><img src="{}"></a>'.format(self.image.url, func()))
|
||||
|
||||
admin_thumbnail.short_description = _('Thumbnail')
|
||||
admin_thumbnail.allow_tags = True
|
||||
|
||||
def cache_path(self):
|
||||
return os.path.join(os.path.dirname(self.image.name), "cache")
|
||||
|
||||
def cache_url(self):
|
||||
return '/'.join([os.path.dirname(self.image.url), "cache"])
|
||||
|
||||
def image_filename(self):
|
||||
return os.path.basename(force_str(self.image.name))
|
||||
|
||||
def _get_filename_for_size(self, size):
|
||||
size = getattr(size, 'name', size)
|
||||
base, ext = os.path.splitext(self.image_filename())
|
||||
return ''.join([base, '_', size, ext])
|
||||
|
||||
def _get_size_photosize(self, size):
|
||||
return PhotoSizeCache().sizes.get(size)
|
||||
|
||||
def _get_size_size(self, size):
|
||||
photosize = PhotoSizeCache().sizes.get(size)
|
||||
if not self.size_exists(photosize):
|
||||
self.create_size(photosize)
|
||||
try:
|
||||
return Image.open(self.image.storage.open(
|
||||
self._get_size_filename(size))).size
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _get_size_url(self, size):
|
||||
photosize = PhotoSizeCache().sizes.get(size)
|
||||
if not self.size_exists(photosize):
|
||||
self.create_size(photosize)
|
||||
if photosize.increment_count:
|
||||
self.increment_count()
|
||||
return '/'.join([
|
||||
self.cache_url(),
|
||||
filepath_to_uri(self._get_filename_for_size(photosize.name))])
|
||||
|
||||
def _get_size_filename(self, size):
|
||||
photosize = PhotoSizeCache().sizes.get(size)
|
||||
return smart_str(os.path.join(self.cache_path(),
|
||||
self._get_filename_for_size(photosize.name)))
|
||||
|
||||
def increment_count(self):
|
||||
self.view_count += 1
|
||||
models.Model.save(self)
|
||||
|
||||
def __getattr__(self, name):
|
||||
global size_method_map
|
||||
if not size_method_map:
|
||||
init_size_method_map()
|
||||
di = size_method_map.get(name, None)
|
||||
if di is not None:
|
||||
result = partial(getattr(self, di['base_name']), di['size'])
|
||||
setattr(self, name, result)
|
||||
return result
|
||||
else:
|
||||
raise AttributeError
|
||||
|
||||
def size_exists(self, photosize):
|
||||
func = getattr(self, "get_%s_filename" % photosize.name, None)
|
||||
if func is not None:
|
||||
if self.image.storage.exists(func()):
|
||||
return True
|
||||
return False
|
||||
|
||||
def resize_image(self, im, photosize):
|
||||
cur_width, cur_height = im.size
|
||||
new_width, new_height = photosize.size
|
||||
if photosize.crop:
|
||||
ratio = max(float(new_width) / cur_width, float(new_height) / cur_height)
|
||||
x = (cur_width * ratio)
|
||||
y = (cur_height * ratio)
|
||||
xd = abs(new_width - x)
|
||||
yd = abs(new_height - y)
|
||||
x_diff = int(xd / 2)
|
||||
y_diff = int(yd / 2)
|
||||
if self.crop_from == 'top':
|
||||
box = (int(x_diff), 0, int(x_diff + new_width), new_height)
|
||||
elif self.crop_from == 'left':
|
||||
box = (0, int(y_diff), new_width, int(y_diff + new_height))
|
||||
elif self.crop_from == 'bottom':
|
||||
# y - yd = new_height
|
||||
box = (int(x_diff), int(yd), int(x_diff + new_width), int(y))
|
||||
elif self.crop_from == 'right':
|
||||
# x - xd = new_width
|
||||
box = (int(xd), int(y_diff), int(x), int(y_diff + new_height))
|
||||
else:
|
||||
box = (int(x_diff), int(y_diff), int(x_diff + new_width), int(y_diff + new_height))
|
||||
im = im.resize((int(x), int(y)), Image.ANTIALIAS).crop(box)
|
||||
else:
|
||||
if not new_width == 0 and not new_height == 0:
|
||||
ratio = min(float(new_width) / cur_width,
|
||||
float(new_height) / cur_height)
|
||||
else:
|
||||
if new_width == 0:
|
||||
ratio = float(new_height) / cur_height
|
||||
else:
|
||||
ratio = float(new_width) / cur_width
|
||||
new_dimensions = (int(round(cur_width * ratio)),
|
||||
int(round(cur_height * ratio)))
|
||||
if new_dimensions[0] > cur_width or \
|
||||
new_dimensions[1] > cur_height:
|
||||
if not photosize.upscale:
|
||||
return im
|
||||
im = im.resize(new_dimensions, Image.ANTIALIAS)
|
||||
return im
|
||||
|
||||
def create_size(self, photosize, recreate=False):
|
||||
if self.size_exists(photosize) and not recreate:
|
||||
return
|
||||
try:
|
||||
im = Image.open(self.image.storage.open(self.image.name))
|
||||
except OSError:
|
||||
return
|
||||
# Save the original format
|
||||
im_format = im.format
|
||||
# Apply effect if found
|
||||
if self.effect is not None:
|
||||
im = self.effect.pre_process(im)
|
||||
elif photosize.effect is not None:
|
||||
im = photosize.effect.pre_process(im)
|
||||
# Rotate if found & necessary
|
||||
if 'Image Orientation' in self.exif() and \
|
||||
self.exif().get('Image Orientation').values[0] in IMAGE_EXIF_ORIENTATION_MAP:
|
||||
im = im.transpose(
|
||||
IMAGE_EXIF_ORIENTATION_MAP[self.EXIF().get('Image Orientation').values[0]])
|
||||
# Resize/crop image
|
||||
if (im.size != photosize.size and photosize.size != (0, 0)) or recreate:
|
||||
im = self.resize_image(im, photosize)
|
||||
# Apply effect if found
|
||||
if self.effect is not None:
|
||||
im = self.effect.post_process(im)
|
||||
elif photosize.effect is not None:
|
||||
im = photosize.effect.post_process(im)
|
||||
# Save file
|
||||
im_filename = getattr(self, "get_%s_filename" % photosize.name)()
|
||||
try:
|
||||
buffer = BytesIO()
|
||||
if im_format != 'JPEG':
|
||||
im.save(buffer, im_format)
|
||||
else:
|
||||
# Issue #182 - test fix from https://github.com/bashu/django-watermark/issues/31
|
||||
if im.mode.endswith('A'):
|
||||
im = im.convert(im.mode[:-1])
|
||||
im.save(buffer, 'JPEG', quality=int(photosize.quality),
|
||||
optimize=True)
|
||||
buffer_contents = ContentFile(buffer.getvalue())
|
||||
self.image.storage.save(im_filename, buffer_contents)
|
||||
except OSError as e:
|
||||
if self.image.storage.exists(im_filename):
|
||||
self.image.storage.delete(im_filename)
|
||||
raise e
|
||||
|
||||
def remove_size(self, photosize, remove_dirs=True):
|
||||
if not self.size_exists(photosize):
|
||||
return
|
||||
filename = getattr(self, "get_%s_filename" % photosize.name)()
|
||||
if self.image.storage.exists(filename):
|
||||
self.image.storage.delete(filename)
|
||||
|
||||
def clear_cache(self):
|
||||
cache = PhotoSizeCache()
|
||||
for photosize in cache.sizes.values():
|
||||
self.remove_size(photosize, False)
|
||||
|
||||
def pre_cache(self, recreate=False):
|
||||
cache = PhotoSizeCache()
|
||||
if recreate:
|
||||
self.clear_cache()
|
||||
for photosize in cache.sizes.values():
|
||||
if photosize.pre_cache:
|
||||
self.create_size(photosize, recreate)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._old_image = self.image
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
recreate = kwargs.pop('recreate', False)
|
||||
image_has_changed = False
|
||||
if self._get_pk_val() and (self._old_image != self.image):
|
||||
image_has_changed = True
|
||||
# If we have changed the image, we need to clear from the cache all instances of the old
|
||||
# image; clear_cache() works on the current (new) image, and in turn calls several other methods.
|
||||
# Changing them all to act on the old image was a lot of changes, so instead we temporarily swap old
|
||||
# and new images.
|
||||
new_image = self.image
|
||||
self.image = self._old_image
|
||||
self.clear_cache()
|
||||
self.image = new_image # Back to the new image.
|
||||
self._old_image.storage.delete(self._old_image.name) # Delete (old) base image.
|
||||
if self.date_taken is None or image_has_changed:
|
||||
# Attempt to get the date the photo was taken from the EXIF data.
|
||||
try:
|
||||
exif_date = self.exif(self.image.file).get('EXIF DateTimeOriginal', None)
|
||||
if exif_date is not None:
|
||||
d, t = exif_date.values.split()
|
||||
year, month, day = d.split(':')
|
||||
hour, minute, second = t.split(':')
|
||||
self.date_taken = datetime(int(year), int(month), int(day),
|
||||
int(hour), int(minute), int(second))
|
||||
except Exception:
|
||||
logger.error('Failed to read EXIF DateTimeOriginal', exc_info=True)
|
||||
super().save(*args, **kwargs)
|
||||
self.pre_cache(recreate)
|
||||
|
||||
def delete(self):
|
||||
assert self._get_pk_val() is not None, \
|
||||
"%s object can't be deleted because its %s attribute is set to None." % \
|
||||
(self._meta.object_name, self._meta.pk.attname)
|
||||
self.clear_cache()
|
||||
# Files associated to a FileField have to be manually deleted:
|
||||
# https://docs.djangoproject.com/en/dev/releases/1.3/#deleting-a-model-doesn-t-delete-associated-files
|
||||
# http://haineault.com/blog/147/
|
||||
# The data loss scenarios mentioned in the docs hopefully do not apply
|
||||
# to Photologue!
|
||||
super().delete()
|
||||
self.image.storage.delete(self.image.name)
|
||||
|
||||
|
||||
class Photo(ImageModel):
|
||||
title = models.CharField(_('title'),
|
||||
max_length=250,
|
||||
unique=True)
|
||||
slug = models.SlugField(_('slug'),
|
||||
unique=True,
|
||||
max_length=250,
|
||||
help_text=_('A "slug" is a unique URL-friendly title for an object.'))
|
||||
caption = models.TextField(_('caption'),
|
||||
blank=True)
|
||||
date_added = models.DateTimeField(_('date added'),
|
||||
default=now)
|
||||
is_public = models.BooleanField(_('is public'),
|
||||
default=True,
|
||||
help_text=_('Public photographs will be displayed in the default views.'))
|
||||
|
||||
class Meta:
|
||||
ordering = ['-date_added']
|
||||
get_latest_by = 'date_added'
|
||||
verbose_name = _("photo")
|
||||
verbose_name_plural = _("photos")
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# If crop_from or effect property has been changed on existing image,
|
||||
# update kwargs to force image recreation in parent class
|
||||
current = Photo.objects.get(pk=self.pk) if self.pk else None
|
||||
if current and (current.crop_from != self.crop_from or current.effect != self.effect):
|
||||
kwargs.update(recreate=True)
|
||||
|
||||
if self.slug is None:
|
||||
self.slug = slugify(self.title)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('photologue:pl-photo', args=[self.slug])
|
||||
|
||||
def public_galleries(self):
|
||||
"""Return the public galleries to which this photo belongs."""
|
||||
return self.galleries.filter(is_public=True)
|
||||
|
||||
def get_previous_in_gallery(self, gallery):
|
||||
"""Find the neighbour of this photo in the supplied gallery.
|
||||
We assume that the gallery and all its photos are on the same site.
|
||||
"""
|
||||
if not self.is_public:
|
||||
raise ValueError('Cannot determine neighbours of a non-public photo.')
|
||||
photos = gallery.photos.filter(is_public=True)
|
||||
if self not in photos:
|
||||
raise ValueError('Photo does not belong to gallery.')
|
||||
previous = None
|
||||
for photo in photos:
|
||||
if photo == self:
|
||||
return previous
|
||||
previous = photo
|
||||
|
||||
def get_next_in_gallery(self, gallery):
|
||||
"""Find the neighbour of this photo in the supplied gallery.
|
||||
We assume that the gallery and all its photos are on the same site.
|
||||
"""
|
||||
if not self.is_public:
|
||||
raise ValueError('Cannot determine neighbours of a non-public photo.')
|
||||
photos = gallery.photos.filter(is_public=True)
|
||||
if self not in photos:
|
||||
raise ValueError('Photo does not belong to gallery.')
|
||||
matched = False
|
||||
for photo in photos:
|
||||
if matched:
|
||||
return photo
|
||||
if photo == self:
|
||||
matched = True
|
||||
return None
|
||||
|
||||
|
||||
class PhotoSize(models.Model):
|
||||
"""About the Photosize name: it's used to create get_PHOTOSIZE_url() methods,
|
||||
so the name has to follow the same restrictions as any Python method name,
|
||||
e.g. no spaces or non-ascii characters."""
|
||||
|
||||
name = models.CharField(_('name'),
|
||||
max_length=40,
|
||||
unique=True,
|
||||
help_text=_(
|
||||
'Photo size name should contain only letters, numbers and underscores. Examples: '
|
||||
'"thumbnail", "display", "small", "main_page_widget".'),
|
||||
validators=[RegexValidator(regex='^[a-z0-9_]+$',
|
||||
message='Use only plain lowercase letters (ASCII), numbers and '
|
||||
'underscores.'
|
||||
)]
|
||||
)
|
||||
width = models.PositiveIntegerField(_('width'),
|
||||
default=0,
|
||||
help_text=_(
|
||||
'If width is set to "0" the image will be scaled to the supplied height.'))
|
||||
height = models.PositiveIntegerField(_('height'),
|
||||
default=0,
|
||||
help_text=_(
|
||||
'If height is set to "0" the image will be scaled to the supplied width'))
|
||||
quality = models.PositiveIntegerField(_('quality'),
|
||||
choices=JPEG_QUALITY_CHOICES,
|
||||
default=70,
|
||||
help_text=_('JPEG image quality.'))
|
||||
upscale = models.BooleanField(_('upscale images?'),
|
||||
default=False,
|
||||
help_text=_('If selected the image will be scaled up if necessary to fit the '
|
||||
'supplied dimensions. Cropped sizes will be upscaled regardless of this '
|
||||
'setting.')
|
||||
)
|
||||
crop = models.BooleanField(_('crop to fit?'),
|
||||
default=False,
|
||||
help_text=_('If selected the image will be scaled and cropped to fit the supplied '
|
||||
'dimensions.'))
|
||||
pre_cache = models.BooleanField(_('pre-cache?'),
|
||||
default=False,
|
||||
help_text=_('If selected this photo size will be pre-cached as photos are added.'))
|
||||
increment_count = models.BooleanField(_('increment view count?'),
|
||||
default=False,
|
||||
help_text=_('If selected the image\'s "view_count" will be incremented when '
|
||||
'this photo size is displayed.'))
|
||||
|
||||
class Meta:
|
||||
ordering = ['width', 'height']
|
||||
verbose_name = _('photo size')
|
||||
verbose_name_plural = _('photo sizes')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def clear_cache(self):
|
||||
for cls in ImageModel.__subclasses__():
|
||||
for obj in cls.objects.all():
|
||||
obj.remove_size(self)
|
||||
if self.pre_cache:
|
||||
obj.create_size(self)
|
||||
PhotoSizeCache().reset()
|
||||
|
||||
def clean(self):
|
||||
if self.crop is True:
|
||||
if self.width == 0 or self.height == 0:
|
||||
raise ValidationError(
|
||||
_("Can only crop photos if both width and height dimensions are set."))
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
PhotoSizeCache().reset()
|
||||
self.clear_cache()
|
||||
|
||||
def delete(self):
|
||||
assert self._get_pk_val() is not None, "%s object can't be deleted because its %s attribute is set to None." \
|
||||
% (self._meta.object_name, self._meta.pk.attname)
|
||||
self.clear_cache()
|
||||
super().delete()
|
||||
|
||||
def _get_size(self):
|
||||
return (self.width, self.height)
|
||||
|
||||
def _set_size(self, value):
|
||||
self.width, self.height = value
|
||||
|
||||
size = property(_get_size, _set_size)
|
||||
|
||||
|
||||
class PhotoSizeCache:
|
||||
__state = {"sizes": {}}
|
||||
|
||||
def __init__(self):
|
||||
self.__dict__ = self.__state
|
||||
if not len(self.sizes):
|
||||
sizes = PhotoSize.objects.all()
|
||||
for size in sizes:
|
||||
self.sizes[size.name] = size
|
||||
|
||||
def reset(self):
|
||||
global size_method_map
|
||||
size_method_map = {}
|
||||
self.sizes = {}
|
||||
|
||||
|
||||
def init_size_method_map():
|
||||
global size_method_map
|
||||
for size in PhotoSizeCache().sizes.keys():
|
||||
size_method_map['get_%s_size' % size] = \
|
||||
{'base_name': '_get_size_size', 'size': size}
|
||||
size_method_map['get_%s_photosize' % size] = \
|
||||
{'base_name': '_get_size_photosize', 'size': size}
|
||||
size_method_map['get_%s_url' % size] = \
|
||||
{'base_name': '_get_size_url', 'size': size}
|
||||
size_method_map['get_%s_filename' % size] = \
|
||||
{'base_name': '_get_size_filename', 'size': size}
|
||||
Loading…
Add table
Add a link
Reference in a new issue