Strong parameters for array of records - ruby-on-rails

Strong parameters has me very confused. I'm writing a form to create several records at once. They are passed in params as an array of attributes:
{ :appointments => [ { :field1 => 'value1'
, :field2 => 'value2'
}
, # next record
]
}
Then in the controller I would like to do something like
params[:appointments].each do |a|
app = Appointment.create! a
end
But I run into lots of trouble with strong parameters, in the form of ForbiddenAttributeErrors. I've tried using appointment_params and whitelisting attributes, but with no luck. I can't find any good documentation matching my use case. They all assume the array of records should be nested below some owner record but this is not the case here.
Any help would be appreciated.

Make sure you are white listing your array in addition to the actual model attributes.
It seems like you have used the scaffolded version of the params.require method and not have updated that method when you changed your controller to deal with an array of appointments rather than one appointment at a time.
Something like this should work:
params.require(:appointment).permit(:field1, :field2, appointments: [:field1, field2])
or
params.require(:appointments).permit(:field1, :field2)
Not sure exactly what the rest of yoru code looks like, but it seems like you're not permitting the array itself, the above code samples attempt to white list what I would assume that attribute might be named.

If you are only using the attributes to create a new Appointment record, you can use the following
Appointment.create!(params.permit(applications: [:field1, :field2])[:applications])
If you really want to iterate over the array, you can do
params[:appointments].each do |a|
app = Appointment.create!(a.permit(:field1, :field2))
end

Related

strong parameters permit all attributes for nested attributes

Is there a way in strong parameters to permit all attributes of a nested_attributes model? Here is a sample code.
class Lever < ActiveRecord::Base
has_one :lever_benefit
accepts_nested_attributes_for :lever_benefit
end
class LeverBenefit < ActiveRecord::Base
# == Schema Information
# id :integer not null, primary key
# lever_id :integer
# explanation :text
end
For lever strong parameters i am writing currently this
def lever
params.require(:lever).permit(:name,:lever_benefit_attributes => [:lever_id, :explanation])
end
Is there a way for nested attributes i can write to permit all attributes without explicitly giving the attributes name like lever_id and explanation ?
Note: Please don't get confused with this question with permit! or permit(:all) this is for permitting all for nested attributes
The only situation I have encountered where permitting arbitrary keys in a nested params hash seems reasonable to me is when writing to a serialized column. I've managed to handle it like this:
class Post
serialize :options, JSON
end
class PostsController < ApplicationController
...
def post_params
all_options = params.require(:post)[:options].try(:permit!)
params.require(:post).permit(:title).merge(:options => all_options)
end
end
try makes sure we do not require the presents of an :options key.
I am surprised at no one suggested this:
params.require(:lever).permit(:name,:lever_benefit_attributes => {})
Actually there is a way to just white-list all nested parameters.
params.require(:lever).permit(:name).tap do |whitelisted|
whitelisted[:lever_benefit_attributes ] = params[:lever][:lever_benefit_attributes ]
end
This method has advantage over other solutions. It allows to permit deep-nested parameters.
While other solutions like:
nested_keys = params.require(:lever).fetch(:lever_benefit_attributes, {}).keys
params.require(:lever).permit(:name,:lever_benefit_attributes => nested_keys)
Don't.
Source:
https://github.com/rails/rails/issues/9454#issuecomment-14167664
First, make sure that you really want to allow all values in a nested hash. Read through Damien MATHIEU's answer to understand the potential opening of security holes...
If you still need/want to allow all values in a hash (there are perfectly valid use cases for this, e.g. storing unstructured, user-provided metadata for a record), you can achieve it using the following bits of code:
def lever_params
nested_keys = params.require(:lever).fetch(:lever_benefit_attributes, {}).keys
params.require(:lever).permit(:name,:lever_benefit_attributes => nested_keys)
end
Note: This is very similar to tf.'s answer but a bit more elegant since you will not get any Unpermitted parameters: lever_benefit_attributes warnings/errors.
try
params.require(:lever).permit(:name, leave_benefit_attributes: LeaveBenefit.attribute_names.collect { |att| att.to_sym })
The whole point of strong parameters is in its name: make your input parameters strong.
Permitting all the parameters would be a very bad idea, as it would permit anyone to insert values you don't necessarily want to be updated by your users.
In the example you give, you mention the two parameters you currently need to provide:
[:lever_id, :explanation].
If you permitted all the parameters, it would be possible for somebody to change any other value.
created_at, or lever_id for example.
This would definitely be a security issue and this is why you should not do it.
Explicitely specifying all your attributes might seem boring when you do it.
But this is necessary to keep your application secure.
Edit: For people downvoting this. This may not be the answer you're looking for, but it is the answer you need.
Whitelisting all nested attributes is a huge security flaw that strong params is trying to protect you with, and you're removing it.
Take a look at what lead to building strong_params, and how not using it can be bad for you: https://gist.github.com/peternixey/1978249

Setting Rails model attributes order

When defining a virtual setter method that relies on another method to be set, it appears that the order of the attributes being set in the hash matters. Is there a way around this while still mass-assigning attributes?
https://gist.github.com/3629539
EDIT
The condition in the real code, not shown in the example, is checking for the existence of an associated object. If the object exists, set a value. If not, ignore the value passed in. However, I am also using accepts_nested_attributes_for. So, the attribute hash may contain the attributes for the association. In which case, the object will exist.
{:name => 'Fred', :nested_attributes => {:color => 'red'}}
Name will not be set because the model will not exist.
{:nested_attributes => {:color => 'red'}, :name => 'Fred'}
accepts_nested_attributes_for will build a Nested instance, then set the attributes. When the name is to be set, the instance will exist and the nested attribute will be set.
Had a similar issue, and I came to the following reasonably generic solution:
def assign_attributes(new_attributes)
assign_first = new_attributes.extract!(:must_be_set_first, :must_also_be_set_first)
super(assign_first) unless assign_first.empty?
super(new_attributes)
end
Using super with the extracted param values you need set first ensures you handle all the weird special cases for attribute assignment (is it a reference? a params hash? a multi-value param?). Calling assign_attributes repeatedly with parts of a hash really should have the same effects as calling it with the whole hash once - this should be reasonably safe.
The only solution I can think of right now is to override the attributes setter...
def attributes=(attrs)
self[:dont_set_name] = attrs.delete(:dont_set_name)
super
end

How do I manually set nested model values in Rails 3 before saving?

I have a single text area input that I would like to get as a blob my new method on my controller, but would like to parse and otherwise mess with the input before it's saved.
I know I can arbitrarily set attributes on a model by saying something like
#post.user_id = current_user.id
where that attribute isn't coming directly from a form. My issue here though is that I want to set a nested model's values.
Let's say the association is post has_many comments and comment belongs_to post
Does post.comments just get set to a hash that looks like comments? Like
#post.comment = {'comment' => 'foo'}
Or something similar?
Thanks for any guidance on this.
Usually I'd say it's best to DRY up this sort of thing and just handle the parsing on the comments model itself with a before_save callback.
class Comment < ActiveRecord::Base
before_save :parse_comment
protected
def parse_comment
self.comment = ...
end
end
But if a callback isn't going to work for you, #corroded's suggestion should work.
if you have nested form fors, you can just get the comment values from your params via:
#post.comment.update_attributes(params[:comment])
(you should have called #post.build_comment in your #new though)
If you're looking to set them in your controller, then you need a hash 'container' for your comment like so:
{'comment' => {:message => 'foo', :author => current_user}}
or something like that

has_many through blowing away the association's metadata on mass association

Hey,
Not a Rails noob but this has stumped me.
With has many through associations in Rails. When I mass assign wines to a winebar through a winelist association (or through) table with something like this.
class WineBarController
def update
#winebar = WineBar.find(params[:id])
#winebar.wines = Wine.find(params[:wine_bar][:wine_ids].split(",")) // Mass assign wines.
render (#winebar.update_attributes(params[:wine_bar]) ? :update_success : :update_failure)
end
end
This will delete every winelist row associated with that winebar. Then it finds all of the wines in wine_ids, which we presume is a comma separated string of wine ids. Then it inserts back into the winelist a new association. This would be expensive, but fine if the destroyed association rows didn't have metadata such as the individual wine bar's price per glass and bottle.
Is there a way to have it not blow everything away, just do an enumerable comparison of the arrays and insert delete whatever changes. I feel like that's something rails does and I'm just missing something obvious.
Thanks.
Your problem looks like it's with your first statement in the update method - you're creating a new wine bar record, instead of loading an existing record and updating it. That's why when you examine the record, there's nothing showing of the relationship. Rails is smart enough not to drop/create every record on the list, so don't worry about that.
If you're using the standard rails setup for your forms:
<% form_for #wine_bar do |f| %>
Then you can call your update like this:
class WineBarController
def update
#winebar = WineBar.find(params[:id])
render (#winebar.update_attributes(params[:wine_bar]) ? :update_success : :update_failure)
end
end
You don't need to explicitly update your record with params[:wine_bar][:wine_ids], because when you updated it with params[:wine_bar], the wine_ids were included as part of that. I hope this helps!
UPDATE: You mentioned that this doesn't work because of how the forms are setup, but you can fix it easily. In your form, you'll want to rename the input field from wine_bar[wine_ids] to wine_bar[wine_ids_string]. Then you just need to create the accessors in your model, like so:
class WineBar < ActiveRecord::Base
def wine_ids_string
wines.map(&:id).join(',')
end
def wine_ids_string= id_string
self.wine_ids = id_string.split(/,/)
end
end
The first method above is the "getter" - it takes the list of associated wine ids and converts them to a string that the form can use. The next method is the "setter", and it accepts a comma-delimited string of ids, and breaks it up into the array that wine_ids= accepts.
You might also be interested in my article Dynamic Form Elements in Rails, which outlines how rails form inputs aren't limited to the attributes in the database record. Any pair of accessor methods can be used.

How can I force valadition to occur when appending an object to another object in Rails?

In one of my model objects I have an array of objects.
In the view I created a simple form to add additional objects to the array via a selection box.
In the controller I use the append method to add user selected objects to the array:
def add_adjacents
#site = Site.find(params[:id])
if request.post?
#site.adjacents << Site.find(params[:adjacents])
redirect_to :back
end
end
I added a validation to the model to validate_the uniqueness_of :neighbors but using the append method appears to be bypassing the validation.
Is there a way to force the validation? Or a more appropriate way to add an element to the array so that the validation occurs? Been googling all over for this and going over the books, but can't find anything on this.
Have you tried checking the validity afterwards by calling the ".valid?" method, as shown below?
def add_adjacents
#site = Site.find(params[:id])
#site.neighbors << Site.find(params[:neighbors])
unless #site.valid?
#it's not valid, do something to fix it!
end
end
A couple of comments:
Then only way to guarantee uniqueness is to add a unique constraint on your database. validates_uniqueness_of has it's gotchas when there are many users in the system:
Process 1 checks uniqueness, returns true.
Process 2 checks uniqueness, returns true.
Process 1 saves.
Process 2 saves.
You're in trouble.
Why do you have to test for request.post?? This should be handled by your routes, so in my view it's logic that is fattening your controller unnecessarily. I'd imagine something like the following in config/routes.rb: map.resources :sites, :member => { :add_adjacents => :post }
Need to know more about your associations to figure out how validates_uniqueness_of should play in with this setup...
I think you're looking for this:
#site.adjacents.build params[:adjacents]
the build method will accept an array of attribute hashes. These will be validated along with the parent model at save time.
Since you're validating_uniqueness_of, you might get some weirdness when you are saving multiple conflicting records at the same time, depending on the rails implementation for the save and validation phases of the association.
A hacky workaround would be to unique your params when they come in the door, like so:
#site.adjacents.build params[:adjacents].inject([]) do |okay_group, candidate|
if okay_group.all? { |item| item[:neighbor_id] != candidate[:neighbor_id] }
okay_group << candidate
end
okay_group
end
For extra credit you can factor this operation back into the model.

Resources