feat: throttle no-change notifications daily

This commit is contained in:
2026-07-27 09:06:06 +02:00
parent e666571023
commit 9126d2016a
6 changed files with 94 additions and 5 deletions
+1
View File
@@ -1,5 +1,6 @@
/ticketing.ini /ticketing.ini
/tickets_state.json /tickets_state.json
/no_change_notification_state.json
/WAPT/certificate.crt /WAPT/certificate.crt
/WAPT/manifest.sha256 /WAPT/manifest.sha256
/WAPT/signature.sha256 /WAPT/signature.sha256
+8
View File
@@ -97,6 +97,8 @@ Hors WAPT, le script ne peut pas etre lance tel quel sans fournir les objets et
Le runtime lit et ecrit ce fichier dans `WAPT.private_dir`. Il n'est pas versionne dans le depot. Les anciens etats au format simple `ticket_id -> write_date` restent lisibles. Le runtime lit et ecrit ce fichier dans `WAPT.private_dir`. Il n'est pas versionne dans le depot. Les anciens etats au format simple `ticket_id -> write_date` restent lisibles.
`no_change_notification_state.json` est aussi stocke dans `WAPT.private_dir`. Il garde la date de derniere notification "aucun changement" afin de ne l'envoyer qu'une fois par jour.
La cle API est utilisee comme mot de passe d'API Odoo pour authentifier `username`. Il est aussi possible de renseigner `uid` dans la section `[odoo]` pour eviter l'appel d'authentification. La cle API est utilisee comme mot de passe d'API Odoo pour authentifier `username`. Il est aussi possible de renseigner `uid` dans la section `[odoo]` pour eviter l'appel d'authentification.
`odoo.fields` controle les champs recuperes depuis Odoo pour enrichir la notification. Les champs `id` et `write_date` sont toujours ajoutes s'ils sont absents, car ils sont necessaires a la comparaison d'etat. `odoo.fields` controle les champs recuperes depuis Odoo pour enrichir la notification. Les champs `id` et `write_date` sont toujours ajoutes s'ils sont absents, car ils sont necessaires a la comparaison d'etat.
@@ -158,6 +160,12 @@ Les tickets suivants ont ete mis a jour depuis la derniere verification :
251 : Sujet : Nouveau ticket gere par Personne : [cliquer ici pour ouvrir le ticket](https://odoo.example.test/web#id=251&model=helpdesk.ticket&view_type=form) 251 : Sujet : Nouveau ticket gere par Personne : [cliquer ici pour ouvrir le ticket](https://odoo.example.test/web#id=251&model=helpdesk.ticket&view_type=form)
``` ```
Quand aucun ticket ne change, Rocket.Chat recoit au maximum une notification par jour :
```text
Aucun ticket n'a ete mis a jour depuis la derniere verification.
```
## Tests ## Tests
Les tests unitaires peuvent etre executes hors WAPT : Les tests unitaires peuvent etre executes hors WAPT :
+8
View File
@@ -151,3 +151,11 @@ Ce fichier consigne les interventions Codex sur le projet afin de garder une tra
- L'etat local stocke maintenant `write_date` du ticket et le dernier marqueur de message chatter connu. - L'etat local stocke maintenant `write_date` du ticket et le dernier marqueur de message chatter connu.
- Ajout de `odoo.chatter_model = mail.message` dans le modele de configuration. - Ajout de `odoo.chatter_model = mail.message` dans le modele de configuration.
- Incrementation de la version WAPT de `0.1-17` a `0.1-18`. - Incrementation de la version WAPT de `0.1-17` a `0.1-18`.
### Notification quotidienne sans changement
- Ajout d'un etat `no_change_notification_state.json` dans `WAPT.private_dir`.
- La notification "aucun ticket mis a jour" est envoyee une seule fois par jour.
- Les notifications de vrais changements restent envoyees a chaque detection.
- Ajout de tests unitaires sur l'envoi unique par jour.
- Incrementation de la version WAPT de `0.1-19` a `0.1-20`.
+4 -4
View File
@@ -1,5 +1,5 @@
package : comi-odoo-ticketing package : comi-odoo-ticketing
version : 0.1-18 version : 0.1-20
architecture : all architecture : all
section : base section : base
priority : optional priority : optional
@@ -29,7 +29,7 @@ editor :
keywords : keywords :
licence : licence :
homepage : homepage :
package_uuid : f974e9d0-c356-77d6-4e13-273510424e1f package_uuid : 2456a8e9-6f8c-ea82-c058-e52636509fec
valid_from : valid_from :
valid_until : valid_until :
forced_install_on : forced_install_on :
@@ -39,6 +39,6 @@ max_os_version :
icon_sha256sum : icon_sha256sum :
signer : pcosson_key signer : pcosson_key
signer_fingerprint: a25582410cf03bad179a60c189f459a0b03821c92c0cedf209e82448a66a9b4e signer_fingerprint: a25582410cf03bad179a60c189f459a0b03821c92c0cedf209e82448a66a9b4e
signature_date : 2026-07-24T08:37:11.000000 signature_date : 2026-07-24T08:45:27.000000
signed_attributes : package,version,architecture,section,priority,name,categories,maintainer,description,depends,conflicts,maturity,locale,target_os,min_wapt_version,sources,installed_size,impacted_process,description_fr,description_pl,description_de,description_es,description_pt,description_it,description_nl,description_ru,audit_schedule,editor,keywords,licence,homepage,package_uuid,valid_from,valid_until,forced_install_on,changelog,min_os_version,max_os_version,icon_sha256sum,signer,signer_fingerprint,signature_date,signed_attributes signed_attributes : package,version,architecture,section,priority,name,categories,maintainer,description,depends,conflicts,maturity,locale,target_os,min_wapt_version,sources,installed_size,impacted_process,description_fr,description_pl,description_de,description_es,description_pt,description_it,description_nl,description_ru,audit_schedule,editor,keywords,licence,homepage,package_uuid,valid_from,valid_until,forced_install_on,changelog,min_os_version,max_os_version,icon_sha256sum,signer,signer_fingerprint,signature_date,signed_attributes
signature : o1xwbr7V5kTvRJ7GrHHWd0xzEjrPovHQjVcEOj4N7j56/m0MaiwScoBAYJ+CaNEfiP2MuMPGqJz+WuYLZrKZvXNZhTfP0fdP6ligApowHvjUJEdwJ/cpzwKcXd/yD8Q0bLLChNrFFQz8wRoaBNsvmcJCPfPp353aNOemKe8+JubgAZTdF4VdyxMbglMDBRy4ECnK5YNpZWYFzHEamVWyoQ3LTo92UHL/PdZrvefQ88MOfGH055npv1YdR/AD235BrjDm1AD0CqIMMe1UnFD41XcBwB9mxD7Wh0jLmzDqRCnCbzfuq98VgNp0L1PZQJXgeu1tVrG9ycGrexdQrDWPzA== signature : TonQ2rHBMLmtP8I6ZgJDESg9TSpyZ42wwpkq34ii3IDvk4XMeuFUFnrDRazrGAAqVn+UVCpoRw+Ne19UOeM/Z/csqSYKYqmdfQpGwEmHjsoAb5A6+oSTDVHa+I6+Pv2iI+6hXAnMediGPfT7y10i+OGCqeKJQxNzsZW5MR+9GjgJQE7YN6lFJ/hDD7v/NG/ry5YL2cv0xEDmqgd5dThPmfHGSOxiqyjZWOHhW4kYrl7HjSHRG4ewdXoUELW4Eo+gNxZnLI8GOt8xVWBu625DnLVaxrhFQ59d4mcQA0xVy3TdxmLL2XiDJ9Cj0zUrNrw+MyD5UUjr/PvTv7X1mnhF/Q==
+44 -1
View File
@@ -4,6 +4,7 @@ import json
import os import os
import shutil import shutil
from configparser import ConfigParser from configparser import ConfigParser
from datetime import date
import requests import requests
@@ -11,6 +12,7 @@ import requests
CONFIG_FILE = "ticketing.ini" CONFIG_FILE = "ticketing.ini"
CONFIG_TEMPLATE_FILE = "ticketing.ini.example" CONFIG_TEMPLATE_FILE = "ticketing.ini.example"
STATE_FILE = "tickets_state.json" STATE_FILE = "tickets_state.json"
NO_CHANGE_STATE_FILE = "no_change_notification_state.json"
DEFAULT_TIMEOUT = 30 DEFAULT_TIMEOUT = 30
DEFAULT_TICKET_FIELDS = ["id", "name", "stage_id", "partner_id", "user_id", "write_date"] DEFAULT_TICKET_FIELDS = ["id", "name", "stage_id", "partner_id", "user_id", "write_date"]
DEFAULT_MAX_NOTIFIED_TICKETS = 50 DEFAULT_MAX_NOTIFIED_TICKETS = 50
@@ -29,6 +31,10 @@ def get_state_path():
return makepath(WAPT.private_dir, STATE_FILE) return makepath(WAPT.private_dir, STATE_FILE)
def get_no_change_state_path():
return makepath(WAPT.private_dir, NO_CHANGE_STATE_FILE)
def install(): def install():
config_path = get_config_path() config_path = get_config_path()
@@ -53,7 +59,13 @@ def audit():
conf_wapt.get("odoo", "model", fallback="helpdesk.ticket"), conf_wapt.get("odoo", "model", fallback="helpdesk.ticket"),
) )
send_to_rocket(message, conf_wapt=conf_wapt) if should_send_notification(tickets_updated, get_no_change_state_path()):
send_to_rocket(message, conf_wapt=conf_wapt)
if not tickets_updated:
record_no_change_notification(get_no_change_state_path())
else:
print("Notification aucun changement deja envoyee aujourd'hui.")
save_tickets_state(get_state_path(), build_tickets_state(tickets)) save_tickets_state(get_state_path(), build_tickets_state(tickets))
return "OK" return "OK"
@@ -284,6 +296,37 @@ def save_tickets_state(state_path, tickets_state):
json.dump(tickets_state, state_file, indent=2, sort_keys=True) json.dump(tickets_state, state_file, indent=2, sort_keys=True)
def get_today():
return date.today().isoformat()
def load_json_state(state_path):
if not os.path.exists(state_path):
return {}
with open(state_path, "r") as state_file:
return json.load(state_file)
def save_json_state(state_path, state):
with open(state_path, "w") as state_file:
json.dump(state, state_file, indent=2, sort_keys=True)
def should_send_notification(tickets_updated, no_change_state_path):
if tickets_updated:
return True
no_change_state = load_json_state(no_change_state_path)
return no_change_state.get("last_no_change_notification_date") != get_today()
def record_no_change_notification(no_change_state_path):
save_json_state(no_change_state_path, {
"last_no_change_notification_date": get_today(),
})
def build_tickets_state(tickets): def build_tickets_state(tickets):
return { return {
str(ticket["id"]): build_ticket_state_value(ticket) str(ticket["id"]): build_ticket_state_value(ticket)
+29
View File
@@ -203,6 +203,35 @@ class TicketingStateTest(unittest.TestCase):
self.assertEqual(ticketing.load_tickets_state(state_path), expected_state) self.assertEqual(ticketing.load_tickets_state(state_path), expected_state)
def test_should_send_notification_always_sends_when_tickets_changed(self):
with tempfile.TemporaryDirectory() as tmpdir:
state_path = os.path.join(tmpdir, "no_change_notification_state.json")
ticket = {"id": 42, "write_date": "2026-07-24 12:00:00"}
self.assertTrue(ticketing.should_send_notification([ticket], state_path))
@patch("ticketing_setup.get_today", return_value="2026-07-27")
def test_should_send_no_change_notification_once_per_day(self, get_today):
with tempfile.TemporaryDirectory() as tmpdir:
state_path = os.path.join(tmpdir, "no_change_notification_state.json")
self.assertTrue(ticketing.should_send_notification([], state_path))
ticketing.record_no_change_notification(state_path)
self.assertFalse(ticketing.should_send_notification([], state_path))
get_today.assert_called()
@patch("ticketing_setup.get_today", return_value="2026-07-28")
def test_should_send_no_change_notification_again_next_day(self, get_today):
with tempfile.TemporaryDirectory() as tmpdir:
state_path = os.path.join(tmpdir, "no_change_notification_state.json")
ticketing.save_json_state(state_path, {
"last_no_change_notification_date": "2026-07-27",
})
self.assertTrue(ticketing.should_send_notification([], state_path))
@patch("ticketing_setup.requests.post") @patch("ticketing_setup.requests.post")
def test_fetch_odoo_tickets_authenticates_and_returns_result(self, post): def test_fetch_odoo_tickets_authenticates_and_returns_result(self, post):
auth_response = Mock() auth_response = Mock()