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

Don't Repeat Yourself (DRY) mit Jinja2

Geben Sie einen Seitennamen in eine Jinja2 -Vorlagendatei ein und teilen Sie ihn überall.

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

Ich habe einige Dinge mit Jinja2 ausprobiert, eine kleine Flask -App erstellt und dachte, warum nicht teilen. Was ich erreichen wollte, war, alle Seitennamen in eine Vorlagendatei zu packen.

Wie bei Python muss man auch bei Jinja2 aufpassen, dass man sich beim Schreiben von viel Code nicht wiederholt. Bevor Sie es merken, haben Sie viele Vorlagendateien, die die gleichen Informationen enthalten. Und wenn Sie etwas ändern wollen, müssen Sie alle diese Vorlagendateien bearbeiten.

Dieser Beitrag enthält den gesamten Code einer einfachen Flask -Anwendung:

  • Vier Seiten, eine davon mit einem Formular
  • Zweispaltiges Layout
  • Kopfzeile mit Navigation
  • Verwendet Bootstrap (kann ohne es nicht leben ...)

Für den Fall, dass Sie es ausprobieren möchten, müssen Sie nur zwei Dateien erstellen. Ich habe Jinja2's DictLoader benutzt, um die Vorlagen in eine Python -Datei zu packen.

Wie immer mache ich das auf Ubuntu 22.04.

Erweitern, erweitern, erweitern

Nachdem Sie etwas Code geschrieben haben, möchten Sie die Seite sehen. Sie erstellen die Vorlage. Sie fügen eine Kopfzeile, eine Fußzeile, einen Titel, ein Menü usw. hinzu. Tun Sie dies nicht! Nur die Ausgabe Ihres endpoint sollte auf dieser Vorlage stehen!

Im Folgenden wird gezeigt, was wir erreichen wollen. Die FAQ TEMPLATE enthält nur 'faq data' und sonst nichts!

                   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 Variablen in Jinja2

Wir können globale Variablen oder Datenstrukturen auf verschiedene Weise an unsere Vorlagen übergeben:

Python Code - Flask's app.config

Beispiel:

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

Python Code - Jinja2 Environment.globals

Beispiel:

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

Python Code - Jinja2 Context processor

Beispiel:

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

Jinja2 -Code - Eine Jinja2 -Vorlagendatei mit Variablen

Diese Lösung erfordert, dass wir diese Vorlage einbinden, wenn wir eine oder mehrere Variablen benötigen. Der Vorteil ist, dass wir alle Seitennamen in einer einzigen Jinja2 -Vorlagendatei haben können.

Beispiel:

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

Verwendung von page_ids

Ihre endpoint -Funktionen können Seitenspezifika wie einen Seitennamen ausgeben. Sie verwenden aber auch Seitennamen in der Navigation der Website. Und wir wollen uns nicht wiederholen!

Wir lösen dieses Problem, indem wir eine einzige Jinja2 -Vorlagendatei erstellen, die Informationen über unsere Seiten enthält, einschließlich der Seitennamen. Um auf Seiteninformationen zu verweisen, verwenden wir eine page_id. Und wir verwenden eine Jinja2 macro , um die gewünschten Informationen zu extrahieren.

Ausführen der Anwendung

Erstellen Sie eine virtual environment und installieren Sie Flask und WTForms:

pip install flask
pip install flask-wtf

Für diese Flask App verwende ich die Jinja2 DictLoader: Alle Vorlagen befinden sich in factory.py , was bedeutet, dass Sie nur zwei Dateien erstellen müssen!

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

Der Code

Wir haben zwei Python -Dateien:

  • run.py, im Verzeichnis 'project'
  • factory.py, im Verzeichnis '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 -%}""",
    
    }

Zur Ausführung gehen Sie in das Projektverzeichnis und geben Sie ein:

python run.py

Zeigen Sie dann mit Ihrem Browser auf:

127.0.0.1:5050

Zusammenfassung

Wir wollen uns nicht wiederholen und wollen den Namen einer Seite in nur einer Jinja2 -Vorlagendatei. Wir haben dies durch die Verwendung eines 'page_id' gelöst, das überall verwendet wird, um den Seitennamen/Titel aus einem globalen Variablenwörterbuch in einer Jinja2 -Vorlagendatei abzurufen.

Das Wörterbuch muss von Vorlagendateien importiert werden, in denen wir auf diese Informationen zugreifen wollen. Dies ist nur eine der Möglichkeiten, globale Variablen zu speichern.

Links / Impressum

Bootstrap
https://getbootstrap.com

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

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

Mehr erfahren

Flask Jinja2

Einen Kommentar hinterlassen

Kommentieren Sie anonym oder melden Sie sich zum Kommentieren an.

Kommentare

Eine Antwort hinterlassen

Antworten Sie anonym oder melden Sie sich an, um zu antworten.