I have two models Project and ProjectPipeline.
I want to create a Project form that also has fields from the ProjectPipeline model. I have created the form successfully but when I hit save the values aren't stored on the database.
project.rb
class Project < ActiveRecord::Base
has_one :project_pipeline
accepts_nested_attributes_for :project_pipeline
self.primary_key = :project_id
end
projectpipeline.rb
class ProjectPipeline < ActiveRecord::Base
belongs_to :project, autosave: :true
validates_uniqueness_of :project_id
end
I don't always want a project pipeline but under the right conditions based on a user viewing a project. I want the project pipeline fields to be built, but only saved if the user chooses to save/populate them.
So when the project is shown I build a temporary project pipeline using the project_id: from params[:id] (not sure if I really need to do this). Then when the project is saved I use the create_attributes. But if it has already been created or built I just want to let the has_one and belongs_to association kick in and then use update_attributes.
My issue is when I am trying to save, I am either hitting a 'Forbidden Attribute' error if I use params[:project_pipeline] or having nothing saved at all if I used project_params. I have checked and rechecked all my fields are in project_params and even tried using a project_pipeline_params but that didn't feel right.
It is driving me nuts and I need to sleep.
projects_controller.rb
def show
@project = Project.find(params[:id])
if @project.project_pipeline
else
@project.build_project_pipeline(project_id: params[:id])
end
autopopulate
end
def update
@project = Project.find(params[:id])
if @project.project_pipeline
else
@project.build_project_pipeline(project_id: params[:id], project_type: params[:project_pipeline][:project_type], project_stage: params[:project_pipeline][:project_stage])
end
if @project.update_attributes(project_params)
flash[:success] = "Project Updated"
redirect_to [@project]
else
render 'edit'
end
end
def project_params
params.require(:project).permit(:user_id, project_pipeline_attributes:[:project_id,:project_type,:project_stage,
:product_volume,:product_value,:project_status,:outcome, :_destroy])
end
show.html.haml
- provide(:title, "Show Project")
%h1= @project.project_title
= simple_form_for(@project) do |f|
= f.input :id, :as => :hidden, :value => @project, :readonly => true
= f.input :user_id, label: 'Assigned to Account Manager', :collection => @account_managers, :label_method => lambda { |r| "#{r.first_name} #{r.last_name}" }
= f.input :project_id, :readonly => true
= f.input :status, :readonly => true
= f.input :project_stage, :readonly => true
- if @project.project_codename = "project pipeline"
= simple_fields_for @project.project_pipeline do |i|
%h2 Project Pipeline
- if @project.user_id == current_user.id
= i.input :project_volume, label: 'Project Status', collection: @project_status
= i.input :project_value, label: 'Project Status', collection: @project_status
= i.input :project_status, label: 'Project Status', collection: @project_status
= i.input :outcome, label: 'Outcome', collection: @outcome
= f.submit 'Save'
If you've gotten this far I sincerely thank you.
I'm not sure from your description if this is the problem, but one the thing is that a update_attributes with a has_one, by default, will rebuild the children(!), so you would lose the attributes you initialised. You should provide de
update_only: true
option toaccepts_nested_attributes_for
. You can find more on this here, in the rails docs. The line would be this:Considering the after_initialize, that would result in every project always having a pipeline. While that could be desirable, it isn't necessarily, depending on your domain, so I'd be a bit careful with that.
Cheers, Niels
Solution
You need to change few things here. Firstly:
When you pass the object, rails have no idea it is to be associated with the parent object and as a result will create a field named
project[project_pipeline]
instead ofproject[project_pipeline_attributes]
. Instead you need to pass the association name and call this method on the form builder:This will check find out that you have defined
project_pipeline_attributes=
method (using accept_nested_attributes_for` and will treat it as association. Then in your controller change your show action to:And all should work. As a separate note, since you are allowing
:_destroy
attribute in nested params, I am assuming you want to be able to remove the record using nested attributes. If so, you need to addallow_destroy: true
to youraccepts_nested_attributes_for
call.Now a bit of styling:
You can improve your show action a bit. First of all, I've noticed you are building an empty pipeline in every single action if none has been declared yet. That mean that you probably should move this logic into your model:
You also have the mysterious method
prepopulate
- most likely it should be model concern as well.Another point: This syntax:
is somehow quite popular and makes the code unreadable as hell. Instead, use:
or (preferred)