Strong paramters and nested attributes/forms giving me grief - ruby-on-rails

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/

Related

Rails Issue with submitting form when adding nested form

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|

Eager Load Depending on Type of Association in Ruby on Rails

I have a polymorphic association (belongs_to :resource, polymorphic: true) where resource can be a variety of different models. To simplify the question assume it can be either a Order or a Customer.
If it is a Order I'd like to preload the order, and preload the Address. If it is a customer I'd like to preload the Customer and preload the Location.
The code using these associations does something like:
<%- #issues.each do |issue| -%>
<%- case issue.resource -%>
<%- when Customer -%>
<%= issue.resource.name %> <%= issue.resource.location.name %>
<%- when Order -%>
<%= issue.resource.number %> <%= issue.resource.address.details %>
<%- end -%>
Currently my preload uses:
#issues.preload(:resource)
However I still see n-plus-one issues for loading the conditional associations:
SELECT "addresses".* WHERE "addresses"."order_id" = ...
SELECT "locations".* WHERE "locations"."customer_id" = ...
...
What's a good way to fix this? Is it possible to manually preload an association?
You can do that with the help of ActiveRecord::Associations::Preloader class. Here is the code:
#issues = Issue.all # Or whatever query
ActiveRecord::Associations::Preloader.new.preload(#issues.select { |i| i.resource_type == "Order" }, { resource: :address })
ActiveRecord::Associations::Preloader.new.preload(#issues.select { |i| i.resource_type == "Customer" }, { resource: :location })
You can use different approach when filtering the collection. For example, in my project I am using group_by
groups = sale_items.group_by(&:item_type)
groups.each do |type, items|
conditions = case type
when "Product" then :item
when "Service" then { item: { service: [:group] } }
end
ActiveRecord::Associations::Preloader.new.preload(items, conditions)
You can easily wrap this code in some helper class and use it in different parts of your app.
This is now working in Rails v6.0.0.rc1: https://github.com/rails/rails/pull/32655
You can do .includes(resource: [:address, :location])
You can break out your polymorphic association into individual associations. I have followed this and been extremely pleased at how it has simplified my applications.
class Issue
belongs_to :order
belongs_to :customer
# You should validate that one and only one of order and customer is present.
def resource
order || customer
end
end
Issue.preload(order: :address, customer: :location)
I have actually written a gem which wraps up this pattern so that the syntax becomes
class Issue
has_owner :order, :customer, as: :resource
end
and sets up the associations and validations appropriately. Unfortunately that implementation is not open or public. However, it is not difficult to do yourself.
You need to define associations in models like this:
class Issue < ActiveRecord::Base
belongs_to :resource, polymorphic: true
belongs_to :order, -> { includes(:issues).where(issues: { resource_type: 'Order' }) }, foreign_key: :resource_id
belongs_to :customer, -> { includes(:issues).where(issues: { resource_type: 'Customer' }) }, foreign_key: :resource_id
end
class Order < ActiveRecord::Base
belongs_to :address
has_many :issues, as: :resource
end
class Customer < ActiveRecord::Base
belongs_to :location
has_many :issues, as: :resource
end
Now you may do required preload:
Issue.includes(order: :address, customer: :location).all
In views you should use explicit relation name:
<%- #issues.each do |issue| -%>
<%- case issue.resource -%>
<%- when Customer -%>
<%= issue.customer.name %> <%= issue.customer.location.name %>
<%- when Order -%>
<%= issue.order.number %> <%= issue.order.address.details %>
<%- end -%>
That's all, no more n-plus-one queries.
I would like to share one of my query that i have used for conditional eager loading but not sure if this might help you, which i am not sure but its worth a try.
i have an address model, which is polymorphic to user and property.
So i just check the addressable_type manually and then call the appropriate query as shown below:-
after getting either user or property,i get the address to with eager loading required models
###record can be user or property instance
if #record.class.to_s == "Property"
Address.includes(:addressable=>[:dealers,:property_groups,:details]).where(:addressable_type=>"Property").joins(:property).where(properties:{:status=>"active"})
else if #record.class.to_s == "User"
Address.includes(:addressable=>[:pictures,:friends,:ratings,:interests]).where(:addressable_type=>"User").joins(:user).where(users:{is_guest:=>true})
end
The above query is a small snippet of actual query, but you can get an idea about how to use it for eager loading using joins because its a polymorphic table.
Hope it helps.
If you instantiate the associated object as the object in question, e.g. call it the variable #object or some such. Then the render should handle the determination of the correct view via the object's class. This is a Rails convention, i.e. rails' magic.
I personally hate it because it's so hard to debug the current scope of a bug without something like byebug or pry but I can attest that it does work, as we use it here at my employer to solve a similar problem.
Instead of faster via preloading, I think the speed issue is better solved through this method and rails caching.
I've come up with a viable solution for myself when I was stuck in this problem. What I followed was to iterate through each type of implementations and concatenate it into an array.
To start with it, we will first note down what attributes will be loaded for a particular type.
ATTRIBS = {
'Order' => [:address],
'Customer' => [:location]
}.freeze
AVAILABLE_TYPES = %w(Order Customer).freeze
The above lists out the associations to load eagerly for the available implementation types.
Now in our code, we will simply iterate through AVAILABLE_TYPES and then load the required associations.
issues = []
AVAILABLE_TYPES.each do |type|
issues += #issues.where(resource_type: type).includes(resource: ATTRIBS[type])
end
Through this, we have a managed way to preload the associations based on the type. If you've another type, just add it to the AVAILABLE_TYPES, and the attributes to ATTRIBS, and you'll be done.

Simple way in ruby to ignore error if object doesn't exist

I have two objects: Wine, Brand
Brand has_many :wines
Wine belongs_to :brand
How can I simplify the following code:
<%= #wine.brand.name if #wine.brand %>
I realize it's already very simple, but I have some different complexities in my code that make this cumbersome. What I'd like to do is something along the lines of:
<%= &#wine.brand.name %>
Where it basically ignores the error. In PHP you can do this, I just can't find a corollary for ruby.
You can use try method:
<%= #wine.brand.try(:name) %>
I'd rather do this as follows:
class Wine
def brand_name
brand.present? ? brand.name : ''
end
end
This keeps your view slightly cleaner:
<%= #wine.brand_name %>
You can use delegate:
class Wine < ActiveRecord::Base
belongs_to :brand
delegate :name, to: :brand, prefix: true, allow_nil: true
end
This creates a Wine#brand_name method, returning either the brand's name or nil if brand does not exist.

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) %>

HashWithIndifferentAccess when saving array of objects

Ok so I have these 2 classes
class Interface < ActiveRecord::Base
belongs_to :hardware
end
and
class Hardware < ActiveRecord::Base
has_many :interfaces
end
I have a form for a predefined #hardware.interfaces which includes array of Interfaces which is handled like below
<%= text_field_tag "hardware[interfaces][][name]",interface.name %>
<%= text_field_tag "hardware[interfaces][][ip]",interface.ip %>
Now I try to do...
#hardware = Hardware.find(params[:id])
#hardware.update_attributes(params[:hardware])
and it throws the error
Interface(#37298420) expected, got HashWithIndifferentAccess(#24204840)
Could someone clue me in on what's going on? and how to solve this problem?
update_attributes updates the model attributes.. and you are trying to update another model attributes (the Interface class)
you want to use nested form & accepts_nested_attributes_for - you can see how in this guide

Resources