Aller au contenu principal

Architecture

Architecture générale

L'application est principalement composée :

  • d'un backend utilisé comme API REST.
  • d'un frontend sous forme de single page application consommant l'API du backend.

Le frontend est toujours servi depuis le backend, comme ressource complètement statique en production, et comme redirection vers le serveur du frontend en développement. C'est pourquoi on accède toujours à l'application à partir du serveur du backend, y compris en développement.

Un des avantages est qu'il est possible de générer des données côté serveur par une vue Django, et de l'ajouter en tant que code HTML ou JavaScript directement dans le gabarit de la page d'index qui est utilisé par React. Ceci permet dans une certaine mesure de mettre en place du rendu côté serveur.

remarque

La manière de gérer le frontend à travers la backend a été inspirée d'un article de Tristan Wagner, avec l'utilisation de django-webpack-loader et webpack-bundle-tracker.

Cette configuration permet le hot reload du frontend en développement, même s'il est servi à travers une vue du backend, et même en utilisant Docker.

Il y a actuellement 2 exceptions où le backend sert des pages HTML au lieu d'être utilisé comme une API web par un autre programme :

  • la browsable API fournie par Django REST Framework sur le chemin /api/ lorsqu'il est visité par un navigateur, pour pouvoir exécuter des appels d'API au travers d'une page web sans utiliser le frontend.
  • l'interface d'administration fournie par Django sur le chemin /admin/ (et sur un chemin secret configuré dans .envs/.production/.django pour l'environnement de production) pour pouvoir facilement manipuler les objets de la base de donnéesans avoir d'interface frontend personnalisée pour eux.

Backend

Le backendest écrit en Python avec le framework Django. Le code initial a été crée avec cookiecutter-django. La documentation de cookiecutter-django peut grandement aider à comprendre la manière dont est structuré le code du backend.

De plus, il est structuré de manière à ce que toutes les fonctionnalités (applications django) de base puissent être personnalisées pour répondre aux besoins de votre projet.

Il utilise une base de données PostgreSQL dans tous les environnements, et un message broker RabbitMQ à travers Celery en production.

Internationalisation

L'internationalisation est faite à l'aide du mécanisme principal d'internationalisation de Django :

from django.utils.translation import gettext_lazy as _

_("Contenu à traduire")

Une fois que du contenu à traduire est crée, vous pouvez regénérer les fichiers de traduction pour obtenir les chaînes à traduire, ajouter les traductions, et créer une version compilée de la traduction.

Les chaînes traduites sont dans backend/locale/[LANG]/django.po.

python manage.py makemessages -l fr # créer les traductions pour la langue française
# changer le contenu des traductions dans /backend/locale/fr/django.po
python manage.py compilemessages # compiler les fichiers de traduction

Frontend

Le frontend est écrit en TypeScript avec le framework React. Le code initial a été généré avec create-react-app.

Style

Les fichiers source de style sont dans frontend/src/sass.

Le style est crée avec le préprocesseur SASS. Les fichiers sont organisés avec l'architecture 7-1, et le code en lui-même est organisé avec la méthodologie BEM

Ceci implique que les blocks de style sont indépendants des composants React. Ils peuvent être réutilisés ou être plus gros ou plus petits qu'un composant spécifique.

Internationalisation

L'internationalisation est faite à l'aide de LinguiJS. Maincipalement avec l'utilisation des macros Trans and t.

import { t, Trans } from "@lingui/macro";

function MyComponent(): JSX.Element {
return (
<>
<Trans>Contenu à traduire</Trans>
<AnotherComponent customProp={t`Contenu à traduire`} />
</>
);
}

Une fois que du contenu à traduire est crée, vous pouvez regénérer les fichiers de traduction, ajouter les traductions correctes, et créer une version compilée de ces traductions.

Les chaînes de traduction sont dans frontend/src/locales/[LANG]/messages.po.

yarn lang:extract # créer des traductions pour les langues française et anglaise
# changer le contenu des traductions dans /frontend/src/locales/fr/messages.po
yarn lang:compile # compiler les fichiers de traduction

Communication frontend - backend

Appels REST

Étant donné que le backend expose une API REST, le principal canal de communication est l'envoi d'appels REST à partir du frontend. C'est fait paincipalement en utilisant axios.

Stale While Revalidate

Pour toutes les ressources qui sont régulièrement mises à jour à partir du frontend pour rester à jour parce que le backend en est la source de vérité, nous utilisons SWR pour gérer les mises à jour lorsque c'est nécessaire. C'est utilisé comme hook dans le composant qui a directement besoin de cette ressource.

Génération côté serveur

React est injecté dans un gabarit de Django qui est situé dans frontend/public/index.html. Nous sommes ainsi capables de créer une variable JavaScript appelée window.SERVER_DATA dans ce gabarit, et de mettre dedans tout ce qui peut être utile depuis la vue Django qui gère ce gabarit (située dans backend/connect_access/views.py).

Ces données sont disponibles côté React immédiatement après que le navigateur ait commencé à exécuter le code JavaScript, sans avoir besoin de faire d'appels REST via une API.

Personnaliser Connect Access

Backend

Fork d'une application de base

Création d'un module python avec le même label

Vous devez créer un module Python avec le même label d'application que l'application Connect Access que vous souhaitez étendre.

Par exemple, pour créer une version locale de connect_access.apps.mediations, procédez comme suit :

mkdir votreprojet/mediations
mkdir votreprojet/mediations/__init__.py
Import et/ou modification d'un modèle de base

Si l'application Connect Access originale possède un fichier models.py, vous devrez créer un fichier models.py dans votre application locale. Il doit importer tous les modèles de l'application Connect Access qui sont remplacés, vous pouvez aussi y modifier le modèle :

# votreprojet/mediations/models.py

from django.db import models

from connect_access.apps.mediations.abstract_models import AbstractMediationRequest

# your custom models go here
class MediationRequest(AbstractMediationRequest):
new_field = models.CharField()

from connect_access.apps.mediations.models import * #noqa

Veillez à importer les autres modèles d'origine à la fin de votre fichier.

remarque

L'utilisation de from ... import * est étrange, n'est-ce pas ? Oui, mais cela doit être fait au bas du module en raison de la manière dont Django enregistre les modèles. Si deux modèles portant le même nom sont déclarés dans une application, Django n'utilisera que le premier. Cela signifie que si vous souhaitez personnaliser les modèles de Connect Access, vous devez déclarer vos modèles personnalisés avant d'importer les modèles de Connect Access pour cette application.

Import et/ou modification à l'API d'un modèle de base

Si l'application Connect Access originale possède des fichiers api.py et serializers.py, vous devrez recréer ses fichiers dans votre application locale.

  • api.pydoit importer tous les ViewSet de l'application de base ;
# votreprojet/mediations/api.py

from connect_access.apps.mediations.api import (
MediationRequestViewSet as BaseMediationRequestViewSet,
)

from .serializers import MediationRequestSerializer

class MediationRequestViewSet(BaseMediationRequestViewSet):
pass
  • serializers.py doit importer tous les Serializers de l'application de base.
# votreprojet/mediations/serializers.py

from connect_access.apps.mediations.serializers import (
MediationRequestSerializer as BaseMeditionRequestSerializer,
)

class MediationRequestSerializer(BaseMeditionRequestSerializer):
pass
Mise à jours du schéma

La dernière chose que vous devez faire maintenant est de faire en sorte que Django mette à jour le schéma de la base de données et crée une nouvelle colonne dans la table des mediation requests. Nous recommandons d'utiliser des migrations pour cela, il vous suffit donc de créer une nouvelle migration de schéma.

Il est possible de créer simplement une nouvelle migration de catalogue (en utilisant ./manage.py makemigrations mediations) mais ce n'est pas recommandé car toutes les dépendances entre les migrations devront être appliquées manuellement (en ajoutant un attribut dependencies à la classe de migration).

La méthode recommandée pour gérer les migrations est de copier le répertoire des migrations depuis connect_access/apps/mediations dans votre nouvelle application mediations. Vous pouvez ensuite créer une nouvelle migration (supplémentaire) en utilisant la commande de gestion makemigrations :

python manage.py makemigrations mediations

Pour appliquer la migration que vous venez de créer, il vous suffit d'exécuter python manage.py migrate mediations et la nouvelle colonne est ajoutée à la table des mediation requests dans la base de données.

Ajout du modèle à l'interface d'administration Django

Lorsque vous remplacez une des applications de Connect Access par une application locale, l'intégration de l'administration de Django est perdue. Si vous souhaitez l'utiliser, vous devez créer un fichier admin.py et importer le admin.py de l'application principale (qui exécutera le code d'enregistrement) :

# votreprojet/mediations/admin.py
from connect_access.apps.mediations.admin import (
MediationRequestAdmin as BaseMediationRequestAdmin
)

from votreprojet.mediations.models import MediationRequest

class MediationRequestAdmin(BaseMediationRequestAdmin):
pass

MediationRequestAdmin.unregister(MediationRequest)
MediationRequestAdmin.register(MediationRequest)
Définir la configuration de l'application

Pour que Django charge cette nouvelle application il faut définir sa configuration en définissant une sous-classe de AppConfig avec l'attribut default = True.

# votreprojet/mediations/apps.py
from connect_access.apps.mediations.apps import MediationsConfig as BaseMediationsConfig

class MediationsConfig(BaseMediationsConfig):
name = "votreprojet.mediations"
default = True
Remplacer l'application de Connect Access par la vôtre dans INSTALLED_APPS

Vous devez indiquer à Django que vous avez remplacé l'une des applications principales de Connect Access. Vous pouvez le faire en remplaçant son entrée dans le paramètre INSTALLED_APPS par celle de votre propre application.

INSTALLED_APPS = [
# toutes vos applications non Connect Access
...
# applications Connect Access
...
# 'connect_access.apps.mediations', # remplaced by
'votreprojet.mediations',
...
]

Stratégie de test

Tests unitaires

La majeure partie du code du backend et du frontend a été écrit avec le Développement Piloté par les Tests (TDD), en utilisant les étapes d'itération les plus petites possibles entre le test et l'implémentation, pour piloter l'implémentation par le test. Cependant ces tests sont plus proches de ce qu'on pourrait appeler des tests d'intégration dans la mesure où ils sont effectués à travers la base de données dans le cas du backend, et à travers l'interface utilisateur (même si une version plus légère est utilisée avec jsdom) pour le frontend.

Ainsi, l'exécution de ces tests unitaires est plus lente que s'ils testaient du Python ou du TypeScript pur, surtout pour les tests du frontend qui mettent plus de 30 secondes à s'exécuter sur un ordinateur moderne. Vous êtes par conséquent encouragés à tester unitairement le code complexe en dehors de l'utilisation de la base de données pour le backend, et en dehors de React pour le frontend.

Concernant les tests du frontend, tous les appels d'API sont simulés avec des mocks.

Tests de bout en bout

Pour les tests de bout en bout tout est démarré comme en production, et les tests sont exécutés avec un vrai navigateur, en mode visible ou headless.

Ces tests sont beaucoup moins nombreux parce qu'ils deviennent facilement sujets aux faux positifs, et parce qu'un seul test peut prendre plusieurs secondes à s'exécuter et que la parallélisation est plus difficile dans le mode de bout en bout.

Plutôt que de tester tous les chemins de succès et d'erreur possibles comme avec les tests unitaires, nous testons seulement le chemin utilisateur principal pour chaque fonctionnalité. Ceci assure notamment que la communication entre le frontend et le backend se fait correctement.