{"id":329503,"date":"2022-02-13T15:00:52","date_gmt":"2022-02-13T15:00:52","guid":{"rendered":"http:\/\/savepearlharbor.com\/?p=329503"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=329503","title":{"rendered":"<span>ActiveStorage::Variant. \u041a\u0440\u0435\u0430\u0442\u0438\u0432 \u043d\u0430 \u0440\u0435\u043b\u044c\u0441\u0430\u0445<\/span>"},"content":{"rendered":"<div><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w780q1\/getpro\/habr\/upload_files\/287\/3bc\/810\/2873bc810e9589895db0f18df42473bf.jpg\" width=\"800\" height=\"630\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/287\/3bc\/810\/2873bc810e9589895db0f18df42473bf.jpg\" data-blurred=\"true\"\/><figcaption><\/figcaption><\/figure>\n<p>\u041a\u0440\u0430\u0442\u043a\u0438\u0439 \u044d\u0442\u043e\u0442 \u0442\u0443\u0442\u043e\u0440\u0438\u0430\u043b \u0431\u0443\u0434\u0435\u0442 \u043f\u043e\u043b\u0435\u0437\u0435\u043d, \u0445\u043e\u0442\u0435\u043b\u043e\u0441\u044c \u0431\u044b \u043d\u0430\u0434\u0435\u044f\u0442\u044c\u0441\u044f, \u043a\u0430\u043a \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u0438\u0441\u0442\u0430\u043c, \u0440\u0430\u0431\u043e\u0442\u0430\u044e\u0449\u0438\u043c \u0441 Ruby on Rails, \u0442\u0430\u043a \u0438 \u0442\u0435\u043c \u0438\u0437 &#171;\u043f\u043b\u0435\u043c\u0435\u043d\u0438 \u043c\u043b\u0430\u0434\u043e\u0433\u043e \u043d\u0435\u0437\u043d\u0430\u043a\u043e\u043c\u043e\u0433\u043e&#187; \u0432\u0435\u0431-\u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u043e\u0432, \u043a\u0442\u043e \u0437\u0430\u0445\u043e\u0447\u0435\u0442 \u043e\u0441\u0432\u043e\u0438\u0442\u044c \u043e\u0442\u043b\u0438\u0447\u043d\u044b\u0439 \u0438\u043d\u0441\u0442\u0440\u0443\u043c\u0435\u043d\u0442, \u0443\u0432\u0438\u0434\u0435\u0432\u0448\u0438\u0439 \u0441\u0432\u0435\u0442 \u0432 Rails 5.2 (&#171;\u0438\u0437\u0432\u043b\u0435\u0447\u0435\u043d \u0438\u0437 \u043f\u0440\u043e\u0434\u0430&#187; Basecamp 3 \u0443\u0441\u0438\u043b\u0438\u044f\u043c\u0438 <em>George Claghorn<\/em> \u0438 <em>Javan Makhmali<\/em>) &#8212; \u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a Active Storage. \u0424\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a \u0434\u0435\u043b\u0430\u0435\u0442 \u043f\u0440\u043e\u0441\u0442\u044b\u043c \u0438 \u043d\u0430 \u0440\u0435\u0434\u043a\u043e\u0441\u0442\u044c \u0443\u0434\u043e\u0431\u043d\u044b\u043c \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0443 \u0444\u0430\u0439\u043b\u043e\u0432 \u0432 \u043e\u0431\u043b\u0430\u043a\u043e (\u043f\u0440\u044f\u043c\u043e \u0441\u0440\u0430\u0437\u0443 &#171;\u0438\u0437 \u043a\u043e\u0440\u043e\u0431\u043a\u0438&#187; \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b Amazon S3, Google Cloud Storage \u0438 Microsoft Azure Storage), \u0442\u0430\u043a\u0436\u0435 &#8212; \u0435\u0441\u043b\u0438 \u0440\u0435\u0447\u044c \u043e\u0431 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f\u0445, \u0432\u0438\u0434\u0435\u043e \u0438\u043b\u0438 PDF-\u0444\u0430\u0439\u043b\u0430\u0445 &#8212; \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u043f\u0440\u0435\u0432\u044c\u044e \u043d\u0430 \u043b\u0435\u0442\u0443.<\/p>\n<p>\u041d\u0435 \u0431\u0443\u0434\u0443 \u0441\u0435\u0439\u0447\u0430\u0441 \u043d\u0438 \u0441\u0440\u0430\u0432\u043d\u0438\u0432\u0430\u0442\u044c Active Storage \u0441 \u0437\u0430\u0434\u043e\u043b\u0433\u043e \u0434\u043e \u043d\u0435\u0433\u043e \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u043c\u0438 \u0440\u0435\u0448\u0435\u043d\u0438\u044f\u043c\u0438 CarrierWave, Paperclip \u0438\u043b\u0438 Shrine, \u043d\u0438 \u0440\u0430\u0441\u0441\u0443\u0436\u0434\u0430\u0442\u044c \u043e \u043f\u043e\u043b\u0438\u043c\u043e\u0440\u0444\u0438\u0437\u043c\u0435 \u0442\u0430\u0431\u043b\u0438\u0446\u044b <em>active_storage_attachments<\/em>; \u0432\u0441\u0435 \u044d\u0442\u043e \u0445\u043e\u0440\u043e\u0448\u043e \u0438 \u043c\u043d\u043e\u0433\u0430\u0436\u0434\u044b \u043e\u043f\u0438\u0441\u0430\u043d\u043e, \u0431\u0435\u0437 \u043f\u0440\u043e\u0431\u043b\u0435\u043c \u0441\u043f\u043e\u0441\u043e\u0431\u043d\u043e \u0431\u044b\u0442\u044c, \u043f\u0440\u0438 \u0436\u0435\u043b\u0430\u043d\u0438\u0438 \u0438 \u043d\u0430\u043b\u0438\u0447\u0438\u0438 \u0438\u043d\u0442\u0435\u0440\u0435\u0441\u0430, \u043d\u0430\u0439\u0434\u0435\u043d\u043e \u0432 \u0434\u043e\u043a\u0430\u0445. \u041e\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u044e\u0441\u044c \u0432\u0441\u0435\u0433\u043e \u0442\u043e\u043b\u044c\u043a\u043e \u043d\u0430 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u0438\u0445 \u043f\u0440\u0430\u043a\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0445 \u043c\u043e\u043c\u0435\u043d\u0442\u0430\u0445 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u043a\u043b\u0430\u0441\u0441\u0430 ActiveStorage::Variant, \u0442\u0440\u0435\u0431\u0443\u044e\u0449\u0438\u0445, \u043a\u0430\u043a \u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e, ImageMagick \u0438\u043b\u0438 libvips \u043f\u043e\u0441\u0440\u0435\u0434\u0441\u0442\u0432\u043e\u043c \u0434\u0436\u0435\u043c\u0430 <em>image_processing<\/em>:<\/p>\n<pre><code># Gemfile gem \"image_processing\", \"~> 1.0\"<\/code><\/pre>\n<p>\u0417\u0430\u0431\u0435\u0433\u0430\u044f \u0432\u043f\u0435\u0440\u0435\u0434 \u0438 \u0447\u0442\u043e\u0431\u044b \u043d\u0435 \u043a\u0430\u0437\u0430\u0442\u044c\u0441\u044f \u0433\u043e\u043b\u043e\u0441\u043b\u043e\u0432\u043d\u044b\u043c: \u0436\u0438\u0432\u0430\u044f \u0434\u0435\u043c\u043a\u0430, \u0442\u0430\u043a\u0436\u0435 \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u0433\u0438\u0442\u0445\u0430\u0431 <a href=\"https:\/\/mstp.herokuapp.com\/slideshow\" rel=\"noopener noreferrer nofollow\">\u0437\u0434\u0435\u0441\u044c<\/a>. &#171;\u0422\u043e\u0442, \u043a\u0442\u043e \u0434\u043e\u0439\u0434\u0435\u0442 &#8212; \u0443\u0432\u0438\u0434\u0438\u0442&#187;, \u0442\u0430\u043a \u0447\u0442\u043e \u0432\u043f\u0435\u0440\u0435\u0434.<\/p>\n<p>\u0413\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u044f HTML \u0433\u0430\u043b\u0435\u0440\u0435\u0438 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0439 \u043d\u0435 \u0432\u044b\u0437\u044b\u0432\u0430\u0435\u0442 \u0431\u043e\u043b\u044c\u0448\u0438\u0445 \u0432\u043e\u043f\u0440\u043e\u0441\u043e\u0432, \u0431\u043e\u043b\u0435\u0435-\u043c\u0435\u043d\u0435\u0435 \u0432 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0438\u0438 \u0441 \u0434\u043e\u043a\u043e\u0439 \u0438\u0442\u0435\u0440\u0438\u0440\u0443\u0435\u043c \u043c\u0430\u0441\u0441\u0438\u0432 \u0438 \u0443\u0432\u044f\u0437\u044b\u0432\u0430\u0435\u043c \u043f\u0443\u0442\u0438 \u0441 \u043a\u043b\u0430\u0441\u0441\u0430\u043c\u0438 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440) \u0431\u0443\u0442\u0441\u0442\u0440\u0430\u043f\u0430, \u0441\u043e\u0437\u0434\u0430\u0432 \u0434\u043b\u044f \u0443\u0434\u043e\u0431\u0441\u0442\u0432\u0430 \u043f\u0440\u043e\u0441\u0442\u0435\u043d\u044c\u043a\u0438\u0439 \u0445\u0435\u043b\u043f\u0435\u0440:<\/p>\n<pre><code class=\"ruby\">def path_to_file(x)   Rails.application.routes.url_helpers.rails_blob_path(x, only_path: true) end<\/code><\/pre>\n<pre><code class=\"ruby\">&lt;% @slideshow.images.each do |x| %> &lt;%= link_to(     path_to_file(x), html_options = {             \"data-lightbox\" => \"photo\", \"class\" => \"col-sm-4\" }) do %> &lt;%= image_tag x.variant(@options), \"class\" => \"img-fluid\" %> &lt;% end %> &lt;% end %><\/code><\/pre>\n<p>\u041b\u0430\u0439\u0442\u0431\u043e\u043a\u0441 &#8212; \u043b\u044e\u0431\u043e\u0439 \u0438\u0437 \u043d\u0435\u0441\u0442\u044c \u0438\u043c \u0447\u0438\u0441\u043b\u0430. \u041a\u0430\u043a\u043e\u0439 \u0445\u043e\u0442\u0438\u0442\u0435, \u0431\u0435\u0437 \u0440\u0430\u0437\u043d\u0438\u0446\u044b.<\/p>\n<p>\u041f\u0430\u043d\u0435\u043b\u044c \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0433\u0430\u043b\u0435\u0440\u0435\u0438 \u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0435 Active Admin \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0432\u043f\u043e\u043b\u043d\u0435 \u043f\u0440\u0438\u0441\u0442\u043e\u0439\u043d\u043e:<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w780q1\/getpro\/habr\/upload_files\/ed7\/5d6\/fff\/ed75d6fff81e9ca9b8d0b71e127c1d10.jpg\" width=\"771\" height=\"867\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/ed7\/5d6\/fff\/ed75d6fff81e9ca9b8d0b71e127c1d10.jpg\" data-blurred=\"true\"\/><figcaption><\/figcaption><\/figure>\n<p>, \u0447\u0442\u043e \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u043e \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c:<\/p>\n<pre><code class=\"ruby\">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 <\/code><\/pre>\n<p>\u041d\u043e \u0442\u0443\u0442 \u0432\u0434\u0440\u0443\u0433 \u0432\u043e\u0437\u043d\u0438\u043a\u0430\u0435\u0442 \u0432\u043e\u043f\u0440\u043e\u0441: \u043a\u0430\u043a\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c \u043f\u0435\u0440\u0435\u0434\u0430\u0442\u044c \u0432 ActiveStorage::Variant \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0438 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0439? &#8212; \u043d\u0435\u0441\u043b\u043e\u0436\u043d\u043e \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u043d\u0430\u043f\u0440\u044f\u043c\u0443\u044e \u0432 \u043a\u043e\u0434\u0435, \u043d\u043e \u0434\u043b\u044f \u0443\u0434\u043e\u0431\u0441\u0442\u0432\u0430 \u0440\u0430\u0431\u043e\u0442\u044b \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u0430 \u0441\u0430\u0439\u0442\u0430 \u0445\u043e\u0442\u0435\u043b\u043e\u0441\u044c \u0431\u044b (\u043d\u0435\u0441\u043c\u043e\u0442\u0440\u044f \u043d\u0430 \u043b\u044e\u0431\u0443\u044e \u043a\u043e\u043d\u0442\u0440\u0430\u0440\u0433\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044e) \u0437\u0430\u043f\u0438\u0441\u0430\u0442\u044c \u0438\u0445 \u0432 \u0431\u0430\u0437\u0443 \u0434\u0430\u043d\u043d\u044b\u0445, \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0438\u0432 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u0432\u0432\u043e\u0434\u0430 \u0438 \u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0438\u0437 \u0430\u0434\u043c\u0438\u043d\u043a\u0438. \u041d\u043e \u043d\u0435 <em>eval<\/em>\u043e\u043c \u0436\u0435 \u0438\u0445 \u043f\u043e\u0442\u043e\u043c \u0434\u043e\u0441\u0442\u0430\u0432\u0430\u0442\u044c, \u0432 \u0441\u0430\u043c\u043e\u043c-\u0442\u043e \u0434\u0435\u043b\u0435! &#8212; \u043d\u0435\u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e, \u0441\u043e\u0433\u043b\u0430\u0441\u0438\u0442\u0435\u0441\u044c. \u0414\u0430\u0436\u0435 \u0441 \u0443\u0447\u0435\u0442\u043e\u043c \u0440\u0443\u0431\u0438\u043d\u043e\u0432\u044b\u0445 \u0443\u0440\u043e\u0432\u043d\u0435\u0439 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u0438 (\u0441 \u043a\u043e\u0442\u043e\u0440\u044b\u043c\u0438, \u043a \u0441\u043b\u043e\u0432\u0443, \u0432\u043e\u043e\u0431\u0449\u0435 \u043d\u0435\u043f\u043e\u043d\u044f\u0442\u043d\u043e \u0447\u0442\u043e \u043f\u0440\u043e\u0438\u0441\u0445\u043e\u0434\u0438\u0442):<\/p>\n<pre><code class=\"ruby\">@options = proc {   $SAFE = 1   eval(Slideshow.take.options) }.call<\/code><\/pre>\n<p>\u041d\u0435\u0442, \u0442\u0430\u043a \u043d\u0435 \u043f\u043e\u0439\u0434\u0435\u0442. \u041f\u043b\u0430\u0432\u043d\u043e \u0434\u0432\u0438\u0433\u0430\u044f\u0441\u044c \u044d\u043c\u043f\u0438\u0440\u0438\u0447\u0435\u0441\u043a\u0438\u043c (\u0430 \u043a\u0430\u043a \u0435\u0449\u0435) \u043f\u0443\u0442\u0435\u043c \u043f\u043e\u0437\u043d\u0430\u043d\u0438\u044f &#8212; \u043f\u0440\u0438\u0445\u043e\u0434\u0438\u043c \u043a \u0442\u043e\u043c\u0443, \u0447\u0442\u043e \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0432\u043f\u043e\u043b\u043d\u0435 \u043c\u043e\u0436\u043d\u043e \u0437\u0430\u043f\u0438\u0441\u044b\u0432\u0430\u0442\u044c \u0438 \u0445\u0440\u0430\u043d\u0438\u0442\u044c \u043a\u0430\u043a JSON:<\/p>\n<pre><code class=\"ruby\">class AddOptionsToSlideshow &lt; ActiveRecord::Migration[6.1]   def change     add_column :slideshows, :options, :json   end end<\/code><\/pre>\n<p>\u0422\u0430\u043a\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c, \u0441\u043e\u0432\u0441\u0435\u043c \u043f\u0440\u043e\u0441\u0442\u043e:<\/p>\n<pre><code class=\"ruby\">@options = Slideshow.take.options<\/code><\/pre>\n<p>\u0414\u0430\u043b\u044c\u0448\u0435 &#8212; \u0431\u043e\u043b\u044c\u0448\u0435. \u0427\u0442\u043e \u043c\u0435\u0448\u0430\u0435\u0442 \u043d\u0430\u043c \u0432\u0430\u043b\u0438\u0434\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0432\u0432\u043e\u0434\u0438\u043c\u044b\u0439 \u0430\u0434\u043c\u0438\u043d\u043e\u043c JSON, \u0443\u043a\u0430\u0437\u0430\u0432 \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0435 \u0434\u043b\u044f \u0442\u0435\u0445 \u0438\u043b\u0438 \u0438\u043d\u044b\u0445 \u043a\u043b\u044e\u0447\u0435\u0439 (\u043a\u043e\u0438\u0445  ImageMagick <a href=\"https:\/\/imagemagick.org\/script\/command-line-options.php\" rel=\"noopener noreferrer nofollow\">\u0438\u043c\u0435\u0435\u0442<\/a> \u0432\u0435\u043b\u0438\u043a\u043e\u0435 \u043c\u043d\u043e\u0436\u0435\u0441\u0442\u0432\u043e, \u0433\u043b\u0430\u0437\u0430 \u0440\u0430\u0437\u0431\u0435\u0433\u0430\u044e\u0442\u0441\u044f) \u0442\u0438\u043f\u044b \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0439? &#8212; \u0440\u043e\u0432\u043d\u043e \u043d\u0438\u0447\u0442\u043e \u043d\u0435 \u043c\u0435\u0448\u0430\u0435\u0442. \u0418\u043d\u0441\u0442\u0430\u043b\u043b\u0438\u0440\u0443\u0435\u043c:<\/p>\n<pre><code class=\"xml\">gem 'activerecord_json_validator', '~> 2.0.0'<\/code><\/pre>\n<p>, \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u044f \u0432 \u043c\u043e\u0434\u0435\u043b\u044c \u0432\u0430\u043b\u0438\u0434\u0430\u0446\u0438\u044e:<\/p>\n<pre><code class=\"ruby\">PROFILE_JSON_SCHEMA = Pathname.new(Rails.root.join('config', 'schemas', 'slideshow.json')) validates :options, presence: true, json: {   schema: PROFILE_JSON_SCHEMA,   message: ->(errors) { errors } }<\/code><\/pre>\n<p>, \u043f\u0440\u043e\u0444\u0430\u0439\u043b \u0436\u0435 <em>slideshow.json<\/em>, \u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0435 \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u043f\u043e\u0439\u0434\u0435\u0442 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430, \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0443 \u043d\u0430\u0441, \u043f\u0440\u0435\u0434\u043f\u043e\u043b\u043e\u0436\u0438\u043c (\u043f\u043e\u043b\u043d\u043e\u0441\u0442\u044c\u044e \u0437\u0430\u0432\u0438\u0441\u0438\u0442 \u043e\u0442 \u0447\u0443\u0432\u0441\u0442\u0432\u0430 \u043c\u0435\u0440\u044b \u0438 \u0445\u0443\u0434\u043e\u0436\u0435\u0441\u0442\u0432\u0435\u043d\u043d\u043e\u0433\u043e \u0432\u043a\u0443\u0441\u0430 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0430), \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c:<\/p>\n<pre><code class=\"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\"] } <\/code><\/pre>\n<p>\u0412\u0441\u0435 \u0443\u0436\u0435 \u0434\u0430\u0432\u043d\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442, \u043d\u043e \u0432\u0435\u0434\u044c \u043d\u0430 \u0440\u0435\u043b\u044c\u0441\u0430\u0445 \u0430\u043f\u043f\u0435\u0442\u0438\u0442 \u043f\u0440\u0438\u0445\u043e\u0434\u0438\u0442 \u0432\u043e \u0432\u0440\u0435\u043c\u044f \u0435\u0437\u0434\u044b. \u0425\u043c, \u0430 \u043a\u0430\u043a\u043e\u0439 \u0440\u0443\u0441\u0441\u043a\u0438\u0439 \u043d\u0435 \u043b\u044e\u0431\u0438\u0442 \u0431\u044b\u0441\u0442\u0440\u043e\u0439 \u0435\u0437\u0434\u044b&#8230; \u043e\u043a, \u043f\u0438\u0448\u0435\u043c \u0441\u043f\u0435\u043a:<\/p>\n<pre><code class=\"ruby\">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<\/code><\/pre>\n<p>\u0412\u0441\u0435\u0433\u043e \u0441\u0435\u043c\u044c \u0442\u0435\u0441\u0442\u043e\u0432: \u0443\u0431\u0435\u0436\u0434\u0430\u0435\u043c\u0441\u044f, \u0447\u0442\u043e \u0434\u0430\u043d\u043d\u044b\u0435 <em>:valid_data<\/em> \u0430\u0434\u0435\u043a\u0432\u0430\u0442\u043d\u044b, \u0437\u0430\u0442\u0435\u043c \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u043c \u043a \u043d\u0438\u043c \u043e\u0434\u0438\u043d \u0437\u0430 \u0434\u0440\u0443\u0433\u0438\u043c \u043f\u0430\u0440\u0443 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432, \u0433\u0434\u0435 value \u043f\u0440\u0438\u043d\u0430\u0434\u043b\u0435\u0436\u0430\u0442 \u0442\u0438\u043f\u0443 \u0434\u0430\u043d\u043d\u044b\u0445, \u043f\u0440\u043e\u0442\u0438\u0432\u043e\u0440\u0435\u0447\u0430\u0449\u0438\u0445 \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u043c \u0432 \u043f\u0440\u043e\u0444\u0430\u0439\u043b\u0435, \u0438 \u0435\u0449\u0435 \u043e\u0434\u0438\u043d, \u0433\u0434\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u043d\u0443\u0436\u043d\u043e\u043c\u0443; \u043d\u0430\u043f\u043e\u0441\u043b\u0435\u0434\u043e\u043a \u0443\u0434\u0430\u043b\u044f\u0435\u043c \u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u044d\u043b\u0435\u043c\u0435\u043d\u0442 \u0438, \u043d\u0430\u043a\u043e\u043d\u0435\u0446, \u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439. \u0412\u0441\u0435 \u0442\u0435\u0441\u0442\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043f\u0440\u043e\u0439\u0442\u0438:<\/p>\n<pre><code class=\"xml\">$ bundle exec rspec spec\/models\/slideshow_spec.rb  .......  Finished in 0.33026 seconds (files took 10.42 seconds to load) 7 examples, 0 failures<\/code><\/pre>\n<p>Finished. Cool. &#171;If you ain&#8217;t drunk, then you&#8217;re in the wrong club.&#187; (c)<\/p>\n<\/div>\n<\/div>\n<\/div>\n<div class=\"v-portal\" style=\"display:none;\"><\/div>\n<\/div>\n<p> <!----> <!----><br \/> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/post\/651227\/\"> https:\/\/habr.com\/ru\/post\/651227\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<div><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<figure class=\"full-width\"><figcaption><\/figcaption><\/figure>\n<p>\u041a\u0440\u0430\u0442\u043a\u0438\u0439 \u044d\u0442\u043e\u0442 \u0442\u0443\u0442\u043e\u0440\u0438\u0430\u043b \u0431\u0443\u0434\u0435\u0442 \u043f\u043e\u043b\u0435\u0437\u0435\u043d, \u0445\u043e\u0442\u0435\u043b\u043e\u0441\u044c \u0431\u044b \u043d\u0430\u0434\u0435\u044f\u0442\u044c\u0441\u044f, \u043a\u0430\u043a \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u0438\u0441\u0442\u0430\u043c, \u0440\u0430\u0431\u043e\u0442\u0430\u044e\u0449\u0438\u043c \u0441 Ruby on Rails, \u0442\u0430\u043a \u0438 \u0442\u0435\u043c \u0438\u0437 &#171;\u043f\u043b\u0435\u043c\u0435\u043d\u0438 \u043c\u043b\u0430\u0434\u043e\u0433\u043e \u043d\u0435\u0437\u043d\u0430\u043a\u043e\u043c\u043e\u0433\u043e&#187; \u0432\u0435\u0431-\u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u043e\u0432, \u043a\u0442\u043e \u0437\u0430\u0445\u043e\u0447\u0435\u0442 \u043e\u0441\u0432\u043e\u0438\u0442\u044c \u043e\u0442\u043b\u0438\u0447\u043d\u044b\u0439 \u0438\u043d\u0441\u0442\u0440\u0443\u043c\u0435\u043d\u0442, \u0443\u0432\u0438\u0434\u0435\u0432\u0448\u0438\u0439 \u0441\u0432\u0435\u0442 \u0432 Rails 5.2 (&#171;\u0438\u0437\u0432\u043b\u0435\u0447\u0435\u043d \u0438\u0437 \u043f\u0440\u043e\u0434\u0430&#187; Basecamp 3 \u0443\u0441\u0438\u043b\u0438\u044f\u043c\u0438 <em>George Claghorn<\/em> \u0438 <em>Javan Makhmali<\/em>) &#8212; \u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a Active Storage. \u0424\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a \u0434\u0435\u043b\u0430\u0435\u0442 \u043f\u0440\u043e\u0441\u0442\u044b\u043c \u0438 \u043d\u0430 \u0440\u0435\u0434\u043a\u043e\u0441\u0442\u044c \u0443\u0434\u043e\u0431\u043d\u044b\u043c \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0443 \u0444\u0430\u0439\u043b\u043e\u0432 \u0432 \u043e\u0431\u043b\u0430\u043a\u043e (\u043f\u0440\u044f\u043c\u043e \u0441\u0440\u0430\u0437\u0443 &#171;\u0438\u0437 \u043a\u043e\u0440\u043e\u0431\u043a\u0438&#187; \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b Amazon S3, Google Cloud Storage \u0438 Microsoft Azure Storage), \u0442\u0430\u043a\u0436\u0435 &#8212; \u0435\u0441\u043b\u0438 \u0440\u0435\u0447\u044c \u043e\u0431 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f\u0445, \u0432\u0438\u0434\u0435\u043e \u0438\u043b\u0438 PDF-\u0444\u0430\u0439\u043b\u0430\u0445 &#8212; \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u043f\u0440\u0435\u0432\u044c\u044e \u043d\u0430 \u043b\u0435\u0442\u0443.<\/p>\n<p>\u041d\u0435 \u0431\u0443\u0434\u0443 \u0441\u0435\u0439\u0447\u0430\u0441 \u043d\u0438 \u0441\u0440\u0430\u0432\u043d\u0438\u0432\u0430\u0442\u044c Active Storage \u0441 \u0437\u0430\u0434\u043e\u043b\u0433\u043e \u0434\u043e \u043d\u0435\u0433\u043e \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u043c\u0438 \u0440\u0435\u0448\u0435\u043d\u0438\u044f\u043c\u0438 CarrierWave, Paperclip \u0438\u043b\u0438 Shrine, \u043d\u0438 \u0440\u0430\u0441\u0441\u0443\u0436\u0434\u0430\u0442\u044c \u043e \u043f\u043e\u043b\u0438\u043c\u043e\u0440\u0444\u0438\u0437\u043c\u0435 \u0442\u0430\u0431\u043b\u0438\u0446\u044b <em>active_storage_attachments<\/em>; \u0432\u0441\u0435 \u044d\u0442\u043e \u0445\u043e\u0440\u043e\u0448\u043e \u0438 \u043c\u043d\u043e\u0433\u0430\u0436\u0434\u044b \u043e\u043f\u0438\u0441\u0430\u043d\u043e, \u0431\u0435\u0437 \u043f\u0440\u043e\u0431\u043b\u0435\u043c \u0441\u043f\u043e\u0441\u043e\u0431\u043d\u043e \u0431\u044b\u0442\u044c, \u043f\u0440\u0438 \u0436\u0435\u043b\u0430\u043d\u0438\u0438 \u0438 \u043d\u0430\u043b\u0438\u0447\u0438\u0438 \u0438\u043d\u0442\u0435\u0440\u0435\u0441\u0430, \u043d\u0430\u0439\u0434\u0435\u043d\u043e \u0432 \u0434\u043e\u043a\u0430\u0445. \u041e\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u044e\u0441\u044c \u0432\u0441\u0435\u0433\u043e \u0442\u043e\u043b\u044c\u043a\u043e \u043d\u0430 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u0438\u0445 \u043f\u0440\u0430\u043a\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0445 \u043c\u043e\u043c\u0435\u043d\u0442\u0430\u0445 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u043a\u043b\u0430\u0441\u0441\u0430 ActiveStorage::Variant, \u0442\u0440\u0435\u0431\u0443\u044e\u0449\u0438\u0445, \u043a\u0430\u043a \u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e, ImageMagick \u0438\u043b\u0438 libvips \u043f\u043e\u0441\u0440\u0435\u0434\u0441\u0442\u0432\u043e\u043c \u0434\u0436\u0435\u043c\u0430 <em>image_processing<\/em>:<\/p>\n<pre><code># Gemfile gem \"image_processing\", \"~> 1.0\"<\/code><\/pre>\n<p>\u0417\u0430\u0431\u0435\u0433\u0430\u044f \u0432\u043f\u0435\u0440\u0435\u0434 \u0438 \u0447\u0442\u043e\u0431\u044b \u043d\u0435 \u043a\u0430\u0437\u0430\u0442\u044c\u0441\u044f \u0433\u043e\u043b\u043e\u0441\u043b\u043e\u0432\u043d\u044b\u043c: \u0436\u0438\u0432\u0430\u044f \u0434\u0435\u043c\u043a\u0430, \u0442\u0430\u043a\u0436\u0435 \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u0433\u0438\u0442\u0445\u0430\u0431 <a href=\"https:\/\/mstp.herokuapp.com\/slideshow\" rel=\"noopener noreferrer nofollow\">\u0437\u0434\u0435\u0441\u044c<\/a>. &#171;\u0422\u043e\u0442, \u043a\u0442\u043e \u0434\u043e\u0439\u0434\u0435\u0442 &#8212; \u0443\u0432\u0438\u0434\u0438\u0442&#187;, \u0442\u0430\u043a \u0447\u0442\u043e \u0432\u043f\u0435\u0440\u0435\u0434.<\/p>\n<p>\u0413\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u044f HTML \u0433\u0430\u043b\u0435\u0440\u0435\u0438 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0439 \u043d\u0435 \u0432\u044b\u0437\u044b\u0432\u0430\u0435\u0442 \u0431\u043e\u043b\u044c\u0448\u0438\u0445 \u0432\u043e\u043f\u0440\u043e\u0441\u043e\u0432, \u0431\u043e\u043b\u0435\u0435-\u043c\u0435\u043d\u0435\u0435 \u0432 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0438\u0438 \u0441 \u0434\u043e\u043a\u043e\u0439 \u0438\u0442\u0435\u0440\u0438\u0440\u0443\u0435\u043c \u043c\u0430\u0441\u0441\u0438\u0432 \u0438 \u0443\u0432\u044f\u0437\u044b\u0432\u0430\u0435\u043c \u043f\u0443\u0442\u0438 \u0441 \u043a\u043b\u0430\u0441\u0441\u0430\u043c\u0438 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440) \u0431\u0443\u0442\u0441\u0442\u0440\u0430\u043f\u0430, \u0441\u043e\u0437\u0434\u0430\u0432 \u0434\u043b\u044f \u0443\u0434\u043e\u0431\u0441\u0442\u0432\u0430 \u043f\u0440\u043e\u0441\u0442\u0435\u043d\u044c\u043a\u0438\u0439 \u0445\u0435\u043b\u043f\u0435\u0440:<\/p>\n<pre><code class=\"ruby\">def path_to_file(x)   Rails.application.routes.url_helpers.rails_blob_path(x, only_path: true) end<\/code><\/pre>\n<pre><code class=\"ruby\">&lt;% @slideshow.images.each do |x| %> &lt;%= link_to(     path_to_file(x), html_options = {             \"data-lightbox\" => \"photo\", \"class\" => \"col-sm-4\" }) do %> &lt;%= image_tag x.variant(@options), \"class\" => \"img-fluid\" %> &lt;% end %> &lt;% end %><\/code><\/pre>\n<p>\u041b\u0430\u0439\u0442\u0431\u043e\u043a\u0441 &#8212; \u043b\u044e\u0431\u043e\u0439 \u0438\u0437 \u043d\u0435\u0441\u0442\u044c \u0438\u043c \u0447\u0438\u0441\u043b\u0430. \u041a\u0430\u043a\u043e\u0439 \u0445\u043e\u0442\u0438\u0442\u0435, \u0431\u0435\u0437 \u0440\u0430\u0437\u043d\u0438\u0446\u044b.<\/p>\n<p>\u041f\u0430\u043d\u0435\u043b\u044c \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0433\u0430\u043b\u0435\u0440\u0435\u0438 \u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0435 Active Admin \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0432\u043f\u043e\u043b\u043d\u0435 \u043f\u0440\u0438\u0441\u0442\u043e\u0439\u043d\u043e:<\/p>\n<figure class=\"full-width\"><figcaption><\/figcaption><\/figure>\n<p>, \u0447\u0442\u043e \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u043e \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c:<\/p>\n<pre><code class=\"ruby\">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 <\/code><\/pre>\n<p>\u041d\u043e \u0442\u0443\u0442 \u0432\u0434\u0440\u0443\u0433 \u0432\u043e\u0437\u043d\u0438\u043a\u0430\u0435\u0442 \u0432\u043e\u043f\u0440\u043e\u0441: \u043a\u0430\u043a\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c \u043f\u0435\u0440\u0435\u0434\u0430\u0442\u044c \u0432 ActiveStorage::Variant \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0438 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0439? &#8212; \u043d\u0435\u0441\u043b\u043e\u0436\u043d\u043e \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u043d\u0430\u043f\u0440\u044f\u043c\u0443\u044e \u0432 \u043a\u043e\u0434\u0435, \u043d\u043e \u0434\u043b\u044f \u0443\u0434\u043e\u0431\u0441\u0442\u0432\u0430 \u0440\u0430\u0431\u043e\u0442\u044b \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u0430 \u0441\u0430\u0439\u0442\u0430 \u0445\u043e\u0442\u0435\u043b\u043e\u0441\u044c \u0431\u044b (\u043d\u0435\u0441\u043c\u043e\u0442\u0440\u044f \u043d\u0430 \u043b\u044e\u0431\u0443\u044e \u043a\u043e\u043d\u0442\u0440\u0430\u0440\u0433\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044e) \u0437\u0430\u043f\u0438\u0441\u0430\u0442\u044c \u0438\u0445 \u0432 \u0431\u0430\u0437\u0443 \u0434\u0430\u043d\u043d\u044b\u0445, \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0438\u0432 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u0432\u0432\u043e\u0434\u0430 \u0438 \u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0438\u0437 \u0430\u0434\u043c\u0438\u043d\u043a\u0438. \u041d\u043e \u043d\u0435 <em>eval<\/em>\u043e\u043c \u0436\u0435 \u0438\u0445 \u043f\u043e\u0442\u043e\u043c \u0434\u043e\u0441\u0442\u0430\u0432\u0430\u0442\u044c, \u0432 \u0441\u0430\u043c\u043e\u043c-\u0442\u043e \u0434\u0435\u043b\u0435! &#8212; \u043d\u0435\u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e, \u0441\u043e\u0433\u043b\u0430\u0441\u0438\u0442\u0435\u0441\u044c. \u0414\u0430\u0436\u0435 \u0441 \u0443\u0447\u0435\u0442\u043e\u043c \u0440\u0443\u0431\u0438\u043d\u043e\u0432\u044b\u0445 \u0443\u0440\u043e\u0432\u043d\u0435\u0439 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u0438 (\u0441 \u043a\u043e\u0442\u043e\u0440\u044b\u043c\u0438, \u043a \u0441\u043b\u043e\u0432\u0443, \u0432\u043e\u043e\u0431\u0449\u0435 \u043d\u0435\u043f\u043e\u043d\u044f\u0442\u043d\u043e \u0447\u0442\u043e \u043f\u0440\u043e\u0438\u0441\u0445\u043e\u0434\u0438\u0442):<\/p>\n<pre><code class=\"ruby\">@options = proc {   $SAFE = 1   eval(Slideshow.take.options) }.call<\/code><\/pre>\n<p>\u041d\u0435\u0442, \u0442\u0430\u043a \u043d\u0435 \u043f\u043e\u0439\u0434\u0435\u0442. \u041f\u043b\u0430\u0432\u043d\u043e \u0434\u0432\u0438\u0433\u0430\u044f\u0441\u044c \u044d\u043c\u043f\u0438\u0440\u0438\u0447\u0435\u0441\u043a\u0438\u043c (\u0430 \u043a\u0430\u043a \u0435\u0449\u0435) \u043f\u0443\u0442\u0435\u043c \u043f\u043e\u0437\u043d\u0430\u043d\u0438\u044f &#8212; \u043f\u0440\u0438\u0445\u043e\u0434\u0438\u043c \u043a \u0442\u043e\u043c\u0443, \u0447\u0442\u043e \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0432\u043f\u043e\u043b\u043d\u0435 \u043c\u043e\u0436\u043d\u043e \u0437\u0430\u043f\u0438\u0441\u044b\u0432\u0430\u0442\u044c \u0438 \u0445\u0440\u0430\u043d\u0438\u0442\u044c \u043a\u0430\u043a JSON:<\/p>\n<pre><code class=\"ruby\">class AddOptionsToSlideshow &lt; ActiveRecord::Migration[6.1]   def change     add_column :slideshows, :options, :json   end end<\/code><\/pre>\n<p>\u0422\u0430\u043a\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c, \u0441\u043e\u0432\u0441\u0435\u043c \u043f\u0440\u043e\u0441\u0442\u043e:<\/p>\n<pre><code class=\"ruby\">@options = Slideshow.take.options<\/code><\/pre>\n<p>\u0414\u0430\u043b\u044c\u0448\u0435 &#8212; \u0431\u043e\u043b\u044c\u0448\u0435. \u0427\u0442\u043e \u043c\u0435\u0448\u0430\u0435\u0442 \u043d\u0430\u043c \u0432\u0430\u043b\u0438\u0434\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0432\u0432\u043e\u0434\u0438\u043c\u044b\u0439 \u0430\u0434\u043c\u0438\u043d\u043e\u043c JSON, \u0443\u043a\u0430\u0437\u0430\u0432 \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0435 \u0434\u043b\u044f \u0442\u0435\u0445 \u0438\u043b\u0438 \u0438\u043d\u044b\u0445 \u043a\u043b\u044e\u0447\u0435\u0439 (\u043a\u043e\u0438\u0445  ImageMagick <a href=\"https:\/\/imagemagick.org\/script\/command-line-options.php\" rel=\"noopener noreferrer nofollow\">\u0438\u043c\u0435\u0435\u0442<\/a> \u0432\u0435\u043b\u0438\u043a\u043e\u0435 \u043c\u043d\u043e\u0436\u0435\u0441\u0442\u0432\u043e, \u0433\u043b\u0430\u0437\u0430 \u0440\u0430\u0437\u0431\u0435\u0433\u0430\u044e\u0442\u0441\u044f) \u0442\u0438\u043f\u044b \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0439? &#8212; \u0440\u043e\u0432\u043d\u043e \u043d\u0438\u0447\u0442\u043e \u043d\u0435 \u043c\u0435\u0448\u0430\u0435\u0442. \u0418\u043d\u0441\u0442\u0430\u043b\u043b\u0438\u0440\u0443\u0435\u043c:<\/p>\n<pre><code class=\"xml\">gem 'activerecord_json_validator', '~> 2.0.0'<\/code><\/pre>\n<p>, \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u044f \u0432 \u043c\u043e\u0434\u0435\u043b\u044c \u0432\u0430\u043b\u0438\u0434\u0430\u0446\u0438\u044e:<\/p>\n<pre><code class=\"ruby\">PROFILE_JSON_SCHEMA = Pathname.new(Rails.root.join('config', 'schemas', 'slideshow.json')) validates :options, presence: true, json: {   schema: PROFILE_JSON_SCHEMA,   message: ->(errors) { errors } }<\/code><\/pre>\n<p>, \u043f\u0440\u043e\u0444\u0430\u0439\u043b \u0436\u0435 <em>slideshow.json<\/em>, \u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0435 \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u043f\u043e\u0439\u0434\u0435\u0442 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430, \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0443 \u043d\u0430\u0441, \u043f\u0440\u0435\u0434\u043f\u043e\u043b\u043e\u0436\u0438\u043c (\u043f\u043e\u043b\u043d\u043e\u0441\u0442\u044c\u044e \u0437\u0430\u0432\u0438\u0441\u0438\u0442 \u043e\u0442 \u0447\u0443\u0432\u0441\u0442\u0432\u0430 \u043c\u0435\u0440\u044b \u0438 \u0445\u0443\u0434\u043e\u0436\u0435\u0441\u0442\u0432\u0435\u043d\u043d\u043e\u0433\u043e \u0432\u043a\u0443\u0441\u0430 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0430), \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c:<\/p>\n<pre><code class=\"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\"] } <\/code><\/pre>\n<p>\u0412\u0441\u0435 \u0443\u0436\u0435 \u0434\u0430\u0432\u043d\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442, \u043d\u043e \u0432\u0435\u0434\u044c \u043d\u0430 \u0440\u0435\u043b\u044c\u0441\u0430\u0445 \u0430\u043f\u043f\u0435\u0442\u0438\u0442 \u043f\u0440\u0438\u0445\u043e\u0434\u0438\u0442 \u0432\u043e \u0432\u0440\u0435\u043c\u044f \u0435\u0437\u0434\u044b. \u0425\u043c, \u0430 \u043a\u0430\u043a\u043e\u0439 \u0440\u0443\u0441\u0441\u043a\u0438\u0439 \u043d\u0435 \u043b\u044e\u0431\u0438\u0442 \u0431\u044b\u0441\u0442\u0440\u043e\u0439 \u0435\u0437\u0434\u044b&#8230; \u043e\u043a, \u043f\u0438\u0448\u0435\u043c \u0441\u043f\u0435\u043a:<\/p>\n<pre><code class=\"ruby\">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<\/code><\/pre>\n<p>\u0412\u0441\u0435\u0433\u043e \u0441\u0435\u043c\u044c \u0442\u0435\u0441\u0442\u043e\u0432: \u0443\u0431\u0435\u0436\u0434\u0430\u0435\u043c\u0441\u044f, \u0447\u0442\u043e \u0434\u0430\u043d\u043d\u044b\u0435 <em>:valid_data<\/em> \u0430\u0434\u0435\u043a\u0432\u0430\u0442\u043d\u044b, \u0437\u0430\u0442\u0435\u043c \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u043c \u043a \u043d\u0438\u043c \u043e\u0434\u0438\u043d \u0437\u0430 \u0434\u0440\u0443\u0433\u0438\u043c \u043f\u0430\u0440\u0443 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432, \u0433\u0434\u0435 value \u043f\u0440\u0438\u043d\u0430\u0434\u043b\u0435\u0436\u0430\u0442 \u0442\u0438\u043f\u0443 \u0434\u0430\u043d\u043d\u044b\u0445, \u043f\u0440\u043e\u0442\u0438\u0432\u043e\u0440\u0435\u0447\u0430\u0449\u0438\u0445 \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u043c \u0432 \u043f\u0440\u043e\u0444\u0430\u0439\u043b\u0435, \u0438 \u0435\u0449\u0435 \u043e\u0434\u0438\u043d, \u0433\u0434\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u043d\u0443\u0436\u043d\u043e\u043c\u0443; \u043d\u0430\u043f\u043e\u0441\u043b\u0435\u0434\u043e\u043a \u0443\u0434\u0430\u043b\u044f\u0435\u043c \u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u044d\u043b\u0435\u043c\u0435\u043d\u0442 \u0438, \u043d\u0430\u043a\u043e\u043d\u0435\u0446, \u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439. \u0412\u0441\u0435 \u0442\u0435\u0441\u0442\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043f\u0440\u043e\u0439\u0442\u0438:<\/p>\n<pre><code class=\"xml\">$ bundle exec rspec spec\/models\/slideshow_spec.rb  .......  Finished in 0.33026 seconds (files took 10.42 seconds to load) 7 examples, 0 failures<\/code><\/pre>\n<p>Finished. Cool. &#171;If you ain&#8217;t drunk, then you&#8217;re in the wrong club.&#187; (c)<\/p>\n<\/div>\n<\/div>\n<\/div>\n<div class=\"v-portal\" style=\"display:none;\"><\/div>\n<\/div>\n<p> <!----> <!----><br \/> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/post\/651227\/\"> https:\/\/habr.com\/ru\/post\/651227\/<\/a><br \/><\/br><\/br><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-329503","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/329503","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=329503"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/329503\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=329503"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=329503"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=329503"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}