Commit 6da25450 authored by Thom Wiggers's avatar Thom Wiggers 📐
Browse files

Merge branch '28-extra-controls-for-photos' into 'master'

Enable rotating photos, adjusting visibility

Closes #28

See merge request !28
parents 44420ea0 35eb0bd4
from django.contrib import admin
from django import forms
from django.conf import settings
from django.core.files.base import ContentFile
from .models import Album
from .models import Album, Photo
import os
import shutil
from zipfile import ZipFile
class AlbumForm(forms.ModelForm):
album_archive = forms.FileField()
album_archive = forms.FileField(
required=False,
help_text="Uploading a zip file adds all contained images as photos.",
)
class Meta:
exclude = ['dirname']
......@@ -23,8 +26,10 @@ class AlbumAdmin(admin.ModelAdmin):
def save_model(self, request, obj, form, change):
obj.save()
archive = form.cleaned_data.get('album_archive', None)
path = os.path.join(settings.MEDIA_ROOT, 'photos', obj.dirname)
if archive is None:
return
with ZipFile(archive) as zip_file:
path = os.path.join(settings.MEDIA_ROOT, 'photos', obj.dirname)
os.makedirs(path, exist_ok=True)
# Notably, this can also be used to add photos to existing albums
for photo in zip_file.namelist():
......@@ -36,9 +41,11 @@ class AlbumAdmin(admin.ModelAdmin):
if not photo_filename:
continue
# Cannot use .extract as that would recreate directory paths
source = zip_file.open(photo)
target = open(os.path.join(path, photo_filename), "wb")
with source, target:
shutil.copyfileobj(source, target)
photo_obj = Photo()
photo_obj.album = obj
with zip_file.open(photo) as f:
photo_obj.file.save(photo_filename, ContentFile(f.read()))
photo_obj.save()
admin.site.register(Album, AlbumAdmin)
admin.site.register(Photo)
# -*- coding: utf-8 -*-
# Generated by Django 1.10 on 2016-08-09 15:55
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import photos.models
class Migration(migrations.Migration):
dependencies = [
('photos', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Photo',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('file', models.ImageField(upload_to=photos.models.photo_uploadto)),
('rotation', models.IntegerField(default=0)),
('album', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='photos.Album')),
],
),
migrations.AddField(
model_name='album',
name='_cover',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='covered_album', to='photos.Photo'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.10 on 2016-08-09 16:56
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('photos', '0002_auto_20160809_1755'),
]
operations = [
migrations.AlterField(
model_name='photo',
name='rotation',
field=models.IntegerField(choices=[(0, 0), (90, 90), (180, 180), (270, 270)], default=0, help_text='This does not modify the original image file.'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.10 on 2016-08-10 12:47
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('photos', '0003_auto_20160809_1856'),
]
operations = [
migrations.AddField(
model_name='album',
name='hidden',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='photo',
name='hidden',
field=models.BooleanField(default=False),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.10 on 2016-08-12 15:06
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('photos', '0002_album_shareable'),
('photos', '0004_auto_20160810_1447'),
]
operations = [
]
......@@ -10,33 +10,45 @@ import random
COVER_FILENAME = 'cover.jpg'
def photo_uploadto(instance, filename):
return os.path.join(Album.photosdir, instance.album.dirname, filename)
class Photo(models.Model):
album = models.ForeignKey('Album', on_delete=models.CASCADE)
file = models.ImageField(upload_to=photo_uploadto)
rotation = models.IntegerField(
default=0,
choices=((x, x) for x in (0, 90, 180, 270)),
help_text="This does not modify the original image file.",
)
hidden = models.BooleanField(default=False)
def __str__(self):
return self.file.name
class Album(models.Model):
title = models.CharField(max_length=200)
dirname = models.CharField(max_length=200)
date = models.DateField()
slug = models.SlugField()
hidden = models.BooleanField(default=False)
_cover = models.OneToOneField(Photo, on_delete=models.SET_NULL, blank=True,
null=True, related_name='covered_album')
shareable = models.BooleanField(default=False)
photosdir = 'photos'
photospath = os.path.join(settings.MEDIA_ROOT, photosdir)
@property
def path(self):
return os.path.join(Album.photospath, self.dirname)
@cached_property
def cover(self):
if os.path.isfile(os.path.join(self.path, COVER_FILENAME)):
cover = COVER_FILENAME
if self._cover is not None:
return self._cover
else:
random.seed(self.dirname)
cover = random.choice(os.listdir(self.path))
return os.path.join(Album.photosdir, self.dirname, cover)
@cached_property
def photos(self):
return [os.path.join(Album.photosdir, self.dirname, photo)
for photo in os.listdir(self.path) if photo != COVER_FILENAME]
cover = random.choice(self.photo_set.all())
return cover
def __str__(self):
return '{} {}'.format(self.date.strftime('%Y-%m-%d'), self.title)
......
/* Solving this with dynamic a data attribute (i.e. attr(data-rotation)) does not work, as that's a string. */
img.rotate90 {
transform: rotate(90deg);
}
img.rotate180 {
transform: rotate(180deg);
}
img.rotate270 {
transform: rotate(270deg);
}
......@@ -3,6 +3,11 @@
{% block page_title %}{% trans "Photos" %}{% endblock %}
{% block css_head %}
{{ block.super }}
<link href="{% static "photos/css/style.css" %}" rel="stylesheet" type="text/css">
{% endblock %}
{% block body %}
<h1>{{ album.title }}</h1>
<h2>{{ album.date|date:"d-m-Y" }}</h2>
......@@ -16,9 +21,9 @@
{% endif %}
<div class="gallery">
<ul class="gallery-photos row">
{% for photo in album.photos %}
{% for photo in photos %}
<li class="post gallery-photo span3 has-overlay {% if forloop.counter0|divisibleby:4 %}first-child{% endif %}">
<a class="gallery-box" rel="gallery"
<a data-fancybox-rotation="{{ photo.rotation }}" class="gallery-box" rel="gallery"
{% if album.shareable %}
data-download="{% url 'photos:shared-download' album.slug album.access_token photo %}" href="{% shared_thumbnail album.slug album.access_token photo '1024x768' fit=False %}"
{% else %}
......@@ -27,9 +32,9 @@
<span class="post-inner">
<span class="inner-img">
{% if album.shareable %}
<img src="{% shared_thumbnail album.slug album.access_token photo '220x220' %}" alt="" />
<img class="rotate{{ photo.rotation }}" src="{% shared_thumbnail album.slug album.access_token photo '220x220' %}" alt="" />
{% else %}
<img src="{% thumbnail photo '220x220' %}" alt="" />
<img class="rotate{{ photo.rotation }}" src="{% thumbnail photo '220x220' %}" alt="" />
{% endif %}
</span>
<span class="post-overlay">
......
{% extends 'base.html' %}
{% load i18n thumbnail %}
{% load i18n thumbnail staticfiles %}
{% block page_title %}{% trans "Photos" %}{% endblock %}
{% block css_head %}
{{ block.super }}
<link href="{% static "photos/css/style.css" %}" rel="stylesheet" type="text/css">
{% endblock %}
{% block body %}
<h1>{% trans "Photos" %}</h1>
......@@ -13,7 +18,7 @@
<a href="{% url 'photos:album' album.slug %}">
<span class="post-inner">
<span class="inner-img">
<img src="{% thumbnail album.cover '220x220' %}" alt="" />
<img class="rotate{{ album.cover.rotation }}" src="{% thumbnail album.cover.file '220x220' %}" alt="" />
</span>
<span class="post-overlay">
<span class="post-overlay-meta">
......
......@@ -16,7 +16,7 @@ COVER_FILENAME = 'cover.jpg'
@login_required
def index(request):
albums = Album.objects.all().order_by('-date')
albums = Album.objects.filter(hidden=False).order_by('-date')
paginator = Paginator(albums, 12)
page = request.GET.get('page')
......@@ -34,7 +34,8 @@ def index(request):
@login_required
def album(request, slug):
album = get_object_or_404(Album, slug=slug)
return render(request, 'photos/album.html', {'album': album})
context = {'album': album, 'photos': album.photo_set.filter(hidden=False)}
return render(request, 'photos/album.html', context)
def _checked_shared_album(slug, token):
......
This diff is collapsed.
......@@ -32,7 +32,7 @@
{% block js_head %}
<script type="text/javascript" src="{% static "plugins/jquery-1.8.3.min.js" %}"></script>
<script type="text/javascript" src="{% static "js/jquery.jscroll.min.js" %}"></script>
<script type="text/javascript" src="{% static "js/jquery.fancybox.pack.js" %}"></script>
<script type="text/javascript" src="{% static "js/jquery.fancybox.js" %}"></script>
<script type="text/javascript" src="{% static "js/thimbus.js" %}"></script>
<script type="text/javascript" src="{% static "js/scripts.js" %}"></script>
{% endblock %}
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment