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

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.

24 maart 2020
In Flask
post main image
https://unsplash.com/@ostshem

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

Laat een reactie achter

Reageer anoniem of log in om commentaar te geven.

Opmerkingen

Laat een antwoord achter

Antwoord anoniem of log in om te antwoorden.