diff --git a/activestorage/CHANGELOG.md b/activestorage/CHANGELOG.md index 75658a540f..8eb24c8791 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 do |attachable| + attachable.variant :thumb, resize: "100x100" + attachable.variant :medium, resize: "300x300", monochrome: true + end + end + + class Gallery < ActiveRecord::Base + has_many_attached :photos do |attachable| + attachable.variant :thumb, resize: "100x100" + attachable.variant :medium, resize: "300x300", monochrome: true + end + end + + <%= image_tag user.avatar.variant(:thumb) %> + ``` + + *fatkodima* + * After setting `config.active_storage.resolve_model_to_route = :rails_storage_proxy` `rails_blob_path` and `rails_representation_path` will generate proxy URLs by default. diff --git a/activestorage/app/models/active_storage/attachment.rb b/activestorage/app/models/active_storage/attachment.rb index 3c47fa2d29..4bde8d9bce 100644 --- a/activestorage/app/models/active_storage/attachment.rb +++ b/activestorage/app/models/active_storage/attachment.rb @@ -37,6 +37,19 @@ class ActiveStorage::Attachment < ActiveStorage::Record 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 analyze_blob_later blob.analyze_later unless blob.analyzed? @@ -53,6 +66,10 @@ class ActiveStorage::Attachment < ActiveStorage::Record def dependent record.attachment_reflections[name]&.options[:dependent] end + + def variants + record.attachment_reflections[name]&.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 85ddbb81b5..f0e5e6949a 100644 --- a/activestorage/lib/active_storage/attached/model.rb +++ b/activestorage/lib/active_storage/attached/model.rb @@ -83,6 +83,7 @@ module ActiveStorage { dependent: dependent, service_name: service }, self ) + yield reflection if block_given? ActiveRecord::Reflection.add_attachment_reflection(self, name, reflection) end @@ -178,6 +179,7 @@ module ActiveStorage { dependent: dependent, service_name: service }, self ) + yield reflection if block_given? ActiveRecord::Reflection.add_attachment_reflection(self, name, reflection) end diff --git a/activestorage/lib/active_storage/reflection.rb b/activestorage/lib/active_storage/reflection.rb index ce248c88b5..5bec40d2d3 100644 --- a/activestorage/lib/active_storage/reflection.rb +++ b/activestorage/lib/active_storage/reflection.rb @@ -2,9 +2,19 @@ module ActiveStorage module Reflection + class HasAttachedReflection < ActiveRecord::Reflection::MacroReflection #:nodoc: + def variant(name, transformations) + variants[name] = transformations + end + + def variants + @variants ||= {} + end + end + # Holds all the metadata about a has_one_attached attachment as it was # specified in the Active Record class. - class HasOneAttachedReflection < ActiveRecord::Reflection::MacroReflection #:nodoc: + class HasOneAttachedReflection < HasAttachedReflection #:nodoc: def macro :has_one_attached end @@ -12,7 +22,7 @@ module ActiveStorage # Holds all the metadata about a has_many_attached attachment as it was # specified in the Active Record class. - class HasManyAttachedReflection < ActiveRecord::Reflection::MacroReflection #:nodoc: + class HasManyAttachedReflection < HasAttachedReflection #:nodoc: def macro :has_many_attached end diff --git a/activestorage/test/models/attached/many_test.rb b/activestorage/test/models/attached/many_test.rb index 5ecf2693cd..1cfdd3182b 100644 --- a/activestorage/test/models/attached/many_test.rb +++ b/activestorage/test/models/attached/many_test.rb @@ -628,6 +628,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 dbdf547995..ac42dc5c65 100644 --- a/activestorage/test/models/attached/one_test.rb +++ b/activestorage/test/models/attached/one_test.rb @@ -609,4 +609,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..4e258eb8e9 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.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.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 11e3eac98e..e85c82ae99 100644 --- a/activestorage/test/test_helper.rb +++ b/activestorage/test/test_helper.rb @@ -119,9 +119,15 @@ class User < ActiveRecord::Base has_one_attached :avatar has_one_attached :cover_photo, dependent: false, service: :local + has_one_attached :avatar_with_variants do |attachable| + attachable.variant :thumb, resize: "100x100" + end has_many_attached :highlights has_many_attached :vlogs, dependent: false, service: :local + has_many_attached :highlights_with_variants do |attachable| + attachable.variant :thumb, resize: "100x100" + end end class Group < ActiveRecord::Base diff --git a/guides/source/active_storage_overview.md b/guides/source/active_storage_overview.md index 4ed259c2e2..09d376eadc 100644 --- a/guides/source/active_storage_overview.md +++ b/guides/source/active_storage_overview.md @@ -352,6 +352,22 @@ class User < ApplicationRecord end ``` +You can configure specific variants per attachment by calling the `variant` method on yielded attachable object: + +```ruby +class User < ApplicationRecord + has_one_attached :avatar do |attachable| + attachable.variant :thumb, resize: "100x100" + end +end +``` + +Call `avatar.variant(:thumb)` to get a thumb variant of an avatar: + +```ruby +<%= image_tag user.avatar.variant(:thumb) %> +``` + [`has_one_attached`]: https://api.rubyonrails.org/classes/ActiveStorage/Attached/Model.html#method-i-has_one_attached [Attached::One#attach]: https://api.rubyonrails.org/classes/ActiveStorage/Attached/One.html#method-i-attach [Attached::One#attached?]: https://api.rubyonrails.org/classes/ActiveStorage/Attached/One.html#method-i-attached-3F @@ -406,10 +422,21 @@ class Message < ApplicationRecord end ``` +Configuring specific variants is done the same way as `has_one_attached`, by calling the `variant` method on the yielded attachable object: + +```ruby +class Message < ApplicationRecord + has_many_attached :images do |attachable| + attachable.variant :thumb, resize: "100x100" + end +end +``` + [`has_many_attached`]: https://api.rubyonrails.org/classes/ActiveStorage/Attached/Model.html#method-i-has_many_attached [Attached::Many#attach]: https://api.rubyonrails.org/classes/ActiveStorage/Attached/Many.html#method-i-attach [Attached::Many#attached?]: https://api.rubyonrails.org/classes/ActiveStorage/Attached/Many.html#method-i-attached-3F + ### Attaching File/IO Objects Sometimes you need to attach a file that doesn’t arrive via an HTTP request.