Compare commits

...

13 commits

37 changed files with 927 additions and 1051 deletions

View file

@ -14,3 +14,42 @@ 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

View file

@ -0,0 +1,33 @@
name: Docker
on:
release:
types:
- published
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set image tag
id: meta
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
- name: Login to Forgejo registry
uses: docker/login-action@v3
with:
registry: git.sinfonie.org
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Build and push image
run: |
docker build \
-t git.sinfonie.org/sinfonie/photo26:${{ steps.meta.outputs.TAG }} \
-t git.sinfonie.org/sinfonie/photo26:latest \
.
docker push git.sinfonie.org/sinfonie/photo26:${{ steps.meta.outputs.TAG }}
docker push git.sinfonie.org/sinfonie/photo26:latest

23
Dockerfile Normal file
View file

@ -0,0 +1,23 @@
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
RUN apt-get update && apt-get install -y gettext && rm -rf /var/lib/apt/lists/*
COPY . .
RUN SECRET_KEY=dummy python manage.py compilemessages
# Create volume mount points
RUN mkdir -p /app/media /app/static /app/data
EXPOSE 8000
RUN chmod +x entrypoint.sh
ENTRYPOINT ["./entrypoint.sh"]

130
README.md
View file

@ -1,64 +1,121 @@
# Photo server 2021-2023
# Photo server
[![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0.txt)
This is the source code for the webserver hosting pictures from the
ENS Paris-Saclay student life.
ENS Rennes student life.
The philosophy of this project is to keep this code as simple as possible to
run and to maintain.
## Setup
This project is a fork of [Photo21](https://gitlab.crans.org/bde/photo21/),
originally developed at ENS Paris-Saclay.
1. **Dependency installation.**
## Docker install (recommended for production)
1. Create a `docker-compose.yml` (a ready-to-use file is provided in the repository):
```yaml
version: "3.9"
networks:
photo26:
services:
db:
image: postgres:16
container_name: photo26-db
restart: unless-stopped
environment:
POSTGRES_DB: photo26
POSTGRES_USER: photo26
POSTGRES_PASSWORD: change-me
volumes:
- ./postgres_data:/var/lib/postgresql/data
networks:
- photo26
photo26:
image: git.sinfonie.org/sinfonie/photo26:latest
container_name: photo26-app
restart: unless-stopped
depends_on:
- db
environment:
DB_ENGINE: postgres
DB_NAME: photo26
DB_USER: photo26
DB_PASSWORD: change-me
DB_HOST: db
DB_PORT: 5432
SECRET_KEY: change-me
EXTRA_HOSTS: photos.example.org
volumes:
- ./media:/app/media
ports:
- "8080:8000"
networks:
- photo26
```
2. Start the stack:
```bash
docker compose up -d
```
On first start the container will run migrations and create a default admin account automatically.
3. **Default credentials** — change these immediately after first login:
| Field | Value |
|----------|-----------------|
| Username | `admin` |
| Password | `admin` |
| Email | `admin@localhost` |
Admin panel: `http://localhost:8080/admin/`
4. **Passwords to change** in `docker-compose.yml` before going to production:
- `POSTGRES_PASSWORD` / `DB_PASSWORD` — database password
- `SECRET_KEY` — Django secret key (use a long random string)
- Log in to the admin panel and change the `admin` user password
## Development setup
1. **Cloning.**
Change directory to where you want the project to be.
```bash
git clone https://codeberg.org/krek0/photo21.git && cd photo21
```
2. **Dependency installation.**
If you are not using Debian, please feel free to adapt the following instructions.
```bash
sudo apt install git gettext python3-django python3-django-allauth python3-django-crispy-forms python3-docutils python3-exifread python3-pil
# Only for production
sudo apt install nginx uwsgi uwsgi-plugin-python3 python3-certbot-nginx
```
2. **Cloning.**
Change directory to where you want the project to be.
In production, we usually use `/var/www/photos/` as the `root` user.
3. **Configuration.**
```bash
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).**
4. **Database.**
In development, you may use SQLite (no setup).
In production, we use PostgreSQL which require a bit of setup:
```bash
sudo apt install postgresql postgresql-contrib
sudo -u postgres psql
postgres=# CREATE USER photo21 WITH PASSWORD 'un_mot_de_passe_sur';
postgres=# CREATE USER photo21 WITH PASSWORD 'your_password';
postgres=# CREATE DATABASE photo21 OWNER photo21;
```
5. **Initialization.**,
In production, please use `www-data` user.
5. **Initialization.**
```
./manage.py collectstatic
@ -69,19 +126,14 @@ run and to maintain.
# Only when creating a new database
./manage.py loaddata initial
./manage.py createsuperuser
# change DEBUG to True in photo21/settings.py
```
6. **Maintenance Mode.**,
In production to toggle the server mainteance mode
6. **Maintenance Mode.**
In production to toggle the server maintenance mode
```./maintenance_tool.sh```
6. *Enjoy \o/*
In production, the NGINX site should now work.
In development, you can launch the development server using:
7. *Enjoy \o/*
```bash
(env)$ ./manage.py runserver

View file

@ -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"

View file

@ -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]

View file

@ -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)

View file

@ -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)

40
docker-compose.yml Normal file
View file

@ -0,0 +1,40 @@
version: "3.9"
networks:
photo26:
services:
db:
image: postgres:16
container_name: photo26-db
restart: unless-stopped
environment:
POSTGRES_DB: photo26
POSTGRES_USER: photo26
POSTGRES_PASSWORD: change-me
volumes:
- ./postgres_data:/var/lib/postgresql/data
networks:
- photo26
photo26:
image: git.sinfonie.org/sinfonie/photo26:latest
container_name: photo26-app
restart: unless-stopped
depends_on:
- db
environment:
DB_ENGINE: postgres
DB_NAME: photo26
DB_USER: photo26
DB_PASSWORD: change-me
DB_HOST: db
DB_PORT: 5432
SECRET_KEY: change-me
EXTRA_HOSTS: photos.example.org
volumes:
- ./media:/app/media
ports:
- "8080:8000"
networks:
- photo26

8
entrypoint.sh Normal file
View file

@ -0,0 +1,8 @@
#!/bin/sh
set -e
python manage.py collectstatic --noinput
python manage.py migrate --noinput
python manage.py loaddata initial
python manage.py create_default_admin
exec gunicorn photo21.wsgi:application --bind 0.0.0.0:8000 --workers 3

View file

@ -30,7 +30,7 @@
"height": 180,
"quality": 70,
"upscale": false,
"crop": true,
"crop": false,
"pre_cache": true,
"increment_count": false
}

View file

@ -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

View file

@ -1,321 +0,0 @@
# This file is part of photo21
# Copyright (C) 2022 Amicale des élèves de l'ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-12-07 20:03+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: .\photo21\forms.py:16
msgid ""
"Please enter a valid email address ending with `@crans.org` or `@ens-paris-"
"saclay.fr`."
msgstr ""
#: .\photo21\forms.py:29
msgid "Must end with `@crans.org` or `@ens-paris-saclay.fr`."
msgstr ""
#: .\photo21\settings.py:171
msgid "German"
msgstr ""
#: .\photo21\settings.py:172
msgid "English"
msgstr ""
#: .\photo21\settings.py:173
msgid "Spanish"
msgstr ""
#: .\photo21\settings.py:174
msgid "French"
msgstr ""
#: .\photo21\templates\400.html:12
msgid "Bad request"
msgstr ""
#: .\photo21\templates\400.html:16
msgid ""
"Sorry, your request was bad. Don't know what could be wrong. An email has "
"been sent to webmasters with the details of the error. You can now drink a "
"coke."
msgstr ""
#: .\photo21\templates\403.html:12
msgid "Permission denied"
msgstr ""
#: .\photo21\templates\403.html:15
msgid "You don't have the right to perform this request."
msgstr ""
#: .\photo21\templates\403.html:17 .\photo21\templates\404.html:21
msgid "Exception message:"
msgstr ""
#: .\photo21\templates\404.html:12
msgid "Page not found"
msgstr ""
#: .\photo21\templates\404.html:16
#, python-format
msgid ""
"The requested path <code>%(request_path)s</code> was not found on the server."
msgstr ""
#: .\photo21\templates\500.html:12
msgid "Server error"
msgstr ""
#: .\photo21\templates\500.html:16
msgid ""
"Sorry, an error occurred when processing your request. An email has been "
"sent to webmasters with the detail of the error, and this will be fixed "
"soon. You can go drink a soft."
msgstr ""
#: .\photo21\templates\account\email.html:8
#: .\photo21\templates\account\email.html:16
#: .\photo21\templates\socialaccount\connections.html:16
msgid "E-mail Addresses"
msgstr ""
#: .\photo21\templates\account\email.html:11 .\photo21\templates\base.html:63
#: .\photo21\templates\socialaccount\connections.html:11
msgid "Account"
msgstr ""
#: .\photo21\templates\account\email.html:19
#: .\photo21\templates\socialaccount\connections.html:19
msgid "Social connections"
msgstr ""
#: .\photo21\templates\account\email.html:25
msgid "The following e-mail addresses are associated with your account:"
msgstr ""
#: .\photo21\templates\account\email.html:36
msgid "Verified"
msgstr ""
#: .\photo21\templates\account\email.html:38
msgid "Unverified"
msgstr ""
#: .\photo21\templates\account\email.html:40
msgid "Primary"
msgstr ""
#: .\photo21\templates\account\email.html:46
msgid "Make Primary"
msgstr ""
#: .\photo21\templates\account\email.html:47
msgid "Re-send Verification"
msgstr ""
#: .\photo21\templates\account\email.html:48
#: .\photo21\templates\socialaccount\connections.html:47
msgid "Remove"
msgstr ""
#: .\photo21\templates\account\email.html:53
msgid "Warning:"
msgstr ""
#: .\photo21\templates\account\email.html:53
msgid ""
"You currently do not have any e-mail address set up. You should really add "
"an e-mail address so you can receive notifications, reset your password, etc."
msgstr ""
#: .\photo21\templates\account\email.html:57
msgid "Add E-mail Address"
msgstr ""
#: .\photo21\templates\account\email.html:62
msgid "Add E-mail"
msgstr ""
#: .\photo21\templates\account\login.html:8
#: .\photo21\templates\account\login.html:36
msgid "Sign In"
msgstr ""
#: .\photo21\templates\account\login.html:19
#, python-format
msgid ""
"Please sign in with one of your existing third party accounts. Or, <a "
"href=\"%(signup_url)s\">sign up</a> for a %(site_name)s account and sign in "
"below:"
msgstr ""
#: .\photo21\templates\account\login.html:26
#, python-format
msgid ""
"If you have not created an account yet, then please <a "
"href=\"%(signup_url)s\">sign up</a> first."
msgstr ""
#: .\photo21\templates\account\login.html:39
msgid "Forgot Password?"
msgstr ""
#: .\photo21\templates\account\login.html:42
msgid "If any problem, please contact the server owners at"
msgstr ""
#: .\photo21\templates\account\logout.html:8
#: .\photo21\templates\account\logout.html:13
#: .\photo21\templates\account\logout.html:22
msgid "Sign Out"
msgstr ""
#: .\photo21\templates\account\logout.html:16
msgid "Are you sure you want to sign out?"
msgstr ""
#: .\photo21\templates\account\signup.html:8
msgid "Signup"
msgstr ""
#: .\photo21\templates\account\signup.html:13
#: .\photo21\templates\account\signup.html:24
msgid "Sign Up"
msgstr ""
#: .\photo21\templates\account\signup.html:16
#, python-format
msgid ""
"Already have an account? Then please <a href=\"%(login_url)s\">sign in</a>."
msgstr ""
#: .\photo21\templates\base.html:16
msgid "The ENS Paris-Saclay pictures server."
msgstr ""
#: .\photo21\templates\base.html:41
msgid "Galleries"
msgstr ""
#: .\photo21\templates\base.html:46
msgid "Upload"
msgstr ""
#: .\photo21\templates\base.html:51
msgid "Manage"
msgstr ""
#: .\photo21\templates\base.html:72
msgid "Log out"
msgstr ""
#: .\photo21\templates\base.html:82
msgid "Log in"
msgstr ""
#: .\photo21\templates\base.html:91
msgid "Sign up"
msgstr ""
#: .\photo21\templates\base.html:116
msgid "Connected as"
msgstr ""
#: .\photo21\templates\base.html:118
msgid "Source code"
msgstr ""
#: .\photo21\templates\index.html:8
msgid "Home"
msgstr ""
#: .\photo21\templates\index.html:11
msgid "Welcome to the pictures server!"
msgstr ""
#: .\photo21\templates\index.html:13
msgid ""
"This website aims to collect the pictures and movies taken in the student "
"life of ENS Paris-Saclay or involving its students."
msgstr ""
#: .\photo21\templates\index.html:20
#, python-format
msgid ""
"The pictures are visible in <a href=\"%(gallery_archive_url)s\">the "
"galleries</a> and are downloadable. <b>However, the agreement of the "
"photographer and the persons present on the photo is necessary before any "
"republication on another platform. </b>"
msgstr ""
#: .\photo21\templates\index.html:29
msgid ""
"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>"
msgstr ""
#: .\photo21\templates\index.html:36
msgid ""
"If you want to obtain the right to upload pictures, please let us know: <a "
"href=\"mailto:photos@crans.org?subject=[Photographe] Demande de droits "
"photographe\" class=\"btn btn-secondary btn-sm\">Become a photograph</a>"
msgstr ""
#: .\photo21\templates\index.html:43
msgid "Last galleries"
msgstr ""
#: .\photo21\templates\index.html:52
msgid "Behind the scene"
msgstr ""
#: .\photo21\templates\index.html:54
msgid ""
"Because we value your privacy, we do not sell the data on this site, unlike "
"many free online platforms. The dedicated server running this website is "
"kindly hosted by the <a href=\"https://www.crans.org/\">Crans</a> at the ENS "
"Paris-Saclay basement. It is not managed by the Crans. Current active "
"administrators are:"
msgstr ""
#: .\photo21\templates\index.html:63
msgid "They should be contacted at"
msgstr ""
#: .\photo21\templates\socialaccount\connections.html:8
msgid "Account Connections"
msgstr ""
#: .\photo21\templates\socialaccount\connections.html:25
msgid ""
"You can sign in to your account using any of the following third party "
"accounts:"
msgstr ""
#: .\photo21\templates\socialaccount\connections.html:53
msgid ""
"You currently have no social network accounts connected to this account."
msgstr ""
#: .\photo21\templates\socialaccount\connections.html:56
msgid "Add a 3rd Party Account"
msgstr ""
#: .\photo21\templates\socialaccount\snippets\provider_list.html:20
msgid "Sign in with"
msgstr ""

View file

@ -1,320 +0,0 @@
# This file is part of photo21
# Copyright (C) 2022 Amicale des élèves de l'ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-12-07 20:03+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: .\photo21\forms.py:16
msgid ""
"Please enter a valid email address ending with `@crans.org` or `@ens-paris-"
"saclay.fr`."
msgstr ""
#: .\photo21\forms.py:29
msgid "Must end with `@crans.org` or `@ens-paris-saclay.fr`."
msgstr ""
#: .\photo21\settings.py:171
msgid "German"
msgstr ""
#: .\photo21\settings.py:172
msgid "English"
msgstr ""
#: .\photo21\settings.py:173
msgid "Spanish"
msgstr ""
#: .\photo21\settings.py:174
msgid "French"
msgstr ""
#: .\photo21\templates\400.html:12
msgid "Bad request"
msgstr ""
#: .\photo21\templates\400.html:16
msgid ""
"Sorry, your request was bad. Don't know what could be wrong. An email has "
"been sent to webmasters with the details of the error. You can now drink a "
"coke."
msgstr ""
#: .\photo21\templates\403.html:12
msgid "Permission denied"
msgstr ""
#: .\photo21\templates\403.html:15
msgid "You don't have the right to perform this request."
msgstr ""
#: .\photo21\templates\403.html:17 .\photo21\templates\404.html:21
msgid "Exception message:"
msgstr ""
#: .\photo21\templates\404.html:12
msgid "Page not found"
msgstr ""
#: .\photo21\templates\404.html:16
#, python-format
msgid ""
"The requested path <code>%(request_path)s</code> was not found on the server."
msgstr ""
#: .\photo21\templates\500.html:12
msgid "Server error"
msgstr ""
#: .\photo21\templates\500.html:16
msgid ""
"Sorry, an error occurred when processing your request. An email has been "
"sent to webmasters with the detail of the error, and this will be fixed "
"soon. You can go drink a soft."
msgstr ""
#: .\photo21\templates\account\email.html:8
#: .\photo21\templates\account\email.html:16
#: .\photo21\templates\socialaccount\connections.html:16
msgid "E-mail Addresses"
msgstr ""
#: .\photo21\templates\account\email.html:11 .\photo21\templates\base.html:63
#: .\photo21\templates\socialaccount\connections.html:11
msgid "Account"
msgstr ""
#: .\photo21\templates\account\email.html:19
#: .\photo21\templates\socialaccount\connections.html:19
msgid "Social connections"
msgstr ""
#: .\photo21\templates\account\email.html:25
msgid "The following e-mail addresses are associated with your account:"
msgstr ""
#: .\photo21\templates\account\email.html:36
msgid "Verified"
msgstr ""
#: .\photo21\templates\account\email.html:38
msgid "Unverified"
msgstr ""
#: .\photo21\templates\account\email.html:40
msgid "Primary"
msgstr ""
#: .\photo21\templates\account\email.html:46
msgid "Make Primary"
msgstr ""
#: .\photo21\templates\account\email.html:47
msgid "Re-send Verification"
msgstr ""
#: .\photo21\templates\account\email.html:48
#: .\photo21\templates\socialaccount\connections.html:47
msgid "Remove"
msgstr ""
#: .\photo21\templates\account\email.html:53
msgid "Warning:"
msgstr ""
#: .\photo21\templates\account\email.html:53
msgid ""
"You currently do not have any e-mail address set up. You should really add "
"an e-mail address so you can receive notifications, reset your password, etc."
msgstr ""
#: .\photo21\templates\account\email.html:57
msgid "Add E-mail Address"
msgstr ""
#: .\photo21\templates\account\email.html:62
msgid "Add E-mail"
msgstr ""
#: .\photo21\templates\account\login.html:8
#: .\photo21\templates\account\login.html:36
msgid "Sign In"
msgstr ""
#: .\photo21\templates\account\login.html:19
#, python-format
msgid ""
"Please sign in with one of your existing third party accounts. Or, <a "
"href=\"%(signup_url)s\">sign up</a> for a %(site_name)s account and sign in "
"below:"
msgstr ""
#: .\photo21\templates\account\login.html:26
#, python-format
msgid ""
"If you have not created an account yet, then please <a "
"href=\"%(signup_url)s\">sign up</a> first."
msgstr ""
#: .\photo21\templates\account\login.html:39
msgid "Forgot Password?"
msgstr ""
#: .\photo21\templates\account\login.html:42
msgid "If any problem, please contact the server owners at"
msgstr ""
#: .\photo21\templates\account\logout.html:8
#: .\photo21\templates\account\logout.html:13
#: .\photo21\templates\account\logout.html:22
msgid "Sign Out"
msgstr ""
#: .\photo21\templates\account\logout.html:16
msgid "Are you sure you want to sign out?"
msgstr ""
#: .\photo21\templates\account\signup.html:8
msgid "Signup"
msgstr ""
#: .\photo21\templates\account\signup.html:13
#: .\photo21\templates\account\signup.html:24
msgid "Sign Up"
msgstr ""
#: .\photo21\templates\account\signup.html:16
#, python-format
msgid ""
"Already have an account? Then please <a href=\"%(login_url)s\">sign in</a>."
msgstr ""
#: .\photo21\templates\base.html:16
msgid "The ENS Paris-Saclay pictures server."
msgstr ""
#: .\photo21\templates\base.html:41
msgid "Galleries"
msgstr ""
#: .\photo21\templates\base.html:46
msgid "Upload"
msgstr ""
#: .\photo21\templates\base.html:51
msgid "Manage"
msgstr ""
#: .\photo21\templates\base.html:72
msgid "Log out"
msgstr ""
#: .\photo21\templates\base.html:82
msgid "Log in"
msgstr ""
#: .\photo21\templates\base.html:91
msgid "Sign up"
msgstr ""
#: .\photo21\templates\base.html:116
msgid "Connected as"
msgstr ""
#: .\photo21\templates\base.html:118
msgid "Source code"
msgstr ""
#: .\photo21\templates\index.html:8
msgid "Home"
msgstr ""
#: .\photo21\templates\index.html:11
msgid "Welcome to the pictures server!"
msgstr ""
#: .\photo21\templates\index.html:13
msgid ""
"This website aims to collect the pictures and movies taken in the student "
"life of ENS Paris-Saclay or involving its students."
msgstr ""
#: .\photo21\templates\index.html:20
#, python-format
msgid ""
"The pictures are visible in <a href=\"%(gallery_archive_url)s\">the "
"galleries</a> and are downloadable. <b>However, the agreement of the "
"photographer and the persons present on the photo is necessary before any "
"republication on another platform. </b>"
msgstr ""
#: .\photo21\templates\index.html:29
msgid ""
"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>"
msgstr ""
#: .\photo21\templates\index.html:36
msgid ""
"If you want to obtain the right to upload pictures, please let us know: <a "
"href=\"mailto:photos@crans.org?subject=[Photographe] Demande de droits "
"photographe\" class=\"btn btn-secondary btn-sm\">Become a photograph</a>"
msgstr ""
#: .\photo21\templates\index.html:43
msgid "Last galleries"
msgstr ""
#: .\photo21\templates\index.html:52
msgid "Behind the scene"
msgstr ""
#: .\photo21\templates\index.html:54
msgid ""
"Because we value your privacy, we do not sell the data on this site, unlike "
"many free online platforms. The dedicated server running this website is "
"kindly hosted by the <a href=\"https://www.crans.org/\">Crans</a> at the ENS "
"Paris-Saclay basement. It is not managed by the Crans. Current active "
"administrators are:"
msgstr ""
#: .\photo21\templates\index.html:63
msgid "They should be contacted at"
msgstr ""
#: .\photo21\templates\socialaccount\connections.html:8
msgid "Account Connections"
msgstr ""
#: .\photo21\templates\socialaccount\connections.html:25
msgid ""
"You can sign in to your account using any of the following third party "
"accounts:"
msgstr ""
#: .\photo21\templates\socialaccount\connections.html:53
msgid ""
"You currently have no social network accounts connected to this account."
msgstr ""
#: .\photo21\templates\socialaccount\connections.html:56
msgid "Add a 3rd Party Account"
msgstr ""
#: .\photo21\templates\socialaccount\snippets\provider_list.html:20
msgid "Sign in with"
msgstr ""

View file

@ -1,163 +1,158 @@
# This file is part of photo21
# Copyright (C) 2022 Amicale des élèves de l'ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Project-Id-Version: photo21\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-12-07 20:03+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"Language-Team: French\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: .\photo21\forms.py:16
msgid ""
"Please enter a valid email address ending with `@crans.org` or `@ens-paris-"
"saclay.fr`."
msgstr ""
"Veuillez entrer une adresse email valide finissant par `@crans.org` ou `@ens-"
"paris-saclay.fr`."
#: photo21/forms.py:16
msgid "Please enter a valid email address ending with `@ens-rennes.fr`"
msgstr "Veuillez entrer une adresse e-mail valide finissant par `@ens-rennes.fr`."
#: .\photo21\forms.py:29
msgid "Must end with `@crans.org` or `@ens-paris-saclay.fr`."
msgstr "Doit finir par `@crans.org` ou `@ens-paris-saclay.fr`."
#: photo21/forms.py:26
msgid "Must end with `@ens-rennes.fr`."
msgstr "Doit finir par `@ens-rennes.fr`."
#: .\photo21\settings.py:171
msgid "German"
msgstr ""
#: .\photo21\settings.py:172
#: photo21/settings.py:201
msgid "English"
msgstr ""
msgstr "Anglais"
#: .\photo21\settings.py:173
msgid "Spanish"
msgstr ""
#: .\photo21\settings.py:174
#: photo21/settings.py:202
msgid "French"
msgstr ""
msgstr "Français"
#: .\photo21\templates\400.html:12
#: photo21/templates/400.html:12
msgid "Bad request"
msgstr ""
msgstr "Requête incorrecte"
#: .\photo21\templates\400.html:16
#: photo21/templates/400.html:16
msgid ""
"Sorry, your request was bad. Don't know what could be wrong. An email has "
"been sent to webmasters with the details of the error. You can now drink a "
"coke."
msgstr ""
"Désolé, votre requête était incorrecte. Un e-mail a été envoyé aux "
"administrateurs avec les détails de l'erreur. Vous pouvez aller boire un "
"coca."
#: .\photo21\templates\403.html:12
#: photo21/templates/403.html:12
msgid "Permission denied"
msgstr ""
msgstr "Permission refusée"
#: .\photo21\templates\403.html:15
#: photo21/templates/403.html:15
msgid "You don't have the right to perform this request."
msgstr ""
msgstr "Vous n'avez pas le droit d'effectuer cette requête."
#: .\photo21\templates\403.html:17 .\photo21\templates\404.html:21
#: photo21/templates/403.html:17 photo21/templates/404.html:21
msgid "Exception message:"
msgstr ""
msgstr "Message d'exception :"
#: .\photo21\templates\404.html:12
#: photo21/templates/404.html:12
msgid "Page not found"
msgstr ""
msgstr "Page introuvable"
#: .\photo21\templates\404.html:16
#: photo21/templates/404.html:16
#, python-format
msgid ""
"The requested path <code>%(request_path)s</code> was not found on the server."
msgstr ""
"Le chemin demandé <code>%(request_path)s</code> n'a pas été trouvé sur le "
"serveur."
#: .\photo21\templates\500.html:12
#: photo21/templates/500.html:12
msgid "Server error"
msgstr ""
msgstr "Erreur serveur"
#: .\photo21\templates\500.html:16
#: photo21/templates/500.html:16
msgid ""
"Sorry, an error occurred when processing your request. An email has been "
"sent to webmasters with the detail of the error, and this will be fixed "
"soon. You can go drink a soft."
msgstr ""
"Désolé, une erreur s'est produite lors du traitement de votre requête. Un e-"
"mail a été envoyé aux administrateurs avec les détails de l'erreur, et cela "
"sera corrigé prochainement. Vous pouvez aller boire un soda."
#: .\photo21\templates\account\email.html:8
#: .\photo21\templates\account\email.html:16
#: .\photo21\templates\socialaccount\connections.html:16
#: photo21/templates/account/email.html:8
#: photo21/templates/account/email.html:16
#: photo21/templates/socialaccount/connections.html:16
msgid "E-mail Addresses"
msgstr ""
msgstr "Adresses e-mail"
#: .\photo21\templates\account\email.html:11 .\photo21\templates\base.html:63
#: .\photo21\templates\socialaccount\connections.html:11
#: photo21/templates/account/email.html:11 photo21/templates/base.html:66
#: photo21/templates/socialaccount/connections.html:11
msgid "Account"
msgstr "Compte"
#: .\photo21\templates\account\email.html:19
#: .\photo21\templates\socialaccount\connections.html:19
#: photo21/templates/account/email.html:19
#: photo21/templates/socialaccount/connections.html:19
msgid "Social connections"
msgstr "Connexions sociales"
#: .\photo21\templates\account\email.html:25
#: photo21/templates/account/email.html:25
msgid "The following e-mail addresses are associated with your account:"
msgstr ""
msgstr "Les adresses e-mail suivantes sont associées à votre compte :"
#: .\photo21\templates\account\email.html:36
#: photo21/templates/account/email.html:36
msgid "Verified"
msgstr ""
msgstr "Vérifié"
#: .\photo21\templates\account\email.html:38
#: photo21/templates/account/email.html:38
msgid "Unverified"
msgstr ""
msgstr "Non vérifié"
#: .\photo21\templates\account\email.html:40
#: photo21/templates/account/email.html:40
msgid "Primary"
msgstr ""
msgstr "Principal"
#: .\photo21\templates\account\email.html:46
#: photo21/templates/account/email.html:46
msgid "Make Primary"
msgstr ""
msgstr "Définir comme principal"
#: .\photo21\templates\account\email.html:47
#: photo21/templates/account/email.html:47
msgid "Re-send Verification"
msgstr ""
msgstr "Renvoyer la vérification"
#: .\photo21\templates\account\email.html:48
#: .\photo21\templates\socialaccount\connections.html:47
#: photo21/templates/account/email.html:48
#: photo21/templates/socialaccount/connections.html:47
msgid "Remove"
msgstr ""
msgstr "Supprimer"
#: .\photo21\templates\account\email.html:53
#: photo21/templates/account/email.html:53
msgid "Warning:"
msgstr ""
msgstr "Avertissement :"
#: .\photo21\templates\account\email.html:53
#: photo21/templates/account/email.html:53
msgid ""
"You currently do not have any e-mail address set up. You should really add "
"an e-mail address so you can receive notifications, reset your password, etc."
msgstr ""
"Vous n'avez actuellement aucune adresse e-mail configurée. Vous devriez "
"vraiment en ajouter une pour recevoir des notifications, réinitialiser votre "
"mot de passe, etc."
#: .\photo21\templates\account\email.html:57
#: photo21/templates/account/email.html:57
msgid "Add E-mail Address"
msgstr "Ajouter une Adresse E-mail"
#: .\photo21\templates\account\email.html:62
#: photo21/templates/account/email.html:62
msgid "Add E-mail"
msgstr "Ajouter un E-mail"
#: .\photo21\templates\account\login.html:8
#: .\photo21\templates\account\login.html:36
#: photo21/templates/account/login.html:8
#: photo21/templates/account/login.html:36
msgid "Sign In"
msgstr "Se Connecter"
#: .\photo21\templates\account\login.html:19
#: photo21/templates/account/login.html:19
#, python-format
msgid ""
"Please sign in with one of your existing third party accounts. Or, <a "
@ -168,7 +163,7 @@ msgstr ""
"href=\"%(signup_url)s\">inscrivez-vous</a> pour un compte sur %(site_name)s "
"et identifiez-vous ci-dessous :"
#: .\photo21\templates\account\login.html:26
#: photo21/templates/account/login.html:26
#, python-format
msgid ""
"If you have not created an account yet, then please <a "
@ -177,92 +172,94 @@ msgstr ""
"Si vous n'avez pas déjà créé de compte, veuillez vous <a "
"href=\"%(signup_url)s\">inscrire</a>."
#: .\photo21\templates\account\login.html:39
#: photo21/templates/account/login.html:39
msgid "Forgot Password?"
msgstr "Mot de passe oublié ?"
#: .\photo21\templates\account\login.html:42
#: photo21/templates/account/login.html:42
msgid "If any problem, please contact the server owners at"
msgstr "En cas de problème, contactez les administrateurs à"
#: .\photo21\templates\account\logout.html:8
#: .\photo21\templates\account\logout.html:13
#: .\photo21\templates\account\logout.html:22
#: photo21/templates/account/logout.html:8
#: photo21/templates/account/logout.html:13
#: photo21/templates/account/logout.html:22
msgid "Sign Out"
msgstr "Déconnexion"
#: .\photo21\templates\account\logout.html:16
#: photo21/templates/account/logout.html:16
msgid "Are you sure you want to sign out?"
msgstr ""
msgstr "Êtes-vous sûr de vouloir vous déconnecter ?"
#: .\photo21\templates\account\signup.html:8
#: photo21/templates/account/signup.html:8
msgid "Signup"
msgstr ""
msgstr "Inscription"
#: .\photo21\templates\account\signup.html:13
#: .\photo21\templates\account\signup.html:24
#: photo21/templates/account/signup.html:13
#: photo21/templates/account/signup.html:24
msgid "Sign Up"
msgstr ""
msgstr "S'inscrire"
#: .\photo21\templates\account\signup.html:16
#: photo21/templates/account/signup.html:16
#, python-format
msgid ""
"Already have an account? Then please <a href=\"%(login_url)s\">sign in</a>."
msgstr ""
"Vous avez déjà un compte ? Veuillez alors <a href=\"%(login_url)s\">vous "
"connecter</a>."
#: .\photo21\templates\base.html:16
msgid "The ENS Paris-Saclay pictures server."
msgstr ""
#: photo21/templates/base.html:16
msgid "The ENS Rennes pictures server."
msgstr "Le serveur photos de l'ENS Rennes."
#: .\photo21\templates\base.html:41
#: photo21/templates/base.html:43
msgid "Galleries"
msgstr "Galeries"
#: .\photo21\templates\base.html:46
#: photo21/templates/base.html:48
msgid "Upload"
msgstr "Téléversement"
msgstr "Upload"
#: .\photo21\templates\base.html:51
#: photo21/templates/base.html:53
msgid "Manage"
msgstr "Gestion"
msgstr "Manage"
#: .\photo21\templates\base.html:72
#: photo21/templates/base.html:75
msgid "Log out"
msgstr ""
msgstr "Déconnection"
#: .\photo21\templates\base.html:82
#: photo21/templates/base.html:85
msgid "Log in"
msgstr ""
msgstr "Connection"
#: .\photo21\templates\base.html:91
#: photo21/templates/base.html:94
msgid "Sign up"
msgstr "Inscription"
#: .\photo21\templates\base.html:116
#: photo21/templates/base.html:119
msgid "Connected as"
msgstr "Connecté en tant que"
#: .\photo21\templates\base.html:118
#: photo21/templates/base.html:121
msgid "Source code"
msgstr "Code source"
#: .\photo21\templates\index.html:8
#: photo21/templates/index.html:8
msgid "Home"
msgstr "Accueil"
#: .\photo21\templates\index.html:11
#: photo21/templates/index.html:11
msgid "Welcome to the pictures server!"
msgstr "Bienvenue sur le serveur photos !"
#: .\photo21\templates\index.html:13
#: photo21/templates/index.html:13
msgid ""
"This website aims to collect the pictures and movies taken in the student "
"life of ENS Paris-Saclay or involving its students."
"life of ENS Rennes or involving its students."
msgstr ""
"Ce site à pour objectif de recenser les photos et films pris dans la vie "
"associative de l'ENS Paris-Saclay ou impliquant ses usager·ères."
"Ce site a pour objectif de recenser les photos et films pris dans la vie "
"associative de l'ENS Rennes ou impliquant ses étudiant·es."
#: .\photo21\templates\index.html:20
#: photo21/templates/index.html:20
#, python-format
msgid ""
"The pictures are visible in <a href=\"%(gallery_archive_url)s\">the "
@ -275,73 +272,77 @@ msgstr ""
"photographe et des personnes présentes sur la photo est nécessaire avant "
"toute republication sur un autre site.</b>"
#: .\photo21\templates\index.html:29
msgid ""
"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>"
msgstr ""
"Si vous souhaitez qu'une photo soit supprimée, signalez le nous : <a "
"href=\"mailto:photos@crans.org?subject=[ABUS] Nouvelle requête\" class=\"btn "
"btn-secondary btn-sm\">Signaler un abus</a>"
#: .\photo21\templates\index.html:36
#: photo21/templates/index.html:30
msgid ""
"If you want to obtain the right to upload pictures, please let us know: <a "
"href=\"mailto:photos@crans.org?subject=[Photographe] Demande de droits "
"href=\"mailto:sinfonie@ens-rennes.fr?subject=[Photographe] Demande de droits "
"photographe\" class=\"btn btn-secondary btn-sm\">Become a photograph</a>"
msgstr ""
"Si vous souhaitez obtenir les droits photographes pour téléverser vos "
"photos, signalez le nous : <a href=\"mailto:photos@crans.org?"
"photos, contactez-nous : <a href=\"mailto:sinfonie@ens-rennes.fr?"
"subject=[Photographe] Demande de droits photographe\" class=\"btn btn-"
"secondary btn-sm\">Devenir photographe</a>"
#: .\photo21\templates\index.html:43
#: photo21/templates/index.html:38
msgid "Last galleries"
msgstr "Galeries récentes"
#: .\photo21\templates\index.html:52
#: photo21/templates/index.html:47
msgid "Behind the scene"
msgstr "Derrière la scène"
msgstr "Behind the scene"
#: .\photo21\templates\index.html:54
#: photo21/templates/index.html:49
msgid ""
"Because we value your privacy, we do not sell the data on this site, unlike "
"many free online platforms. The dedicated server running this website is "
"kindly hosted by the <a href=\"https://www.crans.org/\">Crans</a> at the ENS "
"Paris-Saclay basement. It is not managed by the Crans. Current active "
"administrators are:"
"<!-- 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, --> <!-- unlike many free online platforms. --> "
"The dedicated server running this website is kindly hosted by <a "
"href=\"https://sinfonie.org/\">Sinfonie</a> at the ENS Rennes. <!-- Current "
"active administrators are: --> <!--"
msgstr ""
"Comme nous accordons une grande importance au respect de la vie privée, nous "
"ne vendons pas les données de ce site, contrairement à de nombreuses "
"plateformes en ligne gratuites. Le serveur dédié qui fait fonctionner ce "
"site est gentiment hébergé par le <a href=\"https://www.crans.org/\">Crans</"
"a> au sous-sol de l'ENS Paris-Saclay. <b>Il n'est pas géré par le Crans.</b> "
"Les administrateur·trices sont :"
"<!-- Ce projet est un fork de <a href=\"https://gitlab.crans.org/bde/"
"photo21/\">Photo21</a>. --> <!-- Comme nous accordons une grande importance "
"au respect de la vie privée, nous ne vendons pas les données de ce site, --> "
"<!-- contrairement à de nombreuses plateformes en ligne gratuites. --> "
"Le serveur qui fait fonctionner ce site est gentiment hébergé par "
"<a href=\"https://sinfonie.org/\">Sinfonie</a> à l'ENS Rennes. "
"<!-- Les administrateur·trices actuel·les sont : --> <!--"
#: .\photo21\templates\index.html:63
#: photo21/templates/index.html:58
msgid "They should be contacted at"
msgstr "Ils peuvent être contactés à"
#: .\photo21\templates\socialaccount\connections.html:8
#: photo21/templates/socialaccount/connections.html:8
msgid "Account Connections"
msgstr ""
msgstr "Connexions de compte"
#: .\photo21\templates\socialaccount\connections.html:25
#: photo21/templates/socialaccount/connections.html:25
msgid ""
"You can sign in to your account using any of the following third party "
"accounts:"
msgstr ""
"Vous pouvez vous connecter à votre compte en utilisant l'un des comptes "
"tiers suivants :"
#: .\photo21\templates\socialaccount\connections.html:53
#: photo21/templates/socialaccount/connections.html:53
msgid ""
"You currently have no social network accounts connected to this account."
msgstr ""
"Vous n'avez actuellement aucun compte de réseau social connecté à ce compte."
#: .\photo21\templates\socialaccount\connections.html:56
#: photo21/templates/socialaccount/connections.html:56
msgid "Add a 3rd Party Account"
msgstr ""
msgstr "Ajouter un compte tiers"
#: .\photo21\templates\socialaccount\snippets\provider_list.html:20
#: photo21/templates/socialaccount/snippets/provider_list.html:20
msgid "Sign in with"
msgstr "Se Connecter avec"
#~ msgid ""
#~ "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>"
#~ msgstr ""
#~ "Si vous souhaitez qu'une photo soit supprimée, signalez le nous : <a "
#~ "href=\"mailto:photos@crans.org?subject=[ABUS] Nouvelle requête\" "
#~ "class=\"btn btn-secondary btn-sm\">Signaler un abus</a>"

View file

@ -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
@ -53,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",
@ -66,17 +78,20 @@ 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",
@ -121,12 +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"),
_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": 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": {
@ -164,9 +198,7 @@ USE_TZ = True
# Limit available languages to this subset
LANGUAGES = [
("de", _("German")),
("en", _("English")),
("es", _("Spanish")),
("fr", _("French")),
]
@ -189,9 +221,20 @@ 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")]
# Do not send email during debug
# By default Django sends mails to localhost:25 without authentification
@ -202,6 +245,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 = "/"
@ -221,16 +269,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"

View file

@ -2,10 +2,9 @@
// Copyright (C) 2022 Amicale des élèves de l'ENS Paris-Saclay
// SPDX-License-Identifier: GPL-3.0-or-later
// On language selection, submit form
const langSelect = document.getElementsByName("language")[0];
if (langSelect) {
langSelect.addEventListener("change", (e) => {
e.target.form.submit();
document.querySelectorAll('[data-lang]').forEach(btn => {
btn.addEventListener('click', () => {
document.getElementById('lang-input').value = btn.dataset.lang;
document.getElementById('lang-form').submit();
});
}
});

View file

@ -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;

12
photo21/storage.py Normal file
View file

@ -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

View file

@ -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>
</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 %}

View file

@ -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="Referrer-Policy" content="no-referrer">
<title>{% block title %}{{ title }}{% endblock title %} - {{ request.site.name }}</title>
<meta name="description" content="{% trans "The ENS Paris-Saclay pictures server." %}">
<meta name="description" content="{% trans "The ENS Rennes pictures server." %}">
<script src="{% static "theme.js" %}"></script>
<link rel="stylesheet" href="{% static "bootstrap5/css/bootstrap.min.css" %}">
<link rel="stylesheet" href="{% static "layout.css" %}">
@ -56,6 +56,21 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% endif %}
</ul>
<ul class="navbar-nav">
{% get_available_languages as LANGUAGES %}
<li class="nav-item dropdown">
<button class="btn nav-link dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
{{ LANGUAGE_CODE|upper }}
</button>
<ul class="dropdown-menu dropdown-menu-end">
{% for lang_code, lang_name in LANGUAGES %}
<li>
<button class="dropdown-item{% if lang_code == LANGUAGE_CODE %} active{% endif %}" type="button" data-lang="{{ lang_code }}">
{{ lang_name }}
</button>
</li>
{% endfor %}
</ul>
</li>
{% if request.user.is_authenticated %}
<li class="nav-item">
{% url 'account_email' as url %}
@ -112,26 +127,18 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% endblock %}
</main>
<footer>
<form action="{% url 'set_language' %}" method="post">
{% csrf_token %}
<p class="small text-center text-muted mt-1">
{% if request.user.is_authenticated %}
{% trans "Connected as" %} <code>{{ request.user.username }}</code> &middot;
{% endif %}
<a class="text-reset" href="https://gitlab.crans.org/bde/photo21/">{% trans "Source code" %}</a> &middot;
<select title="language" name="language" class="lang-select">
{% get_current_language as LANGUAGE_CODE %}
{% get_available_languages as LANGUAGES %}
{% for lang_code, lang_name in LANGUAGES %}
<option value="{{ lang_code }}" {% if lang_code == LANGUAGE_CODE %}selected{% endif %}>
{{ lang_name }} ({{ lang_code }})
</option>
{% endfor %}
</select>
<noscript><input type="submit"></noscript>
</p>
</form>
<p class="small text-center text-muted mt-1">
{% if request.user.is_authenticated %}
{% trans "Connected as" %} <code>{{ request.user.username }}</code> &middot;
{% endif %}
<a class="text-reset" href="https://git.sinfonie.org/sinfonie/photo26">{% trans "Source code" %}</a>
</p>
</footer>
<form id="lang-form" action="{% url 'set_language' %}" method="post" hidden>
{% csrf_token %}
<input type="hidden" name="language" id="lang-input">
<input type="hidden" name="next" value="{{ request.path }}">
</form>
<script src="{% static "lang-select.js" %}"></script>
<script src="{% static "bootstrap5/js/bootstrap.bundle.min.js" %}"></script>

View file

@ -12,7 +12,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
<p>
{% 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 %}
</p>
<p>
@ -25,21 +25,16 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% endblocktrans %}
</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>
{% blocktrans trimmed %}
If you want to obtain the right to upload pictures, please let us know:
<a href="mailto:photos@crans.org?subject=[Photographe] Demande de droits photographe" class="btn btn-secondary btn-sm">Become a photograph</a>
<a href="mailto:sinfonie@ens-rennes.fr?subject=[Photographe] Demande de droits photographe" class="btn btn-secondary btn-sm">Become a photograph</a>
{% endblocktrans %}
</p>
{% endif %}
<h3>{% trans "Last galleries" %}</h3>
<div class="row mb-2">
{% for gallery in object_list %}
@ -52,15 +47,15 @@ SPDX-License-Identifier: GPL-3.0-or-later
<h3 class="mt-4">{% trans "Behind the scene" %}</h3>
<p>
{% blocktrans trimmed %}
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
<a href="https://www.crans.org/">Crans</a> at the ENS Paris-Saclay
basement.
It is not managed by the Crans. Current active administrators are:
{% endblocktrans %}
{% for user in superusers %} <code>{{ user.username }}</code>{% endfor %}.
{% trans "They should be contacted at" %}
<a href="mailto:photos@crans.org">photos@crans.org</a>.
<!-- 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, -->
<!-- unlike many free online platforms. -->
The dedicated server running this website is kindly hosted by
<a href="https://sinfonie.org/">Sinfonie</a> at the ENS Rennes.
<!-- Current active administrators are: -->
<!-- {% endblocktrans %} -->
<!-- {% for user in superusers %} <code>{{ user.username }}</code>{% endfor %}. -->
<!-- {% trans "They should be contacted at" %} -->
<!-- <a href="mailto:photos@crans.org">photos@crans.org</a>. -->
</p>
{% endblock %}

View file

@ -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

View file

@ -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

View file

@ -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)

View file

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

View file

@ -0,0 +1,32 @@
from django.contrib.auth import get_user_model
from django.core.management.base import BaseCommand
class Command(BaseCommand):
help = "Create default admin user (admin@localhost / admin) if it does not exist"
def handle(self, *args, **kwargs):
User = get_user_model()
email = "admin@localhost"
username = "admin"
password = "admin"
if User.objects.filter(username=username).exists():
self.stdout.write("Default admin already exists, skipping.")
return
user = User.objects.create_superuser(username=username, email=email, password=password)
# Mark the email as verified via allauth
try:
from allauth.account.models import EmailAddress
EmailAddress.objects.create(
user=user,
email=email,
primary=True,
verified=True,
)
except Exception as e:
self.stderr.write(f"Could not create allauth EmailAddress: {e}")
self.stdout.write(f"Default admin created: {username} / {password}")

View file

@ -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',
),
),
]

View file

@ -0,0 +1,31 @@
from django.db import migrations
def regen_thumbnails(apps, schema_editor):
Photo = apps.get_model("photologue", "Photo")
PhotoSize = apps.get_model("photologue", "PhotoSize")
try:
thumbnail = PhotoSize.objects.get(name="thumbnail")
except PhotoSize.DoesNotExist:
return
for photo in Photo.objects.all():
# Use the real model instance to access file methods
from photologue.models import Photo as RealPhoto
try:
real_photo = RealPhoto.objects.get(pk=photo.pk)
real_photo.remove_size(thumbnail)
except Exception:
pass
class Migration(migrations.Migration):
dependencies = [
("photologue", "0008_photo_dimensions"),
]
operations = [
migrations.RunPython(regen_thumbnails, migrations.RunPython.noop),
]

View file

@ -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(

View file

@ -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
}
});

View file

@ -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);
})();

View file

@ -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(`<a href="#" id="lg-report" title="Notify abuse" class="lg-icon lg-bi-icon">${reportIcon}</a>`);
this.core.$toolbar.append(`<a href="#" id="lg-report" title="Request removal" class="lg-icon lg-bi-icon">${deleteIcon}</a>`);
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;

View file

@ -20,11 +20,13 @@ 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="user_id" value="{{ request.user.id }}">
<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/plugins/admin/lg-admin.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/zoom/lg-zoom.min.js' %}"></script>
<script src="{% static 'gallery_justified.js' %}"></script>
<script src="{% static 'gallery_detail.js' %}"></script>
<script src="{% static 'sweetalert.js' %}"></script>
<script src="{% static 'copy-button.js' %}"></script>
@ -87,10 +89,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
</ul>
</div>
{% endif %}
<div class="card-body row" id="lightgallery">
<div class="card-body p-0" id="lightgallery">
{% for photo in photos %}
<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 }}" 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 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 }}">
<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 %}">
</a>
{% endfor %}
</div>

View file

@ -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)

View file

@ -6,3 +6,7 @@ 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

View file

@ -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