diff --git a/gallery/converters.py b/gallery/converters.py deleted file mode 100644 index 3467908..0000000 --- a/gallery/converters.py +++ /dev/null @@ -1,10 +0,0 @@ -class FilenamePathConverter: - regex = '[a-zA-Z0-9_-]{1,50}\.[a-zA-Z]{3,4}$' - - def to_python(self, value): - # convert value to its corresponding python datatype - return value - - def to_url(self, value): - # convert the value to str data - return value \ No newline at end of file diff --git a/gallery/urls.py b/gallery/urls.py index aa27bf4..b7aff71 100644 --- a/gallery/urls.py +++ b/gallery/urls.py @@ -1,13 +1,13 @@ -from django.urls import path, re_path, register_converter +from django.urls import path, re_path -from . import converters, views +from . import views app_name = 'gallery' urlpatterns = [ path('', views.Main.as_view(), name='main_url'), path('about/', views.About.as_view(), name='about_url'), - path('search/', views.Search.as_view(), name='search_url'), + path('search/', views.AlbumSearch.as_view(), name='search_url'), path('photostream/', views.PhotosList.as_view(), name='photos_url'), re_path(r'^like/(?P.+)/(?P[a-zA-Z0-9_-]{1,50}\.[a-zA-Z]{3,4})/?$', views.PhotoLike.as_view(), name='photo_like_url'), re_path(r'^slideshow/(?P.+)/(?P[a-zA-Z0-9_-]{1,50}\.[a-zA-Z]{3,4})/?$', views.PhotoSlideshow.as_view(), name='photo_slideshow_url'), diff --git a/gallery/views.py b/gallery/views.py deleted file mode 100644 index 8997e38..0000000 --- a/gallery/views.py +++ /dev/null @@ -1,253 +0,0 @@ -from django.conf import settings -from django.core.cache import cache -from django.core.paginator import Paginator -from django.db.models import Count, F, Q, Sum -from django.http import JsonResponse -from django.shortcuts import get_object_or_404, redirect -from django.urls import reverse -from django.views import View -from django.views.generic import DetailView, ListView, TemplateView - -from .models import Album, Photo, Redir - - -class Main(TemplateView): - """Main homepage.""" - template_name = "gallery/main.html" - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['canonical_url'] = self.request.build_absolute_uri(reverse('gallery:main_url')) - context['latest_albums'] = Album.objects.filter(is_public=True).order_by('-album_date')[:6] - - return context - - -class AlbumsList(ListView): - """Displays a paginated list of public albums.""" - model = Album - template_name = 'gallery/album_list.html' - paginate_by = 30 - - def get_queryset(self): - page = self.request.GET.get('page', 1) - key = f'album_list_queryset_page_{page}' - queryset = cache.get(key) - - if not queryset: - queryset = ( - Album.objects.filter(is_public=True) - .select_related('cover') - .annotate( - photo_count=Count('photos'), - total_views=Sum('photos__views') - ) - .order_by('-album_date') - ) - cache.set(key, queryset, 60 * 5) - - return queryset - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - page = int(self.request.GET.get('page', 1)) - - # Canonical_url - canonical_url = self.request.build_absolute_uri(reverse('gallery:albums_url')) - if page > 1: - canonical_url += f'?page={page}' - context['canonical_url'] = canonical_url - - return context - - -class AlbumDetail(DetailView): - """Shows a single album and its paginated photos.""" - model = Album - template_name = 'gallery/album_detail.html' - - def get_object(self, queryset=None): - return get_object_or_404(Album, slug=self.kwargs.get('album_slug')) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - # photos = self.object.photos.all().order_by('taken_at') - photos = self.object.photos.all().select_related('album').order_by('taken_at') - - paginator = Paginator(photos, 30) - page_obj = paginator.get_page(self.request.GET.get('page')) - - # Canonical_url - page = int(self.request.GET.get('page', 1)) - canonical_url = self.request.build_absolute_uri(self.object.get_absolute_url()) - if page > 1: - canonical_url += f'?page={page}' - - context.update({ - 'photos': page_obj.object_list, - 'page_obj': page_obj, - 'canonical_url': canonical_url, - }) - return context - - -class PhotosList(ListView): - """Shows all public photos sorted by user-selected ordering.""" - model = Photo - paginate_by = 30 - - def get_queryset(self): - ordering_options = { - 'latest': '-taken_at', - 'liked': '-likes', - 'popular': '-views', - 'favorite': '-is_favorite' - } - order = ordering_options.get(self.request.GET.get('order', 'latest'), '-taken_at') - return Photo.objects.filter(album__is_public=True).order_by(order).select_related('album') - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['order'] = self.request.GET.get('order', 'latest') - context['canonical_url'] = self.request.build_absolute_uri(reverse('gallery:photos_url')) - return context - - -class PhotoDetail(DetailView): - """Shows a single photo and handles liking functionality.""" - model = Photo - slug_url_kwarg = 'photo_slug' - template_name = 'gallery/photo_detail.html' - - def get_object(self, queryset=None): - 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) - photo = get_object_or_404(Photo.objects.select_related('album'), slug=photo_slug, album__slug=album_slug) - - # Track views using session to avoid duplicate counts - if photo.slug not in self.request.session: - photo.add_view() - self.request.session[photo.slug] = 0 - - return photo - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context.update({ - "next": self.object.get_next(), - "prev": self.object.get_prev(), - "canonical_url": self.request.build_absolute_uri(self.object.get_absolute_url()), - "liked": self.request.session.get(f"liked_{self.object.slug}", False) - }) - return context - - -class PhotoLike(View): - """Ajax like photo button.""" - - def get(self, request, *args, **kwargs): - return JsonResponse({'error': 'This endpoint only supports POST requests.'}, status=405) - - def post(self, request, album_slug, photo_slug): - photo = get_object_or_404(Photo.objects.select_related('album'), slug=photo_slug, album__slug=album_slug) - session_key = f"liked_{photo.slug}" - liked = request.session.get(session_key, False) - - if not liked: - photo.likes += 1 - if request.user.is_authenticated: - photo.is_favorite = True - request.session[session_key] = True - status = "liked" - else: - photo.likes = max(0, photo.likes - 1) - if request.user.is_authenticated: - photo.is_favorite = False - request.session[session_key] = False - status = "unliked" - - photo.save() - return JsonResponse({'success': True, 'likes': photo.likes, 'status': status}) - - -class PhotoSlideshow(DetailView): - model = Photo - slug_url_kwarg = 'photo_slug' - template_name = 'gallery/photo_slideshow.html' - - def get_object(self, queryset=None): - album_slug = self.kwargs.get('album_slug') - photo_slug = self.kwargs.get('photo_slug') - photo = get_object_or_404( - Photo.objects.select_related('album'), - 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, **kwargs): - context = super().get_context_data(**kwargs) - current_photo = self.object - - next_photo = current_photo.get_next() - - if not next_photo: - next_photo = Photo.objects.filter(album=current_photo.album).order_by('taken_at').first() - - context.update({ - "next_photo": next_photo, - "liked": self.request.session.get(f"liked_{current_photo.slug}", False) - }) - return context - -class Search(TemplateView): - """Search view for public albums by name, place or city.""" - template_name = "gallery/album_search.html" - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - query = self.request.GET.get('q', '').strip() - context['query'] = query - - results = Album.objects.none() - if query: - results = Album.objects.filter( - Q(is_public=True), - Q(name__icontains=query) | - Q(location__place__icontains=query) | - Q(location__city__name__icontains=query) - ).order_by('-album_date').distinct() - - paginator = Paginator(results, 30) - page_obj = paginator.get_page(self.request.GET.get('page')) - - context.update({ - 'results': page_obj.object_list, - 'page_obj': page_obj, - 'paginator': paginator, - 'is_paginated': page_obj.has_other_pages(), - 'canonical_url': self.request.build_absolute_uri(reverse('gallery:search_url')) - }) - return context - - -class About(TemplateView): - """Static about page.""" - template_name = "gallery/about.html" - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['canonical_url'] = self.request.build_absolute_uri(reverse('gallery:about_url')) - return context - - -def redirect_to_album(request, redir_path): - """Handles redirect logic for shortened/legacy album URLs.""" - redir = get_object_or_404(Redir, path=redir_path) - return redirect('gallery:album_url', album_slug=redir.album.slug) diff --git a/gallery/views/__init__.py b/gallery/views/__init__.py new file mode 100644 index 0000000..06e46d2 --- /dev/null +++ b/gallery/views/__init__.py @@ -0,0 +1,3 @@ +from .album import * +from .main import * +from .photo import * diff --git a/gallery/views/album.py b/gallery/views/album.py new file mode 100644 index 0000000..8cbca2e --- /dev/null +++ b/gallery/views/album.py @@ -0,0 +1,110 @@ +from django.conf import settings +from django.core.cache import cache +from django.core.paginator import Paginator +from django.db.models import Count, F, Q, Sum +from django.http import JsonResponse +from django.shortcuts import get_object_or_404, redirect +from django.urls import reverse +from django.views import View +from django.views.generic import DetailView, ListView, TemplateView + +from ..models import Album, Photo, Redir + + +class AlbumsList(ListView): + """Displays a paginated list of public albums.""" + model = Album + template_name = 'gallery/album_list.html' + paginate_by = 30 + + def get_queryset(self): + page = self.request.GET.get('page', 1) + key = f'album_list_queryset_page_{page}' + queryset = cache.get(key) + + if not queryset: + queryset = ( + Album.objects.filter(is_public=True) + .select_related('cover') + .annotate( + photo_count=Count('photos'), + total_views=Sum('photos__views') + ) + .order_by('-album_date') + ) + cache.set(key, queryset, 60 * 5) + + return queryset + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + page = int(self.request.GET.get('page', 1)) + + # Canonical_url + canonical_url = self.request.build_absolute_uri(reverse('gallery:albums_url')) + if page > 1: + canonical_url += f'?page={page}' + context['canonical_url'] = canonical_url + + return context + + +class AlbumDetail(DetailView): + """Shows a single album and its paginated photos.""" + model = Album + template_name = 'gallery/album_detail.html' + + def get_object(self, queryset=None): + return get_object_or_404(Album, slug=self.kwargs.get('album_slug')) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + # photos = self.object.photos.all().order_by('taken_at') + photos = self.object.photos.all().select_related('album').order_by('taken_at') + + paginator = Paginator(photos, 30) + page_obj = paginator.get_page(self.request.GET.get('page')) + + # Canonical_url + page = int(self.request.GET.get('page', 1)) + canonical_url = self.request.build_absolute_uri(self.object.get_absolute_url()) + if page > 1: + canonical_url += f'?page={page}' + + context.update({ + 'photos': page_obj.object_list, + 'page_obj': page_obj, + 'canonical_url': canonical_url, + }) + return context + + +class AlbumSearch(TemplateView): + """Search view for public albums by name, place or city.""" + template_name = "gallery/album_search.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + query = self.request.GET.get('q', '').strip() + context['query'] = query + + results = Album.objects.none() + if query: + results = Album.objects.filter( + Q(is_public=True), + Q(name__icontains=query) | + Q(location__place__icontains=query) | + Q(location__city__name__icontains=query) + ).order_by('-album_date').distinct() + + paginator = Paginator(results, 30) + page_obj = paginator.get_page(self.request.GET.get('page')) + + context.update({ + 'results': page_obj.object_list, + 'page_obj': page_obj, + 'paginator': paginator, + 'is_paginated': page_obj.has_other_pages(), + 'canonical_url': self.request.build_absolute_uri(reverse('gallery:search_url')) + }) + return context diff --git a/gallery/views/main.py b/gallery/views/main.py new file mode 100644 index 0000000..4f4edfc --- /dev/null +++ b/gallery/views/main.py @@ -0,0 +1,39 @@ +from django.conf import settings +from django.core.cache import cache +from django.core.paginator import Paginator +from django.db.models import Count, F, Q, Sum +from django.http import JsonResponse +from django.shortcuts import get_object_or_404, redirect +from django.urls import reverse +from django.views import View +from django.views.generic import DetailView, ListView, TemplateView + +from ..models import Album, Photo, Redir + + +class Main(TemplateView): + """Main homepage.""" + template_name = "gallery/main.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['canonical_url'] = self.request.build_absolute_uri(reverse('gallery:main_url')) + context['latest_albums'] = Album.objects.filter(is_public=True).order_by('-album_date')[:6] + + return context + + +class About(TemplateView): + """Static about page.""" + template_name = "gallery/about.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['canonical_url'] = self.request.build_absolute_uri(reverse('gallery:about_url')) + return context + + +def redirect_to_album(request, redir_path): + """Handles redirect logic for shortened/legacy album URLs.""" + redir = get_object_or_404(Redir, path=redir_path) + return redirect('gallery:album_url', album_slug=redir.album.slug) diff --git a/gallery/views/photo.py b/gallery/views/photo.py new file mode 100644 index 0000000..a0fdb3e --- /dev/null +++ b/gallery/views/photo.py @@ -0,0 +1,126 @@ +from django.conf import settings +from django.core.cache import cache +from django.core.paginator import Paginator +from django.db.models import Count, F, Q, Sum +from django.http import JsonResponse +from django.shortcuts import get_object_or_404, redirect +from django.urls import reverse +from django.views import View +from django.views.generic import DetailView, ListView, TemplateView + +from ..models import Album, Photo, Redir + + +class PhotosList(ListView): + """Shows all public photos sorted by user-selected ordering.""" + model = Photo + paginate_by = 30 + + def get_queryset(self): + ordering_options = { + 'latest': '-taken_at', + 'liked': 'likes', + 'popular': '-views', + 'favorite': '-is_favorite' + } + order = ordering_options.get(self.request.GET.get('order', 'latest'), '-taken_at') + return Photo.objects.filter(album__is_public=True).order_by(order).select_related('album') + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['order'] = self.request.GET.get('order', 'latest') + context['canonical_url'] = self.request.build_absolute_uri(reverse('gallery:photos_url')) + return context + + +class PhotoDetail(DetailView): + """Shows a single photo and handles liking functionality.""" + model = Photo + slug_url_kwarg = 'photo_slug' + template_name = 'gallery/photo_detail.html' + + def get_object(self, queryset=None): + album_slug = self.kwargs.get('album_slug') + photo_slug = self.kwargs.get('photo_slug') + photo = get_object_or_404(Photo.objects.select_related('album'), 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, **kwargs): + context = super().get_context_data(**kwargs) + context.update({ + "next": self.object.get_next(), + "prev": self.object.get_prev(), + "canonical_url": self.request.build_absolute_uri(self.object.get_absolute_url()), + "liked": self.request.session.get(f"liked_{self.object.slug}", False) + }) + return context + + +class PhotoLike(View): + """Ajax like photo button.""" + + def get(self, request, *args, **kwargs): + return JsonResponse({'error': 'This endpoint only supports POST requests.'}, status=405) + + def post(self, request, album_slug, photo_slug): + photo = get_object_or_404(Photo.objects.select_related('album'), slug=photo_slug, album__slug=album_slug) + session_key = f"liked_{photo.slug}" + liked = request.session.get(session_key, False) + + if not liked: + photo.likes += 1 + if request.user.is_authenticated: + photo.is_favorite = True + request.session[session_key] = True + status = "liked" + else: + photo.likes = max(0, photo.likes - 1) + if request.user.is_authenticated: + photo.is_favorite = False + request.session[session_key] = False + status = "unliked" + + photo.save() + return JsonResponse({'success': True, 'likes': photo.likes, 'status': status}) + + +class PhotoSlideshow(DetailView): + """Slideshow.""" + model = Photo + slug_url_kwarg = 'photo_slug' + template_name = 'gallery/photo_slideshow.html' + + def get_object(self, queryset=None): + album_slug = self.kwargs.get('album_slug') + photo_slug = self.kwargs.get('photo_slug') + photo = get_object_or_404( + Photo.objects.select_related('album'), + 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, **kwargs): + context = super().get_context_data(**kwargs) + current_photo = self.object + + next_photo = current_photo.get_next() + + if not next_photo: + next_photo = Photo.objects.filter(album=current_photo.album).order_by('taken_at').first() + + context.update({ + "next_photo": next_photo, + "liked": self.request.session.get(f"liked_{current_photo.slug}", False) + }) + return context