##// END OF EJS Templates
Use Object#tap instead of #returning (#6887)....
Jean-Philippe Lang -
r4292:2ee45e8cac90
parent child
Show More
@@ -1,152 +1,152
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 module Redmine
19 19 module Hook
20 20 include ActionController::UrlWriter
21 21
22 22 @@listener_classes = []
23 23 @@listeners = nil
24 24 @@hook_listeners = {}
25 25
26 26 class << self
27 27 # Adds a listener class.
28 28 # Automatically called when a class inherits from Redmine::Hook::Listener.
29 29 def add_listener(klass)
30 30 raise "Hooks must include Singleton module." unless klass.included_modules.include?(Singleton)
31 31 @@listener_classes << klass
32 32 clear_listeners_instances
33 33 end
34 34
35 35 # Returns all the listerners instances.
36 36 def listeners
37 37 @@listeners ||= @@listener_classes.collect {|listener| listener.instance}
38 38 end
39 39
40 40 # Returns the listeners instances for the given hook.
41 41 def hook_listeners(hook)
42 42 @@hook_listeners[hook] ||= listeners.select {|listener| listener.respond_to?(hook)}
43 43 end
44 44
45 45 # Clears all the listeners.
46 46 def clear_listeners
47 47 @@listener_classes = []
48 48 clear_listeners_instances
49 49 end
50 50
51 51 # Clears all the listeners instances.
52 52 def clear_listeners_instances
53 53 @@listeners = nil
54 54 @@hook_listeners = {}
55 55 end
56 56
57 57 # Calls a hook.
58 58 # Returns the listeners response.
59 59 def call_hook(hook, context={})
60 returning [] do |response|
60 [].tap do |response|
61 61 hls = hook_listeners(hook)
62 62 if hls.any?
63 63 hls.each {|listener| response << listener.send(hook, context)}
64 64 end
65 65 end
66 66 end
67 67 end
68 68
69 69 # Base class for hook listeners.
70 70 class Listener
71 71 include Singleton
72 72 include Redmine::I18n
73 73
74 74 # Registers the listener
75 75 def self.inherited(child)
76 76 Redmine::Hook.add_listener(child)
77 77 super
78 78 end
79 79
80 80 end
81 81
82 82 # Listener class used for views hooks.
83 83 # Listeners that inherit this class will include various helpers by default.
84 84 class ViewListener < Listener
85 85 include ERB::Util
86 86 include ActionView::Helpers::TagHelper
87 87 include ActionView::Helpers::FormHelper
88 88 include ActionView::Helpers::FormTagHelper
89 89 include ActionView::Helpers::FormOptionsHelper
90 90 include ActionView::Helpers::JavaScriptHelper
91 91 include ActionView::Helpers::PrototypeHelper
92 92 include ActionView::Helpers::NumberHelper
93 93 include ActionView::Helpers::UrlHelper
94 94 include ActionView::Helpers::AssetTagHelper
95 95 include ActionView::Helpers::TextHelper
96 96 include ActionController::UrlWriter
97 97 include ApplicationHelper
98 98
99 99 # Default to creating links using only the path. Subclasses can
100 100 # change this default as needed
101 101 def self.default_url_options
102 102 {:only_path => true }
103 103 end
104 104
105 105 # Helper method to directly render a partial using the context:
106 106 #
107 107 # class MyHook < Redmine::Hook::ViewListener
108 108 # render_on :view_issues_show_details_bottom, :partial => "show_more_data"
109 109 # end
110 110 #
111 111 def self.render_on(hook, options={})
112 112 define_method hook do |context|
113 113 context[:controller].send(:render_to_string, {:locals => context}.merge(options))
114 114 end
115 115 end
116 116 end
117 117
118 118 # Helper module included in ApplicationHelper and ActionControllerso that
119 119 # hooks can be called in views like this:
120 120 #
121 121 # <%= call_hook(:some_hook) %>
122 122 # <%= call_hook(:another_hook, :foo => 'bar' %>
123 123 #
124 124 # Or in controllers like:
125 125 # call_hook(:some_hook)
126 126 # call_hook(:another_hook, :foo => 'bar'
127 127 #
128 128 # Hooks added to views will be concatenated into a string. Hooks added to
129 129 # controllers will return an array of results.
130 130 #
131 131 # Several objects are automatically added to the call context:
132 132 #
133 133 # * project => current project
134 134 # * request => Request instance
135 135 # * controller => current Controller instance
136 136 #
137 137 module Helper
138 138 def call_hook(hook, context={})
139 139 if is_a?(ActionController::Base)
140 140 default_context = {:controller => self, :project => @project, :request => request}
141 141 Redmine::Hook.call_hook(hook, default_context.merge(context))
142 142 else
143 143 default_context = {:controller => controller, :project => @project, :request => request}
144 144 Redmine::Hook.call_hook(hook, default_context.merge(context)).join(' ')
145 145 end
146 146 end
147 147 end
148 148 end
149 149 end
150 150
151 151 ApplicationHelper.send(:include, Redmine::Hook::Helper)
152 152 ActionController::Base.send(:include, Redmine::Hook::Helper)
@@ -1,448 +1,448
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'tree' # gem install rubytree
19 19
20 20 # Monkey patch the TreeNode to add on a few more methods :nodoc:
21 21 module TreeNodePatch
22 22 def self.included(base)
23 23 base.class_eval do
24 24 attr_reader :last_items_count
25 25
26 26 alias :old_initilize :initialize
27 27 def initialize(name, content = nil)
28 28 old_initilize(name, content)
29 29 @last_items_count = 0
30 30 extend(InstanceMethods)
31 31 end
32 32 end
33 33 end
34 34
35 35 module InstanceMethods
36 36 # Adds the specified child node to the receiver node. The child node's
37 37 # parent is set to be the receiver. The child is added as the first child in
38 38 # the current list of children for the receiver node.
39 39 def prepend(child)
40 40 raise "Child already added" if @childrenHash.has_key?(child.name)
41 41
42 42 @childrenHash[child.name] = child
43 43 @children = [child] + @children
44 44 child.parent = self
45 45 return child
46 46
47 47 end
48 48
49 49 # Adds the specified child node to the receiver node. The child node's
50 50 # parent is set to be the receiver. The child is added at the position
51 51 # into the current list of children for the receiver node.
52 52 def add_at(child, position)
53 53 raise "Child already added" if @childrenHash.has_key?(child.name)
54 54
55 55 @childrenHash[child.name] = child
56 56 @children = @children.insert(position, child)
57 57 child.parent = self
58 58 return child
59 59
60 60 end
61 61
62 62 def add_last(child)
63 63 raise "Child already added" if @childrenHash.has_key?(child.name)
64 64
65 65 @childrenHash[child.name] = child
66 66 @children << child
67 67 @last_items_count += 1
68 68 child.parent = self
69 69 return child
70 70
71 71 end
72 72
73 73 # Adds the specified child node to the receiver node. The child node's
74 74 # parent is set to be the receiver. The child is added as the last child in
75 75 # the current list of children for the receiver node.
76 76 def add(child)
77 77 raise "Child already added" if @childrenHash.has_key?(child.name)
78 78
79 79 @childrenHash[child.name] = child
80 80 position = @children.size - @last_items_count
81 81 @children.insert(position, child)
82 82 child.parent = self
83 83 return child
84 84
85 85 end
86 86
87 87 # Wrapp remove! making sure to decrement the last_items counter if
88 88 # the removed child was a last item
89 89 def remove!(child)
90 90 @last_items_count -= +1 if child && child.last
91 91 super
92 92 end
93 93
94 94
95 95 # Will return the position (zero-based) of the current child in
96 96 # it's parent
97 97 def position
98 98 self.parent.children.index(self)
99 99 end
100 100 end
101 101 end
102 102 Tree::TreeNode.send(:include, TreeNodePatch)
103 103
104 104 module Redmine
105 105 module MenuManager
106 106 class MenuError < StandardError #:nodoc:
107 107 end
108 108
109 109 module MenuController
110 110 def self.included(base)
111 111 base.extend(ClassMethods)
112 112 end
113 113
114 114 module ClassMethods
115 115 @@menu_items = Hash.new {|hash, key| hash[key] = {:default => key, :actions => {}}}
116 116 mattr_accessor :menu_items
117 117
118 118 # Set the menu item name for a controller or specific actions
119 119 # Examples:
120 120 # * menu_item :tickets # => sets the menu name to :tickets for the whole controller
121 121 # * menu_item :tickets, :only => :list # => sets the menu name to :tickets for the 'list' action only
122 122 # * menu_item :tickets, :only => [:list, :show] # => sets the menu name to :tickets for 2 actions only
123 123 #
124 124 # The default menu item name for a controller is controller_name by default
125 125 # Eg. the default menu item name for ProjectsController is :projects
126 126 def menu_item(id, options = {})
127 127 if actions = options[:only]
128 128 actions = [] << actions unless actions.is_a?(Array)
129 129 actions.each {|a| menu_items[controller_name.to_sym][:actions][a.to_sym] = id}
130 130 else
131 131 menu_items[controller_name.to_sym][:default] = id
132 132 end
133 133 end
134 134 end
135 135
136 136 def menu_items
137 137 self.class.menu_items
138 138 end
139 139
140 140 # Returns the menu item name according to the current action
141 141 def current_menu_item
142 142 @current_menu_item ||= menu_items[controller_name.to_sym][:actions][action_name.to_sym] ||
143 143 menu_items[controller_name.to_sym][:default]
144 144 end
145 145
146 146 # Redirects user to the menu item of the given project
147 147 # Returns false if user is not authorized
148 148 def redirect_to_project_menu_item(project, name)
149 149 item = Redmine::MenuManager.items(:project_menu).detect {|i| i.name.to_s == name.to_s}
150 150 if item && User.current.allowed_to?(item.url, project) && (item.condition.nil? || item.condition.call(project))
151 151 redirect_to({item.param => project}.merge(item.url))
152 152 return true
153 153 end
154 154 false
155 155 end
156 156 end
157 157
158 158 module MenuHelper
159 159 # Returns the current menu item name
160 160 def current_menu_item
161 161 @controller.current_menu_item
162 162 end
163 163
164 164 # Renders the application main menu
165 165 def render_main_menu(project)
166 166 render_menu((project && !project.new_record?) ? :project_menu : :application_menu, project)
167 167 end
168 168
169 169 def display_main_menu?(project)
170 170 menu_name = project && !project.new_record? ? :project_menu : :application_menu
171 171 Redmine::MenuManager.items(menu_name).size > 1 # 1 element is the root
172 172 end
173 173
174 174 def render_menu(menu, project=nil)
175 175 links = []
176 176 menu_items_for(menu, project) do |node|
177 177 links << render_menu_node(node, project)
178 178 end
179 179 links.empty? ? nil : content_tag('ul', links.join("\n"))
180 180 end
181 181
182 182 def render_menu_node(node, project=nil)
183 183 if node.hasChildren? || !node.child_menus.nil?
184 184 return render_menu_node_with_children(node, project)
185 185 else
186 186 caption, url, selected = extract_node_details(node, project)
187 187 return content_tag('li',
188 188 render_single_menu_node(node, caption, url, selected))
189 189 end
190 190 end
191 191
192 192 def render_menu_node_with_children(node, project=nil)
193 193 caption, url, selected = extract_node_details(node, project)
194 194
195 html = returning [] do |html|
195 html = [].tap do |html|
196 196 html << '<li>'
197 197 # Parent
198 198 html << render_single_menu_node(node, caption, url, selected)
199 199
200 200 # Standard children
201 standard_children_list = returning "" do |child_html|
201 standard_children_list = "".tap do |child_html|
202 202 node.children.each do |child|
203 203 child_html << render_menu_node(child, project)
204 204 end
205 205 end
206 206
207 207 html << content_tag(:ul, standard_children_list, :class => 'menu-children') unless standard_children_list.empty?
208 208
209 209 # Unattached children
210 210 unattached_children_list = render_unattached_children_menu(node, project)
211 211 html << content_tag(:ul, unattached_children_list, :class => 'menu-children unattached') unless unattached_children_list.blank?
212 212
213 213 html << '</li>'
214 214 end
215 215 return html.join("\n")
216 216 end
217 217
218 218 # Returns a list of unattached children menu items
219 219 def render_unattached_children_menu(node, project)
220 220 return nil unless node.child_menus
221 221
222 returning "" do |child_html|
222 "".tap do |child_html|
223 223 unattached_children = node.child_menus.call(project)
224 224 # Tree nodes support #each so we need to do object detection
225 225 if unattached_children.is_a? Array
226 226 unattached_children.each do |child|
227 227 child_html << content_tag(:li, render_unattached_menu_item(child, project))
228 228 end
229 229 else
230 230 raise MenuError, ":child_menus must be an array of MenuItems"
231 231 end
232 232 end
233 233 end
234 234
235 235 def render_single_menu_node(item, caption, url, selected)
236 236 link_to(h(caption), url, item.html_options(:selected => selected))
237 237 end
238 238
239 239 def render_unattached_menu_item(menu_item, project)
240 240 raise MenuError, ":child_menus must be an array of MenuItems" unless menu_item.is_a? MenuItem
241 241
242 242 if User.current.allowed_to?(menu_item.url, project)
243 243 link_to(h(menu_item.caption),
244 244 menu_item.url,
245 245 menu_item.html_options)
246 246 end
247 247 end
248 248
249 249 def menu_items_for(menu, project=nil)
250 250 items = []
251 251 Redmine::MenuManager.items(menu).root.children.each do |node|
252 252 if allowed_node?(node, User.current, project)
253 253 if block_given?
254 254 yield node
255 255 else
256 256 items << node # TODO: not used?
257 257 end
258 258 end
259 259 end
260 260 return block_given? ? nil : items
261 261 end
262 262
263 263 def extract_node_details(node, project=nil)
264 264 item = node
265 265 url = case item.url
266 266 when Hash
267 267 project.nil? ? item.url : {item.param => project}.merge(item.url)
268 268 when Symbol
269 269 send(item.url)
270 270 else
271 271 item.url
272 272 end
273 273 caption = item.caption(project)
274 274 return [caption, url, (current_menu_item == item.name)]
275 275 end
276 276
277 277 # Checks if a user is allowed to access the menu item by:
278 278 #
279 279 # * Checking the conditions of the item
280 280 # * Checking the url target (project only)
281 281 def allowed_node?(node, user, project)
282 282 if node.condition && !node.condition.call(project)
283 283 # Condition that doesn't pass
284 284 return false
285 285 end
286 286
287 287 if project
288 288 return user && user.allowed_to?(node.url, project)
289 289 else
290 290 # outside a project, all menu items allowed
291 291 return true
292 292 end
293 293 end
294 294 end
295 295
296 296 class << self
297 297 def map(menu_name)
298 298 @items ||= {}
299 299 mapper = Mapper.new(menu_name.to_sym, @items)
300 300 if block_given?
301 301 yield mapper
302 302 else
303 303 mapper
304 304 end
305 305 end
306 306
307 307 def items(menu_name)
308 308 @items[menu_name.to_sym] || Tree::TreeNode.new(:root, {})
309 309 end
310 310 end
311 311
312 312 class Mapper
313 313 def initialize(menu, items)
314 314 items[menu] ||= Tree::TreeNode.new(:root, {})
315 315 @menu = menu
316 316 @menu_items = items[menu]
317 317 end
318 318
319 319 @@last_items_count = Hash.new {|h,k| h[k] = 0}
320 320
321 321 # Adds an item at the end of the menu. Available options:
322 322 # * param: the parameter name that is used for the project id (default is :id)
323 323 # * if: a Proc that is called before rendering the item, the item is displayed only if it returns true
324 324 # * caption that can be:
325 325 # * a localized string Symbol
326 326 # * a String
327 327 # * a Proc that can take the project as argument
328 328 # * before, after: specify where the menu item should be inserted (eg. :after => :activity)
329 329 # * parent: menu item will be added as a child of another named menu (eg. :parent => :issues)
330 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.
331 331 # eg. :children => Proc.new {|project| [Redmine::MenuManager::MenuItem.new(...)] }
332 332 # * last: menu item will stay at the end (eg. :last => true)
333 333 # * html_options: a hash of html options that are passed to link_to
334 334 def push(name, url, options={})
335 335 options = options.dup
336 336
337 337 if options[:parent]
338 338 subtree = self.find(options[:parent])
339 339 if subtree
340 340 target_root = subtree
341 341 else
342 342 target_root = @menu_items.root
343 343 end
344 344
345 345 else
346 346 target_root = @menu_items.root
347 347 end
348 348
349 349 # menu item position
350 350 if first = options.delete(:first)
351 351 target_root.prepend(MenuItem.new(name, url, options))
352 352 elsif before = options.delete(:before)
353 353
354 354 if exists?(before)
355 355 target_root.add_at(MenuItem.new(name, url, options), position_of(before))
356 356 else
357 357 target_root.add(MenuItem.new(name, url, options))
358 358 end
359 359
360 360 elsif after = options.delete(:after)
361 361
362 362 if exists?(after)
363 363 target_root.add_at(MenuItem.new(name, url, options), position_of(after) + 1)
364 364 else
365 365 target_root.add(MenuItem.new(name, url, options))
366 366 end
367 367
368 368 elsif options[:last] # don't delete, needs to be stored
369 369 target_root.add_last(MenuItem.new(name, url, options))
370 370 else
371 371 target_root.add(MenuItem.new(name, url, options))
372 372 end
373 373 end
374 374
375 375 # Removes a menu item
376 376 def delete(name)
377 377 if found = self.find(name)
378 378 @menu_items.remove!(found)
379 379 end
380 380 end
381 381
382 382 # Checks if a menu item exists
383 383 def exists?(name)
384 384 @menu_items.any? {|node| node.name == name}
385 385 end
386 386
387 387 def find(name)
388 388 @menu_items.find {|node| node.name == name}
389 389 end
390 390
391 391 def position_of(name)
392 392 @menu_items.each do |node|
393 393 if node.name == name
394 394 return node.position
395 395 end
396 396 end
397 397 end
398 398 end
399 399
400 400 class MenuItem < Tree::TreeNode
401 401 include Redmine::I18n
402 402 attr_reader :name, :url, :param, :condition, :parent, :child_menus, :last
403 403
404 404 def initialize(name, url, options)
405 405 raise ArgumentError, "Invalid option :if for menu item '#{name}'" if options[:if] && !options[:if].respond_to?(:call)
406 406 raise ArgumentError, "Invalid option :html for menu item '#{name}'" if options[:html] && !options[:html].is_a?(Hash)
407 407 raise ArgumentError, "Cannot set the :parent to be the same as this item" if options[:parent] == name.to_sym
408 408 raise ArgumentError, "Invalid option :children for menu item '#{name}'" if options[:children] && !options[:children].respond_to?(:call)
409 409 @name = name
410 410 @url = url
411 411 @condition = options[:if]
412 412 @param = options[:param] || :id
413 413 @caption = options[:caption]
414 414 @html_options = options[:html] || {}
415 415 # Adds a unique class to each menu item based on its name
416 416 @html_options[:class] = [@html_options[:class], @name.to_s.dasherize].compact.join(' ')
417 417 @parent = options[:parent]
418 418 @child_menus = options[:children]
419 419 @last = options[:last] || false
420 420 super @name.to_sym
421 421 end
422 422
423 423 def caption(project=nil)
424 424 if @caption.is_a?(Proc)
425 425 c = @caption.call(project).to_s
426 426 c = @name.to_s.humanize if c.blank?
427 427 c
428 428 else
429 429 if @caption.nil?
430 430 l_or_humanize(name, :prefix => 'label_')
431 431 else
432 432 @caption.is_a?(Symbol) ? l(@caption) : @caption
433 433 end
434 434 end
435 435 end
436 436
437 437 def html_options(options={})
438 438 if options[:selected]
439 439 o = @html_options.dup
440 440 o[:class] += ' selected'
441 441 o
442 442 else
443 443 @html_options
444 444 end
445 445 end
446 446 end
447 447 end
448 448 end
@@ -1,31 +1,31
1 1 class Project < ActiveRecord::Base
2 2 generator_for :name, :method => :next_name
3 3 generator_for :identifier, :method => :next_identifier_from_object_daddy
4 4 generator_for :enabled_modules, :method => :all_modules
5 5 generator_for :trackers, :method => :next_tracker
6 6
7 7 def self.next_name
8 8 @last_name ||= 'Project 0'
9 9 @last_name.succ!
10 10 @last_name
11 11 end
12 12
13 13 # Project#next_identifier is defined on Redmine
14 14 def self.next_identifier_from_object_daddy
15 15 @last_identifier ||= 'project-0000'
16 16 @last_identifier.succ!
17 17 @last_identifier
18 18 end
19 19
20 20 def self.all_modules
21 returning [] do |modules|
21 [].tap do |modules|
22 22 Redmine::AccessControl.available_project_modules.each do |name|
23 23 modules << EnabledModule.new(:name => name.to_s)
24 24 end
25 25 end
26 26 end
27 27
28 28 def self.next_tracker
29 29 [Tracker.generate!]
30 30 end
31 31 end
@@ -1,547 +1,547
1 1 module CollectiveIdea #:nodoc:
2 2 module Acts #:nodoc:
3 3 module NestedSet #:nodoc:
4 4 def self.included(base)
5 5 base.extend(SingletonMethods)
6 6 end
7 7
8 8 # This acts provides Nested Set functionality. Nested Set is a smart way to implement
9 9 # an _ordered_ tree, with the added feature that you can select the children and all of their
10 10 # descendants with a single query. The drawback is that insertion or move need some complex
11 11 # sql queries. But everything is done here by this module!
12 12 #
13 13 # Nested sets are appropriate each time you want either an orderd tree (menus,
14 14 # commercial categories) or an efficient way of querying big trees (threaded posts).
15 15 #
16 16 # == API
17 17 #
18 18 # Methods names are aligned with acts_as_tree as much as possible, to make replacment from one
19 19 # by another easier, except for the creation:
20 20 #
21 21 # in acts_as_tree:
22 22 # item.children.create(:name => "child1")
23 23 #
24 24 # in acts_as_nested_set:
25 25 # # adds a new item at the "end" of the tree, i.e. with child.left = max(tree.right)+1
26 26 # child = MyClass.new(:name => "child1")
27 27 # child.save
28 28 # # now move the item to its right place
29 29 # child.move_to_child_of my_item
30 30 #
31 31 # You can pass an id or an object to:
32 32 # * <tt>#move_to_child_of</tt>
33 33 # * <tt>#move_to_right_of</tt>
34 34 # * <tt>#move_to_left_of</tt>
35 35 #
36 36 module SingletonMethods
37 37 # Configuration options are:
38 38 #
39 39 # * +:parent_column+ - specifies the column name to use for keeping the position integer (default: parent_id)
40 40 # * +:left_column+ - column name for left boundry data, default "lft"
41 41 # * +:right_column+ - column name for right boundry data, default "rgt"
42 42 # * +:scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id"
43 43 # (if it hasn't been already) and use that as the foreign key restriction. You
44 44 # can also pass an array to scope by multiple attributes.
45 45 # Example: <tt>acts_as_nested_set :scope => [:notable_id, :notable_type]</tt>
46 46 # * +:dependent+ - behavior for cascading destroy. If set to :destroy, all the
47 47 # child objects are destroyed alongside this object by calling their destroy
48 48 # method. If set to :delete_all (default), all the child objects are deleted
49 49 # without calling their destroy method.
50 50 #
51 51 # See CollectiveIdea::Acts::NestedSet::ClassMethods for a list of class methods and
52 52 # CollectiveIdea::Acts::NestedSet::InstanceMethods for a list of instance methods added
53 53 # to acts_as_nested_set models
54 54 def acts_as_nested_set(options = {})
55 55 options = {
56 56 :parent_column => 'parent_id',
57 57 :left_column => 'lft',
58 58 :right_column => 'rgt',
59 59 :order => 'id',
60 60 :dependent => :delete_all, # or :destroy
61 61 }.merge(options)
62 62
63 63 if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/
64 64 options[:scope] = "#{options[:scope]}_id".intern
65 65 end
66 66
67 67 write_inheritable_attribute :acts_as_nested_set_options, options
68 68 class_inheritable_reader :acts_as_nested_set_options
69 69
70 70 include Comparable
71 71 include Columns
72 72 include InstanceMethods
73 73 extend Columns
74 74 extend ClassMethods
75 75
76 76 # no bulk assignment
77 77 attr_protected left_column_name.intern,
78 78 right_column_name.intern,
79 79 parent_column_name.intern
80 80
81 81 before_create :set_default_left_and_right
82 82 before_destroy :prune_from_tree
83 83
84 84 # no assignment to structure fields
85 85 [left_column_name, right_column_name, parent_column_name].each do |column|
86 86 module_eval <<-"end_eval", __FILE__, __LINE__
87 87 def #{column}=(x)
88 88 raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{column}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
89 89 end
90 90 end_eval
91 91 end
92 92
93 93 named_scope :roots, :conditions => {parent_column_name => nil}, :order => quoted_left_column_name
94 94 named_scope :leaves, :conditions => "#{quoted_right_column_name} - #{quoted_left_column_name} = 1", :order => quoted_left_column_name
95 95 if self.respond_to?(:define_callbacks)
96 96 define_callbacks("before_move", "after_move")
97 97 end
98 98
99 99
100 100 end
101 101
102 102 end
103 103
104 104 module ClassMethods
105 105
106 106 # Returns the first root
107 107 def root
108 108 roots.find(:first)
109 109 end
110 110
111 111 def valid?
112 112 left_and_rights_valid? && no_duplicates_for_columns? && all_roots_valid?
113 113 end
114 114
115 115 def left_and_rights_valid?
116 116 count(
117 117 :joins => "LEFT OUTER JOIN #{quoted_table_name} AS parent ON " +
118 118 "#{quoted_table_name}.#{quoted_parent_column_name} = parent.#{primary_key}",
119 119 :conditions =>
120 120 "#{quoted_table_name}.#{quoted_left_column_name} IS NULL OR " +
121 121 "#{quoted_table_name}.#{quoted_right_column_name} IS NULL OR " +
122 122 "#{quoted_table_name}.#{quoted_left_column_name} >= " +
123 123 "#{quoted_table_name}.#{quoted_right_column_name} OR " +
124 124 "(#{quoted_table_name}.#{quoted_parent_column_name} IS NOT NULL AND " +
125 125 "(#{quoted_table_name}.#{quoted_left_column_name} <= parent.#{quoted_left_column_name} OR " +
126 126 "#{quoted_table_name}.#{quoted_right_column_name} >= parent.#{quoted_right_column_name}))"
127 127 ) == 0
128 128 end
129 129
130 130 def no_duplicates_for_columns?
131 131 scope_string = Array(acts_as_nested_set_options[:scope]).map do |c|
132 132 connection.quote_column_name(c)
133 133 end.push(nil).join(", ")
134 134 [quoted_left_column_name, quoted_right_column_name].all? do |column|
135 135 # No duplicates
136 136 find(:first,
137 137 :select => "#{scope_string}#{column}, COUNT(#{column})",
138 138 :group => "#{scope_string}#{column}
139 139 HAVING COUNT(#{column}) > 1").nil?
140 140 end
141 141 end
142 142
143 143 # Wrapper for each_root_valid? that can deal with scope.
144 144 def all_roots_valid?
145 145 if acts_as_nested_set_options[:scope]
146 146 roots(:group => scope_column_names).group_by{|record| scope_column_names.collect{|col| record.send(col.to_sym)}}.all? do |scope, grouped_roots|
147 147 each_root_valid?(grouped_roots)
148 148 end
149 149 else
150 150 each_root_valid?(roots)
151 151 end
152 152 end
153 153
154 154 def each_root_valid?(roots_to_validate)
155 155 left = right = 0
156 156 roots_to_validate.all? do |root|
157 returning(root.left > left && root.right > right) do
157 (root.left > left && root.right > right).tap do
158 158 left = root.left
159 159 right = root.right
160 160 end
161 161 end
162 162 end
163 163
164 164 # Rebuilds the left & rights if unset or invalid. Also very useful for converting from acts_as_tree.
165 165 def rebuild!
166 166 # Don't rebuild a valid tree.
167 167 return true if valid?
168 168
169 169 scope = lambda{|node|}
170 170 if acts_as_nested_set_options[:scope]
171 171 scope = lambda{|node|
172 172 scope_column_names.inject(""){|str, column_name|
173 173 str << "AND #{connection.quote_column_name(column_name)} = #{connection.quote(node.send(column_name.to_sym))} "
174 174 }
175 175 }
176 176 end
177 177 indices = {}
178 178
179 179 set_left_and_rights = lambda do |node|
180 180 # set left
181 181 node[left_column_name] = indices[scope.call(node)] += 1
182 182 # find
183 183 find(:all, :conditions => ["#{quoted_parent_column_name} = ? #{scope.call(node)}", node], :order => "#{quoted_left_column_name}, #{quoted_right_column_name}, #{acts_as_nested_set_options[:order]}").each{|n| set_left_and_rights.call(n) }
184 184 # set right
185 185 node[right_column_name] = indices[scope.call(node)] += 1
186 186 node.save!
187 187 end
188 188
189 189 # Find root node(s)
190 190 root_nodes = find(:all, :conditions => "#{quoted_parent_column_name} IS NULL", :order => "#{quoted_left_column_name}, #{quoted_right_column_name}, #{acts_as_nested_set_options[:order]}").each do |root_node|
191 191 # setup index for this scope
192 192 indices[scope.call(root_node)] ||= 0
193 193 set_left_and_rights.call(root_node)
194 194 end
195 195 end
196 196 end
197 197
198 198 # Mixed into both classes and instances to provide easy access to the column names
199 199 module Columns
200 200 def left_column_name
201 201 acts_as_nested_set_options[:left_column]
202 202 end
203 203
204 204 def right_column_name
205 205 acts_as_nested_set_options[:right_column]
206 206 end
207 207
208 208 def parent_column_name
209 209 acts_as_nested_set_options[:parent_column]
210 210 end
211 211
212 212 def scope_column_names
213 213 Array(acts_as_nested_set_options[:scope])
214 214 end
215 215
216 216 def quoted_left_column_name
217 217 connection.quote_column_name(left_column_name)
218 218 end
219 219
220 220 def quoted_right_column_name
221 221 connection.quote_column_name(right_column_name)
222 222 end
223 223
224 224 def quoted_parent_column_name
225 225 connection.quote_column_name(parent_column_name)
226 226 end
227 227
228 228 def quoted_scope_column_names
229 229 scope_column_names.collect {|column_name| connection.quote_column_name(column_name) }
230 230 end
231 231 end
232 232
233 233 # Any instance method that returns a collection makes use of Rails 2.1's named_scope (which is bundled for Rails 2.0), so it can be treated as a finder.
234 234 #
235 235 # category.self_and_descendants.count
236 236 # category.ancestors.find(:all, :conditions => "name like '%foo%'")
237 237 module InstanceMethods
238 238 # Value of the parent column
239 239 def parent_id
240 240 self[parent_column_name]
241 241 end
242 242
243 243 # Value of the left column
244 244 def left
245 245 self[left_column_name]
246 246 end
247 247
248 248 # Value of the right column
249 249 def right
250 250 self[right_column_name]
251 251 end
252 252
253 253 # Returns true if this is a root node.
254 254 def root?
255 255 parent_id.nil?
256 256 end
257 257
258 258 def leaf?
259 259 new_record? || (right - left == 1)
260 260 end
261 261
262 262 # Returns true is this is a child node
263 263 def child?
264 264 !parent_id.nil?
265 265 end
266 266
267 267 # order by left column
268 268 def <=>(x)
269 269 left <=> x.left
270 270 end
271 271
272 272 # Redefine to act like active record
273 273 def ==(comparison_object)
274 274 comparison_object.equal?(self) ||
275 275 (comparison_object.instance_of?(self.class) &&
276 276 comparison_object.id == id &&
277 277 !comparison_object.new_record?)
278 278 end
279 279
280 280 # Returns root
281 281 def root
282 282 self_and_ancestors.find(:first)
283 283 end
284 284
285 285 # Returns the immediate parent
286 286 def parent
287 287 nested_set_scope.find_by_id(parent_id) if parent_id
288 288 end
289 289
290 290 # Returns the array of all parents and self
291 291 def self_and_ancestors
292 292 nested_set_scope.scoped :conditions => [
293 293 "#{self.class.table_name}.#{quoted_left_column_name} <= ? AND #{self.class.table_name}.#{quoted_right_column_name} >= ?", left, right
294 294 ]
295 295 end
296 296
297 297 # Returns an array of all parents
298 298 def ancestors
299 299 without_self self_and_ancestors
300 300 end
301 301
302 302 # Returns the array of all children of the parent, including self
303 303 def self_and_siblings
304 304 nested_set_scope.scoped :conditions => {parent_column_name => parent_id}
305 305 end
306 306
307 307 # Returns the array of all children of the parent, except self
308 308 def siblings
309 309 without_self self_and_siblings
310 310 end
311 311
312 312 # Returns a set of all of its nested children which do not have children
313 313 def leaves
314 314 descendants.scoped :conditions => "#{self.class.table_name}.#{quoted_right_column_name} - #{self.class.table_name}.#{quoted_left_column_name} = 1"
315 315 end
316 316
317 317 # Returns the level of this object in the tree
318 318 # root level is 0
319 319 def level
320 320 parent_id.nil? ? 0 : ancestors.count
321 321 end
322 322
323 323 # Returns a set of itself and all of its nested children
324 324 def self_and_descendants
325 325 nested_set_scope.scoped :conditions => [
326 326 "#{self.class.table_name}.#{quoted_left_column_name} >= ? AND #{self.class.table_name}.#{quoted_right_column_name} <= ?", left, right
327 327 ]
328 328 end
329 329
330 330 # Returns a set of all of its children and nested children
331 331 def descendants
332 332 without_self self_and_descendants
333 333 end
334 334
335 335 # Returns a set of only this entry's immediate children
336 336 def children
337 337 nested_set_scope.scoped :conditions => {parent_column_name => self}
338 338 end
339 339
340 340 def is_descendant_of?(other)
341 341 other.left < self.left && self.left < other.right && same_scope?(other)
342 342 end
343 343
344 344 def is_or_is_descendant_of?(other)
345 345 other.left <= self.left && self.left < other.right && same_scope?(other)
346 346 end
347 347
348 348 def is_ancestor_of?(other)
349 349 self.left < other.left && other.left < self.right && same_scope?(other)
350 350 end
351 351
352 352 def is_or_is_ancestor_of?(other)
353 353 self.left <= other.left && other.left < self.right && same_scope?(other)
354 354 end
355 355
356 356 # Check if other model is in the same scope
357 357 def same_scope?(other)
358 358 Array(acts_as_nested_set_options[:scope]).all? do |attr|
359 359 self.send(attr) == other.send(attr)
360 360 end
361 361 end
362 362
363 363 # Find the first sibling to the left
364 364 def left_sibling
365 365 siblings.find(:first, :conditions => ["#{self.class.table_name}.#{quoted_left_column_name} < ?", left],
366 366 :order => "#{self.class.table_name}.#{quoted_left_column_name} DESC")
367 367 end
368 368
369 369 # Find the first sibling to the right
370 370 def right_sibling
371 371 siblings.find(:first, :conditions => ["#{self.class.table_name}.#{quoted_left_column_name} > ?", left])
372 372 end
373 373
374 374 # Shorthand method for finding the left sibling and moving to the left of it.
375 375 def move_left
376 376 move_to_left_of left_sibling
377 377 end
378 378
379 379 # Shorthand method for finding the right sibling and moving to the right of it.
380 380 def move_right
381 381 move_to_right_of right_sibling
382 382 end
383 383
384 384 # Move the node to the left of another node (you can pass id only)
385 385 def move_to_left_of(node)
386 386 move_to node, :left
387 387 end
388 388
389 389 # Move the node to the left of another node (you can pass id only)
390 390 def move_to_right_of(node)
391 391 move_to node, :right
392 392 end
393 393
394 394 # Move the node to the child of another node (you can pass id only)
395 395 def move_to_child_of(node)
396 396 move_to node, :child
397 397 end
398 398
399 399 # Move the node to root nodes
400 400 def move_to_root
401 401 move_to nil, :root
402 402 end
403 403
404 404 def move_possible?(target)
405 405 self != target && # Can't target self
406 406 same_scope?(target) && # can't be in different scopes
407 407 # !(left..right).include?(target.left..target.right) # this needs tested more
408 408 # detect impossible move
409 409 !((left <= target.left && right >= target.left) or (left <= target.right && right >= target.right))
410 410 end
411 411
412 412 def to_text
413 413 self_and_descendants.map do |node|
414 414 "#{'*'*(node.level+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})"
415 415 end.join("\n")
416 416 end
417 417
418 418 protected
419 419
420 420 def without_self(scope)
421 421 scope.scoped :conditions => ["#{self.class.table_name}.#{self.class.primary_key} != ?", self]
422 422 end
423 423
424 424 # All nested set queries should use this nested_set_scope, which performs finds on
425 425 # the base ActiveRecord class, using the :scope declared in the acts_as_nested_set
426 426 # declaration.
427 427 def nested_set_scope
428 428 options = {:order => quoted_left_column_name}
429 429 scopes = Array(acts_as_nested_set_options[:scope])
430 430 options[:conditions] = scopes.inject({}) do |conditions,attr|
431 431 conditions.merge attr => self[attr]
432 432 end unless scopes.empty?
433 433 self.class.base_class.scoped options
434 434 end
435 435
436 436 # on creation, set automatically lft and rgt to the end of the tree
437 437 def set_default_left_and_right
438 438 maxright = nested_set_scope.maximum(right_column_name) || 0
439 439 # adds the new node to the right of all existing nodes
440 440 self[left_column_name] = maxright + 1
441 441 self[right_column_name] = maxright + 2
442 442 end
443 443
444 444 # Prunes a branch off of the tree, shifting all of the elements on the right
445 445 # back to the left so the counts still work.
446 446 def prune_from_tree
447 447 return if right.nil? || left.nil?
448 448 diff = right - left + 1
449 449
450 450 delete_method = acts_as_nested_set_options[:dependent] == :destroy ?
451 451 :destroy_all : :delete_all
452 452
453 453 self.class.base_class.transaction do
454 454 nested_set_scope.send(delete_method,
455 455 ["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?",
456 456 left, right]
457 457 )
458 458 nested_set_scope.update_all(
459 459 ["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff],
460 460 ["#{quoted_left_column_name} >= ?", right]
461 461 )
462 462 nested_set_scope.update_all(
463 463 ["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff],
464 464 ["#{quoted_right_column_name} >= ?", right]
465 465 )
466 466 end
467 467 end
468 468
469 469 # reload left, right, and parent
470 470 def reload_nested_set
471 471 reload(:select => "#{quoted_left_column_name}, " +
472 472 "#{quoted_right_column_name}, #{quoted_parent_column_name}")
473 473 end
474 474
475 475 def move_to(target, position)
476 476 raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if self.new_record?
477 477 return if callback(:before_move) == false
478 478 transaction do
479 479 if target.is_a? self.class.base_class
480 480 target.reload_nested_set
481 481 elsif position != :root
482 482 # load object if node is not an object
483 483 target = nested_set_scope.find(target)
484 484 end
485 485 self.reload_nested_set
486 486
487 487 unless position == :root || move_possible?(target)
488 488 raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
489 489 end
490 490
491 491 bound = case position
492 492 when :child; target[right_column_name]
493 493 when :left; target[left_column_name]
494 494 when :right; target[right_column_name] + 1
495 495 when :root; 1
496 496 else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
497 497 end
498 498
499 499 if bound > self[right_column_name]
500 500 bound = bound - 1
501 501 other_bound = self[right_column_name] + 1
502 502 else
503 503 other_bound = self[left_column_name] - 1
504 504 end
505 505
506 506 # there would be no change
507 507 return if bound == self[right_column_name] || bound == self[left_column_name]
508 508
509 509 # we have defined the boundaries of two non-overlapping intervals,
510 510 # so sorting puts both the intervals and their boundaries in order
511 511 a, b, c, d = [self[left_column_name], self[right_column_name], bound, other_bound].sort
512 512
513 513 new_parent = case position
514 514 when :child; target.id
515 515 when :root; nil
516 516 else target[parent_column_name]
517 517 end
518 518
519 519 self.class.base_class.update_all([
520 520 "#{quoted_left_column_name} = CASE " +
521 521 "WHEN #{quoted_left_column_name} BETWEEN :a AND :b " +
522 522 "THEN #{quoted_left_column_name} + :d - :b " +
523 523 "WHEN #{quoted_left_column_name} BETWEEN :c AND :d " +
524 524 "THEN #{quoted_left_column_name} + :a - :c " +
525 525 "ELSE #{quoted_left_column_name} END, " +
526 526 "#{quoted_right_column_name} = CASE " +
527 527 "WHEN #{quoted_right_column_name} BETWEEN :a AND :b " +
528 528 "THEN #{quoted_right_column_name} + :d - :b " +
529 529 "WHEN #{quoted_right_column_name} BETWEEN :c AND :d " +
530 530 "THEN #{quoted_right_column_name} + :a - :c " +
531 531 "ELSE #{quoted_right_column_name} END, " +
532 532 "#{quoted_parent_column_name} = CASE " +
533 533 "WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " +
534 534 "ELSE #{quoted_parent_column_name} END",
535 535 {:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent}
536 536 ], nested_set_scope.proxy_options[:conditions])
537 537 end
538 538 target.reload_nested_set if target
539 539 self.reload_nested_set
540 540 callback(:after_move)
541 541 end
542 542
543 543 end
544 544
545 545 end
546 546 end
547 547 end
@@ -1,98 +1,98
1 1 # Generates a migration which migrates all plugins to their latest versions
2 2 # within the database.
3 3 class PluginMigrationGenerator < Rails::Generator::Base
4 4
5 5 # 255 characters max for Windows NTFS (http://en.wikipedia.org/wiki/Filename)
6 6 # minus 14 for timestamp, minus some extra chars for dot, underscore, file
7 7 # extension. So let's have 230.
8 8 MAX_FILENAME_LENGTH = 230
9 9
10 10 def initialize(runtime_args, runtime_options={})
11 11 super
12 12 @options = {:assigns => {}}
13 13 ensure_schema_table_exists
14 14 get_plugins_to_migrate(runtime_args)
15 15
16 16 if @plugins_to_migrate.empty?
17 17 puts "All plugins are migrated to their latest versions"
18 18 exit(0)
19 19 end
20 20
21 21 @options[:migration_file_name] = build_migration_name
22 22 @options[:assigns][:class_name] = build_migration_name.classify
23 23 end
24 24
25 25 def manifest
26 26 record do |m|
27 27 m.migration_template 'plugin_migration.erb', 'db/migrate', @options
28 28 end
29 29 end
30 30
31 31 protected
32 32
33 33 # Create the schema table if it doesn't already exist.
34 34 def ensure_schema_table_exists
35 35 ActiveRecord::Base.connection.initialize_schema_migrations_table
36 36 end
37 37
38 38 # Determine all the plugins which have migrations that aren't present
39 39 # according to the plugin schema information from the database.
40 40 def get_plugins_to_migrate(plugin_names)
41 41
42 42 # First, grab all the plugins which exist and have migrations
43 43 @plugins_to_migrate = if plugin_names.empty?
44 44 Engines.plugins
45 45 else
46 46 plugin_names.map do |name|
47 47 Engines.plugins[name] ? Engines.plugins[name] : raise("Cannot find the plugin '#{name}'")
48 48 end
49 49 end
50 50
51 51 @plugins_to_migrate.reject! { |p| !p.respond_to?(:latest_migration) || p.latest_migration.nil? }
52 52
53 53 # Then find the current versions from the database
54 54 @current_versions = {}
55 55 @plugins_to_migrate.each do |plugin|
56 56 @current_versions[plugin.name] = Engines::Plugin::Migrator.current_version(plugin)
57 57 end
58 58
59 59 # Then find the latest versions from their migration directories
60 60 @new_versions = {}
61 61 @plugins_to_migrate.each do |plugin|
62 62 @new_versions[plugin.name] = plugin.latest_migration
63 63 end
64 64
65 65 # Remove any plugins that don't need migration
66 66 @plugins_to_migrate.map { |p| p.name }.each do |name|
67 67 @plugins_to_migrate.delete(Engines.plugins[name]) if @current_versions[name] == @new_versions[name]
68 68 end
69 69
70 70 @options[:assigns][:plugins] = @plugins_to_migrate
71 71 @options[:assigns][:new_versions] = @new_versions
72 72 @options[:assigns][:current_versions] = @current_versions
73 73 end
74 74
75 75 # Returns a migration name. If the descriptive migration name based on the
76 76 # plugin names involved is shorter than 230 characters that one will be
77 77 # used. Otherwise a shorter name will be returned.
78 78 def build_migration_name
79 returning descriptive_migration_name do |name|
79 descriptive_migration_name.tap do |name|
80 80 name.replace short_migration_name if name.length > MAX_FILENAME_LENGTH
81 81 end
82 82 end
83 83
84 84 # Construct a unique migration name based on the plugins involved and the
85 85 # versions they should reach after this migration is run. The name constructed
86 86 # needs to be lowercase
87 87 def descriptive_migration_name
88 88 @plugins_to_migrate.map do |plugin|
89 89 "#{plugin.name}_to_version_#{@new_versions[plugin.name]}"
90 90 end.join("_and_").downcase
91 91 end
92 92
93 93 # Short migration name that will be used if the descriptive_migration_name
94 94 # exceeds 230 characters
95 95 def short_migration_name
96 96 'plugin_migrations'
97 97 end
98 98 end No newline at end of file
@@ -1,88 +1,88
1 1 require 'digest/md5'
2 2 require 'cgi'
3 3
4 4 module GravatarHelper
5 5
6 6 # These are the options that control the default behavior of the public
7 7 # methods. They can be overridden during the actual call to the helper,
8 8 # or you can set them in your environment.rb as such:
9 9 #
10 10 # # Allow racier gravatars
11 11 # GravatarHelper::DEFAULT_OPTIONS[:rating] = 'R'
12 12 #
13 13 DEFAULT_OPTIONS = {
14 14 # The URL of a default image to display if the given email address does
15 15 # not have a gravatar.
16 16 :default => nil,
17 17
18 18 # The default size in pixels for the gravatar image (they're square).
19 19 :size => 50,
20 20
21 21 # The maximum allowed MPAA rating for gravatars. This allows you to
22 22 # exclude gravatars that may be out of character for your site.
23 23 :rating => 'PG',
24 24
25 25 # The alt text to use in the img tag for the gravatar. Since it's a
26 26 # decorational picture, the alt text should be empty according to the
27 27 # XHTML specs.
28 28 :alt => '',
29 29
30 30 # The title text to use for the img tag for the gravatar.
31 31 :title => '',
32 32
33 33 # The class to assign to the img tag for the gravatar.
34 34 :class => 'gravatar',
35 35
36 36 # Whether or not to display the gravatars using HTTPS instead of HTTP
37 37 :ssl => false,
38 38 }
39 39
40 40 # The methods that will be made available to your views.
41 41 module PublicMethods
42 42
43 43 # Return the HTML img tag for the given user's gravatar. Presumes that
44 44 # the given user object will respond_to "email", and return the user's
45 45 # email address.
46 46 def gravatar_for(user, options={})
47 47 gravatar(user.email, options)
48 48 end
49 49
50 50 # Return the HTML img tag for the given email address's gravatar.
51 51 def gravatar(email, options={})
52 52 src = h(gravatar_url(email, options))
53 53 options = DEFAULT_OPTIONS.merge(options)
54 54 [:class, :alt, :size, :title].each { |opt| options[opt] = h(options[opt]) }
55 55 "<img class=\"#{options[:class]}\" alt=\"#{options[:alt]}\" title=\"#{options[:title]}\" width=\"#{options[:size]}\" height=\"#{options[:size]}\" src=\"#{src}\" />"
56 56 end
57 57
58 58 # Returns the base Gravatar URL for the given email hash. If ssl evaluates to true,
59 59 # a secure URL will be used instead. This is required when the gravatar is to be
60 60 # displayed on a HTTPS site.
61 61 def gravatar_api_url(hash, ssl=false)
62 62 if ssl
63 63 "https://secure.gravatar.com/avatar/#{hash}"
64 64 else
65 65 "http://www.gravatar.com/avatar/#{hash}"
66 66 end
67 67 end
68 68
69 69 # Return the gravatar URL for the given email address.
70 70 def gravatar_url(email, options={})
71 71 email_hash = Digest::MD5.hexdigest(email)
72 72 options = DEFAULT_OPTIONS.merge(options)
73 73 options[:default] = CGI::escape(options[:default]) unless options[:default].nil?
74 returning gravatar_api_url(email_hash, options.delete(:ssl)) do |url|
74 gravatar_api_url(email_hash, options.delete(:ssl)).tap do |url|
75 75 opts = []
76 76 [:rating, :size, :default].each do |opt|
77 77 unless options[opt].nil?
78 78 value = h(options[opt])
79 79 opts << [opt, value].join('=')
80 80 end
81 81 end
82 82 url << "?#{opts.join('&')}" unless opts.empty?
83 83 end
84 84 end
85 85
86 86 end
87 87
88 88 end
General Comments 0
You need to be logged in to leave comments. Login now