I have a nested form that includes a lesson/questions/answers. The user populates answer fields and clicks submit. The hash is shown below:
Parameters: {"commit"=>"Submit Answers", "action"=>"update", "_method"=>"put", "authenticity_token"=>"y##########o=", "lesson"=>{"questions_attributes"=>{"0"=>{"id"=>"1", "answer"=>{"response"=>"answertextanswertext", "user_id"=>"2"}}, "1"=>{"id"=>"4", "answer"=>{"response"=>"answertextanswertext", "user_id"=>"2"}}}}, "id"=>"1", "controller"=>"lessons"}
In my update statement, I would like to loop through the answers and overwrite user_id for security purposes. I modified my update statement to the following:
def update
#lesson = Lesson.find(params[:id])
lesson_params = params[:lesson]
for q in lesson_params[:questions_attributes].values
for s in q.values
if !s[:user_id].nil?
s[:user_id] = current_user.id.to_s
end
end
end
if #lesson.update_attributes(lesson_params)
flash[:notice] = "Answers submitted successfully."
redirect_to lessons_path
else
render :action => 'edit'
end
end
I am a noob, so traversing the nested hash was a bit of trial and error. Is this the appropriate way to loop through the nested hash? Is this a good way to protect against mass-assignment?
Thanks, Alex
When you have nested data structures, looping over them is pretty much the only way to loop over them. Whether or not it is appropriate is another question. Realistically, this seems like a lot of work (even though it's pretty straightforward work) just to clean up your user ids. Honestly, it seems a lot easier to just modify the view that is passing these parameters in to set all those user ids to the currently-logged-in user before they get sent to your controller than looping over them (assuming that that is an appropriate solution to whatever security issue you are trying to solve).
Also, you may actually have some scoping issues with (for instance) q.values - this is an array that is equivalent to the values in the hash. Modifying s[:user_id] may not modify params[:lesson][:question_attributes][0][:user_id] (or whatever it works out to be).
As far as "protecting against mass-assignment" I am not sure why you are worried about that here, can you elaborate?
Related
I have a texfield in a form where a foreign key's value is displayed. now I want to update the value and save it to the DB. Here is the code:
for the form I use:
f.text_field :port, :value =>#entry.port.number, class:"form-
control", placeholder:"Port"
in the controller I am using a param method:
def entry_params
params.require(:entry).permit(:description,:rule_id, :protocol_id,
:url, :port)
end
the update method looks like this:
def update
#entry.url.name = params[:entry][:url]
#entry.port.number = params[:entry][:port]
if #entry.update(entry_params)
flash[:success] = "Entry was successfully updated!"
redirect_to entry_path(#entry)
else
render 'edit'
end
end
but if I want try to save it, it shows this error:
Url(#70247237379440) expected, got "www.drive.google.com" which is an
instance of String(#70247218839280)
Now my question is, (I'm relativ new to rails) how can I fix this? I know that it expect an object as a parameter but if I change the param like this:
params[:url] = #entry.url
it doesn't work.
There are two approaches I can think of here, depending on exactly what you're looking to achieve.
If you're looking to assign an entry a new url based on a string params, you can use something like the following:
#entry.url = Url.find_by_name(params[:entry][:url])
Depending on your models' setup, if there's a url_id column on your entries, you might be better off using a select field on this, passing the URL's name and id to the options. If you can add this info to your question, I can update / rule this out as needed.
If you're just looking to update the URL via the entry's form, you'd be best looking at using accepts_nested_attributes_for.
Doing this, you can directly update the associated objects through the parent's form. If this sounds like the right approach for you, let me know and I can provide more detail :)
Edit: as per your comment, is sounds like this is the option you're after. So, you'd need something like the following:
entry.rb
accepts_nested_attributes_for :url
In the form:
...
f.nested_fields_for :url do |url_fields|
url_fields.text_field :name
end
...
And you'll need to update the params in your controller to accept these nested fields. I can't remember the exact for they take, but it's something like:
def new / edit # whichever you're in
...
#entry.build_url unless #entry.url.present?
end
def entry_params
params.require(:entry).permit(:description,:rule_id, :protocol_id,
:port, url_attributes: [:name])
end
(It might be you need an empty array or a hash for url_attributes.)
That'll then directly update the associated url. FYI if you've not got an associated URL, you'll need to build one using #entry.build_url in the controller.
Hope this helps - if you've any questions / details to help clarify, let me know and I'll update as needed.
Scenario: I have a has_many association (Post has many Authors), and I have a nested Post form to accept attributes for Authors.
What I found is that when I call post.update_attributes(params[:post]) where params[:post] is a hash with post and all author attributes to add, there doesn't seem to be a way to ask Rails to only create Authors if certain criteria is met, e.g. the username for the Author already exists. What Rails would do is just failing and rollback update_attributes routine if username has uniqueness validation in the model. If not, then Rails would add a new record Author if one that does not have an id is in the hash.
Now my code for the update action in the Post controller becomes this:
def update
#post = Post.find(params[:id])
# custom code to work around by inspecting the author attributes
# and pre-inserting the association of existing authors into the testrun's author
# collection
params[:post][:authors_attributes].values.each do |author_attribute|
if author_attribute[:id].nil? and author_attribute[:username].present?
existing_author = Author.find_by_username(author_attribute[:username])
if existing_author.present?
author_attribute[:id] = existing_author.id
#testrun.authors << existing_author
end
end
end
if #post.update_attributes(params[:post])
flash[:success] = 'great!'
else
flash[:error] = 'Urgg!'
end
redirect_to ...
end
Are there better ways to handle this that I missed?
EDIT: Thanks for #Robd'Apice who lead me to look into overriding the default authors_attributes= function that accepts_nested_attributes_for inserts into the model on my behalf, I was able to come up with something that is better:
def authors_attributes=(authors_attributes)
authors_attributes.values.each do |author_attributes|
if author_attributes[:id].nil? and author_attributes[:username].present?
author = Radar.find_by_username(radar_attributes[:username])
if author.present?
author_attributes[:id] = author.id
self.authors << author
end
end
end
assign_nested_attributes_for_collection_association(:authors, authors_attributes, mass_assignment_options)
end
But I'm not completely satisfied with it, for one, I'm still mucking the attribute hashes from the caller directly which requires understanding of how the logic works for these hashes (:id set or not set, for instance), and two, I'm calling a function that is not trivial to fit here. It would be nice if there are ways to tell 'accepts_nested_attributes_for' to only create new record when certain condition is not met. The one-to-one association has a :update_only flag that does something similar but this is lacking for one-to-many relationship.
Are there better solutions out there?
This kind of logic probably belongs in your model, not your controller. I'd consider re-writing the author_attributes= method that is created by default for your association.
def authors_attributes=(authors_attributes)
authors_attributes.values.each do |author_attributes|
author_to_update = Author.find_by_id(author_attributes[:id]) || Author.find_by_username(author_attributes[:username]) || self.authors.build
author_to_update.update_attributes(author_attributes)
end
end
I haven't tested that code, but I think that should work.
EDIT: To retain the other functionality of accepts_nested_Attributes_for, you could use super:
def authors_attributes=(authors_attributes)
authors_attributes.each do |key, author_attributes|
authors_attributes[key][:id] = Author.find_by_username(author_attributes[:username]).id if author_attributes[:username] && !author_attributes[:username].present?
end
super(authors_attributes)
end
If that implementation with super doesn't work, you probably have two options: continue with the 'processing' of the attributes hash in the controller (but turn it into a private method of your controller to clean it up a bit), or continue with my first solution by adding in the functionality you've lost from :destroy => true and reject_if with your own code (which wouldn't be too hard to do). I'd probably go with the first option.
I'd suggest using a form object instead of trying to get accepts_nested_attributes to work. I find that form object are often much cleaner and much more flexible. Check out this railscast
I am creating a instance variable that gets passed to my view. This variable 'post' has a user_id associated with it and I wanted to add an extra attribute called 'username' so I can also pass that and use it in the view.
Here is an example of what I would like to do.
#post = Post.find(params[:id])
#post.username = User.find(#post.user_id).username
A username column does exist on my Users model but not my Songs model. So it won't let me use
#post.username
I know I can just make an entirely new instance variable and put that information in there but I would like to keep everything nice and neat, in one variable. Which will also make my json rendered code look cleaner.
Any ideas on how I can accomplish this?
Thanks!
Based on the presence of a user_id in your Post model, you probably already have an association set up that can retrieve the username. It will probably save a lot of trouble to simply use the existing association:
#post = Post.find(params[:id])
username = #post.user.username
If you're likely to be querying more than one post at a time (e.g., on an index page, calling .includes to tell Rails to eager-load an association will help you avoid the N+1 problem:
#posts = Post.includes(:user).all
Finally, to include the associated record in your JSON output, pass the :include parameter as you serialize:
# in controller
render :json => #post.to_json(:include => :user)
This question includes a much more comprehensive discussion of serialization options. Well worth a read.
No need to pass a separate instance variable.
1. You can use #post.user.username in view itself.
2. Or you can create a helper and pass #post.user
def username user
user.username
end
I have a form that allows a user to update their profile information, but I would like to prevent some information from being changed. I also would like to keep my controller code very simple. In the update action of my Users Controller, I have the following code:
def update
#user = Users.find params[:id]
if #user.update_attributes(params[:user])
flash[:notice] = 'Update successful.'
redirect_to user_path(#user)
else
render :action => :edit
end
end
This is very clean and simple, and I like that. What I don't like, however, is that a user can add a field to the form, with the same name as an attribute, and use it to modify forbidden attributes. Is there a simple way to do this, or do I need to devise a way to do this myself?
One method I was considering was to generate a hash value, using a hash-based message authentication code, of all the form's element names. This message access code would be a hidden value in the form. Then, once the form is submitted, I would calculate the message access code (MAC) again using the names of the parameter Hash's keys. If the two MACs are different, or if the first MAC is missing from the parameter Hash, I would throw an error. I would rather not spend the time implementing this if there was already and easy solution out there.
Thanks.
On your model you can use attr_protected or attr_accessible to blacklist or whitelist attributes when being set via mass assignment (like when a form is submitted).
Rails will prevent mass assignment if you use attr_protected :protectedcolumn (blacklist) or attr_accessible :safecolumn (whitelist) within your model. More information on this topic can be found in the Ruby on Rails Security Guide (Section 6.1)
I've got a form with quite a bit of params being passed to the controller for processing. The different 'sets' of params are named in a similar fashion:
setname1_paramname
setname1_paramname2
Now, I need to check one of these 'sets' to verify that all of the fields are submitted. Right now, I'm doing this with a manual If Or style statement:
if setname1_paramname.blank? || setname1_paramname2.blank? || ...etc
#object.errors.add_to_base("All setname1 fields are required.").
render :action => 'new'
return false
end
Is there way to programmatically loop over these params, and add them to the #object errors?
Thanks!
Since it sounds like you have a ton of params and also seems like you need to be able to do checks on groups of params, maybe something like this would be useful? Basically, iterate over the params hash, and use regular expressions to target sets of params. Then, inside the loop, you can do any sort of validations:
params.each do |key, value|
# target groups using regular expressions
if (key.to_s[/setname1.*/])
# whatever logic you need for params that start with 'setname1'
if param[key].blank?
#object.errors.add_to_base("All setname1 fields are required.").
end
end
end
If the names are arbitrary and of your own choosing, you could make virtual attributes for them in your model and let Rails handle the presence checking.
class SomeModel < ActiveRecord::Base
VIRTUAL_ATTRIBUTES = [:billing_address, :billing_state, :something_else]
attr_accessor *VIRTUAL_ATTRIBUTES
validates_presence_of *VIRTUAL_ATTRIBUTES
…
end
Is there a reason you wouldn't just store this information in a model, even if temporarily, and then just use rails validations for your information?
I'm rusty but I assume that even if the value is blank the param will still be returned in the params hash as long as it is coming from a form element, yes? Could you just iterate through the params hash and keep a counter of how many values are not blank and then compare the length of the params hash to the counter. If the counter is short then you have blank parameters and can handle the error that way without having to hardcode checks for each individual parameter, yes?
If what you need is a multi-step form as I suspect, you may find the Railscast on Multistep Forms to be useful