angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

Устранить повторение и улучшить обслуживание путем создания Flask view class

Использование view class вместо функций просмотра лучше, потому что это позволяет нам делиться кодом вместо его дублирования и модификации.

24 марта 2020
В Flask
post main image
https://unsplash.com/@ostshem

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

Оставить комментарий

Комментируйте анонимно или войдите в систему, чтобы прокомментировать.

Комментарии

Оставьте ответ

Ответьте анонимно или войдите в систему, чтобы ответить.