Rails: adding to last object if conditions met - ruby-on-rails

I've got a method that scans an HTML string and sort of formats it for prawnpdf:
def format_for_prawn(pdf, string, colour)
body = Nokogiri::HTML::DocumentFragment.parse(string)
result = body.xpath('./*|./text()')
result.each do |breaker|
if breaker.name == "h3"
pdf.fill_color colour
pdf.text breaker.text.to_s, :size => 16
pdf.move_down 5
else
pdf.fill_color '#444444'
pdf.text breaker.text.to_s, :size => 10, :leading => 1
pdf.move_down 10
end
end
end
It works great for <h3>s. In the event that some mid-paragraph <b> (or similar) tags are found it starts a new paragraph because that's where Nokogiri broke the string--which is the correct behaviour.
How could I add the bolded string to the last pdf.text function instead of calling a new pdf.text which results in a new paragraph?
I thought about making an array out of it all but then it'll be out of order with the <h3>s.
Any help would be appreciated.

My first thought was to do a negative match :
body.xpath( './node()[not(self::b)]' )
Sadly, this would exclude <b> rather than ignoring it :
> body = Nokogiri::HTML::DocumentFragment.parse %(<h3><b>foo</b></h3><h3>bar</h3>fooz<b>baz</b>whatever); true
> body.xpath( './node()[not(self::b)]' ).to_a
[
[0] <h3>
<b>foo</b>
</h3>,
[1] <h3>bar</h3>,
[2] fooz,
[3] whatever
]
So, you'll have no choice but using a buffer, here : we can iterate through nodes first, to populate a buffer regarding if we should have a new line or not, then iterate this buffer to have your lines added to pdf :
buffer = []
body.xpath( './node()' ).each do |node|
if %w[text b].include? node.name
# add to previous line or create one
buffer << [] unless buffer.count
buffer.last << { node: node }
else
# set content and create a new line
buffer << [ { node: node, title: node.name == 'h3' } ]
buffer << []
end
end
# Now, each first level item in buffer is a line,
# containing elements we just have to concatenate text of
# to pass to `pdf#text`
buffer.each do |line|
text = line.map do |part|
node = part[ :node ]
inner = node.text.to_s
# restore <b> tag if you want bold style in pdf
node.name == 'b' ? "<b>#{inner}</b>" : inner
end.join
if line.first
if line.first[ :title ]
pdf.fill_color colour
pdf.text text, :size => 16
pdf.move_down 5
else
pdf.fill_color '#444444'
# inline_format ensure basic html formating is used, <b> in our case
# See http://prawn.majesticseacreature.com/docs/0.11.1/Prawn/Text.html#method-i-text
pdf.text text, size: 10, leading: 1, inline_format: true
pdf.move_down 10
end
end
end
Of course, all of this is considering you do not control original html. Else, you should place your text nodes inside <p> or something, and there would not be problems anymore.

Related

Ruby: transform a data structure into an excel sheet

I have processed a log file and created a data structure of below format
values = [ {:VM=>"VM_US_OLA_1"}
{:id=>"OLFTB51", :datum=>{"LAP"=>"6.93817", "YCC_OWER"=>"1.0391"}}
{:id=>"OLFTB10", :datum=>{"LAP_2"=>"2.72646", "CFG_ON"=>"15.9489746", "YCC_ON"=>".401794"}}
{:VM=>"VM_ASIA_FLO_1"}
{:id=>"LOPMLAP", :datum=>{"LAP"=>"1.81048584", "FM_ON"=>".00"} ]
values is an array.
I'm trying to create a spreadsheet of below format where only the VM column gets highlighted in green and for every VM set, a blank line highlighted in 'yellow' should be inserted. I tried multiple approaches and went thru the axlsx documentation too but unable to get the desired format.
My excel snippet below:
require 'axlsx'
p = Axlsx::Package.new
p.workbook.add_worksheet(:name => "Statistics") do |sheet|
style1 = sheet.styles.add_style(:bg_color => "EF0920", :fg_color => "FFFFFF", b:true)
style2 = sheet.styles.add_style(:bg_color => "00FF00", :fg_color => "FFFFFF", b:true)
sheet.add_row ["VM", "NAME", "DETAILS", "OCC"], :style => style1
values.each do |val|
sheet.add_row [ val[:VM], val[:id] ], :style =>[style2, nil]
val[:datum].each do |k, v|
sheet << ["", "", k, v]
end
end
sheet.add_row
end
p.serialize 'Stat.xlsx'
Any suggestions should be really helpful here, many thanks.
Expected Output
Current Output
Adding Log file and my code
Log
---- vm name ----
VM_US_OLA_1
OLFTB51
OWNER IN_GB
------------------------------ ----------
LAP 6.93817
YCC_OWER 1.0391
=========================================
---- vm name ----
OLFTB10
OWNER IN_GB
------------------------------ ----------
LAP_2 2.7266846
CFG_ON 15.9489746
YCC_ON .401794
=========================================
---- vm name ----
VM_ASIA_FLO_1
LOPMLAP
OWNER IN_GB
------------------------------ ----------
LAP 1.81048584
FM_ON .00
=========================================
---- vm name ----
INGTY_2
OWNER IN_GB
------------------------------ ----------
=========================================
so on of the same format
Code to process the logs
require 'csv'
values = []
total = File.read("final.log")
total.each_line do |line|
line.strip!
next if line.empty?
next if line.include?('selected') || line.include?('IN_GB')
next if ['-','='].include? line[0]
parts = line.split ' '
if parts.size == 1 and line.start_with?('size')
values[current += 1] = {vm: line.strip}
next
elsif parts.size == 1 and parts = /^(?!.*size_).*$/
values[current += 1] = {id: line, datum: {}}
next
end
parts.each_cons(2) do |key, value|
values[current][:datum][key] = value
end
end
puts values
The problem is with the structure of your data. Try to change to have the values of your array in this structure
{:VM=>"VM_US_OLA_1", :id=>"OLFTB51", :datum=>{"LAP"=>"6.93817", "YCC_OWER"=>"1.0391"}
What's happening is VM is one record in your array, and the other data is another record in the array, you need to merge them so you access them in the same loop iteration
EDIT
I guess how we parse the log file is a bit tricky, so I am going to leave it but I am sure that this part I am going to introduce can be done in the log file parsing
First, we can change the structure of the array like that (before p.workbook.add_worksheet)
dataset = []
values.each_with_index do |value|
if value[:VM].present? # or :vm, not sure about the key
dataset << value
dataset.last[:data] = []
else
dataset.last[:data] << value
end
end
Then this loop: values.each do |val|
Can be changed to dataset.each do |val|
Then you can continue with the rest of your logic:
sheet.add_row [ val[:VM], val[:data][0][:id] ], :style =>[style2, nil]
val[data].each do |record|
record[:datanum].each do |k, v|
The problem with your previous code is, you used to add a new row sheet.add_row at the end of each iteration, so this was messing things up as you were expecting the data you need to be just right after your VM but a new line has already been inserted

Prawn/PDF: Multiple formats/text on one line?

When I try the following code:
text "Hello "
text "World"
They render Hello on top of World instead of World right after Hello. I have some complicated formatting (highlighting, different font sizes etc) on text that I need on one line. I know that the :inline_formatting option exists but it seems this is too complicated to use that option.
I have the following code:
highlight_callback.rb:
class HighlightCallback
def initialize(options)
#color = options[:color]
#document = options[:document]
end
def render_behind(fragment)
original_color = #document.fill_color
#document.fill_color = #color
#document.fill_rectangle(fragment.top_left,
fragment.width,
fragment.height)
#document.fill_color = original_color
end
end
order.pdf.prawn:
highlight = HighlightCallback.new(:color => 'ffff00', :document => self)
#code....
text "Authorized Signature: "
formatted_text [{:text => "_" * 15, :callback => highlight }], :size => 20
which is producing the attached image. How can I get the signature line on the same level as the text?
Ruby 2.5.1
Rails 5.2.0
It's enough to change method text to text_box, i.e.:
bounding_box([0, cursor], width: 540, height: 40) do
stroke_color 'FFFF00'
stroke_bounds
date = 'Date: '
text_box date, style: :bold
text_box DateTime.now.strftime('%Y/%m/%d'), at: [bounds.left + width_of(date), cursor]
text_box "Signature ________________", align: :right
end
Example:
To place text at a exact position you can use text_box with the option :at.
You can get the width of your text with pdf.width_of(str) (use the same style optione :size etc. otherwise it will use the default settings to calculate)

Splitting a RGB value up for an xml feed

I have the following to generate an xml feed:
def export
#borders = Border.order("display_order_position ASC").all
require 'nokogiri'
builder = Nokogiri::XML::Builder.new do |xml|
xml.dict {
xml.key "colors"
xml.array {
#borders.each do |b|
xml.dict{
xml.key "type"
xml.string b.border_type
if b.border_type == "pattern"
xml.key "image"
xml.string b.pattern
elsif b.border_type == "texture"
xml.key "image"
xml.string b.texture
elsif b.border_type == "color"
xml.key "r"
xml.key "g"
xml.key "b"
end
}
end
}
}
end
puts builder.to_xml
end
For the colour option I have an rgb value like rgb(47, 69, 184) which I need to split into three separate values and I'm not sure how?
If you're certain that your colour value will always be a string in the form "rgb(x, y, z)", then you can use this regex:
rgb\((\d+), *(\d+), *(\d+)\)
That is: the literal string rgb(, a group of digits (\d+) followed by a comma and possibly some whitespace , *, two more similar entries, and finally a closing bracket \). Here's a Ruby demo:
color = "rgb(47, 69, 184)"
regex = /rgb\((\d+), *(\d+), *(\d+)\)/
result = regex.match(color)
p result[1] # "47"
p result[2] # "69"
p result[3] # "184"

One ' to much in string

I try to write to an string something like this:
arr << "Icd3code.create!({:text => '#{variable1}'})" + "\n"
My problem is that variable 1 is an string, that contains an ' :
variable1 = "Ami's house"
So that at the end the ouput of my code is this:
Icd3code.create!({:text => 'Ami's house'})
How you can see now i have one ' to much! I dont know what i can do to avoid this problem! Thanks
If I've understood, you want to loop over some input, building up a list of parameters, which you plan to later use to create some records. If that's the case, I think you're better off using hashes, instead of strings:
# Let's pretend this came from the big, bad, world
inputs = ["Ami's house", "Fred's house", "Jim's house"]
creation_params = []
inputs.each do |input|
creation_params << {:text => input}
end
Then you could create all the Icd3codes, like this:
creation_params.each do |params|
Icd3code.create!(params)
end
Or you could save them in a text file, for later:
File.open('dest', 'w') do |f|
f.write(creation_params.to_json)
end
variable1 = "Ami's house"
puts %Q[Icd3code.create!({:text => "#{variable1}"})] + "\n"
--output:--
Icd3code.create!({:text => "Ami's house"})

Prawn: Table of content with page numbers

I need to create a table of contents with Prawn. I have add_dest function calls in my code and the
right links in the table of content:
add_dest('Komplett', dest_fit(page_count - 1))
and
text "* <link anchor='Komplett'> Vollstaendiges Mitgliederverzeichnis </link>", :inline_format = true
This works and I get clickable links which forward me to the right pages. However, I need to have page numbers in the table of content. How do I get it printed out?
I would suggest a much simpler solution.
Use pdf.page_number to store the page number of all your sections in a hash as you populate the pages
In the code, output the table of contents after populating the rest of your pages. Insert the TOC into the doc in the right spot by navigating in the PDF pdf.go_to_page(page_num).
For example:
render "pdf/frontpage", p: p
toc.merge!(p.page_number => "Section_Title")
p.start_new_page
toc.merge!(p.page_number => "Section_Title")
render "pdf/calendar"
p.start_new_page
toc.merge!(p.page_number => "Section_Title")
render "pdf/another_section"
p.go_to_page(1)
p.start_new_page
toc.merge!(p.page_number => "Table of Contents")
render "pdf/table_of_contents", table_of_contents: toc
you should read the chapter on Outline in this document http://prawn.majesticseacreature.com/manual.pdf, p.96. It explains with examples on how to create TOC.
UPDATE
destinations, page_references = {}, {}
page_count.downto(1).each {|num| page_references[num] = state.store.object_id_for_page(num)}
dests.data.to_hash.each_value do |values|
values.each do |value|
value_array = value.to_s.split(":")
dest_name = value_array[0]
dest_id = value_array[1].split[0]
destinations[dest_name] = Integer(dest_id)
end
end
state.store.each do |reference|
if !(dest_name = destinations.key(reference.identifier)).nil?
puts "Destination - #{dest_name} is on Page #{page_references.key(Integer(reference.data[0].to_s.split[0]))}"
end
end
I also needed to create a dynamic TOC. I put together a quick spike that needs some clean-up but does pretty much what I want. I didn't include click-able links but they could easily be added. The example also assumes the TOC is being placed on the 2nd page of the document.
The basic strategy I used was to store the TOC in a hash. Each time I add a new section to the document that I want to appear in the TOC I add it to the hash, i.e.
#toc[pdf.page_count] = "the toc text for this section"
Then prior to adding the page numbers to the document I iterate thru the hash:
number_of_toc_entries_per_page = 10
offset = (#toc.count.to_f / number_of_toc_entries_per_page).ceil
#toc.each_with_index do |(key, value), index|
pdf.start_new_page if index % number_of_toc_entries_per_page == 0
pdf.text "#{value}.... page #{key + offset}", size: 38
end
Anyway, the full example is below, hope it helps.
require 'prawn'
class TocTest
def self.create
#toc = Hash.new
#current_section_header_number = 0 # used to fake up section header's
pdf = Prawn::Document.new
add_title_page(pdf)
21.times { add_a_content_page(pdf) }
fill_in_toc(pdf)
add_page_numbers(pdf)
pdf.render_file './output/test.pdf'
end
def self.add_title_page(pdf)
pdf.move_down 200
pdf.text "This is my title page", size: 38, style: :bold, align: :center
end
def self.fill_in_toc(pdf)
pdf.go_to_page(1)
number_of_toc_entries_per_page = 10
offset = (#toc.count.to_f / number_of_toc_entries_per_page).ceil
#toc.each_with_index do |(key, value), index|
pdf.start_new_page if index % number_of_toc_entries_per_page == 0
pdf.text "#{value}.... page #{key + offset}", size: 38
end
end
def self.add_a_content_page(pdf)
pdf.start_new_page
toc_heading = grab_some_section_header_text
#toc[pdf.page_count] = toc_heading
pdf.text toc_heading, size: 38, style: :bold
pdf.text "Here is the content for this section"
# randomly span a section over 2 pages
if [true, false].sample
pdf.start_new_page
pdf.text "The content for this section spans 2 pages"
end
end
def self.add_page_numbers(pdf)
page_number_string = 'page <page> of <total>'
options = {
at: [pdf.bounds.right - 175, 9],
width: 150,
align: :right,
size: 10,
page_filter: lambda { |pg| pg > 1 },
start_count_at: 2,
}
pdf.number_pages(page_number_string, options)
end
def self.grab_some_section_header_text
"Section #{#current_section_header_number += 1}"
end
end
I built a report generator featuring a clickable table of contents using code and ideas gathered from this discussion. Here is the relevant parts of the code, in case somebody else needs to do the same.
What it does:
include Prawn::View to use Prawn's methods without having to prefix them with pdf
insert a blank page where the table of contents will be displayed
add the document contents, using h1 and h2 helpers for titles
the h1 and h2 helpers store the position of headings in the document
rewind and generate the actual table of contents
indent subsections in the table of contents
right-align the dots between toc entry and page number for visual consistency
if the table doesn't fit on one page, it adds new pages and increments the relevant page numbers
add a PDF outline with the section and subsection titles for bonus points.
Enjoy!
PDF generator
class ReportPdf
include Prawn::View
COLOR_GRAY = 'BBBBBB' # Color used for the dots in the table of contents
def initialize(report)
#toc = []
#report = report
generate_report
end
private
def generate_report
add_table_of_contents
add_contents
update_table_of_contents
add_outline
end
def add_table_of_contents
# Insert a blank page, which will be filled in later using update_table_of_contents
start_new_page
end
def add_contents
#report.sections.each do |section|
h1(section.title, section.anchor)
section.subsections.each do |subsection|
h2(subsection.title, subsection.anchor)
# subsection contents
end
end
end
def update_table_of_contents
go_to_page(1) # Rewind to where the table needs to be displayed
text 'Table of contents', styles_for(:toc_title)
move_down 20
added_pages = 0
#toc.each do |entry|
unless fits_on_current_page?(entry[:name])
added_pages += 1
start_new_page
end
entry[:page] += added_pages
add_toc_line(entry)
entry[:subsections].each do |subsection_entry|
unless fits_on_current_page?(subsection_entry[:name])
added_pages += 1
start_new_page
end
subsection_entry[:page] += added_pages
add_toc_line(subsection_entry, true)
end
end
end
def add_outline
outline.section 'Table of contents', destination: 2
#toc.each do |entry|
outline.section entry[:name], destination: entry[:page] do
entry[:subsections].each do |subsection|
outline.page title: subsection[:name], destination: subsection[:page]
end
end
end
end
def h1(name, anchor)
add_anchor(anchor, name)
text name, styles_for(:h1)
end
def h2(name, anchor)
add_anchor(anchor, name, true)
text name, styles_for(:h2)
end
def styles_for(element = :p)
case element
when :toc_title then { size: 24, align: :center }
when :h1 then { size: 20, align: :left }
when :h2 then { size: 16, align: :left }
when :p then { size: 12, align: :justify }
end
end
def add_anchor(name, anchor, is_subsection = false)
add_dest anchor, dest_xyz(bounds.absolute_left, y + 20)
if is_subsection
#toc.last[:subsections] << { anchor: anchor, name: name, page: page_count }
else
#toc << { anchor: anchor, name: name, page: page_count, subsections: [] }
end
end
def add_toc_line(entry, is_subsection = false)
anchor = entry[:anchor]
name = entry[:name]
name = "#{Prawn::Text::NBSP * 5}#{name}" if is_subsection
page_number = entry[:page].to_s
dots_info = dots_for(name + ' ' + page_number)
float do
text "<link anchor='#{anchor}'>#{name}</link>", inline_format: true
end
float do
indent(dots_info[:dots_start], dots_info[:right_margin]) do
text "<color rgb='#{COLOR_GRAY}'>#{dots_info[:dots]}</color>", inline_format: true, align: :right
end
end
indent(dots_info[:dots_end]) do
text "<link anchor='#{anchor}'>#{page_number}</link>", inline_format: true, align: :right
end
end
def dots_for(text)
dot_width = text_width('.')
dots_start = text_width(text)
right_margin = text_width(' ') * 6
space_for_dots = bounds.width - dots_start - right_margin
dots = space_for_dots.negative? ? '' : '.' * (space_for_dots / dot_width)
dots_end = space_for_dots - right_margin
{
dots: dots,
dots_start: dots_start,
dots_end: dots_end,
right_margin: right_margin
}
end
def fits_on_current_page?(str)
remaining_height = bounds.top - bounds.absolute_top + y
height_of(str) < remaining_height
end
def text_width(str, size = 12)
font(current_font).compute_width_of(str, size: size)
end
def current_font
#current_font ||= font.inspect.split('<')[1].split(':')[0].strip
end
end
Using the generator
Using Rails, I generate PDFs from a report using the following code:
# app/models/report.rb
class Report < ApplicationRecord
# Additional methods
def pdf
#pdf ||= ReportPdf.new(self)
end
end
# app/controllers/reports_controller.rb
class ReportsController < ApplicationController
def show
respond_to do |format|
format.html
format.pdf do
doc = #report.pdf
send_data doc.render, filename: doc.filename, disposition: :inline, type: Mime::Type.lookup_by_extension(:pdf)
end
end
end

Resources