netzke-basepack 0.3.1 → 0.3.3

Sign up to get free protection for your applications and to get access to all the features.
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