From 687445e414a50d016fa51253e0d932c189969e6e Mon Sep 17 00:00:00 2001 From: krek0 Date: Fri, 24 Apr 2026 21:43:25 +0200 Subject: [PATCH 01/27] Add justified photo grid layout with lazy loading and image dimensions. --- photo21/static/layout.css | 23 ++++++ .../migrations/0008_photo_dimensions.py | 45 +++++++++++ photologue/static/gallery_justified.js | 75 +++++++++++++++++++ .../templates/photologue/gallery_detail.html | 7 +- 4 files changed, 147 insertions(+), 3 deletions(-) create mode 100644 photologue/migrations/0008_photo_dimensions.py create mode 100644 photologue/static/gallery_justified.js diff --git a/photo21/static/layout.css b/photo21/static/layout.css index a0026d1..8e5568e 100644 --- a/photo21/static/layout.css +++ b/photo21/static/layout.css @@ -45,6 +45,29 @@ SPDX-License-Identifier: GPL-3.0-or-later background-color: rgba(163, 163, 163, 0.274); } +/* Gallery - Google Photos style justified grid */ +#lightgallery { + position: relative; +} + +.photo-item { + position: absolute; + overflow: hidden; + visibility: hidden; +} + +.photo-item img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +.photo-item img.photo-private { + outline: 5px solid var(--bs-danger); + outline-offset: -5px; +} + /* Language selector */ .lang-select { border: none; diff --git a/photologue/migrations/0008_photo_dimensions.py b/photologue/migrations/0008_photo_dimensions.py new file mode 100644 index 0000000..033f144 --- /dev/null +++ b/photologue/migrations/0008_photo_dimensions.py @@ -0,0 +1,45 @@ +import photologue.models +from django.db import migrations, models + + +def fill_dimensions(apps, schema_editor): + Photo = apps.get_model('photologue', 'Photo') + for photo in Photo.objects.filter(image_width__isnull=True).iterator(): + try: + photo.image_width = photo.image.width + photo.image_height = photo.image.height + photo.save(update_fields=['image_width', 'image_height']) + except Exception: + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ('photologue', '0007_allow_duplicate_photo_titles'), + ] + + operations = [ + migrations.AddField( + model_name='photo', + name='image_width', + field=models.PositiveIntegerField(editable=False, null=True), + ), + migrations.AddField( + model_name='photo', + name='image_height', + field=models.PositiveIntegerField(editable=False, null=True), + ), + migrations.RunPython(fill_dimensions, migrations.RunPython.noop), + migrations.AlterField( + model_name='photo', + name='image', + field=models.ImageField( + max_length=100, + upload_to=photologue.models.get_storage_path, + verbose_name='image', + width_field='image_width', + height_field='image_height', + ), + ), + ] diff --git a/photologue/static/gallery_justified.js b/photologue/static/gallery_justified.js new file mode 100644 index 0000000..80dcc55 --- /dev/null +++ b/photologue/static/gallery_justified.js @@ -0,0 +1,75 @@ +// Justified photo grid layout (Google Photos style) +// Inspired by https://github.com/flickr/justified-layout +(function () { + const SPACING = 3; + const PADDING = 3; + + function applyLayout() { + const container = document.getElementById('lightgallery'); + if (!container) return; + + const items = [...container.querySelectorAll('.photo-item')]; + if (!items.length) return; + + const containerWidth = container.offsetWidth - PADDING * 2; + const TARGET_HEIGHT = Math.min(220, Math.floor(containerWidth * 0.4)); + const ratios = items.map(a => { + const w = parseInt(a.dataset.width) || 1; + const h = parseInt(a.dataset.height) || 1; + return w / h; + }); + + // Group items into rows + const rows = []; + let rowStart = 0; + while (rowStart < ratios.length) { + let sum = 0; + let end = rowStart; + while (end < ratios.length) { + sum += ratios[end]; + const rowWidth = sum * TARGET_HEIGHT + SPACING * (end - rowStart); + end++; + if (rowWidth >= containerWidth) break; + } + rows.push({ start: rowStart, end }); + rowStart = end; + } + + // Position each item + let top = PADDING; + rows.forEach(({ start, end }, rowIndex) => { + const isLastRow = rowIndex === rows.length - 1; + const count = end - start; + const sumRatios = ratios.slice(start, end).reduce((a, b) => a + b, 0); + const totalSpacing = SPACING * (count - 1); + + // Don't stretch the last row if it's not full + const rowHeight = isLastRow && count < 3 + ? TARGET_HEIGHT + : (containerWidth - totalSpacing) / sumRatios; + + let left = PADDING; + for (let i = start; i < end; i++) { + const width = Math.round(ratios[i] * rowHeight); + const height = Math.round(rowHeight); + const item = items[i]; + item.style.top = top + 'px'; + item.style.left = left + 'px'; + item.style.width = width + 'px'; + item.style.height = height + 'px'; + left += width + SPACING; + } + top += Math.round(rowHeight) + SPACING; + }); + + container.style.height = (top - SPACING + PADDING) + 'px'; + items.forEach(item => { + item.style.visibility = 'visible'; + const img = item.querySelector('img[data-lazy]'); + if (img) img.src = img.dataset.lazy; + }); + } + + document.addEventListener('DOMContentLoaded', applyLayout); + window.addEventListener('resize', applyLayout); +})(); diff --git a/photologue/templates/photologue/gallery_detail.html b/photologue/templates/photologue/gallery_detail.html index 1744a05..155f86a 100755 --- a/photologue/templates/photologue/gallery_detail.html +++ b/photologue/templates/photologue/gallery_detail.html @@ -25,6 +25,7 @@ SPDX-License-Identifier: GPL-3.0-or-later + @@ -87,10 +88,10 @@ SPDX-License-Identifier: GPL-3.0-or-later {% endif %} -
+
{% for photo in photos %} - - {{ photo.title }}{% if photo.date_taken %} - {{ photo.date_taken|date }} {{ photo.date_taken|time }}{% endif %}{% if photo.owner.get_full_name %} - {{ photo.owner.get_full_name }}{% else %} - {{ photo.owner.username }}{% endif %}{% if photo.license %} - {{ photo.license }}{% endif %}{% if not photo.is_public %} - !PRIVATE!{% endif %} + + {{ photo.title }}{% if photo.date_taken %} - {{ photo.date_taken|date }} {{ photo.date_taken|time }}{% endif %}{% if photo.owner.get_full_name %} - {{ photo.owner.get_full_name }}{% else %} - {{ photo.owner.username }}{% endif %}{% if photo.license %} - {{ photo.license }}{% endif %}{% if not photo.is_public %} - !PRIVATE!{% endif %} {% endfor %}
From 3d985e473d0349f2667e79de8f52c8f5fbc92915 Mon Sep 17 00:00:00 2001 From: krek0 Date: Fri, 24 Apr 2026 21:53:44 +0200 Subject: [PATCH 02/27] Rennes rebranding --- README.md | 4 ++- photo21/forms.py | 9 ++---- photo21/templates/account/login.html | 2 +- photo21/templates/base.html | 22 ++++++------- photo21/templates/index.html | 29 +++++++---------- photologue/static/gallery_detail.js | 32 +++++++++---------- .../lightgallery/plugins/admin/lg-admin.js | 7 ++-- .../templates/photologue/gallery_detail.html | 1 + 8 files changed, 52 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index d8e6581..58d8206 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Photo server 2021-2023 +This project is a fork of [Photo21](https://gitlab.crans.org/bde/photo21/) developped at ENS Paris-Saclay. + [![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0.txt) This is the source code for the webserver hosting pictures from the @@ -25,7 +27,7 @@ run and to maintain. In production, we usually use `/var/www/photos/` as the `root` user. ```bash - git clone https://gitlab.crans.org/bde/photo21.git && cd photo21 + git clone https://codeberg.org/krek0/photo21.git && cd photo21 ``` 3. **Configuration (production only).** diff --git a/photo21/forms.py b/photo21/forms.py index 7c15e1e..253fab3 100644 --- a/photo21/forms.py +++ b/photo21/forms.py @@ -13,8 +13,7 @@ class CustomSignupForm(SignupForm): # Add description on email field self.fields["email"].help_text = _( - "Please enter a valid email address ending with `@crans.org` or " - "`@ens-paris-saclay.fr`." + "Please enter a valid email address ending with `@ens-rennes.fr`" ) def clean_email(self): @@ -22,10 +21,8 @@ class CustomSignupForm(SignupForm): Check that the email address ends with a trusted domain. """ email = super().clean_email() - if not email.endswith("@crans.org") and not email.endswith( - "@ens-paris-saclay.fr" - ): + if not email.endswith("@ens-rennes.fr"): raise forms.ValidationError( - _("Must end with `@crans.org` or `@ens-paris-saclay.fr`.") + _("Must end with `@ens-rennes.fr`.") ) return email diff --git a/photo21/templates/account/login.html b/photo21/templates/account/login.html index 1e7723f..a2bc666 100644 --- a/photo21/templates/account/login.html +++ b/photo21/templates/account/login.html @@ -39,5 +39,5 @@ SPDX-License-Identifier: GPL-3.0-or-later {% trans "Forgot Password?" %}
-

{% trans "If any problem, please contact the server owners at" %} photos[at]crans.org.

+ {% endblock %} diff --git a/photo21/templates/base.html b/photo21/templates/base.html index eeca7fd..602ee57 100644 --- a/photo21/templates/base.html +++ b/photo21/templates/base.html @@ -13,7 +13,7 @@ SPDX-License-Identifier: GPL-3.0-or-later {% block title %}{{ title }}{% endblock title %} - {{ request.site.name }} - + @@ -118,16 +118,16 @@ SPDX-License-Identifier: GPL-3.0-or-later {% if request.user.is_authenticated %} {% trans "Connected as" %} {{ request.user.username }} · {% endif %} - {% trans "Source code" %} · - + {% trans "Source code" %} · + + + + + + + + +

diff --git a/photo21/templates/index.html b/photo21/templates/index.html index b7fa2e9..d4db262 100644 --- a/photo21/templates/index.html +++ b/photo21/templates/index.html @@ -12,7 +12,7 @@ SPDX-License-Identifier: GPL-3.0-or-later

{% 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 %}

@@ -25,21 +25,16 @@ SPDX-License-Identifier: GPL-3.0-or-later {% 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 03/27] 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 04/27] 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 05/27] 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 06/27] 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
{% for photo in photos %} - {{ photo.title }}{% if photo.date_taken %} - {{ photo.date_taken|date }} {{ photo.date_taken|time }}{% endif %}{% if photo.owner.get_full_name %} - {{ photo.owner.get_full_name }}{% else %} - {{ photo.owner.username }}{% endif %}{% if photo.license %} - {{ photo.license }}{% endif %}{% if not photo.is_public %} - !PRIVATE!{% endif %} + {{ photo.title }}{% if photo.date_taken %} - {{ photo.date_taken|date }} {{ photo.date_taken|time }}{% endif %}{% if photo.owner.get_full_name %} - {{ photo.owner.get_full_name }}{% else %} - {{ photo.owner.username }}{% endif %}{% if photo.license %} - {{ photo.license }}{% endif %}{% if not photo.is_public %} - !PRIVATE!{% endif %} {% endfor %}
From 7fbc81b9e147e46eb11b19c7ae587391778c4083 Mon Sep 17 00:00:00 2001 From: krek0 Date: Sat, 2 May 2026 14:18:02 +0200 Subject: [PATCH 07/27] Make database, oauth, smtp server, mail verificaiton configurable in .env --- .env.example | 36 ++++++++++ .../__init__.py | 2 + .../provider.py | 12 ++-- {allauth_note_kfet => allauth_oauth}/urls.py | 4 +- {allauth_note_kfet => allauth_oauth}/views.py | 12 ++-- photo21/settings.py | 71 ++++++++++++++----- requirements.txt | 1 + tox.ini | 2 +- 8 files changed, 109 insertions(+), 31 deletions(-) rename {allauth_note_kfet => allauth_oauth}/__init__.py (69%) rename {allauth_note_kfet => allauth_oauth}/provider.py (83%) rename {allauth_note_kfet => allauth_oauth}/urls.py (70%) rename {allauth_note_kfet => allauth_oauth}/views.py (77%) diff --git a/.env.example b/.env.example index 145b31e..9fa2ff9 100644 --- a/.env.example +++ b/.env.example @@ -14,3 +14,39 @@ ADMINS=admin:photos-admin@lists.crans.org # Email address used as sender for server emails SERVER_EMAIL=photos@crans.org + +# Email verification: 'mandatory', 'optional', or 'none' +EMAIL_VERIFICATION=mandatory + +# Mail server settings +SMTP_HOST=localhost +SMTP_PORT=25 +#SMTP_USER= +#SMTP_PASSWORD= +SMTP_USE_TLS=False + +# OAuth2 settings +# Enable OAuth2 login +OAUTH_ENABLED=False +# Disable normal username/password login (requires OAUTH_ENABLED=True) +OAUTH_ONLY=False +# OAuth2 server base URL (e.g. auth.example.com) +#OAUTH_SERVER_URL= +# OAuth2 app credentials +#OAUTH_CLIENT_ID= +#OAUTH_CLIENT_SECRET= +# Button appearance on the login page +#OAUTH_BUTTON_TEXT=Login with OAuth +#OAUTH_BUTTON_IMAGE= +# Space-separated OAuth2 scopes +#OAUTH_SCOPE=openid profile email + +# Database engine: 'sqlite' or 'postgres' +DB_ENGINE=sqlite + +# PostgreSQL settings (only used when DB_ENGINE=postgres) +#DB_NAME=photo21 +#DB_USER=photo21 +#DB_PASSWORD= +#DB_HOST=localhost +#DB_PORT=5432 diff --git a/allauth_note_kfet/__init__.py b/allauth_oauth/__init__.py similarity index 69% rename from allauth_note_kfet/__init__.py rename to allauth_oauth/__init__.py index 6c9e378..eae4948 100644 --- a/allauth_note_kfet/__init__.py +++ b/allauth_oauth/__init__.py @@ -1,3 +1,5 @@ # This file is part of photo21 # Copyright (C) 2022 Amicale des élèves de l'ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later + +default_app_config = "allauth_oauth.apps.AllauthOAuthConfig" diff --git a/allauth_note_kfet/provider.py b/allauth_oauth/provider.py similarity index 83% rename from allauth_note_kfet/provider.py rename to allauth_oauth/provider.py index d2ce6a5..6a6430c 100644 --- a/allauth_note_kfet/provider.py +++ b/allauth_oauth/provider.py @@ -7,15 +7,15 @@ from allauth.socialaccount.providers.base import ProviderAccount from allauth.socialaccount.providers.oauth2.provider import OAuth2Provider -class NoteKfetAccount(ProviderAccount): +class OAuthAccount(ProviderAccount): def to_str(self): return self.account.extra_data.get("username") -class NoteKfetProvider(OAuth2Provider): - id = "notekfet" - name = "Note Kfet" - account_class = NoteKfetAccount +class OAuthProvider(OAuth2Provider): + id = "oauth" + name = "OAuth" + account_class = OAuthAccount def extract_uid(self, data): return str(data["username"]) @@ -39,4 +39,4 @@ class NoteKfetProvider(OAuth2Provider): return ret -provider_classes = [NoteKfetProvider] +provider_classes = [OAuthProvider] diff --git a/allauth_note_kfet/urls.py b/allauth_oauth/urls.py similarity index 70% rename from allauth_note_kfet/urls.py rename to allauth_oauth/urls.py index e1bf561..190eac3 100644 --- a/allauth_note_kfet/urls.py +++ b/allauth_oauth/urls.py @@ -4,6 +4,6 @@ from allauth.socialaccount.providers.oauth2.urls import default_urlpatterns -from .provider import NoteKfetProvider +from .provider import OAuthProvider -urlpatterns = default_urlpatterns(NoteKfetProvider) +urlpatterns = default_urlpatterns(OAuthProvider) diff --git a/allauth_note_kfet/views.py b/allauth_oauth/views.py similarity index 77% rename from allauth_note_kfet/views.py rename to allauth_oauth/views.py index 86a16ee..7a568a5 100644 --- a/allauth_note_kfet/views.py +++ b/allauth_oauth/views.py @@ -10,11 +10,11 @@ from allauth.socialaccount.providers.oauth2.views import ( OAuth2LoginView, ) -from .provider import NoteKfetProvider +from .provider import OAuthProvider -class NoteKfetOAuth2Adapter(OAuth2Adapter): - provider_id = NoteKfetProvider.id +class OAuthAdapter(OAuth2Adapter): + provider_id = OAuthProvider.id def complete_login(self, request, app, token, **kwargs): headers = { @@ -31,7 +31,7 @@ class NoteKfetOAuth2Adapter(OAuth2Adapter): @property def domain(self): - return self.settings.get("DOMAIN", "note.crans.org") + return self.settings.get("DOMAIN", "") @property def access_token_url(self): @@ -46,5 +46,5 @@ class NoteKfetOAuth2Adapter(OAuth2Adapter): return f"https://{self.domain}/api/me/" -oauth2_login = OAuth2LoginView.adapter_view(NoteKfetOAuth2Adapter) -oauth2_callback = OAuth2CallbackView.adapter_view(NoteKfetOAuth2Adapter) +oauth2_login = OAuth2LoginView.adapter_view(OAuthAdapter) +oauth2_callback = OAuth2CallbackView.adapter_view(OAuthAdapter) diff --git a/photo21/settings.py b/photo21/settings.py index fb36c54..a80e727 100644 --- a/photo21/settings.py +++ b/photo21/settings.py @@ -56,6 +56,15 @@ SECURE_HSTS_PRELOAD = True # Application definition +OAUTH_ENABLED = config("OAUTH_ENABLED", default=False, cast=bool) +OAUTH_ONLY = config("OAUTH_ONLY", default=False, cast=bool) +OAUTH_CLIENT_ID = config("OAUTH_CLIENT_ID", default="") +OAUTH_CLIENT_SECRET = config("OAUTH_CLIENT_SECRET", default="") +OAUTH_SERVER_URL = config("OAUTH_SERVER_URL", default="") +OAUTH_BUTTON_TEXT = config("OAUTH_BUTTON_TEXT", default="Login with OAuth") +OAUTH_BUTTON_IMAGE = config("OAUTH_BUTTON_IMAGE", default="") +OAUTH_SCOPE = config("OAUTH_SCOPE", default="openid profile email", cast=Csv(delimiter=" ")) + INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.admindocs", @@ -69,12 +78,14 @@ INSTALLED_APPS = [ "allauth", "allauth.account", "allauth.socialaccount", - "allauth_note_kfet", "crispy_forms", "photologue", "photo21", ] +if OAUTH_ENABLED: + INSTALLED_APPS += ["allauth_oauth"] + if DEBUG: INSTALLED_APPS += ["debug_toolbar",] # For debug and optimisations @@ -125,15 +136,31 @@ WSGI_APPLICATION = "photo21.wsgi.application" # Database # https://docs.djangoproject.com/en/2.2/ref/settings/#databases -DATABASES = { - "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": os.path.join(BASE_DIR, "db.sqlite3"), - "OPTIONS": { - "timeout": 10, - }, +_db_engine = config("DB_ENGINE", default="sqlite").strip().lower() + +if _db_engine == "postgres": + DATABASES = { + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": config("DB_NAME", default="photo21"), + "USER": config("DB_USER", default="photo21"), + "PASSWORD": config("DB_PASSWORD", default=""), + "HOST": config("DB_HOST", default="localhost"), + "PORT": config("DB_PORT", default="5432"), + } } -} +elif _db_engine == "sqlite": + DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": os.path.join(BASE_DIR, "db.sqlite3"), + "OPTIONS": { + "timeout": 10, + }, + } + } +else: + raise ValueError(f"Unknown DB_ENGINE '{_db_engine}'. Must be 'sqlite' or 'postgres'.") CACHES = { "default": { @@ -221,6 +248,11 @@ if DEBUG: SERVER_EMAIL = config("SERVER_EMAIL", default="photos@crans.org") DEFAULT_FROM_EMAIL = f"Serveur photos <{SERVER_EMAIL}>" EMAIL_SUBJECT_PREFIX = "[Serveur photos] " +EMAIL_HOST = config("SMTP_HOST", default="localhost") +EMAIL_PORT = config("SMTP_PORT", default=25, cast=int) +EMAIL_HOST_USER = config("SMTP_USER", default="") +EMAIL_HOST_PASSWORD = config("SMTP_PASSWORD", default="") +EMAIL_USE_TLS = config("SMTP_USE_TLS", default=False, cast=bool) # After login redirect user to transfer page LOGIN_REDIRECT_URL = "/" @@ -240,16 +272,23 @@ MESSAGE_TAGS = { # Allauth configuration ## For the django =< 5.0 ACCOUNT_EMAIL_REQUIRED = True # ACCOUNT_SIGNUP_FIELDS = ['email*', 'username*', 'password1*', 'password2*'] ## For the django =< 5.0 -ACCOUNT_EMAIL_VERIFICATION = "mandatory" +ACCOUNT_EMAIL_VERIFICATION = config("EMAIL_VERIFICATION", default="mandatory") ACCOUNT_AUTHENTICATION_METHOD = "username_email" # ACCOUNT_LOGIN_METHODS = {'username', 'email'} ACCOUNT_FORMS = {"signup": "photo21.forms.CustomSignupForm"} -SOCIALACCOUNT_PROVIDERS = { - "notekfet": { - # Fetch user profile - "SCOPE": ["1_1"], - }, -} + +if OAUTH_ENABLED: + SOCIALACCOUNT_ONLY = OAUTH_ONLY + SOCIALACCOUNT_PROVIDERS = { + "oauth": { + "SCOPE": OAUTH_SCOPE, + "DOMAIN": OAUTH_SERVER_URL, + "APP": { + "client_id": OAUTH_CLIENT_ID, + "secret": OAUTH_CLIENT_SECRET, + }, + }, + } # Use Bootstrap forms CRISPY_TEMPLATE_PACK = "bootstrap4" diff --git a/requirements.txt b/requirements.txt index 31eeff0..c0d5114 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,4 @@ Pillow>=6.0.0 django-debug-toolbar>=3.2.0 python-decouple>=3.6 whitenoise>=6.0 +psycopg2>=2.9 diff --git a/tox.ini b/tox.ini index cc05d77..55bb7f6 100644 --- a/tox.ini +++ b/tox.ini @@ -27,7 +27,7 @@ deps = pep8-naming pyflakes commands = - flake8 allauth_note_kfet photo21 photologue + flake8 allauth_oauth photo21 photologue [flake8] ignore = W503, I100, I101 From faf880a23685227e3fa443dc6155f7f262f26b01 Mon Sep 17 00:00:00 2001 From: krek0 Date: Sat, 2 May 2026 14:19:10 +0200 Subject: [PATCH 08/27] Add requests in requirements --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index c0d5114..242cbf3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,3 +8,4 @@ django-debug-toolbar>=3.2.0 python-decouple>=3.6 whitenoise>=6.0 psycopg2>=2.9 +requests>=2.25 From 792f7517e39b50da0e93a346249e285124eabf78 Mon Sep 17 00:00:00 2001 From: krek0 Date: Sun, 3 May 2026 16:54:46 +0200 Subject: [PATCH 09/27] Add Docker support with Dockerfile and entrypoin --- .env.example | 3 + .forgejo/workflows/docker.yml | 32 +++++++++ Dockerfile | 19 +++++ README.md | 72 +++++++++++++++++++ docker-compose.yml | 40 +++++++++++ entrypoint.sh | 7 ++ photo21/settings.py | 2 +- .../commands/create_default_admin.py | 32 +++++++++ requirements.txt | 3 +- 9 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 .forgejo/workflows/docker.yml create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 entrypoint.sh create mode 100644 photologue/management/commands/create_default_admin.py diff --git a/.env.example b/.env.example index 9fa2ff9..930af5b 100644 --- a/.env.example +++ b/.env.example @@ -50,3 +50,6 @@ DB_ENGINE=sqlite #DB_PASSWORD= #DB_HOST=localhost #DB_PORT=5432 + +# SQLite settings (only used when DB_ENGINE=sqlite) +#DB_PATH=/app/data/db.sqlite3 diff --git a/.forgejo/workflows/docker.yml b/.forgejo/workflows/docker.yml new file mode 100644 index 0000000..90f3a7c --- /dev/null +++ b/.forgejo/workflows/docker.yml @@ -0,0 +1,32 @@ +name: Docker + +on: + release: + types: + - published + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set image tag + id: meta + run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT + + - name: Login to Forgejo registry + uses: docker/login-action@v3 + with: + registry: git.sinfonie.org + username: ${{ secrets.REGISTRY_USER }} + password: ${{ secrets.REGISTRY_TOKEN }} + + - name: Build image + run: | + docker build -t git.sinfonie.org/sinfonie/photo26:${{ steps.meta.outputs.TAG }} . + + - name: Push image + run: | + docker push git.sinfonie.org/sinfonie/photo26:${{ steps.meta.outputs.TAG }} + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f24c177 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM python:3.11-slim + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +# Create volume mount points +RUN mkdir -p /app/media /app/static /app/data + +EXPOSE 8000 + +RUN chmod +x entrypoint.sh +ENTRYPOINT ["./entrypoint.sh"] diff --git a/README.md b/README.md index 5878775..f54258c 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,78 @@ run and to maintain. ```./maintenance_tool.sh``` +## Docker install + +1. Create a `docker-compose.yml` (a ready-to-use file is provided in the repository): + + ```yaml + version: "3.9" + + networks: + photo26: + + services: + db: + image: postgres:16 + container_name: photo26-db + restart: unless-stopped + environment: + POSTGRES_DB: photo26 + POSTGRES_USER: photo26 + POSTGRES_PASSWORD: change-me + volumes: + - ./postgres_data:/var/lib/postgresql/data + networks: + - photo26 + + photo26: + image: git.sinfonie.org/sinfonie/photo26:latest + container_name: photo26-app + restart: unless-stopped + depends_on: + - db + environment: + DB_ENGINE: postgres + DB_NAME: photo26 + DB_USER: photo26 + DB_PASSWORD: change-me + DB_HOST: db + DB_PORT: 5432 + SECRET_KEY: change-me + EXTRA_HOSTS: photos.example.org + volumes: + - ./media:/app/media + ports: + - "8080:8000" + networks: + - photo26 + ``` + +2. Start the stack: + + ```bash + docker compose up -d + ``` + + On first start the container will run migrations and create a default admin account automatically. + +3. **Default credentials** — change these immediately after first login: + + | Field | Value | + |----------|-----------------| + | Username | `admin` | + | Password | `admin` | + | Email | `admin@localhost` | + + Admin panel: `http://localhost:8080/admin/` + +4. **Passwords to change** in `docker-compose.yml` before going to production: + - `POSTGRES_PASSWORD` / `DB_PASSWORD` — database password + - `SECRET_KEY` — Django secret key (use a long random string) + - Log in to the admin panel and change the `admin` user password + +--- + 6. *Enjoy \o/* In development, you can launch the development server using: diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..001fed7 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,40 @@ +version: "3.9" + +networks: + photo26: + +services: + db: + image: postgres:16 + container_name: photo26-db + restart: unless-stopped + environment: + POSTGRES_DB: photo26 + POSTGRES_USER: photo26 + POSTGRES_PASSWORD: change-me + volumes: + - ./postgres_data:/var/lib/postgresql/data + networks: + - photo26 + + photo26: + image: git.sinfonie.org/sinfonie/photo26:latest + container_name: photo26-app + restart: unless-stopped + depends_on: + - db + environment: + DB_ENGINE: postgres + DB_NAME: photo26 + DB_USER: photo26 + DB_PASSWORD: change-me + DB_HOST: db + DB_PORT: 5432 + SECRET_KEY: change-me + EXTRA_HOSTS: photos.example.org + volumes: + - ./media:/app/media + ports: + - "8080:8000" + networks: + - photo26 diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..fca4a2c --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,7 @@ +#!/bin/sh +set -e + +python manage.py collectstatic --noinput +python manage.py migrate --noinput +python manage.py create_default_admin +exec gunicorn photo21.wsgi:application --bind 0.0.0.0:8000 --workers 3 diff --git a/photo21/settings.py b/photo21/settings.py index a80e727..ec542ea 100644 --- a/photo21/settings.py +++ b/photo21/settings.py @@ -153,7 +153,7 @@ elif _db_engine == "sqlite": DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", - "NAME": os.path.join(BASE_DIR, "db.sqlite3"), + "NAME": config("DB_PATH", default=os.path.join(BASE_DIR, "db.sqlite3")), "OPTIONS": { "timeout": 10, }, diff --git a/photologue/management/commands/create_default_admin.py b/photologue/management/commands/create_default_admin.py new file mode 100644 index 0000000..b9fee01 --- /dev/null +++ b/photologue/management/commands/create_default_admin.py @@ -0,0 +1,32 @@ +from django.contrib.auth import get_user_model +from django.core.management.base import BaseCommand + + +class Command(BaseCommand): + help = "Create default admin user (admin@localhost / admin) if it does not exist" + + def handle(self, *args, **kwargs): + User = get_user_model() + email = "admin@localhost" + username = "admin" + password = "admin" + + if User.objects.filter(username=username).exists(): + self.stdout.write("Default admin already exists, skipping.") + return + + user = User.objects.create_superuser(username=username, email=email, password=password) + + # Mark the email as verified via allauth + try: + from allauth.account.models import EmailAddress + EmailAddress.objects.create( + user=user, + email=email, + primary=True, + verified=True, + ) + except Exception as e: + self.stderr.write(f"Could not create allauth EmailAddress: {e}") + + self.stdout.write(f"Default admin created: {username} / {password}") diff --git a/requirements.txt b/requirements.txt index 242cbf3..56bdc65 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,5 +7,6 @@ Pillow>=6.0.0 django-debug-toolbar>=3.2.0 python-decouple>=3.6 whitenoise>=6.0 -psycopg2>=2.9 +psycopg2-binary>=2.9 requests>=2.25 +gunicorn>=21.0 From f782c589765af0bf0b049f0d3cfa27fae384992b Mon Sep 17 00:00:00 2001 From: krek0 Date: Sun, 3 May 2026 18:29:54 +0200 Subject: [PATCH 10/27] update readme --- README.md | 119 +++++++++++++++++++++++++----------------------------- 1 file changed, 56 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index f54258c..af3ec10 100644 --- a/README.md +++ b/README.md @@ -1,72 +1,17 @@ -# Photo server 2021-2023 - -This project is a fork of [Photo21](https://gitlab.crans.org/bde/photo21/) developped at ENS Paris-Saclay. +# Photo server [![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0.txt) This is the source code for the webserver hosting pictures from the -ENS Paris-Saclay student life. +ENS Rennes student life. The philosophy of this project is to keep this code as simple as possible to run and to maintain. -## Setup +This project is a fork of [Photo21](https://gitlab.crans.org/bde/photo21/), +originally developed at ENS Paris-Saclay. -1. **Dependency installation.** - If you are not using Debian, please feel free to adapt the following instructions. - - ```bash - sudo apt install git gettext python3-django python3-django-allauth python3-django-crispy-forms python3-docutils python3-exifread python3-pil - ``` - -2. **Cloning.** - Change directory to where you want the project to be. - In production, we usually use `/var/www/photos/` as the `root` user. - - ```bash - git clone https://codeberg.org/krek0/photo21.git && cd photo21 - ``` - -3. **Configuration (production only).** - - ```bash - sudo mkdir static media - sudo chmod +x maintenance_tool.sh - ``` - -4. **Database (production only).** - In development, you may use SQLite (no setup). - In production, we use PostgreSQL which require a bit of setup: - - ```bash - sudo apt install postgresql postgresql-contrib - sudo -u postgres psql - postgres=# CREATE USER photo21 WITH PASSWORD 'un_mot_de_passe_sur'; - postgres=# CREATE DATABASE photo21 OWNER photo21; - ``` - -5. **Initialization.**, - In production, please use `www-data` user. - - ``` - ./manage.py collectstatic - ./manage.py check - ./manage.py migrate - ./manage.py compilemessages - - # Only when creating a new database - ./manage.py loaddata initial - ./manage.py createsuperuser - # change DEBUG to True in photo21/settings.py - ``` - -6. **Maintenance Mode.**, - In production to toggle the server mainteance mode - - ```./maintenance_tool.sh``` - - -## Docker install +## Docker install (recommended for production) 1. Create a `docker-compose.yml` (a ready-to-use file is provided in the repository): @@ -136,11 +81,59 @@ run and to maintain. - `SECRET_KEY` — Django secret key (use a long random string) - Log in to the admin panel and change the `admin` user password ---- +## Development setup -6. *Enjoy \o/* +1. **Cloning.** + Change directory to where you want the project to be. - In development, you can launch the development server using: + ```bash + git clone https://codeberg.org/krek0/photo21.git && cd photo21 + ``` + +2. **Dependency installation.** + If you are not using Debian, please feel free to adapt the following instructions. + + ```bash + sudo apt install git gettext python3-django python3-django-allauth python3-django-crispy-forms python3-docutils python3-exifread python3-pil + ``` + +3. **Configuration.** + + ```bash + sudo mkdir static media + sudo chmod +x maintenance_tool.sh + ``` + +4. **Database.** + In development, you may use SQLite (no setup). + In production, we use PostgreSQL which require a bit of setup: + + ```bash + sudo apt install postgresql postgresql-contrib + sudo -u postgres psql + postgres=# CREATE USER photo21 WITH PASSWORD 'your_password'; + postgres=# CREATE DATABASE photo21 OWNER photo21; + ``` + +5. **Initialization.** + + ``` + ./manage.py collectstatic + ./manage.py check + ./manage.py migrate + ./manage.py compilemessages + + # Only when creating a new database + ./manage.py loaddata initial + ./manage.py createsuperuser + ``` + +6. **Maintenance Mode.** + In production to toggle the server maintenance mode + + ```./maintenance_tool.sh``` + +7. *Enjoy \o/* ```bash (env)$ ./manage.py runserver From dcd944b8c67b2a3ef709f1cb992b4f62e3762d96 Mon Sep 17 00:00:00 2001 From: krek0 Date: Sun, 3 May 2026 23:35:24 +0200 Subject: [PATCH 11/27] Drop de/es locales, complete fr translations, add navbar language switcher. --- Dockerfile | 4 + photo21/locale/de/LC_MESSAGES/django.po | 321 --------------------- photo21/locale/es/LC_MESSAGES/django.po | 320 -------------------- photo21/locale/fr/LC_MESSAGES/django.po | 297 +++++++++---------- photo21/settings.py | 2 - photo21/static/lang-select.js | 11 +- photo21/templates/base.html | 45 +-- photo21/templates/index.html | 10 +- photologue/locale/fr/LC_MESSAGES/django.po | 228 ++++++++------- 9 files changed, 309 insertions(+), 929 deletions(-) delete mode 100644 photo21/locale/de/LC_MESSAGES/django.po delete mode 100644 photo21/locale/es/LC_MESSAGES/django.po diff --git a/Dockerfile b/Dockerfile index f24c177..d45d054 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,8 +8,12 @@ WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt +RUN apt-get update && apt-get install -y gettext && rm -rf /var/lib/apt/lists/* + COPY . . +RUN python manage.py compilemessages + # Create volume mount points RUN mkdir -p /app/media /app/static /app/data diff --git a/photo21/locale/de/LC_MESSAGES/django.po b/photo21/locale/de/LC_MESSAGES/django.po deleted file mode 100644 index b1514bd..0000000 --- a/photo21/locale/de/LC_MESSAGES/django.po +++ /dev/null @@ -1,321 +0,0 @@ -# This file is part of photo21 -# Copyright (C) 2022 Amicale des élèves de l'ENS Paris-Saclay -# SPDX-License-Identifier: GPL-3.0-or-later -# -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: PACKAGE VERSION\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-12-07 20:03+0100\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE \n" -"Language: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#: .\photo21\forms.py:16 -msgid "" -"Please enter a valid email address ending with `@crans.org` or `@ens-paris-" -"saclay.fr`." -msgstr "" - -#: .\photo21\forms.py:29 -msgid "Must end with `@crans.org` or `@ens-paris-saclay.fr`." -msgstr "" - -#: .\photo21\settings.py:171 -msgid "German" -msgstr "" - -#: .\photo21\settings.py:172 -msgid "English" -msgstr "" - -#: .\photo21\settings.py:173 -msgid "Spanish" -msgstr "" - -#: .\photo21\settings.py:174 -msgid "French" -msgstr "" - -#: .\photo21\templates\400.html:12 -msgid "Bad request" -msgstr "" - -#: .\photo21\templates\400.html:16 -msgid "" -"Sorry, your request was bad. Don't know what could be wrong. An email has " -"been sent to webmasters with the details of the error. You can now drink a " -"coke." -msgstr "" - -#: .\photo21\templates\403.html:12 -msgid "Permission denied" -msgstr "" - -#: .\photo21\templates\403.html:15 -msgid "You don't have the right to perform this request." -msgstr "" - -#: .\photo21\templates\403.html:17 .\photo21\templates\404.html:21 -msgid "Exception message:" -msgstr "" - -#: .\photo21\templates\404.html:12 -msgid "Page not found" -msgstr "" - -#: .\photo21\templates\404.html:16 -#, python-format -msgid "" -"The requested path %(request_path)s was not found on the server." -msgstr "" - -#: .\photo21\templates\500.html:12 -msgid "Server error" -msgstr "" - -#: .\photo21\templates\500.html:16 -msgid "" -"Sorry, an error occurred when processing your request. An email has been " -"sent to webmasters with the detail of the error, and this will be fixed " -"soon. You can go drink a soft." -msgstr "" - -#: .\photo21\templates\account\email.html:8 -#: .\photo21\templates\account\email.html:16 -#: .\photo21\templates\socialaccount\connections.html:16 -msgid "E-mail Addresses" -msgstr "" - -#: .\photo21\templates\account\email.html:11 .\photo21\templates\base.html:63 -#: .\photo21\templates\socialaccount\connections.html:11 -msgid "Account" -msgstr "" - -#: .\photo21\templates\account\email.html:19 -#: .\photo21\templates\socialaccount\connections.html:19 -msgid "Social connections" -msgstr "" - -#: .\photo21\templates\account\email.html:25 -msgid "The following e-mail addresses are associated with your account:" -msgstr "" - -#: .\photo21\templates\account\email.html:36 -msgid "Verified" -msgstr "" - -#: .\photo21\templates\account\email.html:38 -msgid "Unverified" -msgstr "" - -#: .\photo21\templates\account\email.html:40 -msgid "Primary" -msgstr "" - -#: .\photo21\templates\account\email.html:46 -msgid "Make Primary" -msgstr "" - -#: .\photo21\templates\account\email.html:47 -msgid "Re-send Verification" -msgstr "" - -#: .\photo21\templates\account\email.html:48 -#: .\photo21\templates\socialaccount\connections.html:47 -msgid "Remove" -msgstr "" - -#: .\photo21\templates\account\email.html:53 -msgid "Warning:" -msgstr "" - -#: .\photo21\templates\account\email.html:53 -msgid "" -"You currently do not have any e-mail address set up. You should really add " -"an e-mail address so you can receive notifications, reset your password, etc." -msgstr "" - -#: .\photo21\templates\account\email.html:57 -msgid "Add E-mail Address" -msgstr "" - -#: .\photo21\templates\account\email.html:62 -msgid "Add E-mail" -msgstr "" - -#: .\photo21\templates\account\login.html:8 -#: .\photo21\templates\account\login.html:36 -msgid "Sign In" -msgstr "" - -#: .\photo21\templates\account\login.html:19 -#, python-format -msgid "" -"Please sign in with one of your existing third party accounts. Or, sign up for a %(site_name)s account and sign in " -"below:" -msgstr "" - -#: .\photo21\templates\account\login.html:26 -#, python-format -msgid "" -"If you have not created an account yet, then please sign up first." -msgstr "" - -#: .\photo21\templates\account\login.html:39 -msgid "Forgot Password?" -msgstr "" - -#: .\photo21\templates\account\login.html:42 -msgid "If any problem, please contact the server owners at" -msgstr "" - -#: .\photo21\templates\account\logout.html:8 -#: .\photo21\templates\account\logout.html:13 -#: .\photo21\templates\account\logout.html:22 -msgid "Sign Out" -msgstr "" - -#: .\photo21\templates\account\logout.html:16 -msgid "Are you sure you want to sign out?" -msgstr "" - -#: .\photo21\templates\account\signup.html:8 -msgid "Signup" -msgstr "" - -#: .\photo21\templates\account\signup.html:13 -#: .\photo21\templates\account\signup.html:24 -msgid "Sign Up" -msgstr "" - -#: .\photo21\templates\account\signup.html:16 -#, python-format -msgid "" -"Already have an account? Then please sign in." -msgstr "" - -#: .\photo21\templates\base.html:16 -msgid "The ENS Paris-Saclay pictures server." -msgstr "" - -#: .\photo21\templates\base.html:41 -msgid "Galleries" -msgstr "" - -#: .\photo21\templates\base.html:46 -msgid "Upload" -msgstr "" - -#: .\photo21\templates\base.html:51 -msgid "Manage" -msgstr "" - -#: .\photo21\templates\base.html:72 -msgid "Log out" -msgstr "" - -#: .\photo21\templates\base.html:82 -msgid "Log in" -msgstr "" - -#: .\photo21\templates\base.html:91 -msgid "Sign up" -msgstr "" - -#: .\photo21\templates\base.html:116 -msgid "Connected as" -msgstr "" - -#: .\photo21\templates\base.html:118 -msgid "Source code" -msgstr "" - -#: .\photo21\templates\index.html:8 -msgid "Home" -msgstr "" - -#: .\photo21\templates\index.html:11 -msgid "Welcome to the pictures server!" -msgstr "" - -#: .\photo21\templates\index.html:13 -msgid "" -"This website aims to collect the pictures and movies taken in the student " -"life of ENS Paris-Saclay or involving its students." -msgstr "" - -#: .\photo21\templates\index.html:20 -#, python-format -msgid "" -"The pictures are visible in the " -"galleries and are downloadable. However, the agreement of the " -"photographer and the persons present on the photo is necessary before any " -"republication on another platform. " -msgstr "" - -#: .\photo21\templates\index.html:29 -msgid "" -"If you want a photo to be deleted, please let us know: Abuse request" -msgstr "" - -#: .\photo21\templates\index.html:36 -msgid "" -"If you want to obtain the right to upload pictures, please let us know: Become a photograph" -msgstr "" - -#: .\photo21\templates\index.html:43 -msgid "Last galleries" -msgstr "" - -#: .\photo21\templates\index.html:52 -msgid "Behind the scene" -msgstr "" - -#: .\photo21\templates\index.html:54 -msgid "" -"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:" -msgstr "" - -#: .\photo21\templates\index.html:63 -msgid "They should be contacted at" -msgstr "" - -#: .\photo21\templates\socialaccount\connections.html:8 -msgid "Account Connections" -msgstr "" - -#: .\photo21\templates\socialaccount\connections.html:25 -msgid "" -"You can sign in to your account using any of the following third party " -"accounts:" -msgstr "" - -#: .\photo21\templates\socialaccount\connections.html:53 -msgid "" -"You currently have no social network accounts connected to this account." -msgstr "" - -#: .\photo21\templates\socialaccount\connections.html:56 -msgid "Add a 3rd Party Account" -msgstr "" - -#: .\photo21\templates\socialaccount\snippets\provider_list.html:20 -msgid "Sign in with" -msgstr "" diff --git a/photo21/locale/es/LC_MESSAGES/django.po b/photo21/locale/es/LC_MESSAGES/django.po deleted file mode 100644 index a19cb2b..0000000 --- a/photo21/locale/es/LC_MESSAGES/django.po +++ /dev/null @@ -1,320 +0,0 @@ -# This file is part of photo21 -# Copyright (C) 2022 Amicale des élèves de l'ENS Paris-Saclay -# SPDX-License-Identifier: GPL-3.0-or-later -# -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: PACKAGE VERSION\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-12-07 20:03+0100\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE \n" -"Language: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" - -#: .\photo21\forms.py:16 -msgid "" -"Please enter a valid email address ending with `@crans.org` or `@ens-paris-" -"saclay.fr`." -msgstr "" - -#: .\photo21\forms.py:29 -msgid "Must end with `@crans.org` or `@ens-paris-saclay.fr`." -msgstr "" - -#: .\photo21\settings.py:171 -msgid "German" -msgstr "" - -#: .\photo21\settings.py:172 -msgid "English" -msgstr "" - -#: .\photo21\settings.py:173 -msgid "Spanish" -msgstr "" - -#: .\photo21\settings.py:174 -msgid "French" -msgstr "" - -#: .\photo21\templates\400.html:12 -msgid "Bad request" -msgstr "" - -#: .\photo21\templates\400.html:16 -msgid "" -"Sorry, your request was bad. Don't know what could be wrong. An email has " -"been sent to webmasters with the details of the error. You can now drink a " -"coke." -msgstr "" - -#: .\photo21\templates\403.html:12 -msgid "Permission denied" -msgstr "" - -#: .\photo21\templates\403.html:15 -msgid "You don't have the right to perform this request." -msgstr "" - -#: .\photo21\templates\403.html:17 .\photo21\templates\404.html:21 -msgid "Exception message:" -msgstr "" - -#: .\photo21\templates\404.html:12 -msgid "Page not found" -msgstr "" - -#: .\photo21\templates\404.html:16 -#, python-format -msgid "" -"The requested path %(request_path)s was not found on the server." -msgstr "" - -#: .\photo21\templates\500.html:12 -msgid "Server error" -msgstr "" - -#: .\photo21\templates\500.html:16 -msgid "" -"Sorry, an error occurred when processing your request. An email has been " -"sent to webmasters with the detail of the error, and this will be fixed " -"soon. You can go drink a soft." -msgstr "" - -#: .\photo21\templates\account\email.html:8 -#: .\photo21\templates\account\email.html:16 -#: .\photo21\templates\socialaccount\connections.html:16 -msgid "E-mail Addresses" -msgstr "" - -#: .\photo21\templates\account\email.html:11 .\photo21\templates\base.html:63 -#: .\photo21\templates\socialaccount\connections.html:11 -msgid "Account" -msgstr "" - -#: .\photo21\templates\account\email.html:19 -#: .\photo21\templates\socialaccount\connections.html:19 -msgid "Social connections" -msgstr "" - -#: .\photo21\templates\account\email.html:25 -msgid "The following e-mail addresses are associated with your account:" -msgstr "" - -#: .\photo21\templates\account\email.html:36 -msgid "Verified" -msgstr "" - -#: .\photo21\templates\account\email.html:38 -msgid "Unverified" -msgstr "" - -#: .\photo21\templates\account\email.html:40 -msgid "Primary" -msgstr "" - -#: .\photo21\templates\account\email.html:46 -msgid "Make Primary" -msgstr "" - -#: .\photo21\templates\account\email.html:47 -msgid "Re-send Verification" -msgstr "" - -#: .\photo21\templates\account\email.html:48 -#: .\photo21\templates\socialaccount\connections.html:47 -msgid "Remove" -msgstr "" - -#: .\photo21\templates\account\email.html:53 -msgid "Warning:" -msgstr "" - -#: .\photo21\templates\account\email.html:53 -msgid "" -"You currently do not have any e-mail address set up. You should really add " -"an e-mail address so you can receive notifications, reset your password, etc." -msgstr "" - -#: .\photo21\templates\account\email.html:57 -msgid "Add E-mail Address" -msgstr "" - -#: .\photo21\templates\account\email.html:62 -msgid "Add E-mail" -msgstr "" - -#: .\photo21\templates\account\login.html:8 -#: .\photo21\templates\account\login.html:36 -msgid "Sign In" -msgstr "" - -#: .\photo21\templates\account\login.html:19 -#, python-format -msgid "" -"Please sign in with one of your existing third party accounts. Or, sign up for a %(site_name)s account and sign in " -"below:" -msgstr "" - -#: .\photo21\templates\account\login.html:26 -#, python-format -msgid "" -"If you have not created an account yet, then please sign up first." -msgstr "" - -#: .\photo21\templates\account\login.html:39 -msgid "Forgot Password?" -msgstr "" - -#: .\photo21\templates\account\login.html:42 -msgid "If any problem, please contact the server owners at" -msgstr "" - -#: .\photo21\templates\account\logout.html:8 -#: .\photo21\templates\account\logout.html:13 -#: .\photo21\templates\account\logout.html:22 -msgid "Sign Out" -msgstr "" - -#: .\photo21\templates\account\logout.html:16 -msgid "Are you sure you want to sign out?" -msgstr "" - -#: .\photo21\templates\account\signup.html:8 -msgid "Signup" -msgstr "" - -#: .\photo21\templates\account\signup.html:13 -#: .\photo21\templates\account\signup.html:24 -msgid "Sign Up" -msgstr "" - -#: .\photo21\templates\account\signup.html:16 -#, python-format -msgid "" -"Already have an account? Then please sign in." -msgstr "" - -#: .\photo21\templates\base.html:16 -msgid "The ENS Paris-Saclay pictures server." -msgstr "" - -#: .\photo21\templates\base.html:41 -msgid "Galleries" -msgstr "" - -#: .\photo21\templates\base.html:46 -msgid "Upload" -msgstr "" - -#: .\photo21\templates\base.html:51 -msgid "Manage" -msgstr "" - -#: .\photo21\templates\base.html:72 -msgid "Log out" -msgstr "" - -#: .\photo21\templates\base.html:82 -msgid "Log in" -msgstr "" - -#: .\photo21\templates\base.html:91 -msgid "Sign up" -msgstr "" - -#: .\photo21\templates\base.html:116 -msgid "Connected as" -msgstr "" - -#: .\photo21\templates\base.html:118 -msgid "Source code" -msgstr "" - -#: .\photo21\templates\index.html:8 -msgid "Home" -msgstr "" - -#: .\photo21\templates\index.html:11 -msgid "Welcome to the pictures server!" -msgstr "" - -#: .\photo21\templates\index.html:13 -msgid "" -"This website aims to collect the pictures and movies taken in the student " -"life of ENS Paris-Saclay or involving its students." -msgstr "" - -#: .\photo21\templates\index.html:20 -#, python-format -msgid "" -"The pictures are visible in the " -"galleries and are downloadable. However, the agreement of the " -"photographer and the persons present on the photo is necessary before any " -"republication on another platform. " -msgstr "" - -#: .\photo21\templates\index.html:29 -msgid "" -"If you want a photo to be deleted, please let us know: Abuse request" -msgstr "" - -#: .\photo21\templates\index.html:36 -msgid "" -"If you want to obtain the right to upload pictures, please let us know: Become a photograph" -msgstr "" - -#: .\photo21\templates\index.html:43 -msgid "Last galleries" -msgstr "" - -#: .\photo21\templates\index.html:52 -msgid "Behind the scene" -msgstr "" - -#: .\photo21\templates\index.html:54 -msgid "" -"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:" -msgstr "" - -#: .\photo21\templates\index.html:63 -msgid "They should be contacted at" -msgstr "" - -#: .\photo21\templates\socialaccount\connections.html:8 -msgid "Account Connections" -msgstr "" - -#: .\photo21\templates\socialaccount\connections.html:25 -msgid "" -"You can sign in to your account using any of the following third party " -"accounts:" -msgstr "" - -#: .\photo21\templates\socialaccount\connections.html:53 -msgid "" -"You currently have no social network accounts connected to this account." -msgstr "" - -#: .\photo21\templates\socialaccount\connections.html:56 -msgid "Add a 3rd Party Account" -msgstr "" - -#: .\photo21\templates\socialaccount\snippets\provider_list.html:20 -msgid "Sign in with" -msgstr "" diff --git a/photo21/locale/fr/LC_MESSAGES/django.po b/photo21/locale/fr/LC_MESSAGES/django.po index de21f66..6c10127 100644 --- a/photo21/locale/fr/LC_MESSAGES/django.po +++ b/photo21/locale/fr/LC_MESSAGES/django.po @@ -1,163 +1,158 @@ # This file is part of photo21 # Copyright (C) 2022 Amicale des élèves de l'ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later -# -#, fuzzy + msgid "" msgstr "" -"Project-Id-Version: PACKAGE VERSION\n" +"Project-Id-Version: photo21\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-12-07 20:03+0100\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE \n" -"Language: \n" +"Language-Team: French\n" +"Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" -#: .\photo21\forms.py:16 -msgid "" -"Please enter a valid email address ending with `@crans.org` or `@ens-paris-" -"saclay.fr`." -msgstr "" -"Veuillez entrer une adresse email valide finissant par `@crans.org` ou `@ens-" -"paris-saclay.fr`." +#: photo21/forms.py:16 +msgid "Please enter a valid email address ending with `@ens-rennes.fr`" +msgstr "Veuillez entrer une adresse e-mail valide finissant par `@ens-rennes.fr`." -#: .\photo21\forms.py:29 -msgid "Must end with `@crans.org` or `@ens-paris-saclay.fr`." -msgstr "Doit finir par `@crans.org` ou `@ens-paris-saclay.fr`." +#: photo21/forms.py:26 +msgid "Must end with `@ens-rennes.fr`." +msgstr "Doit finir par `@ens-rennes.fr`." -#: .\photo21\settings.py:171 -msgid "German" -msgstr "" - -#: .\photo21\settings.py:172 +#: photo21/settings.py:201 msgid "English" -msgstr "" +msgstr "Anglais" -#: .\photo21\settings.py:173 -msgid "Spanish" -msgstr "" - -#: .\photo21\settings.py:174 +#: photo21/settings.py:202 msgid "French" -msgstr "" +msgstr "Français" -#: .\photo21\templates\400.html:12 +#: photo21/templates/400.html:12 msgid "Bad request" -msgstr "" +msgstr "Requête incorrecte" -#: .\photo21\templates\400.html:16 +#: photo21/templates/400.html:16 msgid "" "Sorry, your request was bad. Don't know what could be wrong. An email has " "been sent to webmasters with the details of the error. You can now drink a " "coke." msgstr "" +"Désolé, votre requête était incorrecte. Un e-mail a été envoyé aux " +"administrateurs avec les détails de l'erreur. Vous pouvez aller boire un " +"coca." -#: .\photo21\templates\403.html:12 +#: photo21/templates/403.html:12 msgid "Permission denied" -msgstr "" +msgstr "Permission refusée" -#: .\photo21\templates\403.html:15 +#: photo21/templates/403.html:15 msgid "You don't have the right to perform this request." -msgstr "" +msgstr "Vous n'avez pas le droit d'effectuer cette requête." -#: .\photo21\templates\403.html:17 .\photo21\templates\404.html:21 +#: photo21/templates/403.html:17 photo21/templates/404.html:21 msgid "Exception message:" -msgstr "" +msgstr "Message d'exception :" -#: .\photo21\templates\404.html:12 +#: photo21/templates/404.html:12 msgid "Page not found" -msgstr "" +msgstr "Page introuvable" -#: .\photo21\templates\404.html:16 +#: photo21/templates/404.html:16 #, python-format msgid "" "The requested path %(request_path)s was not found on the server." msgstr "" +"Le chemin demandé %(request_path)s n'a pas été trouvé sur le " +"serveur." -#: .\photo21\templates\500.html:12 +#: photo21/templates/500.html:12 msgid "Server error" -msgstr "" +msgstr "Erreur serveur" -#: .\photo21\templates\500.html:16 +#: photo21/templates/500.html:16 msgid "" "Sorry, an error occurred when processing your request. An email has been " "sent to webmasters with the detail of the error, and this will be fixed " "soon. You can go drink a soft." msgstr "" +"Désolé, une erreur s'est produite lors du traitement de votre requête. Un e-" +"mail a été envoyé aux administrateurs avec les détails de l'erreur, et cela " +"sera corrigé prochainement. Vous pouvez aller boire un soda." -#: .\photo21\templates\account\email.html:8 -#: .\photo21\templates\account\email.html:16 -#: .\photo21\templates\socialaccount\connections.html:16 +#: photo21/templates/account/email.html:8 +#: photo21/templates/account/email.html:16 +#: photo21/templates/socialaccount/connections.html:16 msgid "E-mail Addresses" -msgstr "" +msgstr "Adresses e-mail" -#: .\photo21\templates\account\email.html:11 .\photo21\templates\base.html:63 -#: .\photo21\templates\socialaccount\connections.html:11 +#: photo21/templates/account/email.html:11 photo21/templates/base.html:66 +#: photo21/templates/socialaccount/connections.html:11 msgid "Account" msgstr "Compte" -#: .\photo21\templates\account\email.html:19 -#: .\photo21\templates\socialaccount\connections.html:19 +#: photo21/templates/account/email.html:19 +#: photo21/templates/socialaccount/connections.html:19 msgid "Social connections" msgstr "Connexions sociales" -#: .\photo21\templates\account\email.html:25 +#: photo21/templates/account/email.html:25 msgid "The following e-mail addresses are associated with your account:" -msgstr "" +msgstr "Les adresses e-mail suivantes sont associées à votre compte :" -#: .\photo21\templates\account\email.html:36 +#: photo21/templates/account/email.html:36 msgid "Verified" -msgstr "" +msgstr "Vérifié" -#: .\photo21\templates\account\email.html:38 +#: photo21/templates/account/email.html:38 msgid "Unverified" -msgstr "" +msgstr "Non vérifié" -#: .\photo21\templates\account\email.html:40 +#: photo21/templates/account/email.html:40 msgid "Primary" -msgstr "" +msgstr "Principal" -#: .\photo21\templates\account\email.html:46 +#: photo21/templates/account/email.html:46 msgid "Make Primary" -msgstr "" +msgstr "Définir comme principal" -#: .\photo21\templates\account\email.html:47 +#: photo21/templates/account/email.html:47 msgid "Re-send Verification" -msgstr "" +msgstr "Renvoyer la vérification" -#: .\photo21\templates\account\email.html:48 -#: .\photo21\templates\socialaccount\connections.html:47 +#: photo21/templates/account/email.html:48 +#: photo21/templates/socialaccount/connections.html:47 msgid "Remove" -msgstr "" +msgstr "Supprimer" -#: .\photo21\templates\account\email.html:53 +#: photo21/templates/account/email.html:53 msgid "Warning:" -msgstr "" +msgstr "Avertissement :" -#: .\photo21\templates\account\email.html:53 +#: photo21/templates/account/email.html:53 msgid "" "You currently do not have any e-mail address set up. You should really add " "an e-mail address so you can receive notifications, reset your password, etc." msgstr "" +"Vous n'avez actuellement aucune adresse e-mail configurée. Vous devriez " +"vraiment en ajouter une pour recevoir des notifications, réinitialiser votre " +"mot de passe, etc." -#: .\photo21\templates\account\email.html:57 +#: photo21/templates/account/email.html:57 msgid "Add E-mail Address" msgstr "Ajouter une Adresse E-mail" -#: .\photo21\templates\account\email.html:62 +#: photo21/templates/account/email.html:62 msgid "Add E-mail" msgstr "Ajouter un E-mail" -#: .\photo21\templates\account\login.html:8 -#: .\photo21\templates\account\login.html:36 +#: photo21/templates/account/login.html:8 +#: photo21/templates/account/login.html:36 msgid "Sign In" msgstr "Se Connecter" -#: .\photo21\templates\account\login.html:19 +#: photo21/templates/account/login.html:19 #, python-format msgid "" "Please sign in with one of your existing third party accounts. Or, inscrivez-vous pour un compte sur %(site_name)s " "et identifiez-vous ci-dessous :" -#: .\photo21\templates\account\login.html:26 +#: photo21/templates/account/login.html:26 #, python-format msgid "" "If you have not created an account yet, then please inscrire." -#: .\photo21\templates\account\login.html:39 +#: photo21/templates/account/login.html:39 msgid "Forgot Password?" msgstr "Mot de passe oublié ?" -#: .\photo21\templates\account\login.html:42 +#: photo21/templates/account/login.html:42 msgid "If any problem, please contact the server owners at" msgstr "En cas de problème, contactez les administrateurs à" -#: .\photo21\templates\account\logout.html:8 -#: .\photo21\templates\account\logout.html:13 -#: .\photo21\templates\account\logout.html:22 +#: photo21/templates/account/logout.html:8 +#: photo21/templates/account/logout.html:13 +#: photo21/templates/account/logout.html:22 msgid "Sign Out" msgstr "Déconnexion" -#: .\photo21\templates\account\logout.html:16 +#: photo21/templates/account/logout.html:16 msgid "Are you sure you want to sign out?" -msgstr "" +msgstr "Êtes-vous sûr de vouloir vous déconnecter ?" -#: .\photo21\templates\account\signup.html:8 +#: photo21/templates/account/signup.html:8 msgid "Signup" -msgstr "" +msgstr "Inscription" -#: .\photo21\templates\account\signup.html:13 -#: .\photo21\templates\account\signup.html:24 +#: photo21/templates/account/signup.html:13 +#: photo21/templates/account/signup.html:24 msgid "Sign Up" -msgstr "" +msgstr "S'inscrire" -#: .\photo21\templates\account\signup.html:16 +#: photo21/templates/account/signup.html:16 #, python-format msgid "" "Already have an account? Then please sign in." msgstr "" +"Vous avez déjà un compte ? Veuillez alors vous " +"connecter." -#: .\photo21\templates\base.html:16 -msgid "The ENS Paris-Saclay pictures server." -msgstr "" +#: photo21/templates/base.html:16 +msgid "The ENS Rennes pictures server." +msgstr "Le serveur photos de l'ENS Rennes." -#: .\photo21\templates\base.html:41 +#: photo21/templates/base.html:43 msgid "Galleries" msgstr "Galeries" -#: .\photo21\templates\base.html:46 +#: photo21/templates/base.html:48 msgid "Upload" -msgstr "Téléversement" +msgstr "Upload" -#: .\photo21\templates\base.html:51 +#: photo21/templates/base.html:53 msgid "Manage" -msgstr "Gestion" +msgstr "Manage" -#: .\photo21\templates\base.html:72 +#: photo21/templates/base.html:75 msgid "Log out" -msgstr "" +msgstr "Déconnection" -#: .\photo21\templates\base.html:82 +#: photo21/templates/base.html:85 msgid "Log in" -msgstr "" +msgstr "Connection" -#: .\photo21\templates\base.html:91 +#: photo21/templates/base.html:94 msgid "Sign up" msgstr "Inscription" -#: .\photo21\templates\base.html:116 +#: photo21/templates/base.html:119 msgid "Connected as" msgstr "Connecté en tant que" -#: .\photo21\templates\base.html:118 +#: photo21/templates/base.html:121 msgid "Source code" msgstr "Code source" -#: .\photo21\templates\index.html:8 +#: photo21/templates/index.html:8 msgid "Home" msgstr "Accueil" -#: .\photo21\templates\index.html:11 +#: photo21/templates/index.html:11 msgid "Welcome to the pictures server!" msgstr "Bienvenue sur le serveur photos !" -#: .\photo21\templates\index.html:13 +#: photo21/templates/index.html:13 msgid "" "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." msgstr "" -"Ce site à pour objectif de recenser les photos et films pris dans la vie " -"associative de l'ENS Paris-Saclay ou impliquant ses usager·ères." +"Ce site a pour objectif de recenser les photos et films pris dans la vie " +"associative de l'ENS Rennes ou impliquant ses étudiant·es." -#: .\photo21\templates\index.html:20 +#: photo21/templates/index.html:20 #, python-format msgid "" "The pictures are visible in the " @@ -275,73 +272,77 @@ msgstr "" "photographe et des personnes présentes sur la photo est nécessaire avant " "toute republication sur un autre site." -#: .\photo21\templates\index.html:29 -msgid "" -"If you want a photo to be deleted, please let us know: Abuse request" -msgstr "" -"Si vous souhaitez qu'une photo soit supprimée, signalez le nous : Signaler un abus" - -#: .\photo21\templates\index.html:36 +#: photo21/templates/index.html:30 msgid "" "If you want to obtain the right to upload pictures, please let us know: Become a photograph" msgstr "" "Si vous souhaitez obtenir les droits photographes pour téléverser vos " -"photos, signalez le nous : Devenir photographe" -#: .\photo21\templates\index.html:43 +#: photo21/templates/index.html:38 msgid "Last galleries" msgstr "Galeries récentes" -#: .\photo21\templates\index.html:52 +#: photo21/templates/index.html:47 msgid "Behind the scene" -msgstr "Derrière la scène" +msgstr "Behind the scene" -#: .\photo21\templates\index.html:54 +#: photo21/templates/index.html:49 msgid "" -"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:" +" " +"The dedicated server running this website is kindly hosted by Sinfonie at the ENS Rennes. " +" " +"Le serveur qui fait fonctionner ce site est gentiment hébergé par " +"Sinfonie à l'ENS Rennes. " +" - - - - - - - - - -

- +

+ {% if request.user.is_authenticated %} + {% trans "Connected as" %} {{ request.user.username }} · + {% endif %} + {% trans "Source code" %} +

+ diff --git a/photo21/templates/index.html b/photo21/templates/index.html index d4db262..252198c 100644 --- a/photo21/templates/index.html +++ b/photo21/templates/index.html @@ -47,11 +47,11 @@ 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 + Sinfonie at the ENS Rennes. diff --git a/photologue/locale/fr/LC_MESSAGES/django.po b/photologue/locale/fr/LC_MESSAGES/django.po index 1993f72..b60677f 100644 --- a/photologue/locale/fr/LC_MESSAGES/django.po +++ b/photologue/locale/fr/LC_MESSAGES/django.po @@ -10,7 +10,7 @@ msgid "" msgstr "" "Project-Id-Version: Photologue\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-03-05 15:30+0100\n" +"POT-Creation-Date: 2026-05-03 21:01+0000\n" "PO-Revision-Date: 2017-12-03 14:47+0000\n" "Last-Translator: Richard Barran \n" "Language-Team: French (http://www.transifex.com/richardbarran/django-" @@ -21,24 +21,23 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" -#: .\photologue\admin.py:26 .\photologue\models.py:168 -#: .\photologue\models.py:786 +#: photologue/admin.py:25 photologue/models.py:168 photologue/models.py:802 msgid "tags" msgstr "balises" -#: .\photologue\admin.py:63 .\photologue\forms.py:68 +#: photologue/admin.py:62 photologue/forms.py:69 msgid "Gallery" msgstr "Galerie" -#: .\photologue\admin.py:67 .\photologue\models.py:529 +#: photologue/admin.py:66 photologue/models.py:542 msgid "owner" msgstr "propriétaire" -#: .\photologue\forms.py:70 +#: photologue/forms.py:71 msgid "-- Create a new gallery --" msgstr "-- Créer une nouvelle galerie --" -#: .\photologue\forms.py:72 +#: photologue/forms.py:73 msgid "" "Select a gallery to add these images to. Leave this empty to create a new " "gallery from the supplied title." @@ -46,117 +45,108 @@ msgstr "" "Sélectionner une galerie à laquelle ajouter ces images. Laisser ce champ " "vide pour créer une nouvelle galerie à partir du titre indiqué." -#: .\photologue\forms.py:77 +#: photologue/forms.py:78 msgid "New gallery title" msgstr "Titre de la nouvelle galerie" -#: .\photologue\forms.py:82 +#: photologue/forms.py:83 msgid "New gallery event start date" msgstr "Date de début de l'évènement de la nouvelle galerie" -#: .\photologue\forms.py:87 +#: photologue/forms.py:89 msgid "New gallery event end date" msgstr "Date de fin de l'évènement de la nouvelle galerie" -#: .\photologue\forms.py:93 +#: photologue/forms.py:96 #, fuzzy #| msgid "description" msgid "Description" msgstr "description" -#: .\photologue\forms.py:98 +#: photologue/forms.py:101 msgid "New gallery tags" msgstr "Balises de la nouvelle galerie" -#: .\photologue\forms.py:101 -msgid "" -"Hold down \"Control\", or \"Command\" on a Mac, to select more than one." -msgstr "" - -#: .\photologue\forms.py:120 .\photologue\templates\photologue\upload.html:8 -#: .\photologue\templates\photologue\upload.html:15 +#: photologue/forms.py:129 photologue/templates/photologue/upload.html:8 +#: photologue/templates/photologue/upload.html:17 msgid "Upload" msgstr "Télécharger" -#: .\photologue\forms.py:126 -msgid "A gallery with that title already exists." -msgstr "Une galerie portant ce nom existe déjà." - -#: .\photologue\forms.py:138 +#: photologue/forms.py:141 msgid "Select an existing gallery, or enter a title for a new gallery." msgstr "" "Sélectionner une galerie existante ou entrer un titre pour une nouvelle " "galerie." -#: .\photologue\models.py:88 +#: photologue/models.py:88 msgid "Very Low" msgstr "Très Bas" -#: .\photologue\models.py:89 +#: photologue/models.py:89 msgid "Low" msgstr "Bas" -#: .\photologue\models.py:90 +#: photologue/models.py:90 msgid "Medium-Low" msgstr "Moyen-Bas" -#: .\photologue\models.py:91 +#: photologue/models.py:91 msgid "Medium" msgstr "Moyen" -#: .\photologue\models.py:92 +#: photologue/models.py:92 msgid "Medium-High" msgstr "Moyen-Haut" -#: .\photologue\models.py:93 +#: photologue/models.py:93 msgid "High" msgstr "Haut" -#: .\photologue\models.py:94 +#: photologue/models.py:94 msgid "Very High" msgstr "Très Haut" -#: .\photologue\models.py:99 +#: photologue/models.py:99 msgid "Top" msgstr "Sommet" -#: .\photologue\models.py:100 +#: photologue/models.py:100 msgid "Right" msgstr "Droite" -#: .\photologue\models.py:101 +#: photologue/models.py:101 msgid "Bottom" msgstr "Bas" -#: .\photologue\models.py:102 +#: photologue/models.py:102 msgid "Left" msgstr "Gauche" -#: .\photologue\models.py:103 +#: photologue/models.py:103 msgid "Center (Default)" msgstr "Centré (par défaut)" -#: .\photologue\models.py:107 +#: photologue/models.py:107 msgid "Flip left to right" msgstr "Inversion de gauche à droite" -#: .\photologue\models.py:108 +#: photologue/models.py:108 msgid "Flip top to bottom" msgstr "Inversion de haut en bas" -#: .\photologue\models.py:109 +#: photologue/models.py:109 msgid "Rotate 90 degrees counter-clockwise" msgstr "Rotation de 90 degrés dans le sens anti-horloger" -#: .\photologue\models.py:110 +#: photologue/models.py:110 msgid "Rotate 90 degrees clockwise" msgstr "Rotation de 90 degrés dans le sens horloger" -#: .\photologue\models.py:111 +#: photologue/models.py:111 msgid "Rotate 180 degrees" msgstr "Rotation de 180 degrés" -#: .\photologue\models.py:125 +#: photologue/models.py:125 #, python-format msgid "" "Chain multiple filters using the following pattern \"FILTER_ONE->FILTER_TWO-" @@ -167,116 +157,119 @@ msgstr "" ">FILTRE_DEUX->FILTRE_TROIS\". Les filtres d'image seront appliqués dans " "l'ordre. Les filtres suivants sont disponibles: %s." -#: .\photologue\models.py:148 .\photologue\models.py:517 +#: photologue/models.py:148 photologue/models.py:530 msgid "title" msgstr "titre" -#: .\photologue\models.py:150 +#: photologue/models.py:150 msgid "title slug" msgstr "version abrégée du titre" -#: .\photologue\models.py:153 .\photologue\models.py:522 -#: .\photologue\models.py:780 +#: photologue/models.py:153 photologue/models.py:535 photologue/models.py:796 msgid "A \"slug\" is a unique URL-friendly title for an object." msgstr "" "Un \"slug\" est un titre abrégé et unique, compatible avec les URL, pour un " "objet." -#: .\photologue\models.py:157 +#: photologue/models.py:157 msgid "start date" msgstr "date de début" -#: .\photologue\models.py:162 +#: photologue/models.py:162 msgid "end date" msgstr "date de fin" -#: .\photologue\models.py:164 +#: photologue/models.py:164 msgid "description" msgstr "description" -#: .\photologue\models.py:174 .\photologue\models.py:548 +#: photologue/models.py:174 photologue/models.py:561 msgid "photos" msgstr "photos" -#: .\photologue\models.py:181 +#: photologue/models.py:178 +msgid "public token" +msgstr "" + +#: photologue/models.py:188 msgid "gallery" msgstr "galerie" -#: .\photologue\models.py:182 +#: photologue/models.py:189 msgid "galleries" msgstr "galleries" -#: .\photologue\models.py:214 +#: photologue/models.py:221 msgid "count" msgstr "nombre" -#: .\photologue\models.py:215 +#: photologue/models.py:222 msgid "private count" msgstr "nombre de photos privées" -#: .\photologue\models.py:220 +#: photologue/models.py:227 msgid "image" msgstr "image" -#: .\photologue\models.py:223 +#: photologue/models.py:236 msgid "date taken" msgstr "date de prise de vue" -#: .\photologue\models.py:226 +#: photologue/models.py:239 msgid "Date image was taken; is obtained from the image EXIF data." msgstr "" "La date à laquelle l'image a été prise ; obtenue à partir des données EXIF " "de l'image." -#: .\photologue\models.py:228 +#: photologue/models.py:241 msgid "view count" msgstr "nombre" -#: .\photologue\models.py:230 +#: photologue/models.py:243 msgid "crop from" msgstr "découper à partir de" -#: .\photologue\models.py:254 +#: photologue/models.py:267 msgid "An \"admin_thumbnail\" photo size has not been defined." msgstr "Une taille de photo \"admin_thumbnail\" n'a pas encore été définie." -#: .\photologue\models.py:267 +#: photologue/models.py:280 msgid "Thumbnail" msgstr "Miniature" -#: .\photologue\models.py:519 .\photologue\models.py:779 +#: photologue/models.py:532 photologue/models.py:795 msgid "slug" msgstr "libellé court" -#: .\photologue\models.py:524 +#: photologue/models.py:537 msgid "caption" msgstr "légende" -#: .\photologue\models.py:525 +#: photologue/models.py:538 msgid "date added" msgstr "date d'ajout" -#: .\photologue\models.py:534 +#: photologue/models.py:547 msgid "license" msgstr "licence" -#: .\photologue\models.py:537 +#: photologue/models.py:550 msgid "is public" msgstr "est public" -#: .\photologue\models.py:539 +#: photologue/models.py:552 msgid "Public photographs will be displayed in the default views." msgstr "Les photographies publique seront affichées dans les vues par défaut." -#: .\photologue\models.py:547 +#: photologue/models.py:560 msgid "photo" msgstr "photo" -#: .\photologue\models.py:610 .\photologue\models.py:774 +#: photologue/models.py:626 photologue/models.py:790 msgid "name" msgstr "nom" -#: .\photologue\models.py:614 +#: photologue/models.py:630 msgid "" "Photo size name should contain only letters, numbers and underscores. " "Examples: \"thumbnail\", \"display\", \"small\", \"main_page_widget\"." @@ -285,41 +278,41 @@ msgstr "" "chiffres et des caractères de soulignement. Exemples: \"miniature\", " "\"affichage\", \"petit\", \"widget_page_principale\"." -#: .\photologue\models.py:626 +#: photologue/models.py:642 msgid "width" msgstr "largeur" -#: .\photologue\models.py:629 +#: photologue/models.py:645 msgid "" "If width is set to \"0\" the image will be scaled to the supplied height." msgstr "" "Si la largeur est réglée à \"0\" l l'image sera redimensionnée par rapport à " "la hauteur fournie." -#: .\photologue\models.py:633 +#: photologue/models.py:649 msgid "height" msgstr "hauteur" -#: .\photologue\models.py:636 +#: photologue/models.py:652 msgid "" "If height is set to \"0\" the image will be scaled to the supplied width" msgstr "" "Si la hauteur est réglée à \"0\" l l'image sera redimensionnée par rapport à " "la largeur fournie." -#: .\photologue\models.py:640 +#: photologue/models.py:656 msgid "quality" msgstr "qualité" -#: .\photologue\models.py:643 +#: photologue/models.py:659 msgid "JPEG image quality." msgstr "Qualité JPEG de l'image." -#: .\photologue\models.py:646 +#: photologue/models.py:662 msgid "upscale images?" msgstr "agrandir les images ?" -#: .\photologue\models.py:649 +#: photologue/models.py:665 msgid "" "If selected the image will be scaled up if necessary to fit the supplied " "dimensions. Cropped sizes will be upscaled regardless of this setting." @@ -328,11 +321,11 @@ msgstr "" "dimensions fournies. Les dimensions ajustées seront agrandies sans prendre " "en compte ce paramètre." -#: .\photologue\models.py:655 +#: photologue/models.py:671 msgid "crop to fit?" msgstr "découper pour adapter à la taille ?" -#: .\photologue\models.py:658 +#: photologue/models.py:674 msgid "" "If selected the image will be scaled and cropped to fit the supplied " "dimensions." @@ -340,21 +333,21 @@ msgstr "" "Si sélectionné l'image sera redimensionnée et recadrée pour coïncider avec " "les dimensions fournies." -#: .\photologue\models.py:663 +#: photologue/models.py:679 msgid "pre-cache?" msgstr "mise en cache ?" -#: .\photologue\models.py:666 +#: photologue/models.py:682 msgid "If selected this photo size will be pre-cached as photos are added." msgstr "" "Si sélectionné cette taille de photo sera mise en cache au moment au les " "photos sont ajoutées." -#: .\photologue\models.py:670 +#: photologue/models.py:686 msgid "increment view count?" msgstr "incrémenter le nombre d'affichages ?" -#: .\photologue\models.py:673 +#: photologue/models.py:689 msgid "" "If selected the image's \"view_count\" will be incremented when this photo " "size is displayed." @@ -362,79 +355,95 @@ msgstr "" "Si sélectionné le \"view_count\" (nombre d'affichage) de l'image sera " "incrémenté quand cette taille de photo sera affichée." -#: .\photologue\models.py:680 +#: photologue/models.py:696 msgid "photo size" msgstr "taille de la photo" -#: .\photologue\models.py:681 +#: photologue/models.py:697 msgid "photo sizes" msgstr "tailles des photos" -#: .\photologue\models.py:699 +#: photologue/models.py:715 msgid "Can only crop photos if both width and height dimensions are set." msgstr "" "La hauteur et la largeur doivent être toutes les deux définies pour " "retailler des photos." -#: .\photologue\models.py:785 +#: photologue/models.py:801 msgid "tag" msgstr "" -#: .\photologue\templates\photologue\gallery_archive.html:9 +#: photologue/templates/photologue/gallery_archive.html:9 msgid "Latest photo galleries" msgstr "Dernières galeries de photos" -#: .\photologue\templates\photologue\gallery_archive.html:14 -#: .\photologue\templates\photologue\gallery_archive_year.html:14 +#: photologue/templates/photologue/gallery_archive.html:14 +#: photologue/templates/photologue/gallery_archive_year.html:14 msgid "Filter by year" msgstr "Filtrer par année" -#: .\photologue\templates\photologue\gallery_archive.html:31 +#: photologue/templates/photologue/gallery_archive.html:31 msgid "No galleries were found" msgstr "Aucune galerie trouvée" -#: .\photologue\templates\photologue\gallery_archive_year.html:9 +#: photologue/templates/photologue/gallery_archive_year.html:9 #, python-format msgid "Galleries for %(show_year)s" msgstr "Galeries de %(show_year)s" -#: .\photologue\templates\photologue\gallery_archive_year.html:31 +#: photologue/templates/photologue/gallery_archive_year.html:31 msgid "No galleries were found." msgstr "Aucune galerie trouvée." -#: .\photologue\templates\photologue\gallery_detail.html:42 +#: photologue/templates/photologue/gallery_detail.html:48 msgid "to" msgstr "au" -#: .\photologue\templates\photologue\gallery_detail.html:59 +#: photologue/templates/photologue/gallery_detail.html:53 +msgid "Public link:" +msgstr "" + +#: photologue/templates/photologue/gallery_detail.html:55 +msgid "Copy" +msgstr "" + +#: photologue/templates/photologue/gallery_detail.html:56 +msgid "Revoke" +msgstr "" + +#: photologue/templates/photologue/gallery_detail.html:59 +msgid "Generate public link" +msgstr "" + +#: photologue/templates/photologue/gallery_detail.html:78 msgid "All pictures" msgstr "Toutes les photos" -#: .\photologue\templates\photologue\gallery_detail.html:85 +#: photologue/templates/photologue/gallery_detail.html:106 msgid "Download all gallery" msgstr "Télécharger toute la galerie" -#: .\photologue\templates\photologue\photo_confirm_delete.html:9 -#: .\photologue\templates\photologue\photo_confirm_delete.html:14 +#: photologue/templates/photologue/photo_confirm_delete.html:9 +#: photologue/templates/photologue/photo_confirm_delete.html:14 msgid "Delete confirmation" msgstr "Confirmation de la suppression" -#: .\photologue\templates\photologue\photo_confirm_delete.html:17 +#: photologue/templates/photologue/photo_confirm_delete.html:17 #, python-format msgid "Are you sure you want to delete %(object)s?" msgstr "Es tu sure que tu veux supprimer %(object)s ?" -#: .\photologue\templates\photologue\photo_confirm_delete.html:22 -#: .\photologue\templates\photologue\photo_confirm_report.html:22 +#: photologue/templates/photologue/photo_confirm_delete.html:22 +#: photologue/templates/photologue/photo_confirm_report.html:22 msgid "Confirm" msgstr "Confimer" -#: .\photologue\templates\photologue\photo_confirm_report.html:9 -#: .\photologue\templates\photologue\photo_confirm_report.html:14 +#: photologue/templates/photologue/photo_confirm_report.html:9 +#: photologue/templates/photologue/photo_confirm_report.html:14 msgid "Report confirmation" msgstr "Raporter la confirmation" -#: .\photologue\templates\photologue\photo_confirm_report.html:17 +#: photologue/templates/photologue/photo_confirm_report.html:17 #, python-format msgid "" "Are you sure you want to report %(object)s? This photo will no " @@ -443,14 +452,17 @@ msgstr "" "Es-tu sur de signaler %(object)s? Cette photo ne va plus être " "publique, et les administrateur vont être notifiés." -#: .\photologue\templates\photologue\photo_detail.html:15 +#: photologue/templates/photologue/photo_detail.html:15 msgid "Published" msgstr "Publiée le" -#: .\photologue\templates\photologue\upload.html:20 +#: photologue/templates/photologue/upload.html:22 msgid "Drag and drop photos here" msgstr "Glissez et déposez les photos ici" -#: .\photologue\templates\photologue\upload.html:24 +#: photologue/templates/photologue/upload.html:26 msgid "Owner will be" msgstr "Le propriétaire sera" + +#~ msgid "A gallery with that title already exists." +#~ msgstr "Une galerie portant ce nom existe déjà." From 640c75e499172056523e7c6064c15ea8f11a6099 Mon Sep 17 00:00:00 2001 From: krek0 Date: Sun, 3 May 2026 23:36:34 +0200 Subject: [PATCH 12/27] Fix image display in Docker by loading initial fixtures on startup --- Dockerfile | 2 +- entrypoint.sh | 1 + photo21/settings.py | 1 - 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index d45d054..8e47172 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ RUN apt-get update && apt-get install -y gettext && rm -rf /var/lib/apt/lists/* COPY . . -RUN python manage.py compilemessages +RUN SECRET_KEY=dummy python manage.py compilemessages # Create volume mount points RUN mkdir -p /app/media /app/static /app/data diff --git a/entrypoint.sh b/entrypoint.sh index fca4a2c..8638676 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -3,5 +3,6 @@ set -e python manage.py collectstatic --noinput python manage.py migrate --noinput +python manage.py loaddata initial python manage.py create_default_admin exec gunicorn photo21.wsgi:application --bind 0.0.0.0:8000 --workers 3 diff --git a/photo21/settings.py b/photo21/settings.py index 222d7fa..953a8c2 100644 --- a/photo21/settings.py +++ b/photo21/settings.py @@ -235,7 +235,6 @@ WHITENOISE_MANIFEST_STRICT = False LOCALE_PATHS = [os.path.join(BASE_DIR, "photo21/locale")] -FIXTURE_DIRS = [os.path.join(BASE_DIR, "photo21/fixtures")] # Do not send email during debug # By default Django sends mails to localhost:25 without authentification From 7319a738f4abbd53f003eeb2d2f99e79c02c94e8 Mon Sep 17 00:00:00 2001 From: krek0 Date: Sun, 3 May 2026 23:50:48 +0200 Subject: [PATCH 13/27] Push latest tag on docker image on release --- .forgejo/workflows/docker.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.forgejo/workflows/docker.yml b/.forgejo/workflows/docker.yml index 90f3a7c..e5da6fc 100644 --- a/.forgejo/workflows/docker.yml +++ b/.forgejo/workflows/docker.yml @@ -22,11 +22,12 @@ jobs: username: ${{ secrets.REGISTRY_USER }} password: ${{ secrets.REGISTRY_TOKEN }} - - name: Build image - run: | - docker build -t git.sinfonie.org/sinfonie/photo26:${{ steps.meta.outputs.TAG }} . - - - name: Push image + - name: Build and push image run: | + docker build \ + -t git.sinfonie.org/sinfonie/photo26:${{ steps.meta.outputs.TAG }} \ + -t git.sinfonie.org/sinfonie/photo26:latest \ + . docker push git.sinfonie.org/sinfonie/photo26:${{ steps.meta.outputs.TAG }} + docker push git.sinfonie.org/sinfonie/photo26:latest From ae1ad6965f50476c052840c6cab7879a4759bed8 Mon Sep 17 00:00:00 2001 From: krek0 Date: Tue, 5 May 2026 14:25:29 +0200 Subject: [PATCH 14/27] Fix thumbnail crop to preserve image aspect ratio in gallery grid --- photo21/fixtures/initial.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/photo21/fixtures/initial.json b/photo21/fixtures/initial.json index f4fd3e8..640ac69 100644 --- a/photo21/fixtures/initial.json +++ b/photo21/fixtures/initial.json @@ -30,7 +30,7 @@ "height": 180, "quality": 70, "upscale": false, - "crop": true, + "crop": false, "pre_cache": true, "increment_count": false } From 981c2c37c02919fe65cd10a545ccf27f13584106 Mon Sep 17 00:00:00 2001 From: krek0 Date: Fri, 24 Apr 2026 21:43:25 +0200 Subject: [PATCH 15/27] Add justified photo grid layout with lazy loading and image dimensions. --- photo21/fixtures/initial.json | 2 +- photo21/static/layout.css | 23 ++++++ .../migrations/0008_photo_dimensions.py | 45 +++++++++++ .../migrations/0009_regen_thumbnails.py | 31 ++++++++ photologue/static/gallery_justified.js | 75 +++++++++++++++++++ .../templates/photologue/gallery_detail.html | 7 +- 6 files changed, 179 insertions(+), 4 deletions(-) create mode 100644 photologue/migrations/0008_photo_dimensions.py create mode 100644 photologue/migrations/0009_regen_thumbnails.py create mode 100644 photologue/static/gallery_justified.js diff --git a/photo21/fixtures/initial.json b/photo21/fixtures/initial.json index f4fd3e8..640ac69 100644 --- a/photo21/fixtures/initial.json +++ b/photo21/fixtures/initial.json @@ -30,7 +30,7 @@ "height": 180, "quality": 70, "upscale": false, - "crop": true, + "crop": false, "pre_cache": true, "increment_count": false } diff --git a/photo21/static/layout.css b/photo21/static/layout.css index a0026d1..8e5568e 100644 --- a/photo21/static/layout.css +++ b/photo21/static/layout.css @@ -45,6 +45,29 @@ SPDX-License-Identifier: GPL-3.0-or-later background-color: rgba(163, 163, 163, 0.274); } +/* Gallery - Google Photos style justified grid */ +#lightgallery { + position: relative; +} + +.photo-item { + position: absolute; + overflow: hidden; + visibility: hidden; +} + +.photo-item img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +.photo-item img.photo-private { + outline: 5px solid var(--bs-danger); + outline-offset: -5px; +} + /* Language selector */ .lang-select { border: none; diff --git a/photologue/migrations/0008_photo_dimensions.py b/photologue/migrations/0008_photo_dimensions.py new file mode 100644 index 0000000..033f144 --- /dev/null +++ b/photologue/migrations/0008_photo_dimensions.py @@ -0,0 +1,45 @@ +import photologue.models +from django.db import migrations, models + + +def fill_dimensions(apps, schema_editor): + Photo = apps.get_model('photologue', 'Photo') + for photo in Photo.objects.filter(image_width__isnull=True).iterator(): + try: + photo.image_width = photo.image.width + photo.image_height = photo.image.height + photo.save(update_fields=['image_width', 'image_height']) + except Exception: + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ('photologue', '0007_allow_duplicate_photo_titles'), + ] + + operations = [ + migrations.AddField( + model_name='photo', + name='image_width', + field=models.PositiveIntegerField(editable=False, null=True), + ), + migrations.AddField( + model_name='photo', + name='image_height', + field=models.PositiveIntegerField(editable=False, null=True), + ), + migrations.RunPython(fill_dimensions, migrations.RunPython.noop), + migrations.AlterField( + model_name='photo', + name='image', + field=models.ImageField( + max_length=100, + upload_to=photologue.models.get_storage_path, + verbose_name='image', + width_field='image_width', + height_field='image_height', + ), + ), + ] diff --git a/photologue/migrations/0009_regen_thumbnails.py b/photologue/migrations/0009_regen_thumbnails.py new file mode 100644 index 0000000..b4d0028 --- /dev/null +++ b/photologue/migrations/0009_regen_thumbnails.py @@ -0,0 +1,31 @@ +from django.db import migrations + + +def regen_thumbnails(apps, schema_editor): + Photo = apps.get_model("photologue", "Photo") + PhotoSize = apps.get_model("photologue", "PhotoSize") + + try: + thumbnail = PhotoSize.objects.get(name="thumbnail") + except PhotoSize.DoesNotExist: + return + + for photo in Photo.objects.all(): + # Use the real model instance to access file methods + from photologue.models import Photo as RealPhoto + try: + real_photo = RealPhoto.objects.get(pk=photo.pk) + real_photo.remove_size(thumbnail) + except Exception: + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ("photologue", "0008_photo_dimensions"), + ] + + operations = [ + migrations.RunPython(regen_thumbnails, migrations.RunPython.noop), + ] diff --git a/photologue/static/gallery_justified.js b/photologue/static/gallery_justified.js new file mode 100644 index 0000000..80dcc55 --- /dev/null +++ b/photologue/static/gallery_justified.js @@ -0,0 +1,75 @@ +// Justified photo grid layout (Google Photos style) +// Inspired by https://github.com/flickr/justified-layout +(function () { + const SPACING = 3; + const PADDING = 3; + + function applyLayout() { + const container = document.getElementById('lightgallery'); + if (!container) return; + + const items = [...container.querySelectorAll('.photo-item')]; + if (!items.length) return; + + const containerWidth = container.offsetWidth - PADDING * 2; + const TARGET_HEIGHT = Math.min(220, Math.floor(containerWidth * 0.4)); + const ratios = items.map(a => { + const w = parseInt(a.dataset.width) || 1; + const h = parseInt(a.dataset.height) || 1; + return w / h; + }); + + // Group items into rows + const rows = []; + let rowStart = 0; + while (rowStart < ratios.length) { + let sum = 0; + let end = rowStart; + while (end < ratios.length) { + sum += ratios[end]; + const rowWidth = sum * TARGET_HEIGHT + SPACING * (end - rowStart); + end++; + if (rowWidth >= containerWidth) break; + } + rows.push({ start: rowStart, end }); + rowStart = end; + } + + // Position each item + let top = PADDING; + rows.forEach(({ start, end }, rowIndex) => { + const isLastRow = rowIndex === rows.length - 1; + const count = end - start; + const sumRatios = ratios.slice(start, end).reduce((a, b) => a + b, 0); + const totalSpacing = SPACING * (count - 1); + + // Don't stretch the last row if it's not full + const rowHeight = isLastRow && count < 3 + ? TARGET_HEIGHT + : (containerWidth - totalSpacing) / sumRatios; + + let left = PADDING; + for (let i = start; i < end; i++) { + const width = Math.round(ratios[i] * rowHeight); + const height = Math.round(rowHeight); + const item = items[i]; + item.style.top = top + 'px'; + item.style.left = left + 'px'; + item.style.width = width + 'px'; + item.style.height = height + 'px'; + left += width + SPACING; + } + top += Math.round(rowHeight) + SPACING; + }); + + container.style.height = (top - SPACING + PADDING) + 'px'; + items.forEach(item => { + item.style.visibility = 'visible'; + const img = item.querySelector('img[data-lazy]'); + if (img) img.src = img.dataset.lazy; + }); + } + + document.addEventListener('DOMContentLoaded', applyLayout); + window.addEventListener('resize', applyLayout); +})(); diff --git a/photologue/templates/photologue/gallery_detail.html b/photologue/templates/photologue/gallery_detail.html index 1744a05..155f86a 100755 --- a/photologue/templates/photologue/gallery_detail.html +++ b/photologue/templates/photologue/gallery_detail.html @@ -25,6 +25,7 @@ SPDX-License-Identifier: GPL-3.0-or-later + @@ -87,10 +88,10 @@ SPDX-License-Identifier: GPL-3.0-or-later

{% endif %} -
+
{% for photo in photos %} - - {{ photo.title }}{% if photo.date_taken %} - {{ photo.date_taken|date }} {{ photo.date_taken|time }}{% endif %}{% if photo.owner.get_full_name %} - {{ photo.owner.get_full_name }}{% else %} - {{ photo.owner.username }}{% endif %}{% if photo.license %} - {{ photo.license }}{% endif %}{% if not photo.is_public %} - !PRIVATE!{% endif %} + + {{ photo.title }}{% if photo.date_taken %} - {{ photo.date_taken|date }} {{ photo.date_taken|time }}{% endif %}{% if photo.owner.get_full_name %} - {{ photo.owner.get_full_name }}{% else %} - {{ photo.owner.username }}{% endif %}{% if photo.license %} - {{ photo.license }}{% endif %}{% if not photo.is_public %} - !PRIVATE!{% endif %} {% endfor %}
From 9a63dea0cbaf370eb3e6178201a899b34784b2e7 Mon Sep 17 00:00:00 2001 From: krek0 Date: Fri, 24 Apr 2026 21:53:44 +0200 Subject: [PATCH 16/27] Rennes rebranding --- README.md | 4 ++- photo21/forms.py | 9 ++---- photo21/templates/account/login.html | 2 +- photo21/templates/base.html | 22 ++++++------- photo21/templates/index.html | 29 +++++++---------- photologue/static/gallery_detail.js | 32 +++++++++---------- .../lightgallery/plugins/admin/lg-admin.js | 7 ++-- .../templates/photologue/gallery_detail.html | 1 + 8 files changed, 52 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index d8e6581..58d8206 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Photo server 2021-2023 +This project is a fork of [Photo21](https://gitlab.crans.org/bde/photo21/) developped at ENS Paris-Saclay. + [![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0.txt) This is the source code for the webserver hosting pictures from the @@ -25,7 +27,7 @@ run and to maintain. In production, we usually use `/var/www/photos/` as the `root` user. ```bash - git clone https://gitlab.crans.org/bde/photo21.git && cd photo21 + git clone https://codeberg.org/krek0/photo21.git && cd photo21 ``` 3. **Configuration (production only).** diff --git a/photo21/forms.py b/photo21/forms.py index 7c15e1e..253fab3 100644 --- a/photo21/forms.py +++ b/photo21/forms.py @@ -13,8 +13,7 @@ class CustomSignupForm(SignupForm): # Add description on email field self.fields["email"].help_text = _( - "Please enter a valid email address ending with `@crans.org` or " - "`@ens-paris-saclay.fr`." + "Please enter a valid email address ending with `@ens-rennes.fr`" ) def clean_email(self): @@ -22,10 +21,8 @@ class CustomSignupForm(SignupForm): Check that the email address ends with a trusted domain. """ email = super().clean_email() - if not email.endswith("@crans.org") and not email.endswith( - "@ens-paris-saclay.fr" - ): + if not email.endswith("@ens-rennes.fr"): raise forms.ValidationError( - _("Must end with `@crans.org` or `@ens-paris-saclay.fr`.") + _("Must end with `@ens-rennes.fr`.") ) return email diff --git a/photo21/templates/account/login.html b/photo21/templates/account/login.html index 1e7723f..a2bc666 100644 --- a/photo21/templates/account/login.html +++ b/photo21/templates/account/login.html @@ -39,5 +39,5 @@ SPDX-License-Identifier: GPL-3.0-or-later {% trans "Forgot Password?" %}
-

{% trans "If any problem, please contact the server owners at" %} photos[at]crans.org.

+ {% endblock %} diff --git a/photo21/templates/base.html b/photo21/templates/base.html index eeca7fd..602ee57 100644 --- a/photo21/templates/base.html +++ b/photo21/templates/base.html @@ -13,7 +13,7 @@ SPDX-License-Identifier: GPL-3.0-or-later {% block title %}{{ title }}{% endblock title %} - {{ request.site.name }} - + @@ -118,16 +118,16 @@ SPDX-License-Identifier: GPL-3.0-or-later {% if request.user.is_authenticated %} {% trans "Connected as" %} {{ request.user.username }} · {% endif %} - {% trans "Source code" %} · - + {% trans "Source code" %} · + + + + + + + + +

diff --git a/photo21/templates/index.html b/photo21/templates/index.html index b7fa2e9..d4db262 100644 --- a/photo21/templates/index.html +++ b/photo21/templates/index.html @@ -12,7 +12,7 @@ SPDX-License-Identifier: GPL-3.0-or-later

{% 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 %}

@@ -25,21 +25,16 @@ SPDX-License-Identifier: GPL-3.0-or-later {% 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 3e1920cb84cdb82b6ab3fae09d9692a63066b32f Mon Sep 17 00:00:00 2001 From: krek0 Date: Fri, 24 Apr 2026 22:26:03 +0200 Subject: [PATCH 17/27] 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 116614a620efff5aa1f92c9f19d8bb3b8a3dc931 Mon Sep 17 00:00:00 2001 From: krek0 Date: Fri, 24 Apr 2026 23:26:09 +0200 Subject: [PATCH 18/27] 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 836e0b2cd05d21c9636cf9febcfbb0256916a25d Mon Sep 17 00:00:00 2001 From: krek0 Date: Sat, 25 Apr 2026 01:09:01 +0200 Subject: [PATCH 19/27] 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 9cc99a0d13fc8d93e16f26c5ff0634fa07545a7c Mon Sep 17 00:00:00 2001 From: krek0 Date: Sat, 25 Apr 2026 12:12:30 +0200 Subject: [PATCH 20/27] 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
{% for photo in photos %} - {{ photo.title }}{% if photo.date_taken %} - {{ photo.date_taken|date }} {{ photo.date_taken|time }}{% endif %}{% if photo.owner.get_full_name %} - {{ photo.owner.get_full_name }}{% else %} - {{ photo.owner.username }}{% endif %}{% if photo.license %} - {{ photo.license }}{% endif %}{% if not photo.is_public %} - !PRIVATE!{% endif %} + {{ photo.title }}{% if photo.date_taken %} - {{ photo.date_taken|date }} {{ photo.date_taken|time }}{% endif %}{% if photo.owner.get_full_name %} - {{ photo.owner.get_full_name }}{% else %} - {{ photo.owner.username }}{% endif %}{% if photo.license %} - {{ photo.license }}{% endif %}{% if not photo.is_public %} - !PRIVATE!{% endif %} {% endfor %}
From 320411fd1829110461bd9e62e334445bf709e222 Mon Sep 17 00:00:00 2001 From: krek0 Date: Sat, 2 May 2026 14:18:02 +0200 Subject: [PATCH 21/27] Make database, oauth, smtp server, mail verificaiton configurable in .env --- .env.example | 36 ++++++++++ .../__init__.py | 2 + .../provider.py | 12 ++-- {allauth_note_kfet => allauth_oauth}/urls.py | 4 +- {allauth_note_kfet => allauth_oauth}/views.py | 12 ++-- photo21/settings.py | 71 ++++++++++++++----- requirements.txt | 1 + tox.ini | 2 +- 8 files changed, 109 insertions(+), 31 deletions(-) rename {allauth_note_kfet => allauth_oauth}/__init__.py (69%) rename {allauth_note_kfet => allauth_oauth}/provider.py (83%) rename {allauth_note_kfet => allauth_oauth}/urls.py (70%) rename {allauth_note_kfet => allauth_oauth}/views.py (77%) diff --git a/.env.example b/.env.example index 145b31e..9fa2ff9 100644 --- a/.env.example +++ b/.env.example @@ -14,3 +14,39 @@ ADMINS=admin:photos-admin@lists.crans.org # Email address used as sender for server emails SERVER_EMAIL=photos@crans.org + +# Email verification: 'mandatory', 'optional', or 'none' +EMAIL_VERIFICATION=mandatory + +# Mail server settings +SMTP_HOST=localhost +SMTP_PORT=25 +#SMTP_USER= +#SMTP_PASSWORD= +SMTP_USE_TLS=False + +# OAuth2 settings +# Enable OAuth2 login +OAUTH_ENABLED=False +# Disable normal username/password login (requires OAUTH_ENABLED=True) +OAUTH_ONLY=False +# OAuth2 server base URL (e.g. auth.example.com) +#OAUTH_SERVER_URL= +# OAuth2 app credentials +#OAUTH_CLIENT_ID= +#OAUTH_CLIENT_SECRET= +# Button appearance on the login page +#OAUTH_BUTTON_TEXT=Login with OAuth +#OAUTH_BUTTON_IMAGE= +# Space-separated OAuth2 scopes +#OAUTH_SCOPE=openid profile email + +# Database engine: 'sqlite' or 'postgres' +DB_ENGINE=sqlite + +# PostgreSQL settings (only used when DB_ENGINE=postgres) +#DB_NAME=photo21 +#DB_USER=photo21 +#DB_PASSWORD= +#DB_HOST=localhost +#DB_PORT=5432 diff --git a/allauth_note_kfet/__init__.py b/allauth_oauth/__init__.py similarity index 69% rename from allauth_note_kfet/__init__.py rename to allauth_oauth/__init__.py index 6c9e378..eae4948 100644 --- a/allauth_note_kfet/__init__.py +++ b/allauth_oauth/__init__.py @@ -1,3 +1,5 @@ # This file is part of photo21 # Copyright (C) 2022 Amicale des élèves de l'ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later + +default_app_config = "allauth_oauth.apps.AllauthOAuthConfig" diff --git a/allauth_note_kfet/provider.py b/allauth_oauth/provider.py similarity index 83% rename from allauth_note_kfet/provider.py rename to allauth_oauth/provider.py index d2ce6a5..6a6430c 100644 --- a/allauth_note_kfet/provider.py +++ b/allauth_oauth/provider.py @@ -7,15 +7,15 @@ from allauth.socialaccount.providers.base import ProviderAccount from allauth.socialaccount.providers.oauth2.provider import OAuth2Provider -class NoteKfetAccount(ProviderAccount): +class OAuthAccount(ProviderAccount): def to_str(self): return self.account.extra_data.get("username") -class NoteKfetProvider(OAuth2Provider): - id = "notekfet" - name = "Note Kfet" - account_class = NoteKfetAccount +class OAuthProvider(OAuth2Provider): + id = "oauth" + name = "OAuth" + account_class = OAuthAccount def extract_uid(self, data): return str(data["username"]) @@ -39,4 +39,4 @@ class NoteKfetProvider(OAuth2Provider): return ret -provider_classes = [NoteKfetProvider] +provider_classes = [OAuthProvider] diff --git a/allauth_note_kfet/urls.py b/allauth_oauth/urls.py similarity index 70% rename from allauth_note_kfet/urls.py rename to allauth_oauth/urls.py index e1bf561..190eac3 100644 --- a/allauth_note_kfet/urls.py +++ b/allauth_oauth/urls.py @@ -4,6 +4,6 @@ from allauth.socialaccount.providers.oauth2.urls import default_urlpatterns -from .provider import NoteKfetProvider +from .provider import OAuthProvider -urlpatterns = default_urlpatterns(NoteKfetProvider) +urlpatterns = default_urlpatterns(OAuthProvider) diff --git a/allauth_note_kfet/views.py b/allauth_oauth/views.py similarity index 77% rename from allauth_note_kfet/views.py rename to allauth_oauth/views.py index 86a16ee..7a568a5 100644 --- a/allauth_note_kfet/views.py +++ b/allauth_oauth/views.py @@ -10,11 +10,11 @@ from allauth.socialaccount.providers.oauth2.views import ( OAuth2LoginView, ) -from .provider import NoteKfetProvider +from .provider import OAuthProvider -class NoteKfetOAuth2Adapter(OAuth2Adapter): - provider_id = NoteKfetProvider.id +class OAuthAdapter(OAuth2Adapter): + provider_id = OAuthProvider.id def complete_login(self, request, app, token, **kwargs): headers = { @@ -31,7 +31,7 @@ class NoteKfetOAuth2Adapter(OAuth2Adapter): @property def domain(self): - return self.settings.get("DOMAIN", "note.crans.org") + return self.settings.get("DOMAIN", "") @property def access_token_url(self): @@ -46,5 +46,5 @@ class NoteKfetOAuth2Adapter(OAuth2Adapter): return f"https://{self.domain}/api/me/" -oauth2_login = OAuth2LoginView.adapter_view(NoteKfetOAuth2Adapter) -oauth2_callback = OAuth2CallbackView.adapter_view(NoteKfetOAuth2Adapter) +oauth2_login = OAuth2LoginView.adapter_view(OAuthAdapter) +oauth2_callback = OAuth2CallbackView.adapter_view(OAuthAdapter) diff --git a/photo21/settings.py b/photo21/settings.py index fb36c54..a80e727 100644 --- a/photo21/settings.py +++ b/photo21/settings.py @@ -56,6 +56,15 @@ SECURE_HSTS_PRELOAD = True # Application definition +OAUTH_ENABLED = config("OAUTH_ENABLED", default=False, cast=bool) +OAUTH_ONLY = config("OAUTH_ONLY", default=False, cast=bool) +OAUTH_CLIENT_ID = config("OAUTH_CLIENT_ID", default="") +OAUTH_CLIENT_SECRET = config("OAUTH_CLIENT_SECRET", default="") +OAUTH_SERVER_URL = config("OAUTH_SERVER_URL", default="") +OAUTH_BUTTON_TEXT = config("OAUTH_BUTTON_TEXT", default="Login with OAuth") +OAUTH_BUTTON_IMAGE = config("OAUTH_BUTTON_IMAGE", default="") +OAUTH_SCOPE = config("OAUTH_SCOPE", default="openid profile email", cast=Csv(delimiter=" ")) + INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.admindocs", @@ -69,12 +78,14 @@ INSTALLED_APPS = [ "allauth", "allauth.account", "allauth.socialaccount", - "allauth_note_kfet", "crispy_forms", "photologue", "photo21", ] +if OAUTH_ENABLED: + INSTALLED_APPS += ["allauth_oauth"] + if DEBUG: INSTALLED_APPS += ["debug_toolbar",] # For debug and optimisations @@ -125,15 +136,31 @@ WSGI_APPLICATION = "photo21.wsgi.application" # Database # https://docs.djangoproject.com/en/2.2/ref/settings/#databases -DATABASES = { - "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": os.path.join(BASE_DIR, "db.sqlite3"), - "OPTIONS": { - "timeout": 10, - }, +_db_engine = config("DB_ENGINE", default="sqlite").strip().lower() + +if _db_engine == "postgres": + DATABASES = { + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": config("DB_NAME", default="photo21"), + "USER": config("DB_USER", default="photo21"), + "PASSWORD": config("DB_PASSWORD", default=""), + "HOST": config("DB_HOST", default="localhost"), + "PORT": config("DB_PORT", default="5432"), + } } -} +elif _db_engine == "sqlite": + DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": os.path.join(BASE_DIR, "db.sqlite3"), + "OPTIONS": { + "timeout": 10, + }, + } + } +else: + raise ValueError(f"Unknown DB_ENGINE '{_db_engine}'. Must be 'sqlite' or 'postgres'.") CACHES = { "default": { @@ -221,6 +248,11 @@ if DEBUG: SERVER_EMAIL = config("SERVER_EMAIL", default="photos@crans.org") DEFAULT_FROM_EMAIL = f"Serveur photos <{SERVER_EMAIL}>" EMAIL_SUBJECT_PREFIX = "[Serveur photos] " +EMAIL_HOST = config("SMTP_HOST", default="localhost") +EMAIL_PORT = config("SMTP_PORT", default=25, cast=int) +EMAIL_HOST_USER = config("SMTP_USER", default="") +EMAIL_HOST_PASSWORD = config("SMTP_PASSWORD", default="") +EMAIL_USE_TLS = config("SMTP_USE_TLS", default=False, cast=bool) # After login redirect user to transfer page LOGIN_REDIRECT_URL = "/" @@ -240,16 +272,23 @@ MESSAGE_TAGS = { # Allauth configuration ## For the django =< 5.0 ACCOUNT_EMAIL_REQUIRED = True # ACCOUNT_SIGNUP_FIELDS = ['email*', 'username*', 'password1*', 'password2*'] ## For the django =< 5.0 -ACCOUNT_EMAIL_VERIFICATION = "mandatory" +ACCOUNT_EMAIL_VERIFICATION = config("EMAIL_VERIFICATION", default="mandatory") ACCOUNT_AUTHENTICATION_METHOD = "username_email" # ACCOUNT_LOGIN_METHODS = {'username', 'email'} ACCOUNT_FORMS = {"signup": "photo21.forms.CustomSignupForm"} -SOCIALACCOUNT_PROVIDERS = { - "notekfet": { - # Fetch user profile - "SCOPE": ["1_1"], - }, -} + +if OAUTH_ENABLED: + SOCIALACCOUNT_ONLY = OAUTH_ONLY + SOCIALACCOUNT_PROVIDERS = { + "oauth": { + "SCOPE": OAUTH_SCOPE, + "DOMAIN": OAUTH_SERVER_URL, + "APP": { + "client_id": OAUTH_CLIENT_ID, + "secret": OAUTH_CLIENT_SECRET, + }, + }, + } # Use Bootstrap forms CRISPY_TEMPLATE_PACK = "bootstrap4" diff --git a/requirements.txt b/requirements.txt index 31eeff0..c0d5114 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,4 @@ Pillow>=6.0.0 django-debug-toolbar>=3.2.0 python-decouple>=3.6 whitenoise>=6.0 +psycopg2>=2.9 diff --git a/tox.ini b/tox.ini index cc05d77..55bb7f6 100644 --- a/tox.ini +++ b/tox.ini @@ -27,7 +27,7 @@ deps = pep8-naming pyflakes commands = - flake8 allauth_note_kfet photo21 photologue + flake8 allauth_oauth photo21 photologue [flake8] ignore = W503, I100, I101 From a41dfb6f9483d04b9d6da6e0a4c1b229bc03e3db Mon Sep 17 00:00:00 2001 From: krek0 Date: Sat, 2 May 2026 14:19:10 +0200 Subject: [PATCH 22/27] Add requests in requirements --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index c0d5114..242cbf3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,3 +8,4 @@ django-debug-toolbar>=3.2.0 python-decouple>=3.6 whitenoise>=6.0 psycopg2>=2.9 +requests>=2.25 From 64f3b0cdbf11cda183d1cd32a899577da164a95a Mon Sep 17 00:00:00 2001 From: krek0 Date: Sun, 3 May 2026 16:54:46 +0200 Subject: [PATCH 23/27] Add Docker support with Dockerfile and entrypoin --- .env.example | 3 + .forgejo/workflows/docker.yml | 32 +++++++++ Dockerfile | 19 +++++ README.md | 72 +++++++++++++++++++ docker-compose.yml | 40 +++++++++++ entrypoint.sh | 7 ++ photo21/settings.py | 2 +- .../commands/create_default_admin.py | 32 +++++++++ requirements.txt | 3 +- 9 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 .forgejo/workflows/docker.yml create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 entrypoint.sh create mode 100644 photologue/management/commands/create_default_admin.py diff --git a/.env.example b/.env.example index 9fa2ff9..930af5b 100644 --- a/.env.example +++ b/.env.example @@ -50,3 +50,6 @@ DB_ENGINE=sqlite #DB_PASSWORD= #DB_HOST=localhost #DB_PORT=5432 + +# SQLite settings (only used when DB_ENGINE=sqlite) +#DB_PATH=/app/data/db.sqlite3 diff --git a/.forgejo/workflows/docker.yml b/.forgejo/workflows/docker.yml new file mode 100644 index 0000000..90f3a7c --- /dev/null +++ b/.forgejo/workflows/docker.yml @@ -0,0 +1,32 @@ +name: Docker + +on: + release: + types: + - published + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set image tag + id: meta + run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT + + - name: Login to Forgejo registry + uses: docker/login-action@v3 + with: + registry: git.sinfonie.org + username: ${{ secrets.REGISTRY_USER }} + password: ${{ secrets.REGISTRY_TOKEN }} + + - name: Build image + run: | + docker build -t git.sinfonie.org/sinfonie/photo26:${{ steps.meta.outputs.TAG }} . + + - name: Push image + run: | + docker push git.sinfonie.org/sinfonie/photo26:${{ steps.meta.outputs.TAG }} + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f24c177 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM python:3.11-slim + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +# Create volume mount points +RUN mkdir -p /app/media /app/static /app/data + +EXPOSE 8000 + +RUN chmod +x entrypoint.sh +ENTRYPOINT ["./entrypoint.sh"] diff --git a/README.md b/README.md index 5878775..f54258c 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,78 @@ run and to maintain. ```./maintenance_tool.sh``` +## Docker install + +1. Create a `docker-compose.yml` (a ready-to-use file is provided in the repository): + + ```yaml + version: "3.9" + + networks: + photo26: + + services: + db: + image: postgres:16 + container_name: photo26-db + restart: unless-stopped + environment: + POSTGRES_DB: photo26 + POSTGRES_USER: photo26 + POSTGRES_PASSWORD: change-me + volumes: + - ./postgres_data:/var/lib/postgresql/data + networks: + - photo26 + + photo26: + image: git.sinfonie.org/sinfonie/photo26:latest + container_name: photo26-app + restart: unless-stopped + depends_on: + - db + environment: + DB_ENGINE: postgres + DB_NAME: photo26 + DB_USER: photo26 + DB_PASSWORD: change-me + DB_HOST: db + DB_PORT: 5432 + SECRET_KEY: change-me + EXTRA_HOSTS: photos.example.org + volumes: + - ./media:/app/media + ports: + - "8080:8000" + networks: + - photo26 + ``` + +2. Start the stack: + + ```bash + docker compose up -d + ``` + + On first start the container will run migrations and create a default admin account automatically. + +3. **Default credentials** — change these immediately after first login: + + | Field | Value | + |----------|-----------------| + | Username | `admin` | + | Password | `admin` | + | Email | `admin@localhost` | + + Admin panel: `http://localhost:8080/admin/` + +4. **Passwords to change** in `docker-compose.yml` before going to production: + - `POSTGRES_PASSWORD` / `DB_PASSWORD` — database password + - `SECRET_KEY` — Django secret key (use a long random string) + - Log in to the admin panel and change the `admin` user password + +--- + 6. *Enjoy \o/* In development, you can launch the development server using: diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..001fed7 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,40 @@ +version: "3.9" + +networks: + photo26: + +services: + db: + image: postgres:16 + container_name: photo26-db + restart: unless-stopped + environment: + POSTGRES_DB: photo26 + POSTGRES_USER: photo26 + POSTGRES_PASSWORD: change-me + volumes: + - ./postgres_data:/var/lib/postgresql/data + networks: + - photo26 + + photo26: + image: git.sinfonie.org/sinfonie/photo26:latest + container_name: photo26-app + restart: unless-stopped + depends_on: + - db + environment: + DB_ENGINE: postgres + DB_NAME: photo26 + DB_USER: photo26 + DB_PASSWORD: change-me + DB_HOST: db + DB_PORT: 5432 + SECRET_KEY: change-me + EXTRA_HOSTS: photos.example.org + volumes: + - ./media:/app/media + ports: + - "8080:8000" + networks: + - photo26 diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..fca4a2c --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,7 @@ +#!/bin/sh +set -e + +python manage.py collectstatic --noinput +python manage.py migrate --noinput +python manage.py create_default_admin +exec gunicorn photo21.wsgi:application --bind 0.0.0.0:8000 --workers 3 diff --git a/photo21/settings.py b/photo21/settings.py index a80e727..ec542ea 100644 --- a/photo21/settings.py +++ b/photo21/settings.py @@ -153,7 +153,7 @@ elif _db_engine == "sqlite": DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", - "NAME": os.path.join(BASE_DIR, "db.sqlite3"), + "NAME": config("DB_PATH", default=os.path.join(BASE_DIR, "db.sqlite3")), "OPTIONS": { "timeout": 10, }, diff --git a/photologue/management/commands/create_default_admin.py b/photologue/management/commands/create_default_admin.py new file mode 100644 index 0000000..b9fee01 --- /dev/null +++ b/photologue/management/commands/create_default_admin.py @@ -0,0 +1,32 @@ +from django.contrib.auth import get_user_model +from django.core.management.base import BaseCommand + + +class Command(BaseCommand): + help = "Create default admin user (admin@localhost / admin) if it does not exist" + + def handle(self, *args, **kwargs): + User = get_user_model() + email = "admin@localhost" + username = "admin" + password = "admin" + + if User.objects.filter(username=username).exists(): + self.stdout.write("Default admin already exists, skipping.") + return + + user = User.objects.create_superuser(username=username, email=email, password=password) + + # Mark the email as verified via allauth + try: + from allauth.account.models import EmailAddress + EmailAddress.objects.create( + user=user, + email=email, + primary=True, + verified=True, + ) + except Exception as e: + self.stderr.write(f"Could not create allauth EmailAddress: {e}") + + self.stdout.write(f"Default admin created: {username} / {password}") diff --git a/requirements.txt b/requirements.txt index 242cbf3..56bdc65 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,5 +7,6 @@ Pillow>=6.0.0 django-debug-toolbar>=3.2.0 python-decouple>=3.6 whitenoise>=6.0 -psycopg2>=2.9 +psycopg2-binary>=2.9 requests>=2.25 +gunicorn>=21.0 From 69a4a3ca9db39fe8ef3bf83d7181714125987aa0 Mon Sep 17 00:00:00 2001 From: krek0 Date: Sun, 3 May 2026 18:29:54 +0200 Subject: [PATCH 24/27] update readme --- README.md | 119 +++++++++++++++++++++++++----------------------------- 1 file changed, 56 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index f54258c..af3ec10 100644 --- a/README.md +++ b/README.md @@ -1,72 +1,17 @@ -# Photo server 2021-2023 - -This project is a fork of [Photo21](https://gitlab.crans.org/bde/photo21/) developped at ENS Paris-Saclay. +# Photo server [![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0.txt) This is the source code for the webserver hosting pictures from the -ENS Paris-Saclay student life. +ENS Rennes student life. The philosophy of this project is to keep this code as simple as possible to run and to maintain. -## Setup +This project is a fork of [Photo21](https://gitlab.crans.org/bde/photo21/), +originally developed at ENS Paris-Saclay. -1. **Dependency installation.** - If you are not using Debian, please feel free to adapt the following instructions. - - ```bash - sudo apt install git gettext python3-django python3-django-allauth python3-django-crispy-forms python3-docutils python3-exifread python3-pil - ``` - -2. **Cloning.** - Change directory to where you want the project to be. - In production, we usually use `/var/www/photos/` as the `root` user. - - ```bash - git clone https://codeberg.org/krek0/photo21.git && cd photo21 - ``` - -3. **Configuration (production only).** - - ```bash - sudo mkdir static media - sudo chmod +x maintenance_tool.sh - ``` - -4. **Database (production only).** - In development, you may use SQLite (no setup). - In production, we use PostgreSQL which require a bit of setup: - - ```bash - sudo apt install postgresql postgresql-contrib - sudo -u postgres psql - postgres=# CREATE USER photo21 WITH PASSWORD 'un_mot_de_passe_sur'; - postgres=# CREATE DATABASE photo21 OWNER photo21; - ``` - -5. **Initialization.**, - In production, please use `www-data` user. - - ``` - ./manage.py collectstatic - ./manage.py check - ./manage.py migrate - ./manage.py compilemessages - - # Only when creating a new database - ./manage.py loaddata initial - ./manage.py createsuperuser - # change DEBUG to True in photo21/settings.py - ``` - -6. **Maintenance Mode.**, - In production to toggle the server mainteance mode - - ```./maintenance_tool.sh``` - - -## Docker install +## Docker install (recommended for production) 1. Create a `docker-compose.yml` (a ready-to-use file is provided in the repository): @@ -136,11 +81,59 @@ run and to maintain. - `SECRET_KEY` — Django secret key (use a long random string) - Log in to the admin panel and change the `admin` user password ---- +## Development setup -6. *Enjoy \o/* +1. **Cloning.** + Change directory to where you want the project to be. - In development, you can launch the development server using: + ```bash + git clone https://codeberg.org/krek0/photo21.git && cd photo21 + ``` + +2. **Dependency installation.** + If you are not using Debian, please feel free to adapt the following instructions. + + ```bash + sudo apt install git gettext python3-django python3-django-allauth python3-django-crispy-forms python3-docutils python3-exifread python3-pil + ``` + +3. **Configuration.** + + ```bash + sudo mkdir static media + sudo chmod +x maintenance_tool.sh + ``` + +4. **Database.** + In development, you may use SQLite (no setup). + In production, we use PostgreSQL which require a bit of setup: + + ```bash + sudo apt install postgresql postgresql-contrib + sudo -u postgres psql + postgres=# CREATE USER photo21 WITH PASSWORD 'your_password'; + postgres=# CREATE DATABASE photo21 OWNER photo21; + ``` + +5. **Initialization.** + + ``` + ./manage.py collectstatic + ./manage.py check + ./manage.py migrate + ./manage.py compilemessages + + # Only when creating a new database + ./manage.py loaddata initial + ./manage.py createsuperuser + ``` + +6. **Maintenance Mode.** + In production to toggle the server maintenance mode + + ```./maintenance_tool.sh``` + +7. *Enjoy \o/* ```bash (env)$ ./manage.py runserver From 8cda6277532637f72f49326ab68294860abfea65 Mon Sep 17 00:00:00 2001 From: krek0 Date: Sun, 3 May 2026 23:35:24 +0200 Subject: [PATCH 25/27] Drop de/es locales, complete fr translations, add navbar language switcher. --- Dockerfile | 4 + photo21/locale/de/LC_MESSAGES/django.po | 321 --------------------- photo21/locale/es/LC_MESSAGES/django.po | 320 -------------------- photo21/locale/fr/LC_MESSAGES/django.po | 297 +++++++++---------- photo21/settings.py | 2 - photo21/static/lang-select.js | 11 +- photo21/templates/base.html | 45 +-- photo21/templates/index.html | 10 +- photologue/locale/fr/LC_MESSAGES/django.po | 228 ++++++++------- 9 files changed, 309 insertions(+), 929 deletions(-) delete mode 100644 photo21/locale/de/LC_MESSAGES/django.po delete mode 100644 photo21/locale/es/LC_MESSAGES/django.po diff --git a/Dockerfile b/Dockerfile index f24c177..d45d054 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,8 +8,12 @@ WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt +RUN apt-get update && apt-get install -y gettext && rm -rf /var/lib/apt/lists/* + COPY . . +RUN python manage.py compilemessages + # Create volume mount points RUN mkdir -p /app/media /app/static /app/data diff --git a/photo21/locale/de/LC_MESSAGES/django.po b/photo21/locale/de/LC_MESSAGES/django.po deleted file mode 100644 index b1514bd..0000000 --- a/photo21/locale/de/LC_MESSAGES/django.po +++ /dev/null @@ -1,321 +0,0 @@ -# This file is part of photo21 -# Copyright (C) 2022 Amicale des élèves de l'ENS Paris-Saclay -# SPDX-License-Identifier: GPL-3.0-or-later -# -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: PACKAGE VERSION\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-12-07 20:03+0100\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE \n" -"Language: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#: .\photo21\forms.py:16 -msgid "" -"Please enter a valid email address ending with `@crans.org` or `@ens-paris-" -"saclay.fr`." -msgstr "" - -#: .\photo21\forms.py:29 -msgid "Must end with `@crans.org` or `@ens-paris-saclay.fr`." -msgstr "" - -#: .\photo21\settings.py:171 -msgid "German" -msgstr "" - -#: .\photo21\settings.py:172 -msgid "English" -msgstr "" - -#: .\photo21\settings.py:173 -msgid "Spanish" -msgstr "" - -#: .\photo21\settings.py:174 -msgid "French" -msgstr "" - -#: .\photo21\templates\400.html:12 -msgid "Bad request" -msgstr "" - -#: .\photo21\templates\400.html:16 -msgid "" -"Sorry, your request was bad. Don't know what could be wrong. An email has " -"been sent to webmasters with the details of the error. You can now drink a " -"coke." -msgstr "" - -#: .\photo21\templates\403.html:12 -msgid "Permission denied" -msgstr "" - -#: .\photo21\templates\403.html:15 -msgid "You don't have the right to perform this request." -msgstr "" - -#: .\photo21\templates\403.html:17 .\photo21\templates\404.html:21 -msgid "Exception message:" -msgstr "" - -#: .\photo21\templates\404.html:12 -msgid "Page not found" -msgstr "" - -#: .\photo21\templates\404.html:16 -#, python-format -msgid "" -"The requested path %(request_path)s was not found on the server." -msgstr "" - -#: .\photo21\templates\500.html:12 -msgid "Server error" -msgstr "" - -#: .\photo21\templates\500.html:16 -msgid "" -"Sorry, an error occurred when processing your request. An email has been " -"sent to webmasters with the detail of the error, and this will be fixed " -"soon. You can go drink a soft." -msgstr "" - -#: .\photo21\templates\account\email.html:8 -#: .\photo21\templates\account\email.html:16 -#: .\photo21\templates\socialaccount\connections.html:16 -msgid "E-mail Addresses" -msgstr "" - -#: .\photo21\templates\account\email.html:11 .\photo21\templates\base.html:63 -#: .\photo21\templates\socialaccount\connections.html:11 -msgid "Account" -msgstr "" - -#: .\photo21\templates\account\email.html:19 -#: .\photo21\templates\socialaccount\connections.html:19 -msgid "Social connections" -msgstr "" - -#: .\photo21\templates\account\email.html:25 -msgid "The following e-mail addresses are associated with your account:" -msgstr "" - -#: .\photo21\templates\account\email.html:36 -msgid "Verified" -msgstr "" - -#: .\photo21\templates\account\email.html:38 -msgid "Unverified" -msgstr "" - -#: .\photo21\templates\account\email.html:40 -msgid "Primary" -msgstr "" - -#: .\photo21\templates\account\email.html:46 -msgid "Make Primary" -msgstr "" - -#: .\photo21\templates\account\email.html:47 -msgid "Re-send Verification" -msgstr "" - -#: .\photo21\templates\account\email.html:48 -#: .\photo21\templates\socialaccount\connections.html:47 -msgid "Remove" -msgstr "" - -#: .\photo21\templates\account\email.html:53 -msgid "Warning:" -msgstr "" - -#: .\photo21\templates\account\email.html:53 -msgid "" -"You currently do not have any e-mail address set up. You should really add " -"an e-mail address so you can receive notifications, reset your password, etc." -msgstr "" - -#: .\photo21\templates\account\email.html:57 -msgid "Add E-mail Address" -msgstr "" - -#: .\photo21\templates\account\email.html:62 -msgid "Add E-mail" -msgstr "" - -#: .\photo21\templates\account\login.html:8 -#: .\photo21\templates\account\login.html:36 -msgid "Sign In" -msgstr "" - -#: .\photo21\templates\account\login.html:19 -#, python-format -msgid "" -"Please sign in with one of your existing third party accounts. Or, sign up for a %(site_name)s account and sign in " -"below:" -msgstr "" - -#: .\photo21\templates\account\login.html:26 -#, python-format -msgid "" -"If you have not created an account yet, then please sign up first." -msgstr "" - -#: .\photo21\templates\account\login.html:39 -msgid "Forgot Password?" -msgstr "" - -#: .\photo21\templates\account\login.html:42 -msgid "If any problem, please contact the server owners at" -msgstr "" - -#: .\photo21\templates\account\logout.html:8 -#: .\photo21\templates\account\logout.html:13 -#: .\photo21\templates\account\logout.html:22 -msgid "Sign Out" -msgstr "" - -#: .\photo21\templates\account\logout.html:16 -msgid "Are you sure you want to sign out?" -msgstr "" - -#: .\photo21\templates\account\signup.html:8 -msgid "Signup" -msgstr "" - -#: .\photo21\templates\account\signup.html:13 -#: .\photo21\templates\account\signup.html:24 -msgid "Sign Up" -msgstr "" - -#: .\photo21\templates\account\signup.html:16 -#, python-format -msgid "" -"Already have an account? Then please sign in." -msgstr "" - -#: .\photo21\templates\base.html:16 -msgid "The ENS Paris-Saclay pictures server." -msgstr "" - -#: .\photo21\templates\base.html:41 -msgid "Galleries" -msgstr "" - -#: .\photo21\templates\base.html:46 -msgid "Upload" -msgstr "" - -#: .\photo21\templates\base.html:51 -msgid "Manage" -msgstr "" - -#: .\photo21\templates\base.html:72 -msgid "Log out" -msgstr "" - -#: .\photo21\templates\base.html:82 -msgid "Log in" -msgstr "" - -#: .\photo21\templates\base.html:91 -msgid "Sign up" -msgstr "" - -#: .\photo21\templates\base.html:116 -msgid "Connected as" -msgstr "" - -#: .\photo21\templates\base.html:118 -msgid "Source code" -msgstr "" - -#: .\photo21\templates\index.html:8 -msgid "Home" -msgstr "" - -#: .\photo21\templates\index.html:11 -msgid "Welcome to the pictures server!" -msgstr "" - -#: .\photo21\templates\index.html:13 -msgid "" -"This website aims to collect the pictures and movies taken in the student " -"life of ENS Paris-Saclay or involving its students." -msgstr "" - -#: .\photo21\templates\index.html:20 -#, python-format -msgid "" -"The pictures are visible in the " -"galleries and are downloadable. However, the agreement of the " -"photographer and the persons present on the photo is necessary before any " -"republication on another platform. " -msgstr "" - -#: .\photo21\templates\index.html:29 -msgid "" -"If you want a photo to be deleted, please let us know: Abuse request" -msgstr "" - -#: .\photo21\templates\index.html:36 -msgid "" -"If you want to obtain the right to upload pictures, please let us know: Become a photograph" -msgstr "" - -#: .\photo21\templates\index.html:43 -msgid "Last galleries" -msgstr "" - -#: .\photo21\templates\index.html:52 -msgid "Behind the scene" -msgstr "" - -#: .\photo21\templates\index.html:54 -msgid "" -"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:" -msgstr "" - -#: .\photo21\templates\index.html:63 -msgid "They should be contacted at" -msgstr "" - -#: .\photo21\templates\socialaccount\connections.html:8 -msgid "Account Connections" -msgstr "" - -#: .\photo21\templates\socialaccount\connections.html:25 -msgid "" -"You can sign in to your account using any of the following third party " -"accounts:" -msgstr "" - -#: .\photo21\templates\socialaccount\connections.html:53 -msgid "" -"You currently have no social network accounts connected to this account." -msgstr "" - -#: .\photo21\templates\socialaccount\connections.html:56 -msgid "Add a 3rd Party Account" -msgstr "" - -#: .\photo21\templates\socialaccount\snippets\provider_list.html:20 -msgid "Sign in with" -msgstr "" diff --git a/photo21/locale/es/LC_MESSAGES/django.po b/photo21/locale/es/LC_MESSAGES/django.po deleted file mode 100644 index a19cb2b..0000000 --- a/photo21/locale/es/LC_MESSAGES/django.po +++ /dev/null @@ -1,320 +0,0 @@ -# This file is part of photo21 -# Copyright (C) 2022 Amicale des élèves de l'ENS Paris-Saclay -# SPDX-License-Identifier: GPL-3.0-or-later -# -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: PACKAGE VERSION\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-12-07 20:03+0100\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE \n" -"Language: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" - -#: .\photo21\forms.py:16 -msgid "" -"Please enter a valid email address ending with `@crans.org` or `@ens-paris-" -"saclay.fr`." -msgstr "" - -#: .\photo21\forms.py:29 -msgid "Must end with `@crans.org` or `@ens-paris-saclay.fr`." -msgstr "" - -#: .\photo21\settings.py:171 -msgid "German" -msgstr "" - -#: .\photo21\settings.py:172 -msgid "English" -msgstr "" - -#: .\photo21\settings.py:173 -msgid "Spanish" -msgstr "" - -#: .\photo21\settings.py:174 -msgid "French" -msgstr "" - -#: .\photo21\templates\400.html:12 -msgid "Bad request" -msgstr "" - -#: .\photo21\templates\400.html:16 -msgid "" -"Sorry, your request was bad. Don't know what could be wrong. An email has " -"been sent to webmasters with the details of the error. You can now drink a " -"coke." -msgstr "" - -#: .\photo21\templates\403.html:12 -msgid "Permission denied" -msgstr "" - -#: .\photo21\templates\403.html:15 -msgid "You don't have the right to perform this request." -msgstr "" - -#: .\photo21\templates\403.html:17 .\photo21\templates\404.html:21 -msgid "Exception message:" -msgstr "" - -#: .\photo21\templates\404.html:12 -msgid "Page not found" -msgstr "" - -#: .\photo21\templates\404.html:16 -#, python-format -msgid "" -"The requested path %(request_path)s was not found on the server." -msgstr "" - -#: .\photo21\templates\500.html:12 -msgid "Server error" -msgstr "" - -#: .\photo21\templates\500.html:16 -msgid "" -"Sorry, an error occurred when processing your request. An email has been " -"sent to webmasters with the detail of the error, and this will be fixed " -"soon. You can go drink a soft." -msgstr "" - -#: .\photo21\templates\account\email.html:8 -#: .\photo21\templates\account\email.html:16 -#: .\photo21\templates\socialaccount\connections.html:16 -msgid "E-mail Addresses" -msgstr "" - -#: .\photo21\templates\account\email.html:11 .\photo21\templates\base.html:63 -#: .\photo21\templates\socialaccount\connections.html:11 -msgid "Account" -msgstr "" - -#: .\photo21\templates\account\email.html:19 -#: .\photo21\templates\socialaccount\connections.html:19 -msgid "Social connections" -msgstr "" - -#: .\photo21\templates\account\email.html:25 -msgid "The following e-mail addresses are associated with your account:" -msgstr "" - -#: .\photo21\templates\account\email.html:36 -msgid "Verified" -msgstr "" - -#: .\photo21\templates\account\email.html:38 -msgid "Unverified" -msgstr "" - -#: .\photo21\templates\account\email.html:40 -msgid "Primary" -msgstr "" - -#: .\photo21\templates\account\email.html:46 -msgid "Make Primary" -msgstr "" - -#: .\photo21\templates\account\email.html:47 -msgid "Re-send Verification" -msgstr "" - -#: .\photo21\templates\account\email.html:48 -#: .\photo21\templates\socialaccount\connections.html:47 -msgid "Remove" -msgstr "" - -#: .\photo21\templates\account\email.html:53 -msgid "Warning:" -msgstr "" - -#: .\photo21\templates\account\email.html:53 -msgid "" -"You currently do not have any e-mail address set up. You should really add " -"an e-mail address so you can receive notifications, reset your password, etc." -msgstr "" - -#: .\photo21\templates\account\email.html:57 -msgid "Add E-mail Address" -msgstr "" - -#: .\photo21\templates\account\email.html:62 -msgid "Add E-mail" -msgstr "" - -#: .\photo21\templates\account\login.html:8 -#: .\photo21\templates\account\login.html:36 -msgid "Sign In" -msgstr "" - -#: .\photo21\templates\account\login.html:19 -#, python-format -msgid "" -"Please sign in with one of your existing third party accounts. Or, sign up for a %(site_name)s account and sign in " -"below:" -msgstr "" - -#: .\photo21\templates\account\login.html:26 -#, python-format -msgid "" -"If you have not created an account yet, then please sign up first." -msgstr "" - -#: .\photo21\templates\account\login.html:39 -msgid "Forgot Password?" -msgstr "" - -#: .\photo21\templates\account\login.html:42 -msgid "If any problem, please contact the server owners at" -msgstr "" - -#: .\photo21\templates\account\logout.html:8 -#: .\photo21\templates\account\logout.html:13 -#: .\photo21\templates\account\logout.html:22 -msgid "Sign Out" -msgstr "" - -#: .\photo21\templates\account\logout.html:16 -msgid "Are you sure you want to sign out?" -msgstr "" - -#: .\photo21\templates\account\signup.html:8 -msgid "Signup" -msgstr "" - -#: .\photo21\templates\account\signup.html:13 -#: .\photo21\templates\account\signup.html:24 -msgid "Sign Up" -msgstr "" - -#: .\photo21\templates\account\signup.html:16 -#, python-format -msgid "" -"Already have an account? Then please sign in." -msgstr "" - -#: .\photo21\templates\base.html:16 -msgid "The ENS Paris-Saclay pictures server." -msgstr "" - -#: .\photo21\templates\base.html:41 -msgid "Galleries" -msgstr "" - -#: .\photo21\templates\base.html:46 -msgid "Upload" -msgstr "" - -#: .\photo21\templates\base.html:51 -msgid "Manage" -msgstr "" - -#: .\photo21\templates\base.html:72 -msgid "Log out" -msgstr "" - -#: .\photo21\templates\base.html:82 -msgid "Log in" -msgstr "" - -#: .\photo21\templates\base.html:91 -msgid "Sign up" -msgstr "" - -#: .\photo21\templates\base.html:116 -msgid "Connected as" -msgstr "" - -#: .\photo21\templates\base.html:118 -msgid "Source code" -msgstr "" - -#: .\photo21\templates\index.html:8 -msgid "Home" -msgstr "" - -#: .\photo21\templates\index.html:11 -msgid "Welcome to the pictures server!" -msgstr "" - -#: .\photo21\templates\index.html:13 -msgid "" -"This website aims to collect the pictures and movies taken in the student " -"life of ENS Paris-Saclay or involving its students." -msgstr "" - -#: .\photo21\templates\index.html:20 -#, python-format -msgid "" -"The pictures are visible in the " -"galleries and are downloadable. However, the agreement of the " -"photographer and the persons present on the photo is necessary before any " -"republication on another platform. " -msgstr "" - -#: .\photo21\templates\index.html:29 -msgid "" -"If you want a photo to be deleted, please let us know: Abuse request" -msgstr "" - -#: .\photo21\templates\index.html:36 -msgid "" -"If you want to obtain the right to upload pictures, please let us know: Become a photograph" -msgstr "" - -#: .\photo21\templates\index.html:43 -msgid "Last galleries" -msgstr "" - -#: .\photo21\templates\index.html:52 -msgid "Behind the scene" -msgstr "" - -#: .\photo21\templates\index.html:54 -msgid "" -"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:" -msgstr "" - -#: .\photo21\templates\index.html:63 -msgid "They should be contacted at" -msgstr "" - -#: .\photo21\templates\socialaccount\connections.html:8 -msgid "Account Connections" -msgstr "" - -#: .\photo21\templates\socialaccount\connections.html:25 -msgid "" -"You can sign in to your account using any of the following third party " -"accounts:" -msgstr "" - -#: .\photo21\templates\socialaccount\connections.html:53 -msgid "" -"You currently have no social network accounts connected to this account." -msgstr "" - -#: .\photo21\templates\socialaccount\connections.html:56 -msgid "Add a 3rd Party Account" -msgstr "" - -#: .\photo21\templates\socialaccount\snippets\provider_list.html:20 -msgid "Sign in with" -msgstr "" diff --git a/photo21/locale/fr/LC_MESSAGES/django.po b/photo21/locale/fr/LC_MESSAGES/django.po index de21f66..6c10127 100644 --- a/photo21/locale/fr/LC_MESSAGES/django.po +++ b/photo21/locale/fr/LC_MESSAGES/django.po @@ -1,163 +1,158 @@ # This file is part of photo21 # Copyright (C) 2022 Amicale des élèves de l'ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later -# -#, fuzzy + msgid "" msgstr "" -"Project-Id-Version: PACKAGE VERSION\n" +"Project-Id-Version: photo21\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-12-07 20:03+0100\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE \n" -"Language: \n" +"Language-Team: French\n" +"Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" -#: .\photo21\forms.py:16 -msgid "" -"Please enter a valid email address ending with `@crans.org` or `@ens-paris-" -"saclay.fr`." -msgstr "" -"Veuillez entrer une adresse email valide finissant par `@crans.org` ou `@ens-" -"paris-saclay.fr`." +#: photo21/forms.py:16 +msgid "Please enter a valid email address ending with `@ens-rennes.fr`" +msgstr "Veuillez entrer une adresse e-mail valide finissant par `@ens-rennes.fr`." -#: .\photo21\forms.py:29 -msgid "Must end with `@crans.org` or `@ens-paris-saclay.fr`." -msgstr "Doit finir par `@crans.org` ou `@ens-paris-saclay.fr`." +#: photo21/forms.py:26 +msgid "Must end with `@ens-rennes.fr`." +msgstr "Doit finir par `@ens-rennes.fr`." -#: .\photo21\settings.py:171 -msgid "German" -msgstr "" - -#: .\photo21\settings.py:172 +#: photo21/settings.py:201 msgid "English" -msgstr "" +msgstr "Anglais" -#: .\photo21\settings.py:173 -msgid "Spanish" -msgstr "" - -#: .\photo21\settings.py:174 +#: photo21/settings.py:202 msgid "French" -msgstr "" +msgstr "Français" -#: .\photo21\templates\400.html:12 +#: photo21/templates/400.html:12 msgid "Bad request" -msgstr "" +msgstr "Requête incorrecte" -#: .\photo21\templates\400.html:16 +#: photo21/templates/400.html:16 msgid "" "Sorry, your request was bad. Don't know what could be wrong. An email has " "been sent to webmasters with the details of the error. You can now drink a " "coke." msgstr "" +"Désolé, votre requête était incorrecte. Un e-mail a été envoyé aux " +"administrateurs avec les détails de l'erreur. Vous pouvez aller boire un " +"coca." -#: .\photo21\templates\403.html:12 +#: photo21/templates/403.html:12 msgid "Permission denied" -msgstr "" +msgstr "Permission refusée" -#: .\photo21\templates\403.html:15 +#: photo21/templates/403.html:15 msgid "You don't have the right to perform this request." -msgstr "" +msgstr "Vous n'avez pas le droit d'effectuer cette requête." -#: .\photo21\templates\403.html:17 .\photo21\templates\404.html:21 +#: photo21/templates/403.html:17 photo21/templates/404.html:21 msgid "Exception message:" -msgstr "" +msgstr "Message d'exception :" -#: .\photo21\templates\404.html:12 +#: photo21/templates/404.html:12 msgid "Page not found" -msgstr "" +msgstr "Page introuvable" -#: .\photo21\templates\404.html:16 +#: photo21/templates/404.html:16 #, python-format msgid "" "The requested path %(request_path)s was not found on the server." msgstr "" +"Le chemin demandé %(request_path)s n'a pas été trouvé sur le " +"serveur." -#: .\photo21\templates\500.html:12 +#: photo21/templates/500.html:12 msgid "Server error" -msgstr "" +msgstr "Erreur serveur" -#: .\photo21\templates\500.html:16 +#: photo21/templates/500.html:16 msgid "" "Sorry, an error occurred when processing your request. An email has been " "sent to webmasters with the detail of the error, and this will be fixed " "soon. You can go drink a soft." msgstr "" +"Désolé, une erreur s'est produite lors du traitement de votre requête. Un e-" +"mail a été envoyé aux administrateurs avec les détails de l'erreur, et cela " +"sera corrigé prochainement. Vous pouvez aller boire un soda." -#: .\photo21\templates\account\email.html:8 -#: .\photo21\templates\account\email.html:16 -#: .\photo21\templates\socialaccount\connections.html:16 +#: photo21/templates/account/email.html:8 +#: photo21/templates/account/email.html:16 +#: photo21/templates/socialaccount/connections.html:16 msgid "E-mail Addresses" -msgstr "" +msgstr "Adresses e-mail" -#: .\photo21\templates\account\email.html:11 .\photo21\templates\base.html:63 -#: .\photo21\templates\socialaccount\connections.html:11 +#: photo21/templates/account/email.html:11 photo21/templates/base.html:66 +#: photo21/templates/socialaccount/connections.html:11 msgid "Account" msgstr "Compte" -#: .\photo21\templates\account\email.html:19 -#: .\photo21\templates\socialaccount\connections.html:19 +#: photo21/templates/account/email.html:19 +#: photo21/templates/socialaccount/connections.html:19 msgid "Social connections" msgstr "Connexions sociales" -#: .\photo21\templates\account\email.html:25 +#: photo21/templates/account/email.html:25 msgid "The following e-mail addresses are associated with your account:" -msgstr "" +msgstr "Les adresses e-mail suivantes sont associées à votre compte :" -#: .\photo21\templates\account\email.html:36 +#: photo21/templates/account/email.html:36 msgid "Verified" -msgstr "" +msgstr "Vérifié" -#: .\photo21\templates\account\email.html:38 +#: photo21/templates/account/email.html:38 msgid "Unverified" -msgstr "" +msgstr "Non vérifié" -#: .\photo21\templates\account\email.html:40 +#: photo21/templates/account/email.html:40 msgid "Primary" -msgstr "" +msgstr "Principal" -#: .\photo21\templates\account\email.html:46 +#: photo21/templates/account/email.html:46 msgid "Make Primary" -msgstr "" +msgstr "Définir comme principal" -#: .\photo21\templates\account\email.html:47 +#: photo21/templates/account/email.html:47 msgid "Re-send Verification" -msgstr "" +msgstr "Renvoyer la vérification" -#: .\photo21\templates\account\email.html:48 -#: .\photo21\templates\socialaccount\connections.html:47 +#: photo21/templates/account/email.html:48 +#: photo21/templates/socialaccount/connections.html:47 msgid "Remove" -msgstr "" +msgstr "Supprimer" -#: .\photo21\templates\account\email.html:53 +#: photo21/templates/account/email.html:53 msgid "Warning:" -msgstr "" +msgstr "Avertissement :" -#: .\photo21\templates\account\email.html:53 +#: photo21/templates/account/email.html:53 msgid "" "You currently do not have any e-mail address set up. You should really add " "an e-mail address so you can receive notifications, reset your password, etc." msgstr "" +"Vous n'avez actuellement aucune adresse e-mail configurée. Vous devriez " +"vraiment en ajouter une pour recevoir des notifications, réinitialiser votre " +"mot de passe, etc." -#: .\photo21\templates\account\email.html:57 +#: photo21/templates/account/email.html:57 msgid "Add E-mail Address" msgstr "Ajouter une Adresse E-mail" -#: .\photo21\templates\account\email.html:62 +#: photo21/templates/account/email.html:62 msgid "Add E-mail" msgstr "Ajouter un E-mail" -#: .\photo21\templates\account\login.html:8 -#: .\photo21\templates\account\login.html:36 +#: photo21/templates/account/login.html:8 +#: photo21/templates/account/login.html:36 msgid "Sign In" msgstr "Se Connecter" -#: .\photo21\templates\account\login.html:19 +#: photo21/templates/account/login.html:19 #, python-format msgid "" "Please sign in with one of your existing third party accounts. Or, inscrivez-vous pour un compte sur %(site_name)s " "et identifiez-vous ci-dessous :" -#: .\photo21\templates\account\login.html:26 +#: photo21/templates/account/login.html:26 #, python-format msgid "" "If you have not created an account yet, then please inscrire." -#: .\photo21\templates\account\login.html:39 +#: photo21/templates/account/login.html:39 msgid "Forgot Password?" msgstr "Mot de passe oublié ?" -#: .\photo21\templates\account\login.html:42 +#: photo21/templates/account/login.html:42 msgid "If any problem, please contact the server owners at" msgstr "En cas de problème, contactez les administrateurs à" -#: .\photo21\templates\account\logout.html:8 -#: .\photo21\templates\account\logout.html:13 -#: .\photo21\templates\account\logout.html:22 +#: photo21/templates/account/logout.html:8 +#: photo21/templates/account/logout.html:13 +#: photo21/templates/account/logout.html:22 msgid "Sign Out" msgstr "Déconnexion" -#: .\photo21\templates\account\logout.html:16 +#: photo21/templates/account/logout.html:16 msgid "Are you sure you want to sign out?" -msgstr "" +msgstr "Êtes-vous sûr de vouloir vous déconnecter ?" -#: .\photo21\templates\account\signup.html:8 +#: photo21/templates/account/signup.html:8 msgid "Signup" -msgstr "" +msgstr "Inscription" -#: .\photo21\templates\account\signup.html:13 -#: .\photo21\templates\account\signup.html:24 +#: photo21/templates/account/signup.html:13 +#: photo21/templates/account/signup.html:24 msgid "Sign Up" -msgstr "" +msgstr "S'inscrire" -#: .\photo21\templates\account\signup.html:16 +#: photo21/templates/account/signup.html:16 #, python-format msgid "" "Already have an account? Then please sign in." msgstr "" +"Vous avez déjà un compte ? Veuillez alors vous " +"connecter." -#: .\photo21\templates\base.html:16 -msgid "The ENS Paris-Saclay pictures server." -msgstr "" +#: photo21/templates/base.html:16 +msgid "The ENS Rennes pictures server." +msgstr "Le serveur photos de l'ENS Rennes." -#: .\photo21\templates\base.html:41 +#: photo21/templates/base.html:43 msgid "Galleries" msgstr "Galeries" -#: .\photo21\templates\base.html:46 +#: photo21/templates/base.html:48 msgid "Upload" -msgstr "Téléversement" +msgstr "Upload" -#: .\photo21\templates\base.html:51 +#: photo21/templates/base.html:53 msgid "Manage" -msgstr "Gestion" +msgstr "Manage" -#: .\photo21\templates\base.html:72 +#: photo21/templates/base.html:75 msgid "Log out" -msgstr "" +msgstr "Déconnection" -#: .\photo21\templates\base.html:82 +#: photo21/templates/base.html:85 msgid "Log in" -msgstr "" +msgstr "Connection" -#: .\photo21\templates\base.html:91 +#: photo21/templates/base.html:94 msgid "Sign up" msgstr "Inscription" -#: .\photo21\templates\base.html:116 +#: photo21/templates/base.html:119 msgid "Connected as" msgstr "Connecté en tant que" -#: .\photo21\templates\base.html:118 +#: photo21/templates/base.html:121 msgid "Source code" msgstr "Code source" -#: .\photo21\templates\index.html:8 +#: photo21/templates/index.html:8 msgid "Home" msgstr "Accueil" -#: .\photo21\templates\index.html:11 +#: photo21/templates/index.html:11 msgid "Welcome to the pictures server!" msgstr "Bienvenue sur le serveur photos !" -#: .\photo21\templates\index.html:13 +#: photo21/templates/index.html:13 msgid "" "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." msgstr "" -"Ce site à pour objectif de recenser les photos et films pris dans la vie " -"associative de l'ENS Paris-Saclay ou impliquant ses usager·ères." +"Ce site a pour objectif de recenser les photos et films pris dans la vie " +"associative de l'ENS Rennes ou impliquant ses étudiant·es." -#: .\photo21\templates\index.html:20 +#: photo21/templates/index.html:20 #, python-format msgid "" "The pictures are visible in the " @@ -275,73 +272,77 @@ msgstr "" "photographe et des personnes présentes sur la photo est nécessaire avant " "toute republication sur un autre site." -#: .\photo21\templates\index.html:29 -msgid "" -"If you want a photo to be deleted, please let us know: Abuse request" -msgstr "" -"Si vous souhaitez qu'une photo soit supprimée, signalez le nous : Signaler un abus" - -#: .\photo21\templates\index.html:36 +#: photo21/templates/index.html:30 msgid "" "If you want to obtain the right to upload pictures, please let us know: Become a photograph" msgstr "" "Si vous souhaitez obtenir les droits photographes pour téléverser vos " -"photos, signalez le nous : Devenir photographe" -#: .\photo21\templates\index.html:43 +#: photo21/templates/index.html:38 msgid "Last galleries" msgstr "Galeries récentes" -#: .\photo21\templates\index.html:52 +#: photo21/templates/index.html:47 msgid "Behind the scene" -msgstr "Derrière la scène" +msgstr "Behind the scene" -#: .\photo21\templates\index.html:54 +#: photo21/templates/index.html:49 msgid "" -"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:" +" " +"The dedicated server running this website is kindly hosted by Sinfonie at the ENS Rennes. " +" " +"Le serveur qui fait fonctionner ce site est gentiment hébergé par " +"Sinfonie à l'ENS Rennes. " +" - - - - - - - - - -

- +

+ {% if request.user.is_authenticated %} + {% trans "Connected as" %} {{ request.user.username }} · + {% endif %} + {% trans "Source code" %} +

+ diff --git a/photo21/templates/index.html b/photo21/templates/index.html index d4db262..252198c 100644 --- a/photo21/templates/index.html +++ b/photo21/templates/index.html @@ -47,11 +47,11 @@ 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 + Sinfonie at the ENS Rennes. diff --git a/photologue/locale/fr/LC_MESSAGES/django.po b/photologue/locale/fr/LC_MESSAGES/django.po index 1993f72..b60677f 100644 --- a/photologue/locale/fr/LC_MESSAGES/django.po +++ b/photologue/locale/fr/LC_MESSAGES/django.po @@ -10,7 +10,7 @@ msgid "" msgstr "" "Project-Id-Version: Photologue\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-03-05 15:30+0100\n" +"POT-Creation-Date: 2026-05-03 21:01+0000\n" "PO-Revision-Date: 2017-12-03 14:47+0000\n" "Last-Translator: Richard Barran \n" "Language-Team: French (http://www.transifex.com/richardbarran/django-" @@ -21,24 +21,23 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" -#: .\photologue\admin.py:26 .\photologue\models.py:168 -#: .\photologue\models.py:786 +#: photologue/admin.py:25 photologue/models.py:168 photologue/models.py:802 msgid "tags" msgstr "balises" -#: .\photologue\admin.py:63 .\photologue\forms.py:68 +#: photologue/admin.py:62 photologue/forms.py:69 msgid "Gallery" msgstr "Galerie" -#: .\photologue\admin.py:67 .\photologue\models.py:529 +#: photologue/admin.py:66 photologue/models.py:542 msgid "owner" msgstr "propriétaire" -#: .\photologue\forms.py:70 +#: photologue/forms.py:71 msgid "-- Create a new gallery --" msgstr "-- Créer une nouvelle galerie --" -#: .\photologue\forms.py:72 +#: photologue/forms.py:73 msgid "" "Select a gallery to add these images to. Leave this empty to create a new " "gallery from the supplied title." @@ -46,117 +45,108 @@ msgstr "" "Sélectionner une galerie à laquelle ajouter ces images. Laisser ce champ " "vide pour créer une nouvelle galerie à partir du titre indiqué." -#: .\photologue\forms.py:77 +#: photologue/forms.py:78 msgid "New gallery title" msgstr "Titre de la nouvelle galerie" -#: .\photologue\forms.py:82 +#: photologue/forms.py:83 msgid "New gallery event start date" msgstr "Date de début de l'évènement de la nouvelle galerie" -#: .\photologue\forms.py:87 +#: photologue/forms.py:89 msgid "New gallery event end date" msgstr "Date de fin de l'évènement de la nouvelle galerie" -#: .\photologue\forms.py:93 +#: photologue/forms.py:96 #, fuzzy #| msgid "description" msgid "Description" msgstr "description" -#: .\photologue\forms.py:98 +#: photologue/forms.py:101 msgid "New gallery tags" msgstr "Balises de la nouvelle galerie" -#: .\photologue\forms.py:101 -msgid "" -"Hold down \"Control\", or \"Command\" on a Mac, to select more than one." -msgstr "" - -#: .\photologue\forms.py:120 .\photologue\templates\photologue\upload.html:8 -#: .\photologue\templates\photologue\upload.html:15 +#: photologue/forms.py:129 photologue/templates/photologue/upload.html:8 +#: photologue/templates/photologue/upload.html:17 msgid "Upload" msgstr "Télécharger" -#: .\photologue\forms.py:126 -msgid "A gallery with that title already exists." -msgstr "Une galerie portant ce nom existe déjà." - -#: .\photologue\forms.py:138 +#: photologue/forms.py:141 msgid "Select an existing gallery, or enter a title for a new gallery." msgstr "" "Sélectionner une galerie existante ou entrer un titre pour une nouvelle " "galerie." -#: .\photologue\models.py:88 +#: photologue/models.py:88 msgid "Very Low" msgstr "Très Bas" -#: .\photologue\models.py:89 +#: photologue/models.py:89 msgid "Low" msgstr "Bas" -#: .\photologue\models.py:90 +#: photologue/models.py:90 msgid "Medium-Low" msgstr "Moyen-Bas" -#: .\photologue\models.py:91 +#: photologue/models.py:91 msgid "Medium" msgstr "Moyen" -#: .\photologue\models.py:92 +#: photologue/models.py:92 msgid "Medium-High" msgstr "Moyen-Haut" -#: .\photologue\models.py:93 +#: photologue/models.py:93 msgid "High" msgstr "Haut" -#: .\photologue\models.py:94 +#: photologue/models.py:94 msgid "Very High" msgstr "Très Haut" -#: .\photologue\models.py:99 +#: photologue/models.py:99 msgid "Top" msgstr "Sommet" -#: .\photologue\models.py:100 +#: photologue/models.py:100 msgid "Right" msgstr "Droite" -#: .\photologue\models.py:101 +#: photologue/models.py:101 msgid "Bottom" msgstr "Bas" -#: .\photologue\models.py:102 +#: photologue/models.py:102 msgid "Left" msgstr "Gauche" -#: .\photologue\models.py:103 +#: photologue/models.py:103 msgid "Center (Default)" msgstr "Centré (par défaut)" -#: .\photologue\models.py:107 +#: photologue/models.py:107 msgid "Flip left to right" msgstr "Inversion de gauche à droite" -#: .\photologue\models.py:108 +#: photologue/models.py:108 msgid "Flip top to bottom" msgstr "Inversion de haut en bas" -#: .\photologue\models.py:109 +#: photologue/models.py:109 msgid "Rotate 90 degrees counter-clockwise" msgstr "Rotation de 90 degrés dans le sens anti-horloger" -#: .\photologue\models.py:110 +#: photologue/models.py:110 msgid "Rotate 90 degrees clockwise" msgstr "Rotation de 90 degrés dans le sens horloger" -#: .\photologue\models.py:111 +#: photologue/models.py:111 msgid "Rotate 180 degrees" msgstr "Rotation de 180 degrés" -#: .\photologue\models.py:125 +#: photologue/models.py:125 #, python-format msgid "" "Chain multiple filters using the following pattern \"FILTER_ONE->FILTER_TWO-" @@ -167,116 +157,119 @@ msgstr "" ">FILTRE_DEUX->FILTRE_TROIS\". Les filtres d'image seront appliqués dans " "l'ordre. Les filtres suivants sont disponibles: %s." -#: .\photologue\models.py:148 .\photologue\models.py:517 +#: photologue/models.py:148 photologue/models.py:530 msgid "title" msgstr "titre" -#: .\photologue\models.py:150 +#: photologue/models.py:150 msgid "title slug" msgstr "version abrégée du titre" -#: .\photologue\models.py:153 .\photologue\models.py:522 -#: .\photologue\models.py:780 +#: photologue/models.py:153 photologue/models.py:535 photologue/models.py:796 msgid "A \"slug\" is a unique URL-friendly title for an object." msgstr "" "Un \"slug\" est un titre abrégé et unique, compatible avec les URL, pour un " "objet." -#: .\photologue\models.py:157 +#: photologue/models.py:157 msgid "start date" msgstr "date de début" -#: .\photologue\models.py:162 +#: photologue/models.py:162 msgid "end date" msgstr "date de fin" -#: .\photologue\models.py:164 +#: photologue/models.py:164 msgid "description" msgstr "description" -#: .\photologue\models.py:174 .\photologue\models.py:548 +#: photologue/models.py:174 photologue/models.py:561 msgid "photos" msgstr "photos" -#: .\photologue\models.py:181 +#: photologue/models.py:178 +msgid "public token" +msgstr "" + +#: photologue/models.py:188 msgid "gallery" msgstr "galerie" -#: .\photologue\models.py:182 +#: photologue/models.py:189 msgid "galleries" msgstr "galleries" -#: .\photologue\models.py:214 +#: photologue/models.py:221 msgid "count" msgstr "nombre" -#: .\photologue\models.py:215 +#: photologue/models.py:222 msgid "private count" msgstr "nombre de photos privées" -#: .\photologue\models.py:220 +#: photologue/models.py:227 msgid "image" msgstr "image" -#: .\photologue\models.py:223 +#: photologue/models.py:236 msgid "date taken" msgstr "date de prise de vue" -#: .\photologue\models.py:226 +#: photologue/models.py:239 msgid "Date image was taken; is obtained from the image EXIF data." msgstr "" "La date à laquelle l'image a été prise ; obtenue à partir des données EXIF " "de l'image." -#: .\photologue\models.py:228 +#: photologue/models.py:241 msgid "view count" msgstr "nombre" -#: .\photologue\models.py:230 +#: photologue/models.py:243 msgid "crop from" msgstr "découper à partir de" -#: .\photologue\models.py:254 +#: photologue/models.py:267 msgid "An \"admin_thumbnail\" photo size has not been defined." msgstr "Une taille de photo \"admin_thumbnail\" n'a pas encore été définie." -#: .\photologue\models.py:267 +#: photologue/models.py:280 msgid "Thumbnail" msgstr "Miniature" -#: .\photologue\models.py:519 .\photologue\models.py:779 +#: photologue/models.py:532 photologue/models.py:795 msgid "slug" msgstr "libellé court" -#: .\photologue\models.py:524 +#: photologue/models.py:537 msgid "caption" msgstr "légende" -#: .\photologue\models.py:525 +#: photologue/models.py:538 msgid "date added" msgstr "date d'ajout" -#: .\photologue\models.py:534 +#: photologue/models.py:547 msgid "license" msgstr "licence" -#: .\photologue\models.py:537 +#: photologue/models.py:550 msgid "is public" msgstr "est public" -#: .\photologue\models.py:539 +#: photologue/models.py:552 msgid "Public photographs will be displayed in the default views." msgstr "Les photographies publique seront affichées dans les vues par défaut." -#: .\photologue\models.py:547 +#: photologue/models.py:560 msgid "photo" msgstr "photo" -#: .\photologue\models.py:610 .\photologue\models.py:774 +#: photologue/models.py:626 photologue/models.py:790 msgid "name" msgstr "nom" -#: .\photologue\models.py:614 +#: photologue/models.py:630 msgid "" "Photo size name should contain only letters, numbers and underscores. " "Examples: \"thumbnail\", \"display\", \"small\", \"main_page_widget\"." @@ -285,41 +278,41 @@ msgstr "" "chiffres et des caractères de soulignement. Exemples: \"miniature\", " "\"affichage\", \"petit\", \"widget_page_principale\"." -#: .\photologue\models.py:626 +#: photologue/models.py:642 msgid "width" msgstr "largeur" -#: .\photologue\models.py:629 +#: photologue/models.py:645 msgid "" "If width is set to \"0\" the image will be scaled to the supplied height." msgstr "" "Si la largeur est réglée à \"0\" l l'image sera redimensionnée par rapport à " "la hauteur fournie." -#: .\photologue\models.py:633 +#: photologue/models.py:649 msgid "height" msgstr "hauteur" -#: .\photologue\models.py:636 +#: photologue/models.py:652 msgid "" "If height is set to \"0\" the image will be scaled to the supplied width" msgstr "" "Si la hauteur est réglée à \"0\" l l'image sera redimensionnée par rapport à " "la largeur fournie." -#: .\photologue\models.py:640 +#: photologue/models.py:656 msgid "quality" msgstr "qualité" -#: .\photologue\models.py:643 +#: photologue/models.py:659 msgid "JPEG image quality." msgstr "Qualité JPEG de l'image." -#: .\photologue\models.py:646 +#: photologue/models.py:662 msgid "upscale images?" msgstr "agrandir les images ?" -#: .\photologue\models.py:649 +#: photologue/models.py:665 msgid "" "If selected the image will be scaled up if necessary to fit the supplied " "dimensions. Cropped sizes will be upscaled regardless of this setting." @@ -328,11 +321,11 @@ msgstr "" "dimensions fournies. Les dimensions ajustées seront agrandies sans prendre " "en compte ce paramètre." -#: .\photologue\models.py:655 +#: photologue/models.py:671 msgid "crop to fit?" msgstr "découper pour adapter à la taille ?" -#: .\photologue\models.py:658 +#: photologue/models.py:674 msgid "" "If selected the image will be scaled and cropped to fit the supplied " "dimensions." @@ -340,21 +333,21 @@ msgstr "" "Si sélectionné l'image sera redimensionnée et recadrée pour coïncider avec " "les dimensions fournies." -#: .\photologue\models.py:663 +#: photologue/models.py:679 msgid "pre-cache?" msgstr "mise en cache ?" -#: .\photologue\models.py:666 +#: photologue/models.py:682 msgid "If selected this photo size will be pre-cached as photos are added." msgstr "" "Si sélectionné cette taille de photo sera mise en cache au moment au les " "photos sont ajoutées." -#: .\photologue\models.py:670 +#: photologue/models.py:686 msgid "increment view count?" msgstr "incrémenter le nombre d'affichages ?" -#: .\photologue\models.py:673 +#: photologue/models.py:689 msgid "" "If selected the image's \"view_count\" will be incremented when this photo " "size is displayed." @@ -362,79 +355,95 @@ msgstr "" "Si sélectionné le \"view_count\" (nombre d'affichage) de l'image sera " "incrémenté quand cette taille de photo sera affichée." -#: .\photologue\models.py:680 +#: photologue/models.py:696 msgid "photo size" msgstr "taille de la photo" -#: .\photologue\models.py:681 +#: photologue/models.py:697 msgid "photo sizes" msgstr "tailles des photos" -#: .\photologue\models.py:699 +#: photologue/models.py:715 msgid "Can only crop photos if both width and height dimensions are set." msgstr "" "La hauteur et la largeur doivent être toutes les deux définies pour " "retailler des photos." -#: .\photologue\models.py:785 +#: photologue/models.py:801 msgid "tag" msgstr "" -#: .\photologue\templates\photologue\gallery_archive.html:9 +#: photologue/templates/photologue/gallery_archive.html:9 msgid "Latest photo galleries" msgstr "Dernières galeries de photos" -#: .\photologue\templates\photologue\gallery_archive.html:14 -#: .\photologue\templates\photologue\gallery_archive_year.html:14 +#: photologue/templates/photologue/gallery_archive.html:14 +#: photologue/templates/photologue/gallery_archive_year.html:14 msgid "Filter by year" msgstr "Filtrer par année" -#: .\photologue\templates\photologue\gallery_archive.html:31 +#: photologue/templates/photologue/gallery_archive.html:31 msgid "No galleries were found" msgstr "Aucune galerie trouvée" -#: .\photologue\templates\photologue\gallery_archive_year.html:9 +#: photologue/templates/photologue/gallery_archive_year.html:9 #, python-format msgid "Galleries for %(show_year)s" msgstr "Galeries de %(show_year)s" -#: .\photologue\templates\photologue\gallery_archive_year.html:31 +#: photologue/templates/photologue/gallery_archive_year.html:31 msgid "No galleries were found." msgstr "Aucune galerie trouvée." -#: .\photologue\templates\photologue\gallery_detail.html:42 +#: photologue/templates/photologue/gallery_detail.html:48 msgid "to" msgstr "au" -#: .\photologue\templates\photologue\gallery_detail.html:59 +#: photologue/templates/photologue/gallery_detail.html:53 +msgid "Public link:" +msgstr "" + +#: photologue/templates/photologue/gallery_detail.html:55 +msgid "Copy" +msgstr "" + +#: photologue/templates/photologue/gallery_detail.html:56 +msgid "Revoke" +msgstr "" + +#: photologue/templates/photologue/gallery_detail.html:59 +msgid "Generate public link" +msgstr "" + +#: photologue/templates/photologue/gallery_detail.html:78 msgid "All pictures" msgstr "Toutes les photos" -#: .\photologue\templates\photologue\gallery_detail.html:85 +#: photologue/templates/photologue/gallery_detail.html:106 msgid "Download all gallery" msgstr "Télécharger toute la galerie" -#: .\photologue\templates\photologue\photo_confirm_delete.html:9 -#: .\photologue\templates\photologue\photo_confirm_delete.html:14 +#: photologue/templates/photologue/photo_confirm_delete.html:9 +#: photologue/templates/photologue/photo_confirm_delete.html:14 msgid "Delete confirmation" msgstr "Confirmation de la suppression" -#: .\photologue\templates\photologue\photo_confirm_delete.html:17 +#: photologue/templates/photologue/photo_confirm_delete.html:17 #, python-format msgid "Are you sure you want to delete %(object)s?" msgstr "Es tu sure que tu veux supprimer %(object)s ?" -#: .\photologue\templates\photologue\photo_confirm_delete.html:22 -#: .\photologue\templates\photologue\photo_confirm_report.html:22 +#: photologue/templates/photologue/photo_confirm_delete.html:22 +#: photologue/templates/photologue/photo_confirm_report.html:22 msgid "Confirm" msgstr "Confimer" -#: .\photologue\templates\photologue\photo_confirm_report.html:9 -#: .\photologue\templates\photologue\photo_confirm_report.html:14 +#: photologue/templates/photologue/photo_confirm_report.html:9 +#: photologue/templates/photologue/photo_confirm_report.html:14 msgid "Report confirmation" msgstr "Raporter la confirmation" -#: .\photologue\templates\photologue\photo_confirm_report.html:17 +#: photologue/templates/photologue/photo_confirm_report.html:17 #, python-format msgid "" "Are you sure you want to report %(object)s? This photo will no " @@ -443,14 +452,17 @@ msgstr "" "Es-tu sur de signaler %(object)s? Cette photo ne va plus être " "publique, et les administrateur vont être notifiés." -#: .\photologue\templates\photologue\photo_detail.html:15 +#: photologue/templates/photologue/photo_detail.html:15 msgid "Published" msgstr "Publiée le" -#: .\photologue\templates\photologue\upload.html:20 +#: photologue/templates/photologue/upload.html:22 msgid "Drag and drop photos here" msgstr "Glissez et déposez les photos ici" -#: .\photologue\templates\photologue\upload.html:24 +#: photologue/templates/photologue/upload.html:26 msgid "Owner will be" msgstr "Le propriétaire sera" + +#~ msgid "A gallery with that title already exists." +#~ msgstr "Une galerie portant ce nom existe déjà." From 87fbcdc013e39cee3ff3423a0144c8fee6503ced Mon Sep 17 00:00:00 2001 From: krek0 Date: Sun, 3 May 2026 23:36:34 +0200 Subject: [PATCH 26/27] Fix image display in Docker by loading initial fixtures on startup --- Dockerfile | 2 +- entrypoint.sh | 1 + photo21/settings.py | 1 - 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index d45d054..8e47172 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ RUN apt-get update && apt-get install -y gettext && rm -rf /var/lib/apt/lists/* COPY . . -RUN python manage.py compilemessages +RUN SECRET_KEY=dummy python manage.py compilemessages # Create volume mount points RUN mkdir -p /app/media /app/static /app/data diff --git a/entrypoint.sh b/entrypoint.sh index fca4a2c..8638676 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -3,5 +3,6 @@ set -e python manage.py collectstatic --noinput python manage.py migrate --noinput +python manage.py loaddata initial python manage.py create_default_admin exec gunicorn photo21.wsgi:application --bind 0.0.0.0:8000 --workers 3 diff --git a/photo21/settings.py b/photo21/settings.py index 222d7fa..953a8c2 100644 --- a/photo21/settings.py +++ b/photo21/settings.py @@ -235,7 +235,6 @@ WHITENOISE_MANIFEST_STRICT = False LOCALE_PATHS = [os.path.join(BASE_DIR, "photo21/locale")] -FIXTURE_DIRS = [os.path.join(BASE_DIR, "photo21/fixtures")] # Do not send email during debug # By default Django sends mails to localhost:25 without authentification From 737f07328793f3094eebd25e8e5beeca9fc1d142 Mon Sep 17 00:00:00 2001 From: krek0 Date: Sun, 3 May 2026 23:50:48 +0200 Subject: [PATCH 27/27] Push latest tag on docker image on release --- .forgejo/workflows/docker.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.forgejo/workflows/docker.yml b/.forgejo/workflows/docker.yml index 90f3a7c..e5da6fc 100644 --- a/.forgejo/workflows/docker.yml +++ b/.forgejo/workflows/docker.yml @@ -22,11 +22,12 @@ jobs: username: ${{ secrets.REGISTRY_USER }} password: ${{ secrets.REGISTRY_TOKEN }} - - name: Build image - run: | - docker build -t git.sinfonie.org/sinfonie/photo26:${{ steps.meta.outputs.TAG }} . - - - name: Push image + - name: Build and push image run: | + docker build \ + -t git.sinfonie.org/sinfonie/photo26:${{ steps.meta.outputs.TAG }} \ + -t git.sinfonie.org/sinfonie/photo26:latest \ + . docker push git.sinfonie.org/sinfonie/photo26:${{ steps.meta.outputs.TAG }} + docker push git.sinfonie.org/sinfonie/photo26:latest