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
@@ -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 | |||
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'] |
@@ -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: | |||
@@ -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')) |
@@ -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> | |||