To check out this repository please hg clone the following URL, or open the URL using EasyMercurial or your preferred Mercurial client.

Statistics Download as Zip
| Branch: | Tag: | Revision:

root / lib / redmine / menu_manager.rb @ 912:5e80956cc792

History | View | Annotate | Download (14.8 KB)

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