I seem to be stuck with a problem that probably should be more obvious to me: How can I get certain attributes from a related model to show in a view.
In my application there are these 2 models:
products
product_images
I'm writing this while I'm on the go and don't have the exact code available. However I created the necessary associations, so that a product has_many product_images and a product_image belongs_to a product. The image model has an url, a default (boolean) flag and of course the product_id.
In the product index view I'd like to display the default image for that product. For the sake of simplicity though let's just assume I'm fine with showing the first picture - conditions should be easy to introduce once this works.
So in my products index view there's something like this (again, just from memory):
#products.each do |p|
<h3><%= p.name %></h3>
<%= image_tag p.product_images.first.url %>
<p><%= p.description %></p>
end
While the description and name of the product alone display just fine as soon as I include the image_tag my view breaks with a NoMethodError stating that url is an undefined method in Class Nil.
To keep it even simpler I got rid of the image_tag and just wanted to see the url printed in a paragraph - the problem remains of course.
If I just try to get p.product_images.first the view prints what I assume is some sort of ID for the object/model just fine, which tells me that the association itself is ok. So why does Rails think that the url attribute should be a method?
I also checked back with the rails console to see if this is the correct syntax to retrieve a related attribute. Like so (again, from memory - syntax errors possible):
p = Product.first
=> [successful - shows the first product]
p.product_images.first
=> [successful - shows the first image model]
p.product_images.first.url
=> [successful - shows me the single attribute, the url to the image]
As you can tell by now I'm very new to this and your help is very much appreciated. Of course I read the Rails documentation, however the Active Record Query Guide mostly focuses on getting data from the current model and I wasn't able to find what I'm obviously missing in my sample app.
Why does this work in the console but not in the view?
It is probably because one of your Product does not have any ProductImage.
You can correct this by adding a method in your ProductsHelper like so:
def product_image(product)
return if product.product_images.blank?
image_url = product.product_images.first.url
return if image_url.nil?
image_tag(image_url).html_safe
end
And then call it from your view like so:
#products.each do |p|
<h3><%= p.name %></h3>
<%= product_image(p) %>
<p><%= p.description %></p>
end
Related
In my Rails 5 application, building a JSON API & using Activestorage for my attachments:
To avoid accessing an attachment that doesn't exist, I am using this if condition as shown in this answer but unfortunately this if condition makes a SQL query to check if the attachment is available or not, causing n+1 queries although I have already included (pre-loaded) the attachments for the records I have
How to check if the attachment exist on a record without hitting the DB, if I have preloaded the attachments already?
I tried to use .nil? but that will give a false positive that there is attachment even if there isn't (as shown below)
ActionView::Template::Error (to_model delegated to attachment, but attachment is nil):
if !product.logo.nil?
json.logo_img url_for(product.logo)
.attached? check works but causes the n+1 queries:
if product.logo.attached?
json.logo_img url_for(product.logo)
So while the below code looks very ugly, it works, preloading the attachments for all the records at once, but when checking if an attachment exists => this leads to n+1 queries
json.array!(#some_records.includes(integrated_product: [company: [ logo_attachment: :blob ]])) do |one_record|
product ||= one_record.integrated_product
if !product.logo.attached?
json.logo_img url_for(product.logo)
elsif !product.company.logo.attached?
json.logo_img url_for(product.company.logo)
end
....
Any thoughts on how to handle this in a proper way (preloading attachments & avoiding n+1) are appreciated.
Update:
I want to have the whole collection that I passed, including items with & without attachment, searching for a condition that will check if that already loaded item has an attachment or not to display an image if it has attachment
Rails generates a collection method for has_one_attached relationships. You can just do something like this:
Product.all.with_attached_logo.map{|product| product.logo.attached? }
Which will make a single eager loaded query.
Lets say you want to show the logo's of each product inside views/products/index.htm.erb
So let's set up the #products variable in the index action of your controllers/products_controller.rb
def index
#products = Product.all.with_attached_logo
end
This will eager load the logo attachment into each product if they have one. Now we can create the products index page without the N+1 problem like so:
<% #product.each do |product| %>
<% if product.logo.attached? %>
<%= image_tag product.logo %>
<% else %>
<%= image_tag "product_logo_placeholder.jpg" %>
<% end %>
... the rest of the product's information ...
<% end %>
The problem turned out to be that the complex includes I had was not actually preloading the attachments (it was preloading for the nested record but not the main one), therefore the .attached? method was making a SQL query to check.
However, when the correct preloading is done, the .attached? works correctly, checking the in-memory object without firing a seperate SQL query to check again
I am leaving the question for the reference of the community if someone face a similar issue.
If I understand you correctly, you might want do do something like this.
In your model:
class Product < ApplicationRecord
has_one_attached :logo
scope :with_eager_loaded_logo, -> { eager_load(logo_attachment: :blob) }
end
Then in your controller, for example:
def index
#products = current_user.products.with_eager_loaded_logo
end
Not a JSON, but still some view example(HTML):
<%= image_tag (product.logo) if product.logo.attached? %>
See more in this post.
So, I have a nested form where Shelter has many Cats (just using cats as an example to melt the hearts of those eager to help).
So, in my nested form I can use:
<%= f.object.shelter_id %>
which gives a value when I run the application. But, and here is the cute cat's but:
<%= f.object.shelter.name %>
doesn't work, nor does:
<%= Shelter.find(f.object.shelter_id).name %>
Above statement gives an error can't find Shelter where 'id'=
One would think the value would be passed to the query as it is displayed when the app is run? How do I access the associated values in the form? (Its only in Edit, my Show and other controllers and views work fine.)
And yes, the associations are declared in my models.
I have a model course which has_many subcategories. I want to build a page that shows courses grouped by their subcategory. So far, I have
#courses = Course.personal_enrichment.order('subcategory_id').page params[:page]
#courses_facet = #courses.group_by(&:subcategory_id)
which works fine, but I need to show the actual subcategory name in the view, not the number. I've seen some other answers about this type of thing, but most of them assume the attribute you're grouping by is already human readable. Maybe I'm missing something?
When rendering the view you can just access the referenced models' attributes. Since group_by returns a hash, you could do something like this:
<% #courses_facet.each do |subcategory_id, courses| %>
<% subcategory_name = courses.first.subcategory.name rescue nil %>
<label><%= subcategory_name %></label>
<% end%>
Unless relevant subcategory models are cached this will generate N+1 queries to fetch the subcategory names. One way to avoid that is to include subcategory records to the initial resultset.
#courses.includes(:subcategories).group_by(&:subcategory_id)
I have 2 tables with a relation between them:
Users table: user_id,
Blogs table: user_id, blog_content,
Im working with a view that should show a users latest blog entry.. When I use
<%= #users.blogs.last %>
I get "#"
Can someone assist as to why its showing "#" and how to actually show the the last blog entry made by a #user?
Many thanks!!
I found out, I needed to define what part of the hash I needed to show
<%= #users.blogs.last[:blog_content] %>
I'd have thought you want to do:
<%= #users.blogs.last.blog_content %>
What you've done will try and display the Blog object itself with the to_s method, which returns something that looks like the following:
#<Blog:0x?????????>
Which on screen will just look like a hash.
Hey guys, I'm having trouble understanding a Rails construct. I'm using Rails 3 but I doubt this is specific to this version.
I have a model, Goal that has_many :commits and naturally, a Commit model that belongs_to :goal. I created the proper migration so that commits_table.references :goal.
I am actually going through the Rails Getting Started guide, except in that article they use a Post and Comment respectively.
Now that I've described the situation I can express my confusion. On the Goal's show view, I have embedded a form to create a new Commit which is 'attached' to the currently viewed Goal. This works fine and all. However, I am having trouble understanding why we do this
<%= form_for([#goal, #goal.commits.build]) do |f| %>
Shouldn't it be form_for(#commit)? I understand why we want the #goal, to provide some context since the commit is a nested resource. However, in the actual generated source, the form is appropriately named as commit, that is, the fields are named commit_blah. How did Rails know this? I understand that there's this whole system of "trust and magic" and all, but I mean at least at the high level, what from this code hinted to Rails that I wanted a commit?
I looked at the documentation for form_for and it seems like one of the parameters could be the action to take for the form. I imagine that in this case, that's what the #goal.commits.build parameter is for? To designate the action to take? Is this how Rails deduces that I want a commit? Would this also explain why this form is handled by the Commit controller even though this code is in the Goal's view?
Also, why are these parameters passed as an array ([])? In Ruby, will the method still just take it as two separate parameters, or is there a reason why this was passed this way?
Finally, rails generate automatically gave me some error showing code in my other _form.html.erb partials:
<% if #commit.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(#commit.errors.count, "error") %> prohibited this commit from being saved:</h2>
<ul>
<% #commit.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
If I want to add this to this embedded form, how would I do so? I guess my question is, what would I use in place of #commit?
Thanks. I'm just trying to get my head around these new concepts.
If you go back to the documentation and click 'show source', you'll see
def form_for(record_or_name_or_array, *args, &proc)
...
case record_or_name_or_array
when String, Symbol
...
when Array
object = record_or_name_or_array.last
object_name = options[:as] || ActiveModel::Naming.singular(object)
apply_form_for_options!(record_or_name_or_array, options)
args.unshift object
else
...
end
...
output << fields_for(object_name, *(args << options), &proc)
...
For form_for, the first parameter can be a record, name or an array. In your case, you pass it an array. The code then determines the 'object' as the last member of that array, which is your #goal.commits.build object. The object name is determined from the ActiveModel::Naming.singular method.
console > ActiveModel::Naming.singular(#goal.commits.build)
=> "commit"
Then it generated the appropriate form fields using fields_for and 'commit'.
It looks like you are using nested resources. Check your routes.rb file to see if you have something like:
map.resources :commits, :has_many => :goals
or perhaps:
map.resources :commits do |commit|
commit.resources :goals
end
If that is the case, then you will need to supply both the commit and goal objects to the form_for method.