Rails3 Nested Attributes Using Parent Values in Save Action - ruby-on-rails

I've got a simple nested form working in my rails3 application. I'm trying to work out how to save a value from the parent model into the child when saving.
class Radcheck < ActiveRecord::Base
set_table_name 'radcheck'
attr_accessible :attribute_name, :username, :value, :op
belongs_to :raduser
end
class Raduser < ActiveRecord::Base
has_many :radcheck, :dependent => :destroy
accepts_nested_attributes_for :radcheck
end
And my form:
<%= form_for #raduser do |f| %>
<p>
<%= f.label :username %><br />
<%= f.text_field :username %>
</p>
<%= f.fields_for :radcheck do |builder| %>
<li>
<%= builder.label :attribute_name %><%= builder.text_field :attribute_name %>
</li>
<% end %>
<p><%= f.submit "Submit" %></p>
<% end %>
What I want to do is save the Raduser.username value in to the radcheck table on save. For each record.
I've tried putting something in the controller but that wasn't really working for us.
-- Update --
In my radcheck controller, I've tried this (as a test) but the values don't save.
def create
#radcheck = Radcheck.new(params[:radcheck])
#radcheck.username = '123'
respond_to do |format|
if #radcheck.save
format.html { redirect_to #radcheck, notice: 'Radcheck was successfully created.' }
format.json { render json: #radcheck, status: :created, radcheck: #radcheck }
else
format.html { render action: "new" }
format.json { render json: #radcheck.errors, status: :unprocessable_entity }
end
end
end
Have also tried in radusers but that gave error.

It looks like the controller code you posted is for the Radcheck controller, correct? If so, the form you have also posted will be using the create action of the RaduserController class, not the one from RadcheckController. This would explain why you aren't seeing the username '123' in the radcheck rows.
If the username field is the same between parent and child, a common way to sync these two up would be with an observer or a before_save callback. I'll outline the before_save method below:
class Radcheck < ActiveRecord::Base
set_table_name 'radcheck'
attr_accessible :attribute_name, :username, :value, :op
belongs_to :raduser
before_save :sync_usernames
private
def sync_usernames
self.username = self.raduser.username
end
end
I've tested this with Rails 3.1 and it works; however, I know I have run into issues accessing parent models within a child model in previous versions of Rails. In those cases, I had to put the before_save action on the parent model, and have it iterate over each child, setting the appropriate attributes that way.

Related

Rails Mass Assignment

I have a site that keeps track of SAT tutoring sessions. The curriculum that the students learn is a collection of rules. I have a model for each tutoring session called "Sittings" and the rules model is called "Rules". I want the site admin to be able to enter a Sitting by date, and then use checkboxes to select which "rules" the student got wrong in that sitting. I'm a little confused as to how I can create the form to pull out specific rules without adding attributes to my Sitting model of rule1, rule2, etc. I'm using simple_form to create my forms.
My Sitting model:
class Sitting < ActiveRecord::Base
attr_accessible :date, :comment, :rule_id, :user_id
validates :date, presence: true
belongs_to :user
has_many :combos
has_many :rules, :through => :combos
end
My Rules model:
class Rule < ActiveRecord::Base
attr_accessible :name, :subject, :session_id, :hint_id, :question_id, :trigger_id
validates :name, presence: true
validates :subject, presence: true
has_many :questions
has_many :triggers
has_many :hints
has_many :combos
has_many :sittings, :through => :combos
end
My Combo model:
class Combo < ActiveRecord::Base
belongs_to :sitting
belongs_to :rule
end
Edit:
Here's what I have tried for the form. It does create the checkbox form, but my DB isn't updating the rule_id. (shows as nil when I create a Sitting)
form:
<%= simple_form_for(#sitting, html: { class: "form-horizontal"}) do |f| %>
<%= f.error_notification %>
<% Rule.all.each do |rule| %>
<%= check_box_tag "sitting[rule_ids][]", rule.id, #sitting.rule_ids.include?(rule.id) %> <%= rule.id %>
<% end %>
<div class="form-group">
<%= f.input :comment, as: :text, input_html: { rows: "2", :class => "form-control" }, label: "Comments:" %>
</div>
<div class="form-group">
<%= f.date_select :date, as: :date, label: "Taken Date:" %>
</div>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
I updated my strong params to allow an array:
def create
#sitting = Sitting.new(sitting_params)
respond_to do |format|
if #sitting.save
format.html { redirect_to #sitting, notice: 'Sitting was successfully created.' }
format.json { render action: 'show', status: :created, location: #sitting }
else
format.html { render action: 'new' }
format.json { render json: #sitting.errors, status: :unprocessable_entity }
end
end
end
def sitting_params
params.require(:sitting).permit(:comment, :date, :user_id, :rule_id => [])
end
Am I missing something in order to update the Sitting.rule_id properly? I get the following error in my logs:
WARNING: Can't mass-assign protected attributes for Sitting: rule_ids
app/controllers/sittings_controller.rb:27:in `create'
Just to summarize what we got through over the chat.
Firstly, you do not need both attr_accessible and strong_params at the same time. I've posted another answer some time ago explaining how those two approaches differs from each other.
You're running rails 4, so you should take advantage of strong params and not use protected_attributes gem. In short, remove this gem from you Gemfile as well as all attr_accessible calls.
As Marian noticed, you have a typo in your strong params method, you need to permit rule_ids, not rule_id. rule_id column is obsolete, as sitting has_many :rules :through rather than sitting belongs_to :rule - most likely it is an artifact of old association code.
As soon as rule_ids are assigned within your model, it will create new join models in your join table, creating an association between given sitting and passed rules.

Create two hashes from one form

I want every user to upload a profile image on sign up. I have two models, a User model, and an Image model. So how should I update the User model with a new user, and an Image model with a new image for that user with one form?
user model
class User < ActiveRecord::Base
has_one :profile_image, class_name: 'Image', foreign_key: 'user_id'
end
image model
class Image < ActiveRecord::Base
# do I need to put something else here to make this relationship work?
end
user/new view
<%= form_for(#user) do |f| %>
<%= f.label :email %><br>
<%= f.text_field :email %>
<%= f.label :password %><br>
<%= f.password_field :password %>
# this needs to update a seperate params hash, but how?
<%= f.file_field :profile_image %>
<%= f.label :password_confirmation %><br>
<%= f.password_field :password_confirmation %>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
user#create action
def create
#user = User.new(user_params)
respond_to do |format|
if #user.save
# save their uploaded image
Image.create() # help needed here!
sign_in #user
format.html { redirect_to #user, notice: 'Welcome, ' + #user.user_name + '!' }
format.json { render action: 'show', status: :created, location: #user }
else
format.html { render action: 'new' }
format.json { render json: #user.errors, status: :unprocessable_entity }
end
end
end
private
# how do I create a method to require certain parameters for the Image?
def user_params
params.require(:user).permit(:email, :password, :password_confirmation, :profile_image)
end
Now I've actually done the hard bit of creating their image in the filesystem (via rmagick), the easy bit, simply updating the images table with information pertaining to this image, is where I'm struggling!
Also I know of nested forms, but I'm thinking the new image and the new user should both be created in the users#create action. It makes it a lot easier to see what's going on rather than if the image was created in its own image#create action. I was also thinking of putting the user creation and the image creation in a transaction block, but this is impossible if they were both happening in their own actions, so I do think the answer is splitting a form into two params hashes and doing something with the first hash, then doing something else with the second hash.
Use accepts_nested_attributes_for, it's a little hard to figure out at first, but it's exactly what you need in this case. Something like this:
user.rb:
class User < ActiveRecord::Base
has_one :profile_image, class_name: 'Image', foreign_key: 'user_id'
accepts_nested_attributes_for :profile_image
end
the view:
<%= f.fields_for :profile_image do |f| %>
<%= file_field :file %>
<% end %>
The controller seems to be all set.

How to make a form for three models with many-to-many relations

What's the best way to create a form for models with many-to-many relations?
In detail:
I have 3 models: User, Task, TaskAssignment:
User Model
class User < ActiveRecord::Base
has_many :task_assignments
has_many :tasks, through: :task_assignments
end
Task Model
class Task < ActiveRecord::Base
has_many :task_assignments
has_many :users, through: :task_assignments
end
TaskAssignment Model (Join Table)
I can't use has_and_belongs_to_many, because I need additional fields in the TaskAssignment Model.
class TaskAssignment < ActiveRecord::Base
belongs_to :task
belongs_to :user
end
By creating a new task, there should be the possibility to assign multiple users to a task, so I made this form view:
Task Edit Form View
<%= form_for(#task) do |f| %>
<div class="field">
<%= f.label :note %><br>
<%= f.text_field :note %>
</div>
<select name="task[users]" size="5" multiple>
<% #users.each do |user| %>
<option value="<%= user.id %>"><%= user.email %></option>
<% end %>
</select>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
Now, I wonder how to go on in my controller:
Task Controller
def create
#task = Task.new(task_params)
respond_to do |format|
if #task.save
format.html { redirect_to #task, notice: 'Task was successfully created.' }
format.json { render action: 'show', status: :created, location: #task }
else
format.html { render action: 'new' }
format.json { render json: #task.errors, status: :unprocessable_entity }
end
end
end
I think I have to do something like that:
#task = Task.new(task_params)
users = ???
#task.users << users
Is that the way n:m data should be saved or are there any other magic rails functions?
How do i get the data from the select-box? I tried to receive them by setting the name of the select-box to name="task[users]", but the variable task_params contains only the note-field
There's a helper called accepts_nested_attributes_for that allows a parent object to create and update its nested objects. In this case, you want Task to be able to create and update TaskAssignment
First, allow Task to accept attributes for its kids and to assign attributes to them.
class Task < ActiveRecord::Base
has_many :task_assignments
has_many :users, through: :task_assignments
accepts_nested_attributes_for :task_assignments
end
That should point you in the right direction. Creating the form will look something like explained in fields_for for one-to-many.
You don't need to save your object in any special way. Just remember to allow :user_ids => [] in your task_params.
Also, your life might be a bit easier with the form helper collection_select.
<%= f.collection_select :user_ids, #users, :id, :email, {}, { :multiple => true, :size => 5 } %>

Rails: Updating Nested attributes - undefined method `to_sym' for nil:NilClass

Edit: Added the update action, and on what line the error occurs
Model:
class Match < ActiveRecord::Base
has_and_belongs_to_many :teams
has_many :match_teams
has_many :teams, :through => :match_teams
accepts_nested_attributes_for :match_teams, :allow_destroy => true
end
Controller:
def new
#match = Match.new
#match_teams = 2.times do
#match.match_teams.build
end
respond_to do |format|
format.html # new.html.erb
format.json { render json: #match }
end
end
def update
#match = Match.find(params[:id])
respond_to do |format|
if #match.update_attributes(params[:match])
format.html { redirect_to #match, notice: 'Match was successfully updated.' }
format.json { head :ok }
else
format.html { render action: "edit" }
format.json { render json: #match.errors, status: :unprocessable_entity }
end
end
end
Nested model:
class MatchTeam < ActiveRecord::Base
belongs_to :match
belongs_to :team
end
Association:
class Team < ActiveRecord::Base
has_and_belongs_to_many :matches
end
View:
<%= form_for(#match) do |f| %>
<%= f.fields_for :match_teams, #match_teams do |builder| %>
<%= builder.collection_select :team_id, Team.all, :id, :name, :include_blank => true %>
<% end %>
<% unless #match.new_record? %>
<div class="field">
<%= f.label :winning_team_id %><br />
<%= f.collection_select :winning_team_id, #match.teams, :id, :representation %>
</div>
<% end %>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
Params:
Processing by MatchesController#update as HTML
Parameters: {"utf8"=>"Ô£ô", "authenticity_token"=>"QIJChzkYOPZ1hxbzTZS8H3AXc7i
BzkKv3Z5daRmlOsQ=", "match"=>{"match_teams_attributes"=>{"0"=>{"team_id"=>"1", "
id"=>""}, "1"=>{"team_id"=>"3", "id"=>""}}, "winning_team_id"=>"3"}, "commit"=>"
Update Match", "id"=>"2"}
Creating a new match with 2 teams work fine, the edit view also shows the correct values, but the update action gives me this error.
undefined method `to_sym' for nil:NilClass
app/controllers/matches_controller.rb:65:in `block in update'
line 65: if #match.update_attributes(params[:match])
I've figured it out. I read that a join table like MatchTeams doesn't need an ID. I'm guessing this is true when not doing any nested forms. I redid my migration removing the exclusion of the id column, and now everything works fine. Don't we all love this stupid errors? :)
Without seeing the offending to_sym in your code, just know that the thing it's attached to has not been defined properly. If this is a variable such as #var.to_sym, you most likely:
Haven't set #var at all
Set it but it's returning nil because there are no matches (e.g. #var = #project.companies.first but #project has no companies tied to it).
You are missing a relevant bit of data in your params. If your to_sym is relying on data submitted through the form, it won't work if the user leaves out the bit of data you're assuming. In this case, you should test first to see if the data was entered before running .to_sym on it.

Cannot get nested form with a has_one association to work

I have these models:
class User < ActiveRecord::Base
has_one :city
accepts_nested_attributes_for :city
end
class City < ActiveRecord::Base
belongs_to :user
end
This controller action:
def create
#user = User.new(params[:user])
respond_to do |format|
if #user.save
format.html { redirect_to(#user, :notice => 'User was successfully created.') }
format.xml { render :xml => #user, :status => :created, :location => #user }
else
format.html { render :action => "new" }
format.xml { render :xml => #user.errors, :status => :unprocessable_entity }
end
end
end
and this view:
<%= form_for :user,:url => users_path,:method => :post do |f| %>
<%= f.fields_for :city do |b| %>
<%= b.collection_select :id,City.all,:id,:name %>
<% end %>
<div class="field">
<%= f.label :name %><br />
<%= f.text_field :name %>
</div>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
I am trying to allow the user to select a city from the list of already added cities. I am trying to present him a select. The select part it works, but the generated html code for it, looks like this:
<select name="user[city][id]" id="user_city_id">
<option value="1">One</option>
<option value="2">Two</option>
</select>
Notice that it's name doesn't have attribute anywhere. So, when I try to save it, I get this error:
City(#37815120) expected, got ActiveSupport::HashWithIndifferentAccess(#32969916)
How can I fix this?
EDIT: there is some progress, I tried to change the fields_for to this:
<%= f.fields_for :city_attributes do |b| %>
<%= b.collection_select :id,City.all,:id,:name %>
<% end %>
and now, the html seems to generate correctly. But I get this error now:
Couldn't find City with ID=1 for User with ID=
I have no idea what to do next.
EDIT2: overriding the city_attributes= method seems to work:
def city_attributes=(attribs)
self.city = City.find(attribs[:id])
end
I don't know if it's the way to go, but it seems good.
Have a look at this question that seems similar to yours :
Rails 3: How does "accepts_nested_attributes_for" work?
Actually, since the Cities already exsit, I think there is no need for nested forms here.
Try Replacing
<%= f.fields_for :city_attributes do |b| %>
<%= b.collection_select :id,City.all,:id,:name %>
<% end %>
With
<%= f.collection_select :city, City.all,:id,:name %>
Updated afters comments
Could you change your relationship with (and update database scheme accordingly)
class User < ActiveRecord::Base
belongs_to :city
end
class City < ActiveRecord::Base
has_many :users
end
And then try using:
<%= f.collection_select :city_id, City.all,:id,:name %>
You could also do a
<%= f.collection_select :city_id, City.all, :id, :name %>
in your view and then add virtual attributes to your User model:
class User < ActiveRecord::Base
...
def city_id(c_id)
update_attribute(:city, City.find(c_id))
end
def city_id
city.id
end
end
This might not be very clean, since the associated City model is "saved" whenever assigning an ID to some_user.city_id. However, this solution keeps your controller and view nice and clean.
Note: you might also want to account for a blank ID being passed in to the setter method.
Try this
<%= f.select(:city_id, City.all.collect {|p| [ p.name, p.id ] }) %>

Resources