diff --git a/wagtail/wagtailimages/migrations/0003_fix_focal_point_fields.py b/wagtail/wagtailimages/migrations/0003_fix_focal_point_fields.py new file mode 100644 index 0000000000..b5cae9f8d3 --- /dev/null +++ b/wagtail/wagtailimages/migrations/0003_fix_focal_point_fields.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + """ + When the initial migration was created, the focal point fields on image + did not have blank=True set. + + This migration fixes this. + """ + + dependencies = [ + ('wagtailimages', '0002_initial_data'), + ] + + operations = [ + migrations.AlterField( + model_name='image', + name='focal_point_height', + field=models.PositiveIntegerField(null=True, blank=True), + ), + migrations.AlterField( + model_name='image', + name='focal_point_width', + field=models.PositiveIntegerField(null=True, blank=True), + ), + migrations.AlterField( + model_name='image', + name='focal_point_x', + field=models.PositiveIntegerField(null=True, blank=True), + ), + migrations.AlterField( + model_name='image', + name='focal_point_y', + field=models.PositiveIntegerField(null=True, blank=True), + ), + ] diff --git a/wagtail/wagtailimages/migrations/0004_make_focal_point_key_not_nullable.py b/wagtail/wagtailimages/migrations/0004_make_focal_point_key_not_nullable.py new file mode 100644 index 0000000000..47e8c1f669 --- /dev/null +++ b/wagtail/wagtailimages/migrations/0004_make_focal_point_key_not_nullable.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('wagtailimages', '0003_fix_focal_point_fields'), + ] + + operations = [ + migrations.AlterField( + model_name='rendition', + name='focal_point_key', + field=models.CharField(blank=True, default='', max_length=255, editable=False), + ), + ] diff --git a/wagtail/wagtailimages/models.py b/wagtail/wagtailimages/models.py index abd7c06a70..35d55a7986 100644 --- a/wagtail/wagtailimages/models.py +++ b/wagtail/wagtailimages/models.py @@ -143,7 +143,7 @@ class AbstractImage(models.Model, TagSearchable): else: rendition = self.renditions.get( filter=filter, - focal_point_key=None, + focal_point_key='', ) except ObjectDoesNotExist: file_field = self.file @@ -175,7 +175,6 @@ class AbstractImage(models.Model, TagSearchable): else: rendition, created = self.renditions.get_or_create( filter=filter, - focal_point_key=None, defaults={'file': generated_image_file} ) @@ -345,7 +344,7 @@ class AbstractRendition(models.Model): file = models.ImageField(upload_to='images', width_field='width', height_field='height') width = models.IntegerField(editable=False) height = models.IntegerField(editable=False) - focal_point_key = models.CharField(max_length=255, null=True, editable=False) + focal_point_key = models.CharField(max_length=255, blank=True, default='', editable=False) @property def url(self): diff --git a/wagtail/wagtailimages/south_migrations/0004_auto__chg_field_rendition_focal_point_key.py b/wagtail/wagtailimages/south_migrations/0004_auto__chg_field_rendition_focal_point_key.py new file mode 100644 index 0000000000..4628273aa3 --- /dev/null +++ b/wagtail/wagtailimages/south_migrations/0004_auto__chg_field_rendition_focal_point_key.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Changing field 'Rendition.focal_point_key' + db.alter_column('wagtailimages_rendition', 'focal_point_key', self.gf('django.db.models.fields.CharField')(max_length=255, default='')) + + def backwards(self, orm): + + # Changing field 'Rendition.focal_point_key' + db.alter_column('wagtailimages_rendition', 'focal_point_key', self.gf('django.db.models.fields.CharField')(max_length=255, null=True)) + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'user_set'", 'blank': 'True', 'to': "orm['auth.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'user_set'", 'blank': 'True', 'to': "orm['auth.Permission']"}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'wagtailimages.filter': { + 'Meta': {'object_name': 'Filter'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'spec': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}) + }, + 'wagtailimages.image': { + 'Meta': {'object_name': 'Image'}, + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'file': ('django.db.models.fields.files.ImageField', [], {'max_length': '100'}), + 'focal_point_height': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}), + 'focal_point_width': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}), + 'focal_point_x': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}), + 'focal_point_y': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}), + 'height': ('django.db.models.fields.IntegerField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'uploaded_by_user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'width': ('django.db.models.fields.IntegerField', [], {}) + }, + 'wagtailimages.rendition': { + 'Meta': {'unique_together': "(('image', 'filter', 'focal_point_key'),)", 'object_name': 'Rendition'}, + 'file': ('django.db.models.fields.files.ImageField', [], {'max_length': '100'}), + 'filter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'+'", 'to': "orm['wagtailimages.Filter']"}), + 'focal_point_key': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'blank': 'True'}), + 'height': ('django.db.models.fields.IntegerField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'image': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'renditions'", 'to': "orm['wagtailimages.Image']"}), + 'width': ('django.db.models.fields.IntegerField', [], {}) + } + } + + complete_apps = ['wagtailimages'] \ No newline at end of file diff --git a/wagtail/wagtailimages/tests.py b/wagtail/wagtailimages/tests.py index 46931bc755..80e9932af6 100644 --- a/wagtail/wagtailimages/tests.py +++ b/wagtail/wagtailimages/tests.py @@ -14,9 +14,10 @@ from django.contrib.auth import get_user_model from django.contrib.auth.models import Group, Permission from django.core.urlresolvers import reverse from django.core.files.uploadedfile import SimpleUploadedFile +from django.db.utils import IntegrityError from wagtail.tests.utils import unittest, WagtailTestUtils -from wagtail.wagtailimages.models import get_image_model +from wagtail.wagtailimages.models import get_image_model, Rendition from wagtail.wagtailimages.formats import ( Format, get_image_format, @@ -1132,3 +1133,28 @@ class TestIssue613(TestCase, WagtailTestUtils): # Check self.assertEqual(len(results), 1) self.assertEqual(results[0].id, image.id) + + +class TestIssue312(TestCase): + def test_duplicate_renditions(self): + # Create an image + image = Image.objects.create( + title="Test image", + file=get_test_image_file(), + ) + + # Get two renditions and check that they're the same + rend1 = image.get_rendition('fill-100x100') + rend2 = image.get_rendition('fill-100x100') + self.assertEqual(rend1, rend2) + + # Now manually duplicate the renditon and check that the database blocks it + self.assertRaises( + IntegrityError, + Rendition.objects.create, + image=rend1.image, + filter=rend1.filter, + width=rend1.width, + height=rend1.height, + focal_point_key=rend1.focal_point_key, + )