Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Changes to `ImageFileDescriptor` and `ImageField` to fix a few cases …

…of setting image dimension fields.

 * Moved dimension field update logic out of `ImageFileDescriptor.__set__` and into its own method on `ImageField`.
 * New `ImageField.update_dimension_fields` method is attached to model instance's `post_init` signal so that:
   * Dimension fields are set when defined before the ImageField.
   * Dimension fields are set when the field is assigned in the model constructor (fixes #11196), but only if the dimension fields don't already have values, so we avoid updating the dimensions every time an object is loaded from the database (fixes #11084).
 * Clear dimension fields when the ImageField is set to None, which also causes dimension fields to be cleared when `ImageFieldFile.delete()` is used.
 * Added many more tests for ImageField that test edge cases we weren't testing before, and moved the ImageField tests out of `file_storage` and into their own module within `model_fields`.

git-svn-id: http://code.djangoproject.com/svn/django/branches/soc2009/http-wsgi-improvements@10987 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit f36391ecf53cf179c73e88fa3bdf0990fba5f230 1 parent 2b9cefc
Christopher Cahoon authored June 13, 2009
133  django/db/models/fields/files.py
@@ -142,13 +142,13 @@ class FileDescriptor(object):
142 142
     """
143 143
     The descriptor for the file attribute on the model instance. Returns a
144 144
     FieldFile when accessed so you can do stuff like::
145  
-    
  145
+
146 146
         >>> instance.file.size
147  
-        
  147
+
148 148
     Assigns a file object on assignment so you can do::
149  
-    
  149
+
150 150
         >>> instance.file = File(...)
151  
-        
  151
+
152 152
     """
153 153
     def __init__(self, field):
154 154
         self.field = field
@@ -156,9 +156,9 @@ def __init__(self, field):
156 156
     def __get__(self, instance=None, owner=None):
157 157
         if instance is None:
158 158
             raise AttributeError(
159  
-                "The '%s' attribute can only be accessed from %s instances." 
  159
+                "The '%s' attribute can only be accessed from %s instances."
160 160
                 % (self.field.name, owner.__name__))
161  
-                
  161
+
162 162
         # This is slightly complicated, so worth an explanation.
163 163
         # instance.file`needs to ultimately return some instance of `File`,
164 164
         # probably a subclass. Additionally, this returned object needs to have
@@ -168,8 +168,8 @@ def __get__(self, instance=None, owner=None):
168 168
         # peek below you can see that we're not. So depending on the current
169 169
         # value of the field we have to dynamically construct some sort of
170 170
         # "thing" to return.
171  
-        
172  
-        # The instance dict contains whatever was originally assigned 
  171
+
  172
+        # The instance dict contains whatever was originally assigned
173 173
         # in __set__.
174 174
         file = instance.__dict__[self.field.name]
175 175
 
@@ -186,14 +186,14 @@ def __get__(self, instance=None, owner=None):
186 186
 
187 187
         # Other types of files may be assigned as well, but they need to have
188 188
         # the FieldFile interface added to the. Thus, we wrap any other type of
189  
-        # File inside a FieldFile (well, the field's attr_class, which is 
  189
+        # File inside a FieldFile (well, the field's attr_class, which is
190 190
         # usually FieldFile).
191 191
         elif isinstance(file, File) and not isinstance(file, FieldFile):
192 192
             file_copy = self.field.attr_class(instance, self.field, file.name)
193 193
             file_copy.file = file
194 194
             file_copy._committed = False
195 195
             instance.__dict__[self.field.name] = file_copy
196  
-            
  196
+
197 197
         # Finally, because of the (some would say boneheaded) way pickle works,
198 198
         # the underlying FieldFile might not actually itself have an associated
199 199
         # file. So we need to reset the details of the FieldFile in those cases.
@@ -201,7 +201,7 @@ def __get__(self, instance=None, owner=None):
201 201
             file.instance = instance
202 202
             file.field = self.field
203 203
             file.storage = self.field.storage
204  
-        
  204
+
205 205
         # That was fun, wasn't it?
206 206
         return instance.__dict__[self.field.name]
207 207
 
@@ -212,7 +212,7 @@ class FileField(Field):
212 212
     # The class to wrap instance attributes in. Accessing the file object off
213 213
     # the instance will always return an instance of attr_class.
214 214
     attr_class = FieldFile
215  
-    
  215
+
216 216
     # The descriptor to use for accessing the attribute off of the class.
217 217
     descriptor_class = FileDescriptor
218 218
 
@@ -300,40 +300,20 @@ class ImageFileDescriptor(FileDescriptor):
300 300
     assigning the width/height to the width_field/height_field, if appropriate.
301 301
     """
302 302
     def __set__(self, instance, value):
  303
+        previous_file = instance.__dict__.get(self.field.name)
303 304
         super(ImageFileDescriptor, self).__set__(instance, value)
304  
-        
305  
-        # The rest of this method deals with width/height fields, so we can
306  
-        # bail early if neither is used.
307  
-        if not self.field.width_field and not self.field.height_field:
308  
-            return
309  
-        
310  
-        # We need to call the descriptor's __get__ to coerce this assigned 
311  
-        # value into an instance of the right type (an ImageFieldFile, in this
312  
-        # case).
313  
-        value = self.__get__(instance)
314  
-        
315  
-        if not value:
316  
-            return
317  
-        
318  
-        # Get the image dimensions, making sure to leave the file in the same
319  
-        # state (opened or closed) that we got it in. However, we *don't* rewind
320  
-        # the file pointer if the file is already open. This is in keeping with
321  
-        # most Python standard library file operations that leave it up to the
322  
-        # user code to reset file pointers after operations that move it.
323  
-        from django.core.files.images import get_image_dimensions
324  
-        close = value.closed
325  
-        value.open()
326  
-        try:
327  
-            width, height = get_image_dimensions(value)
328  
-        finally:
329  
-            if close:
330  
-                value.close()
331  
-        
332  
-        # Update the width and height fields
333  
-        if self.field.width_field:
334  
-            setattr(value.instance, self.field.width_field, width)
335  
-        if self.field.height_field:
336  
-            setattr(value.instance, self.field.height_field, height)
  305
+
  306
+        # To prevent recalculating image dimensions when we are instantiating
  307
+        # an object from the database (bug #11084), only update dimensions if
  308
+        # the field had a value before this assignment.  Since the default
  309
+        # value for FileField subclasses is an instance of field.attr_class,
  310
+        # previous_file will only be None when we are called from
  311
+        # Model.__init__().  The ImageField.update_dimension_fields method
  312
+        # hooked up to the post_init signal handles the Model.__init__() cases.
  313
+        # Assignment happening outside of Model.__init__() will trigger the
  314
+        # update right here.
  315
+        if previous_file is not None:
  316
+            self.field.update_dimension_fields(instance, force=True)
337 317
 
338 318
 class ImageFieldFile(ImageFile, FieldFile):
339 319
     def delete(self, save=True):
@@ -350,6 +330,69 @@ def __init__(self, verbose_name=None, name=None, width_field=None, height_field=
350 330
         self.width_field, self.height_field = width_field, height_field
351 331
         FileField.__init__(self, verbose_name, name, **kwargs)
352 332
 
  333
+    def contribute_to_class(self, cls, name):
  334
+        super(ImageField, self).contribute_to_class(cls, name)
  335
+        # Attach update_dimension_fields so that dimension fields declared
  336
+        # after their corresponding image field don't stay cleared by
  337
+        # Model.__init__, see bug #11196.
  338
+        signals.post_init.connect(self.update_dimension_fields, sender=cls)
  339
+
  340
+    def update_dimension_fields(self, instance, force=False, *args, **kwargs):
  341
+        """
  342
+        Updates field's width and height fields, if defined.
  343
+
  344
+        This method is hooked up to model's post_init signal to update
  345
+        dimensions after instantiating a model instance.  However, dimensions
  346
+        won't be updated if the dimensions fields are already populated.  This
  347
+        avoids unnecessary recalculation when loading an object from the
  348
+        database.
  349
+
  350
+        Dimensions can be forced to update with force=True, which is how
  351
+        ImageFileDescriptor.__set__ calls this method.
  352
+        """
  353
+        # Nothing to update if the field doesn't have have dimension fields.
  354
+        has_dimension_fields = self.width_field or self.height_field
  355
+        if not has_dimension_fields:
  356
+            return
  357
+
  358
+        # getattr will call the ImageFileDescriptor's __get__ method, which
  359
+        # coerces the assigned value into an instance of self.attr_class
  360
+        # (ImageFieldFile in this case).
  361
+        file = getattr(instance, self.attname)
  362
+
  363
+        # Nothing to update if we have no file and not being forced to update.
  364
+        if not file and not force:
  365
+            return
  366
+
  367
+        dimension_fields_filled = not(
  368
+            (self.width_field and not getattr(instance, self.width_field))
  369
+            or (self.height_field and not getattr(instance, self.height_field))
  370
+        )
  371
+        # When both dimension fields have values, we are most likely loading
  372
+        # data from the database or updating an image field that already had
  373
+        # an image stored.  In the first case, we don't want to update the
  374
+        # dimension fields because we are already getting their values from the
  375
+        # database.  In the second case, we do want to update the dimensions
  376
+        # fields and will skip this return because force will be True since we
  377
+        # were called from ImageFileDescriptor.__set__.
  378
+        if dimension_fields_filled and not force:
  379
+            return
  380
+
  381
+        # file should be an instance of ImageFieldFile or should be None.
  382
+        if file:
  383
+            width = file.width
  384
+            height = file.height
  385
+        else:
  386
+            # No file, so clear dimensions fields.
  387
+            width = None
  388
+            height = None
  389
+
  390
+        # Update the width and height fields.
  391
+        if self.width_field:
  392
+            setattr(instance, self.width_field, width)
  393
+        if self.height_field:
  394
+            setattr(instance, self.height_field, height)
  395
+
353 396
     def formfield(self, **kwargs):
354 397
         defaults = {'form_class': forms.ImageField}
355 398
         defaults.update(kwargs)
21  tests/modeltests/model_forms/models.py
@@ -1175,8 +1175,9 @@ def __unicode__(self):
1175 1175
 >>> instance.height
1176 1176
 16
1177 1177
 
1178  
-# Delete the current file since this is not done by Django.
1179  
->>> instance.image.delete()
  1178
+# Delete the current file since this is not done by Django, but don't save
  1179
+# because the dimension fields are not null=True.
  1180
+>>> instance.image.delete(save=False)
1180 1181
 
1181 1182
 >>> f = ImageFileForm(data={'description': u'An image'}, files={'image': SimpleUploadedFile('test.png', image_data)})
1182 1183
 >>> f.is_valid()
@@ -1207,9 +1208,9 @@ def __unicode__(self):
1207 1208
 >>> instance.width
1208 1209
 16
1209 1210
 
1210  
-# Delete the current image since this is not done by Django.
1211  
-
1212  
->>> instance.image.delete()
  1211
+# Delete the current file since this is not done by Django, but don't save
  1212
+# because the dimension fields are not null=True.
  1213
+>>> instance.image.delete(save=False)
1213 1214
 
1214 1215
 # Override the file by uploading a new one.
1215 1216
 
@@ -1224,8 +1225,9 @@ def __unicode__(self):
1224 1225
 >>> instance.width
1225 1226
 48
1226 1227
 
1227  
-# Delete the current file since this is not done by Django.
1228  
->>> instance.image.delete()
  1228
+# Delete the current file since this is not done by Django, but don't save
  1229
+# because the dimension fields are not null=True.
  1230
+>>> instance.image.delete(save=False)
1229 1231
 >>> instance.delete()
1230 1232
 
1231 1233
 >>> f = ImageFileForm(data={'description': u'Changed it'}, files={'image': SimpleUploadedFile('test2.png', image_data2)})
@@ -1239,8 +1241,9 @@ def __unicode__(self):
1239 1241
 >>> instance.width
1240 1242
 48
1241 1243
 
1242  
-# Delete the current file since this is not done by Django.
1243  
->>> instance.image.delete()
  1244
+# Delete the current file since this is not done by Django, but don't save
  1245
+# because the dimension fields are not null=True.
  1246
+>>> instance.image.delete(save=False)
1244 1247
 >>> instance.delete()
1245 1248
 
1246 1249
 # Test the non-required ImageField
93  tests/regressiontests/file_storage/models.py
... ...
@@ -1,93 +0,0 @@
1  
-import os
2  
-import tempfile
3  
-import shutil
4  
-from django.db import models
5  
-from django.core.files.storage import FileSystemStorage
6  
-from django.core.files.base import ContentFile
7  
-
8  
-# Test for correct behavior of width_field/height_field.
9  
-# Of course, we can't run this without PIL.
10  
-
11  
-try:
12  
-    # Checking for the existence of Image is enough for CPython, but
13  
-    # for PyPy, you need to check for the underlying modules
14  
-    from PIL import Image, _imaging
15  
-except ImportError:
16  
-    Image = None
17  
-
18  
-# If we have PIL, do these tests
19  
-if Image:
20  
-    temp_storage_dir = tempfile.mkdtemp()
21  
-    temp_storage = FileSystemStorage(temp_storage_dir)
22  
-
23  
-    class Person(models.Model):
24  
-        name = models.CharField(max_length=50)
25  
-        mugshot = models.ImageField(storage=temp_storage, upload_to='tests',
26  
-                                    height_field='mug_height',
27  
-                                    width_field='mug_width')
28  
-        mug_height = models.PositiveSmallIntegerField()
29  
-        mug_width = models.PositiveSmallIntegerField()
30  
-
31  
-    __test__ = {'API_TESTS': """
32  
->>> from django.core.files import File
33  
->>> image_data = open(os.path.join(os.path.dirname(__file__), "test.png"), 'rb').read()
34  
->>> p = Person(name="Joe")
35  
->>> p.mugshot.save("mug", ContentFile(image_data))
36  
->>> p.mugshot.width
37  
-16
38  
->>> p.mugshot.height
39  
-16
40  
->>> p.mug_height
41  
-16
42  
->>> p.mug_width
43  
-16
44  
-
45  
-# Bug #9786: Ensure '==' and '!=' work correctly.
46  
->>> image_data = open(os.path.join(os.path.dirname(__file__), "test1.png"), 'rb').read()
47  
->>> p1 = Person(name="Bob")
48  
->>> p1.mugshot.save("mug", ContentFile(image_data))
49  
->>> p2 = Person.objects.get(name="Joe")
50  
->>> p.mugshot == p2.mugshot
51  
-True
52  
->>> p.mugshot != p2.mugshot
53  
-False
54  
->>> p.mugshot != p1.mugshot
55  
-True
56  
-
57  
-Bug #9508: Similarly to the previous test, make sure hash() works as expected
58  
-(equal items must hash to the same value).
59  
->>> hash(p.mugshot) == hash(p2.mugshot)
60  
-True
61  
-
62  
-# Bug #8175: correctly delete files that have been removed off the file system.
63  
->>> import os
64  
->>> p2 = Person(name="Fred")
65  
->>> p2.mugshot.save("shot", ContentFile(image_data))
66  
->>> os.remove(p2.mugshot.path)
67  
->>> p2.delete()
68  
-
69  
-# Bug #8534: FileField.size should not leave the file open.
70  
->>> p3 = Person(name="Joan")
71  
->>> p3.mugshot.save("shot", ContentFile(image_data))
72  
-
73  
-# Get a "clean" model instance
74  
->>> p3 = Person.objects.get(name="Joan")
75  
-
76  
-# It won't have an opened file.
77  
->>> p3.mugshot.closed
78  
-True
79  
-
80  
-# After asking for the size, the file should still be closed.
81  
->>> _ = p3.mugshot.size
82  
->>> p3.mugshot.closed
83  
-True
84  
-
85  
-# Make sure that wrapping the file in a file still works
86  
->>> p3.mugshot.file.open()
87  
->>> p = Person.objects.create(name="Bob The Builder", mugshot=File(p3.mugshot.file))
88  
->>> p.save()
89  
->>> p3.mugshot.file.close() 
90  
-
91  
-# Delete all test files
92  
->>> shutil.rmtree(temp_storage_dir)
93  
-"""}
BIN  tests/regressiontests/model_fields/4x8.png
BIN  tests/regressiontests/model_fields/8x4.png
403  tests/regressiontests/model_fields/imagefield.py
... ...
@@ -0,0 +1,403 @@
  1
+import os
  2
+import shutil
  3
+
  4
+from django.core.files import File
  5
+from django.core.files.base import ContentFile
  6
+from django.core.files.images import ImageFile
  7
+from django.test import TestCase
  8
+
  9
+from models import Image, Person, PersonWithHeight, PersonWithHeightAndWidth, \
  10
+        PersonDimensionsFirst, PersonTwoImages, TestImageFieldFile
  11
+
  12
+
  13
+# If PIL available, do these tests.
  14
+if Image:
  15
+
  16
+    from models import temp_storage_dir
  17
+
  18
+
  19
+    class ImageFieldTestMixin(object):
  20
+        """
  21
+        Mixin class to provide common functionality to ImageField test classes.
  22
+        """
  23
+
  24
+        # Person model to use for tests.
  25
+        PersonModel = PersonWithHeightAndWidth
  26
+        # File class to use for file instances.
  27
+        File = ImageFile
  28
+
  29
+        def setUp(self):
  30
+            """
  31
+            Creates a pristine temp directory (or deletes and recreates if it
  32
+            already exists) that the model uses as its storage directory.
  33
+
  34
+            Sets up two ImageFile instances for use in tests.
  35
+            """
  36
+            if os.path.exists(temp_storage_dir):
  37
+                shutil.rmtree(temp_storage_dir)
  38
+            os.mkdir(temp_storage_dir)
  39
+
  40
+            file_path1 = os.path.join(os.path.dirname(__file__), "4x8.png")
  41
+            self.file1 = self.File(open(file_path1, 'rb'))
  42
+
  43
+            file_path2 = os.path.join(os.path.dirname(__file__), "8x4.png")
  44
+            self.file2 = self.File(open(file_path2, 'rb'))
  45
+
  46
+        def tearDown(self):
  47
+            """
  48
+            Removes temp directory and all its contents.
  49
+            """
  50
+            shutil.rmtree(temp_storage_dir)
  51
+
  52
+        def check_dimensions(self, instance, width, height,
  53
+                             field_name='mugshot'):
  54
+            """
  55
+            Asserts that the given width and height values match both the
  56
+            field's height and width attributes and the height and width fields
  57
+            (if defined) the image field is caching to.
  58
+
  59
+            Note, this method will check for dimension fields named by adding
  60
+            "_width" or "_height" to the name of the ImageField.  So, the
  61
+            models used in these tests must have their fields named
  62
+            accordingly.
  63
+
  64
+            By default, we check the field named "mugshot", but this can be
  65
+            specified by passing the field_name parameter.
  66
+            """
  67
+            field = getattr(instance, field_name)
  68
+            # Check height/width attributes of field.
  69
+            if width is None and height is None:
  70
+                self.assertRaises(ValueError, getattr, field, 'width')
  71
+                self.assertRaises(ValueError, getattr, field, 'height')
  72
+            else:
  73
+                self.assertEqual(field.width, width)
  74
+                self.assertEqual(field.height, height)
  75
+
  76
+            # Check height/width fields of model, if defined.
  77
+            width_field_name = field_name + '_width'
  78
+            if hasattr(instance, width_field_name):
  79
+                self.assertEqual(getattr(instance, width_field_name), width)
  80
+            height_field_name = field_name + '_height'
  81
+            if hasattr(instance, height_field_name):
  82
+                self.assertEqual(getattr(instance, height_field_name), height)
  83
+
  84
+
  85
+    class ImageFieldTests(ImageFieldTestMixin, TestCase):
  86
+        """
  87
+        Tests for ImageField that don't need to be run with each of the
  88
+        different test model classes.
  89
+        """
  90
+
  91
+        def test_equal_notequal_hash(self):
  92
+            """
  93
+            Bug #9786: Ensure '==' and '!=' work correctly.
  94
+            Bug #9508: make sure hash() works as expected (equal items must
  95
+            hash to the same value).
  96
+            """
  97
+            # Create two Persons with different mugshots.
  98
+            p1 = self.PersonModel(name="Joe")
  99
+            p1.mugshot.save("mug", self.file1)
  100
+            p2 = self.PersonModel(name="Bob")
  101
+            p2.mugshot.save("mug", self.file2)
  102
+            self.assertEqual(p1.mugshot == p2.mugshot, False)
  103
+            self.assertEqual(p1.mugshot != p2.mugshot, True)
  104
+
  105
+            # Test again with an instance fetched from the db.
  106
+            p1_db = self.PersonModel.objects.get(name="Joe")
  107
+            self.assertEqual(p1_db.mugshot == p2.mugshot, False)
  108
+            self.assertEqual(p1_db.mugshot != p2.mugshot, True)
  109
+
  110
+            # Instance from db should match the local instance.
  111
+            self.assertEqual(p1_db.mugshot == p1.mugshot, True)
  112
+            self.assertEqual(hash(p1_db.mugshot), hash(p1.mugshot))
  113
+            self.assertEqual(p1_db.mugshot != p1.mugshot, False)
  114
+
  115
+        def test_instantiate_missing(self):
  116
+            """
  117
+            If the underlying file is unavailable, still create instantiate the
  118
+            object without error.
  119
+            """
  120
+            p = self.PersonModel(name="Joan")
  121
+            p.mugshot.save("shot", self.file1)
  122
+            p = self.PersonModel.objects.get(name="Joan")
  123
+            path = p.mugshot.path
  124
+            shutil.move(path, path + '.moved')
  125
+            p2 = self.PersonModel.objects.get(name="Joan")
  126
+
  127
+        def test_delete_when_missing(self):
  128
+            """
  129
+            Bug #8175: correctly delete an object where the file no longer
  130
+            exists on the file system.
  131
+            """
  132
+            p = self.PersonModel(name="Fred")
  133
+            p.mugshot.save("shot", self.file1)
  134
+            os.remove(p.mugshot.path)
  135
+            p.delete()
  136
+
  137
+        def test_size_method(self):
  138
+            """
  139
+            Bug #8534: FileField.size should not leave the file open.
  140
+            """
  141
+            p = self.PersonModel(name="Joan")
  142
+            p.mugshot.save("shot", self.file1)
  143
+
  144
+            # Get a "clean" model instance
  145
+            p = self.PersonModel.objects.get(name="Joan")
  146
+            # It won't have an opened file.
  147
+            self.assertEqual(p.mugshot.closed, True)
  148
+
  149
+            # After asking for the size, the file should still be closed.
  150
+            _ = p.mugshot.size
  151
+            self.assertEqual(p.mugshot.closed, True)
  152
+
  153
+
  154
+    class ImageFieldTwoDimensionsTests(ImageFieldTestMixin, TestCase):
  155
+        """
  156
+        Tests behavior of an ImageField and its dimensions fields.
  157
+        """
  158
+
  159
+        def test_constructor(self):
  160
+            """
  161
+            Tests assigning an image field through the model's constructor.
  162
+            """
  163
+            p = self.PersonModel(name='Joe', mugshot=self.file1)
  164
+            self.check_dimensions(p, 4, 8)
  165
+            p.save()
  166
+            self.check_dimensions(p, 4, 8)
  167
+
  168
+        def test_image_after_constructor(self):
  169
+            """
  170
+            Tests behavior when image is not passed in constructor.
  171
+            """
  172
+            p = self.PersonModel(name='Joe')
  173
+            # TestImageField value will default to being an instance of its
  174
+            # attr_class, a  TestImageFieldFile, with name == None, which will
  175
+            # cause it to evaluate as False.
  176
+            self.assertEqual(isinstance(p.mugshot, TestImageFieldFile), True)
  177
+            self.assertEqual(bool(p.mugshot), False)
  178
+
  179
+            # Test setting a fresh created model instance.
  180
+            p = self.PersonModel(name='Joe')
  181
+            p.mugshot = self.file1
  182
+            self.check_dimensions(p, 4, 8)
  183
+
  184
+        def test_create(self):
  185
+            """
  186
+            Tests assigning an image in Manager.create().
  187
+            """
  188
+            p = self.PersonModel.objects.create(name='Joe', mugshot=self.file1)
  189
+            self.check_dimensions(p, 4, 8)
  190
+
  191
+        def test_default_value(self):
  192
+            """
  193
+            Tests that the default value for an ImageField is an instance of
  194
+            the field's attr_class (TestImageFieldFile in this case) with no
  195
+            name (name set to None).
  196
+            """
  197
+            p = self.PersonModel()
  198
+            self.assertEqual(isinstance(p.mugshot, TestImageFieldFile), True)
  199
+            self.assertEqual(bool(p.mugshot), False)
  200
+
  201
+        def test_assignment_to_None(self):
  202
+            """
  203
+            Tests that assigning ImageField to None clears dimensions.
  204
+            """
  205
+            p = self.PersonModel(name='Joe', mugshot=self.file1)
  206
+            self.check_dimensions(p, 4, 8)
  207
+
  208
+            # If image assigned to None, dimension fields should be cleared.
  209
+            p.mugshot = None
  210
+            self.check_dimensions(p, None, None)
  211
+
  212
+            p.mugshot = self.file2
  213
+            self.check_dimensions(p, 8, 4)
  214
+
  215
+        def test_field_save_and_delete_methods(self):
  216
+            """
  217
+            Tests assignment using the field's save method and deletion using
  218
+            the field's delete method.
  219
+            """
  220
+            p = self.PersonModel(name='Joe')
  221
+            p.mugshot.save("mug", self.file1)
  222
+            self.check_dimensions(p, 4, 8)
  223
+
  224
+            # A new file should update dimensions.
  225
+            p.mugshot.save("mug", self.file2)
  226
+            self.check_dimensions(p, 8, 4)
  227
+
  228
+            # Field and dimensions should be cleared after a delete.
  229
+            p.mugshot.delete(save=False)
  230
+            self.assertEqual(p.mugshot, None)
  231
+            self.check_dimensions(p, None, None)
  232
+
  233
+        def test_dimensions(self):
  234
+            """
  235
+            Checks that dimensions are updated correctly in various situations.
  236
+            """
  237
+            p = self.PersonModel(name='Joe')
  238
+
  239
+            # Dimensions should get set if file is saved.
  240
+            p.mugshot.save("mug", self.file1)
  241
+            self.check_dimensions(p, 4, 8)
  242
+
  243
+            # Test dimensions after fetching from database.
  244
+            p = self.PersonModel.objects.get(name='Joe')
  245
+            # Bug 11084: Dimensions should not get recalculated if file is
  246
+            # coming from the database.  We test this by checking if the file
  247
+            # was opened.
  248
+            self.assertEqual(p.mugshot.was_opened, False)
  249
+            self.check_dimensions(p, 4, 8)
  250
+            # After checking dimensions on the image field, the file will have
  251
+            # opened.
  252
+            self.assertEqual(p.mugshot.was_opened, True)
  253
+            # Dimensions should now be cached, and if we reset was_opened and
  254
+            # check dimensions again, the file should not have opened.
  255
+            p.mugshot.was_opened = False
  256
+            self.check_dimensions(p, 4, 8)
  257
+            self.assertEqual(p.mugshot.was_opened, False)
  258
+
  259
+            # If we assign a new image to the instance, the dimensions should
  260
+            # update.
  261
+            p.mugshot = self.file2
  262
+            self.check_dimensions(p, 8, 4)
  263
+            # Dimensions were recalculated, and hence file should have opened.
  264
+            self.assertEqual(p.mugshot.was_opened, True)
  265
+
  266
+
  267
+    class ImageFieldNoDimensionsTests(ImageFieldTwoDimensionsTests):
  268
+        """
  269
+        Tests behavior of an ImageField with no dimension fields.
  270
+        """
  271
+
  272
+        PersonModel = Person
  273
+
  274
+
  275
+    class ImageFieldOneDimensionTests(ImageFieldTwoDimensionsTests):
  276
+        """
  277
+        Tests behavior of an ImageField with one dimensions field.
  278
+        """
  279
+
  280
+        PersonModel = PersonWithHeight
  281
+
  282
+
  283
+    class ImageFieldDimensionsFirstTests(ImageFieldTwoDimensionsTests):
  284
+        """
  285
+        Tests behavior of an ImageField where the dimensions fields are
  286
+        defined before the ImageField.
  287
+        """
  288
+
  289
+        PersonModel = PersonDimensionsFirst
  290
+
  291
+
  292
+    class ImageFieldUsingFileTests(ImageFieldTwoDimensionsTests):
  293
+        """
  294
+        Tests behavior of an ImageField when assigning it a File instance
  295
+        rather than an ImageFile instance.
  296
+        """
  297
+
  298
+        PersonModel = PersonDimensionsFirst
  299
+        File = File
  300
+
  301
+
  302
+    class TwoImageFieldTests(ImageFieldTestMixin, TestCase):
  303
+        """
  304
+        Tests a model with two ImageFields.
  305
+        """
  306
+
  307
+        PersonModel = PersonTwoImages
  308
+
  309
+        def test_constructor(self):
  310
+            p = self.PersonModel(mugshot=self.file1, headshot=self.file2)
  311
+            self.check_dimensions(p, 4, 8, 'mugshot')
  312
+            self.check_dimensions(p, 8, 4, 'headshot')
  313
+            p.save()
  314
+            self.check_dimensions(p, 4, 8, 'mugshot')
  315
+            self.check_dimensions(p, 8, 4, 'headshot')
  316
+
  317
+        def test_create(self):
  318
+            p = self.PersonModel.objects.create(mugshot=self.file1,
  319
+                                                headshot=self.file2)
  320
+            self.check_dimensions(p, 4, 8)
  321
+            self.check_dimensions(p, 8, 4, 'headshot')
  322
+
  323
+        def test_assignment(self):
  324
+            p = self.PersonModel()
  325
+            self.check_dimensions(p, None, None, 'mugshot')
  326
+            self.check_dimensions(p, None, None, 'headshot')
  327
+
  328
+            p.mugshot = self.file1
  329
+            self.check_dimensions(p, 4, 8, 'mugshot')
  330
+            self.check_dimensions(p, None, None, 'headshot')
  331
+            p.headshot = self.file2
  332
+            self.check_dimensions(p, 4, 8, 'mugshot')
  333
+            self.check_dimensions(p, 8, 4, 'headshot')
  334
+
  335
+            # Clear the ImageFields one at a time.
  336
+            p.mugshot = None
  337
+            self.check_dimensions(p, None, None, 'mugshot')
  338
+            self.check_dimensions(p, 8, 4, 'headshot')
  339
+            p.headshot = None
  340
+            self.check_dimensions(p, None, None, 'mugshot')
  341
+            self.check_dimensions(p, None, None, 'headshot')
  342
+
  343
+        def test_field_save_and_delete_methods(self):
  344
+            p = self.PersonModel(name='Joe')
  345
+            p.mugshot.save("mug", self.file1)
  346
+            self.check_dimensions(p, 4, 8, 'mugshot')
  347
+            self.check_dimensions(p, None, None, 'headshot')
  348
+            p.headshot.save("head", self.file2)
  349
+            self.check_dimensions(p, 4, 8, 'mugshot')
  350
+            self.check_dimensions(p, 8, 4, 'headshot')
  351
+
  352
+            # We can use save=True when deleting the image field with null=True
  353
+            # dimension fields and the other field has an image.
  354
+            p.headshot.delete(save=True)
  355
+            self.check_dimensions(p, 4, 8, 'mugshot')
  356
+            self.check_dimensions(p, None, None, 'headshot')
  357
+            p.mugshot.delete(save=False)
  358
+            self.check_dimensions(p, None, None, 'mugshot')
  359
+            self.check_dimensions(p, None, None, 'headshot')
  360
+
  361
+        def test_dimensions(self):
  362
+            """
  363
+            Checks that dimensions are updated correctly in various situations.
  364
+            """
  365
+            p = self.PersonModel(name='Joe')
  366
+
  367
+            # Dimensions should get set for the saved file.
  368
+            p.mugshot.save("mug", self.file1)
  369
+            p.headshot.save("head", self.file2)
  370
+            self.check_dimensions(p, 4, 8, 'mugshot')
  371
+            self.check_dimensions(p, 8, 4, 'headshot')
  372
+
  373
+            # Test dimensions after fetching from database.
  374
+            p = self.PersonModel.objects.get(name='Joe')
  375
+            # Bug 11084: Dimensions should not get recalculated if file is
  376
+            # coming from the database.  We test this by checking if the file
  377
+            # was opened.
  378
+            self.assertEqual(p.mugshot.was_opened, False)
  379
+            self.assertEqual(p.headshot.was_opened, False)
  380
+            self.check_dimensions(p, 4, 8,'mugshot')
  381
+            self.check_dimensions(p, 8, 4, 'headshot')
  382
+            # After checking dimensions on the image fields, the files will
  383
+            # have been opened.
  384
+            self.assertEqual(p.mugshot.was_opened, True)
  385
+            self.assertEqual(p.headshot.was_opened, True)
  386
+            # Dimensions should now be cached, and if we reset was_opened and
  387
+            # check dimensions again, the file should not have opened.
  388
+            p.mugshot.was_opened = False
  389
+            p.headshot.was_opened = False
  390
+            self.check_dimensions(p, 4, 8,'mugshot')
  391
+            self.check_dimensions(p, 8, 4, 'headshot')
  392
+            self.assertEqual(p.mugshot.was_opened, False)
  393
+            self.assertEqual(p.headshot.was_opened, False)
  394
+
  395
+            # If we assign a new image to the instance, the dimensions should
  396
+            # update.
  397
+            p.mugshot = self.file2
  398
+            p.headshot = self.file1
  399
+            self.check_dimensions(p, 8, 4, 'mugshot')
  400
+            self.check_dimensions(p, 4, 8, 'headshot')
  401
+            # Dimensions were recalculated, and hence file should have opened.
  402
+            self.assertEqual(p.mugshot.was_opened, True)
  403
+            self.assertEqual(p.headshot.was_opened, True)
107  tests/regressiontests/model_fields/models.py
... ...
@@ -1,10 +1,23 @@
1  
-from django.db import models
  1
+import os
  2
+import tempfile
2 3
 
3 4
 try:
4 5
     import decimal
5 6
 except ImportError:
6 7
     from django.utils import _decimal as decimal    # Python 2.3 fallback
7 8
 
  9
+try:
  10
+    # Checking for the existence of Image is enough for CPython, but for PyPy,
  11
+    # you need to check for the underlying modules.
  12
+    from PIL import Image, _imaging
  13
+except ImportError:
  14
+    Image = None
  15
+
  16
+from django.core.files.storage import FileSystemStorage
  17
+from django.db import models
  18
+from django.db.models.fields.files import ImageFieldFile, ImageField
  19
+
  20
+
8 21
 class Foo(models.Model):
9 22
     a = models.CharField(max_length=10)
10 23
     d = models.DecimalField(max_digits=5, decimal_places=3)
@@ -31,9 +44,97 @@ class Whiz(models.Model):
31 44
         (0,'Other'),
32 45
     )
33 46
     c = models.IntegerField(choices=CHOICES, null=True)
34  
-    
  47
+
35 48
 class BigD(models.Model):
36 49
     d = models.DecimalField(max_digits=38, decimal_places=30)
37 50
 
38 51
 class BigS(models.Model):
39  
-    s = models.SlugField(max_length=255)
  52
+    s = models.SlugField(max_length=255)
  53
+
  54
+
  55
+###############################################################################
  56
+# ImageField
  57
+
  58
+# If PIL available, do these tests.
  59
+if Image:
  60
+    class TestImageFieldFile(ImageFieldFile):
  61
+        """
  62
+        Custom Field File class that records whether or not the underlying file
  63
+        was opened.
  64
+        """
  65
+        def __init__(self, *args, **kwargs):
  66
+            self.was_opened = False
  67
+            super(TestImageFieldFile, self).__init__(*args,**kwargs)
  68
+        def open(self):
  69
+            self.was_opened = True
  70
+            super(TestImageFieldFile, self).open()
  71
+
  72
+    class TestImageField(ImageField):
  73
+        attr_class = TestImageFieldFile
  74
+
  75
+    # Set up a temp directory for file storage.
  76
+    temp_storage_dir = tempfile.mkdtemp()
  77
+    temp_storage = FileSystemStorage(temp_storage_dir)
  78
+    temp_upload_to_dir = os.path.join(temp_storage.location, 'tests')
  79
+
  80
+    class Person(models.Model):
  81
+        """
  82
+        Model that defines an ImageField with no dimension fields.
  83
+        """
  84
+        name = models.CharField(max_length=50)
  85
+        mugshot = TestImageField(storage=temp_storage, upload_to='tests')
  86
+
  87
+    class PersonWithHeight(models.Model):
  88
+        """
  89
+        Model that defines an ImageField with only one dimension field.
  90
+        """
  91
+        name = models.CharField(max_length=50)
  92
+        mugshot = TestImageField(storage=temp_storage, upload_to='tests',
  93
+                                 height_field='mugshot_height')
  94
+        mugshot_height = models.PositiveSmallIntegerField()
  95
+
  96
+    class PersonWithHeightAndWidth(models.Model):
  97
+        """
  98
+        Model that defines height and width fields after the ImageField.
  99
+        """
  100
+        name = models.CharField(max_length=50)
  101
+        mugshot = TestImageField(storage=temp_storage, upload_to='tests',
  102
+                                 height_field='mugshot_height',
  103
+                                 width_field='mugshot_width')
  104
+        mugshot_height = models.PositiveSmallIntegerField()
  105
+        mugshot_width = models.PositiveSmallIntegerField()
  106
+
  107
+    class PersonDimensionsFirst(models.Model):
  108
+        """
  109
+        Model that defines height and width fields before the ImageField.
  110
+        """
  111
+        name = models.CharField(max_length=50)
  112
+        mugshot_height = models.PositiveSmallIntegerField()
  113
+        mugshot_width = models.PositiveSmallIntegerField()
  114
+        mugshot = TestImageField(storage=temp_storage, upload_to='tests',
  115
+                                 height_field='mugshot_height',
  116
+                                 width_field='mugshot_width')
  117
+
  118
+    class PersonTwoImages(models.Model):
  119
+        """
  120
+        Model that:
  121
+        * Defines two ImageFields
  122
+        * Defines the height/width fields before the ImageFields
  123
+        * Has a nullalble ImageField
  124
+        """
  125
+        name = models.CharField(max_length=50)
  126
+        mugshot_height = models.PositiveSmallIntegerField()
  127
+        mugshot_width = models.PositiveSmallIntegerField()
  128
+        mugshot = TestImageField(storage=temp_storage, upload_to='tests',
  129
+                                 height_field='mugshot_height',
  130
+                                 width_field='mugshot_width')
  131
+        headshot_height = models.PositiveSmallIntegerField(
  132
+                blank=True, null=True)
  133
+        headshot_width = models.PositiveSmallIntegerField(
  134
+                blank=True, null=True)
  135
+        headshot = TestImageField(blank=True, null=True,
  136
+                                  storage=temp_storage, upload_to='tests',
  137
+                                  height_field='headshot_height',
  138
+                                  width_field='headshot_width')
  139
+
  140
+###############################################################################
16  tests/regressiontests/model_fields/tests.py
@@ -6,13 +6,26 @@
6 6
 from django.db import models
7 7
 from django.core.exceptions import ValidationError
8 8
 
9  
-from models import Foo, Bar, Whiz, BigD, BigS
  9
+from models import Foo, Bar, Whiz, BigD, BigS, Image
10 10
 
11 11
 try:
12 12
     from decimal import Decimal
13 13
 except ImportError:
14 14
     from django.utils._decimal import Decimal
15 15
 
  16
+
  17
+# If PIL available, do these tests.
  18
+if Image:
  19
+    from imagefield import \
  20
+            ImageFieldTests, \
  21
+            ImageFieldTwoDimensionsTests, \
  22
+            ImageFieldNoDimensionsTests, \
  23
+            ImageFieldOneDimensionTests, \
  24
+            ImageFieldDimensionsFirstTests, \
  25
+            ImageFieldUsingFileTests, \
  26
+            TwoImageFieldTests
  27
+
  28
+
16 29
 class DecimalFieldTests(django.test.TestCase):
17 30
     def test_to_python(self):
18 31
         f = models.DecimalField(max_digits=4, decimal_places=2)
@@ -131,4 +144,3 @@ def test_slugfield_max_length(self):
131 144
         bs = BigS.objects.create(s = 'slug'*50)
132 145
         bs = BigS.objects.get(pk=bs.pk)
133 146
         self.assertEqual(bs.s, 'slug'*50)
134  
-

0 notes on commit f36391e

Please sign in to comment.
Something went wrong with that request. Please try again.