Why can't I access contents of flash when it's a hash? - ruby-on-rails

According to the Flash documentation, I should be able to pass strings, arrays or hashes through Flash. Strings and arrays work fine but hashes aren't working.
Here's a stripped down (but still failing) version of my code:
Flash messages partial
# views/layouts/flash_messages.html.haml
- flash.each do |type, content|
- message = content if content.class == String
- message, detail = content if content.class == Array
- message, detail = content[:message], content[:detail] if content.class == Hash
- if message || detail
.alert
%h3= message if message.present?
%p= detail if detail.present?
Home controller
class HomeController < ApplicationController
def index
end
def test_string
redirect_to root_url, alert: 'Plain string works'
end
def test_array
redirect_to root_url, alert: ['Array works', 'Tested with 0, 1, 2 or more items without problems']
end
def test_hash
redirect_to root_url, alert: {message: 'Hash not working :(', detail: 'Even though I think it should'}
end
end
The problem seems to be with the assignment line in the case of the hash, the keys are present but method and detail always end up nil for hashes. But when I try the same code in the console it works fine...
IRB
irb> content = { message: 'This hash works...', detail: '...as expected' }
=> {:message=>"This hash works...", :detail=>"...as expected"}
irb> message, detail = content[:message], content[:detail] if content.class == Hash
=> [ 'This hash works...', '...as expected' ]
irb> message
=> 'This hash works...'
irb> detail
=> '...as expected'

Closer inspection revealed that, while the keys were indeed set, they'd been converted from symbols to strings.
To fix this I had to change line 4 of the controller from symbols to strings:
- message, detail = content[:message], content[:detail] if content.class == Hash
- message, detail = content['message'], content['detail'] if content.class == Hash
If I understand correctly, this is a result of flashes being stored in the session and the session object being stored in cookies as JSON objects. JSON doesn't support symbolised keys.
As an experiment I tried setting matching string and symbol keys. If you try doing both in one assignment, Ruby takes the first key and the second value (with a warning):
irb> content = { message: 'Symbol key first', 'message': 'String key second' }
=> warning: key :message is duplicated and overwritten on line X
=> {:message=>"String key second"}
But if you deliberately duplicate the keys in a hash passed to flash, whichever one is defined last "wins" (in my limited testing, but it makes sense given hashes are most likely iterated in insertion order):
symbol_first = {}
symbol_first[:message] = 'Symbol wins'
symbol_first['message'] = 'String wins'
flash[:alert] = symbol_first # 'String wins' is displayed
string_first = {}
string_first['message'] = 'String wins'
string_first[:message] = 'Symbol wins'
flash[:alert] = string_first # 'Symbol wins' is displayed

Related

Rails Parse CSV with empty cells and correctly handle exceptions

I am trying to allow users to upload csv files, however I am having some issues trying to figure out how to handle errors from the file itself.
My controller method:
def create
Product.import_csv(file)
redirect_to products_path, :flash => { :notice => "Items Added!" }
end
My model method to import the file:
def self.import_csv(file)
csv = CSV.read(file.path), :headers => true)
csv.each do |row|
item_id = row[0]
start_date = Date.parse(row[1])
order_date = Date.parse(row[2])
new_rec = where(item_id:item_id, order_date:order_date).first_or_initialize
new_rec.save!
end
end
All this works well when the file is properly formatted, however Im confused as to how to handle exceptions. Once such exception is when start_date or order_date are missing; I get an no implicit conversion of nil into String because I'm attempting to parse the date of an empty cell. Even though I have validations in my model for presence, they only get fired on the save action.
I don't want to silently ignore these errors with a rescue block, but instead redirect back, and notify the user.
How can I handle such exceptions, so that the jobs fails, and the user gets notified of the error, not specifically solely the error given above, but including errors we can't account for? Another example would be a empty line or something we cant even account for. How can I handle such errors, and notify the user with, for example, a generic "Bad data" message?
Should I handle this in my model or my controller?
If you have presence validations for the dates, you can simply assign them only if they exist, and let your model do the rest:
def self.import_csv(file)
csv = CSV.read(file.path), :headers => true)
csv.each do |row|
item_id = row[0]
start_date = row[1].nil? ? nil : Date.parse(row[1])
order_date = row[2].nil? ? nil : Date.parse(row[2])
new_rec = where(item_id:item_id, order_date:order_date).first_or_initialize
new_rec.save!
end
end

Problems querying JSON nested hash response in Ruby

Cannot seem to solve this problem:
I'm getting JSON nested hash responses from Lastfm and everything works fine when the response is structures as such:
{"topalbums" =>{"album" =>{"name =>"Friday Night in Dixie"}}}
However if the artist does not have a top album the response is structured this way and I get a NoMethodError undefined method '[]' for nil:NilClass.
{"topalbums" =>{"#text"=>"\n ", "artist"=>"Mark Chestnutt"}}
What I want to do is query the response so I do not keep getting this error.
Here is my method:
def get_albums
#albums = Array.new
#artistname.each do |name|
s = LastFM::Artist.get_top_albums(:artist => name, :limit => 1)
r = JSON.parse(s.to_json)['topalbums']['album']['name']
#albums.push(r)
end
end
which gives me exactly what I want if the artist has a top album, what I need to do is somehow add a condition to query the keys in the nested hash. However, I cannot seem to grasp how to do this as when I add this line of code to check key values:
s.each_key { |key, value| puts "#{key} is #{value}" }
the output I get is this:
topalbums is
so topalbums key does not have a value associated with it.
This is what I have tried so far:
def get_albums
#albums = Array.new
#artistname.each do |name|
s = LastFM::Artist.get_top_albums(:artist => name, :limit => 1)
if s.has_key?('album') #I know this won't work but how can I query this?
r = JSON.parse(s.to_json)['topalbums']['album']['name']
#albums.push r
else
#albums.push(name << "does not have a top album")
end
end
end
How can I fix this so I get 'Mark Chestnut does not have a top album' instead of the NoMethodError? Cheers
Use Hash#fetch default values, I would do as below:
No "album" key present
hash = {"topalbums" =>{"#text"=>"\n ", "artist"=>"Mark Chestnutt"}}
default_album = {"name" => "does not have a top album"}
hash["topalbums"].fetch("album", default_album)["name"]
#=> "does not have a top album"
"album" key present
hash = {"topalbums" =>{"#text"=>"\n ", "artist"=>"Mark Chestnutt", "album" => {"name" => "Foo"}}}
hash["topalbums"].fetch("album", default_album)["name"]
#=> "Foo"
So if the hash does not have an "album" key fetch defaults to default_album else it uses the key it find as in the second case

Rails Parse Active record validation error

I have the following in my controller:
def create
equipment = Equipment.create(:name => params[:name])
errors = equipment.errors.messages
puts "equipment errors: " + errors.inspect
respond_to do |format|
format.json { render :json => #equipment }
end
end
The response from the 'puts' is:
equipment errors: {:name=>["has already been taken"]}
How do I parse the error and get the message?
Thanks
equipment.errors.full_messages.join(", ")
This should give you "Name has already been taken". It concatenates all the errors.
Its just a hash. Access the message with
errors[:name].first
This gets the value of the :name key from the hash, which is an array with one element, and then returns the value of the first element of the array, which is the error message.
With more errors, use Array functions to access all of them and display them appropriately.

How to remove special characters from params hash?

I have one application with the following code:
quantity = 3
unit_types = ['MarineTrac','MotoTrac','MarineTrac']
airtime_plan = 'Monthly Airtime Plan'
url = "http://localhost:3000/home/create_units_from_paypal?quantity=#{quantity}&unit_types=#{unit_types}&airtime_plan=#{airtime_plan}"
begin
resp = Net::HTTP.get(URI.parse(URI.encode(url.strip)))
resp = JSON.parse(resp)
puts "resp is: #{resp}"
true
rescue => error
puts "Error: #{error}"
return nil
end
It sends data to my other application via the URL params query string. This is what the controller method of that other application looks like:
def create_units_from_paypal
quantity = params[:quantity]
unit_types = params[:unit_types]
airtime_plan = params[:airtime_plan]
quantity.times do |index|
Unit.create! unit_type_id: UnitType.find_by_name(unit_types[index]),
airtime_plan_id: AirtimePlan.find_by_name(airtime_plan),
activation_state: ACTIVATION_STATES[:activated]
end
respond_to do |format|
format.json { render :json => {:status => "success"}}
end
end
I get this error:
<h1>
NoMethodError
in HomeController#create_units_from_paypal
</h1>
<pre>undefined method `times' for "3":String</pre>
<p><code>Rails.root: /Users/johnmerlino/Documents/github/my_app</code></p>
I tried using both raw and html_safe on the params[:quantity] and other params, but still I get the error. Note I had to use URI.encode(url) because URI.parse(url) returned bad uri probably because of the array of unit_types.
Change:
quantity.times do |index|
To:
quantity.to_i.times do |index|
The reason you are having this problem is because you are treating the params values as the types that you originally tried to send, but they are actually always going to be strings. Converting back to the expected 'type' solves your problem.
However, you have some more fundamental problems. Firstly, you are trying to send an array by simply formatting it to a string. However, this is not the format that the receiving application expects to translate back to an array. Secondly, there is duplication in your request - you don't need to specify a quantity. The length of the array itself is the quantity. A better method would be to build your url like this:
url = 'http://localhost:3000/home/create_units_from_paypal?'
url << URI.escape("airtime_plan=#{airtime_plan}") << "&"
url << unit_types.map{|ut| URI.escape "unit_types[]=#{ut}" }.join('&')
On the receiving side, you can do this:
def create_units_from_paypal
unit_types = params[:unit_types]
airtime_plan = params[:airtime_plan]
quantity = unit_types.try(:length) || 0
#...

Rspec testing a controller search method

I'm trying to test the behavior of a custom search method in my controller:
#RecordingsController
def search
# raise params.inspect
#search = params[:search]
searches = []
searches2 = []
for n in 1..5
searches << #search["date(#{n}i)"].to_i
searches2 << #search["date2(#{n}i)"].to_i
end
start_date = date_format(*searches)
end_date = date_format(*searches2)
conditions = []
conditions << "agent like '%#{#search["agent"]}%'" unless #search["agent"].blank?
conditions << "phone like '%#{#search["phone"]}%'" unless #search["phone"].blank?
conditions << "date between '#{start_date}' and '#{end_date}'"
#recordings = Recording.where(conditions.join(" AND ")).order('date ASC')
if #recordings.blank?
redirect_to("/", alert: "No results were found for this search. Please try again.")
else
render "recordings/search"
end
end
using the following layout:
#recordings_controller_spec.rb
describe RecordingsController do
describe "POST #search" do
context "with valid attributes" do
it "assigns a new search to #search" do
search = #recording_search
get :search, #recording_search
assigns(:search).should eq(search)
end
it "populates an array of recordings"
it "renders the :search view"
end
end
end
The furthest I've gotten is trying to build a hash that mimics what my params hash would be for the form
#params hash
params = {"search" => { "date_1i" => "2012", "date_2i" => "1", ... "date2_5i" => "00" } }
where date_#{n}i is the start date [year, month, day, hour, minute], and date2_#{n}i is the end date. I'm trying to follow the answer posted here, mimicking the params hash with just a regular hash. As you can see from my controller, I don't actually pass parameters to my #search method. Should I be? Or is there a way to mock a params hash in an rspec test and determine if my #search, #recordings, and redirect_to/render variables/actions are being performed? I'm already kind of testing the render/redirect in my request spec, but I'd like to fully test this method if I can.
You should be able to generate a controller spec that GETs the search action with a given set of parameters. This will cause those parameters to be available to the params hash. You can then verify how the search is constructed and which results are returned.
describe RecordingsController do
describe '#search' do
it 'should return results' do
get :search, "search" => { "date_1i" => "2012", "date_2i" => "1", ... "date2_5i" => "00" }
response.should be_ok
#recordings.map(&:name).should == ['expected1', 'expected2']
end
end
end
This example executes a search with some search criteria as query parameters, verifies the response is successful (http 200 status), and then extracts the list of recordings returned and tries to map them to a list of friendly recording names (you can use any key on this model) to compare them to an expected list of results.
It'll be easier to write/test this code if you separate the concerns in this controller - you could write a helper that processes the query parameters and builds a search filter, which it then passes to the Recording model in the controller:
class RecordingController
def search
#search_filter = SearchFilter.for_params(params[:search])
#recordings = Recording.where(#search_filter).order('date ASC')
render "recordings/search"
end
end
class SearchFilter
# Return search params as a hash for given request params hash
def self.for_params(params)
...
end
end
This would let you write a unit test for the logic that generates search filters and only verify that the controller is doing the more simple operation of passing information between the search logic and the Recording model collection. I'd also recommend moving your logic about displaying empty results into the view on the results page and out of the controller.

Resources