Export CSV with Mongoid and FasterCSV - ruby-on-rails

Class UserController
def export_users
users = User.all
stream_csv do |csv|
csv << ["Name","Email","Gender"]
users.each do |i|
csv << [i.name,i.email,i.gender]
end
end
end
def stream_csv
require 'fastercsv'
filename = params[:action] + ".csv"
#this is required if you want this to work with IE
if request.env['HTTP_USER_AGENT'] =~ /msie/i
headers['Pragma'] = 'public'
headers["Content-type"] = "text/plain"
headers['Cache-Control'] = 'no-cache, must-revalidate, post-check=0, pre-check=0'
headers['Content-Disposition'] = "attachment; filename=\"#{filename}\""
headers['Expires'] = "0"
else
headers["Content-Type"] ||= 'text/csv'
headers["Content-Disposition"] = "attachment; filename=\"#{filename}\""
controller.response.headers["Content-Transfer-Encoding"] = "binary"
end
render :text => Proc.new { |response, output|
csv = FasterCSV.new(output, :row_sep => "\r\n")
yield csv
}
end
end
Err: "#Proc:0x9382539#/sites/app/controllers/export_controller.rb:56"
Using Ruby 1.8 and Rails 3.0.9
So I think the problem here is that I'm not using "Proc" right. Or it's not supposed to act like just another block...
I thought about programming a new logic into the class so that reads better. But if somebody could explain to me why my code is wrong or at least point me in a new direction than I might be able to learn something new here. Thanks
Note: Found a better way:
def export_inverts
require 'fastercsv'
inverts = Invert.all
filename = params[:action] + ".csv"
#this is required if you want this to work with IE
if request.env['HTTP_USER_AGENT'] =~ /msie/i
headers['Pragma'] = 'public'
headers["Content-type"] = "text/plain"
headers['Cache-Control'] = 'no-cache, must-revalidate, post-check=0, pre-check=0'
headers['Content-Disposition'] = "attachment; filename=\"#{filename}\""
headers['Expires'] = "0"
else
headers["Content-Type"] ||= 'text/csv'
headers["Content-Disposition"] = "attachment; filename=\"#{filename}\""
headers["Content-Transfer-Encoding"] = "binary"
end
csv_string = FasterCSV.generate do |csv|
csv << ["Genus","Species","Common Name","Pet Name","Gender"]
inverts.each do |i|
csv << [i.scientific_name,i.scientific_name,i.common_name,i.pet_name,i.gender]
end
end
render :text => csv_string
end

Yield can only be used inside a function or a block. Yield is used in a function that takes a block to say, yield some value into the block. Actually it says yield this value into the proc that the block has been converted into with the ampersand operator (in most cases). However, you could pass a Proc to a function that was expecting it.
Here, you just want to return the value from the proc and "yield" isn't needed.

Related

Rspec bypass inner function and return mock data

In rails I am writing a test for a controller method search_backups with Rspec:
def elastic_mongo_lookup(search_term)
devices_ids_from_elastic = ConfigTextSearch.search search_term
puts devices_ids_from_elastic
device_ids = devices_ids_from_elastic.map { |device| device._source.device_id }
csv_string = CSV.generate do |csv|
Device.where(:_id.in => device_ids).each do |device|
csv << [device.logical_name, device.primary_ip]
end
end
return csv_string
end
def search_backups
authorize! :read, :custom_report
csv_string = elastic_mongo_lookup params[:search_term]
if csv_string.blank?
flash[:notice] = "No results were found"
redirect_to reports_path
else
render text: "DeviceID, primary_ip\n" + csv_string
end
end#search_backups
describe "try controller method" do
let(:reports_controller) { ReportsController.new }
before do
allow(CSV).to receive(:generate).and_return("1234", "blah")
allow(ConfigTextSearch).to receive(:search).and_return(['"hits": [ {"_source":{"device_id":"54afe167b3000006"}]'])
allow(:devices_ids_from_elastic).to receive(:map).and_return('54afe167b3000006')
stub_request(:get, "http://localhost:9200/mongo_index/config_files/_search?q=").
with(:headers => {'Expect'=>'', 'User-Agent'=>'Faraday v0.9.1'}).
to_return(:status => 200, :body => '', :headers => {})
end
it "allows people to search backups" do
reports = double(ReportsController)
post 'search_backups'
end
end
The issue is that ConfigTextSearch.search search_term returns a elasticsearch ORM object.. which means I can't stub it because the .map() method on devices_ids_from_elastic.map is unique with it's nested _source method.
How could I bypass elastic_mongo_lookup entirely and just return a mocked csv_string to search_backups?
In an RSpec controller test, controller is defined as the controller under test. You can therefore achieve what you're asking about with the following:
allow(controller).to receive(:elastic_mongo_lookup).and_return('whatever string you choose')

Axlsx Rails. Generate .xlsx file and respond filename as json/html

I am generating xlsx files with axlsx_rails gem.
After collecting user input I am posting data to /basic_report_post.xlsx
Controller action looks like
def basic_report_post
#config = params[:config]
#data = params[:data]
#filename = "#{Rails.root}/public/test.xlsx"
respond_to do |format|
format.xlsx {
render xlsx: 'basic_report_post'
}
end
end
View file for this action basic_report_post.xlsx.axlsx
wb = xlsx_package.workbook
wb.add_worksheet(name: 'Data1') do |s|
# Drawing columns
end
xlsx_package.serialize #filename
My problem is that I am getting response data(in post success action) that is raw .xlsx file.
But I need somehow respond #filename (format json/html) to download it after.
It is possible to use the axlsx_rails template renderer to create a string and save it to file:
def basic_report_post
#config = params[:config]
#data = params[:data]
#filename = "#{Rails.root}/public/test.xlsx"
File.open(#filename, 'w') do |f|
f.write render_to_string(handlers: [:axlsx], formats: [:xlsx], template: 'path/to/template')
end
render json: {name: #filename}
end
Then you can use the template to serve the file directly if need be.
After some experiments with respond_to I move .xlsx generation logic to view helper.
So I have included BasicReportHelper in controller.
basic_report_helper.rb
module BasicReportsHelper
def generate_basic_report(filename)
p = Axlsx::Package.new
wb = p.workbook
wb.add_worksheet(:name => "Basic Worksheet") do |sheet|
# Drawing here
end
p.serialize filename
end
end
Changed post call to /basic_report_post.json and changed action to
def basic_report_post
#config = params[:config]
#data = params[:data]
#filename = "#{Rails.root}/public/test.xlsx"
generate_basic_report(#filename)
respond_to do |format|
format.json {
render json: {name: #filename}
}
end
end

Streaming CSV Download from Rails 3.2 app

I am desperate to get a streaming CSV download working in my Rails 3.2.2 app.
I have tried the 'csv_builder' gem (https://github.com/dasil003/csv_builder), which advertises support for this feature, however it seems that there were some changes in Rails 3.2 that are keeping it from working (it produces an 'uninitialized constant ActionView::Template::Handler' error on app startup).
Any other ideas or solutions out there? Thanks!
EDIT: To clarify, I am needing to export all entries of a model as a CSV file. There are so many rows, that it is timing out... therefore the need for streaming. I have used the Comma gem (https://github.com/crafterm/comma) for this in the past, but it doesn't support streaming either, at the moment.
OK, after a bit more research I hacked together the following in my controller. It'll stream if response_body is given something enumeratable (is that a word?). Also, the server needs to be able to stream (I am using Unicorn on Heroku). I'd like very much to not have all this stuff in the controller, so my next step is to extract it out somehow.
format.csv {
#entries = Entry.all
#columns = ["First Name", "Last Name"].to_csv
#filename = "entries-#{Date.today.to_s(:db)}"
self.response.headers["Content-Type"] ||= 'text/csv'
self.response.headers["Content-Disposition"] = "attachment; filename=#{#filename}"
self.response.headers["Content-Transfer-Encoding"] = "binary"
self.response_body = Enumerator.new do |y|
#entries.each_with_index do |entry, i|
if i == 0
y << #columns
end
y << [entry.first_name, entry.last_name].to_csv
end
end
}
The approach that I took with a Rails 2.3.8 app was to spawn a new thread to handle the csv parsing and then use an AJAX call to check the server to see if the file was ready (I relied on File.mtime).
Just ripped it out of the app to post here so I've removed alot of the csv parsing code, and haven't included all of the views
sorry end of the day rush :D
controllers/exports_controller.rb
class ExportsController < ApplicationController
require 'fastercsv'
require 'generic_agent'
require 'generic_record'
def listing
#this_filepath = "../html/whatever/" << Time.now.strftime("%I:%M:%S_%d:%m:%y") << ".csv"
#spawn_id = spawn(:nice => 1) do
FasterCSV.open(#this_filepath, "w") do |csv|
csv << [ "outbreak_id"]
end
end
render :update do |page|
page.replace_html 'export_status', :partial => 'export_status_partial'
end
end
def send_export
#this_filepath = params[:with]
csv_file = File.open(#this_filepath.to_s, 'r')
csv_string = ""
csv_file.each_line do |line|
csv_string << line
end
send_data csv_string, :filename => "export.csv",
:type => 'text/csv; charset=iso-8859-1; header=present',
:disposition => "attachment; filename=export.csv"
#send_file #this_filepath.to_s, :stream => false, :type=>"text/csv", :x_sendfile=>true
#send_data csv_string, :filename => export.csv
#File.delete(#this_filepath.to_s)
end
def export_checker
filename_array = params['filename'].split(/\//)
#file_found = 0
#file_ready = 0
#file_size = File.size(params['filename'])
#this_filepath = params['filename']
if File.exists?(params['filename'])
release_time = Time.now - 5.seconds
if File.mtime(params['filename']).utc < release_time.utc
#file_found = 1
#file_ready = 1
#file_access_time = File.mtime(params['filename'])
#file_release_time = release_time
#file_size = File.size(params['filename'])
else
#file_found = 1
#file_ready = 0
#file_size = File.size(params['filename'])
end
else
#file_found = 0
#file_ready = 0
#file_size = File.size(params['filename'])
end
render :action => "export_checker"
end
end
views/exports/export_checker.rjs
if #file_found == 1 && #file_ready == 1 && #file_size > 0
page.replace_html 'link_to_file', :partial => "export_ready"
if #file_release_time
page.replace_html 'notice', "<div>Completed #{#file_release_time.strftime("%I:%M:%S %A %d %B %Y")} :: file size #{#file_size.to_s}</div>"
end
page.visual_effect :highlight, 'link_to_file', :endcolor => '#D3EDAB'
elsif #file_found == 1
page.replace_html 'link_to_file', "<div> File found, but still being constructed.</div><div>#{#this_filepath.to_s}</div>"
page.visual_effect :highlight, 'link_to_file', :endcolor => '#FF9900'
else
page.replace_html 'link_to_file', "<div> File not found #file_found #{#file_found.to_s} #file_ready #{#file_ready.to_s}</div>"
page.visual_effect :highlight, 'link_to_file', :endcolor => '#FF0000'
end

Rails rewrite how to rewrite this code to ruby code?

I have a this class middleware:
class RedirectIt
require "net/https"
require "uri"
require 'open-uri'
APP_DOMAIN = 'http://www.mydomain.com'
def initialize(app)
#app = app
end
def call(env)
request = Rack::Request.new(env)
response = Rack::Response.new(env)
response.headers['Cache-Control'] = "public, max-age=#{84.hours.to_i}"
response.headers['Content-Type'] = 'image/png'
response.headers['Content-Disposition'] = 'inline'
response.body = "#{open('http://s3-eu-west-1.amazonaws.com/bucket/asdas.png').read}"
end
end
The problem is just that it gives the error:
Started GET "/?view=boks" for 127.0.0.1 at 2012-04-01 04:07:58 +0200
NoMethodError (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.[]=):
Are am doing something wrong? I have tried to rewrite this code I had in the controller:
def image_proxy
image_url = "http://s3-eu-west-1.amazonaws.com/bucket#{request.path}"
response.headers['Cache-Control'] = "public, max-age=#{84.hours.to_i}"
response.headers['Content-Type'] = 'image/png'
response.headers['Content-Disposition'] = 'inline'
render :text => open(image_url, "rb").read
end
The solution.
#PROXY BILLEDER
status, headers, response = #app.call(env)
headers['Cache-Control'] = "public, max-age=#{84.hours.to_i}"
headers['Content-Type'] = 'image/png'
headers['Content-Disposition'] = 'inline'
response_body = "#{(open('http://s3-eu-west-1.amazonaws.com/mybucket#{request.path()}')).read}"
[status, headers, response_body]

FasterCSV layout

I need to layout my CSV into columns not rows. So going down the spreadsheet not across. For example:
Header 1, value1.1, value2.1
Header 2, value1.2, value2.2
Header 3, value1.3, value2.3
Does anyone know how to do this? I've been through the documentation and can't find anything about changing the layout to columns.
EDIT:
row_data = [];
csv_string = FasterCSV.generate do |csv|
# header row
row_data << ["id", "Name", "Age"]
# data rows
Playerapplication.find_each do |player|
row_data << [player.id, player.name, player.age]
end
row_data.transpose
csv << row_data
end
# send it to the browser
send_data csv_string,
:type => 'text/csv; charset=iso-8859-1; header=present',
:disposition => "attachment; filename=players_application.csv"
Simply use Array#transpose on your data before writing to CSV.
If you modify your code like this:
row_data = [];
csv_string = FasterCSV.generate do |csv|
# header row
row_data << ["id", "Name", "Age"]
# data rows
Playerapplication.find_each do |player|
row_data << [player.id, player.name, player.age]
end
row_data.transpose.each do |row|
csv << row
end
end
it works for me.

Resources