diff --git a/app/api/issues.py b/app/api/issues.py index eded629..45f9f56 100644 --- a/app/api/issues.py +++ b/app/api/issues.py @@ -58,10 +58,12 @@ def _issue_payload(issue): 'reported_at': issue.reported_at.isoformat() if issue.reported_at else None, 'resolved_at': issue.resolved_at.isoformat() if issue.resolved_at else None, '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 [], + # photo_path: primary evidence photo (first iPad photo). + # mobile_photo_paths: extra evidence photos from iPad (shown under Photo Evidence). + # result_photos: resolution photos added via the web update form. + 'photo_path': issue.photo_path or None, + 'mobile_photo_paths': issue.mobile_photo_paths or [], + 'result_photos': issue.result_photos or [], } @@ -204,26 +206,24 @@ def create_issue(): inspection = None # ── Create ──────────────────────────────────────────────────────────── - # result_photos may be supplied by the mobile app as a list of paths that - # were already uploaded via /api/v1/photos/upload (one call per photo). - # Validate that it is a list of non-empty strings; silently drop bad entries. - raw_result_photos = data.get('result_photos') - if isinstance(raw_result_photos, list): - result_photos = [p for p in raw_result_photos if isinstance(p, str) and p.strip()] - else: - result_photos = [] + # result_photos in the POST body = extra evidence photos from the iPad. + # Store in mobile_photo_paths (not result_photos) so they appear under + # "Photo Evidence" on the web, not "Resolution Details". + raw_mobile = data.get('result_photos') + mobile_photo_paths = [p for p in raw_mobile if isinstance(p, str) and p.strip()] \ + if isinstance(raw_mobile, list) else [] issue = Issue( - inspection_id = inspection_id, - facility_id = facility_id, - severity = severity, - description = description, - photo_path = data.get('photo_path') or None, - result_photos = result_photos or None, - status = 'open', - reported_at = now_eastern(), - reported_by = user.id, - mobile_local_id = mobile_local_id, + inspection_id = inspection_id, + facility_id = facility_id, + severity = severity, + description = description, + photo_path = data.get('photo_path') or None, + mobile_photo_paths = mobile_photo_paths or None, + status = 'open', + reported_at = now_eastern(), + reported_by = user.id, + mobile_local_id = mobile_local_id, ) db.session.add(issue) db.session.flush() # get issue.id @@ -352,17 +352,16 @@ def update_issue_status(issue_id): @jwt_required def update_issue_photos(issue_id): """ - Attach additional photos to an existing issue created from the mobile app. + Attach additional evidence photos to an issue created from the mobile app. - Called by the iOS app immediately after create_issue when the inspector - attached more than one photo. The photos are already uploaded to the server - via /api/v1/photos/upload; this endpoint stores their paths in result_photos. + Called by the iOS app after create_issue when the inspector attached more + than one photo. Photos are already uploaded via /api/v1/photos/upload. + Stored in mobile_photo_paths so they display under "Photo Evidence" on the + web, not "Resolution Details". Request JSON ------------ - { - "result_photos": ["uploads/issue_photos/a.jpg", "uploads/issue_photos/b.jpg"] - } + { "result_photos": ["uploads/issue_photos/a.jpg", "uploads/issue_photos/b.jpg"] } Response 200 ------------ @@ -376,12 +375,11 @@ def update_issue_photos(issue_id): if issue is None: return api_error('Issue not found', 404) - # Inspectors may only update issues they reported or are assigned to if user.role == 'inspector' and issue.assigned_to != user.id and issue.reported_by != user.id: return api_error('Access denied', 403) data = request.get_json(silent=True) or {} - raw = data.get('result_photos') + raw = data.get('result_photos') if not isinstance(raw, list): return api_error('result_photos must be a list of path strings', 400) @@ -390,19 +388,18 @@ def update_issue_photos(issue_id): if not new_photos: return api_error('result_photos must contain at least one valid path', 400) - # Merge with any existing result_photos rather than overwriting, - # so multiple PATCH calls (e.g. retry) are idempotent. - existing = issue.result_photos or [] + # Merge idempotently with any existing mobile_photo_paths + existing = issue.mobile_photo_paths or [] merged = existing + [p for p in new_photos if p not in existing] - issue.result_photos = merged + issue.mobile_photo_paths = merged db.session.commit() log_action(ACTION_UPDATE, 'Issue', issue.id, - f'result_photos updated (+{len(new_photos)} photos)', + f'mobile_photo_paths updated (+{len(new_photos)} photos)', f'source=mobile; updated_by={user.username}') logger.info('API ISSUES | photos_updated | issue_id=%d | added=%d | user=%s', issue.id, len(new_photos), user.username) - return api_ok({'issue_id': issue.id, 'result_photos_count': len(merged)}) \ No newline at end of file + return api_ok({'issue_id': issue.id, 'result_photos_count': len(merged)}) diff --git a/app/models/issue.py b/app/models/issue.py index 153080d..e06c2cb 100644 --- a/app/models/issue.py +++ b/app/models/issue.py @@ -63,6 +63,10 @@ class Issue(db.Model): resolved_at = db.Column(db.DateTime) result_notes = db.Column(db.Text) result_photos = db.Column(db.JSON) # list of relative paths e.g. ["uploads/issue_photos/abc.jpg"] + # Extra evidence photos submitted from the iPad at issue-creation time. + # Stored separately from result_photos (resolution photos added via web) + # so they display under "Photo Evidence" rather than "Resolution Details". + mobile_photo_paths = db.Column(db.JSON, nullable=True) # ── Resolution verification ────────────────────────────────────────── verified_by = db.Column(db.Integer, db.ForeignKey('users.id', ondelete='SET NULL'), nullable=True) diff --git a/app/templates/issues/issues_view.html b/app/templates/issues/issues_view.html index 1490c21..52b22f8 100644 --- a/app/templates/issues/issues_view.html +++ b/app/templates/issues/issues_view.html @@ -54,12 +54,23 @@
Description

{{ issue.description }}

- {% if issue.photo_path %} + {% if issue.photo_path or issue.mobile_photo_paths %}
Photo Evidence
- - - +
+ {% if issue.photo_path %} + + + + {% endif %} + {% for photo in (issue.mobile_photo_paths or []) %} + + + + {% endfor %} +
{% endif %} {% if issue.result_notes or issue.result_photos %} diff --git a/app/templates/issues/view.html b/app/templates/issues/view.html index 9581d43..b9a12ee 100644 --- a/app/templates/issues/view.html +++ b/app/templates/issues/view.html @@ -43,12 +43,23 @@
Description

{{ issue.description }}

- {% if issue.photo_path %} + {% if issue.photo_path or issue.mobile_photo_paths %}
Photo Evidence
- - - +
+ {% if issue.photo_path %} + + + + {% endif %} + {% for photo in (issue.mobile_photo_paths or []) %} + + + + {% endfor %} +
{% endif %} {% if issue.result_notes or issue.result_photos %} diff --git a/migrations/versions/phase19_issue_mobile_photos.py b/migrations/versions/phase19_issue_mobile_photos.py new file mode 100644 index 0000000..afe5eb4 --- /dev/null +++ b/migrations/versions/phase19_issue_mobile_photos.py @@ -0,0 +1,46 @@ +"""phase19 — add mobile_photo_paths column to issues table + +Background +---------- +Issues created on the iPad can have multiple evidence photos. The first photo +is stored in `photo_path` (existing single-string column). Additional photos +were previously stored in `result_photos` (intended for resolution photos), +causing them to appear under "Resolution Details" on the web instead of +"Photo Evidence". + +This migration adds `mobile_photo_paths JSON NULL` to store the extra +evidence photos from the iPad separately from resolution photos. + +Safe to re-run — uses INFORMATION_SCHEMA existence check. +""" + +import sqlalchemy as sa +from alembic import op + +revision = 'phase19_issue_mobile_photos' +down_revision = 'phase18_issue_reported_by' +branch_labels = None +depends_on = None + + +def _column_exists(bind, table, column): + result = bind.execute(sa.text( + "SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS " + "WHERE TABLE_SCHEMA = DATABASE() " + "AND TABLE_NAME = :t AND COLUMN_NAME = :c" + ), {"t": table, "c": column}) + return result.scalar() > 0 + + +def upgrade(): + bind = op.get_bind() + if not _column_exists(bind, 'issues', 'mobile_photo_paths'): + op.add_column('issues', sa.Column( + 'mobile_photo_paths', sa.JSON(), nullable=True + )) + + +def downgrade(): + bind = op.get_bind() + if _column_exists(bind, 'issues', 'mobile_photo_paths'): + op.execute(sa.text("ALTER TABLE issues DROP COLUMN mobile_photo_paths"))