Rails 3.2 delayed job a pdf email - ruby-on-rails

I have a page where the user can select multiple costprojects to create a single pdf (including S3 attachments). It works fine - except if the user selects quite a few, then app will time out (30 sec).
So, I would like to create the pdf and email it in the background using gem 'delayed_job_active_record'.
This works without delayed job:
def pdfemail
#costprojects = Costproject.find(params[:costproject_ids])
pdf = CombinePDF.new
#costprojects.each do |costproject|
#costproject = costproject
pdf2 = render_to_string pdf: "SLCO Captital Projects.pdf", template: "costprojects/viewproject", encoding: "UTF-8"
pdf << CombinePDF.parse(pdf2)
costproject.attachments.each do |attachment|
pdf << CombinePDF.parse( Net::HTTP.get( URI.parse( attachment.attach.url ) ) )
end
end
useremail = current_user.email
CostpdfMailer.costpdf_email(useremail,pdf).deliver
redirect_to :back
flash[:notice] = 'An Email containing a PDF has been sent to you!'
end
This was my first try that didn't work - I added:
handle_asynchronously :pdfemail
My second try:
def pdfemail
#costprojects = Costproject.find(params[:costproject_ids])
CostprojectsController.delay.pdfemail2(#costprojects)
end
def self.pdfemail2(costprojects)
#costprojects = costprojects
pdf = CombinePDF.new
#costprojects.each do |costproject|
#costproject = costproject
pdf2 = render_to_string pdf: "SLCO Captital Projects.pdf", template: "costprojects/viewproject", encoding: "UTF-8"
pdf << CombinePDF.parse(pdf2)
costproject.attachments.each do |attachment|
pdf << CombinePDF.parse( Net::HTTP.get( URI.parse( attachment.attach.url ) ) )
end
end
useremail = current_user.email
CostpdfMailer.costpdf_email(useremail,pdf).deliver
redirect_to :back
flash[:notice] = 'An Email containing a PDF has been sent to you!'
end
With the 2nd try, I get:
undefined method `render_to_string' for CostprojectsController:Class
The same render_to_string worked when it was just pdfemail.
3rd try:
pdf2 = CostprojectsController.new.render_to_string pdf: "SLCO Captital Projects.pdf", template: "costprojects/viewproject", encoding: "UTF-8"
With the 3rd try, #costproject isn't getting passed to costprojects/viewproject.pdf.erb

You can't handle a controller action asynchronously.
And you can't call controller methods like render, render_to_string and flash[:notice] from within a class method self.pdfemail since those methods are instance methods on the controller.
You'll have to separate out the pdf email logic from the controller response logic, like this:
def pdfemail
pdf_template = render_to_string pdf: "SLCO Captital Projects.pdf", template: "costprojects/viewproject", encoding: "UTF-8"
PdfMailer.new.pdf_email params[:costproject_ids], current_user.email, pdf_template
redirect_to :back
flash[:notice] = 'An Email containing a PDF has been sent to you!'
end
Then in another file:
class PdfMailer
def pdf_email(costproject_ids, useremail, pdf_template)
#costprojects = Costproject.find(costproject_ids)
pdf = CombinePDF.new
#costprojects.each do |costproject|
#costproject = costproject
# This part needs work, you can't use render_to_string here.
# You'll have to generate this pdf some other way.
pdf2 = render_to_string pdf: "SLCO Captital Projects.pdf", template: "costprojects/viewproject", encoding: "UTF-8"
pdf << CombinePDF.parse(pdf2)
costproject.attachments.each do |attachment|
pdf_response = Net::HTTP.get URI.parse(attachment.attach.url)
pdf << CombinePDF.parse(pdf_response)
end
end
CostpdfMailer.costpdf_email(useremail, pdf).deliver
end
handle_asynchronously :pdf_email
end
The PdfMailer class does not use any controller instance methods such as render, render_to_string, or flash, that has to be taken care of in the controller since you can't handle controller action asynchronously. Consequently you have to pass in the data you need, in this case the costproject_ids, useremail from current_user.email.
Inside the PdfMailer class you'll have to generate the pdf in that loop with some other method than the controller's render_to_string. There's plenty of other tools for this here: https://www.ruby-toolbox.com/categories/pdf_generation.
On your 3rd try: it isn't working because you're instantiating a new controller which doesn't have the #costproject instance variable set in it.

Related

Export xlsx file as a link in a mail using axlsx-rails

I'm using Rails 4.2, sidekiq, rubyzip, axlsx and axlsx-rails
What I need to do:
I need to export file in a mail, but as a link and not as attachment.
I want an email send with a url to the download for it
Problem:
I don't know how to do it with this gem, in the git documentation there's nothing about it and every url I use don't work. I need to know how to create a download link for it and not as attachment to the mail
Code:
controller:
def export
export_args = {account_id: current_account.id}
ExportReportJob.perform_later(export_args)
redirect_to action: :index
end
job:
def perform(export_args)
begin
export_failed = false
account = Admin::Account.find_by(id: export_args[:account_id])
account.connect_to_target_db
#accounts = Admin::Account.active_accounts
file_name = "#{Time.now.strftime("%d_%m_%Y_at_%I_%M%p")}_export.xlsx"
dir_path = "#{Rails.root}/public/exports"
FileUtils.mkdir_p(dir_path) unless File.directory?(dir_path)
file_location = "#{absolute_path}/#{file_name}"
Admin::Mailer.export(account.pi, #accounts, file_location).deliver
rescue => e
export_failed = true
Airbrake.notify(e, environment_name: Rails.env, error_message: "export account failed #{e.message}")
ensure
ActiveRecord::Base.clear_active_connections!
end
end
template:
wb = xlsx_package.workbook
wb.add_worksheet(name: "Accounts") do |sheet|
sheet.add_row ["Account id", "Account name", "Organization"]
accounts.each do |account|
sheet.add_row [account.id, accoount.name, account.organization_id]
end
end
xlsx_package.to_stream.read
mailer:
def export(member, accounts, file_location)
#member = member
#title = "Export"
#header = subject = "Your Export is Ready"
#link = file_location
xlsx = render_to_string(layout: false, handlers: [:xlsx], template: "export.xlsx.axlsx", locals: {accounts: accounts})
# attachment = Base64.encode64(xlsx)
# attachments["export_#{DateTime.now.strftime("%d_%m_%Y_%HH%MM")}.xlsx"] = {mime_type: Mime::XLSX, content: attachment, encoding: 'base64'}
# ^ this is not what I want, I want to get the url for it and insert to #link
mail(:to => member.email, :subject => subject) do |format|
format.html
end
end
mail template:
= render partial: 'header'
= h_tag 'The export you requested has been completed'
= button 'Download the file', #link, { download: true }
= p_tag '(the file download will expire after <b>2</b> days)'
= render partial: 'footer'

Can't resolve image into URL: undefined method `polymorphic_url' for #<ActionView::Base

In my Rails app I have a Job to create PDF files with WickedPDF. This worked fine until I migrated to Rails 5.2, now I get this error:
ActionView::Template::Error (Can't resolve image into URL: undefined method `polymorphic_url' for #<ActionView::Base:0x0000000004854590>):
The code where this happens is this line in the view:
image_tag(image)
This view is rendered from the Job, which I added in full below. The error happens when executing the last method in the class: private def render_image_pdf(view, pdf_name, image), where it is rendering a PDF.
After rolling back to Rails 5.1, everything works fine again, so I'm pretty sure it has something to do with an added/changed feature in Rails 5.2. So what should I change to make this working again?
The PDF Job:
class SendInvoiceAsAttachmentJob < ApplicationJob
require 'open-uri'
queue_as :default
def perform(target, invoice, send_invoice, to, cc, subject, body1, body2, body3)
view = ActionView::Base.new(ActionController::Base.view_paths, {})
view.extend(ApplicationHelper)
view.extend(Rails.application.routes.url_helpers)
# Create invoice as PDF
pdf = Rails.root.join('tmp', "tmp_invoice.pdf")
File.open(pdf, 'wb') do |file|
file << render_invoice_pdf(invoice, view, "tmp_invoice.pdf")
end
# Add (signed) timesheet as PDF
projectuser = Projectuser.where(project_id: invoice.project_id, user_id: invoice.user_id).order(:updated_at).last
if projectuser.blank? or invoice.fixed_price? or invoice.project.service?
pdf_name = "#{invoice.set_pdf_filename}.pdf"
pdf_to_email = pdf
else
if invoice.timesheet and invoice.timesheet.signed_copy?(projectuser)
# Download signed copy
timesheet_copy = TimesheetCopy.where(projectuser_id: projectuser.id, timesheet_id: invoice.timesheet_id).first
timesheet_file = Rails.root.join('tmp', timesheet_copy.attachment_file_name)
timesheet_name = timesheet_copy.attachment_file_name
if timesheet_copy.attachment.url.index("http").blank?
download = open("https:#{timesheet_copy.attachment.url}")
else
download = open(timesheet_copy.attachment.url)
end
IO.copy_stream(download, timesheet_file)
# Push into a PDF when image
if timesheet_copy.attachment_file_name.index(".pdf") == nil
timesheet_tmp = Rails.root.join('tmp', "tmp_timesheet.pdf")
File.open(timesheet_tmp, 'wb') do |file|
file << render_image_pdf(view, "tmp_timesheet.pdf", timesheet_file)
end
timesheet_file = timesheet_tmp
File.delete Rails.root.join('tmp', timesheet_copy.attachment_file_name)
end
else
# Create timesheet PDF
timesheet = Timesheet.find(invoice.timesheet_id)
timesheet_builder = TimesheetBuilder.new(timesheet.month, timesheet.year)
timesheet_name = "Timesheet #{timesheet.user.full_name} #{I18n.t("date.month_names")[timesheet.month]} #{timesheet.year}"
timesheet_file = Rails.root.join('tmp', timesheet_name)
File.open(timesheet_file, 'wb') do |file|
file << render_timesheet_pdf(timesheet, view, timesheet_name, projectuser, timesheet_builder)
end
end
# Combine the 2 PDF's
combined_pdf = CombinePDF.new
combined_pdf << CombinePDF.load(pdf, allow_optional_content: true)
combined_pdf << CombinePDF.load(timesheet_file, allow_optional_content: true)
pdf_name = "#{invoice.set_pdf_filename}.pdf"
combined_pdf.save Rails.root.join('tmp', pdf_name)
pdf_to_email = Rails.root.join('tmp', pdf_name)
File.delete(timesheet_file)
end
# Send email
if target == "basecone"
company = Company.find(invoice.company_id)
UserMailer.send_pdf_to_basecone(company.basecone_email, pdf_to_email, pdf_name).deliver
else
UserMailer.invoice_email(invoice, send_invoice, to, cc, subject, body1, body2, body3, pdf_to_email, pdf_name).deliver
end
File.delete(pdf_to_email)
end
private def render_invoice_pdf(invoice, view, pdf_name)
WickedPdf.new.pdf_from_string(
view.render(
pdf: pdf_name,
template: 'admin/invoices/show_as_attachment.pdf.haml',
locals: { invoice: invoice, copy_invoice: nil },
print_media_type: true,
orientation: 'Portrait',
page_size: 'A4'
)
)
end
private def render_timesheet_pdf(timesheet, view, pdf_name, projectuser, timesheet_builder)
WickedPdf.new.pdf_from_string(
view.render(
pdf: pdf_name,
template: 'timesheets/show_as_attachment.pdf.haml',
locals: { timesheet: timesheet, timesheet_builder: timesheet_builder, projectuser: projectuser },
print_media_type: true,
orientation: 'Portrait',
page_size: 'A4'
)
)
end
private def render_image_pdf(view, pdf_name, image)
WickedPdf.new.pdf_from_string(
view.render(
pdf: pdf_name,
template: 'admin/invoices/image_timesheet_as_attachment.pdf.haml',
locals: { image: image },
print_media_type: true,
orientation: 'Portrait',
page_size: 'A4'
)
)
end
end
Use wicked_pdf_image_tag Instead of image_tag
<%=wicked_pdf_image_tag image%>
Instead of manually constructing the ActionView setup (and thus needing to add all the right helpers, which is where you're going wrong -- I think some moved around), you can use the newish ActionController::Renderer API to perform a view render within the context of an arbitrary controller.
# Very untested; please treat this is syntactically-valid pseudocode...
WickedPdf.new.pdf_from_string(
ApplicationController.render(
template: 'admin/invoices/show_as_attachment.pdf.haml',
assigns: {
pdf: pdf_name,
print_media_type: true,
orientation: 'Portrait',
page_size: 'A4',
},
locals: { invoice: invoice, copy_invoice: nil },
)
)

Rails wicked_pdf gem generating empty pdf

I am trying to download my dynamic content as a pdf file. The pdf is being generated but with no content in it, not even a simple text(when I test with that).
As I need a lot of data which I need to calculate and send parameters without refreshing page so I am doing it through an Ajax request.
$("#download_pdf").click(function() {
report_type = $("#report_type").val();
start_date = $("#start_date").val();
end_date = $("#end_date").val();
date_type = $("#date_type").val();
$.ajax({
data: { report_type: report_type, start_date: start_date, end_date: end_date, date_type: date_type },
url: '/reports/generate_pdf.pdf',
success: function(data) {
var blob=new Blob([data]);
var link=document.createElement('a');
link.href=window.URL.createObjectURL(blob);
link.download="Report_"+new Date()+".pdf";
link.click();
console.log("pdf printed");
}
});
});
Here is my ruby code in the controller:
def generate_pdf
#results = get_report_result_by_datetype(params[:report_type], params[:start_date], params[:end_date], params[:date_type])
#type = params[:report_type].to_i
#start_date = params[:start_date]
#end_date = params[:end_date]
respond_to do |format|
format.html
format.pdf do
render pdf: "report",
layout: 'pdf_layout',
template: 'reports/generate_pdf.html.erb',
encoding: 'UTF8',
print_media_type: true,
disposition: 'attachment',
page_size: 'letter',
orientation: 'landscape',
lowquality: 'false',
debug: true
end
end
end
There is no issue in the data which I am fetching.
Note: The most strange thing I noticed is that when the calculated array is small the generated pdf has one page and if it is big the generated pdf shows multiple pages.
I would really appreciate if someone points where I am going wrong.
Thanks in Advance!!
Although I don't know how to fix your wicked_pdf code, I can recommend the following using the gem prawn (which is not rails-specific and removes some of the Rails magic from the equation):
format.pdf do
pdf_path = Rails.root.join("tmp", "my_file_name.pdf")
template_path = Rails.root.join("app", "views", "reports", "generate_pdf.html.erb")
this = binding
Prawn::Document.generate(pdf_path) do
text Erb.new(template_path).result(this)
end
render file: pdf_path
end
Haven't actually tested it so apologies if it doesn't work.

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

Url helper is not available when rendering a partial from a class

I have a helper class that renders a partial to store the content in the database.
It works fine but when the view contains a url provided by the url helper
<%= link_to "Show project", projects_url, class: "button" %>
it throws the following exception
undefined local variable or method projects_url for #
The code to render in my helper named NotificationRender is
def render(options)
viewer = ActionView::Base.new()
viewer.view_paths = ActionController::Base.view_paths
viewer.extend ApplicationHelper
viewer.render options
end
I include this helper in a class Notification
class Notification < ActiveRecord::Base
include NotificationRender
.....
def self.create_for(user, event, params)
template = "notifications/_email_project"
list_params = {:template => template,:locals => params}
notification = Notification.new
notification.message = notification.render(list_params) #here I render the partial
notification.subject = I18n.t "subject.#{event}"
notification.to = user.email
notification.save
end
end

Resources