Migrating from djmvc

The last djmvc release used Controller, ModelController, per-app djmvc.py modules, and JSON on the same URLs as HTML. djcrud renames the package and core types, moves JSON to DRF at /api/, and centralizes permissions in a registry shared by HTML, API, and MCP.

This guide is for projects already on djmvc (including Tildette). Greenfield setup is in Install djcrud and Tutorial.

Overview

What changed at a glance:

Area

djmvc (last release)

djcrud

Package / import

djmvc

djcrud

Route group

djmvc.Controller

Router

Model CRUD group

djmvc.ModelController

ModelRouter

App hook file

myapp/djmvc.py

myapp/djcrud.py

Row scoping

ModelController.get_queryset(self, view)

add_queryset()

Action / object gates

has_permission_object(), has_permission_backend()

add_perm()

JSON CRUD

Same URL as HTML (wants_json, json_*, get_swagger_*)

DRF ModelViewSet at /api/<model>/

OpenAPI

Swagger 2 from view methods

OpenAPI 3 from drf-spectacular at GET /api/schema/

Bearer tokens

djmvc_api

djcrud_api

Templates

{% load djmvc %}

{% load djcrud %}

MCP / agents

djcrud-cli (formerly djmvc-cli), DJMVC_* env aliases

djcrud-client, DJCRUD_* env, ViewSet-based tools (see Agents (MCP bridge))

There is no compatibility shim — plan a focused port rather than mixing imports.

Upgrade checklist

Work through these phases in order. Commit after each phase so regressions are easy to bisect.

Phase A — dependencies

  1. Replace the package:

    pip uninstall djmvc
    pip install --pre "djcrud[drf]"
    

    Add [mcp] when you need the stdio MCP client (Agents (MCP bridge)).

  2. Update INSTALLED_APPS — swap every djmvc_* app for its djcrud_* counterpart:

    djmvc

    djcrud

    djmvc

    djcrud

    djmvc_bulma

    djcrud_bulma

    djmvc_auth

    djcrud_auth

    djmvc_dal

    djcrud_dal

    djmvc_dal_topbar

    djcrud_dal_topbar

    djmvc_history

    djcrud_history

    djmvc_triggers

    djcrud_triggers

    djmvc_api

    djcrud_api

    (none)

    djcrud_drf (new — enable for REST API)

  3. Replace import djmvc.settings with import djcrud.settings in settings.py.

  4. Merge API URLs in urls.py when enabling DRF:

    import djcrud
    import djcrud_drf
    
    urlpatterns = (
        djcrud.site.build().urlpatterns
        + djcrud_drf.site.build().urlpatterns
    )
    
  5. Run migrations — see Database upgrade (djmvc_api → djcrud_api) below.

Phase B — rename surface (mechanical)

  1. Rename each djmvc.pydjcrud.py.

  2. Global replace in Python:

    • import djmvcimport djcrud

    • djmvc.Controllerdjcrud.Router

    • djmvc.ModelControllerdjcrud.ModelRouter

    • djmvc.genericdjcrud.views

    • djmvc.sitedjcrud.site

    • from djmvc.from djcrud.

  3. Rename classes: FooControllerFooRouter, FooSectionControllerFooSectionRouter. Keep explicit codename overrides unchanged — they control URL prefixes and reverse names, not the class suffix.

  4. Templates: {% load djmvc %}{% load djcrud %}.

  5. Custom CSS/JS: update selectors from djmvc-* to djcrud-* where you target the framework shell (e.g. immersive layout classes).

  6. Tests: rebuild the site registry the same way as before, using djcrud.site:

    import djcrud
    djcrud.site.registry.clear()
    djcrud.site.build()
    

Phase C — permissions registry

This is the largest behavioral change. djmvc scoped rows and gated actions on controller and view methods. djcrud registers rules once in djcrud.py and shares them across HTML, DRF, and MCP.

Registry API

djmvc pattern

djcrud replacement

ModelController.get_queryset(self, view)

)()

has_permission_object() on action views

)()

has_permission_backend() on a view

add_perm() with full perm string

ModelController.has_permission(self, view)

add_perm() on the router

Predicates compose with any_of(), all_of(), authenticated(), superuser(), and is_owner().

Example — owner-scoped queryset

djmvc:

class FileController(djmvc.ModelController):
    model = File

    def get_queryset(self, view):
        qs = super().get_queryset(view)
        if view.request.user.is_staff:
            return qs
        return qs.filter(owner=view.request.user)

djcrud:

def file_queryset(user, *, model, action, perm, obj, **ctx):
    qs = model._default_manager.all()
    if user.is_staff:
        return qs
    if not user.is_authenticated:
        return qs.none()
    return qs.filter(owner=user)

class FileRouter(djcrud.ModelRouter):
    model = File

djcrud.permissions.add_queryset(File, scoper=file_queryset)
from djcrud.permissions import authenticated
djcrud.permissions.add_perm(FileRouter, "view,add,change,delete", check=authenticated)

Full worked example: djcrud_example.security_example in src/djcrud_example/security_example/djcrud.py and Permissions.

Example — custom object action

djmvc — permission on the view:

class PublishView(ObjectMixin, ActionMixin, ModelMixin, djmvc.View):
    permission_shortcode = "publish"

    def has_permission_object(self, obj):
        return (
            obj.owner_id == self.request.user.pk
            and not obj.published
        )

djcrud — same view class (HTML unchanged), rule in the registry:

from djcrud.permissions import is_owner

def can_publish(user, *, obj, **ctx):
    if not user.is_authenticated:
        return False
    if obj is not None and (
        not is_owner(user, obj=obj, **ctx) or obj.published
    ):
        return False
    return True

djcrud.permissions.add_perm(Article, "publish", check=can_publish)

The custom action’s permission_shortcode (or DRF @action method name) must match the shortcode passed to add_perm().

Phase D — API and agents

djmvc served JSON from the same paths as HTML when the client sent Accept: application/json or used PUT/PATCH/DELETE. djcrud removes wants_json, json_get/json_post, get_swagger_*, and ModelController.serialize() from HTML views. Machine clients use DRF instead.

Per model

  1. Add a ViewSet:

    import djcrud_drf
    
    class TaskViewSet(djcrud_drf.ModelViewSet):
        model = Task
    
  2. Register it:

    djcrud_drf.site.register(TaskViewSet)
    
  3. Register permissions in djcrud.py (same rules as HTML).

  4. Remove dead JSON handlers from HTML views (get_json_form_kwargs, json_form_valid_response, get_swagger_post, etc.) once the ViewSet covers the workflow.

Custom actions

Use DRF @action. The method name becomes the permission shortcode and the MCP tool suffix (article_publishpublish rule):

from rest_framework.decorators import action
from rest_framework.response import Response

class ArticleViewSet(djcrud_drf.ModelViewSet):
    model = Article

    @action(detail=True, methods=["post"])
    def publish(self, request, pk=None):
        article = self.get_object()
        article.publish()
        return Response(self.get_serializer(article).data)

djcrud.permissions.add_perm(Article, "publish", check=can_publish)

See Agents (MCP bridge) for the full single-file example.

URL and tool naming breaks

HTML prefixes are usually unchanged when you preserve codename overrides (TasksSectionController at /taskssection/TasksSectionRouter with the same codename).

The REST API uses a separate prefix per registered ViewSet: /api/<model_name_lower>/. Agents and MCP clients that called /taskssection/item/ with Bearer must switch to /api/task/ (example).

MCP tool names change from path-derived heuristics to {model}_{drf_action} (e.g. task_list, task_create). Environment variables rename to DJCRUD_BASE_URL and DJCRUD_TOKEN; DJMVC_* and TILDETTE_* remain accepted aliases (djcrud_mcp).

Database upgrade (djmvc_apidjcrud_api)

Enabling djcrud_api runs migrations that upgrade existing databases:

  • 0002_rename_from_djmvc_swagger — renames djmvc_swagger_tokendjmvc_api_token if present.

  • 0003_rename_from_djmvc_api — renames djmvc_api_tokendjcrud_api_token and updates django_migrations rows from djmvc_api to djcrud_api.

After pip install djcrud and updating INSTALLED_APPS:

python manage.py migrate

Existing Bearer tokens remain valid (same table, new name). No manual SQL is required for standard upgrades.

Patterns that stay the same

These conventions carry over with import renames only:

  • djcrud.site.routes.append(MyRouter) — same as djmvc.site.routes.append

  • ModelRouter.routes + [MyView.clone(...)] — extend or replace by codename

  • View metadata — icon, color, title, tags, table_fields, fields, search_fields, paginate_by

  • ObjectMixin, ActionMixin, ModelMixin, FormMixin

  • Clonable.clone() for ad-hoc view variants

  • Reverse names — site:<section>:<router>:<view> (replace controller in docs and tests with router; the namespace string is unchanged if codename values are unchanged)

  • FULL_PAGE_LINK_ATTRIBUTES — still on djcrud.redirect

What to remove

After the port, delete or stop relying on:

  • wants_json branching and json_* methods on HTML views

  • get_swagger_get / get_swagger_post on views

  • ModelController.json_fields, serialize(), get_<field>_json()

  • Controller-level get_queryset / has_permission overrides (moved to registry)

  • djmvc-cli entry point — renamed to djcrud-cli; standalone MCP uses djcrud-client extra

  • TILDETTE_CONTROLLER_PREFIX / path-prefix OpenAPI filtering — replace with ViewSet registration (djcrud_mcp design)

Large-app porting notes (Tildette)

Tildette is the reference large port. Suggested order:

App

Notes

tildette_triggers

Small; swap djmvc_triggers imports first

tildette_mcp

Registry section; non-CRUD secret endpoints become DRF @action or APIView routes in /api/schema/ (see djcrud_mcp design)

tildette_tasks

Heavy JSON on views; add TaskViewSet; MCP tool names change

tildette_workspace

Multiple get_queryset overrides → per-model add_queryset

tildette_process

Process scoping + shared helpers (update import paths)

tildette_acp

HTML routers; WebSocket timeline paths are not djcrud (unchanged)

Provider apps (grok, claude, …)

Small FormView routers under agents section

Update Tildette docs (docs/reference/urls-and-sections.rst, docs/install.rst) when the port lands.

Verification

After each phase:

HTML

  • Sidebar navigation shows only routes the user may access

  • Object action menus respect registry checks

  • Rows outside scoped querysets return 404 on detail/update/delete

API

  • GET /api/schema/ lists registered ViewSets

  • Bearer CRUD returns 403 for denied actions, 404 for out-of-scope PKs

  • Custom @action endpoints match registry shortcodes

MCP / agents

  • Tools follow {model}_{action} naming

  • Token env vars set (DJCRUD_TOKEN or alias)

  • No hardcoded controller_prefix path filters

Further reading