Меню сайта на Flask

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

В данном случае в меню текст ссылки на страницу будет совпадать с названием статьи. Иначе в таблице статей базы данных пришлось бы вводить еще один столбец, хранящий название для ссылки.

Выполнить запрос к БД для получения меню не так просто. Сначала задумаемся, в каком виде данные будут передаваться в шаблон? Это может быть словарь, в котором ключом каждого элемента является название раздела, а значением ‒ список кортежей. Каждый кортеж будет представлять собой пару: названий статьи и ее адрес. То есть одна запись словаря будет выглядеть примерно так:

"Растения": [("botany/plants", "Общее о растениях"),
             ("botany/flower", "Строение цветка"),
             …]

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

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

con = sqlite3.connect(db_name)
sections = con.execute('SELECT id, name FROM sections \
                        ORDER BY pos').fetchall()
menu = {}
for theme_id, theme_name in sections:
    articles = con.execute(
        'SELECT path, h1 FROM articles WHERE theme=? ORDER BY pos_in_theme',
        (theme_id, )).fetchall()
    menu[theme_name] = articles
con.close()

Передадим значение переменной menu в шаблон в вызовах функции render_template. Например:

render_template('create.html', sections=sections, menu=menu)

В шаблоне base.html, от которого наследует create.html, в том месте, где должно находится навигационное меню сайта, напишем такой код:

{% for theme, articles in menu.items() %}
  <button class="accordion">{{theme}}</button>
  <div class="panel">
    <ul>
      {% for i in articles %}
        <li><a href="/{{i[0]}}">{{i[1]}}</a></li>
      {% endfor %}
    </ul>
  </div>
{% endfor %}

Обратите внимание, что html-теги и классы специфичны для конкретного стиля оформления и html-структуры меню. Если у вас другой шаблон, то код буквально не подойдет. Только извлечение составляющих словаря.

После этого на сайте должно появиться меню со ссылками на ранее созданные страницы.

Сайт на Flask с меню

Однако если добавить новую статью или внести правки в существующую (поменять заголовок, раздел или позицию в нем), то сайдбар не обновится. Новая ссылка не появится, как и не произойдет изменения названия и места существующей. Для обновления меню вам потребуется перезапустить сервер.

Дело в том, что во flash-приложении код получения словаря меню выполняется единожды. Только когда программа запускается. Да, если вы вносите изменения в программный код, то в режиме --debug изменения применяются. Приложение как-будто перезапускается. Но если изменений в программном коде нет, то он повторно не выполнится.

Поэтому когда вносятся изменения в базу данных, словарь для меню не создается заново. Он остается прежним. Чтобы словарь менялся, надо поместить код в функцию и потом уже вызывать ее, когда в этом появляется необходимость.

При этом разделим получение разделов сайта и меню по разным функциям. Ведь разделы бывают нам нужны отдельно, без выстраивания меню. А помещение их в функцию позволит также обновлять их динамически как меню, если состав разделов в БД будет изменен.

def get_sections():
    con = sqlite3.connect(db_name)
    sections = con.execute('SELECT id, name FROM sections \
                            ORDER BY pos').fetchall()
    con.close()
    return sections


def get_menu():
    sections = get_sections()
    con = sqlite3.connect(db_name)
    menu = {}
    for theme_id, theme_name in sections:
        articles = con.execute(
            'SELECT path, h1 FROM articles WHERE theme=? \
            ORDER BY pos_in_theme',
            (theme_id, )).fetchall()
        menu[theme_name] = articles
    con.close()
    return menu

Теперь надо решить, откуда эти функции вызывать? Можно делать это из функций-представлений при передаче аргументов в render_template():

render_template('create.html', sections=get_sections(), menu=get_menu())

В этом случае в шаблоне ничего не придется менять.

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

render_template('create.html',
                get_sections=get_sections, get_menu=get_menu)

или

render_template('create.html',
                get_sections=get_sections, get_menu=get_menu, post=p)

В самом шаблоне, до того как использовать переменные sections и menu, надо вызвать переданные функции. В файл base.html перед кодом разметки меню добавим:

{% set menu = get_menu() %}

В файл create.html надо добавить:

{% set sections = get_sections() %}

Обратим внимание, что в коде flask-приложения функция render_template вызывается несколько раз, и делается это с почти одинаковым набором аргументов. Микрофреймворк Flask предоставляет возможность помещать идентификаторы в словарь глобальных объектов для Jinja. Добавим в конце кода admin.py следующую строку (за пределами какой-либо функции):

app.jinja_env.globals.update(get_menu=get_menu, get_sections=get_sections)

После этого вызовы render_template следует сократить до

render_template('create.html')

и

render_template('create.html', post=p)