photo26/photologue/views.py
2026-04-22 08:21:52 +02:00

343 lines
12 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 uuid
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 get_object_or_404, redirect
from django.urls import reverse, reverse_lazy
from django.utils.text import slugify
from django.views import View
from django.views.generic.dates import ArchiveIndexView, YearArchiveView
from django.views.generic.detail import DetailView
from django.views.generic.edit import DeleteView, FormView
from django.conf import settings
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(DetailView):
model = Photo
template_name = "photologue/photo_confirm_report.html"
def post(self, request, *args, **kwargs):
photo = self.get_object()
photo.is_public = False
photo.save()
galleries = photo.galleries.all()
gallery_slug = galleries[0].slug if galleries else ""
if gallery_slug:
url = reverse_lazy("photologue:pl-gallery", args=[gallery_slug])
else:
url = reverse_lazy("photologue:pl-gallery-archive")
url = request.build_absolute_uri(url)
reporter = request.user.username if request.user.is_authenticated else "anonymous"
mail_admins(
subject=f"Abuse report for photo id {photo.pk}",
message=f"{reporter} reported an abuse for `{photo.title}`: {url}#lg=1&slide={photo.pk}",
)
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"])
if self.object.public_token:
public_path = reverse("photologue:pl-gallery-public", args=[self.object.public_token])
context["public_url"] = self.request.build_absolute_uri(public_path)
context["photos"].update(view_count=F("view_count") + 1)
return context
class GalleryDownload(LoginRequiredMixin, DetailView):
### IN FUTURE, PUT IT as Django Task
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()
gallery_year = os.path.join("/photos/", str(gallery.date_start.year))
gallery_zip = os.path.join(gallery_year, (gallery.slug + ".zip"))
with open(settings.MEDIA_ROOT + gallery_zip, "wb") as zip_bytes:
zip_file = zipfile.ZipFile(zip_bytes, "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 the path to it
return redirect(
(settings.MEDIA_URL + str(gallery_zip)).replace("\\", "/")
) # windows fix
# 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 GalleryPublicView(DetailView):
model = Gallery
template_name = "photologue/gallery_detail.html"
def get_object(self):
return get_object_or_404(Gallery, public_token=self.kwargs["token"])
def get(self, request, *args, **kwargs):
request.guest_mode = True
return super().get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["photos"] = self.object.photos.filter(is_public=True).select_related("owner")
context["owners"] = []
context["guest_mode"] = True
context["photos"].update(view_count=F("view_count") + 1)
return context
class GalleryTokenView(LoginRequiredMixin, View):
def post(self, request, slug):
if not request.user.is_staff:
from django.core.exceptions import PermissionDenied
raise PermissionDenied
gallery = get_object_or_404(Gallery, slug=slug)
action = request.POST.get("action")
if action == "generate":
gallery.public_token = uuid.uuid4()
gallery.save()
elif action == "revoke":
gallery.public_token = None
gallery.save()
return redirect(reverse("photologue:pl-gallery", args=[slug]))
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)