# 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.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 .forms import UploadForm from .models import Gallery, Photo, Tag 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 if self.request.user.is_staff: context["photos"] = self.object.photos.all() else: context["photos"] = self.object.photos.filter(is_public=True) # List owners context["owners"] = [] 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"]) 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): # Upload photos # We take files from the request to support multiple upload files = self.request.FILES.getlist("file_field") # Get or create gallery gallery = form.get_or_create_gallery() gallery_year = Path(str(gallery.date_start.year)) gallery_dir = gallery_year / gallery.slug # Upload pictures uploaded_photo_name = [] already_exists = 0 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" ) 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 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: 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}", ) return super().form_valid(form)