gridify 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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" }