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

Don't Repeat Yourself (DRY) met Jinja2

Zet een paginanaam in een Jinja2 sjabloonbestand en deel het overal.

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

Ik was wat dingen aan het uitproberen met Jinja2, maakte een kleine Flask app, en dacht waarom zou ik dit niet delen. Wat ik wilde bereiken was om alle paginanamen in één sjabloonbestand te zetten.

Net als bij Python moet je bij het schrijven van veel code oppassen dat je niet in herhaling valt met Jinja2. Voor je het weet heb je veel sjabloonbestanden met dezelfde soort informatie. En wanneer u iets wilt veranderen, moet u al deze sjabloonbestanden bewerken.

Deze post bevat alle code van een basis Flask app:

  • Vier pagina's, één met een formulier
  • Lay-out met twee kolommen
  • Koptekst met navigatie
  • Gebruikt Bootstrap (kan niet zonder...)

Als je het wilt proberen, hoef je maar twee bestanden te maken. Ik heb Jinja2's DictLoader gebruikt om de sjablonen in een Python bestand te zetten.

Zoals altijd doe ik dit op Ubuntu 22.04.

Uitbreiden, uitbreiden, uitbreiden

Nadat je wat code hebt geschreven, wil je de pagina zien. U maakt het sjabloon. Je voegt een header, footer, titel, menu, enz. toe. Doe dit niet! Alleen de uitvoer van uw endpoint moet op dit sjabloon staan!

Hieronder wordt getoond wat we gaan bereiken. De FAQ TEMPLATE bevat alleen 'faq data', en niets anders!

                   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      

Globale variabelen in Jinja2

We kunnen globale variabelen, of gegevensstructuren, op verschillende manieren doorgeven aan onze sjablonen:

Python code - Flask's app.config

Voorbeeld:

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

Python code - Jinja2 Environment.globals

Voorbeeld:

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

Python code - Jinja2 Context processor

Voorbeeld:

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

Jinja2 code - Een Jinja2 sjabloonbestand met variabelen

Deze oplossing vereist dat we dit sjabloon opnemen wanneer we een of meer variabelen nodig hebben. Het voordeel is dat we alle paginanamen in één Jinja2 sjabloonbestand kunnen opnemen.

Voorbeeld:

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

page_ids gebruiken

Uw endpoint functies kunnen paginaspecificaties uitvoeren, zoals een paginanaam. Maar je gebruikt ook paginanamen in de navigatie van de site. En we willen niet in herhaling vallen!

We lossen dit op door een enkel Jinja2 sjabloonbestand te maken dat informatie over onze pagina's bevat, inclusief de paginanamen. Om naar pagina-informatie te verwijzen, gebruiken we een page_id. En we gebruiken een Jinja2 macro om de informatie op te halen die we willen.

De app uitvoeren

Maak een virtual environment en installeer Flask en WTForms:

pip install flask
pip install flask-wtf

Hier is de boom van de directories en bestanden. Voor deze Flask app gebruik ik de Jinja2 DictLoader: Alle templates staan in factory.py wat betekent dat je maar twee bestanden hoeft te maken!

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

De code

We hebben twee Python bestanden:

  • run.py, in de map 'project'
  • factory.py, in de map '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 -%}""",
    
    }

Ga om uit te voeren naar de projectmap en typ:

python run.py

Wijs vervolgens uw browser op:

127.0.0.1:5050

Samenvatting

We willen niet in herhaling vallen en willen de naam van een pagina in slechts één Jinja2 sjabloonbestand. We hebben dit opgelost door een 'page_id' te gebruiken, die overal wordt gebruikt om de paginanaam / -titel op te halen uit een woordenboek met globale variabelen in een Jinja2 sjabloonbestand.

Het woordenboek moet worden geïmporteerd door sjabloonbestanden waarin we deze informatie willen gebruiken. Dit is slechts één van de opties om globale variabelen op te slaan.

Links / credits

Bootstrap
https://getbootstrap.com

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

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

Laat een reactie achter

Reageer anoniem of log in om commentaar te geven.

Opmerkingen

Laat een antwoord achter

Antwoord anoniem of log in om te antwoorden.