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

Python3 установка pip пакетов offline

Бывает необходимо установить пакеты для python на машине без интернета. Способов много, я выбрал для себя один, его и использую. «Легко» и «просто» можно скачать необходимые пакеты вместе с зависимостями и установить на другой машине, если сделать следующие.

На машине с интернетом

python3 -m venv vevn
source venv/bin/active
pip install pip --upgrade
# директория для скачивания пакетов
mkdir pkg
cd pkg
# отдельно скачиваю последнею версию pip
pip download pip
# скачиваю необходимые пакеты с зависимостями
pip download -r ../requirements.txt

На машине без интернета

python3 -m venv vevn
source venv/bin/active
# устанавливаю ранее скаченный pip (версия может быть другая)
pip install pkg/pip-20.1-py2.py3-none-any.whl
# установка пакетов из списка requirements.txt, пакеты должны лежать в pkg (директория)
pip install --no-index --find-links pkg -r requirements.txt

Результат выполнения

# вывод консоли у меня
pip install --no-index --find-links pkg -r requirements.txt 
Looking in links: pkg
Processing ./pkg/Flask-1.1.2-py2.py3-none-any.whl
Processing ./pkg/Flask_WTF-0.14.3-py2.py3-none-any.whl
Processing ./pkg/et_xmlfile-1.0.1.tar.gz
Processing ./pkg/openpyxl-3.0.3.tar.gz
Processing ./pkg/jdcal-1.4.1-py2.py3-none-any.whl
Processing ./pkg/pylint-2.5.2-py3-none-any.whl
Processing ./pkg/itsdangerous-1.1.0-py2.py3-none-any.whl
Processing ./pkg/Jinja2-2.11.2-py2.py3-none-any.whl
Processing ./pkg/Werkzeug-1.0.1-py2.py3-none-any.whl
Processing ./pkg/click-7.1.2-py2.py3-none-any.whl
Processing ./pkg/WTForms-2.3.1-py2.py3-none-any.whl
Processing ./pkg/mccabe-0.6.1-py2.py3-none-any.whl
Processing ./pkg/isort-4.3.21-py2.py3-none-any.whl
Processing ./pkg/toml-0.10.1-py2.py3-none-any.whl
Processing ./pkg/astroid-2.4.1-py3-none-any.whl
Processing ./pkg/MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl
Processing ./pkg/typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl
Processing ./pkg/wrapt-1.12.1.tar.gz
Processing ./pkg/lazy_object_proxy-1.4.3-cp37-cp37m-manylinux1_x86_64.whl
Processing ./pkg/six-1.14.0-py2.py3-none-any.whl
Could not build wheels for et-xmlfile, since package 'wheel' is not installed.
Could not build wheels for openpyxl, since package 'wheel' is not installed.
Could not build wheels for wrapt, since package 'wheel' is not installed.
Installing collected packages: itsdangerous, MarkupSafe, Jinja2, Werkzeug, click, Flask, WTForms, Flask-WTF, et-xmlfile, jdcal, openpyxl, mccabe, isort, toml, typed-ast, wrapt, lazy-object-proxy, six, astroid, pylint
    Running setup.py install for et-xmlfile ... done
    Running setup.py install for openpyxl ... done
    Running setup.py install for wrapt ... done
Successfully installed Flask-1.1.2 Flask-WTF-0.14.3 Jinja2-2.11.2 MarkupSafe-1.1.1 WTForms-2.3.1 Werkzeug-1.0.1 astroid-2.4.1 click-7.1.2 et-xmlfile-1.0.1 isort-4.3.21 itsdangerous-1.1.0 jdcal-1.4.1 lazy-object-proxy-1.4.3 mccabe-0.6.1 openpyxl-3.0.3 pylint-2.5.2 six-1.14.0 toml-0.10.1 typed-ast-1.4.1 wrapt-1.12.1

Мой файл requirements.txt для примера

Flask==1.1.2
Flask-WTF==0.14.3
et-xmlfile==1.0.1
openpyxl==3.0.3
jdcal==1.4.1
pylint==2.5.2

Установка из tar.gz или whl

# установка одного пакета из архива
pip install ./pkg-name.tar.gz

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

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.