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:
@@ -1,9 +1,11 @@
|
||||
# app/services/ticket_service.py
|
||||
import uuid
|
||||
from typing import List, Optional
|
||||
from pathlib import Path
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.exc import NoResultFound
|
||||
from app.models.ticket_model import User, Ticket, TicketAudit, TicketStatus, SeverityLevel
|
||||
from app.utils import normalize_image_path_for_url, UPLOADS_DIR_RESOLVED
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
@@ -26,8 +28,6 @@ class TicketService:
|
||||
self.db.refresh(user)
|
||||
logger.info(f"Created user {user}")
|
||||
return user # <-- return User object
|
||||
|
||||
|
||||
|
||||
def get_user(self, user_id: str) -> Optional[User]:
|
||||
return self.db.query(User).filter(User.id == user_id).first()
|
||||
@@ -44,16 +44,27 @@ class TicketService:
|
||||
latitude: float,
|
||||
longitude: float,
|
||||
description: str = "",
|
||||
address: Optional[str] = None,
|
||||
) -> Ticket:
|
||||
"""
|
||||
Create a Ticket record.
|
||||
|
||||
image_path should be a relative POSIX path (e.g. 'static/uploads/uuid.jpg').
|
||||
report.route uses Path.as_posix() to ensure forward slashes on save.
|
||||
"""
|
||||
# Normalize stored path to POSIX
|
||||
image_path_posix = Path(str(image_path)).as_posix() if image_path else None
|
||||
|
||||
ticket = Ticket(
|
||||
id=str(uuid.uuid4()),
|
||||
user_id=user_id,
|
||||
image_path=image_path,
|
||||
image_path=image_path_posix,
|
||||
category=category,
|
||||
severity=severity,
|
||||
latitude=latitude,
|
||||
longitude=longitude,
|
||||
description=description,
|
||||
address=address,
|
||||
)
|
||||
self.db.add(ticket)
|
||||
self.db.commit()
|
||||
@@ -91,6 +102,9 @@ class TicketService:
|
||||
severity: Optional[SeverityLevel] = None,
|
||||
status: Optional[TicketStatus] = None
|
||||
) -> List[Ticket]:
|
||||
"""
|
||||
Return tickets. By default returns all tickets unless optional filters are provided.
|
||||
"""
|
||||
query = self.db.query(Ticket)
|
||||
if user_id:
|
||||
query = query.filter(Ticket.user_id == user_id)
|
||||
@@ -101,3 +115,57 @@ class TicketService:
|
||||
if status:
|
||||
query = query.filter(Ticket.status == status)
|
||||
return query.order_by(Ticket.created_at.desc()).all()
|
||||
|
||||
def delete_ticket(self, ticket_id: str) -> bool:
|
||||
"""
|
||||
Delete a ticket and its associated image file if it exists.
|
||||
|
||||
Safety:
|
||||
- Normalize stored image_path to a relative POSIX path under the static/ mount using
|
||||
normalize_image_path_for_url().
|
||||
- Resolve the resulting path and only delete if the resolved path is under the configured
|
||||
uploads directory (UPLOADS_DIR_RESOLVED) to prevent path traversal.
|
||||
"""
|
||||
ticket = self.db.query(Ticket).filter(Ticket.id == ticket_id).first()
|
||||
if not ticket:
|
||||
raise NoResultFound(f"Ticket with id {ticket_id} not found")
|
||||
|
||||
# Attempt to delete the image file if present
|
||||
try:
|
||||
rel = normalize_image_path_for_url(ticket.image_path)
|
||||
if rel:
|
||||
file_path = Path(rel)
|
||||
# Resolve to absolute path safely (works if file missing too)
|
||||
try:
|
||||
absolute = file_path.resolve()
|
||||
except Exception:
|
||||
absolute = (Path.cwd() / file_path).resolve()
|
||||
|
||||
# Ensure the file is inside the uploads directory
|
||||
try:
|
||||
absolute.relative_to(UPLOADS_DIR_RESOLVED)
|
||||
inside_uploads = True
|
||||
except Exception:
|
||||
inside_uploads = False
|
||||
|
||||
if inside_uploads and absolute.exists():
|
||||
try:
|
||||
absolute.unlink()
|
||||
logger.info(f"Deleted image file: {absolute}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to delete image file {absolute}: {e}")
|
||||
else:
|
||||
logger.debug(f"Image file not deleted (missing or outside uploads): {absolute}")
|
||||
except Exception as e:
|
||||
logger.exception(f"Error while attempting to remove image for ticket {ticket_id}: {e}")
|
||||
|
||||
# Delete ticket record
|
||||
try:
|
||||
self.db.delete(ticket)
|
||||
self.db.commit()
|
||||
logger.info(f"Deleted ticket {ticket_id}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.exception(f"Failed to delete ticket {ticket_id} from DB: {e}")
|
||||
self.db.rollback()
|
||||
raise
|
||||
Reference in New Issue
Block a user