Использование шаблонов страниц сайта
Продолжаем изучение Flask и вначале хочу обратить ваше внимание, что программа из прошлого занятия возвращала довольно простую информацию. Но реальные страницы имеют довольно внушительную структуру, и даже используя многострочные строки, программа будет не очень читабельной:
from flask import Flask app = Flask(__name__) @app.route("/") def index(): return ''' ''' @app.route("/about") def about(): return "Про Flask
" if __name__ == "__main__": app.run(debug=True)
Кроме того структура страницы может меняться и вносить изменения непосредственно в программе – это плохой подход к программированию. Поэтому все HTML-шаблоны хранятся в виде отдельных файлов и загружаются по мере необходимости.
Для работы с шаблонами Flask использует стандартный модуль Jinja (если вы не знаете что это такое и как происходит обработка шаблонов, то смотрите занятия по этой ссылке).
Чтобы воспользоваться шаблонизатором во Flask нужно импортировать его элемент render_template:
from flask import Flask, render_template
и, затем, в обработчике index вызвать его:
@app.route("/") def index(): return render_template('index.html')
Следующий вопрос: где расположить шаблон ‘index.html’, чтобы он был найден и загружен модулем Flask. Существует следующее простое соглашение: по умолчанию все шаблоны берутся из подкаталога templates, относительно рабочего каталога программы (или соответствующем подкаталоге пакета). Так мы и сделаем. Разместим в этом подкаталоге файл index.html со следующим содержимым:
DOCTYPE html> html> head> title>Главная страница сайта/title> /head> body> h1>Главная страница сайта/h1> /body> /html>
Обратите внимание, для корректного отображения кириллицы все шаблоны рекомендуется сохранять в кодировке utf-8. Тем более, что сам Python, начиная с версии 3, по умолчанию использует юникод.
Запустим программу и при переходе на главную страницу увидим отображение нашего шаблона в браузере:

По аналогии создадим такой же шаблон about.html и также будем загружать его при обращении по URL /about:
@app.route("/about") def about(): return render_template('about.html')
Теперь, посещая эту страницу, пользователь увидит:

Вот так легко и просто мы добавили два шаблона для наших двух страниц. Далее, можно их редактировать, не меняя код самой программы, что очень удобно.
Передача шаблонам параметров
Я много раз произносил слово «шаблон», но что оно означает? Если посмотреть на файлы index.html или about.html, то это просто текст, который загружается и отдается браузеру по соответствующему запросу. Все так, но в этих же файлах можно прописать конструкции для отображения информации, например, из БД. Давайте для начала сделаем так, чтобы на каждой странице был свой заголовок, переданный ей через параметр title. Это можно сделать так:
DOCTYPE html> html> head> title>{{title}}/title> /head> body> h1>{{title}}/h1> /body> /html>
А в обработчике указать этот параметр:
return render_template('index.html', title="Про Flask")
Все, теперь вместо title будет подставлена строка «Про Flask». Удобно, правда? Вот в этом и есть роль шаблонов: они описывают структуру страницы, а ее наполнение происходит динамически в самой программе.
Но какие вообще конструкции можно использовать в шаблонах? Как я уже отмечал, модуль Flask использует шаблонизатор Jinja и шаблоны строятся по его правилам. Если вы не знаете как работать с шаблонами, то под этим видео увидите ссылку на этот курс.
Давайте для примера еще добавим в документ простой список, пусть он символизирует наше меню. Шаблон для него можно прописать так (в файле index.html):
ul> {% for m in menu %} li>{{m}}/li> {% endfor %} /ul>
А в программе добавить список и передать его шаблону:
menu = ["Установка", "Первое приложение", "Обратная связь"] @app.route("/") def index(): return render_template('index.html', title="Про Flask", menu = menu)
При обновлении страницы увидим следующее:

Добавим большей гибкости нашему шаблону и для заголовка пропишем следующую конструкцию:
{% if title %} title>Про Flask - {{title}}/title> {% else %} title>Про Flask/title> {% endif %}
И то же самое для тега h1:
{% if title -%} h1>{{title}}/h1> {% else -%} h1>Про Flask/h1> {% endif %}
Теперь в обработчике можно не указывать параметр title, тогда будет отображаться строка «Про Flask», а иначе, подставляться другой заголовок, причем во вкладке браузеры будем видеть «Про Flask — ». Например, пропишем такой же шаблон для about.html и в обработчике добавим:
def about(): return render_template('about.html', title = "О сайте", menu = menu)
В результате, вид страницы будет такой:

Но, наши созданные шаблоны, мягко говоря, не очень, т.к. они содержат много повторяющегося кода. Лучшим вариантом будет воспользоваться механизмом расширения (наследования) шаблона для создания дочерних страниц сайта. Для начала определим базовый шаблон страницы – ее структуру, следующим образом (файл base.html):
DOCTYPE html> html> head> {% block title -%} {% if title %} title>Про Flask - {{title}}/title> {% else %} title>Про Flask/title> {% endif %} {% endblock %} /head> body> {% block content -%} {%- block mainmenu -%} ul> {% for m in menu -%} li>{{m}}/li> {% endfor -%} /ul> {% endblock mainmenu -%} {% if title -%} h1>{{title}}/h1> {% else -%} h1>Про Flask/h1> {% endif -%} {% endblock -%} /body> /html>
А в дочерних расширим этот базовый шаблон:
— для index.html:
{% extends 'base.html' %} {% block content %} {{ super() }} Содержимое главной страницы {% endblock %}
— для about.html:
{% extends 'base.html' %} {% block content %} {{ super() }} Содержимое страницы "о сайте" {% endblock %}
Все, теперь никакого дублирования в наших шаблонах нет и мы можем достаточно просто создавать множество страниц сайта и при необходимости менять их структуру, просто меняя базовый шаблон base.html.
Видео по теме

Flask #1: Что это такое? Простое WSGI-приложение

Flask #2: Использование шаблонов страниц сайта

Flask #3: Контекст приложения и контекст запроса

Flask #4: Функция url_for и переменные URL-адреса

Flask #5: Подключение внешних ресурсов и работа с формами

Flask #6: Мгновенные сообщения — flash, get_flashed_messages

Flask #7: Декоратор errorhandler, функции redirect и abort

Flask #8: Создание БД, установление и разрыв соединения при запросах

Flask #9: Добавление и отображение статей из БД

Flask #10: Способ представления полноценных HTML-страниц на сервере

Flask #11: Формирование ответа сервера, декораторы перехвата запроса

Flask #12: Порядок работы с cookies (куками)

Flask #13: Порядок работы с сессиями (session)

Flask #14: Регистрация пользователей и шифрование паролей

Flask #15: Авторизация пользователей на сайте через Flask-Login

Flask #16: Улучшение процесса авторизации (Flask-Login)

Flask #17: Загрузка файлов на сервер и сохранение в БД

Flask #18: Применение WTForms для работы с формами сайта

Flask #19: Обработка ошибок во Flask-WTF

Flask #20: Blueprint — что это такое, где и как использовать

Flask #21: Blueprint — подключение к БД и работа с ней

Flask #22: Flask-SQLAlchemy — установка, создание таблиц, добавление записей

Flask #23: Операции с таблицами через Flask-SQLAlchemy
© 2023 Частичное или полное копирование информации с данного сайта для распространения на других ресурсах, в том числе и бумажных, строго запрещено. Все тексты и изображения являются собственностью сайта
Голосовалка на Flask

В этой серии статей мы будем делать приложение для запуска голосований и, может быть, даже опросов с использованием Flask.
Дополнительно к этой статье, вы можете следить за развитием приложения в этом репозитории.
Давайте начнем с создания простого Flask-based приложения.
Мы создали новую директорию и в ней создали такой скрипт на Python:
examples/flask/poll1/poll.py
from flask import Flask, render_template import os app = Flask(__name__) @app.route('/') def root(): return render_template('poll.html') if __name__ == "__main__": app.run(debug=True)
Мы создали обработчик для / , который вызывает функцию root() . Эта функция возвращает страницу, собранную из шаблона poll.html . Сам шаблон находится в поддиректории templates/ .
examples/flask/poll1/templates/poll.html
Poll Poll
Сейчас директория нашего проекта выглядит вот так:
$ tree . ├── poll.py └── templates └── poll.html
Теперь мы можем запустить приложение командой python poll.py , которая скажет нам:
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) * Restarting with stat
Если открыть браузер по полученному адресу, то мы увидим следующее:

Немного, но оно работает. Прям как hello world, но с шаблоном.
$ git init $ git add . $ git commit -m "step 1 - hello world with a template"
Отображение голосования
Чтобы провести голосование, нам нужен вопрос и варианты ответа для выбора. Возможно, позже это все будет перемещено в конфигурационный файл, но сейчас давайте просто создадим словарь с данными в нашем приложении.
poll_data =
Мы также изменили вызов рендеринга шаблона render_template и теперь передаем туда наш словарь под ключом data .
examples/flask/poll2/poll.py
from flask import Flask, render_template import os app = Flask(__name__) poll_data = < 'question' : 'Which web framework do you use?', 'fields' : ['Flask', 'Django', 'TurboGears', 'web2py', 'pylonsproject'] >@app.route('/') def root(): return render_template('poll.html', data=poll_data) if __name__ == "__main__": app.run(debug=True)
В шаблоне мы используем выражение > , чтобы добавить вопрос. Мы используем эту конструкцию для двух случаев: заголовка нашей страницы, который вы видите во вкладке браузера, и в качестве элемента h1 .
Затем мы создаем форму с action=»/poll»> , это значит, что мы должны создать новый обработчик для этого запроса в нашем приложении. Внутри формы мы создаем несколько элементов radio для ввода данных (выбора значений). По одному для каждого варианта ответа. Тип поля radio хорошо подходит, когда нам нужно получить ровно один ответ.
examples/flask/poll2/templates/poll.html
После всех этих изменений зайдем на нашу страницу, и вот что мы увидим:

Если выбрать один из элементов и нажать кнопку «Vote», то получим такой ответ:

Это значит, что мы еще не добавили обработчик для /poll . Давайте сделаем его.
$ git add . $ git commit -m "add poll data and display it"
Принимаем результаты голосования
Первый шаг это добавление хендлера для обработки /poll , прием значения поля field формы с помощью request.args.get(‘field’) . Для начала, просто вернем выбранное значение пользователю:
@app.route('/poll') def poll(): vote = request.args.get('field') return vote
Мы можем перезагрузить страницу в браузере и увидим там выбранный нами вариант:

Следующий шаг — сохранение результатов. Для простоты мы будем использовать обычный файл. В начале скрипта poll.py мы добавим имя файла в виде переменной: filename = ‘data.txt’ (всегда хорошо иметь переменные для таких случаев), а затем открываем файл, чтобы добавить туда контент (используя ‘a’ в качестве аргумента для функции open ), записываем результат в файл и закрываем файл.
Мы собираемся хранить по одному результату голосований в строке. Тогда будет легко собирать данные впоследствии.
@app.route('/poll') def poll(): vote = request.args.get('field') out = open(filename, 'a') out.write( vote + '\n' ) out.close() return vote
Теперь, если мы обновим веб-страницу, наш выбранный вариант сохранится в файл данных, но мы все еще получаем обратно выбранный вариант. Вместо этого, давайте добавим более дружественную страницу благодарности:
return render_template('thankyou.html', data=poll_data)
Теперь Flask скрипт выглядит вот так:
examples/flask/poll3/poll.py
from flask import Flask, render_template, request import os app = Flask(__name__) poll_data = < 'question' : 'Which web framework do you use?', 'fields' : ['Flask', 'Django', 'TurboGears', 'web2py', 'pylonsproject'] >filename = 'data.txt' @app.route('/') def root(): return render_template('poll.html', data=poll_data) @app.route('/poll') def poll(): vote = request.args.get('field') out = open(filename, 'a') out.write( vote + '\n' ) out.close() return render_template('thankyou.html', data=poll_data) if __name__ == "__main__": app.run(debug=True)
Шаблон страницы благодарности вот такой:
examples/flask/poll3/templates/thankyou.html
> Thank you for submitting your vote for
>
$ git add poll.py templates/thankyou.html $ git commit -m "save the vote and thank the voter"
Эта версия голосовалки уже работает, но давайте добавим еще одну страницу — с результатами голосования.
Отображение результатов
Для отображения результатов мы создадим еще один обработчик (для пути с именем /results ), который будет читать файл с данными и показывать количество голосов по каждому варианту.
Вот наш обработчик:
@app.route('/results') def show_results(): votes = <> for f in poll_data['fields']: votes[f] = 0 f = open(filename, 'r') for line in f: vote = line.rstrip("\n") votes[vote] += 1 return render_template('results.html', data=poll_data, votes=votes)
Сначала мы создали словарь votes , куда собираемся собрать количество голосов. Затем мы идем по списку ожидаемых значений из исходного списка значений и создаем там элементы для каждого из них с количеством 0. Это будет гарантией, что каждый элемент из нашего списка вариантов представлен в результатах, даже есть за него никто не проголосовал.
Затем мы открываем файл с данными для чтения и читаем построчно. Перед обновлением нашего словаря votes , мы должны удалить символы перевода строки с помощью line.rstrip(«\n») .
Затем мы передаем собранные результаты голосований в функцию render_template .
Шаблон выглядит вот так:
examples/flask/poll4/templates/results.html
и вот скрипт целиком:
examples/flask/poll4/poll.py
from flask import Flask, render_template, request import os app = Flask(__name__) poll_data = < 'question' : 'Which web framework do you use?', 'fields' : ['Flask', 'Django', 'TurboGears', 'web2py', 'pylonsproject'] >filename = 'data.txt' @app.route('/') def root(): return render_template('poll.html', data=poll_data) @app.route('/poll') def poll(): vote = request.args.get('field') out = open(filename, 'a') out.write( vote + '\n' ) out.close() return render_template('thankyou.html', data=poll_data) @app.route('/results') def show_results(): votes = <> for f in poll_data['fields']: votes[f] = 0 f = open(filename, 'r') for line in f: vote = line.rstrip("\n") votes[vote] += 1 return render_template('results.html', data=poll_data, votes=votes) if __name__ == "__main__": app.run(debug=True)
Если мы перейдем по ссылке http://127.0.0.1:5000/results, то увидим такой ответ:

$ git add poll.py templates/results.html $ git commit -m "show the results"
1. Разработка web-приложение на Flask
В данной лабораторной работе рассматривается пример создания простейшего веб-приложения, назвываемого Flaskr. Пользователи могут регистрироваться, входить в систему, создавать посты, редактировать и удалять свои посты.
За основу взята пошаговая инструкция с официального сайта Flask, однако в качестве СУБД выступает встраиваемая версия российской СУБД Ред База Данных.
1.1. Создание приложения
Прежде всего необходимо создать каталог, в котором будет располагаться проект.
$ mkdir redflaskr $ cd redflaskr
Далее будем предполагать, что вся работа выполняется в каталоге redflaskr . Все пути к файлам будем указывать относительно него.
Теперь необходимо настроить виртуальное окружение для Python (Python virtual environment) и установить Flask и драйвер СУБД Ред База Данных.
Виртуальное окружение используется для управления зависимостями проекта как при разработке так и при разворачивании на продуктиве. Чем больше проектов есть у разработчика, тем более вероятно что они требуют различных версий библиотек Python или даже разных версий самого Python. Новые версии одних библиотек могут разрушить совместимости в другом проекте.
Виртуальное окружение (virtual environment)
Независимая группа библиотек для каждого проекта. Пакеты установленные для одного проекта не влияют на другие проекты или пакеты операционной системы.
Python поставляется с модулем venv для создания виртуального окружения.
Для создания виртуального окружения выполните команду
$ python3 -m venv venv
Перед началом работы над проектом активируйте соответствующее окружение:
$ . venv/bin/activate
В активированном окружении установите Flask и драйвер для СУБД Ред База Данных:
$ pip install Flask $ pip install fdb
Проекты на Python используют пакеты для организации кода и мы этим воспользуемся.
Каталог проекта будет содержать:
- flaskr : Пакет Python, содержащий код приложения и другие файлы.
- venv : Виртуальное окружение, в котором будет установлен Flask, драйвер СУБД fdb и другие зависимости.
Приложение Flask это объект (instance) класса Flask. Все, что связано с приложением (настройки, URL адреса, прочее) будет настраиваться в этом объекте.
Наиболее простой путь создания приложения — это создать глобальный объект непосредственно в начале программы, однако по мере роста проекта это может принести проблемы.
Вместо создания объекта глобально, мы будем создавать его внутри функции. Такая функция называется фабрикой приложения (application factory). Все настройки, регистрации и т.п. будут происходить внутри функции, после чего объект приложения будет возвращен.
1.1.1. Фабрика приложения
Создайте каталог flaskr , а внутри файл __init__.py , который содержит фабрику приложения и говорит Python, что катало flaskr должен рассматриваться как пакет.
$ mkdir flaskr
import os from flask import Flask def create_app(test_config=None): # create and configure the app app = Flask(__name__, instance_relative_config=True) app.config.from_mapping( SECRET_KEY='dev', DATABASE=os.path.join(app.instance_path, 'flaskr.fdb'), USER='sysdba', PASSWORD='masterkey', LIBRARY=os.path.join(app.root_path, 'rdb/libfbclient.so') ) if test_config is None: # load the instance config, if it exists, when not testing app.config.from_pyfile('config.py', silent=True) else: # load the test config if passed in app.config.from_mapping(test_config) # ensure the instance folder exists try: os.makedirs(app.instance_path) except OSError: pass # a simple page that says hello @app.route('/hello') def hello(): return 'Hello, World!' return app
create_app это функция фабрика приложения. Позже она будет дополнена, но и сейчас она многое делает:
- app = Flask(__name__, instance_relative_config=True) создает объект приложения Flask. __name__ : имя текущего модуля Python. Приложению необходимо знать где оно располагается, чтобы установить некоторые пути. instance_relative_config : говорит приложению, что файлы конфигурации размещаются относительно каталога instance . Он размещается вне каталога flaskr и содержит локальные данные: конфигурационные файлы, БД.
- app.config.from_mapping устанавливает значения параметров конфигурации по умолчанию. SECRET_KEY : используется классом Flask и расширениями для обеспечения безопасности хранимых данных. Значение dev позволяет удобно разрабатывать приложения, но должно быть заменено случайным значением при поставке приложения заказчику. DATABASE : путь к файлу БД. БД размещается в каталоге instance . В зависимости от нужд приложения, может быть любым, в том числе псевдонимом БД на удаленном сервере. В нашем случае мы воспользуемся встроенным сервером. USER : Имя пользователя, от которого будет производиться соединение. PASSWORD : Пароль. Для встроенного сервера игнорируется. LIBRARY : путь до клиентской библиотеки libfbclient.so , которую мы установим в следующей части.
- app.config.from_pyfile перезаписывает значения параметров конфигурации значениями из файла config.py каталога instance , если он существует. Например, при поставке приложения в нем можно указать реальное значение SECRET_KEY .
- os.makedirs() гарантирует существование каталога app.instance_path . Flask не создает каталог автоматически, но он нужен для файла БД.
- @app.route() создает простой маршрут, чтобы убедиться что приложение работает, прежде чем продолжить его разрабатывать. Это связывает URL /hello и функцию, которая сформирует ответ. В данном случае строку „Hello, World!“.
1.1.2. Запуск приложения
Теперь можно запустить приложение, используя команду flask . Укажите Flask где искать приложение и запустите его в режиме разработчика.
Вы должны быть в каталоге redflaskr, но не в его подкаталогах.
Режим разработчика показывает интерактивный отладчик когда страница выбрасывает исключение и перезапускает сервер, когда вы делаете изменения в коде. Его можно оставить запущенным и просто обновлять страницу в браузере по мере разработки.
$ export FLASK_APP=flaskr $ export FLASK_ENV=development $ flask run
Вы увидите вывод, подобный этому:
* Serving Flask app "flaskr" * Environment: development * Debug mode: on * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) * Restarting with stat * Debugger is active! * Debugger PIN: 855-212-761
Перейдите по адресу http://127.0.0.1:5000/hello в браузере и вы увидите сообщение «Hello, World!».
1.2. Работа с БД
Приложение будет использовать СУБД Ред База Данных для хранения пользователей и их постов.
Данная СУБД поддерживает SQL-2011 и может работать как во встроенном режиме без установки сервера, так и в традиционном клиент-сервером варианте. Для веб-приложения удобным является использование встроенного режима.
1.2.1. Установка встроенной СУБД
Скачайте последнюю версию СУБД с официального сайта https://reddatabase.ru/downloads. Для работы будет достаточно открытой редакции XXX-linux-x86_64.tar.gz и распакуйте его в любой каталог, например /tmp .
В каталоге flaskr сделайте подкаталог rdb для установки встроенной версии СУБД Ред База Данных, в в нем подкаталог plugins .
$ mkdir flaskr/rdb $ mkdir flaskr/rdb/plugins
Скопируйте из распакованного архива СУБД файлы libfbclient.so и libEngine12.so в только что созданные каталоги.
$ cp /tmp/RedDatabase-X.X.X-x86_64/lib/libfbclient.so.X.X.X flaskr/rdb/libfbclient.so $ cp /tmp/RedDatabase-X.X.X-x86_64/plugins/libEngine12.so flaskr/rdb/plugins
Если СУБД установлена на этом сервере или любом другом, то потребуется лишь библиотека клиента libfbclient.so . Можно ее и не копировать, а просто указать ее расположение в файле конфигурации, в параметре LIBRARY .
1.2.2. Подключение к БД
При работе с БД первое что необходимо сделать, создать подключение. Все запросы выполняются через подключение, которое закрывается когда работа выполнена.
В web-приложениях подключение обычно привязывается к запросу. В какой-то момент обработки запроса оно создается, а перед отправкой ответа закрывается.
Кроме этого необходимо написать код для инициализации БД.
import fdb import click from flask import current_app, g from flask.cli import with_appcontext def init_db(): try: conn = fdb.connect( dsn=current_app.config['DATABASE'], user=current_app.config['USER'], password=current_app.config['PASSWORD'], fb_library_name=current_app.config['LIBRARY'] ) conn.drop_database() except Exception as e: print(e) conn = fdb.create_database( dsn=current_app.config['DATABASE'], user=current_app.config['USER'], password=current_app.config['PASSWORD'], fb_library_name=current_app.config['LIBRARY'] ) metadata = [ ''' RECREATE TABLE users ( id integer generated by default as identity primary key, username varchar(256) UNIQUE NOT NULL, password varchar(256) NOT NULL ) ''', ''' RECREATE TABLE posts ( id integer generated by default as identity primary key, author_id INTEGER NOT NULL REFERENCES users (id), created TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, title varchar(120) NOT NULL, body varchar(5000) NOT NULL ) ''' ] cursor = conn.cursor() for query in metadata: cursor.execute(query) conn.commit() def get_db(): if 'db' not in g: g.db = fdb.connect( dsn=current_app.config['DATABASE'], user=current_app.config['USER'], password=current_app.config['PASSWORD'], fb_library_name=current_app.config['LIBRARY'] ) return g.db def close_db(e=None): db = g.pop('db', None) if db is not None: db.close() @click.command('init-db') @with_appcontext def init_db_command(): """Clear the existing data and create new tables.""" init_db() click.echo('Initialized the database.') def init_app(app): app.teardown_appcontext(close_db) app.cli.add_command(init_db_command)
get_db создает подключение к БД.
специальный объект, уникальный для каждого запроса. Он используется для хранения данных, которые могут использоваться множеством функции во время обработки запросы. Подключение создается и повторно используется, если get_db вызывается не первый раз.
другой специальный объект, который указывает на приложение Flask, обрабатывающее запрос.
устанавливает подключение к БД используя параметры конфигурации. Файл БД создается в функции init_db .
close_db закрывает подключение к БД, если g.db установлен.
init_db создает БД и необходимые объекты.
список SQL команд для создания объектов БД: таблицы пользователей users и таблицы постов posts .
Вначале функция пытается установить соединение с имеющейся БД для того, чтобы ее удалить. В случае неудачи печатается исключение для целей отладки. Далее в люблом случае создается новая БД, используя все теже параметры конфигурации.
создает объект курсор, с помощью которого можно выполнять все запросы к СУБД.
В цикле выполняются все запросы из списка метаданных и в завершении производиться завершение транзакции и применение всех изменений.
click.command() определяет команду командной строки «init-db», которая вызывает функцию init_db и сообщает об успешности выполнения инициализации пользователю.
init_app(app) регистрирует созданные функции.
Функции close_db и init_db_command должны быть зарегистрированы в объекте приложения, иначе они не будут использоваться.
говорит Flask вызвать указанную функцию при очистке после отправки ответа.
добавляет новую команду, которая может вызываться с командой flask .
Импортируйте и добавьте вызов этой функции в фабрике приложения.
def create_app(): app = . # existing code omitted from . import db db.init_app(app) return app
1.2.3. Инициализация БД
Теперь, когда команды init-db зарегистрирована, она может быть вызвана используя команду flask , аналогично команде run .
Если у вас все еще запущен сервер с предыдущего этапа, необходимо его остановить или выполнить команду в другом терминале. Помните что необходимо находиться в каталоге redflaskr и активировать виртуальное окружение.
$ flask init-db Initialized the database.
В каталоге instance должен появится файл flaskr.fdb .
1.3. Эскизы (Blueprints) и представления (Views)
Функция-представление :
(View function) функция, которыя отвечает на запросы к приложению. Flask использует шаблоны, чтобы сопоставить входящие URL запросов функциям-представлениям.
(Blueprint) способ организовать группы связанных представлений и другой код. Вместо регистрации представление и другого кода непосредственно в приложении, они регистрируются в эскизе. Далее эксиз регистрируется в приложении, в фабрике приложения.
1.3.1. Создание эскиза
Flaskr будет иметь два эскиза. Один для аутентификации, другой для функций работы с постами. Код для каждого эскиза будет размещатся в отдельных модулях. В первую очередь сделаем модуль аутентификации.
import functools from flask import ( Blueprint, flash, g, redirect, render_template, request, session, url_for ) from werkzeug.security import check_password_hash, generate_password_hash from flaskr.db import get_db bp = Blueprint('auth', __name__, url_prefix='/auth')
Этот код создает эскиз под названием „auth“. Как и объект приложение, эскиз должен знать где его создали. Для этого __name__ передается вторым параметром. url_prefix будет предшествовать всем URL адресам, связанным с этим эскизом.
Импортируйте и зарегистрируйте эскиз в фабрике приложения, используя app.register_blueprint() . Добавьте следующий код в конец фабрики приложения перед возвращением объекта приложения.
def create_app(): app = . # existing code omitted from . import auth app.register_blueprint(auth.bp) return app
Эскиз аутентификации будет иметь функции-представления для регистрации новых пользователей и входа/выхода существующих.
1.3.2. Первое представление: регистрация
Когда пользователь заходит на страницу /auth/register представление register вернет HTML код с формой для заполнения. При отправке данных формы она будет проверена и либо будет показано сообщение об ошибке, либо будет создан новый пользователь и осуществлен переход на страницу входа.
Сейчас просто допишем код функции-представления к имеющемуся коду файлы flaskr/auth.py . Далее будут написаны шаблоны для генерации HTML форм.
# existing code #. @bp.route('/register', methods=('GET', 'POST')) def register(): if request.method == 'POST': username = request.form['username'] password = request.form['password'] db = get_db() error = None if not username: error = 'Username is required.' elif not password: error = 'Password is required.' if error is None: try: db.cursor().execute( "INSERT INTO users (username, password) VALUES (?, ?)", (username, generate_password_hash(password)), ) db.commit() except db.DatabaseError as e: if e.args[2] == 335544665: #isc_unique_key_violation error = f"User username> is already registered." else: return redirect(url_for("auth.login")) flash(error) return render_template('auth/register.html')
Вот что делает эта функция:
- @bp.route : сопоставляет URL /register с функцией-представлением register . Когда Flask получит запрос на /auth/register будет вызвана функция register и ее результат будет отправлен пользователю.
- Для отправки формы пользователем используется метод POST . Если это так, то проверяет входные параметры.
- request.form : специальный тип ассоциативного массива с параметрами формы, содержащий пары ключ-значение. Пользователь будет вводить туда username и password .
- Проверяем что введенные значение не пустые.
- Если проверка успешна, вставляем данные нового пользователя в базу данных.
- db.cursor().execute : создает объект курсор из подключения и вызывает его метод execute для выполнения запроса на вставку в таблицу. Метод принимает SQL динамического запроса, с ? вместо значений и список значений. Такой способ защищает от SQL инъекций и строго рекомендуется в отличие от конструирования запроса как статического в виде одной строки.
- Для целей безопасности пароль не храниться в БД в открытом виде. Он хешируется функцией generate_password_hash() . Поскольку этот метод меняет данные, то транзакцию необходимо подтвердить с помощью db.commit() .
- db.DatabaseError : исключение, которое будет выброшено при ошибке выполнения запроса. Код ошибки 335544665 соответствует ошибке дубликата первичного ключа, т.е. возникнет когда такой пользователь уже есть.
- После сохранения пользователя он перенаправляется на страницу входа. url_for() генерирует URL адрес представления на основе его имени. Это предпочтительнее прямой записи URL, т.к. позволяет менять адрес позднее, без смены остального кода. regirect() генерирует ответ для перенаправление на указанный (сгенерированный) URL адрес.
- Если проверка параметров будет неудачной, то пользователю будет показана ошибка. flash() сохраняет сообщения и они будут показаны при следующем рендеринге шаблона.
- При первом открытии страницы /auth/register или при ошибке проверки параметров, должна быть снова показана страница регистрации. render_template() рендерит шаблон, содержащий HTML, который мы вскоре напишем.
1.3.3. Представление: вход
Это представление пишется по той же схеме, что и представление register выше, также дописывается к существующему коду.
# existing code #. @bp.route('/login', methods=('GET', 'POST')) def login(): if request.method == 'POST': username = request.form['username'] password = request.form['password'] db = get_db() error = None user = db.cursor().execute('SELECT * FROM users WHERE username = ?', (username,) ).fetchonemap() if user is None: error = 'Incorrect username.' elif not check_password_hash(user['password'], password): error = 'Incorrect password.' if error is None: session.clear() session['user_id'] = user['id'] return redirect(url_for('index')) flash(error) return render_template('auth/login.html')
Есть несколько отличий от представления register :
- Для последующего использования в переменной сохраняется пользователь из таблицы users . fetchonemap() возвращает одну строку из запроса. Результат имеет тип словарь. Если запрос не вернул ни одной строки, результат будет None .
- check_password_hash() хеширует пароль из формы и сравнивает его с сохраненным. Если равны хеши паролей, то и пароли совпадают.
- session переменная типа словарь, которая хранит данные между запросами. Если аутентификация прошла успешно, тогда id пользователя сохраняется для новой сессии. Данные сохраняются в cookie которые отправляются в браузер. Браузер пришлет их обратно в запросах. Flask безопасно подписывает данные, так что они не могут подменены.
Теперь, когда идентификатор пользователя сохранен в сессии, он будет доступен последующим запросам. В начале каждого запроса, если пользователь прошел аутентификацию, информация о нем должна быть загружена и стать доступной другим представлениям.
# existing code #. @bp.before_app_request def load_logged_in_user(): user_id = session.get('user_id') if user_id is None: g.user = None else: g.user = get_db().cursor().execute('SELECT * FROM users WHERE >, (user_id,)).fetchonemap()
@bp.before_app_request() регистрирует функцию, которая выполняется перед функциями-представлениями, безотносительно какой URL был запрошен. load_logged_in_user() проверяет сохранен ли в session и если да, то читает информацию о нем из БД, сохраняя в g.user , который существует на протяжении всей обработки запроса. В противном случае g.user будет установлен в None .
1.3.4. Представления: выход
Для выхода необходимо удалить user_id из session . Тогда load_logged_in_user не сможет загружать пользователя в последующих запросах.
# existing code #. @bp.route('/logout') def logout(): session.clear() return redirect(url_for('index'))
1.3.5. Требования входа в других представлениях
Создание, редактирование и удаление постов блога требует чтобы пользователь был авторизован. Декоратор используется для проверки каждого представления, к которому он примеряется.
# existing code #. def login_required(view): @functools.wraps(view) def wrapped_view(**kwargs): if g.user is None: return redirect(url_for('auth.login')) return view(**kwargs) return wrapped_view
Этот декоратор возвращает новую функцию-представление, которая оборачивает оригинальное представление. Новая функция проверяет загружен ли пользователь и если нет перенаправляет на страницу входа. Если пользователь загружен, то вызывается оригинальная функция-представление и выполнение продолжается как обычно. Этот декоратор будет использован далее, при написании представлений блога.
1.4. Шаблоны (Templates)
Функции-представления написаны, но если мы перейдем по любому адресу, мы получим ошибку TemplateNotFound . Это потому что используется функция render_template , но для нее еще не написаны шаблоны. Шаблоны располагаются в каталоге templates внутри пакета flaskr .
Шаблоны — это файлы, содержащие статические данные вместе с подстановками (placeholders) для динамических данных.Шаблоны рендерятся с конкретными данными для получения финального документа. Flask использует библиотеку шаблонов Jinja.
Jinja похожа на Python. Специальные разделители используются для отличия синтаксиса Jinja от статических данных шаблона. Содержимое между > является выражением, результат которого будет выведен в финальный документ. выделяют управлящие потоки типа if и for . В отличие от Python блоки выделяются в начале и конце тегами.
1.4.1. Базовый шаблон
Каждая страница приложения будет иметь одинаковую структуру с различным заполнением. Вместо написания HTML структуры для каждого шаблона, каждый шаблон будет расширять базовый и перезаписывать отдельные секции.
title> - Flaskrtitle> link rel="stylesheet" href=">"> nav> h1>Flaskrh1> ul> li>span>>span> li>a href=">">Log Outa> li>a href=">">Registera> li>a href=">">Log Ina> ul> nav> section class="content"> header> header> div class="flash">>div> section>
g автоматически доступна в шаблонах. В зависимости от g.user показываются либо имя пользователя и ссылка на выход, либо ссылка на регистрацию и ссылка на вход. url_for() тоже автоматически доступна и используется для генерации URL адресов на представления вместо ручного их написания.
После заголовка страницы и перед основным содержимым шаблон в цикле проходит по всем сообщениям, возвращаемым get_flashed_messaged() . Функция flask() использовалась в представлениям для показа сообщений об ошибках и этот код их показывает.
Здесь определены 3 блока, которые будут переписаны другими шаблонами:
- будет изменять отображаемый в браузере заголовок вкладки.
- подобен title , но будет изменять заголовок, отображаемый на странице.
- будет отображать содержимое каждой страницы, например форму входа или пост блога.
Этот базовый шаблон размещается прямо в каталоге templates . Для организованного хранения остальных, разместим их в каталогах по названиям экскизов.
1.4.2. Регистрация
h1>Registerh1> form method=«post»> label for=«username»>Usernamelabel> input name=«username» id=«username» required> label for=«password»>Passwordlabel> input type=«password» name=«password» id=«password» required> input type=«submit» value=«Register»> form>
говорит Jinja что этот шаблон заменяет блоки базового шаблона. Все отображаемое содержимое должно содержаться внутри блока , которые перезаписывают блоки базового шаблона.
Здесь используется полезный паттерн размещения блока внутри блока . Это установит title block и затем итоговое значение будет использовано в header block . Так что оба заголовка используют одно и тоже название, без необходимости писать его дважды.
1.4.3. Вход
Этот шаблон аналогичен шаблону регистрации, за исключением заголовка и кнопки submit.
h1>Log Inh1> form method=«post»> label for=«username»>Usernamelabel> input name=«username» id=«username» required> label for=«password»>Passwordlabel> input type=«password» name=«password» id=«password» required> input type=«submit» value=«Log In»> form>
1.4.4. Регистрация
Теперь когда все шаблоны аутентификации написаны, можно зарегистрировать пользователя. Убедитесь что сервер запущен и перейдите по адресу http://127.0.0.1:5000/auth/register.
Попробуйте нажать на кнопку «Register» без заполнения формы и посмотрите на ошибки.
При успешном заполнении имени пользователя и пароля, вы будете перенаправлены на страницу логина. Попробуйте ввести некорректный логин или пароль.
Если вы войдете в систему, то все равно должны увидеть ошибку, т.к. еще нет представления для index .
1.5. Статические файлы
Представления аутентификации и шаблоны выглядят слишком примитивными. Чтобы немного стилизовать HTML добавим CSS. Таблицы стилей являются статическими файлами.
Flask автоматически добавляет представление static , которое принимает путь относительно каталога flaskr/static и обрабатывает его. Базовый шаблон base.html уже имеет статическую ссылку на файл style.css :
Кроме CSS могут быть и другие типы статических файлов: JavaScript, изображения и т.п. Все они располагаются в каталог flaskr/static и для ссылки используется url_for(‘static’, filename=’. ‘) .
Здесь мы не делаем упор на изучение CSS, так что просто скопируем содержимое файла в:
html font-family: sans-serif; background: #eee; padding: 1rem; > body max-width: 960px; margin: 0 auto; background: white; > h1 font-family: serif; color: #377ba8; margin: 1rem 0; > a color: #377ba8; > hr border: none; border-top: 1px solid lightgray; > nav background: lightgray; display: flex; align-items: center; padding: 0 0.5rem; > nav h1 flex: auto; margin: 0; > nav h1 a text-decoration: none; padding: 0.25rem 0.5rem; > nav ul display: flex; list-style: none; margin: 0; padding: 0; > nav ul li a, nav ul li span, header .action display: block; padding: 0.5rem; > .content padding: 0 1rem 1rem; > .content > header border-bottom: 1px solid lightgray; display: flex; align-items: flex-end; > .content > header h1 flex: auto; margin: 1rem 0 0.25rem 0; > .flash margin: 1em 0; padding: 1em; background: #cae6f6; border: 1px solid #377ba8; > .post > header display: flex; align-items: flex-end; font-size: 0.85em; > .post > header > div:first-of-type flex: auto; > .post > header h1 font-size: 1.5em; margin-bottom: 0; > .post .about color: slategray; font-style: italic; > .post .body white-space: pre-line; > .content:last-child margin-bottom: 0; > .content form margin: 1em 0; display: flex; flex-direction: column; > .content label font-weight: bold; margin-bottom: 0.5em; > .content input, .content textarea margin-bottom: 1em; > .content textarea min-height: 12em; resize: vertical; > input.danger color: #cc2f2e; > input[type=submit] align-self: start; min-width: 10em; >
Откройте ссылку http://127.0.0.1:5000/auth/login и сейчас страница должна выглядеть как на картинке.

1.6. Эскиз блога
Будут использованы теже подходы, что и для написания эскиза аутентификации. Блог показывает список всех постов, позволяя авторизованным пользователям создавать посты, а авторам менять и удалять посты.
Во время реализации каждого представления, оставляйте сервер запущенным (в режиме разработчика). После сохранения изменений, переходите по соответствующему URL в браузере и тестируйте его.
Объявим эскиз и добавим его в фабрику приложения.
from flask import ( Blueprint, flash, g, redirect, render_template, request, url_for ) from werkzeug.exceptions import abort from flaskr.auth import login_required from flaskr.db import get_db bp = Blueprint('blog', __name__)
Импортируйте и зарегистрируйте эскиз.
def create_app(): app = . # existing code omitted from . import blog app.register_blueprint(blog.bp) app.add_url_rule('/', endpoint='index') return app
В отличие от эскиза аутентификации, эскиз блога не имеет url_prefix . Таким образом представление index будет размещаться в корне / , представление create по адресу /create и т.д. Блог — основная функция Flaskr и логично сделать index основным представлением.
Однако, точка входа для index будет blog.index . Некоторые представления аутентификации ссылаются на простой index . app.add_url_rule() связывает точку входа index с адресом / , так что url_for(‘index’) и url_for(‘blog.index’) будут работать одинаково, генерируя одинаковый адрес URL.
В других приложениях вам может потребоваться дать другой url_prefix для эскиза и определить другой index для приложения, подобный представлению hello .
1.6.1. Представление Index
Индекс будет показывать все посты, начиная с последних. SQL запрос использует JOIN для получения аутентификационной информации из таблицы users .
@bp.route('/') def index(): db = get_db() posts = db.cursor().execute( 'SELECT p.id, title, body, created, author_id, username' ' FROM posts p JOIN users u ON p.author_id = u.id' ' ORDER BY created DESC' ).fetchallmap() return render_template('blog/index.html', posts=posts)
h1>Postsh1> a class=«action» href=«>»>Newa> article class=«post»> header> div> h1>>h1> div class=«about»>by > on >div> div> a class=«action» href=«>»>Edita> header> p class=«body»>>p> article> hr>
Когда пользователь авторизован, блок header добавляет ссылку на представление create . Когда пользователь автор поста, он увидит ссылку «Edit» связанную с представлением update для поста. loop.last — это специальная переменная, доступная внутри циклов Jinja. Она используется для исключения печати разделительной линии для последнего поста.
1.6.2. Представление Create
Представление create походе на представление register . Либо отображается форма для заполнения данных, либо введенные данные проверяются и пост добавляется в базу данных или показывается ошибка.
Декоратор login_required , который мы написали ранее, будет использован для представлений блога. Пользователь должен быть авторизован, чтобы открывать эти представления, иначе он будет перенаправлен на страницу входа.
@bp.route('/create', methods=('GET', 'POST')) @login_required def create(): if request.method == 'POST': title = request.form['title'] body = request.form['body'] error = None if not title: error = 'Title is required.' if error is not None: flash(error) else: db = get_db() db.cursor().execute( 'INSERT INTO posts (title, body, author_id)' ' VALUES (?, ?, ?)', (title, body, g.user['id']) ) db.commit() return redirect(url_for('blog.index')) return render_template('blog/create.html')
h1>New Posth1> form method=«post»> label for=«title»>Titlelabel> input name=«title» id=«title» value=«>» required> label for=«body»>Bodylabel> textarea name=«body» id=«body»>>textarea> input type=«submit» value=«Save»> form>
1.6.3. Представление Update
Представлениям update и delete необходимо извлечь пост по его идентификатору и сравнить автора с зарегистрированным пользователем. Чтобы избежать дублирования кода, напишем функцию получения поста и в дальнейшем используем ее в обоих представлениях.
def get_post(id, check_author=True): post = get_db().cursor().execute( 'SELECT p.id, title, body, created, author_id, username' ' FROM posts p JOIN users u ON p.author_id = u.id' ' WHERE p.id = ?', (id,) ).fetchonemap() if post is None: abort(404, f"Post id id> doesn't exist.") if check_author and post['author_id'] != g.user['id']: abort(403) return post
abort() выбрасывает специальное исключение, которое возвращает код статуса HTTP. Она принимает текст ошибки. Код 404 означает что страница не найдена (Not Found), а код 403 означает что доступ к странице запрещен (Forbidden).
Аргумент check_author может быть полезен в будущем.
Код представления update .
@bp.route('//update', methods=('GET', 'POST')) @login_required def update(id): post = get_post(id) if request.method == 'POST': title = request.form['title'] body = request.form['body'] error = None if not title: error = 'Title is required.' if error is not None: flash(error) else: db = get_db() db.cursor().execute( 'UPDATE posts SET title = ?, body = ?' ' WHERE >, (title, body, id) ) db.commit() return redirect(url_for('blog.index')) return render_template('blog/update.html', post=post)
В отличие от представлений, что мы писали до этого, update принимает аргумент, id . На это указывает . Реальный адрес будет выглядеть, например, /1/update . Flask извлечет 1 из адреса, убедиться что это значение типа int и передаст его как аргумент функции. Если не указать int: то аргумент будет string . Чтобы генерировать адреса для страницы обновления, url_for() необходимо передать id для заполнения: url_for(‘blog.update’, id=post[‘id’]) .
Представления create и update похожи друг на друга. Основное отличие в том, что update использует объект post и запрос UPDATE вместо INSERT . Теоретически можно придумать единый шаблон для этих двух представлений, но для наших целей мы не будем усложнять.
h1>Edit «>»h1> form method=«post»> label for=«title»>Titlelabel> input name=«title» id=«title» value=«>» required> label for=«body»>Bodylabel> textarea name=«body» id=«body»>>textarea> input type=«submit» value=«Save»> form> hr> form action=«>» method=«post»> input class=«danger» type=«submit» value=«Delete» onclick=«return confirm(‘Are you sure?’);»> form>
Шаблон имеет две формы. Первая отправляет отредактированные данные на текущую страницу //update . Вторая форма содержит единственную кнопку и определяет атрибут action ссылающийся на представление delete . Кнопка использует JavaScript для запроса подтверждения действия.
Паттерн > используется в зависимости от данных, которые надо показать в форме. Когда форма не отправлена, показываются данные оригинального поста, но если в форму переданы некорректные данные, необходимо показать это чтобы пользователь исправил ошибку. Для этого используется request.form . request — это еще одна переменная, автоматически доступная в шаблонах.
1.6.4. Представление Delete
Это представление не имеет своего шаблона, а кнопка удаления является частью шаблона update.html . Таким образом, достаточно написать только функцию представление и обработать только метод POST , а потом перенаправить на представление index .
@bp.route('//delete', methods=('POST',)) @login_required def delete(id): get_post(id) db = get_db() db.cursor().execute('DELETE FROM posts WHERE >, (id,)) db.commit() return redirect(url_for('blog.index'))
На этом разработка первого приложения завершена. Необходимо его тщательно протестировать.
Для изучения того, как тестировать и распространять подобные приложения рекомендуем изучить материалы по Flask — https://flask.palletsprojects.com/en/2.0.x/tutorial
© Copyright 2021, Roman Simakov. Ревизия 6b28dce3 .
Flask название вкладки как сделать

Создайте шаблоны resume.html, counters.html, skills.html, interests.html. Фото автора для резюме поместите в static/img. Обновите страницу – раздел «Резюме» полностью готов:

Последнее, что мы сделаем на этом этапе – шаблон для секции «Портфолио». Код для шаблона возьмите здесь – portfolio.html. Не забудьте включить портфолио в index.html:

Весь код и контент для третьего этапа есть здесь.
Четвертый этап
На заключительном этапе мы реализуем просмотр карточек проектов в портфолио, а затем сделаем секцию блога, контактную форму и страницу 404.
Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека питониста»
Карточки портфолио
Вся базовая информация о проектах (название и краткое описание) пока что находится прямо в шаблоне portfolio.html. Если добавлять туда еще и подробное описание проектов, объем шаблона быстро станет неприлично большим. Гораздо удобнее иметь отдельный Markdown-файл с подробным описанием каждого проекта: такие карточки проще редактировать и сортировать в нужном порядке.
Для просмотра детальной информации по каждому проекту в портфолио нужно:
- добавить в директорию content вложенную папку portfolio;
- сохранить в этой папке тестовые карточки проектов;
- добавить в mysite.py новый маршрут;
- сделать шаблон card.html.
Начнем с mysite.py. Добавьте туда новую переменную:

Секция «Блог» и фильтрация по тегам
Как и в секции «Портфолио», в блоге статьи представлены в виде карточек. Загрузка содержимого статьи происходит без обновления страницы, для перехода между записями используется горизонтальный свайп.
Фильтрацию записей в блоге выполняет скрипт isotope.js. Сортировка проводится в соответствии с тегами, которые указываются в YAML-записей. Извлечь теги поможет этот фрагмент кода – его нужно вставить в функцию представления в файле mysite.py:

Но записи по-прежнему рендерятся в тестовом шаблоне post.html и выглядят неказисто. Измените код шаблона на новый и добавьте в папку /static/img/portfolio фоновое изображение . Блоки кода пока что выделены только моноширинным шрифтом. Однако мы установили модуль Pygments, который определяет стили форматирования кода. Все они находятся здесь – .venv\Lib\site-packages\pygments\styles.
Для подсветки синтаксиса добавим в mysite.py маршрут к pigments :

Контактная форма
Для секции «Контакты» мы создадим отдельный шаблон contacts.html и включим его в index.html:

Теперь можно получать сообщения от посетителей сайта:

Посетитель увидит уведомление об отправке, а владелец сайта получит сообщение на емейл, указанный при регистрации. Formspree фильтрует спам. Другие подобные сервисы по обработке форм на статических сайтах – 99Inbound и KwesForms .
Страница 404
Осталось создать функцию представления и шаблон для страницы ошибки 404. Сохраните этот код в 404.html и добавьте обработку ошибки 404 в mysite.py:

Нюансы GitHub Pages
Если сайт будет размещаться на username.github.io, в коде ничего менять не надо. В случае размещения сайта в проектной директории username.github.io/site нужно добавить название папки ко всем ссылкам на статические файлы. То есть вместо таких привычных ссылок:
должны быть такие:
Кроме того, у ссылок на записи и карточки портфолио не должно быть ведущих слешей. Они должны выглядеть так:
Кастомная страница для GitHub Pages
Чтобы GitHub Pages показывал кастомную страницу ошибки 404 вместо стандартной, достаточно загрузить готовый файл 404.html в корневую директорию репозитория. Переадресацию хостинг обеспечит сам.
Подведем итоги
Простота, гибкость, наличие огромного количества расширений и удобный шаблонизатор – главные преимущества Flask. При работе над проектом модуль FlatPages избавил нас и от необходимости подключения базы данных, и от написания скрипта для конвертации Markdown-файлов в html. Модуль Frozen Flask обеспечил автоматический экспорт статической копии сайта без ущерба для функциональности. Реализация этих операций на каком-либо другом фреймворке потребовала бы куда больше кода и усилий. Напоминаем, что весь код проекта находится здесь , а готовый сайт доступен на GitHub Pages.
Материалы по теме
- Django Junior: путь продолжающего
- Безопасная загрузка изображений в веб-приложении на Django
- Самый полный видеокурс по Django от установки до проекта