Jul 7 - Implement additional recipient per contract

This commit is contained in:
2026-07-07 20:28:17 -04:00
parent 8f971a403b
commit a3c6fd73bd
9 changed files with 613 additions and 8 deletions
+144
View File
@@ -236,6 +236,150 @@ def remove_assignment(assignment_id):
return redirect(url_for('projects.view', project_id=project_id))
# ── Notification Recipients — per-contract (phase37) ─────────────────────────
@bp.route('/<int:project_id>/recipients')
@login_required
@supervisor_required
def recipients(project_id):
"""Manage additional notification recipients for one contract.
Recipients are either staff users (in-app + email) or external email
addresses (email only), each subscribed to a chosen set of matrix events.
Dispatched by notify_by_matrix() for events in this contract's facilities.
"""
from app.models.project_recipient import ProjectNotificationRecipient
from app.models.notification_matrix import MATRIX_EVENTS
project = db.session.get(Project, project_id)
if project is None:
abort(404)
recipient_rows = (
ProjectNotificationRecipient.query
.filter_by(project_id=project_id)
.order_by(ProjectNotificationRecipient.created_at)
.all()
)
staff_users = (
User.query
.filter(User.active == True, User.role != 'customer') # noqa: E712
.order_by(User.username)
.all()
)
return render_template(
'projects/recipients.html',
project=project,
recipients=recipient_rows,
staff_users=staff_users,
matrix_events=MATRIX_EVENTS,
)
@bp.route('/<int:project_id>/recipients/add', methods=['POST'])
@login_required
@supervisor_required
def add_recipient(project_id):
"""Add (or update, if the recipient already exists) a contract recipient."""
import json as _json
import re as _re
from app.models.project_recipient import ProjectNotificationRecipient
from app.models.notification_matrix import MATRIX_EVENTS
project = db.session.get(Project, project_id)
if project is None:
abort(404)
events = [e for e in request.form.getlist('events') if e in MATRIX_EVENTS]
if not events:
flash('Select at least one event to notify this recipient about.', 'warning')
return redirect(url_for('projects.recipients', project_id=project_id))
recipient_type = request.form.get('recipient_type', 'staff')
if recipient_type == 'staff':
user_id = request.form.get('user_id', type=int)
user = db.session.get(User, user_id) if user_id else None
if user is None or not user.active or user.role == 'customer':
flash('Please choose a valid staff user.', 'warning')
return redirect(url_for('projects.recipients', project_id=project_id))
row = ProjectNotificationRecipient.query.filter_by(
project_id=project_id, user_id=user.id
).first()
if row:
row.events = _json.dumps(events)
action, verb = 'updated', 'UPDATE'
else:
row = ProjectNotificationRecipient(
project_id=project_id, user_id=user.id,
events=_json.dumps(events),
)
db.session.add(row)
action, verb = 'added', 'CREATE'
label = user.display_name
else:
email = (request.form.get('email') or '').strip()
if not _re.match(r'^[^@\s]+@[^@\s]+\.[^@\s]+$', email):
flash('Please enter a valid email address.', 'warning')
return redirect(url_for('projects.recipients', project_id=project_id))
row = ProjectNotificationRecipient.query.filter(
ProjectNotificationRecipient.project_id == project_id,
ProjectNotificationRecipient.user_id.is_(None),
db.func.lower(ProjectNotificationRecipient.email) == email.lower(),
).first()
if row:
row.events = _json.dumps(events)
action, verb = 'updated', 'UPDATE'
else:
row = ProjectNotificationRecipient(
project_id=project_id, email=email,
events=_json.dumps(events),
)
db.session.add(row)
action, verb = 'added', 'CREATE'
label = email
db.session.commit()
logger.info('PROJECTS | recipient_%s | user=%s project_id=%s recipient=%s events=%s',
action, current_user.username, project_id, label, events)
log_action(ACTION_CREATE if verb == 'CREATE' else ACTION_UPDATE,
'ProjectNotificationRecipient', row.id,
f'{label}{project.name}',
f'events={",".join(events)}')
flash(f'Recipient "{label}" {action} for contract "{project.name}".', 'success')
return redirect(url_for('projects.recipients', project_id=project_id))
@bp.route('/recipients/<int:recipient_id>/remove', methods=['POST'])
@login_required
@supervisor_required
def remove_recipient(recipient_id):
"""Remove a contract notification recipient."""
from app.models.project_recipient import ProjectNotificationRecipient
row = db.session.get(ProjectNotificationRecipient, recipient_id)
if row is None:
abort(404)
project = db.session.get(Project, row.project_id)
if project is None:
abort(404)
label = row.user.display_name if row.user else (row.email or f'id={row.id}')
project_id_snap = row.project_id
row_id_snap = row.id
db.session.delete(row)
db.session.commit()
logger.info('PROJECTS | recipient_remove | user=%s project_id=%s recipient=%s',
current_user.username, project_id_snap, label)
log_action(ACTION_DELETE, 'ProjectNotificationRecipient', row_id_snap,
f'{label}{project.name}')
flash(f'Recipient "{label}" removed.', 'success')
return redirect(url_for('projects.recipients', project_id=project_id_snap))
# ── Bulk Import — Excel template download ─────────────────────────────────────
@bp.route('/import/template')