Completed
Push — master ( 84e615...a5de1a )
by Johannes
01:08
created

StdImageField.__init__()   F

Complexity

Conditions 10

Size

Total Lines 45

Duplication

Lines 0
Ratio 0 %

Importance

Changes 18
Bugs 4 Features 2
Metric Value
c 18
b 4
f 2
dl 0
loc 45
rs 3.1304
cc 10

How to fix   Complexity   

Complexity

Complex classes like StdImageField.__init__() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
# -*- coding: utf-8 -*-
2
from __future__ import absolute_import, unicode_literals
3
4
import logging
5
import os
6
from io import BytesIO
7
8
from django.core.files.base import ContentFile
9
from django.core.files.storage import default_storage
10
from django.db.models import signals
11
from django.db.models.fields.files import (
12
    ImageField, ImageFieldFile, ImageFileDescriptor
13
)
14
from PIL import Image, ImageOps
15
16
from .validators import MinSizeValidator
17
18
logger = logging.getLogger()
19
20
21
class StdImageFileDescriptor(ImageFileDescriptor):
22
    """The variation property of the field is accessible in instance cases."""
23
24
    def __set__(self, instance, value):
25
        super(StdImageFileDescriptor, self).__set__(instance, value)
26
        self.field.set_variations(instance)
27
28
29
class StdImageFieldFile(ImageFieldFile):
30
    """Like ImageFieldFile but handles variations."""
31
32
    def save(self, name, content, save=True):
33
        super(StdImageFieldFile, self).save(name, content, save)
34
        render_variations = self.field.render_variations
35
        if callable(render_variations):
36
            render_variations = render_variations(
37
                file_name=self.name,
38
                variations=self.field.variations,
39
                storage=self.storage,
40
            )
41
        if not isinstance(render_variations, bool):
42
            msg = (
43
                '"render_variations" callable expects a boolean return value,'
44
                ' but got %s'
45
                ) % type(render_variations)
46
            raise TypeError(msg)
47
        if render_variations:
48
            self.render_variations()
49
50
    @staticmethod
51
    def is_smaller(img, variation):
52
        return img.size[0] > variation['width'] \
53
            or img.size[1] > variation['height']
54
55
    def render_variations(self, replace=False):
56
        """Render all image variations and saves them to the storage."""
57
        for _, variation in self.field.variations.items():
58
            self.render_variation(self.name, variation, replace, self.storage)
59
60
    @classmethod
61
    def render_variation(cls, file_name, variation, replace=False,
62
                         storage=default_storage):
63
        """Render an image variation and saves it to the storage."""
64
        variation_name = cls.get_variation_name(file_name, variation['name'])
65
        if storage.exists(variation_name):
66
            if replace:
67
                storage.delete(variation_name)
68
                logger.info('File "{}" already exists and has been replaced.')
69
            else:
70
                logger.info('File "{}" already exists.')
71
                return variation_name
72
73
        resample = variation['resample']
74
75
        with storage.open(file_name) as f:
76
            with Image.open(f) as img:
77
                file_format = img.format
78
79
                if cls.is_smaller(img, variation):
80
                    factor = 1
81
                    while img.size[0] / factor \
82
                            > 2 * variation['width'] \
83
                            and img.size[1] * 2 / factor \
84
                            > 2 * variation['height']:
85
                        factor *= 2
86
                    if factor > 1:
87
                        img.thumbnail(
88
                            (int(img.size[0] / factor),
89
                             int(img.size[1] / factor)),
90
                            resample=resample
91
                        )
92
93
                    size = variation['width'], variation['height']
94
                    size = tuple(int(i) if i != float('inf') else i
95
                                 for i in size)
96
                    if variation['crop']:
97
                        img = ImageOps.fit(
98
                            img,
99
                            size,
100
                            method=resample
101
                        )
102
                    else:
103
                        img.thumbnail(
104
                            size,
105
                            resample=resample
106
                        )
107
108
                with BytesIO() as file_buffer:
109
                    img.save(file_buffer, file_format)
110
                    f = ContentFile(file_buffer.getvalue())
111
                    storage.save(variation_name, f)
112
        return variation_name
113
114
    @classmethod
115
    def get_variation_name(cls, file_name, variation_name):
116
        """Return the variation file name based on the variation."""
117
        path, ext = os.path.splitext(file_name)
118
        path, file_name = os.path.split(path)
119
        file_name = '{file_name}.{variation_name}{extension}'.format(**{
120
            'file_name': file_name,
121
            'variation_name': variation_name,
122
            'extension': ext,
123
        })
124
        return os.path.join(path, file_name)
125
126
    def delete(self, save=True):
127
        self.delete_variations()
128
        super(StdImageFieldFile, self).delete(save)
129
130
    def delete_variations(self):
131
        for variation in self.field.variations:
132
            variation_name = self.get_variation_name(self.name, variation)
133
            self.storage.delete(variation_name)
134
135
136
class StdImageField(ImageField):
137
    """
138
    Django ImageField that is able to create different size variations.
139
140
    Extra features are:
141
        - Django-Storages compatible (S3)
142
        - Python 2, 3 and PyPy support
143
        - Django 1.5 and later support
144
        - Resize images to different sizes
145
        - Access thumbnails on model level, no template tags required
146
        - Preserves original image
147
        - Asynchronous rendering (Celery & Co)
148
        - Multi threading and processing for optimum performance
149
        - Restrict accepted image dimensions
150
        - Rename files to a standardized name (using a callable upload_to)
151
152
    :param variations: size variations of the image
153
    """
154
155
    descriptor_class = StdImageFileDescriptor
156
    attr_class = StdImageFieldFile
157
    def_variation = {
158
        'width': float('inf'),
159
        'height': float('inf'),
160
        'crop': False,
161
        'resample': Image.ANTIALIAS
162
    }
163
164
    def __init__(self, verbose_name=None, name=None, variations=None,
165
                 render_variations=True, force_min_size=False,
166
                 *args, **kwargs):
167
        """
168
        Standardized ImageField for Django.
169
170
        Usage: StdImageField(upload_to='PATH',
171
         variations={'thumbnail': {"width", "height", "crop", "resample"}})
172
        :param variations: size variations of the image
173
        :rtype variations: StdImageField
174
        :param render_variations: boolean or callable that returns a boolean.
175
         The callable gets passed the app_name, model, field_name and pk.
176
         Default: True
177
        :rtype render_variations: bool, callable
178
        """
179
        if not variations:
180
            variations = {}
181
        if not isinstance(variations, dict):
182
            msg = ('"variations" expects a dict,'
183
                   ' but got %s') % type(variations)
184
            raise TypeError(msg)
185
        if not (isinstance(render_variations, bool) or
186
                callable(render_variations)):
187
            msg = ('"render_variations" excepts a boolean or callable,'
188
                   ' but got %s') % type(render_variations)
189
            raise TypeError(msg)
190
191
        self._variations = variations
192
        self.force_min_size = force_min_size
193
        self.render_variations = render_variations
194
        self.variations = {}
195
196
        for nm, prm in list(variations.items()):
197
            self.add_variation(nm, prm)
198
199
        if self.variations and self.force_min_size:
200
            self.min_size = (
201
                max(self.variations.values(),
202
                    key=lambda x: x["width"])["width"],
203
                max(self.variations.values(),
204
                    key=lambda x: x["height"])["height"]
205
            )
206
207
        super(StdImageField, self).__init__(verbose_name, name,
208
                                            *args, **kwargs)
209
210
    def add_variation(self, name, params):
211
        variation = self.def_variation.copy()
212
        if isinstance(params, (list, tuple)):
213
            variation.update(dict(zip(("width", "height", "crop"), params)))
214
        else:
215
            variation.update(params)
216
        variation["name"] = name
217
        self.variations[name] = variation
218
219
    def set_variations(self, instance=None, **kwargs):
220
        """
221
        Create a "variation" object as attribute of the ImageField instance.
222
223
        Variation attribute will be of the same class as the original image, so
224
        "path", "url"... properties can be used
225
226
        :param instance: FileField
227
        """
228
        if getattr(instance, self.name):
229
            field = getattr(instance, self.name)
230
            if field._committed:
231
                for name, variation in list(self.variations.items()):
232
                    variation_name = self.attr_class.get_variation_name(
233
                        field.name,
234
                        variation['name']
235
                    )
236
                    variation_field = ImageFieldFile(instance,
237
                                                     self,
238
                                                     variation_name)
239
                    setattr(field, name, variation_field)
240
241
    def contribute_to_class(self, cls, name):
242
        """Generating all operations on specified signals."""
243
        super(StdImageField, self).contribute_to_class(cls, name)
244
        signals.post_init.connect(self.set_variations, sender=cls)
245
246
    def validate(self, value, model_instance):
247
        super(StdImageField, self).validate(value, model_instance)
248
        if self.force_min_size:
249
            MinSizeValidator(self.min_size[0], self.min_size[1])(value)
250