From 3efa2177161ccb782c8fea9464ebec3c93129b9e Mon Sep 17 00:00:00 2001 From: krek0 Date: Fri, 24 Apr 2026 21:42:23 +0200 Subject: [PATCH 01/10] Load settings from .env file. --- .env.example | 16 ++++++++++++++++ photo21/settings.py | 19 +++++++------------ photologue/models.py | 8 +++++++- requirements.txt | 1 + 4 files changed, 31 insertions(+), 13 deletions(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..145b31e --- /dev/null +++ b/.env.example @@ -0,0 +1,16 @@ +# Copy this file to .env and fill in the values +# .env is gitignored and must never be committed + +SECRET_KEY=change-me-to-a-long-random-string + +# Set to True only for development +DEBUG=False + +# Comma-separated list of additional allowed hosts (beyond 127.0.0.1 and localhost) +EXTRA_HOSTS=photos.crans.org,photos-dev.crans.org + +# Comma-separated list of admins in "Name:email" format +ADMINS=admin:photos-admin@lists.crans.org + +# Email address used as sender for server emails +SERVER_EMAIL=photos@crans.org diff --git a/photo21/settings.py b/photo21/settings.py index 7f86536..2436141 100644 --- a/photo21/settings.py +++ b/photo21/settings.py @@ -14,6 +14,7 @@ https://docs.djangoproject.com/en/2.2/ref/settings/ import os +from decouple import Csv, config from django.contrib.messages import constants as messages from django.utils.translation import gettext_lazy as _ @@ -25,17 +26,12 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = "CHANGE ME" +SECRET_KEY = config("SECRET_KEY") # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = False +DEBUG = config("DEBUG", default=False, cast=bool) -ALLOWED_HOSTS = [ - "127.0.0.1", - "localhost", - "photos.crans.org", - "photos-dev.crans.org", -] +ALLOWED_HOSTS = ["127.0.0.1", "localhost"] + config("EXTRA_HOSTS", default="", cast=Csv()) INTERNAL_IPS = [ "127.0.0.1", @@ -43,9 +39,8 @@ INTERNAL_IPS = [ ] # Admins receive server errors, this is useful to be notified of potential bugs -ADMINS = [ - ("admin", "photos-admin@lists.crans.org"), -] +# Format: "Name:email,Name2:email2" +ADMINS = [tuple(a.split(":")) for a in config("ADMINS", default="", cast=Csv()) if a] # Use secure cookies in production SESSION_COOKIE_SECURE = not DEBUG @@ -204,7 +199,7 @@ if DEBUG: EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" # Mail will be sent from this address -SERVER_EMAIL = "photos@crans.org" +SERVER_EMAIL = config("SERVER_EMAIL", default="photos@crans.org") DEFAULT_FROM_EMAIL = f"Serveur photos <{SERVER_EMAIL}>" EMAIL_SUBJECT_PREFIX = "[Serveur photos] " diff --git a/photologue/models.py b/photologue/models.py index 5f97cf7..3585e8c 100644 --- a/photologue/models.py +++ b/photologue/models.py @@ -224,8 +224,14 @@ class Gallery(models.Model): class ImageModel(models.Model): image = models.ImageField( - _("image"), max_length=IMAGE_FIELD_MAX_LENGTH, upload_to=get_storage_path + _("image"), + max_length=IMAGE_FIELD_MAX_LENGTH, + upload_to=get_storage_path, + width_field="image_width", + height_field="image_height", ) + image_width = models.PositiveIntegerField(editable=False, null=True) + image_height = models.PositiveIntegerField(editable=False, null=True) date_taken = models.DateTimeField( _("date taken"), null=True, diff --git a/requirements.txt b/requirements.txt index f3dfcdb..3f38e2d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ django-select2>=4.8 ExifRead>=2.1.2 Pillow>=6.0.0 django-debug-toolbar>=3.2.0 +python-decouple>=3.6 From 687445e414a50d016fa51253e0d932c189969e6e Mon Sep 17 00:00:00 2001 From: krek0 Date: Fri, 24 Apr 2026 21:43:25 +0200 Subject: [PATCH 02/10] 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 03/10] 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 04/10] Remove Nginx-specific static and media serving, and serve media through Django using WhiteNoise and FileResponse. --- README.md | 15 --------------- photo21/settings.py | 16 ++++++++++++++++ photo21/storage.py | 12 ++++++++++++ photo21/views.py | 24 ++++++++++++++++-------- photologue/views.py | 1 + requirements.txt | 1 + 6 files changed, 46 insertions(+), 23 deletions(-) create mode 100644 photo21/storage.py diff --git a/README.md b/README.md index 58d8206..5878775 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,6 @@ run and to maintain. ```bash sudo apt install git gettext python3-django python3-django-allauth python3-django-crispy-forms python3-docutils python3-exifread python3-pil - - # Only for production - sudo apt install nginx uwsgi uwsgi-plugin-python3 python3-certbot-nginx ``` 2. **Cloning.** @@ -33,19 +30,8 @@ run and to maintain. 3. **Configuration (production only).** ```bash - # Only for production sudo mkdir static media - sudo cp docs/maintenance.html static/maintenance.html - sudo chown www-data:www-data -R static media - sudo chmod g+rwx -R static media sudo chmod +x maintenance_tool.sh - sudo cp docs/uwsgi_photos.ini /etc/uwsgi/apps-available/uwsgi_photos.ini - sudo ln -s /etc/uwsgi/apps-available/uwsgi_photos.ini /etc/uwsgi/apps-enabled/ - sudo cp docs/nginx_photos_maintenance /etc/nginx/sites-available/photos.crans.org - sudo ln -s /etc/nginx/sites-available/photos.crans.org /etc/nginx/sites-enabled/ - sudo cp docs/letsencrypt_photos.crans.org /etc/letsencrypt/conf.d/photos.crans.org - sudo cp docs/renewal-hooks_post_nginx /etc/letsencrypt/renewal-hooks/post/nginx - sudo certbot --config /etc/letsencrypt/conf.d/photos.crans.org.ini certonly ``` 4. **Database (production only).** @@ -82,7 +68,6 @@ run and to maintain. 6. *Enjoy \o/* - In production, the NGINX site should now work. In development, you can launch the development server using: ```bash diff --git a/photo21/settings.py b/photo21/settings.py index 2436141..88390ae 100644 --- a/photo21/settings.py +++ b/photo21/settings.py @@ -46,6 +46,9 @@ ADMINS = [tuple(a.split(":")) for a in config("ADMINS", default="", cast=Csv()) SESSION_COOKIE_SECURE = not DEBUG CSRF_COOKIE_SECURE = not DEBUG +# Trust Caddy's forwarded proto header +SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') + # Remember HTTPS for 1 year SECURE_HSTS_SECONDS = 31536000 SECURE_HSTS_INCLUDE_SUBDOMAINS = True @@ -77,6 +80,7 @@ if DEBUG: MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", + "whitenoise.middleware.WhiteNoiseMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", @@ -189,6 +193,18 @@ STATIC_ROOT = os.path.join(BASE_DIR, "static/") MEDIA_ROOT = os.path.join(BASE_DIR, "media") MEDIA_URL = "/media/" +STORAGES = { + "default": { + "BACKEND": "django.core.files.storage.FileSystemStorage", + }, + "staticfiles": { + "BACKEND": "photo21.storage.CompressedManifestStorage", + }, +} + + +WHITENOISE_MANIFEST_STRICT = False + LOCALE_PATHS = [os.path.join(BASE_DIR, "photo21/locale")] FIXTURE_DIRS = [os.path.join(BASE_DIR, "photo21/fixtures")] diff --git a/photo21/storage.py b/photo21/storage.py new file mode 100644 index 0000000..fa1b779 --- /dev/null +++ b/photo21/storage.py @@ -0,0 +1,12 @@ +from whitenoise.storage import CompressedManifestStaticFilesStorage + + +class CompressedManifestStorage(CompressedManifestStaticFilesStorage): + """Like CompressedManifestStaticFilesStorage but silently skips missing + referenced files (e.g. source maps not included in the package).""" + + def hashed_name(self, name, content=None, filename=None): + try: + return super().hashed_name(name, content, filename) + except ValueError: + return name diff --git a/photo21/views.py b/photo21/views.py index 8245c4f..434be5b 100644 --- a/photo21/views.py +++ b/photo21/views.py @@ -2,21 +2,29 @@ # Copyright (C) 2021-2022 Amicale des élèves de l'ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later +import os + +from django.conf import settings from django.contrib.auth import get_user_model from django.contrib.auth.mixins import LoginRequiredMixin -from django.http import HttpResponse +from django.http import FileResponse, Http404 from django.views.generic import ListView, View from photologue.models import Gallery - -class MediaAccess(LoginRequiredMixin, View): +class MediaAccess(View): def get(self, request, path): - response = HttpResponse() - # Content-type will be detected by nginx - del response["Content-Type"] - response["X-Accel-Redirect"] = "/protected/media/" + path - response["Cache-Control"] = 'max-age=2678400' + if not request.user.is_authenticated and not request.session.get('public_gallery_access'): + from django.contrib.auth.views import redirect_to_login + return redirect_to_login(request.get_full_path()) + media_root = os.path.realpath(settings.MEDIA_ROOT) + file_path = os.path.realpath(os.path.join(media_root, path)) + if not file_path.startswith(media_root + os.sep): + raise Http404 + if not os.path.isfile(file_path): + raise Http404 + response = FileResponse(open(file_path, 'rb')) + response['Cache-Control'] = 'max-age=2678400' return response diff --git a/photologue/views.py b/photologue/views.py index c44b1f8..a523fa1 100644 --- a/photologue/views.py +++ b/photologue/views.py @@ -237,6 +237,7 @@ class GalleryPublicView(DetailView): if request.user.is_authenticated: gallery = self.get_object() return redirect("photologue:pl-gallery", slug=gallery.slug) + request.session['public_gallery_access'] = True request.guest_mode = True return super().get(request, *args, **kwargs) diff --git a/requirements.txt b/requirements.txt index 3f38e2d..31eeff0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,4 @@ ExifRead>=2.1.2 Pillow>=6.0.0 django-debug-toolbar>=3.2.0 python-decouple>=3.6 +whitenoise>=6.0 From 1a5f1d5e81b9b7f51f09f0567fdc471920ef4970 Mon Sep 17 00:00:00 2001 From: krek0 Date: Fri, 24 Apr 2026 23:26:09 +0200 Subject: [PATCH 05/10] Enable Write-Ahead Logging (WAL) for SQLite to improve performance when using it. --- photo21/settings.py | 3 +++ photologue/apps.py | 17 +++++++++++++++++ photologue/models.py | 2 +- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/photo21/settings.py b/photo21/settings.py index 88390ae..fb36c54 100644 --- a/photo21/settings.py +++ b/photo21/settings.py @@ -129,6 +129,9 @@ DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", "NAME": os.path.join(BASE_DIR, "db.sqlite3"), + "OPTIONS": { + "timeout": 10, + }, } } diff --git a/photologue/apps.py b/photologue/apps.py index a446591..10f0a9b 100644 --- a/photologue/apps.py +++ b/photologue/apps.py @@ -3,8 +3,25 @@ # SPDX-License-Identifier: GPL-3.0-or-later from django.apps import AppConfig +from django.db.backends.signals import connection_created class PhotologueConfig(AppConfig): default_auto_field = "django.db.models.AutoField" name = "photologue" + + def ready(self): + from django.db import connection + + def enable_sqlite_wal(sender, connection, **kwargs): + if connection.vendor == "sqlite": + cursor = connection.cursor() + cursor.execute("PRAGMA journal_mode=WAL;") + cursor.execute("PRAGMA synchronous=OFF;") + cursor.execute("PRAGMA journal_size_limit=67108864;") + cursor.execute("PRAGMA wal_autocheckpoint=1000;") + cursor.execute("PRAGMA cache_size=-65536;") + cursor.execute("PRAGMA temp_store=MEMORY;") + cursor.execute("PRAGMA mmap_size=268435456;") + + connection_created.connect(enable_sqlite_wal) diff --git a/photologue/models.py b/photologue/models.py index 3585e8c..594ac2c 100644 --- a/photologue/models.py +++ b/photologue/models.py @@ -486,7 +486,7 @@ class ImageModel(models.Model): self._old_image.storage.delete( self._old_image.name ) # Delete (old) base image. - if self.date_taken is None or image_has_changed: + if (self.date_taken is None or image_has_changed) and self.image: # Attempt to get the date the photo was taken from the EXIF data. try: exif_date = self.exif(self.image.file).get( From 74609215e0dd9d256e8566751f23c9e7c71ff0f4 Mon Sep 17 00:00:00 2001 From: krek0 Date: Sat, 25 Apr 2026 01:09:01 +0200 Subject: [PATCH 06/10] Add public link filter to Gallery admin --- photologue/admin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/photologue/admin.py b/photologue/admin.py index d6337f6..f994402 100644 --- a/photologue/admin.py +++ b/photologue/admin.py @@ -3,6 +3,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from django.contrib import admin +from django.contrib.admin import EmptyFieldListFilter from django.utils.translation import gettext_lazy as _ from .models import Gallery, Photo, Tag @@ -10,7 +11,7 @@ from .models import Gallery, Photo, Tag class GalleryAdmin(admin.ModelAdmin): list_display = ("title", "date_start", "photo_count", "get_tags") - list_filter = ["date_start", "tags"] + list_filter = ["date_start", "tags", ("public_token", EmptyFieldListFilter)] date_hierarchy = "date_start" prepopulated_fields = {"slug": ("title",)} model = Gallery From 3a73bb88876edabc9bb694f1cd471d91d54f6aac Mon Sep 17 00:00:00 2001 From: krek0 Date: Sat, 25 Apr 2026 12:12:30 +0200 Subject: [PATCH 07/10] Fix thumbnail bar render --- photologue/templates/photologue/gallery_detail.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/photologue/templates/photologue/gallery_detail.html b/photologue/templates/photologue/gallery_detail.html index ea5c671..c435f8d 100755 --- a/photologue/templates/photologue/gallery_detail.html +++ b/photologue/templates/photologue/gallery_detail.html @@ -92,7 +92,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% 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 08/10] 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 09/10] 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 8dd3c8701cdbe3c7015a72b4454f27f584e79b15 Mon Sep 17 00:00:00 2001 From: krek0 Date: Sat, 2 May 2026 14:31:39 +0200 Subject: [PATCH 10/10] Add Docker support with Dockerfile and entrypoint --- .env.example | 3 +++ .forgejo/workflows/docker.yml | 44 +++++++++++++++++++++++++++++++++++ Dockerfile | 22 ++++++++++++++++++ allauth_oauth/apps.py | 12 ++++++++++ allauth_oauth/signals.py | 29 +++++++++++++++++++++++ entrypoint.sh | 5 ++++ photo21/settings.py | 2 +- requirements.txt | 3 ++- 8 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 .forgejo/workflows/docker.yml create mode 100644 Dockerfile create mode 100644 allauth_oauth/apps.py create mode 100644 allauth_oauth/signals.py create mode 100644 entrypoint.sh 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..b5e40dd --- /dev/null +++ b/.forgejo/workflows/docker.yml @@ -0,0 +1,44 @@ +name: Docker + +on: + push: + branches: + - master + tags: + - 'v*' + +jobs: + build: + runs-on: docker + steps: + - uses: actions/checkout@v3 + + - name: Log in to Codeberg registry + if: startsWith(github.ref, 'refs/tags/') + uses: docker/login-action@v3 + with: + registry: codeberg.org + username: ${{ secrets.REGISTRY_USER }} + password: ${{ secrets.REGISTRY_TOKEN }} + + - name: Extract version tag + if: startsWith(github.ref, 'refs/tags/') + id: meta + run: echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT + + - name: Build (verify only, no push) + if: github.ref == 'refs/heads/master' + uses: docker/build-push-action@v5 + with: + context: . + push: false + + - name: Build and push (tagged release) + if: startsWith(github.ref, 'refs/tags/') + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: | + codeberg.org/${{ github.repository }}:${{ steps.meta.outputs.tag }} + codeberg.org/${{ github.repository }}:latest diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..850dd33 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +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 + +# Collect static files at build time (uses a dummy key, no DB needed) +RUN SECRET_KEY=build-time-placeholder DB_ENGINE=sqlite python manage.py collectstatic --noinput + +EXPOSE 8000 + +RUN chmod +x entrypoint.sh +ENTRYPOINT ["./entrypoint.sh"] diff --git a/allauth_oauth/apps.py b/allauth_oauth/apps.py new file mode 100644 index 0000000..69b727a --- /dev/null +++ b/allauth_oauth/apps.py @@ -0,0 +1,12 @@ +# 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 + +from django.apps import AppConfig + + +class AllauthOAuthConfig(AppConfig): + name = "allauth_oauth" + + def ready(self): + import allauth_oauth.signals # noqa: F401 diff --git a/allauth_oauth/signals.py b/allauth_oauth/signals.py new file mode 100644 index 0000000..d7f1cf6 --- /dev/null +++ b/allauth_oauth/signals.py @@ -0,0 +1,29 @@ +# 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 + +from allauth.socialaccount.signals import pre_social_login +from django.dispatch import receiver + + +@receiver(pre_social_login) +def sync_user_fields(sender, request, sociallogin, **kwargs): + if not sociallogin.is_existing: + return + + user = sociallogin.user + data = sociallogin.account.extra_data + changed = False + + email = data.get("email") + if email and user.email != email: + user.email = email + changed = True + + username = data.get("username") + if username and user.username != username: + user.username = username + changed = True + + if changed: + user.save() diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..37554d0 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,5 @@ +#!/bin/sh +set -e + +python manage.py migrate --noinput +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/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