Files
social-media-automation/app/api/routes/image_templates.py
Consultoría AS ecc2ca73ea 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>
2026-01-28 03:10:42 +00:00

272 lines
7.3 KiB
Python

"""
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]]
}