First commit
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
"""
|
||||
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'}
|
||||
|
||||
|
||||
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" (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'
|
||||
else:
|
||||
subfolder = 'inspection_photos'
|
||||
|
||||
ext = file_obj.filename.rsplit('.', 1)[-1].lower()
|
||||
filename = f'{uuid.uuid4().hex}.{ext}'
|
||||
|
||||
dest_dir = os.path.join(current_app.config['UPLOAD_FOLDER'], subfolder)
|
||||
os.makedirs(dest_dir, exist_ok=True)
|
||||
|
||||
dest_path = os.path.join(dest_dir, filename)
|
||||
file_obj.save(dest_path)
|
||||
|
||||
server_path = f'uploads/{subfolder}/{filename}'
|
||||
|
||||
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})
|
||||
Reference in New Issue
Block a user