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 |
|
|
Route group |
|
|
Model CRUD group |
|
|
App hook file |
|
|
Row scoping |
|
|
Action / object gates |
|
|
JSON CRUD |
Same URL as HTML ( |
DRF |
OpenAPI |
Swagger 2 from view methods |
OpenAPI 3 from drf-spectacular at |
Bearer tokens |
|
|
Templates |
|
|
MCP / agents |
|
|
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¶
Replace the package:
pip uninstall djmvc pip install --pre "djcrud[drf]"
Add
[mcp]when you need the stdio MCP client (Agents (MCP bridge)).Update
INSTALLED_APPS— swap everydjmvc_*app for itsdjcrud_*counterpart:djmvc
djcrud
djmvcdjcruddjmvc_bulmadjcrud_bulmadjmvc_authdjcrud_authdjmvc_daldjcrud_daldjmvc_dal_topbardjcrud_dal_topbardjmvc_historydjcrud_historydjmvc_triggersdjcrud_triggersdjmvc_apidjcrud_api(none)
djcrud_drf(new — enable for REST API)Replace
import djmvc.settingswithimport djcrud.settingsinsettings.py.Merge API URLs in
urls.pywhen enabling DRF:import djcrud import djcrud_drf urlpatterns = ( djcrud.site.build().urlpatterns + djcrud_drf.site.build().urlpatterns )
Run migrations — see Database upgrade (djmvc_api → djcrud_api) below.
Phase B — rename surface (mechanical)¶
Rename each
djmvc.py→djcrud.py.Global replace in Python:
import djmvc→import djcruddjmvc.Controller→djcrud.Routerdjmvc.ModelController→djcrud.ModelRouterdjmvc.generic→djcrud.viewsdjmvc.site→djcrud.sitefrom djmvc.→from djcrud.
Rename classes:
FooController→FooRouter,FooSectionController→FooSectionRouter. Keep explicitcodenameoverrides unchanged — they control URL prefixes and reverse names, not the class suffix.Templates:
{% load djmvc %}→{% load djcrud %}.Custom CSS/JS: update selectors from
djmvc-*todjcrud-*where you target the framework shell (e.g. immersive layout classes).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 |
|---|---|
|
|
|
|
|
|
|
|
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¶
Add a ViewSet:
import djcrud_drf class TaskViewSet(djcrud_drf.ModelViewSet): model = Task
Register it:
djcrud_drf.site.register(TaskViewSet)
Register permissions in
djcrud.py(same rules as HTML).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_publish → publish 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_api → djcrud_api)¶
Enabling djcrud_api runs migrations that upgrade existing databases:
0002_rename_from_djmvc_swagger— renamesdjmvc_swagger_token→djmvc_api_tokenif present.0003_rename_from_djmvc_api— renamesdjmvc_api_token→djcrud_api_tokenand updatesdjango_migrationsrows fromdjmvc_apitodjcrud_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 asdjmvc.site.routes.appendModelRouter.routes + [MyView.clone(...)]— extend or replace by codenameView metadata —
icon,color,title,tags,table_fields,fields,search_fields,paginate_byObjectMixin,ActionMixin,ModelMixin,FormMixinClonable.clone()for ad-hoc view variantsReverse names —
site:<section>:<router>:<view>(replacecontrollerin docs and tests withrouter; the namespace string is unchanged ifcodenamevalues are unchanged)FULL_PAGE_LINK_ATTRIBUTES— still ondjcrud.redirect
What to remove¶
After the port, delete or stop relying on:
wants_jsonbranching andjson_*methods on HTML viewsget_swagger_get/get_swagger_poston viewsModelController.json_fields,serialize(),get_<field>_json()Controller-level
get_queryset/has_permissionoverrides (moved to registry)djmvc-clientry point — renamed todjcrud-cli; standalone MCP usesdjcrud-clientextraTILDETTE_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 |
|---|---|
|
Small; swap |
|
Registry section; non-CRUD secret endpoints become DRF |
|
Heavy JSON on views; add |
|
Multiple |
|
Process scoping + shared helpers (update import paths) |
|
HTML routers; WebSocket timeline paths are not djcrud (unchanged) |
Provider apps (grok, claude, …) |
Small |
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 ViewSetsBearer CRUD returns 403 for denied actions, 404 for out-of-scope PKs
Custom
@actionendpoints match registry shortcodes
MCP / agents
Tools follow
{model}_{action}namingToken env vars set (
DJCRUD_TOKENor alias)No hardcoded
controller_prefixpath filters
Further reading¶
Philosophy — why the registry exists
Permissions —
add_perm/add_querysetin depthDRF API — DRF setup
Agents (MCP bridge) — MCP after DRF
djcrud_mcp design — tool discovery design