netzke-core 0.1.1.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 (40) hide show
  1. data/CHANGELOG +13 -0
  2. data/LICENSE +20 -0
  3. data/Manifest +39 -0
  4. data/README.mdown +11 -0
  5. data/Rakefile +14 -0
  6. data/generators/netzke_core/USAGE +8 -0
  7. data/generators/netzke_core/netzke_core_generator.rb +13 -0
  8. data/generators/netzke_core/templates/create_netzke_layouts.rb +14 -0
  9. data/generators/netzke_core/templates/create_netzke_preferences.rb +18 -0
  10. data/generators/netzke_core/templates/netzke.html.erb +10 -0
  11. data/init.rb +1 -0
  12. data/install.rb +1 -0
  13. data/javascripts/core.js +124 -0
  14. data/lib/app/controllers/netzke_controller.rb +16 -0
  15. data/lib/app/models/netzke_layout.rb +75 -0
  16. data/lib/app/models/netzke_preference.rb +66 -0
  17. data/lib/netzke-core.rb +28 -0
  18. data/lib/netzke/base.rb +210 -0
  19. data/lib/netzke/controller_extensions.rb +95 -0
  20. data/lib/netzke/core_ext.rb +77 -0
  21. data/lib/netzke/js_class_builder.rb +114 -0
  22. data/lib/vendor/facets/hash/recursive_merge.rb +28 -0
  23. data/netzke-core.gemspec +32 -0
  24. data/tasks/netzke_core_tasks.rake +4 -0
  25. data/test/app_root/app/controllers/application.rb +2 -0
  26. data/test/app_root/config/boot.rb +114 -0
  27. data/test/app_root/config/database.yml +21 -0
  28. data/test/app_root/config/environment.rb +13 -0
  29. data/test/app_root/config/environments/in_memory.rb +0 -0
  30. data/test/app_root/config/environments/mysql.rb +0 -0
  31. data/test/app_root/config/environments/postgresql.rb +0 -0
  32. data/test/app_root/config/environments/sqlite.rb +0 -0
  33. data/test/app_root/config/environments/sqlite3.rb +0 -0
  34. data/test/app_root/config/routes.rb +4 -0
  35. data/test/app_root/script/console +6 -0
  36. data/test/core_ext_test.rb +35 -0
  37. data/test/netzke_core_test.rb +133 -0
  38. data/test/test_helper.rb +20 -0
  39. data/uninstall.rb +1 -0
  40. metadata +109 -0
@@ -0,0 +1,28 @@
1
+ # NetzkeCore
2
+ require 'netzke/js_class_builder'
3
+ require 'netzke/base'
4
+ require 'netzke/core_ext'
5
+ require 'netzke/controller_extensions'
6
+
7
+ # Vendor
8
+ require 'vendor/facets/hash/recursive_merge'
9
+
10
+
11
+ %w{ models controllers }.each do |dir|
12
+ path = File.join(File.dirname(__FILE__), 'app', dir)
13
+ $LOAD_PATH << path
14
+ ActiveSupport::Dependencies.load_paths << path
15
+ ActiveSupport::Dependencies.load_once_paths.delete(path)
16
+ end
17
+
18
+ # raise 'test'
19
+
20
+ ActionController::Base.class_eval do
21
+ include Netzke::ControllerExtensions
22
+ end
23
+
24
+ # Make this plugin reloadable for easier development
25
+ ActiveSupport::Dependencies.load_once_paths.delete(File.join(File.dirname(__FILE__)))
26
+
27
+ # Include the javascript
28
+ Netzke::Base.config[:javascripts] << "#{File.dirname(__FILE__)}/../javascripts/core.js"
@@ -0,0 +1,210 @@
1
+ require 'json'
2
+ module Netzke
3
+ 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
+ def self.js_class_code(cached_dependencies = [])
12
+ self.new(:js_class => true).js_missing_code(cached_dependencies)
13
+ end
14
+
15
+ # Helper class to read/write from/to widget's persistent preferences. TODO: rework it.
16
+ class Config
17
+ def initialize(widget_name)
18
+ @widget_name = widget_name
19
+ end
20
+ def []=(k,v)
21
+ NetzkePreference.custom_field = @widget_name
22
+ NetzkePreference[k] = v
23
+ end
24
+ def [](k)
25
+ NetzkePreference.custom_field = @widget_name
26
+ NetzkePreference[k]
27
+ end
28
+ end
29
+
30
+ # client-side code (generates JS-classes of the widgets)
31
+ include Netzke::JsClassBuilder
32
+
33
+ attr_accessor :config, :server_confg, :parent, :logger, :id_name, :permissions
34
+ attr_reader :pref
35
+
36
+ def initialize(config = {}, parent = nil)
37
+ @logger = Logger.new("debug.log")
38
+ @config = initial_config.recursive_merge(config)
39
+ @parent = parent
40
+ @id_name = parent.nil? ? config[:name].to_s : "#{parent.id_name}__#{config[:name]}"
41
+
42
+ @flash = []
43
+ @pref = Config.new(@id_name)
44
+
45
+ @config[:ext_config] ||= {} # configuration used to instantiate JS class
46
+
47
+ process_permissions_config
48
+ end
49
+
50
+ def initial_config
51
+ {}
52
+ end
53
+
54
+ # 'Netzke::Grid' => 'Grid'
55
+ def short_widget_class_name
56
+ self.class.short_widget_class_name
57
+ end
58
+
59
+ def self.short_widget_class_name
60
+ name.split("::").last
61
+ end
62
+
63
+ #
64
+ # 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
65
+ #
66
+ def self.interface(*interface_points)
67
+ interfacep = read_inheritable_attribute(:interface_points) || []
68
+ interface_points.each{|p| interfacep << p}
69
+ write_inheritable_attribute(:interface_points, interfacep)
70
+
71
+ interface_points.each do |interfacep|
72
+ module_eval <<-END, __FILE__, __LINE__
73
+ def interface_#{interfacep}(*args)
74
+ #{interfacep}(*args).to_js
75
+ end
76
+ # FIXME: commented out because otherwise ColumnOperations stop working
77
+ # def #{interfacep}(*args)
78
+ # flash :warning => "API point '#{interfacep}' is not implemented for widget '#{short_widget_class_name}'"
79
+ # {:flash => @flash}
80
+ # end
81
+ END
82
+ end
83
+ end
84
+
85
+ def self.interface_points
86
+ read_inheritable_attribute(:interface_points)
87
+ end
88
+
89
+ def interface_points
90
+ self.class.interface_points
91
+ end
92
+
93
+ interface :get_widget # default
94
+
95
+ ## Dependencies
96
+ def dependencies
97
+ @dependencies ||= begin
98
+ non_late_aggregatees_widget_classes = aggregatees.values.map{|v| v[:widget_class_name]}
99
+ (initial_dependencies + non_late_aggregatees_widget_classes).uniq
100
+ end
101
+ end
102
+
103
+ # override this method if you need some extra dependencies, which are not the aggregatees
104
+ def initial_dependencies
105
+ []
106
+ end
107
+
108
+ ### Aggregation
109
+ def initial_aggregatees
110
+ {}
111
+ end
112
+
113
+ def aggregatees
114
+ @aggregatees ||= initial_aggregatees.merge(initial_late_aggregatees.each_pair{|k,v| v.merge!(:late_aggregation => true)})
115
+ end
116
+
117
+ def non_late_aggregatees
118
+ aggregatees.reject{|k,v| v[:late_aggregation]}
119
+ end
120
+
121
+ def add_aggregatee(aggr)
122
+ aggregatees.merge!(aggr)
123
+ end
124
+
125
+ # 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.
126
+ def initial_late_aggregatees
127
+ {}
128
+ end
129
+
130
+ def add_late_aggregatee(aggr)
131
+ aggregatees.merge!(aggr.merge(:late_aggregation => true))
132
+ end
133
+
134
+ # 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"
135
+ def aggregatee_instance(name)
136
+ aggregator = self
137
+ name.to_s.split('__').each do |aggr|
138
+ aggr = aggr.to_sym
139
+ # TODO: should we put all the classes under Netzke::-scope?
140
+ # widget_class = full_widget_class_name(aggregator.aggregatees[aggr][:widget_class_name]).constantize
141
+ widget_class = "Netzke::#{aggregator.aggregatees[aggr][:widget_class_name]}".constantize
142
+ aggregator = widget_class.new(aggregator.aggregatees[aggr].merge(:name => aggr), aggregator)
143
+ end
144
+ aggregator
145
+ end
146
+
147
+
148
+ def full_widget_class_name(short_name)
149
+ "Netzke::#{short_name}"
150
+ end
151
+
152
+ def flash(flash_hash)
153
+ level = flash_hash.keys.first
154
+ raise "Unknown message level for flash" unless %(notice warning error).include?(level.to_s)
155
+ @flash << {:level => level, :msg => flash_hash[level]}
156
+ end
157
+
158
+ def widget_action(action_name)
159
+ "#{@id_name}__#{action_name}"
160
+ end
161
+
162
+ # permissions
163
+ def available_permissions
164
+ []
165
+ end
166
+
167
+ def process_permissions_config
168
+ if !available_permissions.empty?
169
+ # First, process permissions from the config
170
+ @permissions = available_permissions.inject({}){|h,p| h.merge(p.to_sym => true)} # by default anything is allowed
171
+
172
+ config[:prohibit] = available_permissions if config[:prohibit] == :all # short-cut for all permissions
173
+ config[:prohibit] = [config[:prohibit]] if config[:prohibit].is_a?(Symbol) # so that config[:prohibit] => :write works
174
+ config[:prohibit] && config[:prohibit].each{|p| @permissions.merge!(p.to_sym => false)} # prohibit
175
+
176
+ config[:allow] = [config[:allow]] if config[:allow].is_a?(Symbol) # so that config[:allow] => :write works
177
+ config[:allow] && config[:allow].each{|p| @permissions.merge!(p.to_sym => true)} # allow
178
+
179
+ # ... and then merge it with NetzkePreferences (if not instantiated to only generate JS-class code)
180
+ !config[:js_class] && available_permissions.each do |p|
181
+ @permissions[p.to_sym] = @pref["permissions.#{p}"] if !@pref["permissions.#{p}"].nil?
182
+ end
183
+ end
184
+ end
185
+
186
+ ## method dispatcher - sends method to the proper aggregatee
187
+ def method_missing(method_name, params = {})
188
+ widget, *action = method_name.to_s.split('__')
189
+ widget = widget.to_sym
190
+ action = !action.empty? && action.join("__").to_sym
191
+
192
+ if action && aggregatees[widget]
193
+ # only actions starting with "interface_" are accessible
194
+ interface_action = action.to_s.index('__') ? action : "interface_#{action}"
195
+ aggregatee_instance(widget).send(interface_action, params)
196
+ else
197
+ super
198
+ end
199
+ end
200
+
201
+ #### API section
202
+ def get_widget(params = {})
203
+ # if browser does not have our component class cached (and all dependencies), send it to him
204
+ components_cache = (JSON.parse(params[:components_cache]) if params[:components_cache]) || []
205
+
206
+ {:config => js_config, :class_definition => js_missing_code(components_cache)}
207
+ end
208
+
209
+ end
210
+ end
@@ -0,0 +1,95 @@
1
+ module Netzke
2
+ module ControllerExtensions
3
+ def self.included(base)
4
+ base.extend ControllerClassMethods
5
+ end
6
+
7
+ def method_missing(method_name)
8
+ if self.class.widget_config_storage == {}
9
+ super
10
+ else
11
+ widget, *action = method_name.to_s.split('__')
12
+ widget = widget.to_sym
13
+ action = !action.empty? && action.join("__").to_sym
14
+
15
+ # only widget's actions starting with "interface_" are accessible from outside (security)
16
+ if action
17
+ interface_action = action.to_s.index('__') ? action : "interface_#{action}"
18
+
19
+ # widget module
20
+ widget_class = "Netzke::#{self.class.widget_config_storage[widget][:widget_class_name]}".constantize
21
+
22
+ # instantiate the server part of the widget
23
+ widget_instance = widget_class.new(self.class.widget_config_storage[widget].merge(:controller => self)) # OPTIMIZE: top-level widgets have access to the controller - can we avoid that?
24
+ render :text => widget_instance.send(interface_action, params)
25
+ end
26
+ end
27
+ end
28
+
29
+ module ControllerClassMethods
30
+
31
+ # widget_config_storage for all widgets
32
+ def widget_config_storage
33
+ @@widget_config_storage ||= {}
34
+ end
35
+
36
+ #
37
+ # Use this method to declare a widget in the controller
38
+ #
39
+ def netzke_widget(name, config={})
40
+ # which module is the widget?
41
+ config[:widget_class_name] ||= name.to_s.classify
42
+ config[:name] ||= name
43
+
44
+ # register the widget config in the storage
45
+ widget_config_storage[name] = config
46
+
47
+ # provide widget helpers
48
+ ActionView::Base.module_eval <<-END_EVAL, __FILE__, __LINE__
49
+ def #{name}_widget_instance(config = {})
50
+ # get the global config from the controller's singleton class
51
+ global_config = controller.class.widget_config_storage[:#{name}]
52
+
53
+ # when instantiating a client side instance, the configuration may be overwritten
54
+ # (but the server side will know nothing about it!)
55
+ local_config = global_config.merge(config)
56
+
57
+ # instantiate it
58
+ widget_instance = Netzke::#{config[:widget_class_name]}.new(local_config)
59
+
60
+ # return javascript code for instantiating on the javascript level
61
+ widget_instance.js_widget_instance
62
+ end
63
+
64
+ def #{name}_class_definition
65
+ result = ""
66
+ config = controller.class.widget_config_storage[:#{name}]
67
+ @@generated_widget_classes ||= []
68
+ # do not duplicate javascript code on the same page
69
+ unless @@generated_widget_classes.include?("#{config[:widget_class_name]}")
70
+ @@generated_widget_classes << "#{config[:widget_class_name]}"
71
+ result = Netzke::#{config[:widget_class_name]}.js_class_code
72
+ end
73
+ result
74
+ end
75
+ END_EVAL
76
+
77
+ # add controller action which will render a simple HTML page containing the widget
78
+ define_method("#{name}_test") do
79
+ @widget_name = name
80
+ render :inline => %Q(
81
+ <script type="text/javascript" charset="utf-8">
82
+ <%= #{name}_class_definition %>
83
+ Ext.onReady(function(){
84
+ <%= #{name}_widget_instance %>
85
+ #{name.to_js}.render("#{name.to_s.split('_').join('-')}");
86
+ })
87
+ </script>
88
+ <div id="#{name.to_s.split('_').join('-')}"></div>
89
+ ), :layout => "netzke"
90
+ end
91
+ end
92
+ end
93
+ end
94
+
95
+ end
@@ -0,0 +1,77 @@
1
+ class Hash
2
+
3
+ # Recursively convert the keys. Example:
4
+ # irb> {:bla_bla => 1, "wow_now" => {:look_ma => true}}.convert_keys{|k| k.camelize}
5
+ # irb> => {:BlaBla => 1, "WowNow" => {:LookMa => true}}
6
+ def convert_keys(&block)
7
+ block_given? ? self.inject({}) do |h,(k,v)|
8
+ h[k.is_a?(Symbol) ? yield(k.to_s).to_sym : yield(k.to_s)] = v.respond_to?('convert_keys') ? v.convert_keys(&block) : v
9
+ h
10
+ end : self
11
+ end
12
+
13
+ # First camelizes the keys, then convert the whole hash to JSON
14
+ def to_js
15
+ # self.delete_if{ |k,v| v.nil? } # we don't need to explicitely pass null values to javascript
16
+ self.recursive_delete_if_nil.convert_keys{|k| k.camelize(:lower)}.to_json
17
+ end
18
+
19
+ # Converts values to strings
20
+ def stringify_values!
21
+ self.each_pair{|k,v| self[k] = v.to_s if v.is_a?(Symbol)}
22
+ end
23
+
24
+ def recursive_delete_if_nil
25
+ self.inject({}) do |h,(k,v)|
26
+ if !v.nil?
27
+ h[k] = v.respond_to?('recursive_delete_if_nil') ? v.recursive_delete_if_nil : v
28
+ end
29
+ h
30
+ end
31
+ end
32
+
33
+ end
34
+
35
+ class Array
36
+ # Camelizes the keys of hashes and converts them to JSON
37
+ def to_js
38
+ self.recursive_delete_if_nil.map{|el| el.is_a?(Hash) ? el.convert_keys{|k| k.camelize(:lower)} : el}.to_json
39
+ end
40
+
41
+ # Applies convert_keys to each element which responds to convert_keys
42
+ def convert_keys(&block)
43
+ block_given? ? self.map do |i|
44
+ i.respond_to?('convert_keys') ? i.convert_keys(&block) : i
45
+ end : self
46
+ end
47
+
48
+ def recursive_delete_if_nil
49
+ self.map{|el| el.respond_to?('recursive_delete_if_nil') ? el.recursive_delete_if_nil : el}
50
+ end
51
+ end
52
+
53
+ class String
54
+ # Converts self to "literal JSON"-string - one that doesn't get quotes appended when being sent "to_json" method
55
+ def l
56
+ def self.to_json
57
+ self
58
+ end
59
+ self
60
+ end
61
+
62
+ def to_js
63
+ self.camelize(:lower)
64
+ end
65
+
66
+ # removes JS-comments (both single-line and multiple-line) from the string
67
+ def strip_js_comments
68
+ regexp = /\/\/.*$|(?m:\/\*.*?\*\/)/
69
+ self.gsub(regexp, '')
70
+ end
71
+ end
72
+
73
+ class Symbol
74
+ def to_js
75
+ self.to_s.camelize(:lower).to_sym
76
+ end
77
+ end
@@ -0,0 +1,114 @@
1
+ module Netzke
2
+ #
3
+ # Module which provides JS-class generation functionality for the widgets ("client-side"). This code is executed only once per widget class, and the results are cached at the server (unless widget specifies config[:no_caching] => true).
4
+ # Included into Netzke::Base class
5
+ # Most of the methods below are meant to be overwritten by a concrete widget class.
6
+ #
7
+ module JsClassBuilder
8
+ # the JS (Ext) class that we inherit from
9
+ def js_base_class; "Ext.Panel"; end
10
+
11
+ # widget's actions that are loaded at the moment of instantiating a widget
12
+ def actions; null; end
13
+
14
+ # widget's tools that are loaded at the moment of instantiating a widget see (js_config method)
15
+ def tools; []; end
16
+
17
+ def tbar; null; end
18
+
19
+ def bbar; null; end
20
+
21
+ # functions and properties that will be used to extend the functionality of (Ext) JS-class specified in js_base_class
22
+ def js_extend_properties; {}; end
23
+
24
+ # code executed before and after the constructor
25
+ def js_before_constructor; ""; end
26
+ def js_after_constructor; ""; end
27
+
28
+ # widget's listeners
29
+ def js_listeners; {}; end
30
+
31
+ # widget's menus
32
+ def js_menus; []; end
33
+
34
+ # items
35
+ def js_items; null; end
36
+
37
+ # default config that is passed into the constructor
38
+ def js_default_config
39
+ {
40
+ :title => short_widget_class_name,
41
+ :listeners => js_listeners,
42
+ :tools => "config.tools".l,
43
+ :actions => "config.actions".l,
44
+ :tbar => "config.tbar".l,
45
+ :bbar => "config.bbar".l,
46
+ :items => js_items,
47
+ :height => 400,
48
+ :width => 800,
49
+ :border => false
50
+ }
51
+ end
52
+
53
+ # declaration of widget's class (stored directly in the cache storage at the client side to be reused at the moment of widget instantiation)
54
+ def js_class
55
+ <<-JS
56
+ Ext.componentCache['#{short_widget_class_name}'] = Ext.extend(#{js_base_class}, Ext.chainApply([Ext.widgetMixIn, {
57
+ constructor: function(config){
58
+ this.widgetInit(config);
59
+ #{js_before_constructor}
60
+ Ext.componentCache['#{short_widget_class_name}'].superclass.constructor.call(this, Ext.apply(#{js_default_config.to_js}, config));
61
+ #{js_after_constructor}
62
+ this.setEvents();
63
+ this.addMenus(#{js_menus.to_js});
64
+ }
65
+ }, #{js_extend_properties.to_js}]))
66
+ JS
67
+ end
68
+
69
+ # generate instantiating - used when a widget is generated stand-alone (as a part of a HTML page)
70
+ def js_widget_instance
71
+ %Q(var #{config[:name].to_js} = new Ext.componentCache['#{short_widget_class_name}'](#{js_config.to_js});)
72
+ end
73
+
74
+ # the config that is send from the server and is used for instantiating a widget
75
+ def js_config
76
+ res = {}
77
+
78
+ # recursively include configs of all (non-late) aggregatees, so that the widget can instantiate them, too
79
+ aggregatees.each_pair do |aggr_name, aggr_config|
80
+ next if aggr_config[:late_aggregation]
81
+ res["#{aggr_name}_config".to_sym] = aggregatee_instance(aggr_name.to_sym).js_config
82
+ end
83
+
84
+ # interface
85
+ interface = interface_points.inject({}){|h,interfacep| h.merge(interfacep => widget_action(interfacep))}
86
+ res.merge!(:interface => interface)
87
+
88
+ res.merge!(:widget_class_name => short_widget_class_name)
89
+
90
+ res.merge!(config[:ext_config])
91
+ res.merge!(:id => @id_name)
92
+
93
+ # include tools and actions
94
+ res.merge!(:tools => tools)
95
+ res.merge!(:actions => actions)
96
+ res
97
+ end
98
+
99
+ # class definition of the widget plus that of all the dependencies, minus those that are specified as dependencies_to_exclude
100
+ def js_missing_code(cached_dependencies = [])
101
+ result = ""
102
+ dependencies.each do |dep_name|
103
+ dependency_class = "Netzke::#{dep_name}".constantize
104
+ result << dependency_class.js_class_code(cached_dependencies)
105
+ end
106
+ result << js_class.strip_js_comments unless cached_dependencies.include?(short_widget_class_name) && !config[:no_caching]
107
+ result
108
+ end
109
+
110
+ # little helpers
111
+ def this; "this".l; end
112
+ def null; "null".l; end
113
+ end
114
+ end