skozlov-netzke_core 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. data/CHANGELOG +1 -0
  2. data/LICENSE +20 -0
  3. data/Manifest +64 -0
  4. data/README.mdown +18 -0
  5. data/Rakefile +11 -0
  6. data/generators/netzke_basepack/USAGE +8 -0
  7. data/generators/netzke_basepack/netzke_basepack_generator.rb +8 -0
  8. data/generators/netzke_basepack/netzke_grid_generator.rb +7 -0
  9. data/generators/netzke_basepack/templates/create_netzke_grid_columns.rb +21 -0
  10. data/init.rb +2 -0
  11. data/install.rb +1 -0
  12. data/javascripts/basepack.js +41 -0
  13. data/lib/app/models/netzke_grid_column.rb +23 -0
  14. data/lib/netzke/accordion.rb +11 -0
  15. data/lib/netzke/ar_ext.rb +163 -0
  16. data/lib/netzke/column.rb +43 -0
  17. data/lib/netzke/container.rb +81 -0
  18. data/lib/netzke/grid.rb +132 -0
  19. data/lib/netzke/grid_interface.rb +132 -0
  20. data/lib/netzke/grid_js_builder.rb +249 -0
  21. data/lib/netzke/preference_grid.rb +43 -0
  22. data/lib/netzke/properties_tool.rb +66 -0
  23. data/lib/netzke/property_grid.rb +60 -0
  24. data/lib/netzke_basepack.rb +14 -0
  25. data/netzke_core.gemspec +38 -0
  26. data/tasks/netzke_basepack_tasks.rake +4 -0
  27. data/test/app_root/app/controllers/application.rb +2 -0
  28. data/test/app_root/app/models/book.rb +9 -0
  29. data/test/app_root/app/models/category.rb +2 -0
  30. data/test/app_root/app/models/city.rb +3 -0
  31. data/test/app_root/app/models/continent.rb +2 -0
  32. data/test/app_root/app/models/country.rb +3 -0
  33. data/test/app_root/app/models/genre.rb +3 -0
  34. data/test/app_root/config/boot.rb +114 -0
  35. data/test/app_root/config/database.yml +21 -0
  36. data/test/app_root/config/environment.rb +13 -0
  37. data/test/app_root/config/environments/in_memory.rb +0 -0
  38. data/test/app_root/config/environments/mysql.rb +0 -0
  39. data/test/app_root/config/environments/postgresql.rb +0 -0
  40. data/test/app_root/config/environments/sqlite.rb +0 -0
  41. data/test/app_root/config/environments/sqlite3.rb +0 -0
  42. data/test/app_root/config/routes.rb +4 -0
  43. data/test/app_root/db/migrate/20081222033343_create_books.rb +15 -0
  44. data/test/app_root/db/migrate/20081222033440_create_genres.rb +14 -0
  45. data/test/app_root/db/migrate/20081222035855_create_netzke_preferences.rb +18 -0
  46. data/test/app_root/db/migrate/20081223024935_create_categories.rb +13 -0
  47. data/test/app_root/db/migrate/20081223025635_create_countries.rb +14 -0
  48. data/test/app_root/db/migrate/20081223025653_create_continents.rb +13 -0
  49. data/test/app_root/db/migrate/20081223025732_create_cities.rb +15 -0
  50. data/test/app_root/script/console +7 -0
  51. data/test/ar_ext_test.rb +39 -0
  52. data/test/column_test.rb +27 -0
  53. data/test/console_with_fixtures.rb +4 -0
  54. data/test/fixtures/books.yml +9 -0
  55. data/test/fixtures/categories.yml +7 -0
  56. data/test/fixtures/cities.yml +21 -0
  57. data/test/fixtures/continents.yml +7 -0
  58. data/test/fixtures/countries.yml +9 -0
  59. data/test/fixtures/genres.yml +9 -0
  60. data/test/grid_test.rb +43 -0
  61. data/test/netzke_basepack_test.rb +8 -0
  62. data/test/schema.rb +10 -0
  63. data/test/test_helper.rb +20 -0
  64. data/uninstall.rb +1 -0
  65. metadata +159 -0
@@ -0,0 +1,81 @@
1
+ module Netzke
2
+ #
3
+ # Base class for Accordion and TabPanel widgets, it shouldn't be used as a stand-alone class.
4
+ #
5
+ class Container < Base
6
+ def initialize(*args)
7
+ super
8
+ for item in initial_items do
9
+ add_aggregatee item
10
+ items << item.keys.first
11
+ end
12
+ end
13
+
14
+ def initial_dependencies
15
+ dep = super
16
+ for item in items
17
+ candidate_dependency = aggregatees[item][:widget_class_name]
18
+ dep << candidate_dependency unless dep.include?(candidate_dependency)
19
+ end
20
+ dep
21
+ end
22
+
23
+ def js_before_constructor
24
+ js_widget_items
25
+ end
26
+
27
+ def items
28
+ @items ||= []
29
+ end
30
+
31
+ def initial_items
32
+ config[:items] || []
33
+ end
34
+
35
+ def js_widget_items
36
+ res = ""
37
+ item_aggregatees.each_pair do |k,v|
38
+ next if v[:late_aggregation]
39
+ res << <<-JS
40
+ var #{k.to_js} = new Ext.componentCache['#{v[:widget_class_name]}'](config.#{k.to_js}Config);
41
+ JS
42
+ end
43
+ res
44
+ end
45
+
46
+ def js_items
47
+ items.inject([]) do |a,i|
48
+ a << {
49
+ :title => i.to_s.humanize,
50
+ :layout => 'fit',
51
+ :id => i.to_s,
52
+ # :id => "#{config[:name]}_#{i.to_s}",
53
+ :items => ([i.to_s.to_js.l] if !aggregatees[i][:late_aggregation]),
54
+ # these listeners will be different for tab_panel and accordion
55
+ :collapsed => !aggregatees[i][:active],
56
+ :listeners => {
57
+ # :activate => {:fn => "function(p){this.feedback(p.id)}".l, :scope => this},
58
+ :expand => {:fn => "this.loadItemWidget".l, :scope => this}
59
+ }
60
+ }
61
+ end
62
+ end
63
+
64
+ def js_extend_properties
65
+ {
66
+ # loads widget into the panel if it's not loaded yet
67
+ :load_item_widget => <<-JS.l,
68
+ function(panel) {
69
+ if (!panel.getWidget()) panel.loadWidget(this.id + "__" + panel.id + "__get_widget");
70
+ // if (!this.getWidgetByPanel(panel)) this.loadWidget(panel, this.initialConfig[panel.id+'Config'].interface.getWidget);
71
+ }
72
+ JS
73
+ }
74
+ end
75
+
76
+ protected
77
+ def item_aggregatees
78
+ aggregatees.delete_if{|k,v| !@items.include?(k)}
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,132 @@
1
+ require 'searchlogic'
2
+ module Netzke
3
+ #
4
+ # Functionality:
5
+ # * data operations - get, post, delete, create
6
+ # * column resize and move
7
+ # * permissions
8
+ # * sorting - TODO
9
+ # * pagination
10
+ # * validation - TODO
11
+ # * properties and column configuration
12
+ #
13
+ class Grid < Base
14
+ include GridJsBuilder
15
+ include GridInterface
16
+ # define connection points between client side and server side of Grid. See implementation of equally named methods in the GridInterface module.
17
+ interface :get_data, :post_data, :delete_data, :resize_column, :move_column, :get_cb_choices
18
+
19
+ def initial_config
20
+ {
21
+ :ext_config => {:properties => true},
22
+ :layout_manager => "NetzkeLayout"
23
+ }
24
+ end
25
+
26
+ def property_widgets
27
+ [{
28
+ :columns => {
29
+ :widget_class_name => "Grid",
30
+ :data_class_name => column_manager_class_name,
31
+ :ext_config => {:title => false, :properties => false},
32
+ :active => true
33
+ }
34
+ },{
35
+ :general => {
36
+ :widget_class_name => "PreferenceGrid",
37
+ :host_widget_name => @id_name,
38
+ :default_properties => available_permissions.map{ |k| {:name => "permissions.#{k}", :value => @permissions[k.to_sym]}},
39
+ :ext_config => {:title => false}
40
+ }
41
+ }]
42
+ end
43
+
44
+ ## Data for properties grid
45
+ def properties__columns__get_data(params = {})
46
+ columns_widget = aggregatee_instance(:properties__columns)
47
+
48
+ layout_id = layout_manager_class.by_widget(id_name).id
49
+ columns_widget.interface_get_data(params.merge(:filters => {:layout_id => layout_id}))
50
+ end
51
+
52
+ def properties__general__load_source(params = {})
53
+ w = aggregatee_instance(:properties__general)
54
+ w.interface_load_source(params)
55
+ end
56
+
57
+ # we pass column config at the time of instantiating the JS class
58
+ def js_config
59
+ res = super
60
+ res.merge!(:columns => get_columns || config[:columns]) # first try to get columns from DB, then from config
61
+ res.merge!(:data_class_name => config[:data_class_name])
62
+ res
63
+ end
64
+
65
+ def js_listeners
66
+ super.merge({
67
+ :columnresize => (config[:column_resize] ? {:fn => "this.onColumnResize".l, :scope => this} : nil),
68
+ :columnmove => (config[:column_move] ? {:fn => "this.onColumnMove".l, :scope => this} : nil)
69
+ })
70
+ end
71
+
72
+
73
+ protected
74
+
75
+ def layout_manager_class
76
+ config[:layout_manager] && config[:layout_manager].constantize
77
+ end
78
+
79
+ def column_manager_class_name
80
+ "NetzkeGridColumn"
81
+ end
82
+
83
+ def column_manager_class
84
+ column_manager_class_name.constantize
85
+ rescue NameError
86
+ nil
87
+ end
88
+
89
+ def available_permissions
90
+ %w(read update create delete)
91
+ end
92
+
93
+ public
94
+
95
+ # get columns from layout manager
96
+ def get_columns
97
+ if layout_manager_class
98
+ layout = layout_manager_class.by_widget(id_name)
99
+ layout ||= column_manager_class.create_layout_for_widget(self)
100
+ layout.items_hash # TODO: bad name!
101
+ else
102
+ Netzke::Column.default_columns_for_widget(self)
103
+ end
104
+ end
105
+
106
+ def tools
107
+ [{:id => 'refresh', :on => {:click => 'refreshClick'}}]
108
+ end
109
+
110
+ def actions
111
+ [{
112
+ :text => 'Add', :handler => 'add', :disabled => @pref['permissions.create'] == false
113
+ },{
114
+ :text => 'Edit', :handler => 'edit', :disabled => @pref['permissions.update'] == false
115
+ },{
116
+ :text => 'Delete', :handler => 'delete', :disabled => @pref['permissions.delete'] == false
117
+ },{
118
+ :text => 'Apply', :handler => 'submit', :disabled => @pref['permissions.update'] == false && @pref['permissions.create'] == false
119
+ }]
120
+ end
121
+
122
+
123
+
124
+ # Uncomment to enable a menu duplicating the actions
125
+ # def js_menus
126
+ # [{:text => "config.dataClassName".l, :menu => "config.actions".l}]
127
+ # end
128
+
129
+ # include ColumnOperations
130
+ include PropertiesTool # it will load aggregation with name :properties into a modal window
131
+ end
132
+ end
@@ -0,0 +1,132 @@
1
+ module Netzke::GridInterface
2
+ def post_data(params)
3
+ [:create, :update].each do |operation|
4
+ data = JSON.parse(params.delete("#{operation}d_records".to_sym)) if params["#{operation}d_records".to_sym]
5
+ process_data(data, operation) if !data.nil?
6
+ end
7
+ {:success => true, :flash => @flash}
8
+ end
9
+
10
+ def get_data(params = {})
11
+ if @permissions[:read]
12
+ records = get_records(params)
13
+ {:data => records, :total => records.total_records}
14
+ else
15
+ flash :error => "You don't have permissions to read data"
16
+ {:success => false, :flash => @flash}
17
+ end
18
+ end
19
+
20
+ def delete_data(params = {})
21
+ if @permissions[:delete]
22
+ record_ids = JSON.parse(params.delete(:records))
23
+ klass = config[:data_class_name].constantize
24
+ klass.delete(record_ids)
25
+ flash :notice => "Deleted #{record_ids.size} record(s)"
26
+ success = true
27
+ else
28
+ flash :error => "You don't have permissions to delete data"
29
+ success = false
30
+ end
31
+ {:success => success, :flash => @flash}
32
+ end
33
+
34
+ def resize_column(params)
35
+ raise "Called interface_resize_column while not configured to do so" unless config[:column_resize]
36
+ l_item = layout_manager_class.by_widget(id_name).layout_items[params[:index].to_i]
37
+ l_item.width = params[:size]
38
+ l_item.save!
39
+ {}
40
+ end
41
+
42
+ def move_column(params)
43
+ raise "Called interface_move_column while not configured to do so" unless config[:column_move]
44
+ layout_manager_class.by_widget(id_name).move_item(params[:old_index].to_i, params[:new_index].to_i)
45
+ {}
46
+ end
47
+
48
+ # Return the choices for the column
49
+ def get_cb_choices(params)
50
+ column = params[:column]
51
+ query = params[:query]
52
+
53
+ {:data => config[:data_class_name].constantize.choices_for(column, query).map{|s| [s]}}
54
+ end
55
+
56
+
57
+ protected
58
+ #
59
+ # operation => :update || :create
60
+ #
61
+ def process_data(data, operation)
62
+ if @permissions[operation]
63
+ klass = config[:data_class_name].constantize
64
+ modified_records = 0
65
+ data.each do |record_hash|
66
+ record = operation == :create ? klass.create : klass.find(record_hash.delete('id'))
67
+ logger.debug { "!!! record: #{record.inspect}" }
68
+ success = true
69
+ exception = nil
70
+
71
+ # process all attirubutes for the same record (OPTIMIZE: we can use update_attributes separately for regular attributes to speed things up)
72
+ record_hash.each_pair do |k,v|
73
+ begin
74
+ record.send("#{k}=",v)
75
+ rescue ArgumentError => exc
76
+ flash :error => exc.message
77
+ success = false
78
+ break
79
+ end
80
+ end
81
+
82
+ # try to save
83
+ modified_records += 1 if success && record.save
84
+
85
+ # flash eventual errors
86
+ record.errors.each_full do |msg|
87
+ flash :error => msg
88
+ end
89
+
90
+ flash :notice => "#{operation.to_s.capitalize}d #{modified_records} records"
91
+ end
92
+ else
93
+ flash :error => "You don't have permissions to #{operation} data"
94
+ end
95
+ end
96
+
97
+ # get records
98
+ def get_records(params)
99
+ conditions = normalize_params(params)
100
+ raise ArgumentError, "No data_class_name specified for widget '#{config[:name]}'" if !config[:data_class_name]
101
+ records = config[:data_class_name].constantize.all(conditions)
102
+ output_array = []
103
+ records.each do |r|
104
+ r_array = []
105
+ self.get_columns.each do |column|
106
+ r_array << r.send(column[:name])
107
+ end
108
+ output_array << r_array
109
+ end
110
+
111
+ # add total_entries method to the result
112
+ class << output_array
113
+ attr :total_records, true
114
+ end
115
+ total_records_count = config[:data_class_name].constantize.count(conditions)
116
+ output_array.total_records = total_records_count
117
+
118
+ output_array
119
+ end
120
+
121
+ # make params understandable to searchlogic
122
+ def normalize_params(params)
123
+ # sorting
124
+ order_by = if params[:sort]
125
+ assoc, method = params[:sort].split('__')
126
+ method.nil? ? assoc : {assoc => method}
127
+ end
128
+
129
+ page = params[:start].to_i/params[:limit].to_i + 1 if params[:limit]
130
+ {:per_page => params[:limit], :page => page, :order_by => order_by, :order_as => params[:dir]}
131
+ end
132
+ end
@@ -0,0 +1,249 @@
1
+ module Netzke::GridJsBuilder
2
+ def js_base_class
3
+ 'Ext.grid.EditorGridPanel'
4
+ end
5
+
6
+ def js_bbar
7
+ <<-JS.l
8
+ (config.rowsPerPage) ? new Ext.PagingToolbar({
9
+ pageSize:config.rowsPerPage,
10
+ items:config.actions,
11
+ store:ds,
12
+ emptyMsg:'Empty'}) : config.actions
13
+ JS
14
+ end
15
+
16
+ def js_default_config
17
+ super.merge({
18
+ :store => "ds".l,
19
+ :cm => "cm".l,
20
+ :sel_model => "new Ext.grid.RowSelectionModel()".l,
21
+ :auto_scroll => true,
22
+ :click_to_edit => 2,
23
+ :track_mouse_over => true,
24
+ # :bbar => "config.actions".l,
25
+ :bbar => js_bbar,
26
+
27
+ #custom configs
28
+ :auto_load_data => true
29
+ })
30
+ end
31
+
32
+ def js_before_constructor
33
+ <<-JS
34
+ this.recordConfig = [];
35
+
36
+ if (!config.columns) {
37
+ this.feedback('No columns defined for grid '+config.id);
38
+ }
39
+
40
+ Ext.each(config.columns, function(column){this.recordConfig.push({name:column.name})}, this);
41
+ this.Row = Ext.data.Record.create(this.recordConfig);
42
+ var ds = new Ext.data.Store({
43
+ proxy: this.proxy = new Ext.data.HttpProxy({url:config.interface.getData}),
44
+ reader: new Ext.data.ArrayReader({root: "data", totalProperty: "total", successProperty: "succes", id:0}, this.Row),
45
+ remoteSort: true
46
+ });
47
+
48
+ this.cmConfig = [];
49
+ Ext.each(config.columns, function(c){
50
+ var editor = c.readOnly ? null : Ext.netzke.editors[c.showsAs](c, config);
51
+
52
+ this.cmConfig.push({
53
+ header: c.label || c.name,
54
+ dataIndex: c.name,
55
+ hidden: c.hidden,
56
+ width: c.width,
57
+ editor: editor,
58
+ sortable: true
59
+ })
60
+ }, this);
61
+
62
+ var cm = new Ext.grid.ColumnModel(this.cmConfig);
63
+
64
+ this.addEvents("refresh");
65
+
66
+ JS
67
+ end
68
+
69
+ def js_extend_properties
70
+ {
71
+ :on_widget_load => <<-JS.l,
72
+ function(){
73
+ if (this.initialConfig.autoLoadData) {
74
+ this.loadWithFeedback({start:0, limit: this.initialConfig.rowsPerPage})
75
+ }
76
+ }
77
+ JS
78
+
79
+ :load_with_feedback => <<-JS.l,
80
+ function(params){
81
+ if (!params) params = {};
82
+ var exceptionHandler = function(proxy, options, response, error){
83
+ if (response.status == 200 && (responseObject = Ext.decode(response.responseText)) && responseObject.flash){
84
+ this.feedback(responseObject.flash)
85
+ } else {
86
+ if (error){
87
+ this.feedback(error.message);
88
+ } else {
89
+ this.feedback(response.statusText)
90
+ }
91
+ }
92
+ }.createDelegate(this);
93
+ this.store.proxy.on('loadexception', exceptionHandler);
94
+ this.store.load({callback:function(r, options, success){
95
+ this.store.proxy.un('loadexception', exceptionHandler);
96
+ }, params: params, scope:this});
97
+ }
98
+ JS
99
+
100
+ :add => <<-JS.l,
101
+ function(){
102
+ var rowConfig = {};
103
+ Ext.each(this.initialConfig.columns, function(c){
104
+ rowConfig[c.name] = c.defaultValue || ''; // FIXME: if the user is happy with all the defaults, the record won't be 'dirty'
105
+ }, this);
106
+
107
+ var r = new this.Row(rowConfig); // TODO: add default values
108
+ r.set('id', -r.id); // to distinguish new records by negative values
109
+ this.stopEditing();
110
+ this.store.add(r);
111
+ this.store.newRecords = this.store.newRecords || []
112
+ this.store.newRecords.push(r);
113
+ // console.info(this.store.newRecords);
114
+ this.tryStartEditing(this.store.indexOf(r));
115
+ }
116
+ JS
117
+
118
+ :edit => <<-JS.l,
119
+ function(){
120
+ var row = this.getSelectionModel().getSelected();
121
+ if (row){
122
+ this.tryStartEditing(this.store.indexOf(row))
123
+ }
124
+ }
125
+ JS
126
+
127
+ # try editing the first editable (not hidden, not read-only) sell
128
+ :try_start_editing => <<-JS.l,
129
+ function(row){
130
+ if (row == null) return;
131
+ var editableColumns = this.getColumnModel().getColumnsBy(function(columnConfig, index){
132
+ return !columnConfig.hidden && !!columnConfig.editor;
133
+ });
134
+ // console.info(editableColumns);
135
+ var firstEditableColumn = editableColumns[0];
136
+ if (firstEditableColumn){
137
+ this.startEditing(row, firstEditableColumn.id);
138
+ }
139
+ }
140
+ JS
141
+
142
+ :delete => <<-JS.l,
143
+ function() {
144
+ if (this.getSelectionModel().hasSelection()){
145
+ Ext.Msg.confirm('Confirm', 'Are you sure?', function(btn){
146
+ if (btn == 'yes') {
147
+ var records = []
148
+ this.getSelectionModel().each(function(r){
149
+ records.push(r.get('id'));
150
+ }, this);
151
+ Ext.Ajax.request({
152
+ url: this.initialConfig.interface.deleteData,
153
+ params: {records: Ext.encode(records)},
154
+ success:function(r){
155
+ var m = Ext.decode(r.responseText);
156
+ this.loadWithFeedback();
157
+ this.feedback(m.flash);
158
+ },
159
+ scope:this
160
+ });
161
+ }
162
+ }, this);
163
+ }
164
+ }
165
+ JS
166
+ :submit => <<-JS.l,
167
+ function(){
168
+
169
+ var newRecords = [];
170
+ if (this.store.newRecords){
171
+ Ext.each(this.store.newRecords, function(r){
172
+ newRecords.push(r.getChanges())
173
+ r.commit() // commit the changes, so that they are not picked up by getModifiedRecords() further down
174
+ }, this);
175
+ delete this.store.newRecords;
176
+ }
177
+
178
+ var updatedRecords = [];
179
+ Ext.each(this.store.getModifiedRecords(),
180
+ function(record) {
181
+ var completeRecordData = {};
182
+ Ext.apply(completeRecordData, Ext.apply(record.getChanges(), {id:record.get('id')}));
183
+ updatedRecords.push(completeRecordData);
184
+ },
185
+ this);
186
+
187
+ if (newRecords.length > 0 || updatedRecords.length > 0) {
188
+ Ext.Ajax.request({
189
+ url:this.initialConfig.interface.postData,
190
+ params: {
191
+ updated_records: Ext.encode(updatedRecords),
192
+ created_records: Ext.encode(newRecords),
193
+ filters: this.store.baseParams.filters
194
+ },
195
+ success:function(response){
196
+ var m = Ext.decode(response.responseText);
197
+ if (m.success) {
198
+ this.loadWithFeedback();
199
+ this.store.commitChanges();
200
+ this.feedback(m.flash);
201
+ } else {
202
+ this.feedback(m.flash);
203
+ }
204
+ },
205
+ failure:function(response){
206
+ this.feedback('Bad response from server');
207
+ },
208
+ scope:this
209
+ });
210
+ }
211
+
212
+ }
213
+ JS
214
+
215
+ :refresh_click => <<-JS.l,
216
+ function() {
217
+ // console.info(this);
218
+ if (this.fireEvent('refresh', this) !== false) this.loadWithFeedback();
219
+ }
220
+ JS
221
+
222
+ :on_column_resize => <<-JS.l,
223
+ function(index, size){
224
+ // var column = this.getColumnModel().getDataIndex(index);
225
+ Ext.Ajax.request({
226
+ url:this.initialConfig.interface.resizeColumn,
227
+ params:{
228
+ index:index,
229
+ size:size
230
+ }
231
+ })
232
+ }
233
+ JS
234
+
235
+ :on_column_move => <<-JS.l
236
+ function(oldIndex, newIndex){
237
+ Ext.Ajax.request({
238
+ url:this.initialConfig.interface.moveColumn,
239
+ params:{
240
+ old_index:oldIndex,
241
+ new_index:newIndex
242
+ }
243
+ })
244
+ }
245
+ JS
246
+
247
+ }
248
+ end
249
+ end