diff options
-rw-r--r-- | app/TODO | 19 | ||||
-rw-r--r-- | app/photos/admin.py | 5 | ||||
-rw-r--r-- | app/photos/forms.py | 28 | ||||
-rw-r--r-- | app/photos/migrations/0002_auto_20151216_1958.py | 26 | ||||
-rw-r--r-- | app/photos/migrations/0003_luxgallery_caption_style.py | 20 | ||||
-rw-r--r-- | app/photos/models.py | 22 | ||||
-rw-r--r-- | app/photos/urls.py | 9 | ||||
-rw-r--r-- | app/photos/views.py | 13 | ||||
-rw-r--r-- | design/sass/_inbox.scss | 55 | ||||
-rw-r--r-- | design/templates/admin/photos/luxgallery/change_form.html | 134 | ||||
-rw-r--r-- | design/templates/archives/gallery_list.html | 57 | ||||
-rw-r--r-- | design/templates/details/photo_gallery.html | 2 |
12 files changed, 370 insertions, 20 deletions
@@ -36,24 +36,17 @@ iron out templates and push first piece live. # photos: -Okay we're uploading/creating galleries/adding images +Add map and location picker to upload form -Then resize all images for responsive gallery. +handle portrait orientation in resizing -Then play with gallery templates and add a template selecter to form. +finish builder -like this full width design: http://www.photobyrichard.com/photobyrichard/page/2/ - -Next step is to create private galleries and password protect them on the server at a common url. Flow will be something like: +Then play with gallery templates -galleries = PhotoGallery.object.filter(private=True) -for gal in galleries: - create at url like /photos/private/gallery-list and - /photos/private/gallery-name - -Then just require a password to get to anything under /photos/private/ +like this full width design: http://www.photobyrichard.com/photobyrichard/page/2/ -Then change the default PhotoGallery manager to filter out anything marked private. Or make sure to write all those queries with .filter(private=False) +port existing galleries to new structure --- diff --git a/app/photos/admin.py b/app/photos/admin.py index d8ad853..659dcb6 100644 --- a/app/photos/admin.py +++ b/app/photos/admin.py @@ -1,4 +1,5 @@ from django.contrib import admin +from django import forms from django.contrib.gis.admin import OSMGeoAdmin from django.conf.urls import patterns from django.conf.urls import url @@ -9,10 +10,11 @@ from django.contrib.admin import helpers from django.http import HttpResponseRedirect -from .forms import UploadZipForm +from .forms import UploadZipForm, GalleryForm class LuxImageAdmin(OSMGeoAdmin): + list_display = ('pk', 'admin_thumbnail', 'pub_date',) list_filter = ('pub_date',) search_fields = ['title', 'caption'] @@ -31,6 +33,7 @@ admin.site.register(LuxImage, LuxImageAdmin) class LuxGalleryAdmin(OSMGeoAdmin): + form = GalleryForm list_display = ('title', 'location', 'pub_date') list_filter = ('location',) diff --git a/app/photos/forms.py b/app/photos/forms.py index f12ce42..08927cd 100644 --- a/app/photos/forms.py +++ b/app/photos/forms.py @@ -13,6 +13,7 @@ except ImportError: from django import forms from django.utils.translation import ugettext_lazy as _ +from django.utils.safestring import mark_safe from django.contrib import messages from django.core.files.base import ContentFile from django.contrib.admin import widgets @@ -28,6 +29,26 @@ from .utils import resize_image logger = logging.getLogger('photos.forms') +class GalleryForm(forms.ModelForm): + class Meta: + model = LuxGallery + fields = '__all__' + widgets = { + 'images': forms.SelectMultiple, + } + + def __init__(self, *args, **kwargs): + super(GalleryForm, self).__init__(*args, **kwargs) + + images = LuxImage.objects.all()[:100] + items = [] + for image in images: + items.append( + (image.pk, mark_safe('%s' % image.get_image_name())) + ) + self.fields['images'].choices = items + self.fields['images'].allow_tags = True + class UploadZipForm(forms.Form): """ Handles the uploading of a gallery of photos packed in a .zip file @@ -129,7 +150,7 @@ class UploadZipForm(forms.Form): contentfile = ContentFile(data) image.image.save(filename, contentfile) image.save() - gallery.image.add(image) + gallery.images.add(image) with exiftool.ExifTool() as et: meta = et.get_metadata(image.image.path) @@ -146,7 +167,10 @@ class UploadZipForm(forms.Form): try: image.exif_lens = meta["MakerNotes:LensType"] except: - pass + try: + image.exif_lens = meta["XMP:Lens"] + except: + pass try: image.point = Point(meta["XMP:GPSLongitude"], meta["XMP:GPSLatitude"], srid=4326) try: diff --git a/app/photos/migrations/0002_auto_20151216_1958.py b/app/photos/migrations/0002_auto_20151216_1958.py new file mode 100644 index 0000000..2a2bffb --- /dev/null +++ b/app/photos/migrations/0002_auto_20151216_1958.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9 on 2015-12-16 19:58 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('photos', '0001_initial'), + ] + + operations = [ + migrations.RenameField( + model_name='luxgallery', + old_name='image', + new_name='images', + ), + migrations.AlterField( + model_name='luxgallery', + name='thumb', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='gallery_thumb', to='photos.LuxImage'), + ), + ] diff --git a/app/photos/migrations/0003_luxgallery_caption_style.py b/app/photos/migrations/0003_luxgallery_caption_style.py new file mode 100644 index 0000000..b46f970 --- /dev/null +++ b/app/photos/migrations/0003_luxgallery_caption_style.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9 on 2015-12-17 11:01 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('photos', '0002_auto_20151216_1958'), + ] + + operations = [ + migrations.AddField( + model_name='luxgallery', + name='caption_style', + field=models.CharField(blank=True, max_length=400, null=True), + ), + ] diff --git a/app/photos/models.py b/app/photos/models.py index c2d77e4..50ae43b 100644 --- a/app/photos/models.py +++ b/app/photos/models.py @@ -43,7 +43,10 @@ class LuxImage(models.Model): get_latest_by = 'pub_date' def __str__(self): - return "%s" % self.pk + if self.title: + return "%s" % self.title + else: + return "%s" % self.pk def get_image_name(self): return self.image.url.split("galleries/original/")[1] @@ -80,12 +83,13 @@ 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.CharField(blank=True, max_length=300) - image = models.ManyToManyField(LuxImage) + thumb = models.ForeignKey(LuxImage, related_name="gallery_thumb", null=True, blank=True) + images = models.ManyToManyField(LuxImage) pub_date = models.DateTimeField(null=True) point = models.PointField(null=True, blank=True) location = models.ForeignKey(Location, null=True, blank=True) is_public = models.BooleanField(default=True) + caption_style = models.CharField(blank=True, null=True, max_length=400) class Meta: ordering = ('-pub_date', 'id') @@ -104,6 +108,18 @@ class LuxGallery(models.Model): else: return "/photos/galleries/private/%s" % (self.slug) + def latitude(self): + return self.point.y + + def longitude(self): + return self.point.x + + 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 Photo(models.Model): description = models.TextField(blank=True, null=True) diff --git a/app/photos/urls.py b/app/photos/urls.py index 0c7b752..67375da 100644 --- a/app/photos/urls.py +++ b/app/photos/urls.py @@ -16,6 +16,15 @@ urlpatterns = [ name="private" ), url( + r'galleries/private/(?P<page>\d+)/$', + views.PrivateGalleryList.as_view(), + name="private_list" + ), + url( + r'galleries/private/$', + RedirectView.as_view(url="/photos/galleries/private/1/", permanent=False) + ), + url( r'galleries/(?P<slug>[-\w]+)/$', views.gallery, ), diff --git a/app/photos/views.py b/app/photos/views.py index 970bbd9..4b89e4c 100644 --- a/app/photos/views.py +++ b/app/photos/views.py @@ -10,12 +10,25 @@ 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 + def gallery_list(request, page): request.page_url = '/photos/%d/' request.page = int(page) diff --git a/design/sass/_inbox.scss b/design/sass/_inbox.scss index 7f44753..f74fc31 100644 --- a/design/sass/_inbox.scss +++ b/design/sass/_inbox.scss @@ -13,3 +13,58 @@ margin-top: 3em; @include constrain_wide; } + +.photo-gallery { + margin: 0 auto; + margin-top: 1.5em; + position: relative; + width: 100%; /* for IE 6 */ + img { + width: 100%; + max-width: 100%; + padding: 0; + } + h4 { + font-weight: 500; + margin: .5em 0; + letter-spacing: 1px; + font-size: 2em; + } + time { display: inline; } + a { color: inherit;} + @include breakpoint(alpha) { + div { + position: absolute; + bottom: 28%; + left: .35em; + text-align: left; + } + time { + margin-left: .5em; + } + h4 { + font-size: 2.75em; + margin-bottom: 0; + } + p { + line-height: 1.3; + max-width: 80%; + margin-left: .25em; + @include fontsize(18); + } + } + @include breakpoint(beta) { + div { + bottom: 22%; + } + } + @include breakpoint(gamma) { + div { + bottom: 20%; + } + p { + max-width: 40%; + } + @include constrain_wide; + } +} diff --git a/design/templates/admin/photos/luxgallery/change_form.html b/design/templates/admin/photos/luxgallery/change_form.html new file mode 100644 index 0000000..448d415 --- /dev/null +++ b/design/templates/admin/photos/luxgallery/change_form.html @@ -0,0 +1,134 @@ +{% extends "admin/base_site.html" %} +{% load i18n admin_urls admin_static admin_modify %} + +{% block extrahead %}{{ block.super }} +<script type="text/javascript" src="{% url 'admin:jsi18n' %}"></script> +{{ media }} +<script> +if (!$) { + $ = django.jQuery; +} +$(function(){ + $('#id_images').css('width', '400px').css('height', '400px'); + $('#id_images option').each(function(){ + $(this).attr('style', 'background: url({{MEDIA_URL}}images/galleries/thumb/'+$(this).text()+') no-repeat; background-size: 70px 70px; height: 80px; padding-left: 80px; line-height: 80px;'); + }); +}); +</script> + +{% endblock %} + +{% block extrastyle %}{{ block.super }}<link rel="stylesheet" type="text/css" href="{% static "admin/css/forms.css" %}" />{% endblock %} + +{% block coltype %}colM{% endblock %} + +{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} change-form{% endblock %} + +{% if not is_popup %} +{% block breadcrumbs %} +<div class="breadcrumbs"> +<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a> +› <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a> +› {% if has_change_permission %}<a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>{% else %}{{ opts.verbose_name_plural|capfirst }}{% endif %} +› {% if add %}{% trans 'Add' %} {{ opts.verbose_name }}{% else %}{{ original|truncatewords:"18" }}{% endif %} +</div> +{% endblock %} +{% endif %} + +{% block content %}<div id="content-main"> +{% block object-tools %} +{% if change %}{% if not is_popup %} + <ul class="object-tools"> + {% block object-tools-items %} + <li> + {% url opts|admin_urlname:'history' original.pk|admin_urlquote as history_url %} + <a href="{% add_preserved_filters history_url %}" class="historylink">{% trans "History" %}</a> + </li> + {% if has_absolute_url %}<li><a href="{{ absolute_url }}" class="viewsitelink">{% trans "View on site" %}</a></li>{% endif %} + {% endblock %} + </ul> +{% endif %}{% endif %} +{% endblock %} +<form {% if has_file_field %}enctype="multipart/form-data" {% endif %}action="{{ form_url }}" method="post" id="{{ opts.model_name }}_form" novalidate>{% csrf_token %}{% block form_top %}{% endblock %} +<div> +{% if is_popup %}<input type="hidden" name="{{ is_popup_var }}" value="1" />{% endif %} +{% if to_field %}<input type="hidden" name="{{ to_field_var }}" value="{{ to_field }}" />{% endif %} +{% if save_on_top %}{% block submit_buttons_top %}{% submit_row %}{% endblock %}{% endif %} +{% if errors %} + <p class="errornote"> + {% if errors|length == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %} + </p> + {{ adminform.form.non_field_errors }} +{% endif %} + +{% block field_sets %} +{% for fieldset in adminform %} + {% include "admin/includes/fieldset.html" %} +{% endfor %} +{% endblock %} + +{% block after_field_sets %}{% endblock %} + +{% block inline_field_sets %} +{% for inline_admin_formset in inline_admin_formsets %} + {% include inline_admin_formset.opts.template %} +{% endfor %} +{% endblock %} + +{% block after_related_objects %}{% endblock %} + +{% block submit_buttons_bottom %}{% submit_row %}{% endblock %} + +{% block admin_change_form_document_ready %} + <script type="text/javascript"> + (function($) { + $(document).ready(function() { + $('.add-another').click(function(e) { + e.preventDefault(); + var event = $.Event('django:add-another-related'); + $(this).trigger(event); + if (!event.isDefaultPrevented()) { + showAddAnotherPopup(this); + } + }); + $('.related-lookup').click(function(e) { + e.preventDefault(); + var event = $.Event('django:lookup-related'); + $(this).trigger(event); + if (!event.isDefaultPrevented()) { + showRelatedObjectLookupPopup(this); + } + }); + $('body').on('click', '.related-widget-wrapper-link', function(e) { + e.preventDefault(); + if (this.href) { + var event = $.Event('django:show-related', {href: this.href}); + $(this).trigger(event); + if (!event.isDefaultPrevented()) { + showRelatedObjectPopup(this); + } + } + }); + $('body').on('change', '.related-widget-wrapper select', function(e) { + var event = $.Event('django:update-related'); + $(this).trigger(event); + if (!event.isDefaultPrevented()) { + updateRelatedObjectLinks(this); + } + }); + $('.related-widget-wrapper select').trigger('change'); + + {% if adminform and add %} + $('form#{{ opts.model_name }}_form :input:visible:enabled:first').focus() + {% endif %} + }); + })(django.jQuery); + </script> +{% endblock %} + +{# JavaScript for prepopulated fields #} +{% prepopulated_fields_js %} + +</div> +</form></div> +{% endblock %} diff --git a/design/templates/archives/gallery_list.html b/design/templates/archives/gallery_list.html new file mode 100644 index 0000000..1ae8696 --- /dev/null +++ b/design/templates/archives/gallery_list.html @@ -0,0 +1,57 @@ +{% extends 'base.html' %} +{% load get_image_size %} +{% load typogrify_tags %} +{% load pagination_tags %} + +{% block pagetitle %}Luxagraf | {% if region %}Photo Galleries: Images from {{region.name|title|smartypants|safe}}{%else%}Photo Galleries: Images from Around the World {%endif%} Page {{page}}{% endblock %} +{% block metadescription %}{% if region %}Photo Galleries from {{region.name|title|smartypants|safe}}{%else%}Photo Galleries: Images from Around the World {%endif%} Page {{page}}{% endblock %} + +{%block extrahead%} +<style> + @media screen and (min-width: 38em){ + {% autopaginate object_list 24 %} {% for object in object_list %} + {%if object.caption_style%} +#image-{{forloop.counter}} .gallery-text { + {{object.caption_style}} +} + {%endif%} + {%endfor%} +</style> +{%endblock%} + +{%block bodyid%}id="photo-archive"{%endblock%} + +{% block primary %}<ul class="bl" id="breadcrumbs" itemscope itemtype="http://data-vocabulary.org/Breadcrumb"> + <li><a href="/" title="luxagraf homepage" itemprop="url"><span itemprop="title">Home</span></a> →</li> + {% if region %}{%if region.name == 'United States'%} <li><a href="/photos/" title="See all Photos" itemprop="url"><span itemprop="title">Photos</span></a> →</li> + <li>the United States</li>{%else%}<li><a href="/photos/" title="See all Photos" itemprop="url"><span itemprop="title">Photos</span></a> →</li> <li>{{region.name|title|smartypants|safe}}{%endif%}{%else%}<li>{% if is_private %}<a href="/photos/">Photos</a> →</li>{%else%}<li>Photos </li>{%endif%}{%endif%} + {% if is_private %}<li>Private</li>{%endif%} + </ul> + <main role="main"> + <h1 class="hide">{% if region %}Photographs from {{region.name|title|smartypants|safe}}{%else%}Photographs from Around the World {%endif%}</h1> + {% autopaginate object_list 24 %}{% for object in object_list %} + <article id="image-{{forloop.counter}}" class="photo-gallery"> + <a href="{{object.get_absolute_url}}" title="view images from {{ object.title }}"> + <img sizes="(max-width: 1140px) 100vw" + srcset="{% get_image_size object.thumb 'small' %} 720w, + {% get_image_size object.thumb 'medium' %} 1140w, + {% get_image_size object.thumb 'large' %} 2280w," + src="{% get_image_size object.thumb 'medium' %}" alt="{{object.title}}" ></a> + <div class="gallery-text"> + <h4>{{object.title}}</h4> + <time class="dt-published published dt-updated post--date" datetime="{{object.pub_date|date:'c'}}">{{object.pub_date|date:"F"}} <span>{{object.pub_date|date:"j, Y"}}</span></time> – + <span class="p-location h-adr adr post--location" itemprop="contentLocation" itemscope itemtype="http://schema.org/Place"> + {% if object.location.state.country.name == "United States" %}<span class="p-locality locality">{{object.location.name|smartypants|safe}}</span>, <a class="p-region region" href="/photos/united-states/" title="all galleries from the United States">{{object.location.state.name}}</a>, <span class="p-country-name">U.S.</span>{%else%}<span class="p-region">{{object.location.name|smartypants|safe}}</span>, <a class="p-country-name country-name" href="/jrnl/{{object.location.state.country.slug}}/" title="travel writing from {{object.location.state.country.name}}">{{object.state.country.name}}</a>{%endif%} + <span style="display: none;" itemprop="geo" itemscope itemtype="http://schema.org/GeoCoordinates"> + <data itemprop="latitude" class="p-latitude" value="{{object.latitude}}">{{object.latitude}}</data> + <data itemprop="longitude" class="p-longitude" value="{{object.longitude}}">{{object.longitude}}</data> + </span> + </span> + {%if object.description%}<p>{{object.description|safe|smartypants|widont}}</p>{%endif%} + </div> + </article>{% endfor %} + </ul> + </main> + <nav class="pagination">{% paginate %} + </nav> +{% endblock %} diff --git a/design/templates/details/photo_gallery.html b/design/templates/details/photo_gallery.html index b991ba2..d34f4e4 100644 --- a/design/templates/details/photo_gallery.html +++ b/design/templates/details/photo_gallery.html @@ -24,7 +24,7 @@ </ul> <p class="directions">Use left/right arrow keys to navigate through photos</p> <main id="slides" class="image-gallery--wrapper"> - <h1 class="hide">Photos from {{object.title}}</h1>{%for photo in object.image.all reversed %} + <h1 class="hide">Photos from {{object.title}}</h1>{%for photo in object.images.all reversed %} <article id="image-{{forloop.counter}}"> <h6><a href="#image-{{forloop.counter}}" class="permalink" title="link to this image">∞ {{forloop.counter|number_to_word}} ∞</a></h6> <figure class="fig"> |