Flaxon 0.3.0 and Teloce-Py 0.3.0 are available on PyPI. Release instructions are maintained on this site; source links identify the framework snapshot. Check version and upgrade guidance.
12 | Staff Admin
Goal
Create staff credentials and manage existing domain records with explicit permissions.
What you are building
The custom ORM adapters deliberately expose only approved project/task fields and enforce the application's ownership creation rules. Staff create customer-owned records through the customer workflow; permitted staff may inspect and edit existing records. Root admin.py describes registration for management checks; backoffice.py configures the actual custom dashboard adapters. This is an example of business-specific adapters. Flaxon also supports direct ORM registration for ordinary CRUD. Staff records use a separate durable AdminStore. setup-admin creates the first privileged staff account interactively, with no default password. Grant view/change/delete capabilities separately for ordinary staff.
Build this chapter
Stop the server. Work in project_manager. Follow each CREATE, whole-file replacement, or marked-block edit below in order.
Step 1: EDIT - replace the entire file - admin.py
Keep explicit registration in this file. At chapter 3 it is empty; chapter 12 adds the domain models.
Select all existing contents of this file and replace them with the complete code below.
"""Registration metadata checked by management.py; the custom backoffice uses this same schema."""
from models import Project, Task
def register(admin):
admin.register(
Project,
name="project",
fields=["name", "description"],
readonly_fields=["id", "owner_id"],
search_fields=["name"],
)
admin.register(
Task,
name="task",
fields=["title", "status", "due_date"],
readonly_fields=["id", "project_id"],
list_filter=["status"],
)
Say: "Keep explicit registration in this file. At chapter 3 it is empty; chapter 12 adds the domain models."
Type: Follow the action above in admin.py; explain each new function or binding as you add it.
Viewers should see: The saved file matches this step. After all steps, run the chapter checks below and show the expected result.
Step 2: CREATE - make parent folders, then create this file - backoffice.py
These adapters query the same ORM models as the API and expose a deliberate staff field whitelist.
Create the parent directories, then save this complete file.
"""Staff model adapters reuse the domain database; CMS owns only editorial content."""
from pathlib import Path
from models import Project, Task
from flaxon.admin import AdminDashboard, AdminConfig
from flaxon.admin.registry import Registry
from flaxon.admin.cms import CMS, ContentType, CMSField
from flaxon.exceptions import BadRequest
from validation import text, task_fields
def configure_backoffice(app, admin_path, uploads_path):
Path(admin_path).parent.mkdir(parents=True, exist_ok=True)
admin = AdminDashboard(
app,
registry=Registry(),
users=[],
storage_path=str(admin_path),
upload_dir=str(uploads_path),
strict_permissions=True,
microservices=False,
config=AdminConfig(site_title="Project Manager Operations"),
)
class ProjectAdmin:
@classmethod
async def get_instances(cls):
return await Project.all().order_by("-id").values()
@classmethod
async def get_instance(cls, object_id):
return await Project.filter(id=object_id).first().values()
@classmethod
async def create_instance(cls, data):
raise BadRequest(
"Create projects through the application to assign an owner."
)
@classmethod
async def update_instance(cls, object_id, data):
current = await cls.get_instance(object_id)
if current is None:
return None
name = text(data, "name")
description = text(data, "description", 1000, required=False)
await Project.filter(id=object_id).update(
name=name, description=description
)
return await cls.get_instance(object_id)
@classmethod
async def delete_instance(cls, object_id):
current = await cls.get_instance(object_id)
if not current:
return False
await Project.filter(id=object_id).delete()
return True
class TaskAdmin:
@classmethod
async def get_instances(cls):
return await Task.all().order_by("-id").values()
@classmethod
async def get_instance(cls, object_id):
return await Task.filter(id=object_id).first().values()
@classmethod
async def create_instance(cls, data):
raise BadRequest("Create tasks through their project in the application.")
@classmethod
async def update_instance(cls, object_id, data):
current = await cls.get_instance(object_id)
if current is None:
return None
values = {**current, **task_fields(data, partial=True)}
await Task.filter(id=object_id).update(
**{key: values[key] for key in ("title", "status", "due_date")}
)
return await cls.get_instance(object_id)
@classmethod
async def delete_instance(cls, object_id):
current = await cls.get_instance(object_id)
if not current:
return False
await Task.filter(id=object_id).delete()
return True
admin.register(
ProjectAdmin,
name="project",
list_display=["id", "name", "owner_id"],
search_fields=["name"],
fields=["name", "description"],
readonly_fields=["id", "owner_id"],
)
admin.register(
TaskAdmin,
name="task",
list_display=["id", "title", "status", "due_date"],
search_fields=["title"],
list_filter=["status"],
fields=["title", "status", "due_date"],
readonly_fields=["id", "project_id"],
)
cms = CMS(app, auth=admin.auth)
cms.register(
ContentType(
"help_article",
label="Help article",
label_plural="Help articles",
fields=[
CMSField("title", required=True),
CMSField("summary", type="textarea"),
CMSField("body", type="richtext"),
],
statuses=["draft", "published"],
list_display=["title", "status"],
)
)
cms.register(
ContentType(
"announcement",
fields=[
CMSField("title", required=True),
CMSField("body", type="textarea"),
],
statuses=["draft", "published"],
)
)
app.backoffice, app.cms = admin, cms
Say: "These adapters query the same ORM models as the API and expose a deliberate staff field whitelist."
Type: Follow the action above in backoffice.py; explain each new function or binding as you add it.
Viewers should see: The saved file matches this step. After all steps, run the chapter checks below and show the expected result.
Step 3: EDIT - replace the marked blocks - app.py
The application factory mounts only the features already introduced. Follow the action label: a marked-block edit keeps the surrounding factory intact.
Edit 1: find this exact block
Open app.py. Locate this block once. The unchanged surrounding lines identify its position; do not use line numbers from another revision.
from argon2 import PasswordHasher
from flaxon import Flaxon, Request
from flaxon.http import JSONResponse
from modules.auth.module import auth
from modules.projects.module import projects
from modules.tasks.module import tasks
Replace that block with
from argon2 import PasswordHasher
from flaxon import Flaxon, Request
from flaxon.http import JSONResponse
from backoffice import configure_backoffice
from modules.auth.module import auth
from modules.projects.module import projects
from modules.tasks.module import tasks
Leave the rest of the file unchanged.
Edit 2: find this exact block
Open app.py. Locate the management guard after app.use_teloce():
if app.is_management:
return app
@app.get("/health/course")
Replace that block with
if app.is_management:
return app
configure_backoffice(
app,
admin_path or ADMIN_DATABASE_PATH,
Path(admin_path or ADMIN_DATABASE_PATH).parent / "uploads",
)
@app.get("/health/course")
Leave the rest of the file unchanged.
Say: "The application factory mounts only the features already introduced. Follow the action label: a marked-block edit keeps the surrounding factory intact."
Type: Follow the action above in app.py; explain each new function or binding as you add it.
Viewers should see: The saved file matches this step. After all steps, run the chapter checks below and show the expected result.
Run and check
python management.py setup-admin
python management.py runserver
# Open /admin/login with the staff account you just created.
Expected result
Staff sign-in works at /admin/login. The model lists show the same projects and tasks used by the customer API. A public customer account cannot access staff tools.
Common errors
Role labels alone do not grant every capability. Use project.view_project and task.view_task for readers; add change permissions only when needed. These staff adapters are global operations tools, not a tenant-scoped customer portal.
Short exercise
Create a read-only staff user and prove a direct edit request is denied as well as hiding its edit controls.
What to say and show
Say: "Create staff credentials and manage existing domain records with explicit permissions. The server remains responsible for persistence and authorization; the browser presents the result."
Show the expected result above, then point to the file responsible for it. Run the chapter check before the next lesson.
Save your checkpoint
git add .
git commit -m "Complete chapter 12: Staff Admin"