Admin CRUD Interface

Website Rebuild with Ruby on Rails

Chapter 10 · Admin CRUD Interface

Django's admin is a live, framework-owned interface that reads the model at runtime — always current, never a file you'd open and edit. Laravel has nothing free by default at all. Rails reaches a genuine third position: rails generate scaffold produces real, ordinary source files — a model, a migration, a Controller with all seven RESTful actions, ERB views — in one command, at generation time. It's "free" the way a starting template is free, not the way a live, self-updating admin panel is free.

What Scaffold Would Generate

# illustrative — not run against Page itself, see below rails generate scaffold AdminNote title:string body:text

For a plain, flat resource like this hypothetical AdminNote, that one command would generate a working index/show/new/edit/create/update/destroy interface immediately — genuinely useful, genuinely "free" in the one-time sense. But it's not what builds this chapter's own admin interface.

Why Page itself isn't scaffolded
Scaffold's generated views assume a flat, non-hierarchical resource. Page's own self-referencing tree — a parent picker, reparenting logic, a delete guard tied to children — has no equivalent in what scaffold produces by default. This is the same reason Django's and Laravel's own admin interfaces both needed hand-written code too: a generic starting point, whether live-introspected or one-time-generated, still runs out exactly where this specific model's own real complexity begins.

The Parent Picker: Same Searchable List

Matching every sibling course's own approach — a plain, client-side-filterable flat list of every other page's full_path, rather than a nested tree widget, for the same scale-justified reason established since the Next.js rebuild's own Chapter 10.

Paying Off Chapter 2's Deferred Cost

# app/models/page.rb def move_to(new_parent) update!( parent: new_parent, full_path: new_parent ? "#{new_parent.full_path}/#{slug}" : slug ) update_descendant_paths! end def update_descendant_paths! children.each do |child| child.update!(full_path: "#{full_path}/#{child.slug}") child.update_descendant_paths! end end

Rails' own version of the same explicit recursive walk both Laravel's updateDescendantPaths() and this course's own three predecessors ultimately needed — move_to updates the moved page first, then recurses through update_descendant_paths! to fix every descendant's own stale full_path.

Delete Refusal — a Callback to Chapter 2's Own Finding

# app/controllers/admin/pages_controller.rb def destroy page = Page.find(params[:id]) if page.destroy redirect_to admin_pages_path else redirect_to admin_pages_path, alert: page.errors.full_messages.to_sentence end end
A two-sided finding, not a one-sided verdict
Chapter 2 named dependent: :restrict_with_error as an application-level check — a real limitation, since a raw SQL delete could bypass it in a way Laravel's database-level restrictOnDelete() never could. Here in Chapter 10, that same application-level design pays a genuine ergonomic dividend: destroy simply returns false and populates page.errors when a child still exists — no begin/rescue block needed at all. Laravel's Chapter 10 needed a try/catch around a raw QueryException specifically because its own protection lived one layer deeper, at the database. The same design choice that was a real weakness in Chapter 2 is a real convenience here — worth naming both sides honestly rather than declaring one approach simply better.

Three Admin Interfaces, Compared

DjangoLaravelRails
Free admin?Yes — live, runtime-introspectedNo — Nova/Filament are separate packagesYes, but one-time — rails generate scaffold writes real files
Reparent cascadeImplicit — re-triggered save()Explicit — updateDescendantPaths()Explicit — update_descendant_paths!
Delete-with-children handlingRefused, surfaced in the adminCaught QueryException, surfaced manuallyReturns false, errors already populated — no exception handling needed

Hands-On Exercises

Exercise 1

Explain what rails generate scaffold produces, and explain why calling it a "one-time free" is a meaningful distinction from Django's own live, runtime-introspected admin.

📄 View solution
Exercise 2

Explain why Page's own admin interface isn't built by running rails generate scaffold directly against it.

📄 View solution
Exercise 3

Explain the two-sided finding about dependent: :restrict_with_error — a real limitation back in Chapter 2, but a genuine advantage here — and explain specifically why no begin/rescue block is needed in this chapter's own destroy action.

📄 View solution

Chapter 10 Quick Reference

  • rails generate scaffold — a genuine third kind of "free," real editable code, generated once
  • Not run against Page itself — its tree logic needs hand-written code, matching both siblings' own reasons
  • move_to / update_descendant_paths! — the explicit recursive cascade paying off Chapter 2's deferred cost
  • Two-sided finding — Chapter 2's application-level delete guard was a real limitation there, a real ergonomic win here
  • Next chapter: Deployment