Philosophy

Faster Django development by getting more out of less. djmvc 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 controllers and views, not by hand-editing urls.py for every endpoint. A ModelController 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 controllers to djmvc.site in djmvc.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 ModelController.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.

Security and data scope in one place

Secure by default. Views check permissions before dispatch; anonymous users go to login; denied users get 403. CRUD maps to Django’s add / change / delete / view_<model> permissions.

Policy concentrates on the model controller:

If you already use django.contrib.auth.Permission, you are in familiar territory — the same model as the admin. Users with view_<model> can list and open every row; add / change / delete gate writes. Groups and per-user permission assignments in the database decide who gets which codename. That is secure by default: permissions can come from the database already.

In practice, this is fine until you need per-object permissions. So, the first customization is usually get_queryset(), the djmvc analogue of ModelAdmin.get_queryset — filter rows by tenant, owner, or role while leaving permission checks on the default path. That is enough for most apps. When you need per-object rules (this user may change this document but not another), override has_permission() on the controller or has_permission() on individual views — the same escape hatch Django’s permission backends provide.

Lists, object views, and bulk actions all share that queryset. PKs outside it → 404, not a leak.

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.

Stage 5 — View mixins is the manifesto: understand the mixins, understand the whole system. See View mixins for each mixin.

Django all the way down

djmvc 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 (djmvc_auth, djmvc_history, djmvc_debug) plug in the same way: add to INSTALLED_APPS, routes appear. See Install djmvc.

Progressive complexity

The Tutorial mirrors how you would actually adopt djmvc:

Stage

Idea

0

One model → full CRUD

1

Nest under an app controller

2

Custom non-CRUD views

3

Clone and replace defaults

4

List actions + i18n

5

Mixin tour

Each stage is a working app, literal-included in the docs, validated by pytest -m tutorial.

In one sentence

Declare a tree of controllers and views, inherit secure CRUD and routing, expose the view object to templates, and override only what you need — policy once on the controller, 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.