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_data for 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:

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:

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 urlpatterns

  • Bulma 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

Routing

One model → full CRUD (superuser-only until you open it); override defaults by codename

Permissions

Your rules via add_perm / add_queryset — same registry for HTML, API, and future MCP agents

Views

Clone list views, object actions, bulk list actions, mixin tour

SPA shell

DRF API and SPA shell reusing the permissions you registered in step 2

Agents (MCP bridge)

stdio MCP bridge (djcrud_mcp) over the same API and permissions

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.