Philosophy¶
Faster Django development by getting more out of less. djcrud is not a parallel web stack — it is a thin MVC layer on top of Django that removes repetitive wiring while keeping Django’s models, permissions, and generic views.
Structure is code, not configuration¶
Routing is declared by nesting routers and views, not by hand-editing
urls.py for every endpoint. A ModelRouter on a model
gives you list, detail, create, update, delete, and bulk delete. URL segments,
names, and nesting follow from class names and conventions.
Each app appends routers to djcrud.site in djcrud.py (site.routes.append(...)).
build() autodiscovers those modules — the same “drop a
module in each app” pattern as Django admin’s admin.py.
Sane defaults, surgical overrides¶
Defaults should work on day one. Customization should be local and explicit:
Replace a default view by registering another route with the same codename
Extend with
ModelRouter.routes + [MyView]Tweak at runtime:
site.routes['delete'] = MyDelete.clone(...)Use
clone()to specialize without new module-level classes
The registry is an ordered override table, not a pile of magic.
The view is the template API¶
Ain’t no way I’m defining
get_context_datafor everything.
Add a method or property on the view → use {{ view.something }} in the
template. TemplateMixin puts view in
context by default. Template logic stays on the view class, not scattered
across context dicts and one-off template tags.
Template power without template-tag sprawl¶
Ain’t no way I’m defining a templatetag for everything.
Django templates get Jinja-like freedom via {% eval %} (see
Template tags), plus filters like html_attributes and
unpoly_attributes. Call view methods from templates; render attribute dicts
without bespoke tags for every case.
One permissions framework, every surface¶
Permissions always go through djcrud’s registry — not ad hoc checks in
templates, views, serializers, or future tool handlers. Register rules once in
each app’s djcrud.py with add_perm() and
add_queryset(); build() imports them
automatically.
The same rules drive every CRUD surface you hang off the route tree:
HTML —
has_permission()before dispatch; menus viaget_tagged_views()API — djcrud_drf ViewSets call the same registry through
has_permission()MCP and other agents — djcrud_mcp mirrors registered
ModelViewSetCRUD automatically; Bearer HTTP hits the same registry as HTML/API — no per-action decorators, no parallel auth stack (see djcrud_mcp design)
Your application code lives inside this framework: predicates such as
superuser(), authenticated(), is_owner(),
any_of(), and all_of() compose the checks you
register. Override a router or view only when a route needs a one-off escape
hatch.
Secure by default: superuser or nothing¶
Secure by default means superuser or nothing until you explicitly open
access. A freshly registered ModelRouter does not grant
CRUD to regular users, anonymous visitors, or API tokens — only superusers pass
until you add rules.
Every view checks permissions before dispatch: anonymous users go to login; authenticated users who fail the check get 403. Navigation, object menus, and list-action bars call the same checks. For list actions with per-row permissions the checkboxes and bar buttons are filtered client-side so the UI only offers actions the user may perform on the selected rows.
Open access deliberately in djcrud.py:
import djcrud
from djcrud.permissions import authenticated, is_owner, superuser, any_of
# Grant list/detail to any logged-in user on this router
djcrud.permissions.add_perm(ItemRouter, "view", check=authenticated)
# Writes: owner or superuser
djcrud.permissions.add_perm(
ItemRouter,
"change",
check=any_of(superuser, is_owner),
)
# Narrow row visibility for writes (see tutorial/permission)
djcrud.permissions.add_queryset(Item, "change", scoper=my_change_queryset)
Model routers delegate to the registry:
has_permission()— who may perform each actionget_queryset()— which rows exist for that user
Lists, object views, bulk actions, and API list endpoints share those querysets. PKs outside the scoped queryset → 404, not a leak.
See Permissions for owner-based add_perm / add_queryset
examples.
Basic template you can copy¶
The main goal for code sharing is a small, secure shell you inherit instead
of re-wiring urls.py, permissions, and navigation in every project.
Standard pages use djcrud/base.html: top navbar, sidebar built from
navigation tags, and [up-main] for content. SPA pages subclass
SPAView and use djcrud/base_spa.html — same
sidebar navigation, full-screen client mount, unpoly_target = 'body'.
Both shells plug into the same permissions framework: dispatch runs
has_permission() before rendering (superuser or
nothing until you register grants); navigation lists only permitted views (see
get_tagged_views()).
Declare assets with Django Media — no inline JavaScript in
the reference templates:
from django.forms.widgets import Script
import djcrud
from djcrud.static import vite_asset
from djcrud.views.spa import SPAView
class SpaView(SPAView):
title = "SPA demo"
icon = "grid"
mount_element = '<div id="app"></div>'
class Media(SPAView.Media):
js = SPAView.Media.js + (
Script(vite_asset("spa_example/js/app.js"), type="module"),
)
djcrud.site.routes.append(SpaView)
The shell loads Bulma, Unpoly, CSRF config, and sidebar markup; your subclass
sets mount_element and adds the client bundle
via class Media. Copy base.html or extend SPAView — routing and
security stay on the djcrud defaults.
Composition over monoliths¶
Generic views are stacks of small mixins — one concern each (filter,
pagination, tables2, form, object, delete, list_action, …). Override mixin
attributes on a subclass or clone; templates read them from view.
Views is the manifesto: understand the mixins, understand the whole system. See View mixins for each mixin.
Django all the way down¶
djcrud embraces Django rather than fighting it:
user.has_perm()and custom backends (crudlfap lineage)Django generic views and
urlpatternsBulma templates that are simple enough to copy and adapt — reference UI, not a locked theme
Optional packages (djcrud_auth, djcrud_history, djcrud_debug) plug in
the same way: add to INSTALLED_APPS, routes appear. See Install djcrud.
The optional djcrud_drf package adds a DRF layer on
/api/ that calls the same add_perm() registry as HTML
views — install with pip install djcrud[drf] when you need REST; widen API
access with the same djcrud.py rules, not duplicate serializer guards.
The optional djcrud_mcp client (pip install djcrud[mcp])
exposes tagged schema operations as stdio MCP tools for agents — install on the
subprocess host, not in INSTALLED_APPS.
Progressive complexity¶
The Tutorial mirrors how you would actually adopt djcrud:
Chapter |
Idea |
|---|---|
One model → full CRUD (superuser-only until you open it); override defaults by codename |
|
Your rules via |
|
Clone list views, object actions, bulk list actions, mixin tour |
|
DRF API and SPA shell reusing the permissions you registered in step 2 |
|
stdio MCP bridge ( |
Each chapter is a working app in djcrud_example, literal-included in the
docs, validated by pytest -m tutorial. The adoption path is deliberate:
ship locked-down CRUD first, then widen access in djcrud.py without
touching templates or serializers.
In one sentence¶
Declare a tree of routers and views, inherit routing and superuser-only CRUD, register your permission rules once in ``djcrud.py`` for HTML/API/MCP, expose the view object to templates, and override only what you need — presentation through composable mixins and introspected menus.
That is the voice behind “Get more out of less” and the README’s informal “ain’t no way I’m…” lines: less boilerplate, fewer files, same Django underneath.