Aug 6 - Update enrollment page, notify admin user

This commit is contained in:
2026-08-06 12:35:09 -04:00
parent 264eeabd70
commit 9586d21ca2
5 changed files with 221 additions and 23 deletions
+113 -15
View File
@@ -13,7 +13,11 @@ response), and the From identity comes from branded_sender() so it stays an
SMTP-authorized address that actually delivers (rules 64 / 76).
This is the only part of app/enrollment that touches shared mail
infrastructure; it still imports no models and writes no DB row.
infrastructure. It performs exactly ONE database read — resolving the active
admin accounts to notify — and no write. That read is a deliberate, narrowed
exception to the package's no-models rule (rule 88): the alternative, a
hand-maintained recipient list in config, drifts out of step with reality the
first time someone joins or leaves. Everything else here stays model-free.
"""
import logging
@@ -53,6 +57,113 @@ def _text_body(record, people, corrections_email):
return '\n'.join(lines)
def _dispatch(msg, label, record):
"""Send one message on a background thread. Never raises.
Rule 14 — the HTTP response must not wait on SMTP. The submission is
already on disk by the time anything here runs, so a mail failure is
logged and dropped rather than surfaced to the customer.
"""
app = current_app._get_current_object()
def _send():
with app.app_context():
try:
from app import mail
mail.send(msg)
logger.info('ENROLLMENT %s SENT | to=%s | id=%s',
label, msg.recipients, record.get('id'))
except Exception as exc:
logger.error('ENROLLMENT %s FAILED | to=%s | id=%s | error=%s',
label, msg.recipients, record.get('id'), exc)
threading.Thread(target=_send, daemon=True).start()
def _admin_recipients():
"""Addresses to alert when a new enrollment arrives.
Active `admin` accounts, plus any extra addresses in the optional
ENROLLMENT_NOTIFY_EMAILS config (comma-separated) for people who should be
told but do not hold a JQC login. Deduplicated case-insensitively.
The User import is function-local and read-only — see the module docstring.
"""
emails = []
try:
from app.models.user import User
rows = User.query.filter(User.role == 'admin',
User.active == True).all() # noqa: E712
emails += [u.email for u in rows if u.email]
except Exception:
# A DB problem must not stop the confirmation going out, nor the
# submission from succeeding.
logger.exception('ENROLLMENT | could not resolve admin recipients')
extra = current_app.config.get('ENROLLMENT_NOTIFY_EMAILS') or ''
emails += [e.strip() for e in extra.split(',') if e.strip()]
seen, out = set(), []
for e in emails:
low = e.lower()
if low not in seen:
seen.add(low)
out.append(e)
return out
def send_admin_notification(record, base_url=None):
"""Alert JQC admins that a new enrollment form has arrived. Never raises."""
if not current_app.config.get('MAIL_SERVER'):
logger.warning('ENROLLMENT ADMIN EMAIL SKIPPED | no MAIL_SERVER | id=%s',
record.get('id'))
return
try:
from flask_mail import Message
from app.utils.mail_utils import branded_sender
from . import schema
recipients = _admin_recipients()
if not recipients:
logger.warning('ENROLLMENT | no admin recipients for id=%s',
record.get('id'))
return
effective_base = (base_url
or current_app.config.get('APP_BASE_URL', '')).rstrip('/')
people = schema.people_of(record)
link = f'{effective_base}/enrollment/admin/{record.get("id")}'
lines = [
'A new JQC enrollment form has been submitted.',
'',
f'Project: {record.get("project_name")}',
f'Requester: {record.get("request_by")} <{record.get("requester_email")}>',
f'Reference: {record.get("id")}',
f'People: {len(people)}',
'',
f'Open it here: {link}',
]
if record.get('notes'):
lines += ['', f'Customer notes: {record["notes"]}']
msg = Message(
subject = f'[JQC] New enrollment — {record.get("project_name")}',
sender = branded_sender(effective_base),
recipients = recipients,
body = '\n'.join(lines),
html = render_template('enrollment/email_admin_notice.html',
record=record, people=people,
schema=schema, link=link),
)
_dispatch(msg, 'ADMIN EMAIL', record)
except Exception:
logger.exception('ENROLLMENT ADMIN EMAIL BUILD FAILED | id=%s',
record.get('id'))
def send_confirmation(record, base_url=None):
"""Email the requester a copy of what they submitted. Never raises."""
email = (record.get('requester_email') or '').strip()
@@ -66,7 +177,6 @@ def send_confirmation(record, base_url=None):
try:
from flask_mail import Message
from app import mail
from app.utils.mail_utils import branded_sender
from . import schema
@@ -84,19 +194,7 @@ def send_confirmation(record, base_url=None):
schema=schema),
)
app = current_app._get_current_object()
def _send():
with app.app_context():
try:
mail.send(msg)
logger.info('ENROLLMENT EMAIL SENT | to=%s | id=%s',
email, record.get('id'))
except Exception as exc:
logger.error('ENROLLMENT EMAIL FAILED | to=%s | id=%s | error=%s',
email, record.get('id'), exc)
threading.Thread(target=_send, daemon=True).start()
_dispatch(msg, 'EMAIL', record)
except Exception:
# Building the message failed (bad template, mail misconfigured, …).