Aug 19 - Update code to catch up with ST

This commit is contained in:
2026-08-19 14:05:18 -04:00
parent c9984e7ae6
commit 12141c2f75
52 changed files with 3321 additions and 342 deletions
+70 -1
View File
@@ -214,6 +214,28 @@ def notify(
in-app Notification row is unchanged, so a recipient reading
it in the bell menu simply follows `link` as before.
"""
# ── Per-account override (phase51) ───────────────────────────────────
# A customer-side account's own notification matrix governs EVERY path
# that reaches it, not just matrix broadcasts: follower fan-out
# (_notify_followers) and direct assignee notifications both call notify()
# straight, so without this the editor would offer rows — "Issue follow
# update", "Issue assigned" — that appeared to be off while the
# notifications kept arriving.
#
# Only an explicit `False` suppresses. No row means inherit, which is the
# default for every account and leaves behaviour exactly as before. The
# getattr fallback is deliberate: if the attribute is unavailable for any
# reason we send, never silently drop.
if event_type and getattr(recipient, 'is_customer_account', False):
from app.models.user_notification_matrix import override_for
if override_for(recipient.id, event_type) is False:
logger.info(
'NOTIFICATION SUPPRESSED | user=%s | event=%s | '
'reason=per_account_override_off',
recipient.username, event_type,
)
return
# Determine digest flag before creating the record.
# Digest mode is only respected when individual preferences are in effect.
hold_for_digest = (
@@ -342,6 +364,7 @@ def notify_customers_for_facility(
link: str = None,
issue_id: int = None,
inspection_id: int = None,
allowed_user_ids: set = None,
):
"""Dispatch in-app + email notifications to all customer users assigned
to the given facility.
@@ -362,6 +385,12 @@ def notify_customers_for_facility(
link : Relative URL for 'View Details'.
issue_id : FK to issues.id (optional).
inspection_id : FK to inspections.id (optional).
allowed_user_ids :
Optional whitelist. When notify_by_matrix() calls this it has already
applied each account's per-event override (phase51), so it passes the
surviving ids here — this function re-derives recipients from the
assignment rows and would otherwise notify accounts that opted out.
None (the default, used by direct callers) means no filtering.
"""
try:
from app.models.project import CustomerAssignment
@@ -399,8 +428,20 @@ def notify_customers_for_facility(
)
return
if allowed_user_ids is not None:
notified_user_ids &= set(allowed_user_ids)
if not notified_user_ids:
logger.debug(
'notify_customers_for_facility | facility_id=%s | all '
'assigned customers filtered out by per-account overrides',
facility_id,
)
return
for user_id in notified_user_ids:
user = db.session.get(User, user_id)
# role != 'customer' stays an EQUALITY check: a Customer Inspector
# is not a portal customer and is routed by the inspector column.
if not user or not user.active or user.role != 'customer':
continue
try:
@@ -562,11 +603,20 @@ def notify_by_matrix(
from app.models.notification_matrix import (
is_enabled, get_custom_emails_for, MATRIX_ROLES,
)
from app.models.user_notification_matrix import overrides_for_event
from app.models.user import User
exclude = set(exclude_user_ids or [])
notified = set() # deduplicate across roles
# ── Per-account overrides (phase51) ───────────────────────────────────
# {user_id: bool} for this event, one query. Applies to the two
# customer-side role columns only; staff roles use the global matrix alone.
# An account with no entry inherits the global column, which is why this
# feature is a no-op until an admin actually sets something.
overrides = overrides_for_event(event_type)
customer_keys = User.CUSTOMER_ROLES # ('customer', 'external_inspector')
role_to_db = {
'admin': 'admin',
'director': 'director',
@@ -585,7 +635,14 @@ def notify_by_matrix(
enabled = is_enabled(event_type, role_key)
logger.info('MATRIX NOTIFY | event=%s | role=%s | enabled=%s',
event_type, role_key, enabled)
if not enabled:
# A customer-side column must NOT be skipped just because the global
# switch is off — an account that opted IN individually still has to be
# reached. Only skip when the column is off AND nobody opted in.
# (Getting this wrong is silent: the per-account "on" would save fine,
# show as on, and never send.)
is_customer_col = role_key in customer_keys
if not enabled and not (is_customer_col and any(overrides.values())):
continue
db_role = role_to_db.get(role_key)
@@ -615,6 +672,15 @@ def notify_by_matrix(
'submitting inspector_id=%s',
event_type, role_key, target_id)
# Apply the per-account overrides to the customer-side columns. An
# account with no override falls back to `enabled`, i.e. the global
# column — so this line is what makes both directions work: opt-in
# against an off column, and opt-out of an on one.
if is_customer_col:
users = [u for u in users if overrides.get(u.id, enabled)]
logger.info('MATRIX NOTIFY | event=%s | role=%s | after overrides=%s',
event_type, role_key, [u.username for u in users])
# Scope customer role to facility if provided
if role_key == 'customer' and facility_id:
from app.utils.notifications import notify_customers_for_facility
@@ -626,6 +692,9 @@ def notify_by_matrix(
link = link,
issue_id = issue_id,
inspection_id = inspection_id,
# Without this the facility-scoped path would re-query customers
# itself and bypass every override applied just above.
allowed_user_ids = {u.id for u in users},
)
continue # notify_customers_for_facility handles dedup internally