rails: how to add child association through a text field? - ruby-on-rails

I have two models: Company and Person
class Person < ActiveRecord::Base
belongs_to :company
end
class Company < ActiveRecord::Base
has_many :people
accepts_nested_attributes_for :people, :allow_destroy => true, :reject_if => proc {|attrs| attrs.all? {|k,v| v.blank? } }
end
And my HTML form partial for new and edit actions looks like this:
<% form_for(#company) do |company_f| %>
<p>
<b>Name</b><br />
<%= company_f.text_field :name %>
</p>
<ul>
<%= render :partial => 'person_fields', :collection => #company.people, :locals => {:company_f => company_f} %>
<%= link_to_add_fields(:people, company_f) %>
</ul>
<p>
<%= company_f.submit "Submit" %>
</p>
<% end %>
where "_person_fields" partial looks like this:
<li>
<% company_f.fields_for :people, person_fields do |person_f| %>
<%= person_f.label :name %>
<%= person_f.text_field :name %>
<% end %>
</li>
At the moment, if I typed in person_f.text_fiel :name the name of the person, and hit save, a new Person model with that name gets created. Not what I want at all, I already HAVE that person's Person model in the database, I rather want to ASSOCIATE this person to this company.
Another thing is that I wouldn't mind using the name for human-friendly identification of the person rather than id like this for the "_person_fields" partial
<li>
<% company_f.fields_for :people, person_fields do |person_f| %>
<%= person_f.label :id %>
<%= person_f.text_field :id %>
<% end %>
</li>
this by the way, doesn't work either. when I hit submit, nothing happens. nothing gets saved or changed or anything.
So I thought, just for the sake of experiment, say I did use id's for identification for a Person model, (so that I don't have to go in to autocomplete with a hidden id field which I am using for another project. I hate it). All I want is: go to a new/edit Company page, there's a bunch of textfields for me to type in ids of people, and save and then these people are then associated with the company. I mean, it's exactly like
people = Person.find(1,2,3)
#=>["romeo","juliet","henry"]
company = Company.first
#=>["Shakespeare Co."]
company.people<<people
company.people
#=>["romeo","juliet","henry"]
And it'd be best if I didn't have to use select menus because eventually if the project takes off and I have a thousand people, that's too big for any select menu. I know then I will have to use autocomplete + hidden id field that gets set when a person's name is chosen.
Thanks!!

accepts_nested_attributes_for :people defines people_attributes and people_attributes= methods in your Company model. Those two methods are used when you have fields_for :people in form. As stated in documentation (read whole page, not only method definition):
Nested attributes allow you to save attributes on associated records through the parent.
It doesn't work to associate two objects, that can be done without it, you still have people method in Company.
Formtastic for example uses select with multiple attribute (you can check more than one option with ctrl) or list of check_boxes (easier for normal user, don't have to touch keyboard).
If you want to use autocomplete, it could be done, but you need to append your ids to people[] array (don't remember format right now, I'll check later. You can check its format when you create attribute in your form for :people I think)
Edit:
I think I know hot to do it simple way (without autocomplete, simple html only).
In your companies/edit (or new) view place:
<p>
<%= company_f.label :people %>
<%= company_f.collection_select :person_ids, Person.all, :id, :name, {}, {:multiple => true} %>
</p>
That will allow you to select multiple people to company (with Ctrl). Params generated by this part transfered to your controller should now look like:
"company"=>{"name"=>"CompanyA", "person_ids"=>["1", "3"]}, "commit"=>"Update", "id"=>"4"
If you want to show people with checkboxes I managed to get it with this (I copied part of markup from formtastic):
<ul>
<% Person.all.each do |person| %>
<li>
<%= check_box :person, :id, {:name => "company[person_ids][]", :checked => #company.people.include?(person) }, person.id, nil %>
<%= person.name %>
</li>
<% end %>
</ul>
I had no time today to check how to do it with hidden fields, so you can use autocomplete, but I believe that it should be similar to checkboxes - you need to use something which adds <input type="hidden" name="company[person_ids][]" value="#{person.id}"> for every autocompleted person, and also need to create initial list of people already in company (YOU OVERRIDE THE LIST, not add to list).
If you want field to add or remove (separate fields, sorry) people from company, then this should work:
in Company model:
def add_people=(people_ids)
ids = people_ids.split(/,/).map(&:to_i)
person_ids += ids if ids
end
def add_people
""
end
def remove_people=(people_ids)
ids = people_ids.split(/,/).map(&:to_i)
person_ids -= ids if ids
end
def remove_people
""
end
and in your new/edit view:
<p>
<%= f.label :add_people %><br />
<%= f.text_field :add_people %>
</p>
<p>
<%= f.label :remove_people %><br />
<%= f.text_field :remove_people %>
</p>
Now all you need to do is to find JavaScript for auto-complete, connect it to ids and names of all people (probably PeopleController#index, :format => :json should be good) and tell it to fill those text_fields (can be hidden fields if you would use autocomplete).
This approach should work, because you define virtual attributes in company model, and by assigning to them string in format "1, 2, 6", you add/remove those ids from your collection of associated people

Related

Rails: Create Model and join table at the same time, has_many through

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

Ruby on Rails: assign relationship on creation

I'm new to Ruby on Rails. There are two models in my project: room and guest. The association is "room has_many guests" and "guest belongs to room".
I have separated views for manage rooms and guests. Rooms don't require "guests" value on creation. However, I want to create new guests and assign it to certain room at the same time. What will be the proper way to do it? How do I transfer the input from web and match the entities in database.
The code is pretty much the same as "Getting Started with Rails". In the tutorial, they add "comments" in the "article" view and use "comment" as a sub-resource of "article". In my case, I treat the two models equally and want to manage them in separated views.
Update:
I used the collection_select and try to work with my guest_controller.
<%= form_for :guest, url: guests_path do |f| %>
<% if #guest.errors.any? %>
<div id="error_explanation">
<h2>
<%= pluralize(#guest.errors.count, "error") %> prohibited this guest from being added:
</h2>
<ul>
<% #guest.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<p>
<%= f.label :name %><br>
<%= f.text_field :name %>
</p>
<p>
<%= f.label :phone %><br>
<%= f.text_field :phone %>
</p>
<p>
<%= f.label :room%><br>
<%= f.text_field :room %>
</p>
<p>
<%= f.label :room %><br>
<%= f.collection_select(:room_id, Room.all, :id, :title) %>
</p>
<p>
<%= f.submit %>
</p>
<% end %>
<%= link_to 'Back', guests_path %>
In my guest_controller, the create method called by the form above is :
def create
#guest = Guest.new(guest_params)
#guest.room = Room.find(params[:room_id])
if #guest.save
redirect_to #guest
else
render 'new'
end
end
However, when I create a new guest, it shows that:
ActiveRecord::RecordNotFound in GuestsController#create
Couldn't find Room with 'id'=
I checked that room_id=4 and Room.find(4) return the proper room.
What's wrong?
If you want to select one room from those that exist, use collection_select form helper, here is a relevant snippet from the docs:
f.collection_select(:city_id, City.all, :id, :name)
This outputs a dropdown list that:
fills in city_id parameter in this context
uses City.all for filling in the options in the list (I will be referring to "each" city as city)
uses city.id as data (that gets sent in the form)
shows city.name for each city in the dropdown list (hopefully, human-readable)
Bear in mind though, that in terms of security it's like "look, you can select this, and this and this!", that does not prevent users from selecting an unlisted option: either by modifying form markup by hand or sending handcrafted queries.
So should you ever be limiting access to specific rooms, and list only Room.unlocked (unlocked assumed a scope), make sure the received room_id refers to a room from that scope as well. Most of these problems are dealt with using either validations or careful association management (Room.unlocked.find_by_id(:room_id) that outputs nil if the room is not in that scope).
UPD: as for the latest problem you're having -- your understanding on how the form contents look in params seems to be wrong. It's quite a common misconception actually.
form_for :guest will construct a separate object/hash in params[:guest], with all the form's fields inside it. So it actually is inside params[:guest][:room_id], but no, don't rush with adding the missing part.
You've already built a #guest object from entire params[:guest], so if the room actually exists, it's inside #guest.room already and can be validated inside the model during save. Have a look at Rails validators.
Take a look at the fields_for tag:
http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-fields_for
It allows just that, to create a guest while creating a room and associating each other.

Using form helpers to picking an object by name, but assigning the ID associated with that name

I have a Product model, an Order, and an OrderItem model.
An OrderItem object has a single Product (that is, it belongs to a product)
An Order has multiple order items.
When you want to add an OrderItem to your order, you fill out a form. One of the fields is to enter the product that will be assigned to this order item. The order model stores the product_id as a foreign key, and so Rails complains (as it should) when I write
<%= form_for #orderItem, url: {:action => :create} do |f| %>
<p>
<%= f.label :product_name %>
<%= f.text_field :product_name %>
</p>
<%= f.submit %>
<% end %>
What I have in mind is to to have someone use the form to create order items. When they choose which product to assign to this order item, they will pick a name from a list of product names (which are unique), and then when they submit the form, the controller retrieves the appropriate Product object and assigns its ID to the OrderItem.
How can I set up my form to accomplish this?
Sounds like you want to use nested attributes. They'll let you save information about both an OrderItem and a Product at the same time from a single form.
In your OrderItem, add the line accepts_nested_attributes_for :product.
Then in your form you'll want something like:
<%= form_for #orderItem, url: {:action => :create} do |f| %>
<p>
<%= fields_for :product do |fp| %>
<%= fp.label :product_name %>
<%= fp.text_field :product_name %>
<% end %>
</p>
<%= f.submit %>
<% end %>
(Note the use of fp.label instead of f.label within the fields_for block.
The above is from memory so there may be some mistakes (and you'll probably need to change things in your OrderItemsController too), but it should give you a starting point.
You might find this Railscast helpful too.

Iterate through records in view and create text_fields

I have a table: family_children (the model is family_child) where family has many children.
I get the children like this:
#family_children = #family.children
where .children is an association to family_children table.
In a view I want to iterate through the children, and put each of them in a text_field. Of course, I need these fields as params when the page is POSTing. I.e. I think that I should get the children as an array.
How can I achieve that?
I mean, if I'll write
<%= text_field 'child', 'name' %>
I don't really get what I need.
Try something like this in your view:
<% #family_children.each_with_index do |c, i| %>
<%= text_field_tag "children[#{i}]", c.name %>
<br />
<% end %>
This should return params[:children] after posting which should be an array. I wasn't sure of the name of the property you want to show in the text box so I have assumed it is called 'name'.
Actually, #family_children object acts as array, so you can simply call each or map on it.
Do you want to put the children's names in a form field or in the view as just a part of the page text? could you include the view file?
since u want the family_children data to be POSTed, u need to see the concept of nested attributes. please see http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html and http://apidock.com/rails/ActionView/Helpers/FormHelper/fields_for
class Family
has_many :children, :class => "FamilyChild"
accepts_nested_attributes_for :children
end
class FamilyChild
belongs_to :family
end
and the form can be as
<%= form_for #family do |p| %>
First name: <%= p.text_field :first_name %>
Last name : <%= p.text_field :last_name %>
<%= fields_for #family.children do |c| %>
<%= c.text_field :child_name %>
<% end %>
<%= f.submit %>
<% end %>

Display list of data based on id of another model chosen from collection_set

I have an app that has 2 models, City and Neighborhood. on the root page, I use collection_set to display all cities
<%= form_tag('/sales/neighborhood', :method => :get) %>`
<%= collection_select(:neighborhood, :city_id, City.all, :id, :name) %>
<%= submit_tag 'Go' %>
I then want to have in the following view page, a dropdown list of all neighborhoods with a city_id that matches the id of the city chosen on the first page. I think I have this part right and the city id is being passed in the params because I get a url like this http://localhost:3000/sales/neighborhood?utf8=%E2%9C%93&city%5Bid%5D=1&commit=Go. I just can't get a list of neighborhoods to display. I tried this in sales#neighborhood
<%= form_tag('sales/locations', :method => :get) %>
<%= collection_select(:location, :neighborhood_id, #nbhds.all, :id, :name) %>
<%= submit_tag 'Go' %>
but I get nothing in the dropdown box. I even tried this to just get a list of neighborhoods with a city_id matching the id of the city chosen like this...
<ul>
<% #nbhds.each do |n| %>
<li>
<%= n.name %>,
<%= n.city.name %>
</li>
<% end %>
</ul>
I then want the application to list a set of locations based on the neighborhood_id, which would be chosen from the collection_set on the second page. Can anybody point me in the right direction? I think I've just about got it, just missing something.
My models look like this:
class City < ActiveRecord::Base
attr_accessible :name, :state
has_many :neighborhoods
end
and:
class Neighborhood < ActiveRecord::Base
attr_accessible :city_id, :name
belongs_to :city
has_many :locations
end
Here is the Sales controller action being called:
def neighborhood
#nbhds = Neighborhood.where(:city_id => params[:id])
end
I figure my problem is either in the where clause in sales#neighborhood or in my view but I put the code for the ul from the view in rails console and it lists all of the neighborhoods that belong to the city, the only difference is in the console I used
nbhds = Neighborhood.where(:city_id => 1)
instead of accessing it via params
I still need help with this if anyone can
According to the collection select
<%= collection_select(:neighborhood, :city_id, City.all, :id, :name) %>
parameter you should get in your controller will be like
params[:neighborhood][:city_id]
But according to your GET url http://localhost:3000/sales/neighborhood?utf8=%E2%9C%93&city%5Bid%5D=1&commit=Go, it seems the two parameters you are sending is city[id]=1 and commit=Go "city%5Bid%5D=1&commit=Go"
%5B & %5D are the HTML url encoding for '[' & ']' respectively.
If this is the correct request url then in you controller you would rather use the parameter [:city][:id] than [:id]
def neighborhood
#nbhds = Neighborhood.where(:city_id => params[:city][:id])
end
Please check your rails log to identify which is the correct params key before you start trying with any of the solution I mentioned.
You have city_id as a query parameter, and yet you look for params[:id] in your controller.
I would change that to
def neighborhood
city = City.find(params[:city_id])
#nbhds = city.neighborhoods
end
If that doesn't solve your problem I'll have a better look tomorrow :)

Resources