Избавляемся от повторения кода с помощью DRY CRUD

в 12:23, , рубрики: crud, DRY, ruby on rails, метки: , ,

Фреймворк Ruby on Rails меня просто очаровывает, но недавнего времени некоторую сложность представляла из себя генерация CRUD контроллеров.

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

  • стандартный генератор scaffold_controller — ничего подобного нет, CRUD с простейшим дизайном
  • nifty Scaffold — его разработка приостановлена, но все равно фильтрации и сортировки нет
  • гем для DataTable — не прижился, он обеспечивает только представление данных, а код для фильтрации и сортировки пришлось бы писать самому

Долгое время не удавалось найти ничего похожего на полюбившийся мне виджет CGridView из Yii framework. Уже почти смирился с необходимостью писать свой велосипед, но наткнулся на DRY CRUD и хочу поделится опытом его использования. Может кому-то он окажется полезным, а может кто-то подскажет еще более подходящий инструмент.

Установка

DRY CRUD это гем который с помощью генератора создает в проекте свой класс контроллера, после этой генерации гем можно отключать, а сгенерированный им код при необходимости изменять.
Для установки достаточно указать гем в Gemfile

gem 'dry_crud', '= 1.7.0'

запустить

bundle install

и генератор

rails generate dry_crud --templates haml --tests rspec

Демонстрация возможностей

В качестве демонстрации я создал приложение из двух моделей User и Post, связанные отношением один-к-многим.

rails generate model User name:string email:string pass:string
rails generate model Post  author_id:integer title:string content:text is_draft:boolean

Для более понятного для человека отображения определил метод to_s и получился такой код моделей:

#user.rb
class User < ActiveRecord::Base
  attr_accessible :email, :name, :pass
  has_many :posts

  def to_s
    "#{name}"
  end
end

#post.rb
class Post < ActiveRecord::Base
  attr_accessible :user_id, :content, :is_draft, :title
  belongs_to :user

  def to_s
    "#{title}"
  end
end

после чего в папке /app/controllers достаточно создать файл контроллера и наследовать его от CrudController. Можно также указать по каким столбцам обеспечить фильтрацию.

#users_controller.rb
class UsersController < CrudController
  self.search_columns = [:name, :email]
end

Вот и все, не обязательно даже создавать папку для вьюх, пока не потребуется более сложный функционал. Запускаем приложение и получаем работающий CRUD с сортировкой (кликабельные столбцы) и фильтрацией (если в контроллере установлена self.search_columns) а пагинацию можно прикрутить с минимальными усилиями и она будет совместима и с фильтром и с сортировкой.
Избавляемся от повторения кода с помощью DRY CRUD

Форма редактирования тоже не разочаровала, она определяет тип каждого поля и использует соответствующий контрол для boolean, string и text поля, а также для связи «belongs_to»:
Избавляемся от повторения кода с помощью DRY CRUD
и конечно же все это можно настраивать!

Подключение Twitter bootstrap

Много моих проектов используют Twitter bootstrap и адаптация DRY CRUD происходит очень просто, достаточно подключить гем «twitter-bootstrap-rails», установить генератором

rails generate bootstrap:install less

удалить app/views/layouts/crud.html.haml и /app/assets/stylesheets/sample.scss и переписать /app/views/layouts/application.html. Кому интересно, может посмотреть исходник проекта.
В итоге получаем узнаваемый дизайн:
Избавляемся от повторения кода с помощью DRY CRUD

Скрытие колонок

Если например необходимо скрыть колонки created_at и updated_at во всех контроллерах, достаточно изменить хелпер базового класса.

#/app/helpers/list_helper.rb
  def default_attrs
    attrs = model_class.column_names.collect(&:to_sym)
    attrs - [:id, :position, :password, :created_at, :updated_at] #< добавил сюда
  end
Изменение набора колонок

Теперь в Users скроем столбец pass и добавим created_at. Для этого необходимо создать папку /app/views/users скопировать в неё общий партиал /app/views/crud/_form.html.haml
В первую и единственную строку = crud_table добавим список столбцов, которые мы хотим видеть:

= crud_table :name, :email, :created_at

Результат:
Избавляемся от повторения кода с помощью DRY CRUD

Форматирование содержимого ячеек

По умолчанию содержимое постов отображается полностью:
Избавляемся от повторения кода с помощью DRY CRUD
Что бы сделать его обрезку до 100 символов достаточно создать в хелпере метод format_

#/app/helpers/post_helper.rb
module PostHelper
  def format_content(post)
    truncate(post.content, :length => 100, :omission => '...')
  end
end

результат:
Избавляемся от повторения кода с помощью DRY CRUD

Локализация

Менять надписи для полей не изменяя представления можно используя возможности рельсового L18n. Для этого необходимо установить язык по умолчанию:

#/config/application.rb
....
module DryCrudSample
  class Application < Rails::Application
     ...
    I18n.default_locale = :ru
  end
end

для генерации языковых файлов я воспользовался гемом «l18n_generator»

#Gemfile
group :development do
  gem 'i18n_generators'
end

качаем файл локали с репозитория rails-i18n

rails generate i18n_locale ru

копируем сгенерированный при установке DRY CRUD файл перевода /config/locales/en_crud.yml в ru_crud.yml и переводим.
Генерируем YAML файл для наших моделей

rails generate i18n_translation ru

получаем файл /config/locales/translation_ru.yml который я предпочитаю переименовать в ru_models.yml
Остается только написать желаемые названия для полей наших моделей

После рестарта приложения получаем локализированный интерфейс:
Избавляемся от повторения кода с помощью DRY CRUD

Изменение поведения после редактирования

Картинки кончились, начинается более глубокая настройка.
По умолчанию после успешного создания или редактирования записи происходит redirect на просмотр этой записи (действие «show») мне было более привычно, что бы в таком случае редирект происходил на «index» решение:

#/app/controllers/crud_controller.rb
  def update(options = {}, &block)
    assign_attributes
    updated = with_callbacks(:update, :save) { entry.save }
    #respond_with(entry, options.reverse_merge(:success => updated), &block) #<- было
    respond_with(entry, options.reverse_merge(:success => updated, :location=> index_url), &block) #стало
  end

Подобная замена понадобилась и в методе create

Потом в проекте понадобилось после редактирования записи возвращаться на её же форму редактирования. Что бы это реализовать в нашем контроллере Posts всего лишь необходимо переопределить метод update в котором передавать желаемый URL:

#/app/controllers/posts_controller.rb
  def update
    super location: edit_post_path(params[:id])
  end
Добавление кнопок в столбец действий

На одном из проектов надо было реализовать функцию клонирования записи. Эта функциональность, насколько я понимаю отсутствует по умолчанию, поэтому я реализовывал её следующим образом. На примере Post, сначала определял новое действие в ресурсном маршруте:

#routes.rb
  resources :posts do
    get :clone, :on => :member
  end

Это действие должно будет получать id копируемой записи и отображать форму создания с уже заполненными полями. Submit формы произойдет на действие create.
код клонирования модели я разместил в базовом классе:

#/app/controllers/crud_controller.rb
  def duplicate_entry()
    set_model_ivar( find_entry.dup()  )
  end

Код генерации кнопки разместился в хелпере:

#/app/helpers/crud_helper.rb
  def action_col_clone(table, &block)
    action_col(table) do |e|
      link_table_action('list-alt', action_path(e, &block)) #тут указывается какое изображение отображать на кнопке
    end
  end

Код добавления новой кнопки в столбец место в представлении:

#/app/views/posts/_list.html.haml
= crud_table :title, :content, :is_draft do |t|
  - action_col_clone t do |e|
    - clone_post_path e #тут указывается урл на действие

Все заработало. Добавлено всего две функции, и теперь по всему проекту можно добавить кнопку «клонировать» всего одной строкой!

Вывод

На самом деле с помощью гема «deep_cloneable» дублицировать ActiveRecord можно вместе с её зависимостями. А в форме можно редактировать не только саму запись, но и её связанные через «has_many». А фильтр сделать более сложным, не один textfield а несколько select'ов. И все это достаточно просто, частично описано в документации, частично постигается изучением исходников.

Я пришел к выводу, что DRY CRUD очень гибкий и мощный.

Немного смущает, что для внесения некоторых изменений приходится править «исходный код», хоть он и находится внутри проекта, наверное это может усложнить применение обновлений от автора. Но пока с такой необходимостью я не сталкивался, да и объем изменений невелик, что бы это составляло реальную проблему.

Ссылки

Инструкция по гему DRY CRUD
Исходник примера на GitHub

Автор: charger_lda

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js