Rails form with three models and namespace - ruby-on-rails

Banging my head against this one for a long time. On Rails 2.3.2, Ruby 1.9.1.
Trying to use one form to create three objects that have these relations:
class Person
has_one :goat
end
class Goat
belongs_to :person
has_many :kids
end
class Goat::Kid
belongs_to :goat
end
Here's a summary of the schema:
Person
first_name
last_name
Goat
name
color
Goat::Kid
nickname
age
I'd like my #create action to instantiate new instances of all three models with the specified associations. However, while it appears that my params hash is being passed to the controller as it should (based on the backtrace logs in the browser when it blows up), the Goat::Kid object is not collecting the params.
irb (irb session is just a psuedo-representation of what I'm trying to accomplish so if it doesn't call #save! or any other necessities it's not really meant to be correct. I'm trying to do this all through the browser/web form.)
a = Person.new :first_name => 'Leopold', :last_name => 'Bloom'
b = Goat.new :name => 'Billy', :color => 'white'
c = Goat::Kid.new :nickname => 'Jr.', :age => 2
a.goat.kids
>> []
Now, I cannot figure out how to get the view to pass the params to each object and to get the controller to save these params to the db.
My questions: A) is this a good place to use nested_attributes_for and if so how do I declare that with a namespace? B) is there a much simpler, easier to understand way to do this?
Passing params to three models has just been very challenging to me and no matter how much documentation I read I can't wrap my head around it (#form_for and #fields_for). The namespace further complexifies this. Thanks for any help!
Addendum: if I end up declaring
accepts_nested_attributes_for
what's the proper way to use the symbol argument for a namespaced model?
accepts_nested_attributes_for :kids, :through => :goats
or
accepts_nested_attributes_for :goats_kids, :through => :goats
or
accepts_nested_attributes_for :goats::kids, :through => :goats
I'm not sure how namespaced models translate to their symbol identifiers. Thanks!

Well, this is my first time playing with accepts_nested_attributes_for, but with a little playing around I was able to get something to work.
First the model setup:
class Person < ActiveRecord::Base
has_one :goat
accepts_nested_attributes_for :goat
end
class Goat < ActiveRecord::Base
belongs_to :person
has_many :kids
accepts_nested_attributes_for :kids
end
class Goat::Kid < ActiveRecord::Base
belongs_to :goat
end
With a simple restful controller:
ActionController::Routing::Routes.draw do |map|
map.resources :farm
end
class FarmController < ApplicationController
def new
end
def create
person = Person.new params[:person]
person.save
render :text => person.inspect
end
end
Then comes the semi-complex form:
Next, the form setup:
<% form_for :person, :url => farm_index_path do |p| %>
<%= p.label :first_name %>: <%= p.text_field :first_name %><br />
<%= p.label :last_name %>: <%= p.text_field :last_name %><br />
<% p.fields_for :goat_attributes do |g| %>
<%= g.label :name %>: <%= g.text_field :name %><br />
<%= g.label :color %>: <%= g.text_field :color %><br />
<% g.fields_for 'kids_attributes[]', Goat::Kid.new do |k| %>
<%= k.label :nickname %>: <%= k.text_field :nickname %><br />
<%= k.label :age %>: <%= k.text_field :age %><br />
<% end %>
<% end %>
<%= p.submit %>
<% end %>
From looking at the source for accepts_nested_attributes_for, it looks like it will create a method for you called #{attr_name}_attributes=, so I needed to setup my fields_for to reflect that (Rails 2.3.3). Next, getting the has_many :kids working with accepts_nested_attributes_for. The kids_attributes= method was looking for an array of objects, so I needed to specify the array association in the form manually and tell fields_for what type of model to use.
Hope this helps.

Related

Rails has_many :through nested forms with simple form

I am trying to make a player character generator. I have a form that hopefully will allow me to attach skills with their values to a character sheet model. I made models like this:
class CharacterSheet < ApplicationRecord
has_many :character_sheet_skills, dependent: :destroy
has_many :skills, through: :character_sheet_skills
belongs_to :user
accepts_nested_attributes_for :skills
end
class Skill < ApplicationRecord
has_many :character_sheet_skills, dependent: :destroy
has_many :character_sheets, through: :character_sheet_skills
attr_reader :value
end
class CharacterSheetSkill < ApplicationRecord
belongs_to :skill
belongs_to :character_sheet
end
Character sheet model holds data about player character and skill model has all skills available in game. In CharacterSheetSkill I'd like to store the skills that the player chooses for his character together with an integer field setting the skill value.
When opening form, I already have a full list of skills in database. All I want to do in form is create a character sheet that has all of these skills with added value. I tried using "fields_for" in form, but I couldn't really get that to work. Right now it looks like this:
<%= simple_form_for [#user, #sheet] do |f| %>
<%= f.input :name %>
<%= f.input :experience, readonly: true, input_html: {'data-target': 'new-character-sheet.exp', class: 'bg-transparent'} %>
...
<%= f.simple_fields_for :skills do |s| %>
<%= s.input :name %>
<%= s.input :value %>
<% end %>
<% end %>
How can I make that form so it saves character sheet together with CharacterSheetSkills?
A better idea here is to use skills as a normalization table where you store the "master" definition of a skill such as the name and the description.
class CharacterSheetSkill < ApplicationRecord
belongs_to :skill
belongs_to :character_sheet
delegate :name, to: :skill
end
You then use fields_for :character_sheet_skills to create rows on the join table explicitly:
<%= f.fields_for :character_sheet_skills do |cs| %>
<fieldset>
<legend><%= cs.name %></legend>
<div class="field">
<%= cs.label :value %>
<%= cs.number_field :value %>
</div>
<%= cs.hidden_field :skill_id %>
</fieldset>
<% end %>
Instead of a hidden fields you could use a select if you want let the user select the skills.
Of course nothing will show up unless you "seed" the inputs:
class CharacterSheetController < ApplicationController
def new
#character_sheet = CharacterSheet.new do |cs|
# this seeds the association so that the fields appear
Skill.all.each do |skill|
cs.character_sheet_skills.new(skill: skill)
end
end
end
def create
#character_sheet = CharacterSheet.new(character_sheet_params)
if #character_sheet.save
redirect_to #character_sheet
else
render :new
end
end
private
def character_sheet_params
params.require(:character_sheet)
.permit(
:foo, :bar, :baz,
character_sheet_skill_attributes: [:skill_id, :value]
)
end
end

Reading value with association (simple_form)

I have such models:
class Grade < ActiveRecord::Base
has_many :question_grades
end
class QuestionGrade < ActiveRecord::Base
belongs_to :grade
belongs_to :question
# it has integer :number
end
class Question < ActiveRecord::Base
# it has string :label
end
I have a simple_form for the 'grade' model, which iterates question_grades:
<%= simple_form_for #grade, :url => "/homeworks/update_grade", :method => :post do |f| %>
<%= f.simple_fields_for :question_grades do |q| %>
<%= q.association :question %>
<%= q.input :number, :collection => 0..2, label: false%>
</div>
</div>
This form creates an editable form for each 'question_grade', where allows visitors to edit 'number' attribute of question_grade. I also want to show a label by using the value, 'question_grade.question.label'. I created an association with 'q.association :question' but it creates an editable input form item. I want to access a value in the association. How can I do that?
When you do
<%= q.association :question %>
you are creating a field to edit this association, as you can see.
What do you need, is to access the q.object, defined as attr_reader here .
in this case, it will be your QuestionGrade instance.
so this:
<%= q.object.question.label %>
may solve your problem.

How do I reference an existing instance of a model in a nested Rails form?

I'm attempting to build a recipe-keeper app with three primary models:
Recipe - The recipe for a particular dish
Ingredient - A list of ingredients, validated on uniqueness
Quantity - A join table between Ingredient and Recipe that also reflects the amount of a particular ingredient required for a particular recipe.
I'm using a nested form (see below) that I constructed using an awesome Railscast on Nested Forms (Part 1, Part 2) for inspiration. (My form is in some ways more complex than the tutorial due to the needs of this particular schema, but I was able to make it work in a similar fashion.)
However, when my form is submitted, any and all ingredients listed are created anew—and if the ingredient already exists in the DB, it fails the uniqueness validation and prevents the recipe from being created. Total drag.
So my question is: Is there a way to submit this form so that if an ingredient exists whose name matches one of my ingredient-name fields, it references the existing ingredient instead of attempting to create a new one with the same name?
Code specifics below...
In Recipe.rb:
class Recipe < ActiveRecord::Base
attr_accessible :name, :description, :directions, :quantities_attributes,
:ingredient_attributes
has_many :quantities, dependent: :destroy
has_many :ingredients, through: :quantities
accepts_nested_attributes_for :quantities, allow_destroy: true
In Quantity.rb:
class Quantity < ActiveRecord::Base
attr_accessible :recipe_id, :ingredient_id, :amount, :ingredient_attributes
belongs_to :recipe
belongs_to :ingredient
accepts_nested_attributes_for :ingredient
And in Ingredient.rb:
class Ingredient < ActiveRecord::Base
attr_accessible :name
validates :name, :uniqueness => { :case_sensitive => false }
has_many :quantities
has_many :recipes, through: :quantities
Here's my nested form that displays at Recipe#new:
<%= form_for #recipe do |f| %>
<%= render 'recipe_form_errors' %>
<%= f.label :name %><br>
<%= f.text_field :name %><br>
<h3>Ingredients</h3>
<div id='ingredients'>
<%= f.fields_for :quantities do |ff| %>
<div class='ingredient_fields'>
<%= ff.fields_for :ingredient_attributes do |fff| %>
<%= fff.label :name %>
<%= fff.text_field :name %>
<% end %>
<%= ff.label :amount %>
<%= ff.text_field :amount, size: "10" %>
<%= ff.hidden_field :_destroy %>
<%= link_to_function "remove", "remove_fields(this)" %><br>
</div>
<% end %>
<%= link_to 'Add ingredient', "new_ingredient_button", id: 'new_ingredient' %>
</div><br>
<%= f.label :description %><br>
<%= f.text_area :description, rows: 4, columns: 100 %><br>
<%= f.label :directions %><br>
<%= f.text_area :directions, rows: 4, columns: 100 %><br>
<%= f.submit %>
<% end %>
The link_to and link_to_function are there to allow the addition and removal of quantity/ingredient pairs on the fly, and were adapted from the Railscast mentioned earlier. They could use some refactoring, but work more or less as they should.
Update: Per Leger's request, here's the relevant code from recipes_controller.rb. In the Recipes#new route, 3.times { #recipe.quantities.build } sets up three blank quantity/ingredient pairs for any given recipe; these can be removed or added to on the fly using the "Add ingredient" and "remove" links mentioned above.
class RecipesController < ApplicationController
def new
#recipe = Recipe.new
3.times { #recipe.quantities.build }
#quantity = Quantity.new
end
def create
#recipe = Recipe.new(params[:recipe])
if #recipe.save
redirect_to #recipe
else
render :action => 'new'
end
end
You shouldn't put the logic of ingredients match into view - it's duty of Recipe#create to create proper objects before passing 'em to Model. Pls share the relevant code for controller
Few notes before coming to code:
I use Rails4#ruby2.0, but tried to write Rails3-compatible code.
attr_acessible was deprecated in Rails 4, so strong parameters are used instead. If you ever think to upgrade your app, just go with strong parameters from the beginning.
Recommend to make Ingredient low-cased to provide uniform appearance on top of case-insensitivity
OK, here we go:
Remove attr_accessible string in Recipe.rb, Quantity.rb and Ingredient.rb.
Case-insensitive, low-cased Ingredient.rb:
class Ingredient < ActiveRecord::Base
before_save { self.name.downcase! } # to simplify search and unified view
validates :name, :uniqueness => { :case_sensitive => false }
has_many :quantities
has_many :recipes, through: :quantities
end
<div id='ingredients'> part of adjusted form to create/update Recipe:
<%= f.fields_for :quantities do |ff| %>
<div class='ingredient_fields'>
<%= ff.fields_for :ingredient do |fff| %>
<%= fff.label :name %>
<%= fff.text_field :name, size: "10" %>
<% end %>
...
</div>
<% end %>
<%= link_to 'Add ingredient', "new_ingredient_button", id: 'new_ingredient' %>
We should use :ingredient from Quantity nested_attributes and Rails will add up _attributes-part while creating params-hash for further mass assignment. It allows to use same form in both new and update actions. For this part works properly association should be defined in advance. See adjusted Recipe#new bellow.
and finally recipes_controller.rb:
def new
#recipe = Recipe.new
3.times do
#recipe.quantities.build #initialize recipe -> quantities association
#recipe.quantities.last.build_ingredient #initialize quantities -> ingredient association
end
end
def create
#recipe = Recipe.new(recipe_params)
prepare_recipe
if #recipe.save ... #now all saved in proper way
end
def update
#recipe = Recipe.find(params[:id])
#recipe.attributes = recipe_params
prepare_recipe
if #recipe.save ... #now all saved in proper way
end
private
def prepare_recipe
#recipe.quantities.each do |quantity|
# do case-insensitive search via 'where' and building SQL-request
if ingredient = Ingredient.where('LOWER(name) = ?', quantity.ingredient.name.downcase).first
quantity.ingredient_id = quantity.ingredient.id = ingredient.id
end
end
end
def recipe_params
params.require(:recipe).permit(
:name,
:description,
:directions,
:quantities_attributes => [
:id,
:amount,
:_destroy,
:ingredient_attributes => [
#:id commented bc we pick 'id' for existing ingredients manually and for new we create it
:name
]])
end
In prepare_recipe we do the following things:
Find ID of ingredient with given name
Set foreign_key quantity.ingredient_id to ID
Set quantity.ingredient.id to ID (think what happens if you don't do that and change ingredient name in Recipe)
Enjoy!

Use rails3-jquery-autocomplete with simple form on a has_and_belongs_to_many association

I'd like to have a form where groups can add their interests and this form should help them with autocompletion.
As this is a many-to-many relation, I don't understand how to implement it
class Group < ActiveRecord::Base
attr_accessible :description, :name, :project_id, :interests
before_save :get_next_available_name
has_many :users
belongs_to :project
has_and_belongs_to_many :interests
end
class Interest < ActiveRecord::Base
attr_accessible :name
has_and_belongs_to_many :groups
end
In the view I have:
<%= simple_form_for #group do |f| %>
<%= f.autocomplete_field :interests, autocomplete_interest_name_groups_path %>
<%= f.submit "Interesse eintragen" %>
<% end %>
Of course I entered "rails g autocomplete:install"
I probably have to edit the routes and added this to the routes.rb:
resources :groups do
get :autocomplete_interest_name, :on => :collection
end
UPDATE:
As Saurabh suggested, I put he "autocomplete :interest, :name" in the controller, not in the model.
But I have two problems now:
First: There is no autocomplete coming up.
Second: After adding 'ruby' Interest and submitting the form: In the input field there is written this:
[#<Interest id: 38, name: "ruby", created_at: "2013-02-28 09:25:53", updated_at: "2013-02-28 09:25:53">]
But of course the field itself should be empty.
If this gem is not compatible with Rails 3.2, someone should tell me...
Define the autocomplete in your controller and remove it from model as:
class GroupsController < ApplicationController
autocomplete :interest, :name
# ...
# ...
end
UPDATE:
According to your question update, this is what you need to do now:
Your view:
<%= simple_form_for #group do |f| %>
<%= f.autocomplete_field :interests, autocomplete_interest_name_groups_path %>
<%= f.submit "Interesse eintragen" %>
<% end %>
More from the rails3 jquery autocomplete sources.

Rails updating multiple models on a single form

Im writing a form which uses formtastic to manage the BusinessUnit model, however when creating a new BusinessUnit it also has to create a number of other record types. The associations between the models are as below:
class BusinessUnit < ActiveRecord::Base
has_many :business_unit_sites
has_many :locations
class BusinessUnitSite < ActiveRecord::Base
belongs_to :site
belongs_to :business_unit
class Site < ActiveRecord::Base
has_many :locations
has_many :business_unit_sites
class Location < ActiveRecord::Base
belongs_to :business_unit
belongs_to :site
When a BusinessUnit is created, a Site must also be created using BusinessUnitSite as a join table. In addition a Location record should be created which must hold a foreign key to the new Site record and this is where Im having problems.
I can create a new Location using a nested form (below) but the Site will have to be created manually.
<%= semantic_form_for #business_unit do |f| %>
<%= f.inputs do %>
<%= f.input :name %>
<%= f.input :business_unit_id %>
<%= f.input :business_unit_group, :include_blank => false %>
<%= f.input :business_unit_type %>
<%= f.input :tax_region, :include_blank => false %>
<%= f.semantic_fields_for :locations do |l| %>
<%= l.input :name, :label => "Location Name" %>
<% end %>
<% end %>
<%= f.buttons %>
<% end %>
What is the best way to create the Location, Site records and ensure that Location holds the foreign key of the newly created Site?
You probably want to do something like using the "fields_for" approach for the sub-objects in your form.
See this related answer:
Multiple objects in a Rails form
More info about fields_for:
http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html
http://apidock.com/rails/ActionView/Helpers/FormHelper/fields_for

Resources