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

SQLite3 создать dump базы данных и восстановить из него

Бывает необходимость, поработать с текстом в базе SQLite. Для этого я выгружаю в sql все таблицы с данными, провожу манипуляцию и заливаю обратно.

Ниже представлены команды для работы с дампом базы данных, создание и восстановление.

 

Создание и восстановление из дампа БД SQLite

Создать dump:

sqlite3 bd.sqlite .dump > dump.sql

Восстановить из dump:

sqlite3 new-bd.db < dump.sql

Работа с одной таблицей

Можно создать только dump определенной таблицы, в данном случаи user:

sqlite new-bd.db
sqlite> .output user.sql
sqlite> .dump user
sqlite> .exit

Отредактировать файл как нужно и залить таблицу обратно, удалив её перед этим из базы:

Удалить таблицу:

#  работа с базой
sqlite new-bd.db 
#  список таблиц
sqlite> .tables
DROP TABLE user;
sqlite> .exit

Восстановить одну таблицу:

sqlite3 new-bd.db < user.sql

Книга: Стандартная библиотека Python3 Справочник с примерами — Даг Хеллман 2-е издание

Книга: Стандартная библиотека Python 3 Справочник с примерами
Книга: Справочник с примерами.
Даг Хеллман «Стандартная библиотека Python 3» Диалектика, 2019 год, 1375 стр., 2-е издание

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

Книга огромная, читать (смотреть её) удобно только за столом.

Стандаартная библиотека Python3 Даг Хеллман

Описывает относительно современную версию языка Python3.6 и 3.7. Материал из книги будет актуален ещё несколько лет. Если 2.7 версию использовали столько лет (и будет ещё тянуть старые проекты) то приобретение данный книги, если вы в профессии оправданно.

Оглавление

Глава 1. Текст
Глава 2. Структуры данных
Глава 3. Алгоритмы
Глава 4. Дата и время
Глава 5. Математика
Глава 6. Файловая система
Глава 7. Постоянное хранение и обмен данными
Глава 8. Сжатие и архивирование данных
Глава 9. Криптография
Глава 10. Параллельные вычисления: процессы, потоки и сопрограммы
Глава 11. Обмен данными по сети
Глава 12. Интернет
Глава 13. Электронная почта
Глава 14. Строительные блоки приложений
Глава 15. Интернационализация и локализация приложений
Глава 16. Инструменты разработки
Глава 17. Инструменты среды времени выполнения
Глава 18. Инструменты языка
Глава 19. Модули и пакеты
Приложение А. Замечания относительно портирования программ
Приложение Б. Внешние ресурсы, дополняющие стандартную библиотеку
Указатель модулей Python
Предметный указатель

Пример страницы из книги
Пример страницы из книги

Книга CSS для профи — Кит Грант [2019]

Книга CSS для профи - Кит Грант
CSS для профи — Кит Грант [2019]

Хочу написать отзыв на книгу по CSS. Книга мне понравилась, купил в бумаге. Хорошо написана. В последнее время очень сильно WEB технологии разогнались. Но сейчас не об этом. Мне нужно было самостоятельно приступить к вёрстке крупного проекта. Информация которой я владел была неструктурированная, надёргана тут и там, в добавок устаревшая. Поверхностное владение Flex и Grid. В итоге решил купить книгу, что бы привести знания в порядок и использовать современный подход на сколько позволяет книга (информация в наше время очень быстро устаревает).

Книга написана легко, перевод меня устроил, читать её в удовольствие. Но и как все книги подобного рода, требуют сразу переменить полученные знания на практике. Описаны WebPack и адаптивная вёрстка, Flex и Grid, использование шрифтов и многое другое. Минимум воды, если есть отступления автора то они к месту.
Если CSS нужен по работе, определённо стоит обратить на данную книгу внимание.

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

Python3 virtualenv

Наконец для всех библиотек  на Python3.6+ появилось всё, что мне нужно, так же свежий Python 3.6+ и 3.7+ доступен на хостинге который я использую. Виртуальное окружение в новых «питонах» создаю так:

python3 -m venv venv

или

python3 -m venv venv -system-site-packages

Использовать точно так же:

 
source venv/bin/activate
pip install --upgrade  pip
pip install -r requirements.txt

Что бы меньше было проблем я всегда ставлю самую новую версию pip.

Динамическая подгруздка 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

Vim переключение раскладки

В Emacs мне нравится, что там есть встроенное переключение раскладки клавиатуры, и проблем с горячими клавишами нет, когда включен русский язык.
В Vim по умолчанию ничего нет и я долго мучился, когда нужно писать то на одном языке то на другом. Решение найдено на просторах интернета, запишу его сюда, что бы не потерять.
В .vimrc нужно добавить:

set keymap=russian-jcukenwin
set iminsert=0
set imsearch=0
highlight lCursor guifg=NONE guibg=Cyan

Теперь при включенной русской раскладки можно использовать сочетания клавиш и всё будет хорошо. Язык же нужно переключать нажатием сочетания Ctrl+^.

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