skozlov-netzke-basepack 0.1.1.2 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.autotest +1 -0
- data/.gitignore +5 -0
- data/LICENSE +2 -19
- data/README.rdoc +87 -0
- data/Rakefile +28 -12
- data/TODO.rdoc +7 -0
- data/VERSION +1 -0
- data/autotest/discover.rb +3 -0
- data/init.rb +0 -1
- data/javascripts/basepack.js +839 -49
- data/lib/app/models/netzke_auto_column.rb +56 -0
- data/lib/netzke/accordion_panel.rb +113 -0
- data/lib/netzke/active_record/basepack.rb +104 -0
- data/lib/netzke/active_record/data_accessor.rb +21 -0
- data/lib/netzke/basic_app.rb +325 -0
- data/lib/netzke/border_layout_panel.rb +128 -0
- data/lib/netzke/configuration_panel.rb +24 -0
- data/lib/netzke/data_accessor.rb +71 -0
- data/lib/netzke/ext.rb +6 -0
- data/lib/netzke/field_model.rb +131 -0
- data/lib/netzke/fields_configurator.rb +95 -0
- data/lib/netzke/form_panel.rb +214 -0
- data/lib/netzke/form_panel_api.rb +74 -0
- data/lib/netzke/form_panel_extras/javascripts/xcheckbox.js +82 -0
- data/lib/netzke/form_panel_js.rb +129 -0
- data/lib/netzke/grid_panel.rb +442 -0
- data/lib/netzke/grid_panel_api.rb +352 -0
- data/lib/netzke/grid_panel_extras/javascripts/check-column.js +33 -0
- data/{javascripts → lib/netzke/grid_panel_extras/javascripts}/filters.js +0 -0
- data/lib/netzke/grid_panel_extras/javascripts/rows-dd.js +280 -0
- data/lib/netzke/grid_panel_js.rb +721 -0
- data/lib/netzke/panel.rb +13 -0
- data/lib/netzke/plugins/configuration_tool.rb +121 -0
- data/lib/netzke/property_editor.rb +105 -0
- data/lib/netzke/property_editor_extras/helper_model.rb +126 -0
- data/lib/netzke/search_panel.rb +62 -0
- data/lib/netzke/tab_panel.rb +160 -0
- data/lib/netzke/table_editor.rb +118 -0
- data/lib/netzke/tree_panel.rb +73 -0
- data/lib/netzke/wrapper.rb +42 -0
- data/lib/netzke-basepack.rb +9 -15
- data/netzke-basepack.gemspec +147 -20
- data/stylesheets/basepack.css +26 -0
- data/test/app_root/app/models/book.rb +1 -1
- data/test/app_root/config/environment.rb +1 -0
- data/test/app_root/db/migrate/20081222033440_create_genres.rb +1 -0
- data/test/app_root/db/migrate/20081222035855_create_netzke_preferences.rb +1 -1
- data/test/app_root/db/migrate/20090102223630_create_netzke_layouts.rb +14 -0
- data/test/app_root/vendor/plugins/acts_as_list/README +23 -0
- data/test/app_root/vendor/plugins/acts_as_list/init.rb +3 -0
- data/test/app_root/vendor/plugins/acts_as_list/lib/active_record/acts/list.rb +256 -0
- data/test/test_helper.rb +1 -2
- data/test/unit/accordion_panel_test.rb +20 -0
- data/test/unit/active_record_basepack_test.rb +54 -0
- data/test/unit/grid_panel_test.rb +43 -0
- data/test/unit/helper_model_test.rb +30 -0
- data/test/unit/netzke_basepack_test.rb +4 -0
- data/test/unit/tab_panel_test.rb +21 -0
- metadata +96 -72
- data/CHANGELOG +0 -14
- data/Manifest +0 -65
- data/README.mdown +0 -18
- data/generators/netzke_basepack/USAGE +0 -8
- data/generators/netzke_basepack/netzke_basepack_generator.rb +0 -8
- data/generators/netzke_basepack/netzke_grid_generator.rb +0 -7
- data/generators/netzke_basepack/templates/create_netzke_grid_columns.rb +0 -21
- data/lib/app/models/netzke_grid_column.rb +0 -23
- data/lib/netzke/accordion.rb +0 -11
- data/lib/netzke/ar_ext.rb +0 -163
- data/lib/netzke/column.rb +0 -43
- data/lib/netzke/container.rb +0 -81
- data/lib/netzke/grid.rb +0 -120
- data/lib/netzke/grid_interface.rb +0 -156
- data/lib/netzke/grid_js_builder.rb +0 -276
- data/lib/netzke/preference_grid.rb +0 -43
- data/lib/netzke/properties_tool.rb +0 -66
- data/lib/netzke/property_grid.rb +0 -60
- data/test/ar_ext_test.rb +0 -39
- data/test/column_test.rb +0 -27
- data/test/grid_test.rb +0 -43
- data/test/netzke_basepack_test.rb +0 -8
@@ -0,0 +1,442 @@
|
|
1
|
+
require 'searchlogic'
|
2
|
+
|
3
|
+
module Netzke
|
4
|
+
# == GridPanel
|
5
|
+
# Ext.grid.EditorGridPanel + server-side code
|
6
|
+
#
|
7
|
+
# == Features:
|
8
|
+
# * multi-line CRUD operations - get, post, delete, create
|
9
|
+
# * (multe-record) editing and adding records through a form
|
10
|
+
# * column resize, move and hide
|
11
|
+
# * permissions
|
12
|
+
# * sorting
|
13
|
+
# * pagination
|
14
|
+
# * filtering
|
15
|
+
# * extended configurable search
|
16
|
+
# * rows reordering (drag-n-drop)
|
17
|
+
# * dynamic configuration of properties and columns
|
18
|
+
#
|
19
|
+
# == Class configuration
|
20
|
+
# Configuration on this level is effective during the life-time of the application. They can be put into a .rb file
|
21
|
+
# inside of config/initializers like this:
|
22
|
+
#
|
23
|
+
# Netzke::GridPanel.configure :column_filters_available, false
|
24
|
+
# Netzke::GridPanel.configure :default_config => {:ext_config => {:enable_config_tool => false}}
|
25
|
+
#
|
26
|
+
# Most of these options directly influence the amount of JavaScript code that is generated for this widget's class.
|
27
|
+
# The less functionality is enabled, the less code is generated.
|
28
|
+
#
|
29
|
+
# The following configuration options are available:
|
30
|
+
# * <tt>:column_filters_available</tt> - (default is true) include code for the filters in the column's context menu
|
31
|
+
# * <tt>:config_tool_available</tt> - (default is true) include code for the configuration tool that launches the configuration panel
|
32
|
+
# * <tt>:edit_in_form_available</tt> - (defaults to true) include code for (multi-record) editing and adding records through a form
|
33
|
+
# * <tt>:extended_search_available</tt> - (defaults to true) include code for extended configurable search
|
34
|
+
# * <tt>:default_config</tt> - a hash of default configuration options for each instance of the GridPanel widget.
|
35
|
+
# See the "Instance configuration" section below.
|
36
|
+
#
|
37
|
+
# == Instance configuration
|
38
|
+
# The following config options are available:
|
39
|
+
# * <tt>:data_class_name</tt> - name of the ActiveRecord model that provides data to this GridPanel.
|
40
|
+
# * <tt>:strong_default_attrs</tt> - a hash of attributes to be merged atop of every created/updated record.
|
41
|
+
# * <tt>:scopes</tt> - an array of searchlogic-compatible scopes to filter grid data like this:
|
42
|
+
#
|
43
|
+
# ["user_id_not", 100]
|
44
|
+
#
|
45
|
+
# In the <tt>:ext_config</tt> hash (see Netzke::Base) the following GridPanel specific options are available:
|
46
|
+
#
|
47
|
+
# * <tt>:enable_column_filters</tt> - enable filters in column's context menu
|
48
|
+
# * <tt>:enable_edit_in_form</tt> - provide buttons into the toolbar that activate editing/adding records via a form
|
49
|
+
# * <tt>:enable_extended_search</tt> - provide a button into the toolbar that shows configurable search form
|
50
|
+
# * <tt>:enable_context_menu</tt> - enable rows context menu
|
51
|
+
# * <tt>:enable_rows_reordering</tt> - enable reordering of rows with drag-n-drop; underlying model (specified in <tt>:data_class_name</tt>) must implement "acts_as_list"-compatible functionality; defaults to <tt>false</tt>
|
52
|
+
# * <tt>:enable_pagination</tt> - enable pagination; defaults to <tt>true</tt>
|
53
|
+
# * <tt>:rows_per_page</tt> - number of rows per page (ignored when <tt>:enable_pagination</tt> is set to <tt>false</tt>)
|
54
|
+
# * <tt>:load_inline_data</tt> - load initial data into the grid right after its instantiation (saves a request to server); defaults to <tt>true</tt>
|
55
|
+
# * <tt>:mode</tt> - when set to <tt>:config</tt>, GridPanel loads in configuration mode
|
56
|
+
#
|
57
|
+
# Additionally supports Netzke::Base config options.
|
58
|
+
class GridPanel < Base
|
59
|
+
# javascript (client-side)
|
60
|
+
include Netzke::GridPanelJs
|
61
|
+
# API (server-side)
|
62
|
+
include Netzke::GridPanelApi
|
63
|
+
# Code shared between GridPanel, FormPanel, and other widgets that serve as interface to database tables
|
64
|
+
include Netzke::DataAccessor
|
65
|
+
|
66
|
+
def self.enforce_config_consistency
|
67
|
+
config[:default_config][:ext_config][:enable_edit_in_form] &&= config[:edit_in_form_available]
|
68
|
+
config[:default_config][:ext_config][:enable_extended_search] &&= config[:extended_search_available]
|
69
|
+
config[:default_config][:ext_config][:enable_rows_reordering] &&= config[:rows_reordering_available]
|
70
|
+
end
|
71
|
+
|
72
|
+
# Class-level configuration. This options directly influence the amount of generated
|
73
|
+
# javascript code for this widget's class. For example, if you don't want filters for the grid,
|
74
|
+
# set :column_filters_available to false, and the javascript for the filters won't be included at all.
|
75
|
+
def self.config
|
76
|
+
set_default_config({
|
77
|
+
|
78
|
+
:column_filters_available => true,
|
79
|
+
:config_tool_available => true,
|
80
|
+
:edit_in_form_available => true,
|
81
|
+
:extended_search_available => true,
|
82
|
+
:rows_reordering_available => false,
|
83
|
+
|
84
|
+
:default_config => {
|
85
|
+
:ext_config => {
|
86
|
+
:enable_edit_in_form => true,
|
87
|
+
:enable_extended_search => true,
|
88
|
+
:enable_column_filters => true,
|
89
|
+
:load_inline_data => true,
|
90
|
+
:enable_context_menu => true,
|
91
|
+
|
92
|
+
:enable_pagination => true,
|
93
|
+
:rows_per_page => 25,
|
94
|
+
|
95
|
+
:mode => :normal, # when set to :config, :configuration button is enabled
|
96
|
+
|
97
|
+
:enable_rows_reordering => false, # drag n drop
|
98
|
+
|
99
|
+
:tools => %w{ refresh }
|
100
|
+
},
|
101
|
+
|
102
|
+
:persistent_config => false
|
103
|
+
}
|
104
|
+
})
|
105
|
+
end
|
106
|
+
|
107
|
+
# Include extra javascript that we depend on
|
108
|
+
def self.include_js
|
109
|
+
res = []
|
110
|
+
|
111
|
+
# Checkcolumn
|
112
|
+
res << "#{File.dirname(__FILE__)}/grid_panel_extras/javascripts/check-column.js"
|
113
|
+
|
114
|
+
# Filters
|
115
|
+
if config[:column_filters_available]
|
116
|
+
ext_examples = Netzke::Base.config[:ext_location] + "/examples/"
|
117
|
+
res << ext_examples + "grid-filtering/menu/EditableItem.js"
|
118
|
+
res << ext_examples + "grid-filtering/menu/RangeMenu.js"
|
119
|
+
res << ext_examples + "grid-filtering/grid/GridFilters.js"
|
120
|
+
|
121
|
+
%w{Boolean Date List Numeric String}.unshift("").each do |f|
|
122
|
+
res << ext_examples + "grid-filtering/grid/filter/#{f}Filter.js"
|
123
|
+
end
|
124
|
+
|
125
|
+
res << "#{File.dirname(__FILE__)}/grid_panel_extras/javascripts/filters.js"
|
126
|
+
|
127
|
+
end
|
128
|
+
|
129
|
+
# DD
|
130
|
+
if config[:rows_reordering_available]
|
131
|
+
res << "#{File.dirname(__FILE__)}/grid_panel_extras/javascripts/rows-dd.js"
|
132
|
+
end
|
133
|
+
|
134
|
+
res
|
135
|
+
end
|
136
|
+
|
137
|
+
# Define connection points between client side and server side of GridPanel.
|
138
|
+
# See implementation of equally named methods in the GridPanelApi module.
|
139
|
+
api :get_data, :post_data, :delete_data, :resize_column, :move_column, :hide_column, :get_combobox_options, :move_rows
|
140
|
+
|
141
|
+
# Edit in form
|
142
|
+
api :create_new_record if config[:edit_in_form_available]
|
143
|
+
|
144
|
+
def data_class
|
145
|
+
@data_class ||= config[:data_class_name].nil? ? raise(ArgumentError, "No data_class_name specified for widget #{id_name}") : config[:data_class_name].constantize
|
146
|
+
end
|
147
|
+
|
148
|
+
|
149
|
+
def initialize(config = {}, parent = nil)
|
150
|
+
super
|
151
|
+
|
152
|
+
apply_helpers
|
153
|
+
end
|
154
|
+
|
155
|
+
# Columns to be displayed by the FieldConfigurator.
|
156
|
+
def self.config_columns
|
157
|
+
[
|
158
|
+
{:name => :name, :type => :string, :editor => :combobox, :width => 200},
|
159
|
+
{:name => :excluded, :type => :boolean, :editor => :checkbox, :width => 40, :header => "Excl"},
|
160
|
+
{:name => :value},
|
161
|
+
{:name => :header},
|
162
|
+
{:name => :hidden, :type => :boolean, :editor => :checkbox},
|
163
|
+
{:name => :read_only, :type => :boolean, :editor => :checkbox, :header => "R"},
|
164
|
+
{:name => :editor, :type => :string, :editor => {:xtype => :combobox, :options => Netzke::Ext::FORM_FIELD_XTYPES}},
|
165
|
+
{:name => :renderer, :type => :string},
|
166
|
+
# {:name => :renderer, :type => :string, :editor => {:xtype => :jsonfield}},
|
167
|
+
{:name => :with_filters, :type => :boolean, :editor => :checkbox, :default => true, :header => "Filters"},
|
168
|
+
|
169
|
+
# some rarely used configurations, hidden
|
170
|
+
{:name => :width, :type => :integer, :editor => :numberfield, :hidden => true},
|
171
|
+
{:name => :hideable, :type => :boolean, :editor => :checkbox, :default => true, :hidden => true},
|
172
|
+
{:name => :sortable, :type => :boolean, :editor => :checkbox, :default => true, :hidden => true},
|
173
|
+
]
|
174
|
+
end
|
175
|
+
|
176
|
+
def self.property_fields
|
177
|
+
res = [
|
178
|
+
{:name => :ext_config__title, :type => :string},
|
179
|
+
{:name => :ext_config__header, :type => :boolean, :default => true},
|
180
|
+
{:name => :ext_config__enable_context_menu, :type => :boolean, :default => true},
|
181
|
+
{:name => :ext_config__context_menu, :type => :json},
|
182
|
+
{:name => :ext_config__enable_pagination, :type => :boolean, :default => true},
|
183
|
+
{:name => :ext_config__rows_per_page, :type => :integer},
|
184
|
+
# {:name => :ext_config__bbar, :type => :json},
|
185
|
+
{:name => :ext_config__prohibit_create, :type => :boolean},
|
186
|
+
{:name => :ext_config__prohibit_update, :type => :boolean},
|
187
|
+
{:name => :ext_config__prohibit_delete, :type => :boolean},
|
188
|
+
{:name => :ext_config__prohibit_read, :type => :boolean}
|
189
|
+
]
|
190
|
+
|
191
|
+
res << {:name => :ext_config__enable_extended_search, :type => :boolean} if config[:extended_search_available]
|
192
|
+
res << {:name => :ext_config__enable_edit_in_form, :type => :boolean} if config[:edit_in_form_available]
|
193
|
+
|
194
|
+
res
|
195
|
+
|
196
|
+
end
|
197
|
+
|
198
|
+
def independent_config
|
199
|
+
res = super
|
200
|
+
|
201
|
+
# Bottom bar
|
202
|
+
if res[:ext_config][:bbar].nil?
|
203
|
+
res[:ext_config][:bbar] = %w{ add edit apply del }
|
204
|
+
res[:ext_config][:bbar] << "-" << "add_in_form" << "edit_in_form" if res[:ext_config][:enable_edit_in_form]
|
205
|
+
res[:ext_config][:bbar] << "-" << "search" if res[:ext_config][:enable_extended_search]
|
206
|
+
end
|
207
|
+
|
208
|
+
# Context menu
|
209
|
+
res[:ext_config][:context_menu] ||= default_context_menu(res)
|
210
|
+
|
211
|
+
res
|
212
|
+
end
|
213
|
+
|
214
|
+
def default_context_menu(indep_config)
|
215
|
+
res = %w{ edit del }
|
216
|
+
res << "-" << "edit_in_form" if indep_config[:ext_config][:enable_edit_in_form]
|
217
|
+
res
|
218
|
+
end
|
219
|
+
|
220
|
+
def configuration_widgets
|
221
|
+
res = []
|
222
|
+
res << {
|
223
|
+
:persistent_config => true,
|
224
|
+
:name => 'columns',
|
225
|
+
:widget_class_name => "FieldsConfigurator",
|
226
|
+
:active => true,
|
227
|
+
:widget => self
|
228
|
+
}
|
229
|
+
res << {
|
230
|
+
:name => 'general',
|
231
|
+
:widget_class_name => "PropertyEditor",
|
232
|
+
:widget => self,
|
233
|
+
:ext_config => {:title => false}
|
234
|
+
}
|
235
|
+
res
|
236
|
+
end
|
237
|
+
|
238
|
+
def actions
|
239
|
+
# Defaults
|
240
|
+
res = {
|
241
|
+
:add => {:text => 'Add', :disabled => ext_config[:prohibit_create]},
|
242
|
+
:edit => {:text => 'Edit', :disabled => true},
|
243
|
+
:del => {:text => 'Delete', :disabled => true},
|
244
|
+
:apply => {:text => 'Apply', :disabled => ext_config[:prohibit_update] && ext_config[:prohibit_create]}
|
245
|
+
}
|
246
|
+
|
247
|
+
# Edit in form
|
248
|
+
res.merge!({
|
249
|
+
:add_in_form => {:text => 'Add in form'},
|
250
|
+
:edit_in_form => {:text => 'Edit in form', :disabled => true}
|
251
|
+
}) if ext_config[:enable_edit_in_form]
|
252
|
+
|
253
|
+
# Extended search
|
254
|
+
res.merge!({
|
255
|
+
:search => {:text => 'Search'}
|
256
|
+
}) if ext_config[:enable_extended_search]
|
257
|
+
|
258
|
+
res
|
259
|
+
end
|
260
|
+
|
261
|
+
def initial_late_aggregatees
|
262
|
+
res = {}
|
263
|
+
|
264
|
+
# Edit in form
|
265
|
+
res.merge!({
|
266
|
+
:edit_form => {
|
267
|
+
:widget_class_name => "FormPanel",
|
268
|
+
:persistent_config => true,
|
269
|
+
:data_class_name => config[:data_class_name],
|
270
|
+
:ext_config => {
|
271
|
+
:bbar => false,
|
272
|
+
:header => false,
|
273
|
+
:mode => ext_config[:mode]
|
274
|
+
}
|
275
|
+
},
|
276
|
+
|
277
|
+
:multi_edit_form => {
|
278
|
+
:widget_class_name => "FormPanel",
|
279
|
+
:persistent_config => true,
|
280
|
+
:data_class_name => config[:data_class_name],
|
281
|
+
:ext_config => {
|
282
|
+
:bbar => false,
|
283
|
+
:header => false,
|
284
|
+
:mode => ext_config[:mode]
|
285
|
+
}
|
286
|
+
},
|
287
|
+
|
288
|
+
:new_record_form => {
|
289
|
+
:widget_class_name => "FormPanel",
|
290
|
+
:persistent_config => true,
|
291
|
+
:data_class_name => config[:data_class_name],
|
292
|
+
:ext_config => {
|
293
|
+
:bbar => false,
|
294
|
+
:header => false,
|
295
|
+
:mode => ext_config[:mode]
|
296
|
+
},
|
297
|
+
:record => config[:data_class_name].constantize.new
|
298
|
+
}
|
299
|
+
}) if ext_config[:enable_edit_in_form]
|
300
|
+
|
301
|
+
# Extended search
|
302
|
+
res.merge!({
|
303
|
+
:search_panel => {
|
304
|
+
:widget_class_name => "SearchPanel",
|
305
|
+
:search_class_name => config[:data_class_name],
|
306
|
+
:persistent_config => true,
|
307
|
+
:ext_config => {
|
308
|
+
:header => false,
|
309
|
+
:bbar => false,
|
310
|
+
:mode => ext_config[:mode]
|
311
|
+
},
|
312
|
+
}
|
313
|
+
}) if ext_config[:enable_extended_search]
|
314
|
+
|
315
|
+
res
|
316
|
+
end
|
317
|
+
|
318
|
+
|
319
|
+
include Plugins::ConfigurationTool if config[:config_tool_available] # it will load ConfigurationPanel into a modal window
|
320
|
+
|
321
|
+
def columns
|
322
|
+
@columns ||= get_columns
|
323
|
+
end
|
324
|
+
|
325
|
+
# Normalized columns
|
326
|
+
def normalized_columns
|
327
|
+
@normalized_columns ||= normalize_columns(columns)
|
328
|
+
end
|
329
|
+
|
330
|
+
def get_columns
|
331
|
+
if config[:persistent_config]
|
332
|
+
persistent_config['layout__columns'] ||= default_columns
|
333
|
+
res = normalize_array_of_columns(persistent_config['layout__columns'])
|
334
|
+
else
|
335
|
+
res = default_columns
|
336
|
+
end
|
337
|
+
|
338
|
+
# denormalize
|
339
|
+
res.map{ |c| c.is_a?(Hash) && c.reject{ |k,v| k == :name }.empty? ? c[:name].to_sym : c }
|
340
|
+
end
|
341
|
+
|
342
|
+
# Normalizes the column at position +index+ and returns it.
|
343
|
+
def column_at(index)
|
344
|
+
if columns[index].is_a?(Hash)
|
345
|
+
columns[index]
|
346
|
+
else
|
347
|
+
column_name = columns.delete_at(index)
|
348
|
+
normalized_column = normalize_column(column_name)
|
349
|
+
columns.insert(index, normalized_column)
|
350
|
+
normalized_column
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
# Stores modified columns in persistent storage
|
355
|
+
def save_columns!
|
356
|
+
persistent_config[:layout__columns] = columns
|
357
|
+
end
|
358
|
+
|
359
|
+
TYPE_EDITOR_MAP = {
|
360
|
+
:integer => :numberfield,
|
361
|
+
:boolean => :checkbox,
|
362
|
+
:date => :datefield,
|
363
|
+
:datetime => :xdatetime,
|
364
|
+
:text => :textarea
|
365
|
+
# :string => :textfield
|
366
|
+
}
|
367
|
+
|
368
|
+
def default_columns
|
369
|
+
# columns specified in widget's config
|
370
|
+
columns_from_config = config[:columns] && normalize_columns(config[:columns])
|
371
|
+
|
372
|
+
if columns_from_config
|
373
|
+
# reverse-merge each column hash from config with each column hash from exposed_attributes (columns from config have higher priority)
|
374
|
+
for c in columns_from_config
|
375
|
+
corresponding_exposed_column = predefined_columns.find{ |k| k[:name] == c[:name] }
|
376
|
+
c.reverse_merge!(corresponding_exposed_column) if corresponding_exposed_column
|
377
|
+
end
|
378
|
+
columns_for_create = columns_from_config
|
379
|
+
else
|
380
|
+
# we didn't have columns configured in widget's config, so, use the columns from the data class
|
381
|
+
columns_for_create = predefined_columns
|
382
|
+
end
|
383
|
+
|
384
|
+
columns_for_create.map! do |c|
|
385
|
+
# detect ActiveRecord column type (if the column is "real") or fall back to :virtual
|
386
|
+
type = (data_class.columns_hash[c[:name].to_s] && data_class.columns_hash[c[:name].to_s].type) || :virtual
|
387
|
+
|
388
|
+
# detect :assoc__method
|
389
|
+
if c[:name].to_s.index('__')
|
390
|
+
assoc_name, method = c[:name].to_s.split('__').map(&:to_sym)
|
391
|
+
if assoc = data_class.reflect_on_association(assoc_name)
|
392
|
+
assoc_column = assoc.klass.columns_hash[method.to_s]
|
393
|
+
assoc_method_type = assoc_column.try(:type)
|
394
|
+
if assoc_method_type
|
395
|
+
c[:editor] ||= TYPE_EDITOR_MAP[assoc_method_type] == :checkbox ? :checkbox : :combobox
|
396
|
+
end
|
397
|
+
type = :association
|
398
|
+
end
|
399
|
+
end
|
400
|
+
|
401
|
+
# detect association column (e.g. :category_id)
|
402
|
+
if assoc = data_class.reflect_on_all_associations.detect{|a| a.primary_key_name.to_sym == c[:name]}
|
403
|
+
c[:editor] ||= :combobox
|
404
|
+
assoc_method = %w{name title label id}.detect{|m| (assoc.klass.instance_methods + assoc.klass.column_names).include?(m) } || assoc.klass.primary_key
|
405
|
+
c[:name] = "#{assoc.name}__#{assoc_method}".to_sym
|
406
|
+
type = :association
|
407
|
+
end
|
408
|
+
|
409
|
+
# Some smart defaults
|
410
|
+
|
411
|
+
# default editor, dependent on column type
|
412
|
+
c[:editor] ||= TYPE_EDITOR_MAP[type] unless TYPE_EDITOR_MAP[type].nil?
|
413
|
+
|
414
|
+
# narrow column for checkbox
|
415
|
+
c[:width] ||= 50 if c[:editor] == :checkbox
|
416
|
+
|
417
|
+
# wider column for xdatetime
|
418
|
+
c[:width] ||= 120 if c[:editor] == :xdatetime
|
419
|
+
|
420
|
+
# hide ID column
|
421
|
+
c[:hidden] = true if c[:name] == data_class.primary_key.to_sym && c[:hidden].nil?
|
422
|
+
|
423
|
+
# Some default limitations for virtual columns
|
424
|
+
if type == :virtual
|
425
|
+
# disable filters
|
426
|
+
c[:with_filters].nil? && c[:with_filters] = false
|
427
|
+
# disable sorting
|
428
|
+
c[:sortable].nil? && c[:sortable] = false
|
429
|
+
# read-only
|
430
|
+
c[:read_only].nil? && c[:read_only] = true
|
431
|
+
end
|
432
|
+
|
433
|
+
# denormalize column (save space)
|
434
|
+
c.reject{ |k,v| k == :name }.empty? ? c[:name] : c
|
435
|
+
end
|
436
|
+
|
437
|
+
columns_for_create
|
438
|
+
|
439
|
+
end
|
440
|
+
|
441
|
+
end
|
442
|
+
end
|
@@ -0,0 +1,352 @@
|
|
1
|
+
module Netzke
|
2
|
+
module GridPanelApi
|
3
|
+
def post_data(params)
|
4
|
+
success = true
|
5
|
+
mod_records = {}
|
6
|
+
[:create, :update].each do |operation|
|
7
|
+
data = ActiveSupport::JSON.decode(params["#{operation}d_records"]) if params["#{operation}d_records"]
|
8
|
+
if !data.nil? && !data.empty? # data may be nil for one of the operations
|
9
|
+
mod_records[operation] = process_data(data, operation)
|
10
|
+
mod_records[operation] = nil if mod_records[operation].empty?
|
11
|
+
end
|
12
|
+
break if !success
|
13
|
+
end
|
14
|
+
{
|
15
|
+
:update_new_records => mod_records[:create],
|
16
|
+
:update_mod_records => mod_records[:update] || {},
|
17
|
+
:feedback => @flash
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
def get_data(params = {})
|
22
|
+
if !ext_config[:prohibit_read]
|
23
|
+
records = get_records(params)
|
24
|
+
{:data => records.map{|r| r.to_array(normalized_columns)}, :total => ext_config[:enable_pagination] && records.total_entries}
|
25
|
+
else
|
26
|
+
flash :error => "You don't have permissions to read data"
|
27
|
+
{:feedback => @flash}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def delete_data(params = {})
|
32
|
+
if !ext_config[:prohibit_delete]
|
33
|
+
record_ids = ActiveSupport::JSON.decode(params[:records])
|
34
|
+
klass = config[:data_class_name].constantize
|
35
|
+
klass.delete(record_ids)
|
36
|
+
{:feedback => "Deleted #{record_ids.size} record(s)", :load_store_data => get_data}
|
37
|
+
else
|
38
|
+
{:feedback => "You don't have permissions to delete data"}
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def resize_column(params)
|
43
|
+
raise "Called api_resize_column while not configured to do so" if ext_config[:enable_column_resize] == false
|
44
|
+
column_at(params[:index].to_i)[:width] = params[:size].to_i
|
45
|
+
save_columns!
|
46
|
+
{}
|
47
|
+
end
|
48
|
+
|
49
|
+
def hide_column(params)
|
50
|
+
raise "Called api_hide_column while not configured to do so" if ext_config[:enable_column_hide] == false
|
51
|
+
column_at(params[:index].to_i)[:hidden] = params[:hidden].to_b
|
52
|
+
save_columns!
|
53
|
+
{}
|
54
|
+
end
|
55
|
+
|
56
|
+
def move_column(params)
|
57
|
+
raise "Called api_move_column while not configured to do so" if ext_config[:enable_column_move] == false
|
58
|
+
column_to_move = columns.delete_at(params[:old_index].to_i)
|
59
|
+
columns.insert(params[:new_index].to_i, column_to_move)
|
60
|
+
save_columns!
|
61
|
+
|
62
|
+
# reorder the columns on the client side (still not sure if it's not an overkill)
|
63
|
+
# {:reorder_columns => columns.map(&:name)} # Well, I think it IS an overkill - commented out
|
64
|
+
# until proven to be necessary
|
65
|
+
{}
|
66
|
+
end
|
67
|
+
|
68
|
+
# Return the choices for the column
|
69
|
+
def get_combobox_options(params)
|
70
|
+
column = params[:column]
|
71
|
+
query = params[:query]
|
72
|
+
|
73
|
+
{:data => config[:data_class_name].constantize.options_for(column, query).map{|s| [s]}}
|
74
|
+
end
|
75
|
+
|
76
|
+
# Returns searchlogic's search with all the conditions
|
77
|
+
def get_search(params)
|
78
|
+
@search ||= begin
|
79
|
+
raise ArgumentError, "No data_class_name specified for widget '#{name}'" if !config[:data_class_name]
|
80
|
+
|
81
|
+
# make params coming from Ext grid filters understandable by searchlogic
|
82
|
+
search_params = normalize_params(params)
|
83
|
+
|
84
|
+
# merge with conditions coming from the config
|
85
|
+
search_params[:conditions].deep_merge!(config[:conditions] || {})
|
86
|
+
|
87
|
+
# merge with extra conditions (in searchlogic format, come from the extended search form)
|
88
|
+
search_params[:conditions].deep_merge!(
|
89
|
+
normalize_extra_conditions(ActiveSupport::JSON.decode(params[:extra_conditions]))
|
90
|
+
) if params[:extra_conditions]
|
91
|
+
|
92
|
+
search = config[:data_class_name].constantize.search(search_params)
|
93
|
+
|
94
|
+
# applying scopes
|
95
|
+
scopes.each do |s|
|
96
|
+
if s.is_a?(Array)
|
97
|
+
scope_name, *args = s
|
98
|
+
search.send(scope_name, *args)
|
99
|
+
else
|
100
|
+
search.send(s)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
search
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def configuration_panel__columns__get_combobox_options(params)
|
109
|
+
query = params[:query]
|
110
|
+
|
111
|
+
data_arry = case params[:column]
|
112
|
+
when "name"
|
113
|
+
predefined_columns.map{ |c| c[:name].to_s }
|
114
|
+
else
|
115
|
+
raise RuntimeError, "Don't know about options for column '#{params[:column]}'"
|
116
|
+
end
|
117
|
+
|
118
|
+
{:data => data_arry.grep(/^#{query}/).map{ |n| [n] }}.to_nifty_json
|
119
|
+
end
|
120
|
+
|
121
|
+
protected
|
122
|
+
|
123
|
+
# operation => :update || :create
|
124
|
+
def process_data(data, operation)
|
125
|
+
success = true
|
126
|
+
# mod_record_ids = []
|
127
|
+
mod_records = {}
|
128
|
+
if !ext_config["prohibit_#{operation}".to_sym]
|
129
|
+
klass = config[:data_class_name].constantize
|
130
|
+
modified_records = 0
|
131
|
+
data.each do |record_hash|
|
132
|
+
id = record_hash.delete('id')
|
133
|
+
record = operation == :create ? klass.new : klass.find(id)
|
134
|
+
success = true
|
135
|
+
|
136
|
+
# merge with strong default attirbutes
|
137
|
+
record_hash.merge!(config[:strong_default_attrs]) if config[:strong_default_attrs]
|
138
|
+
|
139
|
+
# process all attirubutes for this record
|
140
|
+
record_hash.each_pair do |k,v|
|
141
|
+
begin
|
142
|
+
record.send("#{k}=",v)
|
143
|
+
rescue ArgumentError => exc
|
144
|
+
flash :error => exc.message
|
145
|
+
success = false
|
146
|
+
break
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# try to save
|
151
|
+
# modified_records += 1 if success && record.save
|
152
|
+
mod_records[id] = record.to_array(columns) if success && record.save
|
153
|
+
# mod_record_ids << id if success && record.save
|
154
|
+
|
155
|
+
# flash eventual errors
|
156
|
+
if !record.errors.empty?
|
157
|
+
success = false
|
158
|
+
record.errors.each_full do |msg|
|
159
|
+
flash :error => msg
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
# flash :notice => "#{operation.to_s.capitalize}d #{modified_records} record(s)"
|
164
|
+
else
|
165
|
+
success = false
|
166
|
+
flash :error => "You don't have permissions to #{operation} data"
|
167
|
+
end
|
168
|
+
mod_records
|
169
|
+
end
|
170
|
+
|
171
|
+
# get records
|
172
|
+
def get_records(params)
|
173
|
+
# Restore params from widget_session if requested
|
174
|
+
if params[:with_last_params]
|
175
|
+
params = widget_session[:last_params]
|
176
|
+
else
|
177
|
+
# remember the last params
|
178
|
+
widget_session[:last_params] = params
|
179
|
+
end
|
180
|
+
|
181
|
+
search = get_search(params)
|
182
|
+
|
183
|
+
# sorting
|
184
|
+
if params[:sort]
|
185
|
+
assoc, method = params[:sort].split('__')
|
186
|
+
sort_string = method.nil? ? assoc : "#{assoc}_#{method}"
|
187
|
+
sort_string = (params[:dir] == "ASC" ? "ascend_by_" : "descend_by_") + sort_string
|
188
|
+
search.order(sort_string)
|
189
|
+
end
|
190
|
+
|
191
|
+
# pagination
|
192
|
+
if ext_config[:enable_pagination]
|
193
|
+
per_page = ext_config[:rows_per_page]
|
194
|
+
page = params[:limit] ? params[:start].to_i/params[:limit].to_i + 1 : 1
|
195
|
+
search.paginate(:per_page => per_page, :page => page)
|
196
|
+
else
|
197
|
+
search.all
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
# Create record with form
|
202
|
+
def create_new_record(params)
|
203
|
+
form_data = ActiveSupport::JSON.decode(params[:data])
|
204
|
+
res = aggregatee_instance(:new_record_form).create_or_update_record(form_data)
|
205
|
+
|
206
|
+
if res[:set_form_values]
|
207
|
+
# successful creation
|
208
|
+
res[:set_form_values] = nil
|
209
|
+
res[:on_successfull_record_creation] = true
|
210
|
+
end
|
211
|
+
res
|
212
|
+
end
|
213
|
+
|
214
|
+
# Move rows
|
215
|
+
def move_rows(params)
|
216
|
+
if defined?(ActsAsList) && data_class.ancestors.include?(ActsAsList::InstanceMethods)
|
217
|
+
ids = JSON.parse(params[:ids]).reverse
|
218
|
+
ids.each_with_index do |id, i|
|
219
|
+
r = data_class.find(id)
|
220
|
+
r.insert_at(params[:new_index].to_i + i + 1)
|
221
|
+
end
|
222
|
+
else
|
223
|
+
raise RuntimeError, "Data class should 'acts_as_list' to support moving rows"
|
224
|
+
end
|
225
|
+
{}
|
226
|
+
end
|
227
|
+
|
228
|
+
# When providing the edit_form aggregatee, fill in the form with the requested record
|
229
|
+
def load_aggregatee_with_cache(params)
|
230
|
+
if params[:id] == 'edit_form'
|
231
|
+
super(params.merge(:config => {:record_id => params[:record_id]}.to_json))
|
232
|
+
else
|
233
|
+
super
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
# Search scopes, in searchlogic format
|
238
|
+
def scopes
|
239
|
+
@scopes ||= config[:scopes] || []
|
240
|
+
end
|
241
|
+
|
242
|
+
# Converts Ext.grid.GridFilters filters to searchlogic conditions, e.g.
|
243
|
+
# {"0" => {
|
244
|
+
# "data" => {
|
245
|
+
# "type" => "numeric",
|
246
|
+
# "comparison" => "gt",
|
247
|
+
# "value" => 10 },
|
248
|
+
# "field" => "id"
|
249
|
+
# },
|
250
|
+
# "1" => {
|
251
|
+
# "data" => {
|
252
|
+
# "type" => "string",
|
253
|
+
# "value" => "pizza"
|
254
|
+
# },
|
255
|
+
# "field" => "food_name"
|
256
|
+
# }}
|
257
|
+
#
|
258
|
+
# =>
|
259
|
+
#
|
260
|
+
# {"id_gt" => 100, "food_name_contains" => "pizza"}
|
261
|
+
def convert_filters(column_filter)
|
262
|
+
res = {}
|
263
|
+
column_filter.each_pair do |k,v|
|
264
|
+
field = v["field"].dup
|
265
|
+
case v["data"]["type"]
|
266
|
+
when "string"
|
267
|
+
field << "_contains"
|
268
|
+
when "numeric"
|
269
|
+
field << "_#{v["data"]["comparison"]}"
|
270
|
+
end
|
271
|
+
value = v["data"]["value"]
|
272
|
+
res.merge!({field => value})
|
273
|
+
end
|
274
|
+
res
|
275
|
+
end
|
276
|
+
|
277
|
+
def normalize_extra_conditions(conditions)
|
278
|
+
conditions.convert_keys{|k| k.to_s.gsub("__", "_").to_sym}
|
279
|
+
end
|
280
|
+
|
281
|
+
# make params understandable to searchlogic
|
282
|
+
def normalize_params(params)
|
283
|
+
# filters
|
284
|
+
conditions = params[:filter] && convert_filters(params[:filter])
|
285
|
+
|
286
|
+
normalized_conditions = {}
|
287
|
+
conditions && conditions.each_pair do |k, v|
|
288
|
+
assoc, method = k.split('__')
|
289
|
+
normalized_conditions.merge!(method.nil? ? {assoc => v} : {assoc => {method => v}})
|
290
|
+
end
|
291
|
+
|
292
|
+
{:conditions => normalized_conditions}
|
293
|
+
end
|
294
|
+
|
295
|
+
## Edit in form specific API
|
296
|
+
def new_record_form__submit(params)
|
297
|
+
form_data = ActiveSupport::JSON.decode(params[:data])
|
298
|
+
|
299
|
+
# merge with strong default attirbutes
|
300
|
+
form_data.merge!(config[:strong_default_attrs]) if config[:strong_default_attrs]
|
301
|
+
|
302
|
+
res = aggregatee_instance(:new_record_form).create_or_update_record(form_data)
|
303
|
+
|
304
|
+
if res[:set_form_values]
|
305
|
+
# successful creation
|
306
|
+
res[:set_form_values] = nil
|
307
|
+
res.merge!({
|
308
|
+
:parent => {:on_successfull_record_creation => true}
|
309
|
+
})
|
310
|
+
end
|
311
|
+
res.to_nifty_json
|
312
|
+
end
|
313
|
+
|
314
|
+
def check_for_positive_result(res)
|
315
|
+
if res[:set_form_values]
|
316
|
+
# successful creation
|
317
|
+
res[:set_form_values] = nil
|
318
|
+
res.merge!({
|
319
|
+
:parent => {:on_successfull_edit => true}
|
320
|
+
})
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
def edit_form__submit(params)
|
325
|
+
form_data = ActiveSupport::JSON.decode(params[:data])
|
326
|
+
res = aggregatee_instance(:new_record_form).create_or_update_record(form_data)
|
327
|
+
|
328
|
+
check_for_positive_result(res)
|
329
|
+
|
330
|
+
res.to_nifty_json
|
331
|
+
end
|
332
|
+
|
333
|
+
def multi_edit_form__submit(params)
|
334
|
+
form_data = ActiveSupport::JSON.decode(params[:data])
|
335
|
+
form_instance = aggregatee_instance(:new_record_form)
|
336
|
+
|
337
|
+
ids = ActiveSupport::JSON.decode(params[:ids])
|
338
|
+
|
339
|
+
res = {}
|
340
|
+
ids.each do |id|
|
341
|
+
res = form_instance.create_or_update_record(form_data.merge("id" => id))
|
342
|
+
break if !res[:set_form_values]
|
343
|
+
end
|
344
|
+
|
345
|
+
check_for_positive_result(res)
|
346
|
+
|
347
|
+
res.to_nifty_json
|
348
|
+
end
|
349
|
+
|
350
|
+
|
351
|
+
end
|
352
|
+
end
|