Definición de macros
Los macros en Jinja2 son funciones reutilizables que permiten encapsular fragmentos de código HTML repetitivo, facilitando el mantenimiento y la consistencia en nuestras aplicaciones FastAPI. Un macro funciona de manera similar a una función en Python, pero genera código HTML en lugar de ejecutar lógica de programación.
¿Qué es un macro en Jinja2?
Un macro es esencialmente una plantilla reutilizable que acepta parámetros y genera contenido HTML dinámico. Los macros resuelven el problema de la duplicación de código en plantillas, permitiendo definir una vez un componente y reutilizarlo múltiples veces con diferentes datos.
La sintaxis básica para definir un macro utiliza las etiquetas {% macro %}
y {% endmacro %}
:
{% macro nombre_macro(parametros) %}
<!-- Contenido HTML del macro -->
{% endmacro %}
Creación de un macro simple
Veamos un ejemplo básico de un macro que genera una tarjeta de usuario:
{% macro user_card(name, email, role="Usuario") %}
<div class="user-card">
<h3>{{ name }}</h3>
<p class="email">{{ email }}</p>
<span class="role">{{ role }}</span>
</div>
{% endmacro %}
Este macro acepta tres parámetros: name
y email
son obligatorios, mientras que role
tiene un valor por defecto. Para utilizar este macro, simplemente lo invocamos con los parámetros necesarios:
{{ user_card("Ana García", "ana@ejemplo.com", "Administradora") }}
{{ user_card("Carlos López", "carlos@ejemplo.com") }}
Macros con lógica condicional
Los macros pueden incluir estructuras de control para generar contenido dinámico más complejo. Consideremos un macro para mostrar alertas:
{% macro alert(message, type="info") %}
<div class="alert alert-{{ type }}">
{% if type == "danger" %}
<i class="icon-error"></i>
{% elif type == "success" %}
<i class="icon-check"></i>
{% else %}
<i class="icon-info"></i>
{% endif %}
<span>{{ message }}</span>
</div>
{% endmacro %}
Este macro genera diferentes tipos de alertas según el parámetro type
, mostrando iconos apropiados para cada situación:
{{ alert("Datos guardados correctamente", "success") }}
{{ alert("Error al procesar la solicitud", "danger") }}
{{ alert("Información importante") }}
Macros con bucles y datos complejos
Los macros pueden procesar estructuras de datos complejas como listas o diccionarios. Un ejemplo útil es un macro para generar tablas HTML:
{% macro data_table(headers, rows) %}
<table class="table">
<thead>
<tr>
{% for header in headers %}
<th>{{ header }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for row in rows %}
<tr>
{% for cell in row %}
<td>{{ cell }}</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
{% endmacro %}
Para usar este macro desde FastAPI, pasaríamos los datos desde la ruta:
from fastapi import FastAPI, Request
from fastapi.templating import Jinja2Templates
app = FastAPI()
templates = Jinja2Templates(directory="templates")
@app.get("/productos")
async def listar_productos(request: Request):
headers = ["ID", "Nombre", "Precio", "Stock"]
productos = [
[1, "Laptop", "$999", 5],
[2, "Mouse", "$29", 15],
[3, "Teclado", "$79", 8]
]
return templates.TemplateResponse(
"productos.html",
{"request": request, "headers": headers, "productos": productos}
)
Y en el template productos.html
:
<!DOCTYPE html>
<html>
<head>
<title>Lista de Productos</title>
</head>
<body>
{{ data_table(headers, productos) }}
</body>
</html>
Macros con contenido anidado
Los macros pueden incluir contenido anidado utilizando la palabra clave caller
. Esto permite crear macros que actúan como contenedores:
{% macro modal(title, id) %}
<div class="modal" id="{{ id }}">
<div class="modal-content">
<div class="modal-header">
<h2>{{ title }}</h2>
<button class="close" data-dismiss="modal">×</button>
</div>
<div class="modal-body">
{{ caller() }}
</div>
</div>
</div>
{% endmacro %}
Para usar este macro con contenido personalizado:
{% call modal("Confirmar acción", "confirm-modal") %}
<p>¿Estás seguro de que deseas eliminar este elemento?</p>
<button class="btn btn-danger">Confirmar</button>
<button class="btn btn-secondary" data-dismiss="modal">Cancelar</button>
{% endcall %}
Organización de macros en archivos separados
Para mantener organizados los macros en aplicaciones grandes, es recomendable crear archivos dedicados. Por ejemplo, crear macros.html
:
<!-- macros.html -->
{% macro form_field(name, label, type="text", required=false) %}
<div class="form-group">
<label for="{{ name }}">
{{ label }}
{% if required %}<span class="required">*</span>{% endif %}
</label>
<input type="{{ type }}" id="{{ name }}" name="{{ name }}"
class="form-control" {% if required %}required{% endif %}>
</div>
{% endmacro %}
{% macro submit_button(text="Enviar", class="btn-primary") %}
<button type="submit" class="btn {{ class }}">{{ text }}</button>
{% endmacro %}
Para importar estos macros en otras plantillas:
{% from 'macros.html' import form_field, submit_button %}
<form method="post">
{{ form_field("name", "Nombre completo", required=true) }}
{{ form_field("email", "Correo electrónico", "email", required=true) }}
{{ form_field("phone", "Teléfono", "tel") }}
{{ submit_button("Registrarse", "btn-success") }}
</form>
Ventajas de utilizar macros
Los macros proporcionan varios beneficios importantes:
- Reutilización: Evitan duplicación de código HTML repetitivo
- Mantenimiento: Cambios en un macro se reflejan automáticamente en todos los lugares donde se usa
- Consistencia: Garantizan que los componentes tengan el mismo aspecto y comportamiento
- Legibilidad: Hacen que las plantillas sean más fáciles de leer y entender
- Parametrización: Permiten personalizar el contenido sin duplicar la estructura
Los macros se convierten en una herramienta fundamental cuando desarrollamos aplicaciones FastAPI con interfaces de usuario complejas, ya que nos permiten crear componentes reutilizables que mantienen la coherencia visual y funcional a lo largo de toda la aplicación.
Funciones globales personalizadas
Las funciones globales personalizadas en Jinja2 son funciones Python que se registran en el entorno de templates para estar disponibles directamente en cualquier plantilla sin necesidad de importarlas. A diferencia de los macros, estas funciones se ejecutan del lado del servidor y pueden realizar operaciones más complejas antes de renderizar el contenido.
Diferencias entre macros y funciones globales
Mientras que los macros son fragmentos de código HTML reutilizables definidos dentro de las plantillas, las funciones globales son funciones Python que se definen en el código del servidor y se registran en el entorno de Jinja2. Las funciones globales permiten:
- Ejecutar lógica de negocio compleja
- Acceder a bases de datos o APIs externas
- Realizar cálculos matemáticos avanzados
- Formatear datos de manera específica
- Integrar funcionalidades del sistema
Registrando funciones globales en FastAPI
Para agregar funciones personalizadas al entorno de Jinja2 en FastAPI, necesitamos acceder al objeto Environment
y usar el método globals.update()
:
from fastapi import FastAPI, Request
from fastapi.templating import Jinja2Templates
import datetime
from decimal import Decimal
app = FastAPI()
templates = Jinja2Templates(directory="templates")
# Función personalizada para formatear fechas
def format_date(date_obj, format_str="%d/%m/%Y"):
"""Formatea una fecha según el formato especificado"""
if isinstance(date_obj, str):
date_obj = datetime.datetime.fromisoformat(date_obj)
return date_obj.strftime(format_str)
# Función para formatear precios
def format_currency(amount, currency="€"):
"""Formatea un número como moneda"""
return f"{amount:.2f} {currency}"
# Función para calcular descuentos
def calculate_discount(price, discount_percent):
"""Calcula el precio con descuento aplicado"""
discount_amount = price * (discount_percent / 100)
return price - discount_amount
# Registrar las funciones en el entorno de Jinja2
templates.env.globals.update({
'format_date': format_date,
'format_currency': format_currency,
'calculate_discount': calculate_discount,
'now': datetime.datetime.now
})
Usando las funciones globales en templates
Una vez registradas, las funciones están disponibles en cualquier template sin necesidad de pasarlas como variables:
<!DOCTYPE html>
<html>
<head>
<title>Productos en Oferta</title>
</head>
<body>
<h1>Ofertas del {{ format_date(now(), "%d de %B de %Y") }}</h1>
<div class="products">
{% for product in products %}
<div class="product-card">
<h3>{{ product.name }}</h3>
<p class="original-price">
Precio original: {{ format_currency(product.price) }}
</p>
<p class="discount-price">
Precio con descuento: {{ format_currency(calculate_discount(product.price, 15)) }}
</p>
<small>Oferta válida hasta {{ format_date(product.offer_end) }}</small>
</div>
{% endfor %}
</div>
</body>
</html>
Funciones globales con parámetros opcionales
Las funciones globales pueden incluir parámetros con valores por defecto y lógica condicional compleja:
def truncate_text(text, max_length=100, suffix="..."):
"""Trunca un texto a una longitud específica"""
if len(text) <= max_length:
return text
return text[:max_length].rstrip() + suffix
def get_status_badge(status):
"""Genera un badge HTML según el estado"""
status_config = {
'active': {'class': 'badge-success', 'text': 'Activo'},
'inactive': {'class': 'badge-danger', 'text': 'Inactivo'},
'pending': {'class': 'badge-warning', 'text': 'Pendiente'},
'draft': {'class': 'badge-secondary', 'text': 'Borrador'}
}
config = status_config.get(status, {'class': 'badge-light', 'text': 'Desconocido'})
return f'<span class="badge {config["class"]}">{config["text"]}</span>'
# Registrar las nuevas funciones
templates.env.globals.update({
'truncate_text': truncate_text,
'get_status_badge': get_status_badge
})
Estas funciones se pueden usar directamente en los templates:
<div class="article-list">
{% for article in articles %}
<article>
<h2>{{ article.title }}</h2>
<p>{{ truncate_text(article.content, 150) }}</p>
<div class="article-meta">
{{ get_status_badge(article.status)|safe }}
<span class="date">{{ format_date(article.created_at) }}</span>
</div>
</article>
{% endfor %}
</div>
Funciones globales para utilidades comunes
Las funciones globales son especialmente útiles para operaciones frecuentes que se necesitan en múltiples templates:
import math
import re
from urllib.parse import urlencode
def pluralize(count, singular, plural=None):
"""Devuelve la forma singular o plural según la cantidad"""
if plural is None:
plural = singular + "s"
return singular if count == 1 else plural
def paginate_range(current_page, total_pages, window=2):
"""Genera un rango de páginas para paginación"""
start = max(1, current_page - window)
end = min(total_pages, current_page + window)
return range(start, end + 1)
def build_query_string(**params):
"""Construye una query string a partir de parámetros"""
# Filtra valores None y vacíos
clean_params = {k: v for k, v in params.items() if v is not None and v != ''}
return urlencode(clean_params)
def extract_domain(url):
"""Extrae el dominio de una URL"""
pattern = r'https?://([^/]+)'
match = re.search(pattern, url)
return match.group(1) if match else url
# Registrar funciones de utilidad
templates.env.globals.update({
'pluralize': pluralize,
'paginate_range': paginate_range,
'build_query_string': build_query_string,
'extract_domain': extract_domain,
'abs': abs,
'round': round,
'len': len
})
Integración con FastAPI y datos dinámicos
Las funciones globales pueden acceder a servicios o datos externos. Aquí un ejemplo que integra con una base de datos:
from sqlalchemy.orm import Session
from database import get_db
from models import User, Category
def get_user_name(user_id):
"""Obtiene el nombre de usuario por ID"""
db = next(get_db()) # En producción, usar dependency injection apropiado
try:
user = db.query(User).filter(User.id == user_id).first()
return user.name if user else "Usuario desconocido"
finally:
db.close()
def get_category_tree():
"""Obtiene el árbol de categorías"""
db = next(get_db())
try:
categories = db.query(Category).filter(Category.parent_id.is_(None)).all()
return categories
finally:
db.close()
def calculate_reading_time(content):
"""Calcula el tiempo estimado de lectura"""
word_count = len(content.split())
# Promedio de 200 palabras por minuto
minutes = math.ceil(word_count / 200)
return f"{minutes} min de lectura"
# Registrar funciones que acceden a datos
templates.env.globals.update({
'get_user_name': get_user_name,
'get_category_tree': get_category_tree,
'calculate_reading_time': calculate_reading_time
})
Ejemplo práctico completo
Veamos un ejemplo completo que utiliza múltiples funciones globales personalizadas en una aplicación FastAPI:
from fastapi import FastAPI, Request, Depends
from fastapi.templating import Jinja2Templates
from sqlalchemy.orm import Session
import datetime
app = FastAPI()
templates = Jinja2Templates(directory="templates")
# Funciones globales personalizadas
def is_recent(date_obj, days=7):
"""Verifica si una fecha es reciente"""
if isinstance(date_obj, str):
date_obj = datetime.datetime.fromisoformat(date_obj)
delta = datetime.datetime.now() - date_obj
return delta.days <= days
def get_initials(full_name):
"""Obtiene las iniciales de un nombre completo"""
words = full_name.strip().split()
return ''.join(word[0].upper() for word in words if word)
def format_file_size(bytes_size):
"""Formatea el tamaño de archivo en unidades legibles"""
for unit in ['B', 'KB', 'MB', 'GB']:
if bytes_size < 1024:
return f"{bytes_size:.1f} {unit}"
bytes_size /= 1024
return f"{bytes_size:.1f} TB"
# Registrar todas las funciones globales
templates.env.globals.update({
'is_recent': is_recent,
'get_initials': get_initials,
'format_file_size': format_file_size,
'enumerate': enumerate,
'zip': zip
})
@app.get("/dashboard")
async def dashboard(request: Request):
# Datos de ejemplo
documents = [
{
'name': 'Informe_Q1_2024.pdf',
'author': 'María González',
'size': 2048576,
'created_at': '2024-01-15T10:30:00'
},
{
'name': 'Presentación_Ventas.pptx',
'author': 'Carlos Ruiz',
'size': 5242880,
'created_at': '2024-01-20T14:15:00'
}
]
return templates.TemplateResponse(
"dashboard.html",
{"request": request, "documents": documents}
)
Y el template correspondiente:
<!DOCTYPE html>
<html>
<head>
<title>Dashboard de Documentos</title>
<style>
.recent { color: #28a745; font-weight: bold; }
.avatar {
display: inline-block;
width: 40px;
height: 40px;
border-radius: 50%;
background: #007bff;
color: white;
text-align: center;
line-height: 40px;
}
</style>
</head>
<body>
<h1>Documentos Recientes</h1>
<div class="documents-grid">
{% for doc in documents %}
<div class="document-card">
<div class="doc-header">
<span class="avatar">{{ get_initials(doc.author) }}</span>
<div class="doc-info">
<h3>{{ doc.name }}</h3>
<p>por {{ doc.author }}</p>
</div>
</div>
<div class="doc-meta">
<span class="file-size">{{ format_file_size(doc.size) }}</span>
<span class="created-date {% if is_recent(doc.created_at) %}recent{% endif %}">
{{ format_date(doc.created_at, "%d %b %Y") }}
{% if is_recent(doc.created_at) %}(Nuevo){% endif %}
</span>
</div>
</div>
{% endfor %}
</div>
</body>
</html>
Consideraciones importantes
Al trabajar con funciones globales personalizadas, es importante tener en cuenta:
- Rendimiento: Las funciones que acceden a bases de datos pueden impactar el rendimiento si se ejecutan múltiples veces por template
- Seguridad: Funciones que generan HTML deben usar el filtro
|safe
con precaución - Mantenimiento: Organizar las funciones en módulos separados cuando la aplicación crece
- Testing: Las funciones globales deben ser testeadas independientemente
- Documentación: Documentar claramente qué hace cada función y sus parámetros
Las funciones globales personalizadas proporcionan una manera elegante de extender las capacidades de Jinja2, permitiendo que la lógica de presentación compleja se mantenga organizada y reutilizable en toda la aplicación FastAPI.
Fuentes y referencias
Documentación oficial y recursos externos para profundizar en FastAPI
Documentación oficial de FastAPI
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, FastAPI es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de FastAPI
Explora más contenido relacionado con FastAPI y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
- Comprender qué son los macros en Jinja2 y cómo definirlos para reutilizar código HTML.
- Aprender a incluir lógica condicional, bucles y contenido anidado dentro de macros.
- Conocer cómo organizar macros en archivos separados e importarlos en plantillas.
- Entender la diferencia entre macros y funciones globales personalizadas en Jinja2.
- Saber registrar y utilizar funciones globales personalizadas en FastAPI para operaciones complejas y reutilizables en templates.