Phase 1 - Analytics y Reportes: - PostMetrics and AnalyticsReport models for tracking engagement - Analytics service with dashboard stats, top posts, optimal times - 8 API endpoints at /api/analytics/* - Interactive dashboard with Chart.js charts - Celery tasks for metrics fetch (15min) and weekly reports Phase 2 - Integración Odoo: - Lead and OdooSyncLog models for CRM integration - Odoo fields added to Product and Service models - XML-RPC service for bidirectional sync - Lead management API at /api/leads/* - Leads dashboard template - Celery tasks for product/service sync and lead export Phase 3 - A/B Testing y Recycling: - ABTest, ABTestVariant, RecycledPost models - Statistical winner analysis using chi-square test - Content recycling with engagement-based scoring - APIs at /api/ab-tests/* and /api/recycling/* - Automated test evaluation and content recycling tasks Phase 4 - Thread Series y Templates: - ThreadSeries and ThreadPost models for multi-post threads - AI-powered thread generation - Enhanced ImageTemplate with HTML template support - APIs at /api/threads/* and /api/templates/* - Thread scheduling with reply chain support Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
151 lines
5.3 KiB
Python
151 lines
5.3 KiB
Python
"""
|
|
Modelo de Post - Posts generados y programados.
|
|
"""
|
|
|
|
from datetime import datetime
|
|
from sqlalchemy import Column, Integer, String, Text, Boolean, DateTime, ForeignKey, JSON, Enum
|
|
from sqlalchemy.dialects.postgresql import ARRAY
|
|
from sqlalchemy.orm import relationship
|
|
import enum
|
|
|
|
from app.core.database import Base
|
|
|
|
|
|
class PostStatus(enum.Enum):
|
|
"""Estados posibles de un post."""
|
|
DRAFT = "draft"
|
|
PENDING_APPROVAL = "pending_approval"
|
|
APPROVED = "approved"
|
|
SCHEDULED = "scheduled"
|
|
PUBLISHING = "publishing"
|
|
PUBLISHED = "published"
|
|
FAILED = "failed"
|
|
CANCELLED = "cancelled"
|
|
|
|
|
|
class ContentType(enum.Enum):
|
|
"""Tipos de contenido."""
|
|
TIP_TECH = "tip_tech"
|
|
DATO_CURIOSO = "dato_curioso"
|
|
FRASE_MOTIVACIONAL = "frase_motivacional"
|
|
EFEMERIDE = "efemeride"
|
|
PRODUCTO = "producto"
|
|
SERVICIO = "servicio"
|
|
HILO_EDUCATIVO = "hilo_educativo"
|
|
CASO_EXITO = "caso_exito"
|
|
PROMOCION = "promocion"
|
|
ANUNCIO = "anuncio"
|
|
MANUAL = "manual"
|
|
|
|
|
|
class Post(Base):
|
|
"""Modelo para posts de redes sociales."""
|
|
|
|
__tablename__ = "posts"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
|
|
# Contenido
|
|
content = Column(Text, nullable=False)
|
|
content_type = Column(String(50), nullable=False, index=True)
|
|
|
|
# Contenido adaptado por plataforma (opcional)
|
|
content_x = Column(Text, nullable=True) # Versión para X (280 chars)
|
|
content_threads = Column(Text, nullable=True)
|
|
content_instagram = Column(Text, nullable=True)
|
|
content_facebook = Column(Text, nullable=True)
|
|
|
|
# Plataformas destino
|
|
platforms = Column(ARRAY(String), nullable=False)
|
|
# Ejemplo: ["x", "threads", "instagram", "facebook"]
|
|
|
|
# Estado y programación
|
|
status = Column(String(50), default="draft", index=True)
|
|
scheduled_at = Column(DateTime, nullable=True, index=True)
|
|
published_at = Column(DateTime, nullable=True)
|
|
|
|
# Imagen
|
|
image_url = Column(String(500), nullable=True)
|
|
image_template_id = Column(Integer, ForeignKey("image_templates.id"), nullable=True)
|
|
|
|
# IDs de publicación en cada plataforma
|
|
platform_post_ids = Column(JSON, nullable=True)
|
|
# Ejemplo: {"x": "123456", "instagram": "789012", ...}
|
|
|
|
# Errores de publicación
|
|
error_message = Column(Text, nullable=True)
|
|
retry_count = Column(Integer, default=0)
|
|
|
|
# Aprobación
|
|
approval_required = Column(Boolean, default=False)
|
|
approved_by = Column(String(100), nullable=True)
|
|
approved_at = Column(DateTime, nullable=True)
|
|
|
|
# Relaciones con contenido fuente
|
|
product_id = Column(Integer, ForeignKey("products.id"), nullable=True)
|
|
service_id = Column(Integer, ForeignKey("services.id"), nullable=True)
|
|
tip_template_id = Column(Integer, ForeignKey("tip_templates.id"), nullable=True)
|
|
|
|
# Metadatos
|
|
hashtags = Column(ARRAY(String), nullable=True)
|
|
mentions = Column(ARRAY(String), nullable=True)
|
|
|
|
# Métricas (actualizadas después de publicar)
|
|
metrics = Column(JSON, nullable=True)
|
|
# Ejemplo: {"likes": 10, "retweets": 5, "comments": 3}
|
|
|
|
# A/B Testing
|
|
ab_test_id = Column(Integer, ForeignKey("ab_tests.id"), nullable=True, index=True)
|
|
|
|
# Recycling
|
|
is_recyclable = Column(Boolean, default=True)
|
|
recycled_from_id = Column(Integer, ForeignKey("posts.id"), nullable=True)
|
|
recycle_count = Column(Integer, default=0) # Times this post has been recycled
|
|
|
|
# Timestamps
|
|
created_at = Column(DateTime, default=datetime.utcnow)
|
|
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
|
|
|
# Relationships
|
|
metrics_history = relationship("PostMetrics", back_populates="post", cascade="all, delete-orphan")
|
|
|
|
def __repr__(self):
|
|
return f"<Post {self.id} - {self.status}>"
|
|
|
|
def to_dict(self):
|
|
"""Convertir a diccionario."""
|
|
return {
|
|
"id": self.id,
|
|
"content": self.content,
|
|
"content_type": self.content_type,
|
|
"content_x": self.content_x,
|
|
"content_threads": self.content_threads,
|
|
"content_instagram": self.content_instagram,
|
|
"content_facebook": self.content_facebook,
|
|
"platforms": self.platforms,
|
|
"status": self.status,
|
|
"scheduled_at": self.scheduled_at.isoformat() if self.scheduled_at else None,
|
|
"published_at": self.published_at.isoformat() if self.published_at else None,
|
|
"image_url": self.image_url,
|
|
"platform_post_ids": self.platform_post_ids,
|
|
"error_message": self.error_message,
|
|
"approval_required": self.approval_required,
|
|
"hashtags": self.hashtags,
|
|
"metrics": self.metrics,
|
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
|
"ab_test_id": self.ab_test_id,
|
|
"is_recyclable": self.is_recyclable,
|
|
"recycled_from_id": self.recycled_from_id,
|
|
"recycle_count": self.recycle_count
|
|
}
|
|
|
|
def get_content_for_platform(self, platform: str) -> str:
|
|
"""Obtener contenido adaptado para una plataforma específica."""
|
|
platform_content = {
|
|
"x": self.content_x,
|
|
"threads": self.content_threads,
|
|
"instagram": self.content_instagram,
|
|
"facebook": self.content_facebook
|
|
}
|
|
return platform_content.get(platform) or self.content
|