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.
- data/.travis.yml +11 -0
- data/CHANGELOG.rdoc +10 -0
- data/README.md +36 -2
- data/Rakefile +1 -3
- data/config/ci/before-travis.sh +28 -0
- data/lib/netzke/active_record.rb +10 -8
- data/lib/netzke/active_record/attributes.rb +28 -17
- data/lib/netzke/active_record/relation_extensions.rb +3 -1
- data/lib/netzke/basepack.rb +10 -2
- data/lib/netzke/basepack/action_column.rb +6 -8
- data/lib/netzke/basepack/data_accessor.rb +11 -174
- data/lib/netzke/basepack/data_adapters/abstract_adapter.rb +164 -0
- data/lib/netzke/basepack/data_adapters/active_record_adapter.rb +279 -0
- data/lib/netzke/basepack/data_adapters/data_mapper_adapter.rb +264 -0
- data/lib/netzke/basepack/data_adapters/sequel_adapter.rb +260 -0
- data/lib/netzke/basepack/form_panel.rb +3 -3
- data/lib/netzke/basepack/form_panel/fields.rb +6 -10
- data/lib/netzke/basepack/form_panel/javascripts/form_panel.js +1 -0
- data/lib/netzke/basepack/form_panel/services.rb +15 -16
- data/lib/netzke/basepack/grid_panel.rb +16 -10
- data/lib/netzke/basepack/grid_panel/columns.rb +6 -7
- data/lib/netzke/basepack/grid_panel/javascripts/event_handling.js +29 -27
- data/lib/netzke/basepack/grid_panel/services.rb +13 -90
- data/lib/netzke/basepack/paging_form_panel.rb +3 -3
- data/lib/netzke/basepack/query_builder.rb +2 -0
- data/lib/netzke/basepack/query_builder/javascripts/query_builder.js +29 -19
- data/lib/netzke/basepack/search_panel.rb +6 -3
- data/lib/netzke/basepack/search_panel/javascripts/search_panel.js +2 -1
- data/lib/netzke/basepack/search_window.rb +2 -1
- data/lib/netzke/basepack/version.rb +1 -1
- data/lib/netzke/data_mapper.rb +18 -0
- data/lib/netzke/data_mapper/attributes.rb +273 -0
- data/lib/netzke/data_mapper/combobox_options.rb +11 -0
- data/lib/netzke/data_mapper/relation_extensions.rb +38 -0
- data/lib/netzke/sequel.rb +18 -0
- data/lib/netzke/sequel/attributes.rb +274 -0
- data/lib/netzke/sequel/combobox_options.rb +10 -0
- data/lib/netzke/sequel/relation_extensions.rb +40 -0
- data/netzke-basepack.gemspec +24 -13
- data/test/basepack_test_app/Gemfile +33 -8
- data/test/basepack_test_app/Gemfile.lock +98 -79
- data/test/basepack_test_app/Guardfile +46 -0
- data/test/basepack_test_app/app/components/book_grid_with_persistence.rb +3 -0
- data/test/basepack_test_app/app/components/extras/book_presentation.rb +10 -3
- data/test/basepack_test_app/app/models/address.rb +27 -1
- data/test/basepack_test_app/app/models/author.rb +28 -0
- data/test/basepack_test_app/app/models/book.rb +43 -0
- data/test/basepack_test_app/app/models/book_with_custom_primary_key.rb +22 -0
- data/test/basepack_test_app/app/models/role.rb +21 -0
- data/test/basepack_test_app/app/models/user.rb +24 -0
- data/test/basepack_test_app/config/database.yml.sample +11 -10
- data/test/basepack_test_app/config/database.yml.travis +15 -0
- data/test/basepack_test_app/config/initializers/data_mapper_logging.rb +3 -0
- data/test/basepack_test_app/config/initializers/sequel.rb +26 -0
- data/test/basepack_test_app/db/schema.rb +0 -3
- data/test/basepack_test_app/features/grid_panel.feature +28 -8
- data/test/basepack_test_app/features/grid_sorting.feature +6 -6
- data/test/basepack_test_app/features/paging_form_panel.feature +13 -13
- data/test/basepack_test_app/features/search_in_grid.feature +31 -31
- data/test/basepack_test_app/features/step_definitions/generic_steps.rb +3 -1
- data/test/basepack_test_app/features/support/env.rb +17 -4
- data/test/basepack_test_app/lib/tasks/travis.rake +7 -0
- data/test/basepack_test_app/spec/components/form_panel_spec.rb +2 -2
- data/test/basepack_test_app/spec/data_adapter/adapter_spec.rb +68 -0
- data/test/basepack_test_app/spec/{active_record → data_adapter}/attributes_spec.rb +12 -4
- data/test/basepack_test_app/spec/data_adapter/relation_extensions_spec.rb +125 -0
- data/test/basepack_test_app/spec/spec_helper.rb +9 -0
- data/test/unit/active_record_basepack_test.rb +1 -1
- data/test/unit/grid_panel_test.rb +1 -1
- metadata +26 -31
- data/app/models/netzke_field_list.rb +0 -261
- data/app/models/netzke_model_attr_list.rb +0 -21
- data/app/models/netzke_persistent_array_auto_model.rb +0 -57
- 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
|