##// END OF EJS Templates
Use Object#tap instead of #returning (#6887)....
Jean-Philippe Lang -
r4292:2ee45e8cac90
parent child
Show More
@@ -1,152 +1,152
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 module Redmine
18 module Redmine
19 module Hook
19 module Hook
20 include ActionController::UrlWriter
20 include ActionController::UrlWriter
21
21
22 @@listener_classes = []
22 @@listener_classes = []
23 @@listeners = nil
23 @@listeners = nil
24 @@hook_listeners = {}
24 @@hook_listeners = {}
25
25
26 class << self
26 class << self
27 # Adds a listener class.
27 # Adds a listener class.
28 # Automatically called when a class inherits from Redmine::Hook::Listener.
28 # Automatically called when a class inherits from Redmine::Hook::Listener.
29 def add_listener(klass)
29 def add_listener(klass)
30 raise "Hooks must include Singleton module." unless klass.included_modules.include?(Singleton)
30 raise "Hooks must include Singleton module." unless klass.included_modules.include?(Singleton)
31 @@listener_classes << klass
31 @@listener_classes << klass
32 clear_listeners_instances
32 clear_listeners_instances
33 end
33 end
34
34
35 # Returns all the listerners instances.
35 # Returns all the listerners instances.
36 def listeners
36 def listeners
37 @@listeners ||= @@listener_classes.collect {|listener| listener.instance}
37 @@listeners ||= @@listener_classes.collect {|listener| listener.instance}
38 end
38 end
39
39
40 # Returns the listeners instances for the given hook.
40 # Returns the listeners instances for the given hook.
41 def hook_listeners(hook)
41 def hook_listeners(hook)
42 @@hook_listeners[hook] ||= listeners.select {|listener| listener.respond_to?(hook)}
42 @@hook_listeners[hook] ||= listeners.select {|listener| listener.respond_to?(hook)}
43 end
43 end
44
44
45 # Clears all the listeners.
45 # Clears all the listeners.
46 def clear_listeners
46 def clear_listeners
47 @@listener_classes = []
47 @@listener_classes = []
48 clear_listeners_instances
48 clear_listeners_instances
49 end
49 end
50
50
51 # Clears all the listeners instances.
51 # Clears all the listeners instances.
52 def clear_listeners_instances
52 def clear_listeners_instances
53 @@listeners = nil
53 @@listeners = nil
54 @@hook_listeners = {}
54 @@hook_listeners = {}
55 end
55 end
56
56
57 # Calls a hook.
57 # Calls a hook.
58 # Returns the listeners response.
58 # Returns the listeners response.
59 def call_hook(hook, context={})
59 def call_hook(hook, context={})
60 returning [] do |response|
60 [].tap do |response|
61 hls = hook_listeners(hook)
61 hls = hook_listeners(hook)
62 if hls.any?
62 if hls.any?
63 hls.each {|listener| response << listener.send(hook, context)}
63 hls.each {|listener| response << listener.send(hook, context)}
64 end
64 end
65 end
65 end
66 end
66 end
67 end
67 end
68
68
69 # Base class for hook listeners.
69 # Base class for hook listeners.
70 class Listener
70 class Listener
71 include Singleton
71 include Singleton
72 include Redmine::I18n
72 include Redmine::I18n
73
73
74 # Registers the listener
74 # Registers the listener
75 def self.inherited(child)
75 def self.inherited(child)
76 Redmine::Hook.add_listener(child)
76 Redmine::Hook.add_listener(child)
77 super
77 super
78 end
78 end
79
79
80 end
80 end
81
81
82 # Listener class used for views hooks.
82 # Listener class used for views hooks.
83 # Listeners that inherit this class will include various helpers by default.
83 # Listeners that inherit this class will include various helpers by default.
84 class ViewListener < Listener
84 class ViewListener < Listener
85 include ERB::Util
85 include ERB::Util
86 include ActionView::Helpers::TagHelper
86 include ActionView::Helpers::TagHelper
87 include ActionView::Helpers::FormHelper
87 include ActionView::Helpers::FormHelper
88 include ActionView::Helpers::FormTagHelper
88 include ActionView::Helpers::FormTagHelper
89 include ActionView::Helpers::FormOptionsHelper
89 include ActionView::Helpers::FormOptionsHelper
90 include ActionView::Helpers::JavaScriptHelper
90 include ActionView::Helpers::JavaScriptHelper
91 include ActionView::Helpers::PrototypeHelper
91 include ActionView::Helpers::PrototypeHelper
92 include ActionView::Helpers::NumberHelper
92 include ActionView::Helpers::NumberHelper
93 include ActionView::Helpers::UrlHelper
93 include ActionView::Helpers::UrlHelper
94 include ActionView::Helpers::AssetTagHelper
94 include ActionView::Helpers::AssetTagHelper
95 include ActionView::Helpers::TextHelper
95 include ActionView::Helpers::TextHelper
96 include ActionController::UrlWriter
96 include ActionController::UrlWriter
97 include ApplicationHelper
97 include ApplicationHelper
98
98
99 # Default to creating links using only the path. Subclasses can
99 # Default to creating links using only the path. Subclasses can
100 # change this default as needed
100 # change this default as needed
101 def self.default_url_options
101 def self.default_url_options
102 {:only_path => true }
102 {:only_path => true }
103 end
103 end
104
104
105 # Helper method to directly render a partial using the context:
105 # Helper method to directly render a partial using the context:
106 #
106 #
107 # class MyHook < Redmine::Hook::ViewListener
107 # class MyHook < Redmine::Hook::ViewListener
108 # render_on :view_issues_show_details_bottom, :partial => "show_more_data"
108 # render_on :view_issues_show_details_bottom, :partial => "show_more_data"
109 # end
109 # end
110 #
110 #
111 def self.render_on(hook, options={})
111 def self.render_on(hook, options={})
112 define_method hook do |context|
112 define_method hook do |context|
113 context[:controller].send(:render_to_string, {:locals => context}.merge(options))
113 context[:controller].send(:render_to_string, {:locals => context}.merge(options))
114 end
114 end
115 end
115 end
116 end
116 end
117
117
118 # Helper module included in ApplicationHelper and ActionControllerso that
118 # Helper module included in ApplicationHelper and ActionControllerso that
119 # hooks can be called in views like this:
119 # hooks can be called in views like this:
120 #
120 #
121 # <%= call_hook(:some_hook) %>
121 # <%= call_hook(:some_hook) %>
122 # <%= call_hook(:another_hook, :foo => 'bar' %>
122 # <%= call_hook(:another_hook, :foo => 'bar' %>
123 #
123 #
124 # Or in controllers like:
124 # Or in controllers like:
125 # call_hook(:some_hook)
125 # call_hook(:some_hook)
126 # call_hook(:another_hook, :foo => 'bar'
126 # call_hook(:another_hook, :foo => 'bar'
127 #
127 #
128 # Hooks added to views will be concatenated into a string. Hooks added to
128 # Hooks added to views will be concatenated into a string. Hooks added to
129 # controllers will return an array of results.
129 # controllers will return an array of results.
130 #
130 #
131 # Several objects are automatically added to the call context:
131 # Several objects are automatically added to the call context:
132 #
132 #
133 # * project => current project
133 # * project => current project
134 # * request => Request instance
134 # * request => Request instance
135 # * controller => current Controller instance
135 # * controller => current Controller instance
136 #
136 #
137 module Helper
137 module Helper
138 def call_hook(hook, context={})
138 def call_hook(hook, context={})
139 if is_a?(ActionController::Base)
139 if is_a?(ActionController::Base)
140 default_context = {:controller => self, :project => @project, :request => request}
140 default_context = {:controller => self, :project => @project, :request => request}
141 Redmine::Hook.call_hook(hook, default_context.merge(context))
141 Redmine::Hook.call_hook(hook, default_context.merge(context))
142 else
142 else
143 default_context = {:controller => controller, :project => @project, :request => request}
143 default_context = {:controller => controller, :project => @project, :request => request}
144 Redmine::Hook.call_hook(hook, default_context.merge(context)).join(' ')
144 Redmine::Hook.call_hook(hook, default_context.merge(context)).join(' ')
145 end
145 end
146 end
146 end
147 end
147 end
148 end
148 end
149 end
149 end
150
150
151 ApplicationHelper.send(:include, Redmine::Hook::Helper)
151 ApplicationHelper.send(:include, Redmine::Hook::Helper)
152 ActionController::Base.send(:include, Redmine::Hook::Helper)
152 ActionController::Base.send(:include, Redmine::Hook::Helper)
@@ -1,448 +1,448
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'tree' # gem install rubytree
18 require 'tree' # gem install rubytree
19
19
20 # Monkey patch the TreeNode to add on a few more methods :nodoc:
20 # Monkey patch the TreeNode to add on a few more methods :nodoc:
21 module TreeNodePatch
21 module TreeNodePatch
22 def self.included(base)
22 def self.included(base)
23 base.class_eval do
23 base.class_eval do
24 attr_reader :last_items_count
24 attr_reader :last_items_count
25
25
26 alias :old_initilize :initialize
26 alias :old_initilize :initialize
27 def initialize(name, content = nil)
27 def initialize(name, content = nil)
28 old_initilize(name, content)
28 old_initilize(name, content)
29 @last_items_count = 0
29 @last_items_count = 0
30 extend(InstanceMethods)
30 extend(InstanceMethods)
31 end
31 end
32 end
32 end
33 end
33 end
34
34
35 module InstanceMethods
35 module InstanceMethods
36 # Adds the specified child node to the receiver node. The child node's
36 # Adds the specified child node to the receiver node. The child node's
37 # parent is set to be the receiver. The child is added as the first child in
37 # parent is set to be the receiver. The child is added as the first child in
38 # the current list of children for the receiver node.
38 # the current list of children for the receiver node.
39 def prepend(child)
39 def prepend(child)
40 raise "Child already added" if @childrenHash.has_key?(child.name)
40 raise "Child already added" if @childrenHash.has_key?(child.name)
41
41
42 @childrenHash[child.name] = child
42 @childrenHash[child.name] = child
43 @children = [child] + @children
43 @children = [child] + @children
44 child.parent = self
44 child.parent = self
45 return child
45 return child
46
46
47 end
47 end
48
48
49 # Adds the specified child node to the receiver node. The child node's
49 # Adds the specified child node to the receiver node. The child node's
50 # parent is set to be the receiver. The child is added at the position
50 # parent is set to be the receiver. The child is added at the position
51 # into the current list of children for the receiver node.
51 # into the current list of children for the receiver node.
52 def add_at(child, position)
52 def add_at(child, position)
53 raise "Child already added" if @childrenHash.has_key?(child.name)
53 raise "Child already added" if @childrenHash.has_key?(child.name)
54
54
55 @childrenHash[child.name] = child
55 @childrenHash[child.name] = child
56 @children = @children.insert(position, child)
56 @children = @children.insert(position, child)
57 child.parent = self
57 child.parent = self
58 return child
58 return child
59
59
60 end
60 end
61
61
62 def add_last(child)
62 def add_last(child)
63 raise "Child already added" if @childrenHash.has_key?(child.name)
63 raise "Child already added" if @childrenHash.has_key?(child.name)
64
64
65 @childrenHash[child.name] = child
65 @childrenHash[child.name] = child
66 @children << child
66 @children << child
67 @last_items_count += 1
67 @last_items_count += 1
68 child.parent = self
68 child.parent = self
69 return child
69 return child
70
70
71 end
71 end
72
72
73 # Adds the specified child node to the receiver node. The child node's
73 # Adds the specified child node to the receiver node. The child node's
74 # parent is set to be the receiver. The child is added as the last child in
74 # parent is set to be the receiver. The child is added as the last child in
75 # the current list of children for the receiver node.
75 # the current list of children for the receiver node.
76 def add(child)
76 def add(child)
77 raise "Child already added" if @childrenHash.has_key?(child.name)
77 raise "Child already added" if @childrenHash.has_key?(child.name)
78
78
79 @childrenHash[child.name] = child
79 @childrenHash[child.name] = child
80 position = @children.size - @last_items_count
80 position = @children.size - @last_items_count
81 @children.insert(position, child)
81 @children.insert(position, child)
82 child.parent = self
82 child.parent = self
83 return child
83 return child
84
84
85 end
85 end
86
86
87 # Wrapp remove! making sure to decrement the last_items counter if
87 # Wrapp remove! making sure to decrement the last_items counter if
88 # the removed child was a last item
88 # the removed child was a last item
89 def remove!(child)
89 def remove!(child)
90 @last_items_count -= +1 if child && child.last
90 @last_items_count -= +1 if child && child.last
91 super
91 super
92 end
92 end
93
93
94
94
95 # Will return the position (zero-based) of the current child in
95 # Will return the position (zero-based) of the current child in
96 # it's parent
96 # it's parent
97 def position
97 def position
98 self.parent.children.index(self)
98 self.parent.children.index(self)
99 end
99 end
100 end
100 end
101 end
101 end
102 Tree::TreeNode.send(:include, TreeNodePatch)
102 Tree::TreeNode.send(:include, TreeNodePatch)
103
103
104 module Redmine
104 module Redmine
105 module MenuManager
105 module MenuManager
106 class MenuError < StandardError #:nodoc:
106 class MenuError < StandardError #:nodoc:
107 end
107 end
108
108
109 module MenuController
109 module MenuController
110 def self.included(base)
110 def self.included(base)
111 base.extend(ClassMethods)
111 base.extend(ClassMethods)
112 end
112 end
113
113
114 module ClassMethods
114 module ClassMethods
115 @@menu_items = Hash.new {|hash, key| hash[key] = {:default => key, :actions => {}}}
115 @@menu_items = Hash.new {|hash, key| hash[key] = {:default => key, :actions => {}}}
116 mattr_accessor :menu_items
116 mattr_accessor :menu_items
117
117
118 # Set the menu item name for a controller or specific actions
118 # Set the menu item name for a controller or specific actions
119 # Examples:
119 # Examples:
120 # * menu_item :tickets # => sets the menu name to :tickets for the whole controller
120 # * menu_item :tickets # => sets the menu name to :tickets for the whole controller
121 # * menu_item :tickets, :only => :list # => sets the menu name to :tickets for the 'list' action only
121 # * menu_item :tickets, :only => :list # => sets the menu name to :tickets for the 'list' action only
122 # * menu_item :tickets, :only => [:list, :show] # => sets the menu name to :tickets for 2 actions only
122 # * menu_item :tickets, :only => [:list, :show] # => sets the menu name to :tickets for 2 actions only
123 #
123 #
124 # The default menu item name for a controller is controller_name by default
124 # The default menu item name for a controller is controller_name by default
125 # Eg. the default menu item name for ProjectsController is :projects
125 # Eg. the default menu item name for ProjectsController is :projects
126 def menu_item(id, options = {})
126 def menu_item(id, options = {})
127 if actions = options[:only]
127 if actions = options[:only]
128 actions = [] << actions unless actions.is_a?(Array)
128 actions = [] << actions unless actions.is_a?(Array)
129 actions.each {|a| menu_items[controller_name.to_sym][:actions][a.to_sym] = id}
129 actions.each {|a| menu_items[controller_name.to_sym][:actions][a.to_sym] = id}
130 else
130 else
131 menu_items[controller_name.to_sym][:default] = id
131 menu_items[controller_name.to_sym][:default] = id
132 end
132 end
133 end
133 end
134 end
134 end
135
135
136 def menu_items
136 def menu_items
137 self.class.menu_items
137 self.class.menu_items
138 end
138 end
139
139
140 # Returns the menu item name according to the current action
140 # Returns the menu item name according to the current action
141 def current_menu_item
141 def current_menu_item
142 @current_menu_item ||= menu_items[controller_name.to_sym][:actions][action_name.to_sym] ||
142 @current_menu_item ||= menu_items[controller_name.to_sym][:actions][action_name.to_sym] ||
143 menu_items[controller_name.to_sym][:default]
143 menu_items[controller_name.to_sym][:default]
144 end
144 end
145
145
146 # Redirects user to the menu item of the given project
146 # Redirects user to the menu item of the given project
147 # Returns false if user is not authorized
147 # Returns false if user is not authorized
148 def redirect_to_project_menu_item(project, name)
148 def redirect_to_project_menu_item(project, name)
149 item = Redmine::MenuManager.items(:project_menu).detect {|i| i.name.to_s == name.to_s}
149 item = Redmine::MenuManager.items(:project_menu).detect {|i| i.name.to_s == name.to_s}
150 if item && User.current.allowed_to?(item.url, project) && (item.condition.nil? || item.condition.call(project))
150 if item && User.current.allowed_to?(item.url, project) && (item.condition.nil? || item.condition.call(project))
151 redirect_to({item.param => project}.merge(item.url))
151 redirect_to({item.param => project}.merge(item.url))
152 return true
152 return true
153 end
153 end
154 false
154 false
155 end
155 end
156 end
156 end
157
157
158 module MenuHelper
158 module MenuHelper
159 # Returns the current menu item name
159 # Returns the current menu item name
160 def current_menu_item
160 def current_menu_item
161 @controller.current_menu_item
161 @controller.current_menu_item
162 end
162 end
163
163
164 # Renders the application main menu
164 # Renders the application main menu
165 def render_main_menu(project)
165 def render_main_menu(project)
166 render_menu((project && !project.new_record?) ? :project_menu : :application_menu, project)
166 render_menu((project && !project.new_record?) ? :project_menu : :application_menu, project)
167 end
167 end
168
168
169 def display_main_menu?(project)
169 def display_main_menu?(project)
170 menu_name = project && !project.new_record? ? :project_menu : :application_menu
170 menu_name = project && !project.new_record? ? :project_menu : :application_menu
171 Redmine::MenuManager.items(menu_name).size > 1 # 1 element is the root
171 Redmine::MenuManager.items(menu_name).size > 1 # 1 element is the root
172 end
172 end
173
173
174 def render_menu(menu, project=nil)
174 def render_menu(menu, project=nil)
175 links = []
175 links = []
176 menu_items_for(menu, project) do |node|
176 menu_items_for(menu, project) do |node|
177 links << render_menu_node(node, project)
177 links << render_menu_node(node, project)
178 end
178 end
179 links.empty? ? nil : content_tag('ul', links.join("\n"))
179 links.empty? ? nil : content_tag('ul', links.join("\n"))
180 end
180 end
181
181
182 def render_menu_node(node, project=nil)
182 def render_menu_node(node, project=nil)
183 if node.hasChildren? || !node.child_menus.nil?
183 if node.hasChildren? || !node.child_menus.nil?
184 return render_menu_node_with_children(node, project)
184 return render_menu_node_with_children(node, project)
185 else
185 else
186 caption, url, selected = extract_node_details(node, project)
186 caption, url, selected = extract_node_details(node, project)
187 return content_tag('li',
187 return content_tag('li',
188 render_single_menu_node(node, caption, url, selected))
188 render_single_menu_node(node, caption, url, selected))
189 end
189 end
190 end
190 end
191
191
192 def render_menu_node_with_children(node, project=nil)
192 def render_menu_node_with_children(node, project=nil)
193 caption, url, selected = extract_node_details(node, project)
193 caption, url, selected = extract_node_details(node, project)
194
194
195 html = returning [] do |html|
195 html = [].tap do |html|
196 html << '<li>'
196 html << '<li>'
197 # Parent
197 # Parent
198 html << render_single_menu_node(node, caption, url, selected)
198 html << render_single_menu_node(node, caption, url, selected)
199
199
200 # Standard children
200 # Standard children
201 standard_children_list = returning "" do |child_html|
201 standard_children_list = "".tap do |child_html|
202 node.children.each do |child|
202 node.children.each do |child|
203 child_html << render_menu_node(child, project)
203 child_html << render_menu_node(child, project)
204 end
204 end
205 end
205 end
206
206
207 html << content_tag(:ul, standard_children_list, :class => 'menu-children') unless standard_children_list.empty?
207 html << content_tag(:ul, standard_children_list, :class => 'menu-children') unless standard_children_list.empty?
208
208
209 # Unattached children
209 # Unattached children
210 unattached_children_list = render_unattached_children_menu(node, project)
210 unattached_children_list = render_unattached_children_menu(node, project)
211 html << content_tag(:ul, unattached_children_list, :class => 'menu-children unattached') unless unattached_children_list.blank?
211 html << content_tag(:ul, unattached_children_list, :class => 'menu-children unattached') unless unattached_children_list.blank?
212
212
213 html << '</li>'
213 html << '</li>'
214 end
214 end
215 return html.join("\n")
215 return html.join("\n")
216 end
216 end
217
217
218 # Returns a list of unattached children menu items
218 # Returns a list of unattached children menu items
219 def render_unattached_children_menu(node, project)
219 def render_unattached_children_menu(node, project)
220 return nil unless node.child_menus
220 return nil unless node.child_menus
221
221
222 returning "" do |child_html|
222 "".tap do |child_html|
223 unattached_children = node.child_menus.call(project)
223 unattached_children = node.child_menus.call(project)
224 # Tree nodes support #each so we need to do object detection
224 # Tree nodes support #each so we need to do object detection
225 if unattached_children.is_a? Array
225 if unattached_children.is_a? Array
226 unattached_children.each do |child|
226 unattached_children.each do |child|
227 child_html << content_tag(:li, render_unattached_menu_item(child, project))
227 child_html << content_tag(:li, render_unattached_menu_item(child, project))
228 end
228 end
229 else
229 else
230 raise MenuError, ":child_menus must be an array of MenuItems"
230 raise MenuError, ":child_menus must be an array of MenuItems"
231 end
231 end
232 end
232 end
233 end
233 end
234
234
235 def render_single_menu_node(item, caption, url, selected)
235 def render_single_menu_node(item, caption, url, selected)
236 link_to(h(caption), url, item.html_options(:selected => selected))
236 link_to(h(caption), url, item.html_options(:selected => selected))
237 end
237 end
238
238
239 def render_unattached_menu_item(menu_item, project)
239 def render_unattached_menu_item(menu_item, project)
240 raise MenuError, ":child_menus must be an array of MenuItems" unless menu_item.is_a? MenuItem
240 raise MenuError, ":child_menus must be an array of MenuItems" unless menu_item.is_a? MenuItem
241
241
242 if User.current.allowed_to?(menu_item.url, project)
242 if User.current.allowed_to?(menu_item.url, project)
243 link_to(h(menu_item.caption),
243 link_to(h(menu_item.caption),
244 menu_item.url,
244 menu_item.url,
245 menu_item.html_options)
245 menu_item.html_options)
246 end
246 end
247 end
247 end
248
248
249 def menu_items_for(menu, project=nil)
249 def menu_items_for(menu, project=nil)
250 items = []
250 items = []
251 Redmine::MenuManager.items(menu).root.children.each do |node|
251 Redmine::MenuManager.items(menu).root.children.each do |node|
252 if allowed_node?(node, User.current, project)
252 if allowed_node?(node, User.current, project)
253 if block_given?
253 if block_given?
254 yield node
254 yield node
255 else
255 else
256 items << node # TODO: not used?
256 items << node # TODO: not used?
257 end
257 end
258 end
258 end
259 end
259 end
260 return block_given? ? nil : items
260 return block_given? ? nil : items
261 end
261 end
262
262
263 def extract_node_details(node, project=nil)
263 def extract_node_details(node, project=nil)
264 item = node
264 item = node
265 url = case item.url
265 url = case item.url
266 when Hash
266 when Hash
267 project.nil? ? item.url : {item.param => project}.merge(item.url)
267 project.nil? ? item.url : {item.param => project}.merge(item.url)
268 when Symbol
268 when Symbol
269 send(item.url)
269 send(item.url)
270 else
270 else
271 item.url
271 item.url
272 end
272 end
273 caption = item.caption(project)
273 caption = item.caption(project)
274 return [caption, url, (current_menu_item == item.name)]
274 return [caption, url, (current_menu_item == item.name)]
275 end
275 end
276
276
277 # Checks if a user is allowed to access the menu item by:
277 # Checks if a user is allowed to access the menu item by:
278 #
278 #
279 # * Checking the conditions of the item
279 # * Checking the conditions of the item
280 # * Checking the url target (project only)
280 # * Checking the url target (project only)
281 def allowed_node?(node, user, project)
281 def allowed_node?(node, user, project)
282 if node.condition && !node.condition.call(project)
282 if node.condition && !node.condition.call(project)
283 # Condition that doesn't pass
283 # Condition that doesn't pass
284 return false
284 return false
285 end
285 end
286
286
287 if project
287 if project
288 return user && user.allowed_to?(node.url, project)
288 return user && user.allowed_to?(node.url, project)
289 else
289 else
290 # outside a project, all menu items allowed
290 # outside a project, all menu items allowed
291 return true
291 return true
292 end
292 end
293 end
293 end
294 end
294 end
295
295
296 class << self
296 class << self
297 def map(menu_name)
297 def map(menu_name)
298 @items ||= {}
298 @items ||= {}
299 mapper = Mapper.new(menu_name.to_sym, @items)
299 mapper = Mapper.new(menu_name.to_sym, @items)
300 if block_given?
300 if block_given?
301 yield mapper
301 yield mapper
302 else
302 else
303 mapper
303 mapper
304 end
304 end
305 end
305 end
306
306
307 def items(menu_name)
307 def items(menu_name)
308 @items[menu_name.to_sym] || Tree::TreeNode.new(:root, {})
308 @items[menu_name.to_sym] || Tree::TreeNode.new(:root, {})
309 end
309 end
310 end
310 end
311
311
312 class Mapper
312 class Mapper
313 def initialize(menu, items)
313 def initialize(menu, items)
314 items[menu] ||= Tree::TreeNode.new(:root, {})
314 items[menu] ||= Tree::TreeNode.new(:root, {})
315 @menu = menu
315 @menu = menu
316 @menu_items = items[menu]
316 @menu_items = items[menu]
317 end
317 end
318
318
319 @@last_items_count = Hash.new {|h,k| h[k] = 0}
319 @@last_items_count = Hash.new {|h,k| h[k] = 0}
320
320
321 # Adds an item at the end of the menu. Available options:
321 # Adds an item at the end of the menu. Available options:
322 # * param: the parameter name that is used for the project id (default is :id)
322 # * param: the parameter name that is used for the project id (default is :id)
323 # * if: a Proc that is called before rendering the item, the item is displayed only if it returns true
323 # * if: a Proc that is called before rendering the item, the item is displayed only if it returns true
324 # * caption that can be:
324 # * caption that can be:
325 # * a localized string Symbol
325 # * a localized string Symbol
326 # * a String
326 # * a String
327 # * a Proc that can take the project as argument
327 # * a Proc that can take the project as argument
328 # * before, after: specify where the menu item should be inserted (eg. :after => :activity)
328 # * before, after: specify where the menu item should be inserted (eg. :after => :activity)
329 # * parent: menu item will be added as a child of another named menu (eg. :parent => :issues)
329 # * parent: menu item will be added as a child of another named menu (eg. :parent => :issues)
330 # * children: a Proc that is called before rendering the item. The Proc should return an array of MenuItems, which will be added as children to this item.
330 # * children: a Proc that is called before rendering the item. The Proc should return an array of MenuItems, which will be added as children to this item.
331 # eg. :children => Proc.new {|project| [Redmine::MenuManager::MenuItem.new(...)] }
331 # eg. :children => Proc.new {|project| [Redmine::MenuManager::MenuItem.new(...)] }
332 # * last: menu item will stay at the end (eg. :last => true)
332 # * last: menu item will stay at the end (eg. :last => true)
333 # * html_options: a hash of html options that are passed to link_to
333 # * html_options: a hash of html options that are passed to link_to
334 def push(name, url, options={})
334 def push(name, url, options={})
335 options = options.dup
335 options = options.dup
336
336
337 if options[:parent]
337 if options[:parent]
338 subtree = self.find(options[:parent])
338 subtree = self.find(options[:parent])
339 if subtree
339 if subtree
340 target_root = subtree
340 target_root = subtree
341 else
341 else
342 target_root = @menu_items.root
342 target_root = @menu_items.root
343 end
343 end
344
344
345 else
345 else
346 target_root = @menu_items.root
346 target_root = @menu_items.root
347 end
347 end
348
348
349 # menu item position
349 # menu item position
350 if first = options.delete(:first)
350 if first = options.delete(:first)
351 target_root.prepend(MenuItem.new(name, url, options))
351 target_root.prepend(MenuItem.new(name, url, options))
352 elsif before = options.delete(:before)
352 elsif before = options.delete(:before)
353
353
354 if exists?(before)
354 if exists?(before)
355 target_root.add_at(MenuItem.new(name, url, options), position_of(before))
355 target_root.add_at(MenuItem.new(name, url, options), position_of(before))
356 else
356 else
357 target_root.add(MenuItem.new(name, url, options))
357 target_root.add(MenuItem.new(name, url, options))
358 end
358 end
359
359
360 elsif after = options.delete(:after)
360 elsif after = options.delete(:after)
361
361
362 if exists?(after)
362 if exists?(after)
363 target_root.add_at(MenuItem.new(name, url, options), position_of(after) + 1)
363 target_root.add_at(MenuItem.new(name, url, options), position_of(after) + 1)
364 else
364 else
365 target_root.add(MenuItem.new(name, url, options))
365 target_root.add(MenuItem.new(name, url, options))
366 end
366 end
367
367
368 elsif options[:last] # don't delete, needs to be stored
368 elsif options[:last] # don't delete, needs to be stored
369 target_root.add_last(MenuItem.new(name, url, options))
369 target_root.add_last(MenuItem.new(name, url, options))
370 else
370 else
371 target_root.add(MenuItem.new(name, url, options))
371 target_root.add(MenuItem.new(name, url, options))
372 end
372 end
373 end
373 end
374
374
375 # Removes a menu item
375 # Removes a menu item
376 def delete(name)
376 def delete(name)
377 if found = self.find(name)
377 if found = self.find(name)
378 @menu_items.remove!(found)
378 @menu_items.remove!(found)
379 end
379 end
380 end
380 end
381
381
382 # Checks if a menu item exists
382 # Checks if a menu item exists
383 def exists?(name)
383 def exists?(name)
384 @menu_items.any? {|node| node.name == name}
384 @menu_items.any? {|node| node.name == name}
385 end
385 end
386
386
387 def find(name)
387 def find(name)
388 @menu_items.find {|node| node.name == name}
388 @menu_items.find {|node| node.name == name}
389 end
389 end
390
390
391 def position_of(name)
391 def position_of(name)
392 @menu_items.each do |node|
392 @menu_items.each do |node|
393 if node.name == name
393 if node.name == name
394 return node.position
394 return node.position
395 end
395 end
396 end
396 end
397 end
397 end
398 end
398 end
399
399
400 class MenuItem < Tree::TreeNode
400 class MenuItem < Tree::TreeNode
401 include Redmine::I18n
401 include Redmine::I18n
402 attr_reader :name, :url, :param, :condition, :parent, :child_menus, :last
402 attr_reader :name, :url, :param, :condition, :parent, :child_menus, :last
403
403
404 def initialize(name, url, options)
404 def initialize(name, url, options)
405 raise ArgumentError, "Invalid option :if for menu item '#{name}'" if options[:if] && !options[:if].respond_to?(:call)
405 raise ArgumentError, "Invalid option :if for menu item '#{name}'" if options[:if] && !options[:if].respond_to?(:call)
406 raise ArgumentError, "Invalid option :html for menu item '#{name}'" if options[:html] && !options[:html].is_a?(Hash)
406 raise ArgumentError, "Invalid option :html for menu item '#{name}'" if options[:html] && !options[:html].is_a?(Hash)
407 raise ArgumentError, "Cannot set the :parent to be the same as this item" if options[:parent] == name.to_sym
407 raise ArgumentError, "Cannot set the :parent to be the same as this item" if options[:parent] == name.to_sym
408 raise ArgumentError, "Invalid option :children for menu item '#{name}'" if options[:children] && !options[:children].respond_to?(:call)
408 raise ArgumentError, "Invalid option :children for menu item '#{name}'" if options[:children] && !options[:children].respond_to?(:call)
409 @name = name
409 @name = name
410 @url = url
410 @url = url
411 @condition = options[:if]
411 @condition = options[:if]
412 @param = options[:param] || :id
412 @param = options[:param] || :id
413 @caption = options[:caption]
413 @caption = options[:caption]
414 @html_options = options[:html] || {}
414 @html_options = options[:html] || {}
415 # Adds a unique class to each menu item based on its name
415 # Adds a unique class to each menu item based on its name
416 @html_options[:class] = [@html_options[:class], @name.to_s.dasherize].compact.join(' ')
416 @html_options[:class] = [@html_options[:class], @name.to_s.dasherize].compact.join(' ')
417 @parent = options[:parent]
417 @parent = options[:parent]
418 @child_menus = options[:children]
418 @child_menus = options[:children]
419 @last = options[:last] || false
419 @last = options[:last] || false
420 super @name.to_sym
420 super @name.to_sym
421 end
421 end
422
422
423 def caption(project=nil)
423 def caption(project=nil)
424 if @caption.is_a?(Proc)
424 if @caption.is_a?(Proc)
425 c = @caption.call(project).to_s
425 c = @caption.call(project).to_s
426 c = @name.to_s.humanize if c.blank?
426 c = @name.to_s.humanize if c.blank?
427 c
427 c
428 else
428 else
429 if @caption.nil?
429 if @caption.nil?
430 l_or_humanize(name, :prefix => 'label_')
430 l_or_humanize(name, :prefix => 'label_')
431 else
431 else
432 @caption.is_a?(Symbol) ? l(@caption) : @caption
432 @caption.is_a?(Symbol) ? l(@caption) : @caption
433 end
433 end
434 end
434 end
435 end
435 end
436
436
437 def html_options(options={})
437 def html_options(options={})
438 if options[:selected]
438 if options[:selected]
439 o = @html_options.dup
439 o = @html_options.dup
440 o[:class] += ' selected'
440 o[:class] += ' selected'
441 o
441 o
442 else
442 else
443 @html_options
443 @html_options
444 end
444 end
445 end
445 end
446 end
446 end
447 end
447 end
448 end
448 end
@@ -1,31 +1,31
1 class Project < ActiveRecord::Base
1 class Project < ActiveRecord::Base
2 generator_for :name, :method => :next_name
2 generator_for :name, :method => :next_name
3 generator_for :identifier, :method => :next_identifier_from_object_daddy
3 generator_for :identifier, :method => :next_identifier_from_object_daddy
4 generator_for :enabled_modules, :method => :all_modules
4 generator_for :enabled_modules, :method => :all_modules
5 generator_for :trackers, :method => :next_tracker
5 generator_for :trackers, :method => :next_tracker
6
6
7 def self.next_name
7 def self.next_name
8 @last_name ||= 'Project 0'
8 @last_name ||= 'Project 0'
9 @last_name.succ!
9 @last_name.succ!
10 @last_name
10 @last_name
11 end
11 end
12
12
13 # Project#next_identifier is defined on Redmine
13 # Project#next_identifier is defined on Redmine
14 def self.next_identifier_from_object_daddy
14 def self.next_identifier_from_object_daddy
15 @last_identifier ||= 'project-0000'
15 @last_identifier ||= 'project-0000'
16 @last_identifier.succ!
16 @last_identifier.succ!
17 @last_identifier
17 @last_identifier
18 end
18 end
19
19
20 def self.all_modules
20 def self.all_modules
21 returning [] do |modules|
21 [].tap do |modules|
22 Redmine::AccessControl.available_project_modules.each do |name|
22 Redmine::AccessControl.available_project_modules.each do |name|
23 modules << EnabledModule.new(:name => name.to_s)
23 modules << EnabledModule.new(:name => name.to_s)
24 end
24 end
25 end
25 end
26 end
26 end
27
27
28 def self.next_tracker
28 def self.next_tracker
29 [Tracker.generate!]
29 [Tracker.generate!]
30 end
30 end
31 end
31 end
@@ -1,547 +1,547
1 module CollectiveIdea #:nodoc:
1 module CollectiveIdea #:nodoc:
2 module Acts #:nodoc:
2 module Acts #:nodoc:
3 module NestedSet #:nodoc:
3 module NestedSet #:nodoc:
4 def self.included(base)
4 def self.included(base)
5 base.extend(SingletonMethods)
5 base.extend(SingletonMethods)
6 end
6 end
7
7
8 # This acts provides Nested Set functionality. Nested Set is a smart way to implement
8 # This acts provides Nested Set functionality. Nested Set is a smart way to implement
9 # an _ordered_ tree, with the added feature that you can select the children and all of their
9 # an _ordered_ tree, with the added feature that you can select the children and all of their
10 # descendants with a single query. The drawback is that insertion or move need some complex
10 # descendants with a single query. The drawback is that insertion or move need some complex
11 # sql queries. But everything is done here by this module!
11 # sql queries. But everything is done here by this module!
12 #
12 #
13 # Nested sets are appropriate each time you want either an orderd tree (menus,
13 # Nested sets are appropriate each time you want either an orderd tree (menus,
14 # commercial categories) or an efficient way of querying big trees (threaded posts).
14 # commercial categories) or an efficient way of querying big trees (threaded posts).
15 #
15 #
16 # == API
16 # == API
17 #
17 #
18 # Methods names are aligned with acts_as_tree as much as possible, to make replacment from one
18 # Methods names are aligned with acts_as_tree as much as possible, to make replacment from one
19 # by another easier, except for the creation:
19 # by another easier, except for the creation:
20 #
20 #
21 # in acts_as_tree:
21 # in acts_as_tree:
22 # item.children.create(:name => "child1")
22 # item.children.create(:name => "child1")
23 #
23 #
24 # in acts_as_nested_set:
24 # in acts_as_nested_set:
25 # # adds a new item at the "end" of the tree, i.e. with child.left = max(tree.right)+1
25 # # adds a new item at the "end" of the tree, i.e. with child.left = max(tree.right)+1
26 # child = MyClass.new(:name => "child1")
26 # child = MyClass.new(:name => "child1")
27 # child.save
27 # child.save
28 # # now move the item to its right place
28 # # now move the item to its right place
29 # child.move_to_child_of my_item
29 # child.move_to_child_of my_item
30 #
30 #
31 # You can pass an id or an object to:
31 # You can pass an id or an object to:
32 # * <tt>#move_to_child_of</tt>
32 # * <tt>#move_to_child_of</tt>
33 # * <tt>#move_to_right_of</tt>
33 # * <tt>#move_to_right_of</tt>
34 # * <tt>#move_to_left_of</tt>
34 # * <tt>#move_to_left_of</tt>
35 #
35 #
36 module SingletonMethods
36 module SingletonMethods
37 # Configuration options are:
37 # Configuration options are:
38 #
38 #
39 # * +:parent_column+ - specifies the column name to use for keeping the position integer (default: parent_id)
39 # * +:parent_column+ - specifies the column name to use for keeping the position integer (default: parent_id)
40 # * +:left_column+ - column name for left boundry data, default "lft"
40 # * +:left_column+ - column name for left boundry data, default "lft"
41 # * +:right_column+ - column name for right boundry data, default "rgt"
41 # * +:right_column+ - column name for right boundry data, default "rgt"
42 # * +:scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id"
42 # * +:scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id"
43 # (if it hasn't been already) and use that as the foreign key restriction. You
43 # (if it hasn't been already) and use that as the foreign key restriction. You
44 # can also pass an array to scope by multiple attributes.
44 # can also pass an array to scope by multiple attributes.
45 # Example: <tt>acts_as_nested_set :scope => [:notable_id, :notable_type]</tt>
45 # Example: <tt>acts_as_nested_set :scope => [:notable_id, :notable_type]</tt>
46 # * +:dependent+ - behavior for cascading destroy. If set to :destroy, all the
46 # * +:dependent+ - behavior for cascading destroy. If set to :destroy, all the
47 # child objects are destroyed alongside this object by calling their destroy
47 # child objects are destroyed alongside this object by calling their destroy
48 # method. If set to :delete_all (default), all the child objects are deleted
48 # method. If set to :delete_all (default), all the child objects are deleted
49 # without calling their destroy method.
49 # without calling their destroy method.
50 #
50 #
51 # See CollectiveIdea::Acts::NestedSet::ClassMethods for a list of class methods and
51 # See CollectiveIdea::Acts::NestedSet::ClassMethods for a list of class methods and
52 # CollectiveIdea::Acts::NestedSet::InstanceMethods for a list of instance methods added
52 # CollectiveIdea::Acts::NestedSet::InstanceMethods for a list of instance methods added
53 # to acts_as_nested_set models
53 # to acts_as_nested_set models
54 def acts_as_nested_set(options = {})
54 def acts_as_nested_set(options = {})
55 options = {
55 options = {
56 :parent_column => 'parent_id',
56 :parent_column => 'parent_id',
57 :left_column => 'lft',
57 :left_column => 'lft',
58 :right_column => 'rgt',
58 :right_column => 'rgt',
59 :order => 'id',
59 :order => 'id',
60 :dependent => :delete_all, # or :destroy
60 :dependent => :delete_all, # or :destroy
61 }.merge(options)
61 }.merge(options)
62
62
63 if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/
63 if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/
64 options[:scope] = "#{options[:scope]}_id".intern
64 options[:scope] = "#{options[:scope]}_id".intern
65 end
65 end
66
66
67 write_inheritable_attribute :acts_as_nested_set_options, options
67 write_inheritable_attribute :acts_as_nested_set_options, options
68 class_inheritable_reader :acts_as_nested_set_options
68 class_inheritable_reader :acts_as_nested_set_options
69
69
70 include Comparable
70 include Comparable
71 include Columns
71 include Columns
72 include InstanceMethods
72 include InstanceMethods
73 extend Columns
73 extend Columns
74 extend ClassMethods
74 extend ClassMethods
75
75
76 # no bulk assignment
76 # no bulk assignment
77 attr_protected left_column_name.intern,
77 attr_protected left_column_name.intern,
78 right_column_name.intern,
78 right_column_name.intern,
79 parent_column_name.intern
79 parent_column_name.intern
80
80
81 before_create :set_default_left_and_right
81 before_create :set_default_left_and_right
82 before_destroy :prune_from_tree
82 before_destroy :prune_from_tree
83
83
84 # no assignment to structure fields
84 # no assignment to structure fields
85 [left_column_name, right_column_name, parent_column_name].each do |column|
85 [left_column_name, right_column_name, parent_column_name].each do |column|
86 module_eval <<-"end_eval", __FILE__, __LINE__
86 module_eval <<-"end_eval", __FILE__, __LINE__
87 def #{column}=(x)
87 def #{column}=(x)
88 raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{column}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
88 raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{column}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
89 end
89 end
90 end_eval
90 end_eval
91 end
91 end
92
92
93 named_scope :roots, :conditions => {parent_column_name => nil}, :order => quoted_left_column_name
93 named_scope :roots, :conditions => {parent_column_name => nil}, :order => quoted_left_column_name
94 named_scope :leaves, :conditions => "#{quoted_right_column_name} - #{quoted_left_column_name} = 1", :order => quoted_left_column_name
94 named_scope :leaves, :conditions => "#{quoted_right_column_name} - #{quoted_left_column_name} = 1", :order => quoted_left_column_name
95 if self.respond_to?(:define_callbacks)
95 if self.respond_to?(:define_callbacks)
96 define_callbacks("before_move", "after_move")
96 define_callbacks("before_move", "after_move")
97 end
97 end
98
98
99
99
100 end
100 end
101
101
102 end
102 end
103
103
104 module ClassMethods
104 module ClassMethods
105
105
106 # Returns the first root
106 # Returns the first root
107 def root
107 def root
108 roots.find(:first)
108 roots.find(:first)
109 end
109 end
110
110
111 def valid?
111 def valid?
112 left_and_rights_valid? && no_duplicates_for_columns? && all_roots_valid?
112 left_and_rights_valid? && no_duplicates_for_columns? && all_roots_valid?
113 end
113 end
114
114
115 def left_and_rights_valid?
115 def left_and_rights_valid?
116 count(
116 count(
117 :joins => "LEFT OUTER JOIN #{quoted_table_name} AS parent ON " +
117 :joins => "LEFT OUTER JOIN #{quoted_table_name} AS parent ON " +
118 "#{quoted_table_name}.#{quoted_parent_column_name} = parent.#{primary_key}",
118 "#{quoted_table_name}.#{quoted_parent_column_name} = parent.#{primary_key}",
119 :conditions =>
119 :conditions =>
120 "#{quoted_table_name}.#{quoted_left_column_name} IS NULL OR " +
120 "#{quoted_table_name}.#{quoted_left_column_name} IS NULL OR " +
121 "#{quoted_table_name}.#{quoted_right_column_name} IS NULL OR " +
121 "#{quoted_table_name}.#{quoted_right_column_name} IS NULL OR " +
122 "#{quoted_table_name}.#{quoted_left_column_name} >= " +
122 "#{quoted_table_name}.#{quoted_left_column_name} >= " +
123 "#{quoted_table_name}.#{quoted_right_column_name} OR " +
123 "#{quoted_table_name}.#{quoted_right_column_name} OR " +
124 "(#{quoted_table_name}.#{quoted_parent_column_name} IS NOT NULL AND " +
124 "(#{quoted_table_name}.#{quoted_parent_column_name} IS NOT NULL AND " +
125 "(#{quoted_table_name}.#{quoted_left_column_name} <= parent.#{quoted_left_column_name} OR " +
125 "(#{quoted_table_name}.#{quoted_left_column_name} <= parent.#{quoted_left_column_name} OR " +
126 "#{quoted_table_name}.#{quoted_right_column_name} >= parent.#{quoted_right_column_name}))"
126 "#{quoted_table_name}.#{quoted_right_column_name} >= parent.#{quoted_right_column_name}))"
127 ) == 0
127 ) == 0
128 end
128 end
129
129
130 def no_duplicates_for_columns?
130 def no_duplicates_for_columns?
131 scope_string = Array(acts_as_nested_set_options[:scope]).map do |c|
131 scope_string = Array(acts_as_nested_set_options[:scope]).map do |c|
132 connection.quote_column_name(c)
132 connection.quote_column_name(c)
133 end.push(nil).join(", ")
133 end.push(nil).join(", ")
134 [quoted_left_column_name, quoted_right_column_name].all? do |column|
134 [quoted_left_column_name, quoted_right_column_name].all? do |column|
135 # No duplicates
135 # No duplicates
136 find(:first,
136 find(:first,
137 :select => "#{scope_string}#{column}, COUNT(#{column})",
137 :select => "#{scope_string}#{column}, COUNT(#{column})",
138 :group => "#{scope_string}#{column}
138 :group => "#{scope_string}#{column}
139 HAVING COUNT(#{column}) > 1").nil?
139 HAVING COUNT(#{column}) > 1").nil?
140 end
140 end
141 end
141 end
142
142
143 # Wrapper for each_root_valid? that can deal with scope.
143 # Wrapper for each_root_valid? that can deal with scope.
144 def all_roots_valid?
144 def all_roots_valid?
145 if acts_as_nested_set_options[:scope]
145 if acts_as_nested_set_options[:scope]
146 roots(:group => scope_column_names).group_by{|record| scope_column_names.collect{|col| record.send(col.to_sym)}}.all? do |scope, grouped_roots|
146 roots(:group => scope_column_names).group_by{|record| scope_column_names.collect{|col| record.send(col.to_sym)}}.all? do |scope, grouped_roots|
147 each_root_valid?(grouped_roots)
147 each_root_valid?(grouped_roots)
148 end
148 end
149 else
149 else
150 each_root_valid?(roots)
150 each_root_valid?(roots)
151 end
151 end
152 end
152 end
153
153
154 def each_root_valid?(roots_to_validate)
154 def each_root_valid?(roots_to_validate)
155 left = right = 0
155 left = right = 0
156 roots_to_validate.all? do |root|
156 roots_to_validate.all? do |root|
157 returning(root.left > left && root.right > right) do
157 (root.left > left && root.right > right).tap do
158 left = root.left
158 left = root.left
159 right = root.right
159 right = root.right
160 end
160 end
161 end
161 end
162 end
162 end
163
163
164 # Rebuilds the left & rights if unset or invalid. Also very useful for converting from acts_as_tree.
164 # Rebuilds the left & rights if unset or invalid. Also very useful for converting from acts_as_tree.
165 def rebuild!
165 def rebuild!
166 # Don't rebuild a valid tree.
166 # Don't rebuild a valid tree.
167 return true if valid?
167 return true if valid?
168
168
169 scope = lambda{|node|}
169 scope = lambda{|node|}
170 if acts_as_nested_set_options[:scope]
170 if acts_as_nested_set_options[:scope]
171 scope = lambda{|node|
171 scope = lambda{|node|
172 scope_column_names.inject(""){|str, column_name|
172 scope_column_names.inject(""){|str, column_name|
173 str << "AND #{connection.quote_column_name(column_name)} = #{connection.quote(node.send(column_name.to_sym))} "
173 str << "AND #{connection.quote_column_name(column_name)} = #{connection.quote(node.send(column_name.to_sym))} "
174 }
174 }
175 }
175 }
176 end
176 end
177 indices = {}
177 indices = {}
178
178
179 set_left_and_rights = lambda do |node|
179 set_left_and_rights = lambda do |node|
180 # set left
180 # set left
181 node[left_column_name] = indices[scope.call(node)] += 1
181 node[left_column_name] = indices[scope.call(node)] += 1
182 # find
182 # find
183 find(:all, :conditions => ["#{quoted_parent_column_name} = ? #{scope.call(node)}", node], :order => "#{quoted_left_column_name}, #{quoted_right_column_name}, #{acts_as_nested_set_options[:order]}").each{|n| set_left_and_rights.call(n) }
183 find(:all, :conditions => ["#{quoted_parent_column_name} = ? #{scope.call(node)}", node], :order => "#{quoted_left_column_name}, #{quoted_right_column_name}, #{acts_as_nested_set_options[:order]}").each{|n| set_left_and_rights.call(n) }
184 # set right
184 # set right
185 node[right_column_name] = indices[scope.call(node)] += 1
185 node[right_column_name] = indices[scope.call(node)] += 1
186 node.save!
186 node.save!
187 end
187 end
188
188
189 # Find root node(s)
189 # Find root node(s)
190 root_nodes = find(:all, :conditions => "#{quoted_parent_column_name} IS NULL", :order => "#{quoted_left_column_name}, #{quoted_right_column_name}, #{acts_as_nested_set_options[:order]}").each do |root_node|
190 root_nodes = find(:all, :conditions => "#{quoted_parent_column_name} IS NULL", :order => "#{quoted_left_column_name}, #{quoted_right_column_name}, #{acts_as_nested_set_options[:order]}").each do |root_node|
191 # setup index for this scope
191 # setup index for this scope
192 indices[scope.call(root_node)] ||= 0
192 indices[scope.call(root_node)] ||= 0
193 set_left_and_rights.call(root_node)
193 set_left_and_rights.call(root_node)
194 end
194 end
195 end
195 end
196 end
196 end
197
197
198 # Mixed into both classes and instances to provide easy access to the column names
198 # Mixed into both classes and instances to provide easy access to the column names
199 module Columns
199 module Columns
200 def left_column_name
200 def left_column_name
201 acts_as_nested_set_options[:left_column]
201 acts_as_nested_set_options[:left_column]
202 end
202 end
203
203
204 def right_column_name
204 def right_column_name
205 acts_as_nested_set_options[:right_column]
205 acts_as_nested_set_options[:right_column]
206 end
206 end
207
207
208 def parent_column_name
208 def parent_column_name
209 acts_as_nested_set_options[:parent_column]
209 acts_as_nested_set_options[:parent_column]
210 end
210 end
211
211
212 def scope_column_names
212 def scope_column_names
213 Array(acts_as_nested_set_options[:scope])
213 Array(acts_as_nested_set_options[:scope])
214 end
214 end
215
215
216 def quoted_left_column_name
216 def quoted_left_column_name
217 connection.quote_column_name(left_column_name)
217 connection.quote_column_name(left_column_name)
218 end
218 end
219
219
220 def quoted_right_column_name
220 def quoted_right_column_name
221 connection.quote_column_name(right_column_name)
221 connection.quote_column_name(right_column_name)
222 end
222 end
223
223
224 def quoted_parent_column_name
224 def quoted_parent_column_name
225 connection.quote_column_name(parent_column_name)
225 connection.quote_column_name(parent_column_name)
226 end
226 end
227
227
228 def quoted_scope_column_names
228 def quoted_scope_column_names
229 scope_column_names.collect {|column_name| connection.quote_column_name(column_name) }
229 scope_column_names.collect {|column_name| connection.quote_column_name(column_name) }
230 end
230 end
231 end
231 end
232
232
233 # Any instance method that returns a collection makes use of Rails 2.1's named_scope (which is bundled for Rails 2.0), so it can be treated as a finder.
233 # Any instance method that returns a collection makes use of Rails 2.1's named_scope (which is bundled for Rails 2.0), so it can be treated as a finder.
234 #
234 #
235 # category.self_and_descendants.count
235 # category.self_and_descendants.count
236 # category.ancestors.find(:all, :conditions => "name like '%foo%'")
236 # category.ancestors.find(:all, :conditions => "name like '%foo%'")
237 module InstanceMethods
237 module InstanceMethods
238 # Value of the parent column
238 # Value of the parent column
239 def parent_id
239 def parent_id
240 self[parent_column_name]
240 self[parent_column_name]
241 end
241 end
242
242
243 # Value of the left column
243 # Value of the left column
244 def left
244 def left
245 self[left_column_name]
245 self[left_column_name]
246 end
246 end
247
247
248 # Value of the right column
248 # Value of the right column
249 def right
249 def right
250 self[right_column_name]
250 self[right_column_name]
251 end
251 end
252
252
253 # Returns true if this is a root node.
253 # Returns true if this is a root node.
254 def root?
254 def root?
255 parent_id.nil?
255 parent_id.nil?
256 end
256 end
257
257
258 def leaf?
258 def leaf?
259 new_record? || (right - left == 1)
259 new_record? || (right - left == 1)
260 end
260 end
261
261
262 # Returns true is this is a child node
262 # Returns true is this is a child node
263 def child?
263 def child?
264 !parent_id.nil?
264 !parent_id.nil?
265 end
265 end
266
266
267 # order by left column
267 # order by left column
268 def <=>(x)
268 def <=>(x)
269 left <=> x.left
269 left <=> x.left
270 end
270 end
271
271
272 # Redefine to act like active record
272 # Redefine to act like active record
273 def ==(comparison_object)
273 def ==(comparison_object)
274 comparison_object.equal?(self) ||
274 comparison_object.equal?(self) ||
275 (comparison_object.instance_of?(self.class) &&
275 (comparison_object.instance_of?(self.class) &&
276 comparison_object.id == id &&
276 comparison_object.id == id &&
277 !comparison_object.new_record?)
277 !comparison_object.new_record?)
278 end
278 end
279
279
280 # Returns root
280 # Returns root
281 def root
281 def root
282 self_and_ancestors.find(:first)
282 self_and_ancestors.find(:first)
283 end
283 end
284
284
285 # Returns the immediate parent
285 # Returns the immediate parent
286 def parent
286 def parent
287 nested_set_scope.find_by_id(parent_id) if parent_id
287 nested_set_scope.find_by_id(parent_id) if parent_id
288 end
288 end
289
289
290 # Returns the array of all parents and self
290 # Returns the array of all parents and self
291 def self_and_ancestors
291 def self_and_ancestors
292 nested_set_scope.scoped :conditions => [
292 nested_set_scope.scoped :conditions => [
293 "#{self.class.table_name}.#{quoted_left_column_name} <= ? AND #{self.class.table_name}.#{quoted_right_column_name} >= ?", left, right
293 "#{self.class.table_name}.#{quoted_left_column_name} <= ? AND #{self.class.table_name}.#{quoted_right_column_name} >= ?", left, right
294 ]
294 ]
295 end
295 end
296
296
297 # Returns an array of all parents
297 # Returns an array of all parents
298 def ancestors
298 def ancestors
299 without_self self_and_ancestors
299 without_self self_and_ancestors
300 end
300 end
301
301
302 # Returns the array of all children of the parent, including self
302 # Returns the array of all children of the parent, including self
303 def self_and_siblings
303 def self_and_siblings
304 nested_set_scope.scoped :conditions => {parent_column_name => parent_id}
304 nested_set_scope.scoped :conditions => {parent_column_name => parent_id}
305 end
305 end
306
306
307 # Returns the array of all children of the parent, except self
307 # Returns the array of all children of the parent, except self
308 def siblings
308 def siblings
309 without_self self_and_siblings
309 without_self self_and_siblings
310 end
310 end
311
311
312 # Returns a set of all of its nested children which do not have children
312 # Returns a set of all of its nested children which do not have children
313 def leaves
313 def leaves
314 descendants.scoped :conditions => "#{self.class.table_name}.#{quoted_right_column_name} - #{self.class.table_name}.#{quoted_left_column_name} = 1"
314 descendants.scoped :conditions => "#{self.class.table_name}.#{quoted_right_column_name} - #{self.class.table_name}.#{quoted_left_column_name} = 1"
315 end
315 end
316
316
317 # Returns the level of this object in the tree
317 # Returns the level of this object in the tree
318 # root level is 0
318 # root level is 0
319 def level
319 def level
320 parent_id.nil? ? 0 : ancestors.count
320 parent_id.nil? ? 0 : ancestors.count
321 end
321 end
322
322
323 # Returns a set of itself and all of its nested children
323 # Returns a set of itself and all of its nested children
324 def self_and_descendants
324 def self_and_descendants
325 nested_set_scope.scoped :conditions => [
325 nested_set_scope.scoped :conditions => [
326 "#{self.class.table_name}.#{quoted_left_column_name} >= ? AND #{self.class.table_name}.#{quoted_right_column_name} <= ?", left, right
326 "#{self.class.table_name}.#{quoted_left_column_name} >= ? AND #{self.class.table_name}.#{quoted_right_column_name} <= ?", left, right
327 ]
327 ]
328 end
328 end
329
329
330 # Returns a set of all of its children and nested children
330 # Returns a set of all of its children and nested children
331 def descendants
331 def descendants
332 without_self self_and_descendants
332 without_self self_and_descendants
333 end
333 end
334
334
335 # Returns a set of only this entry's immediate children
335 # Returns a set of only this entry's immediate children
336 def children
336 def children
337 nested_set_scope.scoped :conditions => {parent_column_name => self}
337 nested_set_scope.scoped :conditions => {parent_column_name => self}
338 end
338 end
339
339
340 def is_descendant_of?(other)
340 def is_descendant_of?(other)
341 other.left < self.left && self.left < other.right && same_scope?(other)
341 other.left < self.left && self.left < other.right && same_scope?(other)
342 end
342 end
343
343
344 def is_or_is_descendant_of?(other)
344 def is_or_is_descendant_of?(other)
345 other.left <= self.left && self.left < other.right && same_scope?(other)
345 other.left <= self.left && self.left < other.right && same_scope?(other)
346 end
346 end
347
347
348 def is_ancestor_of?(other)
348 def is_ancestor_of?(other)
349 self.left < other.left && other.left < self.right && same_scope?(other)
349 self.left < other.left && other.left < self.right && same_scope?(other)
350 end
350 end
351
351
352 def is_or_is_ancestor_of?(other)
352 def is_or_is_ancestor_of?(other)
353 self.left <= other.left && other.left < self.right && same_scope?(other)
353 self.left <= other.left && other.left < self.right && same_scope?(other)
354 end
354 end
355
355
356 # Check if other model is in the same scope
356 # Check if other model is in the same scope
357 def same_scope?(other)
357 def same_scope?(other)
358 Array(acts_as_nested_set_options[:scope]).all? do |attr|
358 Array(acts_as_nested_set_options[:scope]).all? do |attr|
359 self.send(attr) == other.send(attr)
359 self.send(attr) == other.send(attr)
360 end
360 end
361 end
361 end
362
362
363 # Find the first sibling to the left
363 # Find the first sibling to the left
364 def left_sibling
364 def left_sibling
365 siblings.find(:first, :conditions => ["#{self.class.table_name}.#{quoted_left_column_name} < ?", left],
365 siblings.find(:first, :conditions => ["#{self.class.table_name}.#{quoted_left_column_name} < ?", left],
366 :order => "#{self.class.table_name}.#{quoted_left_column_name} DESC")
366 :order => "#{self.class.table_name}.#{quoted_left_column_name} DESC")
367 end
367 end
368
368
369 # Find the first sibling to the right
369 # Find the first sibling to the right
370 def right_sibling
370 def right_sibling
371 siblings.find(:first, :conditions => ["#{self.class.table_name}.#{quoted_left_column_name} > ?", left])
371 siblings.find(:first, :conditions => ["#{self.class.table_name}.#{quoted_left_column_name} > ?", left])
372 end
372 end
373
373
374 # Shorthand method for finding the left sibling and moving to the left of it.
374 # Shorthand method for finding the left sibling and moving to the left of it.
375 def move_left
375 def move_left
376 move_to_left_of left_sibling
376 move_to_left_of left_sibling
377 end
377 end
378
378
379 # Shorthand method for finding the right sibling and moving to the right of it.
379 # Shorthand method for finding the right sibling and moving to the right of it.
380 def move_right
380 def move_right
381 move_to_right_of right_sibling
381 move_to_right_of right_sibling
382 end
382 end
383
383
384 # Move the node to the left of another node (you can pass id only)
384 # Move the node to the left of another node (you can pass id only)
385 def move_to_left_of(node)
385 def move_to_left_of(node)
386 move_to node, :left
386 move_to node, :left
387 end
387 end
388
388
389 # Move the node to the left of another node (you can pass id only)
389 # Move the node to the left of another node (you can pass id only)
390 def move_to_right_of(node)
390 def move_to_right_of(node)
391 move_to node, :right
391 move_to node, :right
392 end
392 end
393
393
394 # Move the node to the child of another node (you can pass id only)
394 # Move the node to the child of another node (you can pass id only)
395 def move_to_child_of(node)
395 def move_to_child_of(node)
396 move_to node, :child
396 move_to node, :child
397 end
397 end
398
398
399 # Move the node to root nodes
399 # Move the node to root nodes
400 def move_to_root
400 def move_to_root
401 move_to nil, :root
401 move_to nil, :root
402 end
402 end
403
403
404 def move_possible?(target)
404 def move_possible?(target)
405 self != target && # Can't target self
405 self != target && # Can't target self
406 same_scope?(target) && # can't be in different scopes
406 same_scope?(target) && # can't be in different scopes
407 # !(left..right).include?(target.left..target.right) # this needs tested more
407 # !(left..right).include?(target.left..target.right) # this needs tested more
408 # detect impossible move
408 # detect impossible move
409 !((left <= target.left && right >= target.left) or (left <= target.right && right >= target.right))
409 !((left <= target.left && right >= target.left) or (left <= target.right && right >= target.right))
410 end
410 end
411
411
412 def to_text
412 def to_text
413 self_and_descendants.map do |node|
413 self_and_descendants.map do |node|
414 "#{'*'*(node.level+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})"
414 "#{'*'*(node.level+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})"
415 end.join("\n")
415 end.join("\n")
416 end
416 end
417
417
418 protected
418 protected
419
419
420 def without_self(scope)
420 def without_self(scope)
421 scope.scoped :conditions => ["#{self.class.table_name}.#{self.class.primary_key} != ?", self]
421 scope.scoped :conditions => ["#{self.class.table_name}.#{self.class.primary_key} != ?", self]
422 end
422 end
423
423
424 # All nested set queries should use this nested_set_scope, which performs finds on
424 # All nested set queries should use this nested_set_scope, which performs finds on
425 # the base ActiveRecord class, using the :scope declared in the acts_as_nested_set
425 # the base ActiveRecord class, using the :scope declared in the acts_as_nested_set
426 # declaration.
426 # declaration.
427 def nested_set_scope
427 def nested_set_scope
428 options = {:order => quoted_left_column_name}
428 options = {:order => quoted_left_column_name}
429 scopes = Array(acts_as_nested_set_options[:scope])
429 scopes = Array(acts_as_nested_set_options[:scope])
430 options[:conditions] = scopes.inject({}) do |conditions,attr|
430 options[:conditions] = scopes.inject({}) do |conditions,attr|
431 conditions.merge attr => self[attr]
431 conditions.merge attr => self[attr]
432 end unless scopes.empty?
432 end unless scopes.empty?
433 self.class.base_class.scoped options
433 self.class.base_class.scoped options
434 end
434 end
435
435
436 # on creation, set automatically lft and rgt to the end of the tree
436 # on creation, set automatically lft and rgt to the end of the tree
437 def set_default_left_and_right
437 def set_default_left_and_right
438 maxright = nested_set_scope.maximum(right_column_name) || 0
438 maxright = nested_set_scope.maximum(right_column_name) || 0
439 # adds the new node to the right of all existing nodes
439 # adds the new node to the right of all existing nodes
440 self[left_column_name] = maxright + 1
440 self[left_column_name] = maxright + 1
441 self[right_column_name] = maxright + 2
441 self[right_column_name] = maxright + 2
442 end
442 end
443
443
444 # Prunes a branch off of the tree, shifting all of the elements on the right
444 # Prunes a branch off of the tree, shifting all of the elements on the right
445 # back to the left so the counts still work.
445 # back to the left so the counts still work.
446 def prune_from_tree
446 def prune_from_tree
447 return if right.nil? || left.nil?
447 return if right.nil? || left.nil?
448 diff = right - left + 1
448 diff = right - left + 1
449
449
450 delete_method = acts_as_nested_set_options[:dependent] == :destroy ?
450 delete_method = acts_as_nested_set_options[:dependent] == :destroy ?
451 :destroy_all : :delete_all
451 :destroy_all : :delete_all
452
452
453 self.class.base_class.transaction do
453 self.class.base_class.transaction do
454 nested_set_scope.send(delete_method,
454 nested_set_scope.send(delete_method,
455 ["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?",
455 ["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?",
456 left, right]
456 left, right]
457 )
457 )
458 nested_set_scope.update_all(
458 nested_set_scope.update_all(
459 ["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff],
459 ["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff],
460 ["#{quoted_left_column_name} >= ?", right]
460 ["#{quoted_left_column_name} >= ?", right]
461 )
461 )
462 nested_set_scope.update_all(
462 nested_set_scope.update_all(
463 ["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff],
463 ["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff],
464 ["#{quoted_right_column_name} >= ?", right]
464 ["#{quoted_right_column_name} >= ?", right]
465 )
465 )
466 end
466 end
467 end
467 end
468
468
469 # reload left, right, and parent
469 # reload left, right, and parent
470 def reload_nested_set
470 def reload_nested_set
471 reload(:select => "#{quoted_left_column_name}, " +
471 reload(:select => "#{quoted_left_column_name}, " +
472 "#{quoted_right_column_name}, #{quoted_parent_column_name}")
472 "#{quoted_right_column_name}, #{quoted_parent_column_name}")
473 end
473 end
474
474
475 def move_to(target, position)
475 def move_to(target, position)
476 raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if self.new_record?
476 raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if self.new_record?
477 return if callback(:before_move) == false
477 return if callback(:before_move) == false
478 transaction do
478 transaction do
479 if target.is_a? self.class.base_class
479 if target.is_a? self.class.base_class
480 target.reload_nested_set
480 target.reload_nested_set
481 elsif position != :root
481 elsif position != :root
482 # load object if node is not an object
482 # load object if node is not an object
483 target = nested_set_scope.find(target)
483 target = nested_set_scope.find(target)
484 end
484 end
485 self.reload_nested_set
485 self.reload_nested_set
486
486
487 unless position == :root || move_possible?(target)
487 unless position == :root || move_possible?(target)
488 raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
488 raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
489 end
489 end
490
490
491 bound = case position
491 bound = case position
492 when :child; target[right_column_name]
492 when :child; target[right_column_name]
493 when :left; target[left_column_name]
493 when :left; target[left_column_name]
494 when :right; target[right_column_name] + 1
494 when :right; target[right_column_name] + 1
495 when :root; 1
495 when :root; 1
496 else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
496 else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
497 end
497 end
498
498
499 if bound > self[right_column_name]
499 if bound > self[right_column_name]
500 bound = bound - 1
500 bound = bound - 1
501 other_bound = self[right_column_name] + 1
501 other_bound = self[right_column_name] + 1
502 else
502 else
503 other_bound = self[left_column_name] - 1
503 other_bound = self[left_column_name] - 1
504 end
504 end
505
505
506 # there would be no change
506 # there would be no change
507 return if bound == self[right_column_name] || bound == self[left_column_name]
507 return if bound == self[right_column_name] || bound == self[left_column_name]
508
508
509 # we have defined the boundaries of two non-overlapping intervals,
509 # we have defined the boundaries of two non-overlapping intervals,
510 # so sorting puts both the intervals and their boundaries in order
510 # so sorting puts both the intervals and their boundaries in order
511 a, b, c, d = [self[left_column_name], self[right_column_name], bound, other_bound].sort
511 a, b, c, d = [self[left_column_name], self[right_column_name], bound, other_bound].sort
512
512
513 new_parent = case position
513 new_parent = case position
514 when :child; target.id
514 when :child; target.id
515 when :root; nil
515 when :root; nil
516 else target[parent_column_name]
516 else target[parent_column_name]
517 end
517 end
518
518
519 self.class.base_class.update_all([
519 self.class.base_class.update_all([
520 "#{quoted_left_column_name} = CASE " +
520 "#{quoted_left_column_name} = CASE " +
521 "WHEN #{quoted_left_column_name} BETWEEN :a AND :b " +
521 "WHEN #{quoted_left_column_name} BETWEEN :a AND :b " +
522 "THEN #{quoted_left_column_name} + :d - :b " +
522 "THEN #{quoted_left_column_name} + :d - :b " +
523 "WHEN #{quoted_left_column_name} BETWEEN :c AND :d " +
523 "WHEN #{quoted_left_column_name} BETWEEN :c AND :d " +
524 "THEN #{quoted_left_column_name} + :a - :c " +
524 "THEN #{quoted_left_column_name} + :a - :c " +
525 "ELSE #{quoted_left_column_name} END, " +
525 "ELSE #{quoted_left_column_name} END, " +
526 "#{quoted_right_column_name} = CASE " +
526 "#{quoted_right_column_name} = CASE " +
527 "WHEN #{quoted_right_column_name} BETWEEN :a AND :b " +
527 "WHEN #{quoted_right_column_name} BETWEEN :a AND :b " +
528 "THEN #{quoted_right_column_name} + :d - :b " +
528 "THEN #{quoted_right_column_name} + :d - :b " +
529 "WHEN #{quoted_right_column_name} BETWEEN :c AND :d " +
529 "WHEN #{quoted_right_column_name} BETWEEN :c AND :d " +
530 "THEN #{quoted_right_column_name} + :a - :c " +
530 "THEN #{quoted_right_column_name} + :a - :c " +
531 "ELSE #{quoted_right_column_name} END, " +
531 "ELSE #{quoted_right_column_name} END, " +
532 "#{quoted_parent_column_name} = CASE " +
532 "#{quoted_parent_column_name} = CASE " +
533 "WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " +
533 "WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " +
534 "ELSE #{quoted_parent_column_name} END",
534 "ELSE #{quoted_parent_column_name} END",
535 {:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent}
535 {:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent}
536 ], nested_set_scope.proxy_options[:conditions])
536 ], nested_set_scope.proxy_options[:conditions])
537 end
537 end
538 target.reload_nested_set if target
538 target.reload_nested_set if target
539 self.reload_nested_set
539 self.reload_nested_set
540 callback(:after_move)
540 callback(:after_move)
541 end
541 end
542
542
543 end
543 end
544
544
545 end
545 end
546 end
546 end
547 end
547 end
@@ -1,98 +1,98
1 # Generates a migration which migrates all plugins to their latest versions
1 # Generates a migration which migrates all plugins to their latest versions
2 # within the database.
2 # within the database.
3 class PluginMigrationGenerator < Rails::Generator::Base
3 class PluginMigrationGenerator < Rails::Generator::Base
4
4
5 # 255 characters max for Windows NTFS (http://en.wikipedia.org/wiki/Filename)
5 # 255 characters max for Windows NTFS (http://en.wikipedia.org/wiki/Filename)
6 # minus 14 for timestamp, minus some extra chars for dot, underscore, file
6 # minus 14 for timestamp, minus some extra chars for dot, underscore, file
7 # extension. So let's have 230.
7 # extension. So let's have 230.
8 MAX_FILENAME_LENGTH = 230
8 MAX_FILENAME_LENGTH = 230
9
9
10 def initialize(runtime_args, runtime_options={})
10 def initialize(runtime_args, runtime_options={})
11 super
11 super
12 @options = {:assigns => {}}
12 @options = {:assigns => {}}
13 ensure_schema_table_exists
13 ensure_schema_table_exists
14 get_plugins_to_migrate(runtime_args)
14 get_plugins_to_migrate(runtime_args)
15
15
16 if @plugins_to_migrate.empty?
16 if @plugins_to_migrate.empty?
17 puts "All plugins are migrated to their latest versions"
17 puts "All plugins are migrated to their latest versions"
18 exit(0)
18 exit(0)
19 end
19 end
20
20
21 @options[:migration_file_name] = build_migration_name
21 @options[:migration_file_name] = build_migration_name
22 @options[:assigns][:class_name] = build_migration_name.classify
22 @options[:assigns][:class_name] = build_migration_name.classify
23 end
23 end
24
24
25 def manifest
25 def manifest
26 record do |m|
26 record do |m|
27 m.migration_template 'plugin_migration.erb', 'db/migrate', @options
27 m.migration_template 'plugin_migration.erb', 'db/migrate', @options
28 end
28 end
29 end
29 end
30
30
31 protected
31 protected
32
32
33 # Create the schema table if it doesn't already exist.
33 # Create the schema table if it doesn't already exist.
34 def ensure_schema_table_exists
34 def ensure_schema_table_exists
35 ActiveRecord::Base.connection.initialize_schema_migrations_table
35 ActiveRecord::Base.connection.initialize_schema_migrations_table
36 end
36 end
37
37
38 # Determine all the plugins which have migrations that aren't present
38 # Determine all the plugins which have migrations that aren't present
39 # according to the plugin schema information from the database.
39 # according to the plugin schema information from the database.
40 def get_plugins_to_migrate(plugin_names)
40 def get_plugins_to_migrate(plugin_names)
41
41
42 # First, grab all the plugins which exist and have migrations
42 # First, grab all the plugins which exist and have migrations
43 @plugins_to_migrate = if plugin_names.empty?
43 @plugins_to_migrate = if plugin_names.empty?
44 Engines.plugins
44 Engines.plugins
45 else
45 else
46 plugin_names.map do |name|
46 plugin_names.map do |name|
47 Engines.plugins[name] ? Engines.plugins[name] : raise("Cannot find the plugin '#{name}'")
47 Engines.plugins[name] ? Engines.plugins[name] : raise("Cannot find the plugin '#{name}'")
48 end
48 end
49 end
49 end
50
50
51 @plugins_to_migrate.reject! { |p| !p.respond_to?(:latest_migration) || p.latest_migration.nil? }
51 @plugins_to_migrate.reject! { |p| !p.respond_to?(:latest_migration) || p.latest_migration.nil? }
52
52
53 # Then find the current versions from the database
53 # Then find the current versions from the database
54 @current_versions = {}
54 @current_versions = {}
55 @plugins_to_migrate.each do |plugin|
55 @plugins_to_migrate.each do |plugin|
56 @current_versions[plugin.name] = Engines::Plugin::Migrator.current_version(plugin)
56 @current_versions[plugin.name] = Engines::Plugin::Migrator.current_version(plugin)
57 end
57 end
58
58
59 # Then find the latest versions from their migration directories
59 # Then find the latest versions from their migration directories
60 @new_versions = {}
60 @new_versions = {}
61 @plugins_to_migrate.each do |plugin|
61 @plugins_to_migrate.each do |plugin|
62 @new_versions[plugin.name] = plugin.latest_migration
62 @new_versions[plugin.name] = plugin.latest_migration
63 end
63 end
64
64
65 # Remove any plugins that don't need migration
65 # Remove any plugins that don't need migration
66 @plugins_to_migrate.map { |p| p.name }.each do |name|
66 @plugins_to_migrate.map { |p| p.name }.each do |name|
67 @plugins_to_migrate.delete(Engines.plugins[name]) if @current_versions[name] == @new_versions[name]
67 @plugins_to_migrate.delete(Engines.plugins[name]) if @current_versions[name] == @new_versions[name]
68 end
68 end
69
69
70 @options[:assigns][:plugins] = @plugins_to_migrate
70 @options[:assigns][:plugins] = @plugins_to_migrate
71 @options[:assigns][:new_versions] = @new_versions
71 @options[:assigns][:new_versions] = @new_versions
72 @options[:assigns][:current_versions] = @current_versions
72 @options[:assigns][:current_versions] = @current_versions
73 end
73 end
74
74
75 # Returns a migration name. If the descriptive migration name based on the
75 # Returns a migration name. If the descriptive migration name based on the
76 # plugin names involved is shorter than 230 characters that one will be
76 # plugin names involved is shorter than 230 characters that one will be
77 # used. Otherwise a shorter name will be returned.
77 # used. Otherwise a shorter name will be returned.
78 def build_migration_name
78 def build_migration_name
79 returning descriptive_migration_name do |name|
79 descriptive_migration_name.tap do |name|
80 name.replace short_migration_name if name.length > MAX_FILENAME_LENGTH
80 name.replace short_migration_name if name.length > MAX_FILENAME_LENGTH
81 end
81 end
82 end
82 end
83
83
84 # Construct a unique migration name based on the plugins involved and the
84 # Construct a unique migration name based on the plugins involved and the
85 # versions they should reach after this migration is run. The name constructed
85 # versions they should reach after this migration is run. The name constructed
86 # needs to be lowercase
86 # needs to be lowercase
87 def descriptive_migration_name
87 def descriptive_migration_name
88 @plugins_to_migrate.map do |plugin|
88 @plugins_to_migrate.map do |plugin|
89 "#{plugin.name}_to_version_#{@new_versions[plugin.name]}"
89 "#{plugin.name}_to_version_#{@new_versions[plugin.name]}"
90 end.join("_and_").downcase
90 end.join("_and_").downcase
91 end
91 end
92
92
93 # Short migration name that will be used if the descriptive_migration_name
93 # Short migration name that will be used if the descriptive_migration_name
94 # exceeds 230 characters
94 # exceeds 230 characters
95 def short_migration_name
95 def short_migration_name
96 'plugin_migrations'
96 'plugin_migrations'
97 end
97 end
98 end No newline at end of file
98 end
@@ -1,88 +1,88
1 require 'digest/md5'
1 require 'digest/md5'
2 require 'cgi'
2 require 'cgi'
3
3
4 module GravatarHelper
4 module GravatarHelper
5
5
6 # These are the options that control the default behavior of the public
6 # These are the options that control the default behavior of the public
7 # methods. They can be overridden during the actual call to the helper,
7 # methods. They can be overridden during the actual call to the helper,
8 # or you can set them in your environment.rb as such:
8 # or you can set them in your environment.rb as such:
9 #
9 #
10 # # Allow racier gravatars
10 # # Allow racier gravatars
11 # GravatarHelper::DEFAULT_OPTIONS[:rating] = 'R'
11 # GravatarHelper::DEFAULT_OPTIONS[:rating] = 'R'
12 #
12 #
13 DEFAULT_OPTIONS = {
13 DEFAULT_OPTIONS = {
14 # The URL of a default image to display if the given email address does
14 # The URL of a default image to display if the given email address does
15 # not have a gravatar.
15 # not have a gravatar.
16 :default => nil,
16 :default => nil,
17
17
18 # The default size in pixels for the gravatar image (they're square).
18 # The default size in pixels for the gravatar image (they're square).
19 :size => 50,
19 :size => 50,
20
20
21 # The maximum allowed MPAA rating for gravatars. This allows you to
21 # The maximum allowed MPAA rating for gravatars. This allows you to
22 # exclude gravatars that may be out of character for your site.
22 # exclude gravatars that may be out of character for your site.
23 :rating => 'PG',
23 :rating => 'PG',
24
24
25 # The alt text to use in the img tag for the gravatar. Since it's a
25 # The alt text to use in the img tag for the gravatar. Since it's a
26 # decorational picture, the alt text should be empty according to the
26 # decorational picture, the alt text should be empty according to the
27 # XHTML specs.
27 # XHTML specs.
28 :alt => '',
28 :alt => '',
29
29
30 # The title text to use for the img tag for the gravatar.
30 # The title text to use for the img tag for the gravatar.
31 :title => '',
31 :title => '',
32
32
33 # The class to assign to the img tag for the gravatar.
33 # The class to assign to the img tag for the gravatar.
34 :class => 'gravatar',
34 :class => 'gravatar',
35
35
36 # Whether or not to display the gravatars using HTTPS instead of HTTP
36 # Whether or not to display the gravatars using HTTPS instead of HTTP
37 :ssl => false,
37 :ssl => false,
38 }
38 }
39
39
40 # The methods that will be made available to your views.
40 # The methods that will be made available to your views.
41 module PublicMethods
41 module PublicMethods
42
42
43 # Return the HTML img tag for the given user's gravatar. Presumes that
43 # Return the HTML img tag for the given user's gravatar. Presumes that
44 # the given user object will respond_to "email", and return the user's
44 # the given user object will respond_to "email", and return the user's
45 # email address.
45 # email address.
46 def gravatar_for(user, options={})
46 def gravatar_for(user, options={})
47 gravatar(user.email, options)
47 gravatar(user.email, options)
48 end
48 end
49
49
50 # Return the HTML img tag for the given email address's gravatar.
50 # Return the HTML img tag for the given email address's gravatar.
51 def gravatar(email, options={})
51 def gravatar(email, options={})
52 src = h(gravatar_url(email, options))
52 src = h(gravatar_url(email, options))
53 options = DEFAULT_OPTIONS.merge(options)
53 options = DEFAULT_OPTIONS.merge(options)
54 [:class, :alt, :size, :title].each { |opt| options[opt] = h(options[opt]) }
54 [:class, :alt, :size, :title].each { |opt| options[opt] = h(options[opt]) }
55 "<img class=\"#{options[:class]}\" alt=\"#{options[:alt]}\" title=\"#{options[:title]}\" width=\"#{options[:size]}\" height=\"#{options[:size]}\" src=\"#{src}\" />"
55 "<img class=\"#{options[:class]}\" alt=\"#{options[:alt]}\" title=\"#{options[:title]}\" width=\"#{options[:size]}\" height=\"#{options[:size]}\" src=\"#{src}\" />"
56 end
56 end
57
57
58 # Returns the base Gravatar URL for the given email hash. If ssl evaluates to true,
58 # Returns the base Gravatar URL for the given email hash. If ssl evaluates to true,
59 # a secure URL will be used instead. This is required when the gravatar is to be
59 # a secure URL will be used instead. This is required when the gravatar is to be
60 # displayed on a HTTPS site.
60 # displayed on a HTTPS site.
61 def gravatar_api_url(hash, ssl=false)
61 def gravatar_api_url(hash, ssl=false)
62 if ssl
62 if ssl
63 "https://secure.gravatar.com/avatar/#{hash}"
63 "https://secure.gravatar.com/avatar/#{hash}"
64 else
64 else
65 "http://www.gravatar.com/avatar/#{hash}"
65 "http://www.gravatar.com/avatar/#{hash}"
66 end
66 end
67 end
67 end
68
68
69 # Return the gravatar URL for the given email address.
69 # Return the gravatar URL for the given email address.
70 def gravatar_url(email, options={})
70 def gravatar_url(email, options={})
71 email_hash = Digest::MD5.hexdigest(email)
71 email_hash = Digest::MD5.hexdigest(email)
72 options = DEFAULT_OPTIONS.merge(options)
72 options = DEFAULT_OPTIONS.merge(options)
73 options[:default] = CGI::escape(options[:default]) unless options[:default].nil?
73 options[:default] = CGI::escape(options[:default]) unless options[:default].nil?
74 returning gravatar_api_url(email_hash, options.delete(:ssl)) do |url|
74 gravatar_api_url(email_hash, options.delete(:ssl)).tap do |url|
75 opts = []
75 opts = []
76 [:rating, :size, :default].each do |opt|
76 [:rating, :size, :default].each do |opt|
77 unless options[opt].nil?
77 unless options[opt].nil?
78 value = h(options[opt])
78 value = h(options[opt])
79 opts << [opt, value].join('=')
79 opts << [opt, value].join('=')
80 end
80 end
81 end
81 end
82 url << "?#{opts.join('&')}" unless opts.empty?
82 url << "?#{opts.join('&')}" unless opts.empty?
83 end
83 end
84 end
84 end
85
85
86 end
86 end
87
87
88 end
88 end
General Comments 0
You need to be logged in to leave comments. Login now