Rails Issue with submitting form when adding nested form - ruby-on-rails

A Rails newbie here. I believe I am rather confused as to how fields_for operates within a rails form. I have a has many through relationship formed with Projects, Organizations, and Affiliations in the middle. A Project can have many Organizations, and an Organization can have many Projects. Affiliations is in the middle with a :role attribute that is meant to be edited to show what role an Organization plays within a Project. I was able to get the functionality of adding many Organizations to a Project working just fine. I utilized chosen-rails to implement a neat UI to select multiple, search, etc. I can add and remove Organizations to a Project just fine with that. However, upon trying to implement some way to manipulate the role each Organization plays in a Project through altering the role attribute with a fields_for form, submissions of the original form break. I have tried many variations of the fields_form, including one from a guide here.
In project.rb:
def affiliations_attributes=(affiliations_attributes)
affiliations_attributes.each do |i, affiliation_attributes|
if affiliation_attributes[:role].length > 0
self.affiliations.build(affiliation_attributes)
end
end
end
In the project form:
<%= f.fields_for :affiliations do |affiliation_builder|%>
<%= affiliation_builder.label :role %>
<%= affiliation_builder.text_field :role %>
<% end %>
With this setup, on submission of the form, I receive an error stating that 'Affiliations organization must exist.' I don't understand why, since I'm not even editing the Affiliations or Organization here. Is there something I am missing?
Prior to this, I tried doing this for a fields_for form:
<%= f.fields_for :affiliations do |affiliation|%>
<%= affiliation.label :role, ("Role in Project") %>
<%= affiliation.text_field :role, autofocus: true, autocomplete: 'off', placeholder: 'e.g. Donor, Founder', class: 'form-control' %>
<% end %>
Projects Controller:
def project_params
params.require(:project).permit(:organization, {organization_ids: []},
:stream_name, :implementation_date, :narrative,
:length, :primary_contact,:number_of_structures, :structure_description,
:name, affiliations_attributes: [:id, :role], photos: [])
end
Now, surprisingly, this works when updating the role attribute in Affiliations. I can also add Organizations to the project with no issue, and add subsequent roles to the created Affiliation. However, the problem arises when I try to remove an Organization. Rails flashes an ActiveRecord::RecordNotFound in ProjectsController#update error stating "Couldn't find Affiliation with ID= for Project with ID=". I have no idea what is going on here and have been banging my head against the wall for a couple days now trying to fix this. The only thing I can think is somehow there's an ID matching issue, or some parameter not being properly passed. One thing I did notice when toying around with the project_params on the second method above and removing :id from affiliations_attributes is that I get flashed with the same error message about an 'Affiliations organization' being required. Perhaps there's some way to pass an organization ID with the affiliation_builder method? Any help or guidance would be greatly appreciated here. Thank you.
class Affiliation < ApplicationRecord
belongs_to :project
belongs_to :organization
end
class Organization < ApplicationRecord
has_many :affiliations
has_many :projects, through: :affiliations
end
class Project < ApplicationRecord
has_many :affiliations
has_many :organizations, through: :affiliations
accepts_nested_attributes_for :affiliations
def affiliations_attributes=(affiliations_attributes)
affiliations_attributes.each do |i, affiliation_attributes|
if affiliation_attributes[:role].length > 0
self.affiliations.build(affiliation_attributes)
end
end
end
end

I believe I have found a solution, though I am not sure if it is technically correct. I looked at the order of the attributes being pushed when Project was updated, and I noticed that organization_ids was being pushed in the Hash before affiliations_attributes. So I think it was trying to update something that just didn't exist, and was giving some misleading information (at least to me). I was able to fix this issue by forcing the affiliations_attributes to be pushed first in a separate update by itself, and then pushing the rest of the project attributes in the main update.
def project_organization_params
params.require(:project).permit(:id, {affiliations_attributes: [:id,
:role]})
end
And then update with:
#project = Project.find(params[:id])
#project.update(project_organization_params)
if #project.update(project_params)
...
Everything seems to be working just fine now.

Why |i, affiliation_attributes| ?
use |affiliation_attributes|

Related

Issue with nested forms and posting simultaneously to two associated tables

So I have tried now to do this for multiple hours pulling from different sources. I'm sure I can probably hack this together in a very ugly way with multiple creates, but im sure there is a simple way to do this.
im trying to create a simple time and task tracker. I have a time tracker table that records the start and end times of a task and the task id.
There is an associated table that contains the title and details of the task.
When I try to .save! it
it abandons the save with ActiveRecord::RecordInvalid (Validation failed: Task must exist):
the logic seems to be that it should first try to create the task details (title and details) which creates the ID, and then it should create the timerecord(timetracker) record and include the task id in the task_id column of the time record. But based on the error, guess this is not happening.
I appreciate any help getting this working or point me in the right direction of what im doing incorrectly.
##########
Edit After posting, I realized that a task can have multiple time records, since a user can stop and restart on the same task that creates a new record with the same task ID. I've changed the task model from has_one to has_many.
###########
Here are the two models
class Timerecord < ApplicationRecord
belongs_to :task
end
class Task < ApplicationRecord
#has_one :timerecord #this has been changed to
has_many :timerecord
accepts_nested_attributes_for :timerecord
end
in my Timetracker controller #new & #create & private
def new
#timetracker = Timerecord.new
#timetracker.build_task
end
def create
#timetracker = Timerecord.new(timetracker_params)
if #timetracker.save
flash[:notice] = "Time Tracker Started"
redirect_to timetracker_index_path
end
end
private #at the tail of permit you will see including the task attribute title
def timetracker_params
params.require(:timerecord).permit(:user_id, :agency_id, :client_id, :task_id, :is_billable, :time_start, :time_end, :manual_input_hours, :timerecordgroup_id, :is_paused, :service_type_id, task_attributes: [:title])
end
and the form
<%= form_with(model: #timetracker, url: timetracker_index_path, local: true) do |f| %>
... a bunch of fields for #timetracker
<%#= fields_for :task, #timetracker.task do |t| %>
<%#= t.text_field :title, class: "form-control shadow-sm", placeholder: "Title" %>
<%# end %>
... more fields for timetracker
<% end %>
First of all you have to know which model is nested within the other and which one you're going to call the save method on.
Since your models are:
class Timerecord < ApplicationRecord
belongs_to :task
end
class Task < ApplicationRecord
#has_one :timerecord #this has been changed to
has_many :timerecord
accepts_nested_attributes_for :timerecord
end
You should call save on the Task model (which will also save timerecord) and not on the TimeRecord model because your Task model is the one accepting the nested attributes for TimeRecord model and not the other way around.
Your controller should look something like this:
def create
#timetracker = Task.new(task_params)
if #timetracker.save
flash[:notice] = "Time Tracker Started"
redirect_to timetracker_index_path
end
end
private
def task_params
params.require(:task).permit(#your permitted params for task, timetracker_attributes: [:title])
end
if you notice, your Task model accepts nested attributes for timetracker, hence you should permit timetracker_attributes and not task_attributes.
It's easy to know which nested params you should permit, just look at your models and see which model the accept_nested_attributes_for is refering to and then just add _attributes at the end.
Check out the accept_nested_attributes_for documentation, it should help
https://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
Not an answer as such, just a few suggestions to point you in the right direction as there are quite a few things wrong with your implementation.
you don't need accepts_nested_attributes_for :timerecord
in task, as you aren't creating timerecords at the time you are creating/editing the task.
you don't need to require the nested attributes for task in the TimerecordController
the form should have a hidden_field for the :task_id - this is primarily why you are getting the error.
but you don't have an associated task for the timerecord so in addition you need to assign it at the point you generate a new timerecord. Your TimerecordController new action should be something like:
def new
#timetracker = Timerecord.new
#timetracker.task_id = params[:task_id]
end
which means you also need to ensure you are sending task_id to the new action, for example new_timerecord_path(task_id: my_task_instance.id)

Strong paramters and nested attributes/forms giving me grief

So I am having some problems with strong parameters and nested forms (suprise! Nobody's ever had that problem before lol) and I've been looking at several threads, trying different solutions but still can't get a nested attribute to work so I turn to you, fellow programmers. Firstly lets look at the code, shall we? If you find anything fishy, let me know!
Aventure.rb
class Adventure < ActiveRecord::Base
belongs_to :user
has_many :photos
has_many :reservations
has_many :activity_dates
accepts_nested_attributes_for :activity_dates
...
end
Activity_date.rb
class ActivityDate < ActiveRecord::Base
belongs_to :adventure
has_many :time_slots
accepts_nested_attributes_for :time_slots
end
Timeslot.rb
class TimeSlot < ActiveRecord::Base
belongs_to :activity_date
end
_form.hmtl.erb
<%= form_for #adventure, html: {multipart: true} do |f| %>
...
<%= f.fields_for :activity_dates, #adventure.activity_dates.new do |a| %>
<%= a.date_field :date %>
<%= a.hidden_field :adventure_id, value: #adventure.id %>
<% end %>
adventure_controller.erb
def adventure_params
params.require(:adventure).permit(:activity_name,:description,:leader,
:company_name,:adress,:max_perticipants,
:price,:currency,{:activity_dates=>[:dates]})
end
Right, so when I inspect the params like below I get these
hashes (see img link):
render json: { p: params.inspect, ad:adventure_params.inspect }
I have concluded that activity_dates shows up in params, but NOT in ad:adventure_params. Using params[:activity_dates] gives a ForbiddenAttributesError, which was expected. That's not a good way of going about it, as it is not permitted in the strong params. I would however like to get :activity_dates with it's attribute date, and later on even it's nested attribute for :timeslots. But no matter how many solutions I have looked at, I have not been getting the desired results. What am I doing wrong? Help me Obi-Wan, you are my only hope!
For nested attributes you need to add '_attributes' to the end of field name when you are adding them to your strong parameters, so you need to permit activity_dates as activity_dates_attributes as follows:
params.require(:adventure).permit(:activity_dates_attributes=>[:dates])
or as follows with your other permitted parameters:
params.require(:adventure).permit(:activity_name,:description,:leader,
:company_name,:adress,:max_perticipants,
:price,:currency, :activity_dates_attributes=>[:dates])
For more information on whitelisting strong parameters here are some useful links here: http://edgeapi.rubyonrails.org/classes/ActionController/StrongParameters.html
https://github.com/rails/strong_parameters/

Can't mass-assign protected attributes for creating a has_many nested model with Devise

I've watched the RailsCast, another nested attributes video, lots of SO posts, and fought with this for a while, but I still can't figure it out. I hope it's something tiny.
I have two models, User (created by Devise), and Locker (aka, a product wishlist), and I'm trying to create a Locker for a User when they sign up. My login form has a field for the name of their new Locker (aptly called :name) that I'm trying to assign to the locker that gets created upon new user registration. All I'm ever greeted with is:
WARNING: Can't mass-assign protected attributes: locker
I've tried every combination of accepts_nested_attributes and attr_accesible in both of my models, yet still nothing works. I can see from the logs that it's being processed by the Devise#create method, and I know Devise isn't smart enough to create my models how I want :)
Here's the relevant bits of my two models:
# user.rb
class User < ActiveRecord::Base
attr_accessible :username, :email, :password, :password_confirmation, :remember_me, :locker_attributes
# Associations
has_many :lockers
has_many :lockups, :through => :lockers
# Model nesting access
accepts_nested_attributes_for :lockers
end
and
# locker.rb
class Locker < ActiveRecord::Base
belongs_to :user
has_many :lockups
has_many :products, :through => :lockups
attr_accessible :name, :description
end
# lockers_controller.rb (create)
#locker = current_user.lockers.build(params[:locker])
#locker.save
I'm assuming I need to override Devise's create method to somehow get this to work, but I'm quite new to rails and am getting used to the black box "magic" nature of it all.
If anyone can help me out, I'd be incredibly thankful. Already spent too much time on this as it is :)
EDIT: I realized I omitted something in my problem. My Locker model has three attributes - name, description (not mandatory), and user_id to link it back to the User. My signup form only requires the name, so I'm not looping through all the attributes in my nested form. Could that have something to do with my issue too?
EDIT 2: I also figured out how to override Devise's RegistrationsController#create method, I just don't know what to put there. Devise's whole resource thing doesn't make sense to me, and browsing their source code for the RegistrationsController didn't help me much either.
And for bonus points: When a user submits the login form with invalid data, the Locker field always comes back blank, while the regular Devise fields, username & email, are filled in. Could this also be fixed easily? If so, how?
first, you have a typo :
attr_accessible :locker_attributes
should be plural :
attr_accessible :lockers_attributes
then, the standard way to use nested_attributes is :
<%= form_for #user do |f| %>
<%# fields_for will iterate over all user.lockers and
build fields for each one of them using the block below,
with html name attributes like user[lockers_attributes][0][name].
it will also generate a hidden field user[lockers_attributes][0][id]
if the locker is already persisted, which allows nested_attributes
to know if the locker already exists of if it must create a new one
%>
<% f.fields_for :lockers do |locker_fields| %>
<%= locker_fields.label :name %>
<%= locker_fields.text_field :name %>
<% end %>
<% end %>
and in you controller :
def new
#user = User.new
#user.lockers.build
end
def create
# no need to use build here : params[:user] contains a
# :lockers_attributes key, which has an array of lockers attributes as value ;
# it gets assigned to the user using user.lockers_attributes=,
# a method created by nested_attributes
#user = User.new( params[:user] )
end
as a side note, you can avoid building a new locker for new users in controller in different ways:
create a factory method on User, or override new, or use an after_initialize callback to ensure every new user instantiated gets a locker builded automatically
pass a specific object to fields_for :
<% f.fields_for :lockers, f.object.lockers.new do |new_locker_fields| %>
Someone helped me figure out the solution in a more "Rails 4'y" way with strong attributes & how to override Devise's sign_up_params (to catch all the data coming from my signup form).
def sign_up_params
params.require(:user).permit(:username, :email, :password, :lockers_attributes)
end
Gemfile addition: gem 'strong_parameters'
Commenting out the attr_accessible statement in my user.rb file, since apparently strong parameters eliminate the need for attr_accessible declarations.
# attr_accessible :username, :email, :password, :password_confirmation, :lockers
And the/a correct way of building a Locker before submitting the form: at the beginning of the nested form:
<%= l.input :name, :required => true, label: "Locker name", :placeholder => "Name your first locker" %>
Thanks again for all your help. I know a question like this is difficult to answer without seeing the whole codebase.

No foreign key in new scaffold

Good day... I have huge trouble with this and it drives me insane.
My user creation should have some additional data, like this:
<div class="field"><%= label_tag 'species', %>
<%= f.collection_select :species_id, Species.all, :id, :name %></div>
It displays the list of species correctly, yes, but when I do a submit, it is utterly ignored. Not even the number appears in the table of the database, it just disappears. I have put the correct associations:
class Species < ActiveRecord::Base
has_many :users
end
class User < ActiveRecord::Base
# ... Other stuff
belongs_to :species
# ... Other stuff
end
I have also tried manipulating the controller:
class UsersController < ApplicationController
def create
logout_keeping_session!
#user = User.new(params[:user])
#user.species = Species.find(params[:species_id])
# Other stuff
end
end
But that only gives me 'Cannot find Species without id' even though the params array contains an element 'species_id' with a value.
I am at the end of my wits. Quite new to this, but is this RESTful? Not to find out how to get things done that seem easy? I love Rails and would like to continue.
Thanks for listening
your find fails because the params is probably: params[:user][:species_id] but if it is there like it is supposed, it should be set already, too.

Elegant way to process collection_select in controller?

I am currently somewhat stuck figuring out an elegant solution to my following
problem:
Let's say I have the following classes:
class Event < ActiveRecord::Base
belongs_to :reg_template, :class_name => "EmailTemplate"
[...]
end
class EmailTemplate < ActiveRecord::Base
has_many :events
[...]
end
And a view that contains:
<%= f.collection_select(:reg_template_id, EmailTemplate.all, :id, :name) %>
What is the recommended way of processing this form field in an action
controller?
Having a 1:1 relationship between Event and EmailTemplate means that Rails
does not generate a reg_template_id and reg_template_id= method (as it would
do for a 1:n relationship), so attempts to read or assign this field will fail
with
unknown attribute: reg_template_id
when attempting to call
Event.update_attributes
Using
<%= f.collection_select(:reg_template, EmailTemplate.all, :id, :name) %>
instead also does not help much as it will fail with:
EmailTemplate(#70070455907700) expected, got String(#70070510199800)
I guess I must be missing something terribly obvious as I think is is rather
common to update a model instance with a reference to another object through a
collection_select.
If you have the column reg_template_id in the events table, then the following code should work:
<%= f.collection_select(:reg_template_id, EmailTemplate.all, :id, :name) %>

Resources