app/lib/pdf/report.rb
# 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 InvoiceReportermodule PdfClass `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 == :dayPdf::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/43Pdf::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/44Pdf::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 == :dayPdf::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 endend