diff options
author | luxagraf <sng@luxagraf.net> | 2019-04-11 19:46:12 -0500 |
---|---|---|
committer | luxagraf <sng@luxagraf.net> | 2019-04-11 19:46:12 -0500 |
commit | 86fcf7ed710f41fc5324b638d092af54f4bb756f (patch) | |
tree | 28634fddbddb5ac162cd142c8e3a668d2296e414 /app/photos |
initial commit
Diffstat (limited to 'app/photos')
-rw-r--r-- | app/photos/__init__.py | 0 | ||||
-rw-r--r-- | app/photos/admin.py | 32 | ||||
-rw-r--r-- | app/photos/detail_urls.py | 10 | ||||
-rw-r--r-- | app/photos/forms.py | 173 | ||||
-rw-r--r-- | app/photos/migrations/0001_initial.py | 73 | ||||
-rw-r--r-- | app/photos/migrations/__init__.py | 0 | ||||
-rw-r--r-- | app/photos/models.py | 223 | ||||
-rw-r--r-- | app/photos/photos.js | 71 | ||||
-rw-r--r-- | app/photos/retriever.py.bak | 314 | ||||
-rw-r--r-- | app/photos/static/image-preview.js | 42 | ||||
-rw-r--r-- | app/photos/static/my_styles.css | 40 | ||||
-rw-r--r-- | app/photos/templatetags/__init__.py | 0 | ||||
-rw-r--r-- | app/photos/templatetags/get_image_by_size.py | 8 | ||||
-rw-r--r-- | app/photos/templatetags/get_image_width.py | 9 | ||||
-rw-r--r-- | app/photos/urls.py | 56 | ||||
-rw-r--r-- | app/photos/utils.py | 28 | ||||
-rw-r--r-- | app/photos/views.py | 59 |
17 files changed, 1138 insertions, 0 deletions
diff --git a/app/photos/__init__.py b/app/photos/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/app/photos/__init__.py diff --git a/app/photos/admin.py b/app/photos/admin.py new file mode 100644 index 0000000..072808c --- /dev/null +++ b/app/photos/admin.py @@ -0,0 +1,32 @@ +from django.contrib import admin +from photos.models import LuxImage, LuxImageSize, LuxVideo + + +@admin.register(LuxImageSize) +class LuxImageSizeAdmin(admin.ModelAdmin): + list_display = ('name', 'width', 'height', 'quality') + + +@admin.register(LuxVideo) +class LuxVideoAdmin(admin.ModelAdmin): + pass + + +@admin.register(LuxImage) +class LuxImageAdmin(admin.ModelAdmin): + list_display = ('pk', 'admin_thumbnail', 'pub_date', 'caption') + list_filter = ('pub_date',) + search_fields = ['title', 'caption'] + + fieldsets = ( + (None, { + 'fields': ('title', ('image'), 'pub_date', 'sizes', 'alt', 'caption', ('is_public'), ('photo_credit_source', 'photo_credit_url')) + }), + ('Exif Data', { + 'classes': ('collapse',), + 'fields': ('height', 'width'), + }), + ) + + class Media: + js = ('image-preview.js', 'next-prev-links.js') diff --git a/app/photos/detail_urls.py b/app/photos/detail_urls.py new file mode 100644 index 0000000..0ab94f6 --- /dev/null +++ b/app/photos/detail_urls.py @@ -0,0 +1,10 @@ +from django.conf.urls import url +from django.views.generic.detail import DetailView +from photos.models import Photo + +urlpatterns = [ + url( + r'^(?P<object_id>\d+)/$', + DetailView.as_view(model=Photo, template_name='details/photo.html') + ), +] diff --git a/app/photos/forms.py b/app/photos/forms.py new file mode 100644 index 0000000..126cfaf --- /dev/null +++ b/app/photos/forms.py @@ -0,0 +1,173 @@ +import zipfile +from zipfile import BadZipFile +import logging +import datetime +import os +from io import BytesIO +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.core.files.base import ContentFile +from django.contrib.admin import widgets +from django.utils.safestring import mark_safe + +from photos.models import LuxImage, LuxGallery, LuxImageSize + +logger = logging.getLogger('photos.forms') + + +class GalleryForm(forms.ModelForm): + class Meta: + fields = '__all__' + widgets = { + 'images': forms.SelectMultiple, + } + + def __init__(self, *args, **kwargs): + super(GalleryForm, self).__init__(*args, **kwargs) + self.fields['images'].choices = [(image.id, mark_safe('%sqq%sqq%s' % (image.title, image.get_image_by_size('tn'), image.pk))) for image in LuxImage.objects.all()[:40]] + self.fields['images'].allow_tags = True + + +class ImageChoiceField(forms.ModelMultipleChoiceField): + + def label_from_instance(self, obj): + + return mark_safe('%sqq%sqq%s' % (obj.title, obj.get_image_by_size('tn'), obj.pk)) + + +class FKGalleryForm(forms.ModelForm): + class Meta: + fields = '__all__' + widgets = { + 'image': ImageChoiceField(queryset=LuxImage.objects.all()), + } + + def __init__(self, *args, **kwargs): + super(FKGalleryForm, self).__init__(*args, **kwargs) + self.fields['image'].choices = [(o.id, str(o.image.url)) for o in LuxImage.objects.all()] + self.fields['image'].allow_tags = True + + +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 LuxGallery.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 = LuxGallery.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'], + is_public=self.cleaned_data['is_public'] + ) + 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 + + fn, file_extension = os.path.splitext(filename) + if file_extension != ".mp4": + # 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) + if file_extension != ".mp4": + img = Image.open(image.image.path) + if img.size[0] > img.size[1]: + image.sizes.add(LuxImageSize.objects.get(width=2560)) + image.sizes.add(LuxImageSize.objects.get(width=1170)) + image.sizes.add(LuxImageSize.objects.get(width=720)) + if img.size[1] > img.size[0]: + image.sizes.add(LuxImageSize.objects.get(height=1600)) + image.sizes.add(LuxImageSize.objects.get(height=800)) + image.sizes.add(LuxImageSize.objects.get(height=460)) + image.save() + gallery.images.add(image) + + zipper.close() + + if request: + messages.success(request, _('The photos have been uploaded')) diff --git a/app/photos/migrations/0001_initial.py b/app/photos/migrations/0001_initial.py new file mode 100644 index 0000000..f06bad5 --- /dev/null +++ b/app/photos/migrations/0001_initial.py @@ -0,0 +1,73 @@ +# Generated by Django 2.1.7 on 2019-03-30 17:07 + +import datetime +from django.db import migrations, models +import photos.models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='LuxImage', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('image', models.FileField(blank=True, null=True, upload_to=photos.models.get_upload_path)), + ('title', models.CharField(blank=True, max_length=300, null=True)), + ('alt', models.CharField(blank=True, max_length=300, null=True)), + ('photo_credit_source', models.CharField(blank=True, max_length=300, null=True)), + ('photo_credit_url', models.CharField(blank=True, max_length=300, null=True)), + ('caption', models.TextField(blank=True, null=True)), + ('pub_date', models.DateTimeField(default=datetime.datetime.now)), + ('height', models.CharField(blank=True, max_length=6, null=True)), + ('width', models.CharField(blank=True, max_length=6, null=True)), + ('is_public', models.BooleanField(default=True)), + ], + options={ + 'get_latest_by': 'pub_date', + 'ordering': ('-pub_date', 'id'), + 'verbose_name_plural': 'Images', + }, + ), + migrations.CreateModel( + name='LuxImageSize', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(blank=True, max_length=30, null=True)), + ('width', models.IntegerField(blank=True, null=True)), + ('height', models.IntegerField(blank=True, null=True)), + ('quality', models.IntegerField()), + ], + options={ + 'verbose_name_plural': 'Image Sizes', + }, + ), + migrations.CreateModel( + name='LuxVideo', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('video_mp4', models.FileField(blank=True, null=True, upload_to=photos.models.get_vid_upload_path)), + ('video_webm', models.FileField(blank=True, null=True, upload_to=photos.models.get_vid_upload_path)), + ('video_poster', models.FileField(blank=True, null=True, upload_to=photos.models.get_vid_upload_path)), + ('title', models.CharField(blank=True, max_length=300, null=True)), + ('pub_date', models.DateTimeField(default=datetime.datetime.now)), + ('youtube_url', models.CharField(blank=True, max_length=80, null=True)), + ('vimeo_url', models.CharField(blank=True, max_length=300, null=True)), + ], + options={ + 'get_latest_by': 'pub_date', + 'ordering': ('-pub_date', 'id'), + 'verbose_name_plural': 'Videos', + }, + ), + migrations.AddField( + model_name='luximage', + name='sizes', + field=models.ManyToManyField(blank=True, to='photos.LuxImageSize'), + ), + ] diff --git a/app/photos/migrations/__init__.py b/app/photos/migrations/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/app/photos/migrations/__init__.py diff --git a/app/photos/models.py b/app/photos/models.py new file mode 100644 index 0000000..4a1bf99 --- /dev/null +++ b/app/photos/models.py @@ -0,0 +1,223 @@ +import os.path +import io +import datetime +from PIL import Image + +from django.core.exceptions import ValidationError +from django.db import models +from django.contrib.sitemaps import Sitemap +from django.utils.encoding import force_text +from django.urls import reverse +from django.apps import apps +from django.utils.html import format_html +from django.utils.text import slugify +from django.conf import settings +from django import forms + +from resizeimage.imageexceptions import ImageSizeError + +from .utils import resize_image +from django.db.models.signals import post_save +from django.dispatch import receiver +from django.db.models.signals import m2m_changed + + +def get_upload_path(self, filename): + return "images/original/%s/%s" % (datetime.datetime.today().strftime("%Y"), filename) + + +def get_vid_upload_path(self, filename): + return "images/videos/%s/%s" % (datetime.datetime.today().strftime("%Y"), filename) + + +class LuxImageSize(models.Model): + name = models.CharField(null=True, blank=True, max_length=30) + width = models.IntegerField(null=True, blank=True) + height = models.IntegerField(null=True, blank=True) + quality = models.IntegerField() + + class Meta: + verbose_name_plural = 'Image Sizes' + + def __str__(self): + if self.width: + size = self.width + if self.height: + size = self.height + return "%s - %s" %(self.name, str(size)) + + +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) + alt = models.CharField(null=True, blank=True, max_length=300) + photo_credit_source = models.CharField(null=True, blank=True, max_length=300) + photo_credit_url = models.CharField(null=True, blank=True, max_length=300) + caption = models.TextField(blank=True, null=True) + pub_date = models.DateTimeField(default=datetime.datetime.now) + height = models.CharField(max_length=6, blank=True, null=True) + width = models.CharField(max_length=6, blank=True, null=True) + is_public = models.BooleanField(default=True) + sizes = models.ManyToManyField(LuxImageSize, blank=True) + + class Meta: + ordering = ('-pub_date', 'id') + verbose_name_plural = 'Images' + get_latest_by = 'pub_date' + + def __str__(self): + if self.title: + return "%s" % self.title + else: + return "%s" % self.pk + + def get_type(self): + return str(self.__class__.__name__) + + def get_admin_image(self): + for size in self.sizes.all(): + if size.width and size.width <= 820 or size.height and size.height <= 800: + return self.get_image_by_size(size.name) + + def get_admin_insert(self): + return "/media/images/%s/%s_tn.%s" % (self.pub_date.strftime("%Y"), self.get_image_name(), self.get_image_ext()) + + def get_largest_image(self): + t = [] + for size in self.sizes.all(): + t.append(size.width) + t.sort(key=float) + t.reverse() + return self.get_image_path_by_size(t[0]) + + def get_image_name(self): + return self.image.url.split("original/")[1][5:-4] + + def get_image_ext(self): + return self.image.url[-3:] + + def get_image_by_size(self, size="original"): + base = self.get_image_name() + if size == "admin_insert": + return "images/%s/%s.%s" % (self.pub_date.strftime("%Y"), base, self.get_image_ext()) + if size == "original": + return "%soriginal/%s/%s.%s" % (settings.IMAGES_URL, self.pub_date.strftime("%Y"), base, self.get_image_ext()) + else: + if size != 'tn': + s = LuxImageSize.objects.get(name=size) + if s not in self.sizes.all(): + print("new size is "+s.name) + self.sizes.add(s) + return "%s%s/%s_%s.%s" % (settings.IMAGES_URL, self.pub_date.strftime("%Y"), base, size, self.get_image_ext()) + + def get_image_path_by_size(self, size="original"): + base = self.get_image_name() + if size == "original": + return "%s/original/%s/%s.%s" % (settings.IMAGES_ROOT, self.pub_date.strftime("%Y"), base, self.get_image_ext()) + else: + return "%s/%s/%s_%s.%s" % (settings.IMAGES_ROOT, self.pub_date.strftime("%Y"), base, size, self.get_image_ext()) + + def admin_thumbnail(self): + return format_html('<a href="%s"><img src="%s"></a>' % (self.get_image_by_size(), self.get_image_by_size("tn"))) + admin_thumbnail.short_description = 'Thumbnail' + + @property + def get_previous_published(self): + return self.get_previous_by_pub_date() + + @property + def get_next_published(self): + return self.get_next_by_pub_date() + + @property + def get_previous_admin_url(self): + n = self.get_previous_by_pub_date() + return reverse('admin:%s_%s_change' %(self._meta.app_label, self._meta.model_name), args=[n.id] ) + + @property + def get_next_admin_url(self): + model = apps.get_model(app_label=self._meta.app_label, model_name=self._meta.model_name) + try: + return reverse('admin:%s_%s_change' %(self._meta.app_label, self._meta.model_name), args=[self.get_next_by_pub_date().pk] ) + except model.DoesNotExist: + return '' + + @property + def is_portait(self): + if int(self.height) > int(self.width): + return True + else: + return False + + def save(self, *args, **kwargs): + super(LuxImage, self).save() + + +@receiver(post_save, sender=LuxImage) +def post_save_events(sender, update_fields, created, instance, **kwargs): + if instance.exif_raw == '': + filename, file_extension = os.path.splitext(instance.image.path) + if file_extension != ".mp4": + img = Image.open(instance.image.path) + instance.height = img.height + instance.width = img.width + #instance = readexif(instance) + post_save.disconnect(post_save_events, sender=LuxImage) + instance.save() + post_save.connect(post_save_events, sender=LuxImage) + + +@receiver(m2m_changed, sender=LuxImage.sizes.through) +def update_photo_sizes(sender, instance, **kwargs): + base_path = "%s/%s/" % (settings.IMAGES_ROOT, instance.pub_date.strftime("%Y")) + filename, file_extension = os.path.splitext(instance.image.path) + if file_extension != ".mp4": + img = Image.open(instance.image.path) + resize_image(img, 160, None, 78, base_path, "%s_tn.%s" % (instance.get_image_name(), instance.get_image_ext())) + for size in instance.sizes.all(): + if size.width: + print("Image width is:"+str(img.width)) + try: + if size.width <= img.width: + resize_image(img, size.width, None, size.quality, base_path, "%s_%s.%s" % (instance.get_image_name(), slugify(size.name), instance.get_image_ext())) + else: + raise ValidationError({'items': ["Size is larger than source image"]}) + except ImageSizeError: + m2m_changed.disconnect(update_photo_sizes, sender=LuxImage.sizes.through) + instance.sizes.remove(size) + m2m_changed.connect(update_photo_sizes, sender=LuxImage.sizes.through) + if size.height: + try: + if size.height <= img.height: + resize_image(img, None, size.height, size.quality, base_path, "%s_%s.%s" % (instance.get_image_name(), slugify(size.name), instance.get_image_ext())) + + else: + pass + except ImageSizeError: + m2m_changed.disconnect(update_photo_sizes, sender=LuxImage.sizes.through) + instance.sizes.remove(size) + m2m_changed.connect(update_photo_sizes, sender=LuxImage.sizes.through) + + +class LuxVideo(models.Model): + video_mp4 = models.FileField(blank=True, null=True, upload_to=get_vid_upload_path) + video_webm = models.FileField(blank=True, null=True, upload_to=get_vid_upload_path) + video_poster = models.FileField(blank=True, null=True, upload_to=get_vid_upload_path) + title = models.CharField(null=True, blank=True, max_length=300) + pub_date = models.DateTimeField(default=datetime.datetime.now) + youtube_url = models.CharField(null=True, blank=True, max_length=80) + vimeo_url = models.CharField(null=True, blank=True, max_length=300) + + def __str__(self): + if self.title: + return self.title + else: + return str(self.pk) + + def get_type(self): + return str(self.__class__.__name__) + + class Meta: + ordering = ('-pub_date', 'id') + verbose_name_plural = 'Videos' + get_latest_by = 'pub_date' diff --git a/app/photos/photos.js b/app/photos/photos.js new file mode 100644 index 0000000..b93467a --- /dev/null +++ b/app/photos/photos.js @@ -0,0 +1,71 @@ +//Utility functions for map info window +function mapit(obj) { + lat = parseFloat(obj.attr('data-latitude')); + lon = parseFloat(obj.attr('data-longitude')); + elid= obj.attr('data-imgid'); + map = L.map(document.getElementById("mw-"+elid)); + centerCoord = new L.LatLng(lat, lon); + zoom = 8; + L.tileLayer.provider('Esri.WorldTopoMap', {maxZoom: 18, attribution: 'Map data © <a href="http://openstreetmap.org">OpenStreetMap</a> contributors, <a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, Tiles © Esri and the GIS User Community'}).addTo(map); + map.setView(centerCoord, zoom); + L.marker([lat, lon]).addTo(map); +} + //########## utility functions to create/remove map container ############ +function create_map(obj) { + //find id of this image caption: + var imgid = obj.attr('data-imgid'); + //create container divs + $('<div class="map-container" id="mc-'+imgid+'">').insertBefore($(obj).parent().parent()); + //$(obj).parent().parent().parent().prepend('<div class="map-container" id="mc-'+imgid+'">'); + $('#mc-'+imgid).append('<div class="map-wrapper" id="mw-'+imgid+'">'); + //deal with the variable height of div.legend + $('#mc-'+imgid).css({ + bottom: function(index, value) { + return parseFloat($(obj).parent().parent().height())+20; + } + }); + + mapit(obj); +} +function remove_map(imgid) { + $('#mc-'+imgid).remove(); +} + +//############ Document.ready events ############## +$(document).ready(function(){ + + //set up click events for map button + $('.map-link').click( function() { + imgid = $(this).attr('data-imgid'); + if ($('#mc-'+imgid).is(":visible")) { + remove_map(imgid); + } else { + create_map($(this)); + } + return false; + + }); + var $ele = $('#slides').children(); + var $curr = 0; + $(document).bind('keydown', function (e) { + var code = e.which; + switch (code) { + case 39: + if ($curr <= $ele.size()) { + $.scrollTo($ele[$curr], 800 ); + $curr++; + } + break; + case 37: + if ($curr > 0) { + $curr--; + var $now = $curr; + $now--; + $.scrollTo($ele[$now], 800 ); + } + break; + } + return; + }); +}); + diff --git a/app/photos/retriever.py.bak b/app/photos/retriever.py.bak new file mode 100644 index 0000000..d3c572a --- /dev/null +++ b/app/photos/retriever.py.bak @@ -0,0 +1,314 @@ +from __future__ import division +import datetime +import os +import cStringIO +import urllib + +from django.template.defaultfilters import slugify +from django.core.exceptions import ObjectDoesNotExist +from django.utils.encoding import force_unicode +from django.conf import settings + +# Required PIL classes may or may not be available from the root namespace +# depending on the installation +try: + import Image + import ImageFile +except ImportError: + try: + from PIL import Image + from PIL import ImageFile + except ImportError: + raise ImportError("Could not import the Python Imaging Library.") + +ImageFile.MAXBLOCK = 1000000 + +from photos.models import Photo, PhotoGallery + +# from https://github.com/alexis-mignon/python-flickr-api +# terribly documented, but offers a good clean OOP approach if you're willing to figure it out... +import flickr_api + +EXIF_PARAMS = { + "FNumber": 'f/2.8', + "Make": 'Apple', + "Model": 'iPhone', + "ExposureTime": '', + "ISO": '', + "FocalLength": '', + "LensModel": '', + 'DateTimeOriginal': '2013:09:03 22:44:25' +} + + +def sync_flickr_photos(*args, **kwargs): + flickr_api.set_keys(api_key=settings.FLICKR_API_KEY, api_secret=settings.FLICKR_API_SECRET) + flickr_api.set_auth_handler("app/photos/flickrauth") + user = flickr_api.test.login() + photos = user.getPhotos(extras="date_upload,date_taken,geo") + # reverse! reverse! + photos.reverse() + for photo in photos: + info = photo.getInfo() + try: + row = Photo.objects.get(flickr_id=info['id'], flickr_secret=info['secret']) + print('already have ' + info['id'] + ' moving on') + except ObjectDoesNotExist: + get_photo(photo) + + +def get_photo(photo): + info = photo.getInfo() + geo = photo.getLocation() + location, region = get_geo(float(geo['latitude']), float(geo['longitude'])) + exif = exif_handler(photo.getExif()) + p, created = Photo.objects.get_or_create( + title=info['title'], + flickr_id=info['id'], + flickr_owner=info['owner']['id'], + flickr_server=info['server'], + flickr_secret=info['secret'], + flickr_originalsecret=info['originalsecret'], + flickr_farm=info['farm'], + pub_date=flickr_datetime_to_datetime(info['taken']), + description=info['description'], + exif_aperture=exif['FNumber'], + exif_make=exif['Make'], + exif_model=exif['Model'], + exif_exposure=exif['ExposureTime'], + exif_iso=exif['ISO'], + exif_lens=exif['LensModel'], + exif_focal_length=exif['FocalLength'], + exif_date=flickr_datetime_to_datetime(exif["DateTimeOriginal"].replace(':', '-', 2)), + lat=float(geo['latitude']), + lon=float(geo['longitude']), + region=region, + location=location, + ) + if created: + for tag in info['tags']: + p.tags.add(tag['raw']) + p.save() + make_local_copies(p) + #retina image: + #slideshow_image(p, 2000, 1600, 75) + #normal image + print(p.title) + return p + + +def sync_sets(*args, **kwargs): + flickr_api.set_keys(api_key=settings.FLICKR_API_KEY, api_secret=settings.FLICKR_API_SECRET) + flickr_api.set_auth_handler("app/photos/flickrauth") + user = flickr_api.test.login() + photosets = user.getPhotosets() + # reverse! reverse! + photosets.reverse() + disregard = [ + 'POTD 2008', + 'Snow Day', + 'Wedding', + 'Some random stuff', + 'Lilah & Olivia', + '6 months+', + '6-9 months', + '9-18 months', + ] + for photoset in photosets: + if photoset['title'] in disregard: + pass + else: + try: + row = PhotoGallery.objects.get(set_id__exact=photoset['id']) + print('%s %s %s' % ('already have', row.set_title, 'moving on...')) + # okay it already exists, but is it up-to-date? + #get_photos_in_set(row,set.id) + except ObjectDoesNotExist: + s = PhotoGallery.objects.create( + set_id=force_unicode(photoset['id']), + set_title=force_unicode(photoset['title']), + set_desc=force_unicode(photoset['description']), + set_slug=slugify(force_unicode(photoset['title'])), + primary=force_unicode(photoset['primary']), + pub_date=datetime.datetime.fromtimestamp(float(photoset['date_create'])) + ) + + get_photos_in_set(photoset, s) + #create the gallery thumbnail image: + photo = Photo.objects.get(flickr_id__exact=str(photoset['primary'])) + make_gallery_thumb(photo, s) + + +def get_photos_in_set(flickr_photoset, photoset): + for photo in flickr_photoset.getPhotos(): + try: + p = Photo.objects.get(flickr_id__exact=str(photo['id'])) + except ObjectDoesNotExist: + p = get_photo(photo) + if p.is_public: + photoset.photos.add(p) + slideshow_image(p, 1000, 800, 95) + + +################################################ +## Various meta data and geo helper functions ## +################################################ + + +def exif_handler(data): + converted = {} + try: + for t in data: + converted[t['tag']] = t['raw'] + except: + pass + for k, v in EXIF_PARAMS.items(): + if not converted.has_key(k): + converted[k] = v + return converted + + +def flickr_datetime_to_datetime(fdt): + from datetime import datetime + from time import strptime + date_parts = strptime(fdt, '%Y-%m-%d %H:%M:%S') + return datetime(*date_parts[0:6]) + +def get_geo(lat,lon): + from locations.models import Location, Region + from django.contrib.gis.geos import Point + pnt_wkt = Point(lon, lat) + try: + location = Location.objects.get(geometry__contains=pnt_wkt) + except Location.DoesNotExist: + location = None + try: + region = Region.objects.get(geometry__contains=pnt_wkt) + except Region.DoesNotExist: + region = None + return location, region + +####################################################################### +## Photo retrieval functions to pull down images from Flickr servers ## +####################################################################### + +def slideshow_image(photo,max_width, max_height, quality): + slide_dir = settings.IMAGES_ROOT + '/slideshow/'+ photo.pub_date.strftime("%Y") + if not os.path.isdir(slide_dir): + os.makedirs(slide_dir) + + # Is it a retina image or not? + if max_width >= 1001 or max_height >= 801: + filename = '%s/%sx2.jpg' %(slide_dir, photo.flickr_id) + else: + filename = '%s/%s.jpg' %(slide_dir, photo.flickr_id) + + flickr_photo = photo.get_original_url() + fname = urllib.urlopen(flickr_photo) + im = cStringIO.StringIO(fname.read()) # constructs a StringIO holding the image + img = Image.open(im) + cur_width, cur_height = img.size + #if image landscape + if cur_width > cur_height: + new_width = max_width + #check to make sure we aren't upsizing + if cur_width > new_width: + ratio = float(new_width)/cur_width + x = (cur_width * ratio) + y = (cur_height * ratio) + resized = img.resize((int(x), int(y)), Image.ANTIALIAS) + resized.save(filename, 'JPEG', quality=quality, optimize=True) + else: + img.save(filename) + else: + #image portrait + new_height = max_height + #check to make sure we aren't upsizing + if cur_height > new_height: + ratio = float(new_height)/cur_height + x = (cur_width * ratio) + y = (cur_height * ratio) + resized = img.resize((int(x), int(y)), Image.ANTIALIAS) + resized.save(filename, 'JPEG', quality=quality, optimize=True) + else: + img.save(filename) + photo.slideshowimage_width = photo.get_width + photo.slideshowimage_height = photo.get_height + photo.slideshowimage_margintop = photo.get_margin_top + photo.slideshowimage_marginleft = photo.get_margin_left + photo.save() + #now resize the local copy + + + +def make_local_copies(photo): + orig_dir = settings.IMAGES_ROOT + '/flickr/full/'+ photo.pub_date.strftime("%Y") + if not os.path.isdir(orig_dir): + os.makedirs(orig_dir) + full = photo.get_original_url() + fname = urllib.urlopen(full) + im = cStringIO.StringIO(fname.read()) # constructs a StringIO holding the image + img = Image.open(im) + local_full = '%s/%s.jpg' %(orig_dir, photo.flickr_id) + img.save(local_full) + #save large size + large_dir = settings.IMAGES_ROOT + '/flickr/large/'+ photo.pub_date.strftime("%Y") + if not os.path.isdir(large_dir): + os.makedirs(large_dir) + large = photo.get_large_url() + fname = urllib.urlopen(large) + im = cStringIO.StringIO(fname.read()) # constructs a StringIO holding the image + img = Image.open(im) + local_large = '%s/%s.jpg' %(large_dir, photo.flickr_id) + if img.format == 'JPEG': + img.save(local_large) + #save medium size + med_dir = settings.IMAGES_ROOT + '/flickr/med/'+ photo.pub_date.strftime("%Y") + if not os.path.isdir(med_dir): + os.makedirs(med_dir) + med = photo.get_medium_url() + fname = urllib.urlopen(med) + im = cStringIO.StringIO(fname.read()) # constructs a StringIO holding the image + img = Image.open(im) + local_med = '%s/%s.jpg' %(med_dir, photo.flickr_id) + img.save(local_med) + +def make_gallery_thumb(photo,set): + crop_dir = settings.IMAGES_ROOT + '/gallery_thumbs/' + if not os.path.isdir(crop_dir): + os.makedirs(crop_dir) + remote = photo.get_original_url() + print(remote) + fname = urllib.urlopen(remote) + im = cStringIO.StringIO(fname.read()) # constructs a StringIO holding the image + img = Image.open(im) + + #calculate crop: + cur_width, cur_height = img.size + new_width, new_height = 291, 350 + ratio = max(float(new_width)/cur_width,float(new_height)/cur_height) + x = (cur_width * ratio) + y = (cur_height * ratio) + xd = abs(new_width - x) + yd = abs(new_height - y) + x_diff = int(xd / 2) + y_diff = int(yd / 2) + box = (int(x_diff), int(y_diff), int(x_diff+new_width), int(y_diff+new_height)) + + #create resized file + resized = img.resize((int(x), int(y)), Image.ANTIALIAS).crop(box) + # save resized file + resized_filename = '%s/%s.jpg' %(crop_dir, set.id) + try: + if img.format == 'JPEG': + resized.save(resized_filename, 'JPEG', quality=95, optimize=True) + else: + resized.save(resized_filename) + except IOError, e: + if os.path.isfile(resized_filename): + os.unlink(resized_filename) + raise e + #os.unlink(img) + + + diff --git a/app/photos/static/image-preview.js b/app/photos/static/image-preview.js new file mode 100644 index 0000000..b8fead5 --- /dev/null +++ b/app/photos/static/image-preview.js @@ -0,0 +1,42 @@ +function build_image_preview () { + var url = window.location.href + var cur = url.split('/')[6]; + if (cur) { + var container = document.createElement("div"); + container.className = "form-row field-image"; + var wrapper = document.createElement("div"); + var label = document.createElement("label"); + label.textContent = "Image:"; + var pwrap = document.createElement("p"); + var img = document.createElement("img"); + + var request = new XMLHttpRequest(); + request.open('GET', '/photos/luximage/data/admin/preview/'+cur+'/', true); + request.onload = function() { + if (request.status >= 200 && request.status < 400) { + var data = JSON.parse(request.responseText); + //console.log(data); + img.src = data['url']; + } else { + console.log("server error"); + } + }; + request.onerror = function() { + console.log("error on request"); + }; + request.send(); + pwrap.appendChild(img); + wrapper.appendChild(label); + wrapper.appendChild(pwrap); + container.appendChild(wrapper); + parent = document.getElementById("luximage_form"); + node = parent.children[1].children[0]; + node.parentNode.insertBefore(container, node.previousSibling); + } else { + return; + } +} + +document.addEventListener("DOMContentLoaded", function(event) { + build_image_preview(); +}); diff --git a/app/photos/static/my_styles.css b/app/photos/static/my_styles.css new file mode 100644 index 0000000..d13c8e4 --- /dev/null +++ b/app/photos/static/my_styles.css @@ -0,0 +1,40 @@ + +/*o.v.*/ + +#id_featured_image { + /*style the "box" in its minimzed state*/ + border:1px solid black; width:230px; overflow:hidden; + height:300px; overflow-y:scroll; + /*animate collapsing the dropdown from open to closed state (v. fast)*/ +} +#id_featured_image input { + /*hide the nasty default radio buttons. like, completely!*/ + position:absolute;top:0;left:0;opacity:0; +} + + +#id_featured_image label { + /*style the labels to look like dropdown options, kinda*/ + color: #000; + display:block; + margin: 2px 2px 2px 10px; + height:102px; + opacity:.6; + background-repeat: no-repeat; +} +#id_featured_image:hover label{ + /*this is how labels render in the "expanded" state. we want to see only the selected radio button in the collapsed menu, and all of them when expanded*/ +} +#id_featured_image label:hover { + opacity:.8; +} +#id_featured_image input:checked + label { + /*tricky! labels immediately following a checked radio button (with our markup they are semantically related) should be fully opaque regardless of hover, and they should always be visible (i.e. even in the collapsed menu*/ + opacity:1 !important; + display:block; + background: #333; +} + +/*pfft, nothing as cool here, just the value trace*/ +#trace {margin:0 0 20px;} +#id_featured_image li:first-child { display: none;} diff --git a/app/photos/templatetags/__init__.py b/app/photos/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/app/photos/templatetags/__init__.py diff --git a/app/photos/templatetags/get_image_by_size.py b/app/photos/templatetags/get_image_by_size.py new file mode 100644 index 0000000..c56c44e --- /dev/null +++ b/app/photos/templatetags/get_image_by_size.py @@ -0,0 +1,8 @@ +from django import template + +register = template.Library() + +@register.simple_tag +def get_image_by_size(obj, *args): + method = getattr(obj, "get_image_by_size") + return method(*args) diff --git a/app/photos/templatetags/get_image_width.py b/app/photos/templatetags/get_image_width.py new file mode 100644 index 0000000..ac39184 --- /dev/null +++ b/app/photos/templatetags/get_image_width.py @@ -0,0 +1,9 @@ +from math import floor +from django import template + +register = template.Library() + +@register.simple_tag +def get_image_width(obj, size, *args): + ratio = floor(int(size)*100/int(obj.height))/100 + return floor(ratio*int(obj.height)) diff --git a/app/photos/urls.py b/app/photos/urls.py new file mode 100644 index 0000000..73d906c --- /dev/null +++ b/app/photos/urls.py @@ -0,0 +1,56 @@ +from django.urls import path, re_path +from django.views.generic.base import RedirectView + +from . import views + +app_name = "photos" + +urlpatterns = [ + path( + r'daily/<int:page>', + views.DailyPhotoList.as_view(), + name="daily_photo_list" + ), + path( + r'daily/', + views.DailyPhotoList.as_view(), + {'page': 1}, + name="daily_photo_list" + ), + path( + r'data/(<str:slug>/$', + views.photo_json + ), + path( + r'data/admin/preview/<int:pk>/', + views.photo_preview_json, + name="admin_image_preview" + ), + path( + r'data/admin/tn/<int:pk>/', + views.thumb_preview_json, + name="admin_thumb_preview" + ), + re_path( + r'galleries/(?P<slug>[-\w]+)$', + views.Gallery.as_view(), + name="private" + ), + re_path( + r'galleries/(?P<page>\d+)/$', + views.GalleryList.as_view(), + name="private_list" + ), + path( + r'galleries/', + RedirectView.as_view(url="/photos/galleries/1/", permanent=False) + ), + path( + r'<str:slug>/', + RedirectView.as_view(url="/photos/%(slug)s/1/", permanent=False) + ), + re_path( + r'', + RedirectView.as_view(url="/photos/1/", permanent=False) + ), +] diff --git a/app/photos/utils.py b/app/photos/utils.py new file mode 100644 index 0000000..84e72f5 --- /dev/null +++ b/app/photos/utils.py @@ -0,0 +1,28 @@ +import os +import re +import subprocess + +from django.apps import apps +from django.conf import settings + +from PIL import ImageFile +from bs4 import BeautifulSoup +# pip install python-resize-image +from resizeimage import resizeimage + + +def resize_image(img, width=None, height=None, quality=72, base_path="", filename=""): + if width and height: + newimg = resizeimage.resize_cover(img, [width, height]) + if width and not height: + newimg = resizeimage.resize_width(img, width) + if height and not width: + newimg = resizeimage.resize_height(img, height) + if not os.path.isdir(base_path): + os.makedirs(base_path) + path = "%s%s" % (base_path, filename) + ImageFile.MAXBLOCK = img.size[0] * img.size[1] * 4 + newimg.save(path, newimg.format, quality=quality) + subprocess.call(["jpegoptim", "%s" % path]) + + diff --git a/app/photos/views.py b/app/photos/views.py new file mode 100644 index 0000000..4581e07 --- /dev/null +++ b/app/photos/views.py @@ -0,0 +1,59 @@ +import json +from django.shortcuts import render_to_response, render +from django.template import RequestContext +from django.http import Http404, HttpResponse +from django.core import serializers + +from .models import Photo, PhotoGallery, LuxGallery, LuxImage +from locations.models import Country, Region + +from utils.views import PaginatedListView +from django.views.generic import ListView +from django.views.generic.detail import DetailView + + +class Gallery(DetailView): + model = LuxGallery + slug_field = "slug" + template_name = "details/photo_gallery.html" + + +class GalleryList(PaginatedListView): + template_name = 'archives/gallery_list.html' + + def get_queryset(self): + return LuxGallery.objects.filter(is_public=True) + + def get_context_data(self, **kwargs): + # Call the base implementation first to get a context + context = super(GalleryList, self).get_context_data(**kwargs) + context['is_private'] = False + return context + + +class DailyPhotoList(PaginatedListView): + template_name = 'archives/photo_daily_list.html' + + def get_queryset(self): + return LuxImage.objects.filter(is_public=True, title__startswith="daily_") + + +def photo_json(request, slug): + p = PhotoGallery.objects.filter(set_slug=slug) + return HttpResponse(serializers.serialize('json', p), mimetype='application/json') + + +def photo_preview_json(request, pk): + p = LuxImage.objects.get(pk=pk) + data = {} + data['url'] = p.get_admin_image() + data = json.dumps(data) + return HttpResponse(data) + + +def thumb_preview_json(request, pk): + p = LuxImage.objects.get(pk=pk) + data = {} + data['url'] = p.get_admin_insert() + data = json.dumps(data) + return HttpResponse(data) |