gridify 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,149 @@
1
+ require 'gridify/assertions'
2
+ require 'gridify/grid_options'
3
+ require 'gridify/grid_view'
4
+ require 'gridify/grid_column'
5
+ require 'gridify/grid_finder'
6
+
7
+ # NOTE: for readablity/clarity, native jqGrid options should be string; Ruby ones as syms
8
+
9
+ module Gridify
10
+ class Grid
11
+ include Assertions
12
+
13
+ # todo: change this so klass is optional, decouple from active record
14
+ def initialize( klass, *args, &block )
15
+ #debugger
16
+ options = args.extract_options! # => args.last.is_a?(Hash) ? args.pop : {}
17
+ assert_exclusive_keys( options, :only, :except)
18
+
19
+ @resource = klass.to_s.tableize.pluralize
20
+ @name = args.first || :grid
21
+
22
+ # non-persistent options
23
+ # generate model unless explicitly say no
24
+ build_model = options.delete(:build_model) == false ? false : true
25
+ only = options.delete(:only)
26
+ except = options.delete(:except)
27
+ col_presets = options.delete(:columns)
28
+
29
+ # assign options
30
+ update options
31
+
32
+ # build columns from ActiveRecord model (klass)
33
+ if klass.present? && build_model
34
+ @model = build_columns klass, only, except, col_presets
35
+ end
36
+
37
+ instance_eval(&block) if block
38
+ #(note instance_eval cannot access things outside its scope; otherwise do this:
39
+ #yield self if block_given?
40
+ end
41
+
42
+ def update( options )
43
+ options.each {|atr, val| send( "#{atr}=", val )}
44
+ # exception "invalid option..."
45
+ end
46
+
47
+ def column( name, options={} )
48
+ name = name.to_s
49
+ klass = resource.classify.constantize
50
+ # TODO: set edit options based on ar_column
51
+ # TODO: edit
52
+ # TODO: handle file input types
53
+ # TODO: custom input types
54
+ if col = columns_hash[name]
55
+ # update an existing column
56
+ col.update options
57
+
58
+ elsif ar = klass.columns.detect {|c| c.name==name}
59
+ #debugger
60
+ # create column from database schema
61
+ edit = editable &&
62
+ # only edit accessible attributes
63
+ (klass.accessible_attributes.nil? || klass.accessible_attributes.include?(ar.name))
64
+ args = {
65
+ :ar_column => ar,
66
+ :name => ar.name,
67
+ :value_type => ar.type,
68
+ :searchable => searchable,
69
+ :sortable => sortable,
70
+ :editable => edit
71
+ }.merge(options)
72
+ columns << GridColumn.new( args)
73
+
74
+ else
75
+ # create column from scratch
76
+ args = {
77
+ :name => name,
78
+ :value_type => :string,
79
+ :searchable => searchable,
80
+ :sortable => sortable,
81
+ :editable => edit
82
+ }.merge(options)
83
+ columns << GridColumn.new( args)
84
+ end
85
+ end
86
+
87
+ def column_names
88
+ columns.collect {|col| col.name.titleize }
89
+ end
90
+
91
+ def column_model
92
+ columns.collect {|col| col.properties }
93
+ end
94
+
95
+ # normally we need to keep columns an ordered array, sometimes its convenient to have a hash
96
+ def columns_hash
97
+ columns.inject({}) {|h, col| h[col.name] = col; h }
98
+ end
99
+
100
+ protected
101
+
102
+ # # isnt there something in rails to do this already?
103
+ # def parse_options( keys, options )
104
+ # ops = keys.inject({}) do |h, k|
105
+ # val = options.delete(k)
106
+ # h[k] = val unless val.nil?
107
+ # h
108
+ # end
109
+ # ops || {}
110
+ # end
111
+
112
+ # generate list of columns based on AR model
113
+ # option: :only or :except
114
+ # :col_options hash of hash of preset values for columns (eg from cookie) { :title => {:width => 98}}
115
+ def build_columns( klass, only, except, presets )
116
+ #debugger
117
+ # stringify
118
+ only = Array(only).map {|s| s.to_s }
119
+ except = Array(except).map {|s| s.to_s }
120
+ presets ||= {}
121
+ presets.stringify_keys!
122
+
123
+ self.columns = klass.columns.collect do |ar|
124
+ #debugger
125
+ next if only.present? && !only.include?(ar.name)
126
+ next if except.present? && except.include?(ar.name)
127
+ is_key = (ar.name=='id')
128
+ edit = editable && !is_key &&
129
+ # only edit accessible attributes
130
+ (klass.accessible_attributes.nil? || klass.accessible_attributes.include?(ar.name))
131
+ args = {
132
+ :ar_column => ar,
133
+ :name => ar.name,
134
+ :value_type => ar.type,
135
+ :key => is_key,
136
+ :hidden => is_key,
137
+ :searchable => searchable,
138
+ :sortable => sortable,
139
+ :editable => edit
140
+ }
141
+
142
+ # create column with default args merged with options given for this column
143
+ GridColumn.new args.merge( presets[ar.name]||{} )
144
+ end.compact
145
+ end
146
+
147
+
148
+ end
149
+ end
@@ -0,0 +1,121 @@
1
+ module Gridify
2
+ class GridColumn
3
+
4
+ attr_accessor :name, # attribute name (required)
5
+ :label,
6
+ :ar_column, # active_record column
7
+ :value_type, # active_record data type :string, :text, :integer, :float, :boolean, + :currency
8
+ :key, # true if this is the key (only :id)
9
+
10
+ # column formatting
11
+ :width, # initial width (150)
12
+ :fixed_width, # true = not resized when recalc fit to width
13
+ #:formatter, # see jqGrid doc (based on value_type)
14
+ #:format_options, # hash, see jqGrid doc (based on value_type)
15
+ :align, # 'left', 'right', 'center' (left for strings, right for numerics)
16
+ #:classes, # optional classes to add to column
17
+
18
+ # column actions
19
+ :resizable, # t/f (true)
20
+ :sortable, # t/f (true) or jqGrid sorttype: :integer, :float, :currency, :date, :text (true)
21
+ :searchable, # true/false (true) or text or select
22
+ #:search_options,
23
+ :editable, # true/false (false) or text, textarea, select, checkbox, password, button, image and file (based on value_type)
24
+ :edit_options, # html input field options, depends on editable type (eg {"size" => 10, "maxlength" => 15}) see http://www.trirand.com/jqgridwiki/doku.php?id=wiki:common_rules
25
+ :validations, # jqGrid editrules (TODO derive from AR model validations) see http://www.trirand.com/jqgridwiki/doku.php?id=wiki:common_rules
26
+ # "required" "integer" "email" "url" etc
27
+ :form_options, # jqGrid formoptions for formatting input forms
28
+ # select types
29
+ #:select_url, # url to dynamically get select options
30
+
31
+ # visiblity
32
+ #:always_hidden, # (false)
33
+ :hidden # initial hide state (false)
34
+
35
+ def initialize(options)
36
+ update options
37
+ end
38
+
39
+ def update(options)
40
+ options.each {|atr, val| send( "#{atr}=", val )}
41
+ end
42
+
43
+ def to_json
44
+ properties.to_json #_with_js
45
+ end
46
+
47
+ def properties
48
+ jqgrid_properties
49
+ end
50
+
51
+ def resizable
52
+ if @resizable==false
53
+ false
54
+ else
55
+ # true or nil
56
+ true
57
+ end
58
+ end
59
+
60
+ def fixed_width
61
+ if @fixed_width==false
62
+ false
63
+ elsif @fixed_width.nil?
64
+ !resizable
65
+ else
66
+ @fixed_width
67
+ end
68
+ end
69
+
70
+ # ----------------
71
+ private
72
+
73
+ def jqgrid_type
74
+ return sortable unless sortable==true
75
+ case value_type
76
+ when :string : 'text'
77
+ when :text : 'text'
78
+ when :integer : 'integer'
79
+ when :float : 'float'
80
+ when :boolean : 'boolean'
81
+ when :datetime : 'date'
82
+ end
83
+ end
84
+
85
+ # note, we dont vals[:foo] = foo because dont want to bother generating key if its same as jqGrid default
86
+ def jqgrid_properties
87
+ vals = {
88
+ :name => name,
89
+ :index => name
90
+ }
91
+ #xmlmap not required when same as :name
92
+ # vals[:xmlmap] = name if data_type == :xml
93
+ # vals[:jsonmap] = name if data_type == :json
94
+
95
+ vals[:label] = label || name.titleize
96
+ vals[:resizable] = false if resizable==false
97
+ vals[:fixed] = fixed_width unless fixed_width==false
98
+ vals[:sortable] = false if sortable==false
99
+ vals[:sorttype] = jqgrid_type if sortable
100
+ vals[:search] = false if searchable==false
101
+ vals[:editable] = true if editable
102
+ vals[:align] = 'right' if [:integer, :float, :currency].include?(value_type)
103
+ case value_type
104
+ when :datetime
105
+ vals[:formatter] = 'date'
106
+ vals[:formatoptions] = { :srcformat => 'UniversalSortableDateTime', :newformat => 'FullDateTime' }
107
+ end
108
+ vals[:hidden] = true if hidden
109
+ vals[:width] = width if width
110
+ vals[:editoptions] = edit_options if editable && edit_options
111
+ vals[:editrules] = validations if editable && validations
112
+ # and more...
113
+
114
+ vals
115
+ end
116
+
117
+ end
118
+ end
119
+
120
+ #<ActiveRecord::ConnectionAdapters::SQLiteColumn:0x2515a98 @sql_type="varchar(255)", @name="title", @precision=nil, @primary=false, @default=nil, @limit=255, @null=true, @type=:string, @scale=nil>
121
+
@@ -0,0 +1,197 @@
1
+ module Gridify
2
+ class Grid
3
+
4
+ attr_accessor :search_rules, # current search/filter rules, as hash
5
+ :search_rules_op # :and, :or
6
+
7
+ # finds records based on request params
8
+ # e.g. params from jqGrid
9
+ # :_search do search (true/false) ["false"]
10
+ # :sidx sort index (column to search on) [""]
11
+ # :sord sort direction (desc/asc) ["asc"]
12
+ # :nd ?
13
+ # :rows number of items to get ["20"]
14
+ # :page page number (starts at 1) ["1"]
15
+
16
+ def update_from_params( params )
17
+ params.symbolize_keys!
18
+ params_to_rules params
19
+ self.sort_by = params[:sidx] if params[:sidx]
20
+ self.sort_order = params[:sord] if params[:sord]
21
+ self.current_page = params[:page].to_i if params[:page]
22
+ self.rows_per_page = params[:rows].to_i if params[:rows]
23
+ end
24
+
25
+ # return find args (scope) for current settings
26
+ def current_scope
27
+ #debugger
28
+ find_args = {}
29
+ if sort_by.present? && col = columns_hash[sort_by]
30
+ if case_sensitive || !([:string, :text].include?(col.value_type))
31
+ find_args[:order] = "#{sort_by} #{sort_order}"
32
+ else
33
+ find_args[:order] = "upper(#{sort_by}) #{sort_order}"
34
+ end
35
+ end
36
+ if rows_per_page.present? && rows_per_page > 0
37
+ find_args[:limit] = rows_per_page
38
+ offset = (current_page.to_i-1) * rows_per_page if current_page.present?
39
+ find_args[:offset] = offset if offset && offset > 0
40
+ end
41
+ cond = rules_to_conditions
42
+ find_args[:conditions] = cond unless cond.blank?
43
+ find_args
44
+ end
45
+
46
+ def find( params )
47
+ #debugger
48
+ update_from_params params
49
+ find_args = current_scope
50
+ klass = resource.classify.constantize
51
+ records = klass.send( finder, :all, find_args )
52
+ end
53
+
54
+ def encode_records( records, total_count=nil )
55
+ #debugger
56
+ klass = resource.classify.constantize
57
+ total_count ||= klass.count
58
+ total_pages = total_count / rows_per_page + 1
59
+ #TODO: :only => [attributes], :methods => [virtual attributes]
60
+ case data_type
61
+ when :xml
62
+ xml = records.to_xml( :skip_types => true, :dasherize => false ) do |xml|
63
+ if rows_per_page > 0
64
+ xml.page current_page
65
+ xml.total_pages total_pages
66
+ xml.total_records total_count
67
+ end
68
+ end
69
+
70
+ when :json
71
+ #debugger
72
+ data = { resource => records }
73
+ if rows_per_page > 0
74
+ data.merge!(
75
+ :page => current_page,
76
+ :total_pages => total_pages,
77
+ :total_records => total_count
78
+ )
79
+ end
80
+
81
+ save = ActiveRecord::Base.include_root_in_json
82
+ ActiveRecord::Base.include_root_in_json = false
83
+ json = data.to_json
84
+ ActiveRecord::Base.include_root_in_json = save
85
+ json
86
+
87
+ #others...
88
+ else #nop ?
89
+ records.to_s
90
+ end
91
+ end
92
+
93
+ def find_and_encode( params )
94
+ encode_records( find( params ) )
95
+ end
96
+
97
+ # grid doesnt nest attributes inside the resource
98
+ # could change this behavior in jqGrid, see grid.postext.js ?
99
+ # http://www.trirand.com/jqgridwiki/doku.php?id=wiki:post_data_module
100
+ #
101
+ def member_params( params )
102
+ params.inject({}) {|h, (name, value)| h[name] = value if columns_hash[name]; h }
103
+ end
104
+
105
+
106
+ protected
107
+
108
+ OPS = { 'eq' => '=', 'lt' => '<', 'le' => '<=', 'gt' => '>', 'ge' => '>=', 'ne' => '!=' }
109
+ #['eq','ne','lt','le','gt','ge',
110
+ # 'bw','bn','in','ni','ew','en','cn','nc']
111
+ #['equal','not equal', 'less', 'less or equal','greater','greater or equal',
112
+ # 'begins with','does not begin with','is in','is not in','ends with','does not end with','contains','does not contain']
113
+
114
+ OPS_PATTERN = {
115
+ 'bw' => '?%',
116
+ 'bn' => '?%',
117
+ # 'in'
118
+ # 'ni'
119
+ 'ew' => '%?',
120
+ 'en' => '%?',
121
+ 'cn' => '%?%',
122
+ 'nc' => '%?%',
123
+ }
124
+
125
+ STRING_OPS = {
126
+ 'bw' => 'LIKE',
127
+ 'bn' => 'NOT LIKE',
128
+ 'ew' => 'LIKE',
129
+ 'en' => 'NOT LIKE',
130
+ 'cn' => 'LIKE',
131
+ 'nc' => 'NOT LIKE',
132
+ }
133
+
134
+
135
+ # params[:filters] => {"groupOp"=>"AND",
136
+ # "rules"=>[{"data"=>"b", "op"=>"ge", "field"=>"title"}, {"data"=>"f", "op"=>"le", "field"=>"title"}] }
137
+ def params_to_rules( params )
138
+ #debugger
139
+ if params[:_search]=='true' || params[:_search]==true
140
+ if params[:filters]
141
+ # advanced search
142
+ filters = ActiveSupport::JSON.decode( params[:filters] )
143
+ self.search_rules = filters['rules']
144
+ self.search_rules_op = filters['groupOp']
145
+ elsif params[:searchField]
146
+ # simple search
147
+ self.search_rules = [{ "field" => params[:searchField], "op" => params[:searchOper], "data" => params[:searchString]}]
148
+ else
149
+ # toolbar search
150
+ self.search_rules = []
151
+ self.search_rules_op = :and
152
+ columns.each do |col|
153
+ name = col.name
154
+ data = params[name.to_sym]
155
+ self.search_rules << { "field" => name, "op" => "cn", "data" => data } if data
156
+ end
157
+ end
158
+ end
159
+ search_rules
160
+ end
161
+
162
+ def rules_to_conditions
163
+ # note: ignoring case_sensitive as an option, ActiveRecord find is insensitive by default (have to model the db to let it be case sensitive?)
164
+ return nil if search_rules.blank?
165
+ cond = nil
166
+ expr = ''
167
+ vals = []
168
+ search_rules.each do |rule|
169
+ expr << " #{search_rules_op} " unless expr.blank?
170
+ if op = OPS[rule['op']]
171
+ expr << "#{rule['field']} #{op} ?"
172
+ vals << rule['data']
173
+ elsif op = STRING_OPS[rule['op']]
174
+ expr << "#{rule['field']} #{op} ?"
175
+ vals << OPS_PATTERN[rule['op']].gsub('?', rule['data'])
176
+ end
177
+ end
178
+ cond = [ expr ] + vals
179
+ end
180
+
181
+
182
+ end
183
+ end
184
+
185
+
186
+ # # If you need to display error messages
187
+ # err = ""
188
+ # if user
189
+ # user.errors.entries.each do |error|
190
+ # err << "<strong>#{error[0]}</strong> : #{error[1]}<br/>"
191
+ # end
192
+ # end
193
+ #
194
+ # render :text => "#{err}"
195
+
196
+
197
+ # { :add => true, :edit => true, :inline_edit => false, :delete => true, :edit_url => "/users/post_data", :error_handler => "after_submit" }