diff --git a/photo21/templates/base.html b/photo21/templates/base.html
index 91f63cb..ccd17c2 100644
--- a/photo21/templates/base.html
+++ b/photo21/templates/base.html
@@ -37,6 +37,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
+ {% if request.user.is_authenticated %}
-
{% url 'photologue:pl-gallery-archive' as url %}
{% trans 'Galleries' %}
@@ -52,6 +53,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% trans 'Manage' %}
{% endif %}
+ {% endif %}
{% get_available_languages as LANGUAGES %}
diff --git a/photo21/views.py b/photo21/views.py
index bedd2a1..226c552 100644
--- a/photo21/views.py
+++ b/photo21/views.py
@@ -9,14 +9,30 @@ from django.contrib.auth import get_user_model
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import FileResponse, Http404
from django.views.generic import ListView, View
-from photologue.models import Gallery
+from photologue.models import Gallery, Photo
class MediaAccess(View):
def get(self, request, path):
- if not request.user.is_authenticated and not request.session.get('public_gallery_access'):
+ if not request.user.is_authenticated:
from django.contrib.auth.views import redirect_to_login
- return redirect_to_login(request.get_full_path())
+ try:
+ # Direct match (original photo file)
+ allowed = Photo.objects.filter(
+ image=path,
+ galleries__is_public=True,
+ ).exists()
+ # Cache files (thumbnails/display) are derived from original photos
+ if not allowed and '/cache/' in path:
+ original_dir = os.path.dirname(os.path.dirname(path))
+ allowed = Photo.objects.filter(
+ image__startswith=original_dir + '/',
+ galleries__is_public=True,
+ ).exists()
+ except Exception:
+ return redirect_to_login(request.get_full_path())
+ if not allowed:
+ return redirect_to_login(request.get_full_path())
media_root = os.path.realpath(settings.MEDIA_ROOT)
file_path = os.path.realpath(os.path.join(media_root, path))
if not file_path.startswith(media_root + os.sep):
diff --git a/photologue/admin.py b/photologue/admin.py
index f994402..6d28b36 100644
--- a/photologue/admin.py
+++ b/photologue/admin.py
@@ -3,7 +3,6 @@
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib import admin
-from django.contrib.admin import EmptyFieldListFilter
from django.utils.translation import gettext_lazy as _
from .models import Gallery, Photo, Tag
@@ -11,7 +10,7 @@ from .models import Gallery, Photo, Tag
class GalleryAdmin(admin.ModelAdmin):
list_display = ("title", "date_start", "photo_count", "get_tags")
- list_filter = ["date_start", "tags", ("public_token", EmptyFieldListFilter)]
+ list_filter = ["date_start", "tags", "is_public"]
date_hierarchy = "date_start"
prepopulated_fields = {"slug": ("title",)}
model = Gallery
diff --git a/photologue/locale/fr/LC_MESSAGES/django.po b/photologue/locale/fr/LC_MESSAGES/django.po
index 2ac7b65..34fada3 100644
--- a/photologue/locale/fr/LC_MESSAGES/django.po
+++ b/photologue/locale/fr/LC_MESSAGES/django.po
@@ -403,21 +403,17 @@ msgstr "au"
msgid "censored photos"
msgstr "photos censurées"
-#: photologue/templates/photologue/gallery_detail.html:53
-msgid "Public link:"
-msgstr ""
+msgid "Make public"
+msgstr "Rendre publique"
-#: photologue/templates/photologue/gallery_detail.html:55
-msgid "Copy"
-msgstr ""
+msgid "Make private"
+msgstr "Rendre privée"
-#: photologue/templates/photologue/gallery_detail.html:56
-msgid "Revoke"
-msgstr ""
+msgid "Public, anyone with the link can view this gallery."
+msgstr "Publique, n'importe qui avec le lien peut voir cette galerie."
-#: photologue/templates/photologue/gallery_detail.html:59
-msgid "Generate public link"
-msgstr ""
+msgid "Private, login required to view."
+msgstr "Privée, connexion requise pour voir."
#: photologue/templates/photologue/gallery_detail.html:78
msgid "All pictures"
diff --git a/photologue/migrations/0009_alter_photo_options_gallery_public_token.py b/photologue/migrations/0009_alter_photo_options_gallery_public_token.py
new file mode 100644
index 0000000..7117111
--- /dev/null
+++ b/photologue/migrations/0009_alter_photo_options_gallery_public_token.py
@@ -0,0 +1,22 @@
+# Generated by Django 5.2.13 on 2026-04-20 19:54
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('photologue', '0008_regen_thumbnails'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='photo',
+ options={'get_latest_by': 'date_added', 'ordering': ['title'], 'verbose_name': 'photo', 'verbose_name_plural': 'photos'},
+ ),
+ migrations.AddField(
+ model_name='gallery',
+ name='is_public',
+ field=models.BooleanField(default=False, help_text='Public galleries can be accessed without being logged in.', verbose_name='is public'),
+ ),
+ ]
diff --git a/photologue/models.py b/photologue/models.py
index e304b5c..7bd506c 100644
--- a/photologue/models.py
+++ b/photologue/models.py
@@ -174,6 +174,11 @@ class Gallery(models.Model):
verbose_name=_("photos"),
blank=True,
)
+ is_public = models.BooleanField(
+ _("is public"),
+ default=False,
+ help_text=_("Public galleries can be accessed without being logged in."),
+ )
class Meta:
ordering = ["-date_start"]
diff --git a/photologue/static/lightgallery/plugins/admin/lg-admin.js b/photologue/static/lightgallery/plugins/admin/lg-admin.js
index 0c44cc4..d20f6db 100644
--- a/photologue/static/lightgallery/plugins/admin/lg-admin.js
+++ b/photologue/static/lightgallery/plugins/admin/lg-admin.js
@@ -13,7 +13,6 @@ class lgAdmin {
this.isStaff = document.querySelector('[name=is_staff]').value === "true";
this.userId = document.querySelector('[name=user_id]').value;
this.canResolveCensorship = document.querySelector('[name=can_resolve_censorship]').value === "true";
- this.guestMode = document.querySelector('[name=guest_mode]').value === "true";
this.csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value;
this.photoId = 0;
return this;
diff --git a/photologue/templates/photologue/gallery_detail.html b/photologue/templates/photologue/gallery_detail.html
index 402e0c1..d98f173 100755
--- a/photologue/templates/photologue/gallery_detail.html
+++ b/photologue/templates/photologue/gallery_detail.html
@@ -20,7 +20,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
-
@@ -29,6 +28,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
+{% if not request.user.is_authenticated %}{% endif %}
{% endblock %}
{% block content %}
@@ -45,18 +45,22 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% if gallery.date_start %}{{ gallery.date_start }}{% if gallery.date_end and gallery.date_end != gallery.date_start %} {% trans "to" %} {{ gallery.date_end }}{% endif %}
{% endif %}
{% if request.user.is_staff and gallery.photo_private_count %}{{ gallery.photo_private_count }} {% trans "censored photos" %}
{% endif %}
-{% if request.user.is_staff %}
- {% if public_url %}
-
- {% trans "Public link:" %}
-
-
-
-
- {% else %}
-
+
+ {% if request.user.is_staff %}
+
{% endif %}
-{% endif %}
+ {% if gallery.is_public %}
+
{% trans "Public, anyone with the link can view this gallery." %}
+ {% else %}
+
{% trans "Private, login required to view." %}
+ {% endif %}
+
{% if gallery.tags.all %}
Tags : {% for tag in gallery.tags.all %}
@@ -87,7 +91,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% for photo in photos %}
-
+
{% endfor %}
diff --git a/photologue/urls.py b/photologue/urls.py
index 53379e4..0acc12d 100644
--- a/photologue/urls.py
+++ b/photologue/urls.py
@@ -9,6 +9,7 @@ from .views import (
GalleryArchiveIndexView,
GalleryDetailView,
GalleryDownload,
+ GalleryPublicToggleView,
GalleryUpload,
GalleryYearArchiveView,
PhotoDeleteView,
@@ -44,4 +45,5 @@ urlpatterns = [
path("photo/
/report/", PhotoReportView.as_view(), name="pl-photo-report"),
path("photo//uncensor/", PhotoUncensorView.as_view(), name="pl-photo-uncensor"),
path("upload/", GalleryUpload.as_view(), name="pl-gallery-upload"),
+ path("gallery//toggle-public/", GalleryPublicToggleView.as_view(), name="pl-gallery-toggle-public"),
]
diff --git a/photologue/views.py b/photologue/views.py
index 0cf9ab3..6af85fd 100644
--- a/photologue/views.py
+++ b/photologue/views.py
@@ -4,23 +4,23 @@
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.conf import settings
from django.db import transaction
from django.http import HttpResponse
from django.http import JsonResponse
-from django.shortcuts import redirect
-from django.urls import reverse_lazy
+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
@@ -93,10 +93,18 @@ class PhotoDeleteView(LoginRequiredMixin, DeleteView):
return reverse_lazy("photologue:pl-gallery", args=[slug])
-class PhotoReportView(LoginRequiredMixin, DetailView):
+class PhotoReportView(DetailView):
model = Photo
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():
+ 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.
@@ -115,9 +123,10 @@ class PhotoReportView(LoginRequiredMixin, DetailView):
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"{self.request.user.username} reported an abuse for `{photo.title}`: {url}#lg=1&slide={photo.pk}",
+ message=f"{reporter} reported an abuse for `{photo.title}`: {url}#lg=1&slide={photo.pk}",
)
# Redirect to gallery
@@ -150,13 +159,21 @@ class TagDetail(LoginRequiredMixin, DetailView):
return context
-class GalleryDetailView(LoginRequiredMixin, DetailView):
+class GalleryDetailView(DetailView):
"""
Gallery detail view to filter on photo owner
"""
model = Gallery
+ def dispatch(self, request, *args, **kwargs):
+ if not request.user.is_authenticated:
+ self.object = self.get_object()
+ if not self.object.is_public:
+ from django.contrib.auth.views import redirect_to_login
+ return redirect_to_login(request.get_full_path())
+ return super().dispatch(request, *args, **kwargs)
+
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@@ -192,10 +209,18 @@ class GalleryDetailView(LoginRequiredMixin, DetailView):
return context
-class GalleryDownload(LoginRequiredMixin, DetailView):
+class GalleryDownload(DetailView):
### IN FUTURE, PUT IT as Django Task
model = Gallery
+ def dispatch(self, request, *args, **kwargs):
+ if not request.user.is_authenticated:
+ self.object = self.get_object()
+ if not self.object.is_public:
+ from django.contrib.auth.views import redirect_to_login
+ return redirect_to_login(request.get_full_path())
+ return super().dispatch(request, *args, **kwargs)
+
def get(self, request, *args, **kwargs):
"""
Download a zip file of the gallery on GET request.
@@ -218,13 +243,17 @@ class GalleryDownload(LoginRequiredMixin, DetailView):
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 GalleryPublicToggleView(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)
+ gallery.is_public = not gallery.is_public
+ gallery.save()
+ return redirect(reverse("photologue:pl-gallery", args=[slug]))
class GalleryUpload(PermissionRequiredMixin, FormView):