Dodock intègre un serveur Socket.IO écrit en Python qui permet d'envoyer des événements en temps réel à un ou plusieurs utilisateurs connectés. Ce serveur remplace progressivement l'ancien serveur Node.js : il s'exécute directement sous le processus Gunicorn, ce qui réduit la consommation mémoire (~45 Mo économisés) et simplifie l'architecture.
Compatibilité : le serveur Node.js reste disponible en parallèle pendant la période de transition. Le serveur Python est activé via la configuration du site.
frappe.publish_realtime().Le serveur Python Socket.IO se configure dans common_site_config.json :
{
"socketio_port": 9000,
"use_python_socketio": true
}
| Clé | Valeur par défaut | Description |
|---|---|---|
socketio_port | 9000 | Port d'écoute du serveur Socket.IO |
use_python_socketio | false | Active le serveur Python (désactive Node.js) |
frappe.publish_realtime()C'est la fonction principale pour envoyer un événement depuis n'importe quel code Python (contrôleur, tâche planifiée, hook…).
frappe.publish_realtime(
event="mon_evenement",
message={"statut": "terminé", "avancement": 100},
user="utilisateur@example.com", # ou room=...
)
Paramètres :
| Paramètre | Type | Description |
|---|---|---|
event | str | Nom de l'événement Socket.IO |
message | dict | Données envoyées au client |
user | str | None | Envoie à un utilisateur précis |
room | str | None | Envoie à une room Socket.IO |
doctype | str | None | Cible les abonnés au document |
docname | str | None | Identifiant du document |
task_id | str | None | Lié à une tâche de fond |
after_commit | bool | Si True, publie après le commit BDD |
Pour les cas d'usage courants, des fonctions dédiées sont disponibles :
from frappe.realtime import (
publish_to_user,
publish_to_room,
publish_to_doctype,
publish_to_document,
publish_to_task,
publish_to_all,
)
# Vers un utilisateur spécifique
publish_to_user("admin@example.com", "alerte", {"texte": "Nouveau message"})
# Vers tous les abonnés d'un document
publish_to_document("Project", "PROJ-0001", "mise_a_jour", {"avancement": 75})
# Vers tous les utilisateurs connectés
publish_to_all("maintenance", {"message": "Redémarrage dans 5 minutes"})
Vous pouvez réagir à des événements Socket.IO envoyés par le navigateur en définissant des handlers dans votre application.
@realtime.on# monapp/realtime.py
import frappe
from frappe.realtime import Socket, realtime
@realtime.on("project_subscribe")
def project_subscribe(socket: Socket, project: str) -> None:
"""L'utilisateur demande à rejoindre la room d'un projet."""
if socket.has_permission("Project", project):
socket.join(f"project:{project}")
Le décorateur @realtime.on("<nom_evenement>") enregistre automatiquement la fonction dans le registre de Dodock au démarrage du serveur. Aucune configuration supplémentaire n'est nécessaire.
Dodock découvre les handlers en cherchant le module <monapp>.realtime dans chaque application installée. Il suffit de créer le fichier monapp/realtime.py contenant vos handlers décorés.
@realtime.on(
"mon_evenement",
frappe_context=True, # Ouvre un contexte Frappe (BDD + session) — défaut : True
allow_guest=False, # Si False, refuse les sockets non authentifiés — défaut : False
)
def mon_handler(socket: Socket, **data) -> None:
...
| Option | Type | Défaut | Description |
|---|---|---|---|
frappe_context | bool | True | Initialise frappe.init() et ouvre la BDD avant d'appeler le handler |
allow_guest | bool | False | Autorise les connexions de type Invité (Guest) |
Attention : si
frappe_context=True, le handler dispose d'un accès complet àfrappe.db,frappe.get_doc(), etc. Sifrappe_context=False, ces appels échoueront — à utiliser uniquement pour des handlers purement temps réel sans accès BDD.
SocketLe premier argument de chaque handler est un objet Socket qui représente la connexion du client.
from frappe.realtime import Socket
def mon_handler(socket: Socket, **data) -> None:
# Propriétés
socket.user # str : nom d'utilisateur (ex. "admin@example.com")
socket.site # str : nom du site Frappe
socket.sid # str : identifiant de session Socket.IO
# Méthodes
socket.join("ma_room") # Rejoindre une room
socket.leave("ma_room") # Quitter une room
socket.emit("evenement", {}) # Envoyer un événement à CE client
socket.is_connected() # bool : le socket est-il encore connecté ?
# Vérification de permission
socket.has_permission(
doctype="Project",
docname="PROJ-0001",
ptype="read", # "read", "write", "create", "delete"…
)
socket.has_permission() délègue la vérification au processus web via une requête HTTP interne, ce qui garantit que les règles de permissions Dodock standard sont respectées.
@realtime.on("document_watch")
def document_watch(socket: Socket, doctype: str, docname: str) -> None:
if socket.has_permission(doctype, docname, ptype="read"):
socket.join(f"{doctype}::{docname}")
else:
socket.emit("error", {"message": "Accès refusé"})
Scénario : les utilisateurs s'abonnent à un projet et reçoivent des mises à jour en temps réel quand l'avancement change.
monapp/realtime.py — handler côté serveur :
import frappe
from frappe.realtime import Socket, realtime, publish_to_room
@realtime.on("project_subscribe")
def project_subscribe(socket: Socket, project: str) -> None:
"""Abonne le client aux mises à jour du projet."""
if socket.has_permission("Project", project, ptype="read"):
socket.join(f"project:{project}")
socket.emit("project_subscribed", {"project": project})
@realtime.on("project_unsubscribe")
def project_unsubscribe(socket: Socket, project: str) -> None:
"""Désabonne le client."""
socket.leave(f"project:{project}")
monapp/controllers/project.py — publication depuis un hook :
from frappe.realtime import publish_to_room
class Project(Document):
def on_update(self):
publish_to_room(
room=f"project:{self.name}",
event="project_updated",
message={
"name": self.name,
"status": self.status,
"percent_complete": self.percent_complete,
},
)
Côté navigateur (JavaScript) :
// Abonnement
frappe.realtime.emit("project_subscribe", { project: "PROJ-0001" });
// Réception des mises à jour
frappe.realtime.on("project_updated", (data) => {
console.log("Avancement :", data.percent_complete);
});
Host ou Origin valide est immédiatement rejetée.Guest. Passez allow_guest=True explicitement si votre cas d'usage le requiert.| Symptôme | Cause probable | Solution |
|---|---|---|
| Le handler n'est jamais appelé | Module monapp/realtime.py introuvable | Vérifiez que le fichier existe et que l'app est installée |
frappe.get_doc() lève une erreur | frappe_context=False | Passez frappe_context=True (défaut) |
| Connexion refusée | En-tête Origin absent | Vérifiez la configuration du proxy (Nginx) |
| Événements non reçus | Serveur Python non activé | Ajoutez "use_python_socketio": true dans common_site_config.json |
use_python_socketio: true dans common_site_config.json et redémarrez le service frappe-socketio.