Aug 18 - Fix customer roles's notification radio buttons issue
This commit is contained in:
@@ -66,6 +66,28 @@ def overrides_for_user(user_id) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def override_for(user_id, event_type):
|
||||
"""One account's answer for one event: True, False, or None (inherit).
|
||||
|
||||
Used by notify() to enforce the override on EVERY delivery path, not just
|
||||
matrix broadcasts. Best-effort: any failure returns None (inherit), so a
|
||||
lookup problem can never silently swallow a notification.
|
||||
"""
|
||||
import logging
|
||||
if not user_id or not event_type:
|
||||
return None
|
||||
try:
|
||||
row = UserNotificationMatrix.query.filter_by(
|
||||
user_id=user_id, event_type=event_type).first()
|
||||
return row.enabled if row is not None else None
|
||||
except Exception as exc:
|
||||
logging.getLogger(__name__).error(
|
||||
'USER MATRIX | single override lookup failed | user=%s event=%s | %s',
|
||||
user_id, event_type, exc,
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def overrides_for_event(event_type) -> dict:
|
||||
"""Return {user_id: bool} — every account's override for one event.
|
||||
|
||||
|
||||
@@ -13,15 +13,31 @@
|
||||
/* Minimum 44px touch targets on interactive elements */
|
||||
.btn,
|
||||
.nav-link,
|
||||
.dropdown-item,
|
||||
input[type="checkbox"],
|
||||
input[type="radio"],
|
||||
.form-check-input {
|
||||
.dropdown-item {
|
||||
min-height: 44px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Checkboxes and radios are deliberately NOT in the rule above.
|
||||
`display: inline-flex` on a native <input type="radio"> replaces its
|
||||
intrinsic box with a flex container: the control keeps painting its glyph
|
||||
at its natural ~16px size while the element claims a 44px-tall box, so the
|
||||
visible dot and the actual hit area stop coinciding. Taps land next to the
|
||||
control and nothing happens — which is exactly how the per-account
|
||||
notification matrix (Inherit / On / Off) came to look unclickable.
|
||||
|
||||
Grow the target without touching `display`: keep the native box, scale the
|
||||
glyph up, and give it margin so neighbouring options stay separable. Any
|
||||
control that needs a genuinely large tap area should wrap the input in a
|
||||
<label> that fills the cell (see customers/manage.html). */
|
||||
input[type="checkbox"],
|
||||
input[type="radio"],
|
||||
.form-check-input {
|
||||
transform: scale(1.35);
|
||||
margin: 6px;
|
||||
}
|
||||
|
||||
/* Slightly larger form controls for finger input */
|
||||
.form-control,
|
||||
.form-select {
|
||||
|
||||
@@ -318,6 +318,34 @@
|
||||
action="{{ url_for('customers.save_notifications', customer_id=customer.id) }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
<style>
|
||||
/* The whole cell is the control. Padding (not min-height on the
|
||||
input) gives the touch target, so the native radio keeps its
|
||||
own box — see the note in ipad_responsive.css. */
|
||||
.matrix-opt {
|
||||
display: block;
|
||||
padding: .55rem .25rem;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
}
|
||||
.matrix-opt:hover { background: rgba(13,110,253,.06); }
|
||||
.matrix-opt input { cursor: pointer; }
|
||||
.matrix-opt-sub {
|
||||
display: block;
|
||||
font-size: .62rem;
|
||||
color: #6c757d;
|
||||
margin-top: 2px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="d-flex flex-wrap align-items-center gap-2 mb-2">
|
||||
<span class="small text-muted">Set every row:</span>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" data-matrix-all="inherit">Inherit</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-success" data-matrix-all="on">On</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" data-matrix-all="off">Off</button>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle mb-3">
|
||||
<thead class="table-light">
|
||||
@@ -337,23 +365,33 @@
|
||||
<span class="badge bg-warning text-dark ms-1" style="font-size:.6rem;">custom</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<input class="form-check-input" type="radio"
|
||||
name="event_{{ row.event }}" value="inherit"
|
||||
{% if row.override is none %}checked{% endif %}>
|
||||
<div class="text-muted" style="font-size:.62rem;">
|
||||
{{ 'on' if row.global else 'off' }}
|
||||
</div>
|
||||
{# Each option is a <label> filling its whole cell, so the
|
||||
click target is the cell rather than the ~16px glyph. A
|
||||
bare <input> in a centred <td> was effectively unclickable
|
||||
at touch/narrow widths. #}
|
||||
<td class="p-0">
|
||||
<label class="matrix-opt" title="Follow the global matrix">
|
||||
<input class="form-check-input" type="radio"
|
||||
name="event_{{ row.event }}" value="inherit"
|
||||
{% if row.override is none %}checked{% endif %}>
|
||||
<span class="matrix-opt-sub">
|
||||
currently {{ 'on' if row.global else 'off' }}
|
||||
</span>
|
||||
</label>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<input class="form-check-input" type="radio"
|
||||
name="event_{{ row.event }}" value="on"
|
||||
{% if row.override is true %}checked{% endif %}>
|
||||
<td class="p-0">
|
||||
<label class="matrix-opt" title="Always send this to this account">
|
||||
<input class="form-check-input" type="radio"
|
||||
name="event_{{ row.event }}" value="on"
|
||||
{% if row.override is true %}checked{% endif %}>
|
||||
</label>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<input class="form-check-input" type="radio"
|
||||
name="event_{{ row.event }}" value="off"
|
||||
{% if row.override is false %}checked{% endif %}>
|
||||
<td class="p-0">
|
||||
<label class="matrix-opt" title="Never send this to this account">
|
||||
<input class="form-check-input" type="radio"
|
||||
name="event_{{ row.event }}" value="off"
|
||||
{% if row.override is false %}checked{% endif %}>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@@ -396,6 +434,16 @@
|
||||
if (deselectAll) deselectAll.addEventListener('click', function () { setAll(false); });
|
||||
checks.forEach(function (c) { c.addEventListener('change', refreshCount); });
|
||||
|
||||
// ── Notification matrix: set every row at once ──
|
||||
document.querySelectorAll('[data-matrix-all]').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
var want = btn.getAttribute('data-matrix-all');
|
||||
document.querySelectorAll('.matrix-opt input[type=radio]').forEach(function (r) {
|
||||
if (r.value === want) { r.checked = true; }
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ── Customer Director: contract → facility cascade ──
|
||||
// Both selects are absent on the inspector view, so bail out rather than
|
||||
// throwing on addEventListener of null (which would kill the handlers above).
|
||||
|
||||
@@ -209,6 +209,28 @@ def notify(
|
||||
the scheduled-inspection "Confirm receipt" email link. In-app
|
||||
notifications are unaffected — this only shapes the email.
|
||||
"""
|
||||
# ── 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 = (
|
||||
|
||||
Reference in New Issue
Block a user