Added stuffies to reports, like images and coordinate based geolocation

This commit is contained in:
2026-04-06 23:48:19 -06:00
parent 4395a81815
commit f812d4a664
10 changed files with 147 additions and 14 deletions

View File

@@ -7,3 +7,4 @@ pydantic-settings
pymongo
python-dotenv
pika
Pillow

View File

@@ -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()
)

View File

@@ -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}")

View File

@@ -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

View File

@@ -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

View File

@@ -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")
)

View File

@@ -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

View File

@@ -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

View File

@@ -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(

View File

@@ -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]