Eliminieren Sie Wiederholungen und verbessern Sie die Wartung durch die Erstellung eines Flask view class
Die Verwendung eines view class anstelle von Ansichtsfunktionen ist besser, weil wir dadurch Code gemeinsam nutzen können, anstatt ihn zu duplizieren und zu modifizieren.
Flask ist heiß. Jeder liebt Flask. Ich glaube, der Hauptgrund ist, dass es so einfach ist, mit Flask zu beginnen. Sie erstellen ein virtual environment, kopieren und fügen ein paar Zeilen Code aus einem Beispiel ein, zeigen Ihren Browser auf 127.0.0.1.5000 und schon haben Sie Ihre Seite. Dann hacken Sie ein wenig mit einer Jinja -Vorlage und Sie erhalten eine schöne Seite. Sie können sogar Flask auf einem Raspberry Pi ausführen, ist das nicht wunderbar?
Mein Hauptgrund, Flask zu verwenden, war das Erstellen einer Anwendung und das Lernen. Flask ist ein microframework , was bedeutet, dass Sie die meisten anderen Dinge selbst erstellen müssen. Natürlich können Sie Erweiterungen verwenden, aber wie ich in einem früheren Beitrag schrieb, möchte ich mich nicht zu sehr auf Erweiterungen verlassen. Sie werden vielleicht morgen nicht unterstützt, und was werden Sie dann tun?
Blaupausen und views
Wenn Ihre Anwendung wächst, beginnen Sie mit dem Anwendungsfabrikmuster unter Verwendung von create_app() und beginnen mit der Verwendung von Blueprints. Dies gibt Ihrer Anwendung Struktur. Niemand sagt Ihnen, dass Sie dies tun sollen, mit Flask gibt es keine Regeln, Sie folgen einfach den Vorschlägen der Dokumentation zu Flask und anderen. Ich habe ein Unterverzeichnis blueprints mit Verzeichnissen für die Administratorfunktionen der Website erstellt. Beispiele sind user, user_group, page_request, access.
Meine erste Ansichtsfunktion war hässlich, aber sie hat funktioniert. Nach einiger Zeit habe ich sie verfeinert, siehe die Beispiele im Internet. Aber meine Anwendung wurde immer größer, und ich habe für neue Ansichtsfunktionen Kopieren und Einfügen gemacht. Nicht gerade Copy-Paste, weil für jedes Modell eine Reihe von Dingen geändert werden mußte. Einige views waren einzigartig, wie die content_item-Ansicht, aber viele andere benutzten nur ein SQLAlchemy -Modell, wissen Sie, man erstellt, bearbeitet, löscht und hat eine Ansicht, um die Datensätze aufzulisten.
Copy-Paste hat ein sehr großes Problem, es ist fast unmöglich, es zu pflegen. Ein Beispiel für die Editiermethode, die kopiert, eingefügt und dann modifiziert wird:
@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,
)
Ich habe bereits 'geteilte' Vorlagen verwendet. Ich habe eine Vorlage für 'neu und bearbeiten' und eine Vorlage für 'löschen'. Ich hatte keine 'geteilte' Vorlage für 'Liste'. Jedenfalls half das ein wenig, aber ich wusste, dass ich eines Tages die Ansichtsfunktionen neu erstellen musste. Vor ein paar Wochen musste ich dem Administrator eine weitere Funktion hinzufügen, und beim Kopieren von Code entschied ich, dass dies ausreichend war. Ich muss dies beenden und meine Bemühungen darauf richten, ein CRUD-Basismodell (CRUD = Create-Read-Update-Delete) view class zu schreiben, das ich in diesen Fällen verwenden kann. Beginnen Sie einfach, erweitern Sie später.
Wie andere das gemacht haben
Die Verwendung eines view class anstelle von Ansichtsfunktionen ist besser, weil wir dadurch Code gemeinsam nutzen können, anstatt ihn zu duplizieren und zu modifizieren. Natürlich haben viele andere dies bereits erkannt und ihre eigenen Lösungen implementiert. Schauen Sie sich Flask Pluggable Views an. Dies ist nur sehr grundlegend. Zwei gute Beispiele finden Sie in Flask-AppBuilder und Flask-Admin. Ich schlage vor, Sie werfen einen Blick auf deren Code, siehe die Links unten. Sie kennen mich jedoch, anstatt den Code zu kopieren, wollte ich es selbst tun. Meine erste Version wird nichts im Vergleich zu den beiden genannten sein, aber zumindest lerne ich eine Menge.
Das CRUD-Modell view class
Meine erste Version ist auf ein einziges Modell (SQLAlchemy) beschränkt. Die Klasse muss über Listen-, Neu-, Bearbeitungs- und Löschmethoden verfügen. Ich möchte auch, dass sie im Blueprint dort instanziiert wird, wo sie hingehört. Sie verwendet die Formulare aus dem Blueprint-Verzeichnis, also keine Änderungen an dieser Stelle, z.B. die Blueprint-Verzeichnisse page_request und user :
.
|-- blueprints
| |
| |-- page_request
| | |-- forms.py
| | |-- __init__.py
| | `-- views.py
| |-- user
| | |-- forms.py
| | |-- __init__.py
| | `-- views.py
Die größte Herausforderung bestand für mich darin, die Urlen der Methoden in Flask hinzuzufügen. Dies muss geschehen, wenn das CRUD-View-Objekt mit der Methode Flask add_url_rule() erstellt wird. Das CRUD-Sicht-Objekt wird zur Initialisierungszeit erstellt. Das bedeutet, dass Sie hier nicht url_for() verwenden können, da die Routen zu diesem Zeitpunkt nicht bekannt sind. Eine weitere Herausforderung bestand darin, die Anzahl der Parameter zu arrangieren, die bei der Objekterstellung übergeben werden müssen. Diese sollte minimal sein! Zum Beispiel sollte die Klasse die Url für new, editieren und löschen selbst von einer Basis-Url erstellen. Ich habe eine Klasse CRUDView in einer Datei class_crud_view.py erstellt, die wie folgt aussieht:
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 ist eine Liste, weil eine Methode mehr als eine Route haben kann. Zum Beispiel hat die Listenmethode users zwei Routen, eine Route ohne und eine Route mit einer Seitenzahl:
@admin_user_blueprint.route('/users/list', defaults={'page_number': 1})
@admin_user_blueprint.route('/users/list/<int:page_number>')
def users_list(page_number):
...
Die Listenmethode muss flexibel sein, ich sollte in der Lage sein, Felder, Feldtypen und Namen anzugeben. Die neuen, Editier- und Löschmethoden sind mehr oder weniger Kopien der bestehenden Methoden.
Implementierung
Meine Implementierung wird für Sie nicht funktionieren, weil ich eine Paginierungsfunktion verwende, aber hier ist meine CRUD view class, ich habe einige Teile ausgelassen:
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),
}
)
In einem Blueprint instanziiere ich die Klasse wie folgt:
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
)
Das war's. Natürlich brauchen Sie die Jinja -Vorlagen. Die Listenmethode erfordert nun eine Jinja -Vorlage, die die Spalten verarbeiten kann.
Erweiterung der view class durch Hinzufügen von Methoden
Das ist alles sehr einfach, es ist nur ein Anfang. Angenommen, wir wollen, dass die Listenfunktion nach Namen oder was auch immer sortiert wird. Wir können dies tun, indem wir die CRUDView class ändern. Wir verschieben etwas Code aus der Listenmethode und fügen diesen zu einer neuen Methode 'list__get_items_for_page' hinzu:
...
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)
...
Im Blueprint erstellen wir eine neue Klasse CityCRUDView, die die CRUDView class erbt, und fügen unsere eigene 'list__get_items_for_page' hinzu. Dann verwenden wir diese neue Klasse, um das city_demo_crud_view-Objekt zu instanziieren:
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
...
)
Jetzt sind die Städte nach Namen statt nach ID sortiert. Wir können der Klasse CRUDView weitere Methoden hinzufügen und sie in unserem Blueprint überschreiben.
Zusammenfassung
Ich bin froh, dass ich mir endlich die Zeit genommen habe, eine erste Version eines CRUD-Modells view class zu implementieren. Ich habe ein paar Tage gebraucht, um alle Teile zusammenzufügen, aber ich bin sicher, dass sich diese Zeit gelohnt hat. Ich verwende es bereits in einigen Entwürfen. Ein funktionierendes Beispiel finden Sie im Abschnitt Demo auf dieser Website.
Links / Impressum
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/
Mehr erfahren
Flask
Neueste
- Ausblenden der Primärschlüssel der Datenbank UUID Ihrer Webanwendung
- Don't Repeat Yourself (DRY) mit Jinja2
- SQLAlchemy, PostgreSQL, maximale Anzahl von Zeilen pro user
- Anzeige der Werte in den dynamischen Filtern SQLAlchemy
- Sichere Datenübertragung mit Public Key Verschlüsselung und pyNaCl
- rqlite: eine hochverfügbare und distverteilte SQLite -Alternative
Meistgesehen
- Verwendung von Pythons pyOpenSSL zur Überprüfung von SSL-Zertifikaten, die von einem Host heruntergeladen wurden
- Verwendung von UUIDs anstelle von Integer Autoincrement Primary Keys mit SQLAlchemy und MariaDb
- Verbindung zu einem Dienst auf einem Docker -Host von einem Docker -Container aus
- PyInstaller und Cython verwenden, um eine ausführbare Python-Datei zu erstellen
- SQLAlchemy: Verwendung von Cascade Deletes zum Löschen verwandter Objekte
- Flask RESTful API Validierung von Anfrageparametern mit Marshmallow-Schemas