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:
116
app/models/analytics_report.py
Normal file
116
app/models/analytics_report.py
Normal file
@@ -0,0 +1,116 @@
|
||||
"""
|
||||
Analytics Report Model - Aggregated analytics snapshots.
|
||||
"""
|
||||
|
||||
from datetime import datetime, date
|
||||
from sqlalchemy import Column, Integer, Float, String, DateTime, Date, JSON, Text
|
||||
|
||||
from app.core.database import Base
|
||||
|
||||
|
||||
class AnalyticsReport(Base):
|
||||
"""
|
||||
Stores aggregated analytics reports (daily, weekly, monthly).
|
||||
"""
|
||||
__tablename__ = "analytics_reports"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
|
||||
# Report type and period
|
||||
report_type = Column(String(20), nullable=False, index=True) # daily, weekly, monthly
|
||||
period_start = Column(Date, nullable=False, index=True)
|
||||
period_end = Column(Date, nullable=False)
|
||||
platform = Column(String(50), nullable=True) # null = all platforms
|
||||
|
||||
# Aggregated metrics
|
||||
total_posts = Column(Integer, default=0)
|
||||
total_impressions = Column(Integer, default=0)
|
||||
total_reach = Column(Integer, default=0)
|
||||
total_engagements = Column(Integer, default=0)
|
||||
total_likes = Column(Integer, default=0)
|
||||
total_comments = Column(Integer, default=0)
|
||||
total_shares = Column(Integer, default=0)
|
||||
|
||||
# Calculated averages
|
||||
avg_engagement_rate = Column(Float, default=0.0)
|
||||
avg_impressions_per_post = Column(Float, default=0.0)
|
||||
avg_engagements_per_post = Column(Float, default=0.0)
|
||||
|
||||
# Comparison with previous period
|
||||
posts_change_pct = Column(Float, nullable=True)
|
||||
engagement_change_pct = Column(Float, nullable=True)
|
||||
impressions_change_pct = Column(Float, nullable=True)
|
||||
|
||||
# Top performing data (JSON)
|
||||
top_posts = Column(JSON, nullable=True)
|
||||
# [{"post_id": 1, "content": "...", "engagement_rate": 5.2, "platform": "x"}]
|
||||
|
||||
best_times = Column(JSON, nullable=True)
|
||||
# [{"day": 1, "hour": 12, "avg_engagement": 4.5}]
|
||||
|
||||
content_performance = Column(JSON, nullable=True)
|
||||
# {"tip": {"posts": 10, "avg_engagement": 3.2}, "product": {...}}
|
||||
|
||||
platform_breakdown = Column(JSON, nullable=True)
|
||||
# {"x": {"posts": 20, "engagement": 150}, "threads": {...}}
|
||||
|
||||
# Report content for Telegram
|
||||
summary_text = Column(Text, nullable=True)
|
||||
|
||||
# Metadata
|
||||
generated_at = Column(DateTime, default=datetime.utcnow)
|
||||
sent_to_telegram = Column(DateTime, nullable=True)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"report_type": self.report_type,
|
||||
"period_start": self.period_start.isoformat() if self.period_start else None,
|
||||
"period_end": self.period_end.isoformat() if self.period_end else None,
|
||||
"platform": self.platform,
|
||||
"total_posts": self.total_posts,
|
||||
"total_impressions": self.total_impressions,
|
||||
"total_reach": self.total_reach,
|
||||
"total_engagements": self.total_engagements,
|
||||
"avg_engagement_rate": round(self.avg_engagement_rate, 2),
|
||||
"avg_impressions_per_post": round(self.avg_impressions_per_post, 1),
|
||||
"posts_change_pct": round(self.posts_change_pct, 1) if self.posts_change_pct else None,
|
||||
"engagement_change_pct": round(self.engagement_change_pct, 1) if self.engagement_change_pct else None,
|
||||
"top_posts": self.top_posts,
|
||||
"best_times": self.best_times,
|
||||
"content_performance": self.content_performance,
|
||||
"platform_breakdown": self.platform_breakdown,
|
||||
"generated_at": self.generated_at.isoformat() if self.generated_at else None
|
||||
}
|
||||
|
||||
def generate_telegram_summary(self) -> str:
|
||||
"""Generate formatted summary for Telegram."""
|
||||
lines = [
|
||||
f"📊 *Reporte {self.report_type.title()}*",
|
||||
f"📅 {self.period_start} - {self.period_end}",
|
||||
"",
|
||||
f"📝 Posts publicados: *{self.total_posts}*",
|
||||
f"👁 Impresiones: *{self.total_impressions:,}*",
|
||||
f"💬 Interacciones: *{self.total_engagements:,}*",
|
||||
f"📈 Engagement rate: *{self.avg_engagement_rate:.2f}%*",
|
||||
]
|
||||
|
||||
if self.engagement_change_pct is not None:
|
||||
emoji = "📈" if self.engagement_change_pct > 0 else "📉"
|
||||
lines.append(f"{emoji} vs anterior: *{self.engagement_change_pct:+.1f}%*")
|
||||
|
||||
if self.platform_breakdown:
|
||||
lines.append("")
|
||||
lines.append("*Por plataforma:*")
|
||||
for platform, data in self.platform_breakdown.items():
|
||||
lines.append(f" • {platform}: {data.get('posts', 0)} posts, {data.get('engagements', 0)} interacciones")
|
||||
|
||||
if self.top_posts and len(self.top_posts) > 0:
|
||||
lines.append("")
|
||||
lines.append("*Top 3 posts:*")
|
||||
for i, post in enumerate(self.top_posts[:3], 1):
|
||||
content = post.get('content', '')[:50] + "..." if len(post.get('content', '')) > 50 else post.get('content', '')
|
||||
lines.append(f" {i}. {content} ({post.get('engagement_rate', 0):.1f}%)")
|
||||
|
||||
self.summary_text = "\n".join(lines)
|
||||
return self.summary_text
|
||||
Reference in New Issue
Block a user