Implementación inicial del sistema de automatización de redes sociales
- Estructura completa del proyecto con FastAPI - Modelos de base de datos (productos, servicios, posts, calendario, interacciones) - Publishers para X, Threads, Instagram, Facebook - Generador de contenido con DeepSeek API - Worker de Celery con tareas programadas - Dashboard básico con templates HTML - Docker Compose para despliegue - Documentación completa Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
1
app/api/routes/__init__.py
Normal file
1
app/api/routes/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# API Route modules
|
||||
201
app/api/routes/calendar.py
Normal file
201
app/api/routes/calendar.py
Normal file
@@ -0,0 +1,201 @@
|
||||
"""
|
||||
API Routes para gestión del Calendario de Contenido.
|
||||
"""
|
||||
|
||||
from datetime import datetime, time
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.models.content_calendar import ContentCalendar
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# ===========================================
|
||||
# SCHEMAS
|
||||
# ===========================================
|
||||
|
||||
class CalendarEntryCreate(BaseModel):
|
||||
day_of_week: int # 0=Lunes, 6=Domingo
|
||||
time: str # Formato "HH:MM"
|
||||
content_type: str
|
||||
platforms: List[str]
|
||||
requires_approval: bool = False
|
||||
category_filter: Optional[str] = None
|
||||
priority: int = 0
|
||||
description: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class CalendarEntryUpdate(BaseModel):
|
||||
day_of_week: Optional[int] = None
|
||||
time: Optional[str] = None
|
||||
content_type: Optional[str] = None
|
||||
platforms: Optional[List[str]] = None
|
||||
is_active: Optional[bool] = None
|
||||
requires_approval: Optional[bool] = None
|
||||
category_filter: Optional[str] = None
|
||||
priority: Optional[int] = None
|
||||
description: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# ===========================================
|
||||
# ENDPOINTS
|
||||
# ===========================================
|
||||
|
||||
@router.get("/")
|
||||
async def list_calendar_entries(
|
||||
day_of_week: Optional[int] = Query(None),
|
||||
content_type: Optional[str] = Query(None),
|
||||
platform: Optional[str] = Query(None),
|
||||
is_active: Optional[bool] = Query(True),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Listar entradas del calendario."""
|
||||
query = db.query(ContentCalendar)
|
||||
|
||||
if day_of_week is not None:
|
||||
query = query.filter(ContentCalendar.day_of_week == day_of_week)
|
||||
if content_type:
|
||||
query = query.filter(ContentCalendar.content_type == content_type)
|
||||
if is_active is not None:
|
||||
query = query.filter(ContentCalendar.is_active == is_active)
|
||||
if platform:
|
||||
query = query.filter(ContentCalendar.platforms.contains([platform]))
|
||||
|
||||
entries = query.order_by(
|
||||
ContentCalendar.day_of_week,
|
||||
ContentCalendar.time
|
||||
).all()
|
||||
|
||||
return [e.to_dict() for e in entries]
|
||||
|
||||
|
||||
@router.get("/week")
|
||||
async def get_week_schedule(db: Session = Depends(get_db)):
|
||||
"""Obtener el calendario semanal completo."""
|
||||
entries = db.query(ContentCalendar).filter(
|
||||
ContentCalendar.is_active == True
|
||||
).order_by(
|
||||
ContentCalendar.day_of_week,
|
||||
ContentCalendar.time
|
||||
).all()
|
||||
|
||||
# Organizar por día
|
||||
week = {i: [] for i in range(7)}
|
||||
for entry in entries:
|
||||
week[entry.day_of_week].append(entry.to_dict())
|
||||
|
||||
days = ["Lunes", "Martes", "Miércoles", "Jueves", "Viernes", "Sábado", "Domingo"]
|
||||
return {
|
||||
days[i]: week[i] for i in range(7)
|
||||
}
|
||||
|
||||
|
||||
@router.get("/today")
|
||||
async def get_today_schedule(db: Session = Depends(get_db)):
|
||||
"""Obtener el calendario de hoy."""
|
||||
today = datetime.now().weekday()
|
||||
|
||||
entries = db.query(ContentCalendar).filter(
|
||||
ContentCalendar.day_of_week == today,
|
||||
ContentCalendar.is_active == True
|
||||
).order_by(ContentCalendar.time).all()
|
||||
|
||||
return [e.to_dict() for e in entries]
|
||||
|
||||
|
||||
@router.get("/{entry_id}")
|
||||
async def get_calendar_entry(entry_id: int, db: Session = Depends(get_db)):
|
||||
"""Obtener una entrada del calendario por ID."""
|
||||
entry = db.query(ContentCalendar).filter(ContentCalendar.id == entry_id).first()
|
||||
if not entry:
|
||||
raise HTTPException(status_code=404, detail="Entrada no encontrada")
|
||||
return entry.to_dict()
|
||||
|
||||
|
||||
@router.post("/")
|
||||
async def create_calendar_entry(entry_data: CalendarEntryCreate, db: Session = Depends(get_db)):
|
||||
"""Crear una nueva entrada en el calendario."""
|
||||
# Parsear hora
|
||||
hour, minute = map(int, entry_data.time.split(":"))
|
||||
entry_time = time(hour=hour, minute=minute)
|
||||
|
||||
entry = ContentCalendar(
|
||||
day_of_week=entry_data.day_of_week,
|
||||
time=entry_time,
|
||||
content_type=entry_data.content_type,
|
||||
platforms=entry_data.platforms,
|
||||
requires_approval=entry_data.requires_approval,
|
||||
category_filter=entry_data.category_filter,
|
||||
priority=entry_data.priority,
|
||||
description=entry_data.description
|
||||
)
|
||||
|
||||
db.add(entry)
|
||||
db.commit()
|
||||
db.refresh(entry)
|
||||
return entry.to_dict()
|
||||
|
||||
|
||||
@router.put("/{entry_id}")
|
||||
async def update_calendar_entry(
|
||||
entry_id: int,
|
||||
entry_data: CalendarEntryUpdate,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Actualizar una entrada del calendario."""
|
||||
entry = db.query(ContentCalendar).filter(ContentCalendar.id == entry_id).first()
|
||||
if not entry:
|
||||
raise HTTPException(status_code=404, detail="Entrada no encontrada")
|
||||
|
||||
update_data = entry_data.dict(exclude_unset=True)
|
||||
|
||||
# Parsear hora si se proporciona
|
||||
if "time" in update_data and update_data["time"]:
|
||||
hour, minute = map(int, update_data["time"].split(":"))
|
||||
update_data["time"] = time(hour=hour, minute=minute)
|
||||
|
||||
for field, value in update_data.items():
|
||||
setattr(entry, field, value)
|
||||
|
||||
entry.updated_at = datetime.utcnow()
|
||||
db.commit()
|
||||
db.refresh(entry)
|
||||
return entry.to_dict()
|
||||
|
||||
|
||||
@router.delete("/{entry_id}")
|
||||
async def delete_calendar_entry(entry_id: int, db: Session = Depends(get_db)):
|
||||
"""Eliminar una entrada del calendario."""
|
||||
entry = db.query(ContentCalendar).filter(ContentCalendar.id == entry_id).first()
|
||||
if not entry:
|
||||
raise HTTPException(status_code=404, detail="Entrada no encontrada")
|
||||
|
||||
db.delete(entry)
|
||||
db.commit()
|
||||
return {"message": "Entrada eliminada", "entry_id": entry_id}
|
||||
|
||||
|
||||
@router.post("/{entry_id}/toggle")
|
||||
async def toggle_calendar_entry(entry_id: int, db: Session = Depends(get_db)):
|
||||
"""Activar/desactivar una entrada del calendario."""
|
||||
entry = db.query(ContentCalendar).filter(ContentCalendar.id == entry_id).first()
|
||||
if not entry:
|
||||
raise HTTPException(status_code=404, detail="Entrada no encontrada")
|
||||
|
||||
entry.is_active = not entry.is_active
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"message": f"Entrada {'activada' if entry.is_active else 'desactivada'}",
|
||||
"is_active": entry.is_active
|
||||
}
|
||||
118
app/api/routes/dashboard.py
Normal file
118
app/api/routes/dashboard.py
Normal file
@@ -0,0 +1,118 @@
|
||||
"""
|
||||
API Routes para el Dashboard.
|
||||
"""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.models.post import Post
|
||||
from app.models.interaction import Interaction
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
templates = Jinja2Templates(directory="dashboard/templates")
|
||||
|
||||
|
||||
@router.get("/", response_class=HTMLResponse)
|
||||
async def dashboard_home(request: Request, db: Session = Depends(get_db)):
|
||||
"""Página principal del dashboard."""
|
||||
# Estadísticas
|
||||
now = datetime.utcnow()
|
||||
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
week_start = today_start - timedelta(days=now.weekday())
|
||||
|
||||
stats = {
|
||||
"posts_today": db.query(Post).filter(
|
||||
Post.published_at >= today_start
|
||||
).count(),
|
||||
"posts_week": db.query(Post).filter(
|
||||
Post.published_at >= week_start
|
||||
).count(),
|
||||
"pending_approval": db.query(Post).filter(
|
||||
Post.status == "pending_approval"
|
||||
).count(),
|
||||
"scheduled": db.query(Post).filter(
|
||||
Post.status == "scheduled"
|
||||
).count(),
|
||||
"interactions_pending": db.query(Interaction).filter(
|
||||
Interaction.responded == False,
|
||||
Interaction.is_archived == False
|
||||
).count()
|
||||
}
|
||||
|
||||
# Posts pendientes
|
||||
pending_posts = db.query(Post).filter(
|
||||
Post.status == "pending_approval"
|
||||
).order_by(Post.scheduled_at.asc()).limit(5).all()
|
||||
|
||||
# Próximas publicaciones
|
||||
scheduled_posts = db.query(Post).filter(
|
||||
Post.status == "scheduled",
|
||||
Post.scheduled_at >= now
|
||||
).order_by(Post.scheduled_at.asc()).limit(5).all()
|
||||
|
||||
# Interacciones recientes
|
||||
recent_interactions = db.query(Interaction).filter(
|
||||
Interaction.responded == False
|
||||
).order_by(Interaction.interaction_at.desc()).limit(5).all()
|
||||
|
||||
return templates.TemplateResponse("index.html", {
|
||||
"request": request,
|
||||
"stats": stats,
|
||||
"pending_posts": [p.to_dict() for p in pending_posts],
|
||||
"scheduled_posts": [p.to_dict() for p in scheduled_posts],
|
||||
"recent_interactions": [i.to_dict() for i in recent_interactions]
|
||||
})
|
||||
|
||||
|
||||
@router.get("/posts", response_class=HTMLResponse)
|
||||
async def dashboard_posts(request: Request, db: Session = Depends(get_db)):
|
||||
"""Página de gestión de posts."""
|
||||
posts = db.query(Post).order_by(Post.created_at.desc()).limit(50).all()
|
||||
|
||||
return templates.TemplateResponse("posts.html", {
|
||||
"request": request,
|
||||
"posts": [p.to_dict() for p in posts]
|
||||
})
|
||||
|
||||
|
||||
@router.get("/calendar", response_class=HTMLResponse)
|
||||
async def dashboard_calendar(request: Request):
|
||||
"""Página de calendario."""
|
||||
return templates.TemplateResponse("calendar.html", {
|
||||
"request": request
|
||||
})
|
||||
|
||||
|
||||
@router.get("/interactions", response_class=HTMLResponse)
|
||||
async def dashboard_interactions(request: Request, db: Session = Depends(get_db)):
|
||||
"""Página de interacciones."""
|
||||
interactions = db.query(Interaction).filter(
|
||||
Interaction.is_archived == False
|
||||
).order_by(Interaction.interaction_at.desc()).limit(50).all()
|
||||
|
||||
return templates.TemplateResponse("interactions.html", {
|
||||
"request": request,
|
||||
"interactions": [i.to_dict() for i in interactions]
|
||||
})
|
||||
|
||||
|
||||
@router.get("/products", response_class=HTMLResponse)
|
||||
async def dashboard_products(request: Request):
|
||||
"""Página de productos."""
|
||||
return templates.TemplateResponse("products.html", {
|
||||
"request": request
|
||||
})
|
||||
|
||||
|
||||
@router.get("/services", response_class=HTMLResponse)
|
||||
async def dashboard_services(request: Request):
|
||||
"""Página de servicios."""
|
||||
return templates.TemplateResponse("services.html", {
|
||||
"request": request
|
||||
})
|
||||
226
app/api/routes/interactions.py
Normal file
226
app/api/routes/interactions.py
Normal file
@@ -0,0 +1,226 @@
|
||||
"""
|
||||
API Routes para gestión de Interacciones.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.models.interaction import Interaction
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# ===========================================
|
||||
# SCHEMAS
|
||||
# ===========================================
|
||||
|
||||
class InteractionResponse(BaseModel):
|
||||
content: str
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# ===========================================
|
||||
# ENDPOINTS
|
||||
# ===========================================
|
||||
|
||||
@router.get("/")
|
||||
async def list_interactions(
|
||||
platform: Optional[str] = Query(None),
|
||||
interaction_type: Optional[str] = Query(None),
|
||||
responded: Optional[bool] = Query(None),
|
||||
is_lead: Optional[bool] = Query(None),
|
||||
is_read: Optional[bool] = Query(None),
|
||||
limit: int = Query(50, le=100),
|
||||
offset: int = Query(0),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Listar interacciones con filtros."""
|
||||
query = db.query(Interaction)
|
||||
|
||||
if platform:
|
||||
query = query.filter(Interaction.platform == platform)
|
||||
if interaction_type:
|
||||
query = query.filter(Interaction.interaction_type == interaction_type)
|
||||
if responded is not None:
|
||||
query = query.filter(Interaction.responded == responded)
|
||||
if is_lead is not None:
|
||||
query = query.filter(Interaction.is_lead == is_lead)
|
||||
if is_read is not None:
|
||||
query = query.filter(Interaction.is_read == is_read)
|
||||
|
||||
interactions = query.order_by(
|
||||
Interaction.interaction_at.desc()
|
||||
).offset(offset).limit(limit).all()
|
||||
|
||||
return [i.to_dict() for i in interactions]
|
||||
|
||||
|
||||
@router.get("/pending")
|
||||
async def list_pending_interactions(db: Session = Depends(get_db)):
|
||||
"""Listar interacciones sin responder."""
|
||||
interactions = db.query(Interaction).filter(
|
||||
Interaction.responded == False,
|
||||
Interaction.is_archived == False
|
||||
).order_by(
|
||||
Interaction.priority.desc(),
|
||||
Interaction.interaction_at.desc()
|
||||
).all()
|
||||
|
||||
return [i.to_dict() for i in interactions]
|
||||
|
||||
|
||||
@router.get("/leads")
|
||||
async def list_leads(db: Session = Depends(get_db)):
|
||||
"""Listar interacciones marcadas como leads."""
|
||||
interactions = db.query(Interaction).filter(
|
||||
Interaction.is_lead == True
|
||||
).order_by(Interaction.interaction_at.desc()).all()
|
||||
|
||||
return [i.to_dict() for i in interactions]
|
||||
|
||||
|
||||
@router.get("/stats")
|
||||
async def get_interaction_stats(db: Session = Depends(get_db)):
|
||||
"""Obtener estadísticas de interacciones."""
|
||||
total = db.query(Interaction).count()
|
||||
pending = db.query(Interaction).filter(
|
||||
Interaction.responded == False,
|
||||
Interaction.is_archived == False
|
||||
).count()
|
||||
leads = db.query(Interaction).filter(Interaction.is_lead == True).count()
|
||||
|
||||
# Por plataforma
|
||||
by_platform = {}
|
||||
for platform in ["x", "threads", "instagram", "facebook"]:
|
||||
by_platform[platform] = db.query(Interaction).filter(
|
||||
Interaction.platform == platform
|
||||
).count()
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"pending": pending,
|
||||
"leads": leads,
|
||||
"by_platform": by_platform
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{interaction_id}")
|
||||
async def get_interaction(interaction_id: int, db: Session = Depends(get_db)):
|
||||
"""Obtener una interacción por ID."""
|
||||
interaction = db.query(Interaction).filter(Interaction.id == interaction_id).first()
|
||||
if not interaction:
|
||||
raise HTTPException(status_code=404, detail="Interacción no encontrada")
|
||||
return interaction.to_dict()
|
||||
|
||||
|
||||
@router.post("/{interaction_id}/respond")
|
||||
async def respond_to_interaction(
|
||||
interaction_id: int,
|
||||
response_data: InteractionResponse,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Responder a una interacción."""
|
||||
interaction = db.query(Interaction).filter(Interaction.id == interaction_id).first()
|
||||
if not interaction:
|
||||
raise HTTPException(status_code=404, detail="Interacción no encontrada")
|
||||
|
||||
# TODO: Enviar respuesta a la plataforma correspondiente
|
||||
# from app.publishers import get_publisher
|
||||
# publisher = get_publisher(interaction.platform)
|
||||
# result = await publisher.reply(interaction.external_id, response_data.content)
|
||||
|
||||
interaction.responded = True
|
||||
interaction.response_content = response_data.content
|
||||
interaction.responded_at = datetime.utcnow()
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"message": "Respuesta guardada",
|
||||
"interaction_id": interaction_id,
|
||||
"note": "La publicación a la plataforma está en desarrollo"
|
||||
}
|
||||
|
||||
|
||||
@router.post("/{interaction_id}/suggest-response")
|
||||
async def suggest_response(interaction_id: int, db: Session = Depends(get_db)):
|
||||
"""Generar sugerencia de respuesta con IA."""
|
||||
interaction = db.query(Interaction).filter(Interaction.id == interaction_id).first()
|
||||
if not interaction:
|
||||
raise HTTPException(status_code=404, detail="Interacción no encontrada")
|
||||
|
||||
# TODO: Implementar generación con DeepSeek
|
||||
# from app.services.content_generator import generate_response_suggestion
|
||||
# suggestions = await generate_response_suggestion(interaction)
|
||||
|
||||
# Respuestas placeholder
|
||||
suggestions = [
|
||||
f"¡Gracias por tu comentario! Nos da gusto que te interese nuestro contenido.",
|
||||
f"¡Hola! Gracias por escribirnos. ¿En qué podemos ayudarte?",
|
||||
f"¡Excelente pregunta! Te respondemos por DM para darte más detalles."
|
||||
]
|
||||
|
||||
return {
|
||||
"suggestions": suggestions,
|
||||
"note": "La generación con IA está en desarrollo"
|
||||
}
|
||||
|
||||
|
||||
@router.post("/{interaction_id}/mark-as-lead")
|
||||
async def mark_as_lead(interaction_id: int, db: Session = Depends(get_db)):
|
||||
"""Marcar interacción como lead potencial."""
|
||||
interaction = db.query(Interaction).filter(Interaction.id == interaction_id).first()
|
||||
if not interaction:
|
||||
raise HTTPException(status_code=404, detail="Interacción no encontrada")
|
||||
|
||||
interaction.is_lead = not interaction.is_lead
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"message": f"{'Marcado' if interaction.is_lead else 'Desmarcado'} como lead",
|
||||
"is_lead": interaction.is_lead
|
||||
}
|
||||
|
||||
|
||||
@router.post("/{interaction_id}/mark-as-read")
|
||||
async def mark_as_read(interaction_id: int, db: Session = Depends(get_db)):
|
||||
"""Marcar interacción como leída."""
|
||||
interaction = db.query(Interaction).filter(Interaction.id == interaction_id).first()
|
||||
if not interaction:
|
||||
raise HTTPException(status_code=404, detail="Interacción no encontrada")
|
||||
|
||||
interaction.is_read = True
|
||||
db.commit()
|
||||
|
||||
return {"message": "Marcado como leído", "interaction_id": interaction_id}
|
||||
|
||||
|
||||
@router.post("/{interaction_id}/archive")
|
||||
async def archive_interaction(interaction_id: int, db: Session = Depends(get_db)):
|
||||
"""Archivar una interacción."""
|
||||
interaction = db.query(Interaction).filter(Interaction.id == interaction_id).first()
|
||||
if not interaction:
|
||||
raise HTTPException(status_code=404, detail="Interacción no encontrada")
|
||||
|
||||
interaction.is_archived = True
|
||||
db.commit()
|
||||
|
||||
return {"message": "Interacción archivada", "interaction_id": interaction_id}
|
||||
|
||||
|
||||
@router.delete("/{interaction_id}")
|
||||
async def delete_interaction(interaction_id: int, db: Session = Depends(get_db)):
|
||||
"""Eliminar una interacción."""
|
||||
interaction = db.query(Interaction).filter(Interaction.id == interaction_id).first()
|
||||
if not interaction:
|
||||
raise HTTPException(status_code=404, detail="Interacción no encontrada")
|
||||
|
||||
db.delete(interaction)
|
||||
db.commit()
|
||||
|
||||
return {"message": "Interacción eliminada", "interaction_id": interaction_id}
|
||||
216
app/api/routes/posts.py
Normal file
216
app/api/routes/posts.py
Normal file
@@ -0,0 +1,216 @@
|
||||
"""
|
||||
API Routes para gestión de Posts.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.models.post import Post
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# ===========================================
|
||||
# SCHEMAS
|
||||
# ===========================================
|
||||
|
||||
class PostCreate(BaseModel):
|
||||
content: str
|
||||
content_type: str
|
||||
platforms: List[str]
|
||||
scheduled_at: Optional[datetime] = None
|
||||
image_url: Optional[str] = None
|
||||
approval_required: bool = False
|
||||
hashtags: Optional[List[str]] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class PostUpdate(BaseModel):
|
||||
content: Optional[str] = None
|
||||
content_x: Optional[str] = None
|
||||
content_threads: Optional[str] = None
|
||||
content_instagram: Optional[str] = None
|
||||
content_facebook: Optional[str] = None
|
||||
platforms: Optional[List[str]] = None
|
||||
scheduled_at: Optional[datetime] = None
|
||||
status: Optional[str] = None
|
||||
image_url: Optional[str] = None
|
||||
hashtags: Optional[List[str]] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class PostResponse(BaseModel):
|
||||
id: int
|
||||
content: str
|
||||
content_type: str
|
||||
platforms: List[str]
|
||||
status: str
|
||||
scheduled_at: Optional[datetime]
|
||||
published_at: Optional[datetime]
|
||||
image_url: Optional[str]
|
||||
approval_required: bool
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# ===========================================
|
||||
# ENDPOINTS
|
||||
# ===========================================
|
||||
|
||||
@router.get("/", response_model=List[PostResponse])
|
||||
async def list_posts(
|
||||
status: Optional[str] = Query(None, description="Filtrar por estado"),
|
||||
content_type: Optional[str] = Query(None, description="Filtrar por tipo"),
|
||||
platform: Optional[str] = Query(None, description="Filtrar por plataforma"),
|
||||
limit: int = Query(50, le=100),
|
||||
offset: int = Query(0),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Listar posts con filtros opcionales."""
|
||||
query = db.query(Post)
|
||||
|
||||
if status:
|
||||
query = query.filter(Post.status == status)
|
||||
if content_type:
|
||||
query = query.filter(Post.content_type == content_type)
|
||||
if platform:
|
||||
query = query.filter(Post.platforms.contains([platform]))
|
||||
|
||||
posts = query.order_by(Post.created_at.desc()).offset(offset).limit(limit).all()
|
||||
return posts
|
||||
|
||||
|
||||
@router.get("/pending")
|
||||
async def list_pending_posts(db: Session = Depends(get_db)):
|
||||
"""Listar posts pendientes de aprobación."""
|
||||
posts = db.query(Post).filter(
|
||||
Post.status == "pending_approval"
|
||||
).order_by(Post.scheduled_at.asc()).all()
|
||||
|
||||
return [post.to_dict() for post in posts]
|
||||
|
||||
|
||||
@router.get("/scheduled")
|
||||
async def list_scheduled_posts(db: Session = Depends(get_db)):
|
||||
"""Listar posts programados."""
|
||||
posts = db.query(Post).filter(
|
||||
Post.status == "scheduled",
|
||||
Post.scheduled_at >= datetime.utcnow()
|
||||
).order_by(Post.scheduled_at.asc()).all()
|
||||
|
||||
return [post.to_dict() for post in posts]
|
||||
|
||||
|
||||
@router.get("/{post_id}")
|
||||
async def get_post(post_id: int, db: Session = Depends(get_db)):
|
||||
"""Obtener un post por ID."""
|
||||
post = db.query(Post).filter(Post.id == post_id).first()
|
||||
if not post:
|
||||
raise HTTPException(status_code=404, detail="Post no encontrado")
|
||||
return post.to_dict()
|
||||
|
||||
|
||||
@router.post("/", response_model=PostResponse)
|
||||
async def create_post(post_data: PostCreate, db: Session = Depends(get_db)):
|
||||
"""Crear un nuevo post."""
|
||||
post = Post(
|
||||
content=post_data.content,
|
||||
content_type=post_data.content_type,
|
||||
platforms=post_data.platforms,
|
||||
scheduled_at=post_data.scheduled_at,
|
||||
image_url=post_data.image_url,
|
||||
approval_required=post_data.approval_required,
|
||||
hashtags=post_data.hashtags,
|
||||
status="pending_approval" if post_data.approval_required else "scheduled"
|
||||
)
|
||||
|
||||
db.add(post)
|
||||
db.commit()
|
||||
db.refresh(post)
|
||||
|
||||
return post
|
||||
|
||||
|
||||
@router.put("/{post_id}")
|
||||
async def update_post(post_id: int, post_data: PostUpdate, db: Session = Depends(get_db)):
|
||||
"""Actualizar un post."""
|
||||
post = db.query(Post).filter(Post.id == post_id).first()
|
||||
if not post:
|
||||
raise HTTPException(status_code=404, detail="Post no encontrado")
|
||||
|
||||
update_data = post_data.dict(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(post, field, value)
|
||||
|
||||
post.updated_at = datetime.utcnow()
|
||||
db.commit()
|
||||
db.refresh(post)
|
||||
|
||||
return post.to_dict()
|
||||
|
||||
|
||||
@router.post("/{post_id}/approve")
|
||||
async def approve_post(post_id: int, db: Session = Depends(get_db)):
|
||||
"""Aprobar un post pendiente."""
|
||||
post = db.query(Post).filter(Post.id == post_id).first()
|
||||
if not post:
|
||||
raise HTTPException(status_code=404, detail="Post no encontrado")
|
||||
|
||||
if post.status != "pending_approval":
|
||||
raise HTTPException(status_code=400, detail="El post no está pendiente de aprobación")
|
||||
|
||||
post.status = "scheduled"
|
||||
post.approved_at = datetime.utcnow()
|
||||
db.commit()
|
||||
|
||||
return {"message": "Post aprobado", "post_id": post_id}
|
||||
|
||||
|
||||
@router.post("/{post_id}/reject")
|
||||
async def reject_post(post_id: int, db: Session = Depends(get_db)):
|
||||
"""Rechazar un post pendiente."""
|
||||
post = db.query(Post).filter(Post.id == post_id).first()
|
||||
if not post:
|
||||
raise HTTPException(status_code=404, detail="Post no encontrado")
|
||||
|
||||
post.status = "cancelled"
|
||||
db.commit()
|
||||
|
||||
return {"message": "Post rechazado", "post_id": post_id}
|
||||
|
||||
|
||||
@router.post("/{post_id}/regenerate")
|
||||
async def regenerate_post(post_id: int, db: Session = Depends(get_db)):
|
||||
"""Regenerar contenido de un post con IA."""
|
||||
post = db.query(Post).filter(Post.id == post_id).first()
|
||||
if not post:
|
||||
raise HTTPException(status_code=404, detail="Post no encontrado")
|
||||
|
||||
# TODO: Implementar regeneración con DeepSeek
|
||||
# from app.services.content_generator import regenerate_content
|
||||
# new_content = await regenerate_content(post)
|
||||
|
||||
return {"message": "Regeneración en desarrollo", "post_id": post_id}
|
||||
|
||||
|
||||
@router.delete("/{post_id}")
|
||||
async def delete_post(post_id: int, db: Session = Depends(get_db)):
|
||||
"""Eliminar un post."""
|
||||
post = db.query(Post).filter(Post.id == post_id).first()
|
||||
if not post:
|
||||
raise HTTPException(status_code=404, detail="Post no encontrado")
|
||||
|
||||
db.delete(post)
|
||||
db.commit()
|
||||
|
||||
return {"message": "Post eliminado", "post_id": post_id}
|
||||
180
app/api/routes/products.py
Normal file
180
app/api/routes/products.py
Normal file
@@ -0,0 +1,180 @@
|
||||
"""
|
||||
API Routes para gestión de Productos.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.models.product import Product
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# ===========================================
|
||||
# SCHEMAS
|
||||
# ===========================================
|
||||
|
||||
class ProductCreate(BaseModel):
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
short_description: Optional[str] = None
|
||||
category: str
|
||||
subcategory: Optional[str] = None
|
||||
brand: Optional[str] = None
|
||||
model: Optional[str] = None
|
||||
price: float
|
||||
price_usd: Optional[float] = None
|
||||
stock: int = 0
|
||||
specs: Optional[dict] = None
|
||||
images: Optional[List[str]] = None
|
||||
main_image: Optional[str] = None
|
||||
tags: Optional[List[str]] = None
|
||||
highlights: Optional[List[str]] = None
|
||||
is_featured: bool = False
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class ProductUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
short_description: Optional[str] = None
|
||||
category: Optional[str] = None
|
||||
subcategory: Optional[str] = None
|
||||
brand: Optional[str] = None
|
||||
model: Optional[str] = None
|
||||
price: Optional[float] = None
|
||||
price_usd: Optional[float] = None
|
||||
stock: Optional[int] = None
|
||||
is_available: Optional[bool] = None
|
||||
specs: Optional[dict] = None
|
||||
images: Optional[List[str]] = None
|
||||
main_image: Optional[str] = None
|
||||
tags: Optional[List[str]] = None
|
||||
highlights: Optional[List[str]] = None
|
||||
is_featured: Optional[bool] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# ===========================================
|
||||
# ENDPOINTS
|
||||
# ===========================================
|
||||
|
||||
@router.get("/")
|
||||
async def list_products(
|
||||
category: Optional[str] = Query(None),
|
||||
brand: Optional[str] = Query(None),
|
||||
is_available: Optional[bool] = Query(None),
|
||||
is_featured: Optional[bool] = Query(None),
|
||||
min_price: Optional[float] = Query(None),
|
||||
max_price: Optional[float] = Query(None),
|
||||
limit: int = Query(50, le=100),
|
||||
offset: int = Query(0),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Listar productos con filtros."""
|
||||
query = db.query(Product)
|
||||
|
||||
if category:
|
||||
query = query.filter(Product.category == category)
|
||||
if brand:
|
||||
query = query.filter(Product.brand == brand)
|
||||
if is_available is not None:
|
||||
query = query.filter(Product.is_available == is_available)
|
||||
if is_featured is not None:
|
||||
query = query.filter(Product.is_featured == is_featured)
|
||||
if min_price is not None:
|
||||
query = query.filter(Product.price >= min_price)
|
||||
if max_price is not None:
|
||||
query = query.filter(Product.price <= max_price)
|
||||
|
||||
products = query.order_by(Product.created_at.desc()).offset(offset).limit(limit).all()
|
||||
return [p.to_dict() for p in products]
|
||||
|
||||
|
||||
@router.get("/categories")
|
||||
async def list_categories(db: Session = Depends(get_db)):
|
||||
"""Listar categorías únicas de productos."""
|
||||
categories = db.query(Product.category).distinct().all()
|
||||
return [c[0] for c in categories if c[0]]
|
||||
|
||||
|
||||
@router.get("/featured")
|
||||
async def list_featured_products(db: Session = Depends(get_db)):
|
||||
"""Listar productos destacados."""
|
||||
products = db.query(Product).filter(
|
||||
Product.is_featured == True,
|
||||
Product.is_available == True
|
||||
).all()
|
||||
return [p.to_dict() for p in products]
|
||||
|
||||
|
||||
@router.get("/{product_id}")
|
||||
async def get_product(product_id: int, db: Session = Depends(get_db)):
|
||||
"""Obtener un producto por ID."""
|
||||
product = db.query(Product).filter(Product.id == product_id).first()
|
||||
if not product:
|
||||
raise HTTPException(status_code=404, detail="Producto no encontrado")
|
||||
return product.to_dict()
|
||||
|
||||
|
||||
@router.post("/")
|
||||
async def create_product(product_data: ProductCreate, db: Session = Depends(get_db)):
|
||||
"""Crear un nuevo producto."""
|
||||
product = Product(**product_data.dict())
|
||||
db.add(product)
|
||||
db.commit()
|
||||
db.refresh(product)
|
||||
return product.to_dict()
|
||||
|
||||
|
||||
@router.put("/{product_id}")
|
||||
async def update_product(product_id: int, product_data: ProductUpdate, db: Session = Depends(get_db)):
|
||||
"""Actualizar un producto."""
|
||||
product = db.query(Product).filter(Product.id == product_id).first()
|
||||
if not product:
|
||||
raise HTTPException(status_code=404, detail="Producto no encontrado")
|
||||
|
||||
update_data = product_data.dict(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(product, field, value)
|
||||
|
||||
product.updated_at = datetime.utcnow()
|
||||
db.commit()
|
||||
db.refresh(product)
|
||||
return product.to_dict()
|
||||
|
||||
|
||||
@router.delete("/{product_id}")
|
||||
async def delete_product(product_id: int, db: Session = Depends(get_db)):
|
||||
"""Eliminar un producto."""
|
||||
product = db.query(Product).filter(Product.id == product_id).first()
|
||||
if not product:
|
||||
raise HTTPException(status_code=404, detail="Producto no encontrado")
|
||||
|
||||
db.delete(product)
|
||||
db.commit()
|
||||
return {"message": "Producto eliminado", "product_id": product_id}
|
||||
|
||||
|
||||
@router.post("/{product_id}/toggle-featured")
|
||||
async def toggle_featured(product_id: int, db: Session = Depends(get_db)):
|
||||
"""Alternar estado de producto destacado."""
|
||||
product = db.query(Product).filter(Product.id == product_id).first()
|
||||
if not product:
|
||||
raise HTTPException(status_code=404, detail="Producto no encontrado")
|
||||
|
||||
product.is_featured = not product.is_featured
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"message": f"Producto {'destacado' if product.is_featured else 'no destacado'}",
|
||||
"is_featured": product.is_featured
|
||||
}
|
||||
174
app/api/routes/services.py
Normal file
174
app/api/routes/services.py
Normal file
@@ -0,0 +1,174 @@
|
||||
"""
|
||||
API Routes para gestión de Servicios.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.models.service import Service
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# ===========================================
|
||||
# SCHEMAS
|
||||
# ===========================================
|
||||
|
||||
class ServiceCreate(BaseModel):
|
||||
name: str
|
||||
description: str
|
||||
short_description: Optional[str] = None
|
||||
category: str
|
||||
target_sectors: Optional[List[str]] = None
|
||||
benefits: Optional[List[str]] = None
|
||||
features: Optional[List[str]] = None
|
||||
case_studies: Optional[List[dict]] = None
|
||||
icon: Optional[str] = None
|
||||
images: Optional[List[str]] = None
|
||||
main_image: Optional[str] = None
|
||||
price_range: Optional[str] = None
|
||||
has_free_demo: bool = False
|
||||
tags: Optional[List[str]] = None
|
||||
call_to_action: Optional[str] = None
|
||||
is_featured: bool = False
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class ServiceUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
short_description: Optional[str] = None
|
||||
category: Optional[str] = None
|
||||
target_sectors: Optional[List[str]] = None
|
||||
benefits: Optional[List[str]] = None
|
||||
features: Optional[List[str]] = None
|
||||
case_studies: Optional[List[dict]] = None
|
||||
icon: Optional[str] = None
|
||||
images: Optional[List[str]] = None
|
||||
main_image: Optional[str] = None
|
||||
price_range: Optional[str] = None
|
||||
has_free_demo: Optional[bool] = None
|
||||
tags: Optional[List[str]] = None
|
||||
call_to_action: Optional[str] = None
|
||||
is_active: Optional[bool] = None
|
||||
is_featured: Optional[bool] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# ===========================================
|
||||
# ENDPOINTS
|
||||
# ===========================================
|
||||
|
||||
@router.get("/")
|
||||
async def list_services(
|
||||
category: Optional[str] = Query(None),
|
||||
target_sector: Optional[str] = Query(None),
|
||||
is_active: Optional[bool] = Query(True),
|
||||
is_featured: Optional[bool] = Query(None),
|
||||
limit: int = Query(50, le=100),
|
||||
offset: int = Query(0),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Listar servicios con filtros."""
|
||||
query = db.query(Service)
|
||||
|
||||
if category:
|
||||
query = query.filter(Service.category == category)
|
||||
if is_active is not None:
|
||||
query = query.filter(Service.is_active == is_active)
|
||||
if is_featured is not None:
|
||||
query = query.filter(Service.is_featured == is_featured)
|
||||
if target_sector:
|
||||
query = query.filter(Service.target_sectors.contains([target_sector]))
|
||||
|
||||
services = query.order_by(Service.created_at.desc()).offset(offset).limit(limit).all()
|
||||
return [s.to_dict() for s in services]
|
||||
|
||||
|
||||
@router.get("/categories")
|
||||
async def list_categories(db: Session = Depends(get_db)):
|
||||
"""Listar categorías únicas de servicios."""
|
||||
categories = db.query(Service.category).distinct().all()
|
||||
return [c[0] for c in categories if c[0]]
|
||||
|
||||
|
||||
@router.get("/featured")
|
||||
async def list_featured_services(db: Session = Depends(get_db)):
|
||||
"""Listar servicios destacados."""
|
||||
services = db.query(Service).filter(
|
||||
Service.is_featured == True,
|
||||
Service.is_active == True
|
||||
).all()
|
||||
return [s.to_dict() for s in services]
|
||||
|
||||
|
||||
@router.get("/{service_id}")
|
||||
async def get_service(service_id: int, db: Session = Depends(get_db)):
|
||||
"""Obtener un servicio por ID."""
|
||||
service = db.query(Service).filter(Service.id == service_id).first()
|
||||
if not service:
|
||||
raise HTTPException(status_code=404, detail="Servicio no encontrado")
|
||||
return service.to_dict()
|
||||
|
||||
|
||||
@router.post("/")
|
||||
async def create_service(service_data: ServiceCreate, db: Session = Depends(get_db)):
|
||||
"""Crear un nuevo servicio."""
|
||||
service = Service(**service_data.dict())
|
||||
db.add(service)
|
||||
db.commit()
|
||||
db.refresh(service)
|
||||
return service.to_dict()
|
||||
|
||||
|
||||
@router.put("/{service_id}")
|
||||
async def update_service(service_id: int, service_data: ServiceUpdate, db: Session = Depends(get_db)):
|
||||
"""Actualizar un servicio."""
|
||||
service = db.query(Service).filter(Service.id == service_id).first()
|
||||
if not service:
|
||||
raise HTTPException(status_code=404, detail="Servicio no encontrado")
|
||||
|
||||
update_data = service_data.dict(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(service, field, value)
|
||||
|
||||
service.updated_at = datetime.utcnow()
|
||||
db.commit()
|
||||
db.refresh(service)
|
||||
return service.to_dict()
|
||||
|
||||
|
||||
@router.delete("/{service_id}")
|
||||
async def delete_service(service_id: int, db: Session = Depends(get_db)):
|
||||
"""Eliminar un servicio."""
|
||||
service = db.query(Service).filter(Service.id == service_id).first()
|
||||
if not service:
|
||||
raise HTTPException(status_code=404, detail="Servicio no encontrado")
|
||||
|
||||
db.delete(service)
|
||||
db.commit()
|
||||
return {"message": "Servicio eliminado", "service_id": service_id}
|
||||
|
||||
|
||||
@router.post("/{service_id}/toggle-featured")
|
||||
async def toggle_featured(service_id: int, db: Session = Depends(get_db)):
|
||||
"""Alternar estado de servicio destacado."""
|
||||
service = db.query(Service).filter(Service.id == service_id).first()
|
||||
if not service:
|
||||
raise HTTPException(status_code=404, detail="Servicio no encontrado")
|
||||
|
||||
service.is_featured = not service.is_featured
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"message": f"Servicio {'destacado' if service.is_featured else 'no destacado'}",
|
||||
"is_featured": service.is_featured
|
||||
}
|
||||
Reference in New Issue
Block a user