[Ruby, Ruby on Rails] Неофициальный гайд по Active Admin
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Статья про Ruby в блоге компании ДомКлик! Как так получилось, что в молодую компанию завезли мертвый язык? Секрет в том, что на Ruby можно быстро написать и протестировать бизнес-идею. И делается это не без помощи Rails и Active Admin — библиотеки, которая позволяет быстро создать админку с минимальными затратами сил и времени.
Часто можно встретить мнение, что Active Admin хорош только для 15-минутного блога. Мы в ДомКлик считаем (и доказываем на практике), что из этой библиотеки можно выжать намного больше.
Я расскажу про некоторые подходы, которые мы применяем при работе с Active Admin.
Active Admin базируется на нескольких библиотеках, среди которых я бы выделил arbre, formtastic, inherited_resources и ransack. Каждая из них отвечает за свою часть и заслуживает отдельного рассмотрения. Начнем по алфавиту — с библиотеки, которая отпочковалась от самого Active Admin.
Arbre: кастомизация компонентов
Одна из проблем Active Admin — на глазах распухающие файлы ресурсов: фильтры, дополнительные action'ы, верстка страниц, формы, и всё это в одном файле. Где-то вдали слышен протяжный стон одинокого пуриста «где же single responsibility?» Не завезли. Но давайте разберемся, как можно изолировать часть верстки в отдельных классах.
Arbre — библиотека для описания шаблонов с помощью Ruby. Вот пример простейшей страницы, написанной с помощью DSL Arbre:
html do
head do
title('Welcome page')
end
body do
para('Hello, world')
end
end
DSL расширяется с помощью компонентов. Например, в Active Admin это tabs, table_for, paginated_collection и даже сами страницы. Продолжим знакомство с библиотекой рассмотрением структуры простейшего Arbre компонента.
Arbre: hello world компонент
Как и все компоненты Arbre, наш Admin::Components::HelloWorld наследован от класса Arbre::Component:
# app/admin/components/hello_world.rb
module Admin
module Components
class HelloWorld < Arbre::Component
builder_method :hello_world
def build(attributes = {})
super(attributes)
text_node('Hello world!')
add_class('hello-world')
end
def tag_name
'h1'
end
end
end
end
Начнем сверху вниз: builder_method определяет метод, с помощью которого мы сможем создать компонент при использовании DSL. Аргументы, переданные в компонент, попадут в метод #build.
В Arbre каждый компонент — это отдельный DOM-элемент (напоминает механизм работы современных frontend-фреймворков, только датируется 2012 годом). По умолчанию все компоненты представляют из себя div, чтобы изменить это поведение, можно переопределить метод #tag_name. Метод #add_class, как не сложно догадаться, добавляет атрибут class к корневому DOM-элементу.
Осталось вызвать наш новый компонент. Для примера, сделаем это в app/admin/dashboard.rb
# app/admin/dashboard.rb
ActiveAdmin.register_page 'Dashboard' do
menu priority: 1, label: proc { I18n.t('active_admin.dashboard') }
content do
hello_world
end
end
Теперь рассмотрим пример небольшого рефакторинга админки с использованием собственного компонента.
Arbre: пример из реальной жизни (почти)
Для того, чтобы понять, как использовать Arbre в условиях, приближенных к боевым, возьмем синтетический пример. Предположим, что у нас есть блог с записями (Article) и комментариями (Comment) со связью 1:M. Нам необходимо вывести 10 последних комментариев на странице конкретной записи (блок show).
# app/admin/articles.rb
ActiveAdmin.register Article do
permit_params :title, :body
show do
attributes_table(:body, :created_at)
panel I18n.t('active_admin.articles.new_comments') do
table_for resource.comments.order(created_at: :desc).first(10) do
column(:author)
column(:text)
column(:created_at)
end
end
end
end
А теперь вынесем таблицу с комментариями в отдельный компонент. Создадим новый класс и унаследуем его от ActiveAdmin::Views::Panel. Если создать новый компонент с нуля (как в hello_world выше) и в нем вызвать panel, то panel окажется внутри еще одного div, а это наверняка поломает верстку.
Мы в нашей команде разместили бы этот класс в app/admin/components/articles/new_comments.rb, но это вкусовщина. Просто знайте, что Active Admin автоматически загрузит всё, что находится внутри app/admin/**/*:
# app/admin/components/articles/new_comments.rb
module Admin
module Components
module Articles
class NewComments < ActiveAdmin::Views::Panel
builder_method :articles_new_comments
def build(article)
super(I18n.t('active_admin.articles.new_comments'))
table_for last_comments(article) do
column(:author)
column(:text)
column(:created_at)
end
end
private
def last_comments(article)
article.comments
.order(created_at: :desc)
.first(10)
end
end
end
end
end
Теперь заменим panel в app/admin/articles.rb на вызов нашего нового компонента и передадим в него resource:
# app/admin/articles.rb
ActiveAdmin.register Article do
permit_params :title, :body
show do
attributes_table(:body, :created_at)
articles_new_comments(resource)
end
end
Красота! Отмечу, что resource можно было бы не передавать в компонент, а использовать через контекст. Однако, явно передав resource, мы ослабили связность компонента, что позволит переиспользовать его в будущем.
К слову о переиспользовании, всё содержимое блока show (как и других блоков с шаблонами) можно вынести в partial:
# app/admin/articles.rb
ActiveAdmin.register Article do
show do
render('show', article: resource)
end
end
# app/views/admin/articles/_show.html.arb
panel(ActiveAdmin::Localizers.resource(active_admin_config).t(:details)) do
attributes_table_for(article, :body, :created_at)
end
articles_new_comments(article)
Само собой, вы можете использовать знакомый .erb и другие шаблонизаторы, но, пожалуй, оставим это в качестве факультатива.
Arbre: что еще посмотреть
Прежде всего, я посоветовал бы ознакомиться с описанием компонентов Active Admin в официальной документации.
Для более глубокого изучения можно посмотреть код базовых компонентов из arbre и компонентов activeadmin, ведь часто именно на их основе будут строиться ваши собственные. Кроме того, обратите внимание на gem activeadmin_addons, в котором есть множество интересных компонентов.
Ну, а если вы вдруг до сих пор не пишете код без ошибок, то стоит обратить внимание на то, как можно тестировать компоненты.
Formtastic: кастомизация форм
Formtastic — библиотека для описания форм с помощью DSL. Простейшая форма выглядит вот так:
semantic_form_for object do |f|
f.inputs
f.actions
end
В этом примере Formtastic автоматически вытаскивает все атрибуты из переданного объекта object и подставляет их в форму с типами input'ов по-умолчанию. Список доступных типов input'ов можно найти в README. Как и Arbre, Formtastic можно расширить с помощью создания собственных классов-компонентов. Для того, чтобы разобраться в базовых вещах, давайте создадим hello world компонент.
Formtastic: hello world компонент
По аналогии с компонентами Arbre, разместим новый класс в app/admin/inputs:
# app/admin/inputs/hello_world_input.rb
class HelloWorldInput
include Formtastic::Inputs::Base
def to_html
"Input for ##{object.public_send(method)}"
end
end
Чтобы вызвать новый input, достаточно указать его название в параметре :as, например, так:
# app/admin/articles.rb
ActiveAdmin.register Article do
form do |f|
f.inputs do
f.input(:id, as: :hello_world)
f.input(:title)
f.input(:body)
end
f.actions
end
end
Все необходимые для отрисовки формы параметры (в том числе object и символ method) попадают в #initialize, определенный в модуле Formtastic::Inputs::Base. За отображение input'а отвечает метод #to_html.
Может показаться что этот пример бесполезен, но на самом деле на его основе мы в компании рендерим read-only поля. Давайте добавим всего пару методов, доступных в Formtastic, и превратим наш hello world в полезный read-only input. Следите за руками:
# app/admin/inputs/hello_world_input.rb
class HelloWorldInput
include Formtastic::Inputs::Base
def to_html
input_wrapping do
label_html <<
object.public_send(method).to_s
end
end
end
Всё, что мы добавили — это два метода с говорящими названиями. input_wrapping пришел из модуля Formtastic::Inputs::Base::Wrapping и отвечает за обертку input'а. В том числе, он включает в себя элементы для вывода ошибок и подсказок. label_html из модуля Formtastic::Inputs::Base::Labelling рендерит лейбл для input'а. Эти два хелпера мгновенно превращают наш hello world в применимый в бою input (разве что нейминг класса бы еще поправить).
Теперь мы можем перейти к чуть более сложному примеру, который продемонстрирует, как можно интегрировать в форму JS-библиотеку.
Formtastic: пример из реальной жизни (почти)
Возьмем за основу очередной выдуманный пример, который продемонстрирует, как работать с HTML, CSS и JS. То есть покроет все шаги написания нового input'а.
Предположим, что к нам пришел запрос от редактора блога: при написании статьи он хотел бы прямо в форме ввода видеть количество слов. Как известно, в мире JavaScript'ов для всего существуют библиотеки, для нашей задачи такая тоже нашлась: Countable.js. Давайте возьмем стандартный input для текста и расширим его, добавив подсчет слов.
Прикинем, что нам потребуется для реализации нового input'а:
- взять существующий текстовый input и добавить к нему div для вывода количества слов;
- добавить CSS-стили для нового div;
- вызвать Countable.js на нужном нам поле и записать с его помощью информацию о количестве слов в новый div.
Начнем с создания нового класса и наследуем его от Formtastic::Inputs::TextInput. Добавим дополнительный атрибут class="countable-input" к элементу textarea, и рядом с ним создадим новый пустой div с атрибутом class="countable-content":
# app/admin/inputs/countable_input.rb
class CountableInput < Formtastic::Inputs::TextInput
def to_html
input_wrapping do
label_html <<
builder.text_area(method, input_html_options.merge(class: 'countable-input')) <<
template.content_tag(:div, '', class: 'countable-content')
end
end
end
Посмотрим, что нового у нас добавилось. input_html_options— метод родительского класса с говорящим именем. builder — инстанс класса ActiveAdmin::FormBuilder, наследник ActionView::Helpers::FormBuilder. template — это контекст, в котором исполняются темплейты (то есть огромный набор view-helper'ов). Таким образом, если нам нужно создать кусочек формы, то обращаемся к builder. А если хотим использовать что-то типа link_to, то нам поможет template.
Библиотеку Countable.js завендорим: положим в директорию app/assets/javascripts/inputs/countable_input и добавим простенький .js файл, который будет вызывать Countable.js и закидывать информацию в div.countable-content (прошу сильно не пинать ногами за JS-спагетти):
// app/assets/javascripts/inputs/countable_input.js
//= require ./countable_input/countable.min.js
const countable_initializer = function () {
$('.countable-input').each(function (i, e) {
Countable.on(e, function (counter) {
$(e).parent().find('.countable-content').html('words: ' + counter['words']);
});
});
}
$(countable_initializer);
$(document).on('turbolinks:load', countable_initializer);
И теперь подтягиваем файл в app/assets/javascripts/active_admin.js:
// app/assets/javascripts/active_admin.js
// ...
//= require inputs/countable_input
Последний штрих — добавляем CSS-файл и подгружаем его в app/assets/stylesheets/active_admin.scss:
// app/assets/stylesheets/inputs/countable_input.scss
.countable-content {
float: right;
font-weight: bold;
}
// app/assets/stylesheets/active_admin.scss
// ...
@import "inputs/countable_input";
Вот и всё, наш input готов. Осталось только вызвать его в форме:
# app/admin/articles.rb
ActiveAdmin.register Article do
form do |f|
f.inputs do
f.input(:id, as: :hello_world)
f.input(:title)
f.input(:body, as: :countable)
end
f.actions
end
end
Таким образом мы создаем кастомные компоненты для форм в своих проектах. Например, файловые загрузчики или input'ы с хитрым автозаполнением. В подобных компонентах чуть больше кода, но подход остается неизменным.
Formtastic: пламенный привет пуристам
Как и в случае с компонентами Arbre, формы можно выносить в partial'ы, хотя синтаксис немного отличается:
# app/admin/articles.rb
ActiveAdmin.register Article do
form(partial: 'form')
end
# app/views/admin/articles/_form.html.arb
active_admin_form_for resource do
inputs(:title, :body)
actions
end
Недостаток этого подхода в том, что формы лежат где-то глубоко в директории views. На мой взгляд, это немного усложняет навигацию по коду, но тут на вкус и цвет, как говорится.
Formtastic: что еще посмотреть
Formtastic — достаточно обширная библиотека, и я настоятельно рекомендую прочитать подробный README, чтобы ознакомиться со всеми возможностями кастомизации. Также будет полезно посмотреть уже упомянутый activeadmin_addons. В этом gem'е есть множество дополнительных input'ов, которые наверняка пригодятся в хозяйстве.
Отдельно замечу, что хотя в статье я разделил Formtastic и Arbre по разным блокам, они прекрасно работают вместе, ведь вы можете создавать формы или части форм в качестве Arbre-компонентов.
Inherited Resources — кастомизация контроллеров
Чтобы понять. откуда берется магический resource, как поменять поведение при сохранении. и многое другое, нам будет необходимо познакомиться с еще одним gem'ом.
Inherited Resources — библиотека, призванная избавить контроллеры от однообразной CRUD-рутины.
Библиотека, с одной стороны, простая, а с другой обширная. Поэтому галопом по Европам рассмотрим несколько полезных методов:
class ArticlesController < InheritedResources::Base
respond_to :html
respond_to :json, only: :index
actions :index, :new, :create
def update
resource.updated_by = current_user
update! { articles_path }
end
end
Итак, .respond_to отвечает за доступные форматы. Все вызовы .respond_to «складываются», а не переопределяют друг друга. Чтобы сбросить форматы, понадобится метод .clear_respond_to.
.actions определяет доступные CRUD-методы (index, show, new, edit, create, update и destroy).
resource — один из доступных хелперов, среди которых:
resource #=> @article
collection #=> @articles
resource_class #=> Article
И наконец, #update! — это просто alias для #update, который можно использовать при перегрузке методов вместо super.
Отдельно рассмотрим применение метода .has_scope. Предположим, что в классе Article определен scope :published:
class Article < ApplicationRecord
scope :published, -> { where(published: true) }
end
Тогда мы можем использовать в контроллере метод .has_scope:
class ArticlesController < InheritedResources::Base
has_scope :published, type: :boolean
end
.has_scope добавляет возможность фильтрации с помощью query-параметров. В примере выше мы сможем применить scope :published, если обратимся к коллекции по URL /articles?published=true.
Подробное описание этих и других возможностей библиотеки можно найти в обширном README. А мы, пожалуй, остановимся на этом и перейдем, наконец, к взаимодействию с Active Admin.
Inherited Resources: расширение контроллера
Все контроллеры Active Admin наследованы от InheritedResources::Base, а это значит, что у нас есть возможность модифицировать их поведение, используя методы библиотеки.
Например, список доступных action'ов контроллера определяется следующим образом:
# app/admin/articles.rb
ActiveAdmin.register Article do
actions :all, :except => [:destroy]
end
Отлично, мы убрали action удаления статьи. Кажется, всё очевидно: используем ресурс Active Admin как контроллер. Но не будем спешить с выводами и попробуем добавить еще одну фичу.
По умолчанию Active Admin включает рендеринг всех страниц в качестве HTML, JSON и XML (а index доступен еще и в формате CSV). Попробуем избавимся от XML-рендеринга нашей страницы с помощью знакомых нам методов:
# app/admin/articles.rb
ActiveAdmin.register Article do
clear_respond_to
respond_to :html, :json
respond_to :csv, only: :index
end
Ой, теперь мы получили ошибку undefined method 'clear_respond_to' for #<ActiveAdmin::ResourceDSL>.
Дело в том, что когда мы описываем класс-ресурс, мы находимся в контексте ActiveAdmin::ResourceDSL, а не в контексте контроллера. Код из предыдущего примера работает только потому, что ActiveAdmin::ResourceDSL делегирует контроллеру метод #actions.
Но не отчаивайтесь, чтобы добраться до контроллера и выполнить код в его контексте, необходимо всего-навсего вызвать метод #controller:
# app/admin/articles.rb
ActiveAdmin.register Article do
controller do
clear_respond_to
respond_to :html, :json
respond_to :csv, only: :index
end
end
Вуаля, теперь localhost:3000/admin/articles.xml возвращает ошибку. А что на счет модификации поведения action'ов?
Inherited Resources: перегрузка методов
Предположим, что при сохранении нам необходимо задать атрибут Article#created_by_admin. Воспользуемся для этого возможностью перегрузки метода #create:
# app/admin/articles.rb
ActiveAdmin.register Article do
controller do
def create
build_resource
@article.created_by_admin = true
create!
end
end
end
Итак, мы вызываем build_resource — метод, который инициализирует новый объект и присваивает его переменной @article. Далее задаем атрибут created_by_admin и вызываем create! (он же super), который продолжает оперировать созданным нами @article.
Хотелось бы отдельно отметить: будьте внимательны с хелперами. Inherited Resources активно использует instance-переменные для кеширования. В данном случае это помогло нам создать и модифицировать объект, но при неаккуратном использовании, результаты могут быть неожиданными (проверено на собственной шкуре).
А теперь вернемся на пару шагов назад, к моменту, когда мы отключали XML-рендеринг статей. Что, если мы хотим убрать рендеринг XML из всех ресурсов? Не будем же мы писать один и тот же код в каждом новом классе?
Расширение базового контроллера
Не будем! Давайте создадим модуль, который скорректирует поведение класса ActiveAdmin::ResourceController:
# lib/active_admin/remove_xml_rendering_extension.rb
module ActiveAdmin
module RemoveXmlRenderingExtension
def self.included(base)
base.send(:clear_respond_to)
base.send(:respond_to, :html, :json)
base.send(:respond_to, :csv, only: :index)
end
end
end
В метод .included будет передан расширяемый класс, к которому будут применены нужные нам модификаторы. Воспользуемся инициализатором Active Admin и подключим новый модуль к ActiveAdmin::ResourceController:
# config/initializers/active_admin.rb
require 'lib/active_admin/remove_xml_rendering_extension'
ActiveAdmin::ResourceController.send(
:include,
ActiveAdmin::RemoveXmlRenderingExtension
)
# ...
Немного магии метапрограммирования с #include и #included, и готово! Теперь ни один ресурс не ответит на формат .xml.
К слову, если вы думали, что #prepend, #include и #extend — это методы из вопросов, которыми на собеседованиях валят неугодных, то боюсь вас разочаровать. Когда возникает необходимость модифицировать код внешней библиотеки, подобные подходы нередко становятся единственным доступным инструментом.
Inherited Resources: что еще посмотреть
Прежде всего обратите внимание на подробный README. Помимо этого посмотрите на то, как устроены контроллеры в Active Admin, обратите внимание на логику авторизации и другие мелочи, вроде дополнительных хелперов.
Ransack: кастомизация фильтров
По умолчанию Active Admin на каждой index-странице предоставляет развесистый блок с фильтрацией, из которого чаще приходится убирать лишнее, нежели добавлять что-то свое. Но на самом деле это лишь верхушка айсберга под названием Ransack.
Ransack — библиотека для создания поисковых форм, которая позволяет собирать сложные SQL-запросы, интерпретируя переданные имена параметров. Звучит сложно, но я уверен, пример позволит быстро разобраться. о чем идет речь.
Предположим, что нам необходимо фильтровать записи блога (Article) по вхождению строки в название (title). С помощью Ransack мы можем это сделать следующим образом:
Article.ransack(title_cont: 'Домклик').result
Постфикс _cont — это один из множества предикатов, доступных в Ransack. Предикаты определяют то, какой SQL-запрос будет сгенерирован для поиска. Подробно обо всех доступных предикатах можно прочитать в официальной wiki.
А теперь чуть усложним задачу: заказчик попросил нас добавить фильтр, который позволит искать вхождение строки одновременно и в заголовке, и в теле (body). С Ransack это проще некуда:
Article.ransack(title_or_body_cont: 'active admin').result
Помимо этого, Ransack позволяет искать записи, обращаясь к связанным моделям. Для демонстрации, добавим возможность искать статьи по тексту комментариев (Comment#text):
Article.ransack(comments_text_cont: 'I hate type annotations!').result
Как несложно догадаться, подобные конструкции могут быстро разрастись. Да и использование сложных параметров в нескольких местах может привести к проблемам. В качестве решения Ransack предлагает использовать #ransack_alias. Добавим к поиску по тексту комментария поиск по его автору и дадим короткий alias: comments, который в дальнейшем можно будет использовать с нужными нам предикатами:
# app/models/article.rb
class Article < ActiveRecord::Base
has_many :comments
ransack_alias :comments, :comments_text_or_comments_author
end
Article.ransack(comments_cont: 'Matz').result
Разобравшись с тем, как Ransack позволяет структурировать запросы, перейдем, наконец, к тому, как мы можем использовать это в Active Admin.
Ransack: использование составных фильтров
Возьмем за основу примеры выше и используем их в качестве фильтров для ресурса Active Admin:
# app/admin/articles.rb
ActiveAdmin.register Article do
preserve_default_filters!
filter :title_or_body_cont,
as: :string,
label: I18n.t('active_admin.filters.title_or_body_cont')
filter :comments,
as: :string
end
Вот и всё, весьма прямолинейно. Разве что отмечу метод #preserve_default_filters!, который оставляет на месте стандартные фильтры.
Ransack: использование scope-фильтров
По умолчанию Ransack позволяет фильтровать по всем атрибутам и связям модели. Это может быть опасно с точки зрения безопасности, поэтому обратите внимание на возможность ограничения доступа к определенным полям и связям с помощью методов ransackable_attributes, ransackable_associations и ransackable_scopes. Вопросы авторизации я хотел бы оставить за рамками данной статьи (тем более, что у Active Admin в документации есть подробный раздел), поэтому обратим внимание лишь на метод ransackable_scopes.
В отличие от других методов авторизации, ransackable_scopes по умолчанию запрещает использование любых scope'ов. Таким образом, чтобы иметь возможность фильтровать по scope (или по любому другому методу класса модели), необходимо вернуть его название из .ransackable_scopes.
Для примера, добавим фильтр по количеству комментариев с использованием scope:
# app/models/article.rb
class Article < ActiveRecord::Base
has_many :comments
scope :comments_count_gt, (lambda do |comments_count|
joins(:comments)
.group('articles.id')
.having('count(comments.id) > ?', comments_count)
end)
def self.ransackable_scopes(auth_object = nil)
[:comments_count_gt]
end
end
Обратите внимание на auth_object: в теории, это объект по которому можно определить стратегию авторизации. Я бы ожидал, что сюда будет передаваться current_user, однако Active Admin этого не делает.
Мы добавили scope и вернули его название в .ransackable_scopes, осталось только добавить фильтр в ресурс Active Admin:
# app/admin/articles.rb
ActiveAdmin.register Article do
filter :comments_count_gt,
as: :number,
label: I18n.t('active_admin.filters.comments_count_gt')
Осталась одна мелочь: если мы попробуем отфильтровать все статьи с двумя и более комментариями — всё отлично, но если попробовать подать единицу, то мы получим ошибку:
Это нам «помогло» приведение типов, которое по историческим причинам делает Ransack. Чтобы отключить сомнительную фичу, мы добавим инициализатор с заданным параметром sanitize_custom_scope_booleans:
# /config/initializers/ransack.rb
Ransack.configure do |config|
config.sanitize_custom_scope_booleans = false
end
Готово, теперь наш фильтр работает, даже если мы подадим 1 в качестве аргумента, и мы умеем использовать фильтры на основе scope'ов.
Ransack: что еще посмотреть
Прежде всего, стоит еще раз заглянуть в документацию Active Admin про фильтры. Продолжить обзор можно в официальных README и wiki, в которых, помимо всего прочего, вы сможете найти view-хелперы для создания своих поисковых форм.
Для особо запущенных случаев вы можете обратить внимание на то, как создавать собственные предикаты, и на Ransackers — расширения, которые преобразуют параметры напрямую в Arel (внутренняя библиотека ActiveRecord, используемая для конструирования SQL-запросов).
Итоги
Надеюсь, что после этой статьи вы взглянули на Active Admin с новой стороны и, возможно, захотели зарефакторить класс-другой в своих проектах. Ведь Active Admin позволяет быстро запустить рабочую систему и направить все силы frontend-разработчиков на полезный для конечного пользователя продукт.
Я старался не сильно пересекаться с официальной документацией Active Admin, в которой можно найти описание множества интересных возможностей библиотеки, например, авторизацию или использование декораторов.
Также в очередной раз упомяну activeadmin_addons, в котором, помимо множества компонентов, доступна симпатичная тема для Active Admin. Обратите внимание на то, как она устроена, если захотите сделать свою тему для админки и использовать ее во всех проектах (именно так и сделано у нас в Домклике).
===========
Источник:
habr.com
===========
Похожие новости:
- [DevOps, Kubernetes] Kubernetes в ДомКлик: как спать спокойно, управляя кластером на 1000 микросервисов
- [Ruby, Python, Программирование] Как работают профайлеры в Ruby и Python?
- [Ruby, Ruby on Rails, Администрирование баз данных, Хранение данных, Хранилища данных] Миграции данных в Ruby On Rails
- [PostgreSQL] Обновление версий PostgreSQL, или Как не уронить базу при update?
- [Ruby, Совершенный код, Компиляторы, Софт] Сложности работы с ANTLR: пишем грамматику Ruby
- [JavaScript, Node.JS, Разработка веб-сайтов] Lock-файлы npm
- [Гаджеты, Научно-популярное, Носимая электроника] Об отслеживании социальных контактов и аппаратных жетонах (перевод)
- [C, Java, Python, Исследования и прогнозы в IT, Программирование] IEEE опубликовал новый рейтинг языков программирования
- [JavaScript, Node.JS, Разработка веб-сайтов] Выбор зависимостей JavaScript
- [JavaScript, ReactJS, Разработка веб-сайтов] Debouncing с помощью React Hooks: хук для функций
Теги для поиска: #_ruby, #_ruby_on_rails, #_ruby_on_rails, #_active_admin, #_arbre, #_formtastic, #_blog_kompanii_domklik (
Блог компании ДомКлик
), #_ruby, #_ruby_on_rails
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 26-Ноя 07:45
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Статья про Ruby в блоге компании ДомКлик! Как так получилось, что в молодую компанию завезли мертвый язык? Секрет в том, что на Ruby можно быстро написать и протестировать бизнес-идею. И делается это не без помощи Rails и Active Admin — библиотеки, которая позволяет быстро создать админку с минимальными затратами сил и времени. Часто можно встретить мнение, что Active Admin хорош только для 15-минутного блога. Мы в ДомКлик считаем (и доказываем на практике), что из этой библиотеки можно выжать намного больше. Я расскажу про некоторые подходы, которые мы применяем при работе с Active Admin. Active Admin базируется на нескольких библиотеках, среди которых я бы выделил arbre, formtastic, inherited_resources и ransack. Каждая из них отвечает за свою часть и заслуживает отдельного рассмотрения. Начнем по алфавиту — с библиотеки, которая отпочковалась от самого Active Admin. Arbre: кастомизация компонентов Одна из проблем Active Admin — на глазах распухающие файлы ресурсов: фильтры, дополнительные action'ы, верстка страниц, формы, и всё это в одном файле. Где-то вдали слышен протяжный стон одинокого пуриста «где же single responsibility?» Не завезли. Но давайте разберемся, как можно изолировать часть верстки в отдельных классах. Arbre — библиотека для описания шаблонов с помощью Ruby. Вот пример простейшей страницы, написанной с помощью DSL Arbre: html do
head do title('Welcome page') end body do para('Hello, world') end end DSL расширяется с помощью компонентов. Например, в Active Admin это tabs, table_for, paginated_collection и даже сами страницы. Продолжим знакомство с библиотекой рассмотрением структуры простейшего Arbre компонента. Arbre: hello world компонент Как и все компоненты Arbre, наш Admin::Components::HelloWorld наследован от класса Arbre::Component: # app/admin/components/hello_world.rb
module Admin module Components class HelloWorld < Arbre::Component builder_method :hello_world def build(attributes = {}) super(attributes) text_node('Hello world!') add_class('hello-world') end def tag_name 'h1' end end end end Начнем сверху вниз: builder_method определяет метод, с помощью которого мы сможем создать компонент при использовании DSL. Аргументы, переданные в компонент, попадут в метод #build. В Arbre каждый компонент — это отдельный DOM-элемент (напоминает механизм работы современных frontend-фреймворков, только датируется 2012 годом). По умолчанию все компоненты представляют из себя div, чтобы изменить это поведение, можно переопределить метод #tag_name. Метод #add_class, как не сложно догадаться, добавляет атрибут class к корневому DOM-элементу. Осталось вызвать наш новый компонент. Для примера, сделаем это в app/admin/dashboard.rb # app/admin/dashboard.rb
ActiveAdmin.register_page 'Dashboard' do menu priority: 1, label: proc { I18n.t('active_admin.dashboard') } content do hello_world end end Теперь рассмотрим пример небольшого рефакторинга админки с использованием собственного компонента. Arbre: пример из реальной жизни (почти) Для того, чтобы понять, как использовать Arbre в условиях, приближенных к боевым, возьмем синтетический пример. Предположим, что у нас есть блог с записями (Article) и комментариями (Comment) со связью 1:M. Нам необходимо вывести 10 последних комментариев на странице конкретной записи (блок show). # app/admin/articles.rb
ActiveAdmin.register Article do permit_params :title, :body show do attributes_table(:body, :created_at) panel I18n.t('active_admin.articles.new_comments') do table_for resource.comments.order(created_at: :desc).first(10) do column(:author) column(:text) column(:created_at) end end end end А теперь вынесем таблицу с комментариями в отдельный компонент. Создадим новый класс и унаследуем его от ActiveAdmin::Views::Panel. Если создать новый компонент с нуля (как в hello_world выше) и в нем вызвать panel, то panel окажется внутри еще одного div, а это наверняка поломает верстку. Мы в нашей команде разместили бы этот класс в app/admin/components/articles/new_comments.rb, но это вкусовщина. Просто знайте, что Active Admin автоматически загрузит всё, что находится внутри app/admin/**/*: # app/admin/components/articles/new_comments.rb
module Admin module Components module Articles class NewComments < ActiveAdmin::Views::Panel builder_method :articles_new_comments def build(article) super(I18n.t('active_admin.articles.new_comments')) table_for last_comments(article) do column(:author) column(:text) column(:created_at) end end private def last_comments(article) article.comments .order(created_at: :desc) .first(10) end end end end end Теперь заменим panel в app/admin/articles.rb на вызов нашего нового компонента и передадим в него resource: # app/admin/articles.rb
ActiveAdmin.register Article do permit_params :title, :body show do attributes_table(:body, :created_at) articles_new_comments(resource) end end Красота! Отмечу, что resource можно было бы не передавать в компонент, а использовать через контекст. Однако, явно передав resource, мы ослабили связность компонента, что позволит переиспользовать его в будущем. К слову о переиспользовании, всё содержимое блока show (как и других блоков с шаблонами) можно вынести в partial: # app/admin/articles.rb
ActiveAdmin.register Article do show do render('show', article: resource) end end # app/views/admin/articles/_show.html.arb
panel(ActiveAdmin::Localizers.resource(active_admin_config).t(:details)) do attributes_table_for(article, :body, :created_at) end articles_new_comments(article) Само собой, вы можете использовать знакомый .erb и другие шаблонизаторы, но, пожалуй, оставим это в качестве факультатива. Arbre: что еще посмотреть Прежде всего, я посоветовал бы ознакомиться с описанием компонентов Active Admin в официальной документации. Для более глубокого изучения можно посмотреть код базовых компонентов из arbre и компонентов activeadmin, ведь часто именно на их основе будут строиться ваши собственные. Кроме того, обратите внимание на gem activeadmin_addons, в котором есть множество интересных компонентов. Ну, а если вы вдруг до сих пор не пишете код без ошибок, то стоит обратить внимание на то, как можно тестировать компоненты. Formtastic: кастомизация форм Formtastic — библиотека для описания форм с помощью DSL. Простейшая форма выглядит вот так: semantic_form_for object do |f|
f.inputs f.actions end В этом примере Formtastic автоматически вытаскивает все атрибуты из переданного объекта object и подставляет их в форму с типами input'ов по-умолчанию. Список доступных типов input'ов можно найти в README. Как и Arbre, Formtastic можно расширить с помощью создания собственных классов-компонентов. Для того, чтобы разобраться в базовых вещах, давайте создадим hello world компонент. Formtastic: hello world компонент По аналогии с компонентами Arbre, разместим новый класс в app/admin/inputs: # app/admin/inputs/hello_world_input.rb
class HelloWorldInput include Formtastic::Inputs::Base def to_html "Input for ##{object.public_send(method)}" end end Чтобы вызвать новый input, достаточно указать его название в параметре :as, например, так: # app/admin/articles.rb
ActiveAdmin.register Article do form do |f| f.inputs do f.input(:id, as: :hello_world) f.input(:title) f.input(:body) end f.actions end end Все необходимые для отрисовки формы параметры (в том числе object и символ method) попадают в #initialize, определенный в модуле Formtastic::Inputs::Base. За отображение input'а отвечает метод #to_html. Может показаться что этот пример бесполезен, но на самом деле на его основе мы в компании рендерим read-only поля. Давайте добавим всего пару методов, доступных в Formtastic, и превратим наш hello world в полезный read-only input. Следите за руками: # app/admin/inputs/hello_world_input.rb
class HelloWorldInput include Formtastic::Inputs::Base def to_html input_wrapping do label_html << object.public_send(method).to_s end end end Всё, что мы добавили — это два метода с говорящими названиями. input_wrapping пришел из модуля Formtastic::Inputs::Base::Wrapping и отвечает за обертку input'а. В том числе, он включает в себя элементы для вывода ошибок и подсказок. label_html из модуля Formtastic::Inputs::Base::Labelling рендерит лейбл для input'а. Эти два хелпера мгновенно превращают наш hello world в применимый в бою input (разве что нейминг класса бы еще поправить). Теперь мы можем перейти к чуть более сложному примеру, который продемонстрирует, как можно интегрировать в форму JS-библиотеку. Formtastic: пример из реальной жизни (почти) Возьмем за основу очередной выдуманный пример, который продемонстрирует, как работать с HTML, CSS и JS. То есть покроет все шаги написания нового input'а. Предположим, что к нам пришел запрос от редактора блога: при написании статьи он хотел бы прямо в форме ввода видеть количество слов. Как известно, в мире JavaScript'ов для всего существуют библиотеки, для нашей задачи такая тоже нашлась: Countable.js. Давайте возьмем стандартный input для текста и расширим его, добавив подсчет слов. Прикинем, что нам потребуется для реализации нового input'а:
Начнем с создания нового класса и наследуем его от Formtastic::Inputs::TextInput. Добавим дополнительный атрибут class="countable-input" к элементу textarea, и рядом с ним создадим новый пустой div с атрибутом class="countable-content": # app/admin/inputs/countable_input.rb
class CountableInput < Formtastic::Inputs::TextInput def to_html input_wrapping do label_html << builder.text_area(method, input_html_options.merge(class: 'countable-input')) << template.content_tag(:div, '', class: 'countable-content') end end end Посмотрим, что нового у нас добавилось. input_html_options— метод родительского класса с говорящим именем. builder — инстанс класса ActiveAdmin::FormBuilder, наследник ActionView::Helpers::FormBuilder. template — это контекст, в котором исполняются темплейты (то есть огромный набор view-helper'ов). Таким образом, если нам нужно создать кусочек формы, то обращаемся к builder. А если хотим использовать что-то типа link_to, то нам поможет template. Библиотеку Countable.js завендорим: положим в директорию app/assets/javascripts/inputs/countable_input и добавим простенький .js файл, который будет вызывать Countable.js и закидывать информацию в div.countable-content (прошу сильно не пинать ногами за JS-спагетти): // app/assets/javascripts/inputs/countable_input.js
//= require ./countable_input/countable.min.js const countable_initializer = function () { $('.countable-input').each(function (i, e) { Countable.on(e, function (counter) { $(e).parent().find('.countable-content').html('words: ' + counter['words']); }); }); } $(countable_initializer); $(document).on('turbolinks:load', countable_initializer); И теперь подтягиваем файл в app/assets/javascripts/active_admin.js: // app/assets/javascripts/active_admin.js
// ... //= require inputs/countable_input Последний штрих — добавляем CSS-файл и подгружаем его в app/assets/stylesheets/active_admin.scss: // app/assets/stylesheets/inputs/countable_input.scss
.countable-content { float: right; font-weight: bold; } // app/assets/stylesheets/active_admin.scss
// ... @import "inputs/countable_input"; Вот и всё, наш input готов. Осталось только вызвать его в форме: # app/admin/articles.rb
ActiveAdmin.register Article do form do |f| f.inputs do f.input(:id, as: :hello_world) f.input(:title) f.input(:body, as: :countable) end f.actions end end Таким образом мы создаем кастомные компоненты для форм в своих проектах. Например, файловые загрузчики или input'ы с хитрым автозаполнением. В подобных компонентах чуть больше кода, но подход остается неизменным. Formtastic: пламенный привет пуристам Как и в случае с компонентами Arbre, формы можно выносить в partial'ы, хотя синтаксис немного отличается: # app/admin/articles.rb
ActiveAdmin.register Article do form(partial: 'form') end # app/views/admin/articles/_form.html.arb
active_admin_form_for resource do inputs(:title, :body) actions end Недостаток этого подхода в том, что формы лежат где-то глубоко в директории views. На мой взгляд, это немного усложняет навигацию по коду, но тут на вкус и цвет, как говорится. Formtastic: что еще посмотреть Formtastic — достаточно обширная библиотека, и я настоятельно рекомендую прочитать подробный README, чтобы ознакомиться со всеми возможностями кастомизации. Также будет полезно посмотреть уже упомянутый activeadmin_addons. В этом gem'е есть множество дополнительных input'ов, которые наверняка пригодятся в хозяйстве. Отдельно замечу, что хотя в статье я разделил Formtastic и Arbre по разным блокам, они прекрасно работают вместе, ведь вы можете создавать формы или части форм в качестве Arbre-компонентов. Inherited Resources — кастомизация контроллеров Чтобы понять. откуда берется магический resource, как поменять поведение при сохранении. и многое другое, нам будет необходимо познакомиться с еще одним gem'ом. Inherited Resources — библиотека, призванная избавить контроллеры от однообразной CRUD-рутины. Библиотека, с одной стороны, простая, а с другой обширная. Поэтому галопом по Европам рассмотрим несколько полезных методов: class ArticlesController < InheritedResources::Base
respond_to :html respond_to :json, only: :index actions :index, :new, :create def update resource.updated_by = current_user update! { articles_path } end end Итак, .respond_to отвечает за доступные форматы. Все вызовы .respond_to «складываются», а не переопределяют друг друга. Чтобы сбросить форматы, понадобится метод .clear_respond_to. .actions определяет доступные CRUD-методы (index, show, new, edit, create, update и destroy). resource — один из доступных хелперов, среди которых: resource #=> @article
collection #=> @articles resource_class #=> Article И наконец, #update! — это просто alias для #update, который можно использовать при перегрузке методов вместо super. Отдельно рассмотрим применение метода .has_scope. Предположим, что в классе Article определен scope :published: class Article < ApplicationRecord
scope :published, -> { where(published: true) } end Тогда мы можем использовать в контроллере метод .has_scope: class ArticlesController < InheritedResources::Base
has_scope :published, type: :boolean end .has_scope добавляет возможность фильтрации с помощью query-параметров. В примере выше мы сможем применить scope :published, если обратимся к коллекции по URL /articles?published=true. Подробное описание этих и других возможностей библиотеки можно найти в обширном README. А мы, пожалуй, остановимся на этом и перейдем, наконец, к взаимодействию с Active Admin. Inherited Resources: расширение контроллера Все контроллеры Active Admin наследованы от InheritedResources::Base, а это значит, что у нас есть возможность модифицировать их поведение, используя методы библиотеки. Например, список доступных action'ов контроллера определяется следующим образом: # app/admin/articles.rb
ActiveAdmin.register Article do actions :all, :except => [:destroy] end Отлично, мы убрали action удаления статьи. Кажется, всё очевидно: используем ресурс Active Admin как контроллер. Но не будем спешить с выводами и попробуем добавить еще одну фичу. По умолчанию Active Admin включает рендеринг всех страниц в качестве HTML, JSON и XML (а index доступен еще и в формате CSV). Попробуем избавимся от XML-рендеринга нашей страницы с помощью знакомых нам методов: # app/admin/articles.rb
ActiveAdmin.register Article do clear_respond_to respond_to :html, :json respond_to :csv, only: :index end Ой, теперь мы получили ошибку undefined method 'clear_respond_to' for #<ActiveAdmin::ResourceDSL>. Дело в том, что когда мы описываем класс-ресурс, мы находимся в контексте ActiveAdmin::ResourceDSL, а не в контексте контроллера. Код из предыдущего примера работает только потому, что ActiveAdmin::ResourceDSL делегирует контроллеру метод #actions. Но не отчаивайтесь, чтобы добраться до контроллера и выполнить код в его контексте, необходимо всего-навсего вызвать метод #controller: # app/admin/articles.rb
ActiveAdmin.register Article do controller do clear_respond_to respond_to :html, :json respond_to :csv, only: :index end end Вуаля, теперь localhost:3000/admin/articles.xml возвращает ошибку. А что на счет модификации поведения action'ов? Inherited Resources: перегрузка методов Предположим, что при сохранении нам необходимо задать атрибут Article#created_by_admin. Воспользуемся для этого возможностью перегрузки метода #create: # app/admin/articles.rb
ActiveAdmin.register Article do controller do def create build_resource @article.created_by_admin = true create! end end end Итак, мы вызываем build_resource — метод, который инициализирует новый объект и присваивает его переменной @article. Далее задаем атрибут created_by_admin и вызываем create! (он же super), который продолжает оперировать созданным нами @article. Хотелось бы отдельно отметить: будьте внимательны с хелперами. Inherited Resources активно использует instance-переменные для кеширования. В данном случае это помогло нам создать и модифицировать объект, но при неаккуратном использовании, результаты могут быть неожиданными (проверено на собственной шкуре). А теперь вернемся на пару шагов назад, к моменту, когда мы отключали XML-рендеринг статей. Что, если мы хотим убрать рендеринг XML из всех ресурсов? Не будем же мы писать один и тот же код в каждом новом классе? Расширение базового контроллера Не будем! Давайте создадим модуль, который скорректирует поведение класса ActiveAdmin::ResourceController: # lib/active_admin/remove_xml_rendering_extension.rb
module ActiveAdmin module RemoveXmlRenderingExtension def self.included(base) base.send(:clear_respond_to) base.send(:respond_to, :html, :json) base.send(:respond_to, :csv, only: :index) end end end В метод .included будет передан расширяемый класс, к которому будут применены нужные нам модификаторы. Воспользуемся инициализатором Active Admin и подключим новый модуль к ActiveAdmin::ResourceController: # config/initializers/active_admin.rb
require 'lib/active_admin/remove_xml_rendering_extension' ActiveAdmin::ResourceController.send( :include, ActiveAdmin::RemoveXmlRenderingExtension ) # ... Немного магии метапрограммирования с #include и #included, и готово! Теперь ни один ресурс не ответит на формат .xml. К слову, если вы думали, что #prepend, #include и #extend — это методы из вопросов, которыми на собеседованиях валят неугодных, то боюсь вас разочаровать. Когда возникает необходимость модифицировать код внешней библиотеки, подобные подходы нередко становятся единственным доступным инструментом. Inherited Resources: что еще посмотреть Прежде всего обратите внимание на подробный README. Помимо этого посмотрите на то, как устроены контроллеры в Active Admin, обратите внимание на логику авторизации и другие мелочи, вроде дополнительных хелперов. Ransack: кастомизация фильтров По умолчанию Active Admin на каждой index-странице предоставляет развесистый блок с фильтрацией, из которого чаще приходится убирать лишнее, нежели добавлять что-то свое. Но на самом деле это лишь верхушка айсберга под названием Ransack. Ransack — библиотека для создания поисковых форм, которая позволяет собирать сложные SQL-запросы, интерпретируя переданные имена параметров. Звучит сложно, но я уверен, пример позволит быстро разобраться. о чем идет речь. Предположим, что нам необходимо фильтровать записи блога (Article) по вхождению строки в название (title). С помощью Ransack мы можем это сделать следующим образом: Article.ransack(title_cont: 'Домклик').result
Постфикс _cont — это один из множества предикатов, доступных в Ransack. Предикаты определяют то, какой SQL-запрос будет сгенерирован для поиска. Подробно обо всех доступных предикатах можно прочитать в официальной wiki. А теперь чуть усложним задачу: заказчик попросил нас добавить фильтр, который позволит искать вхождение строки одновременно и в заголовке, и в теле (body). С Ransack это проще некуда: Article.ransack(title_or_body_cont: 'active admin').result
Помимо этого, Ransack позволяет искать записи, обращаясь к связанным моделям. Для демонстрации, добавим возможность искать статьи по тексту комментариев (Comment#text): Article.ransack(comments_text_cont: 'I hate type annotations!').result
Как несложно догадаться, подобные конструкции могут быстро разрастись. Да и использование сложных параметров в нескольких местах может привести к проблемам. В качестве решения Ransack предлагает использовать #ransack_alias. Добавим к поиску по тексту комментария поиск по его автору и дадим короткий alias: comments, который в дальнейшем можно будет использовать с нужными нам предикатами: # app/models/article.rb
class Article < ActiveRecord::Base has_many :comments ransack_alias :comments, :comments_text_or_comments_author end Article.ransack(comments_cont: 'Matz').result Разобравшись с тем, как Ransack позволяет структурировать запросы, перейдем, наконец, к тому, как мы можем использовать это в Active Admin. Ransack: использование составных фильтров Возьмем за основу примеры выше и используем их в качестве фильтров для ресурса Active Admin: # app/admin/articles.rb
ActiveAdmin.register Article do preserve_default_filters! filter :title_or_body_cont, as: :string, label: I18n.t('active_admin.filters.title_or_body_cont') filter :comments, as: :string end Вот и всё, весьма прямолинейно. Разве что отмечу метод #preserve_default_filters!, который оставляет на месте стандартные фильтры. Ransack: использование scope-фильтров По умолчанию Ransack позволяет фильтровать по всем атрибутам и связям модели. Это может быть опасно с точки зрения безопасности, поэтому обратите внимание на возможность ограничения доступа к определенным полям и связям с помощью методов ransackable_attributes, ransackable_associations и ransackable_scopes. Вопросы авторизации я хотел бы оставить за рамками данной статьи (тем более, что у Active Admin в документации есть подробный раздел), поэтому обратим внимание лишь на метод ransackable_scopes. В отличие от других методов авторизации, ransackable_scopes по умолчанию запрещает использование любых scope'ов. Таким образом, чтобы иметь возможность фильтровать по scope (или по любому другому методу класса модели), необходимо вернуть его название из .ransackable_scopes. Для примера, добавим фильтр по количеству комментариев с использованием scope: # app/models/article.rb
class Article < ActiveRecord::Base has_many :comments scope :comments_count_gt, (lambda do |comments_count| joins(:comments) .group('articles.id') .having('count(comments.id) > ?', comments_count) end) def self.ransackable_scopes(auth_object = nil) [:comments_count_gt] end end Обратите внимание на auth_object: в теории, это объект по которому можно определить стратегию авторизации. Я бы ожидал, что сюда будет передаваться current_user, однако Active Admin этого не делает. Мы добавили scope и вернули его название в .ransackable_scopes, осталось только добавить фильтр в ресурс Active Admin: # app/admin/articles.rb
ActiveAdmin.register Article do filter :comments_count_gt, as: :number, label: I18n.t('active_admin.filters.comments_count_gt') Осталась одна мелочь: если мы попробуем отфильтровать все статьи с двумя и более комментариями — всё отлично, но если попробовать подать единицу, то мы получим ошибку: Это нам «помогло» приведение типов, которое по историческим причинам делает Ransack. Чтобы отключить сомнительную фичу, мы добавим инициализатор с заданным параметром sanitize_custom_scope_booleans: # /config/initializers/ransack.rb
Ransack.configure do |config| config.sanitize_custom_scope_booleans = false end Готово, теперь наш фильтр работает, даже если мы подадим 1 в качестве аргумента, и мы умеем использовать фильтры на основе scope'ов. Ransack: что еще посмотреть Прежде всего, стоит еще раз заглянуть в документацию Active Admin про фильтры. Продолжить обзор можно в официальных README и wiki, в которых, помимо всего прочего, вы сможете найти view-хелперы для создания своих поисковых форм. Для особо запущенных случаев вы можете обратить внимание на то, как создавать собственные предикаты, и на Ransackers — расширения, которые преобразуют параметры напрямую в Arel (внутренняя библиотека ActiveRecord, используемая для конструирования SQL-запросов). Итоги Надеюсь, что после этой статьи вы взглянули на Active Admin с новой стороны и, возможно, захотели зарефакторить класс-другой в своих проектах. Ведь Active Admin позволяет быстро запустить рабочую систему и направить все силы frontend-разработчиков на полезный для конечного пользователя продукт. Я старался не сильно пересекаться с официальной документацией Active Admin, в которой можно найти описание множества интересных возможностей библиотеки, например, авторизацию или использование декораторов. Также в очередной раз упомяну activeadmin_addons, в котором, помимо множества компонентов, доступна симпатичная тема для Active Admin. Обратите внимание на то, как она устроена, если захотите сделать свою тему для админки и использовать ее во всех проектах (именно так и сделано у нас в Домклике). =========== Источник: habr.com =========== Похожие новости:
Блог компании ДомКлик ), #_ruby, #_ruby_on_rails |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 26-Ноя 07:45
Часовой пояс: UTC + 5