##// END OF EJS Templates
Fix an IndexError if all the :last menu items are deleted from a menu. #4718...
Eric Davis -
r3333:28e9bc5d823e
parent child
Show More
@@ -1,434 +1,443
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
88 # the removed child was a last item
89 def remove!(child)
90 @last_items_count -= +1 if child && child.last
91 super
92 end
93
94
87 # Will return the position (zero-based) of the current child in
95 # Will return the position (zero-based) of the current child in
88 # it's parent
96 # it's parent
89 def position
97 def position
90 self.parent.children.index(self)
98 self.parent.children.index(self)
91 end
99 end
92 end
100 end
93 end
101 end
94 Tree::TreeNode.send(:include, TreeNodePatch)
102 Tree::TreeNode.send(:include, TreeNodePatch)
95
103
96 module Redmine
104 module Redmine
97 module MenuManager
105 module MenuManager
98 class MenuError < StandardError #:nodoc:
106 class MenuError < StandardError #:nodoc:
99 end
107 end
100
108
101 module MenuController
109 module MenuController
102 def self.included(base)
110 def self.included(base)
103 base.extend(ClassMethods)
111 base.extend(ClassMethods)
104 end
112 end
105
113
106 module ClassMethods
114 module ClassMethods
107 @@menu_items = Hash.new {|hash, key| hash[key] = {:default => key, :actions => {}}}
115 @@menu_items = Hash.new {|hash, key| hash[key] = {:default => key, :actions => {}}}
108 mattr_accessor :menu_items
116 mattr_accessor :menu_items
109
117
110 # Set the menu item name for a controller or specific actions
118 # Set the menu item name for a controller or specific actions
111 # Examples:
119 # Examples:
112 # * 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
113 # * 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
114 # * 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
115 #
123 #
116 # 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
117 # Eg. the default menu item name for ProjectsController is :projects
125 # Eg. the default menu item name for ProjectsController is :projects
118 def menu_item(id, options = {})
126 def menu_item(id, options = {})
119 if actions = options[:only]
127 if actions = options[:only]
120 actions = [] << actions unless actions.is_a?(Array)
128 actions = [] << actions unless actions.is_a?(Array)
121 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}
122 else
130 else
123 menu_items[controller_name.to_sym][:default] = id
131 menu_items[controller_name.to_sym][:default] = id
124 end
132 end
125 end
133 end
126 end
134 end
127
135
128 def menu_items
136 def menu_items
129 self.class.menu_items
137 self.class.menu_items
130 end
138 end
131
139
132 # Returns the menu item name according to the current action
140 # Returns the menu item name according to the current action
133 def current_menu_item
141 def current_menu_item
134 @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] ||
135 menu_items[controller_name.to_sym][:default]
143 menu_items[controller_name.to_sym][:default]
136 end
144 end
137
145
138 # Redirects user to the menu item of the given project
146 # Redirects user to the menu item of the given project
139 # Returns false if user is not authorized
147 # Returns false if user is not authorized
140 def redirect_to_project_menu_item(project, name)
148 def redirect_to_project_menu_item(project, name)
141 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}
142 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))
143 redirect_to({item.param => project}.merge(item.url))
151 redirect_to({item.param => project}.merge(item.url))
144 return true
152 return true
145 end
153 end
146 false
154 false
147 end
155 end
148 end
156 end
149
157
150 module MenuHelper
158 module MenuHelper
151 # Returns the current menu item name
159 # Returns the current menu item name
152 def current_menu_item
160 def current_menu_item
153 @controller.current_menu_item
161 @controller.current_menu_item
154 end
162 end
155
163
156 # Renders the application main menu
164 # Renders the application main menu
157 def render_main_menu(project)
165 def render_main_menu(project)
158 render_menu((project && !project.new_record?) ? :project_menu : :application_menu, project)
166 render_menu((project && !project.new_record?) ? :project_menu : :application_menu, project)
159 end
167 end
160
168
161 def render_menu(menu, project=nil)
169 def render_menu(menu, project=nil)
162 links = []
170 links = []
163 menu_items_for(menu, project) do |node|
171 menu_items_for(menu, project) do |node|
164 links << render_menu_node(node, project)
172 links << render_menu_node(node, project)
165 end
173 end
166 links.empty? ? nil : content_tag('ul', links.join("\n"))
174 links.empty? ? nil : content_tag('ul', links.join("\n"))
167 end
175 end
168
176
169 def render_menu_node(node, project=nil)
177 def render_menu_node(node, project=nil)
170 if node.hasChildren? || !node.child_menus.nil?
178 if node.hasChildren? || !node.child_menus.nil?
171 return render_menu_node_with_children(node, project)
179 return render_menu_node_with_children(node, project)
172 else
180 else
173 caption, url, selected = extract_node_details(node, project)
181 caption, url, selected = extract_node_details(node, project)
174 return content_tag('li',
182 return content_tag('li',
175 render_single_menu_node(node, caption, url, selected))
183 render_single_menu_node(node, caption, url, selected))
176 end
184 end
177 end
185 end
178
186
179 def render_menu_node_with_children(node, project=nil)
187 def render_menu_node_with_children(node, project=nil)
180 caption, url, selected = extract_node_details(node, project)
188 caption, url, selected = extract_node_details(node, project)
181
189
182 html = returning [] do |html|
190 html = returning [] do |html|
183 html << '<li>'
191 html << '<li>'
184 # Parent
192 # Parent
185 html << render_single_menu_node(node, caption, url, selected)
193 html << render_single_menu_node(node, caption, url, selected)
186
194
187 # Standard children
195 # Standard children
188 standard_children_list = returning "" do |child_html|
196 standard_children_list = returning "" do |child_html|
189 node.children.each do |child|
197 node.children.each do |child|
190 child_html << render_menu_node(child, project)
198 child_html << render_menu_node(child, project)
191 end
199 end
192 end
200 end
193
201
194 html << content_tag(:ul, standard_children_list, :class => 'menu-children') unless standard_children_list.empty?
202 html << content_tag(:ul, standard_children_list, :class => 'menu-children') unless standard_children_list.empty?
195
203
196 # Unattached children
204 # Unattached children
197 unattached_children_list = render_unattached_children_menu(node, project)
205 unattached_children_list = render_unattached_children_menu(node, project)
198 html << content_tag(:ul, unattached_children_list, :class => 'menu-children unattached') unless unattached_children_list.blank?
206 html << content_tag(:ul, unattached_children_list, :class => 'menu-children unattached') unless unattached_children_list.blank?
199
207
200 html << '</li>'
208 html << '</li>'
201 end
209 end
202 return html.join("\n")
210 return html.join("\n")
203 end
211 end
204
212
205 # Returns a list of unattached children menu items
213 # Returns a list of unattached children menu items
206 def render_unattached_children_menu(node, project)
214 def render_unattached_children_menu(node, project)
207 return nil unless node.child_menus
215 return nil unless node.child_menus
208
216
209 returning "" do |child_html|
217 returning "" do |child_html|
210 unattached_children = node.child_menus.call(project)
218 unattached_children = node.child_menus.call(project)
211 # Tree nodes support #each so we need to do object detection
219 # Tree nodes support #each so we need to do object detection
212 if unattached_children.is_a? Array
220 if unattached_children.is_a? Array
213 unattached_children.each do |child|
221 unattached_children.each do |child|
214 child_html << content_tag(:li, render_unattached_menu_item(child, project))
222 child_html << content_tag(:li, render_unattached_menu_item(child, project))
215 end
223 end
216 else
224 else
217 raise MenuError, ":child_menus must be an array of MenuItems"
225 raise MenuError, ":child_menus must be an array of MenuItems"
218 end
226 end
219 end
227 end
220 end
228 end
221
229
222 def render_single_menu_node(item, caption, url, selected)
230 def render_single_menu_node(item, caption, url, selected)
223 link_to(h(caption), url, item.html_options(:selected => selected))
231 link_to(h(caption), url, item.html_options(:selected => selected))
224 end
232 end
225
233
226 def render_unattached_menu_item(menu_item, project)
234 def render_unattached_menu_item(menu_item, project)
227 raise MenuError, ":child_menus must be an array of MenuItems" unless menu_item.is_a? MenuItem
235 raise MenuError, ":child_menus must be an array of MenuItems" unless menu_item.is_a? MenuItem
228
236
229 if User.current.allowed_to?(menu_item.url, project)
237 if User.current.allowed_to?(menu_item.url, project)
230 link_to(h(menu_item.caption),
238 link_to(h(menu_item.caption),
231 menu_item.url,
239 menu_item.url,
232 menu_item.html_options)
240 menu_item.html_options)
233 end
241 end
234 end
242 end
235
243
236 def menu_items_for(menu, project=nil)
244 def menu_items_for(menu, project=nil)
237 items = []
245 items = []
238 Redmine::MenuManager.items(menu).root.children.each do |node|
246 Redmine::MenuManager.items(menu).root.children.each do |node|
239 if allowed_node?(node, User.current, project)
247 if allowed_node?(node, User.current, project)
240 if block_given?
248 if block_given?
241 yield node
249 yield node
242 else
250 else
243 items << node # TODO: not used?
251 items << node # TODO: not used?
244 end
252 end
245 end
253 end
246 end
254 end
247 return block_given? ? nil : items
255 return block_given? ? nil : items
248 end
256 end
249
257
250 def extract_node_details(node, project=nil)
258 def extract_node_details(node, project=nil)
251 item = node
259 item = node
252 url = case item.url
260 url = case item.url
253 when Hash
261 when Hash
254 project.nil? ? item.url : {item.param => project}.merge(item.url)
262 project.nil? ? item.url : {item.param => project}.merge(item.url)
255 when Symbol
263 when Symbol
256 send(item.url)
264 send(item.url)
257 else
265 else
258 item.url
266 item.url
259 end
267 end
260 caption = item.caption(project)
268 caption = item.caption(project)
261 return [caption, url, (current_menu_item == item.name)]
269 return [caption, url, (current_menu_item == item.name)]
262 end
270 end
263
271
264 # Checks if a user is allowed to access the menu item by:
272 # Checks if a user is allowed to access the menu item by:
265 #
273 #
266 # * Checking the conditions of the item
274 # * Checking the conditions of the item
267 # * Checking the url target (project only)
275 # * Checking the url target (project only)
268 def allowed_node?(node, user, project)
276 def allowed_node?(node, user, project)
269 if node.condition && !node.condition.call(project)
277 if node.condition && !node.condition.call(project)
270 # Condition that doesn't pass
278 # Condition that doesn't pass
271 return false
279 return false
272 end
280 end
273
281
274 if project
282 if project
275 return user && user.allowed_to?(node.url, project)
283 return user && user.allowed_to?(node.url, project)
276 else
284 else
277 # outside a project, all menu items allowed
285 # outside a project, all menu items allowed
278 return true
286 return true
279 end
287 end
280 end
288 end
281 end
289 end
282
290
283 class << self
291 class << self
284 def map(menu_name)
292 def map(menu_name)
285 @items ||= {}
293 @items ||= {}
286 mapper = Mapper.new(menu_name.to_sym, @items)
294 mapper = Mapper.new(menu_name.to_sym, @items)
287 if block_given?
295 if block_given?
288 yield mapper
296 yield mapper
289 else
297 else
290 mapper
298 mapper
291 end
299 end
292 end
300 end
293
301
294 def items(menu_name)
302 def items(menu_name)
295 @items[menu_name.to_sym] || Tree::TreeNode.new(:root, {})
303 @items[menu_name.to_sym] || Tree::TreeNode.new(:root, {})
296 end
304 end
297 end
305 end
298
306
299 class Mapper
307 class Mapper
300 def initialize(menu, items)
308 def initialize(menu, items)
301 items[menu] ||= Tree::TreeNode.new(:root, {})
309 items[menu] ||= Tree::TreeNode.new(:root, {})
302 @menu = menu
310 @menu = menu
303 @menu_items = items[menu]
311 @menu_items = items[menu]
304 end
312 end
305
313
306 @@last_items_count = Hash.new {|h,k| h[k] = 0}
314 @@last_items_count = Hash.new {|h,k| h[k] = 0}
307
315
308 # Adds an item at the end of the menu. Available options:
316 # Adds an item at the end of the menu. Available options:
309 # * param: the parameter name that is used for the project id (default is :id)
317 # * param: the parameter name that is used for the project id (default is :id)
310 # * if: a Proc that is called before rendering the item, the item is displayed only if it returns true
318 # * if: a Proc that is called before rendering the item, the item is displayed only if it returns true
311 # * caption that can be:
319 # * caption that can be:
312 # * a localized string Symbol
320 # * a localized string Symbol
313 # * a String
321 # * a String
314 # * a Proc that can take the project as argument
322 # * a Proc that can take the project as argument
315 # * before, after: specify where the menu item should be inserted (eg. :after => :activity)
323 # * before, after: specify where the menu item should be inserted (eg. :after => :activity)
316 # * parent: menu item will be added as a child of another named menu (eg. :parent => :issues)
324 # * parent: menu item will be added as a child of another named menu (eg. :parent => :issues)
317 # * 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.
325 # * 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.
318 # eg. :children => Proc.new {|project| [Redmine::MenuManager::MenuItem.new(...)] }
326 # eg. :children => Proc.new {|project| [Redmine::MenuManager::MenuItem.new(...)] }
319 # * last: menu item will stay at the end (eg. :last => true)
327 # * last: menu item will stay at the end (eg. :last => true)
320 # * html_options: a hash of html options that are passed to link_to
328 # * html_options: a hash of html options that are passed to link_to
321 def push(name, url, options={})
329 def push(name, url, options={})
322 options = options.dup
330 options = options.dup
323
331
324 if options[:parent]
332 if options[:parent]
325 subtree = self.find(options[:parent])
333 subtree = self.find(options[:parent])
326 if subtree
334 if subtree
327 target_root = subtree
335 target_root = subtree
328 else
336 else
329 target_root = @menu_items.root
337 target_root = @menu_items.root
330 end
338 end
331
339
332 else
340 else
333 target_root = @menu_items.root
341 target_root = @menu_items.root
334 end
342 end
335
343
336 # menu item position
344 # menu item position
337 if first = options.delete(:first)
345 if first = options.delete(:first)
338 target_root.prepend(MenuItem.new(name, url, options))
346 target_root.prepend(MenuItem.new(name, url, options))
339 elsif before = options.delete(:before)
347 elsif before = options.delete(:before)
340
348
341 if exists?(before)
349 if exists?(before)
342 target_root.add_at(MenuItem.new(name, url, options), position_of(before))
350 target_root.add_at(MenuItem.new(name, url, options), position_of(before))
343 else
351 else
344 target_root.add(MenuItem.new(name, url, options))
352 target_root.add(MenuItem.new(name, url, options))
345 end
353 end
346
354
347 elsif after = options.delete(:after)
355 elsif after = options.delete(:after)
348
356
349 if exists?(after)
357 if exists?(after)
350 target_root.add_at(MenuItem.new(name, url, options), position_of(after) + 1)
358 target_root.add_at(MenuItem.new(name, url, options), position_of(after) + 1)
351 else
359 else
352 target_root.add(MenuItem.new(name, url, options))
360 target_root.add(MenuItem.new(name, url, options))
353 end
361 end
354
362
355 elsif options.delete(:last)
363 elsif options[:last] # don't delete, needs to be stored
356 target_root.add_last(MenuItem.new(name, url, options))
364 target_root.add_last(MenuItem.new(name, url, options))
357 else
365 else
358 target_root.add(MenuItem.new(name, url, options))
366 target_root.add(MenuItem.new(name, url, options))
359 end
367 end
360 end
368 end
361
369
362 # Removes a menu item
370 # Removes a menu item
363 def delete(name)
371 def delete(name)
364 if found = self.find(name)
372 if found = self.find(name)
365 @menu_items.remove!(found)
373 @menu_items.remove!(found)
366 end
374 end
367 end
375 end
368
376
369 # Checks if a menu item exists
377 # Checks if a menu item exists
370 def exists?(name)
378 def exists?(name)
371 @menu_items.any? {|node| node.name == name}
379 @menu_items.any? {|node| node.name == name}
372 end
380 end
373
381
374 def find(name)
382 def find(name)
375 @menu_items.find {|node| node.name == name}
383 @menu_items.find {|node| node.name == name}
376 end
384 end
377
385
378 def position_of(name)
386 def position_of(name)
379 @menu_items.each do |node|
387 @menu_items.each do |node|
380 if node.name == name
388 if node.name == name
381 return node.position
389 return node.position
382 end
390 end
383 end
391 end
384 end
392 end
385 end
393 end
386
394
387 class MenuItem < Tree::TreeNode
395 class MenuItem < Tree::TreeNode
388 include Redmine::I18n
396 include Redmine::I18n
389 attr_reader :name, :url, :param, :condition, :parent, :child_menus
397 attr_reader :name, :url, :param, :condition, :parent, :child_menus, :last
390
398
391 def initialize(name, url, options)
399 def initialize(name, url, options)
392 raise ArgumentError, "Invalid option :if for menu item '#{name}'" if options[:if] && !options[:if].respond_to?(:call)
400 raise ArgumentError, "Invalid option :if for menu item '#{name}'" if options[:if] && !options[:if].respond_to?(:call)
393 raise ArgumentError, "Invalid option :html for menu item '#{name}'" if options[:html] && !options[:html].is_a?(Hash)
401 raise ArgumentError, "Invalid option :html for menu item '#{name}'" if options[:html] && !options[:html].is_a?(Hash)
394 raise ArgumentError, "Cannot set the :parent to be the same as this item" if options[:parent] == name.to_sym
402 raise ArgumentError, "Cannot set the :parent to be the same as this item" if options[:parent] == name.to_sym
395 raise ArgumentError, "Invalid option :children for menu item '#{name}'" if options[:children] && !options[:children].respond_to?(:call)
403 raise ArgumentError, "Invalid option :children for menu item '#{name}'" if options[:children] && !options[:children].respond_to?(:call)
396 @name = name
404 @name = name
397 @url = url
405 @url = url
398 @condition = options[:if]
406 @condition = options[:if]
399 @param = options[:param] || :id
407 @param = options[:param] || :id
400 @caption = options[:caption]
408 @caption = options[:caption]
401 @html_options = options[:html] || {}
409 @html_options = options[:html] || {}
402 # Adds a unique class to each menu item based on its name
410 # Adds a unique class to each menu item based on its name
403 @html_options[:class] = [@html_options[:class], @name.to_s.dasherize].compact.join(' ')
411 @html_options[:class] = [@html_options[:class], @name.to_s.dasherize].compact.join(' ')
404 @parent = options[:parent]
412 @parent = options[:parent]
405 @child_menus = options[:children]
413 @child_menus = options[:children]
414 @last = options[:last] || false
406 super @name.to_sym
415 super @name.to_sym
407 end
416 end
408
417
409 def caption(project=nil)
418 def caption(project=nil)
410 if @caption.is_a?(Proc)
419 if @caption.is_a?(Proc)
411 c = @caption.call(project).to_s
420 c = @caption.call(project).to_s
412 c = @name.to_s.humanize if c.blank?
421 c = @name.to_s.humanize if c.blank?
413 c
422 c
414 else
423 else
415 if @caption.nil?
424 if @caption.nil?
416 l_or_humanize(name, :prefix => 'label_')
425 l_or_humanize(name, :prefix => 'label_')
417 else
426 else
418 @caption.is_a?(Symbol) ? l(@caption) : @caption
427 @caption.is_a?(Symbol) ? l(@caption) : @caption
419 end
428 end
420 end
429 end
421 end
430 end
422
431
423 def html_options(options={})
432 def html_options(options={})
424 if options[:selected]
433 if options[:selected]
425 o = @html_options.dup
434 o = @html_options.dup
426 o[:class] += ' selected'
435 o[:class] += ' selected'
427 o
436 o
428 else
437 else
429 @html_options
438 @html_options
430 end
439 end
431 end
440 end
432 end
441 end
433 end
442 end
434 end
443 end
@@ -1,166 +1,183
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 Jean-Philippe Lang
2 # Copyright (C) 2006-2009 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 File.dirname(__FILE__) + '/../../../../test_helper'
18 require File.dirname(__FILE__) + '/../../../../test_helper'
19
19
20 class Redmine::MenuManager::MapperTest < Test::Unit::TestCase
20 class Redmine::MenuManager::MapperTest < ActiveSupport::TestCase
21 context "Mapper#initialize" do
21 context "Mapper#initialize" do
22 should "be tested"
22 should "be tested"
23 end
23 end
24
24
25 def test_push_onto_root
25 def test_push_onto_root
26 menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
26 menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
27 menu_mapper.push :test_overview, { :controller => 'projects', :action => 'show'}, {}
27 menu_mapper.push :test_overview, { :controller => 'projects', :action => 'show'}, {}
28
28
29 menu_mapper.exists?(:test_overview)
29 menu_mapper.exists?(:test_overview)
30 end
30 end
31
31
32 def test_push_onto_parent
32 def test_push_onto_parent
33 menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
33 menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
34 menu_mapper.push :test_overview, { :controller => 'projects', :action => 'show'}, {}
34 menu_mapper.push :test_overview, { :controller => 'projects', :action => 'show'}, {}
35 menu_mapper.push :test_child, { :controller => 'projects', :action => 'show'}, {:parent => :test_overview}
35 menu_mapper.push :test_child, { :controller => 'projects', :action => 'show'}, {:parent => :test_overview}
36
36
37 assert menu_mapper.exists?(:test_child)
37 assert menu_mapper.exists?(:test_child)
38 assert_equal :test_child, menu_mapper.find(:test_child).name
38 assert_equal :test_child, menu_mapper.find(:test_child).name
39 end
39 end
40
40
41 def test_push_onto_grandparent
41 def test_push_onto_grandparent
42 menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
42 menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
43 menu_mapper.push :test_overview, { :controller => 'projects', :action => 'show'}, {}
43 menu_mapper.push :test_overview, { :controller => 'projects', :action => 'show'}, {}
44 menu_mapper.push :test_child, { :controller => 'projects', :action => 'show'}, {:parent => :test_overview}
44 menu_mapper.push :test_child, { :controller => 'projects', :action => 'show'}, {:parent => :test_overview}
45 menu_mapper.push :test_grandchild, { :controller => 'projects', :action => 'show'}, {:parent => :test_child}
45 menu_mapper.push :test_grandchild, { :controller => 'projects', :action => 'show'}, {:parent => :test_child}
46
46
47 assert menu_mapper.exists?(:test_grandchild)
47 assert menu_mapper.exists?(:test_grandchild)
48 grandchild = menu_mapper.find(:test_grandchild)
48 grandchild = menu_mapper.find(:test_grandchild)
49 assert_equal :test_grandchild, grandchild.name
49 assert_equal :test_grandchild, grandchild.name
50 assert_equal :test_child, grandchild.parent.name
50 assert_equal :test_child, grandchild.parent.name
51 end
51 end
52
52
53 def test_push_first
53 def test_push_first
54 menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
54 menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
55 menu_mapper.push :test_second, { :controller => 'projects', :action => 'show'}, {}
55 menu_mapper.push :test_second, { :controller => 'projects', :action => 'show'}, {}
56 menu_mapper.push :test_third, { :controller => 'projects', :action => 'show'}, {}
56 menu_mapper.push :test_third, { :controller => 'projects', :action => 'show'}, {}
57 menu_mapper.push :test_fourth, { :controller => 'projects', :action => 'show'}, {}
57 menu_mapper.push :test_fourth, { :controller => 'projects', :action => 'show'}, {}
58 menu_mapper.push :test_fifth, { :controller => 'projects', :action => 'show'}, {}
58 menu_mapper.push :test_fifth, { :controller => 'projects', :action => 'show'}, {}
59 menu_mapper.push :test_first, { :controller => 'projects', :action => 'show'}, {:first => true}
59 menu_mapper.push :test_first, { :controller => 'projects', :action => 'show'}, {:first => true}
60
60
61 root = menu_mapper.find(:root)
61 root = menu_mapper.find(:root)
62 assert_equal 5, root.children.size
62 assert_equal 5, root.children.size
63 {0 => :test_first, 1 => :test_second, 2 => :test_third, 3 => :test_fourth, 4 => :test_fifth}.each do |position, name|
63 {0 => :test_first, 1 => :test_second, 2 => :test_third, 3 => :test_fourth, 4 => :test_fifth}.each do |position, name|
64 assert_not_nil root.children[position]
64 assert_not_nil root.children[position]
65 assert_equal name, root.children[position].name
65 assert_equal name, root.children[position].name
66 end
66 end
67
67
68 end
68 end
69
69
70 def test_push_before
70 def test_push_before
71 menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
71 menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
72 menu_mapper.push :test_first, { :controller => 'projects', :action => 'show'}, {}
72 menu_mapper.push :test_first, { :controller => 'projects', :action => 'show'}, {}
73 menu_mapper.push :test_second, { :controller => 'projects', :action => 'show'}, {}
73 menu_mapper.push :test_second, { :controller => 'projects', :action => 'show'}, {}
74 menu_mapper.push :test_fourth, { :controller => 'projects', :action => 'show'}, {}
74 menu_mapper.push :test_fourth, { :controller => 'projects', :action => 'show'}, {}
75 menu_mapper.push :test_fifth, { :controller => 'projects', :action => 'show'}, {}
75 menu_mapper.push :test_fifth, { :controller => 'projects', :action => 'show'}, {}
76 menu_mapper.push :test_third, { :controller => 'projects', :action => 'show'}, {:before => :test_fourth}
76 menu_mapper.push :test_third, { :controller => 'projects', :action => 'show'}, {:before => :test_fourth}
77
77
78 root = menu_mapper.find(:root)
78 root = menu_mapper.find(:root)
79 assert_equal 5, root.children.size
79 assert_equal 5, root.children.size
80 {0 => :test_first, 1 => :test_second, 2 => :test_third, 3 => :test_fourth, 4 => :test_fifth}.each do |position, name|
80 {0 => :test_first, 1 => :test_second, 2 => :test_third, 3 => :test_fourth, 4 => :test_fifth}.each do |position, name|
81 assert_not_nil root.children[position]
81 assert_not_nil root.children[position]
82 assert_equal name, root.children[position].name
82 assert_equal name, root.children[position].name
83 end
83 end
84
84
85 end
85 end
86
86
87 def test_push_after
87 def test_push_after
88 menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
88 menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
89 menu_mapper.push :test_first, { :controller => 'projects', :action => 'show'}, {}
89 menu_mapper.push :test_first, { :controller => 'projects', :action => 'show'}, {}
90 menu_mapper.push :test_second, { :controller => 'projects', :action => 'show'}, {}
90 menu_mapper.push :test_second, { :controller => 'projects', :action => 'show'}, {}
91 menu_mapper.push :test_third, { :controller => 'projects', :action => 'show'}, {}
91 menu_mapper.push :test_third, { :controller => 'projects', :action => 'show'}, {}
92 menu_mapper.push :test_fifth, { :controller => 'projects', :action => 'show'}, {}
92 menu_mapper.push :test_fifth, { :controller => 'projects', :action => 'show'}, {}
93 menu_mapper.push :test_fourth, { :controller => 'projects', :action => 'show'}, {:after => :test_third}
93 menu_mapper.push :test_fourth, { :controller => 'projects', :action => 'show'}, {:after => :test_third}
94
94
95
95
96 root = menu_mapper.find(:root)
96 root = menu_mapper.find(:root)
97 assert_equal 5, root.children.size
97 assert_equal 5, root.children.size
98 {0 => :test_first, 1 => :test_second, 2 => :test_third, 3 => :test_fourth, 4 => :test_fifth}.each do |position, name|
98 {0 => :test_first, 1 => :test_second, 2 => :test_third, 3 => :test_fourth, 4 => :test_fifth}.each do |position, name|
99 assert_not_nil root.children[position]
99 assert_not_nil root.children[position]
100 assert_equal name, root.children[position].name
100 assert_equal name, root.children[position].name
101 end
101 end
102
102
103 end
103 end
104
104
105 def test_push_last
105 def test_push_last
106 menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
106 menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
107 menu_mapper.push :test_first, { :controller => 'projects', :action => 'show'}, {}
107 menu_mapper.push :test_first, { :controller => 'projects', :action => 'show'}, {}
108 menu_mapper.push :test_second, { :controller => 'projects', :action => 'show'}, {}
108 menu_mapper.push :test_second, { :controller => 'projects', :action => 'show'}, {}
109 menu_mapper.push :test_third, { :controller => 'projects', :action => 'show'}, {}
109 menu_mapper.push :test_third, { :controller => 'projects', :action => 'show'}, {}
110 menu_mapper.push :test_fifth, { :controller => 'projects', :action => 'show'}, {:last => true}
110 menu_mapper.push :test_fifth, { :controller => 'projects', :action => 'show'}, {:last => true}
111 menu_mapper.push :test_fourth, { :controller => 'projects', :action => 'show'}, {}
111 menu_mapper.push :test_fourth, { :controller => 'projects', :action => 'show'}, {}
112
112
113 root = menu_mapper.find(:root)
113 root = menu_mapper.find(:root)
114 assert_equal 5, root.children.size
114 assert_equal 5, root.children.size
115 {0 => :test_first, 1 => :test_second, 2 => :test_third, 3 => :test_fourth, 4 => :test_fifth}.each do |position, name|
115 {0 => :test_first, 1 => :test_second, 2 => :test_third, 3 => :test_fourth, 4 => :test_fifth}.each do |position, name|
116 assert_not_nil root.children[position]
116 assert_not_nil root.children[position]
117 assert_equal name, root.children[position].name
117 assert_equal name, root.children[position].name
118 end
118 end
119
119
120 end
120 end
121
121
122 def test_exists_for_child_node
122 def test_exists_for_child_node
123 menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
123 menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
124 menu_mapper.push :test_overview, { :controller => 'projects', :action => 'show'}, {}
124 menu_mapper.push :test_overview, { :controller => 'projects', :action => 'show'}, {}
125 menu_mapper.push :test_child, { :controller => 'projects', :action => 'show'}, {:parent => :test_overview }
125 menu_mapper.push :test_child, { :controller => 'projects', :action => 'show'}, {:parent => :test_overview }
126
126
127 assert menu_mapper.exists?(:test_child)
127 assert menu_mapper.exists?(:test_child)
128 end
128 end
129
129
130 def test_exists_for_invalid_node
130 def test_exists_for_invalid_node
131 menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
131 menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
132 menu_mapper.push :test_overview, { :controller => 'projects', :action => 'show'}, {}
132 menu_mapper.push :test_overview, { :controller => 'projects', :action => 'show'}, {}
133
133
134 assert !menu_mapper.exists?(:nothing)
134 assert !menu_mapper.exists?(:nothing)
135 end
135 end
136
136
137 def test_find
137 def test_find
138 menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
138 menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
139 menu_mapper.push :test_overview, { :controller => 'projects', :action => 'show'}, {}
139 menu_mapper.push :test_overview, { :controller => 'projects', :action => 'show'}, {}
140
140
141 item = menu_mapper.find(:test_overview)
141 item = menu_mapper.find(:test_overview)
142 assert_equal :test_overview, item.name
142 assert_equal :test_overview, item.name
143 assert_equal({:controller => 'projects', :action => 'show'}, item.url)
143 assert_equal({:controller => 'projects', :action => 'show'}, item.url)
144 end
144 end
145
145
146 def test_find_missing
146 def test_find_missing
147 menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
147 menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
148 menu_mapper.push :test_overview, { :controller => 'projects', :action => 'show'}, {}
148 menu_mapper.push :test_overview, { :controller => 'projects', :action => 'show'}, {}
149
149
150 item = menu_mapper.find(:nothing)
150 item = menu_mapper.find(:nothing)
151 assert_equal nil, item
151 assert_equal nil, item
152 end
152 end
153
153
154 def test_delete
154 def test_delete
155 menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
155 menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
156 menu_mapper.push :test_overview, { :controller => 'projects', :action => 'show'}, {}
156 menu_mapper.push :test_overview, { :controller => 'projects', :action => 'show'}, {}
157 assert_not_nil menu_mapper.delete(:test_overview)
157 assert_not_nil menu_mapper.delete(:test_overview)
158
158
159 assert_nil menu_mapper.find(:test_overview)
159 assert_nil menu_mapper.find(:test_overview)
160 end
160 end
161
161
162 def test_delete_missing
162 def test_delete_missing
163 menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
163 menu_mapper = Redmine::MenuManager::Mapper.new(:test_menu, {})
164 assert_nil menu_mapper.delete(:test_missing)
164 assert_nil menu_mapper.delete(:test_missing)
165 end
165 end
166
167 test 'deleting all items' do
168 # Exposed by deleting :last items
169 Redmine::MenuManager.map :test_menu do |menu|
170 menu.push :not_last, Redmine::Info.help_url
171 menu.push :administration, { :controller => 'projects', :action => 'show'}, {:last => true}
172 menu.push :help, Redmine::Info.help_url, :last => true
173 end
174
175 assert_nothing_raised do
176 Redmine::MenuManager.map :test_menu do |menu|
177 menu.delete(:administration)
178 menu.delete(:help)
179 menu.push :test_overview, { :controller => 'projects', :action => 'show'}, {}
180 end
181 end
182 end
166 end
183 end
General Comments 0
You need to be logged in to leave comments. Login now