# 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(LoginRequiredMixin, DeleteView): model = Photo def get_object(self, queryset=None): obj = super().get_object(queryset) if obj.owner != self.request.user and not self.request.user.has_perm("photologue.delete_photo"): from django.core.exceptions import PermissionDenied raise PermissionDenied return obj 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 PhotoUncensorView(LoginRequiredMixin, View): def post(self, request, pk): if not request.user.has_perm("photologue.can_resolve_censorship"): from django.core.exceptions import PermissionDenied raise PermissionDenied photo = get_object_or_404(Photo, pk=pk) photo.is_public = True photo.save() return JsonResponse({"ok": True}) 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) can_resolve = self.request.user.has_perm("photologue.can_resolve_censorship") context["can_resolve_censorship"] = can_resolve # Non-staff members only see public photos + prefetch all owners informations (Optimisation) if self.request.user.is_staff or can_resolve: 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): if request.user.is_authenticated: gallery = self.get_object() return redirect("photologue:pl-gallery", slug=gallery.slug) 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)