{% blocktrans trimmed %}
This website aims to collect the pictures and movies taken in the student
- life of ENS Paris-Saclay or involving its students.
+ life of ENS Rennes or involving its students.
{% endblocktrans %}
- {% blocktrans trimmed %}
- If you want a photo to be deleted, please let us know:
- Abuse request
- {% endblocktrans %}
-
- {% if not perms.photologue.add_photo %}
+ {% if not perms.photologue.add_photo %}
{% blocktrans trimmed %}
If you want to obtain the right to upload pictures, please let us know:
- Become a photograph
+ Become a photograph
{% endblocktrans %}
{% endif %}
+
{% trans "Last galleries" %}
{% for gallery in object_list %}
@@ -52,15 +47,15 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% trans "Behind the scene" %}
{% blocktrans trimmed %}
+ This project if a fork of Photo21.
Because we value your privacy, we do not sell the data on this site,
unlike many free online platforms.
- The dedicated server running this website is kindly hosted by the
- Crans at the ENS Paris-Saclay
- basement.
- It is not managed by the Crans. Current active administrators are:
- {% endblocktrans %}
- {% for user in superusers %} {{ user.username }}{% endfor %}.
- {% trans "They should be contacted at" %}
- photos@crans.org.
+
+
+
+
+
+
+
{% endblock %}
diff --git a/photologue/static/gallery_detail.js b/photologue/static/gallery_detail.js
index b9fad3a..03c187a 100644
--- a/photologue/static/gallery_detail.js
+++ b/photologue/static/gallery_detail.js
@@ -34,20 +34,20 @@ lgContainer.addEventListener('lgAfterOpen', () => {
const downloadUrl = this.getAttribute('href');
- // Affichage de la modale stylisée
- Swal.fire({
- title: gettext('Download'),
- text: gettext("This image is free to download, but permission from the photographer and the people in the photo is required before republishing it on another website. Furthermore, it is good practice to credit L[ENS] and the photographers in any republications."),
- icon: 'info',
- showCancelButton: true,
- confirmButtonColor: '#3085d6',
- cancelButtonColor: '#d33',
- confirmButtonText: gettext('Download'),
- cancelButtonText: gettext('Cancel'),
- background: '#1a1a1a', // Optionnel : pour matcher le thème sombre de LightGallery
- color: '#fff'
- }).then((result) => {
- if (result.isConfirmed) {
+ // // Affichage de la modale stylisée
+ // Swal.fire({
+ // title: gettext('Download'),
+ // text: gettext("This image is free to download, but permission from the photographer and the people in the photo is required before republishing it on another website. Furthermore, it is good practice to credit L[ENS] and the photographers in any republications."),
+ // icon: 'info',
+ // showCancelButton: true,
+ // confirmButtonColor: '#3085d6',
+ // cancelButtonColor: '#d33',
+ // confirmButtonText: gettext('Download'),
+ // cancelButtonText: gettext('Cancel'),
+ // background: '#1a1a1a', // Optionnel : pour matcher le thème sombre de LightGallery
+ // color: '#fff'
+ // }).then((result) => {
+ // if (result.isConfirmed) {
// Si validé, on déclenche le téléchargement
const link = document.createElement('a');
link.href = downloadUrl;
@@ -55,8 +55,8 @@ lgContainer.addEventListener('lgAfterOpen', () => {
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
- }
- });
+ // }
+ // });
}, true); // Utilisation du mode capture pour intercepter avant le script interne
}
});
diff --git a/photologue/static/lightgallery/plugins/admin/lg-admin.js b/photologue/static/lightgallery/plugins/admin/lg-admin.js
index 5b198c9..aa0214d 100644
--- a/photologue/static/lightgallery/plugins/admin/lg-admin.js
+++ b/photologue/static/lightgallery/plugins/admin/lg-admin.js
@@ -13,6 +13,7 @@ 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;
@@ -33,7 +34,8 @@ class lgAdmin {
document.getElementById("lg-delete").addEventListener('click', this.onDelete.bind(this));
// Add button to report photo
- this.core.$toolbar.append(`${reportIcon}`);
+ this.core.$toolbar.append(`${deleteIcon}`);
+ document.getElementById("lg-report").style.display = 'none';
document.getElementById("lg-report").addEventListener('click', this.onReport.bind(this));
// Add button to restore a censored photo
@@ -53,6 +55,7 @@ class lgAdmin {
const ownerId = el ? el.dataset.ownerId : null;
const canDelete = this.isStaff || (ownerId && ownerId === this.userId);
document.getElementById("lg-delete").style.display = canDelete ? 'block' : 'none';
+ document.getElementById("lg-report").style.display = canDelete ? 'none' : 'block';
const isCensored = el ? el.dataset.isPublic === 'false' : false;
document.getElementById("lg-restore").style.display = (this.canResolveCensorship && isCensored) ? 'block' : 'none';
}
@@ -135,7 +138,7 @@ class lgAdmin {
// Event called when user click on report button
onReport(event) {
event.preventDefault();
- if(confirm("Are you sure to report this photo?")) {
+ if(confirm("Are you sure to ask removal for this photo?")) {
// Build form request
const photoId = this.photoId;
const currentIndex = this.core.index;
diff --git a/photologue/templates/photologue/gallery_detail.html b/photologue/templates/photologue/gallery_detail.html
index 155f86a..ea5c671 100755
--- a/photologue/templates/photologue/gallery_detail.html
+++ b/photologue/templates/photologue/gallery_detail.html
@@ -20,6 +20,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
+
From d44b31f024a772b5a6058fce5bb8daa0be5c7363 Mon Sep 17 00:00:00 2001
From: krek0
Date: Fri, 24 Apr 2026 22:26:03 +0200
Subject: [PATCH 04/10] Remove Nginx-specific static and media serving, and
serve media through Django using WhiteNoise and FileResponse.
---
README.md | 15 ---------------
photo21/settings.py | 16 ++++++++++++++++
photo21/storage.py | 12 ++++++++++++
photo21/views.py | 24 ++++++++++++++++--------
photologue/views.py | 1 +
requirements.txt | 1 +
6 files changed, 46 insertions(+), 23 deletions(-)
create mode 100644 photo21/storage.py
diff --git a/README.md b/README.md
index 58d8206..5878775 100644
--- a/README.md
+++ b/README.md
@@ -17,9 +17,6 @@ run and to maintain.
```bash
sudo apt install git gettext python3-django python3-django-allauth python3-django-crispy-forms python3-docutils python3-exifread python3-pil
-
- # Only for production
- sudo apt install nginx uwsgi uwsgi-plugin-python3 python3-certbot-nginx
```
2. **Cloning.**
@@ -33,19 +30,8 @@ run and to maintain.
3. **Configuration (production only).**
```bash
- # Only for production
sudo mkdir static media
- sudo cp docs/maintenance.html static/maintenance.html
- sudo chown www-data:www-data -R static media
- sudo chmod g+rwx -R static media
sudo chmod +x maintenance_tool.sh
- sudo cp docs/uwsgi_photos.ini /etc/uwsgi/apps-available/uwsgi_photos.ini
- sudo ln -s /etc/uwsgi/apps-available/uwsgi_photos.ini /etc/uwsgi/apps-enabled/
- sudo cp docs/nginx_photos_maintenance /etc/nginx/sites-available/photos.crans.org
- sudo ln -s /etc/nginx/sites-available/photos.crans.org /etc/nginx/sites-enabled/
- sudo cp docs/letsencrypt_photos.crans.org /etc/letsencrypt/conf.d/photos.crans.org
- sudo cp docs/renewal-hooks_post_nginx /etc/letsencrypt/renewal-hooks/post/nginx
- sudo certbot --config /etc/letsencrypt/conf.d/photos.crans.org.ini certonly
```
4. **Database (production only).**
@@ -82,7 +68,6 @@ run and to maintain.
6. *Enjoy \o/*
- In production, the NGINX site should now work.
In development, you can launch the development server using:
```bash
diff --git a/photo21/settings.py b/photo21/settings.py
index 2436141..88390ae 100644
--- a/photo21/settings.py
+++ b/photo21/settings.py
@@ -46,6 +46,9 @@ ADMINS = [tuple(a.split(":")) for a in config("ADMINS", default="", cast=Csv())
SESSION_COOKIE_SECURE = not DEBUG
CSRF_COOKIE_SECURE = not DEBUG
+# Trust Caddy's forwarded proto header
+SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
+
# Remember HTTPS for 1 year
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
@@ -77,6 +80,7 @@ if DEBUG:
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
+ "whitenoise.middleware.WhiteNoiseMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
@@ -189,6 +193,18 @@ STATIC_ROOT = os.path.join(BASE_DIR, "static/")
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
MEDIA_URL = "/media/"
+STORAGES = {
+ "default": {
+ "BACKEND": "django.core.files.storage.FileSystemStorage",
+ },
+ "staticfiles": {
+ "BACKEND": "photo21.storage.CompressedManifestStorage",
+ },
+}
+
+
+WHITENOISE_MANIFEST_STRICT = False
+
LOCALE_PATHS = [os.path.join(BASE_DIR, "photo21/locale")]
FIXTURE_DIRS = [os.path.join(BASE_DIR, "photo21/fixtures")]
diff --git a/photo21/storage.py b/photo21/storage.py
new file mode 100644
index 0000000..fa1b779
--- /dev/null
+++ b/photo21/storage.py
@@ -0,0 +1,12 @@
+from whitenoise.storage import CompressedManifestStaticFilesStorage
+
+
+class CompressedManifestStorage(CompressedManifestStaticFilesStorage):
+ """Like CompressedManifestStaticFilesStorage but silently skips missing
+ referenced files (e.g. source maps not included in the package)."""
+
+ def hashed_name(self, name, content=None, filename=None):
+ try:
+ return super().hashed_name(name, content, filename)
+ except ValueError:
+ return name
diff --git a/photo21/views.py b/photo21/views.py
index 8245c4f..434be5b 100644
--- a/photo21/views.py
+++ b/photo21/views.py
@@ -2,21 +2,29 @@
# Copyright (C) 2021-2022 Amicale des élèves de l'ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
+import os
+
+from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.mixins import LoginRequiredMixin
-from django.http import HttpResponse
+from django.http import FileResponse, Http404
from django.views.generic import ListView, View
from photologue.models import Gallery
-
-class MediaAccess(LoginRequiredMixin, View):
+class MediaAccess(View):
def get(self, request, path):
- response = HttpResponse()
- # Content-type will be detected by nginx
- del response["Content-Type"]
- response["X-Accel-Redirect"] = "/protected/media/" + path
- response["Cache-Control"] = 'max-age=2678400'
+ if not request.user.is_authenticated and not request.session.get('public_gallery_access'):
+ from django.contrib.auth.views import redirect_to_login
+ 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):
+ raise Http404
+ if not os.path.isfile(file_path):
+ raise Http404
+ response = FileResponse(open(file_path, 'rb'))
+ response['Cache-Control'] = 'max-age=2678400'
return response
diff --git a/photologue/views.py b/photologue/views.py
index c44b1f8..a523fa1 100644
--- a/photologue/views.py
+++ b/photologue/views.py
@@ -237,6 +237,7 @@ class GalleryPublicView(DetailView):
if request.user.is_authenticated:
gallery = self.get_object()
return redirect("photologue:pl-gallery", slug=gallery.slug)
+ request.session['public_gallery_access'] = True
request.guest_mode = True
return super().get(request, *args, **kwargs)
diff --git a/requirements.txt b/requirements.txt
index 3f38e2d..31eeff0 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -6,3 +6,4 @@ ExifRead>=2.1.2
Pillow>=6.0.0
django-debug-toolbar>=3.2.0
python-decouple>=3.6
+whitenoise>=6.0
From 1a5f1d5e81b9b7f51f09f0567fdc471920ef4970 Mon Sep 17 00:00:00 2001
From: krek0
Date: Fri, 24 Apr 2026 23:26:09 +0200
Subject: [PATCH 05/10] Enable Write-Ahead Logging (WAL) for SQLite to improve
performance when using it.
---
photo21/settings.py | 3 +++
photologue/apps.py | 17 +++++++++++++++++
photologue/models.py | 2 +-
3 files changed, 21 insertions(+), 1 deletion(-)
diff --git a/photo21/settings.py b/photo21/settings.py
index 88390ae..fb36c54 100644
--- a/photo21/settings.py
+++ b/photo21/settings.py
@@ -129,6 +129,9 @@ DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": os.path.join(BASE_DIR, "db.sqlite3"),
+ "OPTIONS": {
+ "timeout": 10,
+ },
}
}
diff --git a/photologue/apps.py b/photologue/apps.py
index a446591..10f0a9b 100644
--- a/photologue/apps.py
+++ b/photologue/apps.py
@@ -3,8 +3,25 @@
# SPDX-License-Identifier: GPL-3.0-or-later
from django.apps import AppConfig
+from django.db.backends.signals import connection_created
class PhotologueConfig(AppConfig):
default_auto_field = "django.db.models.AutoField"
name = "photologue"
+
+ def ready(self):
+ from django.db import connection
+
+ def enable_sqlite_wal(sender, connection, **kwargs):
+ if connection.vendor == "sqlite":
+ cursor = connection.cursor()
+ cursor.execute("PRAGMA journal_mode=WAL;")
+ cursor.execute("PRAGMA synchronous=OFF;")
+ cursor.execute("PRAGMA journal_size_limit=67108864;")
+ cursor.execute("PRAGMA wal_autocheckpoint=1000;")
+ cursor.execute("PRAGMA cache_size=-65536;")
+ cursor.execute("PRAGMA temp_store=MEMORY;")
+ cursor.execute("PRAGMA mmap_size=268435456;")
+
+ connection_created.connect(enable_sqlite_wal)
diff --git a/photologue/models.py b/photologue/models.py
index 3585e8c..594ac2c 100644
--- a/photologue/models.py
+++ b/photologue/models.py
@@ -486,7 +486,7 @@ class ImageModel(models.Model):
self._old_image.storage.delete(
self._old_image.name
) # Delete (old) base image.
- if self.date_taken is None or image_has_changed:
+ if (self.date_taken is None or image_has_changed) and self.image:
# Attempt to get the date the photo was taken from the EXIF data.
try:
exif_date = self.exif(self.image.file).get(
From 74609215e0dd9d256e8566751f23c9e7c71ff0f4 Mon Sep 17 00:00:00 2001
From: krek0
Date: Sat, 25 Apr 2026 01:09:01 +0200
Subject: [PATCH 06/10] Add public link filter to Gallery admin
---
photologue/admin.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/photologue/admin.py b/photologue/admin.py
index d6337f6..f994402 100644
--- a/photologue/admin.py
+++ b/photologue/admin.py
@@ -3,6 +3,7 @@
# 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
@@ -10,7 +11,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"]
+ list_filter = ["date_start", "tags", ("public_token", EmptyFieldListFilter)]
date_hierarchy = "date_start"
prepopulated_fields = {"slug": ("title",)}
model = Gallery
From 3a73bb88876edabc9bb694f1cd471d91d54f6aac Mon Sep 17 00:00:00 2001
From: krek0
Date: Sat, 25 Apr 2026 12:12:30 +0200
Subject: [PATCH 07/10] Fix thumbnail bar render
---
photologue/templates/photologue/gallery_detail.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/photologue/templates/photologue/gallery_detail.html b/photologue/templates/photologue/gallery_detail.html
index ea5c671..c435f8d 100755
--- a/photologue/templates/photologue/gallery_detail.html
+++ b/photologue/templates/photologue/gallery_detail.html
@@ -92,7 +92,7 @@ SPDX-License-Identifier: GPL-3.0-or-later