Download zip file from activeadmin batch action - ruby-on-rails

I want to send_data to the admin who is using the activeadmin interface in our website. This data is a zip file and can be downloaded if certain conditions on the selected items are met.
I created a service that handles the logic (quite complex) behind it. So from activeadmin I can call:
batch_action :action_name, form: {selection: ['...']} do |ids, inputs|
response = MyService.new(ids, inputs[:selection]).my_method
redirect_to collection_path
end
In my service MyService.rb:
...
def my_method
...
if condition
zip_data = Zip::OutputStream.write_buffer do |zip|
zip.put_next_entry("#{original_file_name}.xml")
zip << File.read(original_file)
end
send_data(zip_data.read, :type => 'application/zip', :filename => "#{original_file_name}.zip")
# here send_data throws an error because it's a controller method
else
...
end
...
end
...
But how do I use the send_data method properly? Maybe I have to restructure something? I know you can probably do ActionController::DataStreaming.send_data(...) outside of the controller, but this is not recommended for the code's sake.

Solved. I put the send_datain the batch_action code like this:
batch_action :action_name, form: {selection: ['...']} do |ids, inputs|
response = MyService.new(ids, inputs[:selection]).my_method
redirect_to collection_path
send_data(response[:zip][:data].read, :type => 'application/zip', :filename => "#{response[:zip][:name]}.zip") if response[:zip].present?
end
where the response contains the zip data to send (which needs to be rewinded with zip_data.rewind before being sent). my_service.rb is now like:
...
def my_method
...
if condition
zip_data = Zip::OutputStream.write_buffer do |zip|
zip.put_next_entry("#{original_file_name}.xml")
zip << File.read(original_file)
end
zip_data.rewind
response[:zip] = {data: zip_data, name: original_file_name}
else
...
end
...
end
...

Related

Rails' export csv function "undefined method `export' for nil:NilClass"

I'm making an export to csv file functionality in a Ruby on Rails repo and I'm almost done. However, when I press the "Export all" button, I get the undefined method `export' for nil:NilClass error. The log shows that format.csv { send_data #foos.export, filename: "foos-#{Date.today}.csv" } went wrong. What am I missing please?
This is model
class Foo < ApplicationRecord
has_many :bars
def export
[id, name, foos.map(&:name).join(' ')]
end
end
This is part of controller
def index
#foos = Foo.all
end
def export
all = Foo.all
attributes = %w{name}
CSV.generate(headers: true) do |csv|
csv << attributes
all.each do |foo|
csv << attributes.map{ |attr| foo.send(attr) }
end
respond_to do |format|
format.csv { send_data #foos.export, filename: "foos-#{Date.today}.csv" }
end
end
end
def name
"#{foo_id} #{name}"
end
This is View
<button class="btn btn-success">export all</button>
This is Routes
Rails.application.routes.draw do
resources :foos
get :export, controller: :foos
root "foos#index"
end
This is Rake (lib/tasks/export.rb)
namespace :export do
task foo: :environment do
file_name = 'exported_foo.csv'
csv_data = Foo.to_csv
File.write(file_name, csv_data)
end
end
Start by creating a service object that takes a collection of records and returns CSV so that you can test the CSV generation in isolation:
# app/services/foo_export_service.rb
# Just a Plain Old Ruby Object that converts a collection of foos into CSV
class FooExportService
# The initializer gives us a good place to setup our service
# #param [Enumerable] foo - an array or collection of records
def initialize(foos)
#headers = %w{name} # the attributes you want to use
#foos = foos
end
# performs the actual work
# #return [String]
def perform
CSV.generate do |csv|
#foos.each do |foo|
csv << foo.serializable_hash.slice(#headers).values
end
end
end
# A convenient factory method which makes stubbing the
# service easier
# #param [Enumerable] foos - an array or collection of records
# #return [String]
def self.perform(foos)
new(foos).perform
end
end
# example usage
FooExportService.perform(Foo.all)
Not everything in a Rails application needs to be jammed into a model, view or controller. They already have enough responsiblities. This also lets you resuse the code for example in your rake task if you actually need it.
This simply iterates over the collection and uses Rails built in serialization features to turn the model instances into hashes that can be serialized as CSV. It also uses the fact that Hash#slice also reorders the hash keys.
In your controller you then just use the service object:
class FoosController
def export
#foos = Foo.all
respond_to do |format|
format.csv do
send_data FooExportService.perform(#foos),
filename: "foos-#{Date.today}.csv"
end
end
end
end
You don't even really need a separate export action in the first place. Just use MimeResponds to add CSV as an availble response format to the index:
class FoosController
def index
# GET /foos
# GET /foos.csv
#foos = Foo.all
respond_to do |format|
format.html
format.csv do
send_data FooExportService.perform(#foos),
filename: "foos-#{Date.today}.csv"
end
end
end
end
<%= link_to("Export as CSV", foos_path(format: :csv)) %>

How to create a file on users machine instead of creating in server and then to download using rails

I am working on to allow download an excel file with the below code:
login = Etc.getlogin
#dataFile = "C:/rails/#{login}data.csv"
csv1=CSV.open(#dataFile, 'w') do |csv|
$data.each do |eachrow|
csv << [eachrow.name+"#gmail.com"]
end
end
send_file(#dataFile, :filename => "#{login}data", :type => "application/csv")
Using the above code, I am able to create a file and write the data.
Instead of this, how do i write the data in csv and get downloaded into users machine instead of saving in local/server.
What you can do is generate a string with the CSV library, using CSV::generate instead of CSV::open.
Controller:
class DataController < ApplicationController
def download
respond_to do |format|
format.csv { send_csv_download }
end
end
private
def send_csv_download
string = CSV.generate do |csv|
#data.each { |row| csv << ["#{row.name}#gmail.com"] }
end
send_data string, filename: 'foo.csv', type: :csv
end
end
config/routes.rb:
get '/download', to: 'data#download'
View:
<%= link_to 'Download CSV', download_path(format: :csv) %>
Note: Obviously, I have no idea where you get your #data from, since it isn't specified in your question.

Rspec to upload file with params

I'm trying to write spec for testing upload function and the code implementation works as expected however when I tried to write spec I'm not able to figure out why the data conversation is failing during JSON.parse. [ Rails 5.X ]
Method
def upload
#some validation
begin
puts params[:file]
json = JSON.parse(params[:file].read)
#rest of the validation
rescue StandardError, JSON::ParserError, HttpServices::BadHttpResponseError
flash[:style] = :error
end
end
Spec:
describe "upload" do
before do
read = file_fixture("empy_details.json").read
#file = Hash.new
#file['emp'] = read #debugger > #file:{emp: [{"name":"Bob","key":"201","active":true}]}
end
it 'should upload' do
post :upload, params: { :file => #file }, as: :json
expect(flash[:style]).to eq(:success)
end
end
The method puts params[:file] prints
{"emp"=>"[{\"name\":\"Bob\",\"key\":\"201\",\"active\":true}]\n"}
The JSON.parse fails at convert_hashes_to_parameters(key, value) method
and converted gets value of "[{"name":"Bob","key":"201","active":true}]" before failing.
What am I missing ?
params[:file].read was throwing exception when the file was passed through Rspec and I changed the controller method code to accommodate params[:file] instead.
def upload
#some validation
begin
puts params[:file]
if params[:file].respond_to?(:read)
json = JSON.parse(params[:file].read)
else
json = JSON.parse(params[:file])
end
#rest of the validation
rescue StandardError, JSON::ParserError, HttpServices::BadHttpResponseError
flash[:style] = :error
end
end

How do you delay a rendering job?

This method works OK, but if I add delayed_job's handle_asychronously, I get can't convert nil into String:
def onixtwo
s = render_to_string(:template=>"isbns/onix.xml.builder")
send_data(s, :type=>"text/xml",:filename => "onix2.1.xml")
end
handle_asynchronously :onixtwo
So rendering with delayed job is clearly having a problem with params being passed. I've tried putting this job in a rake task but render_to_string is a controller action - and I'm using a current_user variable which needs to be referenced in the controller or view only. So... what's the best way to delay a rendering job?
/////////update////////
Given that I'm currently pair-programming with a toddler, I don't have the free hands to investigate additional class methods as wisely recommended in the comments - so as a quick and dirty I tried this:
def onixtwo
system " s = render_to_string(:template=>'isbns/onix.xml.builder') ; send_data(s, :type=>'text/xml',:filename => 'onix2.1.xml') & "
redirect_to isbns_path, :target => "_blank", :flash => { :success => "ONIX message being generated in the background." }
end
Why doesn't it work? No error message just no file produced - which is the case when I remove system ... &
For what it's worth, this is what I did, bypassing render_to_stream entirely. This is in /lib or app/classes (adding config.autoload_paths += %W(#{config.root}/classes into config/application.rb):
#classes/bookreport.rb
# -*- encoding : utf-8 -*-
require 'delayed_job'
require 'delayed/tasks'
class Bookreport
# This method takes args from the book report controller. First it sets the current period. Then it sorts out which report wants calling. Then it sends on the arguments to the correct class.
def initialize(method_name, client, user, books)
current_period = Period.where(["currentperiod = ? and client_id = ?", true, client]).first
get_class = method_name.capitalize.constantize.new
get_class.send(method_name.to_sym, client, user, books.to_a, current_period.enddate)
end
end
#app/classes/onixtwo.rb
require 'builder'
class Onixtwo
def onixtwo(client_id, user_id, books, enddate)
report_name = "#{Client.find_by_id(client_id).client_name}-#{Time.now}-onix-21"
filename = "#{Rails.root}/public/#{report_name}.xml"
current_company = Company.where(:client_id => client_id).first
File.open(filename, "w") do |file|
xml = ::Builder::XmlMarkup.new(:target => file, :indent => 2)
xml.instruct!(:xml, :version => "1.0", :encoding => "utf-8")
xml.declare! :DOCTYPE, :ONIXMessage, :SYSTEM, "http://www.editeur.org/onix/2.1/03/reference/onix-international.dtd"
xml.ONIXMessage do
xml.Header do
#masses of Builder code goes here...
end
end #of file
xmlfile = File.open(filename, "r")
onx = Onixarchive.new(:client_id => client_id, :user_id => user_id)
onx.xml = xmlfile
onx.save!
end #of method
handle_asynchronously :onixtwo
end #of class
Called from the view like this:
= link_to("Export to ONIX 2.1", params.merge({:controller=>"bookreports" , :action=>:new, :method_name => "onixtwo"}))
Via a controller like this:
class Books::BookreportsController < ApplicationController
#uses Ransack for search, hence the #q variable
def new
#q = Book.search(params[:q])
#books = #q.result.order('pub_date DESC')
method_name = params[:method_name]
Bookreport.new(method_name, #client, #user, #books)
redirect_to books_path, :flash => {:success => t("#{method_name.to_sym}").html_safe}
end
end

rails - Render template and zip

I'm trying to build a KML file in Rails, which I have done successfully, but now I want to provide a KMZ format as well which would render the index.kml file and zip it. Here is where I get stumped. I have updated the MIME Types as follows.
Mime::Type.register_alias "application/vnd.google-earth.kml+xml", :kml
Mime::Type.register_alias "application/vnd.google-earth.kmz", :kmz
Here is my format block
def index
#map_items = Items.all
respond_with(#map_items) do |format|
format.kml
format.kmz { NOT SURE WHAT IS BEST TO DO }
format.georss
end
end
ANy help would be much appreciated. Thanks!
I figured out a way to do this with Delayed Job. Every time the points are updated or created I fire off the MapOverlayJob.
class MapsController < ApplicationController
def overlay
#points = Points.all
return render_to_string("overlay.kml")
end
end
class MapOverlayJob
def initialize
#s3_filename ||= "maps/overlay.kmz"
#zip_filename ||= "overlay.kml"
end
def perform
AWS::S3::S3Object.store(#s3_filename,
build_kmz_file,
S3_BUCKET,
:access => S3_ACL,
:content_type => Mime::KMZ)
end
private
def build_kmz_file
Zippy.new(#zip_filename => MapsController.new.overlay).data
end
end

Resources