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
| Hook | Default | Decides |
|---|---|---|
recourse_label | :name | the column a combobox shows and a foreign-key cell reads |
recourse_hidden | [] | columns kept off every screen |
recourse_order | :id | the index’s order, a Symbol or a Hash |
recourse_position | 'position' | the column rows are dragged into order by, or nil |
recourse_siblings | the rows under the parent | which rows a position is counted among |
recourse_icon | the model’s name | the icon on the sidebar, the crumbs and the tabs |
recourse_includes | every belongs_to | what the index eager-loads |
recourse_comment | the schema’s comment | the note under a form field |
recourse_broadcasts? | true | whether a save redraws every open index |
filter_fields | enums, booleans, keys | the 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.
| Column | Table and page | Form |
|---|---|---|
| enum | a badge; a filter menu | a menu |
| boolean | Yes, No, or a dash; a filter menu | a checkbox |
belongs_to | the label of the record, linked; a filter menu where the table is short | a combobox, or a typed field |
| number | delimited, at the column’s scale; money and percentages with their unit | a number field stepped by the column |
| date, datetime | Aug 12, 2026; a time in the reader’s zone | a date or datetime-local field |
| web address | the host as a link | a text field |
phone | punctuated | a telephone field |
| text | the text | a textarea |
| array | counted on the table, listed on the page | a textarea, one value per line |
| json | off the table; pretty-printed on the page | — |
| generated | as its kind | never 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.
Search, sort and filters
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.