I created a many to many relationship between work packages and tasks:
class WorkPackage < ActiveRecord::Base
has_and_belongs_to_many :tasks
end
class Task < ActiveRecord::Base
has_and_belongs_to_many :work_packages
end
def change
create_table :tasks_work_packages, id: false do |t|
t.belongs_to :work_package, index: true
t.belongs_to :task, index: true
end
end
And if I assign tasks to workpackages it works. But now I want the user to add tasks to a workpackage, what do I have to add to the controller and especially the form to achieve that?
My current solution doesn't work:
work_package_controller:
def work_package_params
params.require(:work_package).permit(:name, :price, tasks_attributes: [:id, :work_package_id, :task_id])
end
work_packages_form (3 different options so far):
<% 3.times do %>
<%= f.fields_for #work_package.tasks.build do |task_fields| %>
<%= task_fields.collection_select(:id, Task.all, :id, :name, {:include_blank => true }) %>
<% end %>
<% end %>
<% #work_package.tasks.each do |task| %>
<%= f.fields_for :tasks, task do |task_fields| %>
<%= task_fields.collection_select(:id, Task.all, :id, :name, {:include_blank => true }) %>
<% end %>
<% end %>
<%= select_tag("work_package[task_ids][]", options_for_select(Task.all.collect { |task| [task.name, task.id] }, #work_package.tasks.collect { |task| task.id}), {:multiple=>true, :size=>5}) %>
What am I missing?
Selecting associated records
If you want a user to be able to choose existing tasks you would use collection_select or collection_checkboxes.
Note that this has nothing to do with nested attributes! Don't confuse the two.
<%= form_for(#work_package) do |f| %>
<div>
<%= f.label :task_ids, "Tasks" %>
<%= f.collection_check_boxes :task_ids, Task.all, :id, :name %>
</div>
<% end %>
This creates a task_ids param which contains an array of ids.
When you use has_many or has_and_belongs_to_many ActiveRecord creates has a special relation_name_ids attribute. In this case task_ids. When you set task_ids and call .save on the model AR will add or remove rows from the tasks_work_packages table accordingly.
To whitelist the task_ids param use:
require(:work_package).permit(task_ids: [])
Nested attributes
You would use nested attributes if you want users to be able to create or modify a work package and the related tasks at the same time.
What fields_for does is create scoped inputs for a model relation. Which means that it loops through the associated records and creates inputs for each:
<% form_for(#work_package) do |f| %>
<%= f.fields_for(:tasks) do |f| %>
<div class="field">
<% f.string :name %>
</div>
<% end %>
<% end %>
fields_for will give us an array of hashes in params[:tasks_attributes].
One big gotcha here is that no fields for tasks will be shown for a new record and that you can't add new tasks from the edit action.
To solve this you need to seed the work package with tasks:
class WorkPackagesController < ApplicationController
def new
#work_package = WorkPackage.new
seed_tasks
end
def edit
seed_tasks
end
def create
# ...
if #work_package.save
# ...
else
seed_tasks
render :new
end
end
def update
# ...
if #work_package.save
# ...
else
seed_tasks
render :edit
end
end
private
# ...
def seed_tasks
3.times { #work_package.tasks.new }
end
end
To whitelist the nested attributes you would do:
params.require(:work_package).permit(tasks_attributes: [:name])
Conclusion
While these are very different tools that do separate things they are not exclusive. You would combine collection_checkboxes and fields_for/nested_attributes to create a form that allows the user to both select and create new tasks on the fly for example.
Related
I have models Software and Version. A Software has_many Version's and has_one :first_version
class Software < ApplicationRecord
has_many :versions
has_one :first_version, -> { order(created_at: :asc) },
class_name: "Version", dependent: :destroy
accepts_nested_attributes_for :versions
end
class Version < ApplicationRecord
belongs_to :software
end
I'm building the nested object in the new controller action.
class SoftwaresController < ApplicationController
def new
#software = current_account.softwares.build
#software.build_first_version
end
def create
#software = current_account.softwares.build(software_params)
if #software.save
redirect_to software_path(#software)
else
render :new
end
end
def software_params
params.require(:software).permit(
:name,
first_version_attributes: %i[id release_date],
)
end
end
form:
<%= simple_form_for :software do |f| %>
<%= f.input :name %>
<%= f.simple_fields_for :first_version do |v|%>
<%= v.input :release_date %>
<% end %>
<% end %>
With the above code, if something fails during creation, the nested object is persisted even though the object itself and it's parent do not have an id yet, and so errors are displayed under each field with invalid values.
At the same time, if I comment out the line where I build the nested object, the form does not break, just no nested fields are displayed. This is good.
Now, because the form is reused in the new and edit views and I don't want to let users edit the :first_version through this form nor rely on the view to render it conditionally if #software.new_record? I put the nested object in a global variable and point the nested form to that variable hoping that the same result will be achieved in the edit view because no global variable will exist.
def new
#software = current_account.softwares.build
#first_version = #software.build_first_version
end
form:
<%= simple_form_for :software do |f| %>
<%= f.input :name %>
<%= f.simple_fields_for #first_version do |v|%>
<%= v.input :release_date %>
<% end %>
<% end %>
Problem:
If something goes wrong during creation the object is no longer persisted and the view breaks due to #first_version being nil. So why is the nested object persisted when I do #parent.build_nested_object but not when #nested_object = #parent.build_nested_object ?
Solving the problem by creating more i_vars can lead to bugs. I think the best option is to disable the field based on a condition and change your view to the following.
<%= simple_form_for #software do |f| %>
<%= f.input :name %>
<%= f.simple_fields_for #software.first_version || #software.build_first_version do |v| %>
<%= v.input :release_date, disabled: (true if #software.first_version.id) %>
<% end %>
<% end %>
Using this view means that you can initialize only #software on your controller.
class SoftwaresController < ApplicationController
def new
#software = current_account.softwares.build
end
end
I have a simple bids model that embeds a supplier:
class Bid
include Mongoid::Document
field :amount, type: Integer
embeds_one :supplier
accepts_nested_attributes_for :supplier
end
Many bids have the same supplier. If I was using a scaffolded edit view, how would I update all of the instances of the embedded supplier in bids in the update method?
I have tried, unsuccessfully, something like
def update
#supplier.update(supplier_params)
#or
#bids = Bid.where('supplier._id' => #supplier.id)
#bids.supplier.update_attributes!(supplier_params)
redirect_to #supplier
end
Since you are using nested attributes you should be able to update the nested record by passing supplier_attributes.
Bid.find(1).update(supplier_attributes: { foo: 'bar' } )
To whitelist nested params you use a hash option:
params.require(:bid).permit(supplier_attributes: [:foo])
To generate the form fields you use fields_for:
<%= form_for(#bid) do |f| %>
<%= f.fields_for(:supplier) do |s| %>
<%= s.label :foo do %>
<%= s.text_field :foo %>
<% end %>
<% end %>
<% end %>
I can't get my CheckIn record to save because the associated Tenancy isn't saving.
I have three models with associations:
class Property < ApplicationRecord
has_many :tenancies
end
class Tenancy < ApplicationRecord
belongs_to :property
has_many :check_ins
end
class CheckIn < ApplicationRecord
belongs_to :tenancy
accepts_nested_attributes_for :tenancy
end
I want the CheckIn new action to create both the CheckIn and the associated Tenancy:
def new
#check_in = CheckIn.new
#check_in.build_tenancy.property_id = params[:property_id]
end
I have to include the property_id part otherwise the Tenancy won't save.
The form in check_ins/new.html.erb:
<%= form_for #check_in, url: property_check_ins_path do |f| %>
<%= f.label :date_time %>
<%= f.datetime_select :date_time, {minute_step: 15} %>
<%= f.label :tenancy %>
<%= f.fields_for :tenancy do |i| %>
<%= i.date_select :start_date %>
<% end %>
<%= f.submit "Create Check In" %>
<% end %>
I've added tenancy attributes to the strong params in the CheckInsController:
def check_in_params
params.require(:check_in).permit(:tenancy_id, :date_time, tenancy_attributes: [:start_date])
end
It's worth noting that the check_ins routes are nested in properties:
resources :properties do
resources :check_ins, only: [:new, :create]
end
So the problem is that by the time I get to the create action in the CheckInsController, the tenancy that I built has disappeared. I'm not sure how and when each of the records should be being saved and the slight complexity of what I'm trying to achieve has made it quite difficult to find relevant help so any ideas?
I'm using Rails 5.
The problem was that the property attached to the tenancy was being forgotten. I removed the property attachment from the new action:
def new
#check_in = CheckIn.new
#check_in.build_tenancy
end
Added a hidden field for property_id to the form (as well as adding :property_id to the strong params):
<%= f.fields_for :tenancy do |i| %>
<%= i.date_select :start_date %>
<%= i.hidden_field :property_id, value: params[:property_id] %>
<% end %>
And saved the tenancy in the CheckIn create action, prior to saving the check in itself:
def create
#check_in = CheckIn.new(check_in_params)
#check_in.tenancy.save
if #check_in.save
redirect_to property_check_in_path(#check_in.tenancy.property.id, #check_in)
else
render :new
end
end
I'd certainly be interested if anyone could pick holes in this solution or offer a better one.
Using nested resources (check_ins depends from properties) you create a namespaces routes. form_for helper ( rails guides - form helpers ) when you build your form, need a Property reference also.
I try to explain me better with an example:
#checks_controller.rb
def new
#property = Property.new
#check_in = #property.build_check_ins
#check_in.build_tenancy
end
#check_ins/new.html.erb
<%= form_for [#property, #check_in], url: property_check_ins_path do |f| %>
<%= f.label :date_time %>
<%= f.datetime_select :date_time, {minute_step: 15} %>
<%= f.label :tenancy %>
<%= f.fields_for :tenancy do |i| %>
<%= i.date_select :start_date %>
<% end %>
<%= f.submit "Create Check In" %>
<% end %>
I haven't tried this code, but I hope this give you at least a way to follow to solve your problem.
When I edit a object, the relationship object values are not displayed in the edit form. The create on other hand is working.
Here are the models:
class LogFile < ActiveRecord::Base
has_one :config_file, dependent: :destroy
accepts_nested_attributes_for :config_file, allow_destroy: true
end
class ConfigFile < ActiveRecord::Base
belongs_to :log_file
end
and this is the controller:
# GET /log_files/1/edit
def edit
end
private
# Use callbacks to share common setup or constraints between actions.
def set_log_file
#log_file = LogFile.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def log_file_params
params.require(:log_file).permit(
:name,
:user_id,
config_file_attributes: [:id, :json, :_destroy]
)
end
The form looks like this:
<%= f.simple_fields_for :config_file_attributes do |n| %>
<%= n.input :json %>
<% end %>
I have firstly try to join or include the relationship model, but was not able to do it. Some of the folks said thar id in the permit() function do the trick, but nothing changes in my situation.
Could anyone advice what to try?
Also when I put the following code in the form template:
<%= debug #log_file %>
no details about the relationship model are returned.
This is the solution I have apply using the accepted answer:
<% if #log_file.id %>
<%= f.simple_fields_for :config_file do |n| %>
<%= n.input :json %>
<% end %>
<% else %>
<%= f.simple_fields_for :config_file_attributes do |n| %>
<%= n.input :json %>
<% end %>
<% end %>
Try using relation name without _attributes suffix:
<%= f.simple_fields_for :config_file do |n| %>
<%= n.input :json %>
<% end %>
I am using the following code to create a drop down in a view to associate a model with another one. This works but I have to prepopulate the tasks in the database. I'd like to be able to create tasks during the creation of a project instead of just selecting ones that are already there.
<%= f.label :task %>
<%= f.collection_select( :task_id,
Task.all,
:id, :name, {selected: #project.task_id, include_blank: false}) %>
From what I understand I'm going to have to do this in the controller as well, but a lot of the code I've seen is out of date and I want to do it the Rails 4 way. Thanks!
If you're creating a new project & trying to create tasks too, you'll need to use a nested model form (accepts_nested_attributes_for):
#app/models/Project.rb
def Project < ActiveRecord::Base
has_many :tasks
accepts_nested_attributes_for :tasks
end
#app/controllers/projects_controller.rb
def new
#project = Project.new
#project.tasks.build #-> do this for as many tasks as you want
end
def create
#project = Project.new(project_params)
#project.save
end
private
def project_params
params.require(:project).permit(:new, :project, :attrs, tasks_attributes: [:task_name])
end
#app/views/projects/new.html.erb
<%= form_for #project do |f| %>
<%= f.text_field :name %>
<%= f.fields_for :tasks do |t| %>
<%= t.text_field :name %>
<% end %>
<% end %>