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
@@ -0,0 +1,258 @@
1
+ module Netzke
2
+ # == BaseJs
3
+ # *TODO: outdated*
4
+ #
5
+ # Module which provides JS-class generation functionality for the widgets ("client-side"). The generated code
6
+ # is evaluated once per widget class, and the results are cached in the browser. Included into Netzke::Base class.
7
+ #
8
+ # == Widget javascript code
9
+ # Here's a brief explanation on how a javascript class for a widget gets built.
10
+ # Widget gets defined as a constructor (a function) by +js_class+ class method (see "Inside widget's contstructor").
11
+ # +Ext.extend+ provides inheritance from an Ext class specified in +js_base_class+ class method.
12
+ #
13
+ # == Inside widget's constructor
14
+ # * Widget's constructor gets called with a parameter that is a configuration object provided by +js_config+ instance method. This configuration is specific for the instance of the widget, and, for example, contains this widget's unique id. As another example, by means of this configuration object, a grid receives the configuration array for its columns, a form - for its fields, etc. With other words, everything that may change from instance to instance of the same widget's class, goes in here.
15
+ # * Widget executes its specific initialization code which is provided by +js_before_consttructor+ class method.
16
+ # For example, a grid may define its column model, a form - its fields, a tab panel - its tabs ("items").
17
+ # * Widget calls the constructor of the inherited class (see +js_class+ class method) with a parameter that is a merge of
18
+ # 1) configuration parameter passed to the widget's constructor.
19
+ module BaseJs
20
+ def self.included(base)
21
+ base.extend ClassMethods
22
+ end
23
+
24
+ #
25
+ # Instance methods
26
+ #
27
+
28
+ # Config that is used for instantiating the widget in javascript
29
+ def js_config
30
+ res = {}
31
+
32
+ # Unique id of the widget
33
+ res.merge!(:id => id_name)
34
+
35
+ # Recursively include configs of all non-late aggregatees, so that the widget can instantiate them
36
+ # in javascript immediately.
37
+ non_late_aggregatees.each_pair do |aggr_name, aggr_config|
38
+ aggr_instance = aggregatee_instance(aggr_name.to_sym)
39
+ aggr_instance.before_load
40
+ res["#{aggr_name}_config".to_sym] = aggr_instance.js_config
41
+ end
42
+
43
+ # Api (besides the default "load_aggregatee_with_cache" - JavaScript side already knows about it)
44
+ res.merge!(:api => self.class.api_points.reject{ |p| p == :load_aggregatee_with_cache })
45
+
46
+ # Widget class name. Needed for dynamic instantiation in javascript.
47
+ res.merge!(:widget_class_name => short_widget_class_name)
48
+
49
+ # Actions, toolbars and menus
50
+ # tools && res.merge!(:tools => tools)
51
+ actions && res.merge!(:actions => actions)
52
+ menu && res.merge!(:menu => menu)
53
+
54
+ # Merge with all config options passed as hash to config[:ext_config]
55
+ res.merge!(ext_config)
56
+
57
+ res
58
+ end
59
+
60
+ # All the JS-code required by this instance of the widget to be instantiated in the browser.
61
+ # It includes the JS-class for the widget itself, as well as JS-classes for all widgets' (non-late) aggregatees.
62
+ def js_missing_code(cached_dependencies = [])
63
+ code = dependency_classes.inject("") do |r,k|
64
+ cached_dependencies.include?(k) ? r : r + "Netzke::#{k}".constantize.js_code(cached_dependencies).strip_js_comments
65
+ end
66
+ code.blank? ? nil : code
67
+ end
68
+
69
+ def css_missing_code(cached_dependencies = [])
70
+ code = dependency_classes.inject("") do |r,k|
71
+ cached_dependencies.include?(k) ? r : r + "Netzke::#{k}".constantize.css_code(cached_dependencies)
72
+ end
73
+ code.blank? ? nil : code
74
+ end
75
+
76
+ #
77
+ # The following methods are used when a widget is generated stand-alone (as a part of a HTML page)
78
+ #
79
+
80
+ # instantiating
81
+ def js_widget_instance
82
+ %Q{var #{name.jsonify} = new Ext.netzke.cache.#{short_widget_class_name}(#{js_config.to_nifty_json});}
83
+ end
84
+
85
+ # rendering
86
+ def js_widget_render
87
+ %Q{#{name.jsonify}.render("#{name.to_s.split('_').join('-')}");}
88
+ end
89
+
90
+ # container for rendering
91
+ def js_widget_html
92
+ %Q{<div id="#{name.to_s.split('_').join('-')}"></div>}
93
+ end
94
+
95
+ #
96
+ #
97
+ #
98
+
99
+ # Widget's actions, tools and menus that are loaded at the moment of instantiation
100
+ def actions; nil; end
101
+ def menu; nil; end
102
+ # def tools; nil; end
103
+
104
+ # Little helpers
105
+ def this; "this".l; end
106
+ def null; "null".l; end
107
+
108
+ # Methods used to create the javascript class (only once per widget class).
109
+ # The generated code gets cached at the browser, and the widget intstances (at the browser side)
110
+ # get instantiated from it.
111
+ # All these methods can be overwritten in case you want to extend the functionality of some pre-built widget
112
+ # instead of using it as is (using both would cause JS-code duplication)
113
+ module ClassMethods
114
+ # the JS (Ext) class that we inherit from on JS-level
115
+ def js_base_class
116
+ "Ext.Panel"
117
+ end
118
+
119
+ # functions and properties that will be used to extend the functionality of (Ext) JS-class specified in js_base_class
120
+ def js_extend_properties
121
+ {}
122
+ end
123
+
124
+ # widget's menus
125
+ def js_menus; []; end
126
+
127
+ # items
128
+ # def js_items; null; end
129
+
130
+ # are we using JS inheritance? for now, if js_base_class is a Netzke class - yes
131
+ def js_inheritance?
132
+ superclass != Netzke::Base
133
+ end
134
+
135
+ # Declaration of widget's class (stored in the cache storage (Ext.netzke.cache) at the client side
136
+ # to be reused at the moment of widget instantiation)
137
+ def js_class
138
+ if js_inheritance?
139
+ # In case of using javascript inheritance, little needs to be done
140
+ <<-END_OF_JAVASCRIPT
141
+ // Create the class
142
+ Ext.netzke.cache.#{short_widget_class_name} = function(config){
143
+ Ext.netzke.cache.#{short_widget_class_name}.superclass.constructor.call(this, config);
144
+ };
145
+ // Extend it with the class that we inherit from, and mix in js_extend_properties
146
+ Ext.extend(Ext.netzke.cache.#{short_widget_class_name}, Ext.netzke.cache.#{superclass.short_widget_class_name}, Ext.applyIf(#{js_extend_properties.to_nifty_json}, Ext.widgetMixIn));
147
+ END_OF_JAVASCRIPT
148
+ else
149
+ js_add_menus = "this.addMenus(#{js_menus.to_nifty_json});" unless js_menus.empty?
150
+ <<-END_OF_JAVASCRIPT
151
+ // Constructor
152
+ Ext.netzke.cache.#{short_widget_class_name} = function(config){
153
+ // Do all the initializations that every Netzke widget should do: create methods for API-points,
154
+ // process actions, tools, toolbars
155
+ this.commonBeforeConstructor(config);
156
+
157
+ // Call the constructor of the inherited class
158
+ Ext.netzke.cache.#{short_widget_class_name}.superclass.constructor.call(this, config);
159
+
160
+ // What every widget should do after calling the constructor of the inherited class, like
161
+ // setting extra events
162
+ this.commonAfterConstructor(config);
163
+ };
164
+ Ext.extend(Ext.netzke.cache.#{short_widget_class_name}, #{js_base_class}, Ext.applyIf(#{js_extend_properties.to_nifty_json}, Ext.widgetMixIn));
165
+ END_OF_JAVASCRIPT
166
+ end
167
+ end
168
+
169
+ def css_include(*args)
170
+ included_css = read_inheritable_attribute(:included_css) || []
171
+ args.each do |inclusion|
172
+ if inclusion.is_a?(Hash)
173
+ # we are signalized a non-default file location (e.g. Ext examples)
174
+ case inclusion.keys.first
175
+ when :ext_examples
176
+ location = Netzke::Base.config[:ext_location] + "/examples/"
177
+ end
178
+ files = inclusion.values.first
179
+ else
180
+ location = ""
181
+ files = inclusion
182
+ end
183
+
184
+ files = [files] if files.is_a?(String)
185
+
186
+ for f in files
187
+ included_css << location + f
188
+ end
189
+ end
190
+ write_inheritable_attribute(:included_css, included_css)
191
+ end
192
+
193
+ # all JS code needed for this class, including one from the ancestor widget
194
+ def js_code(cached_dependencies = [])
195
+ res = ""
196
+
197
+ # include the base-class javascript if doing JS inheritance
198
+ res << superclass.js_code << "\n" if js_inheritance? && !cached_dependencies.include?(superclass.short_widget_class_name)
199
+
200
+ # include static javascripts
201
+ res << js_included << "\n"
202
+
203
+ # our own JS class definition
204
+ res << js_class
205
+ res
206
+ end
207
+
208
+
209
+ # Override this method. Must return an array of paths to javascript files that we depend on.
210
+ # This javascript code will be loaded along with the widget's class, and before it.
211
+ def include_js
212
+ []
213
+ end
214
+
215
+ # returns all extra js-code (as string) required by this widget's class
216
+ def js_included
217
+ res = ""
218
+
219
+ include_js.each do |path|
220
+ f = File.new(path)
221
+ res << f.read << "\n"
222
+ end
223
+
224
+ res
225
+ end
226
+
227
+
228
+ # returns all extra js-code (as string) required by this widget's class
229
+ def css_included
230
+ res = ""
231
+
232
+ included_css = read_inheritable_attribute(:included_css) || []
233
+ res << included_css.inject("") do |r, path|
234
+ f = File.new(path)
235
+ r << f.read
236
+ end
237
+
238
+ res
239
+ end
240
+
241
+ # all JS code needed for this class including the one from the ancestor widget
242
+ def css_code(cached_dependencies = [])
243
+ res = ""
244
+
245
+ # include the base-class javascript if doing JS inheritance
246
+ res << superclass.css_code << "\n" if js_inheritance? && !cached_dependencies.include?(superclass.short_widget_class_name)
247
+
248
+ res << css_included << "\n"
249
+
250
+ res
251
+ end
252
+
253
+ def this; "this".l; end
254
+ def null; "null".l; end
255
+
256
+ end
257
+ end
258
+ end
@@ -2,6 +2,29 @@ module Netzke
2
2
  module ControllerExtensions
3
3
  def self.included(base)
4
4
  base.extend ControllerClassMethods
5
+ base.send(:before_filter, :set_session_data)
6
+ end
7
+
8
+ def set_session_data
9
+ Netzke::Base.session = session
10
+ session[:netzke_user_id] = defined?(current_user) ? current_user.try(:id) : nil
11
+
12
+ Netzke::Base.user = current_user # for backward compatibility (TODO: eliminate the need for this)
13
+
14
+ # set netzke_just_logged_in and netzke_just_logged_out states (may be used by Netzke widgets)
15
+ if session[:_netzke_next_request_is_first_after_login]
16
+ session[:netzke_just_logged_in] = true
17
+ session[:_netzke_next_request_is_first_after_login] = false
18
+ else
19
+ session[:netzke_just_logged_in] = false
20
+ end
21
+
22
+ if session[:_netzke_next_request_is_first_after_logout]
23
+ session[:netzke_just_logged_out] = true
24
+ session[:_netzke_next_request_is_first_after_logout] = false
25
+ else
26
+ session[:netzke_just_logged_out] = false
27
+ end
5
28
  end
6
29
 
7
30
  def method_missing(method_name)
@@ -12,16 +35,19 @@ module Netzke
12
35
  widget = widget.to_sym
13
36
  action = !action.empty? && action.join("__").to_sym
14
37
 
15
- # only widget's actions starting with "interface_" are accessible from outside (security)
38
+ # only widget's actions starting with "api_" are accessible from outside (security)
16
39
  if action
17
- interface_action = action.to_s.index('__') ? action : "interface_#{action}"
40
+ api_action = action.to_s.index('__') ? action : "api_#{action}"
18
41
 
19
42
  # widget module
20
43
  widget_class = "Netzke::#{self.class.widget_config_storage[widget][:widget_class_name]}".constantize
21
44
 
22
45
  # 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)
46
+ widget_instance = widget_class.new(self.class.widget_config_storage[widget])
47
+ # (OLD VERSION)
48
+ # 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?
49
+
50
+ render :text => widget_instance.send(api_action, params)
25
51
  end
26
52
  end
27
53
  end
@@ -31,12 +57,13 @@ module Netzke
31
57
  # widget_config_storage for all widgets
32
58
  def widget_config_storage
33
59
  @@widget_config_storage ||= {}
60
+ @@widget_config_storage[self.name] ||= {} # specific for controller
34
61
  end
35
62
 
36
63
  #
37
64
  # Use this method to declare a widget in the controller
38
65
  #
39
- def netzke_widget(name, config={})
66
+ def netzke(name, config={})
40
67
  # which module is the widget?
41
68
  config[:widget_class_name] ||= name.to_s.classify
42
69
  config[:name] ||= name
@@ -46,39 +73,63 @@ module Netzke
46
73
 
47
74
  # provide widget helpers
48
75
  ActionView::Base.module_eval <<-END_EVAL, __FILE__, __LINE__
76
+ def #{name}_server_instance(config = {})
77
+ default_config = controller.class.widget_config_storage[:#{name}]
78
+ if config.empty?
79
+ # only cache when the config is empty (which means that config specified in controller is used)
80
+ @widget_instance_cache ||= {}
81
+ @widget_instance_cache[:#{name}] ||= Netzke::Base.instance_by_config(default_config)
82
+ else
83
+ # if helper is called with parameters - always return a fresh instance of widget, no caching
84
+ Netzke::Base.instance_by_config(default_config.deep_merge(config))
85
+ end
86
+ end
87
+
49
88
  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
89
+ #{name}_server_instance(config).js_widget_instance
62
90
  end
63
91
 
64
92
  def #{name}_class_definition
65
- config = controller.class.widget_config_storage[:#{name}]
66
- Netzke::#{config[:widget_class_name]}.new(config).js_missing_code
93
+ @generated_widget_classes ||= []
94
+ res = #{name}_server_instance.js_missing_code(@generated_widget_classes)
95
+
96
+ # prevent duplication of javascript when multiple homogeneous widgets are on the same page
97
+ @generated_widget_classes += #{name}_server_instance.dependencies
98
+ @generated_widget_classes.uniq!
99
+ res
67
100
  end
101
+
102
+ def #{name}_widget_html
103
+ #{name}_server_instance.js_widget_html
104
+ end
105
+
106
+ def #{name}_widget_render
107
+ #{name}_server_instance.js_widget_render
108
+ end
109
+
68
110
  END_EVAL
69
111
 
70
112
  # add controller action which will render a simple HTML page containing the widget
71
113
  define_method("#{name}_test") do
72
- render :inline => %Q(
73
- <script type="text/javascript" charset="utf-8">
74
- <%= #{name}_class_definition %>
75
- Ext.onReady(function(){
76
- <%= #{name}_widget_instance %>
77
- #{name.to_js}.render("#{name.to_s.split('_').join('-')}");
78
- })
79
- </script>
80
- <div id="#{name.to_s.split('_').join('-')}"></div>
81
- ), :layout => "netzke"
114
+ @widget_name = name
115
+ render :inline => <<-HTML
116
+ <head>
117
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8">
118
+ <title><%= @widget_name %></title>
119
+ <%= netzke_js_include %>
120
+ <%= netzke_css_include %>
121
+ <script type="text/javascript" charset="utf-8">
122
+ <%= #{name}_class_definition %>
123
+ Ext.onReady(function(){
124
+ <%= #{name}_widget_instance %>
125
+ <%= #{name}_widget_render %>
126
+ });
127
+ </script>
128
+ </head>
129
+ <body>
130
+ <%= #{name}_widget_html %>
131
+ </body>
132
+ HTML
82
133
  end
83
134
  end
84
135
  end
@@ -9,18 +9,29 @@ class Hash
9
9
  h
10
10
  end : self
11
11
  end
12
+
13
+ def jsonify
14
+ self.inject({}) do |h,(k,v)|
15
+ new_key = k.instance_of?(String) || k.instance_of?(Symbol) ? k.jsonify : k
16
+ new_value = v.instance_of?(Array) || v.instance_of?(Hash) ? v.jsonify : v
17
+ h.merge(new_key => new_value)
18
+ end
19
+ end
12
20
 
13
21
  # 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
+ def to_nifty_json
23
+ self.recursive_delete_if_nil.jsonify.to_json
22
24
  end
23
25
 
26
+ # Converts values of a Hash in such a way that they can be easily stored in the database: hashes and arrays are jsonified, symbols - stringified
27
+ def deebeefy_values
28
+ inject({}) do |options, (k, v)|
29
+ options[k] = v.is_a?(Symbol) ? v.to_s : (v.is_a?(Hash) || v.is_a?(Array)) ? v.to_json : v
30
+ options
31
+ end
32
+ end
33
+
34
+ # We don't need to pass null values in JSON, they are null by simply being absent
24
35
  def recursive_delete_if_nil
25
36
  self.inject({}) do |h,(k,v)|
26
37
  if !v.nil?
@@ -29,13 +40,29 @@ class Hash
29
40
  h
30
41
  end
31
42
  end
43
+
44
+ # Javascrit-like access to Hash values
45
+ def method_missing(method, *args)
46
+ if method.to_s =~ /=$/
47
+ method_base = method.to_s.sub(/=$/,'').to_sym
48
+ key = self[method_base.to_s].nil? ? method_base : method_base.to_s
49
+ self[key] = args.first
50
+ else
51
+ key = self[method.to_s].nil? ? method : method.to_s
52
+ self[key]
53
+ end
54
+ end
32
55
 
33
56
  end
34
57
 
35
58
  class Array
59
+ def jsonify
60
+ self.map{ |el| el.instance_of?(Array) || el.instance_of?(Hash) ? el.jsonify : el }
61
+ end
62
+
36
63
  # 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
64
+ def to_nifty_json
65
+ self.recursive_delete_if_nil.jsonify.to_json
39
66
  end
40
67
 
41
68
  # Applies convert_keys to each element which responds to convert_keys
@@ -50,28 +77,52 @@ class Array
50
77
  end
51
78
  end
52
79
 
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
80
+ class LiteralString < String
81
+ def to_json(*args)
59
82
  self
60
83
  end
61
-
62
- def to_js
84
+ end
85
+
86
+ class String
87
+ def jsonify
63
88
  self.camelize(:lower)
64
89
  end
65
90
 
66
- # removes JS-comments (both single-line and multiple-line) from the string
91
+ # Converts self to "literal JSON"-string - one that doesn't get quotes appended when being sent "to_json" method
92
+ def l
93
+ LiteralString.new(self)
94
+ end
95
+
96
+ # removes JS-comments (both single- and multi-line) from the string
67
97
  def strip_js_comments
68
98
  regexp = /\/\/.*$|(?m:\/\*.*?\*\/)/
69
- self.gsub(regexp, '')
99
+ self.gsub!(regexp, '')
100
+
101
+ # also remove empty lines
102
+ regexp = /^\s*\n/
103
+ self.gsub!(regexp, '')
104
+ end
105
+
106
+ # "false" => false, "whatever_else" => true
107
+ def to_b
108
+ self != "false"
70
109
  end
71
110
  end
72
111
 
73
112
  class Symbol
74
- def to_js
113
+ def jsonify
75
114
  self.to_s.camelize(:lower).to_sym
76
115
  end
116
+
117
+ def l
118
+ LiteralString.new(self.to_s)
119
+ end
120
+ end
121
+
122
+ module ActiveSupport
123
+ class TimeWithZone
124
+ def to_json(options = {})
125
+ self.to_s(:db).to_json
126
+ end
127
+ end
77
128
  end