Commit 02df6596 authored by Sébastiaan Versteeg's avatar Sébastiaan Versteeg
Browse files

Merge branch '197-robust-photo-uploads' into 'master'

Resolve "Foto's uploaden moet robuuster"

Closes #197 and #425

See merge request !419
parents 32d8131a 3005a3ef
......@@ -57,17 +57,28 @@ def save_photo(request, archive_file, photo, album):
if not os.path.basename(photo_filename):
return
# Generate unique filename
num = album.photo_set.count()
_, extension = os.path.splitext(photo_filename)
new_filename = str(num).zfill(4) + extension
photo_obj = Photo()
photo_obj.album = album
try:
with extract_file(photo) as f:
photo_obj.file.save(photo_filename, ContentFile(f.read()))
photo_obj.file.save(new_filename, ContentFile(f.read()))
except (OSError, AttributeError):
messages.add_message(request, messages.WARNING,
_("Ignoring {}").format(photo_filename))
else:
photo_obj.save()
if Photo.objects.filter(album=album, _digest=photo_obj._digest)\
.exclude(pk=photo_obj.pk).exists():
messages.add_message(request, messages.WARNING,
_("{} is duplicate.").format(photo_filename))
photo_obj.delete()
class AlbumAdmin(TranslatedModelAdmin):
list_display = ('title', 'date', 'hidden', 'shareable')
......@@ -112,6 +123,7 @@ class PhotoAdmin(admin.ModelAdmin):
list_display = ('__str__', 'album', 'hidden')
search_fields = ('file',)
list_filter = ('album', 'hidden')
exclude = ('_digest',)
def save_model(self, request, obj, form, change):
obj.save()
......
......@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-02-22 20:29+0100\n"
"PO-Revision-Date: 2017-02-22 20:31+0100\n"
"POT-Creation-Date: 2017-03-22 21:31+0100\n"
"PO-Revision-Date: 2017-03-22 21:32+0100\n"
"Last-Translator: Sébastiaan Versteeg <se_bastiaan@outlook.com>\n"
"Language-Team: \n"
"Language: nl\n"
......@@ -16,7 +16,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 1.8.12\n"
"X-Generator: Poedit 1.8.7.1\n"
#: admin.py:33
msgid "Uploading a zip or tar file adds all contained images as photos."
......@@ -24,69 +24,74 @@ msgstr ""
"Indien je een zip of tar file upload worden alle afbeeldingen in het bestand "
"als foto’s toegevoegd."
#: admin.py:64
#: admin.py:69
msgid "Ignoring {}"
msgstr "{} wordt genegeerd"
#: admin.py:94
#: admin.py:76
msgid "{} is duplicate."
msgstr "{} is dubbel."
#: admin.py:109
msgid "The uploaded file is not a zip or tar file."
msgstr "Het geüploade bestand is geen als zip of tar archief."
#: admin.py:98 admin.py:106
#: admin.py:113 admin.py:126
msgid "Full-sized photos will not be saved on the Thalia-website."
msgstr ""
"De foto’s op de Thalia-website worden niet opgeslagen in originele grootte."
#: models.py:23
#: models.py:26
msgid "album"
msgstr "album"
#: models.py:27
#: models.py:30
msgid "file"
msgstr "bestand"
#: models.py:32
#: models.py:35
msgid "rotation"
msgstr "rotatie"
#: models.py:35
#: models.py:38
msgid "This does not modify the original image file."
msgstr "Dit verandert het originele bestand niet."
#: models.py:39 models.py:88
#: models.py:42 models.py:101
msgid "hidden"
msgstr "verborgen"
#: models.py:70
#: models.py:83
msgid "title"
msgstr "titel"
#: models.py:75
#: models.py:88
msgid "directory name"
msgstr "mapnaam"
#: models.py:80
#: models.py:93
msgid "date"
msgstr "datum"
#: models.py:84
#: models.py:97
msgid "slug"
msgstr "slug"
#: models.py:98
#: models.py:111
msgid "cover image"
msgstr "coverafbeelding"
#: models.py:102
#: models.py:115
msgid "shareable"
msgstr "deelbaar"
#: templates/photos/album.html:4 templates/photos/index.html:4
#: templates/photos/index.html:14
#: templates/photos/album.html:4 templates/photos/album.html:5
#: templates/photos/index.html:4 templates/photos/index.html:5
#: templates/photos/index.html:15
msgid "Photos"
msgstr "Foto's"
#: templates/photos/album.html:24
#: templates/photos/album.html:25
msgid ""
"Note: This album can be shared with people outside the association by "
"sending them the following link:"
......@@ -94,7 +99,7 @@ msgstr ""
"Let op: Dit album kan gedeeld worden met mensen buiten de vereniging via de "
"volgende link:"
#: templates/photos/index.html:66
#: templates/photos/index.html:67
msgid "Next"
msgstr "Volgende"
......
# -*- coding: utf-8 -*-
# Generated by Django 1.10.4 on 2017-03-22 19:59
from __future__ import unicode_literals
import hashlib
from django.db import migrations, models
def add_digests(apps, schema_editor):
Photo = apps.get_model('photos', 'Photo')
for photo in Photo.objects.all():
hash_sha1 = hashlib.sha1()
for chunk in iter(lambda: photo.file.read(4096), b""):
hash_sha1.update(chunk)
photo._digest = hash_sha1.hexdigest()
photo.save()
class Migration(migrations.Migration):
atomic = False
dependencies = [
('photos', '0007_auto_20170218_2142'),
]
operations = [
migrations.AddField(
model_name='photo',
name='_digest',
field=models.CharField(blank=True, default='', max_length=40, verbose_name='digest'),
preserve_default=False,
),
migrations.RunPython(
code=add_digests,
),
migrations.AlterField(
model_name='Photo',
name='_digest',
field=models.CharField(max_length=40, verbose_name='digest'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2017-05-03 18:01
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('photos', '0008_photo__digest'),
]
operations = [
migrations.AlterField(
model_name='album',
name='slug',
field=models.SlugField(unique=True, verbose_name='slug'),
),
]
......@@ -15,7 +15,10 @@ COVER_FILENAME = 'cover.jpg'
def photo_uploadto(instance, filename):
return os.path.join(Album.photosdir, instance.album.dirname, filename)
num = instance.album.photo_set.count()
extension = os.path.splitext(filename)[1]
new_filename = str(num).zfill(4) + extension
return os.path.join(Album.photosdir, instance.album.dirname, new_filename)
class Photo(models.Model):
......@@ -42,6 +45,11 @@ class Photo(models.Model):
default=False
)
_digest = models.CharField(
'digest',
max_length=40,
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.file:
......@@ -63,6 +71,11 @@ class Photo(models.Model):
image.save(image_path, "JPEG")
self._orig_file = self.file.path
hash_sha1 = hashlib.sha1()
for chunk in iter(lambda: self.file.read(4096), b""):
hash_sha1.update(chunk)
self._digest = hash_sha1.hexdigest()
class Meta:
ordering = ('file', )
......@@ -85,6 +98,7 @@ class Album(models.Model, metaclass=ModelTranslateMeta):
slug = models.SlugField(
verbose_name=_('slug'),
unique=True,
)
hidden = models.BooleanField(
......
import doctest
from zipfile import ZipFile
from io import BytesIO
from . import views
from django.test import Client, TestCase
from members.models import Member
from photos.models import Photo, Album
def load_tests(loader, tests, ignore):
"""
Load all tests in this module
"""
# Adds the doctests in views
tests.addTests(doctest.DocTestSuite(views))
return tests
def create_zip(photos):
output_file = BytesIO()
with ZipFile(output_file, 'w') as zip_file:
for photo in photos:
zip_file.write(photo)
output_file.seek(0)
return output_file
class AlbumUploadTest(TestCase):
"""Tests album uploads in the admin."""
fixtures = ['members.json']
@classmethod
def setUpTestData(cls):
cls.member = Member.objects.filter(user__last_name="Wiggers").first()
def setUp(self):
self.client = Client()
self.client.force_login(self.member.user)
def test_album_upload(self):
output_file = create_zip(["photos/fixtures/thom_assessor.png"])
self.client.post('/admin/photos/album/add/',
{"title": "test album",
"date": "2017-04-12",
"slug": "2017-04-12-test-album",
"album_archive": output_file},
follow=True)
self.assertEqual(Album.objects.all().count(), 1)
self.assertEqual(Photo.objects.all().count(), 1)
def test_album_create_album_twice(self):
self.client.post('/admin/photos/album/add/',
{"title": "test album",
"date": "2017-04-12",
"slug": "2017-04-12-test-album"},
follow=True)
self.client.post('/admin/photos/album/add/',
{"title": "test album",
"date": "2017-04-12",
"slug": "2017-04-12-test-album"},
follow=True)
self.assertEqual(Album.objects.all().count(), 1)
def test_album_upload_same_photo_twice_in_album(self):
output_file = create_zip(["photos/fixtures/thom_assessor.png"])
self.client.post('/admin/photos/album/add/',
{"title": "test album",
"date": "2017-04-12",
"slug": "2017-04-12-test-album",
"album_archive": output_file},
follow=True)
pk = Album.objects.first().pk
self.client.post('/admin/photos/album/{}/change/'.format(pk),
{"title": "test album",
"date": "2017-04-12",
"slug": "2017-04-12-test-album",
"album_archive": output_file},
follow=True)
self.assertEqual(Album.objects.all().count(), 1)
self.assertEqual(Photo.objects.all().count(), 1)
def test_album_upload_different_photo_in_album(self):
output_file = create_zip(["photos/fixtures/thom_assessor.png"])
self.client.post('/admin/photos/album/add/',
{"title": "test album",
"date": "2017-04-12",
"slug": "2017-04-12-test-album",
"album_archive": output_file},
follow=True)
output_file = create_zip(["photos/fixtures/janbeleid-hoe.jpg"])
pk = Album.objects.first().pk
self.client.post('/admin/photos/album/{}/change/'.format(pk),
{"title": "test album",
"date": "2017-04-12",
"slug": "2017-04-12-test-album",
"album_archive": output_file},
follow=True)
self.assertEqual(Album.objects.all().count(), 1)
self.assertEqual(Photo.objects.all().count(), 2)
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