has_many form with nested attributes for existing objects - ruby-on-rails

I am still fairly new to Rails and fairly sure the way I'm attempting to go about this is inefficient or just plain silly, but here's what I'm trying to accomplish. I have 2 models, Cases (patient case files) and Inventories (medical transplant materials used in Cases).
class Case < ActiveRecord::Base
has_many :inventories
accepts_nested_attributes_for :inventories, :reject_if => :all_blank
end
class Inventory < ActiveRecord::Base
belongs_to :case
end
Inventories are created through a separate process and the goal is to associate them with a Case through the Case form. What I am trying to do is put a table on my Case form that lists the available Inventories along with checkboxes to select the desired Inventories to associate with the Case being created. This is further complicated by the fact that I need to be able to include nested fields for a couple of attributes on each Inventory (:case_price and :case_ship_price). I had previously done this in a very roundabout way using a has_many through association and storing those attributes on the pivot table, but it involved some hacky code to capture the field inputs from params and then save them through this block:
class CasesController < ApplicationController
def create
#case = Case.new(params[:case])
if #case.save
#case.case_lineitems.each do |li|
li.update_attributes(:price => params[:lineitem_price][li.inventory_id.to_s],
:shipping_cost => params[:lineitem_shipping][li.inventory_id.to_s])
end
redirect_to #case
else
render 'new'
end
end
end
This felt extremely clumsy and I was worried about problems it might cause, so I wanted to give a simple has_many, belongs_to relationship a try. However, I'm not sure if the typical <%= check_box_tag :inventory_ids, inventory.id, #case.inventories.include?(inventory), name: 'case[inventory_ids][]' %> works for that type of relationship. Here is what this section of my form looks like presently:
<table>
<thead>
<tr>
<th></th>
<th>Product</th>
<th>Serial #</th>
<th>Price</th>
<th>Shipping</th>
</tr>
</thead>
<tbody>
<% #inventories.each do |inventory| %>
<tr>
<td>
<%= check_box_tag :inventory_ids, inventory.id, #case.inventories.include?(inventory), name: 'case[inventory_ids][]' %>
</td>
<td><%= inventory.product.name %></td>
<td><%= inventory.serial_num %></td>
<%= f.fields_for :inventories, inventory do |inv| %>
<td>
<%= inv.text_field :case_price %>
</td>
<td>
<%= inv.text_field :case_ship_price %>
</td>
<% end %>
</tr>
<% end %>
</tbody>
</table>
This results in the first checkbox defaulting to checked, and if I leave all unchecked, all of the inventories become associated upon submission. Checking only a subset results in an exception along the lines of Couldn't find Inventory with ID=206 for Case with ID=. Finally, checking all of the Inventories seems to result in the associations and nested attributes saving correctly.
How do I clean this up so that it works as desired? And if I need to go back to a has_many through relationship, is there a better way to save attributes on the pivot table on the same form that creates the row on pivot table? I'd really appreciate any help with this, as no amount of searching has gotten me out of this challenge.

Related

Deleting ALL ActiveRecord Join associations through a rails form. (Adding and modifying works fine)

So I think I've narrowed down how to create/modify associations through forms. However, I cannot seem to remove all associations through this same method, because the submitted parameters include a blank array (nothing is selected in the form). When the array is empty, Rails does nothing instead of deleting all the association records.
So here's an example of my application. Here are two models:
#app/models/student.rb
class Student < ApplicationRecord
has_and_belongs_to_many :classes
end
and
#app/models/class.rb
class Class < ApplicationRecord
has_and_belongs_to_many :students
end
Now let's say my form is for Student:
<%= form_with(model: #student, local: true) do |form| %>
<table class="table table-striped table-bordered table-hover student-datatable">
<thead>
<tr>
<th><%= check_box_tag "student_header_checkbox", 0, false %></th>
<th>Class Name</th>
</tr>
</thead>
<tbody>
<% #classes.each do |class| %>
<tr>
<td><%= check_box_tag "student[class_ids][]", class.id, is_student_part_of_class(class) %></td>
<td><%= class.name %></td>
</tr>
<% end %>
</tbody>
</table>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-success btn-sm">
<i class='fa fa-save'></i> Save changes
</button>
</div>
<% end %>
Now in my Student controller, I am permitting class_ids by doing this at the bottom:
#app/controllers/students.rb
def student_params
params.require(:student).permit(:class_ids => [])
end
Ok, so all is well. When the user selects many classes, the classes are passed to the Student controller as an array. If there are classes selected, then an appropriate association record is created, and now that student "has_and_belongs_to_many" classes.
Now here's the problem
Let's say you have multiple classes added to this student, if you delete all the classes, then there is basically no array that gets passed to the controller; therefore, the controller does not delete all classes associated with this student.
If you modify the selection by, let's say, adding a class, removing a class, etc. anything but deselecting ALL classes, then everything works just fine.
Does rails not handle deleting all association records automatically like this? Or am I doing something wrong or missing something?
I just made an additional function in the controller to solve this problem as a temporary workaround for now:
# called after update
def delete_all_associations_if_empty
classes = student_params[:classes]
if classes.nil? or classes.empty?
#student.classes = []
end
end

Display results of a query in a table in rails application

I have 3 models as below.
Model for Item
class Item < ApplicationRecord
belongs_to :item_type, :class_name=>ItemType, :foreign_key=>"item_type_id"
end
and Model for Ingredients
class Ingredient < ApplicationRecord
validates_presence_of :ingredient, :message=>"name cannot be blank!"
end
and model for recipe_ingredients
class RecipeIngredient < ApplicationRecord
belongs_to :item, :class_name=>Item, :foreign_key=>"item_id"
belongs_to :ingredient, :class_name=>Ingredient, :foreign_key=>"ingredient_id"
validates_numericality_of :quantity
end
The table for ingredients has following columns
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
"ingredient" varchar,
"inventory_unit" varchar,
"recipe_unit" varchar,
"created_at" datetime NOT NULL,
"updated_at" datetime NOT NULL
The view for index of recipe ingredients is as below. The ingredients are appearing in the view. I want to pull recipe unit from ingredients table for the selected ingredient and display it in the table. I have tried it doing a query. The code is as below.
<p id="notice"><%= notice %></p>
<h1>Recipe Ingredients</h1>
<table>
<thead>
<tr>
<th>Item</th>
<th>Ingredient</th>
<th>Quantity</th>
<th colspan="3"></th>
</tr>
</thead>
<tbody>
<% #recipe_ingredients.each do |recipe_ingredient| %>
<tr>
<td><%= recipe_ingredient.item %></td>
<td><%= recipe_ingredient.ingredient.ingredient %></td>
<td><%= recipe_ingredient.quantity %></td>
<td><%= Ingredient.select('recipe_unit').where(:id => #ingredient_id) %></td>
<td><%= link_to 'Show', recipe_ingredient %></td>
<td><%= link_to 'Edit', edit_recipe_ingredient_path(recipe_ingredient) %></td>
<td><%= link_to 'Destroy', recipe_ingredient, method: :delete, data: { confirm: 'Are you sure?' } %></td>
</tr>
<% end %>
</tbody>
</table>
<br>
<%= link_to 'New Recipe Ingredient', new_recipe_ingredient_path %>
But the output I am getting is as shown in image given below.
You can simply type the following:
recipe_ingredient.ingredient.recipe_unit
Why is this possible?
Since a RecipeIngredient belongs_to an Ingredient, you can access the Ingredient of a RecipeIngredient as if it was any other field like this:
recipe_ingredient.ingredient
Once you have the ingredient, you can pull the recipe_unit directly by typing:
recipe_ingredient.ingredient.recipe_unit
Side note:
You can omit the :class_name and :foreign_key hashes from your associativity rules in your model classes. For example, take this class:
class Student < ActiveRecord::Base
belongs_to Course
end
By convention, rails will associate the Student class with the Course class. Rails will look for a column in the "students" database table labeled "course_id", and if "course_id" exists as a column in the table, Rails will automatically use that field as the foreign key for Student. You can use the :foreign_key => 'some_key' notation when you want to override the default conventions in rails (which isn't recommended unless its strictly necessary).
For more reading on the subject of active record migrations and foreign key associations, you can check out the rails docs.
Ok, it's displaying as it is because you are displaying the whole class, not an actual detail of the instance.
In this case below, you are selecting a whole set of Ingredients (the set contains only one ingredient, but it will always return a set):
Ingredient.select('recipe_unit').where(:id => #ingredient_id)
What you probably want is something like this:
Ingredient.find(#ingredient_id).recipe_unit
The differences:
where always returns a set of items (even if there's just one item in the set) What is returned is not an ingredient, but an Active Relation. To just return one ingredient, you need to use find or first (or a similar such method).
select just tells Rails what to pull out of the db... it doesn't tell the template what to display in the template. It says "only fetch the recipe_unit column from the db" it doesn't say and then output therecipe_unitcolumn - for that you need to actually call the recipe_unit method on the ingredient (as in the example I gave).
Note that you have the same problem with recipe_ingredient.item
What column from an item do you want to display? does it have a name or a description if so - you need to actually call that eg:
<%= recipe_ingredient.item.name %>
First of all, you may omit :class_name and :foreign_key from your models because of convention over configuration - your models are named exactly like classes and foreign keys you list.
As for your question, you need to use yak shaving for that:
recipe_ingredient.ingredient.recipe_unit

Return Attribute of Class Instance

Basically I have a table output on a page, base view. The headers (milestones) are dynamically integrated by passing the Milestones class and iterating through each with an .each do. The table rows are then initially generated by doing the same thing, pulling in client, trade, and units, into the first three columns. The rest of the columns should be dates (class entry attribute due) that are calculated by a lookup of sorts using the client and trade attributes accessible by the loop, and the milestone ID accessible by an array created when dynamically generating them for the headers. This all works correctly as it should, and the table generates fine.. however the dates are output as #<Entry:0x000000056faa90> and so on.
Now I have come across this problem before, and basically the fix was to add a class definition and tell it to return what I assume to be the attribute of that instance. For example:
class Client < ActiveRecord::Base
validates :id, presence:true
validates :client, presence:true
def name
return self.client
end
end
And this works great, but only for how I access the client. Unfortunately I am accessing entry.due differently. Here are my relevant bits of code.
base_controller.rb
class BaseController < ApplicationController
def index
#trades = Trade.all
#milestones = Milestone.all
##entries = Entry.all #Didn't seem relevant to how I am trying to access the information.
end
end
entry.rb
class Entry < ActiveRecord::Base
belongs_to :client
belongs_to :trade
belongs_to :milestone
validates :client, presence:true
validates :trade, presence:true
validates :milestone, presence:true
validates :due, presence:true
# Some of my trial and error; all to no avail.
def due
return self.due
end
def self.due
return self.due
end
def pickDue(c,t,m)
ret = self.select("date_format(due, '%m/%d/%Y')").where("client_id=? AND trade_id=? AND milestone_id=?", c, t, m).first
return ret
end
end
base > index.html.erb
<table>
<thead>
<tr class="sort">
<th>Client</th>
<th>Trade</th>
<th>Units</th>
<%
ms = []
#milestones.each do |milestone|
ms.push(milestone)
%>
<th><%= milestone.name %></th>
<% end %>
</tr>
</thead>
<tbody>
<% #trades.order("entered").last(10).each do |trade| %>
<tr>
<td><%= trade.client.name %></td>
<td><%= trade.name %></td>
<td><%= trade.units %></td>
<% ms.each do |msr| %>
<td>
<%= Entry.select("date_format(due, '%m/%d/%Y')").where("client_id=? AND trade_id=? AND milestone_id=?", trade.client, trade, msr).first %>
<%#= Entry.pickDue(trade.client, trade, msr) %>
</td>
<% end %>
</tr>
<% end %>
</tbody>
</table>
With this approach, I can load the page error-free, but instead of showing dates, I am shown objects like #<Entry:0x000000056faa90>.
If I add .due to the end of the selector:
<%= Entry.select("date_format(due, '%m/%d/%Y')").where("client_id=? AND trade_id=? AND milestone_id=?", trade.client, trade, msr).first.due %>
# undefined method `due' for nil:NilClass
If I add .due after anything else:
<%= Entry.due.select("date_format(due, '%m/%d/%Y')").where("client_id=? AND trade_id=? AND milestone_id=?", trade.client, trade, msr).first %>
<%= Entry.select("date_format(due, '%m/%d/%Y')").due.where("client_id=? AND trade_id=? AND milestone_id=?", trade.client, trade, msr).first %>
<%= Entry.select("date_format(due, '%m/%d/%Y')").where("client_id=? AND trade_id=? AND milestone_id=?", trade.client, trade, msr).due.first %>
# stack level too deep
I have tried dozens of different methods acquired through several days of looking into this with no luck. Besides the current situation where it outputs objects instead of the object attribute, the next closest I've come I think is when I got an error saying it could not find the due method for an ActiveRecord object, but I don't remember how got to that error.
I would really appreciate any input.
If I understand correctly, I would get rid of that trial and error code in your model, and then in your view you can just call this:
Entry.where(client: trade.client, milestone: msr, trade: trade).pluck(:due)
This goes inside your milestone loop in your view, like this:
<% ms.each do |msr| %>
<td>
<%= Entry.where(client: trade.client, milestone: msr, trade: trade).pluck(:due).first %>
</td>
<% end %>
Pluck returns an array of values from the columns you specify (see the relevant section of the RailsGuide, so note that the .first method is the Array#first method, not the ActiveQuery#first method. You could even make a query that passes in all of the milestones at once instead of running a query for each milestone, thereby preventing an N+1 query situation.
That would be:
<% Entry.where(client: trade.client, trade: trade, milestone: ms).pluck(:due).each do |entry_due_date| %>
<td>
<%= entry_due_date %>
</td>
<% end %>
Lastly, while I gave you the code for the SQL query in the view, it is generally considered a bad practice in Rails to write SQL queries in your view, and you should probably make this into a method in your Entry model.
I think you might be looking for attributes method.
entry = Entry.new
entry.attributes #=> returns a hash of key-value attribute pairs

Issue ordering array in the view

I'm trying to order a column inside array but is not ordering is showing values without ordering by lastname ASC.
Here is my table:
|products|
|id| |money| |client_id|
1 1000 1
2 2000 4
3 2000 7
4 2000 5
5 2000 6
6 2000 3
7 2000 2
|customers|
|id| |name| |lastname1| |lastname2|
1 Lionel Messi Bueno
2 Cristiano Ronaldo Tiger
3 Cecs Fabregas Midtlon
4 Andres Iniesta Blanco
5 Neymar Dos Santos Junior
Here is my controller
class ProductController < ApplicationController
def index
#products= Product.all
end
end
Here is the model
class Client < ActiveRecord::Base
has_many :products
def str_name
return lastname1.to_s+" "+lastname2.to_s+" "+name.to_s
end
end
class Product < ActiveRecord::Base
belongs_to :client
end
Here is my view
<table>
<tr>
<td>Money</td>
<td>Client</td>
</tr>
<% #products.each do |p| %>
<tr>
<td><%= p.money %></td>
<td><%= p.str_name %></td>
<% end %>
</tr>
</table>
I tried but is not ordering by lastname1 asc:
<% #products.each do |p| %>
<tr>
<td><%= p.money %></td>
<td><% #a= p.client(:order=>"lastname1 ASC")%> <%= #a.str_name %></td>
<% end %>
The log is:
SELECT * FROM `clients` WHERE (`clients`.`id` = 1)
SELECT * FROM `clients` WHERE (`clients`.`id` = 2)
SELECT * FROM `clients` WHERE (`clients`.`id` = 3)
SELECT * FROM `clients` WHERE (`clients`.`id` = 4)
And should be like this:
SELECT * FROM `clients` order by last_name1 ASC
Please somebody can help me?
<% #a= p.client(:order=>"lastname1 ASC")%> <%= #a.str_name %></td>
The line above does technically nothing. When you are already in the loop and you ask for the client of the current product, how many is it going to return? Only one, right ? Right, if you have a belongs_to on the other side (which I hope you do, this answer relies on it). So you're actually "sorting" a set of one element every iteration. And that's why your Select is ignoring the "order by". What you should be doing though, is to get a list of all the products ordered by their respective clients' "lastname1". This should not be done in a view like you're trying to do, but in your controller. In your controller:
#In Rails 4
#products = Product.joins(:client).order('clients.lastname1 ASC')
#Other version (be careful where to use plurals and where not to)
#products = Product.find(:all, :joins => :client, :order => 'clients.lastname1 ASC')
And in your view:
<% #products.each do |p| %>
<tr>
<td><%= p.money %></td>
<td><%= p.client.str_name %></td>
</tr>
<% end %>
<% #products.each do |p| %>
<tr>
<td><%= p.money %></td>
<td><% #a= p.client(:order=>"lastname1 ASC")%> <%= #a.str_name %></td>
<% end %>
Not sure if this answers your question, but in the second td, the ERB tag isn't printing out anything to the page. Check your syntax.
If you're actually trying to set #a equal to something like that, it's best done in the controller.
That was just something I noticed. I'll continue to look at your code to see if I can find why you're not getting the result you desire.
In your code, it looks like each product only has one client, however I can't be sure since you did not post code for your Product model. If that is the case, then p.client will only ever return one client, and there is no reason to sort the results; ActiveRecord may be smart enough to skip that step. This could also explain the queries in your log.
If you want a many-to-many relationship between clients and products, you should create a join table.
rails generate model ClientProduct client_id:integer product_id:integer
Your resulting relationships would be
Client:
class Client < ActiveRecord::Base
has_many :client_products
has_many :products, :through => :client_products
...
end
Product:
class Product < ActiveRecord::Base
has_many :client_products
has_many :clients, :through => :client_products
...
end
ClientProduct:
class ClientProduct < ActiveRecord::Base
belongs_to :client
belongs_to :product
...
end

Retrieve info from last record of association

I have a model Lead which has a has_many Relationship with Activity.
I want to display a list of all leads on the leads index page, including a count of all activities and the timestamp of the latest activity of each lead.
The model looks like this:
class Lead < ActiveRecord::Base
has_many :activities, -> { order("activitytimestamp DESC") }, dependent: :destroy
class Activity < ActiveRecord::Base
belongs_to :lead
The view looks like this:
<table>
<thead>
<tr>
<th>Activity | last</th>
...
</tr>
</thead>
<tbody>
<% #leads.each do |lead| %>
<tr>
<td><%= lead.activities.size %> | <%= lead.activities.last %></td>
...
</tr>
<% end %>
</tbody>
</table>
Obviously lead.activities.last doesn't work.
What do I need to do to display the timestamp of the latest lead.activities Record?
You're so close!
General configuration of a general attribute of the most recent record related to a specific record is:
specific_record.related_records.last.general_attribute
or, in your case, I believe:
lead.activities.last.timestamp_field
The issue with
leads.activities.last
is that it returns the object, not the information in the field of that object, which is what you're looking for.

Resources