Iterate over has_many through relationship and include data from joining table - ruby-on-rails

I have a very simple rails app with three models: Recipes, Ingredients, and a joining table Quantities that stores the amount of each ingredient in the recipe. For one recipe, I want to list all the associate ingredients and the amount found in the joining table. How do I iterate over the ingredients, but also include the data from the quantities table?
class Recipe < ActiveRecord::Base
has_many :quantities
has_many :ingredients, through: :quantities
accepts_nested_attributes_for :quantities, :reject_if => :all_blank, :allow_destroy => true
end
and:
class Ingredient < ActiveRecord::Base
has_many :quantities
has_many :recipes, through: :quantities
end
and finally joining table:
class Quantity < ActiveRecord::Base
belongs_to :recipe
belongs_to :ingredient
accepts_nested_attributes_for :ingredient, allow_destroy: true
end
It seems like it should be really easy to do this iteration but I am not sure how.
show.html.erb:
<% #recipe.ingredients.each do |ingredient| %>
<% #I know the line below is wrong, but not sure how
# to iterate over the ingredients for the recipe and
# include the amount field from the quantities table
# as well as the ingredient name. %>
<li><%= ingredient.amount ingredient.name %></li>
<% end %>
Thank you!

In your controller's action do something like this:
#recipe = Recipe.includes(:ingredients, :quantities).find(params[:id]) # avoid N+1
and then, in your view:
<% #recipe.quantities.each do |quantity| %>
<%= quantity.ingredient.name %> -
<%= quantity.amount %>
<% end %>

The join table quantities is likely to have one one row for a combination of recipe and ingredient, even though the has_many :through implementation allows for multiple rows.
This allows for accessing the ingredient quantity and name as follows:
<% #recipe.ingredients.each do |ingredient| %>
<li>
<%= ingredient.quantities.first.amount %>
<%= ingredient.name %>
</li>
<% end %>

Related

Querying and linking to from join tables

Two Questions:
1) I have a retreats/:id view where I can display the team names that are affixed to a specific retreat. I can view the team names with the following query in the view:
<p>Teams: <%= #retreat.teams.pluck(:name).to_sentence %></p>
However, instead of just displaying the name, how would I both display the name of the team and link to the team team/:id
2) In this retreats/:id view, I would also like to display the users that are part of a team, but I am really stuck trying to go through sql joins, etc.
models
retreat.rb
class Retreat < ApplicationRecord
belongs_to :user
delegate :name, to: :user, prefix: true
belongs_to :account
validates :name, presence: true
has_many :retreat_teams
has_many :teams, through: :retreat_teams
accepts_nested_attributes_for :retreat_teams
end
team.rb
class Team < ApplicationRecord
belongs_to :account
has_many :team_members
has_many :users, through: :team_members
accepts_nested_attributes_for :team_members
has_many :retreats
has_many :retreats, through: :retreat_teams
end
team_members.rb
class TeamMember < ApplicationRecord
belongs_to :team
belongs_to :user
end
First part can be done this way
<% retreat.teams.each do |team| %>
<%= link_to(team.name, team_path(team.id)) %> # or whatever the path helper is
<% end %>
Second part, you can run this query instead
#teams = Team.where(retreat_id: #retreat.id).includes(:users)
Then in UI you can show like this
<% #teams.each do |team| %>
Team: <%= link_to(team.name, team_path(team.id)) %> # or whatever the path helper is
Team Users: <%= team.users.pluck(:name).to_sentence %>
<% end %>
Hope that helps!
1) I have a retreats/:id view where I can display the team names that
are affixed to a specific retreat. ...However, instead of just
displaying the name, how would I both display the name of the team and
link to the team team/:id
Don't use .pluck unless you actually want just a single column as an array or the raw data from several columns without instantiating model instances. .pluck is both overused and misused. It makes zero sense to pluck something if you then need to fetch the rest of the columns later anyways.
Instead just iterate though the model instances:
<% #resort.teams.each do |team| %>
<%= link_to team.name, team %>
<% end %>
If you have declared the route with resources :teams. Rails will figure out the route all by itself - that's the power of convention over configuration.
2) In this retreats/:id view, I would also like to display the users
that are part of a team, but I am really stuck trying to go through
sql joins, etc.
You don't really have to do any work joining. Just eager_load the association to avoid n+1 queries:
def show
#resort = Resort.eager_load(teams: :users).find(params[:id])
end
<% #resort.teams.each do |team| %>
<div class="team">
<h3><%= link_to team.name, team %></h3>
<h4>Members:</h4>
<% if team.users.any? %>
<ul>
<%= team.users.each do |user| %>
<li><%= link_to user.name, user %></li>
<% end %>
<ul>
<% end %>
</div>
<% end %>
Another note about naming
The name TeamMember is unfortunate as it implies that its the actual person that's a member and not just a join model.
Membership or Position are better name choices.
class Team
has_many :memberships
has_many :members, through: :memberships
end
class Membership
belongs_to :team
belongs_to :member, class_name: 'User'
end
class User
has_many :memberships, foreign_key: :member_id
has_many :teams, through: :memberships
end
This will let you iterate through team.members and actually get the users instead of some join model. The above example would read after refactoring:
def show
#resort = Resort.eager_load(teams: :members).find(params[:id])
end
<% #resort.teams.each do |team| %>
<div class="team">
<h3><%= link_to team.name, team %></h3>
<h4>Members:</h4>
<% if team.members.any? %>
<ul>
<%= team.members.each do |member| %>
<li><%= link_to member.name, member %></li>
<% end %>
<ul>
<% end %>
</div>
<% end %>

How to get attributes of model in has_many :through association in Rails 5

So I have 2 models in a has_many :through association. The two models are Meal and Food. Basically a Meal can have multiple food items and a food item can be a part of many meals. The third join model is called meal_foods.
I have set it up so that when you are creating a new Meal you can choose via a checkbox all the food items for the meal. A food item has attributes like calories & proteins and the Meal has attributes like total_calories and total_proteins.
How can I make it so that when making a new Meal I can calculate the values of all the attributes (calories, proteins, etc.) of all the food items?
Here's my code so far:
Models
class Meal < ApplicationRecord
belongs_to :user, optional: true
has_many :meal_foods
has_many :foods, through: :meal_foods
end
class Food < ApplicationRecord
has_many :meal_foods
has_many :meals, through: :meal_foods
end
class MealFood < ApplicationRecord
belongs_to :meal
belongs_to :food
end
Meals Controller
def create
#meal = Meal.new(meal_params)
#meal.user_id = current_user.id
#meal.total_calories = #Implement code here...
if #meal.save
redirect_to #meal
else
redirect_to root_path
end
end
Meals View (Create action)
<%= form_for(#meal) do |f| %>
<div class="field">
<%= f.label :meal_type %>
<%= f.select :meal_type, ["Breakfast", "Lunch", "Dinner", "Morning Snack", "Afternoon Snack, Evening Snack"] %>
</div>
<div class="field">
<% Food.all.each do |food| %>
<%= check_box_tag "meal[food_ids][]", food.id %>
<%= food.name %>
<% end %>
</div>
<div class="field">
<%= f.submit class: "button button-highlight button-block" %>
</div>
<% end %>
Thanks, in advance!
You can use sum eg for calories:
class Meal < ApplicationRecord
belongs_to :user, optional: true
has_many :meal_foods
has_many :foods, through: :meal_foods
def total_calories
foods.sum(:calories)
end
end
Sum works on any numeric column in an association.
If for some you need to store the value in the db (eg you're going to sort meals based on caloric content and it's easier to store the value), then you can instead tell a meal that when it is created, it should calculate and store the calories eg:
class Meal < ApplicationRecord
belongs_to :user, optional: true
has_many :meal_foods
has_many :foods, through: :meal_foods
# Tells rails to run this method when each Meal is first created
after_create :store_total_calories
# actually calculates the number of calories for any given meal (can be used even after saving in the db eg if the calories changed)
def calculate_total_calories
foods.sum(:calories)
end
# calculates, then resaves the updated value
def store_total_calories
update(total_calories: calculate_total_calories)
end
end
Note: more on the after_create callback here
Note: nothing needs to be done in the controller for this to all Just Work.

Rails Join Table Nested form_for

I'm learning Rails building an ordering system and I'm stuck trying to build a form for Orders. Where Orders is a nested resource for Restaurant.
Routes:
resources :restaurants do
resources :orders
resources :recipes
end
My models look like this:
class Restaurant < ActiveRecord::Base
has_many :orders
has_many :recipes, dependent: :destroy
end
class Order < ActiveRecord::Base
belongs_to :restaurant
has_many :order_recipes, dependent: :destroy
has_many :recipes, through: :order_recipes
end
class Recipe < ActiveRecord::Base
belongs_to :restaurant
has_many :order_recipes
has_many :orders, through: :order_recipes
end
My controller:
#recipes = #restaurant.recipes
#order_recipes = #recipes.map{|r| #order.order_recipes.build(recipe: r)}
And my view:
<%= form_for([#restaurant, #order]) do |order_form| %>
<%= order_form.label :Table_Number %>
<%= order_form.number_field :table_id %>
<%= order_form.fields_for :order_recipes, #order_recipes do |orf| %>
<%= order_form.hidden_field :recipe_ids %>
<%= order_form.label Recipe.where(id: :recipe_id) %>
<%= orf.number_field :quantity %>
My current problem is displaying the names of each recipe. It seems that :recipe_id is being passed as null all the time. My ultimate goal is to be able to build order_recipes populating the quantity column, and I thought having the recipe_id from order_recipes I could also access the correct recipe object from the DB to display the name or any other relevant data.
try so in your controller:
#order_recipes = #recipes.map{|r| #order.build_order_recipes(recipe: r)}
Eventually I got this the answer posted in this question to work: Rails 4 Accessing Join Table Attributes
I'm only struggling with passing the correct parameters now, but the form is correct.
I assumed, you have a restaurant, it has recipes, it takes orders, orders are tracked by order_recipes table.
# controller
#recipes = #restaurant.recipes
#order = #recipes.map{|r| r.order.build}
# view
<%= form_for([#restaurant, #order]) do |order_form| %>
...
<% #order.each do |index, ord| %>
<%= order_form.fields_for :orders, #order do |orf| %>
...
<%= order_form.label #recipes[index] %>
<%= orf.number_field :quantity %>
# also in restaurant model
accepts_nested_attributes_for :orders

Nested Attributes for a Rich Join Table, using simple_form Rails

I want to create a form that has nested attributes, which populates a record within a rich join table. (That created record within the rich join table of course should have the appropriate foreign keys.)
I have yet to find a thorough answer on creating nested form fields on a has_many :through relationship. Please help!
For this example, I have a user form. Within that form, I am also trying to populate a record within the users_pets table (rich join table).
Additional question: are rich join models supposed to be singular or plural? Example: app/models/owners_pets.rb or app/models/owners_pet.rb.
app/models/owner.rb
class Owner < ActiveRecord::Base
has_many :owners_pets, allow_destroy: true
has_many :pets, through: :owners_pets
end
app/models/pet.rb
class Pet < ActiveRecord::Base
has_many :owners_pets, allow_destroy: true
has_many :owners, through: :owners_pets
end
app/models/owners_pets.rb
class OwnersPet < ActiveRecord::Base
belongs_to :owners
belongs_to :pets
end
app/controller/owners.rb
def owner_params
params.require(:owner).permit(:first_name, owners_pets_attributes: [:id, :pet_name, :pet_id])
end
app/views/owners/_form.html.erb
<%= simple_form_for(#owner) do |f| %>
<%= f.input :first_name %>
<%= f.simple_fields_for :owners_pets do |ff|
<%= ff.input :pet_name %>
<% end %>
<div>
<%= f.button :submit %>
</div>
<% end %>
Here is the answer, thanks to a bunch of help from a mentor. It helps me to keep in mind that rich join naming conventions should NOT be pluralized at the very end, just like other non-rich-join models. Ex: book_page.rb NOT books_pages.rb. Even books_page.rb would work (just update your strong params and database table accordingly). The important part is that the entire model must follow the rails convention of the model being singular (no 's' on the end).
Below in the rich join model, I made the decision to name it the completely singular version: owner_pet.rb as opposed to the other version: owners_pet.rb. (Therefore of course, my database table is named: owner_pets)
app/models/owner.rb
class Owner < ActiveRecord::Base
has_many :owner_pets
has_many :pets, through: :owner_pets
accepts_nested_attributes_for :owner_pets, allow_destroy: true
end
app/models/pet.rb
class Pet < ActiveRecord::Base
has_many :owner_pets
has_many :owners, through: :owner_pets
end
app/models/owner_pet.rb
class OwnerPet < ActiveRecord::Base
belongs_to :owner
belongs_to :pet
end
app/controller/owners.rb
def new
#owner = Owner.new
#owner.owner_pets.build
end
private
def owner_params
params.require(:owner).permit(:first_name, owner_pets_attributes: [:_destroy, :id, :pet_name, :pet_id, :owner_id])
end
app/views/owners/_form.html.erb
<%= simple_form_for(#owner) do |f| %>
<%= f.input :first_name %>
<%= f.simple_fields_for :owner_pets do |ff| %>
<%= ff.input :pet_name %>
<%= ff.input :pet_id, collection: Pet.all, label_method: "pet_type" %>
<% end %>
<div>
<%= f.button :submit %>
</div>
<% end %>
Your join table is the problem:
It should be belongs_to :owners belongs_to :pets for the join table to work
Plus the rich join model should be pluralised, as in: owners_pets

Compile data from different models into one list

My app has a user model, as well as multiple other date related models/tables such as anniversaries, holidays, birthdays, and and "other dates" custom model.
I have a user dashboard view that lists them all separately as shown below. How can i display all of these lists as one (call it upcoming events or something) that is listed chronologically and shows them upcoming dates for a certain period of time.
View
*note - These are displayed in a table/list but i stripped html for clarity
<h1>Holidays</h1>
<% if #user.holidays.any? %>
<% #user.holidays.each do |hld| %>
<%= hld.name %>
<%= hld.date %>
<% end %>
<h1>Friends Birthdays</h1>
<% if #user.friends.any? %>
<% #user.friends.each do |frd| %>
<%= frd.name %>
<%= frd.dob %>
<% end %>
<h1> Anniversary </h1>
<% if #user.anniversaries.any? %>
<% #user.anniversaries.each do |ann| %>
<%= ann.spouse_name %>
<%= ann.anniversary_date %>
<% end %>
Thanks!
Models
class User < ActiveRecord::Base
has_many :friends
has_many :occasions
has_many :user_holidays
has_many :holidays, :through => :user_holidays
has_many :anniversaries
class Holiday < ActiveRecord::Base
has_many :user_holidays
has_many :users, :through => :user_holidays
end
class Friend < ActiveRecord::Base
belongs_to :user
end
class Anniversary < ActiveRecord::Base
belongs_to :user
end
Assuming you want to be efficient (you could just combine the arrays, sort them and be done with it), there is no direct way to do it through the relations. I am assuming you have an events model which has a foreign key to the user, in that case,
Events.where(:user_id => #user.id).where(<EVENT DATE FILTERS>).order("event_date DESC")
-- EDIT --
This is quite dirty, but I cant think of any other direct db way of accomplishing this.
events = #user.holidays.map{|h| [h.name, h.date, :holiday]} + \
#user.friends.map{|f| [f.name, f.dob, :birthday]} + \
#user.anniversaries.map{|a| [a.spouse_name, a.anniversary.date, :anniversary]}
events.map!{|event| {:name => event[0], :date => event[1], :event_type => event[2]}}
# You now have an array of hashes with the events name, date and type.
events.sort{|a, b| a[:date] <=> b[:date]} # sort / filter

Resources