July 7 - Implement notification per contract
This commit is contained in:
+136
-3
@@ -598,17 +598,150 @@ def notify_by_matrix(
|
||||
)
|
||||
notified.add(user.id)
|
||||
|
||||
# ── Custom email recipients ───────────────────────────────────────────
|
||||
# ── Custom email recipients (global, per-event) ───────────────────────
|
||||
custom_emails = get_custom_emails_for(event_type)
|
||||
sent_emails = set() # dedupe email sends across global + per-contract
|
||||
for email in custom_emails:
|
||||
norm = (email or '').strip().lower()
|
||||
if not norm or norm in sent_emails:
|
||||
continue
|
||||
_send_custom_email(email, title, body, link)
|
||||
sent_emails.add(norm)
|
||||
|
||||
# ── Per-contract additional recipients ────────────────────────────────
|
||||
# Extra users/emails attached to the contract that owns this event's
|
||||
# facility. Fires in ADDITION to the global matrix routing above.
|
||||
contract_count = _notify_contract_recipients(
|
||||
event_type = event_type,
|
||||
title = title,
|
||||
body = body,
|
||||
link = link,
|
||||
issue_id = issue_id,
|
||||
inspection_id = inspection_id,
|
||||
facility_id = facility_id,
|
||||
exclude = exclude,
|
||||
notified = notified,
|
||||
sent_emails = sent_emails,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
'MATRIX NOTIFY | event=%s | notified=%s | custom_emails=%s',
|
||||
event_type, len(notified), len(custom_emails),
|
||||
'MATRIX NOTIFY | event=%s | notified=%s | custom_emails=%s | contract_extra=%s',
|
||||
event_type, len(notified), len(sent_emails), contract_count,
|
||||
)
|
||||
|
||||
|
||||
def _resolve_project_id(facility_id=None, issue_id=None, inspection_id=None):
|
||||
"""Best-effort resolution of the owning contract (project) id for an event.
|
||||
|
||||
Tries facility_id first, then the issue's facility, then the inspection's
|
||||
facility. Returns None if no contract can be determined.
|
||||
"""
|
||||
from app.models.facility import Facility
|
||||
|
||||
fid = facility_id
|
||||
try:
|
||||
if fid is None and issue_id:
|
||||
from app.models.issue import Issue
|
||||
iss = db.session.get(Issue, issue_id)
|
||||
if iss is not None:
|
||||
fid = iss.facility_id
|
||||
if fid is None and iss.area is not None:
|
||||
fid = iss.area.facility_id
|
||||
if fid is None and inspection_id:
|
||||
from app.models.inspection import Inspection
|
||||
insp = db.session.get(Inspection, inspection_id)
|
||||
if insp is not None:
|
||||
fid = insp.facility_id
|
||||
if fid is None:
|
||||
return None
|
||||
facility = db.session.get(Facility, fid)
|
||||
return facility.project_id if facility is not None else None
|
||||
except Exception as exc:
|
||||
logger.error('CONTRACT NOTIFY | project resolution failed | error=%s', exc)
|
||||
return None
|
||||
|
||||
|
||||
def _notify_contract_recipients(
|
||||
event_type, title, body, link=None,
|
||||
issue_id=None, inspection_id=None, facility_id=None,
|
||||
exclude=None, notified=None, sent_emails=None,
|
||||
):
|
||||
"""Notify a contract's additional recipients for this event.
|
||||
|
||||
Deduplicates against the users already notified by the matrix (`notified`),
|
||||
the excluded actor set (`exclude`), and emails already sent (`sent_emails`).
|
||||
Best-effort: never raises into the caller.
|
||||
|
||||
Returns the number of extra recipients notified (users + emails).
|
||||
"""
|
||||
exclude = exclude if exclude is not None else set()
|
||||
notified = notified if notified is not None else set()
|
||||
sent_emails = sent_emails if sent_emails is not None else set()
|
||||
|
||||
try:
|
||||
from app.models.notification_recipient import ContractNotificationRecipient
|
||||
from app.models.user import User
|
||||
|
||||
project_id = _resolve_project_id(facility_id, issue_id, inspection_id)
|
||||
if project_id is None:
|
||||
return 0
|
||||
|
||||
rows = ContractNotificationRecipient.query.filter_by(project_id=project_id).all()
|
||||
count = 0
|
||||
|
||||
for r in rows:
|
||||
# Event subscription check
|
||||
if event_type not in r.get_event_types():
|
||||
continue
|
||||
|
||||
# ── Existing-user recipient → in-app + email ──
|
||||
if r.user_id:
|
||||
if r.user_id in exclude or r.user_id in notified:
|
||||
continue
|
||||
user = db.session.get(User, r.user_id)
|
||||
if not user or not user.active:
|
||||
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,
|
||||
respect_preferences = False, # contract config is authoritative
|
||||
)
|
||||
notified.add(user.id)
|
||||
count += 1
|
||||
logger.info(
|
||||
'CONTRACT NOTIFY | project=%s | event=%s | user=%s',
|
||||
project_id, event_type, user.username,
|
||||
)
|
||||
|
||||
# ── Free-form email recipient → email only ──
|
||||
elif r.email:
|
||||
norm = r.email.strip().lower()
|
||||
if not norm or norm in sent_emails:
|
||||
continue
|
||||
_send_custom_email(r.email, title, body, link)
|
||||
sent_emails.add(norm)
|
||||
count += 1
|
||||
logger.info(
|
||||
'CONTRACT NOTIFY | project=%s | event=%s | email=%s',
|
||||
project_id, event_type, r.email,
|
||||
)
|
||||
|
||||
return count
|
||||
|
||||
except Exception as exc:
|
||||
logger.error(
|
||||
'CONTRACT NOTIFY | unexpected error | event=%s | error=%s',
|
||||
event_type, exc,
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def _send_custom_email(to_email: str, title: str, body: str, link: str = None):
|
||||
"""Send a plain email to a custom (non-user) address. Best-effort."""
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user