diff options
Diffstat (limited to 'app')
-rw-r--r-- | app/photos/TODO | 5 | ||||
-rw-r--r-- | app/photos/admin.py | 83 | ||||
-rw-r--r-- | app/photos/forms.py | 174 | ||||
-rw-r--r-- | app/photos/models.py | 62 | ||||
-rw-r--r-- | app/photos/views.py | 1 |
5 files changed, 323 insertions, 2 deletions
diff --git a/app/photos/TODO b/app/photos/TODO new file mode 100644 index 0000000..4584c63 --- /dev/null +++ b/app/photos/TODO @@ -0,0 +1,5 @@ +Okay we're uploading/creating galleries/adding images + +Then resize all images for responsive gallery. + +Then play with gallery templates and add a template selecter to form. diff --git a/app/photos/admin.py b/app/photos/admin.py index 3c70c35..2f71fcc 100644 --- a/app/photos/admin.py +++ b/app/photos/admin.py @@ -1,8 +1,60 @@ from django.contrib import admin from django.contrib.gis.admin import OSMGeoAdmin -from photos.models import Photo, PhotoGallery +from django.conf.urls import patterns +from django.conf.urls import url +from django.utils.translation import ungettext, ugettext_lazy as _ +from photos.models import Photo, PhotoGallery, LuxImage, Gallery +from django.shortcuts import render +from django.contrib.admin import helpers +from django.http import HttpResponseRedirect +from .forms import UploadZipForm + +class LuxImageAdmin(OSMGeoAdmin): + list_display = ('pk','admin_thumbnail', 'pub_date',) + list_filter = ('pub_date',) + search_fields = ['title', 'caption'] + +admin.site.register(LuxImage, LuxImageAdmin) + +class GalleryAdmin(OSMGeoAdmin): + list_display = ('title', 'location', 'pub_date') + list_filter = ('location',) + + def get_urls(self): + urls = super(GalleryAdmin, self).get_urls() + custom_urls = patterns('', + url(r'^upload_zip/$', + self.admin_site.admin_view(self.upload_zip), + name='upload_zip') + ) + return custom_urls + urls + + def upload_zip(self, request): + context = { + 'title': _('Upload a zip archive of photos'), + 'app_label': self.model._meta.app_label, + 'opts': self.model._meta, + 'has_change_permission': self.has_change_permission(request) + } + + # Handle form request + if request.method == 'POST': + form = UploadZipForm(request.POST, request.FILES) + if form.is_valid(): + form.save(request=request) + return HttpResponseRedirect('..') + else: + form = UploadZipForm() + context['form'] = form + context['adminform'] = helpers.AdminForm(form, + list([(None, {'fields': form.base_fields})]), + {}) + return render(request, 'admin/upload_zip.html', context) + +admin.site.register(Gallery, GalleryAdmin) + class PhotoAdmin(OSMGeoAdmin): list_display = ('title', 'admin_thumbnail', 'flickr_id', 'pub_date',) list_filter = ('pub_date',) @@ -73,6 +125,35 @@ class PhotoGalleryAdmin(OSMGeoAdmin): ) }), ) + def get_urls(self): + urls = super(PhotoGalleryAdmin, self).get_urls() + custom_urls = patterns('', + url(r'^upload_zip/$', + self.admin_site.admin_view(self.upload_zip), + name='upload_zip') + ) + return custom_urls + urls + + def upload_zip(self, request): + context = { + 'title': _('Upload a zip archive of photos'), + 'app_label': self.model._meta.app_label, + 'opts': self.model._meta, + 'has_change_permission': self.has_change_permission(request) + } + # Handle form request + if request.method == 'POST': + form = UploadZipForm(request.POST, request.FILES) + if form.is_valid(): + form.save(request=request) + return HttpResponseRedirect('..') + else: + form = UploadZipForm() + context['form'] = form + context['adminform'] = helpers.AdminForm(form, + list([(None, {'fields': form.base_fields})]), + {}) + return render(request, 'admin/upload_zip.html', context) admin.site.register(PhotoGallery, PhotoGalleryAdmin) diff --git a/app/photos/forms.py b/app/photos/forms.py new file mode 100644 index 0000000..eae0e7e --- /dev/null +++ b/app/photos/forms.py @@ -0,0 +1,174 @@ +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')) diff --git a/app/photos/models.py b/app/photos/models.py index dce655e..d3024d9 100644 --- a/app/photos/models.py +++ b/app/photos/models.py @@ -10,6 +10,44 @@ from django.conf import settings from taggit.managers import TaggableManager from locations.models import Location, Region +def get_upload_path(self, filename): + return "images/galleries/originals/%s/%s" % (datetime.datetime.today().strftime("%Y"), filename) + +class LuxImage(models.Model): + image = models.FileField(blank=True, null=True, upload_to=get_upload_path) + title = models.CharField(null=True,blank=True, max_length=300) + caption = models.TextField(blank=True, null=True) + pub_date = models.DateTimeField() + exif_raw = models.TextField(blank=True, null=True) + exif_aperture = models.CharField(max_length=50, blank=True, null=True) + exif_make = models.CharField(max_length=50, blank=True, null=True) + exif_model = models.CharField(max_length=50, blank=True, null=True) + exif_exposure = models.CharField(max_length=50, blank=True, null=True) + exif_iso = models.CharField(max_length=50, blank=True, null=True) + exif_focal_length = models.CharField(max_length=50, blank=True, null=True) + exif_lens = models.CharField(max_length=50, blank=True, null=True) + exif_date = models.DateTimeField(blank=True, null=True) + height = models.CharField(max_length=6, blank=True, null=True) + width = models.CharField(max_length=6, blank=True, null=True) + point = models.PointField(null=True, blank=True) + location = models.ForeignKey(Location, null=True, blank=True) + is_public = models.BooleanField(default=True) + is_video = models.BooleanField(default=False) + flickr_id = models.CharField(null=True,blank=True, max_length=80) + + class Meta: + ordering = ('-pub_date', 'id') + verbose_name_plural = 'Images' + get_latest_by = 'pub_date' + + def __str__(self): + return "%s" % self.pk + + def admin_thumbnail(self): + return force_text('<a href="%s"><img src="%s"></a>' % + (self.image.url, self.image.url)) + admin_thumbnail.allow_tags = True + admin_thumbnail.short_description = 'Thumbnail' class Photo(models.Model): description = models.TextField(blank=True, null=True) @@ -171,6 +209,30 @@ class Photo(models.Model): def save(self, *args, **kwargs): super(Photo, self).save() +class Gallery(models.Model): + title = models.CharField(blank=True, max_length=300) + description = models.TextField(blank=True, null=True) + slug = models.CharField(blank=True, max_length=300) + thumb = models.CharField(blank=True, max_length=300) + image = models.ManyToManyField(LuxImage) + pub_date = models.DateTimeField(null=True) + point = models.PointField(null=True, blank=True) + location = models.ForeignKey(Location, null=True, blank=True) + + class Meta: + ordering = ('-pub_date', 'id') + verbose_name_plural = 'Galleries' + get_latest_by = 'pub_date' + + def __str__(self): + return self.title + + def get_main_image(self): + return "%sgallery_thumbs/%s.jpg" % (settings.IMAGES_URL, self.id) + + def get_absolute_url(self): + return "/photos/galleries/%s/" % (self.slug) + class PhotoGallery(models.Model): set_id = models.CharField(blank=True, max_length=300) diff --git a/app/photos/views.py b/app/photos/views.py index 567f329..0def0e3 100644 --- a/app/photos/views.py +++ b/app/photos/views.py @@ -6,7 +6,6 @@ from django.core import serializers from photos.models import Photo, PhotoGallery from locations.models import Country, Region - def gallery_list(request, page): request.page_url = '/photos/%d/' request.page = int(page) |