From f3a94857d0cf5c1e4b5242eb821016cdb315a311 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9DNyymix=E2=80=9D?= Date: Wed, 15 Jan 2025 20:31:41 +0200 Subject: [PATCH] Add likes, update templates --- gallery/admin.py | 21 ++++++++++- gallery/migrations/0001_initial.py | 8 +++-- gallery/models/album.py | 14 +++++++- gallery/models/photo.py | 7 ++++ gallery/templates/gallery/album_list.html | 3 +- gallery/templates/gallery/photo_detail.html | 35 ++++++++++++++++-- gallery/views.py | 37 +++++++++++++++----- static/img/placeholder.png | Bin 0 -> 6146 bytes 8 files changed, 109 insertions(+), 16 deletions(-) create mode 100644 static/img/placeholder.png diff --git a/gallery/admin.py b/gallery/admin.py index 2b7040f..8cec1db 100644 --- a/gallery/admin.py +++ b/gallery/admin.py @@ -1,4 +1,5 @@ from django.contrib import admin +from django.utils.html import format_html from imagekit import ImageSpec from imagekit.admin import AdminThumbnail from imagekit.cachefiles import ImageCacheFile @@ -36,16 +37,34 @@ class LocationAdmin(admin.ModelAdmin): class AlbumAdmin(admin.ModelAdmin): prepopulated_fields = {'slug': ('name',)} - list_display = ('__str__', 'location', 'album_date', 'is_public', ) + list_display = ('__str__', 'location', 'album_date', 'is_public', 'thumbnail', ) search_fields = ('name',) ordering = ('name',) list_per_page = 30 list_editable = ('is_public', 'location') + def formfield_for_foreignkey(self, db_field, request, **kwargs): + if db_field.name == "cover": + if hasattr(request, 'resolver_match') and request.resolver_match.kwargs.get('object_id'): + album_id = request.resolver_match.kwargs['object_id'] + kwargs["queryset"] = Photo.objects.filter(album_id=album_id) + return super().formfield_for_foreignkey(db_field, request, **kwargs) + + def thumbnail(self, obj): + if obj.cover and obj.cover.photo: + return format_html( + '', + obj.cover.photo.url, + ) + return "-" + thumbnail.short_description = "Thumbnail" + + class PhotoAdmin(admin.ModelAdmin): list_display = ('slug', 'album', 'admin_thumbnail',) list_display_links = ('slug',) + search_fields = ('slug',) readonly_fields = ['slug', 'taken_at', 'height', 'width', 'exif', ] admin_thumbnail = AdminThumbnail(image_field=cached_admin_thumb) diff --git a/gallery/migrations/0001_initial.py b/gallery/migrations/0001_initial.py index e817847..0a71445 100644 --- a/gallery/migrations/0001_initial.py +++ b/gallery/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.4 on 2025-01-13 20:45 +# Generated by Django 5.1.4 on 2025-01-15 18:22 import datetime import django.db.models.deletion @@ -43,7 +43,6 @@ class Migration(migrations.Migration): ('name', models.CharField(max_length=150, unique=True, verbose_name='Album')), ('slug', models.SlugField(max_length=150, unique=True, verbose_name='Slug')), ('album_date', models.DateField(default=datetime.datetime.now, verbose_name='Album Date')), - ('cover', models.ImageField(blank=True, null=True, upload_to='covers/', verbose_name='Album Cover')), ('is_public', models.BooleanField(default=False, verbose_name='Published')), ('location', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='albums', to='gallery.location', verbose_name='Location')), ], @@ -67,4 +66,9 @@ class Migration(migrations.Migration): ('album', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='photos', to='gallery.album', verbose_name='Album')), ], ), + migrations.AddField( + model_name='album', + name='cover', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='cover_to', to='gallery.photo', verbose_name='Album cover'), + ), ] diff --git a/gallery/models/album.py b/gallery/models/album.py index 8c3267b..c084f5d 100644 --- a/gallery/models/album.py +++ b/gallery/models/album.py @@ -1,23 +1,35 @@ from datetime import datetime from django.db import models +from django.templatetags.static import static from django.urls import reverse from django.utils.text import slugify from gallery.models.location import Location +#from gallery.models.photo import Photo + class Album(models.Model): name = models.CharField(max_length=150, unique=True, verbose_name="Album") slug = models.SlugField(max_length=150, unique=True, verbose_name="Slug") location = models.ForeignKey(Location, blank=True, null=True, on_delete=models.SET_NULL, related_name='albums', verbose_name="Location") album_date = models.DateField(default=datetime.now, verbose_name="Album Date") - cover = models.ImageField(upload_to="covers/", blank=True, null=True, verbose_name="Album Cover") + cover = models.ForeignKey("Photo", blank=True, null=True, on_delete=models.SET_NULL, related_name='cover_to', verbose_name="Album cover") is_public = models.BooleanField(default=False, verbose_name="Published") @property def photos_in_album(self): return self.photos.count() + + @property + def cover_url(self): + if self.cover: + return self.cover.photo.url + random_cover = self.photos.order_by('-width').first() + if random_cover: + return random_cover.photo.url + return static('img/placeholder.png') def save(self, *args, **kwargs): if not self.slug: diff --git a/gallery/models/photo.py b/gallery/models/photo.py index 867ad1f..c49759e 100644 --- a/gallery/models/photo.py +++ b/gallery/models/photo.py @@ -43,6 +43,13 @@ class Photo(models.Model): self.views += 1 self.save() + def get_next(self): + return self.__class__.objects.filter(taken_at__gt=self.taken_at, album=self.album.id).order_by('taken_at').first() + + def get_prev(self): + return self.__class__.objects.filter(taken_at__lt=self.taken_at, album=self.album.id).order_by('-taken_at').first() + + def save(self, *args, **kwargs): self.exif_data = Exif(self.photo.path) datetime_taken = getattr(self.exif_data, 'datetimeoriginal', datetime.now)() diff --git a/gallery/templates/gallery/album_list.html b/gallery/templates/gallery/album_list.html index 3d93c1b..5b92ae2 100644 --- a/gallery/templates/gallery/album_list.html +++ b/gallery/templates/gallery/album_list.html @@ -1,4 +1,5 @@ {% extends "base.html" %} +{% load static %} {% block title %} Gallery : Albums {% endblock %} @@ -18,7 +19,7 @@
diff --git a/gallery/templates/gallery/photo_detail.html b/gallery/templates/gallery/photo_detail.html index 5a34f7a..a8b5806 100644 --- a/gallery/templates/gallery/photo_detail.html +++ b/gallery/templates/gallery/photo_detail.html @@ -14,8 +14,37 @@ {% block content %} -

{{ photo.slug }}

-

Views: {{ photo.views }}

- {{ photo.slug }} +
+
+
+ + {{ photo.slug }} + +
+
+ +
+ {% if prev %} + « Prev + {% endif %} + {% if next %} + Next » + {% endif %} +

Views: {{ photo.views }}

+

Likes: {{ photo.likes }}

+

Taken: {{ photo.taken_at }}

+

Favorite: {{ photo.favorite }}

+ + Download {{ photo.width }}x{{ photo.height }} + +
+ {% csrf_token %} + +
+ +
+
{% endblock %} \ No newline at end of file diff --git a/gallery/views.py b/gallery/views.py index aaf161f..4b89074 100644 --- a/gallery/views.py +++ b/gallery/views.py @@ -46,18 +46,39 @@ class PhotosList(ListView): paginate_by = 30 ordering = ['-taken_at'] - + class PhotoDetail(DetailView): model = Photo slug_url_kwarg = 'photo_slug' template_name = 'gallery/photo_detail.html' def get_object(self, queryset=None): - photo = get_object_or_404(Photo, slug=self.kwargs.get(self.slug_url_kwarg)) - - photo_key = f'photo_{photo.slug}' - if photo_key not in self.request.session: - photo.add_view() - self.request.session[photo_key] = 0 - + album_slug = self.kwargs.get('album_slug') + photo_slug = self.kwargs.get('photo_slug') + photo = get_object_or_404(Photo, slug=photo_slug, album__slug=album_slug) + + if photo.slug not in self.request.session: + photo.add_view() + self.request.session[photo.slug] = 0 + return photo + + def get_context_data(self, *args, **kwargs): + context = super().get_context_data(*args, **kwargs) + context["next"] = self.object.get_next() + context["prev"] = self.object.get_prev() + return context + + def post(self, request, *args, **kwargs): + photo = self.get_object() + + if request.POST.get("like") == "true": + if self.request.session.get(photo.slug) == 0: + photo.add_like() + self.request.session[photo.slug] = 1 + + if request.user.is_authenticated: + photo.is_favorite = True + photo.save() + + return redirect('gallery:photo_url', album_slug=self.kwargs['album_slug'], photo_slug=self.kwargs['photo_slug']) diff --git a/static/img/placeholder.png b/static/img/placeholder.png new file mode 100644 index 0000000000000000000000000000000000000000..bf1d31046f6bb3ca7699ba3bc40b953380d1b754 GIT binary patch literal 6146 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 bOZa=5ECsvRPtgOVfAL0oX1c{_EdIX$Go=nF literal 0 HcmV?d00001