import zipfile from zipfile import BadZipFile import logging import datetime import time import os from io import BytesIO from fractions import Fraction try: import Image except ImportError: from PIL import Image from django import forms from django.utils.translation import ugettext_lazy as _ from django.contrib import messages from django.contrib.sites.models import Site from django.conf import settings from django.utils.encoding import force_text from django.template.defaultfilters import slugify from django.core.files.base import ContentFile from django.contrib.admin import widgets from django.contrib.gis.geos import Point import exiftool from photos.models import LuxImage, Gallery from locations.models import Location logger = logging.getLogger('photos.forms') class UploadZipForm(forms.Form): """ Handles the uploading of a gallery of photos packed in a .zip file Creates Gallery object, adds photos with all metadata that's available """ zip_file = forms.FileField() title = forms.CharField(label=_('Gallery Title'), max_length=250) slug = forms.SlugField(label=_('Gallery Slug')) desc = forms.CharField(label=_('Gallery Caption'), widget=forms.Textarea, required=False) date = forms.SplitDateTimeField(label=_('Date'), widget=widgets.AdminSplitDateTime) is_public = forms.BooleanField(label=_('Is public'), initial=True, required=False, help_text=_('Show on site')) def clean_zip_file(self): """Open the zip file a first time, to check that it is a valid zip archive. We'll open it again in a moment, so we have some duplication, but let's focus on keeping the code easier to read! """ zip_file = self.cleaned_data['zip_file'] try: zip = zipfile.ZipFile(zip_file) except BadZipFile as e: raise forms.ValidationError(str(e)) bad_file = zip.testzip() if bad_file: zip.close() raise forms.ValidationError('"%s" in the .zip archive is corrupt.' % bad_file) zip.close() # Close file in all cases. return zip_file def clean_title(self): title = self.cleaned_data['title'] if title and Gallery.objects.filter(title=title).exists(): raise forms.ValidationError(_('A gallery with that title already exists.')) return title def clean(self): cleaned_data = super(UploadZipForm, self).clean() if not self['title'].errors: # If there's already an error in the title, no need to add another # error related to the same field. if not cleaned_data.get('title', None) and not cleaned_data['gallery']: raise forms.ValidationError( _('Select an existing gallery, or enter a title for a new gallery.')) return cleaned_data def save(self, request=None, zip_file=None): if not zip_file: zip_file = self.cleaned_data['zip_file'] gallery, created = Gallery.objects.get_or_create( title= self.cleaned_data['title'], description = self.cleaned_data['desc'], slug= self.cleaned_data['slug'], pub_date= self.cleaned_data['date'] ) zipper = zipfile.ZipFile(zip_file) count = 1 for filename in sorted(zipper.namelist()): f, file_extension = os.path.splitext(filename) logger.debug('Reading file "{0}".'.format(filename)) if filename.startswith('__') or filename.startswith('.'): logger.debug('Ignoring file "{0}".'.format(filename)) continue if os.path.dirname(filename): logger.warning('Ignoring file "{0}" as it is in a subfolder; all images should be in the top ' 'folder of the zip.'.format(filename)) if request: messages.warning(request, _('Ignoring file "{filename}" as it is in a subfolder').format(filename=filename), fail_silently=True) continue data = zipper.read(filename) if not len(data): logger.debug('File "{0}" is empty.'.format(filename)) continue # Basic check that we have a valid image. try: file = BytesIO(data) opened = Image.open(file) opened.verify() except Exception: # Pillow (or PIL) doesn't recognize it as an image. # If a "bad" file is found we just skip it. # But we do flag this both in the logs and to the user. logger.error('Could not process file "{0}" in the .zip archive.'.format( filename)) if request: messages.warning(request, _('Could not process file "{0}" in the .zip archive.').format( filename), fail_silently=True) continue image = LuxImage( pub_date = datetime.datetime.now() ) contentfile = ContentFile(data) image.image.save(filename, contentfile) image.save() gallery.image.add(image) with exiftool.ExifTool() as et: meta = et.get_metadata(image.image.path) et.terminate() image.exif_raw = meta try: image.title = meta["EXIF:ImageDescription"] except: pass try: image.caption = meta["EXIF:UserComment"] except: pass try: image.exif_lens = meta["MakerNotes:LensType"] except: pass try: image.point = Point(meta["XMP:GPSLongitude"], meta["XMP:GPSLatitude"], srid=4326) try: image.location = Location.objects.filter(geometry__contains=image.point).get() except Location.DoesNotExist: pass except KeyError: pass image.exif_aperture = meta["EXIF:FNumber"] image.exif_make = meta["EXIF:Make"] image.exif_model = meta["EXIF:Model"] image.exif_exposure = str(Fraction(float(meta["EXIF:ExposureTime"])).limit_denominator()) image.exif_iso = meta["EXIF:ISO"] image.exif_focal_length = meta["EXIF:FocalLength"] fmt_date = time.strptime(meta["EXIF:DateTimeOriginal"], "%Y:%m:%d %H:%M:%S") image.exif_date = time.strftime("%Y-%m-%d %H:%M:%S", fmt_date) image.height = meta["File:ImageHeight"] image.width = meta["File:ImageWidth"] image.save() count += 1 zipper.close() if request: messages.success(request, _('The photos have been uploaded'))