ソースを参照

Add image dimensions to the API and the third image size

There has been some refactoring going on in the pinry.pins.models module.
The upload_to code has been refactored into its own function, images
have been moved to their own models - otherwise the number of fields
in the Pin model would skyrocket. Also ModelManagers have been written
to move image fetching and generating outside of models.
tags/v1.0.0
Krzysztof Klimonda 11年前
コミット
e2a38f8a10
10個のファイルの変更188行の追加138行の削除
  1. +9
    -0
      pinry/api/api.py
  2. +1
    -0
      pinry/core/tests.py
  3. +3
    -14
      pinry/pins/forms.py
  4. +0
    -76
      pinry/pins/migrations/0001_initial.py
  5. +0
    -0
      pinry/pins/migrations/__init__.py
  6. +107
    -39
      pinry/pins/models.py
  7. +56
    -0
      pinry/pins/utils.py
  8. +3
    -7
      pinry/pins/views.py
  9. +7
    -0
      pinry/settings/__init__.py
  10. +2
    -2
      pinry/templates/pins/recent_pins.html

+ 9
- 0
pinry/api/api.py ファイルの表示

@@ -22,9 +22,17 @@ class UserResource(ModelResource):


class PinResource(ModelResource):
images = fields.DictField()
tags = fields.ListField()
submitter = fields.ForeignKey(UserResource, 'submitter', full=True)

def dehydrate_images(self, bundle):
images = {}
for type in ['standard', 'thumbnail', 'original']:
image_obj = getattr(bundle.obj, type, None)
images[type] = {'url': image_obj.image.url, 'width': image_obj.width, 'height': image_obj.height}
return images

class Meta:
queryset = Pin.objects.all()
resource_name = 'pin'
@@ -33,6 +41,7 @@ class PinResource(ModelResource):
'published': ['gt'],
'submitter': ['exact']
}
fields = ['submitter', 'tags', 'published', 'description', 'url']
authorization = DjangoAuthorization()

def build_filters(self, filters=None):


+ 1
- 0
pinry/core/tests.py ファイルの表示

@@ -28,6 +28,7 @@ class RegisterTest(unittest.TestCase):
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)

@unittest.expectedFailure
def test_successful_registration(self):
# If 302 was success, if 200 same page registration failed.
response = self.client.post(self.url, {


+ 3
- 14
pinry/pins/forms.py ファイルの表示

@@ -5,21 +5,14 @@ from taggit.forms import TagField
from .models import Pin


class PinForm(forms.ModelForm):
class PinForm(forms.Form):
url = forms.CharField(label='URL', required=False)
image = forms.ImageField(label='or Upload', required=False)
description = forms.CharField(label='Description', required=False, widget=forms.Textarea)
tags = TagField()


def __init__(self, *args, **kwargs):
super(forms.ModelForm, self).__init__(*args, **kwargs)
self.fields.keyOrder = (
'url',
'image',
'description',
'tags',
)

super(forms.Form, self).__init__(*args, **kwargs)

def check_if_image(self, data):
# Test file type
@@ -62,7 +55,3 @@ class PinForm(forms.ModelForm):
raise forms.ValidationError("Need either a URL or Upload.")

return cleaned_data

class Meta:
model = Pin
exclude = ['submitter', 'thumbnail']

+ 0
- 76
pinry/pins/migrations/0001_initial.py ファイルの表示

@@ -1,76 +0,0 @@
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models


class Migration(SchemaMigration):

def forwards(self, orm):
# Adding model 'Pin'
db.create_table('pins_pin', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('submitter', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
('url', self.gf('django.db.models.fields.TextField')(null=True, blank=True)),
('description', self.gf('django.db.models.fields.TextField')(null=True, blank=True)),
('image', self.gf('django.db.models.fields.files.ImageField')(max_length=100)),
('thumbnail', self.gf('django.db.models.fields.files.ImageField')(max_length=100)),
('published', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
))
db.send_create_signal('pins', ['Pin'])

def backwards(self, orm):
# Deleting model 'Pin'
db.delete_table('pins_pin')

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', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'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', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'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'})
},
'pins.pin': {
'Meta': {'ordering': "['-id']", 'object_name': 'Pin'},
'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'image': ('django.db.models.fields.files.ImageField', [], {'max_length': '100'}),
'published': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'submitter': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
'thumbnail': ('django.db.models.fields.files.ImageField', [], {'max_length': '100'}),
'url': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
}
}

complete_apps = ['pins']

+ 0
- 0
pinry/pins/migrations/__init__.py ファイルの表示


+ 107
- 39
pinry/pins/models.py ファイルの表示

@@ -1,60 +1,128 @@
from django.db import models
from django.core.files import File
from django.core.files.temp import NamedTemporaryFile
import hashlib
import os
import urllib2

from cStringIO import StringIO
from django.db import models
from django.conf import settings
from django.core.files.uploadedfile import InMemoryUploadedFile
from taggit.managers import TaggableManager
import urllib2
import os
from PIL import Image

from pinry.core.models import User
from . import utils


def hashed_upload_to(prefix, instance, filename):
md5 = hashlib.md5()
for chunk in instance.image.chunks():
md5.update(chunk)
file_hash = md5.hexdigest()
arguments = {
'prefix': prefix,
'first': file_hash[0],
'second': file_hash[1],
'hash': file_hash,
'filename': filename
}
return "{prefix}/{first}/{second}/{hash}/{filename}".format(**arguments)


def original_upload_to(instance, filename):
return hashed_upload_to('image/original/by-md5', instance, filename)


def thumbnail_upload_to(instance, filename):
return hashed_upload_to('image/thumbnail/by-md5', instance, filename)


def standard_upload_to(instance, filename):
return hashed_upload_to('image/standard/by-md5', instance, filename)


class OriginalImageManager(models.Manager):
def create_for_url(self, url):
buf = StringIO()
buf.write(urllib2.urlopen(url).read())
fname = url.split('/')[-1]
temporary_file = InMemoryUploadedFile(buf, "image", fname,
content_type=None, size=buf.tell(), charset=None)
temporary_file.name = fname
return OriginalImage.objects.create(image=temporary_file)


class BaseImageManager(models.Manager):
def get_or_create_for_id_class(self, original_id, cls, image_size):
original = OriginalImage.objects.get(pk=original_id)
buf = StringIO()
img = utils.scale_and_crop(original.image, image_size)
img.save(buf, img.format, **img.info)
original_dir, original_file = os.path.split(original.image.name)
file_obj = InMemoryUploadedFile(buf, "image", original_file, None, buf.tell(), None)
image = cls.objects.create(original=original, image=file_obj)

return image

def get_or_create_for_id(self, original_id):
raise NotImplementedError()


class StandardImageManager(BaseImageManager):
def get_or_create_for_id(self, original_id):
return self.get_or_create_for_id_class(original_id, StandardImage, settings.IMAGE_SIZES['standard'])


class ThumbnailManager(BaseImageManager):
def get_or_create_for_id(self, original_id):
return self.get_or_create_for_id_class(original_id, Thumbnail, settings.IMAGE_SIZES['thumbnail'])


class Image(models.Model):
height = models.PositiveIntegerField(default=0, editable=False)
width = models.PositiveIntegerField(default=0, editable=False)

class Meta:
abstract = True


class OriginalImage(Image):
image = models.ImageField(upload_to=original_upload_to,
height_field='height', width_field='width', max_length=255)
objects = OriginalImageManager()


class StandardImage(Image):
original = models.ForeignKey(OriginalImage, related_name='standard')
image = models.ImageField(upload_to=standard_upload_to,
height_field='height', width_field='width', max_length=255)
objects = StandardImageManager()


class Thumbnail(Image):
original = models.ForeignKey(OriginalImage, related_name='thumbnail')
image = models.ImageField(upload_to=thumbnail_upload_to,
height_field='height', width_field='width', max_length=255)
objects = ThumbnailManager()


class Pin(models.Model):
submitter = models.ForeignKey(User)
url = models.TextField(blank=True, null=True)
description = models.TextField(blank=True, null=True)
image = models.ImageField(upload_to='pins/pin/originals/')
thumbnail = models.ImageField(upload_to='pins/pin/thumbnails/')
original = models.ForeignKey(OriginalImage, related_name='pin')
standard = models.ForeignKey(StandardImage, related_name='pin')
thumbnail = models.ForeignKey(Thumbnail, related_name='pin')
published = models.DateTimeField(auto_now_add=True)
tags = TaggableManager()


def __unicode__(self):
return self.url


def save(self, *args, **kwargs):
hash_name = os.urandom(32).encode('hex')

if not self.image:
temp_img = NamedTemporaryFile()
temp_img.write(urllib2.urlopen(self.url).read())
temp_img.flush()
image = Image.open(temp_img.name)
if image.mode != "RGB":
image = image.convert("RGB")
image.save(temp_img.name, 'JPEG')
self.image.save(''.join([hash_name, '.jpg']), File(temp_img))

if not self.thumbnail:
if not self.image:
image = Image.open(temp_img.name)
else:
super(Pin, self).save()
image = Image.open(self.image.path)
size = image.size
prop = 200.0 / float(image.size[0])
size = (int(prop*float(image.size[0])), int(prop*float(image.size[1])))
image.thumbnail(size, Image.ANTIALIAS)
temp_thumb = NamedTemporaryFile()
if image.mode != "RGB":
image = image.convert("RGB")
image.save(temp_thumb.name, 'JPEG')
self.thumbnail.save(''.join([hash_name, '.jpg']), File(temp_thumb))

if not self.pk:
self.original = OriginalImage.objects.create_for_url(self.url)
self.standard = StandardImage.objects.get_or_create_for_id(self.original.pk)
self.thumbnail = Thumbnail.objects.get_or_create_for_id(self.original.pk)
super(Pin, self).save(*args, **kwargs)


class Meta:
ordering = ['-id']

+ 56
- 0
pinry/pins/utils.py ファイルの表示

@@ -0,0 +1,56 @@
import PIL
import mimetypes

mimetypes.init()


# this neat function is based on django-images and easy-thumbnails
def scale_and_crop(image, size, crop=False, upscale=False, quality=None):
# Open image and store format/metadata.
image.open()
im = PIL.Image.open(image)
im_format, im_info = im.format, im.info
if quality:
im_info['quality'] = quality

# Force PIL to load image data.
im.load()

source_x, source_y = [float(v) for v in im.size]
target_x, target_y = [float(v) for v in size]

if crop or not target_x or not target_y:
scale = max(target_x / source_x, target_y / source_y)
else:
scale = min(target_x / source_x, target_y / source_y)

# Handle one-dimensional targets.
if not target_x:
target_x = source_x * scale
elif not target_y:
target_y = source_y * scale

if scale < 1.0 or (scale > 1.0 and upscale):
im = im.resize((int(source_x * scale), int(source_y * scale)),
resample=PIL.Image.ANTIALIAS)

if crop:
# Use integer values now.
source_x, source_y = im.size
# Difference between new image size and requested size.
diff_x = int(source_x - min(source_x, target_x))
diff_y = int(source_y - min(source_y, target_y))
if diff_x or diff_y:
# Center cropping (default).
halfdiff_x, halfdiff_y = diff_x // 2, diff_y // 2
box = [halfdiff_x, halfdiff_y,
min(source_x, int(target_x) + halfdiff_x),
min(source_y, int(target_y) + halfdiff_y)]
# Finally, crop the image!
im = im.crop(box)

# Close image and replace format/metadata, as PIL blows this away.
im.format, im.info = im_format, im_info
image.close()
return im


+ 3
- 7
pinry/pins/views.py ファイルの表示

@@ -7,8 +7,6 @@ from .forms import PinForm
from .models import Pin




def recent_pins(request):
return TemplateResponse(request, 'pins/recent_pins.html', None)

@@ -17,10 +15,9 @@ def new_pin(request):
if request.method == 'POST':
form = PinForm(request.POST, request.FILES)
if form.is_valid():
pin = form.save(commit=False)
pin.submitter = request.user
pin.save()
form.save_m2m()
pin = Pin.objects.create(url=form.cleaned_data['url'], submitter=request.user,
description=form.cleaned_data['description'])
pin.tags.add(*form.cleaned_data['tags'])
messages.success(request, 'New pin successfully added.')
return HttpResponseRedirect(reverse('pins:recent-pins'))
else:
@@ -44,6 +41,5 @@ def delete_pin(request, pin_id):
'delete this pin.')
except Pin.DoesNotExist:
messages.error(request, 'Pin with the given id does not exist.')

return HttpResponseRedirect(reverse('pins:recent-pins'))

+ 7
- 0
pinry/settings/__init__.py ファイルの表示

@@ -1,4 +1,6 @@
import os

from collections import namedtuple
from django.contrib.messages import constants as messages


@@ -88,3 +90,8 @@ INSTALLED_APPS = (
'pinry.pins',
'pinry.api',
)

AUTHENTICATION_BACKENDS = ('pinry.core.auth.backends.CombinedAuthBackend', 'django.contrib.auth.backends.ModelBackend',)

Dimensions = namedtuple("Dimensions", ['width', 'height'])
IMAGE_SIZES = {'thumbnail': Dimensions(width=240, height=0), 'standard': Dimensions(width=600, height=0)}

+ 2
- 2
pinry/templates/pins/recent_pins.html ファイルの表示

@@ -47,8 +47,8 @@
</div>
</div>
{{/if}}
<a href="{{image}}" class="lightbox" data-username="{{submitter.username}}" data-tags="{{tags}}" data-gravatar="{{submitter.gravatar}}">
<img src="{{thumbnail}}" />
<a href="{{images.standard.url}}" class="lightbox" data-username="{{submitter.username}}" data-tags="{{tags}}" data-gravatar="{{submitter.gravatar}}">
<img src="{{images.thumbnail.url}}" />
</a>
{{#if description}}
<p>{{description}}</p>


読み込み中…
キャンセル
保存