Admin CRUD Interface

Website Rebuild with Django

Chapter 10 · Admin CRUD Interface

Food Tracker (Django) 3 already showed that Django's admin can turn a model into a working CRUD interface with a single line of registration, before any custom view exists at all. That's still true here — and, exactly like Website Rebuild with Next.js 10 discovered, it still isn't enough on its own for a genuinely browsable tree. This chapter's real job is the one Chapter 2 deliberately deferred: moving a page that already has children, and doing it correctly.

Free CRUD From One Line

# content/admin.py from django.contrib import admin from .models import Page admin.site.register(Page)

/admin/content/page/ now lists every page, with working add/edit/delete forms — genuinely free, matching Food Tracker (Django) 3's own precedent exactly.

Why the Free Admin Isn't Enough Here

The default admin's own widget for parent is a plain dropdown listing every Page, by its __str__ (Chapter 2's own full_path). For a site with hundreds of pages, that's one enormous, flat, alphabetically-sorted list — no indentation, no sense of depth, no way to browse "show me this page's children." Genuinely unusable once the tree has any real size, for exactly the reason Next.js Rebuild 10 needed its own custom parent picker.

A Cheap Intermediate Step

class PageAdmin(admin.ModelAdmin): list_display = ['title', 'full_path', 'parent'] search_fields = ['title', 'full_path'] raw_id_fields = ['parent'] admin.site.register(Page, PageAdmin)

raw_id_fields replaces the giant dropdown with a search popup instead — a real, worthwhile improvement for almost no code. It's still a flat search, though, not a browsable tree — genuinely better, not genuinely sufficient.

The Real Build: Paying Off Chapter 2's Own Deferred Cost

Chapter 2's own save() method only ever recomputes the one page being saved's full_path — moving a page with children was left as a known gap, deferred specifically until there was a real admin action to attach it to. This is that action.

def move_page(page, new_parent): page.parent = new_parent page.save() # recalculates THIS page's own full_path, per Chapter 2's own save() # Cascade to every descendant, PARENT-FIRST — a simple breadth-first walk to_process = list(page.children.all()) while to_process: child = to_process.pop(0) child.save() # re-derives full_path from child.parent.full_path — already correct by now to_process.extend(child.children.all())
The central fact this chapter is built on
move_page never manually edits a single full_path string. It doesn't need to — Chapter 2's own save() already recomputes full_path from parent.full_path every time it runs, for any page. The only real trick is processing order: as long as each page is saved only after its own parent's full_path is already correct, calling plain .save() down through the whole subtree — breadth-first, level by level — produces exactly the right cascade, with zero string manipulation anywhere. Chapter 2's own model design didn't just defer this problem; it was quietly built to make the eventual solution this simple.
Order genuinely matters
Saving a grandchild before its own direct child would still leave that grandchild computing its full_path from a stale parent value — the breadth-first queue above exists specifically to guarantee every page is only processed once its own parent is already correct, one level of the tree at a time.

Deletion Still Respects PROTECT

Chapter 2's own on_delete=PROTECT decision surfaces directly in the admin: attempting to delete a page that still has children raises a clear, real error rather than silently cascading — Django's admin handles a PROTECT violation gracefully out of the box, showing exactly which related objects are blocking the deletion, rather than a raw server error.

Hands-On Exercises

Exercise 1

Explain why Django's free automatic admin, even after adding raw_id_fields to the parent field, still isn't sufficient for genuinely browsing or managing this project's own tree structure.

📄 View solution
Exercise 2

Explain why move_page's own cascade loop processes descendants in parent-first (breadth-first) order rather than in any order, and why it never manually edits a full_path string anywhere.

📄 View solution
Exercise 3

Explain what happens, per Chapter 2's own on_delete=PROTECT decision, if an admin tries to delete a page that still has children — and why that's the safer default for this specific project.

📄 View solution

Chapter 10 Quick Reference

  • admin.site.register(Page) — free, working CRUD, one line, no custom code
  • The free admin's real limit — the parent dropdown is a flat, unstructured list, unusable for browsing a real tree
  • raw_id_fields — a cheap improvement (a search popup instead of a giant select), still not a browsable tree
  • move_page — pays off Chapter 2's own deferred full_path-cascade cost, with zero manual string manipulation
  • Parent-first, breadth-first order — the only real trick; each page's save() only produces the right result once its own parent is already correct
  • on_delete=PROTECT, revisited — the admin surfaces a real, clear error rather than a silent cascade or a raw server crash
  • Next chapter: Deployment