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.
- 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" }
|