Skip to content

Custom Pages (non-CRUD features)

Not everything is "list/create/edit/delete a table row." For a feature like an AI-powered PDF extractor, a report generator, or a bulk-import tool, use kloudmint's RBACBaseView -- it adds a page with its own route and template, shown in the sidebar as its own menu item, with the same automatic RBAC gating RBACModelView gives CRUD screens.

1. Define the page

# admin_views.py
from kloudmint import RBACBaseView
from sqladmin import expose
from fastapi import Request

class PdfExtractorAdmin(RBACBaseView):
    name = "PDF Extractor"
    icon = "fa-solid fa-file-pdf"
    # resource_name defaults to "PdfExtractor" (class name minus trailing "Admin")
    # rbac_actions defaults to ["use"] -- shows up in the permission grid as one checkbox

    @expose("/pdf-extractor", methods=["GET", "POST"])
    async def pdf_extractor_page(self, request: Request):
        if request.method == "POST":
            form = await request.form()
            file = form["file"]
            contents = await file.read()
            result = your_ai_pdf_extractor(contents)   # call your own AI service here
            return await self.templates.TemplateResponse(
                request, "pdf_extractor.html", {"result": result}
            )
        return await self.templates.TemplateResponse(
            request, "pdf_extractor.html", {"result": None}
        )

self.templates looks up templates the same way sqladmin's own pages do -- put pdf_extractor.html in a templates/ directory registered with your FastAPI app (a small upload <form> plus wherever you render result).

Shared resource names

When two custom pages should share one permission (e.g. a multi-step wizard), set the same resource_name on both:

class ResumeDraftsAdmin(RBACBaseView):
    resource_name = "ResumeIntake"
    rbac_actions = ["use"]

2. Register it

# main.py
admin.register(PdfExtractorAdmin)

Kloudmint.register() mounts the page and registers it in the permission dropdown automatically -- no separate register_resource() call needed.

A plain sqladmin BaseView (without RBAC) still goes through admin.raw.add_base_view() plus a manual register_resource() call; prefer RBACBaseView unless you have a reason not to.

3. RBAC gating

RBACBaseView implements is_accessible and is_visible using has_permission(request, resource_name, "use"), so the sidebar hides the page and direct URL visits are blocked for roles without the permission.

Important: is_accessible / is_visible must be plain sync def methods, not async def. SQLAdmin calls them without await; an async def returns a coroutine object instead of a bool, and the check silently never denies anything (the same bug this fix avoided in RBACModelView -- see src/kloudmint/rbac.py's module docstring).

If a page has multiple actions behind one route (e.g. a POST that only some "use"-granted users should trigger), add an extra has_permission() check inside that handler.

More building blocks

Related parent/child tables, file uploads, audit trails, detail pages, and column formatters are covered in BuildingBlocks.md.