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)