diff --git a/config/cache_durations.py b/config/cache_durations.py new file mode 100644 index 0000000..8d55502 --- /dev/null +++ b/config/cache_durations.py @@ -0,0 +1,12 @@ +ALBUM_PHOTO_DURATION = 60 * 60 # 1 hour +ALBUM_PHOTO_VIEWS_DURATION = 60 * 5 # 5 min + +ALBUM_LIST_PAGE_DURATION = 60 * 5 # 5 min + +PHOTO_MD_IMAGE_DATA_DURATION = 60 * 60 * 24 # 1 day + +LAST_ALBUMS_DURATION = 60 * 60 # 1 hour +TOP_PHOTOS_DURATION = 60 * 60 # 1 hour +RANDOM_FAVORITES_DURATION = 60 * 60 # 1 hour + +GALLERY_STATS_DURATION = 60 * 60 * 24 # 1 day diff --git a/gallery/cache.py b/gallery/cache.py new file mode 100644 index 0000000..3b2d824 --- /dev/null +++ b/gallery/cache.py @@ -0,0 +1,9 @@ +from django.core.cache import cache + + +def cached_or_set(key, timeout, func): + value = cache.get(key) + if value is None: + value = func() + cache.set(key, value, timeout) + return value diff --git a/gallery/models/album.py b/gallery/models/album.py index 3b8b658..32edc35 100644 --- a/gallery/models/album.py +++ b/gallery/models/album.py @@ -11,6 +11,8 @@ from django.templatetags.static import static from django.urls import reverse from django.utils.text import slugify +from config.cache_durations import * +from gallery.cache import cached_or_set from gallery.models.location import Location @@ -27,25 +29,29 @@ class Album(models.Model): @property def photos_in_album(self): + """ + Returns the number of photos in the album. + Result is cached for PHOTO_COUNT_DURATION. + """ key = self._cache_key('photo_count') - count = cache.get(key) - - if count is None: - count = self.photos.count() - cache.set(key, count, 60 * 10) # Cache 10 min - - return count + return cached_or_set( + key, + ALBUM_PHOTO_DURATION, + lambda: self.photos.count() + ) @property def photos_views(self): + """ + Returns the total number of views for all photos in the album. + Result is cached for PHOTO_VIEWS_DURATION. + """ key = self._cache_key('photo_views') - views = cache.get(key) - - if views is None: - views = self.photos.aggregate(total_views=Sum('views'))['total_views'] or 0 - cache.set(key, views, 60 * 5) # Cache 5 min - - return views + return cached_or_set( + key, + ALBUM_PHOTO_VIEWS_DURATION, + lambda: self.photos.aggregate(total_views=Sum('views'))['total_views'] or 0 + ) def save(self, *args, **kwargs): if not self.slug: diff --git a/gallery/templatetags/gallery_stats.py b/gallery/templatetags/gallery_stats.py index 0925eba..aba418c 100644 --- a/gallery/templatetags/gallery_stats.py +++ b/gallery/templatetags/gallery_stats.py @@ -1,18 +1,25 @@ from django import template from django.core.cache import cache +from config.cache_durations import * +from gallery.cache import cached_or_set from gallery.models import Album, Photo register = template.Library() + @register.simple_tag def gallery_stats(): - cache_key = "gallery_stats" - data = cache.get(cache_key) - if data is None: - total_photos = Photo.objects.count() - total_albums = Album.objects.count() - public_albums = Album.objects.filter(is_public=True).count() - data = (total_albums, public_albums, total_photos) - cache.set(cache_key, data, timeout=60 * 10) # 10 min - return data \ No newline at end of file + """ + Returns a tuple with (total albums, public albums, total photos). + Result is cached for GALLERY_STATS_DURATION. + """ + return cached_or_set( + "gallery_stats", + GALLERY_STATS_DURATION, + lambda: ( + Album.objects.count(), + Album.objects.filter(is_public=True).count(), + Photo.objects.count() + ) + ) diff --git a/gallery/templatetags/image_tags.py b/gallery/templatetags/image_tags.py index c92bae8..1cb14c5 100644 --- a/gallery/templatetags/image_tags.py +++ b/gallery/templatetags/image_tags.py @@ -1,16 +1,20 @@ from math import floor from django import template -from django.core.cache import cache from django.templatetags.static import static +from config.cache_durations import * +from gallery.cache import cached_or_set + register = template.Library() @register.filter def photo_image_data(photo): """ - Generate resized image data for Photo objects. + Returns resized image data (URL, dimensions, aspect ratio) for a Photo object. + Caches the result for PHOTO_IMAGE_DATA_DURATION. + Returns a static placeholder if photo is missing or lacks dimensions. """ if not photo: return { @@ -21,23 +25,18 @@ def photo_image_data(photo): "is_placeholder": True, } - cache_key = f'photo_md_image_data_{photo.pk}' - data = cache.get(cache_key) + def generate_data(): + max_w, max_h = 720, 720 - if data: - return data + if not photo.width or not photo.height: + return { + "url": static("img/placeholder.png"), + "width": 1200, + "height": 800, + "aspect_ratio": round(1200 / 800, 3), + "is_placeholder": True, + } - max_w, max_h = 720, 720 - - if not photo.width or not photo.height: - data = { - "url": static("img/placeholder.png"), - "width": 1200, - "height": 800, - "aspect_ratio": round(1200 / 800, 3), - "is_placeholder": True, - } - else: aspect = photo.aspect_ratio if photo.width > photo.height: w = min(photo.width, max_w) @@ -46,7 +45,7 @@ def photo_image_data(photo): h = min(photo.height, max_h) w = floor(h * aspect) - data = { + return { "url": photo.photo_md.url, "width": w, "height": h, @@ -54,5 +53,5 @@ def photo_image_data(photo): "is_placeholder": False, } - cache.set(cache_key, data, 60 * 60 * 24) - return data + cache_key = f'photo_md_image_data_{photo.pk}' + return cached_or_set(cache_key, PHOTO_MD_IMAGE_DATA_DURATION, generate_data) diff --git a/gallery/templatetags/top_tags.py b/gallery/templatetags/top_tags.py index 583f48d..6899acf 100644 --- a/gallery/templatetags/top_tags.py +++ b/gallery/templatetags/top_tags.py @@ -4,148 +4,153 @@ from django import template from django.core.cache import cache from django.db.models import F +from config.cache_durations import * +from gallery.cache import cached_or_set from gallery.models import Album, Photo register = template.Library() + @register.simple_tag def last_albums(count=5): - # Returns the latest public albums, cached for 1 hour. - cache_key = f"last_albums_{count}" - albums = cache.get(cache_key) - if albums is None: - albums = Album.objects.filter(is_public=True).order_by("-album_date")[:count] - cache.set(cache_key, albums, timeout=60 * 60) # 1h - return albums + """ + Returns the most recent public albums, limited by 'count'. + """ + return cached_or_set( + f"last_albums_{count}", + LAST_ALBUMS_DURATION, + lambda: Album.objects.filter(is_public=True).order_by("-album_date")[:count] + ) @register.simple_tag def top_photos(count=5): - # Returns top photos by is_favorite, likes, and views. - # Uses select_related to reduce DB hits. Cached for 1 hour. - cache_key = f"top_photos_{count}" - photos = cache.get(cache_key) - if photos is None: - photos = ( + """ + Returns top photos from public albums based on favorites, likes, and views. + """ + return cached_or_set( + f"top_photos_{count}", + TOP_PHOTOS_DURATION, + lambda: ( Photo.objects .filter(album__is_public=True) .order_by('-is_favorite', '-likes', '-views') .select_related('album')[:count] ) - # FIXED: previously cached as 'albums' by mistake - cache.set(cache_key, photos, timeout=60 * 60) - return photos + ) @register.simple_tag def random_photos_landscape(count=5): - # Returns random landscape-oriented photos from public albums. - # NO CACHING: always random. + """ + Returns random landscape-oriented photos from public albums. + Does not use cache; always returns new results. + """ all_ids = list( Photo.objects .filter(album__is_public=True, width__gt=F('height')) - .values_list('id', flat=True)[:500] # limit ID pool to improve performance + .values_list('id', flat=True)[:500] ) selected_ids = random.sample(all_ids, min(count, len(all_ids))) - photos = Photo.objects.filter(id__in=selected_ids) - return photos + return Photo.objects.filter(id__in=selected_ids) @register.simple_tag def random_favorite_photos_landscape(count=5): - # Returns random landscape-oriented favorite photos from public albums. - # If fewer than 'count' photos are available, returns an empty list. - # Results are cached for 1 hour for performance. - cache_key = f"random_favorite_photos_landscape_{count}" - photos = cache.get(cache_key) - if photos is None: - # Get all qualifying photos (landscape, favorite, public) + """ + Returns random landscape-oriented favorite photos from public albums. + If fewer than 'count' photos exist, returns an empty list. + Results are cached for RANDOM_FAVORITES_DURATION. + """ + key = f"random_favorite_photos_landscape_{count}" + + def fetch(): queryset = Photo.objects.filter( is_favorite=True, width__gt=F('height'), album__is_public=True ) - if queryset.count() < count: return [] + return list(queryset.order_by('?')[:count]) - # Randomize and take the requested number - photos = queryset.order_by('?')[:count] - cache.set(cache_key, list(photos), timeout=60 * 60) - - return photos + return cached_or_set(key, RANDOM_FAVORITES_DURATION, fetch) @register.simple_tag def random_favorite_photos_portrait(count=5): - # Returns random portrait-oriented favorite photos from public albums. - # If fewer than 'count' photos are available, returns an empty list. - # Results are cached for 1 hour for performance. - cache_key = f"random_favorite_photos_portrait_{count}" - photos = cache.get(cache_key) - if photos is None: - # Get all qualifying photos (portrait, favorite, public) + """ + Returns random portrait-oriented favorite photos from public albums. + If fewer than 'count' photos exist, returns an empty list. + Results are cached for RANDOM_FAVORITES_DURATION. + """ + key = f"random_favorite_photos_portrait_{count}" + + def fetch(): queryset = Photo.objects.filter( is_favorite=True, height__gt=F('width'), album__is_public=True ) - if queryset.count() < count: return [] + return list(queryset.order_by('?')[:count]) - photos = queryset.order_by('?')[:count] - cache.set(cache_key, list(photos), timeout=60 * 60) # Cache as list, not queryset - - return photos + return cached_or_set(key, RANDOM_FAVORITES_DURATION, fetch) @register.simple_tag def top_photos_landscape(count=5): - # Returns top landscape-oriented photos sorted by views. - # Uses select_related and is cached for 1 hour. - cache_key = f"top_photos_landscape_{count}" - photos = cache.get(cache_key) - if photos is None: - photos = ( + """ + Returns top landscape-oriented photos from public albums, sorted by views. + Uses select_related for optimization. + Results are cached for TOP_PHOTOS_DURATION. + """ + return cached_or_set( + f"top_photos_landscape_{count}", + TOP_PHOTOS_DURATION, + lambda: ( Photo.objects .filter(album__is_public=True, width__gt=F('height')) .order_by('-views') .select_related('album')[:count] ) - cache.set(cache_key, photos, timeout=60 * 60) - return photos + ) @register.simple_tag def top_photos_portrait(count=5): - # Returns top portrait-oriented photos sorted by views. - # Uses select_related and is cached for 1 hour. - cache_key = f"top_photos_portrait_{count}" - photos = cache.get(cache_key) - if photos is None: - photos = ( + """ + Returns top portrait-oriented photos from public albums, sorted by views. + Uses select_related for optimization. + Results are cached for TOP_PHOTOS_DURATION. + """ + return cached_or_set( + f"top_photos_portrait_{count}", + TOP_PHOTOS_DURATION, + lambda: ( Photo.objects .filter(album__is_public=True, height__gt=F('width')) .order_by('-views') .select_related('album')[:count] ) - cache.set(cache_key, photos, timeout=60 * 60) - return photos + ) @register.simple_tag def top_photos_in_album(album, count=5): - # Returns top photos in a specific album by favorite, likes, and views. - # Cached for 1 hour using album.id as cache key part. - cache_key = f"top_photos_album_{album.id}_{count}" - photos = cache.get(cache_key) - if photos is None: - photos = ( + """ + Returns top photos in a specific album, sorted by favorite, likes, and views. + Uses album ID in the cache key. + Results are cached for TOP_PHOTOS_DURATION. + """ + return cached_or_set( + f"top_photos_album_{album.id}_{count}", + TOP_PHOTOS_DURATION, + lambda: ( Photo.objects .filter(album=album) .order_by('-is_favorite', '-likes', '-views') .select_related('album')[:count] ) - cache.set(cache_key, photos, timeout=60 * 60) - return photos \ No newline at end of file + ) diff --git a/gallery/views/album.py b/gallery/views/album.py index fcb09ce..d4b36de 100644 --- a/gallery/views/album.py +++ b/gallery/views/album.py @@ -8,11 +8,17 @@ from django.urls import reverse from django.views import View from django.views.generic import DetailView, ListView, TemplateView +from config.cache_durations import * +from gallery.cache import cached_or_set + from ..models import Album, Photo, Redir class AlbumsList(ListView): - """Displays a paginated list of public albums.""" + """ + Displays a paginated list of public albums. + Queryset is cached per page for ALBUM_LIST_PAGE_DURATION. + """ model = Album template_name = 'gallery/album_list.html' paginate_by = 30 @@ -20,10 +26,11 @@ class AlbumsList(ListView): 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 = ( + return cached_or_set( + key, + ALBUM_LIST_PAGE_DURATION, + lambda: ( Album.objects.filter(is_public=True) .select_related('cover') .annotate( @@ -32,14 +39,11 @@ class AlbumsList(ListView): ) .order_by('-album_date') ) - cache.set(key, queryset, 60 * 5) - - return queryset + ) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - # Canonical_url page = self.request.GET.get('page') if page and page != '1': canonical_url = f"{self.request.build_absolute_uri(reverse('gallery:albums_url'))}?page={page}" @@ -58,7 +62,8 @@ class AlbumDetail(DetailView): template_name = 'gallery/album_detail.html' def get_object(self, queryset=None): - return get_object_or_404(Album, slug=self.kwargs.get('album_slug')) + # return get_object_or_404(Album, slug=self.kwargs.get('album_slug')) + return get_object_or_404(Album, slug__iexact=self.kwargs.get('album_slug')) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs)