Rails: Create Model and join table at the same time, has_many through - ruby-on-rails

I have three Models:
class Question < ActiveRecord::Base
has_many :factor_questions
has_many :bigfivefactors, through: :factor_questions
accepts_nested_attributes_for :factor_questions
accepts_nested_attributes_for :bigfivefactors
end
class Bigfivefactor < ActiveRecord::Base
has_many :factor_questions
has_many :questions, through: :factor_questions
end
and my join-table, which holds not only the bigfivefactor_id and question_id but another integer-colum value.
class FactorQuestion < ActiveRecord::Base
belongs_to :bigfivefactor
belongs_to :question
end
Creating an new Question works fine, using in my _form.html.erb
<%= form_for(#question) do |f| %>
<div class="field">
<%= f.label :questiontext %><br>
<%= f.text_field :questiontext %>
</div>
<%= f.collection_check_boxes :bigfivefactor_ids, Bigfivefactor.all, :id, :name do |cb| %>
<p><%= cb.check_box + cb.text %></p>
<% end %>
This let's me check or uncheck as many bigfivefactors as i want.
But, as i mentioned before, the join model also holds a value.
Question:
How can I add a text-field next to each check-box to add/edit the 'value' on the fly?
For better understanding, i added an image
In the console, i was able to basically do this:
q= Question.create(questiontext: "A new Question")
b5 = Bigfivefactor.create(name: "Neuroticism")
q.bigfivefactors << FactorQuestion.create(question: q, bigfivefactor: b5, value: 10)
I also found out to edit my questions_controller:
def new
#question = Question.new
#question.factor_questions.build
end
But i have no idea how to put that into my view.
Thank you so much for your help!

Big Five Factors model considerations
It looks like your Bigfivefactors are not supposed to be modified with each update to question. I'm actually assuming these will be CMS controlled fields (such that an admin defines them). If that is the case, remove the accepts_nested_attributes for the bigfivefactors in the questions model. This is going to allow param injection that will change the behavior sitewide. You want to be able to link to the existing bigfivefactors, so #question.factor_questions.first.bigfivefactor.name is the label and #question.factor_questions.first.value is the value. Notice, these exist on different 'planes' of the object model, so there wont be much magic we can do here.
Parameters
In order to pass the nested attributes that you are looking for the paramater needs to look like this:
params = {
question: {
questiontext: "What is the average air speed velocity of a sparrow?",
factor_questions_attributes: [
{ bigfivefactor_id: 1, value: 10 },
{ bigfivefactor_id: 2, value: 5 } ]
}
}
Once we have paramaters that look like that, running Question.create(params[:question]) will create the Question and the associated #question.factor_questions. In order to create paramaters like that, we need html form checkbox element with a name "question[factor_questions_attributes][0][bigfivefactor_id]" and a value of "1", then a text box with a name of "question[factor_question_attributes][0][value]"
Api: nested_attributes_for has_many
View
Here's a stab at the view you need using fields_for to build the nested attributes through the fields for helper.
<%= f.fields_for :factor_questions do |factors| %>
<%= factors.collection_check_boxes( :bigfivefactor_id, Bigfivefactor.all, :id, :name) do |cb| %>
<p><%= cb.check_box + cb.text %><%= factors.text_field :value %></p>
<% end %>
<% end %>
API: fields_for
I'm not sure exactly how it all comes together in the view. You may not be able to use the built in helpers. You may need to create your own collection helper. #question.factor_questions. Like:
<%= f.fields_for :factor_questions do |factors| %>
<%= factors.check_box :_destroy, {checked => factors.object.persisted?}, '0','1' %> # display all existing checked boxes in form
<%= factors.label :_destroy, factors.object.bigfivefactor.name %>
<%= factors.text_box :value %>
<%= (Bigfivefactor.all - #question.bigfivefactors).each do |bff| %>
<%= factors.check_box bff.id + bff.name %><%= factors.text_field :value %></p> # add check boxes that aren't currently checked
<% end %>
<% end %>
I honestly know that this isn't functional as is. I hope the insight about the paramters help, but without access to an actual rails console, I doubt I can create code that accomplishes what you are looking for. Here's a helpful link: Site point does Complex nested queries

Related

Rails, tips for solving this n+1?

I have an employee view, where are listed all skills, which are written in the skills table on my db. For every employee, all skills are displayed, just as want it to.
Employees and skills are related to each other as has many :through association.
class Employee < ApplicationRecord
has_many :employeeskillsets, foreign_key: "employee_id"
has_many :skills, through: :employeeskillsets
end
class Skill < ApplicationRecord
has_many :employeeskillsets, foreign_key: "skill_id"
has_many :employees, through: :employeeskillsets
end
class Employeeskillset < ApplicationRecord
belongs_to :employee, foreign_key: 'employee_id'
belongs_to :skill, foreign_key: 'skill_id'
end
All those skills are displayed as buttons, which are toggles to enable/disable that particular skill on that employee (per click a direct insert/delete, no extra commit needed).
<%= link_to apply_skill_employee_path(employee: #employee, skill_id: skill.id), method: :put, remote: :true do %>
<%= skill.name %></div><% end %>
But now, I want to the buttons to be shown colored, when you load the page. If the skill is already enabled, the buttoncolor should be green, else grey. And here begins my problem:
My app checks each skill with one separate select statement. I use the following code for this:
<%= link_to apply_skill_employee_path(employee: #employee, skill_id: skill.id), method: :put, remote: :true do %>
<% if #employee.skills.exists?(skill.id) %>
<div class="button skill e-true"><%= skill.name %></div>
<% else %>
<div class="button skill"><%= skill.name %></div>
<% end %>
I have already tried to use includes, but it seems, the exists? checks each skill independently.
Does anybody here have a suggestion, how I could solve this, by using one single select?
Thanks in advance, I hope I have mentioned everything, what is necessary.
Edit 1: I forgot to mention, that i render this through a partial (if that is important to know).
And here is the current used #employee var in the employees_controller.
#employee = Employee.find_by(id: params[:id])
Try plucking ids, and then check for include? as you don't need to fetch all attributes of skills
<% skills = #employee.skills.pluck(:id) %>
<%= link_to apply_skill_employee_path(employee: #employee, skill_id: skill.id), method: :put, remote: :true do %>
<% if skills.include?(skill.id) %>
<div class="button skill e-true"><%= skill.name %></div>
<% else %>
<div class="button skill"><%= skill.name %></div>
<% end %>
Since skill is also an active record model, you can use include? on the employee's skills collection to check if the employee has a particular skill.
#employee.skills.include?(skill)
This way you are free to use includes clause to eagerly load employees' skills.
This will not fire the additional query
<% if #employee.skill_ids.exists?(skill.id) %>
Also to avoid n+1 in the following line
apply_skill_employee_path(employee: #employee, skill_id: skill.id)
Make sure you are including skills
#employee = Employee.includes(:skills).where(......)
The Rails way is far simpler.
When you use the has_many macro in ActiveRecord it also creates a _ids method that can be used to add or remove relations with an array:
#employee.skills_ids = [1,2,3]
This also works with indirect assocations with the :through option.
You can use this together with the form collection helpers to create select or checkbox tags:
<%= form_for(#employee) do |f| %>
<%= f.label :skill_ids, 'Skills' %>
<%= f.collection_check_boxes(:skills_ids, Skill.all, :id, :name) %>
<% end %>
To avoid an extra query you can do a left outer join in the controller:
def edit
# left_outer_joins is new in Rails 5
# see https://blog.bigbinary.com/2016/03/24/support-for-left-outer-joins-in-rails-5.html
#employee.left_outer_joins(:skills).find(params[:id])
end
You also don't need a silly extra method in your controller for something that should be handled as a normal update. KISS.

Rails 4 form to set has_many through additional column

I have a has_many association between Items and their Components through a table called ComponentItems. ComponentItems contains a column quantity in addition to item_id and component_id. How is it possible to add a number_field to my form that shows the quantity of each component required for an item? The form must contain a number_field for each Item in the database, even if no relationship exists (i.e. #item.component_ids.empty? == true).
class Item < ActiveRecord::Base
has_many :components, through: :component_items
has_many :component_items
end
class Component < Item
has_many :items, through: :component_items
has_many :component_items
end
class ComponentItem < ActiveRecord::Base
belongs_to :item
belongs_to :component
end
I believe I've tried every permutation of model, controller and form_builder possible, except the correct one.
In response to the answer below, here's a form that shows a checkbox and the item code for component items that make up one particular item;
<%= form_for [#item] do |f| %>
<%= f.collection_check_boxes :component_items, Item.active.where.not(sku: #item.sku).sort_by{|n| n.sku}, :id, :sku do |b| %>
<%= b.check_box %> <%= b.label %><br/>
<% end %>
<% end %>
So, ideally I'd replace the check_box with a number_field for quantity. How?
So it seems what I wanted is not so straightforward after all. In the end I opted for using some jQuery for adding extra Components to Items via a separate form. Trying to add/remove components and adjust the quantities was beyond me, so choosing to use separate forms for each user action seemed simpler. It may not be the most user-friendly way of working but it's the best I have.
To edit the quantities I did the following;
<% #item.component_items.each do |x| %>
<%= hidden_field_tag "item[component_items_attributes][][id]", x.id%>
<%= label_tag x.component.sku, x.component.sku.upcase, :class=>"col-md-3 control-label" %>
<%= number_field_tag "item[component_items_attributes][][quantity]", x.quantity, :class=>"col-md-5"%>
<%end %>
and ensured the Item model accepted nested attributes for component_items. Finally, add the nested params array for multiple component_items to items_controller.rb...
def item_params
params.require(:item).permit(
:component_items_attributes =>[:component_id, :item_id, :quantity, :id]
)
end
Note I didn't use fields_for which seemed to generate an extra component_items_attributes array that didn't make any sense at all.
This should work:
#item.components.to_a.sum(&:quantity)
This will throw an error if quantity on some component is nil, so you may try like this to avoid errors:
#item.components.to_a.map(&:quantity).compact.sum
UPDATE
<% #item.component_items.each do |component_item| %>
<%= form_for(component_item) do |f| %>
<div class="field">
<%= f.label :quantity, 'Quantity' %><br />
<%= f.number_field :quantity %>
</div>
<% end %>
<% end %>

Accessing different model elements in Rails

I am inexperienced in Rails (version 4.2.5) and struggle with how views access database elements. I have worked through a number of different tutorials but still don't really understand why it doesn't work the way I think it does!
I have models that have been set up with references which I believe that establishes foreign keys in the database. I want to edit entries in the database that belong in a different model.
So, a Wines is a model that references Winemakers.
class Wine < ActiveRecord::Base
belongs_to :winemaker
end
In my _edit_form.html.erb file I have the following code which works but does not give me what I want:
<%= simple_form_for(#wine) do |f| %>
<div class="field">
<%= f.label :winemaker_id %>
<%= f.text_field :winemaker_id %>
</div>
This produces a simple box and in the box the integer that is winemaker_id is displayed but what I want is the actual name of the winemaker. I have tried :winemaker_id.name, #winemaker.name and many variations on those theme but I clearly do not understand how this works. I have tried reading various documentation but I am none the wiser.
Can someone please explain in simple terms how accessing different models works?
If your Winemaker model has been defined as follows:
class Winemaker < ActiveRecord::Base
has_many :wines
end
That means you can write the followings:
#winemaker.wines - returns all the wines belongs to a winemaker
#wine.winemaker - returns the winemaker to whom the wine belongs
If you want to show and edit the Winemaker name from Wine form, then you can do it using accepts_nested_attributes_for
Just modify your Wine model as follows:
class Wine < ActiveRecord::Base
belongs_to :winemaker
accepts_nested_attributes_for :winemaker
end
Now you can make a small change to your form as follows:
<%= form_for #wine do |f| %>
<%= f.fields_for :winemaker do |w|%>
<%= w.text_field :name%>
<% end %>
<%= f.submit%>
<% end %>
Try the following code:
<%= simple_form_for(#wine) do |f| %>
<div class="field">
<%= f.label :winemaker_id %>
<%= f.collection_select(:winemaker_id, Winemaker.all, :id, :name) %>
</div>
Have a look at http://apidock.com/rails/ActionView/Helpers/FormOptionsHelper/collection_select for more information.

Rails 3 - Create View to Insert Multiple Records

I have what seems like a simple query. I need to create a view that will accept multiple records based on a single model. In my case the model is Project, which has 1 foreign key (person) and 2 fields time, role. I need to create a view (form) to insert 5 roles.
<%= form_for(#project) do |f| %>
<% 5.times do |index|%>
<div class="field">
<%= f.label :position %><br />
<%= f.text_field "fields[#{index}][stime]" %>
</div>
<% end %>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
I get an error message: undefined method `fields[0][stime]'
I do not think the railscasts for nested models is what I need.
How would I go about creating this?
EDIT: The Project model code is below:
class Project < ActiveRecord::Base
belongs_to :person
attr_accessible :role, :stime
end
The Projects_Controller code for the new method is below:
def new
#project = Project.new
end
I see you're planning to make some 1-to-many relationship (Product has_many :roles).
Here's some advices.
First, take a look at the accepts_nested_attributes_for method. You need to add it to your model to be able to perform mass-create.
Second, fields_for is what you need to design nested forms.
I'll give you some example of mass-creating for a simple Product has_many :line_items case:
<%= form_for #product do |f| %>
<%= f.fields_for :line_items, [LineItem.new]*5 do |li_fields| %>
<%= li_fields.text_field :quantity %>
<%= li_fields.text_field :price %>
<br>
<% end %>
<%= f.submit "Create line items" %>
<% end %>
All you need is to write in you controller something like:
#product.update_attributes params[:product]
and 5 line_items will be created at once.
Don't forget to white-list association_attributes (see params in your logs to see it). But I think if you get the mass-assignment error you'll do it anyway :)
I hope it helps.

Rails: fields_for only one object

I've got a model Product in my Rails application, its attributes can be edited, and I want to let user comment every change he makes (a comment can be blank, though). So, Product has_many :comments, it accepts_nested_attributes_for :comments and rejects it if the comment is blank.
Hence, the edit form for Product is a multi-model form. The problems I faced are:
Fields_for helper renders text areas for all comments belonged to the product, so the user can edit all previous comments. I need it to render fields for the new one only.
If validation breaks, and there are no comments, fields_for renders nothing. Should I perform #product.comments.build in the view before fields_for statement every time, or there is more elegant way to do it?
Maybe I'm wrong and fields_for isn't suitable in this situation?
Base on Tots answer I just made it a little simplier (Rails 3 compatible):
<%= f.fields_for :comments, #product.comments.build do |comment| %>
<%= comment.label :comments %><br />
<%= comment.text_area :content %>
<% end %>
<% f.fields_for(:comments, Product.reflect_on_association(:comments).klass.new)
do |builder| %>
<%= builder.label :comment %>
<%= builder.text_area :comment, :rows => 3 %>
<% end %>

Resources