Rails -- Export CSV failing if there is a blank field - ruby-on-rails

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

Related

Ruby on Rails beginner question : equality

I'm starting to know ROR and I was doing a kind of blog with articles, etc...
I did this code :
def show
id = params[:id]
list = Article.all
is_valid = false
list.all.each do |article|
if article.id == id
#is_valid = true
break
end
end
As you can see, this code just wants to check if the article ID exists or not. So I'm testing equality between id and article.id (which's a model linked to the appropriated table in the database) BUT when I try to use or display #is_valid boolean I saw that article.id == id is FALSE every time, even if article.id = 2 and id = 2. I tried to think about everything that can make this occuring, but I admit I still misunderstand this.
Then I ask you if you know why this is occuring. Of course, an equality like 2 == 2 will change #is_valid to true.
Thank you for your help !
Maybe its because params[:id] it's a string and article.id it's an Integer
(byebug) params
{"controller"=>"admin/my_controller", "action"=>"edit", "id"=>"1"}
And yes it is... "id" is a string "1", so you may try this:
def show
id = params[:id].to_i
list = Article.all
is_valid = false
list.all.each do |article|
if article.id == id
#is_valid = true
break
end
end
end
And maybe could work.
This is the answer to your question,
But if you want to learn a little more about Activerecord you can do this
Article.exists?(params[:id])
and that will do what you are trying to do just with a query against db.
and if you want to get just a simple article
record = Article.find_by(id: params[:id]) #return nil when not exist
if record # if nil will threat like false on ruby
#my code when exist
else
#my code when not exist
end
will work (you also can use find but find will throw an exception ActiveRecord::RecordNotFound when not exists so you have to catch that exception.
Activerecord has many ways to check this you dont need to do it by hand.
def show
#article = Article.find(params[:id])
end
This will create a database query which returns a single row. .find raises a ActiveRecord::NotFound exception if the record is not found. Rails catches this error and shows a 404 page. Article.find_by(id: params[:id]) is the "safe" alternative that does not raise.
Your code is problematic since list = Article.all will load all the records out of the database which is slow and will exhaust the memory on the server if you have enough articles. Its the least effective way possible to solve the task.
If you want to just test for existence use .exists? or .any?. This creates a COUNT query instead of selecting the rows.
Article.where(title: 'Hello World').exists?

Rails merge (and manual assignment) assigning nil

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.

Couldn't find Post with 'id'=params[:id]

If I don’t have row with id=params[:id] how can i check it, since
when I write
def show
#post=Post.find(params[:id])
if #post.nil?
#post={
title:"No such post",
post:"No such post"
}
end
end
I get error.
From the fine manual:
find(*args)
Find by id - This can either be a specific id (1), a list of ids (1, 5, 6), or an array of ids ([5, 6, 10]). If no record can be found for all of the listed ids, then RecordNotFound will be raised.
So if find can't find anything with the id you're looking for, it raises an ActiveRecord::RecordNotFound exception rather than return nil like you want it to. That exception ends up being handled deep inside Rails and gets converted to a 404.
You could trap that exception yourself:
def show
#post = Post.find(params[:id])
rescue ActiveRecord::RecordNotFound
#post = {
title: "No such post",
post: "No such post"
}
end
Note that you'd only trap the specific exception you're expecting to see, a bare rescue is almost always a mistake because it can hide bugs.
You could also use find_by:
find_by(*args)
[...]
If no record is found, returns nil.
like this:
def show
#post = Post.find_by(:id => params[:id])
if #post.nil?
#post = {
title: "No such post",
post: "No such post"
}
end
end
Exceptions are meant for handling errors and other exceptional conditions, they're not meant to be used for normal flow control. I'd probably use find_by for this sort of thing; it seems that you're expecting the occasional missing record so it a missing record isn't really an error or an unexpected condition.
show controller is expected to show existing elements only. When an element (Post instance) does not exist, find throws an exception. As #Michal suggested in the comments, usually non-existing entities are being handled with 404 response, or like.
For the time, being, though, you might cheat Rails with:
#post = Post.find(params[:id]) rescue {
title: "No such post",
post: "No such post"
}
This is not a production solution, of course, but it might help during learning phase.

Flash[:error] add more than one at the same time

I use 'nokogiri' among others to check the schema from some uploaded xml's. And i will print out all errors which occurs:
xsd.validate(doc).each do |error|
flash[:error] = error.message
end
If I do so, I see only the last added error, if more than one exists.
I find also find a similar question, about this problem rails-easy-way-to-add-more-than-one-flashnotice-at-a-time but the accepted solution dosen't work for me.
Thanks
change the method to
flash[:error] = xsd.validate(doc).map(&:message).to_sentence
UPDATE
Using br tags to separate each error
flash[:error] = xsd.validate(doc).map(&:message).join('<br>').html_safe
I find also find a similar question, about this problem
rails-easy-way-to-add-more-than-one-flashnotice-at-a-time but the
accepted solution dosen't work for me.
In what way doesn't it work for you?
You can add your own flash types like flash[:errors] and write your own helper methods for convenience.
def my_flash(type, message)
flash[type] ||= []
flash[type] += Array.wrap(message)
end
Then you can use an array or a string as the message, making it easy to pass multiple in, like so.
my_flash :errors, "name cannot be blank"
my_flash :errors, ["age must be greater than 17", "phone number is invalid"]
p flash[:errors]
#=> ["name cannot be blank", "age must be greater than 17", "phone number is invalid"]

Rails find_or_create_by where block runs in the find case?

The ActiveRecord find_or_create_by dynamic finder method allows me to specify a block. The documentation isn't clear on this, but it seems that the block only runs in the create case, and not in the find case. In other words, if the record is found, the block doesn't run. I tested it with this console code:
User.find_or_create_by_name("An Existing Name") do |u|
puts "I'M IN THE BLOCK"
end
(nothing was printed). Is there any way to have the block run in both cases?
As far as I understand block will be executed if nothing found. Usecase of it looks like this:
User.find_or_create_by_name("Pedro") do |u|
u.money = 0
u.country = "Mexico"
puts "User is created"
end
If user is not found the it will initialized new User with name "Pedro" and all this stuff inside block and will return new created user. If user exists it will just return this user without executing the block.
Also you can use "block style" other methods like:
User.create do |u|
u.name = "Pedro"
u.money = 1000
end
It will do the same as User.create( :name => "Pedro", :money => 1000 ) but looks little nicer
and
User.find(19) do |u|
..
end
etc
It doesn't seem to me that this question is actually answered so I will. This is the simplest way, I think, you can achieve that:
User.find_or_create_by_name("An Existing Name or Non Existing Name").tap do |u|
puts "I'M IN THE BLOCK REGARDLESS OF THE NAME'S EXISTENCE"
end
Cheers!

Resources