mirror of
https://github.com/pinry/pinry.git
synced 2025-11-13 16:45:41 +01:00
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.
This commit is contained in:
@@ -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):
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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']
|
||||
@@ -1,60 +1,128 @@
|
||||
from django.db import models
|
||||
from django.core.files import File
|
||||
from django.core.files.temp import NamedTemporaryFile
|
||||
|
||||
from taggit.managers import TaggableManager
|
||||
import urllib2
|
||||
import hashlib
|
||||
import os
|
||||
from PIL import Image
|
||||
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
|
||||
|
||||
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
pinry/pins/utils.py
Normal file
56
pinry/pins/utils.py
Normal file
@@ -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
|
||||
|
||||
@@ -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:
|
||||
@@ -45,5 +42,4 @@ def delete_pin(request, pin_id):
|
||||
except Pin.DoesNotExist:
|
||||
messages.error(request, 'Pin with the given id does not exist.')
|
||||
|
||||
|
||||
return HttpResponseRedirect(reverse('pins:recent-pins'))
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user