gridify 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.rspec +3 -0
- data/Gemfile +12 -0
- data/Gemfile.lock +26 -0
- data/README.rdoc +264 -0
- data/Rakefile +47 -0
- data/VERSION +1 -0
- data/autotest/discover.rb +5 -0
- data/init.rb +1 -0
- data/lib/gridify.rb +44 -0
- data/lib/gridify/assertions.rb +37 -0
- data/lib/gridify/grid.rb +149 -0
- data/lib/gridify/grid_column.rb +121 -0
- data/lib/gridify/grid_finder.rb +197 -0
- data/lib/gridify/grid_options.rb +261 -0
- data/lib/gridify/grid_view.rb +346 -0
- data/spec/db/database.yml +3 -0
- data/spec/db/gridify.sqlite3.db +0 -0
- data/spec/db/schema.rb +7 -0
- data/spec/grid_column_spec.rb +72 -0
- data/spec/grid_finder_spec.rb +192 -0
- data/spec/grid_spec.rb +38 -0
- data/spec/grid_view_spec.rb +423 -0
- data/spec/gridify_spec.rb +34 -0
- data/spec/spec_helper.rb +20 -0
- data/tasks/gridify_tasks.rake +4 -0
- metadata +137 -0
data/lib/gridify/grid.rb
ADDED
@@ -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" }
|