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:
271
app/api/routes/image_templates.py
Normal file
271
app/api/routes/image_templates.py
Normal file
@@ -0,0 +1,271 @@
|
||||
"""
|
||||
API Routes for Image Templates.
|
||||
"""
|
||||
|
||||
from typing import Optional, Dict, Any
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.models.image_template import ImageTemplate
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class TemplateCreate(BaseModel):
|
||||
"""Schema for creating an image template."""
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
category: str
|
||||
template_type: str = "general"
|
||||
html_template: Optional[str] = None
|
||||
template_file: Optional[str] = None
|
||||
variables: list
|
||||
design_config: Optional[Dict[str, Any]] = None
|
||||
output_sizes: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
class TemplateUpdate(BaseModel):
|
||||
"""Schema for updating a template."""
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
category: Optional[str] = None
|
||||
template_type: Optional[str] = None
|
||||
html_template: Optional[str] = None
|
||||
variables: Optional[list] = None
|
||||
design_config: Optional[Dict[str, Any]] = None
|
||||
output_sizes: Optional[Dict[str, Any]] = None
|
||||
is_active: Optional[bool] = None
|
||||
|
||||
|
||||
class PreviewRequest(BaseModel):
|
||||
"""Schema for previewing a template."""
|
||||
template_id: Optional[int] = None
|
||||
html_template: Optional[str] = None
|
||||
variables: Dict[str, str]
|
||||
output_size: Optional[Dict[str, int]] = None
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def list_templates(
|
||||
category: Optional[str] = None,
|
||||
template_type: Optional[str] = None,
|
||||
active_only: bool = True,
|
||||
limit: int = Query(50, ge=1, le=200),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
List all image templates.
|
||||
|
||||
- **category**: Filter by category (tip, producto, servicio, etc.)
|
||||
- **template_type**: Filter by type (tip_card, product_card, quote, etc.)
|
||||
- **active_only**: Only show active templates
|
||||
"""
|
||||
query = db.query(ImageTemplate)
|
||||
|
||||
if category:
|
||||
query = query.filter(ImageTemplate.category == category)
|
||||
if template_type:
|
||||
query = query.filter(ImageTemplate.template_type == template_type)
|
||||
if active_only:
|
||||
query = query.filter(ImageTemplate.is_active == True)
|
||||
|
||||
templates = query.order_by(ImageTemplate.name).limit(limit).all()
|
||||
|
||||
return {
|
||||
"templates": [t.to_dict() for t in templates],
|
||||
"count": len(templates)
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{template_id}")
|
||||
async def get_template(
|
||||
template_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Get a specific template with full details.
|
||||
"""
|
||||
template = db.query(ImageTemplate).filter(ImageTemplate.id == template_id).first()
|
||||
if not template:
|
||||
raise HTTPException(status_code=404, detail="Template not found")
|
||||
|
||||
result = template.to_dict()
|
||||
# Include full HTML template
|
||||
if template.html_template:
|
||||
result["full_html_template"] = template.html_template
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/")
|
||||
async def create_template(
|
||||
template_data: TemplateCreate,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Create a new image template.
|
||||
|
||||
Templates can be defined with:
|
||||
- **html_template**: Inline HTML/CSS template
|
||||
- **template_file**: Path to a template file
|
||||
|
||||
Variables are placeholders like: ["title", "content", "accent_color"]
|
||||
"""
|
||||
if not template_data.html_template and not template_data.template_file:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Either html_template or template_file is required"
|
||||
)
|
||||
|
||||
template = ImageTemplate(
|
||||
name=template_data.name,
|
||||
description=template_data.description,
|
||||
category=template_data.category,
|
||||
template_type=template_data.template_type,
|
||||
html_template=template_data.html_template,
|
||||
template_file=template_data.template_file,
|
||||
variables=template_data.variables,
|
||||
design_config=template_data.design_config,
|
||||
output_sizes=template_data.output_sizes,
|
||||
is_active=True
|
||||
)
|
||||
|
||||
db.add(template)
|
||||
db.commit()
|
||||
db.refresh(template)
|
||||
|
||||
return {
|
||||
"message": "Template created successfully",
|
||||
"template": template.to_dict()
|
||||
}
|
||||
|
||||
|
||||
@router.put("/{template_id}")
|
||||
async def update_template(
|
||||
template_id: int,
|
||||
template_data: TemplateUpdate,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Update an existing template.
|
||||
"""
|
||||
template = db.query(ImageTemplate).filter(ImageTemplate.id == template_id).first()
|
||||
if not template:
|
||||
raise HTTPException(status_code=404, detail="Template not found")
|
||||
|
||||
update_data = template_data.dict(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
if value is not None:
|
||||
setattr(template, field, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(template)
|
||||
|
||||
return {
|
||||
"message": "Template updated successfully",
|
||||
"template": template.to_dict()
|
||||
}
|
||||
|
||||
|
||||
@router.delete("/{template_id}")
|
||||
async def delete_template(
|
||||
template_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Delete a template.
|
||||
"""
|
||||
template = db.query(ImageTemplate).filter(ImageTemplate.id == template_id).first()
|
||||
if not template:
|
||||
raise HTTPException(status_code=404, detail="Template not found")
|
||||
|
||||
db.delete(template)
|
||||
db.commit()
|
||||
|
||||
return {"message": "Template deleted successfully"}
|
||||
|
||||
|
||||
@router.post("/preview")
|
||||
async def preview_template(
|
||||
preview_data: PreviewRequest,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Generate a preview of a template with variables.
|
||||
|
||||
You can either provide:
|
||||
- **template_id**: To use an existing template
|
||||
- **html_template**: To preview custom HTML
|
||||
|
||||
The preview returns the rendered HTML (image generation requires separate processing).
|
||||
"""
|
||||
html_template = preview_data.html_template
|
||||
|
||||
if preview_data.template_id:
|
||||
template = db.query(ImageTemplate).filter(
|
||||
ImageTemplate.id == preview_data.template_id
|
||||
).first()
|
||||
|
||||
if not template:
|
||||
raise HTTPException(status_code=404, detail="Template not found")
|
||||
|
||||
html_template = template.html_template
|
||||
|
||||
if not html_template:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="No HTML template available"
|
||||
)
|
||||
|
||||
# Simple variable substitution
|
||||
rendered_html = html_template
|
||||
for var_name, var_value in preview_data.variables.items():
|
||||
rendered_html = rendered_html.replace(f"{{{{{var_name}}}}}", str(var_value))
|
||||
|
||||
# Get output size
|
||||
output_size = preview_data.output_size or {"width": 1080, "height": 1080}
|
||||
|
||||
return {
|
||||
"rendered_html": rendered_html,
|
||||
"output_size": output_size,
|
||||
"variables_used": list(preview_data.variables.keys())
|
||||
}
|
||||
|
||||
|
||||
@router.get("/categories/list")
|
||||
async def list_categories(
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Get list of available template categories.
|
||||
"""
|
||||
from sqlalchemy import distinct
|
||||
|
||||
categories = db.query(distinct(ImageTemplate.category)).filter(
|
||||
ImageTemplate.is_active == True
|
||||
).all()
|
||||
|
||||
return {
|
||||
"categories": [c[0] for c in categories if c[0]]
|
||||
}
|
||||
|
||||
|
||||
@router.get("/types/list")
|
||||
async def list_template_types(
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Get list of available template types.
|
||||
"""
|
||||
from sqlalchemy import distinct
|
||||
|
||||
types = db.query(distinct(ImageTemplate.template_type)).filter(
|
||||
ImageTemplate.is_active == True
|
||||
).all()
|
||||
|
||||
return {
|
||||
"types": [t[0] for t in types if t[0]]
|
||||
}
|
||||
Reference in New Issue
Block a user