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.
- data/CHANGELOG +108 -0
- data/LICENSE +2 -19
- data/Manifest +50 -0
- data/README.rdoc +12 -0
- data/Rakefile +2 -3
- data/TODO +2 -0
- data/autotest/discover.rb +3 -0
- data/generators/netzke_core/netzke_core_generator.rb +6 -6
- data/generators/netzke_core/templates/create_netzke_preferences.rb +2 -2
- data/javascripts/core.js +474 -111
- data/lib/app/controllers/netzke_controller.rb +76 -0
- data/lib/app/models/netzke_preference.rb +128 -39
- data/lib/netzke-core.rb +23 -9
- data/lib/netzke/action_view_ext.rb +26 -0
- data/lib/netzke/base.rb +440 -102
- data/lib/netzke/base_js.rb +258 -0
- data/lib/netzke/controller_extensions.rb +80 -29
- data/lib/netzke/core_ext.rb +72 -21
- data/lib/netzke/feedback_ghost.rb +43 -0
- data/lib/netzke/routing.rb +9 -0
- data/netzke-core.gemspec +10 -11
- data/stylesheets/core.css +12 -0
- data/test/app_root/app/controllers/{application.rb → application_controller.rb} +0 -0
- data/test/app_root/app/models/role.rb +3 -0
- data/test/app_root/app/models/user.rb +3 -0
- data/test/app_root/config/boot.rb +2 -1
- data/test/app_root/config/database.yml +10 -0
- data/test/app_root/config/environment.rb +1 -0
- data/test/app_root/db/migrate/20081222035855_create_netzke_preferences.rb +18 -0
- data/test/app_root/db/migrate/20090423214303_create_roles.rb +11 -0
- data/test/app_root/db/migrate/20090423222114_create_users.rb +12 -0
- data/test/app_root/lib/console_with_fixtures.rb +4 -0
- data/test/app_root/script/console +1 -0
- data/test/fixtures/roles.yml +7 -0
- data/test/fixtures/users.yml +9 -0
- data/test/test_helper.rb +3 -2
- data/test/unit/core_ext_test.rb +66 -0
- data/test/unit/netzke_core_test.rb +167 -0
- data/test/unit/netzke_preference_test.rb +103 -0
- metadata +45 -30
- data/README.mdown +0 -11
- data/generators/netzke_core/templates/create_netzke_layouts.rb +0 -14
- data/generators/netzke_core/templates/netzke.html.erb +0 -10
- data/lib/app/models/netzke_layout.rb +0 -75
- data/lib/netzke/js_class_builder.rb +0 -114
- data/lib/vendor/facets/hash/recursive_merge.rb +0 -28
- data/test/core_ext_test.rb +0 -35
- 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
|
-
|
3
|
-
|
4
|
-
def self.user=(user)
|
5
|
-
@@user = user
|
6
|
-
end
|
9
|
+
belongs_to :user
|
10
|
+
belongs_to :role
|
7
11
|
|
8
|
-
|
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.
|
13
|
-
@@
|
14
|
+
def self.widget_name=(value)
|
15
|
+
@@widget_name = value
|
14
16
|
end
|
15
17
|
|
16
|
-
def self.
|
17
|
-
@@
|
18
|
+
def self.widget_name
|
19
|
+
@@widget_name ||= nil
|
18
20
|
end
|
19
21
|
|
20
22
|
def normalized_value
|
21
|
-
klass
|
23
|
+
klass = read_attribute(:pref_type)
|
22
24
|
norm_value = read_attribute(:value)
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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(
|
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
|
-
|
40
|
-
|
41
|
-
when "
|
42
|
-
|
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
|
51
|
-
|
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
|
59
|
-
|
60
|
-
|
61
|
-
#
|
62
|
-
|
63
|
-
|
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
|
data/lib/netzke-core.rb
CHANGED
@@ -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
|
-
|
8
|
-
require '
|
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
|
19
|
-
|
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
|
-
|
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
|
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
|
+
|
data/lib/netzke/base.rb
CHANGED
@@ -1,100 +1,362 @@
|
|
1
|
-
require '
|
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
|
-
#
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
-
|
17
|
-
|
18
|
-
|
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
|
-
|
21
|
-
|
22
|
-
|
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
|
-
|
30
|
-
|
77
|
+
# Multi-user support (deprecated in favor of controller sessions)
|
78
|
+
def user
|
79
|
+
@@user ||= nil
|
80
|
+
end
|
31
81
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|
-
|
39
|
-
|
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
|
-
|
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
|
-
|
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
|
-
#
|
51
|
-
|
52
|
-
|
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
|
56
|
-
|
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
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
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
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|
-
#
|
73
|
-
#
|
74
|
-
#
|
75
|
-
|
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
|
-
|
82
|
-
|
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
|
86
|
-
|
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
|
-
|
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 ||=
|
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
|
-
|
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
|
-
|
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
|
-
|
128
|
-
#
|
129
|
-
|
130
|
-
|
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
|
156
|
-
|
157
|
-
|
158
|
-
|
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
|
-
|
161
|
-
|
162
|
-
|
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
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
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
|
-
|
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
|
181
|
-
|
182
|
-
|
183
|
-
|
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
|