Flask 2.0.X и axios — AJAX проще простого

Раньше я использовал активно связку jQuery для отправки Ajax запроса на сервер. Например так обрабатывал форму обратной связи или онлайн заказ товара.

Время не стоит на месте с использованием библиотеки VueJS я познакомился с библиотекой axios. И стал использовать не только в проектах VueJS но и просто с Flask и Django.

Flask + axios

Напишу подробно, с начало создаю виртуальное окружение, затем ставлю Flask.

mkdir flask-axios
cd flask-axios
python3 -m venv venv
source venv/bin/activate
pip install pip --upgrade
pip install Flask==2.0.1

Далее создаю директорию templates и шаблоны base.html, index.html, файл проекта app.py и дополнительные файлы app.js в директории static.

Создаю переменную окружения FLASK_ENV=development для того, что бы запускать проект в режиме отладки (DEBUG). В данном режиме приложение будет автоматический производить рестарт при изменении исходного кода.

Ещё одна переменная окружения FLASK_APP=app.py приложение Flask, запуск по команде flask run.

mkdir -p templates static/js
touch templates/{base,index}.html
touch static/js/callback.js
touch {app,forms}.py

export FLASK_ENV=development
export FLASK_APP=app.py

Сам проект на Flask

Файл app.py

from flask import Flask
from flask import render_template
import json
#  форма для обратной сзвязи
from forms import CallBackForm

app = Flask(__name__)
SECRET_KEY = '12345'
app.config['SECRET_KEY'] = SECRET_KEY


@app.route("/")
def index():
    form = CallBackForm()
    return render_template('index.html',
                           title="Пример отправки",
                           form=form)


@app.route("/callback", methods=['POST'])
def callback():
    form = CallBackForm()
    if form.validate_on_submit():
        #  функции отправить почту, записать в БД и т. д.
        return json.dumps({'success': 'true', 'msg': 'Ждите звонка!'})
    else:
        #  обработать ошибку
        return json.dumps({'success': 'false', 'msg': 'Ошибка на сервере!'})

В строке 20 происходит обработка нажатия на кнопку (index.html строка 7).

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

Форма

Файл forms.py

from flask_wtf import FlaskForm
from wtforms import StringField
from wtforms.validators import DataRequired
from wtforms.validators import Length


class CallBackForm(FlaskForm):
    name = StringField('Имя', validators=[DataRequired()])
    phone = StringField('Телефон', validators=[DataRequired(), Length(min=4)])

В сроке 9 параметр Length(min=4) указывает, если количество знаков меньше 4, валидация не пройдёт.

Верстка

Файл base.html в директории templates

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Flask 2.0.X axios simple - {{ title }}</title>
        <script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
        <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
        {% block scripts %}
        {% endblock scripts %}
    </head>
    <body>
        {% block content %}
        {% endblock content %}
    </body>
</html>

В сроке 6 и 7 подключаю через cnd — JQuery и Axios.

Не стал для усложнения переписывать проект без JQuery.

Верстка

Файл index.html в директории templates

{% extends "base.html" %}
{% block content %}
    <form action="/callback" id="form1" method="post">
        {{ form.csrf_token }}
        {{ form.name.label }} {{ form.name() }}
        {{ form.phone.label }} {{ form.phone() }}
        <input type="submit" value="Отправить"/>
    </form>
    <div id="msg"></div>
{% endblock %}

{% block scripts%}
    {{ super() }}
    <script src="{{ url_for('static', filename='js/callback.js') }}"></script>
{% endblock %}

В строке 4 csrf_token иначе не пройдёт валидацию.

JavaScript

Файл callback.html в директории static/js

/* переопределить поведение кнопки "Отправить" */
$(document).ready(function () {
    $("#form1").submit(function( event ) {
        CallBackForm("form1", "msg");
        event.preventDefault();
    });
});

/* отправка данных на сервер по AJAX */
function CallBackForm(form, msg){
    let from = new FormData();
    from.append('name', $( "#name" ).val());
    from.append('phone', $( "#phone" ).val());
    from.append('csrf_token', $( "#csrf_token" ).val());
    axios({
        url: '/callback',
        method: 'post',
        data: from,
    })
    .then(function (response) {
        $( '#' + msg ).html(response.data.msg);
        if (response.data.success == 'true') {
            $( "#" + form ).trigger('reset');
        }
        else
        {
            alert("Что-то пошло не так!");
            console.log("Ошибка");
        }
    })
    .catch(function (error) {
        console.log(error);
    });
};

В строках 12-14 добавляются данные из формы для отправки.  Строка 21 записывает ответ от сервера (файл app.py строка 25 или 28) в зависимости от правильности ввода.

Готовый код примера

git clone https://github.com/ivanov-s/flask-axios.git

VueJS конфликтует с Jinja2 и Django template

vuejs logo jinja2 logo django logo

В приложениях VueJS используется {{ varname }} для обработки шаблонов. В Jinja2 и Django шаблонах при рендеринге шаблона также используются фигурные скобки. Из-за этого возникает конфликт. Есть 3 способа решения проблемы.

VueJS — заменить фигурные скобки на что то другое

<!-- js файл -->
new Vue({
  delimiters: ['{*', '*}'],
  data: {
    VueVAR: 'test'
  }
})
<!-- html файл -->
<div id="app">
  {* VueVAR *} <!-- vuejs -->
  {{ name }}  <!-- переменная в Jinja2 или Django шаблоне переданная из views.py -->
</dev>

Использовать RAW в html для Jinja2

<!-- js файл -->
new Vue({
  data: {
    VueVAR: 'test'
  }
})
<!-- html файл -->
<div id="app">
  {{ VueVAR }}  <!-- vuejs -->
  {% raw %}
     {{ name }}  <!-- переменная в Jinja2 из views.py -->
  {% raw %}
</dev>

Для Django templates поменять фигурные скобки

# в файле setting.py
VARIABLE_TAG_START = '{*'
VARIABLE_TAG_END = '*}'
<!-- js файл -->
new Vue({
  data: {
    VueVAR: 'test'
  }
})
<!-- html файл -->
<div id="app">
{{ VueVAR }} <!-- vuejs -->
{* name *} <!-- переменная в Django template из views.py -->
</dev>

Ещё для Django использовать тег {% verbatim %}

<!-- html файл -->
<div id="app">
{% verbatim %}
{{ VueVAR }} <!-- vuejs -->
{% endverbatim %}
{{ name }} <!-- переменная в Django template из views.py -->
</dev>

Для Django последний способ рекомендуют использовать в большинстве случаев.

AJAX и jQuery — Flask (1.1.x) простой пример

пример работы ajax с Flask
Пример работы AJAX с Flask

Flask не стоит на месте. Решил дополнить пример обработки формы  при помощи AJXA. Я использую данный код для обработки формы заказа звонка на сайтах. Приведу список requirements.txt (для гарантии работы кода лучше использовать его). Версия Python 3.8.2.

Установка Flask и библиотек

#  настройка виртуального окружения python, всё как всегда :)
python3 -m venv venv
#  активация окружения
source venv/bin/activate
#  обновления pip
pip install pip --upgrade
#  установка всего необходимого
pip install -r requirements.txt
Flask==1.1.2
Flask-WTF==0.14.3

Подготовка

Необходимо создать структуру проекта, лучше сделать для примера как я привел ниже:

#  структура проекта
.
├── app.py
├── forms.py
├── requirements.txt
└── templates
    ├── base.html
    └── index.html

Создать директорию templates, и два файла в ней base.html и index.html.

mkdir templates
touch templates/base.html
touch templates/index.html

Содержимое файла base.html, всё как обычно, стоить отметить в строке 6 подключение библиотеки jQuery чере cnd google.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Flask AJAX + jQuery simple - {{ title }}</title>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
</head>
<body>
    {% block content %}
    {% endblock content %}
    <div id="msg"></div>
</body>
</html>

Так же создаю файл с index.html который расширяет base.html.

Строки 3-8 содержат форму.

Строка 10 содержит div с id=»msg» в него будет записан ответ с сервера.

Сроки 13-18 код который связывает события «submit» кнопки из формы, с функцией отправки данных через ajax, в качестве параметра передается id формы и id элемента для записи ответа (без #). В данном случаи «form1» и «msg». event.preventDefault() — отменяет стандартное поведение. 

Строки 23-45 функция sendAjaxForm отправляет данные формы (form1).

 

{% extends "base.html" %}
{% block content %}
    <form action="/send" id="form1" method="post">
        {{ form.csrf_token }}
        {{ form.name.label }} {{ form.name() }}
        {{ form.phone.label }} {{ form.phone() }}

        <input type="submit" value="Отправить"/>
    </form>
    <div id="msg"></div>
    <script>
        /* переопределить поведение кнопки "Отправить" */
        $(document).ready(function () {
            $("#form1" ).submit(function( event ) {
              sendAjaxForm("form1", "msg");
              event.preventDefault();
            });
        });


        /* отправка формы через ajax */
        function sendAjaxForm(form_ajax, msg) {
            var form = $("#" + form_ajax);
            $.ajax({
                type: form.attr('method'),
                url: form.attr('action'),
                data: form.serialize(),
                success: function (response) {
                    var json = jQuery.parseJSON(response);
                    $('#' + msg).html(json.msg);
                    if (json.success == 'true') {
                        form.trigger('reset');
                    }
                    else
                    {
                        alert("Что-то пошло не так!");
                        console.log("Ошибка");
                    }
                },
                error: function (error) {
                    console.log(error);
                }
            });
        }
    </script>
{% endblock %}

Подробней про функцию sendAjaxForm.

Что бы код можно было использовать для различных форм, я сделал передачу параметром формы и сообщения.

Итак, строка 23 просто получаем объект формы по id (#frim1).

Строка 25 type должен быть GET или POST это метод, беру его из параметров формы (method).

Аналогична строка 26 url должен содержать адрес для обработки запроса, в данном случаи так же берётся из формы (action). (её обрабатывает на сервере функция send см. файл app.py строка 22).

Строка 27 данные формы готовятся к отправки на сервер.

Строки 28 если всё прошло успешно, получен ответ от сервера он будет в response.

Строка 29 преобразую данные в объект json для удобства работы.

Строка 30 вывожу ответ в div с id=msg.

Строки 31-33 — успех, очищаю форму.

Строки 34-38 — что то не так, ошибка.

Строки 40-42 — если ошибки в запросе на сервер, нет связи, ошибка в url и т. д.

С HTML и JavaScript всё 🙂

Форма Flask файл forms.py

Строки 8-9 параметр DataRequired говорит о том, что они обязательны для заполнения. Length — размер не меньше 4 знаков.

В старых версиях wtf вместо StringField был TextField, сейчас лучше писать StringField. В версии 3.0 TextField перестанет работать.

from flask_wtf import FlaskForm
from wtforms import StringField
from wtforms.validators import DataRequired
from wtforms.validators import Length


class ContactForm(FlaskForm):
    name = StringField('Имя', validators=[DataRequired()])
    phone = StringField('Телефон', validators=[DataRequired(), Length(min=4)])

Файл app.py сам проект на Flask

Строка 6 форма обратной связи.

Строка 21 декоратор обработки url «/send».

Строка 22 функция отправки «сообщения».

Строка 24 если валидация успешна, возвращает json с положительным ответом (строка 27) иначе с отрицательным (строка 30).

from flask import Flask
from flask import request
from flask import render_template
import json
#  форма для обратной сзвязи
from forms import ContactForm

app = Flask(__name__)
app.config['SECRET_KEY'] = "12345"


@app.route('/', methods=['GET', 'POST'])
def index():
    form = ContactForm()

    return render_template("index.html",
                           title="index page",
                           form=form)


@app.route('/send', methods=['POST'])
def send():
    form = ContactForm()
    if request.method == "POST":
        if form.validate_on_submit():
            #  отправить почту, записать в БД и т. д.
            return json.dumps({'success': 'true', 'msg': 'Ждите звонка!'})
        else:
            #  обработать ошибку
            return json.dumps({'success': 'false', 'msg': 'Ошибка на сервере!'})


if __name__ == '__main__':
    app.run(debug=True)

Запуск app.py

python3 app.py 
 * Serving Flask app "app" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: on
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 334-159-801

Результат работы

Простое приложение Flask + AJAX + jQuery
Результат работы
Ввод данных  Flask + AJAX + jQuery
Ввод данных
Данные успешно отправлены
Данные успешно отправлены
Пример ошибки

Видео с примером работы Flask + jQuery + AJAX.

Исходники

Готовый пример можно скачать с github

git clone https://github.com/newivan/simply-ajax

Flask настройка reCAPTCHA от Google

Вездесущий спам — читал где то, что одно время 80% почтового трафика это был SPAM.

Сейчас уже почтового спама не так много доходит до нас (просто средства борьбы с ним стали лучше).

Я столкнулся со спамом в формах обратной связи на сайтах, да и вообще в формах которые на сайтах позволяют писать и отправлять сообщения. 

С годами, образовалось большое количество сайтов, и клиенты стали жаловаться на спам, конечно при разработке были сделаны простые защиты (скрытое поле и/или 2+2 и т. д. и т. п.) так же был опыт использования reCAPTCHA v2 — но были свои нюансы.

reCAPTCHA v2 считаю, что это хорошее решение. Да и ещё оно работает из коробки с flask-wtf примерно с 2017 года. Раньше нужно было «колхозить».

Решил написать для себя простой шаблон, ничего лишнего, форма обратной связи в контактах сайта защищенная reCAPTCHA. Мне легче использовать такую выжимку из проекта, чем копаться в большом готовом проекте с кучей библиотек. Здесь нечего лишнего, бери и используй.

Что понадобиться?

  1. Ключи от google публичный и приватный. (Я создал для хоста 127.0.0.1 возможно для теста подойдут они. А так нужно при создании указывать домен на котором будет сайт с капчей.)
  2. Flask и flask-wtf.

Структура проекта

.
├── app.py
├── forms.py
├── requirements.txt
└── templates
    ├── base.html
    └── index.html

Пошаговые действия

Создать виртуальное окружение для python, всё как обычно:

python3.7 -m venv venv
source venv/bin/activate
pip install -r requirements.txt

Содержимое requirements.txt

Flask==1.1.2
Flask-WTF==0.14.3

Создаю директорию для шаблонов:

mkdir templates

Создаю файл base.html (в директории templates) следующего вида, строки 8,9 содержат блок который будет расширен содержимым файла index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Flask RecaptchaField simple - {{ title }}</title>
</head>
<body>
{% block content %}
{% endblock content %}
</body>
</html>

Содержимое файла index.html (в директории templates). Строка 1 указывает какой шаблон будет расширен текущим. Строка 5 будет содержать сообщение об успехе или провале валидации. Собственно в 11 строке содержится recaptcha.

{% extends "base.html" %}
{% block content %}

    {% if msg %}
	Результат: {{ msg }}	
    {% endif %}
    
    <form action="/" method="post">
        {{ form.csrf_token }}	
        {{ form.text.label }} {{ form.text(size=20) }}
        {{ form.recaptcha }}
        <input type="submit" value="Отправить">
    </form>

{% endblock %}

Следующий файл forms.py (корень проекта). Строка 8 содержит поле комментарий DataRequired обозначает обязательно для заполнения. Строка 9 recaptcha поле с «галочкой».

from flask_wtf import FlaskForm
from flask_wtf import RecaptchaField
from wtforms import TextField
from wtforms.validators import DataRequired


class ContactForm(FlaskForm):
    text = TextField('Комментарий', validators=[DataRequired()])
    recaptcha = RecaptchaField()

И наконец самый главный файл приложения app.py (корень проекта).

Строка 5 — ранее описанная форма.

Строка 10, 11 — ключи которые нужно получить в google используя аккаунт google.

Строка 16 — форма для передачи в шаблон и проверки валидности.

Строка 18 — если метод POST значит была нажата в форме кнопка submit и соответственно нужно проверить полученные данные на валидность.

Строка 19 — проверка данных на валидность.

Если валидация пройдена, то msg примет значение «Успех!» иначе «Ошибка валидации».

from flask import Flask
from flask import request
from flask import render_template
#  форма с валидацией и капчей
from forms import ContactForm
app = Flask(__name__)
app.config['SECRET_KEY'] = "12345"

#  ключи recaptcha от google
app.config['RECAPTCHA_PUBLIC_KEY'] = "6Ld74-oUAAAAAJC0UOY6PtrOrNcxQ2VQCfGAqBOC"
app.config['RECAPTCHA_PRIVATE_KEY'] = "6Ld74-oUAAAAAD2_Jl2IVKh2uCCI9OPX_7oTdLz4"


@app.route('/', methods=['GET', 'POST'])
def index():
    form = ContactForm()
    msg = ""
    if request.method == "POST":
        if form.validate_on_submit():
            msg="Успех!"
            #  отправить почту, записать в БД
        else:
            msg="Ошибка валидации"
            #  обработать ошибку

    return render_template("index.html",
                           title="index page",
                           form=form,
                           msg=msg)


if __name__ == '__main__':
    app.run(debug=True)

Запуск проекта:

python3 app.py
Заполнили комментарий и нажали на галочку
Успешная валидация
Ошибка валидации
Пример работы reCAPTCHA с Flask

В видео видно, что если ошибиться несколько раз, будет показана капча и просто «галочкой» уже не отделаться.

В заключении

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

В новой версии flask-wtf появиться параметр RECAPTCHA_DISABLE  который позволит красиво отключать и включать проверку капчи.

А как быть, если нужно отключить проверку  капчи сейчас?

Вообще это одна из проблем, почему мне не нравилось использовал данную капчу, решение пока у меня такое, создавать две одинаковые формы с reCAPTCHA и без. Пример кода ниже forms.py и app.py.  Выделил строки которые поменялись.

Файл forms.py

from flask_wtf import FlaskForm
from flask_wtf import RecaptchaField
from wtforms import TextField
from wtforms.validators import DataRequired

class ContactForm(FlaskForm):
    text = TextField('Комментарий', validators=[DataRequired()])

class ContactRecaptchaForm(ContactForm):
    recaptcha = RecaptchaField()

Файл app.py

from flask import Flask
from flask import request
from flask import render_template
app = Flask(__name__)
app.config['SECRET_KEY'] = "12345"

#  ключи recaptcha от google
app.config['RECAPTCHA_PUBLIC_KEY'] = "6Ld74-oUAAAAAJC0UOY6PtrOrNcxQ2VQCfGAqBOC"
app.config['RECAPTCHA_PRIVATE_KEY'] = "6Ld74-oUAAAAAD2_Jl2IVKh2uCCI9OPX_7oTdLz4"
app.config['RECAPTCHA_DISABLE'] = True #  будет капча или нет

#  форма с валидацией и капчей или без неё.
#  в новой версии flask-wtf планируется сделать RECAPTCHA_DISABLE красиво
#  из коробки, это временное решение
if app.config['RECAPTCHA_DISABLE'] == True:
    from forms import ContactRecaptchaForm as ContactForm
else:
    from forms import ContactForm


@app.route('/', methods=['GET', 'POST'])
def index():
    form = ContactForm()
    msg = ""
    if request.method == "POST":
        if form.validate_on_submit():
            msg="Успех!"
            #  отправить почту, записать в БД
        else:
            msg="Ошибка валидации"
            #  обработать ошибку

    return render_template("index.html",
                           title="index page",
                           form=form,
                           msg=msg)


if __name__ == '__main__':
    app.run(debug=True)

Готовый код с примером Flask + reCAPTCHA v2 Google

Можно скачать его тут.

git clone https://github.com/newivan/flask-simple-captcha

Flask и компрессия CSS, JavaScript

Для оптимизации css и JavaScript я использую Flask-Static-Compress.

pip install flask-static-compress
    from flask_static_compress import FlaskStaticCompress
    app = Flask(__name__)
    compress = FlaskStaticCompress(app)
from flask_static_compress import FlaskStaticCompress
#skip code
compress = FlaskStaticCompress()
# skip code
def create_app(config_name):
    app = Flask(__name__)
    # skip code
    compress.init_app(app)
 # skip code

Оно реально стоит того? В моём случаи да. Пусть даже если посещаемость сайта не 10к в минуту, мне нравиться,  что я отдаю css и js минимального размера. Google page speed рад этому.

Важнейший для меня момент: после редактирования файла css (js) он часто продолжает грузиться из кэша, что мешает, и добавляет проблем при обновление сайта, приходилось переименовывать вручную, так же править путь в шаблоне, использование Flask-Static-Compress решает этот вопрос. Короче это удобно когда сайт уже в интернете.
Использовать очень просто:

{% compress 'css' %}
    <link rel="stylesheet" type="text/css" media="all" href="{{ url_for('static', filename='css/style.css') }}"/>
{% endcompress %} 
{% compress 'js' %}
    <script type="text/javascript" src="{{ url_for('static', filename='js/myapp.js') }}"></script>
{% endcompress %} 

В результате будет, что то вроде этого

    <link type="text/css" rel="stylesheet" href="/static/sdist/ed3117165c9910028aec4d167077a78d.css">

Готовые файлы будут в директории /static/sdist/ лучше добавить её в .gitignore

app/static/sdist

Динамическая подгруздка select (ComboBox).

Хочу поделится как я считаю хорошим решением динамической подгруздки в select.
Рассмотрю пример на категория + под категория. При выборе категории будут подгружается под категории.
Использовать для этого буду Flask, AJAX, jQuery, SQLite.
Подгружаться данные будут с помощью AJAX и Json.
Для краткости python код будет в app.py, а шаблон в template/index.html
Структура проекта:

app.py
template/index.html

Библиотеки которые должны стоять:

Flask==0.11
Flask-SQLAlchemy==2.1
Flask-WTF==0.12
Jinja2==2.8
MarkupSafe==0.23
SQLAlchemy==1.0.13
WTForm==1.0
WTForms==2.1
Werkzeug==0.11.10

Для начало необходимо создать минимальное приложение которое отображает шаблон.

from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
def index():
return render_template('index.html')

if __name__ == '__main__':
app.run(debug=True)

Далее создаём index.html в директории templates:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Flask-select</title>
</head>
<body>
    Hello!
</body>
</html>

Запускам:

$ python app.py
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
...

Если всё завилось то продолжаем.
Необходимо где то хранить список категорий и под категорий. Для этого подойдет база SQLite. Работать с ней будем через SQLAlchemy.
Отредактируем app.py и приведём его к виду:

from flask import Flask, render_template
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import String
from sqlalchemy import Integer
from sqlalchemy import Column
from sqlalchemy import ForeignKey
app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///data.sqlite'

db = SQLAlchemy(app)


#  категория
class Category(db.Model):
    __tablename__ = 'category'
    id = Column(Integer, primary_key=True)
    name = Column(String(1024))

    def __repr__(self):
        return '<Category %s>' % self.name

    def __unicode__(self):
        return self.name


#  под категория
class SubCategory(db.Model):
    __tablename__ = 'sub_category'
    id = Column(Integer, primary_key=True)
    name = Column(String(1024))
    category_id = db.Column(Integer, ForeignKey('category.id'))
    category = db.relationship("Category", backref="Ctegory.id")

    def __repr__(self):
        return '<SubCategory %s>' % self.name

    def __unicode__(self):
        return self.name

#  создание таблиц
db.create_all()
#  заполнение базы
#  если записи отсутствуют
if len(Category.query.all()) is 0:
    for name in ['фрукты', 'напитки', 'молочные продукты']:
        category = Category(name=name)
        db.session.add(category)
    db.session.commit()

if len(SubCategory.query.all()) is 0:
    for name in ['апельсины', 'яблоки', 'груши']:
        sub_category = SubCategory(category_id=1, name=name)
        db.session.add(sub_category)

    for name in ['сок', 'вода', 'газировка']:
        sub_category = SubCategory(category_id=2, name=name)
        db.session.add(sub_category)

    for name in ['молоко', 'сметана', 'масло']:
        sub_category = SubCategory(category_id=3, name=name)
        db.session.add(sub_category)
    db.session.commit()


@app.route('/')
def index():
    return render_template('index.html')

if __name__ == '__main__':
    app.run(debug=True)

Это только создаст БД и таблицы в ней. Ещё нам понадобится сама форма для отображения выбора. Здесь я решил не использовать Bootstrap и обойтись простой формой.
В тело index.html необходимо добавить следующий код:

    ...
    Пример работы выбора категории и под категории.
    <form method="post" name="form" action="/">
        {{ form.csrf_token }}
        {{ form.category.label }} 
        {{ form.category }}
        {{ form.sub_category.label }} 
        {{ form.sub_category }}
        <input type="submit" value="Отправить">
    </form>
    ...

Для отображения формы необходимо добавить в app.py импорт библиотек и сам класс формы.

...
from flask_wtf import Form
from wtforms import SelectField
...
class FormCategory(Form):
    category = SelectField(u'Категория', coerce=int)
    sub_category = SelectField(u'Под категория', coerce=int)

    def __init__(self, *args, **kwargs):
        super(FormCategory, self).__init__(*args, **kwargs)
        self.category.choices = \
            [(g.id, u"%s" % g.name) for g in Category.query.order_by('name')]
        #  выбранное поле по умолчанию
        self.category.choices.insert(0, (0, u"Не выбрана"))

        self.sub_category.choices = list()
        #  выбранное поле по умолчанию
        self.sub_category.choices.insert(0, (0, u"Не выбрана"))


@app.route('/')
def index():
    form = FormCategory()
    return render_template('index.html',
                           form=form
                           )
...

Страница должна выглядеть примерно так:

Вид страници
Примерный вид страницы

Необходимо заблокировать выбор под категории пока не выбрана категория. Для этого будем менять свойства sub_category через jQuery.
Подключим её добавив между head в файле index.html строку:

...
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
...

После нашей формы необходимо добавить JavaScript который будет менять свойства sub_category, загружать под категории посредствам Ajax.

...
    <script type="text/javascript">
        function choice_category(){
            var tmp_id = parseInt ($("#category").val());
            if(tmp_id == 0)
            {
                $("#sub_category").attr('disabled', 'disabled');
            }
            else
            {
                $("#sub_category").removeAttr('disabled');
                load_subcategory();
            }
        }

        function load_subcategory(){
            $.ajax({
                type: "POST",
                url: "/get_sub_category",
                data: $('form').serialize(),
                success: function(response) {
                    var json = jQuery.parseJSON(response)
                    obj = Object.keys(json)

                    $("#sub_category")
                        .find('option')
                        .remove()
                        .end()
                        .append('<option value="0">Не выбрано</option>')
                        .val('0');

                    var value, key;
                    for(item in obj){
                        value = json[obj[item]];
                        key = obj[item];
                    $("#sub_category").append($("<option></option>")
                            .attr("value",key)
                            .text(value)); 
                    }
                
                },
            error: function(error) {
                console.log(error);
            }
        });
        }

        $(document).ready(function() {
            choice_category();
            $("#category").change(function() {
                choice_category();
            });

            $("#sub_category").change(function() {
            });
        });
    </script>
...

Но и это ещё не всё! Через Ajax мы обращаемся к get_sub_category для которого так же необходимо добавить обработку в app.py и импорт библиотек.

...
from flask import request
from flask import json
...
@app.route('/get_sub_category', methods=('GET', 'POST'))
def get_sub_category():
    category_id = request.form['category']
    item_list = SubCategory.query.filter_by(category_id=category_id).all()
    result_list = dict()
    for item in item_list:
        result_list[item.id] = item.name
    return json.dumps(result_list)
...

Теперь наконец всё должно заработать.
В самом простом случаи можно обойтись вообще без FormCategory из листинга 5 но тогда всё равно придется передавить список категорий, а в самом шаблоне добавится цикл для заполнения категории. Валидацию в таком случаи будет проблематичней сделать, по этому считаю данный вариант оптимальным. Так, что не слушайте людей которые говорят, что форма для 2-х элементов не нужна и всё проще и быстрей сделать просто через input и label.
Вообще для данной формы идеально бы подошел вариант с wtf.quick_form, если ваш сайт использует Bootstrap.

Готовый исходный код можно взять с github

git clone https://github.com/newivan/flask_select.git

AJAX вместе с Flask

В примере я использую Python3, если у вас Python2 добавьте в начало файла app.py строку:

# -*- coding: utf-8 -*- 

Для начала понадобится Flask. Ниже показано как установить его в виртуальное окружение.

$ mkdir flask_simple_ajax
$ virtualenv --system-site-packages --python=/usr/bin/python3 env
$ source env/bin/activate
$ pip install flask

В корне flask_simple_ajax создайте файл app.py с серверным кодом:

from flask import Flask, render_template, json, request
app = Flask(__name__)

@app.route('/')
def index():
    return render_template('index.html')


@app.route('/get_len', methods=['GET', 'POST'])
def get_len():
    name = request.form['name'];
    return json.dumps({'len': len(name)})


if __name__ == '__main__':
    app.run(debug=True)

Далее нужно создать директории template, static/js:

$ mkdir templates
$ mkdir -p static/js

Скачайте с сайта jQuery библиотеку и положите её в static/js.

В template создайте файл index.html со следующим содержимым:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>AJAX вместе с Flask</title>
    <script type=text/javascript src="{{ url_for('static', filename='js/jquery-2.2.2.min.js') }}"></script>
</head>
<body>
    <script>
        function get_len() {
            $.ajax({
                type: "POST",
                url: "/get_len",
                data: $('form').serialize(),
                type: 'POST',
                success: function(response) {
                    var json = jQuery.parseJSON(response)
                    $('#len').html(json.len)
                    console.log(response);
                },
                error: function(error) {
                    console.log(error);
                }
            });
        }
    </script>
    <form action="/get_len" method="post" name="form">
        <label for="name">Введите текст:</label>
        <input id="name" name="name" type="text">
        <input type="button" value="Отправить" onclick="get_len();">
    </form>
    <div id="len"></div>
</body>
</html>

Не забудьте поправить в строке 6 версию jquery на которую скачали ранее. Можно было ничего не качать и вместо 6-й строки написать:

<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.2/jquery.min.js"></script>

Но в некоторых случаях лучше, что бы всё работало автономно.

Запускаем:

$ python3 app.py

Результат можно посмотреть в браузере по адресу http://localhost:5000/

Пример работа AJAX и Flask
Пример работы AJAX и Flask

Данный пример прост для понимания, вы быстро разберётесь и сожмите добавлять AJAX в свои проекты.
Готовые исходные кода можно взять на github

Flask передача параметра в redirect

Если необходимо передать параметр из одного route в другой, можно воспользоваться следующим способом.


@order.route("/order", methods=['GET', 'POST'])
def view_order():
    #  ....
    return redirect(url_for('.view_ready_order', order_id=order_id))

@order.route("/ready/<int:order_id>", methods=['GET', 'POST'])
def view_ready_order(order_id):
    return render_template('ready.html', title=u'Заказ оформлен', order_id=order_id)

В данном примере я передаю order_id для показа его в шаблоне.