Aller au contenu
  1. Posts/

QFieldCloud auto-hébergé [A06] : habiller l'instance sans toucher au sous-module

Sommaire
QFieldCloud auto-hébergé - Cet article fait partie d'une série.
Partie 11: Cet article

À qui s’adresse cette annexe
#

Mettre son logo sur la page de connexion est la modification la plus tentante à faire dans src/, et celle qu’il ne faut surtout pas faire. Cette annexe montre comment l’obtenir entièrement sans toucher au sous-module — et où se trouve la seule exception.

Le vrai sujet n’est pas le logo #

Les cinq annexes précédentes déplaçaient des briques : le stockage, le courriel, les sauvegardes, le certificat. Celle-ci ne déplace rien. Elle est pourtant le meilleur test du pari de l’épisode 3 — git -C src status doit rester vide —, parce que l’apparence est exactement le domaine où ce pari est le plus facile à perdre.

Le raisonnement qui le fait perdre tient en deux lignes. Le gabarit est là, sous les yeux, dans src/docker-app/qfieldcloud/core/templates/account/base.html. Une balise <img> à changer, trente secondes. Et le jour de la montée de version, git submodule update refuse d’avancer sur un fichier modifié, ou pire, avance en écrasant — et le logo disparaît sans que personne ne sache pourquoi.

La bonne nouvelle, c’est que l’upstream a prévu presque tout ce qu’il faut. La mauvaise, c’est qu’il l’a laissé en commentaire dans settings.py, c’est-à-dire à l’endroit précis qu’on s’est interdit de toucher.

Info : les commandes docker compose se lancent depuis src/, le répertoire du sous-module — c’est de là que COMPOSE_FILE est résolu — et avec --env-file ../.env. Chaque bloc le rappelle par son cd src, à ne faire qu’une fois par session. Le $ en début de ligne est le symbole du terminal, il ne se copie pas.

« La page d’accueil » n’existe pas
#

Premier constat, avant d’écrire la moindre ligne : l’URL qu’on croit personnaliser n’est pas une page, c’est un enchaînement.

$ curl -sko /dev/null -w "%{http_code} -> %{redirect_url}\n" https://qfieldcloud.exemple.fr/
302 -> https://qfieldcloud.exemple.fr/admin/

La racine ne sert rien : elle redirige vers QFIELDCLOUD_ADMIN_URI. L’admin, lui, exige une session et renvoie vers /accounts/login/?next=/admin/. Ce que vous avez sous les yeux quand vous arrivez sur l’instance, ce sont donc deux pages qui n’ont rien en commun :

Page Rendue par Habillée par
/accounts/login/ et tout /accounts/… gabarits account/ de QFieldCloud le dictionnaire WHITELABEL
/admin/…, une fois connecté Django admin JAZZMIN_SETTINGS et JAZZMIN_UI_TWEAKS

Deux systèmes, deux dictionnaires, aucun rapport entre eux. Habiller « l’accueil » sans habiller l’admin donne une instance qui change de peau au moment où l’on s’y connecte, ce qui est plus déroutant que de n’avoir rien fait du tout. Les deux se règlent au même endroit, mais il faut les nommer tous les deux.

Les fichiers qu’on touche
#

Toute l’annexe tient là-dedans : un dossier theme/ à créer, trois fichiers déjà présents auxquels on ajoute. Rien n’est sous src/ — et chaque bloc de code plus bas rappelle en première ligne le fichier auquel il appartient.

Fichier Ce qu’on y met
theme/settings_custom.py nouveauWHITELABEL, JAZZMIN_SETTINGS, JAZZMIN_UI_TWEAKS
theme/static/ nouveau — logos, favicon, theme.css, le fond contours.svg
theme/contours.py nouveau — génère contours.svg. Outil de conception, rien ne l’appelle au déploiement
theme/account-base.html nouveau — le seul gabarit recopié de l’upstream, plus une ligne
.env une ligne : DJANGO_SETTINGS_MODULE
docker-compose.override.yml les montages, sur app et worker_wrapper
Makefile deux cibles : static et theme-diff

Le levier : composer un module de réglages
#

settings.py se termine par ceci :

# src/docker-app/qfieldcloud/settings.py — upstream, en lecture seule
# Whitelabel configuration
# Uncomment and customize to override default branding
# WHITELABEL = {
#     "site_title": "Your Custom Title",
#     …
# }

L’upstream propose de décommenter. Nous ne pouvons pas : ce fichier appartient au sous-module. Mais .env porte déjà la variable qui nous sort de là :

# .env
DJANGO_SETTINGS_MODULE=qfieldcloud.settings

Django ne tient pas à ce que ce module soit celui de l’upstream. Il tient à ce qu’il soit importable. On en écrit donc un second, chez nous, qui importe le premier et n’écrase que l’apparence :

# theme/settings_custom.py
from qfieldcloud.settings import *  # noqa: F401,F403
from qfieldcloud.settings import JAZZMIN_SETTINGS

# La seule ligne à changer : elle nomme l'instance partout, onglet compris.
INSTANCE_NAME = "Mon instance QFieldCloud"

# Pages publiques (connexion, inscription, réinitialisation).
WHITELABEL = {
    "site_title": INSTANCE_NAME,
    "logo_navbar": "custom/logo-navbar.svg",
    "logo_main": "custom/logo-main.svg",
    "logo_alt": INSTANCE_NAME,
    "favicon": "custom/favicon.svg",
}

# Admin Django, habillé par Jazzmin.
JAZZMIN_SETTINGS = {
    **JAZZMIN_SETTINGS,
    "site_title": INSTANCE_NAME,
    "site_header": INSTANCE_NAME,
    "site_brand": INSTANCE_NAME,
    "site_logo": "custom/logo-navbar.svg",
    "site_icon": "custom/favicon.svg",
    "login_logo": "custom/logo-main.svg",
    "welcome_sign": "Plateforme SIG de terrain",
    "copyright": INSTANCE_NAME,
}

# Couleurs de l'admin. Jazzmin les applique sans toucher à un seul gabarit.
# `navy` est ce que la palette AdminLTE offre de plus proche du bleu de la
# marque (#4a6fae) ; `olive` tient le rôle du vert de QField en accent.
JAZZMIN_UI_TWEAKS = {
    "navbar": "navbar-dark navbar-navy",
    "brand_colour": "navbar-navy",
    "accent": "accent-olive",
    "sidebar": "sidebar-dark-navy",
    "button_classes": {"primary": "btn-primary"},
}

Le nom sort en constante parce qu’il apparaît neuf fois : deux dictionnaires, l’onglet du navigateur, le pied de l’admin. Une constante rend l’appropriation littérale — une ligne — au lieu de neuf occurrences à traquer.

Admin Django de l'instance habillé par Jazzmin : logo, nom et couleurs de la marque
L’admin après composition : JAZZMIN_SETTINGS pose le logo et le nom de l’instance, JAZZMIN_UI_TWEAKS la palette (navy/olive), sans un seul gabarit modifié dans src/. Sur l’instance : /admin/.

Deux autres détails qui ne sont pas de la coquetterie.

Le from … import * récupère les majuscules, et elles seules — c’est la convention de Django, et c’est exactement ce qu’on veut : tout ce que l’upstream ajoutera demain à settings.py arrivera ici sans qu’on y touche.

JAZZMIN_SETTINGS est ensuite réimporté nommément, puis réécrit par {**JAZZMIN_SETTINGS, …}. C’est un dictionnaire d’une trentaine de clés : le redéclarer en entier reviendrait à recopier de l’upstream, donc à recréer la dette qu’on essaie d’éviter. On le fusionne. WHITELABEL, lui, n’a pas besoin de ce traitement — il n’existe pas en amont, et whitelabel.py fusionne déjà nos clés avec ses valeurs par défaut.

Le fichier est monté en lecture seule dans le conteneur, et la variable d’environnement le désigne :

# docker-compose.override.yml
  app:
    volumes:
      - ../theme/settings_custom.py:/usr/src/app/qfieldcloud/settings_custom.py:ro
      - ../theme/static:/usr/src/app/qfieldcloud/core/staticfiles/custom:ro
# .env — la seule ligne à changer dans ce fichier
DJANGO_SETTINGS_MODULE=qfieldcloud.settings_custom

Attention : worker_wrapper partage le bloc d’environnement de app — donc la même DJANGO_SETTINGS_MODULE. Si le fichier n’est monté que sur app, le worker ne démarre plus, et l’instance a l’air parfaitement saine tant que personne ne pousse de projet.

ModuleNotFoundError: No module named 'qfieldcloud.settings_custom'

Le montage va donc sur les deux services. Le worker n’a pas besoin des images, seulement du module de réglages.

# docker-compose.override.yml, suite
  worker_wrapper:
    volumes:
      - ../theme/settings_custom.py:/usr/src/app/qfieldcloud/settings_custom.py:ro

Les images, et le geste qu’on oublie
#

Le second montage fait apparaître theme/static/ sous le nom custom/ dans le dossier statique source de l’application. C’est bien ce que les chemins du dictionnaire désignent : "custom/logo-navbar.svg" est résolu par {% static %}, relativement à la racine du statique.

Une question se pose avant de dessiner quoi que ce soit, et elle n’est pas graphique : quel logo ? Celui de QFieldCloud est le plus juste — c’est ce logiciel-là qui tourne, et le reconnaître rend service à qui arrive sur la page. Mais il appartient à OPENGIS.ch, et une instance auto-hébergée n’est pas la leur. La ligne que je me suis fixée : le logo dit quel logiciel tourne, le nom dit qui l’exploite. D’où INSTANCE_NAME, laissé sur un placeholder plutôt que sur « QFieldCloud », et trois déclinaisons tirées du fichier officiel — bleu sur la page, blanc sous la barre, marque seule dans la favicon.

Cinq fichiers — un logo pour la barre de navigation (fond coloré, donc en blanc), un logo principal pour le haut de la carte de connexion, une favicon, la feuille de style, et le fond de courbes de niveau sur lequel on reviendra :

$ ls theme/static/
contours.svg  favicon.svg  logo-main.svg  logo-navbar.svg  theme.css

Et voici le geste que l’on oublie systématiquement, parce qu’aucune autre annexe n’en a eu besoin :

$ cd src && docker compose --env-file ../.env exec app python manage.py collectstatic --noinput
5 static files copied to '/usr/src/app/staticfiles', 505 unmodified, 174 post-processed.

Le dossier source n’est pas celui que nginx sert. nginx sert staticfiles/, un volume peuplé par collectstatic — et ces « 172 post-processed » disent que QFieldCloud utilise le stockage à manifeste : chaque fichier y est copié sous un nom horodaté par son empreinte, et le gabarit va chercher ce nom dans un manifeste.

D’où une erreur qui déroute la première fois, parce qu’elle ne ressemble pas à un problème de fichier manquant :

ValueError: Missing staticfiles manifest entry for 'custom/theme.css'

Ce n’est pas un 404 sur l’image : c’est un 500 sur la page entière. Référencer un fichier statique sans avoir lancé collectstatic ne dégrade pas l’affichage, il l’interrompt. La commande fait donc partie de la recette, pas du dépannage — d’où une cible dédiée dans le Makefile :

# Makefile
static:  ; $(COMPOSE) exec app python manage.py collectstatic --noinput

Le manifeste a un second piège, et celui-là ne se manifeste qu’à la deuxième modification. Changez le contenu d’un fichier déjà collecté, relancez make static, rechargez la page : rien ne bouge.

$ make static
1 static file copied to '/usr/src/app/staticfiles', 509 unmodified, 171 post-processed.
$ curl -sk https://qfieldcloud.exemple.fr/accounts/login/ | grep theme
  <link rel="stylesheet" href="/staticfiles/custom/theme.5bdb43abdb76.css" />   # l'ancienne empreinte

Le manifeste est lu au démarrage du processus. collectstatic a bien écrit le nouveau nom haché sur le disque, mais l’application sert toujours l’inventaire qu’elle a en mémoire. Rien ne casse, rien ne prévient : la modification a simplement l’air sans effet, et on cherche l’erreur dans la CSS. Il faut redémarrer le service :

$ cd src && docker compose --env-file ../.env restart app

À l’installation, la question ne se pose pas — make up recrée les conteneurs juste avant. Elle se pose à chaque retouche ensuite, c’est-à-dire tout le temps.

Le résultat se lit directement dans le HTML servi, empreinte comprise :

$ curl -sk https://qfieldcloud.exemple.fr/accounts/login/?next=/admin/ | grep -i 'logo\|icon'
  <link rel="shortcut icon" type="image/x-icon" href="/staticfiles/custom/favicon.19f052824fbd.svg" />
      <img src="/staticfiles/custom/logo-navbar.11e909cb5139.svg" alt="Mon instance QFieldCloud" class="qfc-header-logo">
      <img src="/staticfiles/custom/logo-main.326dbf896a7e.svg" alt="Mon instance QFieldCloud" class="qfc-main-logo">

Jusqu’ici : logos, favicon, titre d’onglet, admin recoloré. Zéro fichier de l’upstream recopié.

Les couleurs des pages publiques : la seule exception
#

Reste la barre de navigation bleue de la page de connexion. Et là, le mécanisme s’arrête net.

Page de connexion QFieldCloud : logo de l'instance posé, mais barre de navigation restée bleue
La page de connexion : le logo vient de WHITELABEL, mais la barre reste au bleu Bootstrap tant qu’on n’a pas monté sa propre feuille de style. Sur l’instance : /accounts/login/.

On pourrait croire s’en tirer par les variables CSS, puisque qfieldcloud.css en déclare :

/* src/docker-app/qfieldcloud/core/staticfiles/css/qfieldcloud.css — upstream */
:root {
  --bs-primary: #0d6efd;
  
}

C’est un leurre. La feuille de l’upstream les déclare, mais Bootstrap 4 — la version embarquée — ne les consomme pas : ses .bg-primary et .btn-primary portent des couleurs en dur. Redéfinir --bs-primary ne change rien à l’écran.

Il faut donc charger une feuille à nous. Or account/base.html n’offre aucun bloc de gabarit pour cela : extrahead n’existe que dans les pages filles, une par une. Le fichier doit être remplacé, et c’est la seule dette de cette annexe. Autant la prendre en connaissance de cause, et la rendre visible.

On copie le gabarit de src/docker-app/qfieldcloud/core/templates/account/base.html vers theme/account-base.html, et on y ajoute une ligne — une seule :

$ diff -u src/docker-app/qfieldcloud/core/templates/account/base.html theme/account-base.html
@@ -10,6 +10,8 @@
   {% sri_static 'css/vendor.css' %}
   {% sri_static 'css/qfieldcloud.css' %}
   <link rel="shortcut icon" type="image/x-icon" href="{% static whitelabel.favicon %}" />
+  <!-- AJOUT LOCAL — la seule ligne qui distingue ce fichier de l'upstream. -->
+  <link rel="stylesheet" href="{% static 'custom/theme.css' %}" />

Notez le {% static %} là où l’upstream utilise {% sri_static %}. Ce dernier ajoute un attribut integrity calculé sur le contenu du fichier : très bien pour des dépendances figées, inutilement rigide pour une feuille qu’on retouche. Le {% static %} simple suffit, et l’empreinte dans le nom de fichier assure déjà l’invalidation du cache.

La feuille arrive après celle de l’upstream, donc elle gagne à spécificité égale :

/* theme/static/theme.css */
:root {
  /* Bleu de la marque QFieldCloud, relevé sur le logo officiel. */
  --qfc-blue: #4a6fae;
  --qfc-blue-dark: #3a5a92;
  --qfc-blue-deep: #21375c;
  /* Vert de QField, réservé aux accents : jamais deux verts côte à côte. */
  --qfc-green: #80cc28;
  --qfc-muted: #5d6b7d;
}

.navbar.bg-primary {
  background: linear-gradient(100deg, var(--qfc-blue-deep), var(--qfc-blue)) !important;
  border-bottom: 3px solid var(--qfc-green);
}

.btn-primary {
  background-color: var(--qfc-blue);
  border-color: var(--qfc-blue);
}
.btn-primary:hover,
.btn-primary:focus,
.btn-primary:not(:disabled):not(.disabled):active {
  background-color: var(--qfc-blue-dark);
  border-color: var(--qfc-blue-dark);
}

/* `.btn-default` n'existe pas en Bootstrap 4 : sans cela, « Mot de passe
 * oublié ? » sort en bouton nu, sans couleur ni contour. */
.btn-default {
  color: var(--qfc-muted);
  background-color: transparent;
  border-color: transparent;
}

Le !important sur la barre n’est pas de la paresse : bg-primary de Bootstrap en porte un, il faut le rencontrer à armes égales. Le reste s’en passe.

Deux remarques sur ce que la feuille corrige au passage, parce qu’elles ne se voient qu’une fois le thème posé. .qfc-logo-wrapper réserve 15 rem de haut pour le logo — dimension calculée pour un logo carré, qui laisse un trou au-dessus du formulaire dès qu’on met un logo en bandeau. Et .btn-default, qu’emploie le gabarit de connexion pour « Mot de passe oublié ? », n’existe pas en Bootstrap 4 : c’est un reste de la version 3. Le bouton sort donc sans couleur ni contour, et le corriger n’est pas de la décoration, c’est rendre un lien visible.

Rendre la dette visible
#

Un fichier recopié d’un projet qui bouge est une dette silencieuse : elle ne coûte rien jusqu’au jour où l’upstream modifie ce gabarit, et où l’on sert une version d’il y a six mois sans le savoir. On garde donc la copie d’origine à côté de la nôtre, et on compare à chaque montée de version :

# Makefile
theme-diff: ; diff -u theme/account-base.html.upstream src/docker-app/qfieldcloud/core/templates/account/base.html
$ make theme-diff && echo "gabarit inchangé"
gabarit inchangé

Sortie vide : le gabarit n’a pas bougé, notre copie reste valable. Sortie non vide : il faut reporter la ligne du thème dans le nouveau gabarit, puis rafraîchir le fichier .upstream. Deux minutes, une fois par montée de version — à condition que la commande existe et qu’elle soit dans la liste des choses à faire. C’est précisément le rôle du Makefile : transformer une vigilance en cible.

Le fond : des courbes qu’on calcule
#

Le site de QFieldCloud pose derrière son pied de page un motif de courbes de niveau — l’objet cartographique par excellence, et le clin d’œil qui convient à un outil de terrain. Le reprendre pose une question qu’on ne se pose pas assez : télécharger l’image, ou la calculer ?

Télécharger, c’est hériter d’un fichier dont on ne sait rien : sa licence, sa résolution, sa couleur, sa taille. Le calculer coûte deux cents lignes de Python sans dépendance, et rend tout cela réglable — la densité des isolignes, le relief, la couleur du trait. C’est le choix qu’a fait theme/contours.py, un script de conception — le SVG qu’il produit est versionné, rien ne l’appelle au déploiement.

Motif de courbes de niveau généré, en fond de la page de connexion
Le fond produit par theme/contours.py : un terrain inventé, ses isolignes suivies par marching squares, un SVG versionné et jamais téléchargé. Visible derrière le pied de page sur /accounts/login/.

Le principe est celui de la carte topographique, à l’envers. Une carte relève un terrain puis en trace les isolignes ; ici on invente le terrain — quelques dômes gaussiens posés sur une ondulation basse fréquence — et on suit ses lignes de niveau par marching squares : pour chaque cellule de la grille, on regarde quels coins sont au-dessus du niveau, et on en déduit par où la courbe traverse.

Trois pièges, et je les ai tous rencontrés — le premier à l’écran, les deux autres en cherchant pourquoi.

Les cellules-selle. Quand deux coins opposés en diagonale sont au-dessus du niveau et les deux autres en dessous, la cellule est traversée par deux morceaux de courbe — et rien, dans les quatre coins, ne dit lesquels se rejoignent. Les relier dans l’ordre du parcours, c’est tirer à pile ou face : une fois sur deux, la courbe saute d’une branche à l’autre. La règle est d’interroger le centre de la cellule, en moyennant les quatre coins.

Le lissage suivi de la décimation. J’arrondissais les angles par l’algorithme de Chaikin, puis je ne gardais qu’un point sur trois pour alléger le fichier. Couper dans une courbe déjà arrondie recrée exactement les angles qu’on venait d’enlever. L’ordre n’est pas commutatif.

L’arrondi à l’entier. Le viewBox fait 1200 unités de large et la CSS l’étire en cover : à l’écran, tout est agrandi de moitié. Un demi-pixel de gigue à l’écriture devient un ressaut visible à l’affichage.

La conclusion des trois : une polyligne, même dense, est lisse par densité de points — donc jamais assez lisse pour un fond qu’on agrandit. La sortie est passée en courbes de Bézier, lisses par construction. La chaîne complète tient en une ligne : marching squares, lissage, rééchantillonnage à pas constant — ce qu’exige la spline de Catmull-Rom, sinon elle dépasse dans les virages —, puis conversion en Bézier cubique. Cinquante et un chemins, 46 Kio, soit moins que la polyligne qu’ils remplacent.

$ python3 theme/contours.py
51 courbes écrites dans static/contours.svg (46 Kio)

Reste à le poser. Et là, un détail de CSS fait toute la différence, parce que le motif doit apparaître à deux endroits qui n’ont pas la même couleur de fond : bleu pâle sur la page claire, blanc sous la barre de navigation bleue. Deux fichiers ? Non — un masque :

/* theme/static/theme.css */
@supports (mask-size: cover) or (-webkit-mask-size: cover) {
  body::before {
    content: '';
    position: fixed;
    inset: 0;
    background-color: var(--qfc-blue);   /* la teinte vient d'ici */
    opacity: 0.16;
    -webkit-mask: url('contours.svg') center / cover no-repeat;
    mask: url('contours.svg') center / cover no-repeat;
  }
}

En image de fond, un SVG impose ses couleurs. En masque, il n’apporte que sa forme : le trait devient une découpe, et la couleur vient du background-color de l’élément. Un seul fichier, autant de teintes qu’on veut — et le @supports évite l’aplat de couleur là où le masque n’existerait pas.

Info : le stockage à manifeste réécrit aussi les url() à l’intérieur des feuilles CSS. Le url('contours.svg') écrit à la main devient url("contours.16887dc9afd2.svg") dans le fichier servi, sans rien demander. C’est la même mécanique que pour les images du gabarit, et elle vaut pour les masques comme pour les fonds.

La recette complète
#

# depuis la racine du dépôt de déploiement, pas depuis src/
$ mkdir -p theme/static
# écrire theme/settings_custom.py, dont INSTANCE_NAME
# déposer theme/static/{logo-navbar.svg,logo-main.svg,favicon.svg,theme.css}
$ python3 theme/contours.py     # écrit theme/static/contours.svg
$ cp src/docker-app/qfieldcloud/core/templates/account/base.html theme/account-base.html.upstream
$ cp theme/account-base.html.upstream theme/account-base.html
# ajouter la ligne <link> dans theme/account-base.html
# ajouter les montages dans docker-compose.override.yml (app ET worker_wrapper)
# ajouter les cibles static et theme-diff dans le Makefile
$ sed -i 's/^DJANGO_SETTINGS_MODULE=.*/DJANGO_SETTINGS_MODULE=qfieldcloud.settings_custom/' .env
$ make config && make up && make static
$ git -C src status --short   # doit être vide

Le dernier contrôle est le seul qui compte vraiment.

Revenir au thème de l’upstream
#

Le squelette de déploiement qui accompagne cette série est livré thème actif : qui le reprend démarre habillé, et c’est ce chemin-ci qu’il empruntera s’il préfère l’apparence d’origine de QFieldCloud. Autant le décrire avec le même soin que l’aller.

J’aurais aimé écrire qu’il tient en une ligne. Je l’ai cru en écrivant cette annexe, je l’ai essayé sur l’instance, et le thème est resté à l’écran. Voici pourquoi, parce que la raison dit quelque chose du montage lui-même.

Le thème a deux leviers, pas un. Le module de réglages porte les logos, les titres et les couleurs de l’admin : celui-là, .env le commande. Mais le gabarit recopié charge sa feuille de style tout seul, avec une balise <link> en dur — il ne demande rien à Django, et se moque de la valeur de DJANGO_SETTINGS_MODULE. Tant qu’il est monté, theme.css est servi.

C’est le prix du seul fichier qu’on a accepté de recopier. Il s’est payé au retour arrière, pas à l’aller.

Dans l’ordre, donc :

# 1. Les réglages : reprendre ceux de l'upstream.
$ sed -i 's/^DJANGO_SETTINGS_MODULE=.*/DJANGO_SETTINGS_MODULE=qfieldcloud.settings/' .env

# 2. Les montages : commenter le bloc `volumes` du thème sous `app` ET celui
#    sous `worker_wrapper`, dans docker-compose.override.yml.
$ $EDITOR docker-compose.override.yml

# 3. Recréer les conteneurs, puis recollecter.
$ make up && make static

La troisième étape est celle qu’on saute, et c’est elle qui purge. collectstatic réécrit le manifeste de zéro : custom/ en disparaît. Sans elle, les conteneurs sont propres mais le manifeste garde encore l’inventaire du thème.

Vérifier plutôt que croire — on interroge l’instance, pas les fichiers du dépôt :

$ cd src && docker compose --env-file ../.env exec app sh -c '
    echo $DJANGO_SETTINGS_MODULE
    grep -c custom/theme.css qfieldcloud/core/templates/account/base.html
    ls qfieldcloud/core/staticfiles/custom 2>&1'
qfieldcloud.settings
0
ls: cannot access 'qfieldcloud/core/staticfiles/custom': No such file or directory

Les réglages sont ceux de l’upstream, le gabarit servi ne porte plus la ligne <link>, le dossier d’images a disparu du conteneur. Trois leviers, trois preuves.

Rien n’est perdu : theme/ reste sur le disque, et le chemin se refait en sens inverse. Une seule précaution au retour — décommenter les montages de app et de worker_wrapper ensemble. Sous settings_custom, un worker privé du fichier de réglages ne démarre pas, et l’erreur ne se voit qu’au premier paquet QField.

Un dernier mot, sur ce qui est versionné et ce qui ne l’est pas. Les montages et le modèle de configuration sont dans le dépôt ; votre .env, lui, n’y est pas. Une instance revenue à l’upstream le reste donc tant que son .env le dit, et aucun git pull ne le lui rappellera. C’est le fichier qui décide, pas le dépôt — ce qui est la règle depuis l’épisode 3, mais qui ne se sent nulle part aussi bien qu’ici.

Ce qu’il faut retenir
#

  • DJANGO_SETTINGS_MODULE est le point d’extension de tous les réglages Django, pas seulement de l’apparence. Un module qui importe l’upstream et n’écrase que le nécessaire remplace n’importe quelle édition de settings.py.
  • La racine redirige vers l’admin : « habiller l’accueil » veut dire habiller deux systèmes, WHITELABEL et Jazzmin, ou changer de peau à la connexion.
  • Fusionnez JAZZMIN_SETTINGS, ne le redéclarez pas. Le redéclarer, c’est recopier trente clés de l’upstream dans son propre fichier.
  • collectstatic fait partie de la recette. Sans lui, une référence statique ne donne pas une image manquante mais une erreur 500 sur toute la page.
  • Le montage va sur app ET worker_wrapper. L’oubli ne se voit qu’au premier paquet QField.
  • Le manifeste est lu au démarrage. collectstatic seul ne suffit pas quand c’est le contenu d’un fichier qui change : le processus continue de servir l’ancienne empreinte, sans rien signaler.
  • Un seul fichier de l’upstream est recopié, et une cible make theme-diff se charge de le rappeler à chaque montée de version.
  • Un SVG posé en masque n’apporte que sa forme. La couleur vient de l’élément, donc un seul fichier sert le motif en bleu sur la page claire et en blanc sous la barre de navigation.
  • Un fond qu’on agrandit doit être lisse par construction, pas par densité de points. D’où des courbes de Bézier là où une polyligne, même dense, montre ses facettes.
  • Le retour arrière ne tient pas en une ligne. Le gabarit charge sa feuille de style sans passer par Django : .env seul laisse la palette en place. Un montage n’est vraiment sain que le jour où on l’a défait pour de vrai.
Documentation self-hosted de QFieldCloud

Image de couverture : Scott Blake sur Unsplash.

QFieldCloud auto-hébergé - Cet article fait partie d'une série.
Partie 11: Cet article

Articles connexes