require "datagrid/utils" require "active_support/core_ext/class/attribute" module Datagrid module Columns require "datagrid/columns/column" def self.included(base) base.extend ClassMethods base.class_eval do include Datagrid::Core class_attribute :default_column_options, :instance_writer => false self.default_column_options = {} class_attribute :batch_size class_attribute :columns_array self.columns_array = [] class_attribute :dynamic_block, :instance_writer => false class_attribute :cached self.cached = false end base.send :include, InstanceMethods end # self.included module ClassMethods ## # :method: batch_size= # # :call-seq: batch_size=(size) # # Specify a default batch size when generating CSV or just data # Default: 1000 # # self.batch_size = 500 # # Disable batches # self.batch_size = nil # ## # :method: batch_size # # :call-seq: batch_size # # Returns specified batch_size configuration variable # See batch_size= for more information # ## # :method: default_column_options= # # :call-seq: default_column_options=(options) # # Specifies default options for `column` method. # They still can be overwritten at column level. # # # Disable default order # self.default_column_options = { :order => false } # # Makes entire report HTML # self.default_column_options = { :html => true } # ## # :method: default_column_options # # :call-seq: default_column_options # # Returns specified default column options hash # See default_column_options= for more information # # Returns a list of columns defined. # All column definistion are returned by default # You can limit the output with only columns you need like: # # GridClass.columns(:id, :name) # # Supported options: # # * :data - if true returns only non-html columns. Default: false. def columns(*args) filter_columns(columns_array, *args) end # Defines new datagrid column # # Arguments: # # * name - column name # * query - a string representing the query to select this column (supports only ActiveRecord) # * options - hash of options # * block - proc to calculate a column value # # Available options: # # * :html - determines if current column should be present in html table and how is it formatted # * :order - determines if this column could be sortable and how. # The value of order is explicitly passed to ORM ordering method. # Ex: "created_at, id" for ActiveRecord, [:created_at, :id] for Mongoid # * :order_desc - determines a descending order for given column # (only in case when :order can not be easily reversed by ORM) # * :order_by_value - used in case it is easier to perform ordering at ruby level not on database level. # Warning: using ruby to order large datasets is very unrecommended. # If set to true - datagrid will use column value to order by this column # If block is given - datagrid will use value returned from block # * :mandatory - if true, column will never be hidden with #column_names selection # * :url - a proc with one argument, pass this option to easily convert the value into an URL # * :before - determines the position of this column, by adding it before the column passed here # * :after - determines the position of this column, by adding it after the column passed here # * :if - the column is shown if the reult of calling this argument is true # * :unless - the column is shown unless the reult of calling this argument is true # # See: https://github.com/bogdan/datagrid/wiki/Columns for examples def column(name, options_or_query = {}, options = {}, &block) define_column(columns_array, name, options_or_query, options, &block) end # Returns column definition with given name def column_by_name(name) find_column_by_name(columns_array, name) end # Returns an array of all defined column names def column_names columns.map(&:name) end def respond_to(&block) #:nodoc: Datagrid::Columns::Column::ResponseFormat.new(&block) end # Formats column value for HTML. # Helps to distinguish formatting as plain data and HTML # # column(:name) do |model| # format(model.name) do |value| # content_tag(:strong, value) # end # end def format(value, &block) if block_given? respond_to do |f| f.data { value } f.html do instance_exec(value, &block) end end else # Ruby Object#format exists. # We don't want to change the behaviour and overwrite it. super end end # Allows dynamic columns definition, that could not be defined at class level # # class MerchantsGrid # # scope { Merchant } # # column(:name) # # dynamic do # PurchaseCategory.all.each do |category| # column(:"#{category.name.underscore}_sales") do |merchant| # merchant.purchases.where(:category_id => category.id).count # end # end # end # end # # grid = MerchantsGrid.new # grid.data # => [ # # [ "Name", "Swimwear Sales", "Sportswear Sales", ... ] # # [ "Reebok", 2083382, 8382283, ... ] # # [ "Nike", 8372283, 18734783, ... ] # # ] def dynamic(&block) previous_block = dynamic_block self.dynamic_block = proc { instance_eval(&previous_block) if previous_block instance_eval(&block) } end def inherited(child_class) #:nodoc: super(child_class) child_class.columns_array = self.columns_array.clone end def filter_columns(columns, *args) #:nodoc: options = args.extract_options! args.compact! args.map!(&:to_sym) columns.select do |column| (!options[:data] || column.data?) && (!options[:html] || column.html?) && (column.mandatory? || args.empty? || args.include?(column.name)) end end def define_column(columns, name, options_or_query = {}, options = {}, &block) #:nodoc: if options_or_query.is_a?(String) query = options_or_query else options = options_or_query end check_scope_defined!("Scope should be defined before columns") block ||= lambda do |model| model.send(name) end position = Datagrid::Utils.extract_position_from_options(columns, options) column = Datagrid::Columns::Column.new(self, name, query, default_column_options.merge(options), &block) columns.insert(position, column) end def find_column_by_name(columns,name) #:nodoc: return name if name.is_a?(Datagrid::Columns::Column) columns.find do |col| col.name.to_sym == name.to_sym end end end # ClassMethods module InstanceMethods def assets driver.append_column_queries(super, columns.select(&:query)) end # Returns Array of human readable column names. See also "Localization" section # # Arguments: # # * column_names - list of column names if you want to limit data only to specified columns def header(*column_names) data_columns(*column_names).map(&:header) end # Returns Array column values for given asset # # Arguments: # # * column_names - list of column names if you want to limit data only to specified columns def row_for(asset, *column_names) data_columns(*column_names).map do |column| data_value(column, asset) end end # Returns Hash where keys are column names and values are column values for the given asset def hash_for(asset) result = {} self.data_columns.each do |column| result[column.name] = data_value(column, asset) end result end # Returns Array of Arrays with data for each row in datagrid assets without header. # # Arguments: # # * column_names - list of column names if you want to limit data only to specified columns def rows(*column_names) map_with_batches do |asset| self.row_for(asset, *column_names) end end # Returns Array of Arrays with data for each row in datagrid assets with header. # # Arguments: # # * column_names - list of column names if you want to limit data only to specified columns def data(*column_names) self.rows(*column_names).unshift(self.header(*column_names)) end # Return Array of Hashes where keys are column names and values are column values # for each row in filtered datagrid relation. # # Example: # # class MyGrid # scope { Model } # column(:id) # column(:name) # end # # Model.create!(:name => "One") # Model.create!(:name => "Two") # # MyGrid.new.data_hash # => [{:name => "One"}, {:name => "Two"}] # def data_hash map_with_batches do |asset| hash_for(asset) end end # Returns a CSV representation of the data in the grid # You are able to specify which columns you want to see in CSV. # All data columns are included by default # Also you can specify options hash as last argument that is proxied to # Ruby CSV library. # # Example: # # grid.to_csv # grid.to_csv(:id, :name) # grid.to_csv(:col_sep => ';') def to_csv(*column_names) options = column_names.extract_options! csv_class.generate( {:headers => self.header(*column_names), :write_headers => true}.merge(options) ) do |csv| each_with_batches do |asset| csv << row_for(asset, *column_names) end end end # Returns all columns selected in grid instance # # Examples: # # MyGrid.new.columns # => all defined columns # grid = MyGrid.new(:column_names => [:id, :name]) # grid.columns # => id and name columns # grid.columns(:id, :category) # => id and category column def columns(*args) self.class.filter_columns(columns_array, *args).select {|column| column.enabled?(self)} end # Returns all columns that can be represented in plain data(non-html) way def data_columns(*names) options = names.extract_options! options[:data] = true names << options self.columns(*names) end # Returns all columns that can be represented in HTML table def html_columns(*names) options = names.extract_options! options[:html] = true names << options self.columns(*names) end # Finds a column definition by name def column_by_name(name) self.class.find_column_by_name(columns_array, name) end # Gives ability to have a different formatting for CSV and HTML column value. # # Example: # # column(:name) do |model| # format(model.name) do |value| # content_tag(:strong, value) # end # end # # column(:company) do |model| # format(model.company.name) do # render :partial => "company_with_logo", :locals => {:company => model.company } # end # end def format(value, &block) if block_given? self.class.format(value, &block) else # don't override Object#format method super end end # Returns an object representing a grid row. # Allows to access column values # # Example: # # class MyGrid # scope { User } # column(:id) # column(:name) # column(:number_of_purchases) do |user| # user.purchases.count # end # end # # row = MyGrid.new.data_row(User.last) # row.id # => user.id # row.number_of_purchases # => user.purchases.count def data_row(asset) ::Datagrid::Columns::DataRow.new(self, asset) end # Defines a column at instance level # # See Datagrid::Columns::ClassMethods#column for more info def column(name, options_or_query = {}, options = {}, &block) #:nodoc: self.class.define_column(columns_array, name, options_or_query, options, &block) end def initialize(*) #:nodoc: self.columns_array = self.class.columns_array.clone instance_eval(&dynamic_block) if dynamic_block super end # Returns all columns available for current grid configuration # # class MyGrid # filter(:search) # column(:id) # column(:name, :mandatory => true) # column(:search_match, :if => proc {|grid| grid.search.present? } # end # # grid = MyGrid.new # grid.columns # => [ <#Column:name> ] # grid.available_columns # => [ <#Column:id>, <#Column:name> ] # grid.search = "keyword" # grid.available_columns # => [ <#Column:id>, <#Column:name>, <#Column:search_match> ] # def available_columns columns_array.select do |column| column.enabled?(self) end end # Return a cell data value for given column name and asset def data_value(column_name, asset) column = column_by_name(column_name) cache(column, asset, :data_value) do raise "no data value for #{column.name} column" unless column.data? result = generic_value(column, asset) result.is_a?(Datagrid::Columns::Column::ResponseFormat) ? result.call_data : result end end # Return a cell HTML value for given column name and asset and view context def html_value(column_name, context, asset) column = column_by_name(column_name) cache(column, asset, :html_value) do if column.html? && column.html_block value_from_html_block(context, asset, column) else result = generic_value(column, asset) result.is_a?(Datagrid::Columns::Column::ResponseFormat) ? result.call_html(context) : result end end end def generic_value(column, model) #:nodoc: cache(column, model, :generic_value) do unless column.enabled?(self) raise Datagrid::ColumnUnavailableError, "Column #{column.name} disabled for #{inspect}" end if column.data_block.arity >= 1 Datagrid::Utils.apply_args(model, self, data_row(model), &column.data_block) else model.instance_eval(&column.data_block) end end end protected def cache(column, asset, type) @cache ||= {} unless cached? @cache.clear return yield end key = cache_key(asset) unless key raise(Datagrid::CacheKeyError, "Datagrid Cache key is #{key.inspect} for #{asset.inspect}.") end @cache[column.name] ||= {} @cache[column.name][key] ||= {} @cache[column.name][key][type] ||= yield end def cache_key(asset) if cached.respond_to?(:call) cached.call(asset) else driver.default_cache_key(asset) end rescue NotImplementedError raise Datagrid::ConfigurationError, "#{self} is setup to use cache. But there was appropriate cache key found for #{asset.inspect}. Please set cached option to block with asset as argument and cache key as returning value to resolve the issue." end def map_with_batches(&block) result = [] each_with_batches do |asset| result << block.call(asset) end result end def each_with_batches(&block) if batch_size && batch_size > 0 driver.batch_each(assets, batch_size, &block) else assets.each(&block) end end def csv_class if RUBY_VERSION >= "1.9" require 'csv' CSV else require "fastercsv" FasterCSV end end def value_from_html_block(context, asset, column) args = [] remaining_arity = column.html_block.arity if column.data? args << data_value(column, asset) remaining_arity -= 1 end args << asset if remaining_arity > 0 args << self if remaining_arity > 1 context.instance_exec(*args, &column.html_block) end end # InstanceMethods class DataRow def initialize(grid, model) @grid = grid @model = model end def method_missing(meth, *args, &blk) @grid.data_value(meth, @model) end end end end