- PVSM.RU - https://www.pvsm.ru -

Проектирование сложных приложений в Flask

Данная статья, размещенная [1] в репозитории Flask на GitHub, является плодом коллективного творчества небезразличных программистов, а изначальный её автор — Brice Leroy [2]. Она представляет собой достаточно полезный для начинающих материал по Flask. Лично для меня он стал ответом на многие простые вопросы, основным из которых был «как структурировать проект».

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

Описанный пример протестирован на Python 3.5, Flask 0.10, Flask-SQLAlchemy 2.1, Flask-WTG 0.9.

Проектирование сложных приложений в Flask

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

Данная статья является попыткой описать структуру большого проекта, использующего Flask и базовые модули SQLAlchemy и WTForms.

Установка

Flask

Инуструкция [3] по установке Flask.

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

Flask-SQLAlchemy

SQLAlchemy обеспечивает простой и мощный интерфейс взаимодействия ваших объектов и реляционной базы данных любого типа. Для установки Flask-SQLAlchemy в ваше виртуальное окружение используйте pip:

pip install flask-sqlalchemy

Более полное описание [4] пакета Flask-SQLAlchemy.

Flask-WTF

WTForms упрощает получение данных от пользователя.

pip install Flask-WTF

Более полное описание [5] пакета Flask-WTF.

Введение

Итак, необходимые библиотеки подготовлены. Так должна выглядеть основная структура вашего проекта:

/app/users/__init__.py
/app/users/views.py
/app/users/forms.py
/app/users/constants.py
/app/users/models.py
/app/users/decorators.py

Для каждего модуля (элемента приложения) создаётся следующая структура:

/app/templates/404.html
/app/templates/base.html
/app/templates/users/login.html
/app/templates/users/register.html
...

Шаблоны представления (jinja) хранятся в директории templates и поддиректория модулей:

/app/static/js/main.js
/app/static/css/reset.css
/app/static/img/header.png

Для обработки неизменяемых файлов необходимо использовать отдельный веб-сервер, однако на время разработки можно возложить эту работу на Flask. Он автоматически выдает такие файлы из директории static, а для настройки использования другой директории вы можете воспользоваться информацией из данной статьи [6].

Для рассматриваемого приложения будут создан один модуль: users. Он обеспечит управление регистрацией и входом пользователей, просмотр данных своего профайла.

Конфигурация

/run.py используется для запуска веб-сервера:

    from app import app
    app.run(debug=True)

/shell.py даст доступ к консоли с возможностью выполнения команд. Возможно, не так удобно, как отладка через pdb, но достаточно полезно (по крайней мере при инициализации базы данных):

    #!/usr/bin/env python
    import os
    import readline
    from pprint import pprint

    from flask import *
    from app import *

    os.environ['PYTHONINSPECT'] = 'True'

Примечание переводчика:
В случае, если вы работаете в ОС Windows (не надо бросать кирпичи!), библиотека readline недоступна. В таком случае необходимо установить в своё виртуальное или реальное окружение python библиотеку pyreadline и обернуть импорт в конструкцию вида:

try:
    import readline
except:
    import pyreadline

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

/config.py хранит всю конфигурацию приложения. В данном примере в качестве базы данных используется SQLite, так как она очень удобна при разработке. Скорее всего файл /config.py не стоит включать в репозиторий, так как он будет разным на тестовой и промышленной системах.

    import os
    _basedir = os.path.abspath(os.path.dirname(__file__))

    DEBUG = False

    ADMINS = frozenset(['youremail@yourdomain.com'])
    SECRET_KEY = 'This string will be replaced with a proper key in production.'

    SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(_basedir, 'app.db')
    DATABASE_CONNECT_OPTIONS = {}

    THREADS_PER_PAGE = 8

    WTF_CSRF_ENABLED = True
    WTF_CSRF_SECRET_KEY = "somethingimpossibletoguess"

    RECAPTCHA_USE_SSL = False
    RECAPTCHA_PUBLIC_KEY = '6LeYIbsSAAAAACRPIllxA7wvXjIE411PfdB2gt2J'
    RECAPTCHA_PRIVATE_KEY = '6LeYIbsSAAAAAJezaIq3Ft_hSTo0YtyeFG-JgRtu'
    RECAPTCHA_OPTIONS = {'theme': 'white'}

  • _basedir — переменная, в которую помещается исполняемая директория скрипта;
  • DEBUG определяет появление сообщений об ошибках в тестовом окружении;
  • SECRET_KEY используется для подписи cookies, при его изменении пользователям потребуется логиниться заново;
  • ADMINS содержит адрес электронной почты администраторов для рассылок из приложения;
  • SQLALCHEMY_DATABASE_URI и DATABASE_CONNECT_OPTIONS, как несложно догадаться — опции подключения SQLAlchemy;
  • THREADS_PER_PAGE, как мне кажется, ставил 2 на ядро… Могу ошибаться;
  • WTF_CSRF_ENABLED и WTF_CSRF_SECRET_KEY защищают от подмены POST-сообщений;
  • RECAPTCHA_* используется для входящего в WTForms поля RecaptchaField. Получить приватный и публичный ключи можно на сайте **recaptcha.

Модуль

Настроим модуль users в следующем порядке: определим модели, связанные с моделями константы, далее форму и, наконец, представление и шаблоны.

Модель

/app/users/models.py:

    from app import db
    from app.users import constants as USER

    class User(db.Model):

        __tablename__ = 'users_user'
        id = db.Column(db.Integer, primary_key=True)
        name = db.Column(db.String(50), unique=True)
        email = db.Column(db.String(120), unique=True)
        password = db.Column(db.String(120))
        role = db.Column(db.SmallInteger, default=USER.USER)
        status = db.Column(db.SmallInteger, default=USER.NEW)

        def __init__(self, name=None, email=None, password=None):
          self.name = name
          self.email = email
          self.password = password

        def getStatus(self):
          return USER.STATUS[self.status]

        def getRole(self):
          return USER.ROLE[self.role]

        def __repr__(self):
            return '<User %r>' % (self.name)

И её константы в файле /app/users/constants.py:

    # User role
    ADMIN = 0
    STAFF = 1
    USER = 2
    ROLE = {
      ADMIN: 'admin',
      STAFF: 'staff',
      USER: 'user',
    }

    # user status
    INACTIVE = 0
    NEW = 1
    ACTIVE = 2
    STATUS = {
      INACTIVE: 'inactive',
      NEW: 'new',
      ACTIVE: 'active',
    }

К слову о константах: мне нравится, когда константы хранятся в отдельном файле внутри модуля. Константы скорее всего будут использоваться в моделях, формах и представлениях, так что таким образом вы получите удобно организованные данные, которые будет просто найти. К тому же, импортирование констант под именем модуля в верхнем регистре (например USERS для users.constants) поможет избежать конфликтов имен.

Форма

Когда создана модель нужного объекта, необходимо сконструировать форму для работы с ней.

Форма регистрации будет запрашивать имя пользователя, адрес электронной почты и пароль, будут использованы валидаторы для проверки корректности введенных пользователем данных, а поле Recaptcha защитит от регистрации ботов. На случай, если понадобится внедрить пользовательское соглашение, также добавлено поле BooleanField с именем accept_tos. Данное поле помечено, как required, то есть пользователь будет обязан отметить генерируемый формой чекбокс. Форма входа снабжена полями email и password с аналогичными валидаторами.

Описание форм содержится в файле /app/users/forms.py:

    from flask.ext.wtf import Form, RecaptchaField
    from wtforms import TextField, PasswordField, BooleanField
    from wtforms.validators import Required, EqualTo, Email

    class LoginForm(Form):
      email = TextField('Email address', [Required(), Email()])
      password = PasswordField('Password', [Required()])

    class RegisterForm(Form):
      name = TextField('NickName', [Required()])
      email = TextField('Email address', [Required(), Email()])
      password = PasswordField('Password', [Required()])
      confirm = PasswordField('Repeat Password', [
          Required(),
          EqualTo('password', message='Passwords must match')
          ])
      accept_tos = BooleanField('I accept the TOS', [Required()])
      recaptcha = RecaptchaField()

Первый параметр для каждого поля — его метка, например для поля name в форме задана метка NickName. Для полей ввода пароля используется валидатор EqualTo, сравнивающий данные в двух полях.

Более полная информация о возможностях WTForms находится по этой ссылке [7].

Представление

В представлении объявляется Blueprint — объект схемы модуля, в свойствах которого указывается url_prefix, который будет подставляться в начале любого URLа, указанного в route. Также в представлении используется метод формы form.validate_on_submit, выдающий истину для метода HTTP POST и валидной формы. После успешного входа пользователь перенаправляется на страницу профиля (/users/me). Для предотвращения доступа неавторизованных пользователей создаётся специальный декоратор в файле /app/users/decorators.py:

    from functools import wraps

    from flask import g, flash, redirect, url_for, request

    def requires_login(f):
      @wraps(f)
      def decorated_function(*args, **kwargs):
        if g.user is None:
          flash(u'You need to be signed in for this page.')
          return redirect(url_for('users.login', next=request.path))
        return f(*args, **kwargs)
      return decorated_function

Данный декоратор проверяет наличие данных в переменной g.user. В случае, если переменная не задана, то пользователь не аутенфицирован, тогда задаётся информационное сообщение и осуществляется перенаправление на представление login (вход в систему). Данные в переменную g.user помещаются в функции before_request. Когда вы получаете большой объем данных из профиля пользователя (исторические данные, друзья, сообщения, действия) возможно серьезное замедление работы при обращении к БД, так что кеширование данных пользователей может решить эту проблему (но только пока вы модифицируете объекты централизованно и очищаете кеш при каждом обновлении). Ниже прилагается код представления /app/users/views.py:

    from flask import Blueprint, request, render_template, flash, g, session, redirect, url_for
    from werkzeug import check_password_hash, generate_password_hash

    from app import db
    from app.users.forms import RegisterForm, LoginForm
    from app.users.models import User
    from app.users.decorators import requires_login

    mod = Blueprint('users', __name__, url_prefix='/users')

    @mod.route('/me/')
    @requires_login
    def home():
      return render_template("users/profile.html", user=g.user)

    @mod.before_request
    def before_request():
      """
      pull user's profile from the database before every request are treated
      """
      g.user = None
      if 'user_id' in session:
        g.user = User.query.get(session['user_id'])

    @mod.route('/login/', methods=['GET', 'POST'])
    def login():
      """
      Login form
      """
      form = LoginForm(request.form)
      # make sure data are valid, but doesn't validate password is right
      if form.validate_on_submit():
        user = User.query.filter_by(email=form.email.data).first()
        # we use werzeug to validate user's password
        if user and check_password_hash(user.password, form.password.data):
          # the session can't be modified as it's signed, 
          # it's a safe place to store the user id
          session['user_id'] = user.id
          flash('Welcome %s' % user.name)
          return redirect(url_for('users.home'))
        flash('Wrong email or password', 'error-message')
      return render_template("users/login.html", form=form)

    @mod.route('/register/', methods=['GET', 'POST'])
    def register():
      """
      Registration Form
      """
      form = RegisterForm(request.form)
      if form.validate_on_submit():
        # create an user instance not yet stored in the database
        user = User(name=form.name.data, email=form.email.data, 
          password=generate_password_hash(form.password.data))
        # Insert the record in our database and commit it
        db.session.add(user)
        db.session.commit()

        # Log the user in, as he now has an id
        session['user_id'] = user.id

        # flash will display a message to the user
        flash('Thanks for registering')
        # redirect user to the 'home' method of the user module.
        return redirect(url_for('users.home'))
      return render_template("users/register.html", form=form)

Шаблон

Шаблонизатор Jinja встроен в Flask. Одним из его преимуществ является возможность наследования и встроенной логики (зависимости, циклы, контекстные изменения). Создадим шаблон /app/templates/base.html, от которого будут наследоваться остальные шаблоны. Возможно задание более чем одного наследования (например наследование от шаблона twocolumn.html, который в свою очередь наслудется от main.html). Базовый шаблон также упрощает отображение информационных (flash) сообщений из переменной get_flashed_messages в каждом наследующем шаблоне.

Теперь нет необходимости задавать основную структуру страницы и каждое изменение base.html отразится на наследующих шаблонах. Рекомендуется называть шаблоны в соответствии с вызывающими их представлениями, именно так поименован шаблон /app/templates/users/register.html:

    <html>
      <head>
        <title>{% block title %}My Site{% endblock %}</title>
        {% block css %}
        <link rel="stylesheet" href="/static/css/reset-min.css" />
        <link rel="stylesheet" href="/static/css/main.css" />
        {% endblock %}
        {% block script %}
        <script src="/static/js/main.js" type="text/javascript"></script>
        {% endblock %}
      </head>
      <body>
        <div id="header">{% block header %}{% endblock %}</div>
        <div id="messages-wrap">
          <div id="messages">
            {% for category, msg in get_flashed_messages(with_categories=true) %}
              <p class="message flash-{{ category }}">{{ msg }}</p>
            {% endfor %}
          </div>
        </div>
        <div id="content">{% block content %}{% endblock %}</div>
        <div id="footer">{% block footer %}{% endblock %}</div>
      </body>
    </html>

И шаблон /app/templates/users/login.html:

    {% extends "base.html" %}
    {% block content %}
      {% from "forms/macros.html" import render_field %}
      <form method="POST" action="." class="form">
        {{ form.csrf_token }}
        {{ render_field(form.email, class="input text") }}
        {{ render_field(form.password, class="input text") }}
        <input type="submit" value="Login" class="button green">
      </form>
      <a href="{{ url_for('users.register') }}">Register</a>
    {% endblock %}

Созданные шаблоны используют макросы для автоматизации создания полей html. Так как этот макрос будет использоваться в различных модулях, он помещен в отдельный файл /app/templates/forms/macros.html:

    {% macro render_field(field) %}
        <div class="form_field">
        {{ field.label(class="label") }}
        {% if field.errors %}
            {% set css_class = 'has_error ' + kwargs.pop('class', '') %}
            {{ field(class=css_class, **kwargs) }}
            <ul class="errors">{% for error in field.errors %}<li>{{ error|e }}</li>{% endfor %}</ul>
        {% else %}
            {{ field(**kwargs) }}
        {% endif %}
        </div>
    {% endmacro %}

Наконец, создан примитивный шаблон /app/templates/users/profile.html:

   {% extends "base.html" %}
   {% block content %}
     Hi {{ user.name }}!
   {% endblock %}

Инициализация приложения

Как несложно догадаться, инициализация приложения происходит в файле /app/init.py:

    import os
    import sys

    from flask import Flask, render_template
    from flask.ext.sqlalchemy import SQLAlchemy

    app = Flask(__name__)
    app.config.from_object('config')

    db = SQLAlchemy(app)

    ########################
    # Configure Secret Key #
    ########################
    def install_secret_key(app, filename='secret_key'):
        """Configure the SECRET_KEY from a file
        in the instance directory.

        If the file does not exist, print instructions
        to create it from a shell with a random key,
        then exit.
        """
        filename = os.path.join(app.instance_path, filename)

        try:
            app.config['SECRET_KEY'] = open(filename, 'rb').read()
        except IOError:
            print('Error: No secret key. Create it with:')
            full_path = os.path.dirname(filename)
            if not os.path.isdir(full_path):
                print('mkdir -p {filename}'.format(filename=full_path))
            print('head -c 24 /dev/urandom > {filename}'.format(filename=filename))
            sys.exit(1)

    if not app.config['DEBUG']:
        install_secret_key(app)

    @app.errorhandler(404)
    def not_found(error):
        return render_template('404.html'), 404

    from app.users.views import mod as usersModule
    app.register_blueprint(usersModule)

    # Later on you'll import the other blueprints the same way:
    #from app.comments.views import mod as commentsModule
    #from app.posts.views import mod as postsModule
    #app.register_blueprint(commentsModule)
    #app.register_blueprint(postsModule)

Экземпляр БД SQLAlchemy и модель Users находятся в двух разных файлах, необходимо импортировать оба из них в общее пространство имен с помощью строки from app.users.views import mod as usersModule. В противном случае команда db.create_all() не принесет результата.

Активируем виртуальное окружение virtualenv и инициализируем БД:

user@Machine:~/Projects/dev$ . env/bin/activate
(env)user@Machine:~/Projects/dev$ python shell.py 
>>> from app import db
>>> db.create_all()
>>> exit()

Теперь можно выполнить команду python run.py и получить сообщение следующего вида:

(env)user@Machine:~/Projects/dev$ python run.py 
 * Running on http://127.0.0.1:5000/
 * Restarting with reloader

Открыв в браузере адрес http://127.0.0.1:500/users/me/ [8] вы будете перенаправлены на страницу входа и увидите ссылку на страницу регистрации.

Автор: FlashHaos

Источник [9]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/python/108927

Ссылки в тексте:

[1] размещенная: https://github.com/mitsuhiko/flask/wiki/Large-app-how-to

[2] Brice Leroy: http://bbrriiccee@gmail.com

[3] Инуструкция: http://flask.pocoo.org/docs/0.10/installation/

[4] Более полное описание: http://packages.python.org/Flask-SQLAlchemy/

[5] Более полное описание: https://pythonhosted.org/Flask-WTF/

[6] данной статьи: http://flask.pocoo.org/docs/0.10/api/#application-object

[7] по этой ссылке: http://wtforms.readthedocs.org/en/latest/

[8] http://127.0.0.1:500/users/me/: http://127.0.0.1:500/users/me/

[9] Источник: http://habrahabr.ru/post/275099/