feat: Add Analytics, Odoo Integration, A/B Testing, and Content features
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>
This commit is contained in:
160
app/models/thread_series.py
Normal file
160
app/models/thread_series.py
Normal file
@@ -0,0 +1,160 @@
|
||||
"""
|
||||
Thread Series Models - Multi-post thread content scheduling.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, JSON, Boolean
|
||||
from sqlalchemy.orm import relationship
|
||||
import enum
|
||||
|
||||
from app.core.database import Base
|
||||
|
||||
|
||||
class ThreadSeriesStatus(enum.Enum):
|
||||
"""Status options for thread series."""
|
||||
DRAFT = "draft"
|
||||
SCHEDULED = "scheduled"
|
||||
PUBLISHING = "publishing"
|
||||
COMPLETED = "completed"
|
||||
PAUSED = "paused"
|
||||
CANCELLED = "cancelled"
|
||||
|
||||
|
||||
class ThreadSeries(Base):
|
||||
"""
|
||||
A series of related posts published as a thread.
|
||||
E.g., Educational threads, story threads, tip series.
|
||||
"""
|
||||
__tablename__ = "thread_series"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
|
||||
# Series info
|
||||
name = Column(String(255), nullable=False)
|
||||
description = Column(Text, nullable=True)
|
||||
topic = Column(String(100), nullable=True)
|
||||
|
||||
# Platform (threads work best on X and Threads)
|
||||
platform = Column(String(50), nullable=False, index=True)
|
||||
|
||||
# Schedule configuration
|
||||
schedule_type = Column(String(20), default="sequential")
|
||||
# sequential: posts one after another
|
||||
# timed: posts at specific intervals
|
||||
|
||||
interval_minutes = Column(Integer, default=5) # Time between posts
|
||||
start_time = Column(DateTime, nullable=True) # When to start publishing
|
||||
|
||||
# Thread structure
|
||||
total_posts = Column(Integer, default=0)
|
||||
posts_published = Column(Integer, default=0)
|
||||
|
||||
# Status
|
||||
status = Column(String(20), default="draft", index=True)
|
||||
|
||||
# First post in chain (for reply chain)
|
||||
first_platform_post_id = Column(String(100), nullable=True)
|
||||
|
||||
# AI generation settings
|
||||
ai_generated = Column(Boolean, default=False)
|
||||
generation_prompt = Column(Text, nullable=True)
|
||||
|
||||
# Metadata
|
||||
hashtags = Column(JSON, nullable=True) # Common hashtags for the series
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
completed_at = Column(DateTime, nullable=True)
|
||||
|
||||
# Relationships
|
||||
posts = relationship("ThreadPost", back_populates="series", order_by="ThreadPost.sequence_number")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<ThreadSeries {self.id} - {self.name}>"
|
||||
|
||||
def to_dict(self, include_posts: bool = True):
|
||||
"""Convert to dictionary."""
|
||||
result = {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"topic": self.topic,
|
||||
"platform": self.platform,
|
||||
"schedule_type": self.schedule_type,
|
||||
"interval_minutes": self.interval_minutes,
|
||||
"start_time": self.start_time.isoformat() if self.start_time else None,
|
||||
"total_posts": self.total_posts,
|
||||
"posts_published": self.posts_published,
|
||||
"status": self.status,
|
||||
"ai_generated": self.ai_generated,
|
||||
"hashtags": self.hashtags,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||
"completed_at": self.completed_at.isoformat() if self.completed_at else None
|
||||
}
|
||||
|
||||
if include_posts and self.posts:
|
||||
result["posts"] = [p.to_dict() for p in self.posts]
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class ThreadPost(Base):
|
||||
"""
|
||||
Individual post within a thread series.
|
||||
"""
|
||||
__tablename__ = "thread_posts"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
|
||||
# Parent series
|
||||
series_id = Column(Integer, ForeignKey("thread_series.id", ondelete="CASCADE"), nullable=False)
|
||||
|
||||
# Position in thread
|
||||
sequence_number = Column(Integer, nullable=False) # 1, 2, 3, ...
|
||||
|
||||
# Content
|
||||
content = Column(Text, nullable=False)
|
||||
image_url = Column(String(500), nullable=True)
|
||||
|
||||
# Associated post (once created)
|
||||
post_id = Column(Integer, ForeignKey("posts.id"), nullable=True)
|
||||
|
||||
# Platform post ID (for reply chain)
|
||||
platform_post_id = Column(String(100), nullable=True)
|
||||
reply_to_platform_id = Column(String(100), nullable=True) # ID of post to reply to
|
||||
|
||||
# Schedule
|
||||
scheduled_at = Column(DateTime, nullable=True)
|
||||
|
||||
# Status
|
||||
status = Column(String(20), default="pending")
|
||||
# pending, scheduled, published, failed
|
||||
|
||||
error_message = Column(Text, nullable=True)
|
||||
published_at = Column(DateTime, nullable=True)
|
||||
|
||||
# Timestamps
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
series = relationship("ThreadSeries", back_populates="posts")
|
||||
post = relationship("Post", backref="thread_post")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<ThreadPost {self.id} - Series {self.series_id} #{self.sequence_number}>"
|
||||
|
||||
def to_dict(self):
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
"id": self.id,
|
||||
"series_id": self.series_id,
|
||||
"sequence_number": self.sequence_number,
|
||||
"content": self.content[:100] + "..." if len(self.content) > 100 else self.content,
|
||||
"full_content": self.content,
|
||||
"image_url": self.image_url,
|
||||
"post_id": self.post_id,
|
||||
"platform_post_id": self.platform_post_id,
|
||||
"scheduled_at": self.scheduled_at.isoformat() if self.scheduled_at else None,
|
||||
"status": self.status,
|
||||
"error_message": self.error_message,
|
||||
"published_at": self.published_at.isoformat() if self.published_at else None
|
||||
}
|
||||
Reference in New Issue
Block a user