Feb 02 2026: implement issue following preferences
This commit is contained in:
+130
-8
@@ -1,21 +1,24 @@
|
||||
# app/routes/notifications.py
|
||||
import logging
|
||||
from flask import Blueprint, jsonify, request, abort
|
||||
from flask import (Blueprint, jsonify, request, abort,
|
||||
render_template, redirect, url_for, flash, current_app)
|
||||
from flask_login import login_required, current_user
|
||||
from app import db
|
||||
from app.models.notification import Notification
|
||||
from app.models.notification import (
|
||||
Notification, NotificationPreference, ALL_EVENT_TYPES
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
bp = Blueprint('notifications', __name__, url_prefix='/notifications')
|
||||
|
||||
|
||||
# ── Bell feed (navbar dropdown) ───────────────────────────────────────────────
|
||||
|
||||
@bp.route('/feed')
|
||||
@login_required
|
||||
def feed():
|
||||
"""Return the 20 most recent notifications for the current user as JSON.
|
||||
Used by the navbar bell icon to populate the dropdown.
|
||||
"""
|
||||
"""Return the 20 most recent notifications for the current user as JSON."""
|
||||
notifs = (
|
||||
Notification.query
|
||||
.filter_by(user_id=current_user.id)
|
||||
@@ -41,10 +44,42 @@ def feed():
|
||||
return jsonify({'notifications': items, 'unread_count': unread_count})
|
||||
|
||||
|
||||
# ── Full notification history page ────────────────────────────────────────────
|
||||
|
||||
@bp.route('/')
|
||||
@login_required
|
||||
def index():
|
||||
"""Full paginated notification history with read/unread filter."""
|
||||
page = request.args.get('page', 1, type=int)
|
||||
filter_read = request.args.get('filter', 'all') # 'all' | 'unread' | 'read'
|
||||
|
||||
q = Notification.query.filter_by(user_id=current_user.id)
|
||||
|
||||
if filter_read == 'unread':
|
||||
q = q.filter_by(is_read=False)
|
||||
elif filter_read == 'read':
|
||||
q = q.filter_by(is_read=True)
|
||||
|
||||
notifications = q.order_by(Notification.created_at.desc()).paginate(
|
||||
page=page, per_page=25, error_out=False
|
||||
)
|
||||
unread_count = Notification.query.filter_by(
|
||||
user_id=current_user.id, is_read=False
|
||||
).count()
|
||||
|
||||
return render_template(
|
||||
'notifications/index.html',
|
||||
notifications=notifications,
|
||||
filter_read=filter_read,
|
||||
unread_count=unread_count,
|
||||
)
|
||||
|
||||
|
||||
# ── Mark single notification read ─────────────────────────────────────────────
|
||||
|
||||
@bp.route('/<int:notif_id>/mark-read', methods=['POST'])
|
||||
@login_required
|
||||
def mark_read(notif_id):
|
||||
"""Mark a single notification as read."""
|
||||
notif = Notification.query.get_or_404(notif_id)
|
||||
if notif.user_id != current_user.id:
|
||||
abort(403)
|
||||
@@ -57,10 +92,11 @@ def mark_read(notif_id):
|
||||
return jsonify({'ok': True})
|
||||
|
||||
|
||||
# ── Mark all read ─────────────────────────────────────────────────────────────
|
||||
|
||||
@bp.route('/mark-all-read', methods=['POST'])
|
||||
@login_required
|
||||
def mark_all_read():
|
||||
"""Mark all unread notifications for the current user as read."""
|
||||
updated = (
|
||||
Notification.query
|
||||
.filter_by(user_id=current_user.id, is_read=False)
|
||||
@@ -71,4 +107,90 @@ def mark_all_read():
|
||||
'NOTIFICATIONS ALL READ | user=%s | count=%s',
|
||||
current_user.username, updated,
|
||||
)
|
||||
return jsonify({'ok': True, 'marked': updated})
|
||||
|
||||
# Support both AJAX (returns JSON) and form POST (redirects to index)
|
||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest' or \
|
||||
request.content_type == 'application/json':
|
||||
return jsonify({'ok': True, 'marked': updated})
|
||||
return redirect(url_for('notifications.index'))
|
||||
|
||||
|
||||
# ── Notification preferences ──────────────────────────────────────────────────
|
||||
|
||||
@bp.route('/preferences', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def preferences():
|
||||
"""Display and save per-event notification preferences."""
|
||||
if request.method == 'POST':
|
||||
for event_type in ALL_EVENT_TYPES:
|
||||
pref = NotificationPreference.query.filter_by(
|
||||
user_id=current_user.id,
|
||||
event_type=event_type,
|
||||
).first()
|
||||
|
||||
if pref is None:
|
||||
pref = NotificationPreference(
|
||||
user_id=current_user.id,
|
||||
event_type=event_type,
|
||||
)
|
||||
db.session.add(pref)
|
||||
|
||||
pref.email_enabled = bool(request.form.get(f'email_{event_type}'))
|
||||
pref.digest_mode = bool(request.form.get(f'digest_{event_type}'))
|
||||
pref.digest_frequency = request.form.get(f'freq_{event_type}', 'daily')
|
||||
|
||||
# Guard: digest_mode only meaningful when email is enabled
|
||||
if not pref.email_enabled:
|
||||
pref.digest_mode = False
|
||||
|
||||
db.session.commit()
|
||||
logger.info(
|
||||
'NOTIFICATION PREFERENCES SAVED | user=%s',
|
||||
current_user.username,
|
||||
)
|
||||
flash('Notification preferences saved.', 'success')
|
||||
return redirect(url_for('notifications.preferences'))
|
||||
|
||||
# Build a dict keyed by event_type for easy template access
|
||||
prefs_map = {}
|
||||
for pref in NotificationPreference.query.filter_by(user_id=current_user.id).all():
|
||||
prefs_map[pref.event_type] = pref
|
||||
|
||||
return render_template(
|
||||
'notifications/preferences.html',
|
||||
event_types=ALL_EVENT_TYPES,
|
||||
prefs_map=prefs_map,
|
||||
)
|
||||
|
||||
|
||||
# ── Digest trigger (called by cron) ───────────────────────────────────────────
|
||||
|
||||
@bp.route('/send-digest', methods=['POST'])
|
||||
def send_digest():
|
||||
"""Trigger digest email delivery. Protected by a shared secret token.
|
||||
|
||||
Called by a cron job, e.g.:
|
||||
# Hourly digest
|
||||
0 * * * * curl -s -X POST https://yourdomain.com/notifications/send-digest \
|
||||
-d "token=YOUR_DIGEST_SECRET&frequency=hourly"
|
||||
|
||||
# Daily digest at 07:00
|
||||
0 7 * * * curl -s -X POST https://yourdomain.com/notifications/send-digest \
|
||||
-d "token=YOUR_DIGEST_SECRET&frequency=daily"
|
||||
"""
|
||||
token = request.form.get('token') or request.args.get('token')
|
||||
frequency = request.form.get('frequency', 'daily')
|
||||
|
||||
expected = current_app.config.get('DIGEST_SECRET')
|
||||
if not expected or token != expected:
|
||||
logger.warning('DIGEST TRIGGER REJECTED | bad or missing token')
|
||||
abort(403)
|
||||
|
||||
if frequency not in ('hourly', 'daily'):
|
||||
abort(400)
|
||||
|
||||
from app.utils.notifications import send_pending_digests
|
||||
sent = send_pending_digests(frequency=frequency)
|
||||
|
||||
logger.info('DIGEST TRIGGERED | frequency=%s | sent=%s', frequency, sent)
|
||||
return jsonify({'ok': True, 'sent': sent, 'frequency': frequency})
|
||||
Reference in New Issue
Block a user