netzke-basepack 0.7.4 → 0.7.5

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.
Files changed (74) hide show
  1. data/.travis.yml +11 -0
  2. data/CHANGELOG.rdoc +10 -0
  3. data/README.md +36 -2
  4. data/Rakefile +1 -3
  5. data/config/ci/before-travis.sh +28 -0
  6. data/lib/netzke/active_record.rb +10 -8
  7. data/lib/netzke/active_record/attributes.rb +28 -17
  8. data/lib/netzke/active_record/relation_extensions.rb +3 -1
  9. data/lib/netzke/basepack.rb +10 -2
  10. data/lib/netzke/basepack/action_column.rb +6 -8
  11. data/lib/netzke/basepack/data_accessor.rb +11 -174
  12. data/lib/netzke/basepack/data_adapters/abstract_adapter.rb +164 -0
  13. data/lib/netzke/basepack/data_adapters/active_record_adapter.rb +279 -0
  14. data/lib/netzke/basepack/data_adapters/data_mapper_adapter.rb +264 -0
  15. data/lib/netzke/basepack/data_adapters/sequel_adapter.rb +260 -0
  16. data/lib/netzke/basepack/form_panel.rb +3 -3
  17. data/lib/netzke/basepack/form_panel/fields.rb +6 -10
  18. data/lib/netzke/basepack/form_panel/javascripts/form_panel.js +1 -0
  19. data/lib/netzke/basepack/form_panel/services.rb +15 -16
  20. data/lib/netzke/basepack/grid_panel.rb +16 -10
  21. data/lib/netzke/basepack/grid_panel/columns.rb +6 -7
  22. data/lib/netzke/basepack/grid_panel/javascripts/event_handling.js +29 -27
  23. data/lib/netzke/basepack/grid_panel/services.rb +13 -90
  24. data/lib/netzke/basepack/paging_form_panel.rb +3 -3
  25. data/lib/netzke/basepack/query_builder.rb +2 -0
  26. data/lib/netzke/basepack/query_builder/javascripts/query_builder.js +29 -19
  27. data/lib/netzke/basepack/search_panel.rb +6 -3
  28. data/lib/netzke/basepack/search_panel/javascripts/search_panel.js +2 -1
  29. data/lib/netzke/basepack/search_window.rb +2 -1
  30. data/lib/netzke/basepack/version.rb +1 -1
  31. data/lib/netzke/data_mapper.rb +18 -0
  32. data/lib/netzke/data_mapper/attributes.rb +273 -0
  33. data/lib/netzke/data_mapper/combobox_options.rb +11 -0
  34. data/lib/netzke/data_mapper/relation_extensions.rb +38 -0
  35. data/lib/netzke/sequel.rb +18 -0
  36. data/lib/netzke/sequel/attributes.rb +274 -0
  37. data/lib/netzke/sequel/combobox_options.rb +10 -0
  38. data/lib/netzke/sequel/relation_extensions.rb +40 -0
  39. data/netzke-basepack.gemspec +24 -13
  40. data/test/basepack_test_app/Gemfile +33 -8
  41. data/test/basepack_test_app/Gemfile.lock +98 -79
  42. data/test/basepack_test_app/Guardfile +46 -0
  43. data/test/basepack_test_app/app/components/book_grid_with_persistence.rb +3 -0
  44. data/test/basepack_test_app/app/components/extras/book_presentation.rb +10 -3
  45. data/test/basepack_test_app/app/models/address.rb +27 -1
  46. data/test/basepack_test_app/app/models/author.rb +28 -0
  47. data/test/basepack_test_app/app/models/book.rb +43 -0
  48. data/test/basepack_test_app/app/models/book_with_custom_primary_key.rb +22 -0
  49. data/test/basepack_test_app/app/models/role.rb +21 -0
  50. data/test/basepack_test_app/app/models/user.rb +24 -0
  51. data/test/basepack_test_app/config/database.yml.sample +11 -10
  52. data/test/basepack_test_app/config/database.yml.travis +15 -0
  53. data/test/basepack_test_app/config/initializers/data_mapper_logging.rb +3 -0
  54. data/test/basepack_test_app/config/initializers/sequel.rb +26 -0
  55. data/test/basepack_test_app/db/schema.rb +0 -3
  56. data/test/basepack_test_app/features/grid_panel.feature +28 -8
  57. data/test/basepack_test_app/features/grid_sorting.feature +6 -6
  58. data/test/basepack_test_app/features/paging_form_panel.feature +13 -13
  59. data/test/basepack_test_app/features/search_in_grid.feature +31 -31
  60. data/test/basepack_test_app/features/step_definitions/generic_steps.rb +3 -1
  61. data/test/basepack_test_app/features/support/env.rb +17 -4
  62. data/test/basepack_test_app/lib/tasks/travis.rake +7 -0
  63. data/test/basepack_test_app/spec/components/form_panel_spec.rb +2 -2
  64. data/test/basepack_test_app/spec/data_adapter/adapter_spec.rb +68 -0
  65. data/test/basepack_test_app/spec/{active_record → data_adapter}/attributes_spec.rb +12 -4
  66. data/test/basepack_test_app/spec/data_adapter/relation_extensions_spec.rb +125 -0
  67. data/test/basepack_test_app/spec/spec_helper.rb +9 -0
  68. data/test/unit/active_record_basepack_test.rb +1 -1
  69. data/test/unit/grid_panel_test.rb +1 -1
  70. metadata +26 -31
  71. data/app/models/netzke_field_list.rb +0 -261
  72. data/app/models/netzke_model_attr_list.rb +0 -21
  73. data/app/models/netzke_persistent_array_auto_model.rb +0 -57
  74. data/test/basepack_test_app/spec/active_record/relation_extensions_spec.rb +0 -44
@@ -0,0 +1,164 @@
1
+ module Netzke::Basepack::DataAdapters
2
+ # A concrete adapter should implement all the public instance methods of this adapter in order to support all the functionality of Basepack components.
3
+ class AbstractAdapter
4
+
5
+ # Returns records based on passed params. Implements:
6
+ # * pagination
7
+ # * filtering
8
+ # * scopes
9
+ #
10
+ # `params` is a hash that contains the following keys:
11
+ #
12
+ # * :sort - sorting params, which is an array of hashes that contain the following keys in their turn:
13
+ # * :property - the field that is being sorted on
14
+ # * :direction - "asc" or "desc"
15
+ # * :limit - rows per page in pagination
16
+ # * :start - page number in pagination
17
+ # * :scope - the scope as described in Netzke::Basepack::GridPanel
18
+ # * :filter - Ext filters
19
+ #
20
+ # The `columns` parameter may be used to use joins to address the n+1 query problem, and receives an array of column configurations
21
+ def get_records(params, columns)
22
+ []
23
+ end
24
+
25
+ # gets the first record
26
+ def first
27
+ @model_class.first
28
+ end
29
+
30
+ # Returns record count based on passed params. Implements:
31
+ # * filtering
32
+ # * scopes
33
+ #
34
+ # `params` is a hash that contains the following keys:
35
+ #
36
+ # * :scope - the scope as described in Netzke::Basepack::GridPanel
37
+ # * :filter - Ext filters
38
+ #
39
+ # The `columns` parameter may be used to use joins to address the n+1 query problem, and receives an array of column configurations
40
+ def count_records(params, columns)
41
+ 0
42
+ end
43
+
44
+ # Map a ORM type to a type symbol
45
+ # Possible types to return
46
+ # :integer
47
+ # :boolean
48
+ # :date
49
+ # :datetime
50
+ # :time
51
+ # :text
52
+ # :string
53
+ #
54
+ # Default implementation works for ActiveRecord
55
+ def map_type type
56
+ type
57
+ end
58
+
59
+ # gets the type of a model attribute for xtype mapping
60
+ # i.e. get_assoc_property_type :author,:first_name should return :string
61
+ # Possible types to return
62
+ # :integer
63
+ # :boolean
64
+ # :date
65
+ # :datetime
66
+ # :time
67
+ # :text
68
+ # :string
69
+ def get_assoc_property_type assoc_name, prop_name
70
+ raise NotImplementedError
71
+ end
72
+
73
+ # like get_assoc_property_type but for non-association columns
74
+ def get_property_type column
75
+ column.type
76
+ end
77
+
78
+ # should return true if column is virtual
79
+ def column_virtual? c
80
+ raise NotImplementedError
81
+ end
82
+
83
+ # Returns options for comboboxes in grids/forms
84
+ def combobox_options_for_column(column, method_options = {})
85
+ raise NotImplementedError
86
+ end
87
+
88
+ # Returns the foreign key name for an association
89
+ def foreign_key_for assoc_name
90
+ raise NotImplementedError
91
+ end
92
+
93
+ # Returns the model class for association columns
94
+ def class_for assoc_name
95
+ raise NotImplementedError
96
+ end
97
+
98
+ # Destroys records with the provided ids
99
+ def destroy(ids)
100
+ end
101
+
102
+ # Changes records position (e.g. when acts_as_list is used in ActiveRecord).
103
+ #
104
+ # `params` is a hash with the following keys:
105
+ #
106
+ # * :ids - ids of records to move
107
+ # * :new_index - new starting position for the records to move
108
+ def move_records(params)
109
+ end
110
+
111
+ # Returns a new record.
112
+ def new_record(params = {})
113
+ @model_class.new(params)
114
+ end
115
+
116
+ # give the data adapter the opportunity the set special options for
117
+ # saving, must return true on success
118
+ def save_record(record)
119
+ record.save
120
+ end
121
+
122
+ # give the data adapter the opporunity to process error messages
123
+ # must return an raay of the form ["Title can't be blank", "Foo can't be blank"]
124
+ def errors_array(record)
125
+ record.errors.to_a
126
+ end
127
+
128
+ # Finds a record by id, return nil if not found
129
+ def find_record(id)
130
+ @model_class.find(id)
131
+ end
132
+
133
+ # Build a hash of foreign keys and the associated model
134
+ def hash_fk_model
135
+ raise NotImplementedError
136
+ end
137
+
138
+
139
+
140
+ # -- End of overridable methods
141
+
142
+ # Abstract-adapter specifics
143
+ #
144
+
145
+ # Used to determine if the given adapter should be used for the passed in class.
146
+ def self.for_class?(member_class)
147
+ false # override in subclass
148
+ end
149
+
150
+ def self.inherited(subclass)
151
+ @subclasses ||= []
152
+ @subclasses << subclass
153
+ end
154
+
155
+ def self.adapter_class(model_class)
156
+ @subclasses.detect { |subclass| subclass.for_class?(model_class) } || AbstractAdapter
157
+ end
158
+
159
+ def initialize(model_class)
160
+ @model_class = model_class
161
+ end
162
+
163
+ end
164
+ end
@@ -0,0 +1,279 @@
1
+ module Netzke::Basepack::DataAdapters
2
+ class ActiveRecordAdapter < AbstractAdapter
3
+ def self.for_class?(model_class)
4
+ model_class <= ActiveRecord::Base
5
+ end
6
+
7
+ def get_records(params, columns=[])
8
+ # build initial relation based on passed params
9
+ relation = get_relation(params)
10
+
11
+ # addressing the n+1 query problem
12
+ columns.each do |c|
13
+ assoc, method = c[:name].split('__')
14
+ relation = relation.includes(assoc.to_sym) if method
15
+ end
16
+
17
+ # apply sorting if needed
18
+ if params[:sort] && sort_params = params[:sort].first
19
+ assoc, method = sort_params["property"].split('__')
20
+ dir = sort_params["direction"].downcase
21
+
22
+ # if a sorting scope is set, call the scope with the given direction
23
+ column = columns.detect { |c| c[:name] == sort_params["property"] }
24
+ if column.has_key?(:sorting_scope)
25
+ relation = relation.send(column[:sorting_scope].to_sym, dir.to_sym)
26
+ else
27
+ relation = if method.nil?
28
+ relation.order("#{assoc} #{dir}")
29
+ else
30
+ assoc = @model_class.reflect_on_association(assoc.to_sym)
31
+ relation.joins(assoc.name).order("#{assoc.klass.table_name}.#{method} #{dir}")
32
+ end
33
+ end
34
+ end
35
+
36
+ page = params[:limit] ? params[:start].to_i/params[:limit].to_i + 1 : 1
37
+ if params[:limit]
38
+ relation.offset(params[:start]).limit(params[:limit])
39
+ else
40
+ relation.all
41
+ end
42
+ end
43
+
44
+ def count_records(params, columns=[])
45
+ # build initial relation based on passed params
46
+ relation = get_relation(params)
47
+
48
+ # addressing the n+1 query problem
49
+ columns.each do |c|
50
+ assoc, method = c[:name].split('__')
51
+ relation = relation.includes(assoc.to_sym) if method
52
+ end
53
+
54
+ relation.count
55
+ end
56
+
57
+ def get_assoc_property_type assoc_name, prop_name
58
+ if prop_name && assoc=@model_class.reflect_on_association(assoc_name)
59
+ assoc_column = assoc.klass.columns_hash[prop_name.to_s]
60
+ assoc_column.try(:type)
61
+ end
62
+ end
63
+
64
+ def column_virtual? c
65
+ assoc_name, asso = c[:name].split('__')
66
+ assoc, assoc_method = assoc_and_assoc_method_for_attr(c[:name])
67
+
68
+ if assoc
69
+ return !assoc.klass.column_names.map(&:to_sym).include?(assoc_method.to_sym)
70
+ else
71
+ return !@model_class.column_names.map(&:to_sym).include?(c[:name].to_sym)
72
+ end
73
+ end
74
+
75
+ # Returns options for comboboxes in grids/forms
76
+ def combobox_options_for_column(column, method_options = {})
77
+ query = method_options[:query]
78
+
79
+ # First, check if we have options for this column defined in persistent storage
80
+ options = column[:combobox_options] && column[:combobox_options].split("\n")
81
+ if options
82
+ query ? options.select{ |o| o.index(/^#{query}/) }.map{ |el| [el] } : options
83
+ else
84
+ assoc, assoc_method = assoc_and_assoc_method_for_attr(column[:name])
85
+
86
+ if assoc
87
+ # Options for an asssociation attribute
88
+
89
+ relation = assoc.klass.scoped
90
+
91
+ relation = relation.extend_with(method_options[:scope]) if method_options[:scope]
92
+
93
+ if assoc.klass.column_names.include?(assoc_method)
94
+ # apply query
95
+ relation = relation.where(["#{assoc_method} like ?", "%#{query}%"]) if query.present?
96
+ relation.all.map{ |r| [r.id, r.send(assoc_method)] }
97
+ else
98
+ relation.all.map{ |r| [r.id, r.send(assoc_method)] }.select{ |id,value| value =~ /^#{query}/ }
99
+ end
100
+
101
+ else
102
+ # Options for a non-association attribute
103
+ res=@model_class.netzke_combo_options_for(column[:name], method_options)
104
+
105
+ # ensure it is an array-in-array, as Ext will fail otherwise
106
+ raise RuntimeError, "netzke_combo_options_for should return an Array" unless res.kind_of? Array
107
+ return [[]] if res.empty?
108
+
109
+ unless res.first.kind_of? Array
110
+ res=res.map do |v|
111
+ [v]
112
+ end
113
+ end
114
+ return res
115
+ end
116
+ end
117
+ end
118
+
119
+ def foreign_key_for assoc_name
120
+ @model_class.reflect_on_association(assoc_name.to_sym).foreign_key
121
+ end
122
+
123
+ # Returns the model class for association columns
124
+ def class_for assoc_name
125
+ @model_class.reflect_on_association(assoc_name.to_sym).klass
126
+ end
127
+
128
+ def destroy(ids)
129
+ @model_class.destroy(ids)
130
+ end
131
+
132
+ def find_record(id)
133
+ @model_class.find_all_by_id(id).first
134
+ end
135
+
136
+ # Build a hash of foreign keys and the associated model
137
+ def hash_fk_model
138
+ foreign_keys = {}
139
+ @model_class.reflect_on_all_associations(:belongs_to).map{ |r|
140
+ foreign_keys[r.association_foreign_key.to_sym] = r.name
141
+ }
142
+ foreign_keys
143
+ end
144
+
145
+ def move_records(params)
146
+ if defined?(ActsAsList) && @model_class.ancestors.include?(ActsAsList::InstanceMethods)
147
+ ids = JSON.parse(params[:ids]).reverse
148
+ ids.each_with_index do |id, i|
149
+ r = @model_class.find(id)
150
+ r.insert_at(params[:new_index].to_i + i + 1)
151
+ end
152
+ on_data_changed
153
+ else
154
+ raise RuntimeError, "Model class should implement 'acts_as_list' to support reordering records"
155
+ end
156
+ end
157
+
158
+ # Returns association and association method for a column
159
+ def assoc_and_assoc_method_for_attr(column_name)
160
+ assoc_name, assoc_method = column_name.split('__')
161
+ assoc = @model_class.reflect_on_association(assoc_name.to_sym) if assoc_method
162
+ [assoc, assoc_method]
163
+ end
164
+
165
+
166
+ # An ActiveRecord::Relation instance encapsulating all the necessary conditions.
167
+ def get_relation(params = {})
168
+ @arel = @model_class.arel_table
169
+
170
+ relation = @model_class.scoped
171
+
172
+ relation = apply_column_filters(relation, params[:filter]) if params[:filter]
173
+
174
+ if params[:extra_conditions]
175
+ extra_conditions = normalize_extra_conditions(ActiveSupport::JSON.decode(params[:extra_conditions]))
176
+ relation = relation.extend_with_netzke_conditions(extra_conditions) if params[:extra_conditions]
177
+ end
178
+
179
+ query = params[:query] && ActiveSupport::JSON.decode(params[:query])
180
+
181
+ if query.present?
182
+ # array of arrays of conditions that should be joined by OR
183
+ and_predicates = query.map do |conditions|
184
+ predicates_for_and_conditions(conditions)
185
+ end
186
+
187
+ # join them by OR
188
+ predicates = and_predicates[1..-1].inject(and_predicates.first){ |r,c| r.or(c) }
189
+ end
190
+
191
+ relation = relation.where(predicates)
192
+
193
+ relation = relation.extend_with(params[:scope]) if params[:scope]
194
+
195
+ relation
196
+ end
197
+
198
+ # Parses and applies grid column filters, calling consequent "where" methods on the passed relation.
199
+ # Returns the updated relation.
200
+ #
201
+ # Example column grid data:
202
+ #
203
+ # {"0" => {
204
+ # "data" => {
205
+ # "type" => "numeric",
206
+ # "comparison" => "gt",
207
+ # "value" => 10 },
208
+ # "field" => "id"
209
+ # },
210
+ # "1" => {
211
+ # "data" => {
212
+ # "type" => "string",
213
+ # "value" => "pizza"
214
+ # },
215
+ # "field" => "food_name"
216
+ # }}
217
+ #
218
+ # This will result in:
219
+ #
220
+ # relation.where(["id > ?", 10]).where(["food_name like ?", "%pizza%"])
221
+ def apply_column_filters(relation, column_filter)
222
+ res = relation
223
+ operator_map = {"lt" => "<", "gt" => ">", "eq" => "="}
224
+
225
+ # these are still JSON-encoded due to the migration to Ext.direct
226
+ column_filter=JSON.parse(column_filter)
227
+ column_filter.each do |v|
228
+ assoc, method = v["field"].split('__')
229
+ if method
230
+ assoc = @model_class.reflect_on_association(assoc.to_sym)
231
+ field = [assoc.klass.table_name, method].join('.').to_sym
232
+ else
233
+ field = assoc.to_sym
234
+ end
235
+
236
+ value = v["value"]
237
+
238
+ op = operator_map[v['comparison']]
239
+
240
+ case v["type"]
241
+ when "string"
242
+ res = res.where(["#{field} like ?", "%#{value}%"])
243
+ when "date"
244
+ # convert value to the DB date
245
+ value.match /(\d\d)\/(\d\d)\/(\d\d\d\d)/
246
+ res = res.where("#{field} #{op} ?", "#{$3}-#{$1}-#{$2}")
247
+ when "numeric"
248
+ res = res.where(["#{field} #{op} ?", value])
249
+ else
250
+ res = res.where(["#{field} = ?", value])
251
+ end
252
+ end
253
+
254
+ res
255
+ end
256
+
257
+ def predicates_for_and_conditions(conditions)
258
+ return nil if conditions.empty?
259
+
260
+ predicates = conditions.map do |q|
261
+ value = q["value"]
262
+ case q["operator"]
263
+ when "contains"
264
+ @arel[q["attr"]].matches "%#{value}%"
265
+ else
266
+ if value == false || value == true
267
+ @arel[q["attr"]].eq(value ? 1 : 0)
268
+ else
269
+ @arel[q["attr"]].send(q["operator"], value)
270
+ end
271
+ end
272
+ end
273
+
274
+ # join them by AND
275
+ predicates[1..-1].inject(predicates.first){ |r,p| r.and(p) }
276
+ end
277
+
278
+ end
279
+ end
@@ -0,0 +1,264 @@
1
+ module Netzke::Basepack::DataAdapters
2
+ class DataMapperAdapter < AbstractAdapter
3
+ def self.for_class?(model_class)
4
+ model_class <= DataMapper::Resource
5
+ end
6
+
7
+ def get_records(params, columns=[])
8
+ search_query = @model_class
9
+
10
+ # used for specifying models to join (for ordering and column selection)
11
+ links = []
12
+ # join association models into query if they are specified in query
13
+ # NOTE: AFAIK, in DataMapper there is no possibility to do
14
+ # OUTER JOINs, and inner join is not correct if a foreign key is
15
+ # nullable, so we don't join at all and instead rely on strategic
16
+ # eager loading.
17
+
18
+ #columns.each do |column|
19
+ #if column[:name].index('__')
20
+ #assoc, _ = column[:name].split('__')
21
+ #link = @model_class.relationships[assoc.to_sym].inverse
22
+ #links << link unless links.include? link
23
+ #end
24
+ #end
25
+
26
+ # apply filter
27
+ search_query = apply_column_filters search_query, params[:filter] if params[:filter]
28
+ query_options = {}
29
+
30
+ # apply sorting
31
+ if params[:sort] && sort_params = params[:sort]
32
+ order = []
33
+ sort_params.each do |sort_param|
34
+ assoc, method = sort_param["property"].split("__")
35
+ dir = sort_param["direction"].downcase
36
+
37
+ # if a sorting scope is set, call the scope with the given direction
38
+ column = columns.detect { |c| c[:name] == sort_param["property"] }
39
+ if column.try(:'has_key?', :sorting_scope)
40
+ search_query = search_query.send(column[:sorting_scope].to_sym, dir.to_sym)
41
+ else
42
+ if method
43
+ order << @model_class.send(assoc).send(method).send(dir)
44
+ link = @model_class.relationships[assoc.to_sym].inverse
45
+ links << link unless links.include? link
46
+ else
47
+ order << assoc.to_sym.send(dir)
48
+ end
49
+ end
50
+ end
51
+ query_options[:order] = order unless order.empty?
52
+ query_options[:links] = links unless links.empty?
53
+ end
54
+
55
+ # apply paging
56
+ query_options[:limit]=params[:limit] if params[:limit]
57
+ query_options[:offset]=params[:start] if params[:start]
58
+
59
+ # apply scope
60
+ search_query = search_query.extend_with(params[:scope]) if params[:scope]
61
+
62
+ search_query.all(query_options)
63
+ end
64
+
65
+ def count_records(params, columns=[])
66
+ # delete pagig related params, as this would break the query
67
+ params=params.reject { |k, v|
68
+ [:start, :limit, :page].include? k.to_sym
69
+ }
70
+ # this will NOT do a SELECT *, but a SELECT COUNT(*)
71
+ get_records(params, columns).count
72
+ end
73
+
74
+ def map_type type
75
+ @typemap ||= {
76
+ DataMapper::Property::Integer => :integer,
77
+ DataMapper::Property::Serial => :integer,
78
+ DataMapper::Property::Boolean => :boolean,
79
+ DataMapper::Property::Date => :date,
80
+ DataMapper::Property::DateTime => :datetime,
81
+ DataMapper::Property::Time => :time,
82
+ DataMapper::Property::String => :string,
83
+ DataMapper::Property::Text => :text
84
+ }
85
+ @typemap[type]
86
+ end
87
+
88
+ def get_assoc_property_type assoc_name, prop_name
89
+ assoc = @model_class.send(assoc_name)
90
+ # prop_name could be a virtual column, check it first, return nil in this case
91
+ assoc.respond_to?(prop_name) ? map_type(assoc.send(prop_name).property.class) : nil
92
+ end
93
+
94
+ # like get_assoc_property_type but for non-association columns
95
+ def get_property_type column
96
+ map_type(column.class)
97
+ end
98
+
99
+ def column_virtual? c
100
+ assoc_name, assoc_method = c[:name].split '__'
101
+ if assoc_method
102
+ column_names=@model_class.send(assoc_name).model.column_names
103
+ column_name=assoc_method
104
+ else
105
+ column_names=@model_class.column_names
106
+ column_name=c[:name]
107
+ end
108
+ !column_names.include? column_name
109
+ end
110
+
111
+ # Returns options for comboboxes in grids/forms
112
+ def combobox_options_for_column(column, method_options = {})
113
+ query = method_options[:query]
114
+
115
+ # First, check if we have options for this column defined in persistent storage
116
+ options = column[:combobox_options] && column[:combobox_options].split("\n")
117
+ if options
118
+ query ? options.select{ |o| o.index(/^#{query}/) }.map{ |el| [el] } : options
119
+ else
120
+ assoc_name, assoc_method = column[:name].split '__'
121
+
122
+ if assoc_name
123
+ # Options for an asssociation attribute
124
+ relation = @model_class.send(assoc_name).model
125
+
126
+ relation = relation.extend_with(method_options[:scope]) if method_options[:scope]
127
+
128
+ if class_for(assoc_name).column_names.include?(assoc_method)
129
+ # apply query
130
+ relation = relation.all(assoc_method.to_sym.send(:like) => "%#{query}%") if query.present?
131
+ relation.all.map{ |r| [r.id, r.send(assoc_method)] }
132
+ else
133
+ relation.all.map{ |r| [r.id, r.send(assoc_method)] }.select{ |id,value| value =~ /^#{query}/ }
134
+ end
135
+ else
136
+ # Options for a non-association attribute
137
+ res=@model_class.netzke_combo_options_for(column[:name], method_options)
138
+
139
+ # ensure it is an array-in-array, as Ext will fail otherwise
140
+ raise RuntimeError, "netzke_combo_options_for should return an Array" unless res.kind_of? Array
141
+ return [[]] if res.empty?
142
+
143
+ unless res.first.kind_of? Array
144
+ res=res.map do |v|
145
+ [v]
146
+ end
147
+ end
148
+ return res
149
+ end
150
+ end
151
+ end
152
+
153
+ def foreign_key_for assoc_name
154
+ @model_class.relationships[assoc_name].child_key.first.name.to_s
155
+ end
156
+
157
+ # Returns the model class for an association
158
+ def class_for assoc_name
159
+ @model_class.send(assoc_name).model
160
+ end
161
+
162
+ def destroy(ids)
163
+ @model_class.all(:id => ids).destroy
164
+ end
165
+
166
+ def find_record(id)
167
+ @model_class.get(id)
168
+ end
169
+
170
+ # Build a hash of foreign keys and the associated model
171
+ def hash_fk_model
172
+ @model_class.relationships.inject({}) do |foreign_keys, rel|
173
+ if rel.kind_of? DataMapper::Associations::ManyToOne::Relationship
174
+ foreign_keys[rel.child_key.first.name]=rel.parent_model.to_s.downcase.to_sym
175
+ foreign_keys
176
+ end
177
+ end || {}
178
+ end
179
+
180
+ def move_records(params)
181
+ @model_class.all(:id => params[:ids]).each_with_index do |item, index|
182
+ item.move(:to => params[:new_index] + index)
183
+ end
184
+ end
185
+
186
+ # give the data adapter the opporunity to process error messages
187
+ # must return an raay of the form ["Title can't be blank", "Foo can't be blank"]
188
+ def errors_array(record)
189
+ record.errors.to_a.flatten
190
+ end
191
+
192
+ # Needed for seed and tests
193
+ def last
194
+ @model_class.last
195
+ end
196
+
197
+ def destroy_all
198
+ @model_class.all.destroy
199
+ end
200
+
201
+ private
202
+
203
+ # Parses and applies grid column filters
204
+ #
205
+ # Example column grid data:
206
+ #
207
+ # {"0" => {
208
+ # "data" => {
209
+ # "type" => "numeric",
210
+ # "comparison" => "gt",
211
+ # "value" => 10 },
212
+ # "field" => "id"
213
+ # },
214
+ # "1" => {
215
+ # "data" => {
216
+ # "type" => "string",
217
+ # "value" => "pizza"
218
+ # },
219
+ # "field" => "food_name"
220
+ # }}
221
+ #
222
+ # This will result in:
223
+ # Clazz.get(:id => 10, :food_name.like => "%pizza")
224
+ def apply_column_filters(relation, column_filter)
225
+ # these are still JSON-encoded due to the migration to Ext.direct
226
+ column_filter=JSON.parse(column_filter)
227
+
228
+ conditions = {}
229
+ column_filter.each do |v|
230
+ assoc, method = v["field"].split('__')
231
+ if method
232
+ query_path = relation.send(assoc).send(method) # Book.athor.last_name.like
233
+ else
234
+ query_path = assoc.to_sym # :last_name.like
235
+ end
236
+
237
+ value = v["value"]
238
+ type = v["type"]
239
+ case v["comparison"]
240
+ when "lt"
241
+ query_path=query_path.lt if ["date","numeric"].include? type
242
+ when "gt"
243
+ query_path=query_path.gt if ["date","numeric"].include? type
244
+ else
245
+ query_path=query_path.like if type == "string"
246
+ end
247
+
248
+ case type
249
+ when "string"
250
+ conditions[query_path]="%#{value}%"
251
+ when "date"
252
+ # convert value to the DB date
253
+ value.match /(\d\d)\/(\d\d)\/(\d\d\d\d)/
254
+ conditions[query_path]="#{$3}-#{$1}-#{$2}"
255
+ else
256
+ conditions[query_path]=value
257
+ end
258
+ end
259
+ relation.all conditions
260
+ end
261
+
262
+ end
263
+
264
+ end