photo26/photologue/views.py
2025-12-04 01:26:36 +01:00

295 lines
9.7 KiB
Python

# This file is part of photo21
# Copyright (C) 2021-2022 Amicale des élèves de l'ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import os
import zipfile
from io import BytesIO
from pathlib import Path
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.core.mail import mail_admins
from django.db import IntegrityError
from django.http import HttpResponse
from django.http import JsonResponse
from django.shortcuts import redirect
from django.urls import reverse_lazy
from django.utils.text import slugify
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 django.db.models import F
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
date_field = "date_start"
allow_empty = True # Do not 404 if no galleries
def get_queryset(self):
"""Hide galleries with only private photos"""
qs = super().get_queryset()
if self.request.user.is_staff:
return qs
else:
return qs.filter(photos__is_public=True).distinct()
def get_context_data(self, **kwargs):
"""Always show all years in archive"""
context = super().get_context_data(**kwargs)
context["date_list"] = self.get_queryset().dates(
self.date_field, "year", "DESC"
)
return context
class GalleryArchiveIndexView(GalleryDateView, ArchiveIndexView):
pass
class GalleryYearArchiveView(GalleryDateView, YearArchiveView):
make_object_list = True
class PhotoDetailView(LoginRequiredMixin, DetailView):
model = Photo
def get_queryset(self):
"""Non-staff members only see public photos"""
qs = super().get_queryset()
if self.request.user.is_staff:
return qs
else:
return qs.filter(is_public=True)
class PhotoDeleteView(PermissionRequiredMixin, DeleteView):
model = Photo
permission_required = "photologue.delete_photo"
def get_success_url(self):
galleries = self.object.galleries.all()
if not galleries:
return reverse_lazy("photologue:pl-gallery-archive")
slug = galleries[0].slug
return reverse_lazy("photologue:pl-gallery", args=[slug])
class PhotoReportView(LoginRequiredMixin, DetailView):
model = Photo
template_name = "photologue/photo_confirm_report.html"
def post(self, request, *args, **kwargs):
"""
Make photo private on POST.
"""
# Mark photo as private
photo = self.get_object()
photo.is_public = False
photo.save()
# Get gallery
galleries = photo.galleries.all()
gallery_slug = galleries[0].slug if galleries else ""
if not gallery_slug:
url = reverse_lazy("photologue:pl-gallery-archive")
url = reverse_lazy("photologue:pl-gallery", args=[gallery_slug])
url = request.build_absolute_uri(url)
# Send mail to managers
mail_admins(
subject=f"Abuse report for photo id {photo.pk}",
message=f"{self.request.user.username} reported an abuse for `{photo.title}`: {url}#lg=1&slide={photo.pk}",
)
# Redirect to gallery
return redirect(url)
class TagDetail(LoginRequiredMixin, DetailView):
model = Tag
def get_context_data(self, **kwargs):
"""
Insert the single object into the context dict.
"""
current_tag = self.get_object().slug
context = super().get_context_data(**kwargs)
context["galleries"] = Gallery.objects.filter(tags__slug=current_tag).order_by(
"-date_start"
)
return context
class GalleryDetailView(LoginRequiredMixin, DetailView):
"""
Gallery detail view to filter on photo owner
"""
model = Gallery
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Non-staff members only see public photos + prefetch all owners informations (Optimisation)
if self.request.user.is_staff:
context["photos"] = self.object.photos.all().select_related("owner")
else:
context["photos"] = self.object.photos.filter(
is_public=True
).select_related("owner")
# List owners
context["owners"] = []
# owners_pk_distinct = context["photos"].order_by('owner__pk').values_list('owner__pk', flat=True).distinct()
# context["owners"] = User.objects.filter(pk__in=owners_pk_distinct)
for photo in context["photos"]:
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"])
#Increment the photo view count
context["photos"].update(view_count=F('view_count') + 1)
return context
class GalleryDownload(LoginRequiredMixin, DetailView):
model = Gallery
def get(self, request, *args, **kwargs):
"""
Download a zip file of the gallery on GET request.
"""
# Create zip file with pictures
gallery = self.get_object()
byte_data = BytesIO()
zip_file = zipfile.ZipFile(byte_data, "w")
for photo in gallery.photos.filter(is_public=True):
filename = os.path.basename(os.path.normpath(photo.image.path))
zip_file.write(photo.image.path, filename)
zip_file.close()
# Return zip file
response = HttpResponse(
byte_data.getvalue(), content_type="application/x-zip-compressed"
)
response["Content-Disposition"] = f"attachment; filename={gallery.slug}.zip"
return response
class GalleryUpload(PermissionRequiredMixin, FormView):
"""
Form to upload new photos in a gallery
"""
form_class = UploadForm
template_name = "photologue/upload.html"
success_url = reverse_lazy("photologue:pl-gallery-upload")
permission_required = "photologue.add_gallery"
def form_valid(self, form):
# Get or create gallery
if self.request.accepts("text/html") or not self.request.accepts(
"application/json"
):
response_json = False
finish_json = False
else:
response_json = True
finish_json = form.data.get("end", "") == "end"
gallery = form.get_or_create_gallery()
jsondata = {"galleryID": gallery.id, "code": 200}
gallery_year = Path(str(gallery.date_start.year))
gallery_dir = gallery_year / gallery.slug
# Upload pictures
uploaded_photo_name = []
already_exists = 0
files = form.cleaned_data["file_field"]
for photo_file in files:
# Check that we have a valid image
try:
opened = Image.open(photo_file)
opened.verify()
except Exception:
# Pillow doesn't recognize it as an image, skip it
messages.error(
self.request, f"{photo_file.name} was not recognized as an image"
)
jsondata["code"] = 400
jsondata["error"] = f"{photo_file.name} was not recognized as an image"
continue
title = f"{gallery.title} - {photo_file.name}"
try:
photo = Photo(
title=title,
slug=slugify(title),
owner=self.request.user,
)
photo_name = str(gallery_dir / photo_file.name)
photo.save()
photo.galleries.set([gallery])
# Save to disk after successful database edit
photo.image.save(photo_name, photo_file)
except IntegrityError:
already_exists += 1
continue
uploaded_photo_name.append(photo_file.name)
# Notify user then managers
n_success = len(uploaded_photo_name)
if not response_json:
if already_exists:
messages.success(
self.request,
f"{n_success} photo(s) uploaded, {already_exists} photo(s) skipped as they already exist in this gallery.",
)
else:
messages.success(self.request, f"{n_success} photo(s) uploaded.")
# Notify administrators on new uploads
gallery_url = reverse_lazy("photologue:pl-gallery", args=[gallery.slug])
gallery_url = self.request.build_absolute_uri(gallery_url)
if uploaded_photo_name and (not response_json):
photos = ", ".join(uploaded_photo_name)
mail_admins(
subject=f"New upload from {self.request.user.username}",
message=f"{self.request.user.username} has uploaded in <{gallery_url}>:\n{photos}",
)
elif response_json and finish_json:
photos = ", ".join(uploaded_photo_name)
mail_admins(
subject=f"New continious upload from {self.request.user.username}",
message=f"{self.request.user.username} has uploaded multiples photo with continious upload in <{gallery_url}>",
)
if response_json:
jsondata["uploadeds"] = uploaded_photo_name
return JsonResponse(jsondata)
else:
return super().form_valid(form)