Added stuffies to reports, like images and coordinate based geolocation
This commit is contained in:
@@ -7,3 +7,4 @@ pydantic-settings
|
||||
pymongo
|
||||
python-dotenv
|
||||
pika
|
||||
Pillow
|
||||
|
||||
@@ -18,7 +18,8 @@ class CreateReport:
|
||||
self.user_repo = user_repo
|
||||
|
||||
def execute(self, id_usuario: int, tipo_reporte: int, descripcion: str,
|
||||
ubicacion: Optional[str] = None) -> Dict[str, Any]:
|
||||
ubicacion: Optional[str] = None, lat: Optional[float] = None,
|
||||
lng: Optional[float] = None, image_filename: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Sends a create report message to RabbitMQ.
|
||||
Valida previamente:
|
||||
@@ -68,6 +69,9 @@ class CreateReport:
|
||||
tipo_reporte=tipo_reporte,
|
||||
descripcion=descripcion.strip(),
|
||||
ubicacion=ubicacion,
|
||||
lat=lat,
|
||||
lng=lng,
|
||||
image_filename=image_filename,
|
||||
visibilidad=50.0, # Visibilidad inicial neutral
|
||||
fecha_creacion=fecha_creacion.isoformat()
|
||||
)
|
||||
|
||||
@@ -3,6 +3,8 @@ import sys
|
||||
import os
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from shutil import move
|
||||
|
||||
# Add src to path to import modules
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||
@@ -11,7 +13,9 @@ from infrastructure.adapters.rabbitmq.consumer import RabbitMQConsumer
|
||||
from infrastructure.adapters.rabbitmq.messages import ReportMessage, ReportEventType
|
||||
from infrastructure.adapters.persistence.report_repository_mongo import ReportRepositoryMongo
|
||||
from infrastructure.adapters.persistence.user_repository_sql import UserRepositorySQL
|
||||
from infrastructure.adapters.file_storage import image_storage
|
||||
from domain.reports import Report
|
||||
from core.config import ConfSettings
|
||||
|
||||
# Set up logging
|
||||
logging.basicConfig(
|
||||
@@ -64,6 +68,25 @@ class ReportConsumer:
|
||||
# Parse datetime string
|
||||
fecha_creacion = datetime.fromisoformat(message.fecha_creacion)
|
||||
|
||||
# Renombrar imagen temporal si existe
|
||||
final_image_filename = None
|
||||
if message.image_filename:
|
||||
try:
|
||||
# Renombrar de temp_userid_type a report_id
|
||||
temp_path = image_storage.get_image_path(message.image_filename)
|
||||
final_filename = f"{message.id_reporte}.webp"
|
||||
final_path = image_storage.get_image_path(final_filename)
|
||||
|
||||
if temp_path.exists():
|
||||
move(str(temp_path), str(final_path))
|
||||
final_image_filename = final_filename
|
||||
logger.info(f"Image renamed from {message.image_filename} to {final_filename}")
|
||||
else:
|
||||
logger.warning(f"Temporary image not found: {message.image_filename}")
|
||||
except Exception as img_error:
|
||||
logger.error(f"Error renaming image: {img_error}", exc_info=True)
|
||||
# Continuar sin imagen si falla el renombramiento
|
||||
|
||||
# Create Report domain object
|
||||
report = Report(
|
||||
id_reporte=message.id_reporte,
|
||||
@@ -71,6 +94,9 @@ class ReportConsumer:
|
||||
tipo_reporte=message.tipo_reporte,
|
||||
descripcion=message.descripcion,
|
||||
ubicacion=message.ubicacion,
|
||||
lat=message.lat,
|
||||
lng=message.lng,
|
||||
image_filename=final_image_filename,
|
||||
visibilidad=message.visibilidad,
|
||||
fecha_creacion=fecha_creacion
|
||||
)
|
||||
@@ -146,9 +172,21 @@ class ReportConsumer:
|
||||
try:
|
||||
logger.info(f"Deleting report: {message.id_reporte}")
|
||||
|
||||
# Obtener reportepara acceder a image_filename antes de eliminarlo
|
||||
report = self.repo.find_by_id(message.id_reporte)
|
||||
|
||||
# Eliminar del MongoDB
|
||||
success = self.repo.delete(message.id_reporte)
|
||||
if success:
|
||||
logger.info(f"Report deleted successfully: {message.id_reporte}")
|
||||
logger.info(f"Report deleted successfully from MongoDB: {message.id_reporte}")
|
||||
|
||||
# Eliminar imagen del almacenamiento
|
||||
if report and report.image_filename:
|
||||
deleted = image_storage.delete_image(report.image_filename)
|
||||
if deleted:
|
||||
logger.info(f"Image deleted: {report.image_filename}")
|
||||
else:
|
||||
logger.warning(f"Could not delete image: {report.image_filename}")
|
||||
else:
|
||||
logger.warning(f"Failed to delete report: {message.id_reporte}")
|
||||
|
||||
|
||||
@@ -35,6 +35,28 @@ class Settings(BaseSettings):
|
||||
host: str = "0.0.0.0"
|
||||
log_level: str = "info"
|
||||
|
||||
# Almacenamiento de archivos
|
||||
storage_base_path: str = Field(
|
||||
default=os.getenv("STORAGE_BASE_PATH", os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "storage")),
|
||||
description="Ruta base para almacenamiento de archivos"
|
||||
)
|
||||
images_dir: str = Field(
|
||||
default="reports_images",
|
||||
description="Directorio para imágenes de reportes (dentro de storage_base_path)"
|
||||
)
|
||||
images_max_size_mb: int = Field(
|
||||
default=int(os.getenv("IMAGES_MAX_SIZE_MB", 4)),
|
||||
description="Tamaño máximo de imagen en MB"
|
||||
)
|
||||
images_allowed_types: list = Field(
|
||||
default=["image/jpeg", "image/png", "image/webp"],
|
||||
description="Tipos MIME permitidos para imágenes"
|
||||
)
|
||||
images_compression_quality: int = Field(
|
||||
default=int(os.getenv("IMAGES_COMPRESSION_QUALITY", 80)),
|
||||
description="Calidad de compresión WebP (0-100)"
|
||||
)
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
case_sensitive = False
|
||||
|
||||
@@ -10,5 +10,8 @@ class Report:
|
||||
tipo_reporte: int # Número que representa el tipo
|
||||
descripcion: str
|
||||
ubicacion: Optional[str]
|
||||
visibilidad: float # 0-100 (puntuación comunitaria)
|
||||
lat: Optional[float] = None
|
||||
lng: Optional[float] = None
|
||||
image_filename: Optional[str] = None # Nombre del archivo de imagen WebP
|
||||
visibilidad: float = 0.0 # 0-100 (puntuación comunitaria)
|
||||
fecha_creacion: Optional[datetime] = None
|
||||
|
||||
@@ -19,6 +19,9 @@ class ReportRepositoryMongo(ReportRepository):
|
||||
"tipo_reporte": report.tipo_reporte,
|
||||
"descripcion": report.descripcion,
|
||||
"ubicacion": report.ubicacion,
|
||||
"lat": report.lat,
|
||||
"lng": report.lng,
|
||||
"image_filename": report.image_filename,
|
||||
"visibilidad": report.visibilidad,
|
||||
"fecha_creacion": report.fecha_creacion or datetime.utcnow()
|
||||
}
|
||||
@@ -76,6 +79,9 @@ class ReportRepositoryMongo(ReportRepository):
|
||||
tipo_reporte=doc.get("tipo_reporte"),
|
||||
descripcion=doc.get("descripcion"),
|
||||
ubicacion=doc.get("ubicacion"),
|
||||
lat=doc.get("lat"),
|
||||
lng=doc.get("lng"),
|
||||
image_filename=doc.get("image_filename"),
|
||||
visibilidad=doc.get("visibilidad"),
|
||||
fecha_creacion=doc.get("fecha_creacion")
|
||||
)
|
||||
|
||||
@@ -61,6 +61,9 @@ class ReportMessage:
|
||||
tipo_reporte: Optional[int] = None
|
||||
descripcion: Optional[str] = None
|
||||
ubicacion: Optional[str] = None
|
||||
lat: Optional[float] = None
|
||||
lng: Optional[float] = None
|
||||
image_filename: Optional[str] = None # Nombre del archivo de imagen guardado
|
||||
visibilidad: Optional[float] = None
|
||||
fecha_creacion: Optional[str] = None # ISO format datetime string
|
||||
penalize_author: Optional[bool] = None # For update_visibility event
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from pathlib import Path
|
||||
from core.config import ConfSettings
|
||||
from infrastructure.api.reports.router import router
|
||||
|
||||
@@ -10,4 +12,10 @@ def create_app() -> FastAPI:
|
||||
description="Microservicio de gestión de reportes comunitarios"
|
||||
)
|
||||
app.include_router(router)
|
||||
|
||||
# Montar directorio de almacenamiento de imágenes como rutas estáticas
|
||||
images_path = Path(ConfSettings.storage_base_path) / ConfSettings.images_dir
|
||||
images_path.mkdir(parents=True, exist_ok=True)
|
||||
app.mount("/images", StaticFiles(directory=str(images_path)), name="images")
|
||||
|
||||
return app
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from fastapi import APIRouter, HTTPException, status, UploadFile, File, Form
|
||||
from infrastructure.api.reports.schemas import ReportCreateRequest, ReportUpdateVisibilityRequest, ReportResponse
|
||||
from application.services.report_services import (
|
||||
CreateReport, GetReportById, GetReportsByUser, ListAllReports,
|
||||
@@ -6,26 +6,65 @@ from application.services.report_services import (
|
||||
)
|
||||
from infrastructure.adapters.persistence.report_repository_mongo import ReportRepositoryMongo
|
||||
from infrastructure.adapters.persistence.user_repository_sql import UserRepositorySQL
|
||||
from infrastructure.adapters.file_storage import image_storage
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
router = APIRouter()
|
||||
report_repo = ReportRepositoryMongo()
|
||||
user_repo = UserRepositorySQL()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _report_to_response(report) -> dict:
|
||||
"""Convierte un objeto Report a dict con image_url"""
|
||||
return {
|
||||
"id_reporte": report.id_reporte,
|
||||
"id_usuario": report.id_usuario,
|
||||
"tipo_reporte": report.tipo_reporte,
|
||||
"descripcion": report.descripcion,
|
||||
"ubicacion": report.ubicacion,
|
||||
"lat": report.lat,
|
||||
"lng": report.lng,
|
||||
"image_url": image_storage.get_image_url(report.image_filename) if report.image_filename else None,
|
||||
"visibilidad": report.visibilidad,
|
||||
"fecha_creacion": report.fecha_creacion
|
||||
}
|
||||
|
||||
@router.post("/", status_code=status.HTTP_202_ACCEPTED)
|
||||
async def create_report(report_data: ReportCreateRequest):
|
||||
async def create_report(
|
||||
id_usuario: int = Form(...),
|
||||
tipo_reporte: int = Form(...),
|
||||
descripcion: str = Form(...),
|
||||
ubicacion: Optional[str] = Form(None),
|
||||
lat: Optional[float] = Form(None),
|
||||
lng: Optional[float] = Form(None),
|
||||
file: Optional[UploadFile] = File(None)
|
||||
):
|
||||
"""Crea un nuevo reporte - envía a cola de procesamiento con validaciones previas"""
|
||||
try:
|
||||
# Procesar imagen si fue proporcionada
|
||||
image_filename = None
|
||||
if file:
|
||||
logger.info(f"Processing image file: {file.filename} ({file.content_type})")
|
||||
image_filename = image_storage.validate_and_save_image(file, f"temp_{id_usuario}_{tipo_reporte}")
|
||||
|
||||
create_use_case = CreateReport(report_repo, user_repo)
|
||||
result = create_use_case.execute(
|
||||
id_usuario=report_data.id_usuario,
|
||||
tipo_reporte=report_data.tipo_reporte,
|
||||
descripcion=report_data.descripcion,
|
||||
ubicacion=report_data.ubicacion
|
||||
id_usuario=id_usuario,
|
||||
tipo_reporte=tipo_reporte,
|
||||
descripcion=descripcion,
|
||||
ubicacion=ubicacion,
|
||||
lat=lat,
|
||||
lng=lng,
|
||||
image_filename=image_filename
|
||||
)
|
||||
|
||||
if result["status"] == "error":
|
||||
# Si hay error, eliminar imagen si fue guardada
|
||||
if image_filename:
|
||||
image_storage.delete_image(image_filename)
|
||||
|
||||
message = result["message"]
|
||||
if "no existe" in message:
|
||||
# 404 Not Found: usuario no existe
|
||||
@@ -63,7 +102,7 @@ async def get_report(report_id: str):
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Reporte con ID {report_id} no encontrado"
|
||||
)
|
||||
return report
|
||||
return _report_to_response(report)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
@@ -79,7 +118,7 @@ async def get_user_reports(user_id: int):
|
||||
try:
|
||||
get_use_case = GetReportsByUser(report_repo)
|
||||
reports = get_use_case.execute(user_id)
|
||||
return reports
|
||||
return [_report_to_response(report) for report in reports]
|
||||
except Exception as e:
|
||||
logger.error(f"Error al obtener reportes del usuario {user_id}: {e}", exc_info=True)
|
||||
raise HTTPException(
|
||||
@@ -92,7 +131,8 @@ async def list_reports():
|
||||
"""Obtiene todos los reportes - retorna lista vacía si no hay reportes"""
|
||||
try:
|
||||
list_use_case = ListAllReports(report_repo)
|
||||
return list_use_case.execute()
|
||||
reports = list_use_case.execute()
|
||||
return [_report_to_response(report) for report in reports]
|
||||
except Exception as e:
|
||||
logger.error(f"Error al listar reportes: {e}", exc_info=True)
|
||||
raise HTTPException(
|
||||
@@ -105,7 +145,8 @@ async def get_shadowbanned_reports(threshold: float = 20):
|
||||
"""Obtiene reportes shadowbaneados (baja visibilidad) - retorna lista vacía si no hay"""
|
||||
try:
|
||||
get_use_case = GetShadowbannedReports(report_repo)
|
||||
return get_use_case.execute(threshold)
|
||||
reports = get_use_case.execute(threshold)
|
||||
return [_report_to_response(report) for report in reports]
|
||||
except Exception as e:
|
||||
logger.error(f"Error al obtener reportes shadowbaneados: {e}", exc_info=True)
|
||||
raise HTTPException(
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from fastapi import Form, UploadFile, File
|
||||
|
||||
class ReportCreateRequest(BaseModel):
|
||||
"""Solicitud para crear un reporte"""
|
||||
"""Solicitud para crear un reporte - usa multipart/form-data en FastAPI"""
|
||||
id_usuario: int
|
||||
tipo_reporte: int
|
||||
descripcion: str
|
||||
ubicacion: Optional[str] = None
|
||||
lat: Optional[float] = None
|
||||
lng: Optional[float] = None
|
||||
# file se recibe como UploadFile en el endpoint, no en el modelo
|
||||
|
||||
class ReportUpdateVisibilityRequest(BaseModel):
|
||||
"""Solicitud para actualizar la visibilidad de un reporte"""
|
||||
@@ -21,6 +25,9 @@ class ReportResponse(BaseModel):
|
||||
tipo_reporte: int
|
||||
descripcion: str
|
||||
ubicacion: Optional[str]
|
||||
lat: Optional[float]
|
||||
lng: Optional[float]
|
||||
image_url: Optional[str] = None # URL pública para acceder a la imagen
|
||||
visibilidad: float
|
||||
fecha_creacion: Optional[datetime]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user