Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified __pycache__/app.cpython-311.pyc
Binary file not shown.
98 changes: 42 additions & 56 deletions app.py
Original file line number Diff line number Diff line change
@@ -1,50 +1,44 @@
import eventlet
eventlet.monkey_patch()


from util import socketio
import firebase_admin
from dotenv import load_dotenv
from components.contactos.routes import contactos_bp
from components.categorias.routes import categorias_bp
from components.funcionesAdmin.routes import admin_bp
from components.refugios.routes import overpass_bp
from components.roles.routes import roles_bp
from components.etiquetas.routes import etiquetas_bp
from components.ubicacion.routes import ubicacion_bp
from components.pdf.routes import pdf_bp
from components.qr.routes import qr_bp
from components.reportes.routes import reportes_bp
from components.notificaciones.routes import notificaciones_bp
from components.imagenes.routes import imagenes_bp
from components.comentarios.routes import comentarios_bp
from components.usuarios.routes import usuarios_bp
from components.publicaciones.routes import publicaciones_bp
from auth.routes import auth_bp
from core.models import db, Usuario
from flask_migrate import Migrate
from datetime import datetime
from functools import wraps
from flask_cors import CORS
from flask_sqlalchemy import SQLAlchemy
from flask import Flask, request, jsonify
from firebase_admin import credentials, auth as firebase_auth
import psycopg2
import os
import json
import builtins


# Mantenemos esto por compatibilidad de algunas libs viejas de Python 2/3
if not hasattr(builtins, "unicode"):
builtins.unicode = str

import os
import psycopg2
from firebase_admin import credentials, auth as firebase_auth
from flask import Flask, request, jsonify
from flask_sqlalchemy import SQLAlchemy
from flask_cors import CORS
from functools import wraps
from datetime import datetime
from flask_migrate import Migrate

# Imports de modelos y rutas
from core.models import db, Usuario
from auth.routes import auth_bp
from components.publicaciones.routes import publicaciones_bp
from components.usuarios.routes import usuarios_bp
from components.comentarios.routes import comentarios_bp
from components.imagenes.routes import imagenes_bp
from components.notificaciones.routes import notificaciones_bp
from components.reportes.routes import reportes_bp
from components.qr.routes import qr_bp
from components.pdf.routes import pdf_bp
from components.ubicacion.routes import ubicacion_bp
from components.etiquetas.routes import etiquetas_bp
from components.roles.routes import roles_bp
from components.refugios.routes import overpass_bp
from components.funcionesAdmin.routes import admin_bp
from components.categorias.routes import categorias_bp
from components.contactos.routes import contactos_bp

from dotenv import load_dotenv
import firebase_admin

#
load_dotenv()

app = Flask(__name__)


def cerrar_sesion():
"""
Cierra la sesión de base de datos de forma segura.
Expand All @@ -54,13 +48,12 @@ def cerrar_sesion():
except Exception as error:
print(f"Error al cerrar la sesión: {error}")


@app.teardown_appcontext
def shutdown_session(exception=None):
"""Cierra la sesión de base de datos al finalizar el contexto de la app."""
cerrar_sesion()


# Configuración Firebase
service_account_info = {
"type": os.environ.get("FIREBASE_TYPE"),
"project_id": os.environ.get("FIREBASE_PROJECT_ID"),
Expand All @@ -80,21 +73,17 @@ def shutdown_session(exception=None):
cred = credentials.Certificate(json.loads(os.environ["FIREBASE_CREDENTIALS"]))
firebase_admin.initialize_app(cred)

# Configuración de la base de datos con SQLAlchemy
# Configuración de SQLAlchemy con Supabase (Session Pooler)

# Configuración de la base de datos con SQLAlchemy (Session Pooler optimizado)
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv("DATABASE_URL")
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SQLALCHEMY_ENGINE_OPTIONS'] = {
# Mantiene máximo 5 conexiones abiertas permanentemente por instancia
"pool_size": 5,
"max_overflow": 10, # Permite crear 10 extra si hay mucha carga momentánea
"pool_timeout": 30, # Espera 30 seg por una conexión antes de dar error
# Recicla conexiones cada 30 mins para evitar que mueran silenciosamente
"pool_recycle": 1800,
# Verifica que la conexión sirva antes de usarla (VITAL)
"pool_pre_ping": True
"pool_size": 5,
"max_overflow": 10,
"pool_timeout": 30,
"pool_recycle": 1800,
"pool_pre_ping": True
}

db.init_app(app)
migrate = Migrate(app, db)
frontend_url = os.getenv("FRONTEND_URL") # * como fallback
Expand All @@ -106,14 +95,13 @@ def shutdown_session(exception=None):
methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
)


# Configuración a cloudinary
# Cloudinary
app.config['CLOUDINARY_CLOUD_NAME'] = os.getenv("CLOUDINARY_CLOUD_NAME")
app.config['CLOUDINARY_API_KEY'] = os.getenv("CLOUDINARY_API_KEY")
app.config['CLOUDINARY_API_SECRET'] = os.getenv("CLOUDINARY_API_SECRET")
app.config['CLOUDINARY_UPLOAD_PRESET'] = os.getenv("CLOUDINARY_UPLOAD_PRESET")

# Registrar Blueprints
app.register_blueprint(auth_bp)
app.register_blueprint(publicaciones_bp)
app.register_blueprint(usuarios_bp)
Expand All @@ -131,7 +119,6 @@ def shutdown_session(exception=None):
app.register_blueprint(categorias_bp)
app.register_blueprint(contactos_bp)


@app.before_request
def handle_options():
if request.method == "OPTIONS":
Expand All @@ -141,7 +128,6 @@ def handle_options():
headers["Access-Control-Allow-Origin"] = frontend_url
headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization"
headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, PATCH, DELETE, OPTIONS"

return resp


Expand All @@ -155,4 +141,4 @@ def health_check():
socketio.init_app(app)

if __name__ == '__main__':
socketio.run(app, host='0.0.0.0', port=5000, debug=True)
app.run(host='0.0.0.0', port=5000, debug=True)
23 changes: 21 additions & 2 deletions components/categorias/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,36 @@
from core.models import Categoria
categorias_bp = Blueprint("categorias", __name__)

from flask import Blueprint, jsonify
from core.models import Categoria, db
from functools import lru_cache # Importar esto

categorias_bp = Blueprint("categorias", __name__)

# Función auxiliar con caché
# maxsize=1 significa que guarda solo 1 resultado (la lista completa)
@lru_cache(maxsize=1)
def get_cached_categories():
return db.session.query(Categoria.id, Categoria.nombre, Categoria.descripcion)\
.order_by(Categoria.id)\
.all()

@categorias_bp.route('/api/categorias', methods=['GET'])
def get_categorias():
try:
cats = Categoria.query.all()
# Retornamos lista de objetos {id, nombre, descripcion}
# Llamamos a la función cacheada
# La PRIMERA vez irá a la DB.
# Las siguientes veces retornará lo que tiene en memoria RAM instantáneamente.
cats = get_cached_categories()

return jsonify([
{
"id": c.id,
"nombre": c.nombre,
"descripcion": c.descripcion
} for c in cats
]), 200

except Exception as e:
print(f"Error: {e}")
return jsonify({'error': str(e)}), 500
79 changes: 35 additions & 44 deletions components/contactos/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
from core.models import Notificacion, Publicacion, SolicitudContacto, db
from auth.services import require_auth
from datetime import datetime, timezone
from util import socketio

# Eliminamos util y socketio
# from util import socketio

contactos_bp = Blueprint("contactos", __name__)

Expand All @@ -12,38 +14,38 @@ def crear_solicitud():
data = request.json
usuario_actual = g.usuario_actual

# Validaciones básicas
# 1. Validaciones
pub = Publicacion.query.get(data['id_publicacion'])
if not pub:
return jsonify({'error': 'Publicación no encontrada'}), 404
if pub.id_usuario == usuario_actual.id:
return jsonify({'error': 'No puedes contactarte a ti mismo'}), 400

# Verificar si ya existe solicitud pendiente
# Verificar duplicados pendientes
existente = SolicitudContacto.query.filter_by(
id_solicitante=usuario_actual.id,
id_publicacion=pub.id
).first()

if existente and existente.estado == 0:
return jsonify({'error': 'Ya tienes una solicitud pendiente'}), 400

# Obtener tipo y mensaje
# Obtener datos
tipo_contacto = data.get('tipo', 'whatsapp')
mensaje_usuario = data.get('mensaje', '')

# --- CAMBIO: Obtener el dato de contacto del solicitante AHORA ---
# Determinar qué dato mostrar
dato_contacto_mostrable = "No especificado"

if tipo_contacto == 'whatsapp':
# Validar que tenga teléfono cargado
if not usuario_actual.telefono_numero_local:
return jsonify({'error': 'No tienes un número de teléfono configurado en tu perfil'}), 400
return jsonify({'error': 'No tienes un número de teléfono configurado'}), 400

dato_contacto_mostrable = f"{usuario_actual.telefono_pais or ''} {usuario_actual.telefono_numero_local}".strip()
else:
dato_contacto_mostrable = usuario_actual.email

# Creamos la solicitud
# 2. Crear la Solicitud
nueva_solicitud = SolicitudContacto(
id_solicitante=usuario_actual.id,
id_receptor=pub.id_usuario,
Expand All @@ -53,22 +55,23 @@ def crear_solicitud():
)

db.session.add(nueva_solicitud)
db.session.flush()
db.session.flush() # Flush para obtener el ID antes del commit final

# --- CAMBIO: Incluimos el contacto explícitamente en la descripción ---
# 3. Crear Notificación para el DUEÑO (Receptor)
# Esto es lo que el Polling del dueño va a detectar en 15 segundos
descripcion_noti = (
f"{usuario_actual.nombre} quiere contactarte. "
f"Dejó su {tipo_contacto.upper()}: {dato_contacto_mostrable}. "
f"Mensaje: '{mensaje_usuario}'"
)

nueva_noti = Notificacion(
id_usuario=pub.id_usuario,
id_usuario=pub.id_usuario, # Para el dueño
titulo="Nueva solicitud de contacto",
descripcion=descripcion_noti, # <--- Aquí va el dato visible
descripcion=descripcion_noti,
tipo="solicitud_contacto",
id_publicacion=pub.id,
id_referencia=nueva_solicitud.id,
id_referencia=nueva_solicitud.id, # Link a la solicitud para poder Aceptar/Rechazar
fecha_creacion=datetime.now(timezone.utc)
)
db.session.add(nueva_noti)
Expand All @@ -84,39 +87,39 @@ def responder_solicitud(id_solicitud):
data = request.json
solicitud = SolicitudContacto.query.get_or_404(id_solicitud)

# Seguridad: solo el dueño puede responder
if solicitud.id_receptor != g.usuario_actual.id:
return jsonify({'error': 'No autorizado'}), 403

dato_contacto_solicitante_para_front = None

if data['accion'] == 'aceptar':
solicitud.estado = 1
solicitud.estado = 1 # Aceptado

# 1. Preparamos los datos de AMBOS
# --- Recopilar Datos de Contacto ---

# Datos del DUEÑO (Actual usuario)
# Datos del DUEÑO (Yo)
tel_dueno = f"{g.usuario_actual.telefono_pais or ''} {g.usuario_actual.telefono_numero_local or ''}".strip()
email_dueno = g.usuario_actual.email

# Datos del SOLICITANTE (El que mandó la solicitud)
# Datos del SOLICITANTE (El otro)
tel_solicitante = f"{solicitud.solicitante.telefono_pais or ''} {solicitud.solicitante.telefono_numero_local or ''}".strip()
email_solicitante = solicitud.solicitante.email
nombre_solicitante = solicitud.solicitante.nombre

# Definimos qué dato mostrar según el tipo
# Lógica de intercambio (si eligió whatsapp, le doy whatsapp, etc)
dato_para_solicitante = tel_dueno if solicitud.tipo_contacto == 'whatsapp' else email_dueno
dato_para_dueno = tel_solicitante if solicitud.tipo_contacto == 'whatsapp' else email_solicitante

# Esto es lo que el frontend mostrará en el Toast inmediato
# Para el Toast inmediato del frontend
dato_contacto_solicitante_para_front = dato_para_dueno

# ---------------------------------------------------------
# 2. CREACIÓN DE NOTIFICACIONES (GUARDAR EN BASE DE DATOS)
# ---------------------------------------------------------
# --- GENERAR NOTIFICACIONES PERSISTENTES ---

# A) Notificación para el SOLICITANTE (Le llega el dato del dueño)
# 1. Notificación para el SOLICITANTE (Feedback)
# El Polling del solicitante detectará esto.
noti_para_solicitante = Notificacion(
id_usuario=solicitud.id_solicitante, # Destino: Solicitante
id_usuario=solicitud.id_solicitante,
titulo="¡Solicitud Aceptada!",
descripcion=f"{g.usuario_actual.nombre} aceptó tu solicitud. Contacto: {dato_para_solicitante}",
tipo="contacto_aceptado",
Expand All @@ -126,39 +129,27 @@ def responder_solicitud(id_solicitud):
)
db.session.add(noti_para_solicitante)

# B) NUEVO: Notificación para el DUEÑO (Autonotificación para guardar el dato)
# Así, si cierra el toast, puede ir a 'Notificaciones' y ver el número de nuevo.
# 2. Notificación para el DUEÑO (Historial)
# Útil para que el dueño no pierda el número si cierra el popup
noti_para_dueno = Notificacion(
id_usuario=g.usuario_actual.id, # Destino: Dueño (Yo mismo)
id_usuario=g.usuario_actual.id,
titulo="Contacto realizado",
descripcion=f"Aceptaste a {nombre_solicitante}. Su contacto es: {dato_para_dueno}",
tipo="info_contacto", # Tipo informativo, no requiere acción
tipo="info_contacto",
id_publicacion=solicitud.id_publicacion,
id_referencia=solicitud.id,
fecha_creacion=datetime.now(timezone.utc),
leido=True # Podemos marcarla como leída porque la acaba de generar él mismo
leido=True # Nace leída porque él mismo hizo la acción
)
db.session.add(noti_para_dueno)

elif data['accion'] == 'rechazar':
solicitud.estado = 2
# Opcional: Notificar rechazo al solicitante
solicitud.estado = 2 # Rechazado
# Opcional: Crear notificación de "Tu solicitud fue rechazada" para el solicitante

db.session.commit()

# 3. SOCKETS (Opcional pero recomendado para que aparezca al instante)
# Avisar al solicitante que fue aceptado
if data['accion'] == 'aceptar':
try:
socketio.emit(
'nueva_notificacion',
{"mensaje": "Tu solicitud fue aceptada"},
room=solicitud.solicitante.firebase_uid, # Asegúrate de usar el ID correcto para el room
namespace='/notificacion'
)
except Exception as e:
print(f"Error socket: {e}")

# Eliminamos el bloque de socketio.emit

return jsonify({
'mensaje': 'Respuesta guardada',
Expand Down
Loading