angle-up arrow-clockwise arrow-counterclockwise arrow-down-up arrow-left at calendar card-list chat check envelope folder house info-circle pencil people person person-plus phone plus question-circle search tag trash x

Eliminieren Sie Wiederholungen und verbessern Sie die Wartung durch die Erstellung eines Flask view class

24 März 2020 durch Peter
In Flask

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.

post main image
https://unsplash.com/@ostshem

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

Einen Kommentar hinterlassen

Kommentieren Sie anonym oder melden Sie sich zum Kommentieren an.

Kommentare

Eine Antwort hinterlassen

Antworten Sie anonym oder melden Sie sich an, um zu antworten.