Don't Repeat Yourself (DRY) с Jinja2
Поместите имя страницы в файл шаблона Jinja2 и делитесь им повсюду.
Я пробовал некоторые вещи с 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 .
Этот словарь должен быть импортирован в файлы шаблона, где мы хотим получить доступ к этой информации. Это лишь один из вариантов хранения глобальных переменных.
Недавний
- Скрытие первичных ключей базы данных UUID вашего веб-приложения
- Don't Repeat Yourself (DRY) с Jinja2
- SQLAlchemy, PostgreSQL, максимальное количество строк для user
- Показать значения в динамических фильтрах SQLAlchemy
- Безопасная передача данных с помощью шифрования Public Key и pyNaCl
- rqlite: альтернатива dist с высокой степенью готовности и SQLite
Большинство просмотренных
- Используя Python pyOpenSSL для проверки SSL-сертификатов, загруженных с хоста
- Использование UUID вместо Integer Autoincrement Primary Keys с SQLAlchemy и MariaDb
- Подключение к службе на хосте Docker из контейнера Docker
- Использование PyInstaller и Cython для создания исполняемого файла Python
- SQLAlchemy: Использование Cascade Deletes для удаления связанных объектов
- Flask Удовлетворительный запрос API проверка параметров запроса с помощью схем Маршмэллоу