How to preserve Id's of associated models with nested forms for has_many Ecto associations? - phoenix-framework

I couldn't find info about updating models with has_many associations using nested forms with dinamically added associated model's fields, all I could find was about inserting new models.
But when we update the parent model we sometimes need to take care about preserving Id's of associated children models.
Given we have two models, having has_many relationship like this:
parent.ex
defmodule MyApp.Parent do
use MyApp.Web :model
schema "parents" do
field :name, :string
has_many :children, MyApp.Child
timestamps
end
def changeset(parent, params \\ %{}) do
parent
|> cast(params, [:name])
|> validate_required([:name])
|> cast_assoc(:children)
end
end
child.ex
defmodule MyApp.Child do
use MyApp.Web :model
schema "children" do
field :name, :string
belongs_to :parent, MyApp.Parent
timestamps
end
def changeset(child, params \\ %{}) do
child
|> cast(params, [:name])
|> validate_required([:name])
end
end
And nested form for the Parent with inputs_for for it's children like this:
form.html.eex
<%= form_for #changeset, #action, fn f -> %>
<%= if #changeset.action do %>
<div class="alert alert-danger">
<p>Oops, something went wrong! Please check the errors below.</p>
</div>
<% end %>
<div class="form-group">
<%= label f, :name, class: "control-label" %>
<%= text_input f, :name, class: "form-control" %>
<%= error_tag f.source, :name %>
</div>
<%= inputs_for f, :children, [append: [MyApp.Child.changeset(%Child{})]], fn fc -> %>
<div class="form-group">
<%= label fc, :name, class: "control-label" %>
<%= text_input fc, :name, class: "form-control" %>
<%= error_tag fc.source, :name %>
</div>
<div class="form-group">
<%= checkbox fc, :delete, class: "delete-checkbox" %>
</div>
<% end %> <!-- end the nested children -->
<%= submit "Submit" %>
<% end %>
And with some javascript help it also is allowed to add more children to the form.
The question:
If I want to preserve children Id's on parent's update action do I need to add hidden Id's form fields to all existing children and add them to the allowed fields to cast function call in MyApp.Child.changeset? Or is there another way to force Ecto to update old children instead of removing old ones and creating new with new Id's?

Yes, you do need to add hidden fields for the IDs or it will be treated as an insert operation. If you send in the ID it will be an update.
You do NOT need to add the :id field to the cast in the child's changeset.

Related

Rails 5: Is Nested forms set up correct

If I want a Nested Form for volunteer_shift
inside my assignment/_form
How should my model associations be set up?
My current associations look like this..but I am not sure they are correct if the volunteer_shift partial sits inside the assignment form....
When I try to open the "add new record" form...my assignment#new action errors out...
error message
Completed 500 Internal Server Error in 30ms (ActiveRecord: 2.7ms)
NoMethodError - undefined method `build' for nil:NilClass:
app/controllers/assignments_controller.rb:21:in `new'
Here is my new action...
# GET /assignments/new
def new
#assignment = Assignment.new
# binding.pry
#assignment.volunteer_event.build <----Line 21
#my_url = {:action => "create", :id => params[:id]}
end
Here are my current models associations...
class Assignment < ApplicationRecord
attr_accessor :volunteer_event
belongs_to :volunteer_shift
has_one :volunteer_task_type, :through => :volunteer_shift, :source => :volunteer_task_type
belongs_to :contact ,optional: true
validates_presence_of :volunteer_shift #belongs_to takes care of this now
validates_associated :volunteer_shift
accepts_nested_attributes_for :volunteer_shift, allow_destroy: true
...
class VolunteerShift < ApplicationRecord
has_many :assignments
belongs_to :volunteer_event
...
class VolunteerEvent < ApplicationRecord
belongs_to :volunteer_default_event
has_many :volunteer_shifts, :dependent => :destroy
has_many :resources_volunteer_events, :dependent => :destroy
validates_associated :volunteer_shifts
And inside my assignment/form
<%= form_for #assignment, :url => #my_url, remote: true do |f| %>
...
<!--VOLUNTEER SHIFT-->
<!--TODO: make this a partial under field_for-->
<%= f.fields_for :volunteer_shift do |builder| %>
<%= render 'volunteer_shift_fields', vs: builder %>
<% end %>
...
<% end %>
and if you want to see the volunteer_shift_fields partial
<div class="name large flex-row">
<%= vs.label :volunteer_shift %>
</div>
<div id="volunteer_shift" class="d-flex flex-row">
<div class="col-sm-12 p-2">
<div id="volunteer_shift" class="text-right">
<!-- old if: if class is assignment show volunteer shift else show default shift -->
<!-- we need default shift here...NO assignment is attached-->
<div class="field">
<%= vs.label :volunteer_task_type_id %>
<%= select_tag 'volunteer_task_type_id', options_from_collection_for_select([VolunteerTaskType.new(:description => ""), VolunteerTaskType.instantiables.effective_on(Date.today)].flatten, "id", "description") %>
</div>
<div class="field">
<%= vs.label :roster_id %>
<%= select_tag 'roster_id', options_from_collection_for_select([Roster.new(:name => ""), Roster.all].flatten, "id", "name") %>
</div>
<div class="field">
<%= vs.label :program_id %>
<%= select_tag 'program_id', options_from_collection_for_select([Program.new(:name => ""), Program.where(:volunteer => true)].flatten, "id", "name")%>
</div>
<div class="field">
<%= vs.label :set_description %>
<%= vs.text_field(:set_description, nil) %>
</div>
<div class="field">
<%= vs.label :set_date, "Date" %> <
<%= vs.text_field(:set_date, nil) %>
</div>
</div>
</div>
</div>
Are my model associations correct?
What is my "assignment#new" action missing?
Remove attr_accessor :volunteer_event, you need to set up the model association to volunteer_event through volunteer_shift
so
class Assignment < ApplicationRecord
attr_accessor :volunteer_event
needs to become
class Assignment < ApplicationRecord
has_one :volunteer_event, through: :volunteer_shift
Looking at your volunteer_shift associations I'm pretty sure that you only want to ever have a single volunteer_event record for a single assignment? if that's not what you want then you need to look at your associations again
Or...
You could change the attr_accessor to attr_reader and assign the relevant volunteer_event record in an initializer but I would not recommend that approach for this case

Inserting multiple rows in Rails for single form

There are three fields in this form:
employee
project
project
The field project comes up twice and so I want two records created in this case. If I put in the values:
employee: John Doe
project: Project_1
project: Project_2
I would like two records in the model:
employee: John Doe; project: Project_1
employee: John Doe; project: Project_2
This is the view:
<%= simple_form_for(#source) do |f| %>
<div class="form-group">
<%= f.label :employee %>
<%= f.text_field :employee, class: "form-control" %>
</div>
<div class="form-group">
<%= f.input :project, class: "form-control" %>
<%= f.input :project, class: "form-control" %>
</div>
<% end %>
Here is my code for the application controller:
def create
#source = Source.new(source_params)
if #source.save
redirect_to #source, notice: 'Source was successfully created.'
else
render action: 'new'
end
end
Any help will be greatly appreciated.
In your controller, you should build the model along with two projects, then use the form helper fields_for, which will render both project fields
your_controller.rb
class YourController
def your_action_new
#object = YourModel.new
2.times{ #object.projects.build }
end
end
For the view, I don't really know how simple_form behaves, but basically
views/your_views/new.html.erb
<!-- blabla -->
<%= f.fields_for :project do |project_f| %>
<div class="project">
<%= project_f.text_field(:name) %>
<%= project_f.text_field(:description) %>
...
</div>
<% end %>
ALso don't forget in to accept nested attributes
class YourModel
has_many :projects, dependent: :destroy
accept_nested_attributes_for :projects
end
class YourController
def your_model_params
params.require(:your_model).permit(blabla, projects_attributes: [:id, :name, :blabla, ...])
end
end
end

Nested attributes that do not belong to the current object using has_many :through

My end goal is to be able to add costumes to an agreement in the agreement view, regarless of whether or not they exist in the Costume database yet. My main difficulty is that a costume does not belong to an agreement, they exist independently but can be added to an agreement. If a new costume is added that isn't in the Costume database, it will add it to the database. Is there a way to do this? I can't find a tutorial about this anywhere. If I could get the controller from this post, I think that is all I need. I just need to create one costume every time the form is displayed. Thanks so much.
My models are as follows:
# app/models/agreement.rb
class Agreement < ActiveRecord::Base
has_and_belongs_to_many :costumes, join_table: :agreement_costumes
accepts_nested_attributes_for :costumes, :reject_if => :all_blank
end
# app/models/costume.rb
class Costume < ActiveRecord::Base
has_and_belongs_to_many :agreements, join_table: :agreement_costumes
end
# app/models/agreement_costume.rb
class AgreementCostume < ActiveRecord::Base
belongs_to :agreement
belongs_to :costume
end
My agreement controller is as follows:
class AgreementsController < ApplicationController
before_action :set_agreement, only: [:show, :edit, :update, :destroy]
# Some methods ommitted
# GET /agreements/new
def new
#agreement = Agreement.new
#agreement.costumes.build
#costumes = Costume.all
end
private
# Use callbacks to share common setup or constraints between actions.
def set_agreement
#agreement = Agreement.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def agreement_params
params.require(:agreement).permit(:name, :phone, :email, :mailbox, :wesid, :title, :start, :end, :due, :financer, :employee, :costumes_attributes => [:cid, :description, :wd, :back, :photo])
end
:end
And finally, the agreement view
<%= form_for(#agreement) do |f| %>
<% if #agreement.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(#agreement.errors.count, "error") %> prohibited this agreement from being saved:</h2>
<ul>
<% #agreement.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<!-- Agreement fields omitted -->
<%= f.fields_for :costumes do |c| %>
<div class="field">
<%= c.label :cid %><br>
<%= c.number_field :cid %>
</div>
<div class="field">
<%= c.label :description %><br>
<%= c.text_field :description %>
</div>
<!-- etc with costume fields -->
<% end %>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
You need accepts_nested_attributes_for in your Agreement controller if you want to create new costumes there.
class Agreement < ActiveRecord::Base
has_many :agreement_costumes
has_many :costumes, through: :agreement_costumes
accepts_nested_attributes_for :costumes, :reject_if => :all_blank, :allow_destroy => :false,
end
and then in the agreements#new action in your Agreements controller you build a costume entry
def new
#agreement = Agreement.new
#agreement.costumes.build
#costumes = Costumes.all
end
#agreement.costumes.build creates a blank instance of a costume, related to this agreement. You then access the params of that costume in the form using :costumes Don't forget to whitelist your nested params in your Agreements controller:
def agreement_params
params.require(:agreement).permit(:name, :phone, :email, :mailbox, :wesid,
:title, :start, :end, :due, :financer, :employee, costumes_attributes[:name, :price, :id])
end
Now your form has to have a place to choose existing costumes from a list and/or add a new one.
<%= f.label :costumes, "Costumes" %>
<%= f.collection_select :costumes, :agreement_id, :id, :name, price, {}, {multiple: true} %>
<strong>Add a new costume</strong>
<%= f.fields_for :costumes do |c|
<%= c.label :name %>
<%= c.text_field :name %>
<br>
<%= c.label :price %>
<%= c.number_field :price %>
<br>
<% end %>
This should get you most of the way there. I've had to guess at some of your code, so this isn't going to be a cut and paste answer. You'll need to build what you can off of this and probably do a little more Googling here and there. If you wanted to have a popup form to add a costume to the list on the fly and then be able to choose it in the collection_select, you would have to turn on Turbo_links in your app, create a Javascript popup form. Then use AJAX to submit the form, save the costume to the database, run another .js.erb file that would then update the collection_select text list using a reload of just that list via your Javascript. It's actually probably easier than having a new costume form nested in this form.

Save record in rails 4.0 using params hash

I'm trying to create a new record in rails using the form_for helper method. I think the params hash is blank because I keep getting blank errors when I submit the form.
This is my form:
<% provide(:title, "Add Department") %>
<h1>Add Department</h1>
<div class="row">
<div class="span6 offset3">
<%= form_for(#department) do |f| %>
<%= render 'shared/department_error_messages' %>
<%= f.label :Full_Department_Title %>
<%= f.text_field :full_name %>
<%= f.label :Department_Abbreviation %>
<%= f.text_field :abbreviation %>
<%= f.submit "Add department", class: "btn btn-large btn-primary" %>
<% end %>
</div>
</div>
This is my departments controller
class DepartmentsController < ApplicationController
def show
#department = Department.find(params[:id])
end
def new
#department = Department.new
end
def create
#department = Department.new(params[department_params]) # Not the final implementation!
if #department.save
redirect_to root_path
else
render 'new'
end
end
private
def department_params
# This says that params[:department] is required, but inside that, only params[:department][:full_name] and
# params[:department][:abbreviation] are permitted. Unpermitted params will be stripped out
params.require(:department).permit(:full_name, :abbreviation)
end
end
This is the Model:
class Department < ActiveRecord::Base
validates :full_name, presence: true, length: { minimum: 6 }
end
When I submit, errors are rendered saying the full_name can't be blank (and the fields are now empty). The debug info is:
--- !ruby/hash:ActionController::Parameters utf8: ✓
authenticity_token: EjfYjWAzaw7YqVZAkCPZwiEMFfb2YLIRrHbH1CpZOwA=
department: !ruby/hash:ActionController::Parameters
full_name: Sports Department
abbreviation: SD
commit: Add department
action: create
controller: departments
I've also checked the development log. The transaction starts and is then rolled back. I can save a record in the console so I'm guessing its something to do with the params hash, but can't figure it out. Please help
This line:
#department = Department.new(params[department_params])
Should be:
#department = Department.new(department_params)
And, as a minor issue (but not causing this problem), your label tags should look like:
f.label :full_name, "Full Department Title"
This way they are correctly associated with the input.

Rails 3 Nested Form not being created

The models I'm working with look like this:
class ComplexAssertion < ActiveRecord::Base
has_many :expression_groups
has_many :expressions, :through => :expression_group
accepts_nested_attributes_for :expression_groups, :allow_destroy=>true
end
class ExpressionGroup < ActiveRecord::Base
belongs_to :complex_assertion
has_many :expressions
accepts_nested_attributes_for :expressions, :allow_destroy=>true
end
class Expression < ActiveRecord::Base
belongs_to :expression_group
end
My form looks like the following:
<%= form_for(#complex_assertion) do |f| %>
<div id="mainAssertionGroup" style="border:1px; border-style:solid; width:1000px; padding:5px">
<div class="field">
<%= f.label :title %>: <%= f.text_field :title, :size=>'10' %>
<%= f.label :description %>: <%= f.text_field :description, :size=>'25' %>
<%= f.label :scope %>: <%= f.text_field :scope, :size=>'1' %>
Test
Category: <%= collection_select(:complex_assertion, :assertion_category_id, AssertionCategory.all, :id, :name, {:include_blank=>"UNCATEGORIZED"}) %>
</div>
<div id="initialGroup" style="border:1px; margin-left:10px; margin-top:10px; border-style:solid; width:850px;">
<div class="childGroup1" style="padding:5px;">
<%= f.fields_for :expression_groups do |eg| %>
<%= eg.fields_for :expressions do |e| %>
Type: <%= e.collection_select :assertion_type_id, AssertionType.all, :id, :name %>
Attribute: <%= e.collection_select :attribute_name, Attribute.find_by_sql("select distinct a.name from attributes a "), :name, :name %>
<%= e.label :operator_type_id %>
: <%= e.collection_select :operator_type_id, OperatorType.all, :id, :value %>
Value: <%= e.text_field :value, :size=>'1' %>
<% end %>
<div id="innerOperator">
<%= eg.collection_select :logical_operator_type_id, LogicalOperatorType.all, :id, :value %>
</div>
<% end %>
</div>
</div>
</div>
<div id="createComplex" align="center">
<%= f.submit :value=>'Submit' %>
</div>
<% end %>
And my controller looks like:
def new_complex_assertion
#complex_assertion = ComplexAssertion.new
end
When I load the page, I only the ComplexAssertion portion of the form and get nothing back for the ExpressionGroups or the Expressions. It's as if there isn't anything available. But, if you see my controller, I did a ComplexAssertion.new which I though would create the dependent objects automagically; I assume I'm incorrect?
I'm debugging through RubyMine and when I evaluate the ComplexAssertion.new, I only see 5 attributes, the five that are defined for only that object, none of the relational objects. What am I doing incorrectly?
EDIT
Looks like if I do the following:
#complex_assertion = ComplexAssertion.new
#complex_assertion.expression_groups.build
#complex_assertion.expressions.build
And change my form to use:
<%= f.fields_for :expressions do |e| %>
instead of eg.fields_for, it shows the forms.
This DOES NOT give me the correct nesting. I thought I should be able to do:
#complex_assertion.expression_groups.expressions.build
but it tells me that expressions is an undefined method.
Yes, you have to explicitly instantiate the associated objects. It is not done for you.
#complex_assertion.expression_groups.expressions.build
Will not work because expression_groups is an array and not an individual expression group. So, after you create the expressions_groups do the following:
#complex_assertion.expressions_groups.each do |group|
group.expressions.build
end
Also, you could replace the 2nd line with the following as well to create multiple expressions
2.times do { group.expressions.build }
As for using fields_for with nested models, make your code in the form look like this:
<%= f.fields_for :expression_groups, #complex_assertions.expression groups do |eg| %>
<%= eg.fields_for :expressions, eg.object.expressions do |e| %>
I will try to explain what is going on. The :expressions_groups is telling fields_for what class of object it is going to render fields for, and the second part I added is telling fields_for where to find the object(s) to render fields for. If we are passing in an array, which we are in this case, it will automatically iterate over the array. On each iteration, it puts the current model object we are working with into a variable called object which is stored in the form builder instance returned by fields_for. So we use this to tell the second fields_for where to find the expression model objects it needed. This means eg.object points to an expression_group model object.
I hope this helps and makes sense. Also, I have not tested anything and am only pointing out what looks out of place.

Resources