From f1673da45fa04da30db5d9ef58c05a35f1f27f0f Mon Sep 17 00:00:00 2001 From: loulous27 Date: Sun, 23 Nov 2025 14:43:49 +0100 Subject: [PATCH] =?UTF-8?q?Caches=20=C3=A9=20SQL=20optimisations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Scripts/small_pictures_gen.py | 25 +++++++++++++++++++++++++ photo21/settings.py | 16 +++++++++++++--- photo21/urls.py | 2 ++ photologue/models.py | 26 ++++++++++++++++++-------- photologue/views.py | 14 +++++++++++--- 5 files changed, 69 insertions(+), 14 deletions(-) create mode 100644 Scripts/small_pictures_gen.py diff --git a/Scripts/small_pictures_gen.py b/Scripts/small_pictures_gen.py new file mode 100644 index 0000000..0bf84ef --- /dev/null +++ b/Scripts/small_pictures_gen.py @@ -0,0 +1,25 @@ +"""A generator of small picture to test very larges galeries""" + +from PIL import Image +from PIL import ImageDraw +import argparse + + +parser = argparse.ArgumentParser( + description="A generator of small picture to test very larges galeries" +) + +parser.add_argument("count", help="Numbers of photo to generate", type=int) +parser.add_argument( + "-outputfolder", help="The outputfolders by default : ./photos/", default="photos/" +) + +args = parser.parse_args() + +for i in range(args.count): + if (100//5 * (i + 1)) % args.count == 0: # affichage tout les 5% + print(f"Image {i+1} : {(i+1)/args.count:.0%}") + + img = Image.new(mode="RGB",size=(100,100),color=(0,0,0)) + ImageDraw.Draw(img).text((0,0),str(i+1),(255,255,255)) + img.save(args.outputfolder+f"img_{i+1}.jpg",) diff --git a/photo21/settings.py b/photo21/settings.py index ce77e2d..5bf6228 100644 --- a/photo21/settings.py +++ b/photo21/settings.py @@ -28,7 +28,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) SECRET_KEY = "CHANGE ME" # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = False +DEBUG = True ALLOWED_HOSTS = [ "127.0.0.1", @@ -37,6 +37,11 @@ ALLOWED_HOSTS = [ "photos-dev.crans.org", ] +INTERNAL_IPS = [ + "127.0.0.1", + "localhost", +] + # Admins receive server errors, this is useful to be notified of potential bugs ADMINS = [ ("admin", "photos-admin@lists.crans.org"), @@ -68,9 +73,12 @@ INSTALLED_APPS = [ "allauth_note_kfet", "crispy_forms", "photologue", - "photo21" + "photo21", ] +if DEBUG: + INSTALLED_APPS += ["debug_toolbar",] # For debug and optimisations + MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", @@ -81,8 +89,10 @@ MIDDLEWARE = [ "django.middleware.clickjacking.XFrameOptionsMiddleware", "django.middleware.locale.LocaleMiddleware", "django.contrib.sites.middleware.CurrentSiteMiddleware", - "allauth.account.middleware.AccountMiddleware", + "allauth.account.middleware.AccountMiddleware", # For the django =< 5.0 ] +if DEBUG : + MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware",] ROOT_URLCONF = "photo21.urls" diff --git a/photo21/urls.py b/photo21/urls.py index 8c4d5dd..79220f3 100644 --- a/photo21/urls.py +++ b/photo21/urls.py @@ -12,6 +12,7 @@ from django.conf import settings from django.conf.urls.static import static from django.contrib import admin from django.urls import include, path, re_path +from debug_toolbar.toolbar import debug_toolbar_urls from .views import IndexView, MediaAccess @@ -27,6 +28,7 @@ urlpatterns = [ # In production media are served through NGINX with X-Accel-Redirect if settings.DEBUG: urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + urlpatterns += debug_toolbar_urls() else: urlpatterns.append( re_path("^media/(?P.*)", MediaAccess.as_view(), name="media") diff --git a/photologue/models.py b/photologue/models.py index 8b7dbcd..9c308bd 100644 --- a/photologue/models.py +++ b/photologue/models.py @@ -3,7 +3,6 @@ import logging import os -import random import unicodedata from datetime import datetime from functools import partial @@ -16,6 +15,7 @@ from django.conf import settings from django.core.exceptions import ValidationError from django.core.files.base import ContentFile from django.core.validators import RegexValidator +from django.core.cache import caches from django.db import models from django.template.defaultfilters import slugify from django.urls import reverse @@ -25,6 +25,8 @@ from django.utils.timezone import now from django.utils.translation import gettext_lazy as _ from PIL import Image, ImageFile, ImageFilter + + logger = logging.getLogger("photologue.models") # Default limit for gallery.latest @@ -184,13 +186,15 @@ class Gallery(models.Model): def sample(self, public=True): """Return a sample of photos, ordered at random.""" count = 1 - if count > self.photo_count(): - count = self.photo_count() + nb = self.photo_count(public) #Optimisation don't do twice the SQL requests + if nb < count: + count = nb + if public: photo_set = self.photos.filter(is_public=True) else: photo_set = self.photos - return random.sample(list(photo_set), count) + return photo_set.order_by("?")[:count] # Use native SQL random def photo_count(self, public=True): """Return a count of all the photos in this gallery.""" @@ -721,10 +725,16 @@ class PhotoSizeCache: def __init__(self): self.__dict__ = self.__state - if not len(self.sizes): - sizes = PhotoSize.objects.all() - for size in sizes: - self.sizes[size.name] = size + + cached = caches.get("PhotoSizeCache",None) + if cached is None : + if not len(self.sizes): + sizes = PhotoSize.objects.all() + for size in sizes: + self.sizes[size.name] = size + caches.set("PhotoSizeCache",self) + else : + self = cached def reset(self): global size_method_map diff --git a/photologue/views.py b/photologue/views.py index 4a0248c..5a63e54 100644 --- a/photologue/views.py +++ b/photologue/views.py @@ -20,10 +20,16 @@ from django.views.generic.dates import ArchiveIndexView, YearArchiveView from django.views.generic.detail import DetailView from django.views.generic.edit import DeleteView, FormView from PIL import Image +from django.contrib.auth import get_user_model + + from .forms import UploadForm from .models import Gallery, Photo, Tag +# Cette ligne renvoie le modèle d'utilisateur actif (le natif ou le vôtre) +User = get_user_model() + class GalleryDateView(LoginRequiredMixin): model = Gallery @@ -135,11 +141,11 @@ class GalleryDetailView(LoginRequiredMixin, DetailView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - # Non-staff members only see public photos + # Non-staff members only see public photos + prefetch all owners informations (Optimisation) if self.request.user.is_staff: - context["photos"] = self.object.photos.all() + context["photos"] = self.object.photos.all().select_related('owner') else: - context["photos"] = self.object.photos.filter(is_public=True) + context["photos"] = self.object.photos.filter(is_public=True).select_related('onwer') # List owners context["owners"] = [] @@ -147,6 +153,8 @@ class GalleryDetailView(LoginRequiredMixin, DetailView): if photo.owner not in context["owners"]: context["owners"].append(photo.owner) + + # Filter on owner if "owner" in self.kwargs: context["photos"] = context["photos"].filter(owner__id=self.kwargs["owner"])