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

от автора

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

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

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

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

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

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

Установка

Flask

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

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

Flask-SQLAlchemy

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

pip install flask-sqlalchemy 

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

Flask-WTF

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

pip install Flask-WTF 

Более полное описание пакета 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, а для настройки использования другой директории вы можете воспользоваться информацией из данной статьи.

Для рассматриваемого приложения будут создан один модуль: 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 находится по этой ссылке.

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

В представлении объявляется 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/ вы будете перенаправлены на страницу входа и увидите ссылку на страницу регистрации.

ссылка на оригинал статьи http://habrahabr.ru/post/275099/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *