Mercurial > public > sg101
changeset 699:d33bedc3be74
Merge private messages fix with current development of S3 photo upload.
author | Brian Neal <bgneal@gmail.com> |
---|---|
date | Mon, 09 Sep 2013 20:53:08 -0500 |
parents | 67f8d49a9377 (diff) efb525863a75 (current diff) |
children | e888d627928f |
files | |
diffstat | 13 files changed, 322 insertions(+), 1 deletions(-) [+] |
line wrap: on
line diff
--- a/potd/models.py Mon Sep 09 20:30:13 2013 -0500 +++ b/potd/models.py Mon Sep 09 20:53:08 2013 -0500 @@ -32,7 +32,7 @@ thumb = models.ImageField(upload_to='potd/%Y/%m/%d/thumbs', blank=True, null=True) caption = models.CharField(max_length=128) description = models.TextField() - user = models.ForeignKey(User) + user = models.ForeignKey(User, related_name='potd_set') date_added = models.DateField() potd_count = models.IntegerField(default=0)
--- a/sg101/settings/base.py Mon Sep 09 20:30:13 2013 -0500 +++ b/sg101/settings/base.py Mon Sep 09 20:53:08 2013 -0500 @@ -141,6 +141,7 @@ 'potd', 'shoutbox', 'smiley', + 'user_photos', 'weblinks', 'wiki', 'ygroup', @@ -290,6 +291,15 @@ WIKI_COOKIE_AGE = SESSION_COOKIE_AGE WIKI_REDIS_SET = 'wiki_cookie_keys' +# User photo upload settings +USER_PHOTOS_ENABLED = True +USER_PHOTOS_ACCESS_KEY = SECRETS['AWS_ACCESS_KEY'] +USER_PHOTOS_SECRET_KEY = SECRETS['AWS_SECRET_KEY'] +USER_PHOTOS_BUCKET = 'sg101.user.photos' +USER_PHOTOS_BASE_URL = 'https://s3-us-west-1.amazonaws.com' +USER_PHOTOS_MAX_SIZE = (660, 720) +USER_PHOTOS_THUMB_SIZE = (120, 120) + ####################################################################### # Asynchronous settings (queues, queued_search, redis, celery, etc) #######################################################################
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sg101/templates/user_photos/photo_detail.html Mon Sep 09 20:53:08 2013 -0500 @@ -0,0 +1,18 @@ +{% extends 'base.html' %} +{% block title %}User Photo Details{% endblock %} +{% block content %} +<h2>User Photo Details</h2> + +<ul> + <li>Uploader: {{ object.user.username }}</li> + <li>Upload date: {{ object.upload_date|date }}</li> +</li> + +<div> + <img src="{{ object.thumb_url }}" alt="thumbnail" /> +</div> + +<div> + <img src="{{ object.url }}" alt="photo" /> +</div> +{% endblock %}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sg101/templates/user_photos/upload_form.html Mon Sep 09 20:53:08 2013 -0500 @@ -0,0 +1,24 @@ +{% extends 'base.html' %} +{% block title %}User Photo Upload{% endblock %} +{% block content %} +<h2>User Photo Upload</h2> +{% if enabled %} + <p> + This form will allow you to upload a photo from your computer or device, + suitable for displaying in forum or comment posts. We will automatically + resize your photo for you. When the process is complete we will return an + image code you can paste into your post to display your photo. + </p> + <form action="." method="post" enctype="multipart/form-data">{% csrf_token %} + <fieldset> + <legend>Upload a photo:</legend> + {{ form.as_p }} + <p><input type="submit" value="Upload Photo" /></p> + </fieldset> + </form> +{% else %} + <p class="error"> + We're sorry but uploading is currently disabled. Please try back later. + </p> +{% endif %} +{% endblock %}
--- a/sg101/urls.py Mon Sep 09 20:30:13 2013 -0500 +++ b/sg101/urls.py Mon Sep 09 20:53:08 2013 -0500 @@ -84,6 +84,7 @@ (r'^profile/', include('bio.urls')), (r'^shout/', include('shoutbox.urls')), (r'^smiley/', include('smiley.urls')), + (r'^user_photos/', include('user_photos.urls')), (r'^ygroup/', include('ygroup.urls')), )
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/user_photos/admin.py Mon Sep 09 20:53:08 2013 -0500 @@ -0,0 +1,13 @@ +"""Admin definitions for the user_photos application.""" +from django.contrib import admin + +from user_photos.models import Photo + + +class PhotoAdmin(admin.ModelAdmin): + date_hierarchy = 'upload_date' + ordering = ['-upload_date'] + raw_id_fields = ['user'] + search_fields = ['user__username', 'user__email'] + +admin.site.register(Photo, PhotoAdmin)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/user_photos/forms.py Mon Sep 09 20:53:08 2013 -0500 @@ -0,0 +1,32 @@ +"""Forms for the user_photos application.""" +from django import forms +from django.conf import settings + +from user_photos.models import Photo +from user_photos.images import process_file +from user_photos.s3 import S3Bucket + + +class UploadForm(forms.Form): + image_file = forms.ImageField() + + def __init__(self, *args, **kwargs): + self.user = kwargs.pop('user') + super(UploadForm, self).__init__(*args, **kwargs) + + def save(self): + """Processes the image and creates a new Photo object, which is saved to + the database. The new Photo instance is returned. + + This function should only be called if is_valid() returns True. + + """ + bucket = S3Bucket(settings.USER_PHOTOS_ACCESS_KEY, + settings.USER_PHOTOS_SECRET_KEY, + settings.USER_PHOTOS_BUCKET) + url, thumb_url = process_file(self.cleaned_data['image_file'], + self.user, + bucket) + photo = Photo(user=self.user, url=url, thumb_url=thumb_url) + photo.save() + return photo
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/user_photos/images.py Mon Sep 09 20:53:08 2013 -0500 @@ -0,0 +1,69 @@ +"""images.py + +This module contains image processing routines for the user_photos application. + +""" +import datetime +import logging +from io import BytesIO +import os.path +import uuid + +from django.conf import settings +from PIL import Image + + +logger = logging.getLogger(__name__) + + +def process_file(f, user, bucket): + """Perform processing on the given uploaded image file: + + * The image is resized if necessary + * A thumbnail version is created + * The image and thumbnail are uploaded to an S3 bucket + * The image and thumbnail URLs are returned as a tuple + + """ + logger.info('Processing image file for {}: {}'.format(user.username, f.name)) + + unique_key = uuid.uuid4().hex + ext = os.path.splitext(f.name)[1] + filename = '/tmp/' + unique_key + ext + with open(filename, 'wb') as fp: + for chunk in f.chunks(): + fp.write(chunk) + + # Resize image if necessary + image = Image.open(filename) + if image.size > settings.USER_PHOTOS_MAX_SIZE: + logger.debug('Resizing from {} to {}'.format(image.size, settings.USER_PHOTOS_MAX_SIZE)) + image.thumbnail(settings.USER_PHOTOS_MAX_SIZE, Image.ANTIALIAS) + image.save(filename) + + # Create thumbnail + logger.debug('Creating thumbnail') + image = Image.open(filename) + image.thumbnail(settings.USER_PHOTOS_THUMB_SIZE, Image.ANTIALIAS) + thumb = BytesIO() + image.save(thumb, format=image.format) + + # Upload both images to S3 + logging.debug('Uploading image') + now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') + metadata = {'user': user.username, 'date': now} + file_key = unique_key + ext + bucket.upload_from_filename(file_key, filename, metadata) + + logging.debug('Uploading thumbnail') + thumb_key = '{}t{}'.format(unique_key, ext) + bucket.upload_from_string(thumb_key, thumb.getvalue()) + + os.remove(filename) + + logger.info('Completed processing image file for {}: {}'.format(user.username, f.name)) + + url_base = '{}/{}/'.format(settings.USER_PHOTOS_BASE_URL, + settings.USER_PHOTOS_BUCKET) + + return (url_base + file_key, url_base + thumb_key)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/user_photos/models.py Mon Sep 09 20:53:08 2013 -0500 @@ -0,0 +1,29 @@ +"""Models for the user_photos application.""" + +import datetime + +from django.db import models +from django.conf import settings +from django.core.urlresolvers import reverse + + +class Photo(models.Model): + """This model represents data about a user uploaded photo.""" + user = models.ForeignKey(settings.AUTH_USER_MODEL, + related_name='uploaded_photos') + upload_date = models.DateTimeField() + url = models.URLField(max_length=200) + thumb_url = models.URLField(max_length=200, blank=True) + + def __unicode__(self): + return u'Photo by {} on {}'.format(self.user.username, + self.upload_date.strftime('%Y-%m-%d %H:%M:%S')) + + def get_absolute_url(self): + return reverse('user_photos-detail', kwargs={'pk': self.pk}) + + def save(self, *args, **kwargs): + if not self.pk and not self.upload_date: + self.upload_date = datetime.datetime.now() + super(Photo, self).save(*args, **kwargs) +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/user_photos/s3.py Mon Sep 09 20:53:08 2013 -0500 @@ -0,0 +1,59 @@ +"""s3.py + +This module provides the necessary S3 upload functionality for the user_photos +application. + +""" +from boto.s3.connection import S3Connection +from boto.s3.key import Key + + +class S3Bucket(object): + """This class abstracts an Amazon S3 bucket. + + We currently only need upload functionality. + + """ + def __init__(self, access_key, secret_key, bucket_name): + self.conn = S3Connection(access_key, secret_key) + self.bucket = self.conn.get_bucket(bucket_name, validate=False) + + def upload_from_filename(self, key_name, filename, metadata=None, + public=True): + """Uploads data from the file named by filename to a new key named + key_name. metadata, if not None, must be a dict of metadata key / value + pairs which will be added to the key. + + If public is True, the key will be made public after the upload. + + """ + key = self._make_key(key_name, metadata) + key.set_contents_from_filename(filename) + if public: + key.make_public() + + def upload_from_string(self, key_name, content, metadata=None, + public=True): + """Creates a new key with the given key_name, and uploads the string + content to it. metadata, if not None, must be a dict of metadata key / + value pairs which will be added to the key. + + If public is True, the key will be made public after the upload. + + """ + key = self._make_key(key_name, metadata) + key.set_contents_from_string(content) + if public: + key.make_public() + + def _make_key(self, key_name, metadata): + """Private method to create a key and optionally apply metadata to + it. + + """ + key = Key(self.bucket) + key.key = key_name + if metadata: + for k, v in metadata.iteritems(): + key.set_metadata(k, v) + return key
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/user_photos/tests.py Mon Sep 09 20:53:08 2013 -0500 @@ -0,0 +1,16 @@ +""" +This file demonstrates writing tests using the unittest module. These will pass +when you run "manage.py test". + +Replace this with more appropriate tests for your application. +""" + +from django.test import TestCase + + +class SimpleTest(TestCase): + def test_basic_addition(self): + """ + Tests that 1 + 1 always equals 2. + """ + self.assertEqual(1 + 1, 2)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/user_photos/urls.py Mon Sep 09 20:53:08 2013 -0500 @@ -0,0 +1,13 @@ +"""URLs for the user_photos application.""" +from django.conf.urls import patterns, url +from django.views.generic import DetailView + +from user_photos.models import Photo + + +urlpatterns = patterns('user_photos.views', + url(r'^upload/$', 'upload', name='user_photos-upload'), + url(r'^photo/(?P<pk>\d+)/$', + DetailView.as_view(model=Photo), + name='user_photos-detail') +)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/user_photos/views.py Mon Sep 09 20:53:08 2013 -0500 @@ -0,0 +1,37 @@ +"""Views for the user_photos application.""" +from django.conf import settings +from django.contrib.auth.decorators import login_required +from django.shortcuts import render, redirect + +from user_photos.forms import UploadForm + + +@login_required +def upload(request): + """This view function receives an uploaded image file from a user. + The photo will be resized if necessary and a thumbnail image will be + created. The image and thumbnail will then be uploaded to the Amazon + S3 service for storage. + + TODO: rate limiting + pass off the processing to a celery task + ajax version of this view + + """ + form = None + uploads_enabled = settings.USER_PHOTOS_ENABLED + + if uploads_enabled: + if request.method == 'POST': + form = UploadForm(request.POST, request.FILES, user=request.user) + if form.is_valid(): + photo = form.save() + return redirect(photo) + else: + form = UploadForm(user=request.user) + + return render(request, 'user_photos/upload_form.html', { + 'enabled': uploads_enabled, + 'form': form, + }, + status=200 if uploads_enabled else 503)