netzke-basepack 0.4.2 → 0.5.1

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 (62) hide show
  1. data/.autotest +1 -0
  2. data/.gitignore +6 -0
  3. data/{CHANGELOG → CHANGELOG.rdoc} +26 -0
  4. data/README.rdoc +11 -11
  5. data/Rakefile +37 -11
  6. data/TODO.rdoc +8 -0
  7. data/VERSION +1 -0
  8. data/javascripts/basepack.js +71 -28
  9. data/lib/app/models/netzke_auto_column.rb +56 -0
  10. data/lib/netzke-basepack.rb +5 -3
  11. data/lib/netzke/accordion_panel.rb +69 -67
  12. data/lib/netzke/active_record/basepack.rb +104 -0
  13. data/lib/netzke/active_record/data_accessor.rb +33 -0
  14. data/lib/netzke/basic_app.rb +233 -124
  15. data/lib/netzke/border_layout_panel.rb +97 -98
  16. data/lib/netzke/configuration_panel.rb +24 -0
  17. data/lib/netzke/data_accessor.rb +71 -0
  18. data/lib/netzke/ext.rb +6 -0
  19. data/lib/netzke/field_model.rb +1 -1
  20. data/lib/netzke/fields_configurator.rb +62 -37
  21. data/lib/netzke/form_panel.rb +161 -51
  22. data/lib/netzke/form_panel_api.rb +74 -0
  23. data/lib/netzke/form_panel_js.rb +129 -0
  24. data/lib/netzke/grid_panel.rb +385 -80
  25. data/lib/netzke/grid_panel_api.rb +352 -0
  26. data/lib/netzke/grid_panel_extras/javascripts/rows-dd.js +280 -0
  27. data/lib/netzke/grid_panel_js.rb +721 -0
  28. data/lib/netzke/masquerade_selector.rb +53 -0
  29. data/lib/netzke/panel.rb +9 -0
  30. data/lib/netzke/plugins/configuration_tool.rb +121 -0
  31. data/lib/netzke/property_editor.rb +95 -7
  32. data/lib/netzke/property_editor_extras/helper_model.rb +55 -34
  33. data/lib/netzke/search_panel.rb +62 -0
  34. data/lib/netzke/tab_panel.rb +97 -37
  35. data/lib/netzke/table_editor.rb +49 -44
  36. data/lib/netzke/tree_panel.rb +15 -16
  37. data/lib/netzke/wrapper.rb +29 -5
  38. data/netzke-basepack.gemspec +151 -19
  39. data/stylesheets/basepack.css +5 -0
  40. data/test/app_root/app/models/book.rb +1 -1
  41. data/test/app_root/db/migrate/20081222035855_create_netzke_preferences.rb +1 -1
  42. data/test/unit/accordion_panel_test.rb +1 -2
  43. data/test/unit/active_record_basepack_test.rb +54 -0
  44. data/test/unit/grid_panel_test.rb +8 -12
  45. data/test/unit/helper_model_test.rb +30 -0
  46. metadata +69 -78
  47. data/Manifest +0 -86
  48. data/TODO +0 -3
  49. data/lib/app/models/netzke_hash_record.rb +0 -180
  50. data/lib/app/models/netzke_layout_item.rb +0 -11
  51. data/lib/netzke/ar_ext.rb +0 -269
  52. data/lib/netzke/configuration_tool.rb +0 -80
  53. data/lib/netzke/container.rb +0 -77
  54. data/lib/netzke/db_fields.rb +0 -44
  55. data/lib/netzke/fields_configurator_old.rb +0 -62
  56. data/lib/netzke/form_panel_extras/interface.rb +0 -56
  57. data/lib/netzke/form_panel_extras/js_builder.rb +0 -134
  58. data/lib/netzke/grid_panel_extras/interface.rb +0 -206
  59. data/lib/netzke/grid_panel_extras/js_builder.rb +0 -352
  60. data/test/unit/ar_ext_test.rb +0 -53
  61. data/test/unit/netzke_hash_record_test.rb +0 -52
  62. data/test/unit/netzke_layout_item_test.rb +0 -28
@@ -0,0 +1,104 @@
1
+ module Netzke::ActiveRecord
2
+ # Provides extensions to all ActiveRecord-based classes
3
+ module Basepack
4
+ def self.included(base)
5
+ base.extend ClassMethods
6
+ end
7
+
8
+ # Allow nested association access (assocs separated by "." or "__"), e.g.: proxy_service.asset__gui_folder__name
9
+ # Example:
10
+ #
11
+ # Book.first.genre__name = 'Fantasy'
12
+ #
13
+ # is the same as:
14
+ #
15
+ # Book.first.genre = Genre.find_by_name('Fantasy')
16
+ #
17
+ # The result - easier forms and grids that handle nested models: simply specify column/field name as "genre__name".
18
+ def method_missing(method, *args, &block)
19
+ # if refering to a column, just pass it to the original method_missing
20
+ return super if self.class.column_names.include?(method.to_s)
21
+
22
+ split = method.to_s.split(/\.|__/)
23
+ if split.size > 1
24
+ if split.last =~ /=$/
25
+ if split.size == 2
26
+ # search for association and assign it to self
27
+ assoc = self.class.reflect_on_association(split.first.to_sym)
28
+ assoc_method = split.last.chop
29
+ if assoc
30
+ begin
31
+ assoc_instance = assoc.klass.send("find_by_#{assoc_method}", *args)
32
+ rescue NoMethodError
33
+ assoc_instance = nil
34
+ logger.debug "!!! no find_by_#{assoc_method} method for class #{assoc.klass.name}\n"
35
+ end
36
+ if (assoc_instance)
37
+ self.send("#{split.first}=", assoc_instance)
38
+ else
39
+ logger.debug "!!! Couldn't find association #{split.first} by #{assoc_method} '#{args.first}'"
40
+ end
41
+ else
42
+ super
43
+ end
44
+ else
45
+ super
46
+ end
47
+ else
48
+ res = self
49
+ split.each do |m|
50
+ if res.respond_to?(m)
51
+ res = res.send(m) unless res.nil?
52
+ else
53
+ res.nil? ? nil : super
54
+ end
55
+ end
56
+ res
57
+ end
58
+ else
59
+ super
60
+ end
61
+ end
62
+
63
+ module ClassMethods
64
+
65
+ def options_for(column, query = nil)
66
+ # First, check if we have options for this class and column defined in persistent storage
67
+ NetzkePreference.widget_name = self.name
68
+ options = NetzkePreference[:combobox_options] || {}
69
+ if options[column]
70
+ options[column].select{ |o| o.index(/^#{query}/) }
71
+ elsif respond_to?("#{column}_combobox_options")
72
+ # AR class provides the choices itself
73
+ send("#{column}_combobox_options", query)
74
+ else
75
+ # Returns all unique values for a column, filtered with <tt>query</tt>
76
+ if (assoc_name, *assoc_method = column.split('__')).size > 1
77
+ # column is an association column
78
+ assoc_method = assoc_method.join('__') # in case we get something like country__continent__name
79
+ association = reflect_on_association(assoc_name.to_sym) || raise(NameError, "Association #{assoc_name} not known for class #{name}")
80
+ association.klass.options_for(assoc_method, query)
81
+ else
82
+ column = assoc_name
83
+ if self.column_names.include?(column)
84
+ # it's simply a column in the table
85
+ records = query.nil? ? find_by_sql("select distinct #{column} from #{table_name}") : find_by_sql("select distinct #{column} from #{table_name} where #{column} like '#{query}%'")
86
+ records.map{|r| r.send(column)}
87
+ else
88
+ # it's a "virtual" column - the least effective search
89
+ records = self.find(:all).map{|r| r.send(column)}.uniq
90
+ query.nil? ? records : records.select{|r| r.index(/^#{query}/)}
91
+ end
92
+ end
93
+ end
94
+ end
95
+
96
+ end
97
+
98
+ end
99
+ end
100
+
101
+ # Extend ActiveRecord
102
+ ActiveRecord::Base.class_eval do
103
+ include Netzke::ActiveRecord::Basepack
104
+ end
@@ -0,0 +1,33 @@
1
+ module Netzke::ActiveRecord
2
+ # Provides extensions to those ActiveRecord-based models that provide data to the "data accessor" widgets,
3
+ # like GridPanel, FormPanel, etc
4
+ module DataAccessor
5
+
6
+ # Allow specify the netzke widget that requires this data. Virtual attributes may be using it to produce
7
+ # widget-dependent result.
8
+ def netzke_widget=(widget)
9
+ @netzke_widget = widget
10
+ end
11
+
12
+ def netzke_widget
13
+ @netzke_widget
14
+ end
15
+
16
+ # Transforms a record to array of values according to the passed columns.
17
+ def to_array(columns, widget = nil)
18
+ self.netzke_widget = widget
19
+ res = []
20
+ for c in columns
21
+ nc = c.is_a?(Symbol) ? {:name => c} : c
22
+ begin
23
+ res << send(nc[:name]) unless nc[:excluded]
24
+ rescue
25
+ # So that we don't crash at a badly configured column
26
+ res << "UNDEF"
27
+ end
28
+ end
29
+ res
30
+ end
31
+ end
32
+ end
33
+
@@ -1,18 +1,16 @@
1
1
  module Netzke
2
- #
2
+ # == BasicApp
3
3
  # Basis for a Ext.Viewport-based application
4
4
  #
5
5
  # Features:
6
6
  # * dynamic loading of widgets
7
- # * restoring of the last loaded widget (not working for now)
8
7
  # * authentification support
9
8
  # * browser history support (press the "Back"-button to go to the previously loaded widget)
10
9
  # * FeedbackGhost-powered feedback
11
10
  # * aggregation of widget's own menus
12
- #
11
+ # * masquerade support
12
+ # * AJAX activity indicator
13
13
  class BasicApp < Base
14
- interface :app_get_widget # to dynamically load the widgets that are defined in initial_late_aggregatees
15
-
16
14
  module ClassMethods
17
15
 
18
16
  def js_base_class
@@ -22,161 +20,259 @@ module Netzke
22
20
  # Global BasicApp configuration
23
21
  def config
24
22
  set_default_config({
25
- :logout_url => "/logout" # logout url assumed by default
23
+ :logout_url => "/logout" # default logout url
26
24
  })
27
25
  end
28
26
 
29
- # The layout
30
- def js_default_config
31
- super.merge({
32
- :layout => 'border',
33
- :items => [{
34
- :id => 'main-panel',
35
- :region => 'center',
36
- :layout => 'fit'
37
- },{
38
- :id => 'main-toolbar',
39
- :xtype => 'toolbar',
40
- :region => 'north',
41
- :height => 25
42
- }]
43
- })
44
- end
45
-
46
- def js_after_constructor
47
- <<-JS.l
48
- // call appLoaded() once after the application is fully rendered
49
- // this.on("resize", function(){alert('show');this.appLoaded();}, this, {single:true});
50
-
51
- // Initialize menus (upcoming support for dynamically loaded menus)
52
- this.menus = {};
53
-
54
- Ext.History.on('change', this.processHistory, this);
55
-
56
- // If we are given a token, load the corresponding widget, otherwise load the last loaded widget
57
- var currentToken = Ext.History.getToken();
58
- if (currentToken != "") {
59
- this.processHistory(currentToken)
60
- } else {
61
- var lastLoaded = this.initialConfig.widgetToLoad; // passed from the server
62
- if (lastLoaded) Ext.History.add(lastLoaded);
63
- }
64
-
65
- if (this.initialConfig.menu) {this.addMenu(this.initialConfig.menu, this);}
66
-
67
- // add initial menus to the tool-bar
68
- var toolbar = this.findById('main-toolbar');
69
- Ext.each(#{js_initial_menus.to_js}, function(menu){
70
- toolbar.add(menu);
71
- });
72
- JS
73
- end
74
-
75
- # Set the Logout button if Netzke::Base.user is set
76
- def js_initial_menus
77
- res = []
78
- user = Netzke::Base.user
79
- if !user.nil?
80
- user_name = user.respond_to?(:name) ? user.name : user.login # try to display user's name, fallback to login
81
- res << "->" <<
82
- {
83
- :text => "Logout #{user_name}",
84
- :handler => <<-JS.l,
85
- function(){
86
- Ext.MessageBox.confirm('Confirm', 'Are you sure you want to logout?', function(btn){
87
- if( btn == "yes" ) {
88
- this.logout();
89
- }
90
- }.createDelegate(this));
91
- }
92
- JS
93
- :scope => this
94
- }
95
- else
96
- res << "->" <<
97
- {
98
- :text => "Login",
99
- :handler => <<-JS.l,
100
- function(){
101
- window.location = "/login"
102
- }
103
- JS
104
- :scope => this
105
- }
27
+ def js_panels
28
+ # In status bar we want to show what we are masquerading as
29
+ if session[:masq_user]
30
+ user = User.find(session[:masq_user])
31
+ masq = %Q{user "#{user.login}"}
32
+ elsif session[:masq_role]
33
+ role = Role.find(session[:masq_role])
34
+ masq = %Q{role "#{role.name}"}
35
+ elsif session[:masq_world]
36
+ masq = %Q{World}
106
37
  end
107
- res
38
+
39
+ [{
40
+ :id => 'main-panel',
41
+ :region => 'center',
42
+ :layout => 'fit'
43
+ },{
44
+ :id => 'main-toolbar',
45
+ :xtype => 'toolbar',
46
+ :region => 'north',
47
+ :height => 25
48
+ # :items => ["-"]
49
+ },{
50
+ :id => 'main-statusbar',
51
+ :xtype => 'statusbar',
52
+ :region => 'south',
53
+ :statusAlign => 'right',
54
+ :busyText => 'Busy...',
55
+ :default_text => masq.nil? ? "Ready #{"(config mode)" if session[:config_mode]}" : "Masquerading as #{masq}",
56
+ :default_icon_cls => ""
57
+ }]
108
58
  end
109
59
 
110
60
  def js_extend_properties
111
- super.merge({
61
+ {
62
+ :layout => 'border',
63
+
64
+ :panels => js_panels,
65
+
66
+ :init_component => <<-END_OF_JAVASCRIPT.l,
67
+ function(){
68
+ this.items = this.panels; // a bit weird, but working; can't assign it straight
69
+
70
+ Ext.netzke.cache.BasicApp.superclass.initComponent.call(this);
71
+
72
+ // If we are given a token, load the corresponding widget, otherwise load the last loaded widget
73
+ var currentToken = Ext.History.getToken();
74
+ if (currentToken != "") {
75
+ this.processHistory(currentToken)
76
+ } else {
77
+ var lastLoaded = this.initialConfig.widgetToLoad; // passed from the server
78
+ if (lastLoaded) Ext.History.add(lastLoaded);
79
+ }
112
80
 
113
- :host_menu => <<-JS.l,
81
+ Ext.History.on('change', this.processHistory, this);
82
+
83
+ // Hosted menus
84
+ this.menus = {};
85
+
86
+ // Setting the "busy" indicator for Ajax requests
87
+ Ext.Ajax.on('beforerequest', function(){this.findById('main-statusbar').showBusy()}, this);
88
+ Ext.Ajax.on('requestcomplete', function(){this.findById('main-statusbar').hideBusy()}, this);
89
+ Ext.Ajax.on('requestexception', function(){this.findById('main-statusbar').hideBusy()}, this);
90
+ }
91
+ END_OF_JAVASCRIPT
92
+
93
+ :host_menu => <<-END_OF_JAVASCRIPT.l,
114
94
  function(menu, owner){
115
95
  var toolbar = this.findById('main-toolbar');
116
96
  if (!this.menus[owner.id]) this.menus[owner.id] = [];
117
97
  Ext.each(menu, function(item) {
118
- var newMenu = new Ext.Toolbar.Button(item);
119
- var position = toolbar.items.getCount() - 2;
120
- position = position < 0 ? 0 : position;
121
- toolbar.insertButton(position, newMenu);
122
- this.menus[owner.id].push(newMenu);
98
+ // var newMenu = new Ext.Toolbar.Button(item);
99
+ // var position = toolbar.items.getCount() - 2;
100
+ // position = position < 0 ? 0 : position;
101
+ // toolbar.insertButton(position, newMenu);
102
+
103
+ toolbar.add(item);
104
+ // this.menus[owner.id].push(newMenu); // TODO: remember the menus from this owner in some other way
123
105
  }, this);
124
106
  }
125
- JS
107
+ END_OF_JAVASCRIPT
126
108
 
127
- :unhost_menu => <<-JS.l,
109
+ :unhost_menu => <<-END_OF_JAVASCRIPT.l,
128
110
  function(owner){
129
- var toolbar = this.findById('main-toolbar');
130
- if (this.menus[owner.id]) {
131
- Ext.each(this.menus[owner.id], function(menu){
132
- toolbar.items.remove(menu); // remove the item from the toolbar
133
- menu.destroy(); // ... and destroy it
134
- });
135
- }
111
+ // var toolbar = this.findById('main-toolbar');
112
+ // if (this.menus[owner.id]) {
113
+ // Ext.each(this.menus[owner.id], function(menu){
114
+ // toolbar.items.remove(menu); // remove the item from the toolbar
115
+ // menu.destroy(); // ... and destroy it
116
+ // });
117
+ // }
136
118
  }
137
- JS
119
+ END_OF_JAVASCRIPT
138
120
 
139
- :logout => <<-JS.l,
121
+ :logout => <<-END_OF_JAVASCRIPT.l,
140
122
  function(){
141
123
  window.location = "#{config[:logout_url]}"
142
124
  }
143
- JS
125
+ END_OF_JAVASCRIPT
144
126
 
145
127
  # Event handler for history change
146
- :process_history => <<-JS.l,
128
+ :process_history => <<-END_OF_JAVASCRIPT.l,
147
129
  function(token){
148
130
  if (token){
149
- this.findById('main-panel').loadWidget(this.initialConfig.interface.appGetWidget, {widget:token})
131
+ this.loadAggregatee({id:token, container:'main-panel'});
150
132
  } else {
151
- this.findById('main-panel').loadWidget(null)
152
133
  }
153
134
  }
154
- JS
135
+ END_OF_JAVASCRIPT
136
+
137
+ :instantiate_aggregatee => <<-END_OF_JAVASCRIPT.l,
138
+ function(config){
139
+ this.findById('main-panel').instantiateChild(config);
140
+ }
141
+ END_OF_JAVASCRIPT
155
142
 
156
143
  # Loads widget by name
157
- :app_load_widget => <<-JS.l,
144
+ :app_load_widget => <<-END_OF_JAVASCRIPT.l,
158
145
  function(name){
159
- Ext.History.add(name)
146
+ Ext.History.add(name);
160
147
  }
161
- JS
148
+ END_OF_JAVASCRIPT
162
149
 
163
150
  # Loads widget by action
164
- :load_widget_by_action => <<-JS.l
151
+ :load_widget_by_action => <<-END_OF_JAVASCRIPT.l,
165
152
  function(action){
166
- this.appLoadWidget(action.widget || action.name)
153
+ this.appLoadWidget(action.widget || action.name);
167
154
  }
168
- JS
169
- })
155
+ END_OF_JAVASCRIPT
156
+
157
+ # Masquerade selector window
158
+ :show_masquerade_selector => <<-END_OF_JAVASCRIPT.l
159
+ function(){
160
+ var w = new Ext.Window({
161
+ title: 'Masquerade as',
162
+ modal: true,
163
+ width: Ext.lib.Dom.getViewWidth() * 0.6,
164
+ height: Ext.lib.Dom.getViewHeight() * 0.6,
165
+ layout: 'fit',
166
+ closeAction :'destroy',
167
+ buttons: [{
168
+ text: 'Select',
169
+ handler : function(){
170
+ if (role = w.getWidget().masquerade.role) {
171
+ Ext.Msg.confirm("Masquerading as a role", "Individual preferences for all users with this role will get overwritten as you make changes. Continue?", function(btn){
172
+ if (btn === 'yes') {
173
+ w.close();
174
+ }
175
+ });
176
+ } else {
177
+ w.close();
178
+ }
179
+ },
180
+ scope:this
181
+ },{
182
+ text:'As World',
183
+ handler:function(){
184
+ Ext.Msg.confirm("Masquerading as World", "Caution! All settings that you will modify will be ovewritten for all roles and all users. Are you sure you know what you're doing?", function(btn){
185
+ if (btn === "yes") {
186
+ this.masquerade = {world:true};
187
+ w.close();
188
+ }
189
+ }, this);
190
+ },
191
+ scope:this
192
+ },{
193
+ text:'No masquerading',
194
+ handler:function(){
195
+ this.masquerade = {};
196
+ w.close();
197
+ },
198
+ scope:this
199
+ },{
200
+ text:'Cansel',
201
+ handler:function(){
202
+ w.hide();
203
+ },
204
+ scope:this
205
+ }],
206
+ listeners : {close: {fn: function(){
207
+ this.masqueradeAs(this.masquerade || w.getWidget().masquerade || {});
208
+ }, scope: this}}
209
+ });
210
+
211
+ w.show(null, function(){
212
+ this.loadAggregatee({id:"masqueradeSelector", container:w.id})
213
+ }, this);
214
+
215
+ }
216
+ END_OF_JAVASCRIPT
217
+ }
170
218
  end
171
219
  end
172
220
 
173
221
  extend ClassMethods
222
+
223
+ # Set the Logout button if Netzke::Base.user is set
224
+ def menu
225
+ res = []
226
+ user = Netzke::Base.user
227
+ if !user.nil?
228
+ user_name = user.respond_to?(:name) ? user.name : user.login # try to display user's name, fallback to login
229
+ res << "->" <<
230
+ {
231
+ :text => "#{user_name}",
232
+ :menu => user_menu
233
+ }
234
+ else
235
+ res << "->" <<
236
+ {
237
+ :text => "Login",
238
+ :handler => <<-END_OF_JAVASCRIPT.l,
239
+ function(){
240
+ window.location = "/login"
241
+ }
242
+ END_OF_JAVASCRIPT
243
+ :scope => this
244
+ }
245
+ end
246
+ res
247
+ end
248
+
249
+ def user_menu
250
+ ['logout']
251
+ end
252
+
253
+ def initialize(*args)
254
+ super
255
+
256
+ if session[:netzke_just_logged_in] || session[:netzke_just_logged_out]
257
+ session[:config_mode] = false
258
+ session[:masq_world] = session[:masq_user] = session[:masq_roles] = nil
259
+ end
260
+
261
+ strong_children_config.deep_merge!({:ext_config => {:mode => :config}}) if session[:config_mode]
262
+ end
174
263
 
175
- # Pass the last loaded widget from the persistent storage (DB) to the browser
176
- def js_config
177
- super.merge({:widget_to_load => persistent_config['last_loaded_widget']})
264
+ #
265
+ # Available actions
266
+ #
267
+ def actions
268
+ {
269
+ :masquerade_selector => {:text => "Masquerade as ...", :fn => "showMasqueradeSelector"},
270
+ :toggle_config_mode => {:text => "#{session[:config_mode] ? "Leave" : "Enter"} config mode", :fn => "toggleConfigMode"},
271
+ :logout => {:text => "Log out", :fn => "logout"}
272
+ }
178
273
  end
179
274
 
275
+
180
276
  # Html required for Ext.History to work
181
277
  def js_widget_html
182
278
  super << %Q{
@@ -194,18 +290,31 @@ module Netzke
194
290
 
195
291
  # Besides instantiating ourselves, also instantiate the FeedbackGhost
196
292
  def js_widget_instance
197
- %Q{
293
+ <<-END_OF_JAVASCRIPT << super
198
294
  new Ext.netzke.cache['FeedbackGhost']({id:'feedback_ghost'})
199
295
  // Initialize history (can't say why it's not working well inside the appLoaded handler)
200
296
  Ext.History.init();
201
- } << super
297
+ END_OF_JAVASCRIPT
202
298
  end
299
+
300
+ #
301
+ # Interface section
302
+ #
203
303
 
204
- # Interface implementation
205
- def interface_app_get_widget(params)
206
- widget = params.delete(:widget).underscore
207
- persistent_config['last_loaded_widget'] = widget # store the last loaded widget in the persistent storage
208
- send("#{widget}__get_widget", params)
304
+ api :toggle_config_mode
305
+ def toggle_config_mode(params)
306
+ session = Netzke::Base.session
307
+ session[:config_mode] = !session[:config_mode]
308
+ {:js => "window.location.reload();"}
309
+ end
310
+
311
+ api :masquerade_as
312
+ def masquerade_as(params)
313
+ session = Netzke::Base.session
314
+ session[:masq_world] = params[:world]
315
+ session[:masq_role] = params[:role]
316
+ session[:masq_user] = params[:user]
317
+ {:js => "window.location.reload()"}
209
318
  end
210
319
 
211
320
  end