module ReportGenerator def self.included(base) base.send :extend, ReportGenerator::ClassMethods end class MissingColumn < RuntimeError; end module ClassMethods def real_columns self.report_columns.reject(&:virtual?) end def virtual_columns self.report_columns.select(&:virtual?) end def report_columns return acts_as_reportable_columns unless acts_as_reportable_columns.empty? if acts_as_reportable_options[:columns] then acts_as_reportable_options[:columns].each do |name| column = self.content_columns.detect {|column| column.name == name} raise MissingColumn, "Column #{name.inspect} does not exist" unless column acts_as_reportable_columns << ReportColumn.new(:human_name => column.human_name, :name => column.name, :model => self.name) end else self.content_columns.each do |column| acts_as_reportable_columns << ReportColumn.new(:human_name => column.human_name, :name => column.name, :model => self.name) end end if acts_as_reportable_options[:virtuals] then acts_as_reportable_options[:virtuals].each do |name| acts_as_reportable_columns << ReportColumn.new(:human_name => name.humanize, :name => name, :model => self.name, :virtual => true) end else # Add default virtual columns # Are we taggable ? if self.respond_to?(:find_tagged_with) then # Need new columns for tagged_all and tagged_any acts_as_reportable_columns << ReportColumn.new(:human_name => "Tagged any", :name => "tagged_any", :model => self.name, :virtual => true) acts_as_reportable_columns << ReportColumn.new(:human_name => "Tagged all", :name => "tagged_all", :model => self.name, :virtual => true) end end self.report_relationships.each_pair do |alias_name, relationship| klass_name = acts_as_reportable_options[:map][relationship.to_sym] || relationship.singularize klass = klass_name.to_s.classify.constantize relationship = relationship.singularize klass.real_columns.each do |column| acts_as_reportable_columns << ReportColumn.new(:human_name => [relationship.humanize, column.human_name].join(" ").downcase.capitalize, :name => [relationship.downcase, column.name].join("_"), :model => klass.name, :relationship => relationship.pluralize, :virtual => true, :table_name => alias_name) end end acts_as_reportable_columns end # Returns a Hash of +alias_name+ to +relationship_name+. The relationships are inferred # from methods named +join_on_+. The alias is inferred from +_as_+. Returns the plural # version of the table and alias names. # # == Examples # class Party # # Relationship from parties to addresses # def join_on_addresses # end # # # Relationship from parties to addresses aliased as cr_addresses # def join_on_addresses_as_cr_addresses # end # # # Relationship from parties to addresses (which is really contact_routes) aliased as cr_addresses # acts_as_reportable :map => {:addresses => :address_contact_route} # def join_on_addresses_as_cr_addresses # end # end def report_relationships returning({}) do |relationships| ms = self.methods ms.delete_if {|name| !name.starts_with?("join_on_")} ms.collect! {|name| name.sub("join_on_", "")} ms.each do |name| relationship_name, alias_name = name.split("_as_", 2) alias_name.if_nil { alias_name = relationship_name } relationships[alias_name] = relationship_name end end end # Options is a Hash accepting the following keys: # * :columns: The list of physical columns we are allowing # ourselves to be searched on. Other columns # can't be searched (in this model). By default, # we add #content_columns. # * :virtuals: A list of virtual columns that this object knows # about and can be searched on. By default, we add # +tagged_any+ and +tagged_all+ if the object is taggable. # * :map: Maps the relationship names to model names (relationship # must be pluralized, model is the underscored version of # the class' name). def acts_as_reportable(options={}) options = options.reverse_merge(:map => {}) write_inheritable_attribute(:acts_as_reportable_options, options) write_inheritable_attribute(:acts_as_reportable_columns, []) class_inheritable_reader :acts_as_reportable_options, :acts_as_reportable_columns end def to_count_sql(lines) self.flatten_sql_options(self.to_internal_count_sql(lines)) end def to_report_sql(lines) self.flatten_sql_options(self.to_internal_report_sql(lines)) end def flatten_sql_options(sql) returning(sql) do sql[:select] = sql[:select].flatten.map(&:strip).uniq.join(", ") if sql[:select] sql[:joins] = sql[:joins].flatten.map(&:strip).uniq.join(" ") if sql[:joins] sql[:group] = sql[:group].flatten.map(&:strip).uniq.join(", ") if sql[:group] sql[:order] = sql[:order].flatten.map(&:strip).uniq.join(", ") if sql[:order] sql[:having] = sql[:having].flatten.map(&:strip).uniq.map {|c| "(#{c})"}.join(" AND ") if sql[:having] if sql[:conditions] then sql[:conditions][0] = sql[:conditions][0].flatten.map(&:strip).uniq.map {|c| "(#{c})"}.join(" AND ") sql[:conditions][1] = sql[:conditions][1].flatten.inject({}) do |memo, value| memo.merge(value.symbolize_keys) end end sql.delete(:joins) if sql[:joins].blank? sql.delete(:order) if sql[:order].blank? sql.delete(:conditions) if sql[:conditions][0].blank? sql.delete(:group) if sql[:group].blank? sql.delete(:having) if sql[:having].blank? end end def to_internal_count_sql(lines) returning(self.to_internal_report_sql(lines)) do |options| options[:select] = options[:select].select {|e| e =~ /count\(/i} options[:select].unshift "COUNT(*) count_all" options.delete(:order) end end def to_internal_report_sql(lines) returning(:select => ["#{self.table_name}.*"], :joins => [], :order => [], :conditions => [[], []], :group => [], :having => []) do |sql| lines.reject {|l| l.field.blank?}.each do |line| self.send("#{line.field}_to_report_sql", line, sql) end end end def account Thread.current[:account] end def account=(value) Thread.current[:account] = value end def run_report(account, lines, options={}) self.account = account with_scope(:find => options) do self.find(:all, self.to_report_sql(lines)) end end def count_report(account, lines, options={}) self.account = account with_scope(:find => options) do self.find(:all, self.to_count_sql(lines)).first.count_all.to_i end end def tagged_all_to_report_sql(line, sql) ids = tagged_with_ids(:all, line) sql[:conditions][0] << "#{self.table_name}.#{self.primary_key} IN (:#{self.name.downcase}_ids)" sql[:conditions][1] << {"#{self.name.downcase}_ids".to_sym => ids} end def tagged_any_to_report_sql(line, sql) ids = tagged_with_ids(:any, line) sql[:conditions][0] << "#{self.table_name}.#{self.primary_key} IN (:#{self.name.downcase}_ids)" sql[:conditions][1] << {"#{self.name.downcase}_ids".to_sym => ids} end def tagged_with_ids(type, line) self.account.send(self.name.pluralize.downcase).find_tagged_with(type => line.value, :select => "#{self.table_name}.#{self.primary_key}").map(&:id) end def method_missing(symbol, *args) return super unless symbol.to_s =~ /_to_report_sql$/ line, sql = args attr_name = symbol.to_s.sub("_to_report_sql", "") self.report_columns.detect {|col| col.name == attr_name}.if_not_nil {|col| col.to_report_sql(line, sql, self)} end end end