---
url: /reference/controllers.md
---
# Controllers

* **`Trek::BaseController`** — the base for all admin controllers: authentication, authorization, localization, the `admin` layout and Trek's [form builder](/reference/forms).
* **`Trek::ResourceController`** — extends `BaseController` with the full CRUD (`index`, `show`, `new`, `create`, `edit`, `update`, `destroy`), wired to ActionPolicy (`authorized_scope`, `authorize!`) and ordering. Scaffolded controllers inherit from it and only need to declare their `model`:

```ruby
module Admin
  class BooksController < Trek::ResourceController
    private

    def model
      Book
    end
  end
end
```

* **Panel controllers** (`Trek::Panels::*`) — support endpoints used by the admin UI: image uploads (Uppy), link insertion and content editor prompts. The installer generates their `Admin::Panels::*` subclasses in the app, which the admin routes point to: Trek's engine is isolated, so its own controllers only see the engine's routes, not the app's.

## Mixins

* `include Trek::Pagination` — paginates the index with Kaminari
* `include Trek::Search` — adds `search_in_objects!`, to call in `index`: it filters the index by `params[:search]`, for a [Searchable](/reference/concerns/searchable) model
* `include Trek::Filters` — filters the index by attribute equality (`where(column => value)`) from `params[:filters]`
* `include Trek::Scopes` — filters the index by **named scopes** from `params[:scopes]`
* `include Trek::Localization` — answers each request in the [locale](#localization) of the request, the user or the browser

### Localization

`Trek::Localization` sets `I18n.locale` around each action, and restores the previous locale afterwards, even when
the action raises. `Trek::BaseController` includes it, as do the generated `Admin::BaseController` and
`Admin::UploadsController`. Include it in an app's own controllers, e.g. public pages, for the same behaviour.

The locale is the first one found among:

1. `params[:locale]`, e.g. `?locale=fr`
2. the signed-in user's `locale` (`Current.user`, set by `Trek::Authentication`)
3. the browser's `Accept-Language` header: its first language among `I18n.available_locales`, a regional
   variant matching its language (`fr-CA` gives `fr`), through [http_accept_language](https://github.com/iain/http_accept_language)

Without any, the request is answered in `I18n.default_locale`.

To add a source, or a fallback other than the default locale, override `locale_for_request` and call `super` for
Trek's chain:

```ruby
class EventsController < ApplicationController
  include Trek::Localization

  private

  # The event's own language first, then Trek's chain
  def locale_for_request
    Current.event&.locale.presence || super
  end
end
```

### Filters

`Trek::Filters` lets users narrow an index by attribute values. Declare the columns users may filter on via
`available_filters`, then call `apply_filters!` in your `index` action — it rewrites `@objects` from
`params[:filters]`, applying a `where(column => value)` per non-blank filter (a value of `"nil"` matches `NULL`). The filters are read
through strong params (`filter_params`): any other key, and any value that isn't a single value, is ignored:

```ruby
module Admin
  class CitiesController < Trek::ResourceController
    include Trek::Filters

    helper_method :countries

    def index
      super
      apply_filters!
    end

    private

    def available_filters
      %i[country_code]
    end

    def countries
      City.distinct.pluck(:country_code).index_with { |code| I18n.t("countries.#{code}") }
    end
  end
end
```

Render the choices with a [`ButtonGroupComponent`](/reference/components), each button linking back to the index
with the chosen filter:

```slim
= render Trek::ButtonGroupComponent.new do |g|
  - g.with_button text: t("admin.actions.all"), \
      href: [:admin, model], active: params.dig(:filters, :country_code).nil?
  - countries.each do |code, name|
    - g.with_button text: name, \
        href: polymorphic_path([:admin, model], filters: { country_code: code }), \
        active: code == params.dig(:filters, :country_code)
```

> If a controller includes `Trek::Filters` but its `index` never calls `apply_filters!`, an
> `after_action` raises `Trek::Filters::NotAppliedError` (outside production) to flag the missing wiring.

### Scopes

`Trek::Scopes` filters by **named model scopes** rather than raw attributes. Declare the allowed scopes via
`available_scopes`, then call `apply_scopes!` in your `index`; any other name in `params[:scopes]` is ignored. It also exposes `scope_counts` and
`unscoped_objects` helpers (the per-scope match counts and the pre-scope collection) for building scope tabs:

```ruby
module Admin
  class ArticlesController < Trek::ResourceController
    include Trek::Scopes

    def index
      super
      apply_scopes!
    end

    private

    def available_scopes
      %i[published draft]
    end
  end
end
```

Like `Trek::Filters`, forgetting to call `apply_scopes!` raises `Trek::Scopes::NotAppliedError` outside production.

## N+1 detection

`trek:install:prosopite` (part of `trek:install`) sets up [Prosopite](https://github.com/charkost/prosopite)
in development and test: `config/initializers/prosopite.rb` mounts its Rack middleware, so **every request** —
including Trek's own engine controllers — is scanned. A detected N+1 raises `Prosopite::NPlusOneQueriesError`
in test (so request specs fail) and is logged in development.

The generator also adds `spec/requests/admin/n_plus_one_spec.rb`, a smoke request spec that renders the
dashboard, the pages index and the fragments index as an admin — extend its route table with the app's other
admin pages (scaffolded indexes and edit pages in particular), with a few records created beforehand, since an
empty list can't reveal an N+1.

Legitimate repeated queries (e.g. one `COUNT` per row) can be exempted with the escape hatch:

```ruby
Prosopite.pause do
  scopes.index_with { |scope| relation.public_send(scope).count }
end
```

Trek's own concerns already do this where needed (`Trek::Scopes#scope_counts`). Guard with
`if defined?(Prosopite)` in code that must also run in production, where the gem isn't loaded.
