[Ruby]: Сортировка и фильтрация по связанным ресурсам в разных локалях с Ransack

Задача была проста: есть модель User и связанная с ней модель Role, нужно организовать поиск и сортировку, и все бы ничего, да только связаны они many-to-many связью:

has_many :roles, :through => :user_roles

Истоки данного варианта соединения мне, к сожалению, были неизвестны, а задачу надо было решить.

Почему именно ransack?

Это gem, призванный облегчить поиск и сортировку по нужным полям, в тех местах, где пользоваться такими мощными средствами как ElasticSearch + Tire вроде бы нет смысла. Скачать его можно тут, плюс есть demo и код его на github, при работе очень помогло описание построения простейших запросов. Это переписанный MetaSearch, удобный для встраивания поиска в формы вашего приложения. На этом хватит лирики, все, кто хочет более подробно ознакомиться, может пройтись по ссылкам, прочитать там все, и потом залезть в исходный код, ничего особо сложного там нет, а для общего понимания будет полезно. Перейдем непосредственно к проблеме. Для начала подключим и установим гем:

gem 'ransack'

bundle install  

Как сортировать по nested resources?

Nested resources не совсем корректный термин, но нравится как звучит, оно применимо в контексте роутинга приложений на RoR. В Readme гема на github очень скудно описаны варианты сортировки, и в принципе работы с гемом, очень помогает в этом отношении код демо приложения и статься про Basic Searching(ссылки смотри выше). Ситуация, есть таблица пользователей в админке приложения:

Id Name Status Role
1 V. Pupkin Opened Company admin
2 I. Ivanov Opened User
3 S. Sergeev Opened Visitor

Сортировку надо организовать по полю Role. Для начала сформируем action index в контроллере:

def index  
  params[:search] = { s: 'created_at desc' }.merge(params[:search] || {})
  @search = ::User.search(params[:search])
  @users = @search.result
end  

Теперь перейдем во view и будем творить магию там:

sort_link @search, "roles_name_#{I18n.locale}", User.human_attribute_name(:roles)  

На этом все, кажется очень просто, но на написание этой строчки я убил 2 часа :) У гема есть helper sort_link, в который мы передаем объект @search, инициализированный в контроллере, он будет производить поиск и сортировку. Следующим параметром передаем поле для сортировки.

Здесь ключевой момент в формате записи поля. Очень помог понять, что там вообще происходит вот этот issue. У модели Role есть 2 атрибута, это nameru и nameen, для названий в русской и английской локалях соответственно. Чтобы список выдавался в правильном порядке в зависимости от того, с каким языком мы работаем, надо передавать правильное поле, поэтому на конце стоит name_#{I18n.locale}. Т. о., чтобы правильно выбрать поле для сортировки при связи многие ко многим необходимо указать сначала имя связанной модели, затем через нижнее подчеркивание нужное поле для отображения, т. е. в моем случае roles_name_#{I18n.locale}. При сортировке, выполняется запрос в базу, на проекте используется PostgreSQL, и для разработки я использую Mac, поэтому у меня при выдаче возникли проблемы. База при запросе с ORDER BY выдавала неправильную последовательность, лечиться вот этим постом.

С сортировкой на этом разобрались, перейдем к фильтрации выдачи по нескольким параметрам. Приведу сразу результирующий кусок кода, а затем подробно его разберу:

= simple_form_for :search, :url => admin_users_path, :method => :get, :html => {:class => 'form-inline'} do |f| 
  = f.input :user_roles_role_id_in, :as => :chosen_select, :collection => Role.all, :label => User.human_attribute_name(:roles), :input_html => {:multiple => true }
  = f.input :state_in, :as => :select, :collection => User.state_machine(:state).states.map {|s| [s.human_name, s.name]}, :label => User.human_attribute_name(:state), :input_html => {:multiple => true }
  = f.submit t("web.searches.show.search")

Идем по порядку:

  1. simple_form_for, если кто не знает, прошу сюда. Подробно на самом simple останавливаться не буду, просто скажу, что для поисковой формы можно использовать не стандартную ransack search_form_for, а спокойно заменить на simple_form и ничего страшного не произойдет.

    • Форма строится для объекта :search, который мы в контроллере как раз и принимаем:
    • :url => укажем точный урл, куда уйдет запрос
    • :method => каким методом посылать
    • :html => доп. параметры для верстки
  2. f.input — самое интересное, на это я потратил 3 часа, если не больше...

    • :user_roles_role_id_in — конечный правильный вариант, при котором учитываются поисковые запросы по соединению с roles. Во-первых in это специальное окончание, которое воспринимает Ransack, более подробно про них написано в статье про BasicSearching. Остальное вроде бы очевидно, мы передали связующую таблицу и поле, по которому надо осуществлять поиск. Почему-то просто по :roles_in работать не захотел.
    • :as => :chosen_select — специальный гем, который позволяет делать красивый множественный выбор для select поля
    • :collection => сюда пробрасываются параметры, которые надо превратить в id — option
    • остальное должно быть понятно, label просто для описания и :multiple => true для реализации множественного выбора

Следующий input это сортировка по статусу, по скольку у нас используется state_machine, то особых проблем с ним не возникло. Последнее это кнопка, которую описывать смысла не вижу.

На этом я хочу закончить, статье итак можно присвоить теги TL DR. Вопросы, предложения?