05/16 Fix bugs 2
This commit is contained in:
@@ -50,6 +50,7 @@ def _user_payload(user: User) -> dict:
|
||||
return {
|
||||
'id': user.id,
|
||||
'username': user.username,
|
||||
'full_name': user.full_name or '',
|
||||
'email': user.email,
|
||||
'role': user.role,
|
||||
'created_at': user.created_at.isoformat() if user.created_at else None,
|
||||
|
||||
+14
-5
@@ -105,8 +105,16 @@ def list_issues():
|
||||
query = Issue.query
|
||||
|
||||
if user.role == 'inspector':
|
||||
# Inspectors see only issues assigned to them
|
||||
query = query.filter(Issue.assigned_to == user.id)
|
||||
# Inspectors see issues assigned to them OR issues they reported.
|
||||
# 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:
|
||||
# Broader roles: exclude resolved by default so the list stays manageable
|
||||
status_filter = request.args.get('status')
|
||||
@@ -204,6 +212,7 @@ def create_issue():
|
||||
photo_path = data.get('photo_path') or None,
|
||||
status = 'open',
|
||||
reported_at = now_eastern(),
|
||||
reported_by = user.id,
|
||||
mobile_local_id = mobile_local_id,
|
||||
)
|
||||
db.session.add(issue)
|
||||
@@ -264,7 +273,7 @@ def 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:
|
||||
if user.role == 'inspector' and issue.assigned_to != user.id and issue.reported_by != user.id:
|
||||
return api_error('Access denied', 403)
|
||||
|
||||
return api_ok(_issue_payload(issue))
|
||||
@@ -294,8 +303,8 @@ def update_issue_status(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)
|
||||
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 or reported by you', 403)
|
||||
|
||||
data = request.get_json(silent=True) or {}
|
||||
new_status = (data.get('status') or '').strip().lower()
|
||||
|
||||
@@ -54,6 +54,11 @@ class Issue(db.Model):
|
||||
photo_path = db.Column(db.String(255))
|
||||
status = db.Column(db.Enum('open', 'in_progress', 'resolved', 'pending_verification'), default='open')
|
||||
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)
|
||||
resolved_at = db.Column(db.DateTime)
|
||||
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).
|
||||
facility = db.relationship('Facility', foreign_keys=[facility_id], backref='direct_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')
|
||||
comments = db.relationship('IssueComment', backref='issue', lazy='dynamic',
|
||||
order_by='IssueComment.created_at',
|
||||
|
||||
@@ -110,9 +110,14 @@ def index():
|
||||
recent_inspections = recent_q.limit(5).all()
|
||||
|
||||
# ── 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(
|
||||
follow_up_required=True, status='completed'
|
||||
).filter(Inspection.follow_ups == None) # noqa: E711 — SQLAlchemy usage
|
||||
).filter(~Inspection.follow_ups.any())
|
||||
if is_inspector:
|
||||
followup_q = followup_q.filter(Inspection.inspector_id == current_user.id)
|
||||
elif is_customer:
|
||||
|
||||
@@ -730,6 +730,7 @@ def flag_issue(inspection_id):
|
||||
status = 'open',
|
||||
assigned_to = form.assigned_to.data or None,
|
||||
reported_at = now_eastern(),
|
||||
reported_by = current_user.id,
|
||||
)
|
||||
db.session.add(issue)
|
||||
|
||||
|
||||
@@ -447,6 +447,7 @@ def create():
|
||||
status = 'open',
|
||||
assigned_to = form.assigned_to.data or None,
|
||||
reported_at = now_eastern(),
|
||||
reported_by = current_user.id,
|
||||
)
|
||||
db.session.add(issue)
|
||||
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