summaryrefslogtreecommitdiff
path: root/app/photos
diff options
context:
space:
mode:
authorluxagraf <sng@luxagraf.net>2019-04-11 19:46:12 -0500
committerluxagraf <sng@luxagraf.net>2019-04-11 19:46:12 -0500
commit86fcf7ed710f41fc5324b638d092af54f4bb756f (patch)
tree28634fddbddb5ac162cd142c8e3a668d2296e414 /app/photos
initial commit
Diffstat (limited to 'app/photos')
-rw-r--r--app/photos/__init__.py0
-rw-r--r--app/photos/admin.py32
-rw-r--r--app/photos/detail_urls.py10
-rw-r--r--app/photos/forms.py173
-rw-r--r--app/photos/migrations/0001_initial.py73
-rw-r--r--app/photos/migrations/__init__.py0
-rw-r--r--app/photos/models.py223
-rw-r--r--app/photos/photos.js71
-rw-r--r--app/photos/retriever.py.bak314
-rw-r--r--app/photos/static/image-preview.js42
-rw-r--r--app/photos/static/my_styles.css40
-rw-r--r--app/photos/templatetags/__init__.py0
-rw-r--r--app/photos/templatetags/get_image_by_size.py8
-rw-r--r--app/photos/templatetags/get_image_width.py9
-rw-r--r--app/photos/urls.py56
-rw-r--r--app/photos/utils.py28
-rw-r--r--app/photos/views.py59
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 &copy; <a href="http://openstreetmap.org">OpenStreetMap</a> contributors, <a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, Tiles &copy; 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)