- PVSM.RU - https://www.pvsm.ru -
Данная статья, размещенная [1] в репозитории Flask на GitHub, является плодом коллективного творчества небезразличных программистов, а изначальный её автор — Brice Leroy [2]. Она представляет собой достаточно полезный для начинающих материал по Flask. Лично для меня он стал ответом на многие простые вопросы, основным из которых был «как структурировать проект».
Для хоть сколько-то опытных программистов она вряд ли будет полезна, многие могут вовсе не согласиться с описанными принципами, однако для находящихся на ранней стадии обучения она может стать толчком к развитию, как стала для меня. Именно поэтому я сделал перевод на русский язык — у этой статьи очень низкий порог вхождения и стоит сделать его еще ниже.
Описанный пример протестирован на Python 3.5, Flask 0.10, Flask-SQLAlchemy 2.1, Flask-WTG 0.9.
Этот документ не входит в официальную документацию Flask. Он является компиляцией советов, полученных из различных неофициальных источников и никогда не подвергался какой-либо проверке. Описанные методики могут быть весьма полезны, но в то же время и достаточно опасны. Просьба не вносить никаких изменений в оригинальный документ, размещенный на Github, так как на него ссылаются многие ответы на StackOverflow. Вы можете вносить в него любые поправки и заметки, но для размещения используйте личный сайт или блог.
Данная статья является попыткой описать структуру большого проекта, использующего Flask и базовые модули SQLAlchemy и WTForms.
Инуструкция [3] по установке Flask.
Я рекомендую использовать virtualenv — эта система очень проста и позволяет размещать несколько виртуальных окружений на одной системе и не требует прав суперпользователя, так как все библиотеки устанавливаются локально.
SQLAlchemy обеспечивает простой и мощный интерфейс взаимодействия ваших объектов и реляционной базы данных любого типа. Для установки Flask-SQLAlchemy в ваше виртуальное окружение используйте pip:
pip install flask-sqlalchemy
Более полное описание [4] пакета Flask-SQLAlchemy.
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'}
Настроим модуль 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/
Нажмите здесь для печати.