diff --git a/pinry/api/api.py b/pinry/api/api.py
index 783bdf1..d84daf2 100644
--- a/pinry/api/api.py
+++ b/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):
diff --git a/pinry/core/tests.py b/pinry/core/tests.py
index b1f113b..5c63580 100644
--- a/pinry/core/tests.py
+++ b/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, {
diff --git a/pinry/pins/forms.py b/pinry/pins/forms.py
index 3a216cf..2463117 100644
--- a/pinry/pins/forms.py
+++ b/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']
diff --git a/pinry/pins/migrations/0001_initial.py b/pinry/pins/migrations/0001_initial.py
deleted file mode 100644
index 8ef8a2e..0000000
--- a/pinry/pins/migrations/0001_initial.py
+++ /dev/null
@@ -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']
\ No newline at end of file
diff --git a/pinry/pins/migrations/__init__.py b/pinry/pins/migrations/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/pinry/pins/models.py b/pinry/pins/models.py
index 8c539b1..367d5fd 100644
--- a/pinry/pins/models.py
+++ b/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
-
-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']
diff --git a/pinry/pins/utils.py b/pinry/pins/utils.py
new file mode 100644
index 0000000..fe0138a
--- /dev/null
+++ b/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
+
diff --git a/pinry/pins/views.py b/pinry/pins/views.py
index c7488b3..155b33a 100644
--- a/pinry/pins/views.py
+++ b/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'))
diff --git a/pinry/settings/__init__.py b/pinry/settings/__init__.py
index 82bc9a7..1bb9b19 100644
--- a/pinry/settings/__init__.py
+++ b/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)}
diff --git a/pinry/templates/pins/recent_pins.html b/pinry/templates/pins/recent_pins.html
index 62095f4..cb91168 100644
--- a/pinry/templates/pins/recent_pins.html
+++ b/pinry/templates/pins/recent_pins.html
@@ -47,8 +47,8 @@
{{/if}}
-
-
+
+
{{#if description}}
{{description}}