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

Don't Repeat Yourself (DRY) с Jinja2

Поместите имя страницы в файл шаблона Jinja2 и делитесь им повсюду.

20 февраля 2024
post main image
https://www.pexels.com/@monicore

Я пробовал некоторые вещи с Jinja2, создал небольшое приложение Flask и подумал, почему бы не поделиться этим. Я хотел добиться того, чтобы поместить все названия страниц в один файл шаблона.

Как и в случае с Python, когда пишешь много кода, нужно быть осторожным, чтобы не повторяться с Jinja2. Вы не успеете оглянуться, как у вас окажется множество файлов шаблонов, содержащих одну и ту же информацию. И когда вы захотите что-то изменить, вам придется редактировать все эти файлы шаблонов.

В этом посте приведен весь код базового приложения Flask :

  • Четыре страницы, одна с формой
  • Двухколоночный макет
  • Заголовок с навигацией
  • Используется Bootstrap (без него не прожить...)

Если вы хотите попробовать, вам нужно создать всего два файла. Я использовал Jinja2 в DictLoader , чтобы поместить шаблоны в файл Python .

Как обычно, я делаю это на Ubuntu 22.04.

Расширять, расширять, расширять

После написания кода вы хотите увидеть страницу. Вы создаете шаблон. Вы добавляете хедер, футер, заголовок, меню и т. д. Не делайте этого! На этом шаблоне должен быть только вывод вашего endpoint !

Ниже показано, чего мы хотим добиться. FAQ TEMPLATE содержит только 'faq data', и ничего больше!

                   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      

Глобальные переменные в Jinja2

Мы можем передавать глобальные переменные, или структуры данных, в наши шаблоны несколькими способами:

код Python - Flask's app.config

Пример:

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

Python код - Jinja2 Environment.globals

Пример:

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

Python код - Jinja2 Context processor

Пример:

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

Код Jinja2 - Файл шаблона Jinja2 с переменными

Это решение требует, чтобы мы включали этот шаблон, когда нам нужна одна или несколько переменных. Преимуществом является то, что мы можем иметь все имена страниц в одном файле шаблона Jinja2 .

Пример:

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

Использование page_ids

Ваши функции endpoint могут выводить специфику страницы, например, ее название. Но вы также используете названия страниц в навигации по сайту. И мы не хотим повторяться!

Мы решили эту проблему, создав один файл шаблона Jinja2 , в котором хранится информация о наших страницах, включая их названия. Чтобы ссылаться на информацию о страницах, мы используем page_id. А для извлечения нужной нам информации мы используем Jinja2 macro .

Запуск приложения

Создайте virtual environment и установите Flask и WTForms:

pip install flask
pip install flask-wtf

Для этого приложения Flask я использую Jinja2 DictLoader: Все шаблоны находятся в factory.py , что означает, что вам нужно создать только два файла!

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

Код

У нас есть два файла Python :

  • run.py, в директории 'project'
  • factory.py, в каталоге '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 -%}""",
    
    }

Для запуска перейдите в каталог проекта и введите:

python run.py

Затем перейдите в браузер:

127.0.0.1:5050

Summary

Мы не хотим повторяться и хотим, чтобы название страницы было только в одном файле шаблона Jinja2 . Мы решили эту проблему с помощью 'page_id', который используется повсеместно для получения имени/заголовка страницы из словаря глобальных переменных в файле шаблона Jinja2 .

Этот словарь должен быть импортирован в файлы шаблона, где мы хотим получить доступ к этой информации. Это лишь один из вариантов хранения глобальных переменных.

Ссылки / кредиты

Bootstrap
https://getbootstrap.com

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

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

Подробнее

Flask Jinja2

Оставить комментарий

Комментируйте анонимно или войдите в систему, чтобы прокомментировать.

Комментарии

Оставьте ответ

Ответьте анонимно или войдите в систему, чтобы ответить.