# 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)