Don't Repeat Yourself (DRY) con Jinja2
Pon un nombre de página en un archivo de plantilla Jinja2 y compártelo en todas partes.
Estaba probando algunas cosas con Jinja2, creé una pequeña aplicación Flask , y pensé por qué no compartir esto. Lo que quería conseguir era poner todos los nombres de página en un archivo de plantilla.
Al igual que con Python, cuando escribes mucho código, debes tener cuidado de no repetirte con Jinja2. Antes de que te des cuenta, acabas con muchos archivos de plantilla que contienen los mismos tipos de información. Y cuando quieres cambiar algo, debes editar todos estos archivos de plantilla.
Este post contiene todo el código de una aplicación básica Flask :
- Cuatro páginas, una con un formulario
- Diseño en dos columnas
- Cabecera con navegación
- Utiliza Bootstrap (no puedo vivir sin él ...)
En caso de que quieras probar, sólo hay dos archivos que necesitas crear. Usé Jinja2's DictLoader para poner las plantillas en un archivo Python .
Como siempre hago esto en Ubuntu 22.04.
Extender, extender, extender
Después de escribir algo de código, quieres ver la página. Creas la plantilla. Añades un encabezado, pie de página, título, menú, etc. ¡No hagas esto! ¡Sólo la salida de su endpoint debe estar en esta plantilla!
A continuación se muestra lo que vamos a lograr. El FAQ TEMPLATE sólo contiene 'faq data', ¡y nada más!
FAQ
endpoint
|
| faq
| data
v
+-----------------------------------+
| FAQ TEMPLATE |
| |
| {% extends 'page_content.html' %} |
| |
| {%- block page_data -%} |
| ... |
| faq data |
| ... |
| {%- endblock -%} |
+-----------------------------------+
|
|
| page right column
| data +-- data
| |
v v
+---------------------------------------------+
| PAGE CONTENT TEMPLATE |
| |
| {% extends 'base.html' %} |
| |
| {%- block page_content -%} |
| ... |
| {% block page_data %}{% endblock %} |
| ... |
| right column data |
| ... |
| {%- endblock -%} |
+---------------------------------------------+
|
| page content page header
| data +-- html
| |
v v
+---------------------------------------------+
| BASE TEMPLATE |
| |
| {% include 'page_header.html' %} |
| ... |
| {% block page_content %}{% endblock %} |
| ... |
+---------------------------------------------+
|
v
rendered page
Variables globales en Jinja2
Podemos pasar variables globales, o estructuras de datos, a nuestras plantillas de varias maneras:
Código Python - Flask's app.config
Ejemplo:
app.config['MY_GLOBAL_VAR_KEY'] = 'my_global_var_value'
Python código - Jinja2 Environment.globals
Ejemplo:
app.jinja_env.globals['MY_GLOBAL_VAR_KEY'] = 'my_global_var_value'
Python código - Jinja2 Context processor
Ejemplo:
@app.context_processor
def inject_data():
data = {}
data['MY_GLOBAL_VAR_KEY'] = 'my_global_var_value'
return data
Código Jinja2 - Un archivo de plantilla Jinja2 con variables
Esta solución requiere que incluyamos esta plantilla cuando necesitemos una o más variables. La ventaja es que podemos tener todos los nombres de página en un único archivo de plantilla Jinja2 .
Ejemplo:
{%- set global_vars = {
'MY_GLOBAL_VAR_KEY': 'my_global_var_value',
} -&}
Uso de page_ids
Las funciones endpoint pueden mostrar datos específicos de la página, como su nombre. Pero también se utilizan nombres de página en la navegación del sitio. Y no queremos repetirnos.
Resolvemos esto creando un único archivo de plantilla Jinja2 que contiene información sobre nuestras páginas, incluidos los nombres de página. Para hacer referencia a la información de la página, utilizamos un page_id. Y utilizamos un Jinja2 macro para extraer la información que queremos.
Ejecución de la aplicación
Crea un virtual environment e instala Flask y WTForms:
pip install flask
pip install flask-wtf
Aquí está el árbol de los directorios y archivos. Para esta app Flask uso el Jinja2 DictLoader: Todas las plantillas están en factory.py lo que significa que ¡sólo necesitas crear dos archivos!
.
├── project
│ ├── app
│ │ ├── templates (all templates are in factory.py)
│ │ │ ├── pages
│ │ │ │ ├── contact_form.html
│ │ │ │ ├── contact_form_received.html
│ │ │ │ ├── faq.html
│ │ │ │ └── home.html
│ │ │ ├── shared
│ │ │ │ ├── page_vars.html
│ │ │ │ └── page_macros.html
│ │ │ ├── base.html
│ │ │ ├── page_content.html
│ │ │ └── page_header.html
│ │ └── factory.py
│ └── run.py
El código
Tenemos dos archivos Python :
- run.py, en el directorio 'project'
- factory.py, en el directorio 'app'
# run.py
from app.factory import create_app
host = '127.0.0.1'
port = 5050
app = create_app()
app.config['SERVER_NAME'] = host + ':' + str(port)
if __name__ == '__main__':
app.run(
host=host,
port=port,
use_debugger=True,
use_reloader=True,
)
# factory.py
from flask import current_app, Flask, redirect, render_template, url_for
from flask_wtf import FlaskForm
from jinja2 import Environment, DictLoader, FileSystemLoader
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired, Length
class ContactForm(FlaskForm):
name = StringField(
'Name',
validators=[DataRequired(), Length(min=2, max=40)],
)
submit = SubmitField('Next')
def create_app():
app = Flask(__name__, instance_relative_config=True)
app.config['SECRET_KEY'] = 'Your secret key'
app.config["TEMPLATES_AUTO_RELOAD"] = True
# set some global variables
app.jinja_env.globals['site_info'] = {
'name': 'My site',
}
# use a dict instead of file system
app.jinja_loader = DictLoader(get_templates_dict())
@app.route('/')
def home():
return render_template(
'pages/home.html',
page_id='home',
)
@app.route('/faq')
def faq():
faqs = {
'How to make a list?': 'Use a dictionary.',
'How to get an answer?': 'Contact us.',
}
return render_template(
'pages/faq.html',
page_id='faq',
faqs=faqs,
)
@app.route('/contact', methods=['GET', 'POST'])
def contact():
form = ContactForm()
if form.validate_on_submit():
name = form.name.data
return redirect(url_for('contact_form_received', name=name))
return render_template(
'pages/contact.html',
page_id='contact',
form=form,
)
@app.route('/contact-form-received/<name>')
def contact_form_received(name):
return render_template(
'pages/contact_form_received.html',
page_id='contact_form_received',
name=name,
)
@app.context_processor
def inject_data():
current_app.logger.debug('()')
data = {}
right_column_blocks = {
'news': {
'title': 'News',
'text': 'This is some news text ...',
},
'most_viewed': {
'title': 'Most viewed',
'text': 'This is some most viewed text ...',
},
}
data['right_column_blocks'] = right_column_blocks
return data
return app
# here are the 'files' in the templates directory.
# remove the Jinja2 DictLoader if you use the filesystem
def get_templates_dict():
return {
'base.html': """
{# base.html #}
{%- from 'shared/page_macros.html' import page_title -%}
<!DOCTYPE html>
<html lang="en" >
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>{{ site_info['name'] }} | {{ page_title(page_id) }}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
</head>
<body>
{% include 'page_header.html' %}
<div class="container mt-3" id="main">
<div class="row">
<div class="col">
{% block page_content %}{% endblock %}
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script>
</body>
</html>""",
'page_content.html': """
{# page_content.html #}
{%- from 'shared/page_vars.html' import page_vars as pv -%}
{%- from 'shared/page_macros.html' import page_title -%}
{% extends 'base.html' %}
{% block page_content %}
<div class="row">
<div class="col-12 mb-3 col-md-8 border-end">
<h1>
{{ page_title(page_id) }}
</h1>
{% block page_data %}{% endblock %}
</div>
<div class="col-12 mb-3 col-md-4">
{%- for column_block_id, column_block_data in right_column_blocks.items() -%}
<div class="row">
<div class="col">
<h2>
{{ column_block_data['title'] }}
</h2>
<p>
{{ column_block_data['text'] }}
</p>
</div>
</div>
{%- endfor -%}
</div>
</div>
{% endblock %}""",
'page_header.html': """
{# page_header.html #}
{%- from 'shared/page_macros.html' import topnav_menu_item -%}
<nav class="navbar navbar-expand-md bg-body-tertiary">
<div class="container">
<a class="navbar-brand" href="{{ url_for('home') }}">
{{ site_info['name'] }}
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto">
{{ topnav_menu_item(page_id) }}
</ul>
</div>
</div>
</nav>""",
'shared/page_macros.html': """
{# shared/page_macros.html #}
{%- from 'shared/page_vars.html' import page_vars as pv -%}
{%- macro page_title(page_id) -%}
{%- if page_id in pv.page_id_infos -%}
{{ pv.page_id_infos[page_id]['title'] }}
{%- else -%}
{{ '?' }}
{%- endif -%}
{%- endmacro -%}
{%- macro topnav_menu_item(page_id) -%}
{%- for menu_item_page_id, actives in pv.topnav_menu_item_page_id_actives.items() -%}
{%- set menu_item = pv.page_id_infos[menu_item_page_id] -%}
{%- set menu_item_title = menu_item['title'] -%}
{%- set menu_item_url = url_for(menu_item['endpoint']) -%}
{%- set active = '' -%}
{%- if page_id in actives -%}
{%- set active = 'active' -%}
{%- endif -%}
<li class="nav-item">
<a class="nav-link {{ active }}" href="{{ menu_item_url }}">
{{ menu_item_title }}
</a>
</li>
{%- endfor -%}
{%- endmacro -%}""",
'shared/page_vars.html': """
{# shared/page_vars.html #}
{%- set page_vars = {
'page_id_infos': {
'home': {
'title': 'Home',
'endpoint': 'home',
},
'faq': {
'title': 'FAQ',
'endpoint': 'faq',
},
'contact': {
'title': 'Contact',
'endpoint': 'contact',
},
'contact_form_received': {
'title': 'Contact form received',
'endpoint': 'contact_form_received',
},
},
'topnav_menu_item_page_id_actives': {
'home': ['home'],
'faq': ['faq'],
'contact': ['contact', 'contact_form_received'],
},
} -%}""",
'pages/home.html': """
{# pages/home.html #}
{% extends 'page_content.html' %}
{% block page_data %}
<p>
Welcome
</p>
<p>
Your config:
</p>
<table class="table table-sm">
<thead>
<tr><th>Key</th><th>Value</th></tr>
</thead>
<tbody>
{%- for k, v in config.items() -%}
<tr><td>{{ k }}</td><td>{{ v }}</td></tr>
{%- endfor -%}
</tbody>
</table>
{% endblock %}""",
'pages/faq.html': """
{# pages/faq.html #}
{% extends 'page_content.html' %}
{%- block page_data -%}
<ul>
{%- for question, answer in faqs.items() -%}
<li>
Q: {{ question }}
<br>
A: {{ answer }}
</li>
{%- endfor -%}
</ul>
{%- endblock -%}""",
'pages/contact.html': """
{# pages/contact.html #}
{% extends 'page_content.html' %}
{%- block page_data -%}
<form method="POST">
{{ form.csrf_token }}
<div class="mb-3">
<label for="form-field-name" class="form-label">
{{ form.name.label }}
</label>
{{ form.name(size=20, class_='form-control') }}
</div>
{% if form.name.errors %}
<ul class="text-danger">
{% for error in form.name.errors %}
<li>
{{ error }}
</li>
{% endfor %}
</ul>
{% endif %}
{{ form.submit(class_='btn btn-primary') }}
</form>
{%- endblock -%}""",
'pages/contact_form_received.html': """
{# pages/contact_form_received.html #}
{% extends 'page_content.html' %}
{%- block page_data -%}
<p>
Thank you {{ name }}
</p>
{%- endblock -%}""",
}
Para ejecutar, vaya al directorio del proyecto y escriba:
python run.py
A continuación, dirija su navegador a:
127.0.0.1:5050
Resumen
No queremos repetirnos y queremos el nombre de una página en un solo archivo de plantilla Jinja2 . Hemos resuelto esto mediante el uso de un 'page_id', que se utiliza en todas partes para recuperar el nombre de la página / título de un diccionario de variables globales en un archivo de plantilla Jinja2 .
El diccionario debe ser importado por los archivos de plantilla donde queremos acceder a esta información. Esta es sólo una de las opciones para almacenar variables globales.
Recientes
- Un conmutador de base de datos con HAProxy y el HAProxy Runtime API
- Docker Swarm rolling updates
- Cómo ocultar las claves primarias de la base de datos UUID de su aplicación web
- Don't Repeat Yourself (DRY) con Jinja2
- SQLAlchemy, PostgreSQL, número máximo de filas por user
- Mostrar los valores en filtros dinámicos SQLAlchemy
Más vistos
- Usando Python's pyOpenSSL para verificar los certificados SSL descargados de un host
- Usando PyInstaller y Cython para crear un ejecutable de Python
- Reducir los tiempos de respuesta de las páginas de un sitio Flask SQLAlchemy web
- Conectarse a un servicio en un host Docker desde un contenedor Docker
- Usando UUIDs en lugar de Integer Autoincrement Primary Keys con SQLAlchemy y MariaDb
- SQLAlchemy: Uso de Cascade Deletes para eliminar objetos relacionados