feat(api,ui,db): add address, guest users, image URLs; update API
- Backend:
- Add address column to tickets and migration script
- Create guest users when user_id is missing; accept user_name and address
- Normalize stored image paths and expose absolute image_url
- Introduce utils for path normalization and ticket serialization
- Add CORS configuration for dashboard/emulator origins
- Tickets API:
- Serialize via ticket_to_dict with consistent schema
- Change status update to PATCH /api/tickets/{id}/status with JSON body
- Add DELETE /api/tickets/{id} with safe file removal
- Dashboard:
- Fetch tickets from backend, show thumbnails, absolute image URLs
- Status select + PATCH updates, toasts for feedback
- Add i18n key btn.viewDetails
- Mobile app:
- Persist device user_id via SharedPreferences
- Fetch and merge API tickets; prefer network imageUrl
- Submit user_name and address; delete via API when available
- Make location acquisition robust with fallbacks and non-blocking UX
- Android/deps:
- Disable Geolocator NMEA listener to prevent crashes
- Downgrade geolocator to ^11.0.0 for stability
BREAKING CHANGE:
- Status endpoint changed from PATCH /api/tickets/{id}?new_status=... to
PATCH /api/tickets/{id}/status with JSON body: {"status":"in_progress"}.
- /api/tickets and /api/tickets/{id} responses now use "id" (replacing
"ticket_id"), include "image_url", and normalize fields for clients. Update
consumers to use the new schema.
This commit is contained in:
100
backend/app/utils.py
Normal file
100
backend/app/utils.py
Normal file
@@ -0,0 +1,100 @@
|
||||
from typing import Optional
|
||||
from pathlib import Path
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
# Path where uploaded images are stored (relative to project root)
|
||||
UPLOADS_DIR = Path("static") / "uploads"
|
||||
# Resolved absolute path for safety checks
|
||||
try:
|
||||
UPLOADS_DIR_RESOLVED = UPLOADS_DIR.resolve()
|
||||
except Exception:
|
||||
UPLOADS_DIR_RESOLVED = UPLOADS_DIR
|
||||
|
||||
def normalize_image_path_for_url(image_path: Optional[str]) -> Optional[str]:
|
||||
"""
|
||||
Normalize stored image_path to a web-accessible relative path under the /static/ mount.
|
||||
Examples:
|
||||
r"static\\uploads\\uuid.jpg" -> "static/uploads/uuid.jpg"
|
||||
"C:\\project\\static\\uploads\\uuid.jpg" -> "static/uploads/uuid.jpg"
|
||||
"uploads/uuid.jpg" -> "static/uploads/uuid.jpg"
|
||||
"""
|
||||
if not image_path:
|
||||
return None
|
||||
|
||||
p = str(image_path).replace("\\", "/")
|
||||
# strip leading './' and leading slashes
|
||||
while p.startswith("./"):
|
||||
p = p[2:]
|
||||
p = p.lstrip("/")
|
||||
|
||||
# prefer existing 'static/' segment if present
|
||||
if "static/" in p:
|
||||
p = p[p.find("static/"):]
|
||||
elif p.startswith("uploads/"):
|
||||
p = f"static/{p}"
|
||||
else:
|
||||
# fallback to treating as filename only
|
||||
p = f"static/uploads/{Path(p).name}"
|
||||
|
||||
# collapse accidental duplicates
|
||||
p = p.replace("static/static", "static").replace("uploads/uploads", "uploads")
|
||||
return p
|
||||
|
||||
def make_image_url(image_path: Optional[str], request) -> Optional[str]:
|
||||
"""
|
||||
Build an absolute URL for the given stored image_path using the FastAPI request.
|
||||
Returns None if image_path is falsy.
|
||||
"""
|
||||
rel = normalize_image_path_for_url(image_path)
|
||||
if not rel:
|
||||
return None
|
||||
base = str(request.base_url).rstrip("/")
|
||||
return f"{base}/{rel.lstrip('/')}"
|
||||
|
||||
def ticket_to_dict(ticket, request=None) -> dict:
|
||||
"""
|
||||
Serialize a Ticket ORM object to the normalized schema expected by clients.
|
||||
|
||||
Schema:
|
||||
id, category, severity, status, description,
|
||||
user_id, user_name, user_email,
|
||||
created_at (ISO8601), latitude, longitude, address,
|
||||
image_url (absolute), image_path (relative POSIX under static/)
|
||||
"""
|
||||
created = None
|
||||
try:
|
||||
if getattr(ticket, "created_at", None):
|
||||
created = ticket.created_at.isoformat()
|
||||
except Exception:
|
||||
created = None
|
||||
|
||||
# Normalize stored image path to a safe relative POSIX path under 'static/'
|
||||
normalized_path = normalize_image_path_for_url(getattr(ticket, "image_path", None))
|
||||
|
||||
image_url = None
|
||||
if request is not None:
|
||||
try:
|
||||
image_url = make_image_url(normalized_path, request)
|
||||
except Exception:
|
||||
logger.exception("Failed to build image_url")
|
||||
image_url = None
|
||||
|
||||
return {
|
||||
"id": ticket.id,
|
||||
"category": ticket.category,
|
||||
"severity": ticket.severity.value if getattr(ticket, "severity", None) else None,
|
||||
"status": ticket.status.value if getattr(ticket, "status", None) else None,
|
||||
"description": ticket.description,
|
||||
"user_id": ticket.user_id,
|
||||
"user_name": ticket.user.name if getattr(ticket, "user", None) else None,
|
||||
"user_email": ticket.user.email if getattr(ticket, "user", None) else None,
|
||||
"created_at": created,
|
||||
"latitude": ticket.latitude,
|
||||
"longitude": ticket.longitude,
|
||||
"address": ticket.address,
|
||||
"image_url": image_url,
|
||||
"image_path": normalized_path
|
||||
}
|
||||
Reference in New Issue
Block a user