Extend audio_tag and video_tag to accept Active Storage attachments.

Now it's possible to write

    audio_tag(user.audio_file)
    video_tag(user.video_file)

Instead of

    audio_tag(polymorphic_path(user.audio_file))
    video_tag(polymorphic_path(user.video_file))

image_tag already supported that, so this follows the same pattern.
This commit is contained in:
Matheus Richard 2022-01-05 16:06:52 -03:00
parent 6bc36349e8
commit 414394206a
6 changed files with 111 additions and 13 deletions

@ -1,3 +1,23 @@
* Extend audio_tag and video_tag to accept Active Storage attachments.
Now it's possible to write
```ruby
audio_tag(user.audio_file)
video_tag(user.video_file)
```
Instead of
```ruby
audio_tag(polymorphic_path(user.audio_file))
video_tag(polymorphic_path(user.video_file))
```
`image_tag` already supported that, so this follows the same pattern.
*Matheus Richard*
* Ensure models passed to `form_for` attempt to call `to_model`.
*Sean Doyle*

@ -396,7 +396,7 @@ def image_tag(source, options = {})
check_for_image_tag_errors(options)
skip_pipeline = options.delete(:skip_pipeline)
options[:src] = resolve_image_source(source, skip_pipeline)
options[:src] = resolve_asset_source("image", source, skip_pipeline)
if options[:srcset] && !options[:srcset].is_a?(String)
options[:srcset] = options[:srcset].map do |src_path, size|
@ -416,8 +416,8 @@ def image_tag(source, options = {})
# Returns an HTML video tag for the +sources+. If +sources+ is a string,
# a single video tag will be returned. If +sources+ is an array, a video
# tag with nested source tags for each source will be returned. The
# +sources+ can be full paths or files that exist in your public videos
# directory.
# +sources+ can be full paths, files that exist in your public videos
# directory, or Active Storage attachments.
#
# ==== Options
#
@ -456,6 +456,11 @@ def image_tag(source, options = {})
# # => <video><source src="/videos/trailer.ogg" /><source src="/videos/trailer.flv" /></video>
# video_tag(["trailer.ogg", "trailer.flv"], size: "160x120")
# # => <video height="120" width="160"><source src="/videos/trailer.ogg" /><source src="/videos/trailer.flv" /></video>
#
# Active Storage blobs (videos that are uploaded by the users of your app):
#
# video_tag(user.intro_video)
# # => <img src="/rails/active_storage/blobs/.../intro_video.mp4" />
def video_tag(*sources)
options = sources.extract_options!.symbolize_keys
public_poster_folder = options.delete(:poster_skip_pipeline)
@ -469,8 +474,8 @@ def video_tag(*sources)
# Returns an HTML audio tag for the +sources+. If +sources+ is a string,
# a single audio tag will be returned. If +sources+ is an array, an audio
# tag with nested source tags for each source will be returned. The
# +sources+ can be full paths or files that exist in your public audios
# directory.
# +sources+ can be full paths, files that exist in your public audios
# directory, or Active Storage attachments.
#
# When the last parameter is a hash you can add HTML attributes using that
# parameter.
@ -483,6 +488,11 @@ def video_tag(*sources)
# # => <audio autoplay="autoplay" controls="controls" src="/audios/sound.wav"></audio>
# audio_tag("sound.wav", "sound.mid")
# # => <audio><source src="/audios/sound.wav" /><source src="/audios/sound.mid" /></audio>
#
# Active Storage blobs (audios that are uploaded by the users of your app):
#
# audio_tag(user.name_pronunciation_audio)
# # => <img src="/rails/active_storage/blobs/.../name_pronunciation_audio.mp4" />
def audio_tag(*sources)
multiple_sources_tag_builder("audio", sources)
end
@ -497,22 +507,22 @@ def multiple_sources_tag_builder(type, sources)
if sources.size > 1
content_tag(type, options) do
safe_join sources.map { |source| tag("source", src: send("path_to_#{type}", source, skip_pipeline: skip_pipeline)) }
safe_join sources.map { |source| tag("source", src: resolve_asset_source(type, source, skip_pipeline)) }
end
else
options[:src] = send("path_to_#{type}", sources.first, skip_pipeline: skip_pipeline)
options[:src] = resolve_asset_source(type, sources.first, skip_pipeline)
content_tag(type, nil, options)
end
end
def resolve_image_source(source, skip_pipeline)
def resolve_asset_source(asset_type, source, skip_pipeline)
if source.is_a?(Symbol) || source.is_a?(String)
path_to_image(source, skip_pipeline: skip_pipeline)
path_to_asset(source, type: asset_type.to_sym, skip_pipeline: skip_pipeline)
else
polymorphic_url(source)
end
rescue NoMethodError => e
raise ArgumentError, "Can't resolve image into URL: #{e}"
raise ArgumentError, "Can't resolve #{asset_type} into URL: #{e}"
end
def extract_dimensions(size)

@ -39,8 +39,8 @@ class ActiveStorage::ReflectionTest < ActiveSupport::TestCase
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 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] }
assert_equal %i[ avatar avatar_with_variants cover_photo highlights highlights_with_variants intro_video name_pronunciation_audio vlogs ], reflections.collect(&:name)
assert_equal %i[ has_one_attached has_one_attached has_one_attached has_many_attached has_many_attached has_one_attached has_one_attached has_many_attached ], reflections.collect(&:macro)
assert_equal [ :purge_later, :purge_later, false, :purge_later, :purge_later, :purge_later, :purge_later, false ], reflections.collect { |reflection| reflection.options[:dependent] }
end
end

@ -0,0 +1,33 @@
# frozen_string_literal: true
require "test_helper"
require "database/setup"
class ActiveStorage::AudioTagTest < ActionView::TestCase
tests ActionView::Helpers::AssetTagHelper
setup do
@blob = create_file_blob filename: "audio.mp3"
end
test "blob" do
assert_dom_equal %(<audio src="#{polymorphic_url @blob}" />), audio_tag(@blob)
end
test "attachment" do
attachment = ActiveStorage::Attachment.new(blob: @blob)
assert_dom_equal %(<audio src="#{polymorphic_url attachment}" />), audio_tag(attachment)
end
test "error when attachment's empty" do
@user = User.create!(name: "DHH")
assert_not_predicate @user.name_pronunciation_audio, :attached?
assert_raises(ArgumentError) { audio_tag(@user.name_pronunciation_audio) }
end
test "error when object can't be resolved into URL" do
unresolvable_object = ActionView::Helpers::AssetTagHelper
assert_raises(ArgumentError) { audio_tag(unresolvable_object) }
end
end

@ -0,0 +1,33 @@
# frozen_string_literal: true
require "test_helper"
require "database/setup"
class ActiveStorage::VideoTagTest < ActionView::TestCase
tests ActionView::Helpers::AssetTagHelper
setup do
@blob = create_file_blob filename: "video.mp4"
end
test "blob" do
assert_dom_equal %(<video src="#{polymorphic_url @blob}" />), video_tag(@blob)
end
test "attachment" do
attachment = ActiveStorage::Attachment.new(blob: @blob)
assert_dom_equal %(<video src="#{polymorphic_url attachment}" />), video_tag(attachment)
end
test "error when attachment's empty" do
@user = User.create!(name: "DHH")
assert_not_predicate @user.intro_video, :attached?
assert_raises(ArgumentError) { video_tag(@user.intro_video) }
end
test "error when object can't be resolved into URL" do
unresolvable_object = ActionView::Helpers::AssetTagHelper
assert_raises(ArgumentError) { video_tag(unresolvable_object) }
end
end

@ -174,6 +174,8 @@ class User < ActiveRecord::Base
has_one_attached :avatar_with_variants do |attachable|
attachable.variant :thumb, resize_to_limit: [100, 100]
end
has_one_attached :intro_video
has_one_attached :name_pronunciation_audio
has_many_attached :highlights
has_many_attached :vlogs, dependent: false, service: :local