Recourse

Models

Most models need to say nothing: the screens read the validators, the associations, the column types and the indexes. A model overrides one of these class methods when the default is not what it wants.

The hooks

HookDefaultDecides
recourse_label:namethe column a combobox shows and a foreign-key cell reads
recourse_hidden[]columns kept off every screen
recourse_order:idthe index’s order, a Symbol or a Hash
recourse_position'position'the column rows are dragged into order by, or nil
recourse_siblingsthe rows under the parentwhich rows a position is counted among
recourse_iconthe model’s namethe icon on the sidebar, the crumbs and the tabs
recourse_includesevery belongs_towhat the index eager-loads
recourse_commentthe schema’s commentthe note under a form field
recourse_broadcasts?truewhether a save redraws every open index
filter_fieldsenums, booleans, keysthe menus beside the search box
class Post < ApplicationRecord
  def self.recourse_label = :title
  def self.recourse_hidden = :ip_address
  def self.recourse_order = { published_at: :desc }
  def self.recourse_icon = :question
end

The label

The column a record is known by: what a combobox lists, what a key in another table reads, what a toast and a breadcrumb call the record. A real column, since the combobox selects only the id and the label. Where it has a length validator, or the table holds over 200 rows, a form asks for the value to be typed — Country code — and looks it up on the way in.

Hidden columns

recourse_hidden takes a column off the table, the page, the form, the search and the filters at once, and takes an attachment off the same way. There is no table-only form: a column that is only noisy on an index gets a row partial instead. Never hide a column the database requires with no default, or the form cannot save.

Order, icon and includes

A Symbol or a Hash in recourse_order puts rows with nothing in the column last; a SQL string is taken as written. recourse_icon names a concept, :question, which unicon turns into an icon; a name it does not know draws nothing. recourse_includes takes any shape includes takes, and the table’s cache key follows it, so a change to any record the rows draw expires the table.

Counters

A column is drawn as a count — headed with the icon of what it counts, linking to the rows — where it holds a counter cache or is named <association>_count for an association the model has, so a comments_count beside has_many :comments, through: :posts needs nothing declared. A *_count naming no association is an ordinary number; one naming an association it does not count wants another name.

What the column’s kind decides

Asked through type_for_attribute, so an attribute override counts.

ColumnTable and pageForm
enuma badge; a filter menua menu
booleanYes, No, or a dash; a filter menua checkbox
belongs_tothe label of the record, linked; a filter menu where the table is shorta combobox, or a typed field
numberdelimited, at the column’s scale; money and percentages with their unita number field stepped by the column
date, datetimeAug 12, 2026; a time in the reader’s zonea date or datetime-local field
web addressthe host as a linka text field
phonepunctuateda telephone field
textthe texta textarea
arraycounted on the table, listed on the pagea textarea, one value per line
jsonoff the table; pretty-printed on the page
generatedas its kindnever offered

Money and a percentage are told apart from other decimals by an Active Record type reporting :monetary or :percentage, never by the column’s name.

Encrypted columns

An encrypted column never reaches a table, arrives masked on the record’s page behind a Show, and is offered in the clear on the form. A search matches it whole, and only where the encryption is deterministic.

Attachments

A has_one_attached is a file field on the form and a picture on the record’s page. A has_many_attached :photos is a page of its own once recourses :photos, only: %i[index destroy] is nested under the record: a table of the files, with a Delete on each row. Files are added on the record’s form. Pictures need image_processing; video and PDF also need ffmpeg and poppler.

A model that says nothing is fully searchable, sortable and filterable, and ransackable_attributes, ransackable_associations, ransortable_attributes and filter_fields narrow it. filter_fields is a list of predicates; a model adds a menu the schema says nothing about by naming one, and Ransack is told it may reach that far:

def self.filter_fields = super + %i[category_name_in]
def self.ransackable_associations(_ = nil) = super + %w[category]

A predicate Ransack will not answer raises when the page draws.

Wording a deletion

A model that cancels or withdraws rather than deletes says so in the locale, under its own name:

en:
  recourse:
    models:
      subscription:
        delete: Cancel %{model}
        deletion_title: Cancel %{record}?

Broadcasts

With turbo-rails, saving a record redraws every open index listing it. A model written far more often than it is read quiets its own with def self.recourse_broadcasts? = false.