app/models/miq_report.rb
class MiqReport < ApplicationRecord
include ActiveRecord::AttributeAccessorThatYamls
include Formatting
include Formatters
include Seeding
include ImportExport
include Generator
include Notification
include Schedule
include Search
include YamlImportExportMixin
serialize :cols
serialize :conditions
serialize :where_clause
serialize :include
serialize :col_order
serialize :headers
serialize :order
serialize :sortby
serialize :categories
serialize :timeline
serialize :graph
serialize :db_options
serialize :generate_cols
serialize :generate_rows
serialize :col_formats
serialize :col_options
serialize :rpt_options
serialize :display_filter
validates_presence_of :name, :title, :db, :rpt_group
validates :name, :uniqueness_when_changed => true
validates_inclusion_of :rpt_type, :in => %w[Default Custom]
has_many :miq_report_results, :dependent => :destroy
belongs_to :time_profile
belongs_to :miq_group
belongs_to :user
has_many :miq_widgets, :as => :resource
virtual_column :human_expression, :type => :string
virtual_column :based_on, :type => :string
virtual_column :col_format_with_defaults, :type => :string_set
alias_attribute :menu_name, :name
attr_accessor :ext_options
attr_accessor_that_yamls :table, :sub_table, :filter_summary, :extras, :ids, :scoped_association, :html_title, :file_name,
:extras, :record_id, :tl_times, :user_categories, :trend_data, :performance, :include_for_find,
:report_run_time, :chart
attr_accessor_that_yamls :reserved, :skip_references # For legacy imports
GROUPINGS = [[:min, N_("Minimum"), N_("Minima")], [:avg, N_("Average"), N_("Averages")], [:max, N_("Maximum"), N_("Maxima")], [:total, N_("Total"), N_("Totals")]].freeze
PIVOTS = [[:min, "Minimum"], [:avg, "Average"], [:max, "Maximum"], [:total, "Total"]]
IMPORT_CLASS_NAMES = %w[MiqReport].freeze
scope :for_user, lambda { |user|
if user.report_admin_user?
all
else
where(
arel_table[:rpt_type].eq('Custom').and(arel_table[:miq_group_id].in(user.current_tenant.miq_groups.pluck(:id)))
.or(
arel_table[:rpt_type].eq('Default')
)
)
end
}
# Scope on reports that have report results.
#
# Valid options are:
# ::miq_groups: An MiqGroups instance on which to filter
# ::miq_group_ids: An MiqGroup ids on which to filter
def self.having_report_results(options = {})
miq_group_ids = options[:miq_groups].collect(&:id) unless options[:miq_groups].nil?
miq_group_ids ||= options[:miq_group_ids]
joins(:miq_report_results).merge(MiqReportResult.for_groups(miq_group_ids)).distinct
end
def col_format_with_defaults
return [] unless cols.present?
cols.each_with_index.map do |column, index|
column_format = col_formats.try(:[], index)
if column_format
column_format
else
column = Chargeback.default_column_for_format(column.to_s) if Chargeback.db_is_chargeback?(db)
expression_col = col_to_expression_col(column)
column_type = MiqExpression::Target.parse(expression_col).column_type
MiqReport::Formats.default_format_for_path(expression_col, column_type)
end
end
end
def conditions=(exp)
exp = MiqExpression.new(exp) if exp && !exp.kind_of?(MiqExpression)
write_attribute("conditions", exp)
end
# NOTE: this can by dynamically manipulated
def cols
self[:cols] ||= (self[:col_order] || []).reject { |x| x.include?(".") }
end
def human_expression
conditions.to_human
end
def based_on
Dictionary.gettext(db, :type => :model, :notfound => :titleize)
end
def view_filter_columns
col_order.collect { |c| [headers[col_order.index(c)], c] }
end
def self.reportable_models
MiqExpression.base_tables
end
def self.get_expressions_by_model(db)
reports = where(:db => db.to_s, :template_type => "report")
.where.not(:conditions => nil)
.select(:id, :name, :conditions)
.to_a
# We have to redo the filtering on ruby side because nil conditions
# can be serialized as non-NULL value in the database.
reports = reports.select(&:conditions)
reports.each_with_object({}) { |report, hash| hash[report.name] = report.id }
end
def self.get_col_info(path)
data_type = MiqExpression::Target.parse(path).column_type
{
:data_type => data_type,
:available_formats => get_available_formats(path, data_type),
:default_format => Formats.default_format_for_path(path, data_type),
:numeric => [:integer, :decimal, :fixnum, :float].include?(data_type)
}
end
def list_schedules
exp = MiqExpression.new("=" => {"field" => "MiqReport-id",
"value" => id})
MiqSchedule.filter_matches_with(exp)
end
def add_schedule(data)
params = data
params['name'] ||= name
params['description'] ||= title
params['filter'] = MiqExpression.new("=" => {"field" => "MiqReport-id",
"value" => id})
params['resource_type'] = "MiqReport"
params['prod_default'] = "system"
MiqSchedule.create!(params)
end
def db_class
@db_class ||= db.kind_of?(Class) ? db : Object.const_get(db)
end
def contains_records?
(extras.key?(:total_html_rows) && extras[:total_html_rows] > 0) ||
(table && !table.data.empty?)
end
def to_hash
keys = self.class.attr_accessor_that_yamls
keys.each_with_object(attributes.to_hash) { |k, h| h[k] = send(k) }
end
def ascending=(val)
self.order = val ? "Ascending" : "Descending"
end
def ascending?
order != "Descending"
end
def sort_col
sortby ? col_order.index(sortby.first) : 0
end
def column_is_hidden?(col, controller = nil)
return false unless col_options
@hidden_cols ||= col_options.keys.each_with_object([]) do |c, a|
if col_options[c][:hidden]
a << c
else
display_method = col_options[c][:display_method]&.to_sym
is_display_method_available = defined?(controller.class::DISPLAY_GTL_METHODS) && controller.class::DISPLAY_GTL_METHODS.include?(display_method) && controller.respond_to?(display_method)
if controller && display_method && is_display_method_available
# when this display_method returns true it means that column is displayed
is_column_hidden = !controller.try(display_method)
a << c if is_column_hidden
end
end
end
@hidden_cols.include?(col.to_s)
end
def self.from_hash(h)
new(h)
end
def page_size
rpt_options.try(:fetch_path, :pdf, :page_size) || "a4"
end
def all_custom_attributes_are_virtual_sql_attributes?
ca_va_cols = CustomAttributeMixin.select_virtual_custom_attributes(cols)
ca_va_cols.all? { |custom_attribute| va_sql_cols.include?(custom_attribute) }
end
def load_custom_attributes
return unless db_class < CustomAttributeMixin || Chargeback.db_is_chargeback?(db)
db_class.load_custom_attributes_for(cols.uniq)
end
# this method adds :custom_attributes => {} to MiqReport#include
# when report with virtual custom attributes is stored
# we need preload custom_attributes table to main query for building report for elimination of superfluous queries
def add_includes_for_virtual_custom_attributes
include[:custom_attributes] ||= {} if CustomAttributeMixin.select_virtual_custom_attributes(cols).present?
end
# this method removes loading (:custom_attributes => {}) relations for custom_attributes before report is built
# :custom_attributes => {} was added in method add_includes_for_virtual_custom_attributes in MiqReport#include
# vc_attributes == Virtual Custom Attributes
def remove_loading_relations_for_virtual_custom_attributes
vc_attributes = CustomAttributeMixin.select_virtual_custom_attributes(cols).present?
include.delete(:custom_attributes) if vc_attributes.present? && include && include[:custom_attributes].blank?
end
# determine name column from headers for x-axis in chart
def chart_header_column
if graph[:column].blank?
_log.error("The column for the chart's x-axis must be defined in the report")
return
end
chart_column = MiqExpression::Field.parse(graph[:column]).column
column_index = col_order.index { |col| col.include?(chart_column) }
headers[column_index]
end
def self.display_name(number = 1)
n_('Report', 'Reports', number)
end
def userid=(_userid)
# Stubbed method to handle 'userid' attr that may be present in the exported hash
# which does not exist in the MiqReport class
end
def group_description=(_group_description)
# Stubbed method to handle 'group_description' attr that may be present in the exported hash
# which does not exist in the MiqReport class
end
def columns_for_sorting(columns)
columns = columns.split(",") if columns && columns.kind_of?(String)
columns || sortby || col_order
end
def validate_sorting_columns(columns)
validate_columns(columns_for_sorting(columns))
end
def validate_columns(sorting_columns)
Array(sorting_columns).collect do |attr|
if cols_for_report.include?(attr)
attr
else
raise ArgumentError, N_("%{attribute} is not a valid attribute for %{name}") % {:attribute => attr, :name => name}
end
end.compact
end
def col_format_hash
@col_format_hash ||= col_order.zip(col_formats).to_h
end
def format_row(row, allowed_columns = nil, expand_value_format = nil)
tz = get_time_zone(User.current_user.settings.fetch_path(:display, :timezone).presence || Time.zone)
row.map do |key, _|
value = allowed_columns.nil? || allowed_columns&.include?(key) ? format_column(key, row, tz, col_format_hash[key]) : row[key]
[key, expand_value_format.present? ? {:value => value, :style_class => get_style_class(key, row, tz)} : value]
end.to_h
end
def format_result_set(result_set, skip_columns = nil, hash_value_format = nil)
result_set.map { |row| format_row(row, skip_columns, hash_value_format) }
end
def filter_result_set_record(record, filter_options)
filter_options.all? { |column, search_string| record[column].include?(search_string) }
end
def filter_result_set(result_set, filter_options)
validated_filter_columns = validate_columns(filter_options.keys)
formatted_result_set = format_result_set(result_set, validated_filter_columns)
result_set_filtered = formatted_result_set.select { |record| filter_result_set_record(record, filter_options) }
[result_set_filtered, result_set_filtered.count]
end
def self.default_use_sql_view
::Settings.reporting.use_sql_view
end
private
def va_sql_cols
@va_sql_cols ||= cols.select do |col|
db_class.virtual_attribute?(col) && db_class.attribute_supported_by_sql?(col)
end
end
end