commit 86cd7854c2463ef619e429a3b221799ba5f3f989 Author: denis defolie Date: Thu May 28 23:43:20 2026 +0200 First commit diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..8403648 --- /dev/null +++ b/.env.example @@ -0,0 +1,13 @@ +SECRET_KEY=changez-cette-cle-secrete-en-production +DEBUG=True +ALLOWED_HOSTS=localhost,127.0.0.1 +SITE_DOMAIN=your-domain.com + +EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend +EMAIL_HOST=smtp.example.com +EMAIL_PORT=587 +EMAIL_USE_TLS=True +EMAIL_HOST_USER=noreply@your-domain.com +EMAIL_HOST_PASSWORD=votre-mot-de-passe +DEFAULT_FROM_EMAIL=LOGIS +ADMIN_EMAIL=admin@your-domain.com diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c20ffb --- /dev/null +++ b/.gitignore @@ -0,0 +1,43 @@ +# ── Secrets — NE JAMAIS committer ──────────────────────────────────────────── +.env +*.env +context.md + +# ── Base de données ─────────────────────────────────────────────────────────── +db.sqlite3 +*.sqlite3 + +# ── Médias uploadés (photos des biens) ─────────────────────────────────────── +media/ + +# ── Fichiers générés ───────────────────────────────────────────────────────── +staticfiles/ +__pycache__/ +*.py[cod] +*.pyo +.cache/ + +# ── Environnement virtuel ───────────────────────────────────────────────────── +venv/ +.venv/ +env/ + +# ── Configs de déploiement (contiennent domaine/chemins réels) ─────────────── +conf/nginx.conf +conf/supervisor.conf +conf/gunicorn.conf.py + +# ── Logs ───────────────────────────────────────────────────────────────────── +*.log +logs/ + +# ── IDE / OS ────────────────────────────────────────────────────────────────── +.vscode/ +.idea/ +*.swp +*.swo +.DS_Store +Thumbs.db + +# ── Compilations traductions ────────────────────────────────────────────────── +locale/**/*.mo diff --git a/README.md b/README.md new file mode 100644 index 0000000..0666c4c --- /dev/null +++ b/README.md @@ -0,0 +1,134 @@ +# LOGIS PERSONNEL — Application immobilière + +Site vitrine bilingue (français / anglais) de gestion et présentation de biens immobiliers, construit avec Django 4.2. + +**URL de production** : https://logis.miraceti.net +**Admin** : https://logis.miraceti.net/admin/ + +--- + +## Stack technique + +| Composant | Technologie | +|---|---| +| Backend | Django 4.2, Python 3.13 | +| Serveur | Gunicorn 3 workers, port 8091 | +| Proxy | nginx (SSL certbot, rate-limit) | +| Process | Supervisor | +| Images | django-resized → WEBP automatique | +| Tri drag-and-drop | django-admin-sortable2 | +| Fichiers statiques | Whitenoise | +| Config | python-decouple (.env) | +| Base de données | SQLite | + +--- + +## Installation + +```bash +python3 -m venv venv +source venv/bin/activate +unset REQUESTS_CA_BUNDLE SSL_CERT_FILE CURL_CA_BUNDLE # VPS spécifique +pip install -r requirements.txt +cp .env.example .env # puis éditer .env +./manage.py migrate +./manage.py createsuperuser +./manage.py collectstatic +``` + +### Développement + +```bash +./manage.py runserver +``` + +Le fichier `manage.py` utilise `logis_project.settings.development` par défaut. + +### Production + +```bash +# Copier les configs +sudo cp conf/nginx.conf /etc/nginx/sites-available/logis +sudo ln -sf /etc/nginx/sites-available/logis /etc/nginx/sites-enabled/logis +sudo cp conf/supervisor.conf /etc/supervisor/conf.d/logis.conf + +# Créer les répertoires de logs +sudo mkdir -p /var/log/logis +sudo chown miraceti:miraceti /var/log/logis + +# Permissions home (nginx doit traverser le répertoire) +chmod 711 /home/miraceti + +# SSL +sudo certbot certonly --webroot -w /var/www/html -d logis.miraceti.net + +# Démarrer +sudo systemctl reload nginx +sudo supervisorctl reread && sudo supervisorctl update +sudo supervisorctl start logis +``` + +--- + +## Commandes courantes + +```bash +# Redémarrer l'application après modification +sudo supervisorctl restart logis + +# Voir les logs +tail -f /var/log/logis/gunicorn_error.log +tail -f /var/log/logis/django.log + +# Appliquer une migration +DJANGO_SETTINGS_MODULE=logis_project.settings.production ./manage.py migrate + +# Régénérer les statiques +DJANGO_SETTINGS_MODULE=logis_project.settings.production ./manage.py collectstatic --no-input + +# Traductions +./manage.py makemessages -l en +./manage.py compilemessages +``` + +--- + +## Variables d'environnement (.env) + +| Variable | Description | +|---|---| +| `SECRET_KEY` | Clé secrète Django | +| `DEBUG` | `False` en production | +| `ALLOWED_HOSTS` | Hosts autorisés, séparés par des virgules | +| `EMAIL_HOST` | Serveur SMTP | +| `EMAIL_PORT` | Port SMTP (587 pour STARTTLS) | +| `EMAIL_HOST_USER` | Identifiant SMTP | +| `EMAIL_HOST_PASSWORD` | Mot de passe SMTP | +| `EMAIL_USE_TLS` | `True` pour STARTTLS | +| `DEFAULT_FROM_EMAIL` | Adresse expéditeur | +| `ADMIN_EMAIL` | Destinataire des formulaires de contact | + +--- + +## Structure des répertoires + +``` +LOGIS/ +├── conf/ # nginx, gunicorn, supervisor +├── locale/ # Traductions fr/en (.po / .mo) +├── logis_project/ # Settings, urls, wsgi +│ └── settings/ +│ ├── base.py +│ ├── development.py +│ └── production.py +├── properties/ # Application principale +│ ├── models.py # Property, Room, Photo, ContactRequest… +│ ├── admin.py # Interface d'administration +│ ├── views.py # Liste, détail, contact (AJAX) +│ ├── sitemaps.py # Sitemap XML +│ └── templatetags/ # property_tags (filtres, lang_switch_url…) +├── static/ # Sources CSS / JS / images +├── staticfiles/ # Sortie collectstatic (servi par nginx) +├── templates/ # Templates HTML + emails + robots.txt +└── media/ # Uploads (photos des biens) +``` diff --git a/conf/gunicorn.conf.py.example b/conf/gunicorn.conf.py.example new file mode 100644 index 0000000..1326a7c --- /dev/null +++ b/conf/gunicorn.conf.py.example @@ -0,0 +1,22 @@ +""" +Configuration Gunicorn pour LOGIS en production. +""" +# Adresse d'écoute (nginx fait le proxy depuis le port 80/443) +bind = '127.0.0.1:8091' + +# Nombre de workers : (2 × CPU) + 1 est une heuristique classique +workers = 3 +worker_class = 'sync' +timeout = 120 +keepalive = 5 +max_requests = 1000 +max_requests_jitter = 50 + +# Journaux +accesslog = '/var/log/logis/gunicorn_access.log' +errorlog = '/var/log/logis/gunicorn_error.log' +loglevel = 'info' +capture_output = True + +# Répertoire de travail +chdir = '/path/to/LOGIS' diff --git a/conf/nginx.conf.example b/conf/nginx.conf.example new file mode 100644 index 0000000..7fb1fb5 --- /dev/null +++ b/conf/nginx.conf.example @@ -0,0 +1,90 @@ +# Configuration nginx pour LOGIS +# Copier dans : /etc/nginx/sites-available/logis +# Puis : ln -s /etc/nginx/sites-available/logis /etc/nginx/sites-enabled/ +# SSL fourni par : certbot --nginx -d your-domain.com + +# Rate-limit sur le formulaire de contact : 10 requêtes/min par IP +limit_req_zone $binary_remote_addr zone=contact:10m rate=10r/m; + +# Redirection HTTP → HTTPS +server { + listen 80; + listen [::]:80; + server_name your-domain.com; + + location /.well-known/acme-challenge/ { + root /var/www/html; + } + + location / { + return 301 https://$host$request_uri; + } +} + +# Serveur HTTPS principal +server { + listen 443 ssl; + listen [::]:443 ssl; + server_name your-domain.com; + + # Certificats SSL (générés par certbot) + ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem; + include /etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + + # Sécurité des en-têtes + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; + add_header X-Content-Type-Options nosniff always; + add_header X-Frame-Options DENY always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # Taille max des uploads (photos haute résolution) + client_max_body_size 25M; + + # Fichiers statiques servis directement par nginx + location /static/ { + alias /path/to/LOGIS/staticfiles/; + expires 30d; + add_header Cache-Control "public, immutable"; + access_log off; + } + + # Fichiers médias (photos uploadées) + location /media/ { + alias /path/to/LOGIS/media/; + expires 7d; + add_header Cache-Control "public"; + access_log off; + } + + # Scans automatiques — drop silencieux (pas de réponse) + location ~* \.(php|asp|aspx|jsp|cgi|pl|sh|bash|env|git|svn|htaccess)$ { + return 444; + } + location ~* ^/(cgi-bin|vendor|phpunit|Autodiscover|autodiscover|wp-admin|wp-login|phpmyadmin|admin\.php|xmlrpc\.php|shell|config|\.well-known/(?!acme)) { + return 444; + } + + # Rate-limit sur l'endpoint de contact (fr + en) + location ~* ^/(en/)?contact/ { + limit_req zone=contact burst=5 nodelay; + proxy_pass http://127.0.0.1:8091; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Proxy vers Gunicorn (port 8091) + location / { + proxy_pass http://127.0.0.1:8091; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 120s; + proxy_buffering on; + } +} diff --git a/conf/setup_production.sh b/conf/setup_production.sh new file mode 100644 index 0000000..9eb9942 --- /dev/null +++ b/conf/setup_production.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# Script d'installation et de configuration de LOGIS en production +# Exécuter une seule fois sur le VPS en tant que l'utilisateur applicatif +# Usage : bash conf/setup_production.sh + +set -euo pipefail + +BASE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +VENV="$BASE_DIR/venv" +LOG_DIR="/var/log/logis" + +echo "==> Répertoire de l'application : $BASE_DIR" + +# ── 1. Répertoires de logs ──────────────────────────────────────────────────── +echo "==> Création du répertoire de logs…" +sudo mkdir -p "$LOG_DIR" +sudo chown "$USER:$USER" "$LOG_DIR" +sudo chmod 755 "$LOG_DIR" + +# ── 2. Environnement virtuel Python ────────────────────────────────────────── +if [ ! -d "$VENV" ]; then + echo "==> Création du virtualenv…" + python3 -m venv "$VENV" +fi +echo "==> Installation des dépendances…" +"$VENV/bin/pip" install --upgrade pip +"$VENV/bin/pip" install -r "$BASE_DIR/requirements.txt" + +# ── 3. Fichier .env de production ──────────────────────────────────────────── +if [ ! -f "$BASE_DIR/.env" ]; then + echo "==> Copie de .env.example → .env (à personnaliser !)" + cp "$BASE_DIR/.env.example" "$BASE_DIR/.env" + echo "" + echo " ⚠ Éditez maintenant $BASE_DIR/.env avec vos vraies valeurs" + echo " (SECRET_KEY, EMAIL_*, ALLOWED_HOSTS, SITE_DOMAIN…)" + echo "" +fi + +# ── 4. Migrations et fichiers statiques ────────────────────────────────────── +echo "==> Migrations…" +DJANGO_SETTINGS_MODULE=logis_project.settings.production \ + "$VENV/bin/python" "$BASE_DIR/manage.py" migrate --noinput + +echo "==> Fichiers statiques…" +DJANGO_SETTINGS_MODULE=logis_project.settings.production \ + "$VENV/bin/python" "$BASE_DIR/manage.py" collectstatic --noinput + +echo "==> Compilation des traductions…" +DJANGO_SETTINGS_MODULE=logis_project.settings.production \ + "$VENV/bin/python" "$BASE_DIR/manage.py" compilemessages + +# ── 5. Supervisor ──────────────────────────────────────────────────────────── +echo "==> Installation du service Supervisor…" +sudo cp "$BASE_DIR/conf/supervisor.conf" /etc/supervisor/conf.d/logis.conf +sudo supervisorctl reread +sudo supervisorctl update + +# ── 6. Nginx ───────────────────────────────────────────────────────────────── +echo "==> Installation de la configuration nginx…" +sudo cp "$BASE_DIR/conf/nginx.conf" /etc/nginx/sites-available/logis +if [ ! -f /etc/nginx/sites-enabled/logis ]; then + sudo ln -s /etc/nginx/sites-available/logis /etc/nginx/sites-enabled/logis +fi +sudo nginx -t && sudo systemctl reload nginx + +# ── 7. Certbot SSL ──────────────────────────────────────────────────────────── +echo "" +echo "==> Pour générer le certificat SSL, exécutez :" +echo " sudo certbot --nginx -d \$(grep SITE_DOMAIN $BASE_DIR/.env | cut -d= -f2)" +echo "" + +echo "✓ Installation terminée." +echo " Créez un superutilisateur : $VENV/bin/python $BASE_DIR/manage.py createsuperuser" diff --git a/conf/supervisor.conf.example b/conf/supervisor.conf.example new file mode 100644 index 0000000..ffc0c87 --- /dev/null +++ b/conf/supervisor.conf.example @@ -0,0 +1,21 @@ +; Configuration Supervisor pour le processus Gunicorn de LOGIS +; Copier dans : /etc/supervisor/conf.d/logis.conf +; Puis : supervisorctl reread && supervisorctl update + +[program:logis] +command=/path/to/LOGIS/venv/bin/gunicorn + --config /path/to/LOGIS/conf/gunicorn.conf.py + logis_project.wsgi:application +directory=/path/to/LOGIS +environment=DJANGO_SETTINGS_MODULE="logis_project.settings.production" +user=your-user +group=your-user +autostart=true +autorestart=true +startretries=3 +redirect_stderr=true +stdout_logfile=/var/log/logis/supervisor.log +stdout_logfile_maxbytes=10MB +stdout_logfile_backups=5 +stopasgroup=true +killasgroup=true diff --git a/locale/en/LC_MESSAGES/django.po b/locale/en/LC_MESSAGES/django.po new file mode 100644 index 0000000..e2fd36d --- /dev/null +++ b/locale/en/LC_MESSAGES/django.po @@ -0,0 +1,2180 @@ +# LOGIS — English translations +# Copyright (C) 2025 LOGIS +msgid "" +msgstr "" +"Project-Id-Version: LOGIS 1.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-05-28 15:32+0200\n" +"Language-Team: English\n" +"Language: en\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" + +#: logis_project/settings/base.py:75 +msgid "Français" +msgstr "Français" + +#: logis_project/settings/base.py:76 +msgid "English" +msgstr "English" + +#: properties/admin.py:58 +msgid "Aperçu" +msgstr "Preview" + +#: properties/admin.py:67 properties/admin.py:142 +msgid "Localisation" +msgstr "Location" + +#: properties/admin.py:70 templates/properties/detail.html:126 +msgid "Commerces de proximité" +msgstr "Nearby shops" + +#: properties/admin.py:78 +msgid "Services mairie / éducation" +msgstr "Town hall services / education" + +#: properties/admin.py:84 +msgid "Services de santé" +msgstr "Health services" + +#: properties/admin.py:111 +msgid "Identification" +msgstr "Identification" + +#: properties/admin.py:114 +msgid "Classification" +msgstr "Classification" + +#: properties/admin.py:117 +#, fuzzy +#| msgid "Surface terrain" +msgid "Surfaces" +msgstr "Land area" + +#: properties/admin.py:120 +#, fuzzy +#| msgid "Exposition" +msgid "Composition" +msgstr "Exposure" + +#: properties/admin.py:123 +msgid "Équipements" +msgstr "Equipment" + +#: properties/admin.py:130 +#, fuzzy +#| msgid "Année de construction" +msgid "Construction" +msgstr "Year built" + +#: properties/admin.py:133 +msgid "Diagnostics énergétiques" +msgstr "energy audits" + +#: properties/admin.py:139 templates/properties/detail.html:100 +msgid "Description" +msgstr "Description" + +#: properties/admin.py:145 +msgid "Image de couverture" +msgstr "Cover image" + +#: properties/admin.py:152 +msgid "Type" +msgstr "Type" + +#: properties/admin.py:160 +msgid "Statut" +msgstr "Status" + +#: properties/admin.py:164 +msgid "Prix" +msgstr "Price" + +#: properties/admin.py:168 +#, fuzzy +#| msgid "Surface terrain" +msgid "Surface" +msgstr "Land area" + +#: properties/admin.py:172 templates/properties/partials/dpe_scale.html:10 +msgid "DPE" +msgstr "EPC" + +#: properties/admin.py:176 +msgid "GES" +msgstr "GES" + +#: properties/forms.py:19 templates/properties/partials/contact_modal.html:32 +msgid "Nom complet" +msgstr "Full name" + +#: properties/forms.py:21 templates/properties/partials/contact_modal.html:35 +msgid "Votre nom et prénom" +msgstr "Your full name" + +#: properties/forms.py:26 templates/properties/partials/contact_modal.html:40 +msgid "Adresse email" +msgstr "Email address" + +#: properties/forms.py:28 templates/properties/partials/contact_modal.html:43 +msgid "votre@email.com" +msgstr "your@email.com" + +#: properties/forms.py:35 templates/properties/partials/contact_modal.html:48 +msgid "Téléphone" +msgstr "Phone" + +#: properties/forms.py:37 +msgid "+33 6 XX XX XX XX" +msgstr "" + +#: properties/forms.py:42 templates/properties/partials/contact_modal.html:56 +msgid "Message" +msgstr "Message" + +#: properties/forms.py:44 templates/properties/partials/contact_modal.html:58 +msgid "Votre message, vos questions…" +msgstr "Your message or questions…" + +#: properties/forms.py:67 +msgid "Veuillez indiquer votre nom complet." +msgstr "Please enter your full name." + +#: properties/forms.py:73 +msgid "Votre message est trop court." +msgstr "Your message is too short." + +#: properties/models.py:28 properties/models.py:79 properties/models.py:284 +#, fuzzy +#| msgid "Français" +msgid "nom (français)" +msgstr "Français" + +#: properties/models.py:29 properties/models.py:80 properties/models.py:285 +#, fuzzy +#| msgid "English" +msgid "name (English)" +msgstr "English" + +#: properties/models.py:31 +msgid "icône" +msgstr "Icon" + +#: properties/models.py:32 +msgid "Classe CSS ou caractère unicode (ex: 🔥)" +msgstr "CSS class or Unicode character (e.g., 🔥)" + +#: properties/models.py:36 +#, fuzzy +#| msgid "Caractéristiques" +msgid "caractéristique" +msgstr "Features" + +#: properties/models.py:37 properties/models.py:144 +#, fuzzy +#| msgid "Caractéristiques" +msgid "caractéristiques" +msgstr "Features" + +#: properties/models.py:53 +msgid "Maison" +msgstr "House" + +#: properties/models.py:54 +msgid "Appartement" +msgstr "Apartment" + +#: properties/models.py:55 +msgid "Terrain" +msgstr "Ground" + +#: properties/models.py:56 +msgid "Local commercial" +msgstr "Commercial premises" + +#: properties/models.py:57 +msgid "Autre" +msgstr "Other" + +#: properties/models.py:60 templates/index.html:20 templates/index.html:77 +#: templates/properties/detail.html:18 +msgid "À vendre" +msgstr "For sale" + +#: properties/models.py:61 templates/index.html:20 templates/index.html:77 +#: templates/properties/detail.html:18 +msgid "À louer" +msgstr "For rent" + +#: properties/models.py:66 +msgid "Nord" +msgstr "North" + +#: properties/models.py:66 +msgid "Sud" +msgstr "South" + +#: properties/models.py:66 +msgid "Est" +msgstr "East" + +#: properties/models.py:66 +msgid "Ouest" +msgstr "West" + +#: properties/models.py:67 +msgid "Nord-Est" +msgstr "Northeast" + +#: properties/models.py:67 +msgid "Nord-Ouest" +msgstr "Northwest" + +#: properties/models.py:68 +msgid "Sud-Est" +msgstr "Southeast" + +#: properties/models.py:68 +msgid "Sud-Ouest" +msgstr "Southwest" + +#: properties/models.py:71 +msgid "Neuf" +msgstr "New" + +#: properties/models.py:72 +msgid "Exellent état" +msgstr "Excellent" + +#: properties/models.py:73 +msgid "Bon état" +msgstr "Good" + +#: properties/models.py:74 +msgid "À rafraîchir" +msgstr "Needs refreshing" + +#: properties/models.py:75 +msgid "À rénover" +msgstr "Needs renovation" + +#: properties/models.py:81 +msgid "slug" +msgstr "slug" + +#: properties/models.py:82 +#, fuzzy +#| msgid "Type de bien" +msgid "type de bien" +msgstr "Estate type" + +#: properties/models.py:83 +msgid "statut" +msgstr "Status" + +#: properties/models.py:84 +msgid "actif / visible" +msgstr "active / visible" + +#: properties/models.py:87 +msgid "prix (€)" +msgstr "price (€)" + +#: properties/models.py:88 +#, fuzzy +#| msgid "Prix sur demande" +msgid "prix sur demande" +msgstr "Price on request" + +#: properties/models.py:92 +#, fuzzy +#| msgid "Surface terrain" +msgid "surface totale du terrain (m²)" +msgstr "Land area" + +#: properties/models.py:95 +#, fuzzy +#| msgid "Surface habitable" +msgid "surface habitable (m²)" +msgstr "Living area" + +#: properties/models.py:96 +msgid "surface totale (m²)" +msgstr "total area (m²)" + +#: properties/models.py:98 +#, fuzzy +#| msgid "Nombre de pièces" +msgid "nombre de pièces" +msgstr "Rooms" + +#: properties/models.py:99 +#, fuzzy +#| msgid "Nombre de pièces" +msgid "nombre de chambres" +msgstr "Rooms" + +#: properties/models.py:100 +#, fuzzy +#| msgid "Salles d'eau" +msgid "nombre de salles d'eau" +msgstr "Bathrooms" + +#: properties/models.py:103 +#, fuzzy +#| msgid "Ascenseur" +msgid "ascenseur" +msgstr "Elevator" + +#: properties/models.py:104 +#, fuzzy +#| msgid "Exposition" +msgid "exposition" +msgstr "Exposure" + +#: properties/models.py:105 +#, fuzzy +#| msgid "Jardin" +msgid "jardin" +msgstr "Garden" + +#: properties/models.py:106 +#, fuzzy +#| msgid "Piscine" +msgid "piscine" +msgstr "Swimming pool" + +#: properties/models.py:107 +#, fuzzy +#| msgid "Dépendances" +msgid "dépendances" +msgstr "Outbuildings" + +#: properties/models.py:110 +#, fuzzy +#| msgid "Année de construction" +msgid "année de construction" +msgstr "Year built" + +#: properties/models.py:111 +#, fuzzy +#| msgid "État du bien" +msgid "état du bien" +msgstr "Condition" + +#: properties/models.py:114 +#, fuzzy +#| msgid "Classe énergie (DPE)" +msgid "classe énergie (DPE)" +msgstr "Energy class (DPE)" + +#: properties/models.py:116 +msgid "valeur DPE (kWh/m²/an)" +msgstr "Energy Performance Certificate (EPC) value (kWh/m²/year)" + +#: properties/models.py:118 +msgid "classe GES" +msgstr "GES class" + +#: properties/models.py:120 +msgid "valeur GES (kgCO₂/m²/an)" +msgstr "GHG value (kgCO₂/m²/year)" + +#: properties/models.py:124 +msgid "description (français)" +msgstr "description (French)" + +#: properties/models.py:125 +#, fuzzy +#| msgid "Description" +msgid "description (English)" +msgstr "Description (English)" + +#: properties/models.py:128 +#, fuzzy +#| msgid "Adresse email" +msgid "adresse" +msgstr "Email address" + +#: properties/models.py:129 +msgid "ville" +msgstr "city" + +#: properties/models.py:130 +msgid "code postal" +msgstr "postal code" + +#: properties/models.py:131 +msgid "latitude" +msgstr "latitude" + +#: properties/models.py:132 +msgid "longitude" +msgstr "longitude" + +#: properties/models.py:138 +msgid "image de couverture" +msgstr "cover image" + +#: properties/models.py:147 +msgid "créé le" +msgstr "created the" + +#: properties/models.py:148 +msgid "modifié le" +msgstr "modified on" + +#: properties/models.py:151 +#, fuzzy +#| msgid "Biens immobiliers" +msgid "bien immobilier" +msgstr "Properties" + +#: properties/models.py:152 +#, fuzzy +#| msgid "Biens immobiliers" +msgid "biens immobiliers" +msgstr "Properties" + +#: properties/models.py:186 templates/index.html:29 templates/index.html:107 +#: templates/properties/detail.html:27 +msgid "Prix sur demande" +msgstr "Price on request" + +#: properties/models.py:198 +#, fuzzy +#| msgid "Village" +msgid "Ville" +msgstr "Village" + +#: properties/models.py:199 templates/properties/detail.html:114 +msgid "Village" +msgstr "Village" + +#: properties/models.py:200 templates/properties/detail.html:115 +msgid "Campagne" +msgstr "Countryside" + +#: properties/models.py:205 properties/models.py:282 properties/models.py:307 +msgid "bien" +msgstr "estate" + +#: properties/models.py:207 +msgid "type de localisation" +msgstr "location type" + +#: properties/models.py:208 +#, fuzzy +#| msgid "Situation géographique" +msgid "description géographique (français)" +msgstr "Location & surroundings" + +#: properties/models.py:209 +msgid "geographic description (English)" +msgstr "geographic description (English)" + +#: properties/models.py:212 +msgid "supérette" +msgstr "mini-market" + +#: properties/models.py:213 +msgid "alimentation" +msgstr "food" + +#: properties/models.py:214 +msgid "boulangerie" +msgstr "bakery" + +#: properties/models.py:215 +msgid "journaux / tabac presse" +msgstr "newspapers / tobacco press" + +#: properties/models.py:216 +msgid "bar / café" +msgstr "bar / cafe" + +#: properties/models.py:217 +msgid "bureau de tabac" +msgstr "tobacconist's" + +#: properties/models.py:218 +msgid "garage / garagiste" +msgstr "garage / mechanic" + +#: properties/models.py:219 +msgid "station essence" +msgstr "gas station" + +#: properties/models.py:222 +msgid "services de la mairie" +msgstr "municipal services" + +#: properties/models.py:223 +msgid "crèche" +msgstr "nursery" + +#: properties/models.py:224 +msgid "école publique" +msgstr "public school" + +#: properties/models.py:227 +msgid "médecin" +msgstr "doctor" + +#: properties/models.py:228 +msgid "pharmacie" +msgstr "pharmacy" + +#: properties/models.py:229 +msgid "cabinet kinésithérapeute" +msgstr "physiotherapist's office" + +#: properties/models.py:230 +msgid "cabinet infirmier" +msgstr "nursing office" + +#: properties/models.py:233 +#, fuzzy +#| msgid "Environnement urbain" +msgid "environnement" +msgstr "Urban area" + +#: properties/models.py:234 +#, fuzzy +#| msgid "Renseignements" +msgid "environnements" +msgstr "Enquiries" + +#: properties/models.py:249 +msgid "Supérette" +msgstr "Mini-market" + +#: properties/models.py:250 +msgid "Alimentation" +msgstr "Food" + +#: properties/models.py:251 +msgid "Boulangerie" +msgstr "Bakery" + +#: properties/models.py:252 +msgid "Journaux / Presse" +msgstr "Newspapers / Press" + +#: properties/models.py:253 +msgid "Bar / Café" +msgstr "Bar / Cafe" + +#: properties/models.py:254 +msgid "Bureau de tabac" +msgstr "Tobacco shop" + +#: properties/models.py:255 +msgid "Garagiste" +msgstr "Garage owner" + +#: properties/models.py:256 +msgid "Station essence" +msgstr "Gas station" + +#: properties/models.py:262 +#, fuzzy +#| msgid "Services publics" +msgid "Services mairie" +msgstr "Public services" + +#: properties/models.py:263 +msgid "Crèche" +msgstr "Nursery" + +#: properties/models.py:264 +msgid "École publique" +msgstr "Public school" + +#: properties/models.py:270 +msgid "Médecin" +msgstr "Doctor" + +#: properties/models.py:271 +msgid "Pharmacie" +msgstr "Pharmacy" + +#: properties/models.py:272 +msgid "Kinésithérapeute" +msgstr "Physiotherapist" + +#: properties/models.py:273 +msgid "Cabinet infirmier" +msgstr "Nursing office" + +#: properties/models.py:286 properties/models.py:321 +msgid "ordre" +msgstr "order" + +#: properties/models.py:289 properties/models.py:312 +#, fuzzy +#| msgid "pièces" +msgid "pièce" +msgstr "rooms" + +#: properties/models.py:290 templates/index.html:24 +msgid "pièces" +msgstr "rooms" + +#: properties/models.py:316 +msgid "image" +msgstr "image" + +#: properties/models.py:318 +msgid "légende (français)" +msgstr "legend (French)" + +#: properties/models.py:319 +msgid "caption (English)" +msgstr "" + +#: properties/models.py:320 +msgid "photo de couverture" +msgstr "cover photo" + +#: properties/models.py:324 +msgid "photo" +msgstr "photo" + +#: properties/models.py:325 +#, fuzzy +#| msgid "Galerie photos" +msgid "photos" +msgstr "Photo gallery" + +#: properties/models.py:343 +msgid "bien concerné" +msgstr "Estate concerned" + +#: properties/models.py:345 +msgid "nom" +msgstr "Name" + +#: properties/models.py:346 +msgid "email" +msgstr "email" + +#: properties/models.py:347 +#, fuzzy +#| msgid "Téléphone" +msgid "téléphone" +msgstr "Phone" + +#: properties/models.py:348 +#, fuzzy +#| msgid "Message" +msgid "message" +msgstr "Message" + +#: properties/models.py:349 +#, fuzzy +#| msgid "Adresse email" +msgid "adresse IP" +msgstr "Email address" + +#: properties/models.py:350 +msgid "reçu le" +msgstr "received" + +#: properties/models.py:353 +#, fuzzy +#| msgid "Année de construction" +msgid "demande de contact" +msgstr "Year built" + +#: properties/models.py:354 +msgid "demandes de contact" +msgstr "contact requests" + +#: properties/views.py:93 +#, python-format +msgid "" +"Merci de patienter encore %(min)s minute(s) avant d'envoyer un nouveau " +"message." +msgstr "Please wait %(min)s more minute(s) before sending another message." + +#: properties/views.py:139 +msgid "" +"Votre message a bien été envoyé. Nous vous répondrons dans les plus brefs " +"délais." +msgstr "" +"Your message has been sent. We will get back to you as soon as possible." + +#: templates/base.html:6 +msgid "Biens immobiliers à vendre et à louer — LOGIS" +msgstr "Properties for sale and rent — LOGIS" + +#: templates/base.html:7 +msgid "Immobilier" +msgstr "Real Estate" + +#: templates/base.html:23 +msgid "Accueil LOGIS" +msgstr "LOGIS Home" + +#: templates/base.html:32 +msgid "Choisir la langue" +msgstr "Choose language" + +#: templates/base.html:58 +msgid "Transactions immobilières" +msgstr "Real Estate" + +#: templates/base.html:59 +msgid "Tous droits réservés." +msgstr "All rights reserved." + +#: templates/base.html:64 +msgid "Galerie photos" +msgstr "Photo gallery" + +#: templates/base.html:67 templates/properties/partials/contact_modal.html:7 +msgid "Fermer" +msgstr "Close" + +#: templates/base.html:68 +msgid "Photo précédente" +msgstr "Previous photo" + +#: templates/base.html:73 +msgid "Photo suivante" +msgstr "Next photo" + +#: templates/index.html:4 +msgid "Biens immobiliers" +msgstr "Properties" + +#: templates/index.html:32 +msgid "Découvrir ce bien" +msgstr "View estate" + +#: templates/index.html:41 +msgid "Nos biens immobiliers" +msgstr "Our estates" + +#: templates/index.html:42 +msgid "Trouvez votre futur chez-vous" +msgstr "Find your future home" + +#: templates/index.html:53 +msgid "Aucun bien immobilier disponible pour le moment." +msgstr "No properties available at the moment." + +#: templates/index.html:91 templates/properties/detail.html:176 +msgid "Surface habitable" +msgstr "Living area" + +#: templates/index.html:94 +msgid "Pièces" +msgstr "Rooms" + +#: templates/index.html:95 +msgid "p." +msgstr "r." + +#: templates/index.html:97 templates/properties/detail.html:190 +msgid "Chambres" +msgstr "Bedrooms" + +#: templates/index.html:100 +#, fuzzy +#| msgid "Salles d'eau" +msgid "Salles d\\" +msgstr "Bathrooms" + +#: templates/index.html:110 templates/properties/detail.html:30 +msgid "/ mois" +msgstr "/ month" + +#: templates/properties/detail.html:14 +msgid "Retour aux annonces" +msgstr "Back to listings" + +#: templates/properties/detail.html:40 +msgid "Photos par pièce" +msgstr "Photos by room" + +#: templates/properties/detail.html:44 +msgid "Toutes" +msgstr "All" + +#: templates/properties/detail.html:89 +msgid "Aucune photo disponible." +msgstr "No photos available." + +#: templates/properties/detail.html:109 +msgid "Situation géographique" +msgstr "Location & surroundings" + +#: templates/properties/detail.html:113 +msgid "Environnement urbain" +msgstr "Urban area" + +#: templates/properties/detail.html:138 +msgid "Services publics" +msgstr "Public services" + +#: templates/properties/detail.html:150 +msgid "Santé" +msgstr "Healthcare" + +#: templates/properties/detail.html:171 +msgid "Informations clés" +msgstr "Key facts" + +#: templates/properties/detail.html:173 +msgid "Type de bien" +msgstr "Estate type" + +#: templates/properties/detail.html:179 +#, fuzzy +#| msgid "Surface habitable" +msgid "Surface totale" +msgstr "Living area" + +#: templates/properties/detail.html:183 +msgid "Surface terrain" +msgstr "Land area" + +#: templates/properties/detail.html:187 +msgid "Nombre de pièces" +msgstr "Rooms" + +#: templates/properties/detail.html:193 +msgid "Salles d'eau" +msgstr "Bathrooms" + +#: templates/properties/detail.html:197 +msgid "Exposition" +msgstr "Exposure" + +#: templates/properties/detail.html:202 +msgid "Année de construction" +msgstr "Year built" + +#: templates/properties/detail.html:206 +msgid "État du bien" +msgstr "Condition" + +#: templates/properties/detail.html:209 +msgid "Ascenseur" +msgstr "Elevator" + +#: templates/properties/detail.html:210 +msgid "Oui" +msgstr "Yes" + +#: templates/properties/detail.html:210 +msgid "Non" +msgstr "No" + +#: templates/properties/detail.html:217 +msgid "Jardin" +msgstr "Garden" + +#: templates/properties/detail.html:220 +msgid "Piscine" +msgstr "Swimming pool" + +#: templates/properties/detail.html:223 +msgid "Dépendances" +msgstr "Outbuildings" + +#: templates/properties/detail.html:231 +msgid "Caractéristiques" +msgstr "Features" + +#: templates/properties/detail.html:247 +msgid "Diagnostics" +msgstr "Energy diagnostics" + +#: templates/properties/detail.html:251 +msgid "Classe énergie (DPE)" +msgstr "Energy class (DPE)" + +#: templates/properties/detail.html:258 +msgid "Émissions GES" +msgstr "GHG emissions" + +#: templates/properties/detail.html:271 +msgid "Renseignements" +msgstr "Enquiries" + +#: templates/properties/detail.html:273 +msgid "Vous souhaitez visiter ce bien ou obtenir plus d'informations ?" +msgstr "Would you like to arrange a viewing or get more information?" + +#: templates/properties/detail.html:277 +msgid "Envoyer un message" +msgstr "Send a message" + +#: templates/properties/detail.html:315 +msgid "sur" +msgstr "of" + +#: templates/properties/detail.html:316 +msgid "Envoi en cours…" +msgstr "Sending…" + +#: templates/properties/detail.html:317 +#: templates/properties/partials/contact_modal.html:69 +msgid "Envoyer" +msgstr "Send" + +#: templates/properties/detail.html:318 +#: templates/properties/partials/contact_modal.html:16 +msgid "Message envoyé !" +msgstr "Message sent!" + +#: templates/properties/detail.html:319 +#: templates/properties/partials/contact_modal.html:17 +msgid "Nous vous répondrons dans les plus brefs délais." +msgstr "We will get back to you as soon as possible." + +#: templates/properties/partials/contact_modal.html:10 +msgid "Demande de renseignements" +msgstr "Property enquiry" + +#: templates/properties/partials/contact_modal.html:48 +msgid "facultatif" +msgstr "optional" + +#: templates/properties/partials/contact_modal.html:63 +msgid "" +"* champs obligatoires — vos coordonnées ne seront utilisées que pour " +"répondre à votre demande." +msgstr "" +"* required fields — your details will only be used to respond to your " +"enquiry." + +#: templates/properties/partials/dpe_scale.html:4 +msgid "Échelle de performance" +msgstr "Performance scale" + +#: venv/lib/python3.13/site-packages/django/contrib/messages/apps.py:15 +#, fuzzy +#| msgid "Message" +msgid "Messages" +msgstr "Message" + +#: venv/lib/python3.13/site-packages/django/contrib/sitemaps/apps.py:8 +msgid "Site Maps" +msgstr "Site Maps" + +#: venv/lib/python3.13/site-packages/django/contrib/staticfiles/apps.py:9 +msgid "Static Files" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/contrib/syndication/apps.py:7 +msgid "Syndication" +msgstr "" + +#. Translators: String used to replace omitted page numbers in elided page +#. range generated by paginators, e.g. [1, 2, '…', 5, 6, 7, '…', 9, 10]. +#: venv/lib/python3.13/site-packages/django/core/paginator.py:30 +msgid "…" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/core/paginator.py:50 +msgid "That page number is not an integer" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/core/paginator.py:52 +msgid "That page number is less than 1" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/core/paginator.py:54 +msgid "That page contains no results" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/core/validators.py:22 +msgid "Enter a valid value." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/core/validators.py:104 +#: venv/lib/python3.13/site-packages/django/forms/fields.py:751 +msgid "Enter a valid URL." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/core/validators.py:165 +msgid "Enter a valid integer." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/core/validators.py:176 +msgid "Enter a valid email address." +msgstr "" + +#. Translators: "letters" means latin letters: a-z and A-Z. +#: venv/lib/python3.13/site-packages/django/core/validators.py:259 +msgid "" +"Enter a valid “slug” consisting of letters, numbers, underscores or hyphens." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/core/validators.py:267 +msgid "" +"Enter a valid “slug” consisting of Unicode letters, numbers, underscores, or " +"hyphens." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/core/validators.py:281 +#: venv/lib/python3.13/site-packages/django/core/validators.py:289 +#: venv/lib/python3.13/site-packages/django/core/validators.py:318 +msgid "Enter a valid IPv4 address." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/core/validators.py:298 +#: venv/lib/python3.13/site-packages/django/core/validators.py:319 +msgid "Enter a valid IPv6 address." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/core/validators.py:310 +#: venv/lib/python3.13/site-packages/django/core/validators.py:317 +msgid "Enter a valid IPv4 or IPv6 address." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/core/validators.py:353 +msgid "Enter only digits separated by commas." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/core/validators.py:359 +#, python-format +msgid "Ensure this value is %(limit_value)s (it is %(show_value)s)." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/core/validators.py:394 +#, python-format +msgid "Ensure this value is less than or equal to %(limit_value)s." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/core/validators.py:403 +#, python-format +msgid "Ensure this value is greater than or equal to %(limit_value)s." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/core/validators.py:412 +#, python-format +msgid "Ensure this value is a multiple of step size %(limit_value)s." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/core/validators.py:422 +#, python-format +msgid "" +"Ensure this value has at least %(limit_value)d character (it has " +"%(show_value)d)." +msgid_plural "" +"Ensure this value has at least %(limit_value)d characters (it has " +"%(show_value)d)." +msgstr[0] "" +msgstr[1] "" + +#: venv/lib/python3.13/site-packages/django/core/validators.py:440 +#, python-format +msgid "" +"Ensure this value has at most %(limit_value)d character (it has " +"%(show_value)d)." +msgid_plural "" +"Ensure this value has at most %(limit_value)d characters (it has " +"%(show_value)d)." +msgstr[0] "" +msgstr[1] "" + +#: venv/lib/python3.13/site-packages/django/core/validators.py:463 +#: venv/lib/python3.13/site-packages/django/forms/fields.py:346 +#: venv/lib/python3.13/site-packages/django/forms/fields.py:385 +msgid "Enter a number." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/core/validators.py:465 +#, python-format +msgid "Ensure that there are no more than %(max)s digit in total." +msgid_plural "Ensure that there are no more than %(max)s digits in total." +msgstr[0] "" +msgstr[1] "" + +#: venv/lib/python3.13/site-packages/django/core/validators.py:470 +#, python-format +msgid "Ensure that there are no more than %(max)s decimal place." +msgid_plural "Ensure that there are no more than %(max)s decimal places." +msgstr[0] "" +msgstr[1] "" + +#: venv/lib/python3.13/site-packages/django/core/validators.py:475 +#, python-format +msgid "" +"Ensure that there are no more than %(max)s digit before the decimal point." +msgid_plural "" +"Ensure that there are no more than %(max)s digits before the decimal point." +msgstr[0] "" +msgstr[1] "" + +#: venv/lib/python3.13/site-packages/django/core/validators.py:546 +#, python-format +msgid "" +"File extension “%(extension)s” is not allowed. Allowed extensions are: " +"%(allowed_extensions)s." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/core/validators.py:607 +msgid "Null characters are not allowed." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/base.py:1423 +#: venv/lib/python3.13/site-packages/django/forms/models.py:893 +msgid "and" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/base.py:1425 +#, python-format +msgid "%(model_name)s with this %(field_labels)s already exists." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/constraints.py:17 +#, python-format +msgid "Constraint “%(name)s” is violated." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:128 +#, python-format +msgid "Value %(value)r is not a valid choice." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:129 +msgid "This field cannot be null." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:130 +msgid "This field cannot be blank." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:131 +#, python-format +msgid "%(model_name)s with this %(field_label)s already exists." +msgstr "" + +#. Translators: The 'lookup_type' is one of 'date', 'year' or +#. 'month'. Eg: "Title must be unique for pub_date year" +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:135 +#, python-format +msgid "" +"%(field_label)s must be unique for %(date_field_label)s %(lookup_type)s." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:173 +#, python-format +msgid "Field of type: %(field_type)s" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:1094 +#, python-format +msgid "“%(value)s” value must be either True or False." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:1095 +#, python-format +msgid "“%(value)s” value must be either True, False, or None." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:1097 +msgid "Boolean (Either True or False)" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:1147 +#, python-format +msgid "String (up to %(max_length)s)" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:1149 +msgid "String (unlimited)" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:1253 +msgid "Comma-separated integers" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:1354 +#, python-format +msgid "" +"“%(value)s” value has an invalid date format. It must be in YYYY-MM-DD " +"format." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:1358 +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:1493 +#, python-format +msgid "" +"“%(value)s” value has the correct format (YYYY-MM-DD) but it is an invalid " +"date." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:1362 +msgid "Date (without time)" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:1489 +#, python-format +msgid "" +"“%(value)s” value has an invalid format. It must be in YYYY-MM-DD " +"HH:MM[:ss[.uuuuuu]][TZ] format." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:1497 +#, python-format +msgid "" +"“%(value)s” value has the correct format (YYYY-MM-DD HH:MM[:ss[.uuuuuu]]" +"[TZ]) but it is an invalid date/time." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:1502 +msgid "Date (with time)" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:1626 +#, python-format +msgid "“%(value)s” value must be a decimal number." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:1628 +msgid "Decimal number" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:1789 +#, python-format +msgid "" +"“%(value)s” value has an invalid format. It must be in [DD] " +"[[HH:]MM:]ss[.uuuuuu] format." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:1793 +#, fuzzy +#| msgid "Description" +msgid "Duration" +msgstr "Description" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:1845 +msgid "Email address" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:1870 +msgid "File path" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:1948 +#, python-format +msgid "“%(value)s” value must be a float." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:1950 +msgid "Floating point number" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:1990 +#, python-format +msgid "“%(value)s” value must be an integer." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:1992 +msgid "Integer" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:2088 +msgid "Big (8 byte) integer" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:2105 +msgid "Small integer" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:2113 +msgid "IPv4 address" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:2144 +msgid "IP address" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:2237 +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:2238 +#, python-format +msgid "“%(value)s” value must be either None, True or False." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:2240 +msgid "Boolean (Either True, False or None)" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:2291 +msgid "Positive big integer" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:2306 +msgid "Positive integer" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:2321 +msgid "Positive small integer" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:2337 +#, python-format +msgid "Slug (up to %(max_length)s)" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:2373 +msgid "Text" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:2448 +#, python-format +msgid "" +"“%(value)s” value has an invalid format. It must be in HH:MM[:ss[.uuuuuu]] " +"format." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:2452 +#, python-format +msgid "" +"“%(value)s” value has the correct format (HH:MM[:ss[.uuuuuu]]) but it is an " +"invalid time." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:2456 +msgid "Time" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:2564 +msgid "URL" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:2588 +msgid "Raw binary data" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:2653 +#, python-format +msgid "“%(value)s” is not a valid UUID." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:2655 +msgid "Universally unique identifier" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/files.py:232 +msgid "File" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/files.py:393 +msgid "Image" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/json.py:26 +msgid "A JSON object" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/json.py:28 +msgid "Value must be valid JSON." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/related.py:919 +#, python-format +msgid "%(model)s instance with %(field)s %(value)r does not exist." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/related.py:921 +msgid "Foreign Key (type determined by related field)" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/related.py:1212 +msgid "One-to-one relationship" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/related.py:1269 +#, python-format +msgid "%(from)s-%(to)s relationship" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/related.py:1271 +#, python-format +msgid "%(from)s-%(to)s relationships" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/related.py:1319 +msgid "Many-to-many relationship" +msgstr "" + +#. Translators: If found as last label character, these punctuation +#. characters will prevent the default label_suffix to be appended to the label +#: venv/lib/python3.13/site-packages/django/forms/boundfield.py:184 +msgid ":?.!" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/fields.py:90 +msgid "This field is required." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/fields.py:297 +msgid "Enter a whole number." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/fields.py:466 +#: venv/lib/python3.13/site-packages/django/forms/fields.py:1231 +msgid "Enter a valid date." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/fields.py:489 +#: venv/lib/python3.13/site-packages/django/forms/fields.py:1232 +msgid "Enter a valid time." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/fields.py:516 +msgid "Enter a valid date/time." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/fields.py:550 +msgid "Enter a valid duration." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/fields.py:551 +#, python-brace-format +msgid "The number of days must be between {min_days} and {max_days}." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/fields.py:620 +msgid "No file was submitted. Check the encoding type on the form." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/fields.py:621 +msgid "No file was submitted." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/fields.py:622 +msgid "The submitted file is empty." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/fields.py:624 +#, python-format +msgid "Ensure this filename has at most %(max)d character (it has %(length)d)." +msgid_plural "" +"Ensure this filename has at most %(max)d characters (it has %(length)d)." +msgstr[0] "" +msgstr[1] "" + +#: venv/lib/python3.13/site-packages/django/forms/fields.py:629 +msgid "Please either submit a file or check the clear checkbox, not both." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/fields.py:693 +msgid "" +"Upload a valid image. The file you uploaded was either not an image or a " +"corrupted image." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/fields.py:847 +#: venv/lib/python3.13/site-packages/django/forms/fields.py:939 +#: venv/lib/python3.13/site-packages/django/forms/models.py:1566 +#, python-format +msgid "Select a valid choice. %(value)s is not one of the available choices." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/fields.py:941 +#: venv/lib/python3.13/site-packages/django/forms/fields.py:1060 +#: venv/lib/python3.13/site-packages/django/forms/models.py:1564 +msgid "Enter a list of values." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/fields.py:1061 +msgid "Enter a complete value." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/fields.py:1303 +msgid "Enter a valid UUID." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/fields.py:1333 +msgid "Enter a valid JSON." +msgstr "" + +#. Translators: This is the default suffix added to form field labels +#: venv/lib/python3.13/site-packages/django/forms/forms.py:98 +msgid ":" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/forms.py:244 +#: venv/lib/python3.13/site-packages/django/forms/forms.py:328 +#, python-format +msgid "(Hidden field %(name)s) %(error)s" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/formsets.py:63 +#, python-format +msgid "" +"ManagementForm data is missing or has been tampered with. Missing fields: " +"%(field_names)s. You may need to file a bug report if the issue persists." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/formsets.py:67 +#, python-format +msgid "Please submit at most %(num)d form." +msgid_plural "Please submit at most %(num)d forms." +msgstr[0] "" +msgstr[1] "" + +#: venv/lib/python3.13/site-packages/django/forms/formsets.py:72 +#, python-format +msgid "Please submit at least %(num)d form." +msgid_plural "Please submit at least %(num)d forms." +msgstr[0] "" +msgstr[1] "" + +#: venv/lib/python3.13/site-packages/django/forms/formsets.py:484 +#: venv/lib/python3.13/site-packages/django/forms/formsets.py:491 +msgid "Order" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/formsets.py:499 +msgid "Delete" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/models.py:886 +#, python-format +msgid "Please correct the duplicate data for %(field)s." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/models.py:891 +#, python-format +msgid "Please correct the duplicate data for %(field)s, which must be unique." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/models.py:898 +#, python-format +msgid "" +"Please correct the duplicate data for %(field_name)s which must be unique " +"for the %(lookup)s in %(date_field)s." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/models.py:907 +msgid "Please correct the duplicate values below." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/models.py:1338 +msgid "The inline value did not match the parent instance." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/models.py:1429 +msgid "Select a valid choice. That choice is not one of the available choices." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/models.py:1568 +#, python-format +msgid "“%(pk)s” is not a valid value." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/utils.py:226 +#, python-format +msgid "" +"%(datetime)s couldn’t be interpreted in time zone %(current_timezone)s; it " +"may be ambiguous or it may not exist." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/widgets.py:463 +msgid "Clear" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/widgets.py:464 +msgid "Currently" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/widgets.py:465 +msgid "Change" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/widgets.py:794 +msgid "Unknown" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/widgets.py:795 +msgid "Yes" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/widgets.py:796 +#, fuzzy +#| msgid "Non" +msgid "No" +msgstr "No" + +#. Translators: Please do not add spaces around commas. +#: venv/lib/python3.13/site-packages/django/template/defaultfilters.py:874 +msgid "yes,no,maybe" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/template/defaultfilters.py:904 +#: venv/lib/python3.13/site-packages/django/template/defaultfilters.py:921 +#, python-format +msgid "%(size)d byte" +msgid_plural "%(size)d bytes" +msgstr[0] "" +msgstr[1] "" + +#: venv/lib/python3.13/site-packages/django/template/defaultfilters.py:923 +#, python-format +msgid "%s KB" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/template/defaultfilters.py:925 +#, python-format +msgid "%s MB" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/template/defaultfilters.py:927 +#, python-format +msgid "%s GB" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/template/defaultfilters.py:929 +#, python-format +msgid "%s TB" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/template/defaultfilters.py:931 +#, python-format +msgid "%s PB" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dateformat.py:73 +msgid "p.m." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dateformat.py:74 +msgid "a.m." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dateformat.py:79 +msgid "PM" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dateformat.py:80 +msgid "AM" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dateformat.py:152 +msgid "midnight" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dateformat.py:154 +msgid "noon" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:7 +msgid "Monday" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:8 +msgid "Tuesday" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:9 +msgid "Wednesday" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:10 +msgid "Thursday" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:11 +msgid "Friday" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:12 +msgid "Saturday" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:13 +msgid "Sunday" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:16 +#, fuzzy +#| msgid "Non" +msgid "Mon" +msgstr "No" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:17 +#, fuzzy +#| msgid "Toutes" +msgid "Tue" +msgstr "All" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:18 +msgid "Wed" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:19 +msgid "Thu" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:20 +msgid "Fri" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:21 +#, fuzzy +#| msgid "Santé" +msgid "Sat" +msgstr "Healthcare" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:22 +msgid "Sun" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:25 +msgid "January" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:26 +msgid "February" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:27 +msgid "March" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:28 +msgid "April" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:29 +msgid "May" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:30 +msgid "June" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:31 +msgid "July" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:32 +msgid "August" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:33 +msgid "September" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:34 +msgid "October" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:35 +msgid "November" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:36 +msgid "December" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:39 +msgid "jan" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:40 +msgid "feb" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:41 +msgid "mar" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:42 +msgid "apr" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:43 +msgid "may" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:44 +msgid "jun" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:45 +msgid "jul" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:46 +msgid "aug" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:47 +msgid "sep" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:48 +msgid "oct" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:49 +msgid "nov" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:50 +msgid "dec" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:53 +msgctxt "abbrev. month" +msgid "Jan." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:54 +msgctxt "abbrev. month" +msgid "Feb." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:55 +msgctxt "abbrev. month" +msgid "March" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:56 +msgctxt "abbrev. month" +msgid "April" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:57 +msgctxt "abbrev. month" +msgid "May" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:58 +msgctxt "abbrev. month" +msgid "June" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:59 +msgctxt "abbrev. month" +msgid "July" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:60 +msgctxt "abbrev. month" +msgid "Aug." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:61 +msgctxt "abbrev. month" +msgid "Sept." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:62 +msgctxt "abbrev. month" +msgid "Oct." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:63 +msgctxt "abbrev. month" +msgid "Nov." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:64 +msgctxt "abbrev. month" +msgid "Dec." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:67 +msgctxt "alt. month" +msgid "January" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:68 +msgctxt "alt. month" +msgid "February" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:69 +msgctxt "alt. month" +msgid "March" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:70 +msgctxt "alt. month" +msgid "April" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:71 +msgctxt "alt. month" +msgid "May" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:72 +msgctxt "alt. month" +msgid "June" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:73 +msgctxt "alt. month" +msgid "July" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:74 +msgctxt "alt. month" +msgid "August" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:75 +msgctxt "alt. month" +msgid "September" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:76 +msgctxt "alt. month" +msgid "October" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:77 +msgctxt "alt. month" +msgid "November" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:78 +msgctxt "alt. month" +msgid "December" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/ipv6.py:20 +msgid "This is not a valid IPv6 address." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/text.py:138 +#, python-format +msgctxt "String to return when truncating text" +msgid "%(truncated_text)s…" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/text.py:319 +msgid "or" +msgstr "" + +#. Translators: This string is used as a separator between list elements +#: venv/lib/python3.13/site-packages/django/utils/text.py:338 +#: venv/lib/python3.13/site-packages/django/utils/timesince.py:135 +msgid ", " +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/timesince.py:8 +#, python-format +msgid "%(num)d year" +msgid_plural "%(num)d years" +msgstr[0] "" +msgstr[1] "" + +#: venv/lib/python3.13/site-packages/django/utils/timesince.py:9 +#, python-format +msgid "%(num)d month" +msgid_plural "%(num)d months" +msgstr[0] "" +msgstr[1] "" + +#: venv/lib/python3.13/site-packages/django/utils/timesince.py:10 +#, python-format +msgid "%(num)d week" +msgid_plural "%(num)d weeks" +msgstr[0] "" +msgstr[1] "" + +#: venv/lib/python3.13/site-packages/django/utils/timesince.py:11 +#, python-format +msgid "%(num)d day" +msgid_plural "%(num)d days" +msgstr[0] "" +msgstr[1] "" + +#: venv/lib/python3.13/site-packages/django/utils/timesince.py:12 +#, python-format +msgid "%(num)d hour" +msgid_plural "%(num)d hours" +msgstr[0] "" +msgstr[1] "" + +#: venv/lib/python3.13/site-packages/django/utils/timesince.py:13 +#, python-format +msgid "%(num)d minute" +msgid_plural "%(num)d minutes" +msgstr[0] "" +msgstr[1] "" + +#: venv/lib/python3.13/site-packages/django/views/csrf.py:111 +msgid "Forbidden" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/csrf.py:112 +msgid "CSRF verification failed. Request aborted." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/csrf.py:116 +msgid "" +"You are seeing this message because this HTTPS site requires a “Referer " +"header” to be sent by your web browser, but none was sent. This header is " +"required for security reasons, to ensure that your browser is not being " +"hijacked by third parties." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/csrf.py:122 +msgid "" +"If you have configured your browser to disable “Referer” headers, please re-" +"enable them, at least for this site, or for HTTPS connections, or for “same-" +"origin” requests." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/csrf.py:127 +msgid "" +"If you are using the tag or " +"including the “Referrer-Policy: no-referrer” header, please remove them. The " +"CSRF protection requires the “Referer” header to do strict referer checking. " +"If you’re concerned about privacy, use alternatives like for links to third-party sites." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/csrf.py:136 +msgid "" +"You are seeing this message because this site requires a CSRF cookie when " +"submitting forms. This cookie is required for security reasons, to ensure " +"that your browser is not being hijacked by third parties." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/csrf.py:142 +msgid "" +"If you have configured your browser to disable cookies, please re-enable " +"them, at least for this site, or for “same-origin” requests." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/csrf.py:148 +msgid "More information is available with DEBUG=True." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/generic/dates.py:44 +msgid "No year specified" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/generic/dates.py:64 +#: venv/lib/python3.13/site-packages/django/views/generic/dates.py:115 +#: venv/lib/python3.13/site-packages/django/views/generic/dates.py:214 +msgid "Date out of range" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/generic/dates.py:94 +msgid "No month specified" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/generic/dates.py:147 +msgid "No day specified" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/generic/dates.py:194 +msgid "No week specified" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/generic/dates.py:349 +#: venv/lib/python3.13/site-packages/django/views/generic/dates.py:380 +#, python-format +msgid "No %(verbose_name_plural)s available" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/generic/dates.py:652 +#, python-format +msgid "" +"Future %(verbose_name_plural)s not available because " +"%(class_name)s.allow_future is False." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/generic/dates.py:692 +#, python-format +msgid "Invalid date string “%(datestr)s” given format “%(format)s”" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/generic/detail.py:56 +#, python-format +msgid "No %(verbose_name)s found matching the query" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/generic/list.py:70 +msgid "Page is not “last”, nor can it be converted to an int." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/generic/list.py:77 +#, python-format +msgid "Invalid page (%(page_number)s): %(message)s" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/generic/list.py:169 +#, python-format +msgid "Empty list and “%(class_name)s.allow_empty” is False." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/static.py:38 +msgid "Directory indexes are not allowed here." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/static.py:40 +#, python-format +msgid "“%(path)s” does not exist" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/static.py:79 +#, python-format +msgid "Index of %(directory)s" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/templates/default_urlconf.html:7 +#: venv/lib/python3.13/site-packages/django/views/templates/default_urlconf.html:220 +msgid "The install worked successfully! Congratulations!" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/templates/default_urlconf.html:206 +#, python-format +msgid "" +"View release notes for Django %(version)s" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/templates/default_urlconf.html:221 +#, python-format +msgid "" +"You are seeing this page because DEBUG=True is in your settings file and you have not " +"configured any URLs." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/templates/default_urlconf.html:229 +msgid "Django Documentation" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/templates/default_urlconf.html:230 +msgid "Topics, references, & how-to’s" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/templates/default_urlconf.html:238 +msgid "Tutorial: A Polling App" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/templates/default_urlconf.html:239 +msgid "Get started with Django" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/templates/default_urlconf.html:247 +msgid "Django Community" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/templates/default_urlconf.html:248 +msgid "Connect, get help, or contribute" +msgstr "" diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po new file mode 100644 index 0000000..8a8dbc7 --- /dev/null +++ b/locale/fr/LC_MESSAGES/django.po @@ -0,0 +1,2183 @@ +# LOGIS — Traductions françaises +# Copyright (C) 2025 LOGIS +msgid "" +msgstr "" +"Project-Id-Version: LOGIS 1.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-05-28 15:32+0200\n" +"Language-Team: Français\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" + +#: logis_project/settings/base.py:75 +msgid "Français" +msgstr "Français" + +#: logis_project/settings/base.py:76 +msgid "English" +msgstr "English" + +#: properties/admin.py:58 +msgid "Aperçu" +msgstr "" + +#: properties/admin.py:67 properties/admin.py:142 +msgid "Localisation" +msgstr "" + +#: properties/admin.py:70 templates/properties/detail.html:126 +msgid "Commerces de proximité" +msgstr "Commerces de proximité" + +#: properties/admin.py:78 +msgid "Services mairie / éducation" +msgstr "" + +#: properties/admin.py:84 +msgid "Services de santé" +msgstr "" + +#: properties/admin.py:111 +msgid "Identification" +msgstr "" + +#: properties/admin.py:114 +msgid "Classification" +msgstr "" + +#: properties/admin.py:117 +#, fuzzy +#| msgid "Surface terrain" +msgid "Surfaces" +msgstr "Surface terrain" + +#: properties/admin.py:120 +#, fuzzy +#| msgid "Exposition" +msgid "Composition" +msgstr "Exposition" + +#: properties/admin.py:123 +msgid "Équipements" +msgstr "" + +#: properties/admin.py:130 +#, fuzzy +#| msgid "Année de construction" +msgid "Construction" +msgstr "Année de construction" + +#: properties/admin.py:133 +msgid "Diagnostics énergétiques" +msgstr "" + +#: properties/admin.py:139 templates/properties/detail.html:100 +msgid "Description" +msgstr "Description" + +#: properties/admin.py:145 +msgid "Image de couverture" +msgstr "" + +#: properties/admin.py:152 +msgid "Type" +msgstr "" + +#: properties/admin.py:160 +msgid "Statut" +msgstr "" + +#: properties/admin.py:164 +msgid "Prix" +msgstr "" + +#: properties/admin.py:168 +#, fuzzy +#| msgid "Surface terrain" +msgid "Surface" +msgstr "Surface terrain" + +#: properties/admin.py:172 templates/properties/partials/dpe_scale.html:10 +msgid "DPE" +msgstr "DPE" + +#: properties/admin.py:176 +msgid "GES" +msgstr "" + +#: properties/forms.py:19 templates/properties/partials/contact_modal.html:32 +msgid "Nom complet" +msgstr "Nom complet" + +#: properties/forms.py:21 templates/properties/partials/contact_modal.html:35 +msgid "Votre nom et prénom" +msgstr "Votre nom et prénom" + +#: properties/forms.py:26 templates/properties/partials/contact_modal.html:40 +msgid "Adresse email" +msgstr "Adresse email" + +#: properties/forms.py:28 templates/properties/partials/contact_modal.html:43 +msgid "votre@email.com" +msgstr "" + +#: properties/forms.py:35 templates/properties/partials/contact_modal.html:48 +msgid "Téléphone" +msgstr "Téléphone" + +#: properties/forms.py:37 +msgid "+33 6 XX XX XX XX" +msgstr "" + +#: properties/forms.py:42 templates/properties/partials/contact_modal.html:56 +msgid "Message" +msgstr "Message" + +#: properties/forms.py:44 templates/properties/partials/contact_modal.html:58 +msgid "Votre message, vos questions…" +msgstr "Votre message, vos questions…" + +#: properties/forms.py:67 +msgid "Veuillez indiquer votre nom complet." +msgstr "Veuillez indiquer votre nom complet." + +#: properties/forms.py:73 +msgid "Votre message est trop court." +msgstr "Votre message est trop court." + +#: properties/models.py:28 properties/models.py:79 properties/models.py:284 +#, fuzzy +#| msgid "Français" +msgid "nom (français)" +msgstr "Français" + +#: properties/models.py:29 properties/models.py:80 properties/models.py:285 +#, fuzzy +#| msgid "English" +msgid "name (English)" +msgstr "English" + +#: properties/models.py:31 +msgid "icône" +msgstr "" + +#: properties/models.py:32 +msgid "Classe CSS ou caractère unicode (ex: 🔥)" +msgstr "" + +#: properties/models.py:36 +#, fuzzy +#| msgid "Caractéristiques" +msgid "caractéristique" +msgstr "Caractéristiques" + +#: properties/models.py:37 properties/models.py:144 +#, fuzzy +#| msgid "Caractéristiques" +msgid "caractéristiques" +msgstr "Caractéristiques" + +#: properties/models.py:53 +msgid "Maison" +msgstr "" + +#: properties/models.py:54 +msgid "Appartement" +msgstr "" + +#: properties/models.py:55 +msgid "Terrain" +msgstr "" + +#: properties/models.py:56 +msgid "Local commercial" +msgstr "" + +#: properties/models.py:57 +msgid "Autre" +msgstr "" + +#: properties/models.py:60 templates/index.html:20 templates/index.html:77 +#: templates/properties/detail.html:18 +msgid "À vendre" +msgstr "À vendre" + +#: properties/models.py:61 templates/index.html:20 templates/index.html:77 +#: templates/properties/detail.html:18 +msgid "À louer" +msgstr "À louer" + +#: properties/models.py:66 +msgid "Nord" +msgstr "" + +#: properties/models.py:66 +msgid "Sud" +msgstr "" + +#: properties/models.py:66 +msgid "Est" +msgstr "" + +#: properties/models.py:66 +msgid "Ouest" +msgstr "" + +#: properties/models.py:67 +msgid "Nord-Est" +msgstr "" + +#: properties/models.py:67 +msgid "Nord-Ouest" +msgstr "" + +#: properties/models.py:68 +msgid "Sud-Est" +msgstr "" + +#: properties/models.py:68 +msgid "Sud-Ouest" +msgstr "" + +#: properties/models.py:71 +msgid "Neuf" +msgstr "" + +#: properties/models.py:72 +msgid "Exellent état" +msgstr "" + +#: properties/models.py:73 +msgid "Bon état" +msgstr "" + +#: properties/models.py:74 +msgid "À rafraîchir" +msgstr "" + +#: properties/models.py:75 +msgid "À rénover" +msgstr "" + +#: properties/models.py:81 +msgid "slug" +msgstr "" + +#: properties/models.py:82 +#, fuzzy +#| msgid "Type de bien" +msgid "type de bien" +msgstr "Type de bien" + +#: properties/models.py:83 +msgid "statut" +msgstr "" + +#: properties/models.py:84 +msgid "actif / visible" +msgstr "" + +#: properties/models.py:87 +msgid "prix (€)" +msgstr "" + +#: properties/models.py:88 +#, fuzzy +#| msgid "Prix sur demande" +msgid "prix sur demande" +msgstr "Prix sur demande" + +#: properties/models.py:92 +#, fuzzy +#| msgid "Surface terrain" +msgid "surface totale du terrain (m²)" +msgstr "Surface terrain" + +#: properties/models.py:95 +#, fuzzy +#| msgid "Surface habitable" +msgid "surface habitable (m²)" +msgstr "Surface habitable" + +#: properties/models.py:96 +msgid "surface totale (m²)" +msgstr "" + +#: properties/models.py:98 +#, fuzzy +#| msgid "Nombre de pièces" +msgid "nombre de pièces" +msgstr "Nombre de pièces" + +#: properties/models.py:99 +#, fuzzy +#| msgid "Nombre de pièces" +msgid "nombre de chambres" +msgstr "Nombre de pièces" + +#: properties/models.py:100 +#, fuzzy +#| msgid "Salles d'eau" +msgid "nombre de salles d'eau" +msgstr "Salles d'eau" + +#: properties/models.py:103 +#, fuzzy +#| msgid "Ascenseur" +msgid "ascenseur" +msgstr "Ascenseur" + +#: properties/models.py:104 +#, fuzzy +#| msgid "Exposition" +msgid "exposition" +msgstr "Exposition" + +#: properties/models.py:105 +#, fuzzy +#| msgid "Jardin" +msgid "jardin" +msgstr "Jardin" + +#: properties/models.py:106 +#, fuzzy +#| msgid "Piscine" +msgid "piscine" +msgstr "Piscine" + +#: properties/models.py:107 +#, fuzzy +#| msgid "Dépendances" +msgid "dépendances" +msgstr "Dépendances" + +#: properties/models.py:110 +#, fuzzy +#| msgid "Année de construction" +msgid "année de construction" +msgstr "Année de construction" + +#: properties/models.py:111 +#, fuzzy +#| msgid "État du bien" +msgid "état du bien" +msgstr "État du bien" + +#: properties/models.py:114 +#, fuzzy +#| msgid "Classe énergie (DPE)" +msgid "classe énergie (DPE)" +msgstr "Classe énergie (DPE)" + +#: properties/models.py:116 +msgid "valeur DPE (kWh/m²/an)" +msgstr "" + +#: properties/models.py:118 +msgid "classe GES" +msgstr "" + +#: properties/models.py:120 +msgid "valeur GES (kgCO₂/m²/an)" +msgstr "" + +#: properties/models.py:124 +msgid "description (français)" +msgstr "" + +#: properties/models.py:125 +#, fuzzy +#| msgid "Description" +msgid "description (English)" +msgstr "Description" + +#: properties/models.py:128 +#, fuzzy +#| msgid "Adresse email" +msgid "adresse" +msgstr "Adresse email" + +#: properties/models.py:129 +msgid "ville" +msgstr "" + +#: properties/models.py:130 +msgid "code postal" +msgstr "" + +#: properties/models.py:131 +msgid "latitude" +msgstr "" + +#: properties/models.py:132 +msgid "longitude" +msgstr "" + +#: properties/models.py:138 +msgid "image de couverture" +msgstr "" + +#: properties/models.py:147 +msgid "créé le" +msgstr "" + +#: properties/models.py:148 +msgid "modifié le" +msgstr "" + +#: properties/models.py:151 +#, fuzzy +#| msgid "Biens immobiliers" +msgid "bien immobilier" +msgstr "Biens immobiliers" + +#: properties/models.py:152 +#, fuzzy +#| msgid "Biens immobiliers" +msgid "biens immobiliers" +msgstr "Biens immobiliers" + +#: properties/models.py:186 templates/index.html:29 templates/index.html:107 +#: templates/properties/detail.html:27 +msgid "Prix sur demande" +msgstr "Prix sur demande" + +#: properties/models.py:198 +#, fuzzy +#| msgid "Village" +msgid "Ville" +msgstr "Village" + +#: properties/models.py:199 templates/properties/detail.html:114 +msgid "Village" +msgstr "Village" + +#: properties/models.py:200 templates/properties/detail.html:115 +msgid "Campagne" +msgstr "Campagne" + +#: properties/models.py:205 properties/models.py:282 properties/models.py:307 +msgid "bien" +msgstr "" + +#: properties/models.py:207 +msgid "type de localisation" +msgstr "" + +#: properties/models.py:208 +#, fuzzy +#| msgid "Situation géographique" +msgid "description géographique (français)" +msgstr "Situation géographique" + +#: properties/models.py:209 +msgid "geographic description (English)" +msgstr "" + +#: properties/models.py:212 +msgid "supérette" +msgstr "" + +#: properties/models.py:213 +msgid "alimentation" +msgstr "" + +#: properties/models.py:214 +msgid "boulangerie" +msgstr "" + +#: properties/models.py:215 +msgid "journaux / tabac presse" +msgstr "" + +#: properties/models.py:216 +msgid "bar / café" +msgstr "" + +#: properties/models.py:217 +msgid "bureau de tabac" +msgstr "" + +#: properties/models.py:218 +msgid "garage / garagiste" +msgstr "" + +#: properties/models.py:219 +msgid "station essence" +msgstr "" + +#: properties/models.py:222 +msgid "services de la mairie" +msgstr "" + +#: properties/models.py:223 +msgid "crèche" +msgstr "" + +#: properties/models.py:224 +msgid "école publique" +msgstr "" + +#: properties/models.py:227 +msgid "médecin" +msgstr "" + +#: properties/models.py:228 +msgid "pharmacie" +msgstr "" + +#: properties/models.py:229 +msgid "cabinet kinésithérapeute" +msgstr "" + +#: properties/models.py:230 +msgid "cabinet infirmier" +msgstr "" + +#: properties/models.py:233 +#, fuzzy +#| msgid "Environnement urbain" +msgid "environnement" +msgstr "Environnement urbain" + +#: properties/models.py:234 +#, fuzzy +#| msgid "Renseignements" +msgid "environnements" +msgstr "Renseignements" + +#: properties/models.py:249 +msgid "Supérette" +msgstr "" + +#: properties/models.py:250 +msgid "Alimentation" +msgstr "" + +#: properties/models.py:251 +msgid "Boulangerie" +msgstr "" + +#: properties/models.py:252 +msgid "Journaux / Presse" +msgstr "" + +#: properties/models.py:253 +msgid "Bar / Café" +msgstr "" + +#: properties/models.py:254 +msgid "Bureau de tabac" +msgstr "" + +#: properties/models.py:255 +msgid "Garagiste" +msgstr "" + +#: properties/models.py:256 +msgid "Station essence" +msgstr "" + +#: properties/models.py:262 +#, fuzzy +#| msgid "Services publics" +msgid "Services mairie" +msgstr "Services publics" + +#: properties/models.py:263 +msgid "Crèche" +msgstr "" + +#: properties/models.py:264 +msgid "École publique" +msgstr "" + +#: properties/models.py:270 +msgid "Médecin" +msgstr "" + +#: properties/models.py:271 +msgid "Pharmacie" +msgstr "" + +#: properties/models.py:272 +msgid "Kinésithérapeute" +msgstr "" + +#: properties/models.py:273 +msgid "Cabinet infirmier" +msgstr "" + +#: properties/models.py:286 properties/models.py:321 +msgid "ordre" +msgstr "" + +#: properties/models.py:289 properties/models.py:312 +#, fuzzy +#| msgid "pièces" +msgid "pièce" +msgstr "pièces" + +#: properties/models.py:290 templates/index.html:24 +msgid "pièces" +msgstr "pièces" + +#: properties/models.py:316 +msgid "image" +msgstr "" + +#: properties/models.py:318 +msgid "légende (français)" +msgstr "" + +#: properties/models.py:319 +msgid "caption (English)" +msgstr "" + +#: properties/models.py:320 +msgid "photo de couverture" +msgstr "" + +#: properties/models.py:324 +msgid "photo" +msgstr "" + +#: properties/models.py:325 +#, fuzzy +#| msgid "Galerie photos" +msgid "photos" +msgstr "Galerie photos" + +#: properties/models.py:343 +msgid "bien concerné" +msgstr "" + +#: properties/models.py:345 +msgid "nom" +msgstr "" + +#: properties/models.py:346 +msgid "email" +msgstr "" + +#: properties/models.py:347 +#, fuzzy +#| msgid "Téléphone" +msgid "téléphone" +msgstr "Téléphone" + +#: properties/models.py:348 +#, fuzzy +#| msgid "Message" +msgid "message" +msgstr "Message" + +#: properties/models.py:349 +#, fuzzy +#| msgid "Adresse email" +msgid "adresse IP" +msgstr "Adresse email" + +#: properties/models.py:350 +msgid "reçu le" +msgstr "" + +#: properties/models.py:353 +#, fuzzy +#| msgid "Année de construction" +msgid "demande de contact" +msgstr "Année de construction" + +#: properties/models.py:354 +msgid "demandes de contact" +msgstr "" + +#: properties/views.py:93 +#, python-format +msgid "" +"Merci de patienter encore %(min)s minute(s) avant d'envoyer un nouveau " +"message." +msgstr "" +"Merci de patienter encore %(min)s minute(s) avant d'envoyer un nouveau " +"message." + +#: properties/views.py:139 +msgid "" +"Votre message a bien été envoyé. Nous vous répondrons dans les plus brefs " +"délais." +msgstr "" +"Votre message a bien été envoyé. Nous vous répondrons dans les plus brefs " +"délais." + +#: templates/base.html:6 +msgid "Biens immobiliers à vendre et à louer — LOGIS" +msgstr "Biens immobiliers à vendre et à louer — LOGIS" + +#: templates/base.html:7 +msgid "Immobilier" +msgstr "Immobilier" + +#: templates/base.html:23 +msgid "Accueil LOGIS" +msgstr "Accueil LOGIS" + +#: templates/base.html:32 +msgid "Choisir la langue" +msgstr "Choisir la langue" + +#: templates/base.html:58 +msgid "Transactions immobilières" +msgstr "Transactions immobilières" + +#: templates/base.html:59 +msgid "Tous droits réservés." +msgstr "Tous droits réservés." + +#: templates/base.html:64 +msgid "Galerie photos" +msgstr "Galerie photos" + +#: templates/base.html:67 templates/properties/partials/contact_modal.html:7 +msgid "Fermer" +msgstr "Fermer" + +#: templates/base.html:68 +msgid "Photo précédente" +msgstr "Photo précédente" + +#: templates/base.html:73 +msgid "Photo suivante" +msgstr "Photo suivante" + +#: templates/index.html:4 +msgid "Biens immobiliers" +msgstr "Biens immobiliers" + +#: templates/index.html:32 +msgid "Découvrir ce bien" +msgstr "Découvrir ce bien" + +#: templates/index.html:41 +msgid "Nos biens immobiliers" +msgstr "Nos biens immobiliers" + +#: templates/index.html:42 +msgid "Trouvez votre futur chez-vous" +msgstr "Trouvez votre futur chez-vous" + +#: templates/index.html:53 +msgid "Aucun bien immobilier disponible pour le moment." +msgstr "Aucun bien immobilier disponible pour le moment." + +#: templates/index.html:91 templates/properties/detail.html:176 +msgid "Surface habitable" +msgstr "Surface habitable" + +#: templates/index.html:94 +msgid "Pièces" +msgstr "Pièces" + +#: templates/index.html:95 +msgid "p." +msgstr "p." + +#: templates/index.html:97 templates/properties/detail.html:190 +msgid "Chambres" +msgstr "Chambres" + +#: templates/index.html:100 +#, fuzzy +#| msgid "Salles d'eau" +msgid "Salles d\\" +msgstr "Salles d'eau" + +#: templates/index.html:110 templates/properties/detail.html:30 +msgid "/ mois" +msgstr "/ mois" + +#: templates/properties/detail.html:14 +msgid "Retour aux annonces" +msgstr "Retour aux annonces" + +#: templates/properties/detail.html:40 +msgid "Photos par pièce" +msgstr "Photos par pièce" + +#: templates/properties/detail.html:44 +msgid "Toutes" +msgstr "Toutes" + +#: templates/properties/detail.html:89 +msgid "Aucune photo disponible." +msgstr "Aucune photo disponible." + +#: templates/properties/detail.html:109 +msgid "Situation géographique" +msgstr "Situation géographique" + +#: templates/properties/detail.html:113 +msgid "Environnement urbain" +msgstr "Environnement urbain" + +#: templates/properties/detail.html:138 +msgid "Services publics" +msgstr "Services publics" + +#: templates/properties/detail.html:150 +msgid "Santé" +msgstr "Santé" + +#: templates/properties/detail.html:171 +msgid "Informations clés" +msgstr "Informations clés" + +#: templates/properties/detail.html:173 +msgid "Type de bien" +msgstr "Type de bien" + +#: templates/properties/detail.html:179 +#, fuzzy +#| msgid "Surface habitable" +msgid "Surface totale" +msgstr "Surface habitable" + +#: templates/properties/detail.html:183 +msgid "Surface terrain" +msgstr "Surface terrain" + +#: templates/properties/detail.html:187 +msgid "Nombre de pièces" +msgstr "Nombre de pièces" + +#: templates/properties/detail.html:193 +msgid "Salles d'eau" +msgstr "Salles d'eau" + +#: templates/properties/detail.html:197 +msgid "Exposition" +msgstr "Exposition" + +#: templates/properties/detail.html:202 +msgid "Année de construction" +msgstr "Année de construction" + +#: templates/properties/detail.html:206 +msgid "État du bien" +msgstr "État du bien" + +#: templates/properties/detail.html:209 +msgid "Ascenseur" +msgstr "Ascenseur" + +#: templates/properties/detail.html:210 +msgid "Oui" +msgstr "Oui" + +#: templates/properties/detail.html:210 +msgid "Non" +msgstr "Non" + +#: templates/properties/detail.html:217 +msgid "Jardin" +msgstr "Jardin" + +#: templates/properties/detail.html:220 +msgid "Piscine" +msgstr "Piscine" + +#: templates/properties/detail.html:223 +msgid "Dépendances" +msgstr "Dépendances" + +#: templates/properties/detail.html:231 +msgid "Caractéristiques" +msgstr "Caractéristiques" + +#: templates/properties/detail.html:247 +msgid "Diagnostics" +msgstr "Diagnostics" + +#: templates/properties/detail.html:251 +msgid "Classe énergie (DPE)" +msgstr "Classe énergie (DPE)" + +#: templates/properties/detail.html:258 +msgid "Émissions GES" +msgstr "Émissions GES" + +#: templates/properties/detail.html:271 +msgid "Renseignements" +msgstr "Renseignements" + +#: templates/properties/detail.html:273 +msgid "Vous souhaitez visiter ce bien ou obtenir plus d'informations ?" +msgstr "Vous souhaitez visiter ce bien ou obtenir plus d'informations ?" + +#: templates/properties/detail.html:277 +msgid "Envoyer un message" +msgstr "Envoyer un message" + +#: templates/properties/detail.html:315 +msgid "sur" +msgstr "sur" + +#: templates/properties/detail.html:316 +msgid "Envoi en cours…" +msgstr "Envoi en cours…" + +#: templates/properties/detail.html:317 +#: templates/properties/partials/contact_modal.html:69 +msgid "Envoyer" +msgstr "Envoyer" + +#: templates/properties/detail.html:318 +#: templates/properties/partials/contact_modal.html:16 +msgid "Message envoyé !" +msgstr "Message envoyé !" + +#: templates/properties/detail.html:319 +#: templates/properties/partials/contact_modal.html:17 +msgid "Nous vous répondrons dans les plus brefs délais." +msgstr "Nous vous répondrons dans les plus brefs délais." + +#: templates/properties/partials/contact_modal.html:10 +msgid "Demande de renseignements" +msgstr "Demande de renseignements" + +#: templates/properties/partials/contact_modal.html:48 +msgid "facultatif" +msgstr "facultatif" + +#: templates/properties/partials/contact_modal.html:63 +msgid "" +"* champs obligatoires — vos coordonnées ne seront utilisées que pour " +"répondre à votre demande." +msgstr "" +"* champs obligatoires — vos coordonnées ne seront utilisées que pour " +"répondre à votre demande." + +#: templates/properties/partials/dpe_scale.html:4 +msgid "Échelle de performance" +msgstr "Échelle de performance" + +#: venv/lib/python3.13/site-packages/django/contrib/messages/apps.py:15 +#, fuzzy +#| msgid "Message" +msgid "Messages" +msgstr "Message" + +#: venv/lib/python3.13/site-packages/django/contrib/sitemaps/apps.py:8 +msgid "Site Maps" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/contrib/staticfiles/apps.py:9 +msgid "Static Files" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/contrib/syndication/apps.py:7 +msgid "Syndication" +msgstr "" + +#. Translators: String used to replace omitted page numbers in elided page +#. range generated by paginators, e.g. [1, 2, '…', 5, 6, 7, '…', 9, 10]. +#: venv/lib/python3.13/site-packages/django/core/paginator.py:30 +msgid "…" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/core/paginator.py:50 +msgid "That page number is not an integer" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/core/paginator.py:52 +msgid "That page number is less than 1" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/core/paginator.py:54 +msgid "That page contains no results" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/core/validators.py:22 +msgid "Enter a valid value." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/core/validators.py:104 +#: venv/lib/python3.13/site-packages/django/forms/fields.py:751 +msgid "Enter a valid URL." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/core/validators.py:165 +msgid "Enter a valid integer." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/core/validators.py:176 +msgid "Enter a valid email address." +msgstr "" + +#. Translators: "letters" means latin letters: a-z and A-Z. +#: venv/lib/python3.13/site-packages/django/core/validators.py:259 +msgid "" +"Enter a valid “slug” consisting of letters, numbers, underscores or hyphens." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/core/validators.py:267 +msgid "" +"Enter a valid “slug” consisting of Unicode letters, numbers, underscores, or " +"hyphens." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/core/validators.py:281 +#: venv/lib/python3.13/site-packages/django/core/validators.py:289 +#: venv/lib/python3.13/site-packages/django/core/validators.py:318 +msgid "Enter a valid IPv4 address." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/core/validators.py:298 +#: venv/lib/python3.13/site-packages/django/core/validators.py:319 +msgid "Enter a valid IPv6 address." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/core/validators.py:310 +#: venv/lib/python3.13/site-packages/django/core/validators.py:317 +msgid "Enter a valid IPv4 or IPv6 address." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/core/validators.py:353 +msgid "Enter only digits separated by commas." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/core/validators.py:359 +#, python-format +msgid "Ensure this value is %(limit_value)s (it is %(show_value)s)." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/core/validators.py:394 +#, python-format +msgid "Ensure this value is less than or equal to %(limit_value)s." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/core/validators.py:403 +#, python-format +msgid "Ensure this value is greater than or equal to %(limit_value)s." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/core/validators.py:412 +#, python-format +msgid "Ensure this value is a multiple of step size %(limit_value)s." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/core/validators.py:422 +#, python-format +msgid "" +"Ensure this value has at least %(limit_value)d character (it has " +"%(show_value)d)." +msgid_plural "" +"Ensure this value has at least %(limit_value)d characters (it has " +"%(show_value)d)." +msgstr[0] "" +msgstr[1] "" + +#: venv/lib/python3.13/site-packages/django/core/validators.py:440 +#, python-format +msgid "" +"Ensure this value has at most %(limit_value)d character (it has " +"%(show_value)d)." +msgid_plural "" +"Ensure this value has at most %(limit_value)d characters (it has " +"%(show_value)d)." +msgstr[0] "" +msgstr[1] "" + +#: venv/lib/python3.13/site-packages/django/core/validators.py:463 +#: venv/lib/python3.13/site-packages/django/forms/fields.py:346 +#: venv/lib/python3.13/site-packages/django/forms/fields.py:385 +msgid "Enter a number." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/core/validators.py:465 +#, python-format +msgid "Ensure that there are no more than %(max)s digit in total." +msgid_plural "Ensure that there are no more than %(max)s digits in total." +msgstr[0] "" +msgstr[1] "" + +#: venv/lib/python3.13/site-packages/django/core/validators.py:470 +#, python-format +msgid "Ensure that there are no more than %(max)s decimal place." +msgid_plural "Ensure that there are no more than %(max)s decimal places." +msgstr[0] "" +msgstr[1] "" + +#: venv/lib/python3.13/site-packages/django/core/validators.py:475 +#, python-format +msgid "" +"Ensure that there are no more than %(max)s digit before the decimal point." +msgid_plural "" +"Ensure that there are no more than %(max)s digits before the decimal point." +msgstr[0] "" +msgstr[1] "" + +#: venv/lib/python3.13/site-packages/django/core/validators.py:546 +#, python-format +msgid "" +"File extension “%(extension)s” is not allowed. Allowed extensions are: " +"%(allowed_extensions)s." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/core/validators.py:607 +msgid "Null characters are not allowed." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/base.py:1423 +#: venv/lib/python3.13/site-packages/django/forms/models.py:893 +msgid "and" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/base.py:1425 +#, python-format +msgid "%(model_name)s with this %(field_labels)s already exists." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/constraints.py:17 +#, python-format +msgid "Constraint “%(name)s” is violated." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:128 +#, python-format +msgid "Value %(value)r is not a valid choice." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:129 +msgid "This field cannot be null." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:130 +msgid "This field cannot be blank." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:131 +#, python-format +msgid "%(model_name)s with this %(field_label)s already exists." +msgstr "" + +#. Translators: The 'lookup_type' is one of 'date', 'year' or +#. 'month'. Eg: "Title must be unique for pub_date year" +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:135 +#, python-format +msgid "" +"%(field_label)s must be unique for %(date_field_label)s %(lookup_type)s." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:173 +#, python-format +msgid "Field of type: %(field_type)s" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:1094 +#, python-format +msgid "“%(value)s” value must be either True or False." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:1095 +#, python-format +msgid "“%(value)s” value must be either True, False, or None." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:1097 +msgid "Boolean (Either True or False)" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:1147 +#, python-format +msgid "String (up to %(max_length)s)" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:1149 +msgid "String (unlimited)" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:1253 +msgid "Comma-separated integers" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:1354 +#, python-format +msgid "" +"“%(value)s” value has an invalid date format. It must be in YYYY-MM-DD " +"format." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:1358 +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:1493 +#, python-format +msgid "" +"“%(value)s” value has the correct format (YYYY-MM-DD) but it is an invalid " +"date." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:1362 +msgid "Date (without time)" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:1489 +#, python-format +msgid "" +"“%(value)s” value has an invalid format. It must be in YYYY-MM-DD " +"HH:MM[:ss[.uuuuuu]][TZ] format." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:1497 +#, python-format +msgid "" +"“%(value)s” value has the correct format (YYYY-MM-DD HH:MM[:ss[.uuuuuu]]" +"[TZ]) but it is an invalid date/time." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:1502 +msgid "Date (with time)" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:1626 +#, python-format +msgid "“%(value)s” value must be a decimal number." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:1628 +msgid "Decimal number" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:1789 +#, python-format +msgid "" +"“%(value)s” value has an invalid format. It must be in [DD] " +"[[HH:]MM:]ss[.uuuuuu] format." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:1793 +#, fuzzy +#| msgid "Description" +msgid "Duration" +msgstr "Description" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:1845 +msgid "Email address" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:1870 +msgid "File path" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:1948 +#, python-format +msgid "“%(value)s” value must be a float." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:1950 +msgid "Floating point number" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:1990 +#, python-format +msgid "“%(value)s” value must be an integer." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:1992 +msgid "Integer" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:2088 +msgid "Big (8 byte) integer" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:2105 +msgid "Small integer" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:2113 +msgid "IPv4 address" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:2144 +msgid "IP address" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:2237 +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:2238 +#, python-format +msgid "“%(value)s” value must be either None, True or False." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:2240 +msgid "Boolean (Either True, False or None)" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:2291 +msgid "Positive big integer" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:2306 +msgid "Positive integer" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:2321 +msgid "Positive small integer" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:2337 +#, python-format +msgid "Slug (up to %(max_length)s)" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:2373 +msgid "Text" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:2448 +#, python-format +msgid "" +"“%(value)s” value has an invalid format. It must be in HH:MM[:ss[.uuuuuu]] " +"format." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:2452 +#, python-format +msgid "" +"“%(value)s” value has the correct format (HH:MM[:ss[.uuuuuu]]) but it is an " +"invalid time." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:2456 +msgid "Time" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:2564 +msgid "URL" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:2588 +msgid "Raw binary data" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:2653 +#, python-format +msgid "“%(value)s” is not a valid UUID." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/__init__.py:2655 +msgid "Universally unique identifier" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/files.py:232 +msgid "File" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/files.py:393 +msgid "Image" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/json.py:26 +msgid "A JSON object" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/json.py:28 +msgid "Value must be valid JSON." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/related.py:919 +#, python-format +msgid "%(model)s instance with %(field)s %(value)r does not exist." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/related.py:921 +msgid "Foreign Key (type determined by related field)" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/related.py:1212 +msgid "One-to-one relationship" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/related.py:1269 +#, python-format +msgid "%(from)s-%(to)s relationship" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/related.py:1271 +#, python-format +msgid "%(from)s-%(to)s relationships" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/db/models/fields/related.py:1319 +msgid "Many-to-many relationship" +msgstr "" + +#. Translators: If found as last label character, these punctuation +#. characters will prevent the default label_suffix to be appended to the label +#: venv/lib/python3.13/site-packages/django/forms/boundfield.py:184 +msgid ":?.!" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/fields.py:90 +msgid "This field is required." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/fields.py:297 +msgid "Enter a whole number." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/fields.py:466 +#: venv/lib/python3.13/site-packages/django/forms/fields.py:1231 +msgid "Enter a valid date." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/fields.py:489 +#: venv/lib/python3.13/site-packages/django/forms/fields.py:1232 +msgid "Enter a valid time." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/fields.py:516 +msgid "Enter a valid date/time." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/fields.py:550 +msgid "Enter a valid duration." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/fields.py:551 +#, python-brace-format +msgid "The number of days must be between {min_days} and {max_days}." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/fields.py:620 +msgid "No file was submitted. Check the encoding type on the form." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/fields.py:621 +msgid "No file was submitted." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/fields.py:622 +msgid "The submitted file is empty." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/fields.py:624 +#, python-format +msgid "Ensure this filename has at most %(max)d character (it has %(length)d)." +msgid_plural "" +"Ensure this filename has at most %(max)d characters (it has %(length)d)." +msgstr[0] "" +msgstr[1] "" + +#: venv/lib/python3.13/site-packages/django/forms/fields.py:629 +msgid "Please either submit a file or check the clear checkbox, not both." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/fields.py:693 +msgid "" +"Upload a valid image. The file you uploaded was either not an image or a " +"corrupted image." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/fields.py:847 +#: venv/lib/python3.13/site-packages/django/forms/fields.py:939 +#: venv/lib/python3.13/site-packages/django/forms/models.py:1566 +#, python-format +msgid "Select a valid choice. %(value)s is not one of the available choices." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/fields.py:941 +#: venv/lib/python3.13/site-packages/django/forms/fields.py:1060 +#: venv/lib/python3.13/site-packages/django/forms/models.py:1564 +msgid "Enter a list of values." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/fields.py:1061 +msgid "Enter a complete value." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/fields.py:1303 +msgid "Enter a valid UUID." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/fields.py:1333 +msgid "Enter a valid JSON." +msgstr "" + +#. Translators: This is the default suffix added to form field labels +#: venv/lib/python3.13/site-packages/django/forms/forms.py:98 +msgid ":" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/forms.py:244 +#: venv/lib/python3.13/site-packages/django/forms/forms.py:328 +#, python-format +msgid "(Hidden field %(name)s) %(error)s" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/formsets.py:63 +#, python-format +msgid "" +"ManagementForm data is missing or has been tampered with. Missing fields: " +"%(field_names)s. You may need to file a bug report if the issue persists." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/formsets.py:67 +#, python-format +msgid "Please submit at most %(num)d form." +msgid_plural "Please submit at most %(num)d forms." +msgstr[0] "" +msgstr[1] "" + +#: venv/lib/python3.13/site-packages/django/forms/formsets.py:72 +#, python-format +msgid "Please submit at least %(num)d form." +msgid_plural "Please submit at least %(num)d forms." +msgstr[0] "" +msgstr[1] "" + +#: venv/lib/python3.13/site-packages/django/forms/formsets.py:484 +#: venv/lib/python3.13/site-packages/django/forms/formsets.py:491 +msgid "Order" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/formsets.py:499 +msgid "Delete" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/models.py:886 +#, python-format +msgid "Please correct the duplicate data for %(field)s." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/models.py:891 +#, python-format +msgid "Please correct the duplicate data for %(field)s, which must be unique." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/models.py:898 +#, python-format +msgid "" +"Please correct the duplicate data for %(field_name)s which must be unique " +"for the %(lookup)s in %(date_field)s." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/models.py:907 +msgid "Please correct the duplicate values below." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/models.py:1338 +msgid "The inline value did not match the parent instance." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/models.py:1429 +msgid "Select a valid choice. That choice is not one of the available choices." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/models.py:1568 +#, python-format +msgid "“%(pk)s” is not a valid value." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/utils.py:226 +#, python-format +msgid "" +"%(datetime)s couldn’t be interpreted in time zone %(current_timezone)s; it " +"may be ambiguous or it may not exist." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/widgets.py:463 +msgid "Clear" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/widgets.py:464 +msgid "Currently" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/widgets.py:465 +msgid "Change" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/widgets.py:794 +msgid "Unknown" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/widgets.py:795 +msgid "Yes" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/forms/widgets.py:796 +#, fuzzy +#| msgid "Non" +msgid "No" +msgstr "Non" + +#. Translators: Please do not add spaces around commas. +#: venv/lib/python3.13/site-packages/django/template/defaultfilters.py:874 +msgid "yes,no,maybe" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/template/defaultfilters.py:904 +#: venv/lib/python3.13/site-packages/django/template/defaultfilters.py:921 +#, python-format +msgid "%(size)d byte" +msgid_plural "%(size)d bytes" +msgstr[0] "" +msgstr[1] "" + +#: venv/lib/python3.13/site-packages/django/template/defaultfilters.py:923 +#, python-format +msgid "%s KB" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/template/defaultfilters.py:925 +#, python-format +msgid "%s MB" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/template/defaultfilters.py:927 +#, python-format +msgid "%s GB" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/template/defaultfilters.py:929 +#, python-format +msgid "%s TB" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/template/defaultfilters.py:931 +#, python-format +msgid "%s PB" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dateformat.py:73 +msgid "p.m." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dateformat.py:74 +msgid "a.m." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dateformat.py:79 +msgid "PM" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dateformat.py:80 +msgid "AM" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dateformat.py:152 +msgid "midnight" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dateformat.py:154 +msgid "noon" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:7 +msgid "Monday" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:8 +msgid "Tuesday" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:9 +msgid "Wednesday" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:10 +msgid "Thursday" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:11 +msgid "Friday" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:12 +msgid "Saturday" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:13 +msgid "Sunday" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:16 +#, fuzzy +#| msgid "Non" +msgid "Mon" +msgstr "Non" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:17 +#, fuzzy +#| msgid "Toutes" +msgid "Tue" +msgstr "Toutes" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:18 +msgid "Wed" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:19 +msgid "Thu" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:20 +msgid "Fri" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:21 +#, fuzzy +#| msgid "Santé" +msgid "Sat" +msgstr "Santé" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:22 +msgid "Sun" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:25 +msgid "January" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:26 +msgid "February" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:27 +msgid "March" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:28 +msgid "April" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:29 +msgid "May" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:30 +msgid "June" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:31 +msgid "July" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:32 +msgid "August" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:33 +msgid "September" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:34 +msgid "October" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:35 +msgid "November" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:36 +msgid "December" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:39 +msgid "jan" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:40 +msgid "feb" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:41 +msgid "mar" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:42 +msgid "apr" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:43 +msgid "may" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:44 +msgid "jun" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:45 +msgid "jul" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:46 +msgid "aug" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:47 +msgid "sep" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:48 +msgid "oct" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:49 +msgid "nov" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:50 +msgid "dec" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:53 +msgctxt "abbrev. month" +msgid "Jan." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:54 +msgctxt "abbrev. month" +msgid "Feb." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:55 +msgctxt "abbrev. month" +msgid "March" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:56 +msgctxt "abbrev. month" +msgid "April" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:57 +msgctxt "abbrev. month" +msgid "May" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:58 +msgctxt "abbrev. month" +msgid "June" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:59 +msgctxt "abbrev. month" +msgid "July" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:60 +msgctxt "abbrev. month" +msgid "Aug." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:61 +msgctxt "abbrev. month" +msgid "Sept." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:62 +msgctxt "abbrev. month" +msgid "Oct." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:63 +msgctxt "abbrev. month" +msgid "Nov." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:64 +msgctxt "abbrev. month" +msgid "Dec." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:67 +msgctxt "alt. month" +msgid "January" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:68 +msgctxt "alt. month" +msgid "February" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:69 +msgctxt "alt. month" +msgid "March" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:70 +msgctxt "alt. month" +msgid "April" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:71 +msgctxt "alt. month" +msgid "May" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:72 +msgctxt "alt. month" +msgid "June" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:73 +msgctxt "alt. month" +msgid "July" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:74 +msgctxt "alt. month" +msgid "August" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:75 +msgctxt "alt. month" +msgid "September" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:76 +msgctxt "alt. month" +msgid "October" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:77 +msgctxt "alt. month" +msgid "November" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/dates.py:78 +msgctxt "alt. month" +msgid "December" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/ipv6.py:20 +msgid "This is not a valid IPv6 address." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/text.py:138 +#, python-format +msgctxt "String to return when truncating text" +msgid "%(truncated_text)s…" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/text.py:319 +msgid "or" +msgstr "" + +#. Translators: This string is used as a separator between list elements +#: venv/lib/python3.13/site-packages/django/utils/text.py:338 +#: venv/lib/python3.13/site-packages/django/utils/timesince.py:135 +msgid ", " +msgstr "" + +#: venv/lib/python3.13/site-packages/django/utils/timesince.py:8 +#, python-format +msgid "%(num)d year" +msgid_plural "%(num)d years" +msgstr[0] "" +msgstr[1] "" + +#: venv/lib/python3.13/site-packages/django/utils/timesince.py:9 +#, python-format +msgid "%(num)d month" +msgid_plural "%(num)d months" +msgstr[0] "" +msgstr[1] "" + +#: venv/lib/python3.13/site-packages/django/utils/timesince.py:10 +#, python-format +msgid "%(num)d week" +msgid_plural "%(num)d weeks" +msgstr[0] "" +msgstr[1] "" + +#: venv/lib/python3.13/site-packages/django/utils/timesince.py:11 +#, python-format +msgid "%(num)d day" +msgid_plural "%(num)d days" +msgstr[0] "" +msgstr[1] "" + +#: venv/lib/python3.13/site-packages/django/utils/timesince.py:12 +#, python-format +msgid "%(num)d hour" +msgid_plural "%(num)d hours" +msgstr[0] "" +msgstr[1] "" + +#: venv/lib/python3.13/site-packages/django/utils/timesince.py:13 +#, python-format +msgid "%(num)d minute" +msgid_plural "%(num)d minutes" +msgstr[0] "" +msgstr[1] "" + +#: venv/lib/python3.13/site-packages/django/views/csrf.py:111 +msgid "Forbidden" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/csrf.py:112 +msgid "CSRF verification failed. Request aborted." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/csrf.py:116 +msgid "" +"You are seeing this message because this HTTPS site requires a “Referer " +"header” to be sent by your web browser, but none was sent. This header is " +"required for security reasons, to ensure that your browser is not being " +"hijacked by third parties." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/csrf.py:122 +msgid "" +"If you have configured your browser to disable “Referer” headers, please re-" +"enable them, at least for this site, or for HTTPS connections, or for “same-" +"origin” requests." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/csrf.py:127 +msgid "" +"If you are using the tag or " +"including the “Referrer-Policy: no-referrer” header, please remove them. The " +"CSRF protection requires the “Referer” header to do strict referer checking. " +"If you’re concerned about privacy, use alternatives like for links to third-party sites." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/csrf.py:136 +msgid "" +"You are seeing this message because this site requires a CSRF cookie when " +"submitting forms. This cookie is required for security reasons, to ensure " +"that your browser is not being hijacked by third parties." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/csrf.py:142 +msgid "" +"If you have configured your browser to disable cookies, please re-enable " +"them, at least for this site, or for “same-origin” requests." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/csrf.py:148 +msgid "More information is available with DEBUG=True." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/generic/dates.py:44 +msgid "No year specified" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/generic/dates.py:64 +#: venv/lib/python3.13/site-packages/django/views/generic/dates.py:115 +#: venv/lib/python3.13/site-packages/django/views/generic/dates.py:214 +msgid "Date out of range" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/generic/dates.py:94 +msgid "No month specified" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/generic/dates.py:147 +msgid "No day specified" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/generic/dates.py:194 +msgid "No week specified" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/generic/dates.py:349 +#: venv/lib/python3.13/site-packages/django/views/generic/dates.py:380 +#, python-format +msgid "No %(verbose_name_plural)s available" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/generic/dates.py:652 +#, python-format +msgid "" +"Future %(verbose_name_plural)s not available because " +"%(class_name)s.allow_future is False." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/generic/dates.py:692 +#, python-format +msgid "Invalid date string “%(datestr)s” given format “%(format)s”" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/generic/detail.py:56 +#, python-format +msgid "No %(verbose_name)s found matching the query" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/generic/list.py:70 +msgid "Page is not “last”, nor can it be converted to an int." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/generic/list.py:77 +#, python-format +msgid "Invalid page (%(page_number)s): %(message)s" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/generic/list.py:169 +#, python-format +msgid "Empty list and “%(class_name)s.allow_empty” is False." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/static.py:38 +msgid "Directory indexes are not allowed here." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/static.py:40 +#, python-format +msgid "“%(path)s” does not exist" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/static.py:79 +#, python-format +msgid "Index of %(directory)s" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/templates/default_urlconf.html:7 +#: venv/lib/python3.13/site-packages/django/views/templates/default_urlconf.html:220 +msgid "The install worked successfully! Congratulations!" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/templates/default_urlconf.html:206 +#, python-format +msgid "" +"View release notes for Django %(version)s" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/templates/default_urlconf.html:221 +#, python-format +msgid "" +"You are seeing this page because DEBUG=True is in your settings file and you have not " +"configured any URLs." +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/templates/default_urlconf.html:229 +msgid "Django Documentation" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/templates/default_urlconf.html:230 +msgid "Topics, references, & how-to’s" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/templates/default_urlconf.html:238 +msgid "Tutorial: A Polling App" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/templates/default_urlconf.html:239 +msgid "Get started with Django" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/templates/default_urlconf.html:247 +msgid "Django Community" +msgstr "" + +#: venv/lib/python3.13/site-packages/django/views/templates/default_urlconf.html:248 +msgid "Connect, get help, or contribute" +msgstr "" diff --git a/logis_project/__init__.py b/logis_project/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/logis_project/asgi.py b/logis_project/asgi.py new file mode 100644 index 0000000..7ba4b1a --- /dev/null +++ b/logis_project/asgi.py @@ -0,0 +1,9 @@ +""" +Point d'entrée ASGI pour le projet LOGIS. +""" +import os +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'logis_project.settings.production') + +application = get_asgi_application() diff --git a/logis_project/settings/__init__.py b/logis_project/settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/logis_project/settings/base.py b/logis_project/settings/base.py new file mode 100644 index 0000000..c246058 --- /dev/null +++ b/logis_project/settings/base.py @@ -0,0 +1,138 @@ +""" +Configuration de base commune à tous les environnements. +""" +from pathlib import Path +from django.utils.translation import gettext_lazy as _ +from decouple import config, Csv + +BASE_DIR = Path(__file__).resolve().parent.parent.parent + +SECRET_KEY = config('SECRET_KEY') + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'django.contrib.sitemaps', + 'django_resized', + 'adminsortable2', + 'properties', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'whitenoise.middleware.WhiteNoiseMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.locale.LocaleMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'logis_project.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [BASE_DIR / 'templates'], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + 'django.template.context_processors.i18n', + 'properties.context_processors.site_settings', + ], + }, + }, +] + +WSGI_APPLICATION = 'logis_project.wsgi.application' + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + +AUTH_PASSWORD_VALIDATORS = [ + {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'}, + {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'}, + {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'}, + {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'}, +] + +# Internationalisation +LANGUAGE_CODE = 'fr' +LANGUAGES = [ + ('fr', _('Français')), + ('en', _('English')), +] +TIME_ZONE = 'Europe/Paris' +USE_I18N = True +USE_L10N = True +USE_TZ = True +LOCALE_PATHS = [BASE_DIR / 'locale'] + +# Fichiers statiques +STATIC_URL = '/static/' +STATIC_ROOT = BASE_DIR / 'staticfiles' +STATICFILES_DIRS = [BASE_DIR / 'static'] +STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' + +# Fichiers médias +MEDIA_URL = '/media/' +MEDIA_ROOT = BASE_DIR / 'media' + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +# django-resized : configuration par défaut +DJANGORESIZED_DEFAULT_SIZE = [1920, 1080] +DJANGORESIZED_DEFAULT_SCALE = 1.0 +DJANGORESIZED_DEFAULT_QUALITY = 85 +DJANGORESIZED_DEFAULT_KEEP_META = True +DJANGORESIZED_DEFAULT_FORCE_FORMAT = 'WEBP' +DJANGORESIZED_DEFAULT_FORMAT_EXTENSIONS = {'WEBP': '.webp', 'JPEG': '.jpg'} +DJANGORESIZED_DEFAULT_NORMALIZE_ROTATION = True + +# Configuration email (envoi uniquement) +EMAIL_BACKEND = config( + 'EMAIL_BACKEND', + default='django.core.mail.backends.smtp.EmailBackend', +) +EMAIL_HOST = config('EMAIL_HOST', default='localhost') +EMAIL_PORT = config('EMAIL_PORT', default=587, cast=int) +EMAIL_USE_TLS = config('EMAIL_USE_TLS', default=True, cast=bool) +EMAIL_HOST_USER = config('EMAIL_HOST_USER', default='') +EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD', default='') +SITE_DOMAIN = config('SITE_DOMAIN', default='') +DEFAULT_FROM_EMAIL = config('DEFAULT_FROM_EMAIL', default='LOGIS ') +ADMIN_EMAIL = config('ADMIN_EMAIL', default='admin@example.com') + +# Logging +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'console': {'class': 'logging.StreamHandler'}, + }, + 'root': { + 'handlers': ['console'], + 'level': 'WARNING', + }, + 'loggers': { + 'properties': { + 'handlers': ['console'], + 'level': 'INFO', + 'propagate': False, + }, + }, +} diff --git a/logis_project/settings/development.py b/logis_project/settings/development.py new file mode 100644 index 0000000..175e4cf --- /dev/null +++ b/logis_project/settings/development.py @@ -0,0 +1,15 @@ +""" +Configuration pour l'environnement de développement. +""" +from .base import * # noqa: F401, F403 +from decouple import config + +DEBUG = True + +ALLOWED_HOSTS = ['localhost', '127.0.0.1', '0.0.0.0'] + +# Email en console pendant le développement +EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' + +# Désactive la compression des fichiers statiques en dev +STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.StaticFilesStorage' diff --git a/logis_project/settings/production.py b/logis_project/settings/production.py new file mode 100644 index 0000000..919b7a7 --- /dev/null +++ b/logis_project/settings/production.py @@ -0,0 +1,63 @@ +""" +Configuration pour l'environnement de production. +""" +from .base import * # noqa: F401, F403 +from decouple import config, Csv + +DEBUG = False + +ALLOWED_HOSTS = config('ALLOWED_HOSTS', cast=Csv(), default='localhost') + +# Sécurité HTTPS +SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') +SECURE_SSL_REDIRECT = True +SESSION_COOKIE_SECURE = True +CSRF_COOKIE_SECURE = True +SECURE_HSTS_SECONDS = 31536000 +SECURE_HSTS_INCLUDE_SUBDOMAINS = True +SECURE_HSTS_PRELOAD = True +SECURE_CONTENT_TYPE_NOSNIFF = True +X_FRAME_OPTIONS = 'DENY' + +# Cache des sessions +SESSION_ENGINE = 'django.contrib.sessions.backends.db' + +# Logging en production vers fichier +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'verbose': { + 'format': '{levelname} {asctime} {module} {message}', + 'style': '{', + }, + }, + 'handlers': { + 'file': { + 'class': 'logging.FileHandler', + 'filename': '/var/log/logis/django.log', + 'formatter': 'verbose', + }, + }, + 'root': { + 'handlers': ['file'], + 'level': 'WARNING', + }, + 'loggers': { + 'django.request': { + 'handlers': ['file'], + 'level': 'ERROR', # 404 → silencieux, seuls les 500 sont loggués + 'propagate': False, + }, + 'django.security': { + 'handlers': ['file'], + 'level': 'ERROR', + 'propagate': False, + }, + 'properties': { + 'handlers': ['file'], + 'level': 'INFO', + 'propagate': False, + }, + }, +} diff --git a/logis_project/urls.py b/logis_project/urls.py new file mode 100644 index 0000000..2f2b387 --- /dev/null +++ b/logis_project/urls.py @@ -0,0 +1,33 @@ +""" +URLs principales du projet LOGIS. +""" +from django.contrib import admin +from django.conf import settings +from django.conf.urls.static import static +from django.conf.urls.i18n import i18n_patterns +from django.urls import path, include +from django.contrib.sitemaps.views import sitemap +from django.views.generic import TemplateView +from properties.sitemaps import PropertySitemap + +admin.site.site_header = 'LOGIS — Administration' +admin.site.site_title = 'LOGIS' +admin.site.index_title = 'Gestion des biens immobiliers' + +_sitemaps = {'properties': PropertySitemap()} + +urlpatterns = [ + path('admin/', admin.site.urls), + path('i18n/', include('django.conf.urls.i18n')), + path('sitemap.xml', sitemap, {'sitemaps': _sitemaps}, name='django.contrib.sitemaps.views.sitemap'), + path('robots.txt', TemplateView.as_view(template_name='robots.txt', content_type='text/plain')), +] + +urlpatterns += i18n_patterns( + path('', include('properties.urls')), + prefix_default_language=False, +) + +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) diff --git a/logis_project/wsgi.py b/logis_project/wsgi.py new file mode 100644 index 0000000..a0da540 --- /dev/null +++ b/logis_project/wsgi.py @@ -0,0 +1,9 @@ +""" +Point d'entrée WSGI pour le projet LOGIS. +""" +import os +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'logis_project.settings.production') + +application = get_wsgi_application() diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..8def030 --- /dev/null +++ b/manage.py @@ -0,0 +1,23 @@ +#!venv/bin/python +##!/usr/bin/env python +"""Utilitaire de ligne de commande Django pour les tâches administratives.""" +import os +import sys + + +def main(): + """Exécute les tâches administratives.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'logis_project.settings.development') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Impossible d'importer Django. Vérifiez que Django est installé " + "et disponible dans votre variable d'environnement PYTHONPATH, " + "et que vous avez activé votre virtualenv." + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/properties/__init__.py b/properties/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/properties/admin.py b/properties/admin.py new file mode 100644 index 0000000..6520995 --- /dev/null +++ b/properties/admin.py @@ -0,0 +1,293 @@ +""" +Configuration de l'interface d'administration Django pour les biens immobiliers. +""" +from django.contrib import admin +from django.shortcuts import get_object_or_404, redirect, render +from django.urls import path, reverse +from django.utils.html import format_html +from django.utils.translation import gettext_lazy as _ +from adminsortable2.admin import SortableAdminMixin, SortableAdminBase, SortableInlineAdminMixin + +from .models import ( + Property, PropertyEnvironment, PropertyFeature, + Room, Photo, ContactRequest, +) + +# ─── Couleurs des classes DPE / GES ────────────────────────────────────────── +DPE_COLORS = { + 'A': '#00B050', 'B': '#67B346', 'C': '#ACCC54', + 'D': '#F5D33B', 'E': '#F6A600', 'F': '#E95A0C', 'G': '#CE1622', +} + + +def _class_badge(letter, label=''): + """Génère un badge coloré HTML pour une classe DPE/GES.""" + if not letter: + return format_html('') + color = DPE_COLORS.get(letter, '#888') + text = f'{letter} {label}'.strip() + return format_html( + '{}', + color, text, + ) + + +# ─── Inlines ────────────────────────────────────────────────────────────────── + +class RoomInline(SortableInlineAdminMixin, admin.TabularInline): + """Inline d'édition des pièces avec tri drag-and-drop.""" + model = Room + extra = 1 + fields = ('name_fr', 'name_en', 'order') + + +class PhotoInline(SortableInlineAdminMixin, admin.TabularInline): + """Inline d'édition des photos avec tri drag-and-drop.""" + model = Photo + extra = 1 + fields = ('image', 'room', 'caption_fr', 'caption_en', 'is_cover', 'order') + readonly_fields = ('preview',) + + def preview(self, obj): + """Aperçu miniature de la photo.""" + if obj.image: + return format_html( + '', + obj.image.url, + ) + return '—' + preview.short_description = _('Aperçu') + + +class PropertyEnvironmentInline(admin.StackedInline): + """Inline d'édition de l'environnement du bien.""" + model = PropertyEnvironment + can_delete = False + extra = 0 + fieldsets = [ + (_('Localisation'), { + 'fields': ['location_type', 'description_fr', 'description_en'], + }), + (_('Commerces de proximité'), { + 'fields': [ + ('has_supermarket', 'has_food_shop', 'has_bakery'), + ('has_newspaper_shop', 'has_bar', 'has_tobacco_shop'), + ('has_garage', 'has_gas_station'), + ], + 'classes': ['collapse'], + }), + (_('Services mairie / éducation'), { + 'fields': [ + ('has_city_hall_services', 'has_nursery', 'has_public_school'), + ], + 'classes': ['collapse'], + }), + (_('Services de santé'), { + 'fields': [ + ('has_doctor', 'has_pharmacy', 'has_physiotherapist', 'has_nurse'), + ], + 'classes': ['collapse'], + }), + ] + + +# ─── Admin principal ────────────────────────────────────────────────────────── + +@admin.register(Property) +class PropertyAdmin(SortableAdminBase, admin.ModelAdmin): + """Administration des biens immobiliers.""" + + change_form_template = 'admin/properties/property/change_form.html' + + list_display = [ + 'name_fr', 'city', 'type_badge', 'status_badge', + 'price_display', 'living_area_display', + 'energy_badge', 'ges_badge', 'is_active', + ] + list_filter = ['property_type', 'status', 'is_active', 'energy_class', 'condition'] + search_fields = ['name_fr', 'name_en', 'address', 'city', 'postal_code'] + prepopulated_fields = {'slug': ('name_fr',)} + filter_horizontal = ('features',) + inlines = [PropertyEnvironmentInline, RoomInline, PhotoInline] + + fieldsets = [ + (_('Identification'), { + 'fields': ['name_fr', 'name_en', 'slug', 'is_active'], + }), + (_('Classification'), { + 'fields': [('property_type', 'status'), ('price', 'price_on_request')], + }), + (_('Surfaces'), { + 'fields': [('living_area', 'total_area', 'total_land_area')], + }), + (_('Composition'), { + 'fields': [('rooms_count', 'bedrooms_count', 'bathrooms_count')], + }), + (_('Équipements'), { + 'fields': [ + ('has_elevator', 'exposure'), + ('has_garden', 'has_pool', 'has_dependencies'), + 'features', + ], + }), + (_('Construction'), { + 'fields': [('construction_year', 'condition')], + }), + (_('Diagnostics énergétiques'), { + 'fields': [ + ('energy_class', 'energy_value'), + ('ges_class', 'ges_value'), + ], + }), + (_('Description'), { + 'fields': ['description_fr', 'description_en'], + }), + (_('Localisation'), { + 'fields': ['address', ('city', 'postal_code'), ('latitude', 'longitude')], + }), + (_('Image de couverture'), { + 'fields': ['thumbnail'], + }), + (_('SEO'), { + 'fields': [ + ('meta_title_fr', 'meta_title_en'), + ('meta_description_fr', 'meta_description_en'), + ('meta_keywords_fr', 'meta_keywords_en'), + ], + 'classes': ['collapse'], + 'description': 'Laisser vide pour une génération automatique.', + }), + ] + + def type_badge(self, obj): + return obj.get_property_type_display() + type_badge.short_description = _('Type') + + def status_badge(self, obj): + color = '#2196F3' if obj.status == 'sale' else '#4CAF50' + return format_html( + '{}', + color, obj.get_status_display(), + ) + status_badge.short_description = _('Statut') + + def price_display(self, obj): + return obj.price_display + price_display.short_description = _('Prix') + + def living_area_display(self, obj): + return f'{int(obj.living_area)} m²' + living_area_display.short_description = _('Surface') + + def energy_badge(self, obj): + return _class_badge(obj.energy_class, f'{obj.energy_value} kWh' if obj.energy_value else '') + energy_badge.short_description = _('DPE') + + def ges_badge(self, obj): + return _class_badge(obj.ges_class, f'{obj.ges_value} kg' if obj.ges_value else '') + ges_badge.short_description = _('GES') + + # ── URL et vue d'upload groupé ──────────────────────────────────────────── + + def get_urls(self): + """Ajoute l'URL de l'upload groupé de photos.""" + urls = super().get_urls() + custom = [ + path( + '/photos/upload/', + self.admin_site.admin_view(self.upload_photos_view), + name='properties_property_upload_photos', + ), + ] + return custom + urls + + def upload_photos_view(self, request, property_pk): + """Vue d'upload groupé : une ou plusieurs photos vers une pièce en un seul envoi.""" + prop = get_object_or_404(Property, pk=property_pk) + + if request.method == 'POST': + files = request.FILES.getlist('images') + room = None + + # Création d'une nouvelle pièce si demandée + new_room_fr = request.POST.get('new_room_fr', '').strip() + new_room_en = request.POST.get('new_room_en', '').strip() + if new_room_fr: + room = Room.objects.create( + property=prop, + name_fr=new_room_fr, + name_en=new_room_en or new_room_fr, + order=prop.rooms.count(), + ) + else: + room_pk = request.POST.get('room') or None + if room_pk: + room = get_object_or_404(Room, pk=room_pk, property=prop) + + # Ordre de départ : après les photos existantes de cette pièce + base_order = ( + Photo.objects.filter(property=prop, room=room).count() + ) + created = 0 + for i, f in enumerate(files): + Photo.objects.create( + property=prop, + room=room, + image=f, + order=base_order + i, + ) + created += 1 + + room_label = room.name_fr if room else '(général)' + from django.contrib import messages as dj_messages + dj_messages.success( + request, + f'{created} photo(s) ajoutée(s) → pièce : {room_label}', + ) + # Retour vers la page suivante ou la fiche du bien + next_url = request.POST.get('next', '') + if next_url == 'again': + return redirect('.') + return redirect( + reverse('admin:properties_property_change', args=[property_pk]) + ) + + context = { + **self.admin_site.each_context(request), + 'property': prop, + 'rooms': prop.rooms.order_by('order'), + 'title': f'📷 Ajout de photos — {prop.name_fr}', + 'opts': self.model._meta, + } + return render( + request, + 'admin/properties/property/upload_photos.html', + context, + ) + + +@admin.register(PropertyFeature) +class PropertyFeatureAdmin(admin.ModelAdmin): + """Administration des caractéristiques de biens.""" + list_display = ['name_fr', 'name_en', 'icon'] + search_fields = ['name_fr', 'name_en'] + + +@admin.register(ContactRequest) +class ContactRequestAdmin(admin.ModelAdmin): + """Administration des demandes de contact (lecture seule).""" + list_display = ['created_at', 'name', 'email', 'phone', 'property', 'ip_address'] + list_filter = ['created_at', 'property'] + search_fields = ['name', 'email', 'message'] + readonly_fields = [ + 'property', 'name', 'email', 'phone', + 'message', 'ip_address', 'created_at', + ] + date_hierarchy = 'created_at' + + def has_add_permission(self, request): + return False + + def has_change_permission(self, request, obj=None): + return False diff --git a/properties/context_processors.py b/properties/context_processors.py new file mode 100644 index 0000000..d03248b --- /dev/null +++ b/properties/context_processors.py @@ -0,0 +1,13 @@ +""" +Processeurs de contexte globaux injectés dans tous les templates. +""" +from django.conf import settings + + +def site_settings(request): + """Injecte les paramètres globaux du site dans le contexte des templates.""" + return { + 'SITE_NAME': 'LOGIS', + 'SITE_DOMAIN': getattr(settings, 'SITE_DOMAIN', ''), + 'ADMIN_EMAIL': getattr(settings, 'ADMIN_EMAIL', ''), + } diff --git a/properties/forms.py b/properties/forms.py new file mode 100644 index 0000000..f490ce3 --- /dev/null +++ b/properties/forms.py @@ -0,0 +1,74 @@ +""" +Formulaires publics de l'application LOGIS. +""" +from django import forms +from django.utils.translation import gettext_lazy as _ + + +class ContactForm(forms.Form): + """Formulaire de demande de renseignements sur un bien immobilier. + + Protégé par : + - token CSRF Django (automatique) + - champ honeypot invisible (doit rester vide) + - limitation de débit côté session (5 minutes entre deux envois) + """ + + name = forms.CharField( + max_length=200, + label=_('Nom complet'), + widget=forms.TextInput(attrs={ + 'placeholder': _('Votre nom et prénom'), + 'autocomplete': 'name', + }), + ) + email = forms.EmailField( + label=_('Adresse email'), + widget=forms.EmailInput(attrs={ + 'placeholder': _('votre@email.com'), + 'autocomplete': 'email', + }), + ) + phone = forms.CharField( + max_length=30, + required=False, + label=_('Téléphone'), + widget=forms.TextInput(attrs={ + 'placeholder': _('+33 6 XX XX XX XX'), + 'autocomplete': 'tel', + }), + ) + message = forms.CharField( + label=_('Message'), + widget=forms.Textarea(attrs={ + 'placeholder': _('Votre message, vos questions…'), + 'rows': 5, + }), + ) + property_id = forms.IntegerField( + widget=forms.HiddenInput(), + required=False, + ) + # Champ honeypot — doit rester vide ; les robots le remplissent + website = forms.CharField( + required=False, + label='', + widget=forms.TextInput(attrs={ + 'style': 'position:absolute;left:-9999px;top:-9999px;', + 'tabindex': '-1', + 'autocomplete': 'off', + 'aria-hidden': 'true', + }), + ) + + def clean_name(self): + name = self.cleaned_data.get('name', '').strip() + if len(name) < 2: + raise forms.ValidationError(_('Veuillez indiquer votre nom complet.')) + return name + + def clean_message(self): + msg = self.cleaned_data.get('message', '').strip() + if len(msg) < 10: + raise forms.ValidationError(_('Votre message est trop court.')) + return msg diff --git a/properties/migrations/0001_initial.py b/properties/migrations/0001_initial.py new file mode 100644 index 0000000..12a9e78 --- /dev/null +++ b/properties/migrations/0001_initial.py @@ -0,0 +1,166 @@ +# Generated by Django 4.2.30 on 2026-05-28 07:31 + +from django.db import migrations, models +import django.db.models.deletion +import django_resized.forms +import properties.models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Property', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name_fr', models.CharField(max_length=200, verbose_name='nom (français)')), + ('name_en', models.CharField(max_length=200, verbose_name='name (English)')), + ('slug', models.SlugField(blank=True, max_length=220, unique=True, verbose_name='slug')), + ('property_type', models.CharField(choices=[('house', 'Maison'), ('apartment', 'Appartement'), ('land', 'Terrain'), ('commercial', 'Local commercial'), ('other', 'Autre')], max_length=20, verbose_name='type de bien')), + ('status', models.CharField(choices=[('sale', 'À vendre'), ('rent', 'À louer')], max_length=10, verbose_name='statut')), + ('is_active', models.BooleanField(default=True, verbose_name='actif / visible')), + ('price', models.DecimalField(decimal_places=0, max_digits=12, verbose_name='prix (€)')), + ('price_on_request', models.BooleanField(default=False, verbose_name='prix sur demande')), + ('total_land_area', models.DecimalField(blank=True, decimal_places=0, max_digits=10, null=True, verbose_name='surface totale du terrain (m²)')), + ('living_area', models.DecimalField(decimal_places=0, max_digits=8, verbose_name='surface habitable (m²)')), + ('rooms_count', models.PositiveSmallIntegerField(verbose_name='nombre de pièces')), + ('bedrooms_count', models.PositiveSmallIntegerField(verbose_name='nombre de chambres')), + ('bathrooms_count', models.PositiveSmallIntegerField(verbose_name="nombre de salles d'eau")), + ('has_elevator', models.BooleanField(default=False, verbose_name='ascenseur')), + ('exposure', models.CharField(blank=True, choices=[('N', 'Nord'), ('S', 'Sud'), ('E', 'Est'), ('O', 'Ouest'), ('NE', 'Nord-Est'), ('NO', 'Nord-Ouest'), ('SE', 'Sud-Est'), ('SO', 'Sud-Ouest')], max_length=2, verbose_name='exposition')), + ('has_garden', models.BooleanField(default=False, verbose_name='jardin')), + ('has_pool', models.BooleanField(default=False, verbose_name='piscine')), + ('has_dependencies', models.BooleanField(default=False, verbose_name='dépendances')), + ('construction_year', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='année de construction')), + ('condition', models.CharField(choices=[('new', 'Neuf'), ('good', 'Bon état'), ('to_refresh', 'À rafraîchir'), ('to_renovate', 'À rénover')], max_length=20, verbose_name='état du bien')), + ('energy_class', models.CharField(blank=True, choices=[('A', 'A'), ('B', 'B'), ('C', 'C'), ('D', 'D'), ('E', 'E'), ('F', 'F'), ('G', 'G')], max_length=1, verbose_name='classe énergie (DPE)')), + ('energy_value', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='valeur DPE (kWh/m²/an)')), + ('ges_class', models.CharField(blank=True, choices=[('A', 'A'), ('B', 'B'), ('C', 'C'), ('D', 'D'), ('E', 'E'), ('F', 'F'), ('G', 'G')], max_length=1, verbose_name='classe GES')), + ('ges_value', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='valeur GES (kgCO₂/m²/an)')), + ('description_fr', models.TextField(verbose_name='description (français)')), + ('description_en', models.TextField(verbose_name='description (English)')), + ('address', models.CharField(max_length=300, verbose_name='adresse')), + ('city', models.CharField(max_length=100, verbose_name='ville')), + ('postal_code', models.CharField(max_length=10, verbose_name='code postal')), + ('latitude', models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True, verbose_name='latitude')), + ('longitude', models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True, verbose_name='longitude')), + ('thumbnail', django_resized.forms.ResizedImageField(blank=True, crop=None, force_format='WEBP', keep_meta=True, null=True, quality=85, scale=1.0, size=[900, 600], upload_to='properties/covers/', verbose_name='image de couverture')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='créé le')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='modifié le')), + ], + options={ + 'verbose_name': 'bien immobilier', + 'verbose_name_plural': 'biens immobiliers', + 'ordering': ['-created_at'], + }, + bases=(properties.models.BilingualMixin, models.Model), + ), + migrations.CreateModel( + name='PropertyFeature', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name_fr', models.CharField(max_length=100, verbose_name='nom (français)')), + ('name_en', models.CharField(max_length=100, verbose_name='name (English)')), + ('icon', models.CharField(blank=True, help_text='Classe CSS ou caractère unicode (ex: 🔥)', max_length=50, verbose_name='icône')), + ], + options={ + 'verbose_name': 'caractéristique', + 'verbose_name_plural': 'caractéristiques', + 'ordering': ['name_fr'], + }, + bases=(properties.models.BilingualMixin, models.Model), + ), + migrations.CreateModel( + name='Room', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name_fr', models.CharField(max_length=100, verbose_name='nom (français)')), + ('name_en', models.CharField(max_length=100, verbose_name='name (English)')), + ('order', models.PositiveSmallIntegerField(db_index=True, default=0, verbose_name='ordre')), + ('property', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rooms', to='properties.property', verbose_name='bien')), + ], + options={ + 'verbose_name': 'pièce', + 'verbose_name_plural': 'pièces', + 'ordering': ['order', 'id'], + }, + bases=(properties.models.BilingualMixin, models.Model), + ), + migrations.CreateModel( + name='PropertyEnvironment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('location_type', models.CharField(choices=[('city', 'Ville'), ('village', 'Village'), ('countryside', 'Campagne')], max_length=20, verbose_name='type de localisation')), + ('description_fr', models.TextField(blank=True, verbose_name='description géographique (français)')), + ('description_en', models.TextField(blank=True, verbose_name='geographic description (English)')), + ('has_supermarket', models.BooleanField(default=False, verbose_name='supérette')), + ('has_food_shop', models.BooleanField(default=False, verbose_name='alimentation')), + ('has_bakery', models.BooleanField(default=False, verbose_name='boulangerie')), + ('has_newspaper_shop', models.BooleanField(default=False, verbose_name='journaux / tabac presse')), + ('has_bar', models.BooleanField(default=False, verbose_name='bar / café')), + ('has_tobacco_shop', models.BooleanField(default=False, verbose_name='bureau de tabac')), + ('has_garage', models.BooleanField(default=False, verbose_name='garage / garagiste')), + ('has_gas_station', models.BooleanField(default=False, verbose_name='station essence')), + ('has_city_hall_services', models.BooleanField(default=False, verbose_name='services de la mairie')), + ('has_nursery', models.BooleanField(default=False, verbose_name='crèche')), + ('has_public_school', models.BooleanField(default=False, verbose_name='école publique')), + ('has_doctor', models.BooleanField(default=False, verbose_name='médecin')), + ('has_pharmacy', models.BooleanField(default=False, verbose_name='pharmacie')), + ('has_physiotherapist', models.BooleanField(default=False, verbose_name='cabinet kinésithérapeute')), + ('has_nurse', models.BooleanField(default=False, verbose_name='cabinet infirmier')), + ('property', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='environment', to='properties.property', verbose_name='bien')), + ], + options={ + 'verbose_name': 'environnement', + 'verbose_name_plural': 'environnements', + }, + bases=(properties.models.BilingualMixin, models.Model), + ), + migrations.AddField( + model_name='property', + name='features', + field=models.ManyToManyField(blank=True, to='properties.propertyfeature', verbose_name='caractéristiques'), + ), + migrations.CreateModel( + name='Photo', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('image', django_resized.forms.ResizedImageField(crop=None, force_format='WEBP', keep_meta=True, quality=85, scale=1.0, size=[1600, 1067], upload_to='properties/photos/', verbose_name='image')), + ('caption_fr', models.CharField(blank=True, max_length=200, verbose_name='légende (français)')), + ('caption_en', models.CharField(blank=True, max_length=200, verbose_name='caption (English)')), + ('is_cover', models.BooleanField(default=False, verbose_name='photo de couverture')), + ('order', models.PositiveSmallIntegerField(db_index=True, default=0, verbose_name='ordre')), + ('property', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='photos', to='properties.property', verbose_name='bien')), + ('room', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='photos', to='properties.room', verbose_name='pièce')), + ], + options={ + 'verbose_name': 'photo', + 'verbose_name_plural': 'photos', + 'ordering': ['order', 'id'], + }, + bases=(properties.models.BilingualMixin, models.Model), + ), + migrations.CreateModel( + name='ContactRequest', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=200, verbose_name='nom')), + ('email', models.EmailField(max_length=254, verbose_name='email')), + ('phone', models.CharField(blank=True, max_length=30, verbose_name='téléphone')), + ('message', models.TextField(verbose_name='message')), + ('ip_address', models.GenericIPAddressField(blank=True, null=True, verbose_name='adresse IP')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='reçu le')), + ('property', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='contact_requests', to='properties.property', verbose_name='bien concerné')), + ], + options={ + 'verbose_name': 'demande de contact', + 'verbose_name_plural': 'demandes de contact', + 'ordering': ['-created_at'], + }, + ), + ] diff --git a/properties/migrations/0002_property_total_area_alter_property_condition.py b/properties/migrations/0002_property_total_area_alter_property_condition.py new file mode 100644 index 0000000..1267935 --- /dev/null +++ b/properties/migrations/0002_property_total_area_alter_property_condition.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.30 on 2026-05-28 08:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('properties', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='property', + name='total_area', + field=models.DecimalField(decimal_places=0, default=0, max_digits=8, verbose_name='surface totale (m²)'), + ), + migrations.AlterField( + model_name='property', + name='condition', + field=models.CharField(choices=[('new', 'Neuf'), ('exellent', 'Exellent état'), ('good', 'Bon état'), ('to_refresh', 'À rafraîchir'), ('to_renovate', 'À rénover')], max_length=20, verbose_name='état du bien'), + ), + ] diff --git a/properties/migrations/0003_seo_fields.py b/properties/migrations/0003_seo_fields.py new file mode 100644 index 0000000..82e3fb4 --- /dev/null +++ b/properties/migrations/0003_seo_fields.py @@ -0,0 +1,43 @@ +# Generated by Django 4.2.30 on 2026-05-28 16:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('properties', '0002_property_total_area_alter_property_condition'), + ] + + operations = [ + migrations.AddField( + model_name='property', + name='meta_description_en', + field=models.CharField(blank=True, max_length=160, verbose_name='SEO description (English)'), + ), + migrations.AddField( + model_name='property', + name='meta_description_fr', + field=models.CharField(blank=True, max_length=160, verbose_name='description SEO (français)'), + ), + migrations.AddField( + model_name='property', + name='meta_keywords_en', + field=models.CharField(blank=True, max_length=300, verbose_name='SEO keywords (English)'), + ), + migrations.AddField( + model_name='property', + name='meta_keywords_fr', + field=models.CharField(blank=True, max_length=300, verbose_name='mots-clés SEO (français)'), + ), + migrations.AddField( + model_name='property', + name='meta_title_en', + field=models.CharField(blank=True, max_length=70, verbose_name='SEO title (English)'), + ), + migrations.AddField( + model_name='property', + name='meta_title_fr', + field=models.CharField(blank=True, max_length=70, verbose_name='titre SEO (français)'), + ), + ] diff --git a/properties/migrations/__init__.py b/properties/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/properties/models.py b/properties/models.py new file mode 100644 index 0000000..b7e857b --- /dev/null +++ b/properties/models.py @@ -0,0 +1,400 @@ +""" +Modèles de données pour l'application de gestion immobilière LOGIS. +""" +import builtins # accès explicite à @property quand le champ FK 'property' masquerait le built-in + +from django.db import models +from django.utils.translation import gettext_lazy as _ +from django.utils.translation import get_language +from django.utils.text import slugify +from django_resized import ResizedImageField + + +class BilingualMixin: + """Mixin apportant la traduction automatique des champs bilingues (fr/en).""" + + def get_translated(self, field_base): + """Retourne la valeur du champ dans la langue active, avec repli sur le français.""" + lang = (get_language() or 'fr').split('-')[0] + value = getattr(self, f'{field_base}_{lang}', None) + if not value: + value = getattr(self, f'{field_base}_fr', '') + return value + + +class PropertyFeature(BilingualMixin, models.Model): + """Caractéristique d'un bien (ex: parquet, double vitrage, cheminée…).""" + + name_fr = models.CharField(_('nom (français)'), max_length=100) + name_en = models.CharField(_('name (English)'), max_length=100) + icon = models.CharField( + _('icône'), max_length=50, blank=True, + help_text=_('Classe CSS ou caractère unicode (ex: 🔥)') + ) + + class Meta: + verbose_name = _('caractéristique') + verbose_name_plural = _('caractéristiques') + ordering = ['name_fr'] + + def __str__(self): + return self.name_fr + + @property + def name(self): + """Nom dans la langue active.""" + return self.get_translated('name') + + +class Property(BilingualMixin, models.Model): + """Bien immobilier principal.""" + + PROPERTY_TYPES = [ + ('house', _('Maison')), + ('apartment', _('Appartement')), + ('land', _('Terrain')), + ('commercial', _('Local commercial')), + ('other', _('Autre')), + ] + STATUS_CHOICES = [ + ('sale', _('À vendre')), + ('rent', _('À louer')), + ] + ENERGY_CLASSES = [(c, c) for c in 'ABCDEFG'] + GES_CLASSES = [(c, c) for c in 'ABCDEFG'] + EXPOSURE_CHOICES = [ + ('N', _('Nord')), ('S', _('Sud')), ('E', _('Est')), ('O', _('Ouest')), + ('NE', _('Nord-Est')), ('NO', _('Nord-Ouest')), + ('SE', _('Sud-Est')), ('SO', _('Sud-Ouest')), + ] + CONDITION_CHOICES = [ + ('new', _('Neuf')), + ('exellent', _('Exellent état')), + ('good', _('Bon état')), + ('to_refresh', _('À rafraîchir')), + ('to_renovate', _('À rénover')), + ] + + # Identification + name_fr = models.CharField(_('nom (français)'), max_length=200) + name_en = models.CharField(_('name (English)'), max_length=200) + slug = models.SlugField(_('slug'), max_length=220, unique=True, blank=True) + property_type = models.CharField(_('type de bien'), max_length=20, choices=PROPERTY_TYPES) + status = models.CharField(_('statut'), max_length=10, choices=STATUS_CHOICES) + is_active = models.BooleanField(_('actif / visible'), default=True) + + # Prix + price = models.DecimalField(_('prix (€)'), max_digits=12, decimal_places=0) + price_on_request = models.BooleanField(_('prix sur demande'), default=False) + + # Surfaces (m²) + total_land_area = models.DecimalField( + _('surface totale du terrain (m²)'), max_digits=10, decimal_places=0, + null=True, blank=True, + ) + living_area = models.DecimalField(_('surface habitable (m²)'), max_digits=8, decimal_places=0) + total_area = models.DecimalField(_('surface totale (m²)'), max_digits=8, decimal_places=0, default=0) + # Pièces + rooms_count = models.PositiveSmallIntegerField(_('nombre de pièces')) + bedrooms_count = models.PositiveSmallIntegerField(_('nombre de chambres')) + bathrooms_count = models.PositiveSmallIntegerField(_('nombre de salles d\'eau')) + + # Équipements + has_elevator = models.BooleanField(_('ascenseur'), default=False) + exposure = models.CharField(_('exposition'), max_length=2, choices=EXPOSURE_CHOICES, blank=True) + has_garden = models.BooleanField(_('jardin'), default=False) + has_pool = models.BooleanField(_('piscine'), default=False) + has_dependencies = models.BooleanField(_('dépendances'), default=False) + + # Construction + construction_year = models.PositiveSmallIntegerField(_('année de construction'), null=True, blank=True) + condition = models.CharField(_('état du bien'), max_length=20, choices=CONDITION_CHOICES) + + # Diagnostics énergétiques + energy_class = models.CharField(_('classe énergie (DPE)'), max_length=1, choices=ENERGY_CLASSES, blank=True) + energy_value = models.PositiveSmallIntegerField( + _('valeur DPE (kWh/m²/an)'), null=True, blank=True, + ) + ges_class = models.CharField(_('classe GES'), max_length=1, choices=GES_CLASSES, blank=True) + ges_value = models.PositiveSmallIntegerField( + _('valeur GES (kgCO₂/m²/an)'), null=True, blank=True, + ) + + # Descriptions bilingues + description_fr = models.TextField(_('description (français)')) + description_en = models.TextField(_('description (English)')) + + # Localisation + address = models.CharField(_('adresse'), max_length=300) + city = models.CharField(_('ville'), max_length=100) + postal_code = models.CharField(_('code postal'), max_length=10) + latitude = models.DecimalField(_('latitude'), max_digits=9, decimal_places=6, null=True, blank=True) + longitude = models.DecimalField(_('longitude'), max_digits=9, decimal_places=6, null=True, blank=True) + + # Image de couverture (pour la liste des biens) + thumbnail = ResizedImageField( + size=[900, 600], quality=85, upload_to='properties/covers/', + force_format='WEBP', null=True, blank=True, + verbose_name=_('image de couverture'), + ) + + # SEO (facultatif — auto-généré si vide) + meta_title_fr = models.CharField(_('titre SEO (français)'), max_length=70, blank=True) + meta_title_en = models.CharField(_('SEO title (English)'), max_length=70, blank=True) + meta_description_fr = models.CharField(_('description SEO (français)'), max_length=160, blank=True) + meta_description_en = models.CharField(_('SEO description (English)'), max_length=160, blank=True) + meta_keywords_fr = models.CharField(_('mots-clés SEO (français)'), max_length=300, blank=True) + meta_keywords_en = models.CharField(_('SEO keywords (English)'), max_length=300, blank=True) + + # Caractéristiques libres + features = models.ManyToManyField( + PropertyFeature, blank=True, + verbose_name=_('caractéristiques'), + ) + + created_at = models.DateTimeField(_('créé le'), auto_now_add=True) + updated_at = models.DateTimeField(_('modifié le'), auto_now=True) + + class Meta: + verbose_name = _('bien immobilier') + verbose_name_plural = _('biens immobiliers') + ordering = ['-created_at'] + + def __str__(self): + return self.name_fr + + def save(self, *args, **kwargs): + """Génère automatiquement le slug depuis le nom français.""" + if not self.slug: + self.slug = slugify(self.name_fr) + super().save(*args, **kwargs) + + @property + def name(self): + """Nom dans la langue active.""" + return self.get_translated('name') + + @property + def description(self): + """Description dans la langue active.""" + return self.get_translated('description') + + @property + def cover_photo(self): + """Retourne la photo de couverture ou la première photo disponible.""" + cover = self.photos.filter(is_cover=True).first() + if cover: + return cover + return self.photos.first() + + @property + def price_display(self): + """Prix formaté pour l'affichage.""" + if self.price_on_request: + return _('Prix sur demande') + return f'{int(self.price):,} €'.replace(',', ' ') + + @property + def seo_title(self): + """Titre SEO : champ personnalisé ou génération automatique.""" + custom = self.get_translated('meta_title') + if custom: + return custom + return f'{self.name} — {self.get_property_type_display()} à {self.city}' + + @property + def seo_description(self): + """Meta description : champ personnalisé ou début de description tronqué.""" + custom = self.get_translated('meta_description') + if custom: + return custom + desc = self.get_translated('description') + words = desc.split() + truncated = ' '.join(words[:28]) + return (truncated + '…') if len(words) > 28 else truncated + + @property + def seo_keywords(self): + """Mots-clés SEO : champ personnalisé ou génération depuis type/ville/caractéristiques.""" + custom = self.get_translated('meta_keywords') + if custom: + return custom + parts = [ + self.city, + self.postal_code, + self.get_property_type_display(), + self.get_status_display(), + ] + parts += [f.name for f in self.features.all()[:5]] + return ', '.join(filter(None, parts)) + + def get_absolute_url(self): + from django.urls import reverse + return reverse('properties:detail', kwargs={'slug': self.slug}) + + +class PropertyEnvironment(BilingualMixin, models.Model): + """Informations sur l'environnement géographique du bien.""" + + LOCATION_TYPES = [ + ('city', _('Ville')), + ('village', _('Village')), + ('countryside', _('Campagne')), + ] + + property = models.OneToOneField( + Property, on_delete=models.CASCADE, + related_name='environment', verbose_name=_('bien'), + ) + location_type = models.CharField(_('type de localisation'), max_length=20, choices=LOCATION_TYPES) + description_fr = models.TextField(_('description géographique (français)'), blank=True) + description_en = models.TextField(_('geographic description (English)'), blank=True) + + # Commerces de proximité + has_supermarket = models.BooleanField(_('supérette'), default=False) + has_food_shop = models.BooleanField(_('alimentation'), default=False) + has_bakery = models.BooleanField(_('boulangerie'), default=False) + has_newspaper_shop = models.BooleanField(_('journaux / tabac presse'), default=False) + has_bar = models.BooleanField(_('bar / café'), default=False) + has_tobacco_shop = models.BooleanField(_('bureau de tabac'), default=False) + has_garage = models.BooleanField(_('garage / garagiste'), default=False) + has_gas_station = models.BooleanField(_('station essence'), default=False) + + # Services mairie / éducation + has_city_hall_services = models.BooleanField(_('services de la mairie'), default=False) + has_nursery = models.BooleanField(_('crèche'), default=False) + has_public_school = models.BooleanField(_('école publique'), default=False) + + # Santé + has_doctor = models.BooleanField(_('médecin'), default=False) + has_pharmacy = models.BooleanField(_('pharmacie'), default=False) + has_physiotherapist = models.BooleanField(_('cabinet kinésithérapeute'), default=False) + has_nurse = models.BooleanField(_('cabinet infirmier'), default=False) + + class Meta: + verbose_name = _('environnement') + verbose_name_plural = _('environnements') + + def __str__(self): + return f'Environnement — {self.property.name_fr}' + + # @builtins.property car 'property' est un champ FK dans cette classe, + # ce qui masquerait le built-in Python @property. + @builtins.property + def description(self): + """Description dans la langue active.""" + return self.get_translated('description') + + def get_nearby_shops(self): + """Retourne les commerces sous forme de triplets (icône, label, présent).""" + return [ + ('🛒', _('Supérette'), self.has_supermarket), + ('🏪', _('Alimentation'), self.has_food_shop), + ('🥖', _('Boulangerie'), self.has_bakery), + ('📰', _('Journaux / Presse'), self.has_newspaper_shop), + ('🍺', _('Bar / Café'), self.has_bar), + ('🚬', _('Bureau de tabac'), self.has_tobacco_shop), + ('🔧', _('Garagiste'), self.has_garage), + ('⛽', _('Station essence'), self.has_gas_station), + ] + + def get_public_services(self): + """Retourne les services publics sous forme de triplets (icône, label, présent).""" + return [ + ('🏛️', _('Services mairie'), self.has_city_hall_services), + ('🍼', _('Crèche'), self.has_nursery), + ('🏫', _('École publique'), self.has_public_school), + ] + + def get_health_services(self): + """Retourne les services de santé sous forme de triplets (icône, label, présent).""" + return [ + ('🩺', _('Médecin'), self.has_doctor), + ('💊', _('Pharmacie'), self.has_pharmacy), + ('🤸', _('Kinésithérapeute'), self.has_physiotherapist), + ('🩹', _('Cabinet infirmier'), self.has_nurse), + ] + + +class Room(BilingualMixin, models.Model): + """Pièce d'un bien immobilier, utilisée pour organiser l'album photos.""" + + property = models.ForeignKey( + Property, on_delete=models.CASCADE, + related_name='rooms', verbose_name=_('bien'), + ) + name_fr = models.CharField(_('nom (français)'), max_length=100) + name_en = models.CharField(_('name (English)'), max_length=100) + order = models.PositiveSmallIntegerField(_('ordre'), default=0, db_index=True) + + class Meta: + verbose_name = _('pièce') + verbose_name_plural = _('pièces') + ordering = ['order', 'id'] + + def __str__(self): + return f'{self.property.name_fr} — {self.name_fr}' + + @builtins.property + def name(self): + """Nom dans la langue active.""" + return self.get_translated('name') + + +class Photo(BilingualMixin, models.Model): + """Photo associée à un bien et optionnellement à une pièce.""" + + property = models.ForeignKey( + Property, on_delete=models.CASCADE, + related_name='photos', verbose_name=_('bien'), + ) + room = models.ForeignKey( + Room, on_delete=models.SET_NULL, + null=True, blank=True, + related_name='photos', verbose_name=_('pièce'), + ) + image = ResizedImageField( + size=[1600, 1067], quality=85, upload_to='properties/photos/', + force_format='WEBP', verbose_name=_('image'), + ) + caption_fr = models.CharField(_('légende (français)'), max_length=200, blank=True) + caption_en = models.CharField(_('caption (English)'), max_length=200, blank=True) + is_cover = models.BooleanField(_('photo de couverture'), default=False) + order = models.PositiveSmallIntegerField(_('ordre'), default=0, db_index=True) + + class Meta: + verbose_name = _('photo') + verbose_name_plural = _('photos') + ordering = ['order', 'id'] + + def __str__(self): + return f'Photo {self.order + 1} — {self.property.name_fr}' + + @builtins.property + def caption(self): + """Légende dans la langue active.""" + return self.get_translated('caption') + + +class ContactRequest(models.Model): + """Demande de contact envoyée depuis le formulaire public.""" + + property = models.ForeignKey( + Property, on_delete=models.SET_NULL, + null=True, blank=True, + related_name='contact_requests', verbose_name=_('bien concerné'), + ) + name = models.CharField(_('nom'), max_length=200) + email = models.EmailField(_('email')) + phone = models.CharField(_('téléphone'), max_length=30, blank=True) + message = models.TextField(_('message')) + ip_address = models.GenericIPAddressField(_('adresse IP'), null=True, blank=True) + created_at = models.DateTimeField(_('reçu le'), auto_now_add=True) + + class Meta: + verbose_name = _('demande de contact') + verbose_name_plural = _('demandes de contact') + ordering = ['-created_at'] + + def __str__(self): + return f'{self.name} <{self.email}> — {self.created_at:%d/%m/%Y %H:%M}' diff --git a/properties/sitemaps.py b/properties/sitemaps.py new file mode 100644 index 0000000..0e66075 --- /dev/null +++ b/properties/sitemaps.py @@ -0,0 +1,16 @@ +""" +Sitemap XML pour les biens immobiliers actifs. +""" +from django.contrib.sitemaps import Sitemap +from .models import Property + + +class PropertySitemap(Sitemap): + changefreq = 'weekly' + priority = 0.8 + + def items(self): + return Property.objects.filter(is_active=True).order_by('-updated_at') + + def lastmod(self, obj): + return obj.updated_at diff --git a/properties/templatetags/__init__.py b/properties/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/properties/templatetags/property_tags.py b/properties/templatetags/property_tags.py new file mode 100644 index 0000000..c51d320 --- /dev/null +++ b/properties/templatetags/property_tags.py @@ -0,0 +1,98 @@ +""" +Tags et filtres de templates spécifiques à l'application LOGIS. +""" +from django import template +from django.utils.safestring import mark_safe + +register = template.Library() + +# Couleurs officielles des classes DPE +DPE_COLORS = { + 'A': '#00B050', 'B': '#67B346', 'C': '#ACCC54', + 'D': '#F5D33B', 'E': '#F6A600', 'F': '#E95A0C', 'G': '#CE1622', +} + + +@register.filter +def dpe_color(letter): + """Retourne la couleur hexadécimale correspondant à une classe DPE/GES.""" + return DPE_COLORS.get(str(letter).upper(), '#888888') + + +@register.filter +def price_fr(value): + """Formate un nombre décimal en prix européen (ex: 250 000 €).""" + try: + return f'{int(value):,} €'.replace(',', ' ') + except (ValueError, TypeError): + return value + + +@register.simple_tag +def dpe_badge(letter, value=None, unit='kWh/m²/an'): + """Génère un badge HTML coloré pour une classe DPE ou GES.""" + if not letter: + return mark_safe('') + color = DPE_COLORS.get(str(letter).upper(), '#888') + extra = f'{value} {unit}' if value else '' + return mark_safe( + f'' + f'{letter}{extra}' + ) + + +@register.inclusion_tag('properties/partials/dpe_scale.html') +def dpe_scale(energy_class, ges_class): + """Affiche la double échelle DPE/GES avec la classe courante mise en valeur.""" + return { + 'classes': list('ABCDEFG'), + 'dpe_colors': DPE_COLORS, + 'energy_class': energy_class, + 'ges_class': ges_class, + } + + +@register.simple_tag(takes_context=True) +def lang_switch_url(context, lang_code): + """Retourne l'URL de la page courante traduite dans lang_code. + + Calcule la cible directement via resolve/reverse au lieu de déléguer à + translate_url(), qui peut échouer avec prefix_default_language=False. + """ + from django.urls import resolve, reverse, Resolver404, NoReverseMatch + from django.utils.translation import override + + request = context.get('request') + if not request: + return '/' + try: + match = resolve(request.path_info) + with override(lang_code): + return reverse(match.view_name, args=match.args, kwargs=match.kwargs) + except (Resolver404, NoReverseMatch): + pass + with override(lang_code): + try: + return reverse('properties:list') + except NoReverseMatch: + return '/' + + +@register.filter +def get_item(dictionary, key): + """Retourne dictionary[key] depuis un template (dict|get_item:key).""" + return dictionary.get(key, '') + + +@register.filter +def service_icon(service_name): + """Retourne une icône unicode pour un service de proximité.""" + icons = { + 'supermarket': '🛒', 'food_shop': '🥦', 'bakery': '🥖', + 'newspaper_shop': '📰', 'bar': '☕', 'tobacco_shop': '🚬', + 'garage': '🔧', 'gas_station': '⛽', + 'city_hall': '🏛️', 'nursery': '🍼', 'public_school': '🏫', + 'doctor': '🩺', 'pharmacy': '💊', 'physiotherapist': '🏃', + 'nurse': '💉', + } + return icons.get(service_name, '✓') diff --git a/properties/urls.py b/properties/urls.py new file mode 100644 index 0000000..1dcae7a --- /dev/null +++ b/properties/urls.py @@ -0,0 +1,13 @@ +""" +URLs de l'application properties. +""" +from django.urls import path +from . import views + +app_name = 'properties' + +urlpatterns = [ + path('', views.PropertyListView.as_view(), name='list'), + path('bien//', views.PropertyDetailView.as_view(), name='detail'), + path('contact/', views.ContactView.as_view(), name='contact'), +] diff --git a/properties/views.py b/properties/views.py new file mode 100644 index 0000000..76ca351 --- /dev/null +++ b/properties/views.py @@ -0,0 +1,147 @@ +""" +Vues de l'application LOGIS. +""" +import logging +import datetime + +from django.conf import settings +from django.core.mail import send_mail +from django.http import JsonResponse +from django.template.loader import render_to_string +from django.utils import timezone +from django.utils.translation import gettext as _ +from django.views.generic import ListView, DetailView, View + +from .forms import ContactForm +from .models import Property, ContactRequest + +logger = logging.getLogger(__name__) + + +class PropertyListView(ListView): + """Page d'accueil — liste des biens immobiliers actifs.""" + + model = Property + template_name = 'index.html' + context_object_name = 'properties' + + def get_queryset(self): + """Retourne les biens actifs avec leurs relations préchargées.""" + return ( + Property.objects.filter(is_active=True) + .prefetch_related('photos', 'features') + .select_related('environment') + ) + + +class PropertyDetailView(DetailView): + """Page de détail d'un bien immobilier.""" + + model = Property + template_name = 'properties/detail.html' + context_object_name = 'property' + + def get_queryset(self): + """Retourne uniquement les biens actifs.""" + return ( + Property.objects.filter(is_active=True) + .prefetch_related('photos', 'rooms__photos', 'features') + .select_related('environment') + ) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + prop = self.object + context['contact_form'] = ContactForm( + initial={'property_id': prop.pk} + ) + context['rooms_with_photos'] = prop.rooms.prefetch_related('photos').all() + context['general_photos'] = prop.photos.filter(room__isnull=True) + return context + + +class ContactView(View): + """Endpoint AJAX pour le formulaire de contact.""" + + def post(self, request, *args, **kwargs): + # Accepte uniquement les requêtes AJAX + if not request.headers.get('X-Requested-With') == 'XMLHttpRequest': + return JsonResponse({'success': False}, status=400) + + form = ContactForm(request.POST) + + if not form.is_valid(): + return JsonResponse({'success': False, 'errors': form.errors}) + + # Vérification honeypot : si rempli, c'est un robot + if form.cleaned_data.get('website'): + return JsonResponse({'success': True}) + + # Limitation de débit : 5 minutes entre deux envois par session + last_contact = request.session.get('last_contact') + if last_contact: + try: + last_time = datetime.datetime.fromisoformat(last_contact) + if timezone.is_naive(last_time): + last_time = timezone.make_aware(last_time) + elapsed = (timezone.now() - last_time).total_seconds() + if elapsed < 300: + remaining = int((300 - elapsed) / 60) + 1 + return JsonResponse({ + 'success': False, + 'error': str(_( + 'Merci de patienter encore %(min)s minute(s) avant d\'envoyer un nouveau message.' + ) % {'min': remaining}), + }) + except (ValueError, TypeError): + pass + + # Récupération du bien concerné + property_obj = None + pid = form.cleaned_data.get('property_id') + if pid: + try: + property_obj = Property.objects.get(pk=pid, is_active=True) + except Property.DoesNotExist: + pass + + # Enregistrement en base + contact = ContactRequest.objects.create( + property=property_obj, + name=form.cleaned_data['name'], + email=form.cleaned_data['email'], + phone=form.cleaned_data.get('phone', ''), + message=form.cleaned_data['message'], + ip_address=self._get_client_ip(request), + ) + + # Envoi de l'email à l'administrateur + try: + ctx = {'contact': contact, 'property': property_obj} + subject = render_to_string('emails/contact_subject.txt', ctx).strip() + body_text = render_to_string('emails/contact_body.txt', ctx) + body_html = render_to_string('emails/contact_body.html', ctx) + send_mail( + subject=subject, + message=body_text, + from_email=settings.DEFAULT_FROM_EMAIL, + recipient_list=[settings.ADMIN_EMAIL], + html_message=body_html, + fail_silently=False, + ) + except Exception as exc: + # L'email échoue mais le contact est enregistré en base + logger.error('Échec envoi email contact #%s : %s', contact.pk, exc) + + request.session['last_contact'] = timezone.now().isoformat() + return JsonResponse({ + 'success': True, + 'message': str(_('Votre message a bien été envoyé. Nous vous répondrons dans les plus brefs délais.')), + }) + + def _get_client_ip(self, request): + """Extrait l'adresse IP réelle du client derrière un proxy.""" + forwarded = request.META.get('HTTP_X_FORWARDED_FOR') + if forwarded: + return forwarded.split(',')[0].strip() + return request.META.get('REMOTE_ADDR') diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c3a1249 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +Django>=4.2,<5.1 +django-resized>=1.0.2 +Pillow>=10.0.0 +gunicorn>=21.0.0 +python-decouple>=3.8 +django-admin-sortable2>=2.1 +whitenoise>=6.6.0 diff --git a/static/css/main.css b/static/css/main.css new file mode 100644 index 0000000..3effc5a --- /dev/null +++ b/static/css/main.css @@ -0,0 +1,816 @@ +/* ============================================================ + LOGIS — Thème sombre moderne, compatible mobile + ============================================================ */ + +/* ── Variables ───────────────────────────────────────────── */ +:root { + --bg: #0d1117; + --bg-card: #161b22; + --bg-card-hov: #1c2430; + --bg-sidebar: #111827; + --border: #30363d; + --accent: #e94560; + --accent-hov: #c73652; + --gold: #c9a84c; + --text: #e6edf3; + --text-muted: #8b949e; + --text-faint: #484f58; + --success: #3fb950; + --shadow: 0 4px 16px rgba(0,0,0,.45); + --radius: 10px; + --radius-sm: 6px; + --transition: .2s ease; + --max-w: 1200px; +} + +/* ── Reset ───────────────────────────────────────────────── */ +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } +html { font-size: 16px; scroll-behavior: smooth; } +body { + background: var(--bg); + color: var(--text); + font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; + line-height: 1.6; + min-height: 100vh; + display: flex; + flex-direction: column; +} +main { flex: 1; } +img { display: block; max-width: 100%; height: auto; } +a { color: inherit; text-decoration: none; } +button { cursor: pointer; font: inherit; border: none; background: none; } +ul { list-style: none; } + +/* ── Utilitaires ─────────────────────────────────────────── */ +.container { max-width: var(--max-w); margin: 0 auto; padding: 0 1.25rem; } + +/* ── Boutons ─────────────────────────────────────────────── */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: .5rem; + padding: .75rem 1.75rem; + border-radius: var(--radius-sm); + font-size: .95rem; + font-weight: 600; + transition: background var(--transition), transform var(--transition), + box-shadow var(--transition); +} +.btn-primary { + background: var(--accent); + color: #fff; +} +.btn-primary:hover, .btn-primary:focus-visible { + background: var(--accent-hov); + transform: translateY(-1px); + box-shadow: 0 4px 14px rgba(233,69,96,.35); +} +.btn-full { width: 100%; } +.btn-hero { + margin-top: 1.5rem; + padding: .9rem 2.5rem; + font-size: 1.05rem; +} + +/* ── Navigation ──────────────────────────────────────────── */ +.site-header { + position: sticky; + top: 0; + z-index: 100; + background: rgba(13,17,23,.92); + backdrop-filter: blur(12px); + border-bottom: 1px solid var(--border); +} +.navbar { + display: flex; + align-items: center; + justify-content: space-between; + height: 4rem; +} +.navbar-brand { + display: flex; + align-items: center; + gap: .6rem; + font-size: 1.3rem; + font-weight: 700; + letter-spacing: .05em; +} +.brand-icon { font-size: 1.4rem; } +.brand-name { color: var(--text); } + +/* Sélecteur de langue */ +.lang-form { display: inline; } +.lang-switcher { + display: flex; + gap: .25rem; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 20px; + padding: .2rem .3rem; +} +.lang-btn { + padding: .25rem .65rem; + border-radius: 16px; + font-size: .8rem; + font-weight: 600; + color: var(--text-muted); + transition: background var(--transition), color var(--transition); +} +.lang-btn.active, +.lang-btn:hover { background: var(--accent); color: #fff; } + +/* ── Hero ────────────────────────────────────────────────── */ +.hero { + position: relative; + min-height: clamp(400px, 70vh, 680px); + display: flex; + align-items: center; + background: var(--bg-card) + var(--hero-bg, none) + center/cover no-repeat; +} +.hero-generic { background-image: none; background: linear-gradient(135deg,#0d1117,#1a1a2e); } +.hero-overlay { + position: absolute; + inset: 0; + background: linear-gradient( + to right, + rgba(13,17,23,.85) 0%, + rgba(13,17,23,.55) 60%, + rgba(13,17,23,.25) 100% + ); +} +.hero-content { + position: relative; + z-index: 1; + max-width: 620px; +} +.hero-label { + display: inline-block; + padding: .3rem .9rem; + background: var(--accent); + color: #fff; + border-radius: 20px; + font-size: .8rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: .08em; + margin-bottom: 1rem; +} +.hero-title { + font-size: clamp(1.8rem, 5vw, 3.2rem); + font-weight: 800; + line-height: 1.15; + margin-bottom: .75rem; +} +.hero-meta { + color: var(--text-muted); + font-size: 1.05rem; + margin-bottom: .5rem; +} +.hero-price { + font-size: clamp(1.4rem, 3vw, 2rem); + font-weight: 700; + color: var(--gold); +} +.hero-subtitle { + font-size: 1.15rem; + color: var(--text-muted); + margin-top: .5rem; +} + +/* ── Grille des biens ────────────────────────────────────── */ +.properties-section { padding: 4rem 0; } +.properties-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: 1.5rem; +} + +/* ── Carte bien ──────────────────────────────────────────── */ +.property-card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: hidden; + transition: transform var(--transition), box-shadow var(--transition), + border-color var(--transition); +} +.property-card:hover { + transform: translateY(-4px); + box-shadow: var(--shadow); + border-color: var(--accent); +} +.property-card-link { display: block; } + +.property-card-img-wrapper { + position: relative; + aspect-ratio: 4/3; + overflow: hidden; + background: var(--bg); +} +.property-card-img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform .4s ease; +} +.property-card:hover .property-card-img { transform: scale(1.04); } +.property-card-img-placeholder { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + font-size: 4rem; + color: var(--text-faint); +} + +/* Badges sur la vignette */ +.property-card-status { + position: absolute; + top: .75rem; + left: .75rem; + padding: .25rem .75rem; + border-radius: 20px; + font-size: .75rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: .06em; +} +.status-sale { background: var(--accent); color: #fff; } +.status-rent { background: #0e7fe0; color: #fff; } + +.property-card-dpe { + position: absolute; + top: .75rem; + right: .75rem; + width: 2rem; + height: 2rem; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + font-weight: 800; + font-size: .9rem; + color: #fff; +} + +.property-card-body { padding: 1.25rem; } +.property-card-title { + font-size: 1.1rem; + font-weight: 700; + margin-bottom: .4rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.property-card-location { + font-size: .85rem; + color: var(--text-muted); + margin-bottom: .85rem; +} + +.property-card-specs { + display: flex; + gap: .75rem; + flex-wrap: wrap; + font-size: .85rem; + color: var(--text-muted); + margin-bottom: 1rem; +} +.property-card-specs li { display: flex; align-items: center; gap: .3rem; } + +.property-card-price { + font-size: 1.25rem; + font-weight: 800; + color: var(--gold); +} +.property-card-price small { font-size: .8rem; font-weight: 400; color: var(--text-muted); } + +.empty-state { + text-align: center; + padding: 4rem 1rem; + color: var(--text-muted); +} + +/* ── Page de détail ──────────────────────────────────────── */ +.detail-page { padding-bottom: 4rem; } + +.detail-header { + padding-top: 2rem; + padding-bottom: 1.5rem; + border-bottom: 1px solid var(--border); +} +.detail-header-meta { + display: flex; + align-items: center; + gap: 1rem; + margin-bottom: .75rem; +} +.back-link { + font-size: .88rem; + color: var(--text-muted); + transition: color var(--transition); +} +.back-link:hover { color: var(--accent); } +.detail-status { + padding: .25rem .85rem; + border-radius: 20px; + font-size: .78rem; + font-weight: 700; + text-transform: uppercase; +} +.detail-title { + font-size: clamp(1.5rem, 4vw, 2.4rem); + font-weight: 800; + margin-bottom: .5rem; +} +.detail-location { color: var(--text-muted); margin-bottom: .75rem; } +.detail-price { + font-size: clamp(1.4rem, 3vw, 2rem); + font-weight: 800; + color: var(--gold); +} +.detail-price small { font-size: .8rem; font-weight: 400; color: var(--text-muted); } + +/* ── Galerie ─────────────────────────────────────────────── */ +.detail-gallery { + margin-top: 2rem; +} + +.gallery-tabs { + display: flex; + gap: .5rem; + flex-wrap: wrap; + margin-bottom: 1rem; + border-bottom: 1px solid var(--border); + padding-bottom: .75rem; +} +.gallery-tab { + padding: .4rem 1rem; + border-radius: 20px; + font-size: .85rem; + font-weight: 600; + color: var(--text-muted); + background: var(--bg-card); + border: 1px solid var(--border); + transition: background var(--transition), color var(--transition), border-color var(--transition); +} +.gallery-tab:hover { color: var(--text); border-color: var(--text-muted); } +.gallery-tab.active { background: var(--accent); color: #fff; border-color: var(--accent); } + +.gallery-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + gap: .5rem; + display: none; +} +.gallery-grid.active { display: grid; } + +.gallery-item { + position: relative; + aspect-ratio: 4/3; + overflow: hidden; + border-radius: var(--radius-sm); + border: 2px solid transparent; + transition: border-color var(--transition), transform var(--transition); + background: var(--bg-card); +} +.gallery-item:hover { border-color: var(--accent); transform: scale(1.02); } +.gallery-thumb { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform .3s ease; +} +.gallery-item:hover .gallery-thumb { transform: scale(1.06); } +.gallery-item-caption { + position: absolute; + bottom: 0; + left: 0; + right: 0; + padding: .35rem .5rem; + background: linear-gradient(transparent, rgba(0,0,0,.7)); + color: #fff; + font-size: .75rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.no-photos { color: var(--text-muted); padding: 2rem 0; } + +/* ── Corps détail ────────────────────────────────────────── */ +.detail-body { + display: grid; + grid-template-columns: 1fr 340px; + gap: 2rem; + margin-top: 2.5rem; + align-items: start; +} +@media (max-width: 900px) { + .detail-body { grid-template-columns: 1fr; } +} + +.detail-section { margin-bottom: 2.5rem; } +.section-title { + font-size: 1.2rem; + font-weight: 700; + margin-bottom: 1rem; + padding-bottom: .5rem; + border-bottom: 2px solid var(--accent); + display: inline-block; +} +.detail-description { + color: var(--text-muted); + line-height: 1.8; +} +.detail-description p { margin-bottom: .85rem; } + +/* Environnement */ +.env-type { + font-size: 1rem; + font-weight: 600; + margin-bottom: .75rem; +} +.env-description { + color: var(--text-muted); + margin-bottom: 1.5rem; + line-height: 1.7; +} +.env-services { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 1.5rem; +} +.env-group-title { + font-size: .9rem; + font-weight: 700; + margin-bottom: .6rem; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: .05em; +} +.env-list { display: flex; flex-direction: column; gap: .3rem; } +.env-item { + display: flex; + align-items: center; + gap: .5rem; + font-size: .88rem; +} +.env-available { color: var(--success); } +.env-unavailable { color: var(--text-faint); } +.env-icon { + width: 1.2rem; + text-align: center; + font-weight: 700; + flex-shrink: 0; +} + +/* ── Sidebar ─────────────────────────────────────────────── */ +.detail-sidebar { display: flex; flex-direction: column; gap: 1.25rem; } +.sidebar-card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1.5rem; +} +.sidebar-title { + font-size: 1rem; + font-weight: 700; + margin-bottom: 1rem; + padding-bottom: .5rem; + border-bottom: 1px solid var(--border); +} + +.key-info { + display: grid; + grid-template-columns: 1fr 1fr; + gap: .4rem 1rem; +} +.key-info dt { + font-size: .78rem; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: .04em; + padding-top: .6rem; + border-top: 1px solid var(--border); +} +.key-info dd { + font-size: .9rem; + font-weight: 600; + padding-top: .6rem; + border-top: 1px solid var(--border); +} +.key-info dt:nth-child(1), .key-info dd:nth-child(2) { border-top: none; padding-top: 0; } + +.exterior-badges { display: flex; flex-wrap: wrap; gap: .4rem; margin-top: 1rem; } +.badge { + display: inline-flex; + align-items: center; + gap: .3rem; + padding: .25rem .7rem; + border-radius: 20px; + font-size: .78rem; + font-weight: 600; +} +.badge-exterior { background: rgba(201,168,76,.15); color: var(--gold); border: 1px solid rgba(201,168,76,.3); } + +.features-list { margin-top: 1.25rem; } +.features-title { + font-size: .8rem; + font-weight: 700; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: .05em; + margin-bottom: .6rem; +} +.features-list ul { + display: flex; + flex-direction: column; + gap: .35rem; +} +.features-list li { + font-size: .88rem; + color: var(--text-muted); + display: flex; + align-items: center; + gap: .4rem; +} +.features-list li::before { content: '✓'; color: var(--success); font-size: .85rem; } + +/* Diagnostics */ +.diagnostics-card {} +.diag-row { + display: flex; + align-items: center; + justify-content: space-between; + padding: .6rem 0; + border-bottom: 1px solid var(--border); + gap: .5rem; +} +.diag-row:last-of-type { border-bottom: none; } +.diag-label { font-size: .85rem; color: var(--text-muted); } + +.dpe-badge { + display: inline-flex; + align-items: center; + gap: .5rem; + padding: .3rem .8rem; + border-radius: var(--radius-sm); + color: #fff; + font-weight: 700; +} +.dpe-badge strong { font-size: 1.1rem; } +.dpe-badge small { font-size: .75rem; font-weight: 400; opacity: .85; } +.dpe-unknown { background: #444; color: var(--text-muted); } + +.dpe-scale-wrapper { margin-top: 1rem; } +.dpe-scale { display: flex; flex-direction: column; gap: 3px; } +.dpe-scale-label { font-size: .75rem; color: var(--text-faint); margin-bottom: .4rem; } +.dpe-bar { + display: flex; + align-items: center; + justify-content: space-between; + min-width: 50px; + height: 22px; + border-radius: 0 4px 4px 0; + padding: 0 8px; + color: #fff; + font-size: .75rem; + font-weight: 700; + transition: filter .2s; +} +.dpe-active { filter: brightness(1.2); box-shadow: 0 0 0 2px #fff; } +.dpe-arrow { font-size: .7rem; font-weight: 400; opacity: .9; } + +/* Contact sidebar */ +.contact-intro { + font-size: .9rem; + color: var(--text-muted); + margin-bottom: 1.25rem; + line-height: 1.6; +} + +/* ── Modal générique ─────────────────────────────────────── */ +.modal { + position: fixed; + inset: 0; + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; + padding: 1rem; +} +.modal[hidden] { display: none; } +.modal-overlay { + position: absolute; + inset: 0; + background: rgba(0,0,0,.7); + backdrop-filter: blur(4px); +} +.modal-box { + position: relative; + z-index: 1; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius); + width: 100%; + max-width: 520px; + max-height: 90vh; + overflow-y: auto; + padding: 2rem; + box-shadow: var(--shadow); +} +.modal-close { + position: absolute; + top: 1rem; + right: 1rem; + font-size: 1.5rem; + color: var(--text-muted); + line-height: 1; + transition: color var(--transition); +} +.modal-close:hover { color: var(--accent); } +.modal-title { + font-size: 1.15rem; + font-weight: 700; + margin-bottom: 1.5rem; +} + +/* ── Formulaire de contact ───────────────────────────────── */ +.hp-field { position: absolute; left: -9999px; top: -9999px; opacity: 0; } +.contact-form { display: flex; flex-direction: column; gap: 1rem; } +.form-group { display: flex; flex-direction: column; gap: .35rem; } +.form-group label { + font-size: .85rem; + font-weight: 600; + color: var(--text-muted); +} +.required { color: var(--accent); } +.optional { color: var(--text-faint); font-weight: 400; } +.form-group input, +.form-group textarea { + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text); + padding: .65rem .9rem; + font-size: .95rem; + transition: border-color var(--transition); + width: 100%; +} +.form-group input:focus, +.form-group textarea:focus { + outline: none; + border-color: var(--accent); +} +.form-group textarea { resize: vertical; min-height: 100px; } +.field-error { font-size: .8rem; color: var(--accent); min-height: 1em; } +.form-notice { + font-size: .78rem; + color: var(--text-faint); + line-height: 1.5; +} +.form-global-error { + background: rgba(233,69,96,.12); + border: 1px solid rgba(233,69,96,.3); + border-radius: var(--radius-sm); + padding: .75rem 1rem; + font-size: .88rem; + color: var(--accent); +} +.form-global-error[hidden] { display: none; } + +.contact-success { + text-align: center; + padding: 1.5rem 0; +} +.contact-success[hidden] { display: none; } +.success-icon { font-size: 2.5rem; color: var(--success); } +.success-title { font-size: 1.15rem; font-weight: 700; margin: .75rem 0 .4rem; } +.success-body { color: var(--text-muted); } + +/* ── Modal galerie ───────────────────────────────────────── */ +.gallery-modal { + position: fixed; + inset: 0; + z-index: 1100; + display: flex; + align-items: center; + justify-content: center; +} +.gallery-modal[hidden] { display: none; } +.gallery-modal-overlay { + position: absolute; + inset: 0; + background: rgba(0,0,0,.92); +} +.gallery-modal-content { + position: relative; + z-index: 1; + display: flex; + align-items: center; + gap: 1rem; + width: 100%; + max-width: 1100px; + padding: 1rem; +} +.gallery-modal-figure { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + gap: .75rem; +} +.gallery-modal-figure img { + max-height: 80vh; + max-width: 100%; + border-radius: var(--radius-sm); + object-fit: contain; + user-select: none; +} +.gallery-modal-caption { + color: rgba(255,255,255,.7); + font-size: .88rem; + text-align: center; +} +.gallery-modal-counter { + position: absolute; + top: 1.25rem; + left: 50%; + transform: translateX(-50%); + color: rgba(255,255,255,.5); + font-size: .8rem; + background: rgba(0,0,0,.5); + padding: .2rem .6rem; + border-radius: 20px; +} +.gallery-modal-close { + position: absolute; + top: 1rem; + right: 1rem; + color: rgba(255,255,255,.7); + font-size: 2rem; + line-height: 1; + z-index: 2; + transition: color var(--transition); +} +.gallery-modal-close:hover { color: #fff; } +.gallery-modal-nav { + color: rgba(255,255,255,.6); + font-size: 3rem; + line-height: 1; + padding: .5rem; + transition: color var(--transition), transform var(--transition); + flex-shrink: 0; +} +.gallery-modal-nav:hover { color: #fff; transform: scale(1.15); } +.gallery-modal-nav:disabled { opacity: .2; pointer-events: none; } + +/* ── Pied de page ────────────────────────────────────────── */ +.site-footer { + background: var(--bg-card); + border-top: 1px solid var(--border); + padding: 2rem 0; + margin-top: 2rem; +} +.footer-inner { + display: flex; + flex-direction: column; + align-items: center; + gap: .4rem; + text-align: center; +} +.footer-brand { + font-weight: 700; + font-size: 1rem; + letter-spacing: .05em; +} +.footer-copy { font-size: .82rem; color: var(--text-muted); } + +/* ── Responsive ──────────────────────────────────────────── */ +@media (max-width: 768px) { + .hero { min-height: 320px; } + .hero-content { max-width: 100%; } + + .properties-grid { grid-template-columns: 1fr; } + + .gallery-grid { grid-template-columns: repeat(2, 1fr); } + + .env-services { grid-template-columns: 1fr; } + + .gallery-modal-nav { display: none; } + .gallery-modal-content { padding: 3rem .5rem .5rem; } + + .key-info { grid-template-columns: 1fr; } + .key-info dt, .key-info dd { border-top: none; padding-top: 0; } + .key-info dt { margin-top: .5rem; } +} + +@media (max-width: 480px) { + .navbar { height: 3.5rem; } + .brand-name { display: none; } + .gallery-grid { grid-template-columns: repeat(2, 1fr); } +} diff --git a/static/images/favicon.svg b/static/images/favicon.svg new file mode 100644 index 0000000..5e585c6 --- /dev/null +++ b/static/images/favicon.svg @@ -0,0 +1,4 @@ + + + 🏠 + diff --git a/static/js/ContactForm.js b/static/js/ContactForm.js new file mode 100644 index 0000000..0a00830 --- /dev/null +++ b/static/js/ContactForm.js @@ -0,0 +1,188 @@ +/** + * Gestion du formulaire de contact en modal avec envoi AJAX. + * Protections : CSRF, honeypot, limitation de débit côté session. + */ +class ContactForm { + /** + * @param {HTMLFormElement} form + * @param {HTMLElement} modal + * @param {string} url - endpoint POST + * @param {string} csrfToken + */ + constructor(form, modal, url, csrfToken) { + this.form = form; + this.modal = modal; + this.url = url; + this.csrfToken = csrfToken; + + this.overlay = modal.querySelector('#contact-overlay'); + this.btnClose = modal.querySelector('#contact-modal-close'); + this.btnOpen = document.querySelector('#open-contact-modal'); + this.submit = form.querySelector('#contact-submit'); + this.success = modal.querySelector('#contact-success'); + this.globalErr = modal.querySelector('#contact-global-error'); + + this._bindEvents(); + } + + // ── Initialisation ───────────────────────────────────────────────────────── + + _bindEvents() { + // Ouvrir + if (this.btnOpen) { + this.btnOpen.addEventListener('click', () => { + this._setPropertyId(this.btnOpen.dataset.property); + this.open(); + }); + } + + // Fermer + if (this.btnClose) this.btnClose.addEventListener('click', () => this.close()); + if (this.overlay) this.overlay.addEventListener('click', () => this.close()); + + // Clavier + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && !this.modal.hidden) this.close(); + }); + + // Soumission + this.form.addEventListener('submit', (e) => this._handleSubmit(e)); + + // Nettoyage des erreurs à la saisie + this.form.querySelectorAll('input, textarea').forEach((el) => { + el.addEventListener('input', () => this._clearFieldError(el.name)); + }); + } + + // ── API publique ─────────────────────────────────────────────────────────── + + open() { + this._resetForm(); + this.modal.hidden = false; + document.body.style.overflow = 'hidden'; + this.form.querySelector('#id_name')?.focus(); + } + + close() { + this.modal.hidden = true; + document.body.style.overflow = ''; + } + + // ── Soumission ───────────────────────────────────────────────────────────── + + async _handleSubmit(e) { + e.preventDefault(); + if (!this._validateLocal()) return; + + this._setLoading(true); + this._hideGlobalError(); + + const formData = new FormData(this.form); + + try { + const response = await fetch(this.url, { + method: 'POST', + body: formData, + headers: { + 'X-Requested-With': 'XMLHttpRequest', + 'X-CSRFToken': this.csrfToken, + }, + }); + + if (!response.ok) throw new Error(`HTTP ${response.status}`); + const data = await response.json(); + + if (data.success) { + this._showSuccess(); + } else if (data.errors) { + this._showFieldErrors(data.errors); + } else if (data.error) { + this._showGlobalError(data.error); + } + } catch (err) { + this._showGlobalError( + (window.I18N?.networkError) || 'Une erreur est survenue. Veuillez réessayer.' + ); + } finally { + this._setLoading(false); + } + } + + // ── Validation locale légère ─────────────────────────────────────────────── + + _validateLocal() { + let valid = true; + const name = this.form.querySelector('#id_name'); + const email = this.form.querySelector('#id_email'); + const msg = this.form.querySelector('#id_message'); + + if (!name.value.trim()) { + this._setFieldError('name', name.required ? 'Ce champ est requis.' : ''); + valid = false; + } + if (!email.value.trim() || !email.value.includes('@')) { + this._setFieldError('email', 'Adresse email invalide.'); + valid = false; + } + if (!msg.value.trim() || msg.value.trim().length < 10) { + this._setFieldError('message', 'Votre message est trop court.'); + valid = false; + } + return valid; + } + + // ── Utilitaires ──────────────────────────────────────────────────────────── + + _setPropertyId(id) { + const input = this.form.querySelector('#id_property_id'); + if (input) input.value = id || ''; + } + + _setLoading(loading) { + if (!this.submit) return; + const i18n = window.I18N || {}; + this.submit.disabled = loading; + this.submit.textContent = loading + ? (i18n.sending || 'Envoi en cours…') + : (i18n.send || 'Envoyer'); + } + + _showSuccess() { + this.form.hidden = true; + this.success.hidden = false; + } + + _resetForm() { + this.form.reset(); + this.form.hidden = false; + this.success.hidden = true; + this._hideGlobalError(); + this.form.querySelectorAll('.field-error').forEach(el => { el.textContent = ''; }); + } + + _setFieldError(name, msg) { + const el = this.form.querySelector(`.field-error[data-field="${name}"]`); + if (el) el.textContent = msg; + } + + _clearFieldError(name) { + this._setFieldError(name, ''); + } + + _showFieldErrors(errors) { + for (const [field, msgs] of Object.entries(errors)) { + const list = Array.isArray(msgs) ? msgs : [msgs]; + this._setFieldError(field, list.join(' ')); + } + } + + _showGlobalError(msg) { + if (!this.globalErr) return; + this.globalErr.textContent = msg; + this.globalErr.hidden = false; + } + + _hideGlobalError() { + if (this.globalErr) this.globalErr.hidden = true; + } +} diff --git a/static/js/Gallery.js b/static/js/Gallery.js new file mode 100644 index 0000000..a27f73d --- /dev/null +++ b/static/js/Gallery.js @@ -0,0 +1,134 @@ +/** + * Galerie photos avec modal plein écran. + * Gère : onglets par pièce, navigation clavier, swipe mobile. + */ +class Gallery { + /** + * @param {Object} data - { 'gallery-key': [{src, caption}, …], … } + * @param {HTMLElement} modal - élément .gallery-modal + */ + constructor(data, modal) { + this.data = data; // toutes les galeries indexées par clé + this.currentKey = null; // clé de la galerie active + this.currentIndex = 0; // index dans la galerie active + + this.modal = modal; + this.img = modal.querySelector('#gallery-modal-img'); + this.caption = modal.querySelector('#gallery-modal-caption'); + this.counter = modal.querySelector('#gallery-counter'); + this.overlay = modal.querySelector('#gallery-overlay'); + this.btnClose = modal.querySelector('#gallery-close'); + this.btnPrev = modal.querySelector('#gallery-prev'); + this.btnNext = modal.querySelector('#gallery-next'); + + this._touchStartX = 0; + + this._bindEvents(); + this._initTabs(); + } + + // ── Initialisation ───────────────────────────────────────────────────────── + + _bindEvents() { + // Boutons de navigation + this.btnClose.addEventListener('click', () => this.close()); + this.overlay.addEventListener('click', () => this.close()); + this.btnPrev.addEventListener('click', () => this.prev()); + this.btnNext.addEventListener('click', () => this.next()); + + // Clavier + document.addEventListener('keydown', (e) => { + if (this.modal.hidden) return; + if (e.key === 'Escape') this.close(); + if (e.key === 'ArrowLeft') this.prev(); + if (e.key === 'ArrowRight') this.next(); + }); + + // Swipe mobile + this.modal.addEventListener('touchstart', (e) => { + this._touchStartX = e.touches[0].clientX; + }, { passive: true }); + this.modal.addEventListener('touchend', (e) => { + const dx = e.changedTouches[0].clientX - this._touchStartX; + if (Math.abs(dx) > 50) { + dx < 0 ? this.next() : this.prev(); + } + }); + + // Délégation : clic sur les vignettes + document.addEventListener('click', (e) => { + const item = e.target.closest('.gallery-item[data-gallery]'); + if (!item) return; + this.open(item.dataset.gallery, parseInt(item.dataset.index, 10)); + }); + } + + _initTabs() { + document.querySelectorAll('.gallery-tab').forEach((tab) => { + tab.addEventListener('click', () => { + // Mise à jour des onglets actifs + document.querySelectorAll('.gallery-tab').forEach(t => { + t.classList.remove('active'); + t.setAttribute('aria-selected', 'false'); + }); + tab.classList.add('active'); + tab.setAttribute('aria-selected', 'true'); + + // Affichage du panneau correspondant + const roomId = tab.dataset.room; + const panelId = roomId === 'general' ? 'room-general' : `room-${roomId}`; + document.querySelectorAll('.gallery-grid').forEach(g => g.classList.remove('active')); + const panel = document.getElementById(panelId); + if (panel) panel.classList.add('active'); + }); + }); + } + + // ── API publique ─────────────────────────────────────────────────────────── + + open(key, index) { + this.currentKey = key; + this.currentIndex = index; + this._render(); + this.modal.hidden = false; + document.body.style.overflow = 'hidden'; + this.btnClose.focus(); + } + + close() { + this.modal.hidden = true; + document.body.style.overflow = ''; + } + + prev() { + const photos = this.data[this.currentKey] || []; + this.currentIndex = (this.currentIndex - 1 + photos.length) % photos.length; + this._render(); + } + + next() { + const photos = this.data[this.currentKey] || []; + this.currentIndex = (this.currentIndex + 1) % photos.length; + this._render(); + } + + // ── Rendu ────────────────────────────────────────────────────────────────── + + _render() { + const photos = this.data[this.currentKey] || []; + if (!photos.length) return; + + const photo = photos[this.currentIndex]; + this.img.src = photo.src; + this.img.alt = photo.caption || ''; + this.caption.textContent = photo.caption || ''; + + const total = photos.length; + const of = (window.I18N && I18N.of) ? I18N.of : '/'; + this.counter.textContent = `${this.currentIndex + 1} ${of} ${total}`; + + // Désactiver les flèches si une seule photo + this.btnPrev.disabled = total <= 1; + this.btnNext.disabled = total <= 1; + } +} diff --git a/static/js/main.js b/static/js/main.js new file mode 100644 index 0000000..c3de7c5 --- /dev/null +++ b/static/js/main.js @@ -0,0 +1,34 @@ +/** + * Point d'entrée principal — instancie Gallery et ContactForm + * après le chargement du DOM. + */ +document.addEventListener('DOMContentLoaded', () => { + + // ── Galerie photos ───────────────────────────────────────────────────────── + const galleryModal = document.getElementById('gallery-modal'); + if (galleryModal && typeof GALLERY_DATA !== 'undefined') { + window._gallery = new Gallery(GALLERY_DATA, galleryModal); + } + + // ── Formulaire de contact ────────────────────────────────────────────────── + const contactModal = document.getElementById('contact-modal'); + const contactForm = document.getElementById('contact-form'); + if (contactModal && contactForm && typeof CONTACT_URL !== 'undefined') { + window._contactForm = new ContactForm( + contactForm, + contactModal, + CONTACT_URL, + typeof CSRF_TOKEN !== 'undefined' ? CSRF_TOKEN : '', + ); + } + + // ── Disparition auto des messages flash Django ───────────────────────────── + document.querySelectorAll('.alert-auto-hide').forEach((alert) => { + setTimeout(() => { + alert.style.opacity = '0'; + alert.style.transition = 'opacity .5s'; + setTimeout(() => alert.remove(), 500); + }, 4000); + }); + +}); diff --git a/templates/admin/properties/property/change_form.html b/templates/admin/properties/property/change_form.html new file mode 100644 index 0000000..f539eec --- /dev/null +++ b/templates/admin/properties/property/change_form.html @@ -0,0 +1,14 @@ +{% extends "admin/change_form.html" %} +{% load i18n %} + +{% block object-tools-items %} + {% if original %} +
  • + + 📷 Ajout groupé de photos + +
  • + {% endif %} + {{ block.super }} +{% endblock %} diff --git a/templates/admin/properties/property/upload_photos.html b/templates/admin/properties/property/upload_photos.html new file mode 100644 index 0000000..73919c4 --- /dev/null +++ b/templates/admin/properties/property/upload_photos.html @@ -0,0 +1,240 @@ +{% extends "admin/base_site.html" %} +{% load i18n static %} + +{% block title %}📷 Ajout de photos — {{ property.name_fr }}{% endblock %} + +{% block extrahead %} +{{ block.super }} + +{% endblock %} + +{% block breadcrumbs %} + +{% endblock %} + +{% block content %} +

    📷 Ajout groupé de photos — {{ property.name_fr }}

    + +{% if messages %} + {% for msg in messages %} +

    {{ msg }}

    + {% endfor %} +{% endif %} + +
    + {% csrf_token %} + +
    +

    1. Sélectionner ou créer une pièce

    + +
    + + +
    + +
    — ou créer une nouvelle pièce —
    + +
    + + +

    Si renseigné, cette pièce est créée et les photos lui sont attribuées (prioritaire sur la sélection ci-dessus).

    +
    + +
    + + +
    +
    + +
    +

    2. Choisir les photos

    + +
    +

    📂 Glisser-déposer les photos ici

    + ou cliquer pour sélectionner (sélection multiple possible) + +
    +
    +
    +
    + +
    + + + + Annuler + +
    + +
    + + +{% endblock %} diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..d10d983 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,96 @@ +{% load i18n static property_tags %} + + + + + + {% block meta_keywords_tag %}{% endblock %} + {% block title %}LOGIS{% endblock %} — {% trans "Immobilier" %} + + + + + + + + + + {% block og_image_tag %}{% endblock %} + {% block extra_head %}{% endblock %} + + {# Alternate links hreflang pour le SEO bilingue — URLs réelles traduites #} + {% get_available_languages as LANGUAGES %} + {% for lang_code, lang_name in LANGUAGES %} + {% lang_switch_url lang_code as hreflang_url %} + + {% endfor %} + + + + + + +{# ── Navigation ─────────────────────────────────────────────────────────── #} + + +{# ── Contenu principal ───────────────────────────────────────────────────── #} +
    + {% block content %}{% endblock %} +
    + +{# ── Pied de page ────────────────────────────────────────────────────────── #} +
    + +
    + +{# ── Modal de la galerie (instanciée par Gallery.js) ────────────────────── #} + + + + + +{% block extra_js %}{% endblock %} + + diff --git a/templates/emails/contact_body.html b/templates/emails/contact_body.html new file mode 100644 index 0000000..8387a80 --- /dev/null +++ b/templates/emails/contact_body.html @@ -0,0 +1,52 @@ + + + + + Demande de contact LOGIS + + +
    +
    +

    LOGIS — Nouvelle demande de contact

    +
    +
    + + + + + + + + + + + + + + {% if contact.phone %} + + + + + {% endif %} + {% if property %} + + + + + {% endif %} +
    Date{{ contact.created_at|date:"d/m/Y à H:i" }}
    Nom{{ contact.name }}
    Email + {{ contact.email }} +
    Téléphone{{ contact.phone }}
    Bien{{ property.name_fr }} — {{ property.city }}
    + +
    +

    Message

    +

    {{ contact.message }}

    +
    +
    +
    + LOGIS{% if SITE_DOMAIN %} — {{ SITE_DOMAIN }}{% endif %} • IP : {{ contact.ip_address|default:"—" }} +
    +
    + + diff --git a/templates/emails/contact_body.txt b/templates/emails/contact_body.txt new file mode 100644 index 0000000..1b8c8be --- /dev/null +++ b/templates/emails/contact_body.txt @@ -0,0 +1,14 @@ +LOGIS — Nouvelle demande de contact +==================================== + +Date : {{ contact.created_at|date:"d/m/Y H:i" }} +Nom : {{ contact.name }} +Email : {{ contact.email }} +Téléphone : {{ contact.phone|default:"—" }} +{% if property %}Bien : {{ property.name_fr }} ({{ property.city }}) +{% endif %} +Adresse IP : {{ contact.ip_address|default:"—" }} + +Message : +--------- +{{ contact.message }} diff --git a/templates/emails/contact_subject.txt b/templates/emails/contact_subject.txt new file mode 100644 index 0000000..2bccef7 --- /dev/null +++ b/templates/emails/contact_subject.txt @@ -0,0 +1 @@ +{% load i18n %}[LOGIS] Demande de renseignements de {{ contact.name }}{% if property %} — {{ property.name_fr }}{% endif %} diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..0d014c9 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,124 @@ +{% extends "base.html" %} +{% load i18n static property_tags %} + +{% block title %}{% trans "Biens immobiliers" %}{% endblock %} + +{% block content %} + +{# ── Hero ────────────────────────────────────────────────────────────────── #} +{% if properties.count == 1 %} + {% with prop=properties.first %} +
    +
    +
    + + {% if prop.status == 'sale' %}{% trans "À vendre" %}{% else %}{% trans "À louer" %}{% endif %} + +

    {{ prop.name }}

    +

    + {{ prop.city }} • {{ prop.living_area }} m² • {{ prop.rooms_count }} {% trans "pièces" %} +

    + {% if not prop.price_on_request %} +

    {{ prop.price|price_fr }}

    + {% else %} +

    {% trans "Prix sur demande" %}

    + {% endif %} + + {% trans "Découvrir ce bien" %} → + +
    +
    + {% endwith %} +{% else %} +
    +
    +
    +

    {% trans "Nos biens immobiliers" %}

    +

    {% trans "Trouvez votre futur chez-vous" %}

    +
    +
    +{% endif %} + +{# ── Grille des biens ────────────────────────────────────────────────────── #} +
    + +
    + +{% endblock %} diff --git a/templates/properties/detail.html b/templates/properties/detail.html new file mode 100644 index 0000000..648a773 --- /dev/null +++ b/templates/properties/detail.html @@ -0,0 +1,352 @@ +{% extends "base.html" %} +{% load i18n static property_tags %} + +{% block title %}{{ property.seo_title }}{% endblock %} +{% block meta_description %}{{ property.seo_description }}{% endblock %} +{% block meta_keywords_tag %}{% endblock %} +{% block canonical %}{{ request.build_absolute_uri }}{% endblock %} +{% block og_title %}{{ property.seo_title }}{% endblock %} +{% block og_description %}{{ property.seo_description }}{% endblock %} +{% block og_type %}website{% endblock %} +{% block og_image_tag %}{% if property.thumbnail %}{% elif property.cover_photo %}{% endif %}{% endblock %} +{% block extra_head %} + +{% endblock %} + +{% block content %} +
    + + {# ── En-tête du bien ───────────────────────────────────────────────────── #} +
    +
    + + ← {% trans "Retour aux annonces" %} + + + {% if property.status == 'sale' %}{% trans "À vendre" %}{% else %}{% trans "À louer" %}{% endif %} + +
    +

    {{ property.name }}

    +

    + 📍 {{ property.address }}, {{ property.city }} ({{ property.postal_code }}) +

    +

    + {% if property.price_on_request %} + {% trans "Prix sur demande" %} + {% else %} + {{ property.price|price_fr }} + {% if property.status == 'rent' %}{% trans "/ mois" %}{% endif %} + {% endif %} +

    +
    + + {# ── Galerie photos ────────────────────────────────────────────────────── #} + + + {# ── Corps — infos + description ───────────────────────────────────────── #} +
    +
    + + {# Description #} +
    +

    {% trans "Description" %}

    +
    + {{ property.description|linebreaks }} +
    +
    + + {# Environnement #} + {% if property.environment %} +
    +

    {% trans "Situation géographique" %}

    + +

    + {% with ltype=property.environment.location_type %} + {% if ltype == 'city' %}🏙 {% trans "Environnement urbain" %} + {% elif ltype == 'village' %}🏘 {% trans "Village" %} + {% else %}🌿 {% trans "Campagne" %} + {% endif %} + {% endwith %} +

    + + {% if property.environment.description %} +

    {{ property.environment.description }}

    + {% endif %} + +
    +
    +

    🛒 {% trans "Commerces de proximité" %}

    +
      + {% for icon, label, available in property.environment.get_nearby_shops %} +
    • + {{ icon }} + {{ label }} +
    • + {% endfor %} +
    +
    + +
    +

    🏛️ {% trans "Services publics" %}

    +
      + {% for icon, label, available in property.environment.get_public_services %} +
    • + {{ icon }} + {{ label }} +
    • + {% endfor %} +
    +
    + +
    +

    🩺 {% trans "Santé" %}

    +
      + {% for icon, label, available in property.environment.get_health_services %} +
    • + {{ icon }} + {{ label }} +
    • + {% endfor %} +
    +
    +
    + +
    + {% endif %} + +
    {# /detail-main #} + + {# ── Fiche récapitulative (sidebar) ──────────────────────────────────── #} + +
    {# /detail-body #} + +
    {# /detail-page #} + +{# ── Modal de contact ─────────────────────────────────────────────────────── #} +{% include "properties/partials/contact_modal.html" %} + +{% endblock %} + +{% block extra_js %} + +{% endblock %} diff --git a/templates/properties/partials/contact_modal.html b/templates/properties/partials/contact_modal.html new file mode 100644 index 0000000..f8777a5 --- /dev/null +++ b/templates/properties/partials/contact_modal.html @@ -0,0 +1,73 @@ +{% load i18n %} +{# Modale de contact — injectée dans la page detail #} + diff --git a/templates/properties/partials/dpe_scale.html b/templates/properties/partials/dpe_scale.html new file mode 100644 index 0000000..9a76752 --- /dev/null +++ b/templates/properties/partials/dpe_scale.html @@ -0,0 +1,14 @@ +{% load i18n property_tags %} +{# Échelle visuelle DPE / GES style barreaux #} +
    +

    {% trans "Échelle de performance" %}

    + {% for cls in classes %} +
    + {{ cls }} + {% if cls == energy_class %} + ◀ {% trans "DPE" %} + {% endif %} +
    + {% endfor %} +
    diff --git a/templates/robots.txt b/templates/robots.txt new file mode 100644 index 0000000..27d659e --- /dev/null +++ b/templates/robots.txt @@ -0,0 +1,6 @@ +User-agent: * +Disallow: /admin/ +Disallow: /i18n/ +Allow: / + +Sitemap: {{ request.scheme }}://{{ request.get_host }}/sitemap.xml