Skip to content

Admin

What is the admin feature?

This is an Web-GUI to expose the database to the webbrowser. Admins can use the GUI to fix some problems or you can use it as an permissioned interface in your application.

Using Admin from cli

Use something like:

edgy admin_serve

or only if you want to test the feature:

edgy --app tests.cli.main admin_serve --create-all

and watch the console output for an automic generated password. It is required for accessing the admin. The default username is admin.

You can however customize them all via:

edgy admin_serve --auth-name=edgy --auth-pw=123

Warning

This only serves as an example. Please use stronger passwords! Your whole database is open this way!

Warning

Only use --create-all when not using migrations.

Limitations

Despite this is a quite non-invasive way to use the admin feature, you have a quite limited way of integration. There is no user management only a basic auth. For more

Embedding Admin

For embedding the admin you need a lilya session or something compatible (provide scope["session"] with a dict like interface). You can either declare a global session or provide a session_cookie name to prevent collisions.

Global session
from typing import Any
import edgy
from edgy.conf import settings
from lilya.context import request_context
from lilya.apps import Lilya
from lilya.middleware import DefineMiddleware
from lilya.requests import Connection
from lilya.authentication import AuthenticationBackend, AuthCredentials, UserInterface, AuthResult
from lilya.middleware.sessions import SessionMiddleware
from lilya.middleware.session_context import SessionContextMiddleware
from lilya.middleware.authentication import AuthenticationMiddleware
from lilya.middleware.request_context import RequestContextMiddleware
from lilya.routing import Include
from edgy.contrib.admin import create_admin_app

models = edgy.Registry(
    database="...",
)


class User(UserInterface, edgy.Model):
    username: str = edgy.fields.CharField(max_length=100, unique=True)
    active: bool = edgy.fields.BooleanField(default=False)


def get_application() -> Any:
    admin_app = create_admin_app()
    # or with lilya 0.15.5
    # admin_app = create_admin_app(session_sub_path="admin")
    routes = [
        Include(
            path=settings.admin_config.admin_prefix_url,
            app=admin_app,
        ),
    ]
    app: Any = Lilya(
        routes=routes,
        middleware=[
            # you can also use a different secret_key aside from settings.admin_config.SECRET_KEY
            DefineMiddleware(SessionMiddleware, secret_key=settings.admin_config.SECRET_KEY),
        ],
    )
    app = models.asgi(app)
    edgy.monkay.set_instance(edgy.Instance(registry=models, app=app))
    return app


application = get_application()
Different cookie
from typing import Any
import edgy
from edgy.conf import settings
from lilya.apps import Lilya
from lilya.middleware import DefineMiddleware
from lilya.middleware.sessions import SessionMiddleware
from lilya.routing import Include
from edgy.contrib.admin import create_admin_app

models = edgy.Registry(
    database="...",
)


class User(edgy.Model):
    username: str = edgy.fields.CharField(max_length=100, unique=True)
    active: bool = edgy.fields.BooleanField(default=False)


def get_application() -> Any:
    admin_app = create_admin_app()
    routes = [
        Include(
            path=settings.admin_config.admin_prefix_url,
            app=admin_app,
            middleware=[
                DefineMiddleware(
                    SessionMiddleware,
                    secret_key=settings.admin_config.SECRET_KEY,
                    session_cookie="admin_session",
                ),
            ],
        ),
    ]
    app: Any = Lilya(
        routes=routes,
    )
    app = models.asgi(app)
    edgy.monkay.set_instance(edgy.Instance(registry=models, app=app))
    return app


application = get_application()

You can multiplex the session via sub_path (will probably land in lilya 0.15.5)

Multiplexed
from typing import Any
import edgy
from edgy.conf import settings
from lilya.apps import Lilya
from lilya.middleware import DefineMiddleware
from lilya.middleware.sessions import SessionMiddleware
from lilya.middleware.session_context import SessionContextMiddleware
from lilya.routing import Include
from edgy.contrib.admin import create_admin_app

models = edgy.Registry(
    database="...",
)


class User(edgy.Model):
    username: str = edgy.fields.CharField(max_length=100, unique=True)
    active: bool = edgy.fields.BooleanField(default=False)


def get_application() -> Any:
    # multiplexed with lilya 0.15.5
    admin_app = create_admin_app(session_sub_path="admin")
    routes = [
        Include(
            path=settings.admin_config.admin_prefix_url,
            app=admin_app,
        ),
    ]
    app: Any = Lilya(
        routes=routes,
        middleware=[
            # you can also use a different secret_key aside from settings.admin_config.SECRET_KEY
            DefineMiddleware(SessionMiddleware, secret_key=settings.admin_config.SECRET_KEY),
            DefineMiddleware(SessionContextMiddleware),
        ],
    )
    app = models.asgi(app)
    edgy.monkay.set_instance(edgy.Instance(registry=models, app=app))
    return app


application = get_application()

By default the admin_prefix_url is automatically inferred. If you want an explicit url for the admin, you might want to set it.

Excluding models

Just set in Meta, the flag in_admin to False. This flag is inherited. If you just want to exclude models from the creation you can set no_admin_create to True.

Both flags can be just inherited by using a value of None. The default behaviour for in_admin (when nowhere in the hierarchy a value was set) is True. For no_admin_create it is False.

Example:

By default ContentType is in_admin but no_admin_create is true, so we cannot create a new instance.

For m2m models in_admin defaults to false and no_admin_create to true.

We can however change this:

Creatable ContentType
import edgy
from edgy.contrib.contenttypes import ContentType as BaseContentType


class ContentType(BaseContentType):
    system_message: str = edgy.fields.CharField(default="", max_length=20)

    class Meta:
        abstract = True
        # we can create it now
        no_admin_create = False

    @classmethod
    def get_admin_marshall_config(cls, *, phase: str, for_schema: bool) -> dict:
        return {"exclude": ["system_message"]}


models = edgy.Registry(
    database="...",
    with_content_type=ContentType,
)

Customizing model admins

One nice feature of edgy admin is the optional customization of the admin interface.

By default it doesn't care for user permissions but can be adapted to do so on a per model base. Here it is quite unoppinionated. You can use every connection information you like for adding or removing fields.

For example the request.user when embedding the admin in a bigger application.

You can use user attributes or permissions (when used with a user setup) or simply check the connection.

Permission example
from typing import Any
import edgy
from edgy.conf import settings
from lilya.context import request_context
from lilya.apps import Lilya
from lilya.middleware import DefineMiddleware
from lilya.requests import Connection
from lilya.authentication import AuthenticationBackend, AuthCredentials, UserInterface, AuthResult
from lilya.middleware.sessions import SessionMiddleware
from lilya.middleware.session_context import SessionContextMiddleware
from lilya.middleware.authentication import AuthenticationMiddleware
from lilya.middleware.request_context import RequestContextMiddleware
from lilya.routing import Include
from edgy.contrib.admin import create_admin_app

models = edgy.Registry(
    database="...",
)


class User(UserInterface, edgy.Model):
    username: str = edgy.fields.CharField(max_length=100, unique=True)
    active: bool = edgy.fields.BooleanField(default=False)
    is_admin: bool = edgy.fields.BooleanField(default=False)
    # we still need an authenticating backend checking the pw
    pw: str = edgy.fields.PasswordField()

    class Meta:
        registry = models

    @classmethod
    def get_admin_marshall_config(cls, *, phase: str, for_schema: bool) -> dict:
        exclude = []
        # Works only when embedding, by default we have no request.user.and request_context
        user = request_context.user
        if not user.is_authenticated or not user.is_admin:
            exclude.append("is_admin")
            if phase == "update":
                exclude.append("name")

        return {"exclude": exclude}


class SessionBackend(AuthenticationBackend):
    async def authenticate(self, connection: Connection) -> AuthResult | None:
        if not connection.scope["session"].get("username", None):
            return None
        user = await User.query.get(username=connection.scope["session"]["username"])
        if user:
            return AuthCredentials(["authenticated"]), user
        return None


def get_application() -> Any:
    admin_app = create_admin_app()
    # or with lilya 0.15.5
    # admin_app = create_admin_app(session_sub_path="admin")
    routes = [
        Include(
            path=settings.admin_config.admin_prefix_url,
            app=admin_app,
        ),
    ]
    app: Any = Lilya(
        routes=routes,
        middleware=[
            # you can also use a different secret_key aside from settings.admin_config.SECRET_KEY
            DefineMiddleware(SessionMiddleware, secret_key=settings.admin_config.SECRET_KEY),
            DefineMiddleware(SessionContextMiddleware),
            DefineMiddleware(AuthenticationMiddleware, backend=[SessionBackend]),
            DefineMiddleware(RequestContextMiddleware),
        ],
    )
    app = models.asgi(app)
    edgy.monkay.set_instance(edgy.Instance(registry=models, app=app))
    return app


application = get_application()

A word of customization

Unlike django admin we don't pass around the contexts through the functions (despite there are some small exceptions like phase and for_schema). We use ContextVars instead. This allows us to keep the code lean and composable. So if you need some references to e.g. the Request or Connection you will need to use the RequestContextMiddleware.

Hooks in detail

  • get_admin_marshall_config(cls, *, phase, for_schema=False) -> dict: - Customize quickly the marshall_config of the generated Marshall. Use this for excluding fields depending on the phase.
  • get_admin_marshall_class(cls, *, phase, for_schema=False) -> type[Marshall] - Customize the whole marshall. This allows replacing the Marshall completely, adding some fields and other goodies.
  • get_admin_marshall_for_save(cls, instance= None, /, **kwargs) -> Marshall - Classmethod called for getting the final marshall to save. Kwargs contains all the kwargs provided by the extraction. You might can build some customization around the saving here.

Here an simpler example how to use this:

Basic customization example
import edgy
from typing import ClassVar
from pydantic import ConfigDict

models = edgy.Registry(
    database="...",
)


class User(edgy.Model):
    name: str = edgy.fields.CharField(max_length=100, unique=True)
    active: bool = edgy.fields.BooleanField(default=False)

    class Meta:
        registry = models

    @classmethod
    def get_admin_marshall_config(cls, *, phase: str, for_schema: bool) -> dict:
        return {"exclude": ["name"] if phase == "update" else []}

    @classmethod
    def get_admin_marshall_class(
        cls: type[edgy.Model], *, phase: str, for_schema: bool = False
    ) -> type[edgy.marshalls.Marshall]:
        """
        Generate a marshall class for the admin.

        Can be dynamic for the current user.
        """

        class AdminMarshall(edgy.marshalls.Marshall):
            # forbid triggers additionalProperties=false
            model_config: ClassVar[ConfigDict] = ConfigDict(
                title=cls.__name__, extra="forbid" if for_schema else None
            )
            marshall_config = edgy.marshalls.ConfigMarshall(
                model=cls,
                **cls.get_admin_marshall_config(phase=phase, for_schema=for_schema),  # type: ignore
            )

        return AdminMarshall

Admin marshall phase and for_schema parameters

The phase parameter can contain following values: - list: Marshall used for the model list representation in admin. Only for viewing. - view: Marshall used for the model detail view representation in admin. Only for viewing. - create: Marshall used for creating new model instances in admin (when saving). You may can remove some fields. - update: Marshall used for updating model instances in admin (when saving). You may can remove some fields.

The for_schema parameter contains the information if the marshall is used for a json_schema or for validation. When used for a json_schema, we change in model_config the parameter extra to forbid so no arbitary editors are shown.

Customizing the admin templates

You can customize the admin templates by providing admin_extra_templates to settings.admin_config. All templates specified here will be loaded first.