Aiohttp admin documentation

Demo site | Demo source code.

https://img.shields.io/pypi/v/aiohttp_admin2.svg https://github.com/Arfey/aiohttp_admin2/actions/workflows/tests.yaml/badge.svg?branch=master Documentation Status Updates PRs Welcome

The aiohttp admin is a library for build admin interface for applications based on the aiohttp. With this library you can ease to generate CRUD views for your data (for data storages which support by aiohttp admin) and flexibly customize representation and access to these.

_images/index.png

Installation

The first step which you need to do it’s installing library

pip install aiohttp_admin2

If you need more detail information about installation look at Installation section.

Quick start

For simple start you need just import setup admin function and extend your existing aiohttp application. For example:

admin.py

from aiohttp import web
from aiohttp_admin2 import setup_admin

app = web.Application()

# setup admin interface
setup_admin(app)

web.run_app(app)

And run python admin.py. That is it. Now you can open in your browser http://localhost:8080/admin/ and see home page of the our awesome admin interface.

_images/simple_example.png

Dashboard


The first page which you see when setup admin is dashboard. This is startup page and you can to customize it. For that u need to create your custom dashboard’s class

from aiohttp import web
from aiohttp_admin2.views import DashboardView


class CustomDashboard(DashboardView):
    async def get_context(self, req):
        return {
            **await super().get_context(req=req),
            "content": "My custom content"
        }

You can rewrite get_context method and put your new content to the jinja context. After that we need to create your custom admin class and put it into setup function:

from aiohttp import web
from aiohttp_admin2 import setup_admin
from aiohttp_admin2.views import Admin


class CustomAdmin(Admin):
    dashboard_class = CustomDashboard

app = web.Application()

# setup admin interface
setup_admin(app, admin_class=CustomAdmin)

web.run_app(app)
_images/custom_context.png

As the alternative way we can to redefine the template for the our dashboard. The first thing which we need to do is create a new dashboard template.

templates/my_custom_dashboard.html

{% extends 'aiohttp_admin/layouts/base.html' %}

{% block main %}
  <h1>Dashboard</h1>
  <b>{{ content }}...</b>
{% endblock main %}

we nested from the base html template and put to the main block our new content. The second step is declare this template in the CustomDashboard.

class CustomDashboard(DashboardView):
    # redefine `template_name` attribute to your own
    template_name = 'my_custom_dashboard.html'
    ...

The last step is setup jinja for your application and set path to the your templates directory

import aiohttp_jinja2
import jinja2
from pathlib import Path

# path to the your template directory
templates_directory = Path(__file__).parent / 'templates'

app = web.Application()

# setup jinja2
aiohttp_jinja2.setup(
    app=application,
    loader=jinja2.FileSystemLoader(str(templates_directory)),
)

# setup admin interface
setup_admin(app, admin_class=CustomAdmin)

web.run_app(app)

As result you can see that dashboard use your custom html.

_images/custom_template_name_dashboard.png

Custom views


The next thing which you can to do with your admin interface is create your own custom view. For that you need just create a new view and setup it together with admin interface. After that you can to see a new tab in the aside bar.

from aiohttp_admin2.views.aiohttp.views.template_view import TemplateView


class FirstCustomView(TemplateView):
    name = 'Template view'


# setup admin interface
setup_admin(
    application,
    admin_class=CustomAdmin,
    # put here your new template view to register it
    views=[FirstCustomView,]
)

The DashboardView class nested from the TemplateView class so you can do with it all things which we considered above for DashboardView class (redefine context and template).

_images/template_page.png

If you don’t want add current web page to the aside bar then you can specify is_hide_view attribute to True.

class FirstCustomView(TemplateView):
    name = 'Template view'
    # remove link from aside bar
    is_hide_view = True

In this case you can to visit this web page got to the directly via url but admin interface will not to show any links to it.

CRUD views


The most helpful thing in the aiohttp_admin2 is possible to generate views based on models on your data from different databases.

Right now the library support models for:

  • SQLAlchemy (postgres/mysql)
  • umongo (mongodb)

So, if you have a above models then you can easy add create/delete/update views in the your admin interface. Let’s consider a simple example how it might looks like for the SQLAlchemy.

Let’s assume we have current SQLAlchemy’s models:

from enum import Enum

import sqlalchemy as sa


metadata = sa.MetaData()


class PostStatusEnum(Enum):
    published = 'published'
    not_published = 'not published'


users = sa.Table('users', metadata,
    sa.Column('id', sa.Integer, primary_key=True),
    sa.Column('first_name', sa.String(255)),
    sa.Column('last_name', sa.String(255)),
    sa.Column('is_superuser', sa.Boolean),
    sa.Column('joined_at', sa.DateTime()),
)

books = sa.Table('post', metadata,
    sa.Column('id', sa.Integer, primary_key=True),
    sa.Column('title', sa.String(255)),
    sa.Column('body', sa.Text),
    sa.Column('status', sa.Enum(PostStatusEnum)),
    sa.Column('published_at', sa.DateTime()),
    sa.Column('author_id', sa.ForeignKey('users.id', ondelete='CASCADE')),
)

We have the simple users table and the posts table where allocated posts which have been created via our users. Users and posts tables related by foreignKey author_id.

The first thing which we need to do before start to generate CRUD views is set up the PostgreSQL connection in the our application

import aiopg.sa
from aiohttp_admin2.connection_injectors import ConnectionInjector


postgres_injector = ConnectionInjector()


async def init_db(app):
    engine = await aiopg.sa.create_engine(
        user='postgres',
        database='postgres',
        host='0.0.0.0',
        password='postgres',
    )
    app['db'] = engine

    # set our engine to the postgres_injector
    postgres_injector.init(engine)

    yield

    app['db'].close()
    await app['db'].wait_closed()

application = web.Application()
application.cleanup_ctx.extend([init_db])

It’s a standard way to set up connection to the database into aiohttp but we have new lines related with ConnectionInjector. ConnectionInjector is just a class which used to share database connection with between aiohttp and admin library.

The second thing is create registered our model for the admin.

from aiohttp_admin2.views import ControllerView
from aiohttp_admin2.controllers.postgres_controller import PostgresController
from aiohttp_admin2.mappers.generics import PostgresMapperGeneric


# create a mapper for table
class UserMapper(PostgresMapperGeneric, table=users):
    pass


# create controller for table with UserMapper
@postgres_injector.inject
class UserController(PostgresController, table=users):
    mapper = UserMapper
    name = 'user'


# create view for table
class UserView(ControllerView):
    controller = UserController

And after that setup our admin interface with the UserView.

# setup admin interface
setup_admin(
    application,
    admin_class=CustomAdmin,
    views=[
        FirstCustomView,
        UserView, # added new view
    ]
)

Now, you can to see that admin interface has new tab in the aside bar and we have simple list, create, update and delete pages for the our user model.

List page

_images/simple_list.png

Create page

_images/simple_create.png

Update page

_images/simple_update.png

Delete page

_images/simple_delete.png

Let’s make our list page view a little bit better. For that we can to show to user more information using inline_fields attribute in the UserController class.

@postgres_injector.inject
class UserController(PostgresController, table=users):
    mapper = UserMapper
    name = 'user'

    inline_fields = ['id', 'full_name', 'is_superuser', 'joined_at']

Our table has id, is_superuser and joined_at but don’t has full_name but for end user we want to show full name. For do that we can to use custom filed and add to our class the full_name_field method (<field_name>_field).

@postgres_injector.inject
class UserController(PostgresController):

    ...

    async def full_name_field(self, obj):
        return f'{obj.data.first_name} {obj.data.last_name}'

Also we want to give a possible to user use search by fist_name and last_name fields. We can easy do that by add search_fields attribute and specify list of fields which we want to use to search.

@postgres_injector.inject
class UserController(PostgresController):

    ...

    search_fields = ['first_name', 'last_name']

And as final step we want to give a possible to filter a list of our data. For achieve this goal we need only specify list of fields which will use for filtering to the list_filter attribute.

@postgres_injector.inject
class UserController(PostgresController):

    ...

    list_filter = ['joined_at', 'is_superuser', ]

After that view of our list page become much better

_images/full_controller_example.png

Let’s make the same for the post model

# create controller for table with UserMapper
@postgres_injector.inject
class PostController(PostgresController, table=post):
    mapper = PostMapper
    name = 'post'

    inline_fields = ['id', 'title', 'published_at', 'author_id', ]
    search_fields = ['title', ]
    list_filter = ['status', 'author_id', ]

    async def title_field(self, obj):
        if len(obj.data.title) > 10:
            return f'{obj.data.title[:10]}...'
        return obj.data.title


# create view for table
class PostView(ControllerView):
    controller = PostController

The post table has title field but we want to change view of this field. In this cases we can also to use the custom field (title_field).

_images/post_controller_first.png

Okay. We has a representation of the post and the user models. This models have relations between each other and we need to show it in the admin interface.

First relation is one to one relation between post and author. Single post has only one author. To show this relation we can to specify relations_to_one attribute.

from aiohttp_admin2.controllers.relations import ToOneRelation


@postgres_injector.inject
class PostController(PostgresController):

    ...

    relations_to_one = [
        ToOneRelation(
            # the field name of current relation
            name='author_id',
            # the name of field which responsible for relation (foreignkey)
            field_name='author_id',
            # controller of the relation model
            controller=UserController
        ),
    ]


@postgres_injector.inject
class UserController(PostgresController):

    ...

    async def get_object_name(self, obj):
        # need just for better representation instances of current model
        return obj.data.first_name

In ToOneRelation we put name of field which will represented current relation (in our case we replace existing author_id field) and field_name field in table which responsible for current relation and controller of the relation model. After that we has link to the relational model on list page and autocomplete on create/update page.

_images/one_to_one_relation_list.png _images/one_to_one_relation_autocomplete.png

The second relation is one to many relation between auth and posts. User can has many posts. To show this relation we can to specify relations_to_many attribute.

from aiohttp_admin2.controllers.relations import ToManyRelation

@postgres_injector.inject
class UserController(PostgresController):

    ...

    relations_to_many = [
        ToManyRelation(
            # the name of current relation
            name='user posts',
            # the name of field which responsible for relation in the
            # current table
            left_table_pk='id',
            # controller of the relation model (we can use controller
            # class or callable function which return it).
            relation_controller=lambda: PostController,
        )
    ]

In ToManyRelation we put name which will use as title of the current relation, left_table_pk which describe field which responsible for relation between tables and relation_controller which receive the controller class of related model. After that we’ll have a tab bar on detail page of author model. On this tab we see all post of the current user.

_images/one_to_one_example.png

We are continuing improve admin interface and next step is customize detail page of the post model. The first thing which we can to improve its add html editor for the body field. Widgets responsible for view of input on detail page and we can change these for some particular type of field or for concretical field.

from aiohttp_admin2.views import widgets


# create view for table
class PostView(ControllerView):
    controller = PostController

    fields_widgets = {'body': widgets.CKEditorWidget}

Two lines of code and we have a html editor for body field.

_images/body_field_html.png

Also let’s assume that we need to add some validation on create/update post in the admin interface. We can to do that via mappings. Mapping responsible for validation of data in aiohttp admin.

class PostMapper(PostgresMapperGeneric, table=post):
    title = fields.StringField(
        required=True,
        validators=[length(min_value=10)],
    )

The PostgresMapperGeneric mapper generate all fields of table but if we need specify for these fields some properties we need to redefine these. We just redefined title field to make it required and add validator to avoide cases when title will less then 10 symbol. Now, if we try to create a post with small title that we’ll see an error message.

_images/validation_error_example.png

Authorization


The last thing which we need to add to complete our admin interface is authorization. For that we’ll use aiohttp_security and aiohttp_session libs. As a first step let’s setup aiohttp_security.

import base64
from aiohttp_security import AbstractAuthorizationPolicy
from aiohttp_security import SessionIdentityPolicy
from aiohttp_security import setup as setup_security
from aiohttp_session.cookie_storage import EncryptedCookieStorage
from aiohttp_session import setup as session_setup
from cryptography import fernet


class AuthorizationPolicy(AbstractAuthorizationPolicy):
    async def permits(self, identity, permission, context=None) -> bool:
        if identity == 'admin' and permission == 'admin':
            return True

        return False

    async def authorized_userid(self, identity) -> int:
        return identity


async def security(application: web.Application) -> None:
    fernet_key = fernet.Fernet.generate_key()
    secret_key = base64.urlsafe_b64decode(fernet_key)

    session_setup(
        application,
        EncryptedCookieStorage(secret_key, cookie_name='API_SESSION'),
    )

    policy = SessionIdentityPolicy()
    setup_security(application, policy, AuthorizationPolicy())

    yield


application = web.Application()
application.cleanup_ctx.extend([init_db, security])

After that let’s create a login/logut pages.

login.html

<form
    method="POST"
    action="{{ url('login_post') }}"
>
    <label for="username">Username</label>
    <input type="text" name="username" id="username" value="admin">
    <label for="password">Password</label>
    <input type="password" name="password" id="password" value="admin">
    <button type="submit">Submit</button>
</form>
import aiohttp_jinja2
from aiohttp import web
from aiohttp_security import is_anonymous
from aiohttp_security import permits
from aiohttp_security import remember
from aiohttp_security import forget


@aiohttp_jinja2.template('login.html')
async def login_page(request: web.Request) -> None:
    if not await is_anonymous(request):
        raise web.HTTPFound('/admin/')


@aiohttp_jinja2.template('login.html')
async def login_post(request: web.Request) -> None:
    data = await request.post()

    if data['username'] == 'admin' and 'admin' == data['password']:
        admin_page = web.HTTPFound('/admin/')
        await remember(request, admin_page, 'admin')
        raise admin_page

    raise web.HTTPFound('/login')


async def logout_page(request: web.Request) -> None:
    redirect_response = web.HTTPFound('/login')
    await forget(request, redirect_response)
    raise redirect_response


# added these handlers to the web application

application.add_routes([
    web.get('/login', login_page, name='login'),
    web.post('/login', login_post, name='login_post'),
    web.get('/logout', logout_page, name='logout')
])

All these steps are not related with aiohttp admin and can be different in other project so we avoid to explain these (this is naive implementation of authorization, please not use it in production).

As the last step we need to implement method for admin interface which will detect user with access to admin interface. For these purposes we’ll use middleware

import aiohttp_jinja2
from aiohttp import web
from aiohttp_security import is_anonymous
from aiohttp_security import permits


@web.middleware
async def admin_access_middleware(request, handler):
    if await is_anonymous(request):
        raise web.HTTPFound('/')

    if not await permits(request, 'admin'):
        raise web.HTTPFound('/')

    return await handler(request)

# add current middleware to the admin setup

setup_admin(
    application,
    admin_class=CustomAdmin,
    views=[FirstCustomView, UserView, PostView],
     # add middleware for admin
    middleware_list=[admin_access_middleware, ],
    # set logout path
    logout_path='/logout'
)

After that unauthorized users will not access to admin interface. If you need to give access only for particular model, you can use access_hook method in view class (read detail in the docs).

The source code of current examples you might to find here.

Indices and tables