annotate lib/redmine/menu_manager.rb @ 8:0c83d98252d9 yuya

* Add custom repo prefix and proper auth realm, remove auth cache (seems like an unwise feature), pass DB handle around, various other bits of tidying
author Chris Cannam
date Thu, 12 Aug 2010 15:31:37 +0100
parents 513646585e45
children 94944d00e43c
rev   line source
Chris@0 1 # redMine - project management software
Chris@0 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
Chris@0 3 #
Chris@0 4 # This program is free software; you can redistribute it and/or
Chris@0 5 # modify it under the terms of the GNU General Public License
Chris@0 6 # as published by the Free Software Foundation; either version 2
Chris@0 7 # of the License, or (at your option) any later version.
Chris@0 8 #
Chris@0 9 # This program is distributed in the hope that it will be useful,
Chris@0 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
Chris@0 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Chris@0 12 # GNU General Public License for more details.
Chris@0 13 #
Chris@0 14 # You should have received a copy of the GNU General Public License
Chris@0 15 # along with this program; if not, write to the Free Software
Chris@0 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
Chris@0 17
Chris@0 18 require 'tree' # gem install rubytree
Chris@0 19
Chris@0 20 # Monkey patch the TreeNode to add on a few more methods :nodoc:
Chris@0 21 module TreeNodePatch
Chris@0 22 def self.included(base)
Chris@0 23 base.class_eval do
Chris@0 24 attr_reader :last_items_count
Chris@0 25
Chris@0 26 alias :old_initilize :initialize
Chris@0 27 def initialize(name, content = nil)
Chris@0 28 old_initilize(name, content)
Chris@0 29 @last_items_count = 0
Chris@0 30 extend(InstanceMethods)
Chris@0 31 end
Chris@0 32 end
Chris@0 33 end
Chris@0 34
Chris@0 35 module InstanceMethods
Chris@0 36 # Adds the specified child node to the receiver node. The child node's
Chris@0 37 # parent is set to be the receiver. The child is added as the first child in
Chris@0 38 # the current list of children for the receiver node.
Chris@0 39 def prepend(child)
Chris@0 40 raise "Child already added" if @childrenHash.has_key?(child.name)
Chris@0 41
Chris@0 42 @childrenHash[child.name] = child
Chris@0 43 @children = [child] + @children
Chris@0 44 child.parent = self
Chris@0 45 return child
Chris@0 46
Chris@0 47 end
Chris@0 48
Chris@0 49 # Adds the specified child node to the receiver node. The child node's
Chris@0 50 # parent is set to be the receiver. The child is added at the position
Chris@0 51 # into the current list of children for the receiver node.
Chris@0 52 def add_at(child, position)
Chris@0 53 raise "Child already added" if @childrenHash.has_key?(child.name)
Chris@0 54
Chris@0 55 @childrenHash[child.name] = child
Chris@0 56 @children = @children.insert(position, child)
Chris@0 57 child.parent = self
Chris@0 58 return child
Chris@0 59
Chris@0 60 end
Chris@0 61
Chris@0 62 def add_last(child)
Chris@0 63 raise "Child already added" if @childrenHash.has_key?(child.name)
Chris@0 64
Chris@0 65 @childrenHash[child.name] = child
Chris@0 66 @children << child
Chris@0 67 @last_items_count += 1
Chris@0 68 child.parent = self
Chris@0 69 return child
Chris@0 70
Chris@0 71 end
Chris@0 72
Chris@0 73 # Adds the specified child node to the receiver node. The child node's
Chris@0 74 # parent is set to be the receiver. The child is added as the last child in
Chris@0 75 # the current list of children for the receiver node.
Chris@0 76 def add(child)
Chris@0 77 raise "Child already added" if @childrenHash.has_key?(child.name)
Chris@0 78
Chris@0 79 @childrenHash[child.name] = child
Chris@0 80 position = @children.size - @last_items_count
Chris@0 81 @children.insert(position, child)
Chris@0 82 child.parent = self
Chris@0 83 return child
Chris@0 84
Chris@0 85 end
Chris@0 86
Chris@0 87 # Wrapp remove! making sure to decrement the last_items counter if
Chris@0 88 # the removed child was a last item
Chris@0 89 def remove!(child)
Chris@0 90 @last_items_count -= +1 if child && child.last
Chris@0 91 super
Chris@0 92 end
Chris@0 93
Chris@0 94
Chris@0 95 # Will return the position (zero-based) of the current child in
Chris@0 96 # it's parent
Chris@0 97 def position
Chris@0 98 self.parent.children.index(self)
Chris@0 99 end
Chris@0 100 end
Chris@0 101 end
Chris@0 102 Tree::TreeNode.send(:include, TreeNodePatch)
Chris@0 103
Chris@0 104 module Redmine
Chris@0 105 module MenuManager
Chris@0 106 class MenuError < StandardError #:nodoc:
Chris@0 107 end
Chris@0 108
Chris@0 109 module MenuController
Chris@0 110 def self.included(base)
Chris@0 111 base.extend(ClassMethods)
Chris@0 112 end
Chris@0 113
Chris@0 114 module ClassMethods
Chris@0 115 @@menu_items = Hash.new {|hash, key| hash[key] = {:default => key, :actions => {}}}
Chris@0 116 mattr_accessor :menu_items
Chris@0 117
Chris@0 118 # Set the menu item name for a controller or specific actions
Chris@0 119 # Examples:
Chris@0 120 # * menu_item :tickets # => sets the menu name to :tickets for the whole controller
Chris@0 121 # * menu_item :tickets, :only => :list # => sets the menu name to :tickets for the 'list' action only
Chris@0 122 # * menu_item :tickets, :only => [:list, :show] # => sets the menu name to :tickets for 2 actions only
Chris@0 123 #
Chris@0 124 # The default menu item name for a controller is controller_name by default
Chris@0 125 # Eg. the default menu item name for ProjectsController is :projects
Chris@0 126 def menu_item(id, options = {})
Chris@0 127 if actions = options[:only]
Chris@0 128 actions = [] << actions unless actions.is_a?(Array)
Chris@0 129 actions.each {|a| menu_items[controller_name.to_sym][:actions][a.to_sym] = id}
Chris@0 130 else
Chris@0 131 menu_items[controller_name.to_sym][:default] = id
Chris@0 132 end
Chris@0 133 end
Chris@0 134 end
Chris@0 135
Chris@0 136 def menu_items
Chris@0 137 self.class.menu_items
Chris@0 138 end
Chris@0 139
Chris@0 140 # Returns the menu item name according to the current action
Chris@0 141 def current_menu_item
Chris@0 142 @current_menu_item ||= menu_items[controller_name.to_sym][:actions][action_name.to_sym] ||
Chris@0 143 menu_items[controller_name.to_sym][:default]
Chris@0 144 end
Chris@0 145
Chris@0 146 # Redirects user to the menu item of the given project
Chris@0 147 # Returns false if user is not authorized
Chris@0 148 def redirect_to_project_menu_item(project, name)
Chris@0 149 item = Redmine::MenuManager.items(:project_menu).detect {|i| i.name.to_s == name.to_s}
Chris@0 150 if item && User.current.allowed_to?(item.url, project) && (item.condition.nil? || item.condition.call(project))
Chris@0 151 redirect_to({item.param => project}.merge(item.url))
Chris@0 152 return true
Chris@0 153 end
Chris@0 154 false
Chris@0 155 end
Chris@0 156 end
Chris@0 157
Chris@0 158 module MenuHelper
Chris@0 159 # Returns the current menu item name
Chris@0 160 def current_menu_item
Chris@0 161 @controller.current_menu_item
Chris@0 162 end
Chris@0 163
Chris@0 164 # Renders the application main menu
Chris@0 165 def render_main_menu(project)
Chris@0 166 render_menu((project && !project.new_record?) ? :project_menu : :application_menu, project)
Chris@0 167 end
Chris@0 168
Chris@0 169 def display_main_menu?(project)
Chris@0 170 menu_name = project && !project.new_record? ? :project_menu : :application_menu
Chris@0 171 Redmine::MenuManager.items(menu_name).size > 1 # 1 element is the root
Chris@0 172 end
Chris@0 173
Chris@0 174 def render_menu(menu, project=nil)
Chris@0 175 links = []
Chris@0 176 menu_items_for(menu, project) do |node|
Chris@0 177 links << render_menu_node(node, project)
Chris@0 178 end
Chris@0 179 links.empty? ? nil : content_tag('ul', links.join("\n"))
Chris@0 180 end
Chris@0 181
Chris@0 182 def render_menu_node(node, project=nil)
Chris@0 183 if node.hasChildren? || !node.child_menus.nil?
Chris@0 184 return render_menu_node_with_children(node, project)
Chris@0 185 else
Chris@0 186 caption, url, selected = extract_node_details(node, project)
Chris@0 187 return content_tag('li',
Chris@0 188 render_single_menu_node(node, caption, url, selected))
Chris@0 189 end
Chris@0 190 end
Chris@0 191
Chris@0 192 def render_menu_node_with_children(node, project=nil)
Chris@0 193 caption, url, selected = extract_node_details(node, project)
Chris@0 194
Chris@0 195 html = returning [] do |html|
Chris@0 196 html << '<li>'
Chris@0 197 # Parent
Chris@0 198 html << render_single_menu_node(node, caption, url, selected)
Chris@0 199
Chris@0 200 # Standard children
Chris@0 201 standard_children_list = returning "" do |child_html|
Chris@0 202 node.children.each do |child|
Chris@0 203 child_html << render_menu_node(child, project)
Chris@0 204 end
Chris@0 205 end
Chris@0 206
Chris@0 207 html << content_tag(:ul, standard_children_list, :class => 'menu-children') unless standard_children_list.empty?
Chris@0 208
Chris@0 209 # Unattached children
Chris@0 210 unattached_children_list = render_unattached_children_menu(node, project)
Chris@0 211 html << content_tag(:ul, unattached_children_list, :class => 'menu-children unattached') unless unattached_children_list.blank?
Chris@0 212
Chris@0 213 html << '</li>'
Chris@0 214 end
Chris@0 215 return html.join("\n")
Chris@0 216 end
Chris@0 217
Chris@0 218 # Returns a list of unattached children menu items
Chris@0 219 def render_unattached_children_menu(node, project)
Chris@0 220 return nil unless node.child_menus
Chris@0 221
Chris@0 222 returning "" do |child_html|
Chris@0 223 unattached_children = node.child_menus.call(project)
Chris@0 224 # Tree nodes support #each so we need to do object detection
Chris@0 225 if unattached_children.is_a? Array
Chris@0 226 unattached_children.each do |child|
Chris@0 227 child_html << content_tag(:li, render_unattached_menu_item(child, project))
Chris@0 228 end
Chris@0 229 else
Chris@0 230 raise MenuError, ":child_menus must be an array of MenuItems"
Chris@0 231 end
Chris@0 232 end
Chris@0 233 end
Chris@0 234
Chris@0 235 def render_single_menu_node(item, caption, url, selected)
Chris@0 236 link_to(h(caption), url, item.html_options(:selected => selected))
Chris@0 237 end
Chris@0 238
Chris@0 239 def render_unattached_menu_item(menu_item, project)
Chris@0 240 raise MenuError, ":child_menus must be an array of MenuItems" unless menu_item.is_a? MenuItem
Chris@0 241
Chris@0 242 if User.current.allowed_to?(menu_item.url, project)
Chris@0 243 link_to(h(menu_item.caption),
Chris@0 244 menu_item.url,
Chris@0 245 menu_item.html_options)
Chris@0 246 end
Chris@0 247 end
Chris@0 248
Chris@0 249 def menu_items_for(menu, project=nil)
Chris@0 250 items = []
Chris@0 251 Redmine::MenuManager.items(menu).root.children.each do |node|
Chris@0 252 if allowed_node?(node, User.current, project)
Chris@0 253 if block_given?
Chris@0 254 yield node
Chris@0 255 else
Chris@0 256 items << node # TODO: not used?
Chris@0 257 end
Chris@0 258 end
Chris@0 259 end
Chris@0 260 return block_given? ? nil : items
Chris@0 261 end
Chris@0 262
Chris@0 263 def extract_node_details(node, project=nil)
Chris@0 264 item = node
Chris@0 265 url = case item.url
Chris@0 266 when Hash
Chris@0 267 project.nil? ? item.url : {item.param => project}.merge(item.url)
Chris@0 268 when Symbol
Chris@0 269 send(item.url)
Chris@0 270 else
Chris@0 271 item.url
Chris@0 272 end
Chris@0 273 caption = item.caption(project)
Chris@0 274 return [caption, url, (current_menu_item == item.name)]
Chris@0 275 end
Chris@0 276
Chris@0 277 # Checks if a user is allowed to access the menu item by:
Chris@0 278 #
Chris@0 279 # * Checking the conditions of the item
Chris@0 280 # * Checking the url target (project only)
Chris@0 281 def allowed_node?(node, user, project)
Chris@0 282 if node.condition && !node.condition.call(project)
Chris@0 283 # Condition that doesn't pass
Chris@0 284 return false
Chris@0 285 end
Chris@0 286
Chris@0 287 if project
Chris@0 288 return user && user.allowed_to?(node.url, project)
Chris@0 289 else
Chris@0 290 # outside a project, all menu items allowed
Chris@0 291 return true
Chris@0 292 end
Chris@0 293 end
Chris@0 294 end
Chris@0 295
Chris@0 296 class << self
Chris@0 297 def map(menu_name)
Chris@0 298 @items ||= {}
Chris@0 299 mapper = Mapper.new(menu_name.to_sym, @items)
Chris@0 300 if block_given?
Chris@0 301 yield mapper
Chris@0 302 else
Chris@0 303 mapper
Chris@0 304 end
Chris@0 305 end
Chris@0 306
Chris@0 307 def items(menu_name)
Chris@0 308 @items[menu_name.to_sym] || Tree::TreeNode.new(:root, {})
Chris@0 309 end
Chris@0 310 end
Chris@0 311
Chris@0 312 class Mapper
Chris@0 313 def initialize(menu, items)
Chris@0 314 items[menu] ||= Tree::TreeNode.new(:root, {})
Chris@0 315 @menu = menu
Chris@0 316 @menu_items = items[menu]
Chris@0 317 end
Chris@0 318
Chris@0 319 @@last_items_count = Hash.new {|h,k| h[k] = 0}
Chris@0 320
Chris@0 321 # Adds an item at the end of the menu. Available options:
Chris@0 322 # * param: the parameter name that is used for the project id (default is :id)
Chris@0 323 # * if: a Proc that is called before rendering the item, the item is displayed only if it returns true
Chris@0 324 # * caption that can be:
Chris@0 325 # * a localized string Symbol
Chris@0 326 # * a String
Chris@0 327 # * a Proc that can take the project as argument
Chris@0 328 # * before, after: specify where the menu item should be inserted (eg. :after => :activity)
Chris@0 329 # * parent: menu item will be added as a child of another named menu (eg. :parent => :issues)
Chris@0 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.
Chris@0 331 # eg. :children => Proc.new {|project| [Redmine::MenuManager::MenuItem.new(...)] }
Chris@0 332 # * last: menu item will stay at the end (eg. :last => true)
Chris@0 333 # * html_options: a hash of html options that are passed to link_to
Chris@0 334 def push(name, url, options={})
Chris@0 335 options = options.dup
Chris@0 336
Chris@0 337 if options[:parent]
Chris@0 338 subtree = self.find(options[:parent])
Chris@0 339 if subtree
Chris@0 340 target_root = subtree
Chris@0 341 else
Chris@0 342 target_root = @menu_items.root
Chris@0 343 end
Chris@0 344
Chris@0 345 else
Chris@0 346 target_root = @menu_items.root
Chris@0 347 end
Chris@0 348
Chris@0 349 # menu item position
Chris@0 350 if first = options.delete(:first)
Chris@0 351 target_root.prepend(MenuItem.new(name, url, options))
Chris@0 352 elsif before = options.delete(:before)
Chris@0 353
Chris@0 354 if exists?(before)
Chris@0 355 target_root.add_at(MenuItem.new(name, url, options), position_of(before))
Chris@0 356 else
Chris@0 357 target_root.add(MenuItem.new(name, url, options))
Chris@0 358 end
Chris@0 359
Chris@0 360 elsif after = options.delete(:after)
Chris@0 361
Chris@0 362 if exists?(after)
Chris@0 363 target_root.add_at(MenuItem.new(name, url, options), position_of(after) + 1)
Chris@0 364 else
Chris@0 365 target_root.add(MenuItem.new(name, url, options))
Chris@0 366 end
Chris@0 367
Chris@0 368 elsif options[:last] # don't delete, needs to be stored
Chris@0 369 target_root.add_last(MenuItem.new(name, url, options))
Chris@0 370 else
Chris@0 371 target_root.add(MenuItem.new(name, url, options))
Chris@0 372 end
Chris@0 373 end
Chris@0 374
Chris@0 375 # Removes a menu item
Chris@0 376 def delete(name)
Chris@0 377 if found = self.find(name)
Chris@0 378 @menu_items.remove!(found)
Chris@0 379 end
Chris@0 380 end
Chris@0 381
Chris@0 382 # Checks if a menu item exists
Chris@0 383 def exists?(name)
Chris@0 384 @menu_items.any? {|node| node.name == name}
Chris@0 385 end
Chris@0 386
Chris@0 387 def find(name)
Chris@0 388 @menu_items.find {|node| node.name == name}
Chris@0 389 end
Chris@0 390
Chris@0 391 def position_of(name)
Chris@0 392 @menu_items.each do |node|
Chris@0 393 if node.name == name
Chris@0 394 return node.position
Chris@0 395 end
Chris@0 396 end
Chris@0 397 end
Chris@0 398 end
Chris@0 399
Chris@0 400 class MenuItem < Tree::TreeNode
Chris@0 401 include Redmine::I18n
Chris@0 402 attr_reader :name, :url, :param, :condition, :parent, :child_menus, :last
Chris@0 403
Chris@0 404 def initialize(name, url, options)
Chris@0 405 raise ArgumentError, "Invalid option :if for menu item '#{name}'" if options[:if] && !options[:if].respond_to?(:call)
Chris@0 406 raise ArgumentError, "Invalid option :html for menu item '#{name}'" if options[:html] && !options[:html].is_a?(Hash)
Chris@0 407 raise ArgumentError, "Cannot set the :parent to be the same as this item" if options[:parent] == name.to_sym
Chris@0 408 raise ArgumentError, "Invalid option :children for menu item '#{name}'" if options[:children] && !options[:children].respond_to?(:call)
Chris@0 409 @name = name
Chris@0 410 @url = url
Chris@0 411 @condition = options[:if]
Chris@0 412 @param = options[:param] || :id
Chris@0 413 @caption = options[:caption]
Chris@0 414 @html_options = options[:html] || {}
Chris@0 415 # Adds a unique class to each menu item based on its name
Chris@0 416 @html_options[:class] = [@html_options[:class], @name.to_s.dasherize].compact.join(' ')
Chris@0 417 @parent = options[:parent]
Chris@0 418 @child_menus = options[:children]
Chris@0 419 @last = options[:last] || false
Chris@0 420 super @name.to_sym
Chris@0 421 end
Chris@0 422
Chris@0 423 def caption(project=nil)
Chris@0 424 if @caption.is_a?(Proc)
Chris@0 425 c = @caption.call(project).to_s
Chris@0 426 c = @name.to_s.humanize if c.blank?
Chris@0 427 c
Chris@0 428 else
Chris@0 429 if @caption.nil?
Chris@0 430 l_or_humanize(name, :prefix => 'label_')
Chris@0 431 else
Chris@0 432 @caption.is_a?(Symbol) ? l(@caption) : @caption
Chris@0 433 end
Chris@0 434 end
Chris@0 435 end
Chris@0 436
Chris@0 437 def html_options(options={})
Chris@0 438 if options[:selected]
Chris@0 439 o = @html_options.dup
Chris@0 440 o[:class] += ' selected'
Chris@0 441 o
Chris@0 442 else
Chris@0 443 @html_options
Chris@0 444 end
Chris@0 445 end
Chris@0 446 end
Chris@0 447 end
Chris@0 448 end