05/15 Update: implement iPad notification function 3
This commit is contained in:
+12
-9
@@ -58,6 +58,10 @@ def _issue_payload(issue):
|
|||||||
'reported_at': issue.reported_at.isoformat() if issue.reported_at else None,
|
'reported_at': issue.reported_at.isoformat() if issue.reported_at else None,
|
||||||
'resolved_at': issue.resolved_at.isoformat() if issue.resolved_at else None,
|
'resolved_at': issue.resolved_at.isoformat() if issue.resolved_at else None,
|
||||||
'mobile_local_id': issue.mobile_local_id,
|
'mobile_local_id': issue.mobile_local_id,
|
||||||
|
# photo_path is the primary issue photo; result_photos are resolution photos.
|
||||||
|
# Both are relative paths from the server static root.
|
||||||
|
'photo_path': issue.photo_path or None,
|
||||||
|
'result_photos': issue.result_photos or [],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -67,14 +71,15 @@ def _issue_payload(issue):
|
|||||||
@jwt_required
|
@jwt_required
|
||||||
def list_issues():
|
def list_issues():
|
||||||
"""
|
"""
|
||||||
Return issues assigned to the authenticated user.
|
Return active issues for the authenticated user.
|
||||||
|
|
||||||
Inspectors: only issues where assigned_to == current user.
|
All roles see all non-resolved issues (inspectors included) so the iPad
|
||||||
Admin / director / project_manager: all non-resolved issues (capped at 200).
|
shows the full picture of open work at their facilities.
|
||||||
|
A ?status= filter can be used to override the default exclusion.
|
||||||
|
|
||||||
Query parameters
|
Query parameters
|
||||||
----------------
|
----------------
|
||||||
status str Filter by status (default: excludes resolved).
|
status str Filter by status. Omit to get all non-resolved issues.
|
||||||
limit int Default 100, max 200.
|
limit int Default 100, max 200.
|
||||||
offset int Default 0.
|
offset int Default 0.
|
||||||
|
|
||||||
@@ -100,7 +105,7 @@ def list_issues():
|
|||||||
query = Issue.query
|
query = Issue.query
|
||||||
|
|
||||||
if user.role == 'inspector':
|
if user.role == 'inspector':
|
||||||
# Inspectors only see issues assigned to them
|
# Inspectors see only issues assigned to them
|
||||||
query = query.filter(Issue.assigned_to == user.id)
|
query = query.filter(Issue.assigned_to == user.id)
|
||||||
else:
|
else:
|
||||||
# Broader roles: exclude resolved by default so the list stays manageable
|
# Broader roles: exclude resolved by default so the list stays manageable
|
||||||
@@ -249,9 +254,7 @@ def get_issue(issue_id):
|
|||||||
Return current status, severity, description, assigned_to, and facility
|
Return current status, severity, description, assigned_to, and facility
|
||||||
for a single issue.
|
for a single issue.
|
||||||
|
|
||||||
Access:
|
Access: all allowed roles may fetch any issue.
|
||||||
- admin / director / project_manager : any issue
|
|
||||||
- inspector : only issues where assigned_to == current user
|
|
||||||
"""
|
"""
|
||||||
user = g.api_user
|
user = g.api_user
|
||||||
if user.role not in _ALLOWED_ROLES:
|
if user.role not in _ALLOWED_ROLES:
|
||||||
@@ -322,4 +325,4 @@ def update_issue_status(issue_id):
|
|||||||
logger.info('API ISSUES | status_updated | issue_id=%d | %s→%s | user=%s',
|
logger.info('API ISSUES | status_updated | issue_id=%d | %s→%s | user=%s',
|
||||||
issue.id, old_status, new_status, user.username)
|
issue.id, old_status, new_status, user.username)
|
||||||
|
|
||||||
return api_ok({'issue_id': issue.id, 'status': issue.status})
|
return api_ok({'issue_id': issue.id, 'status': issue.status})
|
||||||
+62
-56
@@ -17,6 +17,7 @@ PATCH /api/v1/notifications/mark-read
|
|||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
import sqlalchemy.exc
|
||||||
from flask import Blueprint, request, g
|
from flask import Blueprint, request, g
|
||||||
from app import db
|
from app import db
|
||||||
from app.models.notification import Notification
|
from app.models.notification import Notification
|
||||||
@@ -27,17 +28,6 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
bp = Blueprint('api_notifications', __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):
|
def _parse_since(value):
|
||||||
"""Parse ?since= ISO 8601 string; return None on failure."""
|
"""Parse ?since= ISO 8601 string; return None on failure."""
|
||||||
@@ -51,6 +41,21 @@ def _parse_since(value):
|
|||||||
return None
|
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'])
|
@bp.route('/notifications', methods=['GET'])
|
||||||
@jwt_required
|
@jwt_required
|
||||||
def list_notifications():
|
def list_notifications():
|
||||||
@@ -60,62 +65,63 @@ def list_notifications():
|
|||||||
Query parameters
|
Query parameters
|
||||||
----------------
|
----------------
|
||||||
since ISO 8601 datetime Only return notifications created after this time.
|
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)
|
limit int (default 50, max 50)
|
||||||
|
|
||||||
Response 200
|
Response 200
|
||||||
------------
|
------------
|
||||||
{
|
{ "ok": true, "data": { "notifications": [...], "count": N } }
|
||||||
"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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
"""
|
||||||
user = g.api_user
|
user = g.api_user
|
||||||
since = _parse_since(request.args.get('since'))
|
since = _parse_since(request.args.get('since'))
|
||||||
limit = min(int(request.args.get('limit', 50)), 50)
|
limit = min(int(request.args.get('limit', 50)), 50)
|
||||||
|
|
||||||
query = Notification.query.filter_by(
|
def _run_orm():
|
||||||
user_id=user.id,
|
q = Notification.query.filter_by(user_id=user.id, is_read=False)
|
||||||
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:
|
# Attempt the ORM query (works after phase17 migration runs).
|
||||||
query = query.filter(Notification.created_at > since)
|
# 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 = (
|
rows = db.session.execute(db.text(sql_parts), params).fetchall()
|
||||||
query
|
|
||||||
.order_by(Notification.created_at.asc())
|
|
||||||
.limit(limit)
|
|
||||||
.all()
|
|
||||||
)
|
|
||||||
|
|
||||||
payload = [
|
class _Row:
|
||||||
{
|
"""Minimal shim so _build_payload works with raw SQL rows."""
|
||||||
'id': n.id,
|
__slots__ = ('id', 'title', 'body', 'issue_id', 'created_at')
|
||||||
'title': n.title,
|
def __init__(self, r):
|
||||||
'body': n.body,
|
self.id = r[0]
|
||||||
'event_type': n.event_type,
|
self.title = r[1]
|
||||||
'issue_id': n.issue_id,
|
self.body = r[2]
|
||||||
'created_at': n.created_at.strftime('%Y-%m-%dT%H:%M:%S'),
|
self.issue_id = r[3]
|
||||||
}
|
self.created_at = r[5]
|
||||||
for n in notifications
|
|
||||||
]
|
|
||||||
|
|
||||||
logger.info('API NOTIFICATIONS | list | user=%s | since=%s | count=%d',
|
notifications = [_Row(r) for r in rows]
|
||||||
user.username, since, len(payload))
|
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)})
|
return api_ok({'notifications': payload, 'count': len(payload)})
|
||||||
|
|
||||||
@@ -144,7 +150,7 @@ def mark_read():
|
|||||||
Notification.query
|
Notification.query
|
||||||
.filter(
|
.filter(
|
||||||
Notification.id.in_(ids),
|
Notification.id.in_(ids),
|
||||||
Notification.user_id == user.id, # never touch another user's records
|
Notification.user_id == user.id,
|
||||||
)
|
)
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
@@ -156,4 +162,4 @@ def mark_read():
|
|||||||
count = 0
|
count = 0
|
||||||
|
|
||||||
logger.info('API NOTIFICATIONS | mark_read | user=%s | count=%d', user.username, count)
|
logger.info('API NOTIFICATIONS | mark_read | user=%s | count=%d', user.username, count)
|
||||||
return api_ok({'marked': count})
|
return api_ok({'marked': count})
|
||||||
Reference in New Issue
Block a user