File: lib/redmine/menu_manager.rb

Overview
Module Structure
Class Hierarchy
Code

Overview

Module Structure

  module: <Toplevel Module>
  module: Redmine#18
  module: MenuManager#19
has properties
module method: map / 1 #211
module method: items / 1 #221
  class: MenuError#20
inherits from
  StandardError ( Builtin-Module )
  module: MenuController#23
has properties
module method: included / 1 #24
method: menu_items #50
method: current_menu_item #55
method: redirect_to_project_menu_item / 2 #62
  module: ClassMethods#28
has properties
method: menu_item / 2 #40
  module: MenuHelper#72
has properties
method: current_menu_item #74
method: render_main_menu / 1 #79
method: display_main_menu? / 1 #83
method: render_menu / 2 #88
method: render_menu_node / 2 #96
method: render_menu_node_with_children / 2 #106
method: render_unattached_children_menu / 2 #133
method: render_single_menu_node / 4 #149
method: render_unattached_menu_item / 2 #153
method: menu_items_for / 2 #163
method: extract_node_details / 2 #177
method: allowed_node? / 3 #195
  class: Mapper#226
inherits from
  Object ( Builtin-Module )
has properties
method: initialize / 2 #227
method: push / 3 #246
method: delete / 1 #288
method: exists? / 1 #295
method: find / 1 #299
method: position_of / 1 #303
  class: MenuNode#312
includes
  Enumerable ( Builtin-Module )
inherits from
  Object ( Builtin-Module )
has properties
attribute: parent [RW] #314
attribute: last_items_count [R] #315
attribute: name [R] #315
method: initialize / 2 #317
method: children #323
method: size #332
method: each #336
method: prepend / 1 #342
method: add_at / 2 #347
method: add_last / 1 #356
method: add / 1 #363
alias: << add #367
method: remove! / 1 #370
method: position #378
method: root #383
  class: MenuItem#390
includes
  I18n ( Redmine )
inherits from
  MenuNode ( Redmine::MenuManager )
has properties
attribute: name [R] #392
attribute: url [R] #392
attribute: param [R] #392
attribute: condition [R] #392
attribute: parent [R] #392
attribute: child_menus [R] #392
attribute: last [R] #392
method: initialize / 3 #394
method: caption / 1 #413
method: html_options / 1 #427

Code

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