From 1adea932658aef0c83f1fb7e15288f0bf7e93557 Mon Sep 17 00:00:00 2001 From: fatkodima Date: Sun, 3 May 2020 22:03:46 +0300 Subject: [PATCH] [ActiveStorage] Add ability to use pre-defined variants --- activestorage/CHANGELOG.md | 22 +++++++++++++++++ .../app/models/active_storage/attachment.rb | 17 +++++++++++++ .../lib/active_storage/attached/model.rb | 8 +++---- .../test/models/attached/many_test.rb | 20 ++++++++++++++++ .../test/models/attached/one_test.rb | 20 ++++++++++++++++ activestorage/test/models/reflection_test.rb | 12 +++++++--- activestorage/test/test_helper.rb | 2 ++ guides/source/active_storage_overview.md | 24 ++++++++++++++++++- 8 files changed, 117 insertions(+), 8 deletions(-) diff --git a/activestorage/CHANGELOG.md b/activestorage/CHANGELOG.md index 8e43acc97e..ff0c20f263 100644 --- a/activestorage/CHANGELOG.md +++ b/activestorage/CHANGELOG.md @@ -1,3 +1,25 @@ +* Add ability to use pre-defined variants. + + ```ruby + class User < ActiveRecord::Base + has_one_attached :avatar, variants: { + thumb: { resize: "100x100" }, + medium: { resize: "300x300", monochrome: true } + } + end + + class Gallery < ActiveRecord::Base + has_many_attached :photos, variants: { + thumb: { resize: "100x100" }, + medium: { resize: "300x300", monochrome: true } + } + end + + <%= image_tag user.avatar.variant(:thumb) %> + ``` + + *fatkodima* + * Add `config.active_storage.web_image_content_types` to allow applications to add content types (like `image/webp`) in which variants can be processed, instead of letting those images be converted to the fallback PNG format. diff --git a/activestorage/app/models/active_storage/attachment.rb b/activestorage/app/models/active_storage/attachment.rb index 8938eed825..6910848285 100644 --- a/activestorage/app/models/active_storage/attachment.rb +++ b/activestorage/app/models/active_storage/attachment.rb @@ -30,6 +30,19 @@ class ActiveStorage::Attachment < ActiveRecord::Base blob&.purge_later end + def variant(transformations) + case transformations + when Symbol + variant_name = transformations + transformations = variants.fetch(variant_name) do + record_model_name = record.to_model.model_name.name + raise ArgumentError, "Cannot find variant :#{variant_name} for #{record_model_name}##{name}" + end + end + + blob.variant(transformations) + end + private def identify_blob blob.identify @@ -51,6 +64,10 @@ class ActiveStorage::Attachment < ActiveRecord::Base def dependent record.attachment_reflections[name]&.options[:dependent] end + + def variants + record.attachment_reflections[name]&.options[:variants] + end end ActiveSupport.run_load_hooks :active_storage_attachment, ActiveStorage::Attachment diff --git a/activestorage/lib/active_storage/attached/model.rb b/activestorage/lib/active_storage/attached/model.rb index 027d52c459..939dec37a6 100644 --- a/activestorage/lib/active_storage/attached/model.rb +++ b/activestorage/lib/active_storage/attached/model.rb @@ -40,7 +40,7 @@ module ActiveStorage # has_one_attached :avatar, service: :s3 # end # - def has_one_attached(name, dependent: :purge_later, service: nil) + def has_one_attached(name, dependent: :purge_later, service: nil, variants: {}) validate_service_configuration(name, service) generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1 @@ -72,7 +72,7 @@ module ActiveStorage :has_one_attached, name, nil, - { dependent: dependent, service_name: service }, + { dependent: dependent, service_name: service, variants: variants }, self ) ActiveRecord::Reflection.add_attachment_reflection(self, name, reflection) @@ -110,7 +110,7 @@ module ActiveStorage # has_many_attached :photos, service: :s3 # end # - def has_many_attached(name, dependent: :purge_later, service: nil) + def has_many_attached(name, dependent: :purge_later, service: nil, variants: {}) validate_service_configuration(name, service) generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1 @@ -159,7 +159,7 @@ module ActiveStorage :has_many_attached, name, nil, - { dependent: dependent, service_name: service }, + { dependent: dependent, service_name: service, variants: variants }, self ) ActiveRecord::Reflection.add_attachment_reflection(self, name, reflection) diff --git a/activestorage/test/models/attached/many_test.rb b/activestorage/test/models/attached/many_test.rb index 6fd1676e6e..4a3a8465c7 100644 --- a/activestorage/test/models/attached/many_test.rb +++ b/activestorage/test/models/attached/many_test.rb @@ -610,6 +610,26 @@ class ActiveStorage::ManyAttachedTest < ActiveSupport::TestCase assert_match(/Cannot configure service :unknown for User#featured_photos/, error.message) end + test "creating variation by variation name" do + @user.highlights_with_variants.attach fixture_file_upload("racecar.jpg") + variant = @user.highlights_with_variants.first.variant(:thumb).processed + + image = read_image(variant) + assert_equal "JPEG", image.type + assert_equal 100, image.width + assert_equal 67, image.height + end + + test "raises error when unknown variant name is used" do + @user.highlights_with_variants.attach fixture_file_upload("racecar.jpg") + + error = assert_raises ArgumentError do + @user.highlights_with_variants.first.variant(:unknown).processed + end + + assert_match(/Cannot find variant :unknown for User#highlights_with_variants/, error.message) + end + private def append_on_assign ActiveStorage.replace_on_assign_to_many, previous = false, ActiveStorage.replace_on_assign_to_many diff --git a/activestorage/test/models/attached/one_test.rb b/activestorage/test/models/attached/one_test.rb index 3d5a7dc360..3cd87eaa0e 100644 --- a/activestorage/test/models/attached/one_test.rb +++ b/activestorage/test/models/attached/one_test.rb @@ -584,4 +584,24 @@ class ActiveStorage::OneAttachedTest < ActiveSupport::TestCase assert_match(/Cannot configure service :unknown for User#featured_photo/, error.message) end + + test "creating variation by variation name" do + @user.avatar_with_variants.attach fixture_file_upload("racecar.jpg") + variant = @user.avatar_with_variants.variant(:thumb).processed + + image = read_image(variant) + assert_equal "JPEG", image.type + assert_equal 100, image.width + assert_equal 67, image.height + end + + test "raises error when unknown variant name is used" do + @user.avatar_with_variants.attach fixture_file_upload("racecar.jpg") + + error = assert_raises ArgumentError do + @user.avatar_with_variants.variant(:unknown).processed + end + + assert_match(/Cannot find variant :unknown for User#avatar_with_variants/, error.message) + end end diff --git a/activestorage/test/models/reflection_test.rb b/activestorage/test/models/reflection_test.rb index d3f8b39cad..d8cd2a0ec9 100644 --- a/activestorage/test/models/reflection_test.rb +++ b/activestorage/test/models/reflection_test.rb @@ -12,6 +12,9 @@ class ActiveStorage::ReflectionTest < ActiveSupport::TestCase reflection = User.reflect_on_attachment(:cover_photo) assert_equal :local, reflection.options[:service_name] + + reflection = User.reflect_on_attachment(:avatar_with_variants) + assert_instance_of Hash, reflection.options[:variants] end test "reflection on a singular attachment with the same name as an attachment on another model" do @@ -28,13 +31,16 @@ class ActiveStorage::ReflectionTest < ActiveSupport::TestCase reflection = User.reflect_on_attachment(:vlogs) assert_equal :local, reflection.options[:service_name] + + reflection = User.reflect_on_attachment(:highlights_with_variants) + assert_instance_of Hash, reflection.options[:variants] end test "reflecting on all attachments" do reflections = User.reflect_on_all_attachments.sort_by(&:name) assert_equal [ User ], reflections.collect(&:active_record).uniq - assert_equal %i[ avatar cover_photo highlights vlogs ], reflections.collect(&:name) - assert_equal %i[ has_one_attached has_one_attached has_many_attached has_many_attached ], reflections.collect(&:macro) - assert_equal [ :purge_later, false, :purge_later, false ], reflections.collect { |reflection| reflection.options[:dependent] } + assert_equal %i[ avatar avatar_with_variants cover_photo highlights highlights_with_variants vlogs ], reflections.collect(&:name) + assert_equal %i[ has_one_attached has_one_attached has_one_attached has_many_attached has_many_attached has_many_attached ], reflections.collect(&:macro) + assert_equal [ :purge_later, :purge_later, false, :purge_later, :purge_later, false ], reflections.collect { |reflection| reflection.options[:dependent] } end end diff --git a/activestorage/test/test_helper.rb b/activestorage/test/test_helper.rb index 58dc21950b..5a866c3f95 100644 --- a/activestorage/test/test_helper.rb +++ b/activestorage/test/test_helper.rb @@ -119,9 +119,11 @@ class User < ActiveRecord::Base has_one_attached :avatar has_one_attached :cover_photo, dependent: false, service: :local + has_one_attached :avatar_with_variants, variants: { thumb: { resize: "100x100" } } has_many_attached :highlights has_many_attached :vlogs, dependent: false, service: :local + has_many_attached :highlights_with_variants, variants: { thumb: { resize: "100x100" } } end class Group < ActiveRecord::Base diff --git a/guides/source/active_storage_overview.md b/guides/source/active_storage_overview.md index f5d2521778..8b0104cd9e 100644 --- a/guides/source/active_storage_overview.md +++ b/guides/source/active_storage_overview.md @@ -129,7 +129,7 @@ amazon: secret_access_key: "" region: "" bucket: "" - upload: + upload: server_side_encryption: "" # 'aws:kms' or 'AES256' ``` @@ -332,6 +332,20 @@ class User < ApplicationRecord end ``` +You can configure specific variants per attachment using the `variants` option: + +```ruby +class User < ApplicationRecord + has_one_attached :avatar, variants: { thumb: { resize: "100x100" } } +end +``` + +Call `avatar.variant(:thumb)` to get a thumb variant of an avatar: + +```ruby +<%= image_tag user.avatar.variant(:thumb) %> +``` + ### `has_many_attached` The `has_many_attached` macro sets up a one-to-many relationship between records @@ -382,6 +396,14 @@ class Message < ApplicationRecord end ``` +Configuring specific variants is done the same way as `has_one_attached`, by using the `variants` option: + +```ruby +class Message < ApplicationRecord + has_many_attached :images, variants: { thumb: { resize: "100x100" } } +end +``` + ### Attaching File/IO Objects Sometimes you need to attach a file that doesn’t arrive via an HTTP request.