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>
272 lines
7.3 KiB
Python
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]]
|
|
}
|