models.py 5 KB
Newer Older
1
import hashlib
2
3
import os
import random
Thom Wiggers's avatar
Thom Wiggers committed
4
import logging
5

6
from PIL.JpegImagePlugin import JpegImageFile
7
8
9
10
from django.conf import settings
from django.db import models
from django.urls import reverse
from django.utils.functional import cached_property
11
from django.utils.translation import ugettext_lazy as _
12
from PIL import Image, ExifTags
13

14
15
from utils.translation import ModelTranslateMeta, MultilingualField

16
17
COVER_FILENAME = 'cover.jpg'

18
19
20
21
22
23
24
25
26
27
28
EXIF_ORIENTATION = {
    1: 0,
    2: 0,
    3: 180,
    4: 180,
    5: 90,
    6: 90,
    7: 270,
    8: 270,
}

29

Thom Wiggers's avatar
Thom Wiggers committed
30
31
32
logger = logging.getLogger(__name__)


33
def photo_uploadto(instance, filename):
34
    num = instance.album.photo_set.count()
35
    extension = os.path.splitext(filename)[1]
36
37
    new_filename = str(num).zfill(4) + extension
    return os.path.join(Album.photosdir, instance.album.dirname, new_filename)
38
39


40
41
42
43
44
45
46
47
48
49
50
51
def determine_rotation(pil_image):
    if isinstance(pil_image, JpegImageFile) and pil_image._getexif():
        exif = {
            ExifTags.TAGS[k]: v
            for k, v in pil_image._getexif().items()
            if k in ExifTags.TAGS
        }
        if exif.get('Orientation'):
            return EXIF_ORIENTATION[exif.get('Orientation')]
    return 0


52
class Photo(models.Model):
53

54
55
56
57
58
59
60
61
62
63
64
    album = models.ForeignKey(
        'Album',
        on_delete=models.CASCADE,
        verbose_name=_("album")
    )

    file = models.ImageField(
        _('file'),
        upload_to=photo_uploadto
    )

Joost Rijneveld's avatar
Joost Rijneveld committed
65
    rotation = models.IntegerField(
66
        verbose_name=_('rotation'),
Joost Rijneveld's avatar
Joost Rijneveld committed
67
68
        default=0,
        choices=((x, x) for x in (0, 90, 180, 270)),
69
70
71
72
73
74
        help_text=_('This does not modify the original image file.'),
    )

    hidden = models.BooleanField(
        _('hidden'),
        default=False
Joost Rijneveld's avatar
Joost Rijneveld committed
75
    )
76

77
78
79
80
81
    _digest = models.CharField(
        'digest',
        max_length=40,
    )

82
83
84
85
86
87
88
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if self.file:
            self._orig_file = self.file.path
        else:
            self._orig_file = ""

89
90
91
    def __str__(self):
        return self.file.name

92
    def save(self, *args, **kwargs):
93
        super().save(*args, **kwargs)
94

95
        if self._orig_file != self.file.path:
96
            image_path = self.file.path
97
            image = Image.open(image_path)
Thom Wiggers's avatar
Thom Wiggers committed
98
99
            image_path, _ext = os.path.splitext(image_path)
            image_path = "{}.jpg".format(image_path)
100
101
102

            self.rotation = determine_rotation(image)

103
            # Image.thumbnail does not upscale an image that is smaller
104
            image.thumbnail(settings.PHOTO_UPLOAD_SIZE, Image.ANTIALIAS)
Thom Wiggers's avatar
Thom Wiggers committed
105
106
107
108
109
110

            logger.info("Trying to save to %s", image_path)
            image.convert("RGB").save(image_path, "JPEG")
            self._orig_file = image_path
            image_name, _ext = os.path.splitext(self.file.name)
            self.file.name = "{}.jpg".format(image_name)
111

112
113
114
            hash_sha1 = hashlib.sha1()
            for chunk in iter(lambda: self.file.read(4096), b""):
                hash_sha1.update(chunk)
Tom van Bussel's avatar
Tom van Bussel committed
115
            self.file.close()
116
117
            self._digest = hash_sha1.hexdigest()

118
119
120
            # Save again, to update changes in digest and rotation
            super().save(*args, **kwargs)

121
122
123
    class Meta:
        ordering = ('file', )

124

125
126
127
128
class Album(models.Model, metaclass=ModelTranslateMeta):
    title = MultilingualField(
        models.CharField,
        _("title"),
129
130
131
132
133
134
135
136
137
138
139
140
141
142
        max_length=200,
    )

    dirname = models.CharField(
        verbose_name=_('directory name'),
        max_length=200,
    )

    date = models.DateField(
        verbose_name=_('date'),
    )

    slug = models.SlugField(
        verbose_name=_('slug'),
Tom van Bussel's avatar
Tom van Bussel committed
143
        unique=True,
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
    )

    hidden = models.BooleanField(
        verbose_name=_('hidden'),
        default=False
    )

    _cover = models.OneToOneField(
        Photo,
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
        related_name='covered_album',
        verbose_name=_('cover image'),
    )

    shareable = models.BooleanField(
        verbose_name=_('shareable'),
        default=False
    )
164
165
166
167
168
169

    photosdir = 'photos'
    photospath = os.path.join(settings.MEDIA_ROOT, photosdir)

    @cached_property
    def cover(self):
170
        cover = None
171
172
        if self._cover is not None:
            return self._cover
173
        elif self.photo_set.exists():
174
            random.seed(self.dirname)
175
176
            cover = random.choice(self.photo_set.all())
        return cover
177

178
179
180
181
    def __str__(self):
        return '{} {}'.format(self.date.strftime('%Y-%m-%d'), self.title)

    def get_absolute_url(self):
182
        return reverse('photos:album', args=[str(self.slug)])
183
184

    def save(self, *args, **kwargs):
185
186
187
        # dirname is only set for new objects, to avoid ever changing it
        if self.pk is None:
            self.dirname = self.slug
188
        super().save(*args, **kwargs)
189
190
191
192
193

    @property
    def access_token(self):
        return hashlib.sha256('{}album{}'.format(settings.SECRET_KEY, self.pk)
                              .encode('utf-8')).hexdigest()
194
195

    class Meta:
196
        ordering = ('-date', 'title_en')