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

Eliminar la repetición y mejorar el mantenimiento creando un Flask view class

Usar un view class en lugar de las funciones de vista es mejor porque nos permite compartir el código en lugar de duplicarlo y modificarlo.

24 marzo 2020
En Flask
post main image
https://unsplash.com/@ostshem

Flask está caliente. A todo el mundo le encanta Flask. Creo que la razón principal es que es tan fácil empezar con Flask. Creas un virtual environment, copias-pegas unas pocas líneas de código de algún ejemplo, apuntas tu navegador a 127.0.0.1:5000 y ahí está tu página. Luego hackeas un poco con una plantilla Jinja y obtienes una hermosa página. Incluso puedes ejecutar Flask en un Raspberry Pi, ¿no es maravilloso?

Mi principal razón para empezar a usar Flask fue la construcción de una aplicación y el aprendizaje, Flask es un microframework que significa que debes crear la mayoría de las otras cosas por ti mismo. Por supuesto que puedes usar extensiones, pero como escribí en un post anterior, no quiero depender demasiado de las extensiones. Pueden quedar sin apoyo mañana y entonces, ¿qué harás?

Planos y views

Cuando tu aplicación crece, comienzas a usar el patrón de fábrica de la aplicación, usando create_app(), y empiezas a usar Blueprints. Esto le da estructura a su aplicación. Nadie te dice que hagas esto, con Flask no hay reglas, sólo sigues las sugerencias de la documentación de Flask y otras. Creé un subdirectorio blueprints con directorios para las funciones de administrador del sitio. Los ejemplos son user, user_group, page_request, access.

Mi primera función de vista fue fea, pero funcionó. Después de un tiempo lo refiné, vea los ejemplos en Internet. Pero mi aplicación se hizo más grande y estaba haciendo copiar-pegar para nuevas funciones de vista. No exactamente copiar-pegar porque había que cambiar varias cosas para cada modelo. Algunos views eran únicos, como la vista content_item, pero muchos otros sólo usaban un modelo SQLAlchemy , ya sabes, creas, editas, borras y tienes una vista para listar los registros.

Copiar-pegar tiene un problema muy grande, es casi imposible de mantener. Un ejemplo del método de edición que se copia, se pega y luego se modifica:

@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,
    )

Ya estaba usando plantillas "compartidas". Tengo una plantilla para "nuevo y editar", y una plantilla para "eliminar". No tenía una plantilla "compartida" para "lista". De todos modos, esto ayudó un poco pero sabía que un día tenía que rehacer las funciones de la vista. Hace unas semanas tuve que añadir otra función al administrador y mientras copiaba el código decidí que era suficiente. Debo detener esto y poner mis esfuerzos en escribir un CRUD (Create-Read-Update-Delete) base modelo view class que pueda usar en estos casos. Empieza simple, expándelo después.

Cómo otros hicieron esto

Usar una view class en lugar de funciones de vista es mejor porque nos permite compartir el código en lugar de duplicarlo y modificarlo. Por supuesto, muchos otros reconocieron esto antes e implementaron sus propias soluciones. Echa un vistazo a Flask Pluggable Views. Esto es muy básico. Dos buenos ejemplos se pueden encontrar en Flask-AppBuilder y Flask-Admin. Le sugiero que eche un vistazo a su código, vea los enlaces de abajo. Sin embargo, ya me conoces, en lugar de copiar el código quería hacerlo yo mismo. Mi primera versión no será nada comparada con las dos mencionadas, pero al menos aprendo mucho.

El modelo CRUD view class

Mi primera versión está limitada a un solo modelo (SQLAlchemy). La clase debe tener métodos de lista, nuevos, de edición y de borrado. También quiero que se instale en el plano donde pertenece. Utiliza los formularios del directorio del Blueprint, así que no hay cambios aquí, por ejemplo los directorios page_request y user Blueprint:

.
|--  blueprints
|   |
|   |--  page_request
|   |   |-- forms.py
|   |   |-- __init__.py
|   |   `--  views.py
|   |--  user
|   |   |-- forms.py
|   |   |-- __init__.py
|   |   `--  views.py

La parte más desafiante para mí fue agregar las urls de los métodos a Flask. Esto debe hacerse cuando el objeto de la vista CRUD se crea usando el método Flask add_url_rule(). El objeto de la vista CRUD se crea en el momento de la inicialización. Esto significa que no se puede usar url_for() aquí porque las rutas no son conocidas en este momento. Otro desafío fue organizar el número de parámetros que deben ser pasados durante la creación del objeto. Esto debería ser mínimo! Por ejemplo, la clase debería crear las urls de nuevo, editar y borrar por sí misma de una url base. Creé una clase CRUDView en un archivo class_crud_view.py que se ve así:

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 es una lista porque un método puede tener más de una ruta. Por ejemplo, el método de lista users tiene dos rutas, una ruta sin y una ruta con un número de página:

@admin_user_blueprint.route('/users/list', defaults={'page_number': 1})
@admin_user_blueprint.route('/users/list/<int:page_number>')
def  users_list(page_number):
    ...

El método de lista debe ser flexible, debería poder especificar campos, tipos de campo, nombres. Los nuevos métodos de edición y borrado son más o menos copias de los métodos existentes.

Implementación

Mi implementación no funcionará para ustedes porque uso una función de paginación, pero aquí está mi CRUD view class, he dejado algunas partes:

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),
			}
        )

En un Blueprint instancio la clase de la siguiente manera:

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

)

Eso es todo. Por supuesto que necesitas las plantillas Jinja . El método de lista ahora requiere una plantilla Jinja que pueda manejar las columnas.

Extendiendo el view class añadiendo métodos

Todo esto es muy básico, es sólo un comienzo. Supongamos que queremos que la función de lista se ordene por nombre, o lo que sea. Podemos hacerlo cambiando el CRUDView class. Movemos algo de código del método de la lista y lo añadimos a un nuevo método '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)

        ...

En el Blueprint creamos una nueva clase CityCRUDView que hereda la CRUDView class y añadimos nuestra propia 'list__get_items_for_page'. Luego usamos esta nueva clase para instanciar el objeto 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
    ...
   
)

Ahora las ciudades están ordenadas por nombre en vez de por id. Podemos añadir más métodos a la clase CRUDView y anularlos en nuestro Blueprint.

Resumen

Estoy feliz de haberme tomado el tiempo para implementar una primera versión de un modelo CRUD view class. Me llevó unos días juntar todas las piezas pero estoy seguro de que este tiempo valió la pena. Ya lo uso en algunos planos. Puedes mirar en la sección de Demostración de este sitio para ver un ejemplo de trabajo.

Enlaces / créditos

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/

Leer más

Flask

Deje un comentario

Comente de forma anónima o inicie sesión para comentar.

Comentarios

Deje una respuesta.

Responda de forma anónima o inicie sesión para responder.