Устранить повторение и улучшить обслуживание путем создания Flask view class
Использование view class вместо функций просмотра лучше, потому что это позволяет нам делиться кодом вместо его дублирования и модификации.
Flask горячий. Все любят Flask. Думаю, основная причина в том, что так просто начать с Flask. Вы создаете virtual environment, копируете-вставляете несколько строк кода из какого-нибудь примера, указываете ваш браузер на 127.0.0.1:5000 и вот ваша страница. Затем вы немного взломаете шаблон Jinja и получаете красивую страницу. Вы даже можете запустить Flask на Raspberry Pi, не правда ли, это замечательно?
Основной причиной, по которой я начал использовать Flask , было создание приложения и обучение, Flask - это microframework , что означает, что вы должны создавать большинство других вещей самостоятельно. Конечно, вы можете использовать расширения, но, как я писал в предыдущем посте, я не хочу слишком сильно зависеть от расширений. Они могут завтра стать неподдерживаемыми, и что вы тогда будете делать?
Blueprints и views.
Когда ваше приложение растет, вы начинаете использовать заводской шаблон приложения, используя create_app(), и начинаете использовать Blueprints. Это придает структуру вашему приложению. Никто не говорит вам делать это, с Flask нет никаких правил, вы просто следуете предложениям документации Flask и другим. Я создал подкаталог blueprints с каталогами для функций администратора сайта. Примеры: user, user_group, page_request, access.
Моя первая функция просмотра была уродливой, но она работала. Через некоторое время я ее доработал, смотрите примеры в интернете. Но мое приложение все больше расширялось, и я делал копи-паст для новых функций просмотра. Не совсем copy-paste, потому что для каждой модели нужно было что-то менять. Некоторые views были уникальны, как и представление content_item, но многие другие просто использовали модель SQLAlchemy , вы знаете, вы создаете, редактируете, удаляете и имеете представление для списка записей.
Copy-paste имеет одну очень большую проблему, его практически невозможно поддерживать. Пример метода редактирования, который копируется, вставляется и затем изменяется:
@admin_user_blueprint.route('/user/edit/<int:id>', methods=['GET', 'POST'])
@login_required
def user_edit(id):
user = db_select(
model_class_list=[User],
filter_by_list=[
(User, 'id', 'eq', id),
],
).first()
if user is None:
abort(403)
form = UserEditForm(obj=user)
if form.validate_on_submit():
# update and redirect
form.populate_obj(user)
g.db_session.commit()
flash( _('User %(username)s updated.', username=user.username), 'info')
return redirect(url_for('admin_user.users_list'))
return render_template(
'shared/new_edit.html',
page_title=_('Edit user'),
form=form,
user=user,
)
Я уже использовал 'общие' шаблоны. У меня есть шаблон для 'new and edit', и шаблон для 'delete'. У меня не было шаблона 'shared' для 'list'. В любом случае, это немного помогло, но я знал, что однажды мне пришлось переделать функции просмотра. Несколько недель назад мне пришлось добавить еще одну функцию администратору, и во время копирования кода я решил, что этого достаточно. Я должен остановить это и приложить усилия для написания базовой модели CRUD (Create-Read-Update-Delete) view class , которую я могу использовать в этих случаях. Начните с простого, разверните позже.
Как другие это делали
Использование view class вместо функций просмотра лучше, потому что это позволяет нам делиться кодом вместо его дублирования и модификации. Конечно, многие другие признали это раньше и реализовали свои собственные решения. Взгляните на Flask Pluggable Views. Это очень простое решение. Два хороших примера можно найти в Flask-AppBuilder и Flask-Admin. Предлагаю вам взглянуть на их код, смотрите ссылки ниже. Однако, вы знаете меня, вместо того, чтобы копировать код, я хотел сделать это сам. Моя первая версия будет ничем по сравнению с двумя упомянутыми, но, по крайней мере, я многому научился.
Модель CRUD view class
Моя первая версия ограничена единственной (SQLAlchemy) моделью. Класс должен иметь методы list, new, edit и delete. Я также хочу, чтобы он был инстанцирован в Blueprint, где он принадлежит. Он использует формы из каталога Blueprint, поэтому здесь никаких изменений нет, например, каталоги page_request и user Blueprint:
.
|-- blueprints
| |
| |-- page_request
| | |-- forms.py
| | |-- __init__.py
| | `-- views.py
| |-- user
| | |-- forms.py
| | |-- __init__.py
| | `-- views.py
Самой сложной частью для меня было добавить urls методов в Flask. Это должно быть сделано при создании Объекта представления CRUD с помощью метода Flask add_url_rule(). Объект представления CRUD создается во время инициализации. Это означает, что здесь нельзя использовать url_for(), так как маршруты на данный момент неизвестны. Другой задачей было упорядочить количество параметров, которые должны передаваться при создании объекта. Это должно быть минимально! Например, класс должен создавать urls для нового, редактировать и удалять сам из базового url. Я создал класс CRUDView в файле class_crud_view.py, который выглядит следующим образом:
class CRUDView:
def __init__(self,
blueprint = None,
model = None,
...
):
self.blueprint = blueprint
self.model = model
...
# construct rules, endpoints, view_funcs for list, new, edit, delete
# because we are using a blueprint, the url_rule endpoint is the method
methods = ['GET', 'POST']
self.operations = {
'list': {
'url_rules': [
...
],
},
'new': {
'url_rules': [
...
],
},
'edit': {
'url_rules': [
...
],
},
'delete': {
'url_rules': [
...
],
},
}
# register urls
for operation, operation_defs in self.operations.items():
for url_rule in operation_defs['url_rules']:
self.blueprint.add_url_rule(url_rule['rule'], **url_rule['args'])
def list(self, page_number):
...
def new(self):
...
def edit(self, item_id):
...
def delete(self, item_id):
...
url_rules - это список, так как метод может иметь более одного маршрута. Например, метод списка users имеет два маршрута, маршрут без и маршрут с номером страницы:
@admin_user_blueprint.route('/users/list', defaults={'page_number': 1})
@admin_user_blueprint.route('/users/list/<int:page_number>')
def users_list(page_number):
...
Метод списка должен быть гибким, я должен уметь указывать поля, типы полей, имена. Новые, редактируемые и удаляемые методы являются более или менее копиями существующих методов.
Реализация
Моя реализация не сработает, так как я использую функцию пагинации, но вот мой CRUD view class, я пропустил некоторые части:
class CRUDView:
def __init__(self,
blueprint = None,
model = None,
list_base_rule = None,
list_method = None,
list_page_title = None,
list_page_content_title = None,
crud_item_base_rule = None,
crud_item_base_method = None,
crud_item = None,
list_items_per_page = None,
list_columns = None,
form_new = None,
form_edit = None,
form_delete = None
):
self.blueprint = blueprint
self.model = model
self.list_base_rule = list_base_rule
self.list_method = list_method
self.list_page_title = list_page_title
self.list_page_content_title = list_page_content_title
self.crud_item_base_rule = crud_item_base_rule
self.crud_item_base_method = crud_item_base_method
self.crud_item = crud_item
self.list_items_per_page = list_items_per_page
self.list_columns = list_columns
self.form_new = form_new
self.form_edit = form_edit
self.form_delete = form_delete
if self.list_page_content_title is None:
self.list_page_content_title = self.list_page_title
# construct rules, endpoints, view_funcs for list, new, edit, delete
# because we using a blueprint, the url_rule endpoint is the method
methods = ['GET', 'POST']
self.operations = {
'list': {
'url_rules': [
{
'rule': self.list_base_rule,
'args' : {
'endpoint': self.list_method,
'view_func': self.list,
'methods': methods,
'defaults': {'page_number': 1},
},
},
{
'rule': self.list_base_rule + '/<int:page_number>',
'args' : {
'endpoint': self.list_method,
'view_func': self.list,
'methods': methods,
},
},
],
},
'new': {
'url_rules': [
{
'rule': self.crud_item_base_rule + 'new',
'args' : {
'endpoint': self.crud_item_base_method + 'new',
'view_func': self.new,
'methods': methods,
},
},
],
},
'edit': {
'url_rules': [
{
'rule': self.crud_item_base_rule + 'edit/<int:item_id>',
'args' : {
'endpoint': self.crud_item_base_method + 'edit',
'view_func': self.edit,
'methods': methods,
},
},
],
},
'delete': {
'url_rules': [
{
'rule': self.crud_item_base_rule + 'delete/<int:item_id>',
'args' : {
'endpoint': self.crud_item_base_method + 'delete',
'view_func': self.delete,
'methods': methods,
},
},
],
},
}
# for easy access
self.list_endpoint = self.blueprint.name + '.' + self.list_method
self.new_endpoint = self.blueprint.name + '.' + self.crud_item_base_method + 'new'
self.edit_endpoint = self.blueprint.name + '.' + self.crud_item_base_method + 'edit'
self.delete_endpoint = self.blueprint.name + '.' + self.crud_item_base_method + 'delete'
# register urls
for operation, operation_defs in self.operations.items():
for url_rule in operation_defs['url_rules']:
self.blueprint.add_url_rule(url_rule['rule'], **url_rule['args'])
def list(self, page_number):
# get all items for pagination
total_items_count = db_select(
model_class_list=[(self.model, 'id')],
).count()
pagination = Pagination(items_per_page=self.list_items_per_page)
pagination.set_params(page_number, total_items_count, self.list_endpoint)
# get items for page
items = db_select(
model_class_list=[self.model],
order_by_list=[
(self.model, 'id', 'desc'),
],
limit=pagination.limit,
offset=pagination.offset,
).all()
return render_template(
'shared/items_list.html',
action='List',
page_title=self.list_page_title
pagination=pagination,
page_number=page_number,
items=items,
list_columns=self.list_columns,
item_edit_endpoint=self.edit_endpoint,
item_delete_endpoint=self.delete_endpoint,
new_button={
'name': 'New' + ' ' + self.crud_item['name_lc'],
'endpoint': self.new_endpoint,
}
)
def new(self):
form = self.form_new()
if form.validate_on_submit():
# add and redirect
crud_model = self.model()
form.populate_obj(crud_model)
g.db_session.add(crud_model)
g.db_session.commit()
flash( self.crud_item['name'] + ' ' + _('added') + ': ' + getattr(crud_model, self.crud_item['attribute']), 'info')
return redirect( url_for(self.list_endpoint) )
return render_template(
'shared/item_new_edit.html',
action='Add',
page_title=_('New') + ' ' + self.crud_item['name_lc'],
form=form,
back_button = {
'name': _('Back to list'),
'url': url_for(self.list_endpoint),
}
)
def edit(self, item_id):
# get item
item = db_select(
model_class_list=[self.model],
filter_by_list=[
(self.model, 'id', 'eq', item_id),
],
).first()
if item is None:
abort(403)
form = self.form_edit(obj=item)
if form.validate_on_submit():
# update and redirect
form.populate_obj(item)
g.db_session.commit()
flash( self.crud_item['name'] + ' ' + _('updated') + ': ' + getattr(item, self.crud_item['attribute']), 'info')
return redirect( url_for(self.list_endpoint) )
return render_template(
'shared/item_new_edit.html',
action='Edit',
page_title=_('Edit') + ' ' + self.crud_item['name_lc'],
form=form,
item=item,
back_button = {
'name': _('Back to list'),
'url': url_for(self.list_endpoint),
}
)
def delete(self, item_id):
# get item
item = db_select(
model_class_list=[self.model],
filter_by_list=[
(self.model, 'id', 'eq', item_id),
],
).first()
if item is None:
abort(403)
form = self.form_delete(obj=item)
if form.validate_on_submit():
# delete and redirect
item.status = STATUS_DELETED
g.db_session.commit()
flash( self.crud_item['name'] + ' ' + _('deleted') + ': ' + getattr(item, self.crud_item['attribute']), 'info')
return redirect( url_for(self.list_endpoint) )
return render_template(
'shared/item_delete.html',
action='Delete',
page_title=_('Delete') + ' ' + self.crud_item['name_lc'],
form=form,
item_name=getattr(item, self.crud_item['attribute']),
back_button = {
'name': _('Back to list'),
'url': url_for(self.list_endpoint),
}
)
В примере Blueprint I инстанцируем класс следующим образом:
city_demo_crud_view = CRUDView(
blueprint = frontend_demo_crud_view_blueprint,
model = DemoCRUDViewCity,
list_base_rule = '/cities/list',
list_method = 'cities_list',
list_page_title = _('Cities'),
crud_item_base_rule = '/city/',
crud_item_base_method = 'city_',
crud_item = {
'name': _('City'),
'name_lc': _('city'),
'attribute': 'name'
},
list_items_per_page = DEMO_CRUD_VIEW_CITY_LIST_PAGINATION_CITIES_PER_PAGE,
list_columns = [
{
'attribute': 'name',
'th': {
'name': _('Name'),
},
'td': {
},
},
{
'attribute': 'created_on',
'th': {
'name': _('Created'),
},
'td': {
},
},
{
'attribute': 'updated_on',
'th': {
'name': _('Updated'),
},
'td': {
},
},
],
form_new = DemoCRUDViewCityNewForm,
form_edit = DemoCRUDViewCityEditForm,
form_delete = DemoCRUDViewCityDeleteForm
)
Вот так. Конечно, вам понадобятся шаблоны Jinja . Метод list теперь требует шаблона Jinja , который может обрабатывать колонки.
Расширение view class путем добавления методов
Все это очень просто, это только начало. Предположим, мы хотим, чтобы функция списка сортировалась по имени, или что-то в этом роде. Это можно сделать, изменив CRUDView class. Мы перемещаем некоторый код из метода list и добавляем его в новый метод 'list__get_items_for_page':
...
def list__get_items_for_page(self, pagination):
items = db_select(
model_class_list=[self.model],
order_by_list=[
(self.model, 'id', 'desc'),
],
limit=pagination.limit,
offset=pagination.offset,
).all()
return items
def list(self, page_number):
...
# get items for page
items = self.list__get_items_for_page(pagination)
...
В Blueprint мы создаем новый класс CityCRUDView, который наследует CRUDView class и добавляем свой 'list__get_items_for_page'. Затем мы используем этот новый класс для инстанцирования объекта city_demo_crud_view:
class CityCRUDView(CRUDView):
def list__get_items_for_page(self, pagination):
items = db_select(
model_class_list=[self.model],
order_by_list=[
(self.model, 'name', 'asc'),
],
limit=pagination.limit,
offset=pagination.offset,
).all()
return items
city_demo_crud_view = CityCRUDView(
# same as above
...
)
Теперь города сортируются по названиям, а не по id. Мы можем добавить дополнительные методы в класс CRUDView и переопределить их в нашем Blueprint.
Резюме
Я рад, что наконец-то нашел время реализовать первую версию CRUD модели view class. Мне потребовалось несколько дней, чтобы собрать все части вместе, но я уверен, что это время того стоило. Я уже использую его в некоторых чертежах. Рабочий пример можно посмотреть в разделе Демонстрация на этом сайте.
Ссылки / кредиты
Base Views
https://flask-appbuilder.readthedocs.io/en/latest/views.html
Customizing Built-in Views
https://flask-admin.readthedocs.io/en/latest/introduction/#customizing-built-in-views
Pluggable Views
https://flask.palletsprojects.com/en/1.1.x/views/
Подробнее
Flask
Недавний
- Скрытие первичных ключей базы данных UUID вашего веб-приложения
- Don't Repeat Yourself (DRY) с Jinja2
- SQLAlchemy, PostgreSQL, максимальное количество строк для user
- Показать значения в динамических фильтрах SQLAlchemy
- Безопасная передача данных с помощью шифрования Public Key и pyNaCl
- rqlite: альтернатива dist с высокой степенью готовности и SQLite
Большинство просмотренных
- Используя Python pyOpenSSL для проверки SSL-сертификатов, загруженных с хоста
- Использование UUID вместо Integer Autoincrement Primary Keys с SQLAlchemy и MariaDb
- Подключение к службе на хосте Docker из контейнера Docker
- Использование PyInstaller и Cython для создания исполняемого файла Python
- SQLAlchemy: Использование Cascade Deletes для удаления связанных объектов
- Flask Удовлетворительный запрос API проверка параметров запроса с помощью схем Маршмэллоу