Files
JQC_multi_tenant/app/api/photos.py
T

96 lines
2.9 KiB
Python

"""
app/api/photos.py
-----------------
Mobile API endpoint for uploading photos from the iPad app.
POST /api/v1/photos/upload
Accepts a multipart/form-data upload.
Saves the file to the server's upload folder.
Returns the relative server path used in subsequent inspection/issue submissions.
The iPad app uploads photos BEFORE submitting the inspection or issue,
then includes the returned server_path in the inspection/issue payload.
"""
import os
import uuid
import logging
from flask import Blueprint, request, g, current_app
from app.api.errors import api_ok, api_error
from app.api.decorators import jwt_required
logger = logging.getLogger(__name__)
bp = Blueprint('api_photos', __name__)
_ALLOWED_EXTENSIONS = {'jpg', 'jpeg', 'png', 'gif'}
_ALLOWED_ROLES = {'admin', 'director', 'inspector', 'project_manager', 'auditor'}
def _allowed_file(filename: str) -> bool:
return (
'.' in filename
and filename.rsplit('.', 1)[-1].lower() in _ALLOWED_EXTENSIONS
)
# ── Upload Photo ──────────────────────────────────────────────────────────────
@bp.route('/photos/upload', methods=['POST'])
@jwt_required
def upload_photo():
"""
Upload a photo from the iPad app.
Multipart form fields
---------------------
file — binary image data (jpg / png / gif)
entity_type — "inspection" | "issue" | "issue_result" (controls subfolder)
Response 200
------------
{
"ok": true,
"data": {
"server_path": "uploads/inspection_photos/abc123.jpg"
}
}
"""
user = g.api_user
if user.role not in _ALLOWED_ROLES:
return api_error('Access denied', 403)
if 'file' not in request.files:
return api_error('No file provided', 400)
file_obj = request.files['file']
entity_type = request.form.get('entity_type', 'inspection')
if not file_obj or not file_obj.filename:
return api_error('Empty file', 400)
if not _allowed_file(file_obj.filename):
return api_error(
f'File type not allowed. Accepted: {", ".join(sorted(_ALLOWED_EXTENSIONS))}',
400
)
# Determine destination subfolder
if entity_type == 'issue':
subfolder = 'issue_photos'
elif entity_type == 'issue_result':
subfolder = 'issue_result_photos'
else:
subfolder = 'inspection_photos'
# Write via the active storage backend (local disk or R2). Key format
# 'uploads/<subfolder>/<uuid>.<ext>' is unchanged across backends.
from app.utils import storage
server_path = storage.save(file_obj, subfolder)
logger.info('API PHOTOS | uploaded | entity_type=%s | path=%s | user=%s',
entity_type, server_path, user.username)
return api_ok({'server_path': server_path})