Elimineer herhaling en verbeter het onderhoud door een Flask view class aan te maken.
Het gebruik van een view class in plaats van weergavefuncties is beter omdat het ons in staat stelt om code te delen in plaats van deze te dupliceren en te wijzigen.
Flask is heet. Iedereen houdt van Flask. Ik denk dat de belangrijkste reden is dat het zo gemakkelijk is om te beginnen met Flask. Je maakt een virtual environment, copy-paste een paar regels code van een voorbeeld, richt je browser op 127.0.0.1:5000 en daar is je pagina. Dan hackt u een beetje met een Jinja sjabloon en u krijgt een mooie pagina. U kunt zelfs Flask draaien op een Raspberry Pi, is dat niet prachtig?
Mijn belangrijkste reden om Flask te gaan gebruiken was het bouwen van een applicatie en het leren, Flask is een microframework wat betekent dat je de meeste andere dingen zelf moet maken. Natuurlijk kunt u extensies gebruiken, maar zoals ik in een vorig bericht heb geschreven, wil ik niet te veel afhankelijk zijn van extensies. Het kan zijn dat ze morgen niet meer ondersteund worden en wat ga je dan doen?
Blauwdrukken en views
Wanneer uw applicatie groeit, begint u het applicatie-fabriekspatroon te gebruiken, met behulp van create_app(), en begint u Blueprints te gebruiken. Dit geeft structuur aan je applicatie. Niemand zegt je dit te doen, met Flask zijn er geen regels, je volgt gewoon de suggesties van de Flask documentatie en andere. Ik heb een subdirectory blueprints gemaakt met directories voor de beheerdersfuncties van de site. Voorbeelden zijn user, user_groep, page_request, toegang.
Mijn eerste weergavefunctie was lelijk, maar het werkte. Na enige tijd heb ik het verfijnd, zie de voorbeelden op het internet. Maar mijn applicatie werd steeds groter en ik was bezig met copy-paste voor nieuwe weergavefuncties. Niet echt copy-paste omdat er voor elk model een aantal dingen veranderd moesten worden. Sommige views waren uniek, zoals de content_item view, maar vele anderen gebruikten gewoon een SQLAlchemy model, je weet wel, je maakt, bewerkt, verwijdert en hebt een view om de records op te sommen.
Copy-paste heeft een zeer groot probleem, het is bijna onmogelijk te onderhouden. Een voorbeeld van de bewerkingsmethode die gekopieerd, geplakt en vervolgens aangepast wordt:
@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,
)
Ik gebruikte al 'gedeelde' sjablonen. Ik heb een sjabloon voor 'nieuw en bewerken', en een sjabloon voor 'verwijderen'. Ik had geen 'gedeelde' sjabloon voor 'lijst'. Hoe dan ook, dit hielp een beetje, maar ik wist dat ik op een dag de weergavefuncties opnieuw moest doen. Een paar weken geleden moest ik een andere functie toevoegen aan de beheerder en tijdens het kopiëren van code besloot ik dat het genoeg was. Ik moet dit stoppen en mijn best doen om een basis CRUD (Create-Read-Update-Delete) model view class te schrijven dat ik in deze gevallen kan gebruiken. Begin eenvoudig, breid later uit.
Hoe anderen dit deden
Het gebruik van een view class in plaats van weergavefuncties is beter omdat het ons in staat stelt om code te delen in plaats van deze te dupliceren en te wijzigen. Natuurlijk hebben vele anderen dit al eerder herkend en hun eigen oplossingen geïmplementeerd. Kijk eens naar Flask Pluggable Views. Dit is gewoon heel basaal. Twee goede voorbeelden zijn te vinden in Flask-AppBuilder en Flask-Admin. Ik stel voor dat u hun code bekijkt, zie de links hieronder. U kent mij echter, in plaats van de code te kopiëren wilde ik het zelf doen. Mijn eerste versie zal niets zijn in vergelijking met de twee genoemde, maar ik leer in ieder geval veel.
Het CRUD-model view class
Mijn eerste versie is beperkt tot een enkel (SQLAlchemy) model. De klasse moet een lijst hebben, nieuwe, bewerkings- en verwijderingsmethoden. Ik wil ook dat het in de Blueprint wordt geinstalleerd waar het thuishoort. Het gebruikt de formulieren uit de Blueprint directory, dus geen veranderingen hier, bijvoorbeeld de page_request en user Blueprint directories:
.
|-- blueprints
| |
| |-- page_request
| | |-- forms.py
| | |-- __init__.py
| | `-- views.py
| |-- user
| | |-- forms.py
| | |-- __init__.py
| | `-- views.py
Het meest uitdagende deel voor mij was om de url's van de methodes toe te voegen aan Flask. Dit moet gebeuren wanneer het CRUD view Object wordt aangemaakt met de Flask add_url_rule() methode. Het CRUD view Object wordt aangemaakt bij de initialisatietijd. Dit betekent dat u url_for() hier niet kunt gebruiken omdat de routes op dit moment niet bekend zijn. Een andere uitdaging was het regelen van het aantal parameters dat moet worden doorgegeven tijdens het maken van het object. Dit zou minimaal moeten zijn! De klasse zou bijvoorbeeld de url's voor nieuw moeten aanmaken, bewerken en zelf moeten verwijderen van een basis-url. Ik heb een class CRUDView aangemaakt in een bestand class_crud_view.py dat er als volgt uitziet:
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 is een lijst omdat een methode meer dan één route kan hebben. Bijvoorbeeld de users lijstmethode heeft twee routes, een route zonder en een route met een paginanummer:
@admin_user_blueprint.route('/users/list', defaults={'page_number': 1})
@admin_user_blueprint.route('/users/list/<int:page_number>')
def users_list(page_number):
...
De lijstmethode moet flexibel zijn, ik moet velden, veldtypes en namen kunnen specificeren. De nieuwe, bewerkings- en verwijderingsmethoden zijn min of meer kopieën van bestaande methoden.
Uitvoering
Mijn implementatie werkt niet voor u omdat ik een paginafunctie gebruik, maar hier is mijn CRUD view class, ik heb enkele onderdelen weggelaten:
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 een Blueprint installeer ik de klasse als volgt:
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
)
Dat is het. Natuurlijk heb je de Jinja sjablonen nodig. De lijstmethode vereist nu een Jinja sjabloon dat de kolommen kan verwerken.
Het uitbreiden van de view class door methoden toe te voegen
Dit is allemaal heel eenvoudig, het is slechts een begin. Stel dat we willen dat de lijstfunctie op naam wordt gesorteerd, of wat dan ook. Dit kunnen we doen door de CRUDView class te wijzigen. We verplaatsen wat code van de lijstmethode en voegen deze toe aan een nieuwe methode '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)
...
In de Blauwdruk maken we een nieuwe klasse CityCRUDView die de CRUDView class erft en voegen we onze eigen 'list__get_items_for_page' toe. Vervolgens gebruiken we deze nieuwe klasse om het city_demo_crud_view object te installeren:
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
...
)
Nu zijn de steden gesorteerd op naam in plaats van op id. We kunnen meer methoden toevoegen aan de klasse CRUDView en deze overschrijven in onze Blauwdruk.
Samenvatting
Ik ben blij dat ik eindelijk de tijd heb genomen om een eerste versie van een CRUD model view class te implementeren. Het kostte me een paar dagen om alle stukken in elkaar te zetten, maar ik ben er zeker van dat deze tijd de moeite waard was. Ik gebruik het al in enkele Blueprints. U kunt kijken naar de Demo-sectie van deze site voor een werkend voorbeeld.
Links / credits
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/
Lees meer
Flask
Recent
- Database UUID primaire sleutels van je webapplicatie verbergen
- Don't Repeat Yourself (DRY) met Jinja2
- SQLAlchemy, PostgreSQL, maximum aantal rijen per user
- Toon de waarden in SQLAlchemy dynamische filters
- Veilige gegevensoverdracht met Public Key versleuteling en pyNaCl
- rqlite: een alternatief voor SQLite met hoge beschikbaarheid en distributed
Meest bekeken
- Met behulp van Python's pyOpenSSL om SSL-certificaten die van een host zijn gedownload te controleren
- Gebruik van UUIDs in plaats van Integer Autoincrement Primary Keys met SQLAlchemy en MariaDb
- Maak verbinding met een dienst op een Docker host vanaf een Docker container
- PyInstaller en Cython gebruiken om een Python executable te maken
- SQLAlchemy: Gebruik van Cascade Deletes om verwante objecten te verwijderen
- Flask RESTful API verzoekparametervalidatie met Marshmallow-schema's