I am working on an Ruby on Rails application which uses prawn to generate PDF's when a user clicks on a link. The functionality works when running the application on the localhost but when i deploy it to the server and click on the link the file doesnt load with the message "Failed to load PDF document".
I have done some research and seen references in other places regarding other documents generating gems but none specific to Prawn. Any requests for additional information will gladly be met.
I think showing the code might help so here goes. It is financial application where a page lists statements with a view button and a pdf icon to view them pdf format. When the user clicks on the pdf icon prawn then comes into play. Hope this help clarify my problem somewhat.
Controller Code:
require 'json'
InternetBanking.controllers :statements do
get :index do
#today = Time.now
render 'statements/listing'
end
get :listing do
#today = Time.now
render 'statements/listing'
end
get :activity, provides: [:json] do
content_type :json
#customer.accounts.map{|x| {x.name => x.activity}}.to_json
end
get :show, map: "/statements/:id", provides: [:html, :pdf] do
begin
m = params[:id].force_encoding('UTF-8').match(/(\d+)-(\w+)-(\d+)/)
accountno = m[1].to_i
month = Date::MONTHNAMES.index(m[2])
year = m[3].to_i
raise Exception if month.nil? or year.nil?
# Fetch account_id from logged in customer, prevent crafted URLs.
#account = #customer.fetch_account(accountno)
raise Exception if #account.nil?
#statement = #account.statement(year, month)
# TODO: Deal with no transactions
if content_type == :pdf
content_type 'application/pdf'
#statement.to_pdf
else # HTML
render 'statements/show'
end
rescue
flash[:error]= 'You cannot have an empty month field'
end
end
end
Model Code:
Statement.rb
class Statement
attr_accessor :opening_balance, :closing_balance
def initialize(acct, year = nil, month = nil)
#db = Database.instance
#account = acct
#year = year
#month = month
#month_name = Date::MONTHNAMES[#month]
end
def to_s
"#{#account.number}-#{#month_name}-#{#year}"
end
def to_pdf
title = #account.name
subtitle = "Account Statement: #{#month_name} #{#year}"
StatementPDF.new(title, subtitle, transactions).render
end
def transactions
return #transactions if #transactions
start = Date.civil(#year, #month).strftime('%Y-%m-%d')
finish = Date.civil(#year, #month, -1).strftime('%Y-%m-%d')
rows = #db.call('IBStatement2', #account.account_id, start, finish)
#transactions = rows.map {|txn| Transaction.new(txn)}
end
end
Statement_pdf.rb
require 'prawn'
class StatementPDF < Prawn::Document
BOX_MARGIN = 36
# Additional indentation to keep the line measure with a reasonable size
INNER_MARGIN = 30
# Vertical Rhythm settings
RHYTHM = 10
LEADING = 2
# Colors
BLACK = "000000"
LIGHT_GRAY = "F2F2F2"
GRAY = "DDDDDD"
DARK_GRAY = "333333"
BROWN = "A4441C"
ORANGE = "F28157"
LIGHT_GOLD = "FBFBBE"
DARK_GOLD = "EBE389"
BLUE = "0000D0"
GREY = "CCCCCC"
def initialize(title, subtitle, rows)
#rows = rows
#title = title
#subtitle = subtitle
super(page_size: 'A4') do
define_grid(columns: 4, rows: 16, gutter: 10)
header
transactions_table
footer
end
end
private
def header
grid([0,0],[1,0]).bounding_box do
image 'public/images/Statement/logo.png', width: 110
end
grid([0,1],[1,1]).bounding_box do
font_size 10
text 'Yada', font_weight: 'bold'
font_size 9
text 'Yada'
text 'Yada'
text 'Yada'
text 'Yada'
text 'Yada'
text 'Yada'
end
grid([0,2],[0,3]).bounding_box do
font_size(20)
text #title, color: '#0044AA', :align => :right
font_size(14)
text #subtitle, color: '#0044AA', :align => :right, :valign => :bottom
end
font_size(12)
end
def footer
grid([15,0],[15,3]).bounding_box do
image 'public/images/statement-logo.png', width: 100
end
end
def transactions_table
grid([2,0], [13,3]).bounding_box do
data = [%w(Date Description Amount Balance)]
data += #rows.map{|r| [r.value_date, r.description, r.amount, r.balance]}
options = { header: true, width: 520,
column_widths: {0 => 100, 2 => 100},
row_colors: ['EEEEEE', 'FFFFFF']}
table(data, options) do
cells.padding = 5
cells.border_width = 0.5
cells.border_color = GREY
row(0).font_weight = 'bold'
row(0).border_color = BLACK
row(1).border_top_color = BLACK
column(2).align = :right
column(3).align = :right
end
end
end
end
View Code:
- content_for :title, 'Full Statement Listing'
- content_for :toolbar do
%a.back(href='/balances') Back
%table
%thead
%tr
%th.year(rowspan=2) Year
%th.month(rowspan=2) Month
%th.accounts{colspan: #customer.accounts.size} Accounts
%tr
- #customer.accounts.each do |account|
%th= account.name
- #today.year.downto(#today.year - 3) do |year|
%tbody
- x = (#today.year == year) ? #today.month : 12
- x.downto(1) do |month|
%tr
- if month == x
%td{rowspan: x}= year
%td= Date::MONTHNAMES[month]
- #customer.accounts.each do |acct|
- if acct.activity[year] && acct.activity[year][month]
%td{'data-transactions' => acct.activity[year][month]}
- stmt = acct.statement(year, month)
= link_to 'View', url(:statements, :show, id: stmt.to_s)
= link_to image_tag('/icons/document-pdf.png', alt: 'Download PDF'), url(:statements, :show, id: stmt, format: :pdf), class: :pdf
- else
%td
Related
In my ParametersController, I defined the export method to download a list of parameters as a xlsx formated file:
# Get /parameters_list/1/export.xlsx
def export
### Exports the parameters attached to #parameters_lists in a Workbook
workbook = WriteXLSX.new("./public/#{#parameters_list.code}_parameters.xlsx")
title = workbook.add_format(:font => 'Calibri', :size => 16, :color => 'blue', :bold => 1)
header = workbook.add_format(:font => 'Calibri', :size => 12, :color => 'black', :bold => 1)
data = workbook.add_format(:font => 'Calibri', :size => 10, :color => 'black', :bold => 0)
workbook.set_properties(
:title => "Parameters lists export: #{#parameters_list.code}",
:subject => 'This file contains exported parameters',
:author => 'Open Data Quality',
:comments => translation_for(#parameters_list.name_translations)
#,
#:hyperlink_base => url_for #parameters_list
)
### write Description sheet
cover = workbook.add_worksheet('Parameters list')
cover.write(0, 0, #parameters_list.code, title)
cover.write(1, 0, translation_for(#parameters_list.name_translations), header)
index = 3
#parameters_list.attributes.map do |name, value|
cover.write(index, 0, name, header)
cover.write(index, 1, value, data)
index += 1
end
### write Data sheet
data_sheet = workbook.add_worksheet('Parameters')
index = 0
## Headers
# Write attributes headers
#parameters_list.parameters.first.attributes.each do |attribute, value|
data_sheet.write(0, index, attribute, header)
index += 1
end
# Add translations headers
list_of_languages.order(:property).each do |locution|
data_sheet.write(0, index, "Name_#{locution.code}", header)
index += 1
data_sheet.write(0, index, "Description_#{locution.code}", header)
index += 1
end
## Data
#parameters_list.parameters.each_with_index do |parameter, line_index|
index = 0
# Write attributes
parameter.attributes.each do |attribute, value|
data_sheet.write(line_index + 1, index, value, data)
index += 1
end
# Add translations
list_of_languages.order(:property).each do |locution|
data_sheet.write(line_index + 1, index, parameter.name_translations.where(language: locution.property).first&.translation, data)
index += 1
data_sheet.write(line_index + 1, index, parameter.description_translations.where(language: locution.property).first&.translation, data)
index += 1
end
end
# close workbook
workbook.close
send_file "./public/#{#parameters_list.code}_parameters.xlsx", type: "application/xlsx"
end
With respect to the MVC architecture, I think this would rather be defined as a view, accessible through a URL with specified format, such as my_server/parameters_lists/15.xlsx relying on the show method:
# GET /parameters_list/1
# GET /parameters_list/1.json
# GET /parameters_list/1.csv
# GET /parameters_list/1.xlsx
def show
### #parameters_list retrieved by callback function
respond_to do |format|
format.html # show.html.erb
format.json { render json: #parameters_list.parameters }
format.csv { send_data #parameters_list.parameters.to_csv }
format.xlsx # uses specific template to generate the a file
end
end
I tried several ways to move and encapsulate the code into a show.xlsx.erb file, but it does not generate
more than an text file without data.
Can you help me find out how to achieve this?
You can use caxlsx_rails gem
For eg: Create the template with the .xlsx.axlsx extension
sample.xlsx.axlsx
wb = xlsx_package.workbook
wb.add_worksheet(name: "Samples") do |sheet|
#samples.each do |sample|
sheet.add_row [sample]
end
end
format.xlsx {
response.headers['Content-Disposition'] = 'attachment; filename="my_new_filename.xlsx"'
}
For more info you can go here
Im using the Prawn gem to generate a pdf table. I need to set the width of the table to 100%. How can I do this?
Here's my slmun_pdf.rb
class SlmunPdf < Prawn::Document
def initialize(slmunDbs, view, allcount)
if slmunDbs.table_name == "schools"
super(top_margin: 50)
if slmunDbs.size != allcount
#warn = " (Not all Schools)"
else
#all = " All #{slmunDbs.size} Schools"
end
text "Showing#{#all}", size: 18, style: :bold, align: :center, color: "636363"
text "#{#warn}", size: 11, align: :center, color: "858585"
#schools = slmunDbs
#view = view
school_list
end
end
def school_list
move_down 20
table school_list_rows, :cell_style => { :font => "Helvetica", :size => 9, :border_width => 0.5, :borders => [:top, :bottom], :border_color => "B0B0B0", :text_color => "737373"} do
self.row(0).align = :center
row(0).background_color = 'A0B046'
row(0).text_color = "FFFFFF"
self.row_colors = ["DDDDDD", "FFFFFF"]
self.header = true
end
end
def school_list_rows
[["Name", "Contact", "Country", "Pre Registration", "Full Registration", "Assigned Countries", "Total Delegates" ]] +
#schools.map do |school|
countries = ""
len = school.countries.count
school.countries.each_with_index do |country, index|
countries << "#{country.country }"
if index+1 != len
countries << ","
end
end
[school.name, school.contact, school.country, #view.yesno(school.prereg), #view.yesno(school.fullreg), countries, school.delegates.size ]
end
end
end
The existing documentations didn't help. I might be using a different method? I learnt this from rails cast!
You can use the width method of the Prawn::Document::BoundingBox class:
require 'prawn'
require 'prawn/table'
Prawn::Document.generate("hello.pdf") do
table_content = [["This table"], ["covers the"], ["whole page width"]]
table table_content, width: bounds.width
end
The following code does not work. It says undefined method 'table_name' for nil:NilClass
#members = Members.all
table member_list_rows do
if #members.table_name == members
row(0).background_color = '3498db'
end
end
Full code
class MemberPdf < Prawn::Document
def initialize(members, view, allcount)
super(top_margin: 50)
if members.size != allcount
#warn = " (Not all members)"
else
#all = " All"
end
text "Showing#{#all} #{members.size} Members", size: 18, style: :bold, align: :center, color: "636363"
text "#{#warn}", size: 11, align: :center, color: "858585"
#members = members
#view = view
member_list
end
def member_list
move_down 20
table member_list_rows do
self.row(0).align = :center
if #members.table_name == "members"
row(0).background_color = '3498db'
else
end
row(0).text_color = "FFFFFF"
self.row_colors = ["DDDDDD", "FFFFFF"]
self.header = true
#self.cell.text_color = "B3B3B3"
row(0).columns(0).style size: 20
end
end
def member_list_rows
[["Name", "Awardunit", "Address", "Contact", "Level of Entry", "Current Award", "Disabled?" ]] +
#members.map do |member|
[member.name, member.awardunit.name, member.address, member.name, member.entrylvl, member.currentaward, #view.yesno(member.disabled)]
end
end
end
Members controller
if params[:commit] == "Clear"
params[:q] = nil
end
respond_to do |format|
format.html
format.pdf do
pdf = MemberPdf.new(Member.search(params[:q]).result.order( 'name ASC' ), view_context, Member.all.size)
send_data pdf.render, filename: "Members_List.pdf", type: "application/pdf", disposition: "inline"
end
end
It is due to #members is nil.You are doing it wrong.
Change this
#members = Members.all #Wrong
to
#members = Member.all #Right
Always remember,the Model name should be singular.
Those are called Naming Conventions. For more information,read these Style guides(Ruby and Rails)
Most likely table method is changing context, in which you don't have access to the #members instance variable anymore. This can be achieved easily by this sample code:
def do_stuff(&block)
cls = Class.new
cls.instance_eval(&block)
end
#test_var = "test_var"
do_stuff { puts #test_var }
You will receive nothing, because #test_var does not exist in the cls.
Am not sure what you are doing with "table member_list_rows". Didn't get that.
In the third line though, it should be
if #members.table_name == "members"
I am using Prawn to generate a pdf receipt. Ideally I would like the following format:
Header
Customer Info
Items for order
Total
However when it goes to the second page there is a huge gap on the top from the Header spacing. How can I prevent it from adding the extra spacing on top?
Here is the code generating the receipt below (Sorry there is a lot of code):
class OrderPdf < Prawn::Document
def initialize(order)
super()
#order = order
bounding_box([10, 700], :width => 500) do
logo
end
message
date_shipped
order_number
bounding_box([0, -70], :width => 2000) do
bounding_box([65, 560], :width => 200) do
shipping_address
end
bounding_box([320, 560], :width => 200) do
payment_method
end
bounding_box([25, 450], :width => 500, :position => :center) do
line_items
end
end
end
def logo
image "#{Rails.root}/app/assets/images/letterhead-header.jpg" , :scale => 0.25, :position => :center
end
def message
move_down 20
text "Thanks for shopping with Dwellers. If you have any questions, please email support#dwellers.com"
end
def date_shipped
move_down 10
data = [["Shipment on #{#order.date_to_ship.strftime("%B %d, %Y")}",
"Items: #{#order.order_items.count}",
"Total: #{helpers.humanized_money(Money.new((#order.total_cents), "USD"), {:no_cents_if_whole => false, :symbol => true})}"]]
table data do
cells.style(:borders => [], :background_color => "DDDDDD", :valign => :center)
column(0).font_style = :bold
column(0).width = 400
column(0).size = 18
end
end
def order_number
move_down 10
# order_id = Digest::MD5.hexdigest "#{#order.id}#{#order.date_to_ship}"
text "Order\# #{#order.number}", size: 14, style: :bold
end
def shipping_address
move_down 10
text "Shipping address:", size: 14, style: :bold
indent(20) do
text "#{#order.user.name} #{#order.user.name_last}"
address_fields = #order.user.address.split(",")
text "#{address_fields[0]}"
if !address_fields[1].empty?
text "#{address_fields[1]}"
end
text "#{address_fields[2]}, #{address_fields[3]} #{address_fields[4]}"
end
end
def payment_method
move_down 10
#card = #order.user.card
text "Payment Information:", size: 14, style: :bold
if #card != nil
indent(20) do
text "#{#card[:card_type].capitalize} **** **** **** #{#card[:last_four]}"
billing_address
end
end
end
def billing_address
move_down 10
#billing_address = #order.user.billing_address.split
address_fields = #order.user.billing_address.split(",")
text "#{#card[:name]}"
text "#{address_fields[0]}"
if !address_fields[1].empty?
text "#{address_fields[1]}"
end
text "#{address_fields[2]}, #{address_fields[3]} #{address_fields[4]}"
end
def line_items
move_down 10
text "In this shipment:", size: 14, style: :bold
table line_item_rows + summary do
row(0).font_style = :bold
columns(1..3).align = :left
self.header = true
cells.style(:borders => [])
column(0).width = 380
row(row_heights().length-5).borders = [:bottom]
row(row_heights().length-2..row_heights().length-1).column(1).font_style = :bold
row(row_heights().length-1).column(1..2).size = 14
end
end
def line_item_rows
[["Item names", "Qty", "Price"]] +
#order.order_items.map do |order_item|
product = order_item.item.name + " - " + order_item.item.description
quantity = " x " +order_item.quantity.to_s
[product, quantity, helpers.humanized_money(Money.new((order_item.price_paid_cents), "USD"), {:no_cents_if_whole => false, :symbol => true})]
end
end
def summary
# move_down 15
data = [["", "", ""],
["Subtotal:", "Shipping:", "Total:"],
[helpers.humanized_money(Money.new((#order.subtotal_cents), "USD"), {:no_cents_if_whole => false, :symbol => true}),
helpers.humanized_money(Money.new((#order.shipping_fee_cents), "USD"), {:no_cents_if_whole => false, :symbol => true}),
helpers.humanized_money(Money.new(#order.total_cents, "USD"), {:no_cents_if_whole => false, :symbol => true})]].transpose
end
# Need this to get access to View Helpers, such as number_to_currency
def helpers
ActionController::Base.helpers
end
end
Below is how the receipt looks like.
For your flowing text that is beneath the header, you can use a span rather than a bounding_box, and it will start at the top of the page. There are some examples in the manual, just search for span in there:
http://prawn.majesticseacreature.com/manual.pdf
I'm trying to have an table in text mail, so I write some helpers:
module MailerHelper
def field_width(text, width)
' ' * (width - text.length) + text
end
def cell(text, width)
output = '| ' + field_width(text, width-2) + " |\n"
output << '+-' + '-'*(width-2) + '-+'
end
end
Then in view I write it like this:
<%= cell 'Test', 10 %>
But that what I get (according to letter_opener) is:
| Test |
+----------+
As can you see, the spaces that are repeating before Test. My question is how to prevent ActionMailer (or anything else what is destroying my beautiful table) from doing that.
Mailer code:
def remind(client, invoices)
#client = client
#company = #client.company
#invoices = invoices.to_a
days_left = #invoices.first.pay_date - Date.today
message = #client.group.messages.find_by_period days_left.to_i
raise 'No messages for this invoices.' if message.nil?
#template = message.template || if days_left < 0
t 'message.before'
elsif days_left > 0
t 'message.after'
else
t 'message.today'
end
#text = liquid_parse #template
#html = markdown_parse #text
mail(:to => #client.email, :subject => t('message.title'))
end
private
def markdown_parse(text)
markdown = Redcarpet::Markdown.new Redcarpet::Render::HTML,
:autolink => true, :space_after_headers => true
markdown.render text
end
def liquid_parse(text)
renderer = Liquid::Template.parse text
renderer.render 'company' => #company, 'invoice' => #invoice, 'client' => #client
end
I've found bug. It was caused by Premailer what I use to inline CSS in HTML part.
class InlineCSSInterceptor
def self.delivering_email(message)
#message.text_part.body = Premailer.new(message.text_part.body.to_s, with_html_string: true).to_plain_text # this is line causing the problem.
message.html_part.body = Premailer.new(message.html_part.body.to_s, with_html_string: true).to_inline_css
end
end
Mailer.register_interceptor InlineCSSInterceptor