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:
2026-01-28 03:10:42 +00:00
parent 03b5f9f2e2
commit ecc2ca73ea
31 changed files with 6067 additions and 6 deletions

View File

@@ -0,0 +1,74 @@
"""
Recycled Post Model - Track content recycling for evergreen posts.
"""
from datetime import datetime
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, JSON, Boolean
from sqlalchemy.orm import relationship
from app.core.database import Base
class RecycledPost(Base):
"""
Tracks when posts are recycled/republished.
Evergreen content that performed well can be republished with modifications.
"""
__tablename__ = "recycled_posts"
id = Column(Integer, primary_key=True, index=True)
# Original post reference
original_post_id = Column(Integer, ForeignKey("posts.id"), nullable=False, index=True)
# New post created from recycling
new_post_id = Column(Integer, ForeignKey("posts.id"), nullable=True, index=True)
# Recycle count (how many times this original has been recycled)
recycle_number = Column(Integer, default=1)
# Modifications made
modifications = Column(JSON, nullable=True)
# Example: {"content_changed": true, "hashtags_updated": true, "image_changed": false}
modification_notes = Column(Text, nullable=True)
# Performance comparison
original_engagement_rate = Column(Integer, nullable=True)
new_engagement_rate = Column(Integer, nullable=True)
# Status
status = Column(String(20), default="pending")
# pending, published, cancelled
# Reason for recycling
reason = Column(String(100), nullable=True)
# high_performer, evergreen, seasonal, manual
# Timestamps
recycled_at = Column(DateTime, default=datetime.utcnow)
scheduled_for = Column(DateTime, nullable=True)
# Relationships
original_post = relationship("Post", foreign_keys=[original_post_id], backref="recycled_versions")
new_post = relationship("Post", foreign_keys=[new_post_id])
def __repr__(self):
return f"<RecycledPost {self.id} - Original: {self.original_post_id}>"
def to_dict(self):
"""Convert to dictionary."""
return {
"id": self.id,
"original_post_id": self.original_post_id,
"new_post_id": self.new_post_id,
"recycle_number": self.recycle_number,
"modifications": self.modifications,
"modification_notes": self.modification_notes,
"original_engagement_rate": self.original_engagement_rate,
"new_engagement_rate": self.new_engagement_rate,
"status": self.status,
"reason": self.reason,
"recycled_at": self.recycled_at.isoformat() if self.recycled_at else None,
"scheduled_for": self.scheduled_for.isoformat() if self.scheduled_for else None
}