Merge branch 'AddCache' into 'master'

Add cache & optimise SQL requests

See merge request bde/photo21!33
This commit is contained in:
loulous27 2025-11-23 15:13:56 +01:00
commit 90c7bdfd38
5 changed files with 81 additions and 17 deletions

View file

@ -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",)

View file

@ -37,6 +37,11 @@ ALLOWED_HOSTS = [
"photos-dev.crans.org", "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 receive server errors, this is useful to be notified of potential bugs
ADMINS = [ ADMINS = [
("admin", "photos-admin@lists.crans.org"), ("admin", "photos-admin@lists.crans.org"),
@ -68,9 +73,12 @@ INSTALLED_APPS = [
"allauth_note_kfet", "allauth_note_kfet",
"crispy_forms", "crispy_forms",
"photologue", "photologue",
"photo21" "photo21",
] ]
if DEBUG:
INSTALLED_APPS += ["debug_toolbar",] # For debug and optimisations
MIDDLEWARE = [ MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware", "django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware", "django.contrib.sessions.middleware.SessionMiddleware",
@ -81,8 +89,10 @@ MIDDLEWARE = [
"django.middleware.clickjacking.XFrameOptionsMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware",
"django.middleware.locale.LocaleMiddleware", "django.middleware.locale.LocaleMiddleware",
"django.contrib.sites.middleware.CurrentSiteMiddleware", "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" ROOT_URLCONF = "photo21.urls"
@ -122,6 +132,13 @@ DATABASES = {
} }
} }
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
"LOCATION": "Master",
}
}
# Password validation # Password validation
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
@ -206,11 +223,11 @@ MESSAGE_TAGS = {
} }
# Allauth configuration # Allauth configuration
# ACCOUNT_EMAIL_REQUIRED = True ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_SIGNUP_FIELDS = ['email*', 'username*', 'password1*', 'password2*'] # ACCOUNT_SIGNUP_FIELDS = ['email*', 'username*', 'password1*', 'password2*']
ACCOUNT_EMAIL_VERIFICATION = "mandatory" ACCOUNT_EMAIL_VERIFICATION = "mandatory"
# ACCOUNT_AUTHENTICATION_METHOD = "username_email" ACCOUNT_AUTHENTICATION_METHOD = "username_email"
ACCOUNT_LOGIN_METHODS = {'username', 'email'} # ACCOUNT_LOGIN_METHODS = {'username', 'email'}
ACCOUNT_FORMS = {"signup": "photo21.forms.CustomSignupForm"} ACCOUNT_FORMS = {"signup": "photo21.forms.CustomSignupForm"}
SOCIALACCOUNT_PROVIDERS = { SOCIALACCOUNT_PROVIDERS = {
"notekfet": { "notekfet": {

View file

@ -13,6 +13,9 @@ from django.conf.urls.static import static
from django.contrib import admin from django.contrib import admin
from django.urls import include, path, re_path from django.urls import include, path, re_path
if settings.DEBUG :
from debug_toolbar.toolbar import debug_toolbar_urls
from .views import IndexView, MediaAccess from .views import IndexView, MediaAccess
urlpatterns = [ urlpatterns = [
@ -27,6 +30,7 @@ urlpatterns = [
# In production media are served through NGINX with X-Accel-Redirect # In production media are served through NGINX with X-Accel-Redirect
if settings.DEBUG: if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += debug_toolbar_urls()
else: else:
urlpatterns.append( urlpatterns.append(
re_path("^media/(?P<path>.*)", MediaAccess.as_view(), name="media") re_path("^media/(?P<path>.*)", MediaAccess.as_view(), name="media")

View file

@ -3,7 +3,6 @@
import logging import logging
import os import os
import random
import unicodedata import unicodedata
from datetime import datetime from datetime import datetime
from functools import partial from functools import partial
@ -16,6 +15,7 @@ from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from django.core.cache import caches
from django.db import models from django.db import models
from django.template.defaultfilters import slugify from django.template.defaultfilters import slugify
from django.urls import reverse from django.urls import reverse
@ -25,6 +25,8 @@ from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from PIL import Image, ImageFile, ImageFilter from PIL import Image, ImageFile, ImageFilter
logger = logging.getLogger("photologue.models") logger = logging.getLogger("photologue.models")
# Default limit for gallery.latest # Default limit for gallery.latest
@ -184,13 +186,15 @@ class Gallery(models.Model):
def sample(self, public=True): def sample(self, public=True):
"""Return a sample of photos, ordered at random.""" """Return a sample of photos, ordered at random."""
count = 1 count = 1
if count > self.photo_count(): nb = self.photo_count(public) #Optimisation don't do twice the SQL requests
count = self.photo_count() if nb < count:
count = nb
if public: if public:
photo_set = self.photos.filter(is_public=True) photo_set = self.photos.filter(is_public=True)
else: else:
photo_set = self.photos 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): def photo_count(self, public=True):
"""Return a count of all the photos in this gallery.""" """Return a count of all the photos in this gallery."""
@ -721,10 +725,16 @@ class PhotoSizeCache:
def __init__(self): def __init__(self):
self.__dict__ = self.__state self.__dict__ = self.__state
cached = caches.get("PhotoSizeCache",None)
if cached is None :
if not len(self.sizes): if not len(self.sizes):
sizes = PhotoSize.objects.all() sizes = PhotoSize.objects.all()
for size in sizes: for size in sizes:
self.sizes[size.name] = size self.sizes[size.name] = size
caches.set("PhotoSizeCache",self)
else :
self = cached
def reset(self): def reset(self):
global size_method_map global size_method_map

View file

@ -20,10 +20,16 @@ from django.views.generic.dates import ArchiveIndexView, YearArchiveView
from django.views.generic.detail import DetailView from django.views.generic.detail import DetailView
from django.views.generic.edit import DeleteView, FormView from django.views.generic.edit import DeleteView, FormView
from PIL import Image from PIL import Image
from django.contrib.auth import get_user_model
from .forms import UploadForm from .forms import UploadForm
from .models import Gallery, Photo, Tag 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): class GalleryDateView(LoginRequiredMixin):
model = Gallery model = Gallery
@ -135,11 +141,11 @@ class GalleryDetailView(LoginRequiredMixin, DetailView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**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: if self.request.user.is_staff:
context["photos"] = self.object.photos.all() context["photos"] = self.object.photos.all().select_related('owner')
else: else:
context["photos"] = self.object.photos.filter(is_public=True) context["photos"] = self.object.photos.filter(is_public=True).select_related('onwer')
# List owners # List owners
context["owners"] = [] context["owners"] = []
@ -147,6 +153,8 @@ class GalleryDetailView(LoginRequiredMixin, DetailView):
if photo.owner not in context["owners"]: if photo.owner not in context["owners"]:
context["owners"].append(photo.owner) context["owners"].append(photo.owner)
# Filter on owner # Filter on owner
if "owner" in self.kwargs: if "owner" in self.kwargs:
context["photos"] = context["photos"].filter(owner__id=self.kwargs["owner"]) context["photos"] = context["photos"].filter(owner__id=self.kwargs["owner"])