diff options
Diffstat (limited to 'app')
31 files changed, 2215 insertions, 2 deletions
diff --git a/app/media/0002_auto_20201201_2054.py b/app/media/0002_auto_20201201_2054.py new file mode 100644 index 0000000..843f48b --- /dev/null +++ b/app/media/0002_auto_20201201_2054.py @@ -0,0 +1,46 @@ +# Generated by Django 3.1 on 2020-12-01 19:26 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('media', '0001_initial'), + ] + + operations = [ + migrations.RunSQL(""" + INSERT INTO media_luximagesize ( + id, + name, + width, + height, + quality + ) + SELECT + id, + name, + width, + height, + quality + FROM + photos_luximagesize; + """, reverse_sql=""" + INSERT INTO photos_luximagesize ( + id, + name, + width, + height, + quality + ) + SELECT + id, + name, + width, + height, + quality + FROM + media_luximagesize; + """) + ] diff --git a/app/media/0003_auto_20201201_2055.py b/app/media/0003_auto_20201201_2055.py new file mode 100644 index 0000000..4aeec12 --- /dev/null +++ b/app/media/0003_auto_20201201_2055.py @@ -0,0 +1,118 @@ +# Generated by Django 3.1 on 2020-12-01 20:49 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('media', '0002_auto_20201201_2054'), + ] + + operations = [ + migrations.RunSQL(""" + INSERT INTO media_luximage ( + id, + image, + title, + alt, + photo_credit_source, + photo_credit_url, + caption, + pub_date, + height, + width, + is_public, + exif_raw, + exif_aperture, + exif_make, + exif_model, + exif_exposure, + exif_iso, + exif_focal_length, + exif_lens, + exif_date, + point, + location, + sizes + ) + SELECT + id, + image, + title, + alt, + photo_credit_source, + photo_credit_url, + caption, + pub_date, + height, + width, + is_public, + exif_raw, + exif_aperture, + exif_make, + exif_model, + exif_exposure, + exif_iso, + exif_focal_length, + exif_lens, + exif_date, + point, + location, + sizes + FROM + photos_luximage; + """, reverse_sql=""" + INSERT INTO photos_luximage ( + id, + image, + title, + alt, + photo_credit_source, + photo_credit_url, + caption, + pub_date, + height, + width, + is_public, + sizes, + exif_raw, + exif_aperture, + exif_make, + exif_model, + exif_exposure, + exif_iso, + exif_focal_length, + exif_lens, + exif_date, + point, + location + ) + SELECT + id, + image, + title, + alt, + photo_credit_source, + photo_credit_url, + caption, + pub_date, + height, + width, + is_public, + sizes, + exif_raw, + exif_aperture, + exif_make, + exif_model, + exif_exposure, + exif_iso, + exif_focal_length, + exif_lens, + exif_date, + point, + location + FROM + media_luximage; + """) + ] diff --git a/app/media/__init__.py b/app/media/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/app/media/__init__.py diff --git a/app/media/admin.py b/app/media/admin.py new file mode 100644 index 0000000..3c5a86a --- /dev/null +++ b/app/media/admin.py @@ -0,0 +1,38 @@ +from django.contrib import admin +from django import forms +from django.contrib.gis.admin import OSMGeoAdmin +from django.conf.urls import url +from django.utils.translation import ungettext, ugettext_lazy as _ +from .models import LuxImage, LuxGallery, LuxImageSize, LuxVideo +from django.shortcuts import render +from django.contrib.admin import helpers +from django.http import HttpResponseRedirect + + +class LuxImageSizeAdmin(OSMGeoAdmin): + list_display = ('name', 'width', 'height', 'quality') + pass + + +admin.site.register(LuxImageSize, LuxImageSizeAdmin) + + +@admin.register(LuxVideo) +class LuxVideoAdmin(OSMGeoAdmin): + pass + +@admin.register(LuxImage) +class LuxImageAdmin(OSMGeoAdmin): + list_display = ('pk', 'admin_thumbnail', 'pub_date', 'caption') + list_filter = ('pub_date',) + search_fields = ['title', 'caption'] + # Options for OSM map Using custom ESRI topo map + + fieldsets = ( + (None, { + 'fields': ('title', ('image'), 'pub_date', 'sizes', 'alt', 'caption', ('is_public'), ('photo_credit_source', 'photo_credit_url')) + }), + ) + + class Media: + js = ('image-preview.js', 'next-prev-links.js') diff --git a/app/media/build.py b/app/media/build.py new file mode 100644 index 0000000..e95cbfc --- /dev/null +++ b/app/media/build.py @@ -0,0 +1,48 @@ +import os +from django.urls import reverse +from builder.base import BuildNew + +from .models import LuxImage + + +class BuildLuxPhotos(BuildNew): + + def build(self): + self.build_detail_view() + self.build_daily_photo() + + def get_model_queryset(self): + return self.model.objects.all() + + def build_daily_photo(self): + ''' + build out images that I post daily, found by title prefix daily_ + ''' + self.build_list_view( + base_path=reverse("photos:daily_photo_list"), + qs=LuxImage.objects.filter(is_public=True, title__startswith="daily_"), + paginate_by=10 + ) + + def build_detail_view(self): + ''' + write out all the expenses for each trip + ''' + for obj in self.get_model_queryset(): + url = obj.get_absolute_url() + path, slug = os.path.split(url) + path = '%s/' % path + # write html + response = self.client.get(url) + print(path, slug) + self.write_file(path, response.content, filename=slug) + + +def dailybuilder(): + j = BuildLuxPhotos("photos", "LuxImage") + j.build_daily_photo() + + +def builder(): + j = BuildLuxPhotos("photos", "LuxGallery") + j.build() diff --git a/app/media/migrations/0001_initial.py b/app/media/migrations/0001_initial.py new file mode 100644 index 0000000..4506248 --- /dev/null +++ b/app/media/migrations/0001_initial.py @@ -0,0 +1,129 @@ +# Generated by Django 3.1 on 2020-12-01 21:21 + +import datetime +import django.contrib.gis.db.models.fields +from django.db import migrations, models +import django.db.models.deletion +import media.models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('locations', '0028_auto_20200308_1152'), + ] + + operations = [ + 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)), + ('slug', models.SlugField(blank=True, 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', + 'ordering': ('-name', 'id'), + }, + ), + 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=media.models.get_vid_upload_path)), + ('video_webm', models.FileField(blank=True, null=True, upload_to=media.models.get_vid_upload_path)), + ('video_poster', models.FileField(blank=True, null=True, upload_to=media.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)), + ('point', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=4326)), + ('location', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='locations.location')), + ], + options={ + 'verbose_name': 'Video', + 'verbose_name_plural': 'Videos', + 'ordering': ('-pub_date', 'id'), + 'get_latest_by': 'pub_date', + }, + ), + 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=media.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)), + ('exif_raw', models.TextField(blank=True, null=True)), + ('exif_aperture', models.CharField(blank=True, max_length=50, null=True)), + ('exif_make', models.CharField(blank=True, max_length=50, null=True)), + ('exif_model', models.CharField(blank=True, max_length=50, null=True)), + ('exif_exposure', models.CharField(blank=True, max_length=50, null=True)), + ('exif_iso', models.CharField(blank=True, max_length=50, null=True)), + ('exif_focal_length', models.CharField(blank=True, max_length=50, null=True)), + ('exif_lens', models.CharField(blank=True, max_length=50, null=True)), + ('exif_date', models.DateTimeField(blank=True, null=True)), + ('height', models.CharField(blank=True, max_length=6, null=True)), + ('width', models.CharField(blank=True, max_length=6, null=True)), + ('point', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=4326)), + ('is_public', models.BooleanField(default=True)), + ('location', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='locations.location')), + ('sizes', models.ManyToManyField(blank=True, to='media.LuxImageSize')), + ], + options={ + 'verbose_name_plural': 'Images', + 'ordering': ('-pub_date', 'id'), + 'get_latest_by': 'pub_date', + }, + ), + migrations.CreateModel( + name='LuxGallery', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(blank=True, max_length=300)), + ('description', models.TextField(blank=True, null=True)), + ('slug', models.CharField(blank=True, max_length=300)), + ('pub_date', models.DateTimeField(null=True)), + ('is_public', models.BooleanField(default=True)), + ('caption_style', models.CharField(blank=True, max_length=400, null=True)), + ('images', models.ManyToManyField(to='media.LuxImage')), + ('thumb', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='gallery_thumb', to='media.luximage')), + ], + options={ + 'verbose_name_plural': 'Galleries', + 'ordering': ('-pub_date', 'id'), + 'get_latest_by': 'pub_date', + }, + ), + migrations.CreateModel( + name='LuxAudio', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=200)), + ('subtitle', models.CharField(blank=True, max_length=200)), + ('slug', models.SlugField(blank=True, unique_for_date='pub_date')), + ('body_html', models.TextField(blank=True)), + ('body_markdown', models.TextField(blank=True)), + ('pub_date', models.DateTimeField(default=datetime.datetime.now)), + ('mp3', models.FileField(blank=True, null=True, upload_to=media.models.get_audio_upload_path)), + ('ogg', models.FileField(blank=True, null=True, upload_to=media.models.get_audio_upload_path)), + ('point', django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=4326)), + ('location', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='locations.location')), + ], + options={ + 'verbose_name': 'Audio', + 'verbose_name_plural': 'Audio', + 'ordering': ('-pub_date',), + 'get_latest_by': 'pub_date', + }, + ), + ] diff --git a/app/media/migrations/0002_auto_20201201_2122.py b/app/media/migrations/0002_auto_20201201_2122.py new file mode 100644 index 0000000..f9021b6 --- /dev/null +++ b/app/media/migrations/0002_auto_20201201_2122.py @@ -0,0 +1,46 @@ +# Generated by Django 3.1 on 2020-12-01 21:22 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('media', '0001_initial'), + ] + + operations = [ + migrations.RunSQL(""" + INSERT INTO media_luximagesize ( + id, + name, + width, + height, + quality + ) + SELECT + id, + name, + width, + height, + quality + FROM + photos_luximagesize; + """, reverse_sql=""" + INSERT INTO photos_luximagesize ( + id, + name, + width, + height, + quality + ) + SELECT + id, + name, + width, + height, + quality + FROM + media_luximagesize; + """) + ] diff --git a/app/media/migrations/0003_auto_20201201_2125.py b/app/media/migrations/0003_auto_20201201_2125.py new file mode 100644 index 0000000..498232a --- /dev/null +++ b/app/media/migrations/0003_auto_20201201_2125.py @@ -0,0 +1,65 @@ +# Generated by Django 3.1 on 2020-12-01 21:25 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('media', '0002_auto_20201201_2122'), + ] + + operations = [ + migrations.RunSQL(""" + INSERT INTO media_luximage ( + id, + image, + title, + alt, + photo_credit_source, + photo_credit_url, + caption, + pub_date, + exif_raw, + exif_aperture, + exif_make, + exif_model, + exif_exposure, + exif_iso, + exif_focal_length, + exif_lens, + exif_date, + height, + width, + point, + location_id, + is_public + ) + SELECT + id, + image, + title, + alt, + photo_credit_source, + photo_credit_url, + caption, + pub_date, + exif_raw, + exif_aperture, + exif_make, + exif_model, + exif_exposure, + exif_iso, + exif_focal_length, + exif_lens, + exif_date, + height, + width, + point, + location_id, + is_public + FROM + photos_luximage; + """), + ] + diff --git a/app/media/migrations/0004_auto_20201201_2139.py b/app/media/migrations/0004_auto_20201201_2139.py new file mode 100644 index 0000000..4847ece --- /dev/null +++ b/app/media/migrations/0004_auto_20201201_2139.py @@ -0,0 +1,40 @@ +# Generated by Django 3.1 on 2020-12-01 21:39 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('media', '0003_auto_20201201_2125'), + ] + + operations = [ + migrations.RunSQL(""" + INSERT INTO media_luximage_sizes ( + id, + luximage_id, + luximagesize_id + ) + SELECT + id, + luximage_id, + luximagesize_id + FROM + photos_luximage_sizes; + """, reverse_sql=""" + INSERT INTO photos_luximage_sizes ( + id, + luximage_id, + luximagesize_id + ) + SELECT + id, + luximage_id, + luximagesize_id + FROM + media_luximage_sizes; + """) + ] + + diff --git a/app/media/migrations/0005_auto_20201201_2141.py b/app/media/migrations/0005_auto_20201201_2141.py new file mode 100644 index 0000000..e211ea2 --- /dev/null +++ b/app/media/migrations/0005_auto_20201201_2141.py @@ -0,0 +1,56 @@ +# Generated by Django 3.1 on 2020-12-01 21:41 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('media', '0004_auto_20201201_2139'), + ] + + operations = [ + migrations.RunSQL(""" + INSERT INTO media_luxvideo( + id, + video_mp4, + video_webm, + video_poster, + title, + pub_date, + youtube_url, + vimeo_url + ) + SELECT + id, + video_mp4, + video_webm, + video_poster, + title, + pub_date, + youtube_url, + vimeo_url + FROM + photos_luxvideo; + """, reverse_sql=""" + INSERT INTO photos_luxvideos( + video_mp4, + video_webm, + video_poster, + title, + pub_date, + youtube_url, + vimeo_url + ) + SELECT + video_mp4, + video_webm, + video_poster, + title, + pub_date, + youtube_url, + vimeo_url + FROM + media_luxvideo; + """) + ] diff --git a/app/media/migrations/0006_auto_20201201_2145.py b/app/media/migrations/0006_auto_20201201_2145.py new file mode 100644 index 0000000..9827416 --- /dev/null +++ b/app/media/migrations/0006_auto_20201201_2145.py @@ -0,0 +1,70 @@ +# Generated by Django 3.1 on 2020-12-01 21:45 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('media', '0005_auto_20201201_2141'), + ] + + operations = [ + migrations.RunSQL(""" + INSERT INTO media_luxaudio ( + id, + title, + subtitle, + slug, + body_html, + body_markdown, + pub_date, + mp3, + ogg, + point, + location_id + ) + SELECT + id, + title, + subtitle, + slug, + body_html, + body_markdown, + pub_date, + mp3, + ogg, + point, + location_id + FROM + recordings_audio; + """, reverse_sql=""" + INSERT INTO recordings_audio ( + id, + title, + subtitle, + slug, + body_html, + body_markdown, + pub_date, + mp3, + ogg, + point, + location_id + ) + SELECT + id, + title, + subtitle, + slug, + body_html, + body_markdown, + pub_date, + mp3, + ogg, + point, + location_id + FROM + media_luxaudio; + """) + ] diff --git a/app/media/migrations/0007_luximage_sizes_cache.py b/app/media/migrations/0007_luximage_sizes_cache.py new file mode 100644 index 0000000..944600f --- /dev/null +++ b/app/media/migrations/0007_luximage_sizes_cache.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1 on 2020-12-02 08:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('media', '0006_auto_20201201_2145'), + ] + + operations = [ + migrations.AddField( + model_name='luximage', + name='sizes_cache', + field=models.CharField(blank=True, max_length=300, null=True), + ), + ] diff --git a/app/media/migrations/0008_auto_20201202_1155.py b/app/media/migrations/0008_auto_20201202_1155.py new file mode 100644 index 0000000..0e807d5 --- /dev/null +++ b/app/media/migrations/0008_auto_20201202_1155.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1 on 2020-12-02 11:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('media', '0007_luximage_sizes_cache'), + ] + + operations = [ + migrations.AlterField( + model_name='luximage', + name='sizes', + field=models.ManyToManyField(blank=True, related_name='sizes', to='media.LuxImageSize'), + ), + ] diff --git a/app/media/migrations/__init__.py b/app/media/migrations/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/app/media/migrations/__init__.py diff --git a/app/media/models.py b/app/media/models.py new file mode 100644 index 0000000..fb44c9e --- /dev/null +++ b/app/media/models.py @@ -0,0 +1,382 @@ +import os.path +import io +import datetime +from pathlib import Path +from PIL import Image + +from django.core.exceptions import ValidationError +from django.contrib.gis.db import models +from django.contrib.sitemaps import Sitemap +from django.db.models.signals import post_save +from django.dispatch import receiver +from django.db.models.signals import m2m_changed +from django.utils.encoding import force_text +from django.utils.functional import cached_property +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 taggit.managers import TaggableManager + +from .utils import resize_image +from locations.models import Location + + +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) + + +def get_audio_upload_path(self, filename): + return "audio/%s/%s" % (datetime.datetime.today().strftime("%Y"), filename) + + +class LuxImageSize(models.Model): + name = models.CharField(null=True, blank=True, max_length=30) + slug = models.SlugField(null=True, blank=True) + width = models.IntegerField(null=True, blank=True) + height = models.IntegerField(null=True, blank=True) + quality = models.IntegerField() + + class Meta: + ordering = ('-name', 'id') + 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) + 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("locations.Location", on_delete=models.CASCADE, null=True, blank=True) + is_public = models.BooleanField(default=True) + sizes = models.ManyToManyField(LuxImageSize, blank=True, related_name='sizes') + sizes_cache = models.CharField(null=True, blank=True, max_length=300) + + 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_url_by_size(size.name) + + 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]) + + @cached_property + def image_name(self): + return os.path.basename(self.image.path)[:-4] + + @cached_property + def image_ext(self): + return self.image.url[-3:] + + @cached_property + def get_image_filename(self): + return os.path.basename(self.image.path) + + @cached_property + def get_srcset(self): + srcset = "" + length = len(self.sizes.all()) + print(length) + loopnum = 1 + for size in self.sizes.all(): + srcset += "%s%s/%s_%s.%s %sw" % (settings.IMAGES_URL, self.pub_date.strftime("%Y"), self.image_name, size.name, self.image_ext, size.width) + if loopnum < length: + srcset += ", " + loopnum = loopnum+1 + return srcset + + @cached_property + def get_src(self): + src = "" + if self.sizes.all().count() > 1: + src += "%s%s/%s_%s.%s" % (settings.IMAGES_URL, self.pub_date.strftime("%Y"), self.image_name, 'picwide-med', self.image_ext) + else: + src += "%s%s/%s_%s.%s" % (settings.IMAGES_URL, self.pub_date.strftime("%Y"), self.image_name, [size.name for size in self.sizes.all()], self.image_ext) + return src + + def get_image_url_by_size(self, size="original"): + if size == "original": + return self.image.url + else: + luximagesize = LuxImageSize.objects.get(name=size) + if luximagesize not in self.get_sizes: + print("new size is "+luximagesize.name) + self.sizes.add(luximagesize) + return "%s%s/%s_%s.%s" % (settings.IMAGES_URL, self.pub_date.strftime("%Y"), self.image_name, luximagesize.slug, self.image_ext) + + def get_image_path_by_size(self, size="original"): + if size == "original": + return self.image.path + else: + luximagesize = LuxImageSize.objects.get(name=size) + return "%s%s/%s_%s.%s" % (settings.IMAGES_ROOT, self.pub_date.strftime("%Y"), self.image_name, luximagesize.slug, self.image_ext) + + def get_thumbnail_url(self): + return self.get_image_url_by_size("tn") + + def admin_thumbnail(self): + return format_html('<a href="%s"><img src="%s"></a>' % (self.get_image_url_by_size(), self.get_image_url_by_size("admin-thumbnail"))) + admin_thumbnail.short_description = 'Thumbnail' + + @cached_property + def get_sizes(self): + return self.sizes_cache.split(",") + + @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): + created = self.pk is None + if created: + self.sizes.add(LuxImageSize.objects.get(name="admin-thumbnail")) + img = Image.open(self.image.path) + self.height = img.height + self.width = img.width + self.sizes_cache = ",".join(s.slug for s in self.sizes.all()) + super(LuxImage, self).save() + + +class LuxGallery(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.ForeignKey(LuxImage, on_delete=models.CASCADE, related_name="gallery_thumb", null=True, blank=True) + images = models.ManyToManyField(LuxImage) + pub_date = models.DateTimeField(null=True) + is_public = models.BooleanField(default=True) + caption_style = models.CharField(blank=True, null=True, max_length=400) + + 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): + if self.is_public: + return "/photos/galleries/%s" % (self.slug) + else: + return "/photos/galleries/private/%s" % (self.slug) + + def thumbs(self): + lst = [x.image.name for x in self.images.all()] + lst = ["<a href='/media/%s'>%s</a>" % (x, x.split('/')[-1]) for x in lst] + return ', '.join(item for item in lst if item) + thumbs.allow_tags = True + + +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) + point = models.PointField(blank=True, null=True) + location = models.ForeignKey(Location, on_delete=models.CASCADE, blank=True, null=True) + + 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' + verbose_name = 'Video' + get_latest_by = 'pub_date' + + def save(self, *args, **kwargs): + if not self.point: + self.point = CheckIn.objects.latest().point + try: + self.location = Location.objects.filter(geometry__contains=self.point).get() + except Location.DoesNotExist: + raise forms.ValidationError("There is no location associated with that point, add it: %sadmin/locations/location/add/" % (settings.BASE_URL)) + super(LuxVideo, self).save(*args, **kwargs) + + +class LuxAudio(models.Model): + title = models.CharField(max_length=200) + subtitle = models.CharField(max_length=200, blank=True) + slug = models.SlugField(unique_for_date='pub_date', blank=True) + body_html = models.TextField(blank=True) + body_markdown = models.TextField(blank=True) + pub_date = models.DateTimeField(default=datetime.datetime.now) + mp3 = models.FileField(blank=True, null=True, upload_to=get_audio_upload_path) + ogg = models.FileField(blank=True, null=True, upload_to=get_audio_upload_path) + point = models.PointField(blank=True, null=True) + location = models.ForeignKey(Location, on_delete=models.CASCADE, blank=True, null=True) + + class Meta: + ordering = ('-pub_date',) + verbose_name_plural = 'Audio' + verbose_name = 'Audio' + get_latest_by = 'pub_date' + + def __str__(self): + return self.title + + def get_absolute_url(self): + return reverse("prompt:detail", kwargs={"slug": self.slug}) + + @property + def get_previous_published(self): + return self.get_previous_by_pub_date(status__exact=1) + + @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_published(self): + return self.get_next_by_pub_date(status__exact=1) + + @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 '' + + def get_type(self): + return str(self.__class__.__name__) + + def save(self, *args, **kwargs): + md = render_images(self.body_markdown) + self.body_html = markdown_to_html(md) + if not self.point: + self.point = CheckIn.objects.latest().point + try: + self.location = Location.objects.filter(geometry__contains=self.point).get() + except Location.DoesNotExist: + raise forms.ValidationError("There is no location associated with that point, add it: %sadmin/locations/location/add/" % (settings.BASE_URL)) + super(LuxAudio, self).save(*args, **kwargs) + + + +@receiver(m2m_changed, sender=LuxImage.sizes.through) +def update_photo_sizes(sender, instance, **kwargs): + # update the local cache of sizes + instance.sizes_cache = ",".join(s.slug for s in instance.sizes.all()) + instance.save() + for size in instance.sizes.all(): + # check each size and see if there's an image there already + my_file = Path(instance.get_image_path_by_size(size.name)) + if not my_file.is_file(): + print("creating new image file") + #file doesn't exist, so create it + if size.width: + img = Image.open(instance.image.path) + try: + if size.width <= img.width: + resize_image(img, size.width, None, size.quality, instance.get_image_path_by_size(size.name)) + 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, instance.get_image_path_by_size(size.name)) + 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) + else: + # file exists, might add something here to force it to do the above when I want + print("file %s exists" % size) + pass + + diff --git a/app/media/photos.js b/app/media/photos.js new file mode 100644 index 0000000..b93467a --- /dev/null +++ b/app/media/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/media/resize.py b/app/media/resize.py new file mode 100644 index 0000000..13c0151 --- /dev/null +++ b/app/media/resize.py @@ -0,0 +1,53 @@ +import os +import io + +from django.conf import settings + +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 + + +def make_local_copies(self,photo): + orig_dir = settings.IMAGES_ROOT + '/flickr/full/' + photo.pub_date.strftime("%Y") + if not os.path.isdir(orig_dir): + os.makedirs(orig_dir) + im = io.StringIO(fname.read().decode('UTF-8')) # constructs a StringIO holding the image + img = Image.open(im) + local_full = '%s/%s.jpg' % (orig_dir, photo.flickr_id) + img.save(local_full) + + #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 as e: + if os.path.isfile(resized_filename): + os.unlink(resized_filename) + raise e + os.unlink(img) diff --git a/app/media/retriever.py b/app/media/retriever.py new file mode 100644 index 0000000..f5cae68 --- /dev/null +++ b/app/media/retriever.py @@ -0,0 +1,323 @@ +import json +import datetime +import os +import io +import urllib.request +import urllib.parse +import urllib.error + +from django.template.defaultfilters import slugify +from django.core.exceptions import ObjectDoesNotExist +from django.utils.encoding import force_text +from django.conf import settings + +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 +import flickrapi + +# 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 + +EXIF_PARAMS = { + "FNumber": 'f/2.8', + "Make": 'Apple', + "Model": 'iPhone', + "ExposureTime": '', + "ISO": '', + "FocalLength": '', + "LensModel": '', + 'DateTimeOriginal': '2013:09:03 22:44:25' +} + +class SyncFlickr(): + + def __init__(self): + self.flickr = flickrapi.FlickrAPI(settings.FLICKR_API_KEY, settings.FLICKR_API_SECRET,format='parsed-json') + + + def sync_sets(self, *args, **kwargs): + p = self.flickr.photosets.getList(user_id='85322932@N00') + disregard = [ + 'POTD 2008', + 'Snow Day', + 'Wedding', + 'Some random stuff', + 'Lilah & Olivia', + '6 months+', + '6-9 months', + '9-18 months', + ] + for photoset in p['photosets']['photoset']: + if photoset['title']['_content'] 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? + self.get_photos_in_set(photoset['id'],row) + except ObjectDoesNotExist: + s = PhotoGallery.objects.get_or_create( + set_id=force_text(photoset['id']), + set_title=force_text(photoset['title']['_content']), + set_desc=force_text(photoset['description']['_content']), + set_slug=slugify(force_text(photoset['title']['_content'])[:40]), + primary=force_text(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(self, flickr_id, photoset): + photos = self.flickr.photosets.getPhotos(photoset_id=flickr_id) + for photo in photos['photoset']['photo']: + try: + p = Photo.objects.get(flickr_id__exact=str(photo['id'])) + except ObjectDoesNotExist: + p = self.get_photo(photo['id']) + if p.is_public: + pass #photoset.photos.add(p) + #slideshow_image(p, 1000, 800, 95) + print(p) + + def get_photo(self, photo_id): + photo = self.flickr.photos.getInfo(photo_id=photo_id) + info = photo['photo'] + try: + geo = self.flickr.photos.geo.getLocation(photo_id=photo_id) + location, region = self.get_geo(float(geo['photo']['location']['latitude']), float(geo['photo']['location']['longitude'])) + except KeyError: + print("no effing geodata asshat") + exif = self.exif_handler(self.flickr.photos.getExif(photo_id=photo_id)['photo']['exif']) + p, created = Photo.objects.get_or_create( + title=info['title']['_content'], + flickr_id=info['id'], + flickr_owner=info['owner']['nsid'], + flickr_server=info['server'], + flickr_secret=info['secret'], + flickr_originalsecret=info['originalsecret'], + flickr_farm=info['farm'], + pub_date=self.flickr_datetime_to_datetime(exif["DateTimeOriginal"].replace(':', '-', 2)), + description=info['description']['_content'], + 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=self.flickr_datetime_to_datetime(exif["DateTimeOriginal"].replace(':', '-', 2)), + lat=float(geo['photo']['location']['latitude']), + lon=float(geo['photo']['location']['longitude']), + region=region, + location=location, + ) + if created: + for tag in info['tags']['tag']: + p.tags.add(tag['raw']) + p.save() + + local = FlickrImage() + local.make_local_copies(p) + #retina image: + #slideshow_image(p, 2000, 1600, 75) + #normal image + print("grabbing... "+p.title) + return p + + + def sync_flickr_photos(self, *args, **kwargs): + photos = self.flickr.people.getPhotos(user_id="85322932@N00", extras="date_upload,date_taken,geo") + for photo in photos['photos']['photo']: + try: + row = Photo.objects.get(flickr_id=photo['id'], flickr_secret=photo['secret']) + print('already have ' + photo['id'] + ' moving on') + except ObjectDoesNotExist: + p = self.get_photo(photo['id']) + + + + """ + ################################################ + ## Various meta data and geo helper functions ## + ################################################ + """ + + def exif_handler(self, data): + converted = {} + try: + for t in data: + converted[t['tag']] = t['raw']['_content'] + except: + pass + for k, v in list(EXIF_PARAMS.items()): + if k not in converted: + converted[k] = v + return converted + + + def flickr_datetime_to_datetime(self, 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(self, 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 + + + + + + +class FlickrImage(): + """ + ## Photo retrieval functions to pull down images from Flickr servers ## + """ + + def slideshow_image(self, 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.request.urlopen(flickr_photo) + im = io.StringIO(fname.read().decode('UTF-8')) # 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(self,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.request.urlopen(full) + im = io.StringIO(fname.read().decode('UTF-8')) # 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.request.urlopen(large) + im = io.StringIO(fname.read().decode('UTF-8')) # 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.request.urlopen(med) + im = io.StringIO(fname.read().decode('UTF-8')) # 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(self, 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.request.urlopen(remote) + im = io.StringIO(fname.read().decode('UTF-8')) # 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 as e: + if os.path.isfile(resized_filename): + os.unlink(resized_filename) + raise e + os.unlink(img) diff --git a/app/media/retriever.py.bak b/app/media/retriever.py.bak new file mode 100644 index 0000000..d3c572a --- /dev/null +++ b/app/media/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/media/static/image-preview.js b/app/media/static/image-preview.js new file mode 100644 index 0000000..b8fead5 --- /dev/null +++ b/app/media/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/media/static/my_styles.css b/app/media/static/my_styles.css new file mode 100644 index 0000000..d13c8e4 --- /dev/null +++ b/app/media/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/media/templatetags/__init__.py b/app/media/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/app/media/templatetags/__init__.py diff --git a/app/media/templatetags/get_image_by_size.py b/app/media/templatetags/get_image_by_size.py new file mode 100644 index 0000000..a0a62f0 --- /dev/null +++ b/app/media/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_url_by_size") + return method(*args) diff --git a/app/media/templatetags/get_image_width.py b/app/media/templatetags/get_image_width.py new file mode 100644 index 0000000..ac39184 --- /dev/null +++ b/app/media/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/media/templatetags/get_size_by_name.py b/app/media/templatetags/get_size_by_name.py new file mode 100644 index 0000000..fc64a61 --- /dev/null +++ b/app/media/templatetags/get_size_by_name.py @@ -0,0 +1,8 @@ +from django import template + +register = template.Library() + +@register.simple_tag +def get_size_by_name(obj, *args): + method = getattr(obj, "get_size_by_name") + return method(*args) diff --git a/app/media/urls.py b/app/media/urls.py new file mode 100644 index 0000000..6673135 --- /dev/null +++ b/app/media/urls.py @@ -0,0 +1,74 @@ +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 + ), + re_path( + r'data/admin/preview/(?P<pk>\d+)/$', + views.photo_preview_json, + name="admin_image_preview" + ), + re_path( + r'data/admin/tn/(?P<pk>\d+)/$', + views.thumb_preview_json, + name="admin_thumb_preview" + ), + re_path( + r'galleries/private/(?P<slug>[-\w]+)$', + views.PrivateGallery.as_view(), + name="private" + ), + re_path( + r'galleries/private/(?P<page>\d+)/$', + views.PrivateGalleryList.as_view(), + name="private_list" + ), + re_path( + r'galleries/private/$', + RedirectView.as_view(url="/photos/galleries/private/1/", permanent=False) + ), + 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" + ), + re_path( + r'galleries/$', + RedirectView.as_view(url="/photos/galleries/1/", permanent=False) + ), + re_path( + r'(?P<page>\d+)/$', + views.gallery_list, + ), + re_path( + r'(?P<slug>[-\w]+)/$', + 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/media/utils.py b/app/media/utils.py new file mode 100644 index 0000000..893663c --- /dev/null +++ b/app/media/utils.py @@ -0,0 +1,26 @@ +import os +import subprocess + +from PIL import ImageFile +# pip install python-resize-image +from resizeimage import resizeimage + + +def resize_image(img, width=None, height=None, quality=72, filepath=""): + """ + given an image object, size, and filepath + resize the image, then save it , size, and filepath + resize the image, then save it at the filepath + """ + base_path = os.path.dirname(filepath) + if not os.path.isdir(base_path): + os.makedirs(base_path) + 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) + ImageFile.MAXBLOCK = img.size[0] * img.size[1] * 4 + newimg.save(filepath, newimg.format, quality=quality) + subprocess.call(["jpegoptim", "%s" % filepath]) diff --git a/app/media/views.py b/app/media/views.py new file mode 100644 index 0000000..fdd3c22 --- /dev/null +++ b/app/media/views.py @@ -0,0 +1,130 @@ +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 PrivateGallery(DetailView): + model = LuxGallery + slug_field = "slug" + template_name = "details/photo_gallery.html" + + +class PrivateGalleryList(PaginatedListView): + template_name = 'archives/gallery_list.html' + + def get_queryset(self): + return LuxGallery.objects.filter(is_public=False) + + def get_context_data(self, **kwargs): + # Call the base implementation first to get a context + context = super(PrivateGalleryList, self).get_context_data(**kwargs) + context['is_private'] = True + return context + + +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 OldGalleryList(PaginatedListView): + template_name = 'archives/gallery_list.html' + model = PhotoGallery + + def get_queryset(self): + return PhotoGallery.objects.filter(is_public=True) + + def get_context_data(self, **kwargs): + # Call the base implementation first to get a context + context = super(OldGalleryList, self).get_context_data(**kwargs) + 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 gallery_list(request, page): + request.page_url = '/photos/%d/' + request.page = int(page) + context = { + 'object_list': PhotoGallery.objects.all(), + 'page': page, + } + return render(request, "archives/photos.html", context) + + +def gallery(request, slug): + context = { + 'object': PhotoGallery.objects.get(set_slug=slug) + } + return render_to_response('details/photo_galleries.html', context, context_instance=RequestContext(request)) + + +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_image_url_by_size('admin_insert') + data = json.dumps(data) + return HttpResponse(data) + + +def gallery_list_by_area(request, slug, page): + """Grabs entries by region or country""" + request.page_url = '/photos/' + slug + '/%d/' + request.page = int(page) + try: + region = Region.objects.get(slug__exact=slug) + qs = PhotoGallery.objects.filter(region=region).order_by('-id') + except: + region = Country.objects.get(slug__exact=slug) + qs = PhotoGallery.objects.filter(location__state__country=region).order_by('-id') + if not region: + raise Http404 + context = { + 'object_list': qs, + 'country_list': Country.objects.filter(visited=True), + 'region_list': Region.objects.all(), + 'region': region, + 'page': page + } + return render_to_response("archives/photos.html", context, context_instance=RequestContext(request)) diff --git a/app/photos/migrations/0020_auto_20201201_2116.py b/app/photos/migrations/0020_auto_20201201_2116.py new file mode 100644 index 0000000..39d4fd9 --- /dev/null +++ b/app/photos/migrations/0020_auto_20201201_2116.py @@ -0,0 +1,20 @@ +# Generated by Django 3.1 on 2020-12-01 21:16 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('locations', '0028_auto_20200308_1152'), + ('photos', '0019_auto_20190704_0903'), + ] + + operations = [ + migrations.AlterField( + model_name='luximage', + name='location', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='locations.location'), + ), + ] diff --git a/app/photos/migrations/0021_auto_20201201_2118.py b/app/photos/migrations/0021_auto_20201201_2118.py new file mode 100644 index 0000000..25e4f8f --- /dev/null +++ b/app/photos/migrations/0021_auto_20201201_2118.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1 on 2020-12-01 21:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('photos', '0020_auto_20201201_2116'), + ] + + operations = [ + migrations.AlterField( + model_name='luximage', + name='sizes', + field=models.ManyToManyField(blank=True, related_name='_luximage_sizes_+', to='photos.LuxImageSize'), + ), + ] diff --git a/app/photos/models.py b/app/photos/models.py index 6253336..24a218f 100644 --- a/app/photos/models.py +++ b/app/photos/models.py @@ -70,9 +70,9 @@ class LuxImage(models.Model): 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("locations.Location", on_delete=models.CASCADE, null=True, blank=True) + location = models.ForeignKey("locations.Location", on_delete=models.CASCADE, null=True, blank=True, related_name="+") is_public = models.BooleanField(default=True) - sizes = models.ManyToManyField(LuxImageSize, blank=True) + sizes = models.ManyToManyField(LuxImageSize, blank=True, related_name="+") flickr_id = models.CharField(null=True, blank=True, max_length=80) twitter_link = models.CharField(null=True, blank=True, max_length=300) facebook_link = models.CharField(null=True, blank=True, max_length=300) @@ -110,6 +110,9 @@ class LuxImage(models.Model): def get_image_name(self): return self.image.url.split("original/")[1][5:-4] + def get_image_name_new(self): + return os.path.basename(self.image.path)[:-4] + def get_image_ext(self): return self.image.url[-3:] |