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

Éliminer les répétitions et améliorer la maintenance en créant un Flask view class

L'utilisation d'un view class au lieu des fonctions de visualisation est préférable car elle nous permet de partager le code au lieu de le dupliquer et de le modifier.

24 mars 2020
Dans Flask
post main image
https://unsplash.com/@ostshem

Flask est chaud. Tout le monde aime Flask. Je crois que la raison principale est qu'il est si facile de commencer avec Flask. Vous créez une virtual environment, vous copiez-collez quelques lignes de code d'un exemple, vous pointez votre navigateur sur 127.0.0.1:5000 et voilà votre page. Ensuite, vous piratez un peu avec un modèle Jinja et vous obtenez une belle page. Vous pouvez même exécuter Flask sur un Raspberry Pi, n'est-ce pas merveilleux ?

La principale raison pour laquelle j'ai commencé à utiliser Flask était de créer une application et d'apprendre. Flask est un microframework , ce qui signifie que vous devez créer vous-même la plupart des autres choses. Bien sûr, vous pouvez utiliser des extensions, mais comme je l'ai écrit dans un post précédent, je ne veux pas trop dépendre des extensions. Il se peut qu'elles ne soient plus supportées demain et alors que ferez-vous ?

Plans et views

Lorsque votre application se développe, vous commencez à utiliser le modèle d'usine d'application, en utilisant create_app(), et vous commencez à utiliser les Blueprints. Cela donne une structure à votre application. Personne ne vous dit de faire cela, avec Flask il n'y a pas de règles, vous suivez simplement les suggestions de la documentation Flask et autres. J'ai créé un sous-répertoire blueprints avec des répertoires pour les fonctions d'administrateur du site. Les exemples sont user, user_group, page_request, access.

Ma première fonction d'affichage était moche, mais elle a fonctionné. Après un certain temps, je l'ai affinée, voir les exemples sur Internet. Mais mon application n'a cessé de s'agrandir et je faisais du copier-coller pour de nouvelles fonctions d'affichage. Pas vraiment du copier-coller, car il fallait modifier un certain nombre de choses pour chaque modèle. Certains modèles views étaient uniques, comme la vue content_item, mais beaucoup d'autres utilisaient simplement un modèle SQLAlchemy , vous savez, vous créez, modifiez, supprimez et avez une vue pour lister les enregistrements.

Le copier-coller a un très gros problème, il est presque impossible à maintenir. Un exemple de la méthode d'édition qui est copiée, collée et ensuite modifiée :

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

J'utilisais déjà des modèles "partagés". J'ai un modèle pour "nouveau et modifier", et un modèle pour "supprimer". Je n'avais pas de modèle "partagé" pour "liste". Quoi qu'il en soit, cela m'a un peu aidé mais je savais qu'un jour je devais refaire les fonctions d'affichage. Il y a quelques semaines, j'ai dû ajouter une autre fonction à l'administrateur et en copiant le code, j'ai décidé que c'était suffisant. Je dois arrêter cela et mettre mes efforts dans l'écriture d'un CRUD de base (Create-Read-Update-Delete) modèle view class que je peux utiliser dans ces cas. Commencez simplement, développez plus tard.

Comment d'autres l'ont fait

L'utilisation d'un view class au lieu des fonctions d'affichage est préférable car elle nous permet de partager le code au lieu de le dupliquer et de le modifier. Bien sûr, beaucoup d'autres ont reconnu ce fait avant et ont mis en œuvre leurs propres solutions. Jetez un coup d'œil à Flask Pluggable Views. C'est tout simplement très simple. Deux bons exemples se trouvent dans Flask-AppBuilder et Flask-Admin. Je vous suggère de jeter un coup d'œil à leur code, voir les liens ci-dessous. Cependant, vous me connaissez, au lieu de copier le code, j'ai voulu le faire moi-même. Ma première version ne sera rien par rapport aux deux mentionnées mais au moins j'apprends beaucoup.

Le modèle CRUD view class

Ma première version est limitée à un seul modèle (SQLAlchemy). La classe doit avoir des méthodes de listage, de création, de modification et de suppression. Je veux également qu'elle soit instanciée dans le modèle où elle appartient. Elle utilise les formulaires du répertoire Blueprint, donc pas de changement ici, par exemple les répertoires page_request et user Blueprint :

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

La partie la plus difficile pour moi a été d'ajouter les urls des méthodes à Flask. Cela doit être fait lorsque l'objet de vue CRUD est créé à l'aide de la méthode Flask add_url_rule(). L'objet de vue CRUD est créé au moment de l'initialisation. Cela signifie que vous ne pouvez pas utiliser url_for() ici car les routes ne sont pas connues à ce moment. Un autre défi consistait à organiser le nombre de paramètres qui doivent être passés lors de la création de l'objet. Ce nombre devrait être minimal ! Par exemple, la classe doit créer les urls pour new, éditer et supprimer par elle-même une url de base. J'ai créé une classe CRUDView dans un fichier class_crud_view.py qui ressemble à ceci :

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 est une liste car une méthode peut avoir plus d'une route. Par exemple, la méthode de liste users a deux routes, une route sans et une route avec un numéro de page :

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

La méthode de la liste doit être flexible, je dois pouvoir spécifier des champs, des types de champs, des noms. Les nouvelles méthodes, les méthodes de modification et de suppression sont plus ou moins des copies des méthodes existantes.

Mise en œuvre

Mon implémentation ne fonctionnera pas pour vous car j'utilise une fonction de pagination, mais voici mon CRUD view class, j'ai omis certaines parties :

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

Dans un Blueprint, j'instancie la classe comme suit :

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

)

C'est tout. Bien sûr, vous avez besoin des modèles Jinja . La méthode de la liste nécessite maintenant un modèle Jinja qui peut gérer les colonnes.

Extension du view class par l'ajout de méthodes

Tout cela est très basique, ce n'est qu'un début. Supposons que nous voulions que la fonction de liste trie par nom, ou autre chose. Nous pouvons le faire en modifiant la méthode CRUDView class. Nous déplaçons un code de la méthode de liste et l'ajoutons à une nouvelle méthode "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)

        ...

Dans le Blueprint, nous créons une nouvelle classe CityCRUDView qui hérite du CRUDView class et nous y ajoutons notre propre "liste__get_items_for_page". Ensuite, nous utilisons cette nouvelle classe pour instancier l'objet 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
    ...
   
)

Maintenant les villes sont triées par nom au lieu d'id. Nous pouvons ajouter d'autres méthodes à la classe CRUDView et les remplacer dans notre Blueprint.

Résumé

Je suis heureux d'avoir enfin pris le temps de mettre en œuvre une première version d'un modèle CRUD view class. Il m'a fallu quelques jours pour assembler toutes les pièces du puzzle, mais je suis sûr que ce temps en valait la peine. Je l'utilise déjà dans certains Blueprints. Vous pouvez consulter la section Démo de ce site pour un exemple de travail.

Liens / crédits

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/

En savoir plus...

Flask

Laissez un commentaire

Commentez anonymement ou connectez-vous pour commenter.

Commentaires

Laissez une réponse

Répondez de manière anonyme ou connectez-vous pour répondre.