angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

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.

20 febrero 2024
post main image
https://www.pexels.com/@monicore

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.

Enlaces / créditos

Bootstrap
https://getbootstrap.com

Flask
https://flask.palletsprojects.com/en/3.0.x

Jinja
https://jinja.palletsprojects.com/en/3.1.x

Deje un comentario

Comente de forma anónima o inicie sesión para comentar.

Comentarios

Deje una respuesta.

Responda de forma anónima o inicie sesión para responder.