skozlov-netzke-core 0.1.0.2 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/CHANGELOG +108 -0
  2. data/LICENSE +2 -19
  3. data/Manifest +50 -0
  4. data/README.rdoc +12 -0
  5. data/Rakefile +2 -3
  6. data/TODO +2 -0
  7. data/autotest/discover.rb +3 -0
  8. data/generators/netzke_core/netzke_core_generator.rb +6 -6
  9. data/generators/netzke_core/templates/create_netzke_preferences.rb +2 -2
  10. data/javascripts/core.js +474 -111
  11. data/lib/app/controllers/netzke_controller.rb +76 -0
  12. data/lib/app/models/netzke_preference.rb +128 -39
  13. data/lib/netzke-core.rb +23 -9
  14. data/lib/netzke/action_view_ext.rb +26 -0
  15. data/lib/netzke/base.rb +440 -102
  16. data/lib/netzke/base_js.rb +258 -0
  17. data/lib/netzke/controller_extensions.rb +80 -29
  18. data/lib/netzke/core_ext.rb +72 -21
  19. data/lib/netzke/feedback_ghost.rb +43 -0
  20. data/lib/netzke/routing.rb +9 -0
  21. data/netzke-core.gemspec +10 -11
  22. data/stylesheets/core.css +12 -0
  23. data/test/app_root/app/controllers/{application.rb → application_controller.rb} +0 -0
  24. data/test/app_root/app/models/role.rb +3 -0
  25. data/test/app_root/app/models/user.rb +3 -0
  26. data/test/app_root/config/boot.rb +2 -1
  27. data/test/app_root/config/database.yml +10 -0
  28. data/test/app_root/config/environment.rb +1 -0
  29. data/test/app_root/db/migrate/20081222035855_create_netzke_preferences.rb +18 -0
  30. data/test/app_root/db/migrate/20090423214303_create_roles.rb +11 -0
  31. data/test/app_root/db/migrate/20090423222114_create_users.rb +12 -0
  32. data/test/app_root/lib/console_with_fixtures.rb +4 -0
  33. data/test/app_root/script/console +1 -0
  34. data/test/fixtures/roles.yml +7 -0
  35. data/test/fixtures/users.yml +9 -0
  36. data/test/test_helper.rb +3 -2
  37. data/test/unit/core_ext_test.rb +66 -0
  38. data/test/unit/netzke_core_test.rb +167 -0
  39. data/test/unit/netzke_preference_test.rb +103 -0
  40. metadata +45 -30
  41. data/README.mdown +0 -11
  42. data/generators/netzke_core/templates/create_netzke_layouts.rb +0 -14
  43. data/generators/netzke_core/templates/netzke.html.erb +0 -10
  44. data/lib/app/models/netzke_layout.rb +0 -75
  45. data/lib/netzke/js_class_builder.rb +0 -114
  46. data/lib/vendor/facets/hash/recursive_merge.rb +0 -28
  47. data/test/core_ext_test.rb +0 -35
  48. data/test/netzke_core_test.rb +0 -136
@@ -1,5 +1,9 @@
1
1
  class NetzkeController < ActionController::Base
2
2
 
3
+ def index
4
+ redirect_to :action => :test_widgets
5
+ end
6
+
3
7
  # collect javascripts from all plugins that registered it in Netzke::Base.config[:javascripts]
4
8
  def netzke
5
9
  respond_to do |format|
@@ -9,8 +13,80 @@ class NetzkeController < ActionController::Base
9
13
  f = File.new(path)
10
14
  res << f.read
11
15
  end
16
+ render :text => res.strip_js_comments
17
+ }
18
+
19
+ format.css {
20
+ res = ""
21
+ Netzke::Base.config[:stylesheets].each do |path|
22
+ f = File.new(path)
23
+ res << f.read
24
+ end
12
25
  render :text => res
13
26
  }
14
27
  end
15
28
  end
29
+
30
+ #
31
+ # Primitive tests to quickly test the widgets
32
+ #
33
+
34
+ # FormPanel
35
+ netzke :form_panel, :persistent_config => false, :label_align => "top", :columns => [
36
+ {:name => 'field_one', :xtype => 'textarea'},
37
+ {:name => 'field_two', :xtype => 'textarea'}
38
+ ]
39
+
40
+ # BorderLayoutPanel
41
+ netzke :border_layout_panel, :regions => {
42
+ :west => {
43
+ :widget_class_name => "Panel",
44
+ :region_config => {:width => 300, :split => true}
45
+ },
46
+ :center => {
47
+ :widget_class_name => "Panel"
48
+ }
49
+ }
50
+
51
+ # TabPanel
52
+ netzke :tab_panel, :items => [{
53
+ :widget_class_name => "Panel",
54
+ :ext_config => {
55
+ :html => "Panel 1",
56
+ },
57
+ :active => true
58
+ },{
59
+ :widget_class_name => "Panel",
60
+ :ext_config => {
61
+ :html => "Panel 2",
62
+ }
63
+ }]
64
+
65
+ # AccordionPanel
66
+ netzke :accordion_panel, :items => [{
67
+ :widget_class_name => "Panel",
68
+ :ext_config => {
69
+ :html => "Panel 1",
70
+ }
71
+ # :active => true
72
+ },{
73
+ :widget_class_name => "Panel",
74
+ :ext_config => {
75
+ :html => "Panel 2",
76
+ }
77
+ }]
78
+
79
+ # BasicApp
80
+ netzke :basic_app
81
+
82
+ def test_widgets
83
+ html = "<h3>Quick primitive widgets tests</h3>"
84
+
85
+ self.class.widget_config_storage.each_key.map(&:to_s).sort.each do |w|
86
+ html << "<a href='#{w}_test'>#{w.to_s.humanize}</a><br/>\n"
87
+ end
88
+
89
+ render :text => html
90
+ end
91
+
16
92
  end
@@ -1,66 +1,155 @@
1
+ #
2
+ # TODO: would be great to support something like this:
3
+ # NetzkePreference["name"].merge!({:a => 1, :b => 2}) # if NetzkePreference["name"] returns a hash
4
+ # or
5
+ # NetzkePreference["name"] << 2 # if NetzkePreference["name"] returns an array
6
+ # etc
7
+ #
1
8
  class NetzkePreference < ActiveRecord::Base
2
- CONVERTION_METHODS= {'Fixnum' => 'to_i', 'String' => 'to_s', 'Float' => 'to_f', 'Symbol' => 'to_sym'}
3
-
4
- def self.user=(user)
5
- @@user = user
6
- end
9
+ belongs_to :user
10
+ belongs_to :role
7
11
 
8
- def self.user
9
- @@user ||= nil
10
- end
12
+ ELEMENTARY_CONVERTION_METHODS= {'Fixnum' => 'to_i', 'String' => 'to_s', 'Float' => 'to_f', 'Symbol' => 'to_sym'}
11
13
 
12
- def self.custom_field=(value)
13
- @@custom_field = value
14
+ def self.widget_name=(value)
15
+ @@widget_name = value
14
16
  end
15
17
 
16
- def self.custom_field
17
- @@custom_field ||= nil
18
+ def self.widget_name
19
+ @@widget_name ||= nil
18
20
  end
19
21
 
20
22
  def normalized_value
21
- klass = read_attribute(:pref_type)
23
+ klass = read_attribute(:pref_type)
22
24
  norm_value = read_attribute(:value)
23
- if klass.nil?
24
- # do not cast
25
- r = norm_value
26
- elsif klass == 'Boolean'
27
- r = norm_value == 'false' ? false : (norm_value == 'true' || norm_value)
28
- elsif klass == 'NilClass'
29
- r = nil
30
- elsif klass == 'Array'
31
- r = JSON.parse(norm_value)
25
+
26
+ case klass
27
+ when nil then r = norm_value # do not cast
28
+ when 'Boolean' then r = norm_value == 'false' ? false : (norm_value == 'true' || norm_value)
29
+ when 'NilClass' then r = nil
30
+ when 'Array', 'Hash' then r = ActiveSupport::JSON.decode(norm_value)
32
31
  else
33
- r = norm_value.send(CONVERTION_METHODS[klass])
32
+ r = norm_value.send(ELEMENTARY_CONVERTION_METHODS[klass])
34
33
  end
35
34
  r
36
35
  end
37
36
 
38
37
  def normalized_value=(new_value)
39
- # norm_value = (new_value.to_s if new_value == true or new_value == false) || new_value
40
- case new_value.class.to_s
41
- when "Array"
42
- write_attribute(:value, new_value.to_json)
43
- else
44
- write_attribute(:value, new_value.to_s)
38
+ case new_value.class.name
39
+ when "Array" then write_attribute(:value, new_value.to_json)
40
+ when "Hash" then write_attribute(:value, new_value.to_json)
41
+ else write_attribute(:value, new_value.to_s)
45
42
  end
46
43
  write_attribute(:pref_type, [TrueClass, FalseClass].include?(new_value.class) ? 'Boolean' : new_value.class.to_s)
47
44
  end
48
45
 
49
46
  def self.[](pref_name)
50
- pref_name = pref_name.to_s
51
- conditions = {:name => pref_name, :user_id => self.user, :custom_field => self.custom_field}
52
- pref = self.find(:first, :conditions => conditions)
53
- # pref = @@user.nil? ? self.find_by_name(pref_name) : self.find_by_name_and_user_id(pref_name, @@user.id)
47
+ pref_name = normalize_preference_name(pref_name)
48
+ pref = self.pref_to_read(pref_name)
54
49
  pref && pref.normalized_value
55
50
  end
56
51
 
57
52
  def self.[]=(pref_name, new_value)
58
- pref_name = pref_name.to_s
59
- conditions = {:name => pref_name, :user_id => self.user, :custom_field => self.custom_field}
60
- pref = self.find(:first, :conditions => conditions) || self.create(conditions)
61
- # pref = self.user.nil? ? self.find_or_create_by_name(pref_name) : self.find_or_create_by_name_and_user_id(pref_name, self.user.id)
62
- pref.normalized_value = new_value
63
- pref.save!
53
+ pref_name = normalize_preference_name(pref_name)
54
+ pref = self.pref_to_write(pref_name)
55
+
56
+ # if assigning nil, simply delete the eventually found preference
57
+ if new_value.nil?
58
+ pref && pref.destroy
59
+ else
60
+ # pref ||= self.new(conditions(pref_name))
61
+ pref.normalized_value = new_value
62
+ pref.save!
63
+ end
64
+ end
65
+
66
+ # Overwrite pref_to_read, pref_to_write methods, and find_all_for_widget if you want a different way of
67
+ # identifying the proper preference based on your own authorization strategy.
68
+ #
69
+ # The default strategy is:
70
+ # 1) if no masq_user or masq_role defined
71
+ # pref_to_read will search for the preference for user first, then for user's role
72
+ # pref_to_write will always find or create a preference for the current user (never for its role)
73
+ # 2) if masq_user or masq_role is defined
74
+ # pref_to_read and pref_to_write will always take the masquerade into account, e.g. reads/writes will go to
75
+ # the user/role specified
76
+ #
77
+ def self.pref_to_read(name)
78
+ name = name.to_s
79
+ session = Netzke::Base.session
80
+ cond = {:name => name, :widget_name => self.widget_name}
81
+
82
+ if session[:masq_user]
83
+ # first, get the prefs for this user it they exist
84
+ res = self.find(:first, :conditions => cond.merge({:user_id => session[:masq_user]}))
85
+ # if it doesn't exist, get them for the user's role
86
+ user = User.find(session[:masq_user])
87
+ res ||= self.find(:first, :conditions => cond.merge({:role_id => user.role.id}))
88
+ elsif session[:masq_role]
89
+ res = self.find(:first, :conditions => cond.merge({:role_id => session[:masq_role]}))
90
+ elsif session[:netzke_user_id]
91
+ user = User.find(session[:netzke_user_id])
92
+ res = self.find(:first, :conditions => cond.merge({:user_id => user.id}))
93
+ res ||= self.find(:first, :conditions => cond.merge({:role_id => user.role.id}))
94
+ else
95
+ res = self.find(:first, :conditions => cond)
96
+ end
97
+
98
+ res
99
+ end
100
+
101
+ def self.pref_to_write(name)
102
+ name = name.to_s
103
+ session = Netzke::Base.session
104
+ cond = {:name => name, :widget_name => self.widget_name}
105
+
106
+ if session[:masq_user]
107
+ cond.merge!({:user_id => session[:masq_user]})
108
+ res = self.find(:first, :conditions => cond)
109
+ res ||= self.new(cond)
110
+ elsif session[:masq_role]
111
+ # first, delete all the corresponding preferences for the users that have this role
112
+ Role.find(session[:masq_role]).users.each do |u|
113
+ self.delete_all(cond.merge({:user_id => u.id}))
114
+ end
115
+ cond.merge!({:role_id => session[:masq_role]})
116
+ res = self.find(:first, :conditions => cond)
117
+ res ||= self.new(cond)
118
+ elsif session[:netzke_user_id]
119
+ res = self.find(:first, :conditions => cond.merge({:user_id => session[:netzke_user_id]}))
120
+ res ||= self.new(cond.merge({:user_id => session[:netzke_user_id]}))
121
+ else
122
+ res = self.find(:first, :conditions => cond)
123
+ res ||= self.new(cond)
124
+ end
125
+ res
126
+ end
127
+
128
+ def self.find_all_for_widget(name)
129
+ session = Netzke::Base.session
130
+ cond = {:widget_name => name}
131
+
132
+ if session[:masq_user] || session[:masq_role]
133
+ cond.merge!({:user_id => session[:masq_user], :role_id => session[:masq_role]})
134
+ res = self.find(:all, :conditions => cond)
135
+ elsif session[:netzke_user_id]
136
+ user = User.find(session[:netzke_user_id])
137
+ res = self.find(:all, :conditions => cond.merge({:user_id => session[:netzke_user_id]}))
138
+ res += self.find(:all, :conditions => cond.merge({:role_id => user.role.try(:id)}))
139
+ else
140
+ res = self.find(:all, :conditions => cond)
141
+ end
142
+
143
+ res
144
+ end
145
+
146
+ def self.delete_all_for_widget(name)
147
+ self.destroy(find_all_for_widget(name))
148
+ end
149
+
150
+ private
151
+ def self.normalize_preference_name(name)
152
+ name.to_s.gsub(".", "__").gsub("/", "__")
64
153
  end
65
154
 
66
155
  end
@@ -1,13 +1,16 @@
1
+ require 'active_support'
2
+
1
3
  # NetzkeCore
2
- require 'netzke/js_class_builder'
3
4
  require 'netzke/base'
4
- require 'netzke/core_ext'
5
- require 'netzke/controller_extensions'
6
5
 
7
- # Vendor
8
- require 'vendor/facets/hash/recursive_merge'
6
+ require 'netzke/action_view_ext'
7
+ require 'netzke/controller_extensions'
8
+ require 'netzke/core_ext'
9
+ require 'netzke/routing'
9
10
 
11
+ require 'netzke/feedback_ghost'
10
12
 
13
+ # Load models and controllers from lib/app
11
14
  %w{ models controllers }.each do |dir|
12
15
  path = File.join(File.dirname(__FILE__), 'app', dir)
13
16
  $LOAD_PATH << path
@@ -15,12 +18,23 @@ require 'vendor/facets/hash/recursive_merge'
15
18
  ActiveSupport::Dependencies.load_once_paths.delete(path)
16
19
  end
17
20
 
18
- ActionController::Base.class_eval do
19
- include Netzke::ControllerExtensions
21
+ if defined? ActionController
22
+ ActionController::Base.class_eval do
23
+ include Netzke::ControllerExtensions
24
+ end
25
+
26
+ # Include the route to the Netzke controller
27
+ ActionController::Routing::RouteSet::Mapper.send :include, Netzke::Routing::MapperExtensions
20
28
  end
21
29
 
22
- # Make this plugin reloadable for easier development
30
+ if defined? ActionView
31
+ ActionView::Base.send :include, Netzke::ActionViewExt
32
+ end
33
+
34
+ # Make this plugin auto-reloadable for easier development
23
35
  ActiveSupport::Dependencies.load_once_paths.delete(File.join(File.dirname(__FILE__)))
24
36
 
25
- # Include the javascript
37
+ # Include javascript & styles required by all Netzke widgets.
38
+ # These files will get loaded at the initial load of the framework (along with Ext).
26
39
  Netzke::Base.config[:javascripts] << "#{File.dirname(__FILE__)}/../javascripts/core.js"
40
+ Netzke::Base.config[:stylesheets] << "#{File.dirname(__FILE__)}/../stylesheets/core.css"
@@ -0,0 +1,26 @@
1
+ module Netzke
2
+ module ActionViewExt
3
+
4
+ def netzke_js_include
5
+ res = ""
6
+
7
+ if ENV['RAILS_ENV'] == 'development'
8
+ res << javascript_include_tag("/extjs/adapter/ext/ext-base.js", "/extjs/ext-all-debug.js")
9
+ else
10
+ res << javascript_include_tag("/extjs/adapter/ext/ext-base.js", "/extjs/ext-all.js")
11
+ end
12
+ res << javascript_tag( "Ext.authenticityToken = '#{form_authenticity_token}'") # forgery protection
13
+ res << javascript_include_tag("/netzke/netzke.js")
14
+
15
+ res
16
+ end
17
+
18
+ def netzke_css_include(theme_name = :default)
19
+ res = stylesheet_link_tag("/extjs/resources/css/ext-all.css")
20
+ res << stylesheet_link_tag("/extjs/resources/css/xtheme-#{theme_name}.css") unless theme_name == :default
21
+ res << stylesheet_link_tag("/netzke/netzke.css") # CSS from Netzke
22
+ res
23
+ end
24
+ end
25
+ end
26
+
@@ -1,100 +1,362 @@
1
- require 'json'
1
+ require 'netzke/base_js'
2
+
2
3
  module Netzke
4
+ # = Base
5
+ # Base class for every Netzke widget
6
+ #
7
+ # To instantiate a widget in the controller:
8
+ #
9
+ # netzke :widget_name, configuration_hash
10
+ #
11
+ # == Configuration
12
+ # * <tt>:widget_class_name</tt> - name of the widget class in the scope of the Netzke module, e.g. "FormPanel".
13
+ # When a widget is defined in the controller and this option is omitted, widget class is deferred from the widget's
14
+ # name. E.g.:
15
+ #
16
+ # netzke :grid_panel, :data_class_name => "User"
17
+ #
18
+ # In this case <tt>:widget_class_name</tt> is assumed to be "GridPanel"
19
+ #
20
+ # * <tt>:ext_config</tt> - a config hash that is used to create a javascript instance of the widget. Every
21
+ # configuration that comes here will be available inside the javascript instance of the widget.
22
+ # * <tt>:persistent_config</tt> - if set to <tt>true</tt>, the widget will use persistent storage to store its state;
23
+ # for instance, Netzke::GridPanel stores there its columns state (width, visibility, order, headers, etc).
24
+ # A widget may or may not provide interface to its persistent settings. GridPanel and FormPanel from netzke-basepack
25
+ # are examples of widgets that by default do.
26
+ #
27
+ # Examples of configuration:
28
+ #
29
+ # netzke :books,
30
+ # :widget_class_name => "GridPanel",
31
+ # :data_class_name => "Book", # GridPanel specific option
32
+ # :ext_config => {
33
+ # :icon_cls => 'icon-grid',
34
+ # :title => "My books"
35
+ # }
36
+ #
37
+ # netzke :form_panel,
38
+ # :data_class_name => "User" # FormPanel specific option
3
39
  class Base
4
- # Global Netzke configuration
5
- def self.config
6
- @@config ||= {
7
- :javascripts => ["#{File.dirname(__FILE__)}/../../javascripts/core.js"] # locations of javascript files (which automatically will be collected into one file and sent as netzke.js)
8
- }
9
- end
10
-
11
- # Helper class to read/write from/to widget's persistent preferences. TODO: rework it.
12
- class Config
13
- def initialize(widget_name)
14
- @widget_name = widget_name
40
+ include Netzke::BaseJs # javascript (client-side)
41
+
42
+ module ClassMethods
43
+ # Class-level Netzke::Base configuration. The defaults also get specified here.
44
+ def config
45
+ set_default_config({
46
+ # which javascripts and stylesheets must get included at the initial load (see netzke-core.rb)
47
+ :javascripts => [],
48
+ :stylesheets => [],
49
+
50
+ :persistent_config_manager => "NetzkePreference",
51
+ :ext_location => defined?(RAILS_ROOT) && "#{RAILS_ROOT}/public/extjs",
52
+ :default_config => {
53
+ :persistent_config => true,
54
+ :ext_config => {}
55
+ }
56
+ })
15
57
  end
16
- def []=(k,v)
17
- NetzkePreference.custom_field = @widget_name
18
- NetzkePreference[k] = v
58
+
59
+ def configure(*args)
60
+ if args.first.is_a?(Symbol)
61
+ # first arg is a Symbol
62
+ config[args.first] = args.last
63
+ else
64
+ config.deep_merge!(args.first)
65
+ end
66
+
67
+ enforce_config_consistency
19
68
  end
20
- def [](k)
21
- NetzkePreference.custom_field = @widget_name
22
- NetzkePreference[k]
69
+
70
+ def enforce_config_consistency; end
71
+
72
+ # "Netzke::SomeWidget" => "SomeWidget"
73
+ def short_widget_class_name
74
+ self.name.split("::").last
23
75
  end
24
- end
25
-
26
- # client-side code (generates JS-classes of the widgets)
27
- include Netzke::JsClassBuilder
28
76
 
29
- attr_accessor :config, :server_confg, :parent, :logger, :id_name, :permissions
30
- attr_reader :pref
77
+ # Multi-user support (deprecated in favor of controller sessions)
78
+ def user
79
+ @@user ||= nil
80
+ end
31
81
 
32
- def initialize(config = {}, parent = nil)
33
- @logger = Logger.new("debug.log")
34
- @config = initial_config.recursive_merge(config)
35
- @parent = parent
36
- @id_name = parent.nil? ? config[:name].to_s : "#{parent.id_name}__#{config[:name]}"
82
+ def user=(user)
83
+ @@user = user
84
+ end
85
+
86
+ # Access to controller sessions
87
+ def session
88
+ @@session ||= {}
89
+ end
90
+
91
+ def session=(s)
92
+ @@session = s
93
+ end
94
+
95
+ # called by controller at the moment of successfull login
96
+ def login
97
+ session[:_netzke_next_request_is_first_after_login] = true
98
+ end
37
99
 
38
- @flash = []
39
- @pref = Config.new(@id_name)
100
+ # called by controller at the moment of logout
101
+ def logout
102
+ session[:_netzke_next_request_is_first_after_logout] = true
103
+ end
104
+
105
+ # Use this class method to declare connection points between client side of a widget and its server side.
106
+ # A method in a widget class with the same name will be (magically) called by the client side of the widget.
107
+ # See netzke-basepack's GridPanel for an example.
108
+ def api(*api_points)
109
+ apip = read_inheritable_attribute(:api_points) || []
110
+ api_points.each{|p| apip << p}
111
+ write_inheritable_attribute(:api_points, apip)
112
+
113
+ # It may be needed later for security
114
+ api_points.each do |apip|
115
+ module_eval <<-END, __FILE__, __LINE__
116
+ def api_#{apip}(*args)
117
+ #{apip}(*args).to_nifty_json
118
+ end
119
+ # FIXME: commented out because otherwise ColumnOperations stop working
120
+ # def #{apip}(*args)
121
+ # flash :warning => "API point '#{apip}' is not implemented for widget '#{short_widget_class_name}'"
122
+ # {:flash => @flash}
123
+ # end
124
+ END
125
+ end
126
+ end
127
+
128
+ def api_points
129
+ read_inheritable_attribute(:api_points)
130
+ end
40
131
 
41
- @config[:ext_config] ||= {} # configuration used to instantiate JS class
132
+ # returns an instance of a widget defined in the config
133
+ def instance_by_config(config)
134
+ widget_class = "Netzke::#{config[:widget_class_name]}".constantize
135
+ widget_class.new(config)
136
+ end
137
+
138
+ # persistent_config and layout manager classes
139
+ def persistent_config_manager_class
140
+ Netzke::Base.config[:persistent_config_manager].try(:constantize)
141
+ rescue NameError
142
+ nil
143
+ end
144
+
145
+ # Return persistent config class
146
+ def persistent_config
147
+ # if the class is not present, fake it (it will not store anything, and always return nil)
148
+ if persistent_config_manager_class.nil?
149
+ {}
150
+ else
151
+ persistent_config_manager_class
152
+ end
153
+ end
154
+
155
+ private
156
+ def set_default_config(c)
157
+ @@config ||= {}
158
+ @@config[self.name] ||= c
159
+ end
42
160
 
43
- process_permissions_config
44
161
  end
162
+ extend ClassMethods
45
163
 
46
- def initial_config
47
- {}
164
+ attr_accessor :parent, :name, :id_name, :permissions, :session
165
+
166
+ api :load_aggregatee_with_cache # every widget gets this api
167
+
168
+ # Widget initialization process
169
+ # * the config hash is available to the widget after the "super" call in the initializer
170
+ # * override/add new default configuration options into the "default_config" method
171
+ # (the config hash is not yet available)
172
+ def initialize(config = {}, parent = nil)
173
+ @session = Netzke::Base.session
174
+ @passed_config = config # configuration passed at the moment of instantiation
175
+ @parent = parent
176
+ @name = config[:name].nil? ? short_widget_class_name.underscore : config[:name].to_s
177
+ @id_name = parent.nil? ? @name : "#{parent.id_name}__#{@name}"
178
+ @flash = []
48
179
  end
49
180
 
50
- # 'Netzke::Grid' => 'Grid'
51
- def short_widget_class_name
52
- self.class.short_widget_class_name
181
+ # add flatten method to Hash
182
+ Hash.class_eval do
183
+ def flatten(preffix = "")
184
+ res = []
185
+ self.each_pair do |k,v|
186
+ if v.is_a?(Hash)
187
+ res += v.flatten(k)
188
+ else
189
+ res << {
190
+ :name => ((preffix.to_s.empty? ? "" : preffix.to_s + "__") + k.to_s).to_sym,
191
+ :value => v,
192
+ :type => (["TrueClass", "FalseClass"].include?(v.class.name) ? 'Boolean' : v.class.name).to_sym
193
+ }
194
+ end
195
+ end
196
+ res
197
+ end
198
+ end
199
+
200
+ def default_config
201
+ self.class.config[:default_config].nil? ? {} : {}.merge!(self.class.config[:default_config])
202
+ end
203
+
204
+ # Access to the config that takes into account all possible ways to configure a widget. *Read only*.
205
+ def config
206
+ # Translates into something like this:
207
+ # @config ||= default_config.
208
+ # deep_merge(@passed_config).
209
+ # deep_merge(persistent_config_hash).
210
+ # deep_merge(strong_parent_config).
211
+ # deep_merge(strong_session_config)
212
+ @config ||= independent_config.
213
+ deep_merge(strong_parent_config).
214
+ deep_merge(strong_session_config)
215
+
216
+ end
217
+
218
+ def flat_config(key = nil)
219
+ fc = config.flatten
220
+ key.nil? ? fc : fc.select{ |c| c[:name] == key.to_sym }.first.try(:value)
221
+ end
222
+
223
+ def strong_parent_config
224
+ @strong_parent_config ||= parent.nil? ? {} : parent.strong_children_config
225
+ end
226
+
227
+ # Config that is not overwritten by parents and sessions
228
+ def independent_config
229
+ @independent_config ||= initial_config.deep_merge(persistent_config_hash)
230
+ end
231
+
232
+ def flat_independent_config(key = nil)
233
+ fc = independent_config.flatten
234
+ key.nil? ? fc : fc.select{ |c| c[:name] == key.to_sym }.first.try(:value)
53
235
  end
54
236
 
55
- def self.short_widget_class_name
56
- name.split("::").last
237
+ def flat_default_config(key = nil)
238
+ fc = default_config.flatten
239
+ key.nil? ? fc : fc.select{ |c| c[:name] == key.to_sym }.first.try(:value)
240
+ end
241
+
242
+ # Static, hardcoded config. Consists of default values merged with config that was passed during instantiation
243
+ def initial_config
244
+ @initial_config ||= default_config.deep_merge(@passed_config)
57
245
  end
58
246
 
59
- #
60
- # Use this class-method to declare connection points between client side of a widget and its server side. A method in a widget class with the same name will be (magically) called by the client-side of the widget. See Grid widget for an example
61
- #
62
- def self.interface(*interface_points)
63
- interfacep = read_inheritable_attribute(:interface_points) || []
64
- interface_points.each{|p| interfacep << p}
65
- write_inheritable_attribute(:interface_points, interfacep)
247
+ def flat_initial_config(key = nil)
248
+ fc = initial_config.flatten
249
+ key.nil? ? fc : fc.select{ |c| c[:name] == key.to_sym }.first.try(:value)
250
+ end
251
+
252
+ def build_persistent_config_hash
253
+ return {} if !initial_config[:persistent_config]
66
254
 
67
- interface_points.each do |interfacep|
68
- module_eval <<-END, __FILE__, __LINE__
69
- def interface_#{interfacep}(*args)
70
- #{interfacep}(*args).to_js
255
+ prefs = NetzkePreference.find_all_for_widget(id_name)
256
+ res = {}
257
+ prefs.each do |p|
258
+ hsh_levels = p.name.split("__").map(&:to_sym)
259
+ tmp_res = {} # it decends into itself, building itself
260
+ anchor = {} # it will keep the tail of tmp_res
261
+ hsh_levels.each do |level_prefix|
262
+ tmp_res[level_prefix] ||= level_prefix == hsh_levels.last ? p.normalized_value : {}
263
+ anchor = tmp_res[level_prefix] if level_prefix == hsh_levels.first
264
+ tmp_res = tmp_res[level_prefix]
71
265
  end
72
- # FIXME: commented out because otherwise ColumnOperations stop working
73
- # def #{interfacep}(*args)
74
- # flash :warning => "API point '#{interfacep}' is not implemented for widget '#{short_widget_class_name}'"
75
- # {:flash => @flash}
76
- # end
77
- END
266
+ # Now 'anchor' is a hash that represents the path to the single value,
267
+ # for example: {:ext_config => {:title => 100}} (which corresponds to ext_config__title)
268
+ # So we need to recursively merge it into the final result
269
+ res.deep_merge!(hsh_levels.first => anchor)
78
270
  end
271
+ res
272
+ end
273
+
274
+ def persistent_config_hash
275
+ @persistent_config_hash ||= build_persistent_config_hash
276
+ end
277
+
278
+ def ext_config
279
+ config[:ext_config] || {}
79
280
  end
80
281
 
81
- def self.interface_points
82
- read_inheritable_attribute(:interface_points)
282
+ # Like normal config, but stored in session
283
+ def weak_session_config
284
+ widget_session[:weak_session_config] ||= {}
83
285
  end
286
+
287
+ def strong_session_config
288
+ widget_session[:strong_session_config] ||= {}
289
+ end
290
+
291
+ # configuration of all children will get deep_merge'd with strong_children_config
292
+ # def strong_children_config= (c)
293
+ # @strong_children_config = c
294
+ # end
295
+
296
+ # This config will be picked up by all the descendants
297
+ def strong_children_config
298
+ @strong_children_config ||= parent.nil? ? {} : parent.strong_children_config
299
+ end
300
+
301
+ # configuration of all children will get reverse_deep_merge'd with weak_children_config
302
+ # def weak_children_config= (c)
303
+ # @weak_children_config = c
304
+ # end
84
305
 
85
- def interface_points
86
- self.class.interface_points
306
+ def weak_children_config
307
+ @weak_children_config ||= {}
308
+ end
309
+
310
+ def widget_session
311
+ session[id_name] ||= {}
87
312
  end
88
313
 
89
- interface :get_widget # default
314
+ # Rails' logger
315
+ def logger
316
+ Rails.logger
317
+ end
90
318
 
319
+ def dependency_classes
320
+ res = []
321
+ non_late_aggregatees.keys.each do |aggr|
322
+ res += aggregatee_instance(aggr).dependency_classes
323
+ end
324
+ res << short_widget_class_name
325
+ res.uniq
326
+ end
327
+
328
+ # Store some setting in the database as if it was a hash, e.g.:
329
+ # persistent_config["window.size"] = 100
330
+ # persistent_config["window.size"] => 100
331
+ # This method is user-aware
332
+ def persistent_config
333
+ if config[:persistent_config]
334
+ config_class = self.class.persistent_config
335
+ config_class.widget_name = id_name # pass to the config class our unique name
336
+ config_class
337
+ else
338
+ # if we can't use presistent config, all the calls to it will always return nil, and the "="-operation will be ignored
339
+ logger.debug "==> NETZKE: no persistent config is set up for widget '#{id_name}'"
340
+ {}
341
+ end
342
+ end
343
+
344
+ # 'Netzke::Grid' => 'Grid'
345
+ def short_widget_class_name
346
+ self.class.short_widget_class_name
347
+ end
348
+
91
349
  ## Dependencies
92
350
  def dependencies
93
- @dependencies ||= initial_dependencies
351
+ @dependencies ||= begin
352
+ non_late_aggregatees_widget_classes = non_late_aggregatees.values.map{|v| v[:widget_class_name]}
353
+ (initial_dependencies + non_late_aggregatees_widget_classes << self.class.short_widget_class_name).uniq
354
+ end
94
355
  end
95
356
 
357
+ # override this method if you need some extra dependencies, which are not the aggregatees
96
358
  def initial_dependencies
97
- config[:dependencies] || []
359
+ []
98
360
  end
99
361
 
100
362
  ### Aggregation
@@ -106,11 +368,22 @@ module Netzke
106
368
  @aggregatees ||= initial_aggregatees.merge(initial_late_aggregatees.each_pair{|k,v| v.merge!(:late_aggregation => true)})
107
369
  end
108
370
 
371
+ def non_late_aggregatees
372
+ aggregatees.reject{|k,v| v[:late_aggregation]}
373
+ end
374
+
109
375
  def add_aggregatee(aggr)
110
376
  aggregatees.merge!(aggr)
111
377
  end
112
378
 
113
- # The difference between aggregatees and late aggregatees is the following: the former gets instantiated together with its aggregator and is normally instantly visible as a part of it. While a late aggregatee doesn't get instantiated along with its aggregator. Until it gets requested, it doesn't take any part in its aggregator's lifecycle. An example of late aggregatee could be a widget that is loaded by an application widget on user's request, or a preferences window that only gets instantiated when user wants to edit widget's preferences. An example of a normal aggregatee is any widget (like a grid) within a BorderLayout-based widget (i.e. aggregator) - it should get instantiated and shown along with its aggregator.
379
+ def remove_aggregatee(aggr)
380
+ if config[:persistent_config]
381
+ persistent_config_manager_class.delete_all_for_widget("#{id_name}__#{aggr}")
382
+ end
383
+ aggregatees[aggr] = nil
384
+ end
385
+
386
+ # The difference between aggregatees and late aggregatees is the following: the former gets instantiated together with its aggregator and is normally *instantly* visible as a part of it (for example, the widget in the initially expanded panel in an Accordion). A late aggregatee doesn't get instantiated along with its aggregator. Until it gets requested from the server, it doesn't take any part in its aggregator's life. An example of late aggregatee could be a widget that is loaded dynamically into a previously collapsed panel of an Accordion, or a preferences window (late aggregatee) for a widget (aggregator) that only gets shown when user wants to edit widget's preferences.
114
387
  def initial_late_aggregatees
115
388
  {}
116
389
  end
@@ -120,19 +393,28 @@ module Netzke
120
393
  end
121
394
 
122
395
  # recursively instantiates an aggregatee based on its "path": e.g. if we have an aggregatee :aggr1 which in its turn has an aggregatee :aggr10, the path to the latter would be "aggr1__aggr10"
123
- def aggregatee_instance(name)
396
+ def aggregatee_instance(name, strong_config = {})
124
397
  aggregator = self
125
398
  name.to_s.split('__').each do |aggr|
126
399
  aggr = aggr.to_sym
127
- # TODO: should we put all the classes under Netzke::-scope?
128
- # widget_class = full_widget_class_name(aggregator.aggregatees[aggr][:widget_class_name]).constantize
129
- widget_class = "Netzke::#{aggregator.aggregatees[aggr][:widget_class_name]}".constantize
130
- aggregator = widget_class.new(aggregator.aggregatees[aggr].merge(:name => aggr), aggregator)
400
+ aggregatee_config = aggregator.aggregatees[aggr]
401
+ raise ArgumentError, "No aggregatee '#{aggr}' defined for widget '#{aggregator.id_name}'" if aggregatee_config.nil?
402
+ short_class_name = aggregatee_config[:widget_class_name]
403
+ raise ArgumentError, "No widget_class_name specified for aggregatee #{aggr} of #{aggregator.id_name}" if short_class_name.nil?
404
+ widget_class = "Netzke::#{short_class_name}".constantize
405
+
406
+ conf = weak_children_config.
407
+ deep_merge(aggregatee_config).
408
+ deep_merge(strong_config). # we may want to reconfigure the aggregatee at the moment of instantiation
409
+ merge(:name => aggr)
410
+
411
+ aggregator = widget_class.new(conf, aggregator) # params: config, parent
412
+ # aggregator.weak_children_config = weak_children_config
413
+ # aggregator.strong_children_config = strong_children_config
131
414
  end
132
415
  aggregator
133
416
  end
134
417
 
135
-
136
418
  def full_widget_class_name(short_name)
137
419
  "Netzke::#{short_name}"
138
420
  end
@@ -148,51 +430,107 @@ module Netzke
148
430
  end
149
431
 
150
432
  # permissions
151
- def available_permissions
152
- []
433
+ # def available_permissions
434
+ # []
435
+ # end
436
+
437
+ # def process_permissions_config
438
+ # if !available_permissions.empty?
439
+ # # First, process permissions from the config
440
+ # @permissions = available_permissions.inject({}){|h,p| h.merge(p.to_sym => true)} # by default anything is allowed
441
+ #
442
+ # config[:prohibit] = available_permissions if config[:prohibit] == :all # short-cut for all permissions
443
+ # config[:prohibit] = [config[:prohibit]] if config[:prohibit].is_a?(Symbol) # so that config[:prohibit] => :write works
444
+ # config[:prohibit] && config[:prohibit].each{|p| @permissions.merge!(p.to_sym => false)} # prohibit
445
+ #
446
+ # config[:allow] = [config[:allow]] if config[:allow].is_a?(Symbol) # so that config[:allow] => :write works
447
+ # config[:allow] && config[:allow].each{|p| @permissions.merge!(p.to_sym => true)} # allow
448
+ #
449
+ # # ... and then merge it with NetzkePreferences
450
+ # available_permissions.each do |p|
451
+ # # if nothing is stored in persistent_config, store the permission from the config; otherwise leave what's there
452
+ # persistent_config["permissions/#{p}"].nil? && persistent_config["permissions/#{p}"] = @permissions[p.to_sym]
453
+ #
454
+ # # what's stored in persistent_config has higher priority, so, if there's something there, use that
455
+ # persistent_permisson = persistent_config["permissions/#{p}"]
456
+ # @permissions[p.to_sym] = persistent_permisson unless persistent_permisson.nil?
457
+ # end
458
+ # end
459
+ # end
460
+
461
+ # called when the method_missing tries to processes a non-existing aggregatee
462
+ def aggregatee_missing(aggr)
463
+ flash :error => "Unknown aggregatee #{aggr} for widget #{name}"
464
+ {:feedback => @flash}.to_nifty_json
465
+ end
466
+
467
+ def tools
468
+ persistent_config[:tools] ||= config[:tools] || []
153
469
  end
154
470
 
155
- def process_permissions_config
156
- if !available_permissions.empty?
157
- # First, process permissions from the config
158
- @permissions = available_permissions.inject({}){|h,p| h.merge(p.to_sym => true)} # by default anything is allowed
471
+ def menu
472
+ persistent_config[:menu] ||= config[:menu] == false ? nil : config[:menu]
473
+ end
474
+
475
+ # some convenience for instances
476
+ def persistent_config_manager_class
477
+ self.class.persistent_config_manager_class
478
+ end
159
479
 
160
- config[:prohibit] = available_permissions if config[:prohibit] == :all # short-cut for all permissions
161
- config[:prohibit] = [config[:prohibit]] if config[:prohibit].is_a?(Symbol) # so that config[:prohibit] => :write works
162
- config[:prohibit] && config[:prohibit].each{|p| @permissions.merge!(p.to_sym => false)} # prohibit
480
+ # override this method to do stuff at the moment of loading by some parent
481
+ def before_load
482
+ widget_session.clear
483
+ end
163
484
 
164
- config[:allow] = [config[:allow]] if config[:allow].is_a?(Symbol) # so that config[:allow] => :write works
165
- config[:allow] && config[:allow].each{|p| @permissions.merge!(p.to_sym => true)} # allow
166
-
167
- # ... and then merge it with NetzkePreferences
168
- available_permissions.each do |p|
169
- @permissions[p.to_sym] = @pref["permissions.#{p}"] if !@pref["permissions.#{p}"].nil?
170
- end
171
- end
485
+ # API
486
+ def load_aggregatee_with_cache(params)
487
+ cache = ActiveSupport::JSON.decode(params.delete(:cache))
488
+ relative_widget_id = params.delete(:id).underscore
489
+ passed_config = params[:config] && ActiveSupport::JSON.decode(params[:config]) || {}
490
+ passed_config = passed_config.symbolize_keys
491
+ widget = aggregatee_instance(relative_widget_id, passed_config)
492
+
493
+ # inform the widget that it's being loaded
494
+ widget.before_load
495
+
496
+ [{
497
+ :js => widget.js_missing_code(cache),
498
+ :css => css_missing_code(cache)
499
+ }, {
500
+ :render_widget_in_container => {
501
+ :container => params[:container],
502
+ :config => widget.js_config
503
+ }
504
+ }, {
505
+ :widget_loaded => {
506
+ :id => relative_widget_id
507
+ }
508
+ }]
172
509
  end
173
510
 
174
- ## method dispatcher - sends method to the proper aggregatee
511
+ # Method dispatcher - instantiates an aggregatee and calls the method on it
512
+ # E.g.:
513
+ # users__center__get_data
514
+ # instantiates aggregatee "users", and calls "center__get_data" on it
515
+ # books__move_column
516
+ # instantiates aggregatee "books", and calls "api_move_column" on it
175
517
  def method_missing(method_name, params = {})
176
518
  widget, *action = method_name.to_s.split('__')
177
519
  widget = widget.to_sym
178
520
  action = !action.empty? && action.join("__").to_sym
179
521
 
180
- if action && aggregatees[widget]
181
- # only actions starting with "interface_" are accessible
182
- interface_action = action.to_s.index('__') ? action : "interface_#{action}"
183
- aggregatee_instance(widget).send(interface_action, params)
522
+ if action
523
+ if aggregatees[widget]
524
+ # only actions starting with "api_" are accessible
525
+ api_action = action.to_s.index('__') ? action : "api_#{action}"
526
+ aggregatee_instance(widget).send(api_action, params)
527
+ else
528
+ aggregatee_missing(widget)
529
+ end
184
530
  else
185
531
  super
186
532
  end
187
533
  end
188
-
189
- #### API section
190
- def get_widget(params = {})
191
- # if browser does not have our component class cached (and all dependencies), send it to him
192
- components_cache = (JSON.parse(params[:components_cache]) if params[:components_cache]) || []
193
-
194
- {:config => js_config, :class_definition => js_missing_code(components_cache)}
195
- end
196
-
534
+
197
535
  end
198
536
  end