11. Réagir
Jusqu’ici le module répondait à quelqu’un. Il va maintenant agir tout seul : tous les matins, il relance les emprunts en retard.
Un seul traitement, cinq sous-systèmes du noyau, et l’ordre compte.
Ce qu’il fait
def sweep(engine, connection, loans, *, librarian_id, today=None, language="en") -> int: """Remind for every overdue loan. Returns how many reminders were sent.""" moment = today or datetime.now(UTC).date() late = overdue_on(loans, today=moment) for loan in late: remind(engine, connection, loan, librarian_id=librarian_id, language=language) return len(late)Et pour chaque emprunt en retard : un courriel à l’adhérent, une notification au bibliothécaire, un événement pour le reste du monde.
La planification
Le module déclare son travail périodique. Il ne le planifie pas lui-même.
from kewos.scheduler import register_cron_seed
register_cron_seed( name="library_overdue_sweep", callable_path="modules.library.overdue:run_overdue_sweep", cron_expr="0 7 * * *",)Le noyau sème la tâche sur chaque installation, et un exploitant peut la désactiver chez un client sans toucher au code. L’expression est validée à l’enregistrement : une expression fausse échoue au démarrage, pas six mois plus tard à 7 h du matin.
Le courriel
Déposez mail_templates/library_overdue.html. Une langue par fichier :
<nom>.html pour l’anglais, <nom>.<langue>.html pour les autres.
{% extends "kewos:mail_base" %}{% block title %}Overdue loan{% endblock %}{% block description %}Sent to a member whose loan is past its due date.{% endblock %}{% block subject %}Your loan of {{ title }} is overdue{% endblock %}{% block body %}<h1>Your loan is overdue</h1><p>Hello {{ member_name }},</p><p> <strong>{{ title }}</strong> ({{ barcode }}) was due back on <strong>{{ due_on }}</strong>, {{ days_late }} day(s) ago.</p>{% endblock %}{% endblock %}Une ligne dans le module :
from kewos.mail import register_mail_templates_from_dir
register_mail_templates_from_dir(_MODULE_DIR / "mail_templates")Et l’envoi :
send_template_mail( engine, connection, # the CALLER's connection, not the engine template="library_overdue", to_address=loan.member_email, language=language, context={"member_name": ..., "title": ..., "days_late": ...}, model="library_loan", record_id=str(loan.loan_id),){% raw %}{{ mail_company }}{% endraw %}, l’en-tête et la signature viennent du thème du
client : {% raw %}{% extends "kewos:mail_base" %}{% endraw %} les apporte.
La notification dans l’application
from kewos.notifications import NotificationKind, notify_user
notify_user( connection, # again: the caller's connection user_id=librarian_id, kind=NotificationKind.SYSTEM, payload={ "model": "library_loan", "record_id": loan.loan_id, "message_key": "library_loan.notification.overdue", "title": loan.title, "days_late": loan.days_late, },)La cloche de l’interface se remplit toute seule ; vous n’écrivez aucun écran.
Quatre natures existent : MENTION, COMMENT, STATE_CHANGE et SYSTEM. Une relance
automatique est un SYSTEM : personne ne l’a écrite à la main.
L’événement
from kewos.events import emit
emit("library.loan.overdue", {"loan_id": ..., "member_id": ..., "days_late": ...})C’est ainsi qu’un autre module réagit sans que celui-ci sache qu’il existe. Un module de recouvrement, un tableau de bord de direction, un connecteur SMS : chacun s’abonne, aucun n’est nommé ici.
from kewos.events import event_subscriber
@event_subscriber("library.loan.overdue")def warn_the_desk(event) -> None: """React to a loan going overdue, from another module entirely.""" ...Un abonné synchrone s’exécute dans la transaction et peut la faire échouer. Un abonné asynchrone part sur la file et échoue tout seul, avec ses propres reprises. Choisissez le second dès que l’échec de l’abonné ne doit pas annuler l’action.
La numérotation
Le numéro de carte d’un adhérent n’est pas une séquence de base de données.
from kewos.sequences import register_sequence_seed
register_sequence_seed(code="library_member_card", prefix="LIB-", padding=6)Puis, au moment de créer l’adhérent :
from kewos.sequences import next_value
card = next_value(connection, code="library_member_card") # "LIB-000042"Ce que les tests attrapent
Le rappel lui-même est trois appels au noyau. Ce qui mérite un test, c’est ce qui compte comme en retard :
def test_a_loan_due_today_is_not_late() -> None: # The most common off-by-one in this kind of sweep, and the one that emails # somebody the morning their book is due. assert overdue_on([_Loan(id=1, state="out", due_on=TODAY)], today=TODAY) == ()Un emprunt rendu, un emprunt perdu, un emprunt sans date de retour : trois cas où relancer serait une faute, et trois tests. Le calcul est pur, donc tout cela se vérifie sans base de données.