require 'active_record' require 'meta_where' require 'will_paginate' module Netzke module Basepack class GridPanel < Netzke::Base module Services extend ActiveSupport::Concern included do endpoint :get_data do |*args| params = args.first || {} # params are optional if !config[:prohibit_read] records = get_records(params) {:data => records.map{|r| r.to_array(columns)}, :total => config[:enable_pagination] && records.total_entries} else flash :error => "You don't have permissions to read data" {:feedback => @flash} end end endpoint :post_data do |params| mod_records = {} [:create, :update].each do |operation| data = ActiveSupport::JSON.decode(params["#{operation}d_records"]) if params["#{operation}d_records"] if !data.nil? && !data.empty? # data may be nil for one of the operations mod_records[operation] = process_data(data, operation) mod_records[operation] = nil if mod_records[operation].empty? end end on_data_changed { :update_new_records => mod_records[:create], :update_mod_records => mod_records[:update] || {}, :feedback => @flash } end endpoint :delete_data do |params| if !config[:prohibit_delete] record_ids = ActiveSupport::JSON.decode(params[:records]) data_class.destroy(record_ids) on_data_changed {:feedback => "Deleted #{record_ids.size} record(s)", :load_store_data => get_data} else {:feedback => "You don't have permissions to delete data"} end end endpoint :resize_column do |params| raise "Called api_resize_column while not configured to do so" if config[:enable_column_resize] == false columns[normalize_index(params[:index].to_i)][:width] = params[:size].to_i save_columns! {} end endpoint :move_column do |params| raise "Called api_move_column while not configured to do so" if config[:enable_column_move] == false remove_from = normalize_index(params[:old_index].to_i) insert_to = normalize_index(params[:new_index].to_i) column_to_move = columns.delete_at(remove_from) columns.insert(insert_to, column_to_move) save_columns! # reorder the columns on the client side (still not sure if it's not an overkill) # {:reorder_columns => columns.map(&:name)} # Well, I think it IS an overkill - commented out # until proven to be necessary {} end endpoint :hide_column do |params| raise "Called api_hide_column while not configured to do so" if config[:enable_column_hide] == false columns[normalize_index(params[:index].to_i)][:hidden] = params[:hidden].to_b save_columns! {} end # Returns choices for a column endpoint :get_combobox_options do |params| query = params[:query] column = columns.detect{ |c| c[:name] == params[:column] } scope = column.to_options[:scope] query = params[:query] {:data => combobox_options_for_column(column, :query => query, :scope => scope, :record_id => params[:id])} end endpoint :move_rows do |params| if defined?(ActsAsList) && data_class.ancestors.include?(ActsAsList::InstanceMethods) ids = JSON.parse(params[:ids]).reverse ids.each_with_index do |id, i| r = data_class.find(id) r.insert_at(params[:new_index].to_i + i + 1) end on_data_changed else raise RuntimeError, "Data class should 'acts_as_list' to support moving rows" end {} end end # # Some components' overridden API # ## Edit in form specific API def add_form__form_panel0__netzke_submit(params) res = component_instance(:add_form__form_panel0).netzke_submit(params) if res[:set_form_values] # successful creation on_data_changed res[:set_form_values] = nil end res.to_nifty_json end def edit_form__form_panel0__netzke_submit(params) res = component_instance(:edit_form__form_panel0).netzke_submit(params) if res[:set_form_values] on_data_changed res[:set_form_values] = nil end res.to_nifty_json end def multi_edit_form__multi_edit_form0__netzke_submit(params) ids = ActiveSupport::JSON.decode(params.delete(:ids)) data = ids.collect{ |id| ActiveSupport::JSON.decode(params[:data]).merge("id" => id) } data.map!{|el| el.delete_if{ |k,v| v.blank? }} # only interested in set values mod_records_count = process_data(data, :update).count # remove duplicated flash messages @flash = @flash.inject([]){ |r,hsh| r.include?(hsh) ? r : r.push(hsh) } if mod_records_count > 0 on_data_changed flash :notice => "Updated #{mod_records_count} records." {:set_result => "ok", :feedback => @flash}.to_nifty_json else {:feedback => @flash}.to_nifty_json end end # When providing the edit_form component, fill in the form with the requested record def deliver_component(params) components[:edit_form][:items].first.merge!(:record_id => params[:record_id].to_i) if params[:name] == 'edit_form' super end protected def get_records(params) # Restore params from component_session if requested if params[:with_last_params] params = component_session[:last_params] else # remember the last params component_session[:last_params] = params end # build initial relation based on passed params relation = get_relation(params) # addressing the n+1 query problem columns.each do |c| assoc, method = c[:name].split('__') relation = relation.includes(assoc.to_sym) if method end # apply sorting if needed if params[:sort] assoc, method = params[:sort].split('__') dir = params[:dir].downcase # if a sorting scope is set, call the scope with the given direction column = columns.detect { |c| c[:name] == params[:sort] } if column.has_key?(:sorting_scope) relation = relation.send(column[:sorting_scope].to_sym, dir.to_sym) else relation = if method.nil? relation.order(assoc.to_sym.send(dir)) else assoc = data_class.reflect_on_association(assoc.to_sym) relation.order(assoc.klass.table_name.to_sym => method.to_sym.send(dir)).joins(assoc.name) end end end # apply pagination if needed if config[:enable_pagination] per_page = config[:rows_per_page] page = params[:limit] ? params[:start].to_i/params[:limit].to_i + 1 : 1 relation.paginate(:per_page => per_page, :page => page) else relation.all end end # An ActiveRecord::Relation instance encapsulating all the necessary conditions def get_relation(params) # make params coming from Ext grid filters understandable by meta_where conditions = params[:filter] && convert_filters(params[:filter]) || {} relation = data_class.where(conditions) if params[:extra_conditions] extra_conditions = normalize_extra_conditions(ActiveSupport::JSON.decode(params[:extra_conditions])) relation = relation.extend_with_netzke_conditions(extra_conditions) if params[:extra_conditions] end relation = relation.extend_with(config[:scope]) if config[:scope] relation end # Override this method to react on each operation that caused changing of data def on_data_changed; end # Given an index of a column among enabled (non-excluded) columns, provides the index (position) in the table def normalize_index(index) norm_index = 0 index.times do while true do norm_index += 1 break unless columns[norm_index][:included] == false end end norm_index end # Params: # <tt>:operation</tt>: :update or :create def process_data(data, operation) success = true # mod_record_ids = [] mod_records = {} if !config[:"prohibit_#{operation}"] modified_records = 0 data.each do |record_hash| id = record_hash.delete('id') record = operation == :create ? data_class.new : data_class.find(id) success = true # merge with strong default attirbutes record_hash.merge!(config[:strong_default_attrs]) if config[:strong_default_attrs] # process all attirubutes for this record record_hash.each_pair do |k,v| begin record.send("#{k}=",v) rescue ArgumentError => exc flash :error => exc.message success = false break end end # try to save # modified_records += 1 if success && record.save mod_records[id] = record.to_array(columns) if success && record.save # mod_record_ids << id if success && record.save # flash eventual errors if !record.errors.empty? success = false record.errors.to_a.each do |msg| flash :error => msg end end end # flash :notice => "#{operation.to_s.capitalize}d #{modified_records} record(s)" else success = false flash :error => "You don't have permissions to #{operation} data" end mod_records end # Converts Ext.ux.grid.GridFilters filters to searchlogic conditions, e.g. # {"0" => { # "data" => { # "type" => "numeric", # "comparison" => "gt", # "value" => 10 }, # "field" => "id" # }, # "1" => { # "data" => { # "type" => "string", # "value" => "pizza" # }, # "field" => "food_name" # }} # # => # # metawhere: :id.gt => 100, :food_name.matches => '%pizza%' def convert_filters(column_filter) res = {} column_filter.each_pair do |k,v| assoc, method = v["field"].split('__') if method assoc = data_class.reflect_on_association(assoc.to_sym) field = [assoc.klass.table_name, method].join('.').to_sym else field = assoc.to_sym end value = v["data"]["value"] case v["data"]["type"] when "string" field = field.send :matches value = "%#{value}%" when "numeric", "date" field = field.send :"#{v['data']['comparison']}" end res.merge!({field => value}) end res end def normalize_extra_conditions(conditions) conditions.each_pair do |k,v| conditions[k] = "%#{v}%" if ["like", "matches"].include?(k.to_s.split("__").last) end end # def check_for_positive_result(res) # if res[:set_form_values] # # successful creation # res[:set_form_values] = nil # res.merge!({ # :parent => {:on_successfull_edit => true} # }) # true # else # false # end # end end end end end