theforeman/foreman

View on GitHub
app/models/report_composer.rb

Summary

Maintainability
A
1 hr
Test Coverage
class ReportComposer
  include ActiveModel::Model

  class InputValue
    include ActiveModel::Model

    attr_accessor :value, :template_input

    validates :value, :presence => true, :if => proc { |v| v.template_input.required? || v.value.present? }

    validates :value, :inclusion => { :in => proc { |v| options_for_template_input v.template_input } },
              :if => proc { |v| v.template_input.input_type == 'user' && v.template_input.options_array.present? }

    class << self
      private

      def options_for_template_input(template_input)
        options = template_input.options_array
        options += [''] unless template_input.required?
        options
      end
    end
  end

  class ParamParser
    attr_reader :raw_params

    def mail_to
      raw_params[:mail_to]
    end

    def gzip?
      raw_params[:gzip].nil? ? send_mail? : !!raw_params[:gzip]
    end

    def send_mail?
      raw_params['send_mail'].to_s == '1'
    end

    def generate_at
      raw_params['generate_at']&.to_time
    end

    def format
      raw_params['format']
    end

    def params
      { template_id: raw_params[:id],
        generate_at: generate_at,
        gzip: gzip?,
        send_mail: send_mail?,
        mail_to: mail_to,
        format: format }.with_indifferent_access
    end
  end

  class UiParams < ParamParser
    def initialize(raw_params)
      @raw_params = raw_params.permit!
    end

    def send_mail?
      report_base_params['send_mail'].to_s == '1'
    end

    def mail_to
      report_base_params[:mail_to]
    end

    def generate_at
      Time.zone.parse(report_base_params['generate_at']) if report_base_params['generate_at'].present?
    end

    def format
      report_base_params['format']
    end

    def params
      super.merge(input_values: report_base_params[:input_values])
    end

    def report_base_params
      (raw_params[:report_template_report] || {}).to_hash.with_indifferent_access
    end
  end

  class ApiParams < ParamParser
    def initialize(raw_params)
      @raw_params = raw_params.permit!
    end

    def send_mail?
      !!mail_to
    end

    def generate_at
      Time.find_zone("UTC").parse(raw_params['generate_at']) if raw_params['generate_at'].present?
    end

    def params
      super.merge(
        input_values: convert_input_names_to_ids(
          raw_params[:id],
          (raw_params[:input_values] || {}).to_hash
        ),
        format: raw_params[:report_format]
      )
    end

    def convert_input_names_to_ids(template_id, input_values)
      inputs = TemplateInput.where(:template_id => template_id, :name => input_values.keys)
      Hash[inputs.map { |i| [i.id.to_s, 'value' => input_values[i.name]] }]
    end
  end

  class MailToValidator < ActiveModel::EachValidator
    MAIL_DELIMITER = ','

    def validate_each(model, attribute, value)
      return if value.empty?
      value.split(MAIL_DELIMITER).each do |mail|
        mail_validator.validate_each(model, attribute, mail.strip)
      end
    end

    def mail_validator
      @mail_validator ||= EmailValidator.new(attributes: attributes)
    end
  end

  attr_reader :template, :generate_at

  validates :mail_to, mail_to: true, if: :send_mail?
  validate :valid_format

  def initialize(params)
    @params = params.with_indifferent_access
    @generate_at = @params.delete('generate_at')
    @template = load_report_template(@params[:template_id])
    @input_values = build_inputs(@template, @params[:input_values])
  end

  def self.from_ui_params(ui_params)
    new(UiParams.new(ui_params).params)
  end

  def self.from_api_params(api_params)
    new(ApiParams.new(api_params).params)
  end

  def build_inputs(template, input_values)
    inputs = {}.with_indifferent_access
    return inputs if template.nil?

    # process values from params (including empty hash)
    template.template_inputs.each do |input|
      val = input_values[input.id.to_s].try(:[], 'value') unless input_values.nil?
      val = input.default if val.blank?
      inputs[input.id.to_s] = InputValue.new(value: val, template_input: input)
    end

    inputs
  end

  def to_param
    @params.to_param
  end

  def to_params
    @params
  end

  def valid?
    res = super & @input_values.map { |_, input_value| input_value.valid? }.all?
    merge_input_errors unless res
    res
  end

  def template_input_values
    Hash[@input_values.map { |_, input_value| [input_value.template_input.name, input_value.value] }]
  end

  def input_value_for(input)
    @input_values[input.id.to_s]
  end

  def load_report_template(id)
    ReportTemplate.authorized(:generate_report_templates).find_by_id(id)
  end

  def gzip?
    !!@params['gzip']
  end

  def mime_type
    gzip? ? :gzip : format.mime_type
  end

  def format
    if (format = ReportTemplateFormat.find(@params['format']))
      format
    else
      Rails.logger.debug "Report format #{@params['format']} not found" if @params['format'].present?
      @template.supports_format_selection? ? ReportTemplateFormat.default : ReportTemplateFormat.system
    end
  end

  def valid_format
    return true if @params['format'].blank?
    format_ids = ReportTemplateFormat.all.map { |f| f.id.to_s }
    errors.add :format, "is unsupported, chose one of: %s" % format_ids.join(', ') unless format_ids.include?(@params['format'])
  end

  def send_mail?
    !!@params['send_mail']
  end
  alias_method :send_mail, :send_mail?

  def mail_to
    @params['mail_to'] || User.current.mail
  end

  def report_filename
    name = @template.suggested_report_name.to_s
    name += '.' + format.extension
    name += '.gz' if gzip?
    name
  end

  def schedule_rendering
    scheduler = TemplateRenderJob
    if generate_at
      scheduler = scheduler.set(wait_until: generate_at)
    end
    scheduler.perform_later(to_params, user_id: User.current.id)
  end

  def render(mode: Foreman::Renderer::REAL_MODE, **params)
    params[:params] = { :format => format }.merge(params.fetch(:params, {}))
    result = @template.render(mode: mode, template_input_values: template_input_values, **params)
    result = ActiveSupport::Gzip.compress(result) if gzip?
    result
  end

  private

  def merge_input_errors
    @input_values.each do |id, input_value|
      input_value.errors.full_messages.each do |message|
        errors.add :base, (_("Input %s: ") % input_value.template_input.name) + message
      end
    end
  end
end