July 7 - Implement notification per contract
This commit is contained in:
@@ -83,11 +83,37 @@ def view(project_id):
|
||||
.order_by(User.username)
|
||||
.all()
|
||||
)
|
||||
|
||||
# ── Additional notification recipients (admin-only panel) ──
|
||||
# Only loaded for admins — the panel is admin-gated in the template, and
|
||||
# the add/remove routes are @admin_required. No point querying otherwise.
|
||||
from app.models.notification_matrix import MATRIX_EVENTS
|
||||
notify_recipients = []
|
||||
staff_users = []
|
||||
if current_user.role == 'admin':
|
||||
from app.models.notification_recipient import ContractNotificationRecipient
|
||||
notify_recipients = (
|
||||
ContractNotificationRecipient.query
|
||||
.filter_by(project_id=project_id)
|
||||
.order_by(ContractNotificationRecipient.created_at.asc())
|
||||
.all()
|
||||
)
|
||||
# Internal staff users selectable as recipients (exclude customers)
|
||||
staff_users = (
|
||||
User.query
|
||||
.filter(User.active == True, User.role != 'customer') # noqa: E712
|
||||
.order_by(User.username)
|
||||
.all()
|
||||
)
|
||||
|
||||
return render_template(
|
||||
'projects/view.html',
|
||||
project=project,
|
||||
facilities=facilities,
|
||||
assignments=assignments,
|
||||
notify_recipients=notify_recipients,
|
||||
matrix_events=MATRIX_EVENTS,
|
||||
staff_users=staff_users,
|
||||
)
|
||||
|
||||
|
||||
@@ -236,6 +262,129 @@ def remove_assignment(assignment_id):
|
||||
return redirect(url_for('projects.view', project_id=project_id))
|
||||
|
||||
|
||||
# ── Additional Notification Recipients — Add ──────────────────────────────────
|
||||
|
||||
@bp.route('/<int:project_id>/notify-recipients/add', methods=['POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def add_notify_recipient(project_id):
|
||||
"""Attach an additional notification recipient (user OR email) to a contract.
|
||||
|
||||
These recipients are notified — for the selected event types — whenever
|
||||
those events fire within this contract's facilities, in ADDITION to the
|
||||
global NotificationMatrix routing. See app/utils/notifications.py.
|
||||
"""
|
||||
from app.models.notification_recipient import ContractNotificationRecipient
|
||||
from app.models.notification_matrix import MATRIX_EVENTS
|
||||
|
||||
project = db.session.get(Project, project_id)
|
||||
if project is None:
|
||||
abort(404)
|
||||
|
||||
recipient_type = (request.form.get('recipient_type') or '').strip()
|
||||
event_types = request.form.getlist('event_types')
|
||||
# Keep only valid, known event keys
|
||||
event_types = [e for e in event_types if e in MATRIX_EVENTS]
|
||||
|
||||
if not event_types:
|
||||
flash('Select at least one event type for the recipient.', 'warning')
|
||||
return redirect(url_for('projects.view', project_id=project_id))
|
||||
|
||||
user_id = None
|
||||
email = None
|
||||
|
||||
if recipient_type == 'user':
|
||||
try:
|
||||
user_id = int(request.form.get('user_id') or 0)
|
||||
except (TypeError, ValueError):
|
||||
user_id = 0
|
||||
user = db.session.get(User, user_id) if user_id else None
|
||||
if user is None or user.role == 'customer' or not user.active:
|
||||
flash('Select a valid staff user.', 'warning')
|
||||
return redirect(url_for('projects.view', project_id=project_id))
|
||||
# Guard against duplicate user recipient on the same contract
|
||||
existing = ContractNotificationRecipient.query.filter_by(
|
||||
project_id=project_id, user_id=user_id
|
||||
).first()
|
||||
if existing:
|
||||
existing.set_event_types(event_types)
|
||||
db.session.commit()
|
||||
log_action(ACTION_UPDATE, 'ContractNotificationRecipient', existing.id,
|
||||
f'{user.display_name} → {project.name}',
|
||||
f'events={",".join(event_types)}')
|
||||
flash(f'Updated notification events for "{user.display_name}".', 'success')
|
||||
return redirect(url_for('projects.view', project_id=project_id))
|
||||
target_label = user.display_name
|
||||
|
||||
elif recipient_type == 'email':
|
||||
email = (request.form.get('email') or '').strip()
|
||||
# Minimal email sanity check — a single '@' with text on both sides
|
||||
if '@' not in email or email.startswith('@') or email.endswith('@') or ' ' in email:
|
||||
flash('Enter a valid email address.', 'warning')
|
||||
return redirect(url_for('projects.view', project_id=project_id))
|
||||
email = email[:200]
|
||||
existing = ContractNotificationRecipient.query.filter_by(
|
||||
project_id=project_id, email=email
|
||||
).first()
|
||||
if existing:
|
||||
existing.set_event_types(event_types)
|
||||
db.session.commit()
|
||||
log_action(ACTION_UPDATE, 'ContractNotificationRecipient', existing.id,
|
||||
f'{email} → {project.name}',
|
||||
f'events={",".join(event_types)}')
|
||||
flash(f'Updated notification events for "{email}".', 'success')
|
||||
return redirect(url_for('projects.view', project_id=project_id))
|
||||
target_label = email
|
||||
|
||||
else:
|
||||
flash('Choose whether to add a user or an email address.', 'warning')
|
||||
return redirect(url_for('projects.view', project_id=project_id))
|
||||
|
||||
recipient = ContractNotificationRecipient(
|
||||
project_id=project_id,
|
||||
user_id=user_id,
|
||||
email=email,
|
||||
)
|
||||
recipient.set_event_types(event_types)
|
||||
db.session.add(recipient)
|
||||
db.session.commit()
|
||||
|
||||
logger.info('PROJECTS | notify_recipient_add | admin=%s project_id=%s target=%s events=%s',
|
||||
current_user.username, project_id, target_label, event_types)
|
||||
log_action(ACTION_CREATE, 'ContractNotificationRecipient', recipient.id,
|
||||
f'{target_label} → {project.name}',
|
||||
f'events={",".join(event_types)}')
|
||||
flash(f'Notification recipient "{target_label}" added.', 'success')
|
||||
return redirect(url_for('projects.view', project_id=project_id))
|
||||
|
||||
|
||||
# ── Additional Notification Recipients — Remove ───────────────────────────────
|
||||
|
||||
@bp.route('/notify-recipients/<int:recipient_id>/remove', methods=['POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def remove_notify_recipient(recipient_id):
|
||||
from app.models.notification_recipient import ContractNotificationRecipient
|
||||
|
||||
recipient = db.session.get(ContractNotificationRecipient, recipient_id)
|
||||
if recipient is None:
|
||||
abort(404)
|
||||
project_id = recipient.project_id
|
||||
project = db.session.get(Project, project_id)
|
||||
label = recipient.display_target
|
||||
recipient_id_snap = recipient.id
|
||||
|
||||
db.session.delete(recipient)
|
||||
db.session.commit()
|
||||
|
||||
logger.info('PROJECTS | notify_recipient_remove | admin=%s project_id=%s target=%s',
|
||||
current_user.username, project_id, label)
|
||||
log_action(ACTION_DELETE, 'ContractNotificationRecipient', recipient_id_snap,
|
||||
f'{label} → {project.name if project else project_id}')
|
||||
flash(f'Notification recipient "{label}" removed.', 'success')
|
||||
return redirect(url_for('projects.view', project_id=project_id))
|
||||
|
||||
|
||||
# ── Bulk Import — Excel template download ─────────────────────────────────────
|
||||
|
||||
@bp.route('/import/template')
|
||||
|
||||
Reference in New Issue
Block a user