ActiveStorage::Variant. Креатив на рельсах

от автора

Краткий этот туториал будет полезен, хотелось бы надеяться, как программистам, работающим с Ruby on Rails, так и тем из «племени младого незнакомого» веб-разработчиков, кто захочет освоить отличный инструмент, увидевший свет в Rails 5.2 («извлечен из прода» Basecamp 3 усилиями George Claghorn и Javan Makhmali) — фреймворк Active Storage. Фреймворк делает простым и на редкость удобным загрузку файлов в облако (прямо сразу «из коробки» доступны Amazon S3, Google Cloud Storage и Microsoft Azure Storage), также — если речь об изображениях, видео или PDF-файлах — создание превью на лету.

Не буду сейчас ни сравнивать Active Storage с задолго до него существующими решениями CarrierWave, Paperclip или Shrine, ни рассуждать о полиморфизме таблицы active_storage_attachments; все это хорошо и многажды описано, без проблем способно быть, при желании и наличии интереса, найдено в доках. Остановлюсь всего только на нескольких практических моментах использования класса ActiveStorage::Variant, требующих, как известно, ImageMagick или libvips посредством джема image_processing:

# Gemfile gem "image_processing", "~> 1.0"

Забегая вперед и чтобы не казаться голословным: живая демка, также ссылка на гитхаб здесь. «Тот, кто дойдет — увидит», так что вперед.

Генерация HTML галереи изображений не вызывает больших вопросов, более-менее в соответствии с докой итерируем массив и увязываем пути с классами (например) бутстрапа, создав для удобства простенький хелпер:

def path_to_file(x)   Rails.application.routes.url_helpers.rails_blob_path(x, only_path: true) end
<% @slideshow.images.each do |x| %> <%= link_to(     path_to_file(x), html_options = {             "data-lightbox" => "photo", "class" => "col-sm-4" }) do %> <%= image_tag x.variant(@options), "class" => "img-fluid" %> <% end %> <% end %>

Лайтбокс — любой из несть им числа. Какой хотите, без разницы.

Панель управления галереи на основе Active Admin выглядит вполне пристойно:

, что реализовано следующим образом:

ActiveAdmin.register Slideshow do   permit_params :published_at, :name, :options, images: []   remove_filter :images_attachments, :images_blobs, :options    scope :all   scope :published   scope :unpublished    action_item :publish, only: :show do     link_to 'Publish', publish_admin_slideshow_path(slideshow), method: :put unless slideshow.published_at?   end    action_item :unpublish, only: :show do     link_to 'Unpublish', unpublish_admin_slideshow_path(slideshow), method: :put if slideshow.published_at?   end    action_item :delete_images, only: :show do     if slideshow.images.attached?       link_to 'Delete Images', delete_images_admin_slideshow_path(slideshow), method: :delete     end   end    member_action :publish, method: :put do     slideshow = Slideshow.find(params[:id])     slideshow.update(published_at: Time.zone.now)     redirect_to admin_slideshow_path(slideshow)   end    member_action :unpublish, method: :put do     slideshow = Slideshow.find(params[:id])     slideshow.update(published_at: nil)     redirect_to admin_slideshow_path(slideshow)   end    member_action :delete_images, method: :delete do     slideshow = Slideshow.find(params[:id])     # asset = ActiveStorage::Attachment.find_by(params[:attachment_id])     slideshow.images.purge_later     redirect_to admin_slideshow_path(slideshow)   end    member_action :delete_image, method: :delete do     slideshow = Slideshow.find_by(params[:name])     slideshow.images[params[:id].to_i].purge_later     redirect_to admin_slideshow_path(slideshow)   end    form do |f|     f.inputs 'Slideshow' do       f.input :name       f.input :options,               input_html: { value: f.object.options || '{ "resize_to_limit": [300, 222], "kuwahara": "3%", "quality": 15 }' },               label: 'Options. For example: { "resize_to_limit": [300, 222], "monochrome": true }'       f.input :images, as: :file, input_html: { multiple: true }     end     f.actions   end   show do |t|     attributes_table do       if t.images.attached?         t.images.each_with_index do |img, index|           span do             link_to delete_image_admin_slideshow_path(index), method: :delete do               image_tag img.variant(resize_to_limit: [100, 100])             end           end         end       end       row :name       row :created_at       row :updated_at       row :published_at     end     para 'Click the preview to delete the image.'   end end 

Но тут вдруг возникает вопрос: каким образом передать в ActiveStorage::Variant параметры обработки изображений? — несложно указать напрямую в коде, но для удобства работы администратора сайта хотелось бы (несмотря на любую контраргументацию) записать их в базу данных, обеспечив возможность ввода и редактирования из админки. Но не evalом же их потом доставать, в самом-то деле! — небезопасно, согласитесь. Даже с учетом рубиновых уровней безопасности (с которыми, к слову, вообще непонятно что происходит):

@options = proc {   $SAFE = 1   eval(Slideshow.take.options) }.call

Нет, так не пойдет. Плавно двигаясь эмпирическим (а как еще) путем познания — приходим к тому, что параметры вполне можно записывать и хранить как JSON:

class AddOptionsToSlideshow < ActiveRecord::Migration[6.1]   def change     add_column :slideshows, :options, :json   end end

Таким образом, совсем просто:

@options = Slideshow.take.options

Дальше — больше. Что мешает нам валидировать вводимый админом JSON, указав допустимые для тех или иных ключей (коих ImageMagick имеет великое множество, глаза разбегаются) типы значений? — ровно ничто не мешает. Инсталлируем:

gem 'activerecord_json_validator', '~> 2.0.0'

, добавляя в модель валидацию:

PROFILE_JSON_SCHEMA = Pathname.new(Rails.root.join('config', 'schemas', 'slideshow.json')) validates :options, presence: true, json: {   schema: PROFILE_JSON_SCHEMA,   message: ->(errors) { errors } }

, профайл же slideshow.json, на основе которого пойдет проверка, выглядит у нас, предположим (полностью зависит от чувства меры и художественного вкуса разработчика), следующим образом:

{   "type": "object",   "$schema": "http://json-schema.org/draft-04/schema#",   "properties": {     "resize_to_limit": { "type": "array" },     "monochrome": { "type": "boolean" },     "kuwahara": {"type": "string"},     "sepia-tone": {"type": "string"},     "quality": { "type": "integer" },     "polaroid": { "type": "integer" }   },   "required": ["resize_to_limit"] } 

Все уже давно работает, но ведь на рельсах аппетит приходит во время езды. Хм, а какой русский не любит быстрой езды… ок, пишем спек:

require 'rails_helper'  RSpec.describe Slideshow, type: :model do   it { is_expected.to have_many_attached(:images) }    describe 'validates data column' do     subject { described_class.new(options: options) }     let(:valid_data) do       {         "resize_to_limit": [1, 2],         "kuwahara": 'string',         "polaroid": 1       }     end      describe 'valid data' do       let(:options) { valid_data }       it { is_expected.to be_valid }     end      describe 'value type is invalid (case 1)' do       let(:options) { valid_data.merge quality: 'string' }       it { is_expected.not_to be_valid }     end      describe 'value type is invalid (case 2)' do       let(:options) { valid_data.merge monochrome: 'array' }       it { is_expected.not_to be_valid }     end      describe 'value type is valid' do       let(:options) { valid_data.merge monochrome: true }       it { is_expected.to be_valid }     end      describe 'missing a optional element' do       let(:options) { valid_data.except :kuwahara }       it { is_expected.to be_valid }     end      describe 'missing a necessary element' do       let(:options) { valid_data.except :resize_to_limit }       it { is_expected.not_to be_valid }     end   end end

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

$ bundle exec rspec spec/models/slideshow_spec.rb  .......  Finished in 0.33026 seconds (files took 10.42 seconds to load) 7 examples, 0 failures

Finished. Cool. «If you ain’t drunk, then you’re in the wrong club.» (c)


ссылка на оригинал статьи https://habr.com/ru/post/651227/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *