mirror of
https://github.com/ae-utbm/sith.git
synced 2025-07-10 11:59:23 +00:00
Rewrite documentation with MkDocs
This commit is contained in:
59
docs/howto/direnv.md
Normal file
59
docs/howto/direnv.md
Normal file
@ -0,0 +1,59 @@
|
||||
Pour éviter d'avoir à sourcer l'environnement
|
||||
à chaque fois qu'on rentre dans le projet,
|
||||
il est possible d'utiliser l'utilitaire [direnv](https://direnv.net/).
|
||||
|
||||
Comme pour beaucoup de choses, il faut commencer par l'installer :
|
||||
|
||||
=== "Linux"
|
||||
|
||||
=== "Debian/Ubuntu"
|
||||
|
||||
```bash
|
||||
sudo apt install direnv
|
||||
```
|
||||
|
||||
=== "Arch Linux"
|
||||
|
||||
```bash
|
||||
sudo pacman -S direnv
|
||||
```
|
||||
|
||||
=== "macOS"
|
||||
|
||||
```bash
|
||||
brew install direnv
|
||||
```
|
||||
|
||||
Puis on configure :
|
||||
|
||||
=== "bash"
|
||||
|
||||
```bash
|
||||
echo 'eval "$(direnv hook bash)"' >> ~/.bashrc
|
||||
exit # On redémarre le terminal
|
||||
```
|
||||
|
||||
=== "zsh"
|
||||
|
||||
```zsh
|
||||
echo 'eval "$(direnv hook zsh)"' >> ~/.zshrc
|
||||
exit # On redémarre le terminal
|
||||
```
|
||||
|
||||
=== "nu"
|
||||
|
||||
Désolé, par `direnv hook` pour `nu`
|
||||
|
||||
Une fois le terminal redémarré, dans le répertoire du projet :
|
||||
```bash
|
||||
direnv allow .
|
||||
```
|
||||
|
||||
Une fois que cette configuration a été appliquée,
|
||||
aller dans le dossier du site applique automatiquement
|
||||
l'environnement virtuel.
|
||||
Ça peut faire gagner pas mal de temps.
|
||||
|
||||
Direnv est un utilitaire très puissant
|
||||
et qui peut s'avérer pratique dans bien des situations,
|
||||
n'hésitez pas à aller vous renseigner plus en détail sur celui-ci.
|
354
docs/howto/migrations.md
Normal file
354
docs/howto/migrations.md
Normal file
@ -0,0 +1,354 @@
|
||||
## Qu'est-ce qu'une migration ?
|
||||
|
||||
Une migration est un fichier Python qui contient
|
||||
des instructions pour modifier la base de données.
|
||||
Une base de données évolue au cours du temps,
|
||||
et les migrations permettent de garder une trace
|
||||
de ces modifications.
|
||||
|
||||
Grâce à elles, on peut également apporter des modifications
|
||||
à la base de données sans être obligées de la recréer.
|
||||
On applique seulement les modifications nécessaires.
|
||||
|
||||
## Appliquer les migrations
|
||||
|
||||
Pour appliquer les migrations, exécutez la commande suivante :
|
||||
|
||||
```bash
|
||||
python ./manage.py migrate
|
||||
```
|
||||
|
||||
Vous remarquerez peut-être que cette commande
|
||||
a été utilisée dans la section
|
||||
[Installation](../tutorial/install.md).
|
||||
En effet, en partant d'une base de données vierge
|
||||
et en appliquant toutes les migrations, on arrive
|
||||
à l'état actuel de la base de données.
|
||||
Logique.
|
||||
|
||||
Si vous utilisez cette commande sur une base de données
|
||||
sur laquelle toutes les migrations ont été appliquées,
|
||||
elle ne fera rien.
|
||||
|
||||
Si vous utilisez cette commande sur une base de données
|
||||
sur laquelle seule une partie des migrations ont été appliquées,
|
||||
seules les migrations manquantes seront appliquées.
|
||||
|
||||
## Créer une migration
|
||||
|
||||
Pour créer une migration, exécutez la commande suivante :
|
||||
|
||||
```bash
|
||||
python ./manage.py makemigrations
|
||||
```
|
||||
|
||||
Cette commande comparera automatiquement le contenu
|
||||
des classes de modèles et le comparera avec les
|
||||
migrations déjà appliquées.
|
||||
A partir de cette comparaison, elle générera
|
||||
automatiquement une nouvelle migration.
|
||||
|
||||
!!! note
|
||||
|
||||
La commande `makemigrations` ne fait que
|
||||
générer les fichiers de migration.
|
||||
Elle ne modifie pas la base de données.
|
||||
Pour appliquer la migration, n'oubliez pas la
|
||||
commande `migrate`.
|
||||
|
||||
Un fichier de migration ressemble à ça :
|
||||
|
||||
```python
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
# liste des autres migrations à appliquer avant celle-ci
|
||||
]
|
||||
|
||||
operations = [
|
||||
# liste des opérations à appliquer sur la db
|
||||
]
|
||||
```
|
||||
|
||||
Grâce à la liste des dépendances, Django sait dans
|
||||
quel ordre les migrations doivent être appliquées.
|
||||
Grâce à la liste des opérations, Django sait quelles
|
||||
sont les opérations à appliquer durant cette migration.
|
||||
|
||||
## Revenir à une migration antérieure
|
||||
|
||||
Lorsque vous développez, il peut arriver que vous vouliez
|
||||
revenir à une migration antérieure.
|
||||
Pour cela, il suffit d'appliquer la commande `migrate`
|
||||
en spécifiant le nom de la migration à laquelle vous
|
||||
voulez revenir :
|
||||
|
||||
```bash
|
||||
python ./manage.py migrate <application> <numéro de la migration>
|
||||
```
|
||||
|
||||
Par exemple, si vous voulez revenir à la migration `0001_initial`
|
||||
de l'application `customer`, vous pouvez exécuter la commande suivante :
|
||||
|
||||
```bash
|
||||
python ./manage.py migrate customer 0001
|
||||
```
|
||||
|
||||
## Customiser une migration
|
||||
|
||||
Il peut arriver que vous ayez besoin de modifier
|
||||
le fichier de migration généré par Django.
|
||||
Par exemple, si vous voulez exécuter un script Python
|
||||
lors de l'application de la migration.
|
||||
|
||||
Dans ce cas, vous pouvez trouver les fichiers de migration
|
||||
dans le dossier `migrations` de chaque application.
|
||||
Vous pouvez modifier le fichier Python correspondant
|
||||
à la migration que vous voulez modifier.
|
||||
|
||||
Ajoutez l'opération que vous voulez effectuer
|
||||
dans l'attribut `operations` de la classe `Migration`.
|
||||
|
||||
Par exemple :
|
||||
|
||||
```python
|
||||
from django.db import migrations
|
||||
|
||||
def forwards_func(apps, schema_editor):
|
||||
print("Appplication de la migration")
|
||||
|
||||
def reverse_func(apps, schema_editor):
|
||||
print("Annulation de la migration")
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(forwards_func, reverse_func),
|
||||
]
|
||||
```
|
||||
|
||||
!!! warning "Script d'annulation de la migration"
|
||||
|
||||
Lorsque vous incluez un script Python dans une migration,
|
||||
incluez toujours aussi un script d'annulation,
|
||||
sinon Django ne pourra pas annuler la migration
|
||||
après son application.
|
||||
|
||||
Vous ne pourrez donc pas revenir à un état antérieur
|
||||
de la db, à moins de la recréer de zéro.
|
||||
|
||||
## Fusionner des migrations
|
||||
|
||||
Quand on travaille sur une fonctionnalité
|
||||
qui nécessite une modification de la base de données,
|
||||
les fichiers de migration sont comme toute chose :
|
||||
on peut se rendre compte que les changements
|
||||
apportés pourraient être meilleurs.
|
||||
|
||||
Par exemple, supposons que nous voulons créer un modèle
|
||||
représentant une UE suivie par un étudiant
|
||||
(ne demandez pas pourquoi on voudrait faire ça,
|
||||
c'est juste pour l'exemple).
|
||||
Un tel modèle aurait besoin des informations suivantes :
|
||||
|
||||
- l'utilisateur
|
||||
- le code de l'UE
|
||||
|
||||
On écrirait donc, dans l'application `pedagogy` :
|
||||
```python
|
||||
from django.db import models
|
||||
from core.models import User
|
||||
|
||||
class UserUe(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
ue = models.CharField(max_length=10)
|
||||
```
|
||||
|
||||
Et nous aurions le fichier de migration suivant :
|
||||
```python
|
||||
from django.db import migrations, models
|
||||
from django.conf import settings
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
("pedagogy", "0003_alter_uv_language"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="UserUe",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("ue", models.CharField(max_length=10)),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
on_delete=models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
```
|
||||
|
||||
|
||||
On finit son travail, on soumet la PR.
|
||||
Mais là, quelqu'un fait remarquer qu'il existe déjà
|
||||
un modèle pour représenter une UE.
|
||||
On modifie donc le modèle :
|
||||
|
||||
```python
|
||||
from django.db import models
|
||||
from core.models import User
|
||||
from pedagogy.models import UV
|
||||
|
||||
class UserUe(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
ue = models.ForeignKey(UV, on_delete=models.CASCADE)
|
||||
```
|
||||
|
||||
On refait la commande `makemigrations` et on obtient :
|
||||
```python
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("pedagogy", "0004_userue"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="userue",
|
||||
name="ue",
|
||||
field=models.ForeignKey(
|
||||
on_delete=models.deletion.CASCADE, to="pedagogy.uv"
|
||||
),
|
||||
),
|
||||
]
|
||||
```
|
||||
|
||||
Sauf que maintenant, nous avons deux fichiers de migration,
|
||||
alors qu'en réalité, on ne souhaite faire qu'une seule migration,
|
||||
une fois qu'on aura expédié le code en prod.
|
||||
Certes, ça fonctionnerait d'appliquer les deux, mais ça pose
|
||||
un problème d'encombrement.
|
||||
|
||||
Plus il y a de fichiers de migrations, plus il y a de migrations
|
||||
à résoudre au moment de l'installation du projet chez quelqu'un,
|
||||
plus c'est embêtant à gérer et plus Django prendra du temps
|
||||
à résoudre les migrations.
|
||||
|
||||
C'est pourquoi il est bon de respecter le principe :
|
||||
une PR = un fichier de migration maximum par application.
|
||||
|
||||
Nous voulons donc fusionner les deux, pour n'en garder qu'une.
|
||||
Pour ça, deux manières de procéder :
|
||||
|
||||
- le faire à la main
|
||||
- utiliser la commande squashmigrations
|
||||
|
||||
Pour la méthode manuelle, on ne pourrait pas vous dire exhaustivement
|
||||
comment faire.
|
||||
Mais ne vous inquiétez pas, ce n'est pas très dur.
|
||||
Regardez bien quelles sont les instructions utilisées par django
|
||||
pour les opérations de migrations,
|
||||
et avec un peu d'astuce et quelques copier-coller,
|
||||
vous vous en sortirez comme des chefs.
|
||||
|
||||
Pour la méthode `squashmigrations`, exécutez la commande
|
||||
|
||||
```bash
|
||||
python ./manage.py squasmigrations <app> <migration de début (incluse)> <migration de fin (incluse)>
|
||||
```
|
||||
|
||||
Par exemple, dans notre cas, ça donnera :
|
||||
|
||||
```bash
|
||||
python ./manage.py squasmigrations pedagogy 0004 0005
|
||||
```
|
||||
|
||||
La commande vous donnera ceci :
|
||||
```python
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
replaces = [("pedagogy", "0004_userue"), ("pedagogy", "0005_alter_userue_ue")]
|
||||
|
||||
dependencies = [
|
||||
("pedagogy", "0003_alter_uv_language"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="UserUe",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"ue",
|
||||
models.ForeignKey(
|
||||
on_delete=models.deletion.CASCADE, to="pedagogy.uv"
|
||||
),
|
||||
),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
on_delete=models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
```
|
||||
|
||||
Vous pouvez alors supprimer les deux autres fichiers.
|
||||
|
||||
Vous remarquerez peut-être la présence de la ligne suivante :
|
||||
```python
|
||||
replaces = [("pedagogy", "0004_userue"), ("pedagogy", "0005_alter_userue_ue")]
|
||||
```
|
||||
|
||||
Cela sert à dire que cette migration doit être appliquée
|
||||
à la place des deux autres.
|
||||
Une fois que vous aurez supprimé les deux fichiers,
|
||||
supprimez également cette ligne.
|
||||
|
||||
!!!warning
|
||||
|
||||
Django sait quelles migrations ont été appliquées,
|
||||
en les stockant dans une table de la db.
|
||||
Si une migration est enregistrée en db, sans que le fichier
|
||||
de migration correspondant existe,
|
||||
la commande `migrate` échoue.
|
||||
|
||||
Quand vous faites un `squashmigrations`,
|
||||
pensez donc à appliquer la commande `migrate`
|
||||
juste après (mais avant la suppression des anciens fichiers),
|
||||
pour que Django supprime de la base de données
|
||||
les migrations devenues inutiles.
|
27
docs/howto/prod.md
Normal file
27
docs/howto/prod.md
Normal file
@ -0,0 +1,27 @@
|
||||
## Configurer Sentry
|
||||
|
||||
Pour connecter l'application à une instance de sentry (ex: https://sentry.io),
|
||||
il est nécessaire de configurer la variable `SENTRY_DSN`
|
||||
dans le fichier `settings_custom.py`.
|
||||
Cette variable est composée d'un lien complet vers votre projet sentry.
|
||||
|
||||
## Récupérer les statiques
|
||||
|
||||
Nous utilisons du SCSS dans le projet.
|
||||
En environnement de développement (`DEBUG=True`),
|
||||
le SCSS est compilé à chaque fois que le fichier est demandé.
|
||||
Pour la production, le projet considère
|
||||
que chacun des fichiers est déjà compilé et,
|
||||
pour ce faire, il est nécessaire
|
||||
d'utiliser les commandes suivantes dans l'ordre :
|
||||
|
||||
```bash
|
||||
python ./manage.py collectstatic # Pour récupérer tous les fichiers statiques
|
||||
python ./manage.py compilestatic # Pour compiler les fichiers SCSS qu'ils contiennent
|
||||
```
|
||||
|
||||
!!!tip
|
||||
|
||||
Le dossier où seront enregistrés ces fichiers
|
||||
statiques peut être changé en modifiant la variable
|
||||
`STATIC_ROOT` dans les paramètres.
|
280
docs/howto/querysets.md
Normal file
280
docs/howto/querysets.md
Normal file
@ -0,0 +1,280 @@
|
||||
L'ORM de Django est puissant, très puissant, non par parce qu'il
|
||||
est performant (après tout, ce n'est qu'une interface, le gros du boulot,
|
||||
c'est la db qui le fait), mais parce qu'il permet d'écrire
|
||||
de manière relativement simple un grand panel de requêtes.
|
||||
|
||||
De manière générale, puisqu'un ORM est un système
|
||||
consistant à manipuler avec un code orienté-objet
|
||||
une db relationnelle (c'est-à-dire deux paradigmes
|
||||
qui ne fonctionnent absolument pas pareil),
|
||||
on rencontre un des deux problèmes suivants :
|
||||
|
||||
- soit l'ORM n'offre pas assez d'abstraction,
|
||||
auquel cas, quand on veut faire des requêtes
|
||||
plus complexes qu'un `select` avec un `where`,
|
||||
on s'emmêle les pinceaux et on se dit que
|
||||
ça aurait été plus simple de le faire directement
|
||||
en SQL.
|
||||
- soit l'ORM offre trop d'abstraction,
|
||||
auquel cas, on a tendance à ne pas prêter
|
||||
assez attention aux requêtes envoyées en base
|
||||
de données et on finit par se rendre compte
|
||||
que les temps d'attente explosent
|
||||
parce qu'on envoie trop de requêtes.
|
||||
|
||||
Django est dans ce deuxième cas.
|
||||
|
||||
C'est pourquoi nous ne parlerons pas ici
|
||||
de son fonctionnement exact ni de toutes les fonctions
|
||||
que l'on peut utiliser
|
||||
(la doc officielle fait déjà ça mieux que nous),
|
||||
mais plutôt des pièges courants
|
||||
et des astuces pour les éviter.
|
||||
|
||||
## Les `N+1 queries`
|
||||
|
||||
### Le problème
|
||||
|
||||
Normalement, quand on veut récupérer une liste,
|
||||
on fait une requête et c'est fini.
|
||||
Mais des fois, ça n'est pas si simple.
|
||||
Par exemple, supposons que nous voulons
|
||||
récupérer les 100 utilisateurs les plus riches,
|
||||
avec leurs informations client :
|
||||
|
||||
```python
|
||||
from core.models import User
|
||||
|
||||
for user in User.objects.order_by("-customer__amount")[:100]:
|
||||
print(user.customer.amount)
|
||||
```
|
||||
|
||||
Combien de requêtes le bout de code suivant effectue-t-il ?
|
||||
101\.
|
||||
En deux pauvres lignes de code, nous avons demandé
|
||||
à la base de données d'effectuer 101 requêtes.
|
||||
Une requête toute seule n'est déjà une opération anodine,
|
||||
alors je vous laisse imaginer ce que ça donne pour 101.
|
||||
|
||||
Si vous ne comprenez pourquoi ce nombre, c'est très simple :
|
||||
|
||||
- Une requête pour sélectionner nos 100 utilisateurs
|
||||
- Une requête supplémentaire pour récupérer les informations
|
||||
client de chaque utilisateur, soit 100 requêtes.
|
||||
|
||||
En effet, les informations client sont stockées dans une
|
||||
autre table, mais le fait d'établir un lien de clef
|
||||
étrangère permet de manipuler `customer`
|
||||
comme si c'était un membre à part entière de `User`.
|
||||
|
||||
Il est à noter cependant, que Django n'effectue une requête
|
||||
que pour le premier accès à un membre d'une relation
|
||||
de clef étrangère.
|
||||
Toutes les fois suivantes, l'objet est déjà là,
|
||||
et django le récupère :
|
||||
|
||||
```python
|
||||
from core.models import User
|
||||
|
||||
# l'utilisateur le plus riche
|
||||
user = User.objects.order_by("-customer__amount").first() # <-- requête db
|
||||
print(user.customer.amount) # <-- requête db
|
||||
print(user.customer.account_id) # on a déjà récupéré `customer`, donc pas de requête
|
||||
```
|
||||
|
||||
Ce n'est donc pas gravissime si vous faites cette
|
||||
erreur quand vous manipulez un seul objet.
|
||||
En revanche, quand vous en manipulez plusieurs,
|
||||
il faut régler le problème.
|
||||
Pour ça, il y a plusieurs méthodes, en fonction de votre cas.
|
||||
|
||||
### `select_related`
|
||||
|
||||
La méthode la plus basique consiste à annoter le queryset,
|
||||
avec la méthode `select_related()`.
|
||||
En faisant ça, Django fera une jointure sur l'autre table
|
||||
et demandera des informations en plus
|
||||
à la db lors de la requête.
|
||||
|
||||
De la sorte, lorsque vous appellerez le membre relié,
|
||||
les informations seront déjà là.
|
||||
|
||||
```python
|
||||
from core.models import User
|
||||
|
||||
richest = User.objects.order_by("-customer__amount")
|
||||
for user in richest.select_related("customer")[:100]:
|
||||
print(user.customer)
|
||||
```
|
||||
|
||||
Le code ci-dessus effectue une seule requête.
|
||||
Chaque fois qu'on veut accéder à `customer`, c'est bon,
|
||||
ça a déjà été récupéré à travers le `annotate`.
|
||||
|
||||
### `prefetch_related`
|
||||
|
||||
Maintenant, un cas plus compliqué.
|
||||
Supposons que vous ne vouliez pas récupérer des informations
|
||||
reliées par une relation One-to-One,
|
||||
mais par une relation One-to-Many.
|
||||
|
||||
Par exemple, un utilisateur a un seul compte client,
|
||||
mais il peut avoir plusieurs cotisations à son actif.
|
||||
Et dans ces cas-là, `annotate` ne marche plus.
|
||||
En effet, s'il peut exister plusieurs cotisations,
|
||||
comment savoir laquelle on veut ?
|
||||
|
||||
Il faut alors utiliser un `prefetch_related`.
|
||||
C'est un mécanisme un peu différent :
|
||||
au lieu de faire une jointure et d'ajouter les informations
|
||||
voulues dans la même requête, Django va effectuer
|
||||
une deuxième requête pour récupérer les éléments de l'autre table,
|
||||
puis, à partir de ces éléments, peupler la relation
|
||||
de son côté.
|
||||
|
||||
C'est un mécanisme qui peut être un peu coûteux en mémoire
|
||||
et qui demande une deuxième requête,
|
||||
mais qui reste quand même largement préférable
|
||||
à faire N requêtes en plus.
|
||||
|
||||
```python
|
||||
from core.models import User
|
||||
|
||||
for user in User.objects.prefetch_related("subscriptions")[:100]:
|
||||
# c'est bon, la méthode prefetch a récupéré en avance les `subscriptions`
|
||||
print(user.subscriptions.all())
|
||||
```
|
||||
|
||||
!!! danger
|
||||
|
||||
La méthode `prefetch_related` ne marche que si vous
|
||||
utilisez la méthode `all()` pour accéder au membre.
|
||||
Si vous utilisez une autre méthode (comme `filter` ou `annotate`),
|
||||
alors Django effectuera une nouvelle requête,
|
||||
et vous retomberez dans le problème initial.
|
||||
|
||||
```python
|
||||
from core.models import User
|
||||
from django.db.models import Count
|
||||
|
||||
for user in User.objects.prefetch_related("subscriptions")[:100]:
|
||||
# Le prefetch_related ne marche plus !
|
||||
print(user.subscriptions.annotate(count=Count("*")))
|
||||
```
|
||||
|
||||
### Récupérer ce dont vous avez besoin
|
||||
|
||||
Des fois (souvent, même), penser explicitement
|
||||
à la jointure est le meilleur choix.
|
||||
|
||||
En effet, vous remarquerez que dans tous
|
||||
les exemples précédents, nous n'utilisions
|
||||
qu'une partie des informations
|
||||
(par exemple, nous ne récupérions que la somme
|
||||
d'argent sur les comptes, et éventuellement le numéro de compte).
|
||||
|
||||
Nous pouvons utiliser la méthode `annotate`
|
||||
pour spécifier explicitement les données que l'on veut
|
||||
joindre à notre requête.
|
||||
|
||||
Quand nous voulions récupérer les informations utilisateur,
|
||||
nous aurions tout aussi bien pu écrire :
|
||||
|
||||
```python
|
||||
from core.models import User
|
||||
from django.db.models import F
|
||||
|
||||
richest = User.objects.order_by("-customer__amount")
|
||||
for user in richest.annotate(amount=F("customer__amount"))[:100]:
|
||||
print(user.amount)
|
||||
```
|
||||
|
||||
On aurait même pu réorganiser ça :
|
||||
```python
|
||||
|
||||
from core.models import User
|
||||
from django.db.models import F
|
||||
|
||||
richest = User.objects.annotate(amount=F("customer__amount")).order_by("-amount")
|
||||
for user in richest[:100]:
|
||||
print(user.amount)
|
||||
```
|
||||
|
||||
Ça peut sembler moins bien qu'un `select_related`, comme ça.
|
||||
Des fois, c'est en effet moins bien, et des fois c'est mieux.
|
||||
La comparaison est plus évidente avec le `prefetch_related`.
|
||||
|
||||
En effet, quand nous voulions récupérer
|
||||
le nombre de cotisations des utilisateurs,
|
||||
le `prefetch_related` ne marchait plus.
|
||||
Pourtant, nous voulions récupérer une seule information.
|
||||
|
||||
Il aurait donc été suffisant d'écrire :
|
||||
```python
|
||||
from core.models import User
|
||||
from django.db.models import Count
|
||||
|
||||
for user in User.objects.annotate(nb_subscriptions=Count("subscriptions"))[:100]:
|
||||
# Et là ça marche, en une seule requête.
|
||||
print(user.nb_subscriptions)
|
||||
```
|
||||
|
||||
Faire une jointure, c'est normal en SQL.
|
||||
Et pourtant avec Django on les oublie trop facilement.
|
||||
Posez-vous toujours la question des données que vous pourriez
|
||||
avoir besoin d'annoter, et vous éviterez beaucoup d'ennuis.
|
||||
|
||||
## Les aggrégations manquées
|
||||
|
||||
Il arrive souvent que l'on veuille une information qui
|
||||
porte sur un ensemble d'objets de notre db.
|
||||
|
||||
Imaginons par exemple que nous voulons connaitre
|
||||
la somme totale des ventes faites à un comptoir.
|
||||
|
||||
Nous avons tous suivi nos cours de programmation,
|
||||
nous écrivons donc instinctivement :
|
||||
|
||||
```python
|
||||
from counter.models import Counter
|
||||
|
||||
foyer = Counter.objects.get(name="Foyer")
|
||||
total_amount = sum(
|
||||
sale.amount * sale.unit_price
|
||||
for sale in foyer.sellings.all()
|
||||
)
|
||||
```
|
||||
|
||||
On pourrait penser qu'il n'y a pas de problème.
|
||||
Après tout, on ne fait qu'une seule requête.
|
||||
Eh bien si, il y a un problème :
|
||||
on fait beaucoup de choses en trop.
|
||||
|
||||
Concrètement, on demande à la base de données
|
||||
de renvoyer toutes les informations,
|
||||
ce qui rallonge inutilement la durée
|
||||
de l'échange entre le serveur et la db,
|
||||
puis on perd du temps à convertir ces informations
|
||||
en objets Python (opération qui a un coût également),
|
||||
et enfin on reperd du temps à calculer en Python
|
||||
quelque chose que la db aurait pu calculer
|
||||
à notre plus bien plus vite.
|
||||
|
||||
Nous aurions dû aggréger la requête,
|
||||
avec la méthode `aggregate` :
|
||||
|
||||
```python
|
||||
from counter.models import Counter
|
||||
from django.db.models import Sum, F
|
||||
|
||||
foyer = Counter.objects.get(name="Foyer")
|
||||
total_amount = (
|
||||
foyer.sellings.aggregate(amount=Sum(F("amount") * F("unit_price"), default=0))
|
||||
)["amount__sum"]
|
||||
```
|
||||
|
||||
En effectuant cette requête, la base de données nous renverra exactement
|
||||
l'information dont nous avons besoin.
|
||||
Et de notre côté, nous n'aurons pas à faire de traitement en plus.
|
||||
|
||||
|
46
docs/howto/subscriptions.md
Normal file
46
docs/howto/subscriptions.md
Normal file
@ -0,0 +1,46 @@
|
||||
## Ajouter une nouvelle cotisation
|
||||
|
||||
Il peut arriver que le type de cotisation
|
||||
proposé varie en prix et en durée.
|
||||
Ces paramètres sont configurables directement dans les paramètres du projet.
|
||||
|
||||
Pour modifier les cotisations disponibles,
|
||||
tout se gère dans la configuration avec la variable `SITH_SUBSCRIPTIONS`.
|
||||
|
||||
Par exemple, si nous voulons ajouter une nouvelle cotisation d'un mois,
|
||||
voici ce que nous ajouterons :
|
||||
|
||||
```python title="settings.py"
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
SITH_SUBSCRIPTIONS = {
|
||||
# Voici un échantillon de la véritable configuration à l'heure de l'écriture.
|
||||
# Celle-ci est donnée à titre d'exemple pour mieux comprendre comment cela fonctionne.
|
||||
"un-semestre": {"name": _("One semester"), "price": 15, "duration": 1},
|
||||
"deux-semestres": {"name": _("Two semesters"), "price": 28, "duration": 2},
|
||||
"cursus-tronc-commun": {
|
||||
"name": _("Common core cursus"),
|
||||
"price": 45,
|
||||
"duration": 4,
|
||||
},
|
||||
"cursus-branche": {"name": _("Branch cursus"), "price": 45, "duration": 6},
|
||||
"cursus-alternant": {"name": _("Alternating cursus"), "price": 30, "duration": 6},
|
||||
"membre-honoraire": {"name": _("Honorary member"), "price": 0, "duration": 666},
|
||||
"un-jour": {"name": _("One day"), "price": 0, "duration": 0.00555333},
|
||||
|
||||
# On rajoute ici notre cotisation
|
||||
# Elle se nomme "Un mois"
|
||||
# Coûte 6€
|
||||
# Dure 1 mois (on raisonne en semestre, ici, c'est 1/6 de semestre)
|
||||
"un-mois": {"name": _("One month"), "price": 6, "duration": 0.166}
|
||||
}
|
||||
```
|
||||
|
||||
Une fois ceci fait, il faut créer une nouvelle migration :
|
||||
|
||||
```bash
|
||||
python ./manage.py makemigrations subscription
|
||||
python ./manage.py migrate
|
||||
```
|
||||
|
||||
N'oubliez pas non plus les traductions (cf. [ici](./translation.md))
|
84
docs/howto/terminal.md
Normal file
84
docs/howto/terminal.md
Normal file
@ -0,0 +1,84 @@
|
||||
## Quel terminal utiliser ?
|
||||
|
||||
Quel que soit votre configuration, si vous avez réussi à installer
|
||||
le projet, il y a de fortes chances que bash existe sur
|
||||
votre ordinateur.
|
||||
Certains d'entre vous utilisent peut-être un autre shell,
|
||||
comme `zsh`.
|
||||
|
||||
En effet, `bash` est bien, il fait le taff ;
|
||||
mais son ergonomie finit par montrer ses limites.
|
||||
C'est pourquoi il existe des shells plus avancés,
|
||||
qui peuvent améliorer l'ergonomie, la complétion des commandes,
|
||||
et l'apparence.
|
||||
C'est le cas de `zsh`.
|
||||
Certains vont même plus loin et refont carrément la syntaxe.
|
||||
C'est le cas de `nu`.
|
||||
|
||||
Pour choisir un terminal, demandez-vous juste quel
|
||||
est votre usage du terminal :
|
||||
|
||||
- Si c'est juste quelques commandes basiques et
|
||||
que vous ne voulez pas vous embêter à changer
|
||||
votre configuration, `bash` convient parfaitement.
|
||||
- Si vous commencez à utilisez le terminal
|
||||
de manière plus intensive, à varier les commandes
|
||||
que vous utilisez et/ou que vous voulez customiser
|
||||
un peu votre expérience, `zsh` est parfait pour vous.
|
||||
- Si vous aimez la programmation fonctionnelle,
|
||||
que vous adorez les pipes et que vous voulez faire
|
||||
des scripts complets mais qui restent lisibles,
|
||||
`nu` vous plaira à coup sûr.
|
||||
|
||||
!!! note
|
||||
|
||||
Ce ne sont que des suggestions.
|
||||
Le meilleur choix restera toujours celui
|
||||
avec lequel vous êtes le plus confortable.
|
||||
|
||||
## Commandes utiles
|
||||
|
||||
### Compter le nombre de lignes du projet
|
||||
|
||||
=== "bash/zsh"
|
||||
|
||||
```bash
|
||||
sudo apt install cloc
|
||||
cloc --exclude-dir=doc,env .
|
||||
```
|
||||
Ok, c'est de la triche, on installe un package externe.
|
||||
Mais bon, ça marche, et l'équivalent pur bash
|
||||
serait carrément plus moche.
|
||||
|
||||
=== "nu"
|
||||
|
||||
Nombre de lignes, groupé par fichier :
|
||||
```nu
|
||||
ls **/*.py | insert linecount { get name | open | lines | length }
|
||||
```
|
||||
|
||||
Nombre de lignes total :
|
||||
```nu
|
||||
ls **/*.py | insert linecount { get name | open | lines | length } | math sum
|
||||
```
|
||||
|
||||
Vous pouvez aussi exlure les lignes vides
|
||||
et les les lignes de commentaire :
|
||||
```nu
|
||||
ls **/*.py |
|
||||
insert linecount {
|
||||
get name |
|
||||
open |
|
||||
lines |
|
||||
each { str trim } |
|
||||
filter { |l| not ($l | str starts-with "#") } | # commentaires
|
||||
filter { |l| ($l | str length) > 0 } | # lignes vides
|
||||
length
|
||||
} |
|
||||
math sum
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
71
docs/howto/translation.md
Normal file
71
docs/howto/translation.md
Normal file
@ -0,0 +1,71 @@
|
||||
Le code du site est entièrement écrit en anglais,
|
||||
le texte affiché aux utilisateurs l'est également.
|
||||
La traduction en français se fait
|
||||
ultérieurement avec un fichier de traduction.
|
||||
Voici un petit guide rapide pour apprendre à s'en servir.
|
||||
|
||||
## Dans le code du logiciel
|
||||
|
||||
Imaginons que nous souhaitons afficher "Hello"
|
||||
et le traduire en français.
|
||||
Voici comment signaler que ce mot doit être traduit.
|
||||
|
||||
Si le mot est dans le code Python :
|
||||
|
||||
```python
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
help_text=_("Hello")
|
||||
```
|
||||
|
||||
Si le mot apparaît dans le template Jinja :
|
||||
|
||||
```jinja
|
||||
{% trans %}Hello{% endtrans %}
|
||||
```
|
||||
|
||||
## Générer le fichier django.po
|
||||
|
||||
La traduction se fait en trois étapes.
|
||||
Il faut d'abord générer un fichier de traductions,
|
||||
l'éditer et enfin le compiler au format binaire pour qu'il soit lu par le serveur.
|
||||
|
||||
```bash
|
||||
./manage.py makemessages --locale=fr --ignore "env/*" -e py,jinja
|
||||
```
|
||||
|
||||
## Éditer le fichier django.po
|
||||
|
||||
```locale
|
||||
# locale/fr/LC_MESSAGES/django.po
|
||||
|
||||
# ...
|
||||
msgid "Hello"
|
||||
msgstr "" # Ligne à modifier
|
||||
|
||||
# ...
|
||||
```
|
||||
|
||||
!!!note
|
||||
|
||||
Si les commentaires suivants apparaissent, pensez à les supprimer.
|
||||
Ils peuvent gêner votre traduction.
|
||||
|
||||
```
|
||||
#, fuzzy
|
||||
#| msgid "Bonjour"
|
||||
```
|
||||
|
||||
|
||||
## Générer le fichier django.mo
|
||||
|
||||
Il s'agit de la dernière étape.
|
||||
Un fichier binaire est généré à partir du fichier django.mo.
|
||||
|
||||
```bash
|
||||
./manage.py compilemessages
|
||||
```
|
||||
|
||||
!!!tip
|
||||
|
||||
Pensez à redémarrer le serveur si les traductions ne s'affichent pas
|
71
docs/howto/weekmail.md
Normal file
71
docs/howto/weekmail.md
Normal file
@ -0,0 +1,71 @@
|
||||
Le site est capable de générer des mails automatiques
|
||||
contenant l’agrégation d'articles écrits par les administrateurs de clubs.
|
||||
Le contenu est inséré dans un template standardisé
|
||||
et contrôlé directement dans le code.
|
||||
Il arrive régulièrement que l'équipe communication souhaite modifier ce template.
|
||||
Que ce soient les couleurs,
|
||||
l'agencement ou encore la bannière ou le footer,
|
||||
voici tout ce qu'il y a à savoir sur le fonctionnement
|
||||
du weekmail en commençant par la classe qui le contrôle.
|
||||
|
||||
## Modifier la bannière et le footer
|
||||
|
||||
Ces éléments sont contrôlés par les méthodes `get_banner` et `get_footer`
|
||||
de la classe `Weekmail`.
|
||||
Les modifier est donc très simple,
|
||||
il suffit de modifier le contenu de la fonction et
|
||||
de rajouter les nouvelles images dans les statics.
|
||||
|
||||
Les images sont à ajouter dans `core/static/com/img`
|
||||
et sont à nommer selon le type (banner ou footer), le semestre (Automne ou Printemps) et l'année.
|
||||
Exemple : `weekmail_bannerA18.jpg` pour la bannière de l'automne 2018.
|
||||
|
||||
```python
|
||||
from django.conf import settings
|
||||
from django.templatetags.static import static
|
||||
|
||||
# Sélectionnez le fichier de bannière pour le weekmail de l'automne 2018
|
||||
|
||||
def get_banner(self):
|
||||
return "http://" + settings.SITH_URL + static("com/img/weekmail_bannerA18.jpg")
|
||||
```
|
||||
|
||||
!!!note
|
||||
|
||||
Penser à prendre les images au format **jpg** et à les compresser un peu,
|
||||
pour qu'elles soient le plus léger possible,
|
||||
c'est bien mieux pour l'utilisateur final.
|
||||
|
||||
!!!warning
|
||||
|
||||
Pensez à laisser les anciennes images dans le dossier
|
||||
pour que les anciens weekmails ne soient pas affectés par les changements.
|
||||
|
||||
## Modifier le template
|
||||
|
||||
Il existe deux templates différents :
|
||||
|
||||
- Un en texte pur, qui sert pour le rendu dégradé des lecteurs
|
||||
de mails ne supportant pas le HTML
|
||||
- un qui fait du rendu html.
|
||||
|
||||
Ces deux templates sont respectivement accessibles aux emplacements suivants :
|
||||
|
||||
- `com/templates/com/weekmail_renderer_html.jinja`
|
||||
- `com/templates/com/weekmail_renderer_text.jinja`
|
||||
|
||||
!!!note
|
||||
|
||||
Pour le rendu HTML, pensez à utiliser le CSS et le javascript
|
||||
le plus simple possible pour que le rendu se fasse correctement
|
||||
dans les clients mails qui sont souvent capricieux.
|
||||
|
||||
!!!note
|
||||
|
||||
Le CSS est inclus statiquement pour que toute modification
|
||||
ultérieure de celui-ci n'affecte pas les versions précédemment envoyées.
|
||||
|
||||
!!!warning
|
||||
|
||||
Si vous souhaitez ajouter du contenu,
|
||||
n'oubliez pas de bien inclure ce contenu dans les deux templates.
|
Reference in New Issue
Block a user