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

Don't Repeat Yourself (DRY) avec Jinja2

Placez un nom de page dans un fichier modèle Jinja2 et partagez-le partout.

20 février 2024
Dans Flask, Jinja2
post main image
https://www.pexels.com/@monicore

J'ai fait quelques essais avec Jinja2, j'ai créé une petite application Flask , et je me suis dit pourquoi ne pas la partager. Ce que je voulais faire, c'était mettre tous les noms de pages dans un seul fichier modèle.

Comme pour Python, lorsque vous écrivez beaucoup de code, vous devez faire attention à ne pas vous répéter avec Jinja2. Très vite, vous vous retrouvez avec de nombreux fichiers modèles contenant les mêmes types d'informations. Et lorsque vous souhaitez modifier quelque chose, vous devez éditer tous ces fichiers modèles.

Ce billet contient tout le code d'une application Flask de base :

  • Quatre pages, dont une avec un formulaire
  • Disposition en deux colonnes
  • En-tête avec navigation
  • Utilise Bootstrap (on ne peut pas s'en passer ...)

Si vous voulez essayer, il n'y a que deux fichiers à créer. J'ai utilisé le fichier Jinja2's DictLoader pour mettre les modèles dans un fichier Python .

Comme toujours, je fais cela sur Ubuntu 22.04.

Étendre, étendre, étendre

Après avoir écrit du code, vous voulez voir la page. Vous créez le modèle. Vous ajoutez un en-tête, un pied de page, un titre, un menu, etc. Ne faites pas cela ! Seule la sortie de votre endpoint doit se trouver sur ce modèle !

Voici ce que nous allons faire. Le modèle FAQ TEMPLATE ne contient que 'faq data', et rien d'autre !

                   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 dans Jinja2

Nous pouvons passer des variables globales, ou des structures de données, à nos modèles de plusieurs façons :

code Python - Flask's app.config

Exemple :

app.config['MY_GLOBAL_VAR_KEY'] = 'my_global_var_value'

Code Python - Jinja2 Environment.globals

Exemple de code - Python

app.jinja_env.globals['MY_GLOBAL_VAR_KEY'] = 'my_global_var_value'

Python code - Jinja2 Context processor

Exemple de code - Jinja2

    @app.context_processor
    def inject_data():
        data = {}
        data['MY_GLOBAL_VAR_KEY'] = 'my_global_var_value'
        return data

Code Jinja2 - Un fichier modèle Jinja2 avec des variables

Cette solution exige que nous incluions ce modèle lorsque nous avons besoin d'une ou plusieurs variables. L'avantage est que nous pouvons avoir tous les noms de page dans un seul fichier modèle Jinja2 .

Exemple :

{%- set global_vars = {
        'MY_GLOBAL_VAR_KEY': 'my_global_var_value',
} -&}

Utilisation de page_ids

Vos fonctions endpoint peuvent fournir des informations spécifiques sur les pages, comme leur nom. Mais vous utilisez également les noms de page dans la navigation du site. Et nous ne voulons pas nous répéter !

Nous résolvons ce problème en créant un seul fichier modèle Jinja2 qui contient des informations sur nos pages, y compris les noms de page. Pour référencer les informations relatives aux pages, nous utilisons un fichier page_id. Et nous utilisons un Jinja2 macro pour extraire les informations que nous voulons.

Exécution de l'application

Créer un virtual environment et installer les Flask et WTForms :

pip install flask
pip install flask-wtf

Pour cette application Flask j'utilise le Jinja2 DictLoader : Tous les templates sont dans factory.py ce qui signifie que vous n'avez besoin de créer que deux fichiers !

.
├── 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

Le code

Nous avons deux fichiers Python :

  • run.py, dans le répertoire 'project'
  • factory.py, dans le répertoire '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 -%}""",
    
    }

Pour l'exécuter, allez dans le répertoire du projet et tapez :

python run.py

Pointez ensuite votre navigateur sur :

127.0.0.1:5050

Résumé

Nous ne voulons pas nous répéter et nous voulons que le nom d'une page figure dans un seul fichier modèle Jinja2 . Nous avons résolu ce problème en utilisant un 'page_id', qui est utilisé partout pour récupérer le nom / titre de la page à partir d'un dictionnaire de variables globales dans un fichier modèle Jinja2 .

Le dictionnaire doit être importé par les fichiers modèles dans lesquels nous voulons accéder à ces informations. Ce n'est qu'une des options de stockage des variables globales.

Liens / crédits

Bootstrap
https://getbootstrap.com

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

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

En savoir plus...

Flask Jinja2

Laissez un commentaire

Commentez anonymement ou connectez-vous pour commenter.

Commentaires

Laissez une réponse

Répondez de manière anonyme ou connectez-vous pour répondre.