netzke-basepack 0.4.2 → 0.5.1

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