From b88388d669ec6ac5484a4165a284838c414062dc Mon Sep 17 00:00:00 2001 From: Tom van Bussel Date: Wed, 29 Aug 2018 20:37:05 +0200 Subject: [PATCH] Split photos tests and added new tests --- website/photos/models.py | 29 +- website/photos/services.py | 43 ++- website/photos/tests/__init__.py | 0 .../photos/{tests.py => tests/test_admin.py} | 74 +--- website/photos/tests/test_services.py | 133 +++++++ website/photos/tests/test_views.py | 324 ++++++++++++++++++ website/photos/views.py | 25 +- 7 files changed, 509 insertions(+), 119 deletions(-) create mode 100644 website/photos/tests/__init__.py rename website/photos/{tests.py => tests/test_admin.py} (64%) create mode 100644 website/photos/tests/test_services.py create mode 100644 website/photos/tests/test_views.py diff --git a/website/photos/models.py b/website/photos/models.py index 91aa3226..602c219f 100644 --- a/website/photos/models.py +++ b/website/photos/models.py @@ -9,22 +9,13 @@ from django.db import models from django.urls import reverse from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ -from PIL import Image, ExifTags +from PIL import Image +from photos.services import photo_determine_rotation from utils.translation import ModelTranslateMeta, MultilingualField -COVER_FILENAME = 'cover.jpg' -EXIF_ORIENTATION = { - 1: 0, - 2: 0, - 3: 180, - 4: 180, - 5: 90, - 6: 90, - 7: 270, - 8: 270, -} +COVER_FILENAME = 'cover.jpg' logger = logging.getLogger(__name__) @@ -37,18 +28,6 @@ def photo_uploadto(instance, filename): return os.path.join(Album.photosdir, instance.album.dirname, new_filename) -def determine_rotation(pil_image): - if isinstance(pil_image, JpegImageFile) and pil_image._getexif(): - exif = { - ExifTags.TAGS[k]: v - for k, v in pil_image._getexif().items() - if k in ExifTags.TAGS - } - if exif.get('Orientation'): - return EXIF_ORIENTATION[exif.get('Orientation')] - return 0 - - class Photo(models.Model): album = models.ForeignKey( @@ -98,7 +77,7 @@ class Photo(models.Model): image_path, _ext = os.path.splitext(image_path) image_path = "{}.jpg".format(image_path) - self.rotation = determine_rotation(image) + self.rotation = photo_determine_rotation(image) # Image.thumbnail does not upscale an image that is smaller image.thumbnail(settings.PHOTO_UPLOAD_SIZE, Image.ANTIALIAS) diff --git a/website/photos/services.py b/website/photos/services.py index 30fdc03a..7ea8622b 100644 --- a/website/photos/services.py +++ b/website/photos/services.py @@ -1,6 +1,32 @@ from django.db.models import When, Value, BooleanField, ExpressionWrapper, Q, \ Case +from PIL.JpegImagePlugin import JpegImageFile +from PIL import ExifTags + + +def photo_determine_rotation(pil_image): + EXIF_ORIENTATION = { + 1: 0, + 2: 0, + 3: 180, + 4: 180, + 5: 90, + 6: 90, + 7: 270, + 8: 270, + } + + if isinstance(pil_image, JpegImageFile) and pil_image._getexif(): + exif = { + ExifTags.TAGS[k]: v + for k, v in pil_image._getexif().items() + if k in ExifTags.TAGS + } + if exif.get('Orientation'): + return EXIF_ORIENTATION[exif.get('Orientation')] + return 0 + def is_album_accessible(request, album): if request.member and request.member.current_membership is not None: @@ -15,24 +41,21 @@ def is_album_accessible(request, album): # Annotate the albums which are accessible by the user def get_annotated_accessible_albums(request, albums): - if request.member and request.member.current_membership is None: - # The user is currently not a member - # so only show photos that were made during their membership + if request.member and request.member.current_membership is not None: + albums = albums.annotate(accessible=ExpressionWrapper( + Value(True), output_field=BooleanField())) + elif request.member and request.member.current_membership is None: albums_filter = Q(pk__in=[]) for membership in request.member.membership_set.all(): - if membership.until is not None: - albums_filter |= (Q(date__gte=membership.since) & Q( - date__lte=membership.until)) - else: - albums_filter |= (Q(date__gte=membership.since)) + albums_filter |= (Q(date__gte=membership.since) & Q( + date__lte=membership.until)) albums = albums.annotate(accessible=Case( When(albums_filter, then=Value(True)), default=Value(False), output_field=BooleanField())) else: - # The user is currently a member, so show all albums albums = albums.annotate(accessible=ExpressionWrapper( - Value(True), output_field=BooleanField())) + Value(False), output_field=BooleanField())) return albums diff --git a/website/photos/tests/__init__.py b/website/photos/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/website/photos/tests.py b/website/photos/tests/test_admin.py similarity index 64% rename from website/photos/tests.py rename to website/photos/tests/test_admin.py index e9cfbb3a..ef2cdf11 100644 --- a/website/photos/tests.py +++ b/website/photos/tests/test_admin.py @@ -1,15 +1,10 @@ from io import BytesIO from zipfile import ZipFile -from PIL import Image from django.test import Client, TestCase, RequestFactory -from django.utils.datetime_safe import datetime -from freezegun import freeze_time -from members.models import Member, Membership -from photos import services -from .models import Photo, Album -from .models import determine_rotation +from members.models import Member +from photos.models import Album, Photo def create_zip(photos): @@ -21,27 +16,6 @@ def create_zip(photos): return output_file -class PhotoRotationTest(TestCase): - - fixtures = ['members.json'] - - @classmethod - def setUpTestData(cls): - cls.member = Member.objects.filter(last_name="Wiggers").first() - - def setUp(self): - self.client = Client() - self.client.force_login(self.member) - - def test_rotation_detection(self): - orientations = [0, 0, 180, 180, 90, 90, 270, 270] - for i in range(1, 9): - with self.subTest(orentation=i): - with open('photos/fixtures/poker_{}.jpg'.format(i), 'rb') as f: - rot = determine_rotation(Image.open(f)) - self.assertEqual(orientations[i - 1], rot) - - class AlbumUploadTest(TestCase): """Tests album uploads in the admin.""" @@ -140,47 +114,3 @@ class AlbumUploadTest(TestCase): follow=True) self.assertEqual(Photo.objects.first().rotation, 90) - - -class ServicesTest(TestCase): - - fixtures = ['members.json'] - - @classmethod - def setUpTestData(cls): - cls.member = Member.objects.filter(username="testuser").first() - - def setUp(self): - self.rf = RequestFactory() - - @freeze_time('2017-01-01') - def test_is_album_accessible(self): - request = self.rf.get('/') - request.member = None - album = Album(date=datetime(year=2017, month=1, day=1)) - - with self.subTest(membership=None): - self.assertFalse(services.is_album_accessible(request, album)) - - request.member = self.member - with self.subTest(membership=None): - self.assertFalse(services.is_album_accessible(request, album)) - - membership = Membership.objects.create( - user=self.member, type=Membership.MEMBER, - since=datetime(year=2016, month=1, day=1)) - with self.subTest(membership_since=membership.since, - membership_until=membership.until): - self.assertTrue(services.is_album_accessible(request, album)) - - membership.until = datetime(year=2016, month=1, day=1) - membership.save() - with self.subTest(membership_since=membership.since, - membership_until=membership.until): - self.assertFalse(services.is_album_accessible(request, album)) - - membership.until = datetime(year=2017, month=1, day=1) - membership.save() - with self.subTest(membership_since=membership.since, - membership_until=membership.until): - self.assertTrue(services.is_album_accessible(request, album)) diff --git a/website/photos/tests/test_services.py b/website/photos/tests/test_services.py new file mode 100644 index 00000000..fdd9782e --- /dev/null +++ b/website/photos/tests/test_services.py @@ -0,0 +1,133 @@ +from PIL import Image +from django.test import Client, TestCase, RequestFactory +from django.utils.datetime_safe import datetime +from freezegun import freeze_time + +from members.models import Member, Membership +from photos.models import Album +from photos.services import (is_album_accessible, photo_determine_rotation, + get_annotated_accessible_albums) + + +class IsAlbumAccesibleTest(TestCase): + fixtures = ['members.json'] + + @classmethod + def setUpTestData(cls): + cls.member = Member.objects.filter(username="testuser").first() + + def setUp(self): + self.rf = RequestFactory() + + @freeze_time('2017-01-01') + def test_is_album_accessible(self): + request = self.rf.get('/') + request.member = None + album = Album(date=datetime(year=2017, month=1, day=1)) + + with self.subTest(membership=None): + self.assertFalse(is_album_accessible(request, album)) + + request.member = self.member + with self.subTest(membership=None): + self.assertFalse(is_album_accessible(request, album)) + + membership = Membership.objects.create( + user=self.member, type=Membership.MEMBER, + since=datetime(year=2016, month=1, day=1)) + with self.subTest(membership_since=membership.since, + membership_until=membership.until): + self.assertTrue(is_album_accessible(request, album)) + + membership.until = datetime(year=2016, month=1, day=1) + membership.save() + with self.subTest(membership_since=membership.since, + membership_until=membership.until): + self.assertFalse(is_album_accessible(request, album)) + + membership.until = datetime(year=2017, month=1, day=1) + membership.save() + with self.subTest(membership_since=membership.since, + membership_until=membership.until): + self.assertTrue(is_album_accessible(request, album)) + + +class GetAnnotatedAccessibleAlbumsTest(TestCase): + fixtures = ['members.json'] + + @classmethod + def setUpTestData(cls): + cls.member = Member.objects.filter(username="testuser").first() + + def setUp(self): + self.rf = RequestFactory() + + @freeze_time('2017-01-01') + def test_get_annotated_accessible_albums(self): + request = self.rf.get('/') + request.member = None + album = Album(date=datetime(year=2017, month=1, day=1)) + album.save() + + self.assertEqual(Album.objects.count(), 1) + + with self.subTest(membership=None): + albums = Album.objects.all() + albums = get_annotated_accessible_albums(request, albums) + for album in albums: + self.assertFalse(album.accessible) + + request.member = self.member + with self.subTest(membership=None): + albums = Album.objects.all() + albums = get_annotated_accessible_albums(request, albums) + for album in albums: + self.assertFalse(album.accessible) + + membership = Membership.objects.create( + user=self.member, type=Membership.MEMBER, + since=datetime(year=2016, month=1, day=1)) + with self.subTest(membership_since=membership.since, + membership_until=membership.until): + albums = Album.objects.all() + albums = get_annotated_accessible_albums(request, albums) + for album in albums: + self.assertTrue(album.accessible) + + membership.until = datetime(year=2016, month=1, day=1) + membership.save() + with self.subTest(membership_since=membership.since, + membership_until=membership.until): + albums = Album.objects.all() + albums = get_annotated_accessible_albums(request, albums) + for album in albums: + self.assertFalse(album.accessible) + + membership.until = datetime(year=2017, month=1, day=1) + membership.save() + with self.subTest(membership_since=membership.since, + membership_until=membership.until): + albums = Album.objects.all() + albums = get_annotated_accessible_albums(request, albums) + for album in albums: + self.assertTrue(album.accessible) + + +class DetermineRotationTest(TestCase): + fixtures = ['members.json'] + + @classmethod + def setUpTestData(cls): + cls.member = Member.objects.filter(last_name="Wiggers").first() + + def setUp(self): + self.client = Client() + self.client.force_login(self.member) + + def test_rotation_detection(self): + orientations = [0, 0, 180, 180, 90, 90, 270, 270] + for i in range(1, 9): + with self.subTest(orentation=i): + with open('photos/fixtures/poker_{}.jpg'.format(i), 'rb') as f: + rot = photo_determine_rotation(Image.open(f)) + self.assertEqual(orientations[i - 1], rot) diff --git a/website/photos/tests/test_views.py b/website/photos/tests/test_views.py new file mode 100644 index 00000000..f004725a --- /dev/null +++ b/website/photos/tests/test_views.py @@ -0,0 +1,324 @@ +from datetime import date + +from django.core.files.uploadedfile import SimpleUploadedFile +from django.test import Client, TestCase +from django.urls import reverse + +from members.models import Member, Membership +from photos.models import Album, Photo + + +class AlbumIndexTest(TestCase): + + fixtures = ['members.json'] + + @classmethod + def setUpTestData(cls): + cls.member = Member.objects.filter(last_name="Wiggers").first() + cls.client = Client() + + def setUp(self): + self.client.force_login(self.member) + + def test_index(self): + with self.subTest(album_objects__count=Album.objects.count()): + response = self.client.get(reverse('photos:index')) + self.assertEqual(response.status_code, 200) + + for i in range(12): + Album.objects.create( + title_en='test_album_a%d' % i, + title_nl='test_album_a%d' % i, + date=date(year=2018, month=9, day=5), + slug='test_album_a%d' % i) + + with self.subTest(album_objects__count=Album.objects.count()): + response = self.client.get(reverse('photos:index')) + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.context['albums']), 12) + self.assertEqual(response.context['page_range'], range(1, 2)) + + for i in range(12): + Album.objects.create( + title_en='test_album_b%d' % i, + title_nl='test_album_b%d' % i, + date=date(year=2018, month=9, day=5), + slug='test_album_b%d' % i) + + with self.subTest(album_objects__count=Album.objects.count()): + response = self.client.get(reverse('photos:index')) + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.context['albums']), 12) + self.assertEqual(response.context['page_range'], range(1, 3)) + + for i in range(72): + Album.objects.create( + title_en='test_album_c%d' % i, + title_nl='test_album_c%d' % i, + date=date(year=2018, month=9, day=5), + slug='test_album_c%d' % i) + + with self.subTest(album_objects__count=Album.objects.count()): + response = self.client.get(reverse('photos:index')) + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.context['albums']), 12) + self.assertEqual(response.context['page_range'], range(1, 6)) + + def test_empty_page(self): + Album.objects.create( + title_en='test_album', + title_nl='test_album', + date=date(year=2018, month=9, day=5), + slug='test_album') + + response = self.client.get(reverse('photos:index') + '?page=5') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['page_range'], range(1, 2)) + + def test_many_pages(self): + for i in range(120): + Album.objects.create( + title_en='test_album_%d' % i, + title_nl='test_album_%d' % i, + date=date(year=2018, month=9, day=5), + slug='test_album_%d' % i) + + with self.subTest(page=1): + response = self.client.get(reverse('photos:index') + '?page=1') + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.context['albums']), 12) + self.assertEqual(response.context['page_range'], range(1, 6)) + + with self.subTest(page=4): + response = self.client.get(reverse('photos:index') + '?page=4') + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.context['albums']), 12) + self.assertEqual(response.context['page_range'], range(2, 7)) + + with self.subTest(page=9): + response = self.client.get(reverse('photos:index') + '?page=9') + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.context['albums']), 12) + self.assertEqual(response.context['page_range'], range(6, 11)) + + +class AlbumTest(TestCase): + + fixtures = ['members.json'] + + @classmethod + def setUpTestData(cls): + cls.member = Member.objects.filter(username="testuser").first() + cls.client = Client() + + def setUp(self): + self.album = Album.objects.create( + title_en='test_album', + title_nl='test_album', + date=date(year=2017, month=9, day=5), + slug='test_album') + + self.client.force_login(self.member) + + def test_get(self): + Membership.objects.create( + type=Membership.MEMBER, + user=self.member, + since=date(year=2015, month=1, day=1), + until=None) + + + for i in range(10): + with open("photos/fixtures/thom_assessor.png", "rb") as f: + fi = SimpleUploadedFile(name='photo{}.png'.format(i), + content=f.read(), + content_type='image/png') + photo = Photo(album=self.album, file=fi) + photo.save() + + response = self.client.get(reverse( + 'photos:album', args=(self.album.slug,))) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['album'], self.album) + self.assertEqual(len(response.context['photos']), 10) + + def test_unaccessible(self): + Membership.objects.create( + type=Membership.MEMBER, + user=self.member, + since=date(year=2016, month=1, day=1), + until=date(year=2018, month=1, day=1)) + + with self.subTest(): + self.album.date=date(year=2017, month=1, day=1) + self.album.save() + + response = self.client.get(reverse( + 'photos:album', args=(self.album.slug,))) + self.assertEqual(response.status_code, 200) + + with self.subTest(): + self.album.date=date(year=2018, month=9, day=5) + self.album.save() + + response = self.client.get(reverse( + 'photos:album', args=(self.album.slug,))) + self.assertEqual(response.status_code, 404) + + +class SharedAlbumTest(TestCase): + + fixtures = ['members.json'] + + @classmethod + def setUpTestData(cls): + cls.member = Member.objects.filter(username="testuser").first() + cls.client = Client() + + def setUp(self): + self.album = Album.objects.create( + title_en='test_album', + title_nl='test_album', + date=date(year=2017, month=9, day=5), + shareable=True, + slug='test_album') + + def test_get(self): + for i in range(10): + with open("photos/fixtures/thom_assessor.png", "rb") as f: + fi = SimpleUploadedFile(name='photo{}.png'.format(i), + content=f.read(), + content_type='image/png') + photo = Photo(album=self.album, file=fi) + photo.save() + + response = self.client.get(reverse( + 'photos:shared_album', + args=(self.album.slug, self.album.access_token,))) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['album'], self.album) + self.assertEqual(len(response.context['photos']), 10) + + +class DownloadTest(TestCase): + + fixtures = ['members.json'] + + @classmethod + def setUpTestData(cls): + cls.member = Member.objects.filter(last_name="Wiggers").first() + + def setUp(self): + self.client = Client() + + self.album = Album.objects.create( + title_en='test_album', + title_nl='test_album', + date=date(year=2017, month=9, day=5), + slug='test_album') + + with open("photos/fixtures/thom_assessor.png", "rb") as f: + fi = SimpleUploadedFile( + name='photo.png', + content=f.read(), + content_type='image/png') + + self.photo = Photo(album=self.album, file=fi) + self.photo.save() + + def test_download(self): + self.client.force_login(self.member) + + response = self.client.get(reverse( + 'photos:download', args=(self.photo,))) + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'image/jpeg') + + def test_logged_out(self): + response = self.client.get(reverse( + 'photos:download', args=(self.photo,))) + self.assertEqual(response.status_code, 302) + + +class SharedDownloadTest(TestCase): + + fixtures = ['members.json'] + + @classmethod + def setUpTestData(cls): + cls.member = Member.objects.filter(last_name="Wiggers").first() + cls.client = Client() + + def setUp(self): + self.album = Album.objects.create( + title_en='test_album', + title_nl='test_album', + date=date(year=2017, month=9, day=5), + shareable=True, + slug='test_album') + + with open("photos/fixtures/thom_assessor.png", "rb") as f: + fi = SimpleUploadedFile( + name='photo.png', + content=f.read(), + content_type='image/png') + + self.photo = Photo(album=self.album, file=fi) + self.photo.save() + + def test_download(self): + with self.subTest(): + response = self.client.get(reverse( + 'photos:shared-download', + args=(self.album.slug, self.album.access_token, self.photo,))) + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'image/jpeg') + + self.client.force_login(self.member) + + with self.subTest(): + response = self.client.get(reverse( + 'photos:shared-download', + args=(self.album.slug, self.album.access_token, self.photo,))) + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'image/jpeg') + + +class AlbumDownloadTest(TestCase): + + fixtures = ['members.json'] + + @classmethod + def setUpTestData(cls): + cls.member = Member.objects.filter(last_name="Wiggers").first() + + def setUp(self): + self.client = Client() + + self.album = Album.objects.create( + title_en='test_album', + title_nl='test_album', + date=date(year=2017, month=9, day=5), + slug='test_album') + + with open("photos/fixtures/thom_assessor.png", "rb") as f: + fi = SimpleUploadedFile( + name='photo.png', + content=f.read(), + content_type='image/png') + + self.photo = Photo(album=self.album, file=fi) + self.photo.save() + + def test_download(self): + self.client.force_login(self.member) + + response = self.client.get(reverse( + 'photos:album-download', args=(self.album.slug,))) + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'application/zip') + + def test_logged_out(self): + response = self.client.get(reverse( + 'photos:album-download', args=(self.album.slug,))) + self.assertEqual(response.status_code, 302) diff --git a/website/photos/views.py b/website/photos/views.py index aeaea765..83b463c8 100644 --- a/website/photos/views.py +++ b/website/photos/views.py @@ -36,18 +36,19 @@ def index(request): albums = paginator.page(paginator.num_pages) page = paginator.num_pages - page_range = range(1, paginator.num_pages + 1) - if paginator.num_pages > 7: - if page > 3: - page_range_end = paginator.num_pages - if page + 3 <= paginator.num_pages: - page_range_end = page + 3 - - page_range = range(page - 2, page_range_end) - while page_range.stop - page_range.start < 5: - page_range = range(page_range.start - 1, page_range.stop) - else: - page_range = range(1, 6) + # Show the two pages before and after the current page + page_range_start = max(1, page - 2) + page_range_stop = min(page + 3, paginator.num_pages + 1) + + # Add extra pages if we show less than 5 pages + page_range_start = min(page_range_start, page_range_stop - 5) + page_range_start = max(1, page_range_start) + + # Add extra pages if we still show less than 5 pages + page_range_stop = max(page_range_stop, page_range_start + 5) + page_range_stop = min(page_range_stop, paginator.num_pages + 1) + + page_range = range(page_range_start, page_range_stop) return render(request, 'photos/index.html', {'albums': albums, 'page_range': page_range}) -- GitLab