Taille: 15228
Commentaire: Faut rien casser
|
Taille: 21117
Commentaire: Un peu d'aeration
|
Texte supprimé. | Texte ajouté. |
Ligne 1: | Ligne 1: |
#acl +All:read |
|
Ligne 7: | Ligne 9: |
Django est un framework puissant écrit en Python qui permet de concevoir un site Internet modulaire avec une vision abstraite d'une base de données. Django offre l'avantage de manipuler directement des objets Python plutôt que de gérer des requêtes SQL manuellement. C'est un confort de développement en plus d'assurer un code lisible et puissant. | Django <<FootNote(https://docs.djangoproject.com/fr/3.2/)>> est un framework puissant écrit en Python qui permet de concevoir un site Internet modulaire avec une vision abstraite d'une base de données. Django offre l'avantage de manipuler directement des objets Python plutôt que de gérer des requêtes SQL manuellement. C'est un confort de développement en plus d'assurer un code lisible et puissant. |
Ligne 20: | Ligne 22: |
'''IMPORTANT :''' Afin de minimiser les risques de casser quelque chose d'important, merci d'utiliser `note-beta.crans.org` au lieu de `note.crans.org` lors de vos tests. L'installation est exactement la même, les étapes à suivre aussi (faut juste remplacer `note` par `note-beta` et `bde-note` par `bde-nk20-beta`). Les comptes sur le serveur sont liés (un serveur LDAP derrière), les identifiants sont donc les mêmes. }}} La première étape est de se connecter au serveur de la note, accès réservé aux respos info et trésoriers. Trésoriers, si vous n'avez pas de compte sur le serveur (je dis bien sur le serveur, pas sur la note en elle-même, j'espère bien qu'en tant que trésorier vous ayez un compte), adressez-vous à un respo info pour qu'il vous en crée un et qu'il vous explique comment vous connecter. |
'''IMPORTANT :''' Afin de minimiser les risques de casser quelque chose d'important, merci d'utiliser `note-dev.crans.org` au lieu de `note.crans.org` lors de vos tests. L'installation est exactement la même, les étapes à suivre aussi (faut juste remplacer `note` par `note-dev` et `bde-note` par `bde-note-dev`). Les comptes des differents serveur du BDE sont liés <<FootNote(via un service LDAP, installé sur bde-note)>>, les identifiants sont donc les mêmes. }}} La première étape est de se connecter au serveur de la note, accès réservé aux respos info et trésoriers. Trésoriers, si vous n'avez pas de compte sur le serveur, adressez-vous à un respo info pour qu'il vous en crée un et qu'il vous explique comment vous connecter. |
Ligne 51: | Ligne 53: |
Même si ce ne sont que deux commandes, il peut être assez pénible de les taper à chaque fois. Il est possible de les exécuter automatiquement en les insérant dans votre fichier .bashrc : {{{ cat >> ~/.bashrc cd /var/www/note_kfet source /var/www/note_kfet/env/bin/activate }}} Il vous suffira de taper simultanément Ctrl+D, et au prochain redémarrage de votre terminal, vous serez directement dans le bon dossier et dans le bon environnement. Ah au fait, pour quitter la connexion, exécutez `exit` ou tapez `Ctrl+D`. Si pour une raison ou une autre vous souhaitez quitter l'environnement python, exécutez `deactivate`. |
{{{#!wiki tip Même si ce ne sont que deux commandes, il peut être assez pénible de les taper à chaque fois. Sur les nouveaux comptes, les deux instructions précédentes sont ajoutées dans votre fichier `.bashrc`, ce qui a pour effet de vous déplacer dans le bon dossier et d'entrer dans le bon environnement virtuel à chaque connexion. Ainsi, les opérations précédentes ne sont pas nécessaires. }}} Ah au fait, pour quitter la connexion, exécutez `exit` ou tapez `Ctrl+D`. Si pour une raison ou une autre vous souhaitez quitter l'environnement Python, exécutez `deactivate`. |
Ligne 62: | Ligne 61: |
Je vous ai menti. Vous n'êtes pas encore dans la note, seulement dans son environnement. Il est cependant possible d'intéragir directement avec la note en exécutant dans le bon dossier `./manage.py shell`. Cela a pour effet d'ouvrir un terminal Python connecté avec la note. Cependant, il peut être assez pénible d'avoir à importer à chaque fois les bons modules, alors retenez-le, le script à exécuter en permanence est : | Je vous ai menti. Vous n'êtes pas encore dans la note, seulement dans son environnement. Il est cependant possible d’interagir directement avec la note en exécutant dans le bon dossier `./manage.py shell`. Cela a pour effet d'ouvrir un terminal Python connecté avec la note. Cependant, il peut être assez pénible d'avoir à importer à chaque fois les bons modules, alors retenez-le, le script à exécuter en permanence est : |
Ligne 107: | Ligne 106: |
* ''treasury'' | * '''treasury''' |
Ligne 158: | Ligne 157: |
Vous suivez jusqu'ici ? Tant mieux. Maintenant on va aborder la partie la plus complexe mais aussi la plus puissante : les filtres. Une requête se filtre en utilisant la fonction `filter`, qui prend des paramètres. Son utilisation est intuitive et se comprend par l'exemple. Pour filtrer l'ensemble des boutons qui valent 1 €, on peut exécuter par exemple `TransactionTemplate.objects.filter(amount=100).all()`, sans oublier le `.all()` à la fin pour récupérer les résultats (sans cela la requête est construite mais pas exécutée). La requête exécutée derrière est `SELECT * FROM note_transactiontemplate WHERE amount = 100;`. Il est possible d'appliquer un ET simplement en rentrant plusieurs paramètres : `TransactionTemplate.objects.filter(amount=100, display=True).all()` récupère l'ensemble des boutons de valeur 1 € qui sont visibles (requête générée : `SELECT * FROM note_transactiontemplate WHERE amount = 100 AND display = true;`). Il est important de bien comprendre cette étape. Même si j'ai un peu menti : la fonction `filter` attend en réalité des objets de type Q, et non pas seulement vos paramètres de filtres. L'objet Q (du module `django.db.models`, déjà importé) permet de filtrer vos requêtes de façon très puissante. La requête précédente se réécrit `TransactionTemplate.objects.filter(Q(amount=100, display=True)).all()`. Jusqu'ici, rien d'incroyable, et on comprend pourquoi il est possible d'alléger la notation en se dispensant de l'enveloppe Q. Mais tout ceci n'est pas assez pythonesque. Il est en effet possible d'utiliser les opérateurs unaires `&`, `|` et `~` pour générer vos filtres, qui se comportent comme des `ET`, des `OU` et des `NON`. On aurait pu écrire la même requête `TransactionTemplate.objects.filter(Q(amount=100) & Q(display=True)).all()`. Certes, c'est plus lourd qu'avant, mais désormais, il est possible d'utiliser des OU et des NON dans vos filtres :) |
Vous suivez jusqu'ici ? Tant mieux. Maintenant on va aborder la partie la plus complexe mais aussi la plus puissante : les filtres. Une requête se filtre en utilisant la fonction `filter`, qui prend des paramètres. Pour filtrer l'ensemble des boutons qui valent 1 €, on peut exécuter par exemple `TransactionTemplate.objects.filter(amount=100).all()`, sans oublier le `.all()` à la fin pour récupérer les résultats (sans cela la requête est construite mais pas exécutée). La requête exécutée derrière est `SELECT * FROM note_transactiontemplate WHERE amount = 100;`. Il est possible d'appliquer un ET simplement en rentrant plusieurs paramètres : {{{TransactionTemplate.objects.filter(amount=100, display=True).all()}}} Cette requete récupère l'ensemble des boutons de valeur 1 € <<FootNote(Les montants sont stocké en centime d'euros, toujours entiers) qui sont visibles (requête générée : `SELECT * FROM note_transactiontemplate WHERE amount = 100 AND display = true;`). Il est important de bien comprendre cette étape: Em pratique la fonction `filter` attend en réalité des objets de type `Q`, et non pas seulement vos paramètres de filtres (dans ce cas, ils sont transformé par Django en `Q`) L'objet `Q` , comme "Query" (du module `django.db.models`, déjà importé) permet de filtrer vos requêtes de façon très puissante. La requête précédente se réécrit {{{#!highlight python TransactionTemplate.objects.filter(Q(amount=100, display=True)).all() }}} Avec les `Q` il est en fait possible d'utiliser les opérateurs `&`, `|` et `~` pour combiner vos filtres, qui se comportent comme des `ET`, des `OU` et des `NON`. Ainsi la requete précédente est maintenant: {{{#!highlight python TransactionTemplate.objects.filter(Q(amount=100) & Q(display=True)).all() }}} Certes, c'est plus lourd qu'avant, mais désormais, il est possible d'utiliser des OU et des NON dans vos filtres :) |
Ligne 168: | Ligne 182: |
Bon, on arrive à faire des OU, des ET et des NON. C'est déjà bien, mais on peut faire mieux. Il est possible d'appliquer une recherche plus complexe que juste `champ = valeur` ! On peut par exemple chercher en ignorant la casse, ou juste chercher dans une partie du champ. Pour cela, il suffit d'ajouter `__` après le nom du champ et d'ajouter l'une de ces fonctions, dont la plupart ne s'applique qu'aux chaînes de caractères, d'autres qu'à des nombres/dates : || '''Suffixe''' || '''Description''' || |
En plus des ET, OU, NON, il est possible d'appliquer une recherche plus complexe que juste `champ = valeur` ! On peut par exemple chercher en ignorant la casse, ou juste chercher dans une partie du champ. Pour cela, il suffit d'ajouter `__` après le nom du champ et d'ajouter l'une de ces fonctions, dont la plupart ne s'applique qu'aux chaînes de caractères, d'autres qu'à des nombres/dates : || '''Suffixe''' || '''Description''' || |
Ligne 172: | Ligne 186: |
|| `__endswith` || Éléments finissant par la requête || | || `__endswith` || Éléments finissant par la requête || |
Ligne 179: | Ligne 193: |
|| `__iendswith` || Éléments finissant par la requête, à casse près || | || `__iendswith` || Éléments finissant par la requête, à casse près || |
Ligne 199: | Ligne 213: |
{{{#!wiki caution TODO : Clé étrangères et jointures }}} {{{#!wiki caution TODO : Récupération d'un objet, sauvegarde et prévention "attention c'est dangereux" |
Les requêtes peuvent être encore plus puissantes. On s'intéressait en effet jusque là aux requêtes qui portaient sur un seul type d'objets, mais il est complètement possible de filtrer sur les relations entre les objets. Encore une fois, on utilisera le séparateur {{{__}}}. Par exemple, si on a un modèle {{{A}}} qui a une clé étrangère vers un modèle {{{B}}} nommé {{{b}}}, et que le modèle {{{B}}} dispose d'un champ {{{name}}}, on peut vouloir chercher tous les objets de type {{{A}}} tel que le nom de l'objet {{{b}}} associé soit {{{toto}}}. En SQL, cela donnerait : {{{#!highlight sql SELECT * FROM A JOIN B ON A.b = B.id WHERE B.name = 'toto'; }}} Si la syntaxe est encore lisible, en Django cela devient : {{{#!highlight python A.objects.filter(b__name="toto") }}} Ce qui est tout de même plus facile à utiliser. Bien sûr, cela se chaîne, si {{{c}}} est une clé étrangère de {{{B}}} vers un modèle {{{C}}} : {{{#!highlight python A.objects.filter(b__c__name="toto") }}} On dispose de trois types différents de relations entre les modèles : * Les clés étrangères, {{{ForeignKey}}}. Ce type de relation permet de lier 2 modèles par une unique flèche, sans contrainte. Si {{{A}}} pointe vers {{{B}}}, alors les objets {{{B}}} disposent par défaut d'un paramètre {{{a_set}}} (qui peut être renommé en changeant le champ {{{related}}} dans la définition de la clé étrangère) pour récupérer tous les objets de type {{{A}}} pointant vers cet objet de type {{{B}}}. * Les {{{OneToOneField}}}, qui relie deux modèles par une flèche bidirectionnelle, ajoutant une contrainte d'unicité à la relation. Par exemple, à un {{{Profile}}} est associé un unique {{{user}}}. On peut donc rechercher à la fois {{{profile__user}}} et {{{user__profile}}} selon les besoins. * Les {{{ManyToManyField}}}, qui relie un objet de type {{{A}}} à un nombre arbitraire d'objets de type {{{B}}}. De la même façon que pour les {{{ForeignKey}}}, les objets de type {{{B}}} ont un champ {{{a_set}}} pour récupérer la liste des objets {{{A}}} pointant vers cet objet {{{B}}}, dont le nom peut être renommé à nouveau grâce au paramètre {{{related}}}. {{{#!wiki tip Exercice : compter de transactions valides faites au cours des 90 derniers jours à partir d'un bouton de la catégorie `Alcool`, hors Corona. Bonus : donner la somme totale de ces transactions. }}} {{{#!wiki tip Exercice : compter le nombre d'utilisateurs qui ont au moins un alias dont la version normalisée contient la lettre `z` mais pas la lettre `w`. }}} {{{#!wiki tip Excercice : compter le nombres d'entrées à un pot, invitations comprises, sous le mandat Fina[list], ayant commencé le 17 février 2019 pour se finir le 5 mars 2020. }}} On sait désormais effectuer des requêtes des plus complexes. Reste maintenant à savoir traiter des données. On suppose que l'on a effectué une requête : {{{qs = A.objects.filter(…)}}}. Alors {{{qs.all()}}} renverra un itérateur contenant tous les objets {{{A}}} trouvés. Les objets renvoyés sont des objets Python, qui se traitent comme n'importe quel objet Python : {{{#!highlight python In [4]: for note in NoteSpecial.objects.all(): ...: print(note.id, note.special_type) ...: 1 Espèces 2 Carte bancaire 3 Chèque 4 Virement bancaire }}} Lorsqu'il n'y a pas d'ambiguïté et que notre requête n'est supposée renvoyer qu'un seul objet (ni plus, ni moins), on peut utiliser la fonction {{{get}}}. Les filtres peuvent même s'appliquer dans cette fonction. Ainsi, les deux lignes sont équivalentes : {{{#!highlight python In [5]: me = User.objects.filter(username="ÿnérant").get() In [6]: me = User.objects.get(username="ÿnérant") In [7]: me.email Out[7]: 'ynerant@crans.org' Les relations s'utilisent également sans problème : {{{#!highlight python In [8]: for alias in me.note.alias.filter(name__icontains="z").all(): ...: print(f"{alias.id}\t{alias.name}\t{alias.normalized_name}") Out[8]: … }}} Django se charge derrière de comprendre tout ce que vous vouliez dire et de faire les bonnes requêtes SQL, en transformant les résultats en objets Python souhaités. C'est particulièrement pratique, puissant et plutôt intuitif. {{{#!wiki tip Exercice : préparer le bilan financier }}} Enfin, si jamais vous étiez amenés à modifier des objets, vous pouvez les modifier comme si c'étaient de vrais objets Python. La fonction {{{save()}}} enregistrera les données dans la base de données. {{{#!highlight python In [9]: me.username = "Respo info" # Don't do this In [10]: me.save() In [11]: me Out[11]: <User: Respo info> }}} Bien sûr, évitez de préférence la modification d'objets dans un terminal, ne le faites qu'en cas de besoin ! {{{#!wiki tip Exercice : supprimer tous les droits de super-utilisateur, et auto-nommez-vous super-utilisateur. |
Ligne 208: | Ligne 309: |
=== Récupérer l'ensemble des transactions valides d'un jour donné === {{{#!highlight python transactions = Transaction.objects.filter(created_at__startswith="YYYY-mm-dd", valid=True).all() for transaction in transactions: print(transaction.source, transaction.destination, transaction.total / 100, transaction.reason) print("Total :", sum(tr.total for tr in transactions) / 100) }}} Example de sortie : {{{ plop, BDE, 1.10, Coca toto, BDE, 2.10, Kwak toto, plop, 10, Pizza BDE, plop, 52.34, Courses Total : 65.54 }}} === Classer les M boutons les plus cliqués les N derniers jours === {{{#!highlight python from datetime import timedelta templates = RecurrentTransaction.objects.filter( template__display=True, valid=True, created_at__gte=timezone.now() - timedelta(days=N), ).values("template").annotate(transaction_count=Count("template")) \ .order_by("transaction_count")[:M].all() for obj in templates: template = TransactionTemplate.objects.get(pk=obj["template"]) print(template, obj["transaction_count"]) }}} Exemple de sortie : {{{ Coca 68 Banane ENSC 37 Sirop 2 }}} |
Utiliser la note dans un shell Django
Django, keskecé ?
Django 1 est un framework puissant écrit en Python qui permet de concevoir un site Internet modulaire avec une vision abstraite d'une base de données. Django offre l'avantage de manipuler directement des objets Python plutôt que de gérer des requêtes SQL manuellement. C'est un confort de développement en plus d'assurer un code lisible et puissant.
Un autre intérêt de Django est le fait qu'il soit codé en Python : cela permet d'ouvrir facilement un terminal et d'effectuer des requêtes et des modifications à la base de données.
Ouvrir un terminal Django
Si la plupart des manipulations peuvent sembler évidentes pour un linuxien averti, cela peut l'être beaucoup moins pour un trésorier sans expérience dans un terminal. Cette page expliquera donc tout en supposant que vous ne sachiez pas manipuler un terminal, mais que vous savez utiliser un minimum Python.
Néanmoins, il est fortement recommandé de se documenter sur l'utilisation d'un terminal et de SSH.
IMPORTANT : Afin de minimiser les risques de casser quelque chose d'important, merci d'utiliser note-dev.crans.org au lieu de note.crans.org lors de vos tests. L'installation est exactement la même, les étapes à suivre aussi (faut juste remplacer note par note-dev et bde-note par bde-note-dev). Les comptes des differents serveur du BDE sont liés 2, les identifiants sont donc les mêmes.
La première étape est de se connecter au serveur de la note, accès réservé aux respos info et trésoriers. Trésoriers, si vous n'avez pas de compte sur le serveur, adressez-vous à un respo info pour qu'il vous en crée un et qu'il vous explique comment vous connecter.
Dans un terminal (qui peut s'ouvrir sur Windows en tapant Windows+R, cmd puis Entrée, sur Linux vous savez), connectez-vous au serveur de la note en tapant ssh pseudo@note.crans.org. Sauf configuration de votre part, vous serez invités à taper votre mot de passe du serveur (encore une fois, les identifiants n'ont aucune raison d'être liés à ceux du site de la note). Une fois cela fait, vous serez connectés à la note. La ligne suivante devrait s'afficher :
pseudo@bde-note:~$
Il est possible de changer son mot de passe en exécutant la commande passwd.
Déplacez-vous ensuite dans le dossier dans lequel se trouve le code de la note : cd /var/www/note_kfet/
pseudo@bde-note:~$ cd /var/www/note_kfet/ pseudo@bde-note:/var/www/note_kfet$
Il vous faut désormais charger l'environnement virtuel Python, qui contient l'ensemble des modules externes chargés, en plus de définir votre Python sur Python 3. Pour cela, tapez . env/bin/activate, un (env) devrait maintenant préfixer la dernière ligne de votre terminal :
pseudo@bde-note:/var/www/note_kfet$ . env/bin/activate (env) pseudo@bde-note:/var/www/note_kfet$
Voilà ! Vous êtes entrés dans la note N'hésitez pas à faire un python --version pour vous assurer que vous utilisez bien Python 3.
Même si ce ne sont que deux commandes, il peut être assez pénible de les taper à chaque fois. Sur les nouveaux comptes, les deux instructions précédentes sont ajoutées dans votre fichier .bashrc, ce qui a pour effet de vous déplacer dans le bon dossier et d'entrer dans le bon environnement virtuel à chaque connexion. Ainsi, les opérations précédentes ne sont pas nécessaires.
Ah au fait, pour quitter la connexion, exécutez exit ou tapez Ctrl+D. Si pour une raison ou une autre vous souhaitez quitter l'environnement Python, exécutez deactivate.
Pénétrer dans la note
Je vous ai menti. Vous n'êtes pas encore dans la note, seulement dans son environnement. Il est cependant possible d’interagir directement avec la note en exécutant dans le bon dossier ./manage.py shell. Cela a pour effet d'ouvrir un terminal Python connecté avec la note. Cependant, il peut être assez pénible d'avoir à importer à chaque fois les bons modules, alors retenez-le, le script à exécuter en permanence est :
./manage.py shell_plus
Cette fois vous êtes vraiment dans la note, prêts à tout casser
La structure de la Note
Comme dit précédemment, tout ce qui est en base de données est représenté par des objets Python. À l'heure actuelle, voici l'ensemble des modèles (type d'objet) existant, regroupés par module :
auth
User : Utilisateur inscrit sur la note
activity
Activity : Activité organisée par un club BDE
ActivityType : Type d'activité (pot, soirée, ...)
Entry : Entrée à une activité (adhérent ou invité)
Guest : Personne invitée par un adhérent à une activité
api
aucun modèle enregistré
logs
Changelog : Modification effectuée dans la base de donnée à un certain instant
member
Profile : Informations complémentaires liées à un utilisateur (adresse, numéro de téléphone, payé, ...)
Club : Détails d'un club
Membership : Informations sur une adhésion à un club
note
Note : Objet Note générique qui contient le solde, date de dernier négatif, ...
NoteUser : Type de note lié à un utilisateur
NoteClub : Type de note lié à un club
NoteSpecial : Type de note utilisé lors d'un crédit/retrait
Alias : Champ de texte permettant de désigner une note
TemplateCategory : Catégorie de boutons de consommations (alcool, bouffe, ...)
TransactionTemplate : Boutons de consommation rapide
Transaction : Détails génériques d'une transaction
RecurrentTransaction : Transaction liée à un bouton
SpecialTransaction : Transaction liée à un crédit/retrait (avec nom, prénom, banque en plus)
MembershipTransaction : Transaction liée à une adhésion
permission
Permission : Abstraction d'une permission, qui contient une requête pour déterminer si un objet à le droit de voir/ajouter/modifier/supprimer un objet, avec quel masque de permissions
PermissionMask : Définition des masques de permission
Role : Rôle d'une adhésion dans un club, définissant l'ensemble des permissions octroyées
registration
Aucun modèle enregistré
treasury
Invoice : Facture générée en TeX puis PDF
Product : Produit lié à une facture
RemittanceType : Type de remise, pour savoir ce qui est utile à remiser (les chèques)
Remittance : Remise (de chèques)
SpecialTransactionProxy : Afin de ne pas casser des flèches en base de données et d'éviter une dépendance trop forte du module de trésorerie, ce modèle vient juste rajouter des flèches
SogeCredit : Crédit de la société générale lié à un utilisateur
wei
WEIClub : Surcouche du modèle Club, avec des infos en plus qui concerne le WEI (dates, ...)
Bus : Bus partant au WEI (nom + membres)
BusTeam : Équipe d'un bus WEI
WEIRole : Surcouche du modèle Role, n'inquant que ... le rôle est pour un WEI seulement
WEIRegistration : Pré-inscription d'un utilisateur à un WEI, non payée, contient les infos importantes
WEIMembership : Surcouche du modèle Membership ajoutant en plus les liens à l'inscription, au bus et à l'équipe
L'ensemble des champs et les relations entre chaque modèle peuvent se trouver sur le schéma de relations : Graphe total Graphe des applications ajoutées uniquement
L'ensemble des classes est donc automatiquement importé lorsque vous exécutez ./manage.py shell_plus. Au besoin, chaque classe se trouve dans le module app.models en remplaçant app par le nom de l'application (note, member, ...)
How to casser la Note
J'ai une bonne et une mauvaise nouvelle. La bonne c'est qu'on en a fini avec le bash, la mauvaise c'est qu'on va pas casser la note, je tiens à son bon fonctionnement On va simplement voir comment intéragir avec elle.
Comme déjà dit, on ne va pas utiliser de SQL et utiliser la puissance de Django pour effectuer nos requêtes simplement et efficacement. On désignera dans la suite par Model le nom du modèle qui nous intéresse, la liste des modèles ayant été donnée ci-dessus.
La documentation complète de ce qui suit est disponible ici : https://docs.djangoproject.com/fr/3.1/topics/db/queries/
Le champ qui nous permet d'intéragir avec la base de données est objects. C'est le point de départ de toutes nos requêtes.
Pour récupérer l'ensemble des objets d'un modèle, on peut appeler la fonction all(). Ainsi, ActivityType.objects.all() listera l'ensemble des types d'activité enregistrés :
Dans le fond, cela exécute la requête SELECT * FROM activity_activitytype; et transforme les résultats en objets Python (à quelque chose près).
Exercice : Afficher l'ensemble des factures déjà générées.
Il est possible de compter le nombre d'objets trouvés en remplaçant all() par count() :
Vous suivez jusqu'ici ? Tant mieux.
Maintenant on va aborder la partie la plus complexe mais aussi la plus puissante : les filtres. Une requête se filtre en utilisant la fonction filter, qui prend des paramètres. Pour filtrer l'ensemble des boutons qui valent 1 €, on peut exécuter par exemple TransactionTemplate.objects.filter(amount=100).all(), sans oublier le .all() à la fin pour récupérer les résultats (sans cela la requête est construite mais pas exécutée). La requête exécutée derrière est SELECT * FROM note_transactiontemplate WHERE amount = 100;.
Il est possible d'appliquer un ET simplement en rentrant plusieurs paramètres : TransactionTemplate.objects.filter(amount=100, display=True).all() Cette requete récupère l'ensemble des boutons de valeur 1 € <<FootNote(Les montants sont stocké en centime d'euros, toujours entiers) qui sont visibles (requête générée : SELECT * FROM note_transactiontemplate WHERE amount = 100 AND display = true;).
Il est important de bien comprendre cette étape: Em pratique la fonction filter attend en réalité des objets de type Q, et non pas seulement vos paramètres de filtres (dans ce cas, ils sont transformé par Django en Q) L'objet Q , comme "Query" (du module django.db.models, déjà importé) permet de filtrer vos requêtes de façon très puissante.
La requête précédente se réécrit
1 TransactionTemplate.objects.filter(Q(amount=100, display=True)).all()
Avec les Q il est en fait possible d'utiliser les opérateurs &, | et ~ pour combiner vos filtres, qui se comportent comme des ET, des OU et des NON. Ainsi la requete précédente est maintenant:
1 TransactionTemplate.objects.filter(Q(amount=100) & Q(display=True)).all()
Certes, c'est plus lourd qu'avant, mais désormais, il est possible d'utiliser des OU et des NON dans vos filtres
Exercice : Compter l'ensemble des boutons visibles qui coûtent 1 € et l'ensemble des boutons invisibles qui ne coûtent pas 2 € en une seule requête.
En plus des ET, OU, NON, il est possible d'appliquer une recherche plus complexe que juste champ = valeur ! On peut par exemple chercher en ignorant la casse, ou juste chercher dans une partie du champ. Pour cela, il suffit d'ajouter __ après le nom du champ et d'ajouter l'une de ces fonctions, dont la plupart ne s'applique qu'aux chaînes de caractères, d'autres qu'à des nombres/dates :
Suffixe |
Description |
__contains |
Éléments contenant une partie de la requête (n'importe où) |
__endswith |
Éléments finissant par la requête |
__exact |
Comportement par défaut, teste l'égalité |
__gt |
Teste si le champ est strictement supérieur à la requête |
__gte |
Teste si le champ est supérieur ou égal à la requête |
__iexact |
Teste l'égalité à casse près |
__in |
Teste si l'élément est dans un tableau ou une sous-requête |
__icontains |
Éléments contenant une partie de la requête (n'importe où), à casse près |
__iendswith |
Éléments finissant par la requête, à casse près |
__iregex |
Éléments trouvés par l'expression régulière donnée, à casse près |
__isnull |
Éléments valant NULL si True, les autres sinon |
__istartswith |
Éléments commençant par la requête, à casse près |
__lt |
Teste si le champ est strictement inférieur à la requête |
__lte |
Teste si le champ est inférieur ou égal à la requête |
__regex |
Éléments trouvée par l'expression régulière donnée |
__startwith |
Éléments commençant par la requête |
Un petit exemple, illustrant la récupération de clubs dont le nom finit par BDE :
Exercice : Compter l'ensemble des alias dont la version normalisée contient « bde »
Les requêtes peuvent être encore plus puissantes. On s'intéressait en effet jusque là aux requêtes qui portaient sur un seul type d'objets, mais il est complètement possible de filtrer sur les relations entre les objets. Encore une fois, on utilisera le séparateur __.
Par exemple, si on a un modèle A qui a une clé étrangère vers un modèle B nommé b, et que le modèle B dispose d'un champ name, on peut vouloir chercher tous les objets de type A tel que le nom de l'objet b associé soit toto.
En SQL, cela donnerait :
1 SELECT * FROM A JOIN B ON A.b = B.id WHERE B.name = 'toto';
Si la syntaxe est encore lisible, en Django cela devient :
1 A.objects.filter(b__name="toto")
Ce qui est tout de même plus facile à utiliser.
Bien sûr, cela se chaîne, si c est une clé étrangère de B vers un modèle C :
1 A.objects.filter(b__c__name="toto")
On dispose de trois types différents de relations entre les modèles :
Les clés étrangères, ForeignKey. Ce type de relation permet de lier 2 modèles par une unique flèche, sans contrainte. Si A pointe vers B, alors les objets B disposent par défaut d'un paramètre a_set (qui peut être renommé en changeant le champ related dans la définition de la clé étrangère) pour récupérer tous les objets de type A pointant vers cet objet de type B.
Les OneToOneField, qui relie deux modèles par une flèche bidirectionnelle, ajoutant une contrainte d'unicité à la relation. Par exemple, à un Profile est associé un unique user. On peut donc rechercher à la fois profile__user et user__profile selon les besoins.
Les ManyToManyField, qui relie un objet de type A à un nombre arbitraire d'objets de type B. De la même façon que pour les ForeignKey, les objets de type B ont un champ a_set pour récupérer la liste des objets A pointant vers cet objet B, dont le nom peut être renommé à nouveau grâce au paramètre related.
Exercice : compter de transactions valides faites au cours des 90 derniers jours à partir d'un bouton de la catégorie Alcool, hors Corona.
Bonus : donner la somme totale de ces transactions.
Exercice : compter le nombre d'utilisateurs qui ont au moins un alias dont la version normalisée contient la lettre z mais pas la lettre w.
Excercice : compter le nombres d'entrées à un pot, invitations comprises, sous le mandat Fina[list], ayant commencé le 17 février 2019 pour se finir le 5 mars 2020.
On sait désormais effectuer des requêtes des plus complexes. Reste maintenant à savoir traiter des données.
On suppose que l'on a effectué une requête : qs = A.objects.filter(…). Alors qs.all() renverra un itérateur contenant tous les objets A trouvés. Les objets renvoyés sont des objets Python, qui se traitent comme n'importe quel objet Python :
Lorsqu'il n'y a pas d'ambiguïté et que notre requête n'est supposée renvoyer qu'un seul objet (ni plus, ni moins), on peut utiliser la fonction get. Les filtres peuvent même s'appliquer dans cette fonction. Ainsi, les deux lignes sont équivalentes :
1 In [5]: me = User.objects.filter(username="ÿnérant").get()
2 In [6]: me = User.objects.get(username="ÿnérant")
3 In [7]: me.email
4 Out[7]: 'ynerant@crans.org'
5
6 Les relations s'utilisent également sans problème :
7
8 {{{#!highlight python
9 In [8]: for alias in me.note.alias.filter(name__icontains="z").all():
10 ...: print(f"{alias.id}\t{alias.name}\t{alias.normalized_name}")
11 Out[8]: …
Django se charge derrière de comprendre tout ce que vous vouliez dire et de faire les bonnes requêtes SQL, en transformant les résultats en objets Python souhaités. C'est particulièrement pratique, puissant et plutôt intuitif.
Exercice : préparer le bilan financier
Enfin, si jamais vous étiez amenés à modifier des objets, vous pouvez les modifier comme si c'étaient de vrais objets Python. La fonction save() enregistrera les données dans la base de données.
Bien sûr, évitez de préférence la modification d'objets dans un terminal, ne le faites qu'en cas de besoin !
Exercice : supprimer tous les droits de super-utilisateur, et auto-nommez-vous super-utilisateur.
Exemples de requêtes utiles pour les trésoriers
Récupérer l'ensemble des transactions valides d'un jour donné
Example de sortie :
plop, BDE, 1.10, Coca toto, BDE, 2.10, Kwak toto, plop, 10, Pizza BDE, plop, 52.34, Courses Total : 65.54
Classer les M boutons les plus cliqués les N derniers jours
1 from datetime import timedelta
2
3 templates = RecurrentTransaction.objects.filter(
4 template__display=True,
5 valid=True,
6 created_at__gte=timezone.now() - timedelta(days=N),
7 ).values("template").annotate(transaction_count=Count("template")) \
8 .order_by("transaction_count")[:M].all()
9
10 for obj in templates:
11 template = TransactionTemplate.objects.get(pk=obj["template"])
12 print(template, obj["transaction_count"])
Exemple de sortie :
Coca 68 Banane ENSC 37 Sirop 2
TODO : Avoir de l'inspiration et de la motivation
via un service LDAP, installé sur bde-note (2)