bgneal@695: """Forms for the user_photos application."""
bgneal@700: import datetime
bgneal@749: import hashlib
bgneal@700: 
bgneal@695: from django import forms
bgneal@697: from django.conf import settings
bgneal@695: 
bgneal@700: from core.s3 import S3Bucket
bgneal@700: from core.image_uploader import upload
bgneal@701: from core.services import get_redis_connection
bgneal@696: from user_photos.models import Photo
bgneal@696: 
bgneal@695: 
bgneal@701: def rate_limit(key, limit, seconds):
bgneal@701:     """Use Redis to do a rate limit check. Returns True if the limit is violated
bgneal@701:     and False otherwise.
bgneal@701: 
bgneal@701:     key - the key to check in Redis
bgneal@701:     limit - the rate limit maximum value
bgneal@701:     seconds - the rate limit period in seconds
bgneal@701: 
bgneal@701:     """
bgneal@701:     conn = get_redis_connection()
bgneal@701:     val = conn.incr(key)
bgneal@701:     if val == 1:
bgneal@701:         conn.expire(key, seconds)
bgneal@701:     return val > limit
bgneal@701: 
bgneal@701: 
bgneal@695: class UploadForm(forms.Form):
bgneal@695:     image_file = forms.ImageField()
bgneal@696: 
bgneal@696:     def __init__(self, *args, **kwargs):
bgneal@696:         self.user = kwargs.pop('user')
bgneal@696:         super(UploadForm, self).__init__(*args, **kwargs)
bgneal@696: 
bgneal@701:     def clean(self):
bgneal@701:         cleaned_data = super(UploadForm, self).clean()
bgneal@701: 
bgneal@701:         # rate limit uploads
bgneal@701:         key = 'user_photos:counts:' + self.user.username
bgneal@701:         limit = settings.USER_PHOTOS_RATE_LIMIT
bgneal@701:         if rate_limit(key, *limit):
bgneal@701:             raise forms.ValidationError("You've exceeded your upload quota. "
bgneal@701:                     "Please try again later.")
bgneal@701: 
bgneal@701:         return cleaned_data
bgneal@701: 
bgneal@696:     def save(self):
bgneal@696:         """Processes the image and creates a new Photo object, which is saved to
bgneal@696:         the database. The new Photo instance is returned.
bgneal@696: 
bgneal@749:         Note that we do de-duplication. A signature is computed for the photo.
bgneal@749:         If the user has already uploaded a file with the same signature, that
bgneal@749:         photo object is returned instead.
bgneal@749: 
bgneal@696:         This function should only be called if is_valid() returns True.
bgneal@696: 
bgneal@696:         """
bgneal@749:         # Check for duplicate uploads from this user
bgneal@749:         signature = self._signature()
bgneal@749:         try:
bgneal@749:             return Photo.objects.get(user=self.user, signature=signature)
bgneal@749:         except Photo.DoesNotExist:
bgneal@749:             pass
bgneal@749: 
bgneal@749:         # This must not be a duplicate, proceed with upload to S3
bgneal@700:         bucket = S3Bucket(access_key=settings.USER_PHOTOS_ACCESS_KEY,
bgneal@700:                           secret_key=settings.USER_PHOTOS_SECRET_KEY,
bgneal@700:                           base_url=settings.USER_PHOTOS_BASE_URL,
bgneal@700:                           bucket_name=settings.USER_PHOTOS_BUCKET)
bgneal@700: 
bgneal@700:         now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
bgneal@700:         metadata = {'user': self.user.username, 'date': now}
bgneal@700: 
bgneal@700:         url, thumb_url = upload(fp=self.cleaned_data['image_file'],
bgneal@700:                                 bucket=bucket,
bgneal@700:                                 metadata=metadata,
bgneal@700:                                 new_size=settings.USER_PHOTOS_MAX_SIZE,
bgneal@700:                                 thumb_size=settings.USER_PHOTOS_THUMB_SIZE)
bgneal@700: 
bgneal@749:         photo = Photo(user=self.user, url=url, thumb_url=thumb_url,
bgneal@749:                 signature=signature)
bgneal@696:         photo.save()
bgneal@696:         return photo
bgneal@749: 
bgneal@749:     def _signature(self):
bgneal@749:         """Calculates and returns a signature for the image file as a hex digest
bgneal@749:         string.
bgneal@749: 
bgneal@749:         This function should only be called if is_valid() is True.
bgneal@749: 
bgneal@749:         """
bgneal@749:         fp = self.cleaned_data['image_file']
bgneal@749:         md5 = hashlib.md5()
bgneal@749:         for chunk in fp.chunks():
bgneal@749:             md5.update(chunk)
bgneal@749:         return md5.hexdigest()