mirror of
https://github.com/pinry/pinry.git
synced 2025-11-13 16:45:41 +01:00
Another major Pinry model rewrite
Generate thumbnail and standard image on request, and use http://github.com/mirumee/django-images for generating them. Also, remove the CreatePin page as pin creation is going to be done in JavaScript. Create UploadImage view for uploading images from computer.
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -9,4 +9,4 @@
|
|||||||
/media/
|
/media/
|
||||||
/static/
|
/static/
|
||||||
*.pyc
|
*.pyc
|
||||||
|
/.idea/
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
from django_images.models import Thumbnail
|
||||||
from tastypie.resources import ModelResource
|
from tastypie.resources import ModelResource
|
||||||
from tastypie import fields
|
from tastypie import fields
|
||||||
from tastypie.authorization import DjangoAuthorization
|
from tastypie.authorization import DjangoAuthorization
|
||||||
@@ -27,10 +31,15 @@ class PinResource(ModelResource):
|
|||||||
submitter = fields.ForeignKey(UserResource, 'submitter', full=True)
|
submitter = fields.ForeignKey(UserResource, 'submitter', full=True)
|
||||||
|
|
||||||
def dehydrate_images(self, bundle):
|
def dehydrate_images(self, bundle):
|
||||||
images = {}
|
original = bundle.obj.image
|
||||||
for type in ['standard', 'thumbnail', 'original']:
|
images = {'original': {
|
||||||
image_obj = getattr(bundle.obj, type, None)
|
'url': original.get_absolute_url(), 'width': original.width, 'height': original.height}
|
||||||
images[type] = {'url': image_obj.image.url, 'width': image_obj.width, 'height': image_obj.height}
|
}
|
||||||
|
for image in ['standard', 'thumbnail']:
|
||||||
|
obj = Thumbnail.objects.get_or_create_at_size(original.pk, image)
|
||||||
|
images[image] = {
|
||||||
|
'url': obj.get_absolute_url(), 'width': obj.width, 'height': obj.height
|
||||||
|
}
|
||||||
return images
|
return images
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
@@ -1,44 +1,9 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
|
|
||||||
from .models import Pin
|
from django_images.models import Image
|
||||||
|
|
||||||
|
|
||||||
class PinForm(forms.ModelForm):
|
class ImageForm(forms.ModelForm):
|
||||||
url = forms.CharField(required=False)
|
|
||||||
image = forms.ImageField(label='or Upload', required=False)
|
|
||||||
|
|
||||||
_errors = {
|
|
||||||
'not_image': 'Requested URL is not an image file. Only images are currently supported.',
|
|
||||||
'pinned': 'URL has already been pinned!',
|
|
||||||
'protocol': 'Currently only support HTTP and HTTPS protocols, please be sure you include this in the URL.',
|
|
||||||
'nothing': 'Need either a URL or Upload',
|
|
||||||
}
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Pin
|
model = Image
|
||||||
fields = ['url', 'image', 'description', 'tags']
|
fields = ('image',)
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
cleaned_data = super(PinForm, self).clean()
|
|
||||||
|
|
||||||
url = cleaned_data.get('url')
|
|
||||||
image = cleaned_data.get('image')
|
|
||||||
|
|
||||||
if url:
|
|
||||||
image_file_types = ['png', 'gif', 'jpeg', 'jpg']
|
|
||||||
if not url.split('.')[-1].lower() in image_file_types:
|
|
||||||
raise forms.ValidationError(self._errors['not_image'])
|
|
||||||
protocol = url.split(':')[0]
|
|
||||||
if protocol not in ['http', 'https']:
|
|
||||||
raise forms.ValidationError(self._errors['protocol'])
|
|
||||||
try:
|
|
||||||
Pin.objects.get(url=url)
|
|
||||||
raise forms.ValidationError(self._errors['pinned'])
|
|
||||||
except Pin.DoesNotExist:
|
|
||||||
pass
|
|
||||||
elif image:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
raise forms.ValidationError(self._errors['nothing'])
|
|
||||||
|
|
||||||
return cleaned_data
|
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
import os
|
|
||||||
import urllib2
|
|
||||||
from cStringIO import StringIO
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.core.files.uploadedfile import InMemoryUploadedFile
|
|
||||||
from django.db import models
|
|
||||||
|
|
||||||
from . import utils
|
|
||||||
|
|
||||||
|
|
||||||
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 self.create(image=temporary_file)
|
|
||||||
|
|
||||||
|
|
||||||
class BaseImageManager(models.Manager):
|
|
||||||
image_size = None
|
|
||||||
|
|
||||||
def get_or_create_for(self, original):
|
|
||||||
buf = StringIO()
|
|
||||||
img = utils.scale_and_crop(original.image, settings.IMAGE_SIZES[self.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 = self.create(original=original, image=file_obj)
|
|
||||||
|
|
||||||
return image
|
|
||||||
|
|
||||||
|
|
||||||
class StandardImageManager(BaseImageManager):
|
|
||||||
image_size = 'standard'
|
|
||||||
|
|
||||||
|
|
||||||
class ThumbnailManager(BaseImageManager):
|
|
||||||
image_size = 'thumbnail'
|
|
||||||
@@ -1,89 +1,21 @@
|
|||||||
import hashlib
|
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
|
from django_images.models import Image
|
||||||
from taggit.managers import TaggableManager
|
from taggit.managers import TaggableManager
|
||||||
|
|
||||||
from ..core.models import User
|
from ..core.models import User
|
||||||
from .managers import OriginalImageManager
|
|
||||||
from .managers import StandardImageManager
|
|
||||||
from .managers import ThumbnailManager
|
|
||||||
|
|
||||||
|
|
||||||
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 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):
|
class Pin(models.Model):
|
||||||
submitter = models.ForeignKey(User)
|
submitter = models.ForeignKey(User)
|
||||||
url = models.TextField(blank=True, null=True)
|
url = models.TextField(blank=True, null=True)
|
||||||
description = models.TextField(blank=True, null=True)
|
description = models.TextField(blank=True, null=True)
|
||||||
original = models.ForeignKey(OriginalImage, related_name='pin')
|
image = models.ForeignKey(Image, related_name='pin')
|
||||||
standard = models.ForeignKey(StandardImage, related_name='pin')
|
|
||||||
thumbnail = models.ForeignKey(Thumbnail, related_name='pin')
|
|
||||||
published = models.DateTimeField(auto_now_add=True)
|
published = models.DateTimeField(auto_now_add=True)
|
||||||
tags = TaggableManager()
|
tags = TaggableManager()
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return self.url
|
return self.url
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
if not self.pk:
|
|
||||||
self.original = OriginalImage.objects.create_for_url(self.url)
|
|
||||||
self.standard = StandardImage.objects.get_or_create_for(self.original)
|
|
||||||
self.thumbnail = Thumbnail.objects.get_or_create_for(self.original)
|
|
||||||
super(Pin, self).save(*args, **kwargs)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['-id']
|
ordering = ['-id']
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from django.template.loader import render_to_string
|
|||||||
from django.template import Library
|
from django.template import Library
|
||||||
from django.template import RequestContext
|
from django.template import RequestContext
|
||||||
|
|
||||||
from pinry.pins.forms import PinForm
|
from pinry.pins.forms import ImageForm
|
||||||
|
|
||||||
|
|
||||||
register = Library()
|
register = Library()
|
||||||
@@ -11,5 +11,5 @@ register = Library()
|
|||||||
@register.simple_tag
|
@register.simple_tag
|
||||||
def new_pin(request):
|
def new_pin(request):
|
||||||
return render_to_string('pins/templatetags/new_pin.html',
|
return render_to_string('pins/templatetags/new_pin.html',
|
||||||
{'form': PinForm()},
|
{'form': ImageForm()},
|
||||||
context_instance=RequestContext(request))
|
context_instance=RequestContext(request))
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
from django.conf.urls import patterns, url
|
from django.conf.urls import patterns, url
|
||||||
|
|
||||||
from .views import RecentPins
|
from .views import RecentPins
|
||||||
from .views import NewPin
|
from .views import UploadImage
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = patterns('pinry.pins.views',
|
urlpatterns = patterns('pinry.pins.views',
|
||||||
url(r'^$', RecentPins.as_view(), name='recent-pins'),
|
url(r'^$', RecentPins.as_view(), name='recent-pins'),
|
||||||
url(r'^tag/.+/$', RecentPins.as_view(), name='tag'),
|
url(r'^tag/.+/$', RecentPins.as_view(), name='tag'),
|
||||||
url(r'^new-pin/$', NewPin.as_view(), name='new-pin'),
|
url(r'^upload-pin/$', UploadImage.as_view(), name='new-pin'),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,31 +1,62 @@
|
|||||||
from django.http import HttpResponseRedirect
|
from django.contrib.auth import REDIRECT_FIELD_NAME
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
from django.utils.functional import lazy
|
from django.utils.functional import lazy
|
||||||
from django.views.generic.base import TemplateView
|
from django.views.generic import (
|
||||||
from django.views.generic import CreateView
|
TemplateView, CreateView)
|
||||||
|
|
||||||
from .forms import PinForm
|
from django_images.models import Image
|
||||||
from .models import Pin
|
|
||||||
|
from .forms import ImageForm
|
||||||
|
|
||||||
|
|
||||||
reverse_lazy = lambda name=None, *args: lazy(reverse, str)(name, args=args)
|
reverse_lazy = lambda name=None, *args: lazy(reverse, str)(name, args=args)
|
||||||
|
|
||||||
|
|
||||||
class RecentPins(TemplateView):
|
class LoginRequiredMixin(object):
|
||||||
template_name = 'pins/recent_pins.html'
|
"""
|
||||||
|
A login required mixin for use with class based views. This Class is a light wrapper around the
|
||||||
|
`login_required` decorator and hence function parameters are just attributes defined on the class.
|
||||||
|
|
||||||
|
Due to parent class order traversal this mixin must be added as the left most
|
||||||
|
mixin of a view.
|
||||||
|
|
||||||
|
The mixin has exactly the same flow as `login_required` decorator:
|
||||||
|
|
||||||
|
If the user isn't logged in, redirect to settings.LOGIN_URL, passing the current
|
||||||
|
absolute path in the query string. Example: /accounts/login/?next=/polls/3/.
|
||||||
|
|
||||||
|
If the user is logged in, execute the view normally. The view code is free to
|
||||||
|
assume the user is logged in.
|
||||||
|
|
||||||
|
**Class Settings**
|
||||||
|
`redirect_field_name - defaults to "next"
|
||||||
|
`login_url` - the login url of your site
|
||||||
|
|
||||||
|
"""
|
||||||
|
redirect_field_name = REDIRECT_FIELD_NAME
|
||||||
|
login_url = None
|
||||||
|
|
||||||
|
@method_decorator(login_required(redirect_field_name=redirect_field_name, login_url=login_url))
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
return super(LoginRequiredMixin, self).dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class NewPin(CreateView):
|
class UploadImage(LoginRequiredMixin, CreateView):
|
||||||
model = Pin
|
template_name = 'pins/pin_form.html'
|
||||||
form_class = PinForm
|
model = Image
|
||||||
success_url = reverse_lazy('pins:recent-pins')
|
form_class = ImageForm
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
form.instance.submitter = self.request.user
|
|
||||||
messages.success(self.request, 'New pin successfully added.')
|
messages.success(self.request, 'New pin successfully added.')
|
||||||
return super(NewPin, self).form_valid(form)
|
return super(UploadImage, self).form_valid(form)
|
||||||
|
|
||||||
def form_invalid(self, form):
|
def form_invalid(self, form):
|
||||||
messages.error(self.request, 'Pin did not pass validation!')
|
messages.error(self.request, 'Pin did not pass validation!')
|
||||||
return super(NewPin, self).form_invalid(form)
|
return super(UploadImage, self).form_invalid(form)
|
||||||
|
|
||||||
|
|
||||||
|
class RecentPins(TemplateView):
|
||||||
|
template_name = 'pins/recent_pins.html'
|
||||||
@@ -86,12 +86,13 @@ INSTALLED_APPS = (
|
|||||||
'south',
|
'south',
|
||||||
'compressor',
|
'compressor',
|
||||||
'taggit',
|
'taggit',
|
||||||
|
'django_images',
|
||||||
'pinry.core',
|
'pinry.core',
|
||||||
'pinry.pins',
|
'pinry.pins',
|
||||||
'pinry.api',
|
'pinry.api',
|
||||||
)
|
)
|
||||||
|
|
||||||
AUTHENTICATION_BACKENDS = ('pinry.core.auth.backends.CombinedAuthBackend', 'django.contrib.auth.backends.ModelBackend',)
|
IMAGE_SIZES = {
|
||||||
|
'thumbnail': {'size': [240, 0]},
|
||||||
Dimensions = namedtuple("Dimensions", ['width', 'height'])
|
'standard': {'size': [600, 0]},
|
||||||
IMAGE_SIZES = {'thumbnail': Dimensions(width=240, height=0), 'standard': Dimensions(width=600, height=0)}
|
}
|
||||||
|
|||||||
@@ -5,4 +5,5 @@ django-tastypie
|
|||||||
django_compressor
|
django_compressor
|
||||||
cssmin
|
cssmin
|
||||||
jsmin
|
jsmin
|
||||||
-e git://github.com/hcarvalhoalves/django-taggit.git@e0f9642d7b94c8e6c0feb520d96bb6ae4d78a4d0#egg=django-taggit
|
django-images
|
||||||
|
http://github.com/hcarvalhoalves/django-taggit/tarball/master#egg=django-taggit
|
||||||
Reference in New Issue
Block a user