3scale/porta

View on GitHub
app/lib/pdf/report.rb

Summary

Maintainability
B
5 hrs
Test Coverage
# frozen_string_literal: true
 
File `report.rb` has 286 lines of code (exceeds 250 allowed). Consider refactoring.
require 'SVG/Graph/DataPoint'
require 'SVG/Graph/Line'
 
# REFACTOR: extract abstract Report class, and DRY functionality with InvoiceReporter
module Pdf
Class `Report` has 28 methods (exceeds 20 allowed). Consider refactoring.
Pdf::Report has at least 28 methods
Pdf::Report has at least 6 instance variables
class Report
include Printer
 
Pdf::Report#service is a writable attribute
Pdf::Report#account is a writable attribute
Pdf::Report#pdf is a writable attribute
Pdf::Report#period is a writable attribute
Pdf::Report#report is a writable attribute
attr_accessor :account, :period, :pdf, :service, :report
 
METRIC_HEADINGS_DAY = ["Name", "Today's Total", "% Change"].freeze
METRIC_HEADINGS_WEEK = ["Name", "Week's Total", "% Change"].freeze
SIGNUP_HEADINGS = ["Name", "Registered on", "Email", "Plan"].freeze
TOP_USERS_HEADINGS = %w[Name Hits].freeze
USERS_HEADINGS = %w[Plan Users].freeze
 
def initialize(account, service, options = {})
@account = account
@service = service
@period = options[:period]&.to_sym || :day
# TODO: accept as parameter
@style = Pdf::Styles::Colored.new
@data = Pdf::Data.new(@account, @service, period: @period)
 
@pdf = Prawn::Document.new(
page_size: 'A4',
page_layout: :portrait,
compress: true)
 
set_default_font
end
 
Pdf::Report#generate has approx 14 statements
def generate
three_scale_logo
move_down 2
 
header
 
Pdf::Report#generate calls 'move_down 3' 5 times
move_down 3
traffic_graph
move_down 3
 
traffic_and_users
move_down 3
 
latest_users(10)
move_down 3
 
metrics
move_down 3
 
@pdf.render_file(pdf_file_path)
 
self
end
 
def deliver_notification?(user)
Pdf::Report#deliver_notification? refers to 'user' more than self (maybe move it to another class?)
user.notification_preferences.include?(notification_name) && user.admin?
end
 
Pdf::Report has missing safe method 'send_notification!'
def send_notification!
account.admins.map do |admin|
if deliver_notification?(admin)
NotificationMailer.public_send(notification_name, self, admin).deliver_now
else
Rails.logger.info "[PDF::Report] Skipping delivery of #{period} report to #{admin}"
end
end
end
 
def notification_name
case period
when :day then :daily_report
when :week then :weekly_report
else raise "unknown notification for period #{period}"
end
end
 
# REFACTOR: this class should not be responsible for mailing
def mail_report
PostOffice.report(self, print_period).deliver_now
end
 
def pdf_file_name
['report', @account.internal_domain, @service.id].join('-') + '.pdf'
end
 
def pdf_file_path
Rails.root.join('tmp', pdf_file_name)
end
 
def print_period
notification_name.to_s.split("_").map(&:capitalize).join(" ")
end
 
def header
@pdf.formatted_text([
Pdf::Report#header calls '@style[:period]' 3 times
{ text: print_period.to_s, **@style[:period] },
Pdf::Report#header calls '@style[:period][:size]' 2 times
{ text: " (", size: @style[:period][:size] },
{ text: "#{account.external_domain} - #{@service.name}", **@style[:domain] },
{ text: ")", size: @style[:period][:size] },
])
header_height = @style[:date][:size] + 2.mm
@pdf.repeat :all do
pdf.formatted_text_box header_text,
Pdf::Report#header calls '@pdf.margin_box' 2 times
at: [@pdf.margin_box.left, @pdf.margin_box.top + header_height],
width: @pdf.bounds.width,
height: header_height,
align: :right
end
end
 
def latest_users(count)
subtitle "Latest Signups"
table_if_data(@data.latest_users(count), SIGNUP_HEADINGS)
end
 
def traffic_and_users
two_columns do |column|
case column
when :left
if (users = @data.top_users)
subtitle 'Top Users'
table_if_data(users, TOP_USERS_HEADINGS, TABLE_HALF_WIDTH)
end
when :right
subtitle "Users"
table_if_data(@data.users, USERS_HEADINGS, TABLE_HALF_WIDTH)
end
end
end
 
Method `graph_key_formatter` has a Cognitive Complexity of 10 (exceeds 5 allowed). Consider refactoring.
Pdf::Report#graph_key_formatter has approx 8 statements
def graph_key_formatter(usage)
Pdf::Report tests '@period == :day' at least 3 times
if @period == :day
Pdf::Report#graph_key_formatter refers to 'point' more than self (maybe move it to another class?)
Pdf::Report#graph_key_formatter calls 'point % 4' 2 times
Pdf::Report#graph_key_formatter calls '(point % 4).zero?' 2 times
->(point) { (point % 4).zero? ? sprintf("%02d:00", point) : "" }
else
since = usage.dig(:period, :since)
granularity = usage.dig(:period, :granularity)
->(point) { (point % 4).zero? ? (since + point * granularity).strftime("%d %b") : "" }
end
end
 
Method `traffic_graph` has 28 lines of code (exceeds 25 allowed). Consider refactoring.
def traffic_graph
usage = @data.usage
 
return unless usage
 
options = {
Pdf::Report#traffic_graph refers to 'usage' more than self (maybe move it to another class?)
Pdf::Report#traffic_graph calls 'usage.dig(:metric, :name)' 2 times
graph_title: "Traffic - #{usage.dig(:metric, :name)}",
show_graph_title: true,
key: false,
area_fill: false,
show_data_values: false,
add_popups: false,
width: TABLE_FULL_WIDTH,
height: TABLE_FULL_WIDTH / 4,
step_x_labels: 4,
step_include_first_x_label: true,
Pdf::Report#traffic_graph calls 'usage[:values]' 4 times
fields: usage[:values].each_index.map(&graph_key_formatter(usage)),
show_x_title: false,
x_title: @period == :day ? "Hour" : "Week Days",
show_y_guidelines: true,
scale_integers: true,
scale_divisions: [2, (usage[:values].max - usage[:values].min) / 5].max,
number_format: IntegerWithDelimiterFormatter.new,
show_y_title: false,
y_title: usage.dig(:metric, :name),
y_title_location: :middle,
no_css: false,
}
 
graph = SVG::Graph::Line.new(options)
 
graph.add_data(data: usage[:values], title: options[:y_title])
 
@pdf.svg traffic_graph_style(graph.burn_svg_only)
end
 
Pdf::Report#traffic_graph_style has approx 14 statements
def traffic_graph_style(svg)
xml = Nokogiri::XML(svg)
style = xml.at_css("style")
css = CssParser::Parser.new
css.load_string!(style.text.gsub(/ff0000/i, "9273ED"))
traffic_graph_first_data_point(xml)
traffic_graph_y_align(xml)
traffic_graph_style_clean_up(css)
traffic_graph_style_guide_lines(css)
traffic_graph_style_axes(css)
traffic_graph_style_line_width(css)
traffic_graph_style_background(css)
traffic_graph_style_text(css)
 
style.content = css.to_s
xml.to_s
end
 
# TODO: remove this hack ofter fix is acepted upstream
# https://github.com/lumean/svg-graph2/pull/43
Pdf::Report#traffic_graph_first_data_point doesn't depend on instance state (maybe move it to another class?)
def traffic_graph_first_data_point(xml)
line = xml.at_css(".line1")
line[:d] = line[:d].sub(/^M.+L\s*(\S+\s+\S+)(.*)$/, 'M\1 L\2')
end
 
# TODO: remove this hack after fix is accepted upstream
# https://github.com/lumean/svg-graph2/pull/44
Pdf::Report#traffic_graph_y_align doesn't depend on instance state (maybe move it to another class?)
def traffic_graph_y_align(xml)
xml.css(".yAxisLabels").each do |label|
label[:x] = "-8"
label.delete("style")
label.delete("transform")
end
end
 
Pdf::Report#traffic_graph_style_background doesn't depend on instance state (maybe move it to another class?)
def traffic_graph_style_background(css)
desired = <<-EOT
.graphBackground {
fill:#ffffff;
}
EOT
 
css.remove_rule_set!(css.find_rule_sets([".graphBackground"]).first)
css.add_block!(desired)
end
 
def traffic_graph_style_text(css)
desired = <<-EOT
.xAxisLabels,.yAxisLabels {
fill:#909090;
font-size: 10px;
Pdf::Report#traffic_graph_style_text calls '@style[:font]' 2 times
font-family: "#{@style[:font]}", sans-serif; font-weight: normal;
}
.mainTitle {
fill:#505050;
font-family: "#{@style[:font]}", sans-serif; font-weight: normal;
}
EOT
 
css.add_block!(desired)
end
 
Pdf::Report#traffic_graph_style_axes doesn't depend on instance state (maybe move it to another class?)
def traffic_graph_style_axes(css)
desired = <<-EOT
.axis{
stroke: #ffffff;
stroke-width: 0px;
}
EOT
 
css.remove_rule_set!(css.find_rule_sets([".axis"]).first)
css.add_block!(desired)
end
 
Pdf::Report#traffic_graph_style_line_width doesn't depend on instance state (maybe move it to another class?)
def traffic_graph_style_line_width(css)
desired = <<-EOT
.line1 {
stroke-width: 2px;
}
EOT
 
css.add_block!(desired)
end
 
Pdf::Report#traffic_graph_style_guide_lines doesn't depend on instance state (maybe move it to another class?)
def traffic_graph_style_guide_lines(css)
desired = <<-EOT
.guideLines,#yAxis {
stroke: #eeeeee;
stroke-width: 0.3px;
stroke-dasharray: 0.01 1;
stroke-linejoin: round;
stroke-linecap: round;
}
EOT
 
css.remove_rule_set!(css.find_rule_sets([".guideLines"]).first)
css.add_block!(desired)
end
 
Pdf::Report#traffic_graph_style_clean_up doesn't depend on instance state (maybe move it to another class?)
def traffic_graph_style_clean_up(css)
(2..12).each do |num|
Pdf::Report#traffic_graph_style_clean_up contains iterators nested 2 deep
%w[line fill key dataPoint].each do |type|
rule = css.find_rule_sets([".#{type}#{num}"]).first
css.remove_rule_set!(rule) if rule.present?
end
end
end
 
def metrics
subtitle "Metrics"
return unless %i[day week].include? @period
 
header = self.class.const_get("METRIC_HEADINGS_#{@period}".upcase)
lines = @data.metrics.map { |name, total, percent| [name, total, colorize_num(percent)] }
table_with_header([header, *lines])
end
 
private
 
def header_text
format = @style[:date]
if @period == :day
Pdf::Report#header_text calls '1.day.ago' 2 times
Pdf::Report#header_text calls '1.day' 2 times
Pdf::Report#header_text calls '1.day.ago.to_date.to_s' 2 times
Pdf::Report#header_text calls '1.day.ago.to_date' 2 times
Pdf::Report#header_text calls '{ text: 1.day.ago.to_date.to_s }.merge!(format)' 2 times
[{ text: 1.day.ago.to_date.to_s }.merge!(format)]
else
[
{ text: 1.week.ago.to_date.to_s }.merge!(format),
{ text: " - " },
{ text: 1.day.ago.to_date.to_s }.merge!(format),
]
end
end
 
def three_scale_logo
logo = File.dirname(__FILE__) + "/images/logo.png"
image = @pdf.image logo, width: 100
dimensions = [
Pdf::Report#three_scale_logo calls '@pdf.bounds.absolute_left' 2 times
Pdf::Report#three_scale_logo calls '@pdf.bounds' 4 times
@pdf.bounds.absolute_left,
Pdf::Report#three_scale_logo calls '@pdf.bounds.absolute_top' 2 times
@pdf.bounds.absolute_top - image.scaled_height,
@pdf.bounds.absolute_left + image.scaled_width,
@pdf.bounds.absolute_top
]
url = PDF::Core::LiteralString.new("https://3scale.net")
@pdf.link_annotation(dimensions, Border: [0,0,0], A: { Type: :Action, S: :URI, URI: url})
end
 
def table_if_data(data, header, width = TABLE_FULL_WIDTH)
return @pdf.text("No current data", **@style[:small]) if data.blank?
 
table_with_header([header] + data, width: width)
end
 
# @param numstr [String] string representing a number
def colorize_num(numstr)
case numstr.to_f
when 0.0
numstr
when -Float::INFINITY...0
{ content: numstr, **@style[:red] }
else
{ content: numstr, **@style[:green] }
end
end
 
class IntegerWithDelimiterFormatter
include ActionView::Helpers::NumberHelper
 
def %(num)
number_with_delimiter(num.to_i)
end
end
end
end