05/11 Update: backend updated for iOS app to show issue status and assigned user can change issue's status
This commit is contained in:
+108
-2
@@ -3,10 +3,19 @@ app/api/issues.py
|
|||||||
-----------------
|
-----------------
|
||||||
Mobile API endpoint for submitting issues from the iPad app.
|
Mobile API endpoint for submitting issues from the iPad app.
|
||||||
|
|
||||||
POST /api/v1/issues
|
POST /api/v1/issues
|
||||||
Creates a new issue record.
|
Creates a new issue record.
|
||||||
Accepts a mobile_local_id for idempotency.
|
Accepts a mobile_local_id for idempotency.
|
||||||
Triggers notify_by_matrix() and log_action() identically to the web route.
|
Triggers notify_by_matrix() and log_action() identically to the web route.
|
||||||
|
|
||||||
|
GET /api/v1/issues/<id>
|
||||||
|
Returns current status, severity, description, and assigned_to for an issue.
|
||||||
|
Inspectors may only fetch issues assigned to them.
|
||||||
|
|
||||||
|
PATCH /api/v1/issues/<id>/status
|
||||||
|
Updates the status of an issue.
|
||||||
|
Inspectors may only update issues assigned to them.
|
||||||
|
Admins/directors may update any issue.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@@ -18,7 +27,7 @@ from app.models.facility import Facility, Area
|
|||||||
from app.models.inspection import Inspection
|
from app.models.inspection import Inspection
|
||||||
from app.api.errors import api_ok, api_error
|
from app.api.errors import api_ok, api_error
|
||||||
from app.api.decorators import jwt_required
|
from app.api.decorators import jwt_required
|
||||||
from app.utils.audit import log_action, ACTION_CREATE
|
from app.utils.audit import log_action, ACTION_CREATE, ACTION_UPDATE
|
||||||
from app.utils.notifications import notify_by_matrix
|
from app.utils.notifications import notify_by_matrix
|
||||||
from app.utils.time_utils import now_eastern
|
from app.utils.time_utils import now_eastern
|
||||||
|
|
||||||
@@ -28,6 +37,7 @@ bp = Blueprint('api_issues', __name__)
|
|||||||
|
|
||||||
_ALLOWED_ROLES = {'admin', 'director', 'inspector', 'project_manager'}
|
_ALLOWED_ROLES = {'admin', 'director', 'inspector', 'project_manager'}
|
||||||
_VALID_SEVERITY = {'low', 'medium', 'high', 'critical'}
|
_VALID_SEVERITY = {'low', 'medium', 'high', 'critical'}
|
||||||
|
_VALID_STATUSES = {'open', 'in_progress', 'resolved', 'pending_verification'}
|
||||||
|
|
||||||
|
|
||||||
# ── Create Issue ──────────────────────────────────────────────────────────────
|
# ── Create Issue ──────────────────────────────────────────────────────────────
|
||||||
@@ -144,3 +154,99 @@ def create_issue():
|
|||||||
issue.id, facility.name, severity, user.username)
|
issue.id, facility.name, severity, user.username)
|
||||||
|
|
||||||
return api_ok({'issue_id': issue.id, 'duplicate': False})
|
return api_ok({'issue_id': issue.id, 'duplicate': False})
|
||||||
|
|
||||||
|
|
||||||
|
# ── Get Issue Detail ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@bp.route('/issues/<int:issue_id>', methods=['GET'])
|
||||||
|
@jwt_required
|
||||||
|
def get_issue(issue_id):
|
||||||
|
"""
|
||||||
|
Return current status, severity, description, assigned_to, and facility
|
||||||
|
for a single issue.
|
||||||
|
|
||||||
|
Access:
|
||||||
|
- admin / director / project_manager : any issue
|
||||||
|
- inspector : only issues where assigned_to == current user
|
||||||
|
"""
|
||||||
|
user = g.api_user
|
||||||
|
if user.role not in _ALLOWED_ROLES:
|
||||||
|
return api_error('Access denied', 403)
|
||||||
|
|
||||||
|
issue = db.session.get(Issue, issue_id)
|
||||||
|
if issue is None:
|
||||||
|
return api_error('Issue not found', 404)
|
||||||
|
|
||||||
|
if user.role == 'inspector' and issue.assigned_to != user.id:
|
||||||
|
return api_error('Access denied', 403)
|
||||||
|
|
||||||
|
facility = issue.resolved_facility
|
||||||
|
return api_ok({
|
||||||
|
'id': issue.id,
|
||||||
|
'status': issue.status,
|
||||||
|
'severity': issue.severity,
|
||||||
|
'description': issue.description,
|
||||||
|
'assigned_to': issue.assigned_to,
|
||||||
|
'facility_id': facility.id if facility else None,
|
||||||
|
'facility_name': facility.name if facility 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,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# ── Update Issue Status ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@bp.route('/issues/<int:issue_id>/status', methods=['PATCH'])
|
||||||
|
@jwt_required
|
||||||
|
def update_issue_status(issue_id):
|
||||||
|
"""
|
||||||
|
Update the status of an issue.
|
||||||
|
|
||||||
|
Request JSON
|
||||||
|
------------
|
||||||
|
{ "status": "in_progress" } // one of: open | in_progress | resolved | pending_verification
|
||||||
|
|
||||||
|
Access:
|
||||||
|
- admin / director : any issue
|
||||||
|
- inspector : only issues where assigned_to == current user
|
||||||
|
"""
|
||||||
|
user = g.api_user
|
||||||
|
if user.role not in _ALLOWED_ROLES:
|
||||||
|
return api_error('Access denied', 403)
|
||||||
|
|
||||||
|
issue = db.session.get(Issue, issue_id)
|
||||||
|
if issue is None:
|
||||||
|
return api_error('Issue not found', 404)
|
||||||
|
|
||||||
|
if user.role == 'inspector' and issue.assigned_to != user.id:
|
||||||
|
return api_error('Access denied — you can only update issues assigned to you', 403)
|
||||||
|
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
new_status = (data.get('status') or '').strip().lower()
|
||||||
|
|
||||||
|
if new_status not in _VALID_STATUSES:
|
||||||
|
return api_error(
|
||||||
|
f'status must be one of: {", ".join(sorted(_VALID_STATUSES))}', 400
|
||||||
|
)
|
||||||
|
|
||||||
|
old_status = issue.status
|
||||||
|
issue.status = new_status
|
||||||
|
|
||||||
|
if new_status == 'resolved' and not issue.resolved_at:
|
||||||
|
issue.resolved_at = now_eastern()
|
||||||
|
issue.sla_notified = None
|
||||||
|
elif new_status != 'resolved':
|
||||||
|
issue.resolved_at = None
|
||||||
|
if old_status == 'resolved':
|
||||||
|
issue.sla_notified = None
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
log_action(ACTION_UPDATE, 'Issue', issue.id,
|
||||||
|
f'status {old_status} → {new_status}',
|
||||||
|
f'source=mobile; updated_by={user.username}')
|
||||||
|
|
||||||
|
logger.info('API ISSUES | status_updated | issue_id=%d | %s→%s | user=%s',
|
||||||
|
issue.id, old_status, new_status, user.username)
|
||||||
|
|
||||||
|
return api_ok({'issue_id': issue.id, 'status': issue.status})
|
||||||
|
|||||||
Reference in New Issue
Block a user