308 lines
10 KiB
Python
308 lines
10 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 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(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):
|
|
### 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 = Path("/photos/" + str(+gallery.date_start.year))
|
|
gallery_zip = gallery_year / (gallery.slug + ".zip")
|
|
|
|
with open(settings.MEDIA_ROOT + str(gallery_zip), "wb") as zip_bytes: # I hate pathlib
|
|
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 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)
|