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.
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
Recientes
- Cómo ocultar las claves primarias de la base de datos UUID de su aplicación web
- Don't Repeat Yourself (DRY) con Jinja2
- SQLAlchemy, PostgreSQL, número máximo de filas por user
- Mostrar los valores en filtros dinámicos SQLAlchemy
- Transferencia de datos segura con cifrado de Public Key y pyNaCl
- rqlite: una alternativa de alta disponibilidad y dist distribuida SQLite
Más vistos
- Usando Python's pyOpenSSL para verificar los certificados SSL descargados de un host
- Usando UUIDs en lugar de Integer Autoincrement Primary Keys con SQLAlchemy y MariaDb
- Conectarse a un servicio en un host Docker desde un contenedor Docker
- Usando PyInstaller y Cython para crear un ejecutable de Python
- SQLAlchemy: Uso de Cascade Deletes para eliminar objetos relacionados
- Flask RESTful API validación de parámetros de solicitud con esquemas Marshmallow