netzke/netzke-basepack

View on GitHub
lib/netzke/basepack/columns.rb

Summary

Maintainability
A
1 hr
Test Coverage
module Netzke
  module Basepack
    # Takes care of grid column configuration, as well as the grid's default form fields
    # +Grid::Base+ extends common Ext JS column options with the following ones:
    #
    # [sorting_scope]
    #
    #   A Proc object used for sorting by the column. This can be useful for sorting by a virtual column. The Proc
    #   object will get the relation as the first parameter, and the sorting direction as the second. Example:
    #
    #     columns => [{ name: "complete_user_name", sorting_scope: lambda {|rel, dir| order("users.first_name #{dir.to_s}, users.last_name #{dir.to_s}") }, ...]
    #
    # [filter_with]
    #
    #   A Proc object that receives the relation, the value to filter by and the operator. This allows for more flexible
    #   handling of basic filters and enables filtering of virtual columns. Example:
    #
    #     columns => [{ name: "complete_user_name", filter_with: lambda{|rel, value, op| rel.where("first_name like ? or last_name like ?", "%#{value}%", "%#{value}%" ) } }, ...]
    #
    # [filterable]
    #
    #   Set to false to disable filtering on this column
    #
    # [editor]
    #
    #   A hash that will override the automatic editor configuration. For example, for one-to-many association column
    #   you may set it to +{min_chars: 1}+, which will be passed to the combobox and make it query its remote data after
    #   entering 1 character (instead of default 4).
    #
    # === Configuring default filters on grid columns
    #
    # Default Filters can either be configured on the grid itself
    #
    #     def configure(c)
    #       super
    #       c.default_filters = [{name: "Mark"}, {age: {gt: 10}}]
    #     end
    #
    # or as a component configuration
    #
    #      component :tasks |c|
    #        c.klass = TaskGrid
    #        c.default_filters = [{due_date: {before: Time.now}}]
    #      end
    #
    module Columns
      extend ActiveSupport::Concern

      COLUMN_METHOD_NAME = "%s_column"

      included do
        class_attribute :declared_columns
        self.declared_columns = []
      end

      module ClassMethods
        # Adds/overrides a column config, e.g.:
        #
        #     column :title do |c|
        #       c.flex = 1
        #     end
        #
        # If a new column is declared, it gets appended to the list of default columns.
        def column(name, &block)
          method_name = COLUMN_METHOD_NAME % name
          define_method(method_name, &block)
          self.declared_columns = [*declared_columns, name]
        end
      end

      # Returns the list of (non-normalized) columns to be used. By default returns the list of model column names and declared columns.
      # Can be overridden.
      def columns
        config.columns || attributes
      end

      # An array of complete columns configs ready to be passed to the JS side.
      def final_columns
        @final_columns ||= [].tap do |cols|
          has_primary_column = false

          columns.each do |name|
            c = build_column_config(name)
            next if c.excluded

            has_primary_column ||= c.primary?
            cols << c
          end

          insert_primary_column(cols) unless has_primary_column
          append_association_values_column(cols)
        end
      end

      def build_column_config(name)
        Netzke::Basepack::ColumnConfig.new(name, model_adapter).tap do |c|
          attribute_config = attribute_overrides[c.name.to_sym]
          c.merge_attribute(attribute_config) if attribute_config
          augment_column_config(c)
        end
      end

      # Array of complete config hashes for non-meta columns
      def non_meta_columns
        @non_meta_columns ||= final_columns.reject{|c| c[:meta]}
      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 that have to be used by the JS side of the grid
      def js_columns
        final_columns.each do |c|
          # we are removing the editor on this last step, so that the editor config is still being passed from the
          # column config to the form editor; refactor!
          c.delete(:editor) unless config.edits_inline
        end
      end

      def append_association_values_column(cols)
        cols << Netzke::Basepack::AttrConfig.new("association_values", model_adapter).tap do |c|
          c.meta = true
          c.getter = lambda do |r|
            model_adapter.assoc_values(r, final_columns_hash).netzke_literalize_keys
          end
          defaults = association_value_defaults(cols).netzke_literalize_keys
          c.default_value = defaults if defaults.present?
        end
      end

      def insert_primary_column(cols)
        primary_key = model_adapter.primary_key
        raise "Model #{model_adapter.model.name} does not have a primary column" if primary_key.blank?
        c = Netzke::Basepack::ColumnConfig.new(model_adapter.primary_key, model_adapter)
        c.merge_attribute(attribute_overrides[c.name.to_sym]) if attribute_overrides.has_key?(c.name.to_sym)
        augment_column_config(c)
        cols.insert(0, c)
      end

      # Default form items (non-normalized) that will be displayed in the Add/Edit forms
      def default_form_items
        attributes
      end

      # ATM the same attributes are used as in forms
      def attributes_for_search
        non_meta_columns.map do |c|
          {name: c.name, text: c.text, type: c.type}.tap do |a|
            if c[:assoc]
              a[:text].sub!("  ", " ")
            end
          end
        end
      end

      # Form items that will be used by the Add/Edit forms. May be overridden.
      def form_items
        config.form_items || default_form_items
      end

      private

      def populate_columns_with_filters(c)
        c.default_filters.each do |filter|
          c.columns[:items].each do |column|
            if column[:name].to_sym == filter[:column].to_sym
              extend_column_with_filter(column, filter)
            end
          end
        end
        c.delete(:default_filters)
      end

      def extend_column_with_filter(column, filter)
        if filter[:value].is_a?(Hash)
          val = {}
          filter[:value].each do |k,v|
            val[k] = (v.is_a?(Time) || v.is_a?(Date) || v.is_a?(ActiveSupport::TimeWithZone)) ? Netzke::Core::JsonLiteral.new("new Date('#{v.strftime("%m/%d/%Y")}')") : v
          end
        else
          val = filter[:value]
        end
        new_filter = {value: val, active: true}
        if column[:filter]
          column[:filter].merge! new_filter
        else
          column[:filter] = new_filter
        end
      end

      # Extends passed column config with DSL declaration for this column
      def apply_column_dsl(c)
        method_name = COLUMN_METHOD_NAME % c.name
        send(method_name, c) if respond_to?(method_name)
      end

      # Receives a +Netzke::Basepack::ColumnConfig+ with minimum column configuration and extends it according to the
      # attribute's type. May be overridden.
      def augment_column_config(c)
        apply_column_dsl(c)
        c.set_defaults
      end

      def initial_columns_order
        non_meta_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

      def columns_default_values
        non_meta_columns.inject({}) do |r,c|
          assoc_name, assoc_method = c[:name].split '__'
          if c[:default_value].nil?
            r
          else
            if assoc_method
              r.merge(model_adapter.foreign_key_for(assoc_name) || model_adapter.foreign_key_for(assoc_name) => c[:default_value])
            else
              r.merge(c[:name] => c[:default_value])
            end
          end
        end
      end
    end
  end
end