Skip to content

Controllers ​

  • Trek::BaseController — the base for all admin controllers: authentication, authorization, localization, the admin layout and Trek's form builder.
  • 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 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 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

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, 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 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.

Released under the MIT License.