# Ruport : Extensible Reporting System # # acts_as_reportable.rb provides ActiveRecord integration for Ruport. # # Originally created by Dudley Flanders, 2006 # Revised and updated by Michael Milner, 2007 # Copyright (C) 2006-2007 Dudley Flanders / Michael Milner, All Rights Reserved. # # This is free software distributed under the same terms as Ruby 1.8 # See LICENSE and COPYING for details. # require "ruport" Ruport.quiet { require "active_record" } module Ruport # === Overview # # This module is designed to allow an ActiveRecord model to be converted to # Ruport's data structures. If ActiveRecord is available when Ruport is # loaded, this module will be automatically mixed into ActiveRecord::Base. # # Add the acts_as_reportable call to the model class that you want to # integrate with Ruport: # # class Book < ActiveRecord::Base # acts_as_reportable # belongs_to :author # end # # Then you can use the report_table method to get data from the # model using ActiveRecord. # # Book.report_table(:all, :include => :author) # module Reportable def self.included(base) #:nodoc: base.extend ClassMethods end # === Overview # # This module contains class methods that will automatically be available # to ActiveRecord models. # module ClassMethods # In the ActiveRecord model you wish to integrate with Ruport, add the # following line just below the class definition: # # acts_as_reportable # # Available options: # # :only:: an attribute name or array of attribute # names to include in the results, other # attributes will be excuded. # :except:: an attribute name or array of attribute # names to exclude from the results. # :methods:: a method name or array of method names # whose result(s) will be included in the # table. # :include:: an associated model or array of associated # models to include in the results. # # Example: # # class Book < ActiveRecord::Base # acts_as_reportable, :only => 'title', :include => :author # end # def acts_as_reportable(options = {}) cattr_accessor :aar_options, :aar_columns self.aar_options = options include Ruport::Reportable::InstanceMethods extend Ruport::Reportable::SingletonMethods end end # === Overview # # This module contains methods that will be made available as singleton # class methods to any ActiveRecord model that calls # acts_as_reportable. # module SingletonMethods # Creates a Ruport::Data::Table from an ActiveRecord find. Takes # parameters just like a regular find. # # Additional options include: # # :only:: An attribute name or array of attribute # names to include in the results, other # attributes will be excuded. # :except:: An attribute name or array of attribute # names to exclude from the results. # :methods:: A method name or array of method names # whose result(s) will be included in the # table. # :include:: An associated model or array of associated # models to include in the results. # :filters:: A proc or array of procs that set up # conditions to filter the data being added # to the table. # :transforms:: A proc or array of procs that perform # transformations on the data being added # to the table. # :record_class:: Specify the class of the table's # records. # :eager_loading:: Set to false if you don't want to # eager load included associations. # # The :only, :except, :methods, and :include options may also be passed # to the :include option in order to specify the output for any # associated models. In this case, the :include option must be a hash, # where the keys are the names of the associations and the values # are hashes of options. # # Any options passed to report_table will disable the options set by # the acts_as_reportable class method. # # Example: # # class Book < ActiveRecord::Base # belongs_to :author # acts_as_reportable # end # # Book.report_table(:all, :only => ['title'], # :include => { :author => { :only => 'name' } }).as(:html) # # Returns: # # an html version of the table with two columns, title from # the book, and name from the associated author. # # Example: # # Book.report_table(:all, :include => :author).as(:html) # # Returns: # # an html version of the table with all columns from books and authors. # # Note: column names for attributes of included models will be qualified # with the name of the association. # def report_table(number = :all, options = {}) only = options.delete(:only) except = options.delete(:except) methods = options.delete(:methods) includes = options.delete(:include) filters = options.delete(:filters) transforms = options.delete(:transforms) record_class = options.delete(:record_class) || Ruport::Data::Record self.aar_columns = [] unless options.delete(:eager_loading) == false options[:include] = get_include_for_find(includes) end data = [find(number, options)].flatten data = data.map {|r| r.reportable_data(:include => includes, :only => only, :except => except, :methods => methods) }.flatten table = Ruport::Data::Table.new(:data => data, :column_names => aar_columns, :record_class => record_class, :filters => filters, :transforms => transforms ) end # Creates a Ruport::Data::Table from an ActiveRecord find_by_sql. # # Additional options include: # # :filters:: A proc or array of procs that set up # conditions to filter the data being added # to the table. # :transforms:: A proc or array of procs that perform # transformations on the data being added # to the table. # :record_class:: Specify the class of the table's # records. # # Example: # # class Book < ActiveRecord::Base # belongs_to :author # acts_as_reportable # end # # Book.report_table_by_sql("SELECT * FROM books") # def report_table_by_sql(sql, options = {}) record_class = options.delete(:record_class) || Ruport::Data::Record filters = options.delete(:filters) transforms = options.delete(:transforms) self.aar_columns = [] data = find_by_sql(sql) data = data.map {|r| r.reportable_data }.flatten table = Ruport::Data::Table.new(:data => data, :column_names => aar_columns, :record_class => record_class, :filters => filters, :transforms => transforms) end private def get_include_for_find(report_option) includes = report_option.blank? ? aar_options[:include] : report_option if includes.is_a?(Hash) result = {} includes.each do |k,v| if v.empty? || !v[:include] result.merge!(k => {}) else result.merge!(k => get_include_for_find(v[:include])) end end result elsif includes.is_a?(Array) result = {} includes.each {|i| result.merge!(i => {}) } result else includes end end end # === Overview # # This module contains methods that will be made available as instance # methods to any ActiveRecord model that calls acts_as_reportable. # module InstanceMethods # Grabs all of the object's attributes and the attributes of the # associated objects and returns them as an array of record hashes. # # Associated object attributes are stored in the record with # "association.attribute" keys. # # Passing :only as an option will only get those attributes. # Passing :except as an option will exclude those attributes. # Must pass :include as an option to access associations. Options # may be passed to the included associations by providing the :include # option as a hash. # Passing :methods as an option will include any methods on the object. # # Example: # # class Book < ActiveRecord::Base # belongs_to :author # acts_as_reportable # end # # abook.reportable_data(:only => ['title'], :include => [:author]) # # Returns: # # [{'title' => 'book title', # 'author.id' => 'author id', # 'author.name' => 'author name' }] # # NOTE: title will only be returned if the value exists in the table. # If the books table does not have a title column, it will not be # returned. # # Example: # # abook.reportable_data(:only => ['title'], # :include => { :author => { :only => ['name'] } }) # # Returns: # # [{'title' => 'book title', # 'author.name' => 'author name' }] # def reportable_data(options = {}) options = options.merge(self.class.aar_options) unless has_report_options?(options) data_records = [get_attributes_with_options(options)] Array(options[:methods]).each do |method| if options[:qualify_attribute_names] m = "#{options[:qualify_attribute_names]}.#{method}" else m = "#{method}" end data_records.first[m] = send(method) end # Reorder columns to match options[:only] if Array === options[:only] cols = options[:only].map {|c| c.to_s } self.class.aar_columns = cols end self.class.aar_columns |= data_records.first.keys data_records = add_includes(data_records, options[:include]) if options[:include] data_records end private # Add data for all included associations # def add_includes(data_records, includes) include_has_options = includes.is_a?(Hash) associations = include_has_options ? includes.keys : Array(includes) associations.each do |association| existing_records = data_records.dup data_records = [] if include_has_options assoc_options = includes[association].merge({ :qualify_attribute_names => association }) else assoc_options = { :qualify_attribute_names => association } end association_objects = [send(association)].flatten.compact existing_records.each do |existing_record| if association_objects.empty? data_records << existing_record else association_objects.each do |obj| association_records = obj.reportable_data(assoc_options) association_records.each do |assoc_record| data_records << existing_record.merge(assoc_record) end self.class.aar_columns |= data_records.last.keys end end end end data_records end # Check if the options hash has any report options # (:only, :except, :methods, or :include). # def has_report_options?(options) options[:only] || options[:except] || options[:methods] || options[:include] end # Get the object's attributes using the supplied options. # # Use the :only or :except options to limit the attributes returned. # # Use the :qualify_attribute_names option to append the association # name to the attribute name as association.attribute # def get_attributes_with_options(options = {}) attrs = attributes attrs.slice!(*options[:only].map(&:to_s)) if options[:only] attrs.except!(*options[:except].map(&:to_s)) if options[:except] attrs = attrs.inject({}) {|h,(k,v)| h["#{options[:qualify_attribute_names]}.#{k}"] = v; h } if options[:qualify_attribute_names] attrs end end end end ActiveRecord::Base.send :include, Ruport::Reportable