"""Automatic admin and CRUD for SQLAlchemy models."""
# Standard Library
import string
import typing as t
# Pyramid
import venusian
from pyramid.interfaces import IRequest
# Websauna
from websauna.system.admin.interfaces import IAdmin
from websauna.system.admin.interfaces import IModelAdmin
from websauna.system.core.traversal import Resource
from websauna.system.crud.sqlalchemy import CRUD as CRUD
from websauna.system.crud.sqlalchemy import Resource as AlchemyResource
# We enforce some best practices to readable URL names of model admins. This is an arbitrary choice of the author.
ALLOWED_TRAVERSE_ID_CHARACTERS = string.ascii_lowercase + string.digits + "-"
[docs]class ModelAdmin(CRUD):
"""Resource presenting one model in admin interface.
Provide automatized list, show add, edit and delete actions for an SQLAlchemy model which declares admin interface.
"""
#: Title used in breadcrumbs, other places
title = None
#: Model must be set by subclass
model = None
def __init__(self, request):
super(ModelAdmin, self).__init__(request)
[docs] def get_model(self):
assert self.model, "Model must be set by a subclass as a class attribute"
return self.model
#: Our resource factory
[docs] class Resource(AlchemyResource):
[docs] def get_admin(self):
return self.__parent__.get_admin()
[docs] def get_admin(self) -> IAdmin:
"""Get Admin resource object."""
return self.__parent__.__parent__
[docs] def get_title(self) -> str:
if self.title:
return self.title
return self.id.capitalize()
[docs]class ModelAdminRoot(Resource):
"""Admin resource under which all model admins lurk.
To get access this resource:
"""
[docs] def get_title(self):
return "Models"
[docs] def get_model_admins(self):
"""List all registered model admin classes.
:yield: (model_id, IModelAdmin) tuples
"""
for model_id, model_cls in self.request.registry.getAdapters([self.request], IModelAdmin):
yield(model_id, model_cls)
def __getitem__(self, item):
"""Traverse to model admins. """
registry = self.request.registry
model_admin_resource = registry.queryAdapter(self.request, IModelAdmin, name=item)
if not model_admin_resource:
raise RuntimeError("Did not find model admin with id: {}".format(item))
Resource.make_lineage(self, model_admin_resource, item)
return model_admin_resource
[docs] def items(self) -> t.List[t.Tuple[str, ModelAdmin]]:
for id, model_cls in self.get_model_admins():
yield id, self[id]
[docs]def model_admin(traverse_id: str) -> type:
"""Class decorator to mark the class to become part of model admins.
``Configure.scan()`` must be run on this module for the implementation to be picked up.
If there is already an existing model admin with same ``model``, then the existing model admin is overwritten.
:param traverse_id: Under which URL id this model appears in the admin interface. Allowed to contain lowercase letters, dash and digits. This will be available as ``ModelAdmin.__name__`` instance attribute.
:param model_cls: Which model class this admin resource is controlling
"""
assert all(c in ALLOWED_TRAVERSE_ID_CHARACTERS for c in traverse_id), "traverse_id may only contain lowercase letters, digits and a dash: {}".format(traverse_id)
def _inner(cls):
"The class decorator example"
def register(scanner, name, wrapped):
config = scanner.config
# We can look up midels by
model = getattr(cls, "model", None)
assert model, "Class {} must declare model attribute".format(cls)
registry = config.registry
# Purge existing model admin
registry.unregisterAdapter(required=(IRequest,), provided=IModelAdmin)
registry.registerAdapter(cls, required=(IRequest,), provided=IModelAdmin, name=traverse_id)
registry.model_admin_ids_by_model[model] = traverse_id
venusian.attach(cls, register, category='websauna')
return cls
return _inner