Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend image_tag to accept ActiveStorage Attachments and Variants #30084

Merged
merged 8 commits into from Aug 7, 2017

Conversation

colorfulfool
Copy link
Contributor

@colorfulfool colorfulfool commented Aug 5, 2017

To use an ActiveStorage attachment in image_tag, you have to write

<%= image_tag url_for(user.avatar) %>

This change simplifies it to

<%= image_tag user.avatar %>

And a bonus: if you pass a Blob, not a Variant, and specify the size arg, the image will be automatically downsampled by applying variant(resize: size) behind the scenes. This one I'm not sure of, is it too implicit?

@rails-bot
Copy link

Thanks for the pull request, and welcome! The Rails team is excited to review your changes, and you should hear from @kaspth (or someone else) soon.

If any changes to this PR are deemed necessary, please add them as extra commits. This ensures that the reviewer can see what has changed since they last reviewed the code. Due to the way GitHub handles out-of-date commits, this should also make it reasonably obvious what issues have or haven't been addressed. Large or tricky changes may require several passes of review and changes.

This repository is being automatically checked for code quality issues using Code Climate. You can see results for this analysis in the PR status below. Newly introduced issues should be fixed before a Pull Request is considered ready to review.

Please see the contribution instructions for more information.

@colorfulfool colorfulfool changed the title Extend image_tag to accept an ActiveStorage Blobs and Variants Extend image_tag to accept ActiveStorage Blobs and Variants Aug 5, 2017
@colorfulfool colorfulfool changed the title Extend image_tag to accept ActiveStorage Blobs and Variants Extend image_tag to accept ActiveStorage Attachments and Variants Aug 5, 2017
@georgeclaghorn
Copy link
Contributor

This one I'm not sure of, is it too implicit?

Yeah, I think that’s a bridge too far. Let’s scope this down to just accepting blobs and variants as arguments and consider automatic resizing separately.

@colorfulfool
Copy link
Contributor Author

colorfulfool commented Aug 5, 2017

Where do you think the tests should be? Together with other image_tag tests or with other Blob tests? The latter has easier access to create_blob and ActiveStorage models. If the former, what is the right way to load the models in there?

@georgeclaghorn
Copy link
Contributor

The tests for this probably belong in Active Storage’s test suite.

@@ -364,6 +364,14 @@ def multiple_sources_tag_builder(type, sources)
end
end

def resolve_image_source(source, skip_pipeline)
if source.class.in? [ActiveStorage::Variant, ActiveStorage::Blob, ActiveStorage::Attachment]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering if we can't do it the other way around. If the source is a string/symbol or whatever was reasonable before to use with image_tag, we'll call the existing path_to_image. If it's not, we try polymorphic_url. Then we wouldn't have to hardcode this to Active Storage and would open the door for people to build their own direct URLs for images.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ha! That's how I was going to do it, but thought it will be rejected as too far-fetched. Along the lines of "if someone goes as far as to write their own image framework with their own custom routes, they surely can override image_tag themselves".

@colorfulfool colorfulfool force-pushed the image-tag-blob branch 2 times, most recently from 52aa0f6 to 8f24336 Compare August 5, 2017 20:22
test "attachment on a model" do
@user.avatar.attach @blob
assert_dom_equal %(<img alt="Racecar" src="#{polymorphic_url @user.avatar}" />), image_tag(@user.avatar)
end
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which of the Attachment tests is better?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would keep the plain attachment. A bit clearer.

@dhh
Copy link
Member

dhh commented Aug 5, 2017

Need to update documentation as well. Also, wonder what the error state looks like if you pass something in that polymorphic_url can't resolve?

@colorfulfool
Copy link
Contributor Author

colorfulfool commented Aug 5, 2017

  • something polymorphic_url can't resolve => undefined method to_model
  • attachment is empty => undefined method to_model

The first case is okay I think, pass the wrong object — get an undefined method. But the empty attachment needs a more relevant message. Can delegate_missing_to :attachment be made to raise to_model is delegated to attachment, but attachment is nil instead of undefined method to_model?

@@ -200,7 +200,7 @@ def favicon_link_tag(source = "favicon.ico", options = {})
end

# Returns an HTML image tag for the +source+. The +source+ can be a full
# path or a file.
# path, a file or an ActiveStorage attachment.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When referring to the library and not the Ruby constant, leave a space between the words “Active” and “Storage.”

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it!

@colorfulfool colorfulfool force-pushed the image-tag-blob branch 3 times, most recently from 02c3ce3 to f44e7a4 Compare August 6, 2017 17:06
@dhh
Copy link
Member

dhh commented Aug 6, 2017

Don't love the first error state. We should catch and make it more specific to this usage. Because someone may well have passed something they thought would just auto-convert to a string or whatever, and then they see this instead.

Like the proposed fix for the second case.

@colorfulfool
Copy link
Contributor Author

colorfulfool commented Aug 6, 2017

It should be a change to polymorphic_url, then. People may pass stuff into link_to and others just as well.

Is it okay to amend delegate_missing_to right in this PR?

@dhh
Copy link
Member

dhh commented Aug 6, 2017 via email

@colorfulfool colorfulfool force-pushed the image-tag-blob branch 2 times, most recently from 242b85c to f22891f Compare August 7, 2017 11:59
@user = User.create!(name: "DHH")

assert_not @user.avatar.attached?
assert_raises(ArgumentError) { image_tag(@user.avatar) }
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This now raises ArgumentError: Can't resolve image into URL: to_model delegated to attachment, but attachment is nil. Not exactly what I wanted, but good enough I suppose?

@dhh dhh merged commit 7c89948 into rails:master Aug 7, 2017
@dhh
Copy link
Member

dhh commented Aug 7, 2017

Ace 👌

@colorfulfool
Copy link
Contributor Author

Heeey, I haven't even had the chance to add myself into changelog!

@colorfulfool colorfulfool deleted the image-tag-blob branch August 8, 2017 10:00
@dhh
Copy link
Member

dhh commented Aug 8, 2017

Ah, do another PR with the changelog change and I'll merge that 👍 (assign it to me)

@@ -565,9 +565,6 @@ class PlaceholderImage
def blank?; true; end
def to_s; "no-image-yet.png"; end
end
def test_image_tag_with_blank_placeholder
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this test was removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was testing a rudimentary version of the functionality this PR implements: an object with overloaded to_s.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, but there is no reason to delete this test, even more because it was testing a different kind of object.

else
polymorphic_url(source)
end
rescue NoMethodError => e
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why we are rescuing this exception?

if source.is_a?(Symbol) || source.is_a?(String)
path_to_image(source, skip_pipeline: skip_pipeline)
else
polymorphic_url(source)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use url_for here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

url_for seems to truncate absolute URLs the same way polymorphic_path does (and polymorhic_url doesn't). That is, resolve('Attachment') { "http://dropbox.com/box456" } would turn out as /box456 instead of http://dropbox.com/box456.

@@ -364,6 +375,16 @@ def multiple_sources_tag_builder(type, sources)
end
end

def resolve_image_source(source, skip_pipeline)
if source.is_a?(Symbol) || source.is_a?(String)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd invert this conditional. I'm sure we can check if source respond to a method like to_model before calling url_for.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ActiveStorage::Variant doesn't respond to to_model and it doesn't have to:

def polymorphic_mapping(record)
if record.respond_to?(:to_model)
_routes.polymorphic_mappings[record.to_model.model_name.name]
else
_routes.polymorphic_mappings[record.class.name]
end
end

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to change ActiveStorage::Variant to respond to to_model instead. If we are expecting to use that class with URLs it needs to follow the ActiveModel::Model API.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, to be fair that is not a requirement indeed. But I think in this case it is better to check to_model. It will avoid the rescue of NoMethodError and it also would avoid checking the type of the arguments.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fact the only think that is annoying me here is the NoMethodError. Do you know why we need to rescue this exception?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But what if user passes an object that usually can be resolved into url, but right now can't? E.g. user.avatar when no avatar is attached. It will say Attachment is not a model or has a resolve, which is misleading.

My version will raise Can't resolve image into URL: to_model delegated to attachment, but attachment is nil which, I think, gives user the right clue.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure the "can't resolve" part is necessary there, given an existing reasonable exception from the delegation. But even so, apart from the word "image", that all seems like reasonable behaviour for polymorphic_url.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, can't resolve object into URL + the underlaying exception should be doing the trick. If polymorphic_url raises that, we can remove the rescue here. Would that satisfy you, @rafaelfranca?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems good to me. 👍

Copy link
Contributor

@jhubert jhubert Jul 20, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@colorfulfool @rafaelfranca Fwiw, this broke our use of CarrierWave. Prior to this change, we could pass the thumbnail directly into the image_tag method:

<%= image_tag(user.avatar.tiny) %>

After this change, it throws Can't resolve image into URL: undefined method 'to_model' for #<AvatarUploader...

Adding .url fixes it, and it's arguable that we were using CarrierWave wrong, but passing an object that gets to_s called on it seems pretty common. I was surprised that this broke on a minor point release.

Do you think it's worth changing to support that behavior, or should we keep it as is?

@@ -565,9 +565,6 @@ class PlaceholderImage
def blank?; true; end
def to_s; "no-image-yet.png"; end
end
def test_image_tag_with_blank_placeholder
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, but there is no reason to delete this test, even more because it was testing a different kind of object.

has_one_attached :avatar
end

class ActiveStorage::ImageTagTest < ActionView::TestCase
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This entire file is coupling ActiveStorage to ActionView that is not a dependency of activestorage. We need to move those tests to actionview in the same way we do with activerecord.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should they go under test/activerecord and test with generic models or under test/activestorage and stay as is, with Blobs and stuff?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm more inclined with test/activestorage since we may change the implementation in the future to not use Active Record (not that I see this happening but better to be safe)

@@ -273,6 +273,8 @@ def respond_to_missing?(name, include_private = false)
def method_missing(method, *args, &block)
if #{target}.respond_to?(method)
#{target}.public_send(method, *args, &block)
elsif #{target}.nil?
raise DelegationError, "\#{method} delegated to #{target}, but #{target} is nil"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is changing the behavior of delegating_to_missing so it needs a changelog and a test for it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It merely replaces NoMethodError with its subclass DelegationError in come cases. Any code that relies on it will still work. Does this constitute a change in behavior, then?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That assumes super will raise; the class may well have other method_missing definitions in play.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it!

colorfulfool pushed a commit to colorfulfool/rails that referenced this pull request Aug 17, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

8 participants