Compare commits
No commits in common. "8dd3c8701cdbe3c7015a72b4454f27f584e79b15" and "f22174022882a8036131568dd42acc43a314598d" have entirely different histories.
8dd3c8701c
...
f221740228
30 changed files with 123 additions and 522 deletions
55
.env.example
55
.env.example
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
22
Dockerfile
22
Dockerfile
|
|
@ -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"]
|
|
||||||
19
README.md
19
README.md
|
|
@ -1,7 +1,5 @@
|
||||||
# Photo server 2021-2023
|
# Photo server 2021-2023
|
||||||
|
|
||||||
This project is a fork of [Photo21](https://gitlab.crans.org/bde/photo21/) developped at ENS Paris-Saclay.
|
|
||||||
|
|
||||||
[](https://www.gnu.org/licenses/gpl-3.0.txt)
|
[](https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
This is the source code for the webserver hosting pictures from the
|
This is the source code for the webserver hosting pictures from the
|
||||||
|
|
@ -17,6 +15,9 @@ run and to maintain.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo apt install git gettext python3-django python3-django-allauth python3-django-crispy-forms python3-docutils python3-exifread python3-pil
|
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.**
|
2. **Cloning.**
|
||||||
|
|
@ -24,14 +25,25 @@ run and to maintain.
|
||||||
In production, we usually use `/var/www/photos/` as the `root` user.
|
In production, we usually use `/var/www/photos/` as the `root` user.
|
||||||
|
|
||||||
```bash
|
```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).**
|
3. **Configuration (production only).**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Only for production
|
||||||
sudo mkdir static media
|
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 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).**
|
4. **Database (production only).**
|
||||||
|
|
@ -68,6 +80,7 @@ run and to maintain.
|
||||||
|
|
||||||
6. *Enjoy \o/*
|
6. *Enjoy \o/*
|
||||||
|
|
||||||
|
In production, the NGINX site should now work.
|
||||||
In development, you can launch the development server using:
|
In development, you can launch the development server using:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
# This file is part of photo21
|
# This file is part of photo21
|
||||||
# Copyright (C) 2022 Amicale des élèves de l'ENS Paris-Saclay
|
# Copyright (C) 2022 Amicale des élèves de l'ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
default_app_config = "allauth_oauth.apps.AllauthOAuthConfig"
|
|
||||||
|
|
@ -7,15 +7,15 @@ from allauth.socialaccount.providers.base import ProviderAccount
|
||||||
from allauth.socialaccount.providers.oauth2.provider import OAuth2Provider
|
from allauth.socialaccount.providers.oauth2.provider import OAuth2Provider
|
||||||
|
|
||||||
|
|
||||||
class OAuthAccount(ProviderAccount):
|
class NoteKfetAccount(ProviderAccount):
|
||||||
def to_str(self):
|
def to_str(self):
|
||||||
return self.account.extra_data.get("username")
|
return self.account.extra_data.get("username")
|
||||||
|
|
||||||
|
|
||||||
class OAuthProvider(OAuth2Provider):
|
class NoteKfetProvider(OAuth2Provider):
|
||||||
id = "oauth"
|
id = "notekfet"
|
||||||
name = "OAuth"
|
name = "Note Kfet"
|
||||||
account_class = OAuthAccount
|
account_class = NoteKfetAccount
|
||||||
|
|
||||||
def extract_uid(self, data):
|
def extract_uid(self, data):
|
||||||
return str(data["username"])
|
return str(data["username"])
|
||||||
|
|
@ -39,4 +39,4 @@ class OAuthProvider(OAuth2Provider):
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
provider_classes = [OAuthProvider]
|
provider_classes = [NoteKfetProvider]
|
||||||
|
|
@ -4,6 +4,6 @@
|
||||||
|
|
||||||
from allauth.socialaccount.providers.oauth2.urls import default_urlpatterns
|
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)
|
||||||
|
|
@ -10,11 +10,11 @@ from allauth.socialaccount.providers.oauth2.views import (
|
||||||
OAuth2LoginView,
|
OAuth2LoginView,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .provider import OAuthProvider
|
from .provider import NoteKfetProvider
|
||||||
|
|
||||||
|
|
||||||
class OAuthAdapter(OAuth2Adapter):
|
class NoteKfetOAuth2Adapter(OAuth2Adapter):
|
||||||
provider_id = OAuthProvider.id
|
provider_id = NoteKfetProvider.id
|
||||||
|
|
||||||
def complete_login(self, request, app, token, **kwargs):
|
def complete_login(self, request, app, token, **kwargs):
|
||||||
headers = {
|
headers = {
|
||||||
|
|
@ -31,7 +31,7 @@ class OAuthAdapter(OAuth2Adapter):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def domain(self):
|
def domain(self):
|
||||||
return self.settings.get("DOMAIN", "")
|
return self.settings.get("DOMAIN", "note.crans.org")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def access_token_url(self):
|
def access_token_url(self):
|
||||||
|
|
@ -46,5 +46,5 @@ class OAuthAdapter(OAuth2Adapter):
|
||||||
return f"https://{self.domain}/api/me/"
|
return f"https://{self.domain}/api/me/"
|
||||||
|
|
||||||
|
|
||||||
oauth2_login = OAuth2LoginView.adapter_view(OAuthAdapter)
|
oauth2_login = OAuth2LoginView.adapter_view(NoteKfetOAuth2Adapter)
|
||||||
oauth2_callback = OAuth2CallbackView.adapter_view(OAuthAdapter)
|
oauth2_callback = OAuth2CallbackView.adapter_view(NoteKfetOAuth2Adapter)
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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()
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -13,7 +13,8 @@ class CustomSignupForm(SignupForm):
|
||||||
|
|
||||||
# Add description on email field
|
# Add description on email field
|
||||||
self.fields["email"].help_text = _(
|
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):
|
def clean_email(self):
|
||||||
|
|
@ -21,8 +22,10 @@ class CustomSignupForm(SignupForm):
|
||||||
Check that the email address ends with a trusted domain.
|
Check that the email address ends with a trusted domain.
|
||||||
"""
|
"""
|
||||||
email = super().clean_email()
|
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(
|
raise forms.ValidationError(
|
||||||
_("Must end with `@ens-rennes.fr`.")
|
_("Must end with `@crans.org` or `@ens-paris-saclay.fr`.")
|
||||||
)
|
)
|
||||||
return email
|
return email
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ https://docs.djangoproject.com/en/2.2/ref/settings/
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from decouple import Csv, config
|
|
||||||
from django.contrib.messages import constants as messages
|
from django.contrib.messages import constants as messages
|
||||||
from django.utils.translation import gettext_lazy as _
|
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/
|
# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
# 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!
|
# 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 = [
|
INTERNAL_IPS = [
|
||||||
"127.0.0.1",
|
"127.0.0.1",
|
||||||
|
|
@ -39,16 +43,14 @@ INTERNAL_IPS = [
|
||||||
]
|
]
|
||||||
|
|
||||||
# Admins receive server errors, this is useful to be notified of potential bugs
|
# Admins receive server errors, this is useful to be notified of potential bugs
|
||||||
# Format: "Name:email,Name2:email2"
|
ADMINS = [
|
||||||
ADMINS = [tuple(a.split(":")) for a in config("ADMINS", default="", cast=Csv()) if a]
|
("admin", "photos-admin@lists.crans.org"),
|
||||||
|
]
|
||||||
|
|
||||||
# Use secure cookies in production
|
# Use secure cookies in production
|
||||||
SESSION_COOKIE_SECURE = not DEBUG
|
SESSION_COOKIE_SECURE = not DEBUG
|
||||||
CSRF_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
|
# Remember HTTPS for 1 year
|
||||||
SECURE_HSTS_SECONDS = 31536000
|
SECURE_HSTS_SECONDS = 31536000
|
||||||
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
|
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
|
||||||
|
|
@ -56,15 +58,6 @@ SECURE_HSTS_PRELOAD = True
|
||||||
|
|
||||||
# Application definition
|
# 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 = [
|
INSTALLED_APPS = [
|
||||||
"django.contrib.admin",
|
"django.contrib.admin",
|
||||||
"django.contrib.admindocs",
|
"django.contrib.admindocs",
|
||||||
|
|
@ -78,20 +71,17 @@ INSTALLED_APPS = [
|
||||||
"allauth",
|
"allauth",
|
||||||
"allauth.account",
|
"allauth.account",
|
||||||
"allauth.socialaccount",
|
"allauth.socialaccount",
|
||||||
|
"allauth_note_kfet",
|
||||||
"crispy_forms",
|
"crispy_forms",
|
||||||
"photologue",
|
"photologue",
|
||||||
"photo21",
|
"photo21",
|
||||||
]
|
]
|
||||||
|
|
||||||
if OAUTH_ENABLED:
|
|
||||||
INSTALLED_APPS += ["allauth_oauth"]
|
|
||||||
|
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
INSTALLED_APPS += ["debug_toolbar",] # For debug and optimisations
|
INSTALLED_APPS += ["debug_toolbar",] # For debug and optimisations
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
"django.middleware.security.SecurityMiddleware",
|
"django.middleware.security.SecurityMiddleware",
|
||||||
"whitenoise.middleware.WhiteNoiseMiddleware",
|
|
||||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
"django.middleware.common.CommonMiddleware",
|
"django.middleware.common.CommonMiddleware",
|
||||||
"django.middleware.csrf.CsrfViewMiddleware",
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
|
|
@ -136,31 +126,12 @@ WSGI_APPLICATION = "photo21.wsgi.application"
|
||||||
# Database
|
# Database
|
||||||
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases
|
# 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"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
elif _db_engine == "sqlite":
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
"default": {
|
"default": {
|
||||||
"ENGINE": "django.db.backends.sqlite3",
|
"ENGINE": "django.db.backends.sqlite3",
|
||||||
"NAME": config("DB_PATH", default=os.path.join(BASE_DIR, "db.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 = {
|
CACHES = {
|
||||||
"default": {
|
"default": {
|
||||||
|
|
@ -223,18 +194,6 @@ STATIC_ROOT = os.path.join(BASE_DIR, "static/")
|
||||||
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
|
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
|
||||||
MEDIA_URL = "/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")]
|
LOCALE_PATHS = [os.path.join(BASE_DIR, "photo21/locale")]
|
||||||
|
|
||||||
FIXTURE_DIRS = [os.path.join(BASE_DIR, "photo21/fixtures")]
|
FIXTURE_DIRS = [os.path.join(BASE_DIR, "photo21/fixtures")]
|
||||||
|
|
@ -245,14 +204,9 @@ if DEBUG:
|
||||||
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
||||||
|
|
||||||
# Mail will be sent from this address
|
# 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}>"
|
DEFAULT_FROM_EMAIL = f"Serveur photos <{SERVER_EMAIL}>"
|
||||||
EMAIL_SUBJECT_PREFIX = "[Serveur photos] "
|
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
|
# After login redirect user to transfer page
|
||||||
LOGIN_REDIRECT_URL = "/"
|
LOGIN_REDIRECT_URL = "/"
|
||||||
|
|
@ -272,21 +226,14 @@ MESSAGE_TAGS = {
|
||||||
# Allauth configuration ## For the django =< 5.0
|
# Allauth configuration ## For the django =< 5.0
|
||||||
ACCOUNT_EMAIL_REQUIRED = True
|
ACCOUNT_EMAIL_REQUIRED = True
|
||||||
# ACCOUNT_SIGNUP_FIELDS = ['email*', 'username*', 'password1*', 'password2*'] ## For the django =< 5.0
|
# 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_AUTHENTICATION_METHOD = "username_email"
|
||||||
# ACCOUNT_LOGIN_METHODS = {'username', 'email'}
|
# ACCOUNT_LOGIN_METHODS = {'username', 'email'}
|
||||||
ACCOUNT_FORMS = {"signup": "photo21.forms.CustomSignupForm"}
|
ACCOUNT_FORMS = {"signup": "photo21.forms.CustomSignupForm"}
|
||||||
|
|
||||||
if OAUTH_ENABLED:
|
|
||||||
SOCIALACCOUNT_ONLY = OAUTH_ONLY
|
|
||||||
SOCIALACCOUNT_PROVIDERS = {
|
SOCIALACCOUNT_PROVIDERS = {
|
||||||
"oauth": {
|
"notekfet": {
|
||||||
"SCOPE": OAUTH_SCOPE,
|
# Fetch user profile
|
||||||
"DOMAIN": OAUTH_SERVER_URL,
|
"SCOPE": ["1_1"],
|
||||||
"APP": {
|
|
||||||
"client_id": OAUTH_CLIENT_ID,
|
|
||||||
"secret": OAUTH_CLIENT_SECRET,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,29 +45,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
background-color: rgba(163, 163, 163, 0.274);
|
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 */
|
/* Language selector */
|
||||||
.lang-select {
|
.lang-select {
|
||||||
border: none;
|
border: none;
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -39,5 +39,5 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
<a class="link-secondary" href="{% url 'account_reset_password' %}">{% trans "Forgot Password?" %}</a>
|
<a class="link-secondary" href="{% url 'account_reset_password' %}">{% trans "Forgot Password?" %}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- <p class="small text-center mt-1">{% trans "If any problem, please contact the server owners at" %} <code>photos[at]crans.org</code>.</p> -->
|
<p class="small text-center mt-1">{% trans "If any problem, please contact the server owners at" %} <code>photos[at]crans.org</code>.</p>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src 'self' data:; script-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self'">
|
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src 'self' data:; script-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self'">
|
||||||
<meta http-equiv="Referrer-Policy" content="no-referrer">
|
<meta http-equiv="Referrer-Policy" content="no-referrer">
|
||||||
<title>{% block title %}{{ title }}{% endblock title %} - {{ request.site.name }}</title>
|
<title>{% block title %}{{ title }}{% endblock title %} - {{ request.site.name }}</title>
|
||||||
<meta name="description" content="{% trans "The ENS Rennes pictures server." %}">
|
<meta name="description" content="{% trans "The ENS Paris-Saclay pictures server." %}">
|
||||||
<script src="{% static "theme.js" %}"></script>
|
<script src="{% static "theme.js" %}"></script>
|
||||||
<link rel="stylesheet" href="{% static "bootstrap5/css/bootstrap.min.css" %}">
|
<link rel="stylesheet" href="{% static "bootstrap5/css/bootstrap.min.css" %}">
|
||||||
<link rel="stylesheet" href="{% static "layout.css" %}">
|
<link rel="stylesheet" href="{% static "layout.css" %}">
|
||||||
|
|
@ -118,16 +118,16 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
{% if request.user.is_authenticated %}
|
{% if request.user.is_authenticated %}
|
||||||
{% trans "Connected as" %} <code>{{ request.user.username }}</code> ·
|
{% trans "Connected as" %} <code>{{ request.user.username }}</code> ·
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a class="text-reset" href="https://github.com/krek0/photo21">{% trans "Source code" %}</a> ·
|
<a class="text-reset" href="https://gitlab.crans.org/bde/photo21/">{% trans "Source code" %}</a> ·
|
||||||
<!-- <select title="language" name="language" class="lang-select"> -->
|
<select title="language" name="language" class="lang-select">
|
||||||
<!-- {% get_current_language as LANGUAGE_CODE %} -->
|
{% get_current_language as LANGUAGE_CODE %}
|
||||||
<!-- {% get_available_languages as LANGUAGES %} -->
|
{% get_available_languages as LANGUAGES %}
|
||||||
<!-- {% for lang_code, lang_name in LANGUAGES %} -->
|
{% for lang_code, lang_name in LANGUAGES %}
|
||||||
<!-- <option value="{{ lang_code }}" {% if lang_code == LANGUAGE_CODE %}selected{% endif %}> -->
|
<option value="{{ lang_code }}" {% if lang_code == LANGUAGE_CODE %}selected{% endif %}>
|
||||||
<!-- {{ lang_name }} ({{ lang_code }}) -->
|
{{ lang_name }} ({{ lang_code }})
|
||||||
<!-- </option> -->
|
</option>
|
||||||
<!-- {% endfor %} -->
|
{% endfor %}
|
||||||
<!-- </select> -->
|
</select>
|
||||||
<noscript><input type="submit"></noscript>
|
<noscript><input type="submit"></noscript>
|
||||||
</p>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
<p>
|
<p>
|
||||||
{% blocktrans trimmed %}
|
{% blocktrans trimmed %}
|
||||||
This website aims to collect the pictures and movies taken in the student
|
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 %}
|
{% endblocktrans %}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
|
|
@ -25,16 +25,21 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
If you want a photo to be deleted, please let us know:
|
||||||
|
<a href="mailto:photos@crans.org?subject=[ABUS] Nouvelle requête" class="btn btn-secondary btn-sm">Abuse request</a>
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
{% if not perms.photologue.add_photo %}
|
{% if not perms.photologue.add_photo %}
|
||||||
<p>
|
<p>
|
||||||
{% blocktrans trimmed %}
|
{% blocktrans trimmed %}
|
||||||
If you want to obtain the right to upload pictures, please let us know:
|
If you want to obtain the right to upload pictures, please let us know:
|
||||||
<a href="mailto:sinfonie@ens-rennes.fr?subject=[Photographe] Demande de droits photographe" class="btn btn-secondary btn-sm">Become a photograph</a>
|
<a href="mailto:photos@crans.org?subject=[Photographe] Demande de droits photographe" class="btn btn-secondary btn-sm">Become a photograph</a>
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
<h3>{% trans "Last galleries" %}</h3>
|
<h3>{% trans "Last galleries" %}</h3>
|
||||||
<div class="row mb-2">
|
<div class="row mb-2">
|
||||||
{% for gallery in object_list %}
|
{% for gallery in object_list %}
|
||||||
|
|
@ -47,15 +52,15 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
<h3 class="mt-4">{% trans "Behind the scene" %}</h3>
|
<h3 class="mt-4">{% trans "Behind the scene" %}</h3>
|
||||||
<p>
|
<p>
|
||||||
{% blocktrans trimmed %}
|
{% blocktrans trimmed %}
|
||||||
This project if a fork of <a href="https://gitlab.crans.org/bde/photo21/">Photo21</a>.
|
|
||||||
Because we value your privacy, we do not sell the data on this site,
|
Because we value your privacy, we do not sell the data on this site,
|
||||||
unlike many free online platforms.
|
unlike many free online platforms.
|
||||||
<!-- The dedicated server running this website is kindly hosted by -->
|
The dedicated server running this website is kindly hosted by the
|
||||||
<!-- <a href="https://sinfonie.org/">Sinfonie</a> at the ENS Rennes. -->
|
<a href="https://www.crans.org/">Crans</a> at the ENS Paris-Saclay
|
||||||
<!-- Current active administrators are: -->
|
basement.
|
||||||
<!-- {% endblocktrans %} -->
|
It is not managed by the Crans. Current active administrators are:
|
||||||
<!-- {% for user in superusers %} <code>{{ user.username }}</code>{% endfor %}. -->
|
{% endblocktrans %}
|
||||||
<!-- {% trans "They should be contacted at" %} -->
|
{% for user in superusers %} <code>{{ user.username }}</code>{% endfor %}.
|
||||||
<!-- <a href="mailto:photos@crans.org">photos@crans.org</a>. -->
|
{% trans "They should be contacted at" %}
|
||||||
|
<a href="mailto:photos@crans.org">photos@crans.org</a>.
|
||||||
</p>
|
</p>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -2,29 +2,21 @@
|
||||||
# Copyright (C) 2021-2022 Amicale des élèves de l'ENS Paris-Saclay
|
# Copyright (C) 2021-2022 Amicale des élèves de l'ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# 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 import get_user_model
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
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 django.views.generic import ListView, View
|
||||||
from photologue.models import Gallery
|
from photologue.models import Gallery
|
||||||
|
|
||||||
|
|
||||||
class MediaAccess(View):
|
|
||||||
|
class MediaAccess(LoginRequiredMixin, View):
|
||||||
def get(self, request, path):
|
def get(self, request, path):
|
||||||
if not request.user.is_authenticated and not request.session.get('public_gallery_access'):
|
response = HttpResponse()
|
||||||
from django.contrib.auth.views import redirect_to_login
|
# Content-type will be detected by nginx
|
||||||
return redirect_to_login(request.get_full_path())
|
del response["Content-Type"]
|
||||||
media_root = os.path.realpath(settings.MEDIA_ROOT)
|
response["X-Accel-Redirect"] = "/protected/media/" + path
|
||||||
file_path = os.path.realpath(os.path.join(media_root, path))
|
response["Cache-Control"] = 'max-age=2678400'
|
||||||
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
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.admin import EmptyFieldListFilter
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from .models import Gallery, Photo, Tag
|
from .models import Gallery, Photo, Tag
|
||||||
|
|
@ -11,7 +10,7 @@ from .models import Gallery, Photo, Tag
|
||||||
|
|
||||||
class GalleryAdmin(admin.ModelAdmin):
|
class GalleryAdmin(admin.ModelAdmin):
|
||||||
list_display = ("title", "date_start", "photo_count", "get_tags")
|
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"
|
date_hierarchy = "date_start"
|
||||||
prepopulated_fields = {"slug": ("title",)}
|
prepopulated_fields = {"slug": ("title",)}
|
||||||
model = Gallery
|
model = Gallery
|
||||||
|
|
|
||||||
|
|
@ -3,25 +3,8 @@
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.db.backends.signals import connection_created
|
|
||||||
|
|
||||||
|
|
||||||
class PhotologueConfig(AppConfig):
|
class PhotologueConfig(AppConfig):
|
||||||
default_auto_field = "django.db.models.AutoField"
|
default_auto_field = "django.db.models.AutoField"
|
||||||
name = "photologue"
|
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)
|
|
||||||
|
|
|
||||||
|
|
@ -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',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
@ -224,14 +224,8 @@ class Gallery(models.Model):
|
||||||
|
|
||||||
class ImageModel(models.Model):
|
class ImageModel(models.Model):
|
||||||
image = models.ImageField(
|
image = models.ImageField(
|
||||||
_("image"),
|
_("image"), max_length=IMAGE_FIELD_MAX_LENGTH, upload_to=get_storage_path
|
||||||
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 = models.DateTimeField(
|
||||||
_("date taken"),
|
_("date taken"),
|
||||||
null=True,
|
null=True,
|
||||||
|
|
@ -486,7 +480,7 @@ class ImageModel(models.Model):
|
||||||
self._old_image.storage.delete(
|
self._old_image.storage.delete(
|
||||||
self._old_image.name
|
self._old_image.name
|
||||||
) # Delete (old) base image.
|
) # 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.
|
# Attempt to get the date the photo was taken from the EXIF data.
|
||||||
try:
|
try:
|
||||||
exif_date = self.exif(self.image.file).get(
|
exif_date = self.exif(self.image.file).get(
|
||||||
|
|
|
||||||
|
|
@ -34,20 +34,20 @@ lgContainer.addEventListener('lgAfterOpen', () => {
|
||||||
|
|
||||||
const downloadUrl = this.getAttribute('href');
|
const downloadUrl = this.getAttribute('href');
|
||||||
|
|
||||||
// // Affichage de la modale stylisée
|
// Affichage de la modale stylisée
|
||||||
// Swal.fire({
|
Swal.fire({
|
||||||
// title: gettext('Download'),
|
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."),
|
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',
|
icon: 'info',
|
||||||
// showCancelButton: true,
|
showCancelButton: true,
|
||||||
// confirmButtonColor: '#3085d6',
|
confirmButtonColor: '#3085d6',
|
||||||
// cancelButtonColor: '#d33',
|
cancelButtonColor: '#d33',
|
||||||
// confirmButtonText: gettext('Download'),
|
confirmButtonText: gettext('Download'),
|
||||||
// cancelButtonText: gettext('Cancel'),
|
cancelButtonText: gettext('Cancel'),
|
||||||
// background: '#1a1a1a', // Optionnel : pour matcher le thème sombre de LightGallery
|
background: '#1a1a1a', // Optionnel : pour matcher le thème sombre de LightGallery
|
||||||
// color: '#fff'
|
color: '#fff'
|
||||||
// }).then((result) => {
|
}).then((result) => {
|
||||||
// if (result.isConfirmed) {
|
if (result.isConfirmed) {
|
||||||
// Si validé, on déclenche le téléchargement
|
// Si validé, on déclenche le téléchargement
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = downloadUrl;
|
link.href = downloadUrl;
|
||||||
|
|
@ -55,8 +55,8 @@ lgContainer.addEventListener('lgAfterOpen', () => {
|
||||||
document.body.appendChild(link);
|
document.body.appendChild(link);
|
||||||
link.click();
|
link.click();
|
||||||
document.body.removeChild(link);
|
document.body.removeChild(link);
|
||||||
// }
|
}
|
||||||
// });
|
});
|
||||||
}, true); // Utilisation du mode capture pour intercepter avant le script interne
|
}, true); // Utilisation du mode capture pour intercepter avant le script interne
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
})();
|
|
||||||
|
|
@ -13,7 +13,6 @@ class lgAdmin {
|
||||||
this.isStaff = document.querySelector('[name=is_staff]').value === "true";
|
this.isStaff = document.querySelector('[name=is_staff]').value === "true";
|
||||||
this.userId = document.querySelector('[name=user_id]').value;
|
this.userId = document.querySelector('[name=user_id]').value;
|
||||||
this.canResolveCensorship = document.querySelector('[name=can_resolve_censorship]').value === "true";
|
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.csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value;
|
||||||
this.photoId = 0;
|
this.photoId = 0;
|
||||||
return this;
|
return this;
|
||||||
|
|
@ -34,8 +33,7 @@ class lgAdmin {
|
||||||
document.getElementById("lg-delete").addEventListener('click', this.onDelete.bind(this));
|
document.getElementById("lg-delete").addEventListener('click', this.onDelete.bind(this));
|
||||||
|
|
||||||
// Add button to report photo
|
// Add button to report photo
|
||||||
this.core.$toolbar.append(`<a href="#" id="lg-report" title="Request removal" class="lg-icon lg-bi-icon">${deleteIcon}</a>`);
|
this.core.$toolbar.append(`<a href="#" id="lg-report" title="Notify abuse" class="lg-icon lg-bi-icon">${reportIcon}</a>`);
|
||||||
document.getElementById("lg-report").style.display = 'none';
|
|
||||||
document.getElementById("lg-report").addEventListener('click', this.onReport.bind(this));
|
document.getElementById("lg-report").addEventListener('click', this.onReport.bind(this));
|
||||||
|
|
||||||
// Add button to restore a censored photo
|
// Add button to restore a censored photo
|
||||||
|
|
@ -55,7 +53,6 @@ class lgAdmin {
|
||||||
const ownerId = el ? el.dataset.ownerId : null;
|
const ownerId = el ? el.dataset.ownerId : null;
|
||||||
const canDelete = this.isStaff || (ownerId && ownerId === this.userId);
|
const canDelete = this.isStaff || (ownerId && ownerId === this.userId);
|
||||||
document.getElementById("lg-delete").style.display = canDelete ? 'block' : 'none';
|
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;
|
const isCensored = el ? el.dataset.isPublic === 'false' : false;
|
||||||
document.getElementById("lg-restore").style.display = (this.canResolveCensorship && isCensored) ? 'block' : 'none';
|
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
|
// Event called when user click on report button
|
||||||
onReport(event) {
|
onReport(event) {
|
||||||
event.preventDefault();
|
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
|
// Build form request
|
||||||
const photoId = this.photoId;
|
const photoId = this.photoId;
|
||||||
const currentIndex = this.core.index;
|
const currentIndex = this.core.index;
|
||||||
|
|
|
||||||
|
|
@ -20,13 +20,11 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
<input type="hidden" name="is_staff" value="{{ request.user.is_staff|yesno:'true,false' }}">
|
<input type="hidden" name="is_staff" value="{{ request.user.is_staff|yesno:'true,false' }}">
|
||||||
<input type="hidden" name="user_id" value="{{ request.user.id }}">
|
<input type="hidden" name="user_id" value="{{ request.user.id }}">
|
||||||
<input type="hidden" name="can_resolve_censorship" value="{{ can_resolve_censorship|yesno:'true,false' }}">
|
<input type="hidden" name="can_resolve_censorship" value="{{ can_resolve_censorship|yesno:'true,false' }}">
|
||||||
<input type="hidden" name="guest_mode" value="{{ guest_mode|yesno:'true,false' }}">
|
|
||||||
<script src="{% static 'lightgallery/lightgallery.min.js' %}"></script>
|
<script src="{% static 'lightgallery/lightgallery.min.js' %}"></script>
|
||||||
<script src="{% static 'lightgallery/plugins/admin/lg-admin.js' %}"></script>
|
<script src="{% static 'lightgallery/plugins/admin/lg-admin.js' %}"></script>
|
||||||
<script src="{% static 'lightgallery/plugins/hash/lg-hash.min.js' %}"></script>
|
<script src="{% static 'lightgallery/plugins/hash/lg-hash.min.js' %}"></script>
|
||||||
<script src="{% static 'lightgallery/plugins/thumbnail/lg-thumbnail.min.js' %}"></script>
|
<script src="{% static 'lightgallery/plugins/thumbnail/lg-thumbnail.min.js' %}"></script>
|
||||||
<script src="{% static 'lightgallery/plugins/zoom/lg-zoom.min.js' %}"></script>
|
<script src="{% static 'lightgallery/plugins/zoom/lg-zoom.min.js' %}"></script>
|
||||||
<script src="{% static 'gallery_justified.js' %}"></script>
|
|
||||||
<script src="{% static 'gallery_detail.js' %}"></script>
|
<script src="{% static 'gallery_detail.js' %}"></script>
|
||||||
<script src="{% static 'sweetalert.js' %}"></script>
|
<script src="{% static 'sweetalert.js' %}"></script>
|
||||||
<script src="{% static 'copy-button.js' %}"></script>
|
<script src="{% static 'copy-button.js' %}"></script>
|
||||||
|
|
@ -89,10 +87,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="card-body p-0" id="lightgallery">
|
<div class="card-body row" id="lightgallery">
|
||||||
{% for photo in photos %}
|
{% for photo in photos %}
|
||||||
<a class="photo-item" href="{{ photo.get_absolute_url }}" data-src="{{ photo.get_display_url}}" data-download-url="{{ photo.image.url }}" data-slide-name="{{ photo.id }}" data-owner-id="{{ photo.owner.id }}" data-is-public="{{ photo.is_public|yesno:'true,false' }}" data-width="{{ photo.image_width|default:1 }}" data-height="{{ photo.image_height|default:1 }}">
|
<a class="col-6 col-md-3 mb-2 text-center" href="{{ photo.get_absolute_url }}" data-src="{{ photo.get_display_url}}" data-download-url="{{ photo.image.url }}" data-slide-name="{{ photo.id }}" data-owner-id="{{ photo.owner.id }}" data-is-public="{{ photo.is_public|yesno:'true,false' }}">
|
||||||
<img src="{{ photo.get_thumbnail_url }}" data-lazy="{{ photo.get_thumbnail_url }}" class="{% if not photo.is_public %}photo-private{% endif %}" alt="{{ 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 %}">
|
<img src="{{ photo.get_thumbnail_url }}" loading="lazy" class="img-thumbnail p-0{% if not photo.is_public %} border-danger border-5{% endif %}" alt="{{ 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 %}">
|
||||||
</a>
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -237,7 +237,6 @@ class GalleryPublicView(DetailView):
|
||||||
if request.user.is_authenticated:
|
if request.user.is_authenticated:
|
||||||
gallery = self.get_object()
|
gallery = self.get_object()
|
||||||
return redirect("photologue:pl-gallery", slug=gallery.slug)
|
return redirect("photologue:pl-gallery", slug=gallery.slug)
|
||||||
request.session['public_gallery_access'] = True
|
|
||||||
request.guest_mode = True
|
request.guest_mode = True
|
||||||
return super().get(request, *args, **kwargs)
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,3 @@ django-select2>=4.8
|
||||||
ExifRead>=2.1.2
|
ExifRead>=2.1.2
|
||||||
Pillow>=6.0.0
|
Pillow>=6.0.0
|
||||||
django-debug-toolbar>=3.2.0
|
django-debug-toolbar>=3.2.0
|
||||||
python-decouple>=3.6
|
|
||||||
whitenoise>=6.0
|
|
||||||
psycopg2-binary>=2.9
|
|
||||||
requests>=2.25
|
|
||||||
gunicorn>=21.0
|
|
||||||
|
|
|
||||||
2
tox.ini
2
tox.ini
|
|
@ -27,7 +27,7 @@ deps =
|
||||||
pep8-naming
|
pep8-naming
|
||||||
pyflakes
|
pyflakes
|
||||||
commands =
|
commands =
|
||||||
flake8 allauth_oauth photo21 photologue
|
flake8 allauth_note_kfet photo21 photologue
|
||||||
|
|
||||||
[flake8]
|
[flake8]
|
||||||
ignore = W503, I100, I101
|
ignore = W503, I100, I101
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue