From 9733dab925bdf0fc2b4fceacc0ce19ac032f0601 Mon Sep 17 00:00:00 2001 From: NguyenND Date: Mon, 27 Apr 2026 17:40:55 -0400 Subject: [PATCH] 04/27 Fixed notification hasn't been sent when submitting inspection --- app/utils/notifications.py | 66 +++++++++++++++++++++++++------------- 1 file changed, 43 insertions(+), 23 deletions(-) diff --git a/app/utils/notifications.py b/app/utils/notifications.py index 83d4cfe..c5eff91 100644 --- a/app/utils/notifications.py +++ b/app/utils/notifications.py @@ -171,23 +171,33 @@ def notify( inspection_id: int = None, event_type: str = None, send_email: bool = True, + respect_preferences: bool = True, ): """Create an in-app Notification record and optionally send an email. Parameters ---------- - recipient : User ORM instance - title : Short notification headline - body : Full notification message - link : Relative URL for the 'View Details' button/link - issue_id : FK to issues.id (optional) - inspection_id: FK to inspections.id (optional) - event_type : One of the EVENT_* constants from models.notification - Used to look up the user's preference for this event. - send_email : Master switch — set False to suppress all email (overrides prefs) + recipient : User ORM instance + title : Short notification headline + body : Full notification message + link : Relative URL for the 'View Details' button/link + issue_id : FK to issues.id (optional) + inspection_id : FK to inspections.id (optional) + event_type : One of the EVENT_* constants from models.notification + Used to look up the user's preference for this event. + send_email : Master switch — set False to suppress all email (overrides prefs) + respect_preferences : When True (default), per-user email preferences gate delivery. + Set False for matrix-routed broadcasts — the matrix is the + authority; individual opt-out should not override admin config. """ - # Determine digest flag before creating the record - hold_for_digest = send_email and bool(event_type) and _digest_mode_for(recipient, event_type) + # Determine digest flag before creating the record. + # Digest mode is only respected when individual preferences are in effect. + hold_for_digest = ( + respect_preferences + and send_email + and bool(event_type) + and _digest_mode_for(recipient, event_type) + ) # ── 1. Persist in-app notification ────────────────────────────────────── notif = Notification( @@ -210,9 +220,18 @@ def notify( # ── 2. Send immediate email if applicable ──────────────────────────────── if send_email and not hold_for_digest: - should_send = ( - event_type is None or _email_enabled_for(recipient, event_type) - ) + if respect_preferences: + # Per-user opt-in/opt-out gate — used for direct notifications + # (issue assignment, SLA alerts, follower updates, etc.) + should_send = ( + event_type is None or _email_enabled_for(recipient, event_type) + ) + else: + # Matrix-routed broadcast — the admin matrix is the authority. + # Individual preference rows must not override it, otherwise an + # admin or director who once clicked "Pause All" would silently + # stop receiving inspection completion and other broadcast events. + should_send = True if should_send and recipient.email and current_app.config.get('MAIL_SERVER'): _send_single_email(recipient, title, body, link) @@ -547,14 +566,15 @@ def notify_by_matrix( if user.id in exclude or user.id in notified: continue notify( - recipient = user, - title = title, - body = body, - link = link, - issue_id = issue_id, - inspection_id = inspection_id, - event_type = event_type, - send_email = True, + recipient = user, + title = title, + body = body, + link = link, + issue_id = issue_id, + inspection_id = inspection_id, + event_type = event_type, + send_email = True, + respect_preferences = False, # matrix is the authority for broadcasts ) notified.add(user.id) @@ -607,4 +627,4 @@ def _send_custom_email(to_email: str, title: str, body: str, link: str = None): logger.error('CUSTOM EMAIL FAILED | to=%s | error=%s', to_email, exc) import threading - threading.Thread(target=_send, daemon=True).start() + threading.Thread(target=_send, daemon=True).start() \ No newline at end of file