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.
Files changed (81) hide show
  1. data/.autotest +1 -0
  2. data/.gitignore +5 -0
  3. data/LICENSE +2 -19
  4. data/README.rdoc +87 -0
  5. data/Rakefile +28 -12
  6. data/TODO.rdoc +7 -0
  7. data/VERSION +1 -0
  8. data/autotest/discover.rb +3 -0
  9. data/init.rb +0 -1
  10. data/javascripts/basepack.js +839 -49
  11. data/lib/app/models/netzke_auto_column.rb +56 -0
  12. data/lib/netzke/accordion_panel.rb +113 -0
  13. data/lib/netzke/active_record/basepack.rb +104 -0
  14. data/lib/netzke/active_record/data_accessor.rb +21 -0
  15. data/lib/netzke/basic_app.rb +325 -0
  16. data/lib/netzke/border_layout_panel.rb +128 -0
  17. data/lib/netzke/configuration_panel.rb +24 -0
  18. data/lib/netzke/data_accessor.rb +71 -0
  19. data/lib/netzke/ext.rb +6 -0
  20. data/lib/netzke/field_model.rb +131 -0
  21. data/lib/netzke/fields_configurator.rb +95 -0
  22. data/lib/netzke/form_panel.rb +214 -0
  23. data/lib/netzke/form_panel_api.rb +74 -0
  24. data/lib/netzke/form_panel_extras/javascripts/xcheckbox.js +82 -0
  25. data/lib/netzke/form_panel_js.rb +129 -0
  26. data/lib/netzke/grid_panel.rb +442 -0
  27. data/lib/netzke/grid_panel_api.rb +352 -0
  28. data/lib/netzke/grid_panel_extras/javascripts/check-column.js +33 -0
  29. data/{javascripts → lib/netzke/grid_panel_extras/javascripts}/filters.js +0 -0
  30. data/lib/netzke/grid_panel_extras/javascripts/rows-dd.js +280 -0
  31. data/lib/netzke/grid_panel_js.rb +721 -0
  32. data/lib/netzke/panel.rb +13 -0
  33. data/lib/netzke/plugins/configuration_tool.rb +121 -0
  34. data/lib/netzke/property_editor.rb +105 -0
  35. data/lib/netzke/property_editor_extras/helper_model.rb +126 -0
  36. data/lib/netzke/search_panel.rb +62 -0
  37. data/lib/netzke/tab_panel.rb +160 -0
  38. data/lib/netzke/table_editor.rb +118 -0
  39. data/lib/netzke/tree_panel.rb +73 -0
  40. data/lib/netzke/wrapper.rb +42 -0
  41. data/lib/netzke-basepack.rb +9 -15
  42. data/netzke-basepack.gemspec +147 -20
  43. data/stylesheets/basepack.css +26 -0
  44. data/test/app_root/app/models/book.rb +1 -1
  45. data/test/app_root/config/environment.rb +1 -0
  46. data/test/app_root/db/migrate/20081222033440_create_genres.rb +1 -0
  47. data/test/app_root/db/migrate/20081222035855_create_netzke_preferences.rb +1 -1
  48. data/test/app_root/db/migrate/20090102223630_create_netzke_layouts.rb +14 -0
  49. data/test/app_root/vendor/plugins/acts_as_list/README +23 -0
  50. data/test/app_root/vendor/plugins/acts_as_list/init.rb +3 -0
  51. data/test/app_root/vendor/plugins/acts_as_list/lib/active_record/acts/list.rb +256 -0
  52. data/test/test_helper.rb +1 -2
  53. data/test/unit/accordion_panel_test.rb +20 -0
  54. data/test/unit/active_record_basepack_test.rb +54 -0
  55. data/test/unit/grid_panel_test.rb +43 -0
  56. data/test/unit/helper_model_test.rb +30 -0
  57. data/test/unit/netzke_basepack_test.rb +4 -0
  58. data/test/unit/tab_panel_test.rb +21 -0
  59. metadata +96 -72
  60. data/CHANGELOG +0 -14
  61. data/Manifest +0 -65
  62. data/README.mdown +0 -18
  63. data/generators/netzke_basepack/USAGE +0 -8
  64. data/generators/netzke_basepack/netzke_basepack_generator.rb +0 -8
  65. data/generators/netzke_basepack/netzke_grid_generator.rb +0 -7
  66. data/generators/netzke_basepack/templates/create_netzke_grid_columns.rb +0 -21
  67. data/lib/app/models/netzke_grid_column.rb +0 -23
  68. data/lib/netzke/accordion.rb +0 -11
  69. data/lib/netzke/ar_ext.rb +0 -163
  70. data/lib/netzke/column.rb +0 -43
  71. data/lib/netzke/container.rb +0 -81
  72. data/lib/netzke/grid.rb +0 -120
  73. data/lib/netzke/grid_interface.rb +0 -156
  74. data/lib/netzke/grid_js_builder.rb +0 -276
  75. data/lib/netzke/preference_grid.rb +0 -43
  76. data/lib/netzke/properties_tool.rb +0 -66
  77. data/lib/netzke/property_grid.rb +0 -60
  78. data/test/ar_ext_test.rb +0 -39
  79. data/test/column_test.rb +0 -27
  80. data/test/grid_test.rb +0 -43
  81. 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