diff --git a/backend/edegal/models/album.py b/backend/edegal/models/album.py index a5e0bec..ef43412 100644 --- a/backend/edegal/models/album.py +++ b/backend/edegal/models/album.py @@ -539,6 +539,48 @@ def get_album_by_path(cls, path, or_404=False, **extra_criteria): else: return queryset.get() + @classmethod + def resolve_upstream_redirects(cls, path, **extra_criteria): + """ + Given a path, if there is a redirect higher up in the hierarchy, return a fake album dict + that redirects to the correct path. Otherwise return None. + """ + # TODO cache? + parts = path.strip("/").split("/") + rewritten_parts = [] + rewrites_done = False + + for part in parts: + original_path = "/" + "/".join(rewritten_parts + [part]) + + try: + album = cls.get_album_by_path(original_path, **extra_criteria) + except cls.DoesNotExist: + return None + + if redirect_url := album.redirect_url: + if redirect_url.startswith("/"): + # Local album redirect – resolve further + rewritten_parts = album.redirect_url.strip("/").split("/") + rewrites_done = True + else: + # External redirect – can't resolve further + return cls.fake_album_as_dict(path=path, redirect_url=redirect_url) + elif album.path == original_path: + # path refers to an album + rewritten_parts.append(album.slug) + else: + # path refers to a picture or a technical view + rewritten_parts.append(part) + + if rewrites_done: + return cls.fake_album_as_dict( + path=path, + redirect_url="/" + "/".join(rewritten_parts), + ) + + return None + def get_download_file_path(self, prefix=settings.MEDIA_ROOT + "/"): return f"{prefix}downloads{self.path}.zip" diff --git a/backend/edegal/models/series.py b/backend/edegal/models/series.py index 560fabf..8d6d39e 100644 --- a/backend/edegal/models/series.py +++ b/backend/edegal/models/series.py @@ -45,6 +45,8 @@ class Series(AlbumMixin, models.Model): settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.SET_NULL ) + redirect_url = "" + def save(self, *args, **kwargs): if self.title and not self.slug: self.slug = slugify(self.title) diff --git a/backend/edegal/tests.py b/backend/edegal/tests.py index ac7f486..ea4d165 100644 --- a/backend/edegal/tests.py +++ b/backend/edegal/tests.py @@ -2,49 +2,49 @@ from django.test import TestCase -from .models import Album, Picture, MediaSpec, Media +from .models import Album, Media, MediaSpec, Picture class AlbumTestCase(TestCase): def setUp(self): root, unused = Album.objects.get_or_create( - path='', + path="", defaults=dict( - title='My Swell Picture Gallery', + title="My Swell Picture Gallery", ), ) album1, unused = Album.objects.get_or_create( - path='/album-1', + path="/album-1", defaults=dict( - title='Album, the First of his Name', - slug='album-1', + title="Album, the First of his Name", + slug="album-1", parent=root, - ) + ), ) album2, unused = Album.objects.get_or_create( - path='/album-2', + path="/album-2", defaults=dict( - title='Album 2', + title="Album 2", parent=root, - ) + ), ) picture1, unused = Picture.objects.get_or_create( - path='/album-1/picture-1', + path="/album-1/picture-1", defaults=dict( - title='Picture 1', + title="Picture 1", album=album2, - ) + ), ) picture2, unused = Picture.objects.get_or_create( - path='/album-1/picture-2', + path="/album-1/picture-2", defaults=dict( - title='Picture 2', + title="Picture 2", album=album2, - ) + ), ) original_media, unused = Media.objects.get_or_create( @@ -52,10 +52,10 @@ def setUp(self): spec=None, defaults=dict( src=str(uuid4()), - role='original', + role="original", width=800, height=600, - ) + ), ) spec, unused = MediaSpec.objects.get_or_create( @@ -69,28 +69,69 @@ def setUp(self): spec=spec, defaults=dict( src=str(uuid4()), - role='thumbnail', + role="thumbnail", width=spec.max_width, height=spec.max_height, - ) + ), ) def test_get_album_by_path(self): - album = Album.get_album_by_path('/album-1') - self.assertEqual(album.path, '/album-1') + album = Album.get_album_by_path("/album-1") + self.assertEqual(album.path, "/album-1") - album = Album.get_album_by_path('/album-2/picture-2') - self.assertEqual(album.path, '/album-2') + album = Album.get_album_by_path("/album-2/picture-2") + self.assertEqual(album.path, "/album-2") def test_as_dict(self): - album = Album.get_album_by_path('/album-2') + album = Album.get_album_by_path("/album-2") print(album.as_dict()) def test_canonical_path(self): - picture1 = Picture.objects.get(path='/album-2/picture-1') + picture1 = Picture.objects.get(path="/album-2/picture-1") original = picture1.original - self.assertEqual(original.get_canonical_path(prefix=''), 'pictures/album-2/picture-1.jpeg') + self.assertEqual( + original.get_canonical_path(prefix=""), "pictures/album-2/picture-1.jpeg" + ) derived = picture1.media.get(spec__max_width=640, spec__max_height=480) - self.assertEqual(derived.get_canonical_path(prefix=''), 'previews/album-2/picture-1.thumbnail.jpeg') + self.assertEqual( + derived.get_canonical_path(prefix=""), + "previews/album-2/picture-1.thumbnail.jpeg", + ) + + def test_resolve_redirects(self): + """ + If an upstream album has a redirect URL to another album in this gallery, + handle its descendants as if they were in the target album. + """ + root_album = Album.objects.get(path="/") + + Album.objects.create( + title="Album 3", + redirect_url="/album-4", + parent=root_album, + ) + + album4 = Album.objects.create( + title="Album 4", + parent=root_album, + ) + + child_album = Album.objects.create( + title="Child Album", + parent=album4, + ) + + Picture.objects.create( + album=child_album, + title="Picture", + ) + + album_dict = Album.resolve_upstream_redirects("/album-3/child-album") + assert album_dict is not None + self.assertEqual(album_dict["path"], "/album-4/child-album") + + album_dict = Album.resolve_upstream_redirects("/album-3/child-album/picture") + assert album_dict is not None + self.assertEqual(album_dict["path"], "/album-4/child-album/picture") diff --git a/backend/edegal/views.py b/backend/edegal/views.py index 21e0c7f..4c34483 100644 --- a/backend/edegal/views.py +++ b/backend/edegal/views.py @@ -5,7 +5,6 @@ from .models import Album, Photographer, Picture from .models.media_spec import FORMAT_CHOICES - SUPPORTED_FORMATS = {format for (format, disp) in FORMAT_CHOICES} @@ -31,7 +30,19 @@ def get(self, request, path): if not request.user.is_staff: extra_criteria.update(is_public=True) - album = Album.get_album_by_path(path, or_404=True, **extra_criteria) + try: + album = Album.get_album_by_path(path, **extra_criteria) + except Album.DoesNotExist: + if redirect_dict := Album.resolve_upstream_redirects( + path, **extra_criteria + ): + return JsonResponse(redirect_dict) + else: + return JsonResponse( + {"status": 404, "message": "album not found"}, + status=404, + ) + response = JsonResponse( album.as_dict( include_hidden=request.user.is_staff, @@ -59,7 +70,9 @@ def get(self, request): body=pseudoalbum.body if pseudoalbum else "", subalbums=[ photog.make_subalbum() - for photog in Photographer.objects.filter(cover_picture__media__isnull=False).distinct() + for photog in Photographer.objects.filter( + cover_picture__media__isnull=False + ).distinct() ], pictures=[], breadcrumb=[