netzke-basepack 0.3.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. data/CHANGELOG +22 -0
  2. data/Manifest +6 -4
  3. data/README.rdoc +2 -2
  4. data/Rakefile +1 -1
  5. data/generators/netzke_form_panel/templates/create_netzke_form_panel_fields.rb +5 -6
  6. data/generators/netzke_grid_panel/templates/create_netzke_grid_panel_columns.rb +2 -1
  7. data/javascripts/basepack.js +2 -0
  8. data/lib/app/models/netzke_form_panel_field.rb +35 -3
  9. data/lib/app/models/netzke_grid_panel_column.rb +39 -2
  10. data/lib/netzke-basepack.rb +0 -1
  11. data/lib/netzke/accordion_panel.rb +51 -15
  12. data/lib/netzke/ar_ext.rb +14 -6
  13. data/lib/netzke/basic_app.rb +5 -5
  14. data/lib/netzke/border_layout_panel.rb +19 -4
  15. data/lib/netzke/container.rb +2 -6
  16. data/lib/netzke/db_fields.rb +40 -0
  17. data/lib/netzke/fields_configurator.rb +10 -18
  18. data/lib/netzke/form_panel.rb +42 -128
  19. data/lib/netzke/form_panel_extras/interface.rb +49 -0
  20. data/lib/netzke/form_panel_extras/js_builder.rb +131 -0
  21. data/lib/netzke/grid_panel.rb +20 -12
  22. data/lib/netzke/grid_panel_extras/interface.rb +181 -0
  23. data/lib/netzke/grid_panel_extras/js_builder.rb +322 -0
  24. data/lib/netzke/table_editor.rb +110 -0
  25. data/lib/netzke/wrapper.rb +1 -3
  26. data/netzke-basepack.gemspec +8 -8
  27. data/test/app_root/db/migrate/20090102223811_create_netzke_grid_panel_columns.rb +3 -1
  28. data/test/border_layout_panel_test.rb +8 -12
  29. data/test/grid_panel_test.rb +3 -3
  30. data/test/test_helper.rb +8 -0
  31. metadata +15 -11
  32. data/lib/netzke/column.rb +0 -50
  33. data/lib/netzke/grid_panel_interface.rb +0 -167
  34. data/lib/netzke/grid_panel_js_builder.rb +0 -298
  35. data/test/column_test.rb +0 -27
@@ -12,13 +12,17 @@ module Netzke
12
12
  # * properties and column configuration
13
13
  #
14
14
  class GridPanel < Base
15
- include GridPanelJsBuilder
16
- include GridPanelInterface
15
+ include_extras(__FILE__)
17
16
 
18
17
  # define connection points between client side and server side of GridPanel. See implementation of equally named methods in the GridPanelInterface module.
19
18
  interface :get_data, :post_data, :delete_data, :resize_column, :move_column, :get_cb_choices
20
19
 
20
+ include Netzke::DbFields
21
+
21
22
  module ClassMethods
23
+ def widget_type
24
+ :grid
25
+ end
22
26
 
23
27
  # Global GridPanel configuration
24
28
  def config
@@ -51,7 +55,6 @@ module Netzke
51
55
  :enable_column_filters => Netzke::Base.config[:grid_panel][:filters],
52
56
  :enable_column_move => true,
53
57
  :enable_column_resize => true,
54
- :border => true,
55
58
  :load_mask => true
56
59
  },
57
60
  :persistent_layout => true,
@@ -64,19 +67,24 @@ module Netzke
64
67
  end
65
68
 
66
69
  def property_widgets
67
- [{
70
+ res = []
71
+ res << {
68
72
  :name => 'columns',
69
73
  :widget_class_name => "FieldsConfigurator",
70
74
  :ext_config => {:title => false},
71
75
  :active => true,
72
76
  :layout => NetzkeLayout.by_widget(id_name)
73
- },{
77
+ } if config[:persistent_layout]
78
+
79
+ res << {
74
80
  :name => 'general',
75
81
  :widget_class_name => "PreferenceGrid",
76
82
  :host_widget_name => id_name,
77
83
  :default_properties => available_permissions.map{ |k| {:name => "permissions.#{k}", :value => @permissions[k.to_sym]}},
78
84
  :ext_config => {:title => false}
79
- }]
85
+ }
86
+
87
+ res
80
88
  end
81
89
 
82
90
  def properties__general__load_source(params = {})
@@ -98,9 +106,9 @@ module Netzke
98
106
  if config[:persistent_layout] && layout_manager_class && column_manager_class
99
107
  layout = layout_manager_class.by_widget(id_name)
100
108
  layout ||= column_manager_class.create_layout_for_widget(self)
101
- layout.items_hash # TODO: bad name!
109
+ layout.items_arry
102
110
  else
103
- Netzke::Column.default_columns_for_widget(self)
111
+ default_db_fields
104
112
  end
105
113
  end
106
114
 
@@ -110,13 +118,13 @@ module Netzke
110
118
 
111
119
  def actions
112
120
  [{
113
- :text => 'Add', :handler => 'add', :disabled => !@permissions[:create]
121
+ :text => 'Add', :handler_name => 'add', :disabled => !@permissions[:create], :id => 'add'
114
122
  },{
115
- :text => 'Edit', :handler => 'edit', :disabled => !@permissions[:update]
123
+ :text => 'Edit', :handler_name => 'edit', :disabled => !@permissions[:update], :id => 'edit'
116
124
  },{
117
- :text => 'Delete', :handler => 'delete', :disabled => !@permissions[:delete]
125
+ :text => 'Delete', :handler_name => 'delete', :disabled => !@permissions[:delete], :id => 'delete'
118
126
  },{
119
- :text => 'Apply', :handler => 'submit', :disabled => !@permissions[:update] && !@permissions[:create]
127
+ :text => 'Apply', :handler_name => 'submit', :disabled => !@permissions[:update] && !@permissions[:create], :id => 'apply'
120
128
  }]
121
129
  end
122
130
 
@@ -0,0 +1,181 @@
1
+ module Netzke
2
+ module GridPanelExtras
3
+ module Interface
4
+ def post_data(params)
5
+ success = true
6
+ mod_record_ids = {}
7
+ [:create, :update].each do |operation|
8
+ data = JSON.parse(params.delete("#{operation}d_records".to_sym)) if params["#{operation}d_records".to_sym]
9
+ if !data.nil? && !data.empty? # data may be nil for one of the operations
10
+ mod_record_ids[operation] = process_data(data, operation)
11
+ end
12
+ break if !success
13
+ end
14
+ {:success => success, :flash => @flash, :mod_record_ids => mod_record_ids}
15
+ end
16
+
17
+ def get_data(params = {})
18
+ if @permissions[:read]
19
+ records = get_records(params)
20
+ {:data => records, :total => records.total_records}
21
+ else
22
+ flash :error => "You don't have permissions to read data"
23
+ {:success => false, :flash => @flash}
24
+ end
25
+ end
26
+
27
+ def delete_data(params = {})
28
+ if @permissions[:delete]
29
+ record_ids = JSON.parse(params.delete(:records))
30
+ klass = config[:data_class_name].constantize
31
+ klass.delete(record_ids)
32
+ flash :notice => "Deleted #{record_ids.size} record(s)"
33
+ success = true
34
+ else
35
+ flash :error => "You don't have permissions to delete data"
36
+ success = false
37
+ end
38
+ {:success => success, :flash => @flash}
39
+ end
40
+
41
+ def resize_column(params)
42
+ raise "Called interface_resize_column while not configured to do so" unless config[:ext_config][:enable_column_resize]
43
+ if config[:persistent_layout] && layout_manager_class
44
+ l_item = layout_manager_class.by_widget(id_name).items[params[:index].to_i]
45
+ l_item.width = params[:size]
46
+ l_item.save!
47
+ end
48
+ {}
49
+ end
50
+
51
+ def move_column(params)
52
+ raise "Called interface_move_column while not configured to do so" unless config[:ext_config][:enable_column_move]
53
+ if config[:persistent_layout] && layout_manager_class
54
+ layout_manager_class.by_widget(id_name).move_item(params[:old_index].to_i, params[:new_index].to_i)
55
+ end
56
+ {}
57
+ end
58
+
59
+ # Return the choices for the column
60
+ def get_cb_choices(params)
61
+ column = params[:column]
62
+ query = params[:query]
63
+
64
+ {:data => config[:data_class_name].constantize.choices_for(column, query).map{|s| [s]}}
65
+ end
66
+
67
+
68
+ protected
69
+
70
+ # operation => :update || :create
71
+ def process_data(data, operation)
72
+ success = true
73
+ mod_record_ids = []
74
+ if @permissions[operation]
75
+ klass = config[:data_class_name].constantize
76
+ modified_records = 0
77
+ data.each do |record_hash|
78
+ id = record_hash.delete('id')
79
+ record = operation == :create ? klass.new : klass.find(id)
80
+ success = true
81
+
82
+ # process all attirubutes for the same record (OPTIMIZE: we can use update_attributes separately for regular attributes to speed things up)
83
+ record_hash.each_pair do |k,v|
84
+ begin
85
+ record.send("#{k}=",v)
86
+ rescue ArgumentError => exc
87
+ flash :error => exc.message
88
+ success = false
89
+ break
90
+ end
91
+ end
92
+
93
+ # try to save
94
+ # modified_records += 1 if success && record.save
95
+ mod_record_ids << id if success && record.save
96
+
97
+ # flash eventual errors
98
+ if !record.errors.empty?
99
+ success = false
100
+ record.errors.each_full do |msg|
101
+ flash :error => msg
102
+ end
103
+ end
104
+ end
105
+ # flash :notice => "#{operation.to_s.capitalize}d #{modified_records} record(s)"
106
+ else
107
+ success = false
108
+ flash :error => "You don't have permissions to #{operation} data"
109
+ end
110
+ mod_record_ids
111
+ end
112
+
113
+ # get records
114
+ def get_records(params)
115
+ search_params = normalize_params(params) # make params coming from the browser understandable by searchlogic
116
+ search_params[:conditions].recursive_merge!(config[:conditions] || {}) # merge with conditions coming from the config
117
+
118
+ raise ArgumentError, "No data_class_name specified for widget '#{config[:name]}'" if !config[:data_class_name]
119
+ records = config[:data_class_name].constantize.all(search_params.clone) # clone needed as searchlogic removes :conditions key from the hash
120
+ # output_array = []
121
+ columns = get_columns
122
+ output_array = records.map{|r| r.to_array(columns)}
123
+
124
+ # records.each do |r|
125
+ # r_array = []
126
+ # self.get_columns.each do |column|
127
+ # r_array << r.send(column[:name])
128
+ # end
129
+ # output_array << r_array
130
+ # output_array << r.to_array(columns)
131
+ # end
132
+
133
+ # add total_entries accessor to the result
134
+ class << output_array
135
+ attr :total_records, true
136
+ end
137
+ total_records_count = config[:data_class_name].constantize.count(search_params)
138
+ output_array.total_records = total_records_count
139
+
140
+ output_array
141
+ end
142
+
143
+ def convert_filters(column_filter)
144
+ res = {}
145
+ column_filter.each_pair do |k,v|
146
+ field = v["field"]
147
+ case v["data"]["type"]
148
+ when "string"
149
+ field << "_contains"
150
+ when "numeric"
151
+ field << "_#{v["data"]["comparison"]}"
152
+ end
153
+ value = v["data"]["value"]
154
+ res.merge!({field => value})
155
+ end
156
+ res
157
+ end
158
+
159
+ # make params understandable to searchlogic
160
+ def normalize_params(params)
161
+ # filters
162
+ conditions = params[:filter] && convert_filters(params[:filter])
163
+
164
+ normalized_conditions = {}
165
+ conditions && conditions.each_pair do |k, v|
166
+ assoc, method = k.split('__')
167
+ normalized_conditions.merge!(method.nil? ? {assoc => v} : {assoc => {method => v}})
168
+ end
169
+
170
+ # sorting
171
+ order_by = if params[:sort]
172
+ assoc, method = params[:sort].split('__')
173
+ method.nil? ? assoc : {assoc => method}
174
+ end
175
+
176
+ page = params[:start].to_i/params[:limit].to_i + 1 if params[:limit]
177
+ {:per_page => params[:limit], :page => page, :order_by => order_by, :order_as => params[:dir], :conditions => normalized_conditions}
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,322 @@
1
+ module Netzke
2
+ module GridPanelExtras
3
+ module JsBuilder
4
+ def self.included(base)
5
+ base.extend ClassMethods
6
+ end
7
+
8
+ def js_config
9
+ res = super
10
+ # we pass column config at the time of instantiating the JS class
11
+ res.merge!(:columns => get_columns || config[:columns]) # first try to get columns from DB, then from config
12
+ res.merge!(:data_class_name => config[:data_class_name])
13
+ res
14
+ end
15
+
16
+ module ClassMethods
17
+
18
+ def js_base_class
19
+ 'Ext.grid.EditorGridPanel'
20
+ end
21
+
22
+ def js_bbar
23
+ <<-JS.l
24
+ (config.rowsPerPage) ? new Ext.PagingToolbar({
25
+ pageSize:config.rowsPerPage,
26
+ items:config.actions,
27
+ store:ds,
28
+ emptyMsg:'Empty'}) : config.actions
29
+ JS
30
+ end
31
+
32
+ def js_default_config
33
+ super.merge({
34
+ :store => "ds".l,
35
+ :cm => "cm".l,
36
+ :sel_model => "new Ext.grid.RowSelectionModel()".l,
37
+ :auto_scroll => true,
38
+ :click_to_edit => 2,
39
+ :track_mouse_over => true,
40
+ # :bbar => "config.actions".l,
41
+ :bbar => js_bbar,
42
+ :plugins => "plugins".l,
43
+
44
+ #custom configs
45
+ :auto_load_data => true
46
+ })
47
+ end
48
+
49
+ def js_before_constructor
50
+ <<-JS
51
+ var plugins = [];
52
+ if (!config.columns) this.feedback('No columns defined for grid '+config.id);
53
+ this.recordConfig = [];
54
+ Ext.each(config.columns, function(column){this.recordConfig.push({name:column.name})}, this);
55
+ this.Row = Ext.data.Record.create(this.recordConfig);
56
+
57
+ var ds = new Ext.data.Store({
58
+ proxy: this.proxy = new Ext.data.HttpProxy({url:config.interface.getData}),
59
+ reader: new Ext.data.ArrayReader({root: "data", totalProperty: "total", successProperty: "succes", id:0}, this.Row),
60
+ remoteSort: true,
61
+ listeners:{'loadexception':{
62
+ fn:this.loadExceptionHandler,
63
+ scope:this
64
+ }}
65
+ });
66
+
67
+ this.cmConfig = [];
68
+ Ext.each(config.columns, function(c){
69
+ var extConfig;
70
+ try{
71
+ extConfig = Ext.decode(c.extConfig);
72
+ }
73
+ catch(err){
74
+ extConfig = {}
75
+ }
76
+ delete(c.extConfig);
77
+
78
+ if (c.editor == 'checkbox') {
79
+ var plugin = new Ext.grid.CheckColumn(Ext.apply({
80
+ header : c.label || c.name,
81
+ dataIndex : c.name,
82
+ disabled : c.readOnly,
83
+ hidden : c.hidden,
84
+ width : c.width
85
+ }, extConfig));
86
+
87
+ plugins.push(plugin);
88
+ this.cmConfig.push(plugin);
89
+
90
+ } else {
91
+ var editor = (c.readOnly || !config.permissions.update) ? null : Ext.netzke.editors[c.editor](c, config);
92
+ var renderer = Ext.netzke.renderer(c.renderer);
93
+
94
+ this.cmConfig.push(Ext.apply({
95
+ header : c.label || c.name,
96
+ dataIndex : c.name,
97
+ hidden : c.hidden,
98
+ width : c.width,
99
+ editor : editor,
100
+ renderer : renderer,
101
+ sortable : true
102
+ }, extConfig))
103
+ }
104
+
105
+ }, this);
106
+
107
+ var cm = new Ext.grid.ColumnModel(this.cmConfig);
108
+
109
+ this.addEvents("refresh");
110
+
111
+ // Filters
112
+ if (config.enableColumnFilters) {
113
+ var filters = []
114
+ Ext.each(config.columns, function(c){
115
+ filters.push({type:Ext.netzke.filterMap[c.editor], dataIndex:c.name})
116
+ })
117
+ var gridFilters = new Ext.grid.GridFilters({filters:filters});
118
+ plugins.push(gridFilters);
119
+ }
120
+
121
+ JS
122
+ end
123
+
124
+ def js_listeners
125
+ super.merge({
126
+ :columnresize => {:fn => "this.onColumnResize".l, :scope => this},
127
+ :columnmove => {:fn => "this.onColumnMove".l, :scope => this}
128
+ })
129
+ end
130
+
131
+ def js_extend_properties
132
+ {
133
+ :on_widget_load => <<-JS.l,
134
+ function(){
135
+ // auto-load
136
+ if (this.initialConfig.autoLoadData) {
137
+ // if we have a paging toolbar, load the first page, otherwise
138
+ if (this.getBottomToolbar().changePage) this.getBottomToolbar().changePage(0); else this.store.load();
139
+ }
140
+ }
141
+ JS
142
+
143
+ :load_exception_handler => <<-JS.l,
144
+ function(proxy, options, response, error){
145
+ if (response.status == 200 && (responseObject = Ext.decode(response.responseText)) && responseObject.flash){
146
+ this.feedback(responseObject.flash)
147
+ } else {
148
+ if (error){
149
+ this.feedback(error.message);
150
+ } else {
151
+ this.feedback(response.statusText)
152
+ }
153
+ }
154
+ }
155
+ JS
156
+
157
+ :add => <<-JS.l,
158
+ function(){
159
+ var rowConfig = {};
160
+ Ext.each(this.initialConfig.columns, function(c){
161
+ rowConfig[c.name] = c.defaultValue || ''; // FIXME: if the user is happy with all the defaults, the record won't be 'dirty'
162
+ }, this);
163
+
164
+ var r = new this.Row(rowConfig); // TODO: add default values
165
+ r.new = true; // to distinguish new records
166
+ r.set('id', r.id); // otherwise later r.get('id') returns empty string
167
+ this.stopEditing();
168
+ this.store.add(r);
169
+ // this.store.newRecords = this.store.newRecords || []
170
+ // this.store.newRecords.push(r);
171
+ this.tryStartEditing(this.store.indexOf(r));
172
+ }
173
+ JS
174
+
175
+ :edit => <<-JS.l,
176
+ function(){
177
+ var row = this.getSelectionModel().getSelected();
178
+ if (row){
179
+ this.tryStartEditing(this.store.indexOf(row))
180
+ }
181
+ }
182
+ JS
183
+
184
+ # try editing the first editable (not hidden, not read-only) sell
185
+ :try_start_editing => <<-JS.l,
186
+ function(row){
187
+ if (row == null) return;
188
+ var editableColumns = this.getColumnModel().getColumnsBy(function(columnConfig, index){
189
+ return !columnConfig.hidden && !!columnConfig.editor;
190
+ });
191
+ // console.info(editableColumns);
192
+ var firstEditableColumn = editableColumns[0];
193
+ if (firstEditableColumn){
194
+ this.startEditing(row, firstEditableColumn.id);
195
+ }
196
+ }
197
+ JS
198
+
199
+ :delete => <<-JS.l,
200
+ function() {
201
+ if (this.getSelectionModel().hasSelection()){
202
+ Ext.Msg.confirm('Confirm', 'Are you sure?', function(btn){
203
+ if (btn == 'yes') {
204
+ var records = []
205
+ this.getSelectionModel().each(function(r){
206
+ records.push(r.get('id'));
207
+ }, this);
208
+ Ext.Ajax.request({
209
+ url: this.initialConfig.interface.deleteData,
210
+ params: {records: Ext.encode(records)},
211
+ success:function(r){
212
+ var m = Ext.decode(r.responseText);
213
+ this.store.reload();
214
+ this.feedback(m.flash);
215
+ },
216
+ scope:this
217
+ });
218
+ }
219
+ }, this);
220
+ }
221
+ }
222
+ JS
223
+ :submit => <<-JS.l,
224
+ function(){
225
+
226
+ var newRecords = [];
227
+ // if (this.store.newRecords){
228
+ // Ext.each(this.store.newRecords, function(r){
229
+ // newRecords.push(r.getChanges())
230
+ // }, this);
231
+ // // delete this.store.newRecords;
232
+ // }
233
+
234
+ var updatedRecords = [];
235
+
236
+ Ext.each(this.store.getModifiedRecords(),
237
+ function(r) {
238
+ if (r.new) {
239
+ newRecords.push(Ext.apply(r.getChanges(), {id:r.get('id')}));
240
+ } else {
241
+ updatedRecords.push(Ext.apply(r.getChanges(), {id:r.get('id')}));
242
+ }
243
+ },
244
+ this);
245
+
246
+ if (newRecords.length > 0 || updatedRecords.length > 0) {
247
+ Ext.Ajax.request({
248
+ url:this.initialConfig.interface.postData,
249
+ params: {
250
+ updated_records: Ext.encode(updatedRecords),
251
+ created_records: Ext.encode(newRecords),
252
+ filters: this.store.baseParams.filters
253
+ },
254
+ success:function(response){
255
+ var m = Ext.decode(response.responseText);
256
+ if (m.success) {
257
+ // commit those rows that have successfully been updated/created
258
+ var modRecords = [].concat(this.store.getModifiedRecords()) // there must be a better way to clone an array...
259
+ Ext.each(modRecords, function(r){
260
+ var idsToSearch = r.new ? m.modRecordIds.create : m.modRecordIds.update
261
+ if (idsToSearch.indexOf(r.id) >= 0) r.commit();
262
+ })
263
+
264
+ // reload the grid only when there were no errors
265
+ // (we need to reload because of filtering, sorting, etc)
266
+ if (this.store.getModifiedRecords().length == 0){
267
+ this.store.reload();
268
+ }
269
+
270
+ this.feedback(m.flash);
271
+ } else {
272
+ this.feedback(m.flash);
273
+ }
274
+ },
275
+ failure:function(response){
276
+ this.feedback('Bad response from server');
277
+ },
278
+ scope:this
279
+ });
280
+ }
281
+
282
+ }
283
+ JS
284
+
285
+ :refresh_click => <<-JS.l,
286
+ function() {
287
+ // console.info(this);
288
+ // if (this.fireEvent('refresh', this) !== false) this.loadWithFeedback();
289
+ if (this.fireEvent('refresh', this) !== false) this.store.reload();
290
+ }
291
+ JS
292
+
293
+ :on_column_resize => <<-JS.l,
294
+ function(index, size){
295
+ Ext.Ajax.request({
296
+ url:this.initialConfig.interface.resizeColumn,
297
+ params:{
298
+ index:index,
299
+ size:size
300
+ }
301
+ })
302
+ }
303
+ JS
304
+
305
+ :on_column_move => <<-JS.l
306
+ function(oldIndex, newIndex){
307
+ Ext.Ajax.request({
308
+ url:this.initialConfig.interface.moveColumn,
309
+ params:{
310
+ old_index:oldIndex,
311
+ new_index:newIndex
312
+ }
313
+ })
314
+ }
315
+ JS
316
+
317
+ }
318
+ end
319
+ end
320
+ end
321
+ end
322
+ end