Rails merge (and manual assignment) assigning nil - ruby-on-rails

I am trying to create a model in a controller using strong params in Rails 5.1 (some things changed from previous for strong_params). However, when I inspect the params, the merged ones are NOT present and I am getting an ForbiddenAttributesError tracing back to the Model.new line below. The only thing in the Model is verify presence for all the attributes.
class ModelController < ApplicationController
before_action :application_controller_action
def create
#model = Model.new(strong_params)
if #model.valid?
result = #model.save
else
render html: 'MODEL NOT VALID'
end
render html: 'DONE'
end
private
def strong_params
# attr_1 and attr_2 are set in the application controller and are available here.
params.require(:model).permit(:name, :attribute_1, :attribute_2).merge(attribute_1: #attr_1, attribute_2: #attr_2)
# Inserting the following two lines causes a ForbiddenAttributesError
puts params.inspect # DOES NOT INCLUDE #attr_1 and/or #attr_2
return params
end
I may be doing something wrong though because I've even tried putting the strong params into a model with the attributes (which I can inspect just before) and it still fails because the validation for attr_1 and attr_2 fail in the Model.
def create
puts #user.inspect (not nil)
#model = Model.new(name: strong_params[:name], attribute_1: #attr_1, attribute_2: #attr_2)
UPDATE:
OK, I'm getting some weird errors from my troubleshooting. It seems the merge is not working correctly, though I'm sure it was at one point.
The first thing I checked was #attr_1 and #attr_2, they are definitely getting set.
For troubleshooting purposes, I've reduced the application before_action to this:
def application_before_action
#attr_1 = Model.first
#attr_2 = Model.last
With the code above, inspecting the params object and then returning it after the require().permit(), I am getting a ForbiddenAttributesError (no indication of what). If I remove those lines, I get a missing attributes error from the model indicating that #attr_1 and #attr_2 are missing.
UPDATE 2
Changed the title of the question, because I probably got confused during troubleshooting. I think the issue is just that the merge is assigning nil... but strangely so is the manual assignment suggested by (myself originally) and another answer here. The attributes keys are there, but they're getting assigned nil. Also, noticed my example was using a single Model, when there are actually two Models, Model1 and Model2. I am assigning the values from Model1 to Model2.
Here is a better demonstration of the error:
def create
puts '0:'
puts #model1.inspect
puts '1:'
puts strong_params.inspect
#model2 = Model2.new(strong_params) do |m|
m.user_id = #attr_1
m.account_number = #attr_2
end
puts '3:'
puts #model2.inspect
if #model2.valid?
result = #model2.save
render html: 'SUCCESS' and return
else
render html: #model2.errors.full_messages and return
end
end
Outputs in console:
0:
#<Model1 id: 29, attribute_1: 'test_value_1', attribute_2: 'test_value_2', created_at: "2018-08-15 03:55:08", updated_at: "2018-08-15 04:05:01">
1:
<ActionController::Parameters {"name"=>"test_name", "attribute_1"=>nil, "attribute_2"=>nil} permitted: true>
3:
#<Model2 id: nil, name: 'test_name', attribute_1: nil, attribute_2: nil, created_at: nil, updated_at: nil>
Obviously the nil id and timestamps are because the model has not been saved yet.
The html model2.errors.full_messages are: ["attribute_1 can't be blank", "attribute_2 can't be blank"]
SOLUTION
Coming from a pure ruby environment previously, I was mistaken about ActiveRecord default accessors for models. Removing the accessors seems to have resolved the problem.

Instead of mucking about with the params hash you can just assign the odd values one by one:
class ModelController < ApplicationController
before_action :application_controller_action
def create
#model = Model.new(strong_params) do |m|
m.attribute_1 = #attr_1
m.attribute_2 = #attr_2
end
if #model.valid?
result = #model.save
else
render html: 'MODEL NOT VALID'
end
# don't do this it will just give a double render error
render html: 'DONE'
end
private
private
def strong_params
params.require(:model).permit(:name, :attribute_1, :attribute_2)
end
end
In general this is a much more readable way to merge params with values from the session for example.
The reason your strong parameters method does not work is its just plain broken in every possible way. The main point is that you're not returning the whitelisted and merged params hash. You're returning the whole shebang.
You also seem under the faulty impression that .require, .permit and .merge alter the orginal hash - they don't - they return a new hash (well actually an ActionContoller::Parameters instance to be specific).
def strong_params
# attr_1 and attr_2 are set in the application controller and are available here.
permitted = params.require(:model).permit(:name, :attribute_1, :attribute_2)
.merge(attribute_1: #attr_1, attribute_2: #attr_2)
puts permitted.inspect
permitted # return is implicit
end
Or just:
def strong_params
# attr_1 and attr_2 are set in the application controller and are available here.
params.require(:model).permit(:name, :attribute_1, :attribute_2)
.merge(attribute_1: #attr_1, attribute_2: #attr_2)
end

You could convert to hash before merge
params.require(:model).permit(:name).to_h.merge(attribute_1: #attr_1, attribute_2: #attr_2)
You would have to be very sure that you are assigning non-user input though otherwise you are negating the purpose of strong parameters.

Related

Rails -- Export CSV failing if there is a blank field

I have code in my Rails app that allows me to export a CSV file. It works fine unless there is a record that has a field with no value in it. In that case it fails. As an example, the specific failure I'm getting is saying something liek "No Method Error" and it specifically references "address_line_1" because there are some users with no address_line_1. That is just one example though. Really all fields should be protected against potential blanks. Here is the code:
def download_kids_csv
#csv_headers = ['First',
'Last',
'Child First',
'Child Last',
'Parent Email',
'School',
'Class',
'Address',
'City',
'State',
'Zip',
'Parent Phone']
#kid_data = []
#school = School.find(params[:school_id])
#school.classrooms.each do |classroom|
classroom.kids.includes(:users).each do |kid|
kid.users.each do |parent|
#kid_data << {
first: parent.first_name,
last: parent.last_name,
child_first: kid.first_name,
child_last: kid.last_name,
parent_email: parent.email,
school: #school.name,
class: classroom.classroom_name,
address: parent.addresses.first.address_line_1,
city: parent.addresses.first.city,
state: parent.addresses.first.state,
zip: parent.addresses.first.zip_code,
parent_phone: parent.phones.first.phone_number
}
end
end
end
respond_to do |format|
format.csv do
headers['Content-Disposition'] = "attachment; filename=\"#{#school.name.downcase.gsub(' ', '-')}-data.csv\""
headers['Content-Type'] ||= 'text/csv'
end
end
end
Ok so the problem you are get is because you are calling method on a nil value.
So for example when you do:
kid.first_name
and kid is nil you are doing this
nil.first_name
nil does not implement the first_name method so it throws an error. WHat you could do to circumvent this (its kinda ugly) is this
kid.try(:first_name)
This will prevent you form getting those method missing errors
For those long chains you can do the following
parent.try(:addresses).try(:first).try(:zip_code)
This should save you a lot of headache, but the root cause of your issue is data integrity you would not have to do all of this if you ensured that your data was not blank. I do however understand in the real world it easier said than done. I could give you a lecture about The Law of Demeter and how you should not be running across object to access their attributes, and how thats a code smell of bad organization of data, but its a spread sheet and sometimes you just need the data. Good luck!
To build off of the earlier answer, you can also utilize the so-called lonely operator &. if you're on Ruby 2.3.
An example would look something like this: kid&.first_name.
If you're not on that version of ruby yet, there's a good gem that can help you out in this situation that's a little bit more robust than .try.
Using that gem your code would look like kid.andand.first_name. It might be overkill in this case but the difference here is that it will only perform the first_name method call if kid is not nil. For your longer chains, parent.address.first.zip_code, this would mean that the function chain would exit immediately if parent was nil instead of calling all of the different attributes with try.
Is it possible to use unless or another conditional?
unless parent.addresses.first.address_line_1.blank?
address: parent.addresses.first.address_line_1,
end
or
if parent.addresses.first.address_line_1 != nil
address: parent.addresses.first.address_line_1,
else
address: nil || "address is empty"
end

Missing keywords error on initializing a constructor

I have a Source model and ArticlesController. When user clicks scrape button, the control is passed to below mentioned ArticlesController#Scrape. The scrape then calls Source model where the sources are being initialised and the list of articles are returned in a form of hash to articles inside Scrape.
Source Model -
class Source
attr_accessor :source_name, :source_url, :source_type, :source_key, :tag_name
def self.all_instances
##array
end
# Default constructor
def initialize
end
def initialize(source_name:, source_url:, source_type:, source_key:, tag_name:)
#source_name = source_name
#source_url = source_url
#source_type = source_type
#source_key = source_key
#tag_name = tag_name
##array << self
end
def init
self.new('The Age',
'http://www.theage.com.au/rssheadlines/victoria/article/rss.xml',
'RSS',
'',
'Victoria News')
end
def import
init()
//returns hash of articles back
end
end
class ArticlesController < ApplicationController
def scrape
#get_Articles = Source.new
articles = #get_Articles.import
//stores articles in article model
//redirect to article path
end
end
I am getting ArgumentError in ArticlesController#scrape on #get_Articles = Source.new
Inside Source class the constructor def initialize(source_name:, source_url:, source_type:, source_key:, tag_name:) is being called. To rectify the issue I created a default constructor also, so that the parameterized constructor doesn't get called. However, I am not sure how to fix this problem. Could somebody please help?
I think you are doing it wrong with the def initialize method. You don't want parameterized constructor just removed it.
if you want this as well then you need to handle this for null values also.
Just creating a default constructor will not solve the issue because it will be override with other one.
You can try like this
def initialize(options ={})
#source_name = options[:source_name] if options[:source_name].present?
#handle and assign other keys and values similer to this
##array << self
end
now you can use this as
#get_Articles = Source.new
or
#get_Articles = Source.new(source_name: "abc")
First of all, the way you are trying to overload initialize method is incorrect. In ruby if you define the same method again in same class then the most latest interpretation will take preceding(based on when it get interpreted). So here initialize with parameter taking preceding.
There are many ways to overload a method based on parameters
Approach one: define method with default value assignment like below
def initialize(source_name = nil, source_url = nil, source_type = nil, source_key= nil, tag_name = nil)
end
In this approach the sequence of arguments does matter when invoking. i.e we can not invoke method with only tag_name the other values should also be passed as some value or nil
like Source.new nil, nil, nil, nil, 'tag_name_value'
Approach two: Using Hash as arguments (mentioned by #Prakash): This is the most popular and generic. In this we need to explicitly check for required argument name and need to assign default values to them if needed. This is mostly done by hash merging
def initialize(options ={})
options = {source_name: nil, source_url: nil, source_type: nil, source_key: nil, tag_name: nil}.merge(options)
end
# calling method
Source.new source_name: 'somevalue' #or so one
The disadvantage of this approach is there can be many keys in hash passed to method and to handle that you need to do extra check on input hash
Source.new source_name: 'somevalue', unexpected_key: 'unexpectedvalue'
Approach three
Ruby 2.0 has introduced the keyword arguments (also named argument in ruby 1.9) where you can provide a name to parameters like you were trying, the only thing you should keep in mind is to assign a default value to every parameter.
def initialize(source_name: nil, source_url: nil, source_type: nil, source_key: nil, tag_name: nil)
end
now you can invoke like
Source.new
Source.new source_url: 'somevalue'
Source.new source_name: 'somevalue'
Source.new source_type: 'somevalue', source_name: 'somevalue'
Source.new tag_name: 'somevalue'
# or any combination of arguments in any sequence
# but not the following, this give you error 'unknown keyword: unexpected_key'
Source.new tag_name: 'somevalue', unexpected_key: 'unexpectedvalue'

is that a ruby on rails strange behaviour with overwrite params? Or do I just dont aderstand ruby, again?

pre annotation: I have a solution, I want to understand what happens here, and if this behaviour is intended
edit a try for a better readable shortcut:
if you have the following code in Rails Controller:
def get_page
prepare_anythig params
if is_it_monday?
params=monday_default_paramms
end
finish_any_other_thing params
end
this works only on monday
Following functioning little controller function, not very intersting, I know
class SvgTestController < SiteController
def get_the_page
require "base64"
#main_width="auto"
params[:ci]||=['default']
puts "? params:",params
generate_drawing(params, false)
render ...
end
end
the console shows me how expected:
? params:
{"ci"=>"not default", "controller"=>"svg_test", "action"=>"get_the_page"}
Then I made a small (ok, erroneous or not valid as I now know - or think) change, I extended my get_the_page with 'get params via base64 encode json'
class SvgTestController < SiteController
def get_the_page
require "base64"
#main_width="auto"
params[:ci]||=['default']
# add here
puts "? params:",params
json=params[:json]
puts "json?",json.inspect
if json
plain = Base64.decode64(json)
puts "we are in here:", plain
params=JSON.parse(plain).with_indifferent_access
puts "? params now:",params
end
# end
puts "? params:",params
generate_drawing(params, false)
render ...
end
end
Solution working fine and the output like this:
? params:
{"json"=>"eyJjaSI6eyIwMDAwMDAwMDAyMDQ4MDgiOnsic3J2IjoxfX19", "controller"=>"svg_test", "action"=>"get_the_page", "ci"=>["default"]}
json?
"eyJjaSI6eyIwMDAwMDAwMDAyMDQ4MDgiOnsic3J2IjoxfX19"
we are in here:
{"ci":{"000000000204808":{"srv":1}}}
? params now:
{"ci"=>{"000000000204808"=>{"srv"=>1}}}
? params:
{"ci"=>{"000000000204808"=>{"srv"=>1}}}
later I got, working not with JSON-logic
NoMethodError in SvgTestController#get_the_page
undefined method `[]' for nil:NilClass
and my console shows me:
? params:
{"ci"=>"10.203.192.83", "controller"=>"svg_test", "action"=>"get_the_page"}
json?
nil
? params:
_(nothing to read here)_
So ruby overwrites my params (ok its a method, my fault) even if not in if ... end?
Again I ask: Is this wanted? And if, how to prevent such errors without knowing all and all the time about whats behind words like params?
edit
My solution, but not the answer to my question
...
params_used=params
json=params[:json]
if json
plain = Base64.decode64(json)
params_used=JSON.parse(plain).with_indifferent_access
end
puts "? params:",params_used
generate_drawing(params_used, false)
I think the "error" is because you're actually creating a variable. Annotation of your code:
def get_the_page
require "base64"
#main_width="auto"
params[:ci]||=['default'] # params method
# you modified #params, a mutable hash
# add here
puts "? params:",params # params method
json=params[:json] # params method
# you accessed #params[:json]
puts "json?",json.inspect
if json
plain = Base64.decode64(json)
puts "we are in here:", plain
params=JSON.parse(plain).with_indifferent_access # params variable
puts "? params now:",params # params variable
end
# end
puts "? params:",params # params variable
generate_drawing(params, false) # params variable
render ...
end
What's happening, I'd wager, is that the Ruby interpreter picks up the fact that a variable named params continues to be used after if block, so proceeds to initialize it (to nil) immediately before your if block irrespective of whether the block is visited or not.

Rails - 'can't dump hash with default proc' during custom validation

I have 2 models. User and Want. A User has_many: Wants.
The Want model has a single property besides user_id, that's name.
I have written a custom validation in the Want model so that a user cannot submit to create 2 wants with the same name:
validate :existing_want
private
def existing_want
return unless errors.blank?
errors.add(:existing_want, "you already want that") if user.already_wants? name
end
The already_wants? method is in the User model:
def already_wants? want_name
does_want_already = false
self.wants.each { |w| does_want_already = true if w.name == want_name }
does_want_already
end
The validation specs pass in my model tests, but my feature tests fail when i try and submit a duplicate to the create action in the WantsController:
def create
#want = current_user.wants.build(params[:want])
if #want.save
flash[:success] = "success!"
redirect_to user_account_path current_user.username
else
flash[:validation] = #want.errors
redirect_to user_account_path current_user.username
end
end
The error I get: can't dump hash with default proc
No stack trace that leads to my code.
I have narrowed the issue down to this line:
self.wants.each { |w| does_want_already = true if w.name == want_name }
if I just return true regardless the error shows in my view as I would like.
I don't understand? What's wrong? and why is it so cryptic?
Thanks.
Without a stack trace (does it lead anywhere, or does it just not appear?) it is difficult to know what exactly is happening, but here's how you can reproduce this error in a clean environment:
# initialize a new hash using a block, so it has a default proc
h = Hash.new {|h,k| h[k] = k }
# attempt to serialize it:
Marshal.dump(h)
#=> TypeError: can't dump hash with default proc
Ruby can't serialize procs, so it wouldn't be able to properly reconstitute that serialized hash, hence the error.
If you're reasonably sure that line is the source of your trouble, try refactoring it to see if that solves the problem.
def already_wants? want_name
wants.any? {|want| want_name == want.name }
end
or
def already_wants? want_name
wants.where(name: want_name).count > 0
end

Rails Method Ignoring Default Param - WHY?

I am at a loss as to why this is happening. I have the following function:
def as_json(options = {})
json = {
:id => id,
# ... more unimportant code
}
unless options[:simple]
# ... more unimportant code
end
json
end
It works most of the time, but in one particular partial where I call this:
window.JSONdata = <%= #day.to_json.html_safe %>
I get the following error:
ActionView::Template::Error (You have a nil object when you didn't expect it!
You might have expected an instance of Array.
The error occurred while evaluating nil.[]):
Pointing to the line "unless options[:simple]". As far as I can tell, the options hash is nil - thus the method is ignoring the default param assignment. WHY? I can fix this by changing the method to:
def as_json(options)
options ||= {}
json = {
:id => id,
# ... more unimportant code
}
unless options[:simple]
# ... more unimportant code
end
json
end
Does this make any sense to anyone!? Most appreciative for your help.
This is because you're using to_json, which has a default options of nil. to_json will eventually call as_json and pass the nil as options.
Here's where it happens on the Rails source code. First, to_json is defined with the default options of nil.
# https://github.com/rails/rails/blob/v3.0.7/activesupport/lib/active_support/core_ext/object/to_json.rb#L15
def to_json(options = nil)
ActiveSupport::JSON.encode(self, options)
end
Eventually it will arrive here.
# https://github.com/rails/rails/blob/v3.0.7/activesupport/lib/active_support/json/encoding.rb#L41
def encode(value, use_options = true)
check_for_circular_references(value) do
jsonified = use_options ? value.as_json(options_for(value)) : value.as_json
jsonified.encode_json(self)
end
end
As you see, as_json is called with value.as_json(options_for(value)) and options_for(value) will return the default value of to_json, which is nil.

Resources