annotate user_photos/forms.py @ 964:51a2051588f5

Image uploading now expects a file. Refactor image uploading to not expect a Django UploadedFile and use a regular file instead. This will be needed for the future feature of being able to save and upload images from the Internet.
author Brian Neal <bgneal@gmail.com>
date Wed, 02 Sep 2015 20:50:08 -0500
parents b6e98717690b
children 4f265f61874b
rev   line source
bgneal@695 1 """Forms for the user_photos application."""
bgneal@700 2 import datetime
bgneal@749 3 import hashlib
bgneal@964 4 import os.path
bgneal@700 5
bgneal@695 6 from django import forms
bgneal@697 7 from django.conf import settings
bgneal@695 8
bgneal@700 9 from core.s3 import S3Bucket
bgneal@700 10 from core.image_uploader import upload
bgneal@964 11 from core.functions import TemporaryFile
bgneal@701 12 from core.services import get_redis_connection
bgneal@696 13 from user_photos.models import Photo
bgneal@696 14
bgneal@695 15
bgneal@701 16 def rate_limit(key, limit, seconds):
bgneal@701 17 """Use Redis to do a rate limit check. Returns True if the limit is violated
bgneal@701 18 and False otherwise.
bgneal@701 19
bgneal@701 20 key - the key to check in Redis
bgneal@701 21 limit - the rate limit maximum value
bgneal@701 22 seconds - the rate limit period in seconds
bgneal@701 23
bgneal@701 24 """
bgneal@701 25 conn = get_redis_connection()
bgneal@701 26 val = conn.incr(key)
bgneal@701 27 if val == 1:
bgneal@701 28 conn.expire(key, seconds)
bgneal@701 29 return val > limit
bgneal@701 30
bgneal@701 31
bgneal@695 32 class UploadForm(forms.Form):
bgneal@695 33 image_file = forms.ImageField()
bgneal@696 34
bgneal@696 35 def __init__(self, *args, **kwargs):
bgneal@696 36 self.user = kwargs.pop('user')
bgneal@696 37 super(UploadForm, self).__init__(*args, **kwargs)
bgneal@696 38
bgneal@701 39 def clean(self):
bgneal@701 40 cleaned_data = super(UploadForm, self).clean()
bgneal@701 41
bgneal@701 42 # rate limit uploads
bgneal@701 43 key = 'user_photos:counts:' + self.user.username
bgneal@701 44 limit = settings.USER_PHOTOS_RATE_LIMIT
bgneal@701 45 if rate_limit(key, *limit):
bgneal@701 46 raise forms.ValidationError("You've exceeded your upload quota. "
bgneal@701 47 "Please try again later.")
bgneal@701 48
bgneal@701 49 return cleaned_data
bgneal@701 50
bgneal@696 51 def save(self):
bgneal@696 52 """Processes the image and creates a new Photo object, which is saved to
bgneal@696 53 the database. The new Photo instance is returned.
bgneal@696 54
bgneal@749 55 Note that we do de-duplication. A signature is computed for the photo.
bgneal@749 56 If the user has already uploaded a file with the same signature, that
bgneal@749 57 photo object is returned instead.
bgneal@749 58
bgneal@696 59 This function should only be called if is_valid() returns True.
bgneal@696 60
bgneal@696 61 """
bgneal@749 62 # Check for duplicate uploads from this user
bgneal@749 63 signature = self._signature()
bgneal@749 64 try:
bgneal@749 65 return Photo.objects.get(user=self.user, signature=signature)
bgneal@749 66 except Photo.DoesNotExist:
bgneal@749 67 pass
bgneal@749 68
bgneal@749 69 # This must not be a duplicate, proceed with upload to S3
bgneal@700 70 bucket = S3Bucket(access_key=settings.USER_PHOTOS_ACCESS_KEY,
bgneal@700 71 secret_key=settings.USER_PHOTOS_SECRET_KEY,
bgneal@700 72 base_url=settings.USER_PHOTOS_BASE_URL,
bgneal@700 73 bucket_name=settings.USER_PHOTOS_BUCKET)
bgneal@700 74
bgneal@964 75 # Trying to use PIL (or Pillow) on a Django UploadedFile is often
bgneal@964 76 # problematic because the file is often an in-memory file if it is under
bgneal@964 77 # a certain size. This complicates matters and many of the operations we try
bgneal@964 78 # to perform on it fail if this is the case. To get around these issues,
bgneal@964 79 # we make a copy of the file on the file system and operate on the copy.
bgneal@700 80
bgneal@964 81 fp = self.cleaned_data['image_file']
bgneal@964 82 ext = os.path.splitext(fp.name)[1]
bgneal@964 83
bgneal@964 84 # Write the UploadedFile to a temporary file on disk
bgneal@964 85 with TemporaryFile(suffix=ext) as t:
bgneal@964 86 for chunk in fp.chunks():
bgneal@964 87 t.file.write(chunk)
bgneal@964 88 t.file.close()
bgneal@964 89
bgneal@964 90 now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
bgneal@964 91 metadata = {'user': self.user.username, 'date': now}
bgneal@964 92
bgneal@964 93 url, thumb_url = upload(filename=t.filename,
bgneal@964 94 bucket=bucket,
bgneal@964 95 metadata=metadata,
bgneal@964 96 new_size=settings.USER_PHOTOS_MAX_SIZE,
bgneal@964 97 thumb_size=settings.USER_PHOTOS_THUMB_SIZE)
bgneal@700 98
bgneal@749 99 photo = Photo(user=self.user, url=url, thumb_url=thumb_url,
bgneal@749 100 signature=signature)
bgneal@696 101 photo.save()
bgneal@696 102 return photo
bgneal@749 103
bgneal@749 104 def _signature(self):
bgneal@749 105 """Calculates and returns a signature for the image file as a hex digest
bgneal@749 106 string.
bgneal@749 107
bgneal@749 108 This function should only be called if is_valid() is True.
bgneal@749 109
bgneal@749 110 """
bgneal@749 111 fp = self.cleaned_data['image_file']
bgneal@749 112 md5 = hashlib.md5()
bgneal@749 113 for chunk in fp.chunks():
bgneal@749 114 md5.update(chunk)
bgneal@749 115 return md5.hexdigest()