module Netzke module Basepack module Columns extend ActiveSupport::Concern COLUMN_METHOD_NAME = "%s_column" module ClassMethods # Overrides a column config, e.g.: # # column :title do |c| # c.flex = 1 # end def column(name, &block) method_name = COLUMN_METHOD_NAME % name define_method(method_name, &block) end end # Returns the list of (non-normalized) columns to be used. By default returns the list of model column names. # Can be overridden. def columns config.columns || data_adapter.model_attributes end # An array of complete columns configs ready to be passed to the JS side. # The +options+ hash can have the following keys: # * :with_excluded - when true, include the columns that are marked as excluded # * :with_meta - when true, include the meta column def final_columns(options = {}) @_final_columns ||= {} @_final_columns[options] ||= [].tap do |cols| initial_columns(true).each do |c| name = c.name # merge with column declaration send(:"#{name}_column", c) if respond_to?(:"#{name}_column") # set the defaults as lowest priority augment_column_config(c) cols << c if options[:with_excluded] || !c.excluded end append_meta_column(cols) if options[:with_meta] end end # Columns as a hash, for easier access to a specific column def final_columns_hash @_final_columns_hash ||= final_columns.inject({}){|r,c| r.merge(c[:name].to_sym => c)} end # Columns from config.columns or the `columns` method, after normalization def initial_columns(with_excluded = false) @_initial_columns ||= {} @_initial_columns[with_excluded] ||= [].tap do |cols| has_primary_column = false columns.each do |c| # normalize: # * :title => {name: 'title'} # * {name: :some_column} => {name: 'some_column'} c = ActiveSupport::OrderedOptions.new.replace(c.is_a?(Symbol) ? {name: c.to_s} : c.merge(name: c[:name].to_s)) cols << c if with_excluded || !c.excluded # detect primary key column has_primary_column ||= c.name == data_adapter.primary_key_name end # automatically add a column that reflects the primary key cols.insert(0, ActiveSupport::OrderedOptions.new.replace(:name => data_adapter.primary_key_name)) unless has_primary_column end end def append_meta_column(cols) cols << {}.tap do |c| c.merge!( :name => "meta", :meta => true, :getter => lambda do |r| meta_data(r) end ) c[:default_value] = meta_default_data if meta_default_data.present? end end # default_value for the meta column; used when a new record is being created in the grid def meta_default_data default_association_values(final_columns_hash).present? ? { :association_values => default_association_values(final_columns_hash).literalize_keys } : {} end # Override it when you need extra meta data to be passed through the meta column def meta_data(r) { :association_values => data_adapter.assoc_values(r, final_columns_hash).literalize_keys } end private # Based on initial column config, e.g.: # # {:name=>"author__name", :attr_type=>:string} # # augment it with additional configuration params, e.g.: # # {:name=>"author__name", :attr_type=>:string, :editor=>{:xtype=>:netzkeremotecombo}, :assoc=>true, :virtual=>true, :header=>"Author name", :editable=>true, :sortable=>false, :filterable=>false} # # It may be handy to override it. def augment_column_config(c) set_default_attr_type(c) set_default_xtype(c) set_default_virtual(c) set_default_text(c) set_default_editable(c) set_default_editor(c) set_default_width(c) set_default_hidden(c) set_default_sortable(c) set_default_filterable(c) c[:assoc] = association_attr?(c) # needed on the JS side end def set_default_attr_type(c) c[:attr_type] ||= association_attr?(c) ? :integer : data_adapter.attr_type(c.name) end def set_default_xtype(c) return if c[:renderer] || c[:editor] # if user set those manually, we don't mess with column xtype c[:xtype] ||= attr_type_to_xtype_map[c[:attr_type]] end def set_default_text(c) c[:text] ||= c[:label] || data_adapter.human_attribute_name(c[:name]) end def set_default_editor(c) # if shouldn't be editable, don't set any default editor; also, specifying xtype takes care of the editor return if c[:read_only] || c[:editable] == false if association_attr?(c) set_default_association_editor(c) else c[:editor] ||= editor_for_attr_type(c[:attr_type]) end end def set_default_width(c) c[:width] ||= 50 if c[:attr_type] == :boolean c[:width] ||= 150 if c[:attr_type] == :datetime end def set_default_hidden(c) c[:hidden] = true if data_adapter.primary_key_attr?(c) && c[:hidden].nil? end def set_default_editable(c) if c[:editable].nil? c[:editable] = is_editable_column?(c) end end def set_default_sortable(c) # this *has* to be set to false if we don't want the column to be sortable (it's sortable by default in Ext) c[:sortable] = !(c[:virtual] && !c[:sorting_scope]) if c[:sortable].nil? end def set_default_filterable(c) c[:filterable] = !c[:virtual] if c[:filterable].nil? end # Detects an association column and sets up the proper editor. def set_default_association_editor(c) assoc, assoc_method = c[:name].split('__') return unless assoc assoc_method_type = data_adapter.get_assoc_property_type assoc, assoc_method # if association column is boolean, display a checkbox (or alike), otherwise - a combobox (or alike) if c[:nested_attribute] c[:editor] ||= editor_for_attr_type(assoc_method_type) else c[:editor] ||= assoc_method_type == :boolean ? editor_for_attr_type(:boolean) : editor_for_association end end # If the column should be editable def is_editable_column?(c) not_editable_if = data_adapter.primary_key_attr?(c) not_editable_if ||= c[:virtual] && !association_attr?(c[:name]) not_editable_if ||= c[:read_only] editable_if = data_adapter.attribute_names.include?(c[:name]) editable_if ||= data_class.instance_methods.map(&:to_s).include?("#{c[:name]}=") editable_if ||= association_attr?(c[:name]) editable_if && !not_editable_if end def initial_columns_order final_columns.map do |c| # copy the values that are not null {name: c[:name]}.tap do |r| r[:width] = c[:width] if c[:width] r[:hidden] = c[:hidden] if c[:hidden] end end end def columns_order if config[:persistence] state[:columns_order] = initial_columns_order if columns_have_changed? state[:columns_order] || initial_columns_order else initial_columns_order end end def columns_have_changed? init_column_names = initial_columns_order.map{ |c| c[:name].to_s }.sort stored_column_names = (state[:columns_order] || initial_columns_order).map{ |c| c[:name].to_s }.sort init_column_names != stored_column_names end # Column editor config for attribute type. def editor_for_attr_type(type) {:xtype => attr_type_to_editor_xtype_map[type] || :textfield} end # Column editor config for one-to-many association def editor_for_association {:xtype => :netzkeremotecombo} end # Hash that maps a column type to the editor xtype. Override if you want different editors. def attr_type_to_editor_xtype_map { :integer => :numberfield, :boolean => :checkbox, :date => :datefield, :datetime => :xdatetime, :text => :textarea, :string => :textfield } end def attr_type_to_xtype_map { # :integer => :numbercolumn, # don't like the default formatter :boolean => :checkcolumn, :date => :datecolumn, #:datetime => :datecolumn # TODO: replace with datetimepicker } end # Default fields that will be displayed in the Add/Edit/Search forms # When overriding this method, keep in mind that the fields inside the layout must be expanded (each field represented by a hash, not just a symbol) def default_fields_for_forms selected_columns = final_columns.select do |c| data_adapter.attribute_names.include?(c[:name]) || data_class.instance_methods.include?("#{c[:name]}=") || association_attr?(c[:name]) end selected_columns.map do |c| field_config = { :name => c[:name], :field_label => c[:text] || c[:header] } # scopes for combobox options field_config[:scopes] = c[:editor][:scopes] if c[:editor].is_a?(Hash) field_config.merge!(c[:editor] || {}) field_config end end def columns_default_values final_columns.inject({}) do |r,c| assoc_name, assoc_method = c[:name].split '__' if c[:default_value].nil? r else if assoc_method r.merge(data_adapter.foreign_key_for(assoc_name) || data_adapter.foreign_key_for(assoc_name) => c[:default_value]) else r.merge(c[:name] => c[:default_value]) end end end end # Recursively traversess items (an array) and yields each found field (a hash with :name set) def each_attr_in(items) items.each do |item| if item.is_a?(Hash) each_attr_in(item[:items]) if item[:items].is_a?(Array) yield(item) if item[:name] end end end end end end