É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.
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
Récent
- Masquer les clés primaires de la base de données UUID de votre application web
- Don't Repeat Yourself (DRY) avec Jinja2
- SQLAlchemy, PostgreSQL, nombre maximal de lignes par user
- Afficher les valeurs des filtres dynamiques SQLAlchemy
- Transfert de données sécurisé grâce au cryptage à Public Key et à pyNaCl
- rqlite : une alternative à haute disponibilité et dist distribuée SQLite
Les plus consultés
- Utilisation des Python's pyOpenSSL pour vérifier les certificats SSL téléchargés d'un hôte
- Utiliser UUIDs au lieu de Integer Autoincrement Primary Keys avec SQLAlchemy et MariaDb
- Connexion à un service sur un hôte Docker à partir d'un conteneur Docker
- Utiliser PyInstaller et Cython pour créer un exécutable Python
- SQLAlchemy : Utilisation de Cascade Deletes pour supprimer des objets connexes
- Flask RESTful API validation des paramètres de la requête avec les schémas Marshmallow