05/16 Fix bugs 2
This commit is contained in:
@@ -50,6 +50,7 @@ def _user_payload(user: User) -> dict:
|
|||||||
return {
|
return {
|
||||||
'id': user.id,
|
'id': user.id,
|
||||||
'username': user.username,
|
'username': user.username,
|
||||||
|
'full_name': user.full_name or '',
|
||||||
'email': user.email,
|
'email': user.email,
|
||||||
'role': user.role,
|
'role': user.role,
|
||||||
'created_at': user.created_at.isoformat() if user.created_at else None,
|
'created_at': user.created_at.isoformat() if user.created_at else None,
|
||||||
|
|||||||
+14
-5
@@ -105,8 +105,16 @@ def list_issues():
|
|||||||
query = Issue.query
|
query = Issue.query
|
||||||
|
|
||||||
if user.role == 'inspector':
|
if user.role == 'inspector':
|
||||||
# Inspectors see only issues assigned to them
|
# Inspectors see issues assigned to them OR issues they reported.
|
||||||
query = query.filter(Issue.assigned_to == user.id)
|
# The reported_by path covers issues created on the iPad that haven't
|
||||||
|
# been assigned yet (assigned_to is NULL until a director assigns them).
|
||||||
|
# Pre-phase18 rows with reported_by = NULL still surface via assigned_to.
|
||||||
|
query = query.filter(
|
||||||
|
db.or_(
|
||||||
|
Issue.assigned_to == user.id,
|
||||||
|
Issue.reported_by == 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
|
||||||
status_filter = request.args.get('status')
|
status_filter = request.args.get('status')
|
||||||
@@ -204,6 +212,7 @@ def create_issue():
|
|||||||
photo_path = data.get('photo_path') or None,
|
photo_path = data.get('photo_path') or None,
|
||||||
status = 'open',
|
status = 'open',
|
||||||
reported_at = now_eastern(),
|
reported_at = now_eastern(),
|
||||||
|
reported_by = user.id,
|
||||||
mobile_local_id = mobile_local_id,
|
mobile_local_id = mobile_local_id,
|
||||||
)
|
)
|
||||||
db.session.add(issue)
|
db.session.add(issue)
|
||||||
@@ -264,7 +273,7 @@ def get_issue(issue_id):
|
|||||||
if issue is None:
|
if issue is None:
|
||||||
return api_error('Issue not found', 404)
|
return api_error('Issue not found', 404)
|
||||||
|
|
||||||
if user.role == 'inspector' and issue.assigned_to != user.id:
|
if user.role == 'inspector' and issue.assigned_to != user.id and issue.reported_by != user.id:
|
||||||
return api_error('Access denied', 403)
|
return api_error('Access denied', 403)
|
||||||
|
|
||||||
return api_ok(_issue_payload(issue))
|
return api_ok(_issue_payload(issue))
|
||||||
@@ -294,8 +303,8 @@ def update_issue_status(issue_id):
|
|||||||
if issue is None:
|
if issue is None:
|
||||||
return api_error('Issue not found', 404)
|
return api_error('Issue not found', 404)
|
||||||
|
|
||||||
if user.role == 'inspector' and issue.assigned_to != user.id:
|
if user.role == 'inspector' and issue.assigned_to != user.id and issue.reported_by != user.id:
|
||||||
return api_error('Access denied — you can only update issues assigned to you', 403)
|
return api_error('Access denied — you can only update issues assigned to or reported by you', 403)
|
||||||
|
|
||||||
data = request.get_json(silent=True) or {}
|
data = request.get_json(silent=True) or {}
|
||||||
new_status = (data.get('status') or '').strip().lower()
|
new_status = (data.get('status') or '').strip().lower()
|
||||||
|
|||||||
@@ -54,6 +54,11 @@ class Issue(db.Model):
|
|||||||
photo_path = db.Column(db.String(255))
|
photo_path = db.Column(db.String(255))
|
||||||
status = db.Column(db.Enum('open', 'in_progress', 'resolved', 'pending_verification'), default='open')
|
status = db.Column(db.Enum('open', 'in_progress', 'resolved', 'pending_verification'), default='open')
|
||||||
assigned_to = db.Column(db.Integer, db.ForeignKey('users.id'))
|
assigned_to = db.Column(db.Integer, db.ForeignKey('users.id'))
|
||||||
|
# Set at creation time to the user who filed the issue (inspector or admin).
|
||||||
|
# Nullable for backward compatibility — pre-phase18 rows will be NULL.
|
||||||
|
# Used by the mobile API to return issues the inspector created but hasn't
|
||||||
|
# been assigned yet (assigned_to is NULL until a director assigns them).
|
||||||
|
reported_by = db.Column(db.Integer, db.ForeignKey('users.id', ondelete='SET NULL'), nullable=True)
|
||||||
reported_at = db.Column(db.DateTime, default=now_eastern)
|
reported_at = db.Column(db.DateTime, default=now_eastern)
|
||||||
resolved_at = db.Column(db.DateTime)
|
resolved_at = db.Column(db.DateTime)
|
||||||
result_notes = db.Column(db.Text)
|
result_notes = db.Column(db.Text)
|
||||||
@@ -75,6 +80,7 @@ class Issue(db.Model):
|
|||||||
# with that backref at mapper configuration time (CLAUDE.md rule 31 revised).
|
# with that backref at mapper configuration time (CLAUDE.md rule 31 revised).
|
||||||
facility = db.relationship('Facility', foreign_keys=[facility_id], backref='direct_issues')
|
facility = db.relationship('Facility', foreign_keys=[facility_id], backref='direct_issues')
|
||||||
assigned_user = db.relationship('User', foreign_keys=[assigned_to], backref='assigned_issues')
|
assigned_user = db.relationship('User', foreign_keys=[assigned_to], backref='assigned_issues')
|
||||||
|
reporter = db.relationship('User', foreign_keys=[reported_by], backref='reported_issues')
|
||||||
verifier = db.relationship('User', foreign_keys=[verified_by], backref='verified_issues')
|
verifier = db.relationship('User', foreign_keys=[verified_by], backref='verified_issues')
|
||||||
comments = db.relationship('IssueComment', backref='issue', lazy='dynamic',
|
comments = db.relationship('IssueComment', backref='issue', lazy='dynamic',
|
||||||
order_by='IssueComment.created_at',
|
order_by='IssueComment.created_at',
|
||||||
|
|||||||
@@ -110,9 +110,14 @@ def index():
|
|||||||
recent_inspections = recent_q.limit(5).all()
|
recent_inspections = recent_q.limit(5).all()
|
||||||
|
|
||||||
# ── Pending follow-up inspections ────────────────────────────────────
|
# ── Pending follow-up inspections ────────────────────────────────────
|
||||||
|
# follow_ups is a lazy='dynamic' relationship — comparing it to None does
|
||||||
|
# NOT produce a "has no rows" predicate for dynamic relationships. The
|
||||||
|
# correct idiom is ~.any(), which generates EXISTS (SELECT 1 FROM inspections
|
||||||
|
# WHERE parent_inspection_id = inspections.id). This matches the identical
|
||||||
|
# filter used in routes/inspections.py:follow_up_filter.
|
||||||
followup_q = Inspection.query.filter_by(
|
followup_q = Inspection.query.filter_by(
|
||||||
follow_up_required=True, status='completed'
|
follow_up_required=True, status='completed'
|
||||||
).filter(Inspection.follow_ups == None) # noqa: E711 — SQLAlchemy usage
|
).filter(~Inspection.follow_ups.any())
|
||||||
if is_inspector:
|
if is_inspector:
|
||||||
followup_q = followup_q.filter(Inspection.inspector_id == current_user.id)
|
followup_q = followup_q.filter(Inspection.inspector_id == current_user.id)
|
||||||
elif is_customer:
|
elif is_customer:
|
||||||
|
|||||||
@@ -730,6 +730,7 @@ def flag_issue(inspection_id):
|
|||||||
status = 'open',
|
status = 'open',
|
||||||
assigned_to = form.assigned_to.data or None,
|
assigned_to = form.assigned_to.data or None,
|
||||||
reported_at = now_eastern(),
|
reported_at = now_eastern(),
|
||||||
|
reported_by = current_user.id,
|
||||||
)
|
)
|
||||||
db.session.add(issue)
|
db.session.add(issue)
|
||||||
|
|
||||||
|
|||||||
@@ -447,6 +447,7 @@ def create():
|
|||||||
status = 'open',
|
status = 'open',
|
||||||
assigned_to = form.assigned_to.data or None,
|
assigned_to = form.assigned_to.data or None,
|
||||||
reported_at = now_eastern(),
|
reported_at = now_eastern(),
|
||||||
|
reported_by = current_user.id,
|
||||||
)
|
)
|
||||||
db.session.add(issue)
|
db.session.add(issue)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
"""phase18 — add reported_by column to issues table
|
||||||
|
|
||||||
|
Background
|
||||||
|
----------
|
||||||
|
Issues created on the iPad by an inspector have no `assigned_to` value until
|
||||||
|
a director assigns them via the web portal. The mobile API's list_issues
|
||||||
|
endpoint filtered inspectors to `assigned_to == user.id`, so their own
|
||||||
|
newly-submitted issues were invisible on the iPad until assigned.
|
||||||
|
|
||||||
|
This migration adds `reported_by INT NULL FK → users.id` so the API can
|
||||||
|
return issues the inspector either created OR was assigned to, without
|
||||||
|
a join to the inspections table.
|
||||||
|
|
||||||
|
The column is nullable for backward compatibility: existing issues created
|
||||||
|
before this migration will have reported_by = NULL and continue to surface
|
||||||
|
only via the assigned_to path.
|
||||||
|
|
||||||
|
Uses INFORMATION_SCHEMA existence check — safe to re-run (CLAUDE.md rules 16, 17).
|
||||||
|
|
||||||
|
Revision ID: phase18_issue_reported_by
|
||||||
|
Revises: phase17_notification_event_type
|
||||||
|
"""
|
||||||
|
|
||||||
|
revision = 'phase18_issue_reported_by'
|
||||||
|
down_revision = 'phase17_notification_event_type'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
def _column_exists(conn, table, column):
|
||||||
|
result = conn.execute(sa.text(
|
||||||
|
"SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS "
|
||||||
|
"WHERE TABLE_SCHEMA = DATABASE() "
|
||||||
|
"AND TABLE_NAME = :table AND COLUMN_NAME = :col"
|
||||||
|
), {"table": table, "col": column})
|
||||||
|
return result.scalar() > 0
|
||||||
|
|
||||||
|
|
||||||
|
def _fk_exists(conn, table, constraint_name):
|
||||||
|
result = conn.execute(sa.text(
|
||||||
|
"SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS "
|
||||||
|
"WHERE TABLE_SCHEMA = DATABASE() "
|
||||||
|
"AND TABLE_NAME = :table AND CONSTRAINT_NAME = :name"
|
||||||
|
), {"table": table, "name": constraint_name})
|
||||||
|
return result.scalar() > 0
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
bind = op.get_bind()
|
||||||
|
|
||||||
|
if not _column_exists(bind, 'issues', 'reported_by'):
|
||||||
|
op.execute(sa.text(
|
||||||
|
"ALTER TABLE issues "
|
||||||
|
"ADD COLUMN reported_by INT NULL, "
|
||||||
|
"ADD CONSTRAINT fk_issues_reported_by "
|
||||||
|
" FOREIGN KEY (reported_by) REFERENCES users(id) "
|
||||||
|
" ON DELETE SET NULL"
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
bind = op.get_bind()
|
||||||
|
|
||||||
|
if _fk_exists(bind, 'issues', 'fk_issues_reported_by'):
|
||||||
|
op.execute(sa.text(
|
||||||
|
"ALTER TABLE issues DROP FOREIGN KEY fk_issues_reported_by"
|
||||||
|
))
|
||||||
|
|
||||||
|
if _column_exists(bind, 'issues', 'reported_by'):
|
||||||
|
op.execute(sa.text(
|
||||||
|
"ALTER TABLE issues DROP COLUMN reported_by"
|
||||||
|
))
|
||||||
Reference in New Issue
Block a user