139 lines
5.0 KiB
Python
139 lines
5.0 KiB
Python
# app/routes/broadcast.py
|
|
# -----------------------
|
|
# Admin-only route for composing and sending push notifications to all
|
|
# iOS apps. Uses the existing Notification model and notify() utility —
|
|
# broadcasts arrive on the iPad via the standard 60-second poll cycle
|
|
# (GET /api/v1/notifications?since=...) and trigger a local banner via
|
|
# deliverLocalNotification(). No APNs/FCM required.
|
|
|
|
import logging
|
|
from flask import Blueprint, render_template, request, redirect, url_for, flash
|
|
from flask_login import login_required, current_user
|
|
|
|
from app import db
|
|
from app.routes.broadcast import Broadcast
|
|
from app.models.user import User
|
|
from app.models.notification import Notification, EVENT_INSPECTION_DONE
|
|
from app.utils.decorators import admin_required
|
|
from app.utils.audit import log_action, ACTION_CREATE
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
bp = Blueprint('broadcast', __name__, url_prefix='/admin/broadcast')
|
|
|
|
# All roles that can hold an active iOS session
|
|
BROADCAST_ROLES = ['inspector', 'project_manager', 'director', 'admin']
|
|
|
|
ROLE_LABELS = {
|
|
'inspector': 'Inspectors',
|
|
'project_manager': 'Project Managers',
|
|
'director': 'Directors',
|
|
'admin': 'Admins',
|
|
}
|
|
|
|
|
|
@bp.route('/', methods=['GET'])
|
|
@login_required
|
|
@admin_required
|
|
def index():
|
|
"""Show the compose form and recent broadcast history."""
|
|
history = (
|
|
Broadcast.query
|
|
.order_by(Broadcast.sent_at.desc())
|
|
.limit(50)
|
|
.all()
|
|
)
|
|
return render_template(
|
|
'admin/broadcast.html',
|
|
history=history,
|
|
roles=BROADCAST_ROLES,
|
|
role_labels=ROLE_LABELS,
|
|
)
|
|
|
|
|
|
@bp.route('/send', methods=['POST'])
|
|
@login_required
|
|
@admin_required
|
|
def send():
|
|
"""
|
|
Compose and send a broadcast notification.
|
|
|
|
Form fields
|
|
-----------
|
|
title str Notification title (required, max 255)
|
|
body str Notification body text (required)
|
|
roles[] list One or more role keys to target (required)
|
|
"""
|
|
title = (request.form.get('title') or '').strip()
|
|
body = (request.form.get('body') or '').strip()
|
|
target_roles = request.form.getlist('roles')
|
|
|
|
# ── Validation ─────────────────────────────────────────────────────────
|
|
errors = []
|
|
if not title:
|
|
errors.append('Title is required.')
|
|
elif len(title) > 255:
|
|
errors.append('Title must be 255 characters or fewer.')
|
|
if not body:
|
|
errors.append('Message body is required.')
|
|
valid_roles = [r for r in target_roles if r in BROADCAST_ROLES]
|
|
if not valid_roles:
|
|
errors.append('Select at least one target role.')
|
|
|
|
if errors:
|
|
for e in errors:
|
|
flash(e, 'danger')
|
|
return redirect(url_for('broadcast.index'))
|
|
|
|
# ── Find target users (active only) ────────────────────────────────────
|
|
recipients = (
|
|
User.query
|
|
.filter(User.role.in_(valid_roles), User.active == True) # noqa: E712
|
|
.all()
|
|
)
|
|
if not recipients:
|
|
flash('No active users found for the selected roles.', 'warning')
|
|
return redirect(url_for('broadcast.index'))
|
|
|
|
# ── Create Notification rows ────────────────────────────────────────────
|
|
# One row per recipient — the iOS poll picks them up in the next 60s cycle.
|
|
# Using the same Notification model as all other in-app notifications means
|
|
# no iOS code changes are needed: existing deliverLocalNotification() fires
|
|
# a banner, and unreadNotificationCount increments as usual.
|
|
for user in recipients:
|
|
notif = Notification(
|
|
user_id = user.id,
|
|
title = title,
|
|
body = body,
|
|
link = url_for('broadcast.index', _external=False),
|
|
event_type = 'admin_broadcast',
|
|
is_read = False,
|
|
)
|
|
db.session.add(notif)
|
|
|
|
# ── Record the broadcast ────────────────────────────────────────────────
|
|
broadcast = Broadcast(
|
|
title = title,
|
|
body = body,
|
|
target_roles = valid_roles,
|
|
sent_by_id = current_user.id,
|
|
recipient_count = len(recipients),
|
|
)
|
|
db.session.add(broadcast)
|
|
db.session.commit()
|
|
|
|
log_action(ACTION_CREATE, 'Broadcast', broadcast.id,
|
|
f'"{title}" → {", ".join(valid_roles)} ({len(recipients)} users)')
|
|
|
|
logger.info(
|
|
'BROADCAST | id=%d | title=%r | roles=%s | recipients=%d | by=%s',
|
|
broadcast.id, title, valid_roles, len(recipients), current_user.username,
|
|
)
|
|
|
|
flash(
|
|
f'Broadcast sent to {len(recipients)} user(s) across '
|
|
f'{", ".join(ROLE_LABELS[r] for r in valid_roles)}.',
|
|
'success',
|
|
)
|
|
return redirect(url_for('broadcast.index'))
|