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:
74
app/models/recycled_post.py
Normal file
74
app/models/recycled_post.py
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user