Add video support with unified media display.
All checks were successful
Docker / build (release) Successful in 9s
All checks were successful
Docker / build (release) Successful in 9s
This commit is contained in:
parent
a634cc88bd
commit
f4052a3d99
16 changed files with 700 additions and 224 deletions
|
|
@ -21,13 +21,36 @@ 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 PIL import Image
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from django.db.models import F
|
||||
from django.db.models import F, Q
|
||||
|
||||
from .forms import UploadForm
|
||||
from .models import Gallery, Photo, Tag
|
||||
from .models import Gallery, Photo, Tag, Video
|
||||
from .utils import generate_video_thumbnail, is_photo, is_video
|
||||
|
||||
MEDIA_MODELS = {"photo": Photo, "video": Video}
|
||||
|
||||
|
||||
class ModelFromUrlMixin:
|
||||
"""Sets self.model from the <model_name> URL kwarg using MEDIA_MODELS registry."""
|
||||
def setup(self, request, *args, **kwargs):
|
||||
super().setup(request, *args, **kwargs)
|
||||
model_name = kwargs.get("model_name")
|
||||
self.model = MEDIA_MODELS.get(model_name)
|
||||
if self.model is None:
|
||||
from django.http import Http404
|
||||
raise Http404
|
||||
|
||||
|
||||
def unique_slug(model, title):
|
||||
base = slugify(title)
|
||||
slug, counter = base, 2
|
||||
while model.objects.filter(slug=slug).exists():
|
||||
slug = f"{base}-{counter}"
|
||||
counter += 1
|
||||
return slug
|
||||
|
||||
|
||||
# Cette ligne renvoie le modèle d'utilisateur actif (le natif ou le vôtre)
|
||||
User = get_user_model()
|
||||
|
|
@ -39,12 +62,14 @@ class GalleryDateView(LoginRequiredMixin):
|
|||
allow_empty = True # Do not 404 if no galleries
|
||||
|
||||
def get_queryset(self):
|
||||
"""Hide galleries with only private photos"""
|
||||
"""Hide galleries with no public media"""
|
||||
qs = super().get_queryset()
|
||||
if self.request.user.is_staff:
|
||||
return qs
|
||||
else:
|
||||
return qs.filter(photos__is_public=True).distinct()
|
||||
return qs.filter(
|
||||
Q(photos__is_public=True) | Q(videos__is_public=True)
|
||||
).distinct()
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Always show all years in archive"""
|
||||
|
|
@ -75,12 +100,13 @@ class PhotoDetailView(LoginRequiredMixin, DetailView):
|
|||
return qs.filter(is_public=True)
|
||||
|
||||
|
||||
class PhotoDeleteView(LoginRequiredMixin, DeleteView):
|
||||
model = Photo
|
||||
class MediaDeleteView(ModelFromUrlMixin, LoginRequiredMixin, DeleteView):
|
||||
template_name = "photologue/media_confirm_delete.html"
|
||||
|
||||
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"):
|
||||
delete_perm = f"photologue.delete_{self.model._meta.model_name}"
|
||||
if obj.owner != self.request.user and not self.request.user.has_perm(delete_perm):
|
||||
from django.core.exceptions import PermissionDenied
|
||||
raise PermissionDenied
|
||||
return obj
|
||||
|
|
@ -93,54 +119,53 @@ class PhotoDeleteView(LoginRequiredMixin, DeleteView):
|
|||
return reverse_lazy("photologue:pl-gallery", args=[slug])
|
||||
|
||||
|
||||
class PhotoReportView(DetailView):
|
||||
model = Photo
|
||||
class MediaReportView(ModelFromUrlMixin, DetailView):
|
||||
template_name = "photologue/photo_confirm_report.html"
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not request.user.is_authenticated:
|
||||
photo = self.get_object()
|
||||
if not photo.galleries.filter(is_public=True).exists():
|
||||
obj = self.get_object()
|
||||
if not obj.galleries.filter(is_public=True).exists():
|
||||
from django.contrib.auth.views import redirect_to_login
|
||||
return redirect_to_login(request.get_full_path())
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
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()
|
||||
# Mark media as private
|
||||
obj = self.get_object()
|
||||
obj.is_public = False
|
||||
obj.save()
|
||||
|
||||
# Get gallery
|
||||
galleries = photo.galleries.all()
|
||||
galleries = obj.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])
|
||||
else:
|
||||
url = reverse_lazy("photologue:pl-gallery", args=[gallery_slug])
|
||||
url = request.build_absolute_uri(url)
|
||||
|
||||
# Send mail to managers
|
||||
reporter = request.user.username if request.user.is_authenticated else "Anonymous (public link)"
|
||||
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}",
|
||||
subject=f"Abuse report for {self.model._meta.verbose_name} id {obj.pk}",
|
||||
message=f"{reporter} reported an abuse for `{obj.title}`: {url}#lg=1&slide={obj.pk}",
|
||||
)
|
||||
|
||||
# Redirect to gallery
|
||||
return redirect(url)
|
||||
|
||||
|
||||
class PhotoUncensorView(LoginRequiredMixin, View):
|
||||
def post(self, request, pk):
|
||||
class MediaUncensorView(ModelFromUrlMixin, LoginRequiredMixin, View):
|
||||
model = None
|
||||
|
||||
def post(self, request, pk, **kwargs):
|
||||
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()
|
||||
obj = get_object_or_404(self.model, pk=pk)
|
||||
obj.is_public = True
|
||||
obj.save()
|
||||
return JsonResponse({"ok": True})
|
||||
|
||||
|
||||
|
|
@ -180,13 +205,14 @@ class GalleryDetailView(DetailView):
|
|||
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")
|
||||
# Non-staff members only see public media
|
||||
visible_only = not (self.request.user.is_staff or can_resolve)
|
||||
def media_qs(relation):
|
||||
qs = relation.select_related("owner")
|
||||
return qs.filter(is_public=True) if visible_only else qs.all()
|
||||
|
||||
context["photos"] = media_qs(self.object.photos)
|
||||
context["videos"] = media_qs(self.object.videos)
|
||||
|
||||
# List owners
|
||||
context["owners"] = []
|
||||
|
|
@ -194,13 +220,19 @@ class GalleryDetailView(DetailView):
|
|||
# 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)
|
||||
for media in [*context["photos"], *context["videos"]]:
|
||||
if media.owner not in context["owners"]:
|
||||
context["owners"].append(media.owner)
|
||||
|
||||
# Filter on owner
|
||||
if "owner" in self.kwargs:
|
||||
context["photos"] = context["photos"].filter(owner__id=self.kwargs["owner"])
|
||||
context["videos"] = context["videos"].filter(owner__id=self.kwargs["owner"])
|
||||
|
||||
# Combine photos and videos into a single sorted list for the template
|
||||
context["media_items"] = sorted(
|
||||
[*context["photos"], *context["videos"]], key=lambda x: x.title
|
||||
)
|
||||
|
||||
# Increment the photo view count
|
||||
|
||||
|
|
@ -231,11 +263,12 @@ class GalleryDownload(DetailView):
|
|||
gallery_year = os.path.join("/photos/", str(gallery.date_start.year))
|
||||
gallery_zip = os.path.join(gallery_year, (gallery.slug + ".zip"))
|
||||
|
||||
media = [*gallery.photos.filter(is_public=True), *gallery.videos.filter(is_public=True)]
|
||||
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)
|
||||
for item in media:
|
||||
filename = os.path.basename(os.path.normpath(item.file_path))
|
||||
zip_file.write(item.file_path, filename)
|
||||
zip_file.close()
|
||||
|
||||
# Return the path to it
|
||||
|
|
@ -266,6 +299,26 @@ class GalleryUpload(PermissionRequiredMixin, FormView):
|
|||
success_url = reverse_lazy("photologue:pl-gallery-upload")
|
||||
permission_required = "photologue.add_gallery"
|
||||
|
||||
def _upload_media(self, model, file_field, file_obj, gallery, gallery_dir, post_save=None):
|
||||
"""
|
||||
Create a media object, save it to the DB, schedule file save on commit.
|
||||
Returns True if uploaded, False if already exists.
|
||||
"""
|
||||
title = Path(file_obj.name).stem
|
||||
if model.objects.filter(title=title, owner=self.request.user, galleries=gallery).exists():
|
||||
return False
|
||||
obj = model(title=title, slug=unique_slug(model, title), owner=self.request.user)
|
||||
file_path = str(gallery_dir / file_obj.name)
|
||||
with transaction.atomic():
|
||||
obj.save()
|
||||
obj.galleries.set([gallery])
|
||||
def _save(o=obj, fp=file_path, f=file_obj, ff=file_field, ps=post_save):
|
||||
getattr(o, ff).save(fp, f)
|
||||
if ps:
|
||||
ps(o)
|
||||
transaction.on_commit(_save)
|
||||
return True
|
||||
|
||||
def form_invalid(self, form):
|
||||
if not self.request.accepts("text/html") and self.request.accepts("application/json"):
|
||||
errors = {field: list(errs) for field, errs in form.errors.items()}
|
||||
|
|
@ -292,63 +345,25 @@ class GalleryUpload(PermissionRequiredMixin, FormView):
|
|||
gallery_year = Path(str(gallery.date_start.year))
|
||||
gallery_dir = gallery_year / gallery.slug
|
||||
|
||||
# Upload pictures
|
||||
# Upload pictures and videos
|
||||
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()
|
||||
photo_file.seek(0)
|
||||
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"
|
||||
)
|
||||
if is_photo(photo_file):
|
||||
uploaded = self._upload_media(Photo, "image", photo_file, gallery, gallery_dir)
|
||||
elif is_video(photo_file):
|
||||
uploaded = self._upload_media(Video, "file", photo_file, gallery, gallery_dir, post_save=generate_video_thumbnail)
|
||||
else:
|
||||
messages.error(self.request, f"{photo_file.name} is not a recognized image or video")
|
||||
jsondata["code"] = 400
|
||||
jsondata["error"] = f"{photo_file.name} was not recognized as an image"
|
||||
jsondata["error"] = f"{photo_file.name} is not a recognized image or video"
|
||||
continue
|
||||
|
||||
title = Path(photo_file.name).stem
|
||||
|
||||
already_exist = Photo.objects.filter(
|
||||
title=title,
|
||||
owner=self.request.user,
|
||||
galleries=gallery
|
||||
).exists()
|
||||
|
||||
if already_exist:
|
||||
if not uploaded:
|
||||
already_exists += 1
|
||||
continue
|
||||
|
||||
# Find a uniq slug
|
||||
base_slug = slugify(title)
|
||||
slug = base_slug
|
||||
counter = 2
|
||||
while Photo.objects.filter(slug=slug).exists():
|
||||
slug = f"{base_slug}-{counter}"
|
||||
counter += 1
|
||||
photo = Photo(
|
||||
title=title,
|
||||
slug=slug,
|
||||
owner=self.request.user,
|
||||
)
|
||||
photo_name = str(gallery_dir / photo_file.name)
|
||||
|
||||
# Save photo and associate it with the gallery in a single database operation
|
||||
# Defer image file saving until the database commit succeeds
|
||||
with transaction.atomic():
|
||||
photo.save()
|
||||
photo.galleries.set([gallery])
|
||||
|
||||
def save_file():
|
||||
photo.image.save(photo_name, photo_file)
|
||||
|
||||
transaction.on_commit(save_file)
|
||||
|
||||
uploaded_photo_name.append(photo_file.name)
|
||||
else:
|
||||
uploaded_photo_name.append(photo_file.name)
|
||||
|
||||
# Notify user then managers
|
||||
n_success = len(uploaded_photo_name)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue