--- type: object cluster: content universe: live status: verified entity: app/models/announcement.rb --- # Announcement A site-wide notice written by an admin, with rich text. One may be "featured" at a time. Verified 2026-08-16 against commit `63732df`. ## Why this shape **Content is Action Text, not a column.** `has_rich_text :content` (`app/models/announcement.rb:4`) stores the body in `action_text_rich_texts` (`db/schema.rb:17-25`) as a polymorphic association. So `content` is a record, not a string: it is not selectable, not sortable, and not searchable with a plain `WHERE` on this table. **The `body` column is a ghost.** `announcements.body` exists (`db/schema.rb:56`) but is never read, written, validated, or permitted — `announcement_params` allows only `title`, `content`, `featured` (`app/controllers/admin/announcements_controller.rb:79-81`). It is the pre-Action-Text column, left behind. Do not write to it expecting it to appear. **"Only one featured" is a callback, not a constraint.** `before_save :unfeature_other_announcements` demotes the current holder when a new one is featured (`:9,27-32`), and `Announcement.current` simply does `find_by(featured: true)` (`:13-15`). There is no unique index — concurrent writes can leave two featured rows, and `current` will then return an arbitrary one. The demotion also runs `update` (not `update!`) on the old record (`:31`), so a failure there is silent. ## Shape - Table `announcements`, `db/schema.rb:55-62` — `title`, `featured`, `body` (ghost), timestamps; index on `created_at DESC` (`db/schema.rb:61`) - `validates :title, presence: true, length: { maximum: 255 }` (`:6`) - `validates :content, presence: true` (`:7`) — validating the Action Text association - `scope :latest` — newest first (`:11`) - `self.current` — the featured one, or `nil` (`:13-15`) - `excerpt(limit: 150)` — plain-text truncation (`:17-19`) - `published_at` is an **alias for `created_at`** (`:21-23`); there is no publish workflow and no draft state ## Connected to - **owns:** its Action Text record - **owned-by:** — - **joins:** — - **looks-like-but-is-not:** `published_at` is not a publication timestamp — an announcement is live from the moment it is created. And `content` is not a column. ## If you change this - **Hits:** `Admin::AnnouncementsController` (full CRUD) and `AnnouncementsController#show`; the home page and any layout partial calling `Announcement.current`; Action Text and Active Storage if you touch `content`, since embedded attachments live there. - **Does not hit:** anything financial. Announcements touch no portfolio, order, or gradebook — this is the one object in the map with no path to money. ## Surfaces | Surface | Role | |---|---| | `Admin::AnnouncementsController` | admin CRUD | | `AnnouncementsController#show` | everyone reads | | `HomeController#index` | reads the featured one | ## See - Source: `app/models/announcement.rb`, `db/schema.rb:55-62`