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