diff --git a/.gitignore b/.gitignore index 88f4c05..25662cf 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,4 @@ /media/ /static/ *.pyc - +/.idea/ diff --git a/pinry/api/api.py b/pinry/api/api.py index d84daf2..be92e8d 100644 --- a/pinry/api/api.py +++ b/pinry/api/api.py @@ -1,3 +1,7 @@ +from django.conf import settings + + +from django_images.models import Thumbnail from tastypie.resources import ModelResource from tastypie import fields from tastypie.authorization import DjangoAuthorization @@ -27,10 +31,15 @@ class PinResource(ModelResource): 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} + original = bundle.obj.image + images = {'original': { + 'url': original.get_absolute_url(), 'width': original.width, 'height': original.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 class Meta: diff --git a/pinry/pins/forms.py b/pinry/pins/forms.py index 17ca38a..3e0aa13 100644 --- a/pinry/pins/forms.py +++ b/pinry/pins/forms.py @@ -1,44 +1,9 @@ from django import forms -from .models import Pin +from django_images.models import Image -class PinForm(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 ImageForm(forms.ModelForm): class Meta: - model = Pin - fields = ['url', 'image', 'description', 'tags'] - - 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 + model = Image + fields = ('image',) diff --git a/pinry/pins/managers.py b/pinry/pins/managers.py deleted file mode 100644 index 70bf3c4..0000000 --- a/pinry/pins/managers.py +++ /dev/null @@ -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' diff --git a/pinry/pins/models.py b/pinry/pins/models.py index d6e2554..142716d 100644 --- a/pinry/pins/models.py +++ b/pinry/pins/models.py @@ -1,89 +1,21 @@ -import hashlib - from django.db import models +from django_images.models import Image from taggit.managers import TaggableManager 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): submitter = models.ForeignKey(User) url = models.TextField(blank=True, null=True) description = models.TextField(blank=True, null=True) - original = models.ForeignKey(OriginalImage, related_name='pin') - standard = models.ForeignKey(StandardImage, related_name='pin') - thumbnail = models.ForeignKey(Thumbnail, related_name='pin') + image = models.ForeignKey(Image, related_name='pin') published = models.DateTimeField(auto_now_add=True) tags = TaggableManager() def __unicode__(self): 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: ordering = ['-id'] diff --git a/pinry/pins/templatetags/new_pin.py b/pinry/pins/templatetags/new_pin.py index ac73233..eef890b 100644 --- a/pinry/pins/templatetags/new_pin.py +++ b/pinry/pins/templatetags/new_pin.py @@ -2,7 +2,7 @@ from django.template.loader import render_to_string from django.template import Library from django.template import RequestContext -from pinry.pins.forms import PinForm +from pinry.pins.forms import ImageForm register = Library() @@ -11,5 +11,5 @@ register = Library() @register.simple_tag def new_pin(request): return render_to_string('pins/templatetags/new_pin.html', - {'form': PinForm()}, + {'form': ImageForm()}, context_instance=RequestContext(request)) diff --git a/pinry/pins/urls.py b/pinry/pins/urls.py index 0e88df4..4ea50b2 100644 --- a/pinry/pins/urls.py +++ b/pinry/pins/urls.py @@ -1,11 +1,11 @@ from django.conf.urls import patterns, url from .views import RecentPins -from .views import NewPin +from .views import UploadImage urlpatterns = patterns('pinry.pins.views', url(r'^$', RecentPins.as_view(), name='recent-pins'), 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'), ) diff --git a/pinry/pins/views.py b/pinry/pins/views.py index 9ff6196..be63b91 100644 --- a/pinry/pins/views.py +++ b/pinry/pins/views.py @@ -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.contrib import messages +from django.utils.decorators import method_decorator from django.utils.functional import lazy -from django.views.generic.base import TemplateView -from django.views.generic import CreateView +from django.views.generic import ( + TemplateView, CreateView) -from .forms import PinForm -from .models import Pin +from django_images.models import Image + +from .forms import ImageForm reverse_lazy = lambda name=None, *args: lazy(reverse, str)(name, args=args) -class RecentPins(TemplateView): - template_name = 'pins/recent_pins.html' +class LoginRequiredMixin(object): + """ + 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): - model = Pin - form_class = PinForm - success_url = reverse_lazy('pins:recent-pins') +class UploadImage(LoginRequiredMixin, CreateView): + template_name = 'pins/pin_form.html' + model = Image + form_class = ImageForm def form_valid(self, form): - form.instance.submitter = self.request.user 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): 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' \ No newline at end of file diff --git a/pinry/settings/__init__.py b/pinry/settings/__init__.py index 1bb9b19..637b11e 100644 --- a/pinry/settings/__init__.py +++ b/pinry/settings/__init__.py @@ -86,12 +86,13 @@ INSTALLED_APPS = ( 'south', 'compressor', 'taggit', + 'django_images', 'pinry.core', '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)} +IMAGE_SIZES = { + 'thumbnail': {'size': [240, 0]}, + 'standard': {'size': [600, 0]}, +} diff --git a/requirements.txt b/requirements.txt index d17507d..8e38455 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,5 @@ django-tastypie django_compressor cssmin jsmin --e git://github.com/hcarvalhoalves/django-taggit.git@e0f9642d7b94c8e6c0feb520d96bb6ae4d78a4d0#egg=django-taggit \ No newline at end of file +django-images +http://github.com/hcarvalhoalves/django-taggit/tarball/master#egg=django-taggit \ No newline at end of file