skozlov-netzke_core 0.1.0

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 (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