05/15 Update: implement iPad notification function 3

This commit is contained in:
Nguyen Ngo
2026-05-15 15:35:39 -04:00
parent b69a52e5f5
commit 33efa9402e
2 changed files with 74 additions and 65 deletions
+62 -56
View File
@@ -17,6 +17,7 @@ PATCH /api/v1/notifications/mark-read
import logging
from datetime import datetime
import sqlalchemy.exc
from flask import Blueprint, request, g
from app import db
from app.models.notification import Notification
@@ -27,17 +28,6 @@ logger = logging.getLogger(__name__)
bp = Blueprint('api_notifications', __name__)
_INSPECTOR_RELEVANT = {
'issue_assigned',
'issue_reassigned',
'issue_unassigned',
'issue_status',
'issue_comment',
'issue_follow_update',
'verification_requested',
'sla_alert',
}
def _parse_since(value):
"""Parse ?since= ISO 8601 string; return None on failure."""
@@ -51,6 +41,21 @@ def _parse_since(value):
return None
def _build_payload(notifications, has_event_type):
"""Serialise notification rows to dicts. Works before and after phase17 migration."""
rows = []
for n in notifications:
rows.append({
'id': n.id,
'title': n.title,
'body': n.body,
'event_type': (n.event_type if has_event_type else None),
'issue_id': n.issue_id,
'created_at': n.created_at.strftime('%Y-%m-%dT%H:%M:%S'),
})
return rows
@bp.route('/notifications', methods=['GET'])
@jwt_required
def list_notifications():
@@ -60,62 +65,63 @@ def list_notifications():
Query parameters
----------------
since ISO 8601 datetime Only return notifications created after this time.
Omit for the initial fetch (returns last 50).
limit int (default 50, max 50)
Response 200
------------
{
"ok": true,
"data": {
"notifications": [
{
"id": 12,
"title": "Issue #7 Assigned to You",
"body": "...",
"event_type": "issue_assigned",
"issue_id": 7,
"created_at": "2026-05-15T09:30:00"
},
...
],
"count": 2
}
}
{ "ok": true, "data": { "notifications": [...], "count": N } }
"""
user = g.api_user
since = _parse_since(request.args.get('since'))
limit = min(int(request.args.get('limit', 50)), 50)
query = Notification.query.filter_by(
user_id=user.id,
is_read=False,
)
def _run_orm():
q = Notification.query.filter_by(user_id=user.id, is_read=False)
if since:
q = q.filter(Notification.created_at > since)
return q.order_by(Notification.created_at.asc()).limit(limit).all()
if since:
query = query.filter(Notification.created_at > since)
# Attempt the ORM query (works after phase17 migration runs).
# If the event_type column does not yet exist in the DB, MySQL raises
# OperationalError: Unknown column 'notifications.event_type' in SELECT.
# getattr() does NOT protect against this — the failure is at the SQL layer.
# The fallback raw-SQL query selects only the pre-phase17 columns so the
# endpoint stays functional before the migration runs.
try:
notifications = _run_orm()
has_event_type = True
except sqlalchemy.exc.OperationalError:
db.session.rollback()
sql_parts = (
"SELECT id, title, body, issue_id, is_read, created_at "
"FROM notifications "
"WHERE user_id = :uid AND is_read = 0 "
)
params = {'uid': user.id, 'lim': limit}
if since:
sql_parts += "AND created_at > :since "
params['since'] = since
sql_parts += "ORDER BY created_at ASC LIMIT :lim"
notifications = (
query
.order_by(Notification.created_at.asc())
.limit(limit)
.all()
)
rows = db.session.execute(db.text(sql_parts), params).fetchall()
payload = [
{
'id': n.id,
'title': n.title,
'body': n.body,
'event_type': n.event_type,
'issue_id': n.issue_id,
'created_at': n.created_at.strftime('%Y-%m-%dT%H:%M:%S'),
}
for n in notifications
]
class _Row:
"""Minimal shim so _build_payload works with raw SQL rows."""
__slots__ = ('id', 'title', 'body', 'issue_id', 'created_at')
def __init__(self, r):
self.id = r[0]
self.title = r[1]
self.body = r[2]
self.issue_id = r[3]
self.created_at = r[5]
logger.info('API NOTIFICATIONS | list | user=%s | since=%s | count=%d',
user.username, since, len(payload))
notifications = [_Row(r) for r in rows]
has_event_type = False
payload = _build_payload(notifications, has_event_type)
logger.info('API NOTIFICATIONS | list | user=%s | since=%s | count=%d | has_event_type=%s',
user.username, since, len(payload), has_event_type)
return api_ok({'notifications': payload, 'count': len(payload)})
@@ -144,7 +150,7 @@ def mark_read():
Notification.query
.filter(
Notification.id.in_(ids),
Notification.user_id == user.id, # never touch another user's records
Notification.user_id == user.id,
)
.all()
)
@@ -156,4 +162,4 @@ def mark_read():
count = 0
logger.info('API NOTIFICATIONS | mark_read | user=%s | count=%d', user.username, count)
return api_ok({'marked': count})
return api_ok({'marked': count})