feat: throttle no-change notifications daily
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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 :
|
||||||
|
|||||||
@@ -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
@@ -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==
|
||||||
|
|||||||
@@ -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"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if should_send_notification(tickets_updated, get_no_change_state_path()):
|
||||||
send_to_rocket(message, conf_wapt=conf_wapt)
|
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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user