diff --git a/.env.example b/.env.example deleted file mode 100644 index 930af5b..0000000 --- a/.env.example +++ /dev/null @@ -1,55 +0,0 @@ -# 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 - -# 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 - -# 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 deleted file mode 100644 index b5e40dd..0000000 --- a/.forgejo/workflows/docker.yml +++ /dev/null @@ -1,44 +0,0 @@ -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 deleted file mode 100644 index 850dd33..0000000 --- a/Dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -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/README.md b/README.md index 5878775..d8e6581 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,5 @@ # 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 @@ -17,6 +15,9 @@ 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.** @@ -24,14 +25,25 @@ run and to maintain. In production, we usually use `/var/www/photos/` as the `root` user. ```bash - git clone https://codeberg.org/krek0/photo21.git && cd photo21 + git clone https://gitlab.crans.org/bde/photo21.git && cd photo21 ``` 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).** @@ -68,6 +80,7 @@ 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/allauth_oauth/__init__.py b/allauth_note_kfet/__init__.py similarity index 69% rename from allauth_oauth/__init__.py rename to allauth_note_kfet/__init__.py index eae4948..6c9e378 100644 --- a/allauth_oauth/__init__.py +++ b/allauth_note_kfet/__init__.py @@ -1,5 +1,3 @@ # 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_oauth/provider.py b/allauth_note_kfet/provider.py similarity index 83% rename from allauth_oauth/provider.py rename to allauth_note_kfet/provider.py index 6a6430c..d2ce6a5 100644 --- a/allauth_oauth/provider.py +++ b/allauth_note_kfet/provider.py @@ -7,15 +7,15 @@ from allauth.socialaccount.providers.base import ProviderAccount from allauth.socialaccount.providers.oauth2.provider import OAuth2Provider -class OAuthAccount(ProviderAccount): +class NoteKfetAccount(ProviderAccount): def to_str(self): return self.account.extra_data.get("username") -class OAuthProvider(OAuth2Provider): - id = "oauth" - name = "OAuth" - account_class = OAuthAccount +class NoteKfetProvider(OAuth2Provider): + id = "notekfet" + name = "Note Kfet" + account_class = NoteKfetAccount def extract_uid(self, data): return str(data["username"]) @@ -39,4 +39,4 @@ class OAuthProvider(OAuth2Provider): return ret -provider_classes = [OAuthProvider] +provider_classes = [NoteKfetProvider] diff --git a/allauth_oauth/urls.py b/allauth_note_kfet/urls.py similarity index 70% rename from allauth_oauth/urls.py rename to allauth_note_kfet/urls.py index 190eac3..e1bf561 100644 --- a/allauth_oauth/urls.py +++ b/allauth_note_kfet/urls.py @@ -4,6 +4,6 @@ from allauth.socialaccount.providers.oauth2.urls import default_urlpatterns -from .provider import OAuthProvider +from .provider import NoteKfetProvider -urlpatterns = default_urlpatterns(OAuthProvider) +urlpatterns = default_urlpatterns(NoteKfetProvider) diff --git a/allauth_oauth/views.py b/allauth_note_kfet/views.py similarity index 77% rename from allauth_oauth/views.py rename to allauth_note_kfet/views.py index 7a568a5..86a16ee 100644 --- a/allauth_oauth/views.py +++ b/allauth_note_kfet/views.py @@ -10,11 +10,11 @@ from allauth.socialaccount.providers.oauth2.views import ( OAuth2LoginView, ) -from .provider import OAuthProvider +from .provider import NoteKfetProvider -class OAuthAdapter(OAuth2Adapter): - provider_id = OAuthProvider.id +class NoteKfetOAuth2Adapter(OAuth2Adapter): + provider_id = NoteKfetProvider.id def complete_login(self, request, app, token, **kwargs): headers = { @@ -31,7 +31,7 @@ class OAuthAdapter(OAuth2Adapter): @property def domain(self): - return self.settings.get("DOMAIN", "") + return self.settings.get("DOMAIN", "note.crans.org") @property def access_token_url(self): @@ -46,5 +46,5 @@ class OAuthAdapter(OAuth2Adapter): return f"https://{self.domain}/api/me/" -oauth2_login = OAuth2LoginView.adapter_view(OAuthAdapter) -oauth2_callback = OAuth2CallbackView.adapter_view(OAuthAdapter) +oauth2_login = OAuth2LoginView.adapter_view(NoteKfetOAuth2Adapter) +oauth2_callback = OAuth2CallbackView.adapter_view(NoteKfetOAuth2Adapter) diff --git a/allauth_oauth/apps.py b/allauth_oauth/apps.py deleted file mode 100644 index 69b727a..0000000 --- a/allauth_oauth/apps.py +++ /dev/null @@ -1,12 +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 - -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 deleted file mode 100644 index d7f1cf6..0000000 --- a/allauth_oauth/signals.py +++ /dev/null @@ -1,29 +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 - -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 deleted file mode 100644 index 37554d0..0000000 --- a/entrypoint.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/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/forms.py b/photo21/forms.py index 253fab3..7c15e1e 100644 --- a/photo21/forms.py +++ b/photo21/forms.py @@ -13,7 +13,8 @@ class CustomSignupForm(SignupForm): # Add description on email field self.fields["email"].help_text = _( - "Please enter a valid email address ending with `@ens-rennes.fr`" + "Please enter a valid email address ending with `@crans.org` or " + "`@ens-paris-saclay.fr`." ) def clean_email(self): @@ -21,8 +22,10 @@ class CustomSignupForm(SignupForm): Check that the email address ends with a trusted domain. """ email = super().clean_email() - if not email.endswith("@ens-rennes.fr"): + if not email.endswith("@crans.org") and not email.endswith( + "@ens-paris-saclay.fr" + ): raise forms.ValidationError( - _("Must end with `@ens-rennes.fr`.") + _("Must end with `@crans.org` or `@ens-paris-saclay.fr`.") ) return email diff --git a/photo21/settings.py b/photo21/settings.py index ec542ea..7f86536 100644 --- a/photo21/settings.py +++ b/photo21/settings.py @@ -14,7 +14,6 @@ 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 _ @@ -26,12 +25,17 @@ 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 = config("SECRET_KEY") +SECRET_KEY = "CHANGE ME" # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = config("DEBUG", default=False, cast=bool) +DEBUG = False -ALLOWED_HOSTS = ["127.0.0.1", "localhost"] + config("EXTRA_HOSTS", default="", cast=Csv()) +ALLOWED_HOSTS = [ + "127.0.0.1", + "localhost", + "photos.crans.org", + "photos-dev.crans.org", +] INTERNAL_IPS = [ "127.0.0.1", @@ -39,16 +43,14 @@ INTERNAL_IPS = [ ] # Admins receive server errors, this is useful to be notified of potential bugs -# Format: "Name:email,Name2:email2" -ADMINS = [tuple(a.split(":")) for a in config("ADMINS", default="", cast=Csv()) if a] +ADMINS = [ + ("admin", "photos-admin@lists.crans.org"), +] # Use secure cookies in production 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 @@ -56,15 +58,6 @@ 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", @@ -78,20 +71,17 @@ 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 MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", - "whitenoise.middleware.WhiteNoiseMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", @@ -136,31 +126,12 @@ WSGI_APPLICATION = "photo21.wsgi.application" # Database # https://docs.djangoproject.com/en/2.2/ref/settings/#databases -_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"), - } +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": os.path.join(BASE_DIR, "db.sqlite3"), } -elif _db_engine == "sqlite": - DATABASES = { - "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": config("DB_PATH", default=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": { @@ -223,18 +194,6 @@ 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")] @@ -245,14 +204,9 @@ if DEBUG: EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" # Mail will be sent from this address -SERVER_EMAIL = config("SERVER_EMAIL", default="photos@crans.org") +SERVER_EMAIL = "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 = "/" @@ -272,23 +226,16 @@ 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 = config("EMAIL_VERIFICATION", default="mandatory") +ACCOUNT_EMAIL_VERIFICATION = "mandatory" ACCOUNT_AUTHENTICATION_METHOD = "username_email" # ACCOUNT_LOGIN_METHODS = {'username', 'email'} ACCOUNT_FORMS = {"signup": "photo21.forms.CustomSignupForm"} - -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, - }, - }, - } +SOCIALACCOUNT_PROVIDERS = { + "notekfet": { + # Fetch user profile + "SCOPE": ["1_1"], + }, +} # Use Bootstrap forms CRISPY_TEMPLATE_PACK = "bootstrap4" diff --git a/photo21/static/layout.css b/photo21/static/layout.css index 8e5568e..a0026d1 100644 --- a/photo21/static/layout.css +++ b/photo21/static/layout.css @@ -45,29 +45,6 @@ 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/photo21/storage.py b/photo21/storage.py deleted file mode 100644 index fa1b779..0000000 --- a/photo21/storage.py +++ /dev/null @@ -1,12 +0,0 @@ -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/templates/account/login.html b/photo21/templates/account/login.html index a2bc666..1e7723f 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 602ee57..eeca7fd 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 d4db262..b7fa2e9 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 Rennes or involving its students. + life of ENS Paris-Saclay or involving its students. {% endblocktrans %}

@@ -25,16 +25,21 @@ SPDX-License-Identifier: GPL-3.0-or-later {% endblocktrans %}

- {% if not perms.photologue.add_photo %} +

+ {% blocktrans trimmed %} + If you want a photo to be deleted, please let us know: + Abuse request + {% endblocktrans %} +

+ {% 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 %} @@ -47,15 +52,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/photo21/views.py b/photo21/views.py index 434be5b..8245c4f 100644 --- a/photo21/views.py +++ b/photo21/views.py @@ -2,29 +2,21 @@ # 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 FileResponse, Http404 +from django.http import HttpResponse from django.views.generic import ListView, View from photologue.models import Gallery -class MediaAccess(View): + +class MediaAccess(LoginRequiredMixin, View): def get(self, request, path): - 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' + 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' return response diff --git a/photologue/admin.py b/photologue/admin.py index f994402..d6337f6 100644 --- a/photologue/admin.py +++ b/photologue/admin.py @@ -3,7 +3,6 @@ # SPDX-License-Identifier: GPL-3.0-or-later from django.contrib import admin -from django.contrib.admin import EmptyFieldListFilter from django.utils.translation import gettext_lazy as _ from .models import Gallery, Photo, Tag @@ -11,7 +10,7 @@ from .models import Gallery, Photo, Tag class GalleryAdmin(admin.ModelAdmin): list_display = ("title", "date_start", "photo_count", "get_tags") - list_filter = ["date_start", "tags", ("public_token", EmptyFieldListFilter)] + list_filter = ["date_start", "tags"] date_hierarchy = "date_start" prepopulated_fields = {"slug": ("title",)} model = Gallery diff --git a/photologue/apps.py b/photologue/apps.py index 10f0a9b..a446591 100644 --- a/photologue/apps.py +++ b/photologue/apps.py @@ -3,25 +3,8 @@ # 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/migrations/0008_photo_dimensions.py b/photologue/migrations/0008_photo_dimensions.py deleted file mode 100644 index 033f144..0000000 --- a/photologue/migrations/0008_photo_dimensions.py +++ /dev/null @@ -1,45 +0,0 @@ -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/models.py b/photologue/models.py index 594ac2c..5f97cf7 100644 --- a/photologue/models.py +++ b/photologue/models.py @@ -224,14 +224,8 @@ class Gallery(models.Model): class ImageModel(models.Model): image = models.ImageField( - _("image"), - max_length=IMAGE_FIELD_MAX_LENGTH, - upload_to=get_storage_path, - width_field="image_width", - height_field="image_height", + _("image"), max_length=IMAGE_FIELD_MAX_LENGTH, upload_to=get_storage_path ) - image_width = models.PositiveIntegerField(editable=False, null=True) - image_height = models.PositiveIntegerField(editable=False, null=True) date_taken = models.DateTimeField( _("date taken"), null=True, @@ -486,7 +480,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) and self.image: + if self.date_taken is None or image_has_changed: # Attempt to get the date the photo was taken from the EXIF data. try: exif_date = self.exif(self.image.file).get( diff --git a/photologue/static/gallery_detail.js b/photologue/static/gallery_detail.js index 03c187a..b9fad3a 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/gallery_justified.js b/photologue/static/gallery_justified.js deleted file mode 100644 index 80dcc55..0000000 --- a/photologue/static/gallery_justified.js +++ /dev/null @@ -1,75 +0,0 @@ -// 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/static/lightgallery/plugins/admin/lg-admin.js b/photologue/static/lightgallery/plugins/admin/lg-admin.js index aa0214d..5b198c9 100644 --- a/photologue/static/lightgallery/plugins/admin/lg-admin.js +++ b/photologue/static/lightgallery/plugins/admin/lg-admin.js @@ -13,7 +13,6 @@ class lgAdmin { this.isStaff = document.querySelector('[name=is_staff]').value === "true"; this.userId = document.querySelector('[name=user_id]').value; this.canResolveCensorship = document.querySelector('[name=can_resolve_censorship]').value === "true"; - this.guestMode = document.querySelector('[name=guest_mode]').value === "true"; this.csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value; this.photoId = 0; return this; @@ -34,8 +33,7 @@ class lgAdmin { document.getElementById("lg-delete").addEventListener('click', this.onDelete.bind(this)); // Add button to report photo - this.core.$toolbar.append(`${deleteIcon}`); - document.getElementById("lg-report").style.display = 'none'; + this.core.$toolbar.append(`${reportIcon}`); document.getElementById("lg-report").addEventListener('click', this.onReport.bind(this)); // Add button to restore a censored photo @@ -55,7 +53,6 @@ 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'; } @@ -138,7 +135,7 @@ class lgAdmin { // Event called when user click on report button onReport(event) { event.preventDefault(); - if(confirm("Are you sure to ask removal for this photo?")) { + if(confirm("Are you sure to report 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 c435f8d..1744a05 100755 --- a/photologue/templates/photologue/gallery_detail.html +++ b/photologue/templates/photologue/gallery_detail.html @@ -20,13 +20,11 @@ SPDX-License-Identifier: GPL-3.0-or-later - - @@ -89,10 +87,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 %}
diff --git a/photologue/views.py b/photologue/views.py index a523fa1..c44b1f8 100644 --- a/photologue/views.py +++ b/photologue/views.py @@ -237,7 +237,6 @@ 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 56bdc65..f3dfcdb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,8 +5,3 @@ django-select2>=4.8 ExifRead>=2.1.2 Pillow>=6.0.0 django-debug-toolbar>=3.2.0 -python-decouple>=3.6 -whitenoise>=6.0 -psycopg2-binary>=2.9 -requests>=2.25 -gunicorn>=21.0 diff --git a/tox.ini b/tox.ini index 55bb7f6..cc05d77 100644 --- a/tox.ini +++ b/tox.ini @@ -27,7 +27,7 @@ deps = pep8-naming pyflakes commands = - flake8 allauth_oauth photo21 photologue + flake8 allauth_note_kfet photo21 photologue [flake8] ignore = W503, I100, I101