summaryrefslogtreecommitdiff
path: root/vendor/plugins/awesome_nested_set/lib
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/plugins/awesome_nested_set/lib')
-rw-r--r--vendor/plugins/awesome_nested_set/lib/awesome_nested_set.rb546
-rw-r--r--vendor/plugins/awesome_nested_set/lib/awesome_nested_set/compatability.rb29
-rw-r--r--vendor/plugins/awesome_nested_set/lib/awesome_nested_set/helper.rb40
-rw-r--r--vendor/plugins/awesome_nested_set/lib/awesome_nested_set/named_scope.rb140
4 files changed, 755 insertions, 0 deletions
diff --git a/vendor/plugins/awesome_nested_set/lib/awesome_nested_set.rb b/vendor/plugins/awesome_nested_set/lib/awesome_nested_set.rb
new file mode 100644
index 0000000..3e10891
--- /dev/null
+++ b/vendor/plugins/awesome_nested_set/lib/awesome_nested_set.rb
@@ -0,0 +1,546 @@
1module CollectiveIdea #:nodoc:
2 module Acts #:nodoc:
3 module NestedSet #:nodoc:
4 def self.included(base)
5 base.extend(SingletonMethods)
6 end
7
8 # This acts provides Nested Set functionality. Nested Set is a smart way to implement
9 # an _ordered_ tree, with the added feature that you can select the children and all of their
10 # descendants with a single query. The drawback is that insertion or move need some complex
11 # sql queries. But everything is done here by this module!
12 #
13 # Nested sets are appropriate each time you want either an orderd tree (menus,
14 # commercial categories) or an efficient way of querying big trees (threaded posts).
15 #
16 # == API
17 #
18 # Methods names are aligned with acts_as_tree as much as possible, to make replacment from one
19 # by another easier, except for the creation:
20 #
21 # in acts_as_tree:
22 # item.children.create(:name => "child1")
23 #
24 # in acts_as_nested_set:
25 # # adds a new item at the "end" of the tree, i.e. with child.left = max(tree.right)+1
26 # child = MyClass.new(:name => "child1")
27 # child.save
28 # # now move the item to its right place
29 # child.move_to_child_of my_item
30 #
31 # You can pass an id or an object to:
32 # * <tt>#move_to_child_of</tt>
33 # * <tt>#move_to_right_of</tt>
34 # * <tt>#move_to_left_of</tt>
35 #
36 module SingletonMethods
37 # Configuration options are:
38 #
39 # * +:parent_column+ - specifies the column name to use for keeping the position integer (default: parent_id)
40 # * +:left_column+ - column name for left boundry data, default "lft"
41 # * +:right_column+ - column name for right boundry data, default "rgt"
42 # * +:scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id"
43 # (if it hasn't been already) and use that as the foreign key restriction. You
44 # can also pass an array to scope by multiple attributes.
45 # Example: <tt>acts_as_nested_set :scope => [:notable_id, :notable_type]</tt>
46 # * +:dependent+ - behavior for cascading destroy. If set to :destroy, all the
47 # child objects are destroyed alongside this object by calling their destroy
48 # method. If set to :delete_all (default), all the child objects are deleted
49 # without calling their destroy method.
50 #
51 # See CollectiveIdea::Acts::NestedSet::ClassMethods for a list of class methods and
52 # CollectiveIdea::Acts::NestedSet::InstanceMethods for a list of instance methods added
53 # to acts_as_nested_set models
54 def acts_as_nested_set(options = {})
55 options = {
56 :parent_column => 'parent_id',
57 :left_column => 'lft',
58 :right_column => 'rgt',
59 :dependent => :delete_all, # or :destroy
60 }.merge(options)
61
62 if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/
63 options[:scope] = "#{options[:scope]}_id".intern
64 end
65
66 write_inheritable_attribute :acts_as_nested_set_options, options
67 class_inheritable_reader :acts_as_nested_set_options
68
69 include Comparable
70 include Columns
71 include InstanceMethods
72 extend Columns
73 extend ClassMethods
74
75 # no bulk assignment
76 attr_protected left_column_name.intern,
77 right_column_name.intern,
78 parent_column_name.intern
79
80 before_create :set_default_left_and_right
81 before_destroy :prune_from_tree
82
83 # no assignment to structure fields
84 [left_column_name, right_column_name, parent_column_name].each do |column|
85 module_eval <<-"end_eval", __FILE__, __LINE__
86 def #{column}=(x)
87 raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{column}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
88 end
89 end_eval
90 end
91
92 named_scope :roots, :conditions => {parent_column_name => nil}, :order => quoted_left_column_name
93 named_scope :leaves, :conditions => "#{quoted_right_column_name} - #{quoted_left_column_name} = 1", :order => quoted_left_column_name
94 if self.respond_to?(:define_callbacks)
95 define_callbacks("before_move", "after_move")
96 end
97
98
99 end
100
101 end
102
103 module ClassMethods
104
105 # Returns the first root
106 def root
107 roots.find(:first)
108 end
109
110 def valid?
111 left_and_rights_valid? && no_duplicates_for_columns? && all_roots_valid?
112 end
113
114 def left_and_rights_valid?
115 count(
116 :joins => "LEFT OUTER JOIN #{quoted_table_name} AS parent ON " +
117 "#{quoted_table_name}.#{quoted_parent_column_name} = parent.#{primary_key}",
118 :conditions =>
119 "#{quoted_table_name}.#{quoted_left_column_name} IS NULL OR " +
120 "#{quoted_table_name}.#{quoted_right_column_name} IS NULL OR " +
121 "#{quoted_table_name}.#{quoted_left_column_name} >= " +
122 "#{quoted_table_name}.#{quoted_right_column_name} OR " +
123 "(#{quoted_table_name}.#{quoted_parent_column_name} IS NOT NULL AND " +
124 "(#{quoted_table_name}.#{quoted_left_column_name} <= parent.#{quoted_left_column_name} OR " +
125 "#{quoted_table_name}.#{quoted_right_column_name} >= parent.#{quoted_right_column_name}))"
126 ) == 0
127 end
128
129 def no_duplicates_for_columns?
130 scope_string = Array(acts_as_nested_set_options[:scope]).map do |c|
131 connection.quote_column_name(c)
132 end.push(nil).join(", ")
133 [quoted_left_column_name, quoted_right_column_name].all? do |column|
134 # No duplicates
135 find(:first,
136 :select => "#{scope_string}#{column}, COUNT(#{column})",
137 :group => "#{scope_string}#{column}
138 HAVING COUNT(#{column}) > 1").nil?
139 end
140 end
141
142 # Wrapper for each_root_valid? that can deal with scope.
143 def all_roots_valid?
144 if acts_as_nested_set_options[:scope]
145 roots(:group => scope_column_names).group_by{|record| scope_column_names.collect{|col| record.send(col.to_sym)}}.all? do |scope, grouped_roots|
146 each_root_valid?(grouped_roots)
147 end
148 else
149 each_root_valid?(roots)
150 end
151 end
152
153 def each_root_valid?(roots_to_validate)
154 left = right = 0
155 roots_to_validate.all? do |root|
156 returning(root.left > left && root.right > right) do
157 left = root.left
158 right = root.right
159 end
160 end
161 end
162
163 # Rebuilds the left & rights if unset or invalid. Also very useful for converting from acts_as_tree.
164 def rebuild!
165 # Don't rebuild a valid tree.
166 return true if valid?
167
168 scope = lambda{}
169 if acts_as_nested_set_options[:scope]
170 scope = lambda{|node|
171 scope_column_names.inject(""){|str, column_name|
172 str << "AND #{connection.quote_column_name(column_name)} = #{connection.quote(node.send(column_name.to_sym))} "
173 }
174 }
175 end
176 indices = {}
177
178 set_left_and_rights = lambda do |node|
179 # set left
180 node[left_column_name] = indices[scope.call(node)] += 1
181 # find
182 find(:all, :conditions => ["#{quoted_parent_column_name} = ? #{scope.call(node)}", node], :order => "#{quoted_left_column_name}, #{quoted_right_column_name}, id").each{|n| set_left_and_rights.call(n) }
183 # set right
184 node[right_column_name] = indices[scope.call(node)] += 1
185 node.save!
186 end
187
188 # Find root node(s)
189 root_nodes = find(:all, :conditions => "#{quoted_parent_column_name} IS NULL", :order => "#{quoted_left_column_name}, #{quoted_right_column_name}, id").each do |root_node|
190 # setup index for this scope
191 indices[scope.call(root_node)] ||= 0
192 set_left_and_rights.call(root_node)
193 end
194 end
195 end
196
197 # Mixed into both classes and instances to provide easy access to the column names
198 module Columns
199 def left_column_name
200 acts_as_nested_set_options[:left_column]
201 end
202
203 def right_column_name
204 acts_as_nested_set_options[:right_column]
205 end
206
207 def parent_column_name
208 acts_as_nested_set_options[:parent_column]
209 end
210
211 def scope_column_names
212 Array(acts_as_nested_set_options[:scope])
213 end
214
215 def quoted_left_column_name
216 connection.quote_column_name(left_column_name)
217 end
218
219 def quoted_right_column_name
220 connection.quote_column_name(right_column_name)
221 end
222
223 def quoted_parent_column_name
224 connection.quote_column_name(parent_column_name)
225 end
226
227 def quoted_scope_column_names
228 scope_column_names.collect {|column_name| connection.quote_column_name(column_name) }
229 end
230 end
231
232 # 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.
233 #
234 # category.self_and_descendants.count
235 # category.ancestors.find(:all, :conditions => "name like '%foo%'")
236 module InstanceMethods
237 # Value of the parent column
238 def parent_id
239 self[parent_column_name]
240 end
241
242 # Value of the left column
243 def left
244 self[left_column_name]
245 end
246
247 # Value of the right column
248 def right
249 self[right_column_name]
250 end
251
252 # Returns true if this is a root node.
253 def root?
254 parent_id.nil?
255 end
256
257 def leaf?
258 right - left == 1
259 end
260
261 # Returns true is this is a child node
262 def child?
263 !parent_id.nil?
264 end
265
266 # order by left column
267 def <=>(x)
268 left <=> x.left
269 end
270
271 # Redefine to act like active record
272 def ==(comparison_object)
273 comparison_object.equal?(self) ||
274 (comparison_object.instance_of?(self.class) &&
275 comparison_object.id == id &&
276 !comparison_object.new_record?)
277 end
278
279 # Returns root
280 def root
281 self_and_ancestors.find(:first)
282 end
283
284 # Returns the immediate parent
285 def parent
286 nested_set_scope.find_by_id(parent_id) if parent_id
287 end
288
289 # Returns the array of all parents and self
290 def self_and_ancestors
291 nested_set_scope.scoped :conditions => [
292 "#{self.class.table_name}.#{quoted_left_column_name} <= ? AND #{self.class.table_name}.#{quoted_right_column_name} >= ?", left, right
293 ]
294 end
295
296 # Returns an array of all parents
297 def ancestors
298 without_self self_and_ancestors
299 end
300
301 # Returns the array of all children of the parent, including self
302 def self_and_siblings
303 nested_set_scope.scoped :conditions => {parent_column_name => parent_id}
304 end
305
306 # Returns the array of all children of the parent, except self
307 def siblings
308 without_self self_and_siblings
309 end
310
311 # Returns a set of all of its nested children which do not have children
312 def leaves
313 descendants.scoped :conditions => "#{self.class.table_name}.#{quoted_right_column_name} - #{self.class.table_name}.#{quoted_left_column_name} = 1"
314 end
315
316 # Returns the level of this object in the tree
317 # root level is 0
318 def level
319 parent_id.nil? ? 0 : ancestors.count
320 end
321
322 # Returns a set of itself and all of its nested children
323 def self_and_descendants
324 nested_set_scope.scoped :conditions => [
325 "#{self.class.table_name}.#{quoted_left_column_name} >= ? AND #{self.class.table_name}.#{quoted_right_column_name} <= ?", left, right
326 ]
327 end
328
329 # Returns a set of all of its children and nested children
330 def descendants
331 without_self self_and_descendants
332 end
333
334 # Returns a set of only this entry's immediate children
335 def children
336 nested_set_scope.scoped :conditions => {parent_column_name => self}
337 end
338
339 def is_descendant_of?(other)
340 other.left < self.left && self.left < other.right && same_scope?(other)
341 end
342
343 def is_or_is_descendant_of?(other)
344 other.left <= self.left && self.left < other.right && same_scope?(other)
345 end
346
347 def is_ancestor_of?(other)
348 self.left < other.left && other.left < self.right && same_scope?(other)
349 end
350
351 def is_or_is_ancestor_of?(other)
352 self.left <= other.left && other.left < self.right && same_scope?(other)
353 end
354
355 # Check if other model is in the same scope
356 def same_scope?(other)
357 Array(acts_as_nested_set_options[:scope]).all? do |attr|
358 self.send(attr) == other.send(attr)
359 end
360 end
361
362 # Find the first sibling to the left
363 def left_sibling
364 siblings.find(:first, :conditions => ["#{self.class.table_name}.#{quoted_left_column_name} < ?", left],
365 :order => "#{self.class.table_name}.#{quoted_left_column_name} DESC")
366 end
367
368 # Find the first sibling to the right
369 def right_sibling
370 siblings.find(:first, :conditions => ["#{self.class.table_name}.#{quoted_left_column_name} > ?", left])
371 end
372
373 # Shorthand method for finding the left sibling and moving to the left of it.
374 def move_left
375 move_to_left_of left_sibling
376 end
377
378 # Shorthand method for finding the right sibling and moving to the right of it.
379 def move_right
380 move_to_right_of right_sibling
381 end
382
383 # Move the node to the left of another node (you can pass id only)
384 def move_to_left_of(node)
385 move_to node, :left
386 end
387
388 # Move the node to the left of another node (you can pass id only)
389 def move_to_right_of(node)
390 move_to node, :right
391 end
392
393 # Move the node to the child of another node (you can pass id only)
394 def move_to_child_of(node)
395 move_to node, :child
396 end
397
398 # Move the node to root nodes
399 def move_to_root
400 move_to nil, :root
401 end
402
403 def move_possible?(target)
404 self != target && # Can't target self
405 same_scope?(target) && # can't be in different scopes
406 # !(left..right).include?(target.left..target.right) # this needs tested more
407 # detect impossible move
408 !((left <= target.left && right >= target.left) or (left <= target.right && right >= target.right))
409 end
410
411 def to_text
412 self_and_descendants.map do |node|
413 "#{'*'*(node.level+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})"
414 end.join("\n")
415 end
416
417 protected
418
419 def without_self(scope)
420 scope.scoped :conditions => ["#{self.class.table_name}.#{self.class.primary_key} != ?", self]
421 end
422
423 # All nested set queries should use this nested_set_scope, which performs finds on
424 # the base ActiveRecord class, using the :scope declared in the acts_as_nested_set
425 # declaration.
426 def nested_set_scope
427 options = {:order => quoted_left_column_name}
428 scopes = Array(acts_as_nested_set_options[:scope])
429 options[:conditions] = scopes.inject({}) do |conditions,attr|
430 conditions.merge attr => self[attr]
431 end unless scopes.empty?
432 self.class.base_class.scoped options
433 end
434
435 # on creation, set automatically lft and rgt to the end of the tree
436 def set_default_left_and_right
437 maxright = nested_set_scope.maximum(right_column_name) || 0
438 # adds the new node to the right of all existing nodes
439 self[left_column_name] = maxright + 1
440 self[right_column_name] = maxright + 2
441 end
442
443 # Prunes a branch off of the tree, shifting all of the elements on the right
444 # back to the left so the counts still work.
445 def prune_from_tree
446 return if right.nil? || left.nil?
447 diff = right - left + 1
448
449 delete_method = acts_as_nested_set_options[:dependent] == :destroy ?
450 :destroy_all : :delete_all
451
452 self.class.base_class.transaction do
453 nested_set_scope.send(delete_method,
454 ["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?",
455 left, right]
456 )
457 nested_set_scope.update_all(
458 ["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff],
459 ["#{quoted_left_column_name} >= ?", right]
460 )
461 nested_set_scope.update_all(
462 ["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff],
463 ["#{quoted_right_column_name} >= ?", right]
464 )
465 end
466 end
467
468 # reload left, right, and parent
469 def reload_nested_set
470 reload(:select => "#{quoted_left_column_name}, " +
471 "#{quoted_right_column_name}, #{quoted_parent_column_name}")
472 end
473
474 def move_to(target, position)
475 raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if self.new_record?
476 return if callback(:before_move) == false
477 transaction do
478 if target.is_a? self.class.base_class
479 target.reload_nested_set
480 elsif position != :root
481 # load object if node is not an object
482 target = nested_set_scope.find(target)
483 end
484 self.reload_nested_set
485
486 unless position == :root || move_possible?(target)
487 raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
488 end
489
490 bound = case position
491 when :child; target[right_column_name]
492 when :left; target[left_column_name]
493 when :right; target[right_column_name] + 1
494 when :root; 1
495 else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
496 end
497
498 if bound > self[right_column_name]
499 bound = bound - 1
500 other_bound = self[right_column_name] + 1
501 else
502 other_bound = self[left_column_name] - 1
503 end
504
505 # there would be no change
506 return if bound == self[right_column_name] || bound == self[left_column_name]
507
508 # we have defined the boundaries of two non-overlapping intervals,
509 # so sorting puts both the intervals and their boundaries in order
510 a, b, c, d = [self[left_column_name], self[right_column_name], bound, other_bound].sort
511
512 new_parent = case position
513 when :child; target.id
514 when :root; nil
515 else target[parent_column_name]
516 end
517
518 self.class.base_class.update_all([
519 "#{quoted_left_column_name} = CASE " +
520 "WHEN #{quoted_left_column_name} BETWEEN :a AND :b " +
521 "THEN #{quoted_left_column_name} + :d - :b " +
522 "WHEN #{quoted_left_column_name} BETWEEN :c AND :d " +
523 "THEN #{quoted_left_column_name} + :a - :c " +
524 "ELSE #{quoted_left_column_name} END, " +
525 "#{quoted_right_column_name} = CASE " +
526 "WHEN #{quoted_right_column_name} BETWEEN :a AND :b " +
527 "THEN #{quoted_right_column_name} + :d - :b " +
528 "WHEN #{quoted_right_column_name} BETWEEN :c AND :d " +
529 "THEN #{quoted_right_column_name} + :a - :c " +
530 "ELSE #{quoted_right_column_name} END, " +
531 "#{quoted_parent_column_name} = CASE " +
532 "WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " +
533 "ELSE #{quoted_parent_column_name} END",
534 {:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent}
535 ], nested_set_scope.proxy_options[:conditions])
536 end
537 target.reload_nested_set if target
538 self.reload_nested_set
539 callback(:after_move)
540 end
541
542 end
543
544 end
545 end
546end
diff --git a/vendor/plugins/awesome_nested_set/lib/awesome_nested_set/compatability.rb b/vendor/plugins/awesome_nested_set/lib/awesome_nested_set/compatability.rb
new file mode 100644
index 0000000..2d11da3
--- /dev/null
+++ b/vendor/plugins/awesome_nested_set/lib/awesome_nested_set/compatability.rb
@@ -0,0 +1,29 @@
1# Rails <2.x doesn't define #except
2class Hash #:nodoc:
3 # Returns a new hash without the given keys.
4 def except(*keys)
5 clone.except!(*keys)
6 end unless method_defined?(:except)
7
8 # Replaces the hash without the given keys.
9 def except!(*keys)
10 keys.map! { |key| convert_key(key) } if respond_to?(:convert_key)
11 keys.each { |key| delete(key) }
12 self
13 end unless method_defined?(:except!)
14end
15
16# NamedScope is new to Rails 2.1
17unless defined? ActiveRecord::NamedScope
18 require 'awesome_nested_set/named_scope'
19 ActiveRecord::Base.class_eval do
20 include CollectiveIdea::NamedScope
21 end
22end
23
24# Rails 1.2.x doesn't define #quoted_table_name
25class ActiveRecord::Base #:nodoc:
26 def self.quoted_table_name
27 self.connection.quote_column_name(self.table_name)
28 end unless methods.include?('quoted_table_name')
29end \ No newline at end of file
diff --git a/vendor/plugins/awesome_nested_set/lib/awesome_nested_set/helper.rb b/vendor/plugins/awesome_nested_set/lib/awesome_nested_set/helper.rb
new file mode 100644
index 0000000..09c803f
--- /dev/null
+++ b/vendor/plugins/awesome_nested_set/lib/awesome_nested_set/helper.rb
@@ -0,0 +1,40 @@
1module CollectiveIdea #:nodoc:
2 module Acts #:nodoc:
3 module NestedSet #:nodoc:
4 # This module provides some helpers for the model classes using acts_as_nested_set.
5 # It is included by default in all views.
6 #
7 module Helper
8 # Returns options for select.
9 # You can exclude some items from the tree.
10 # You can pass a block receiving an item and returning the string displayed in the select.
11 #
12 # == Params
13 # * +class_or_item+ - Class name or top level times
14 # * +mover+ - The item that is being move, used to exlude impossible moves
15 # * +&block+ - a block that will be used to display: { |item| ... item.name }
16 #
17 # == Usage
18 #
19 # <%= f.select :parent_id, nested_set_options(Category, @category) {|i|
20 # "#{'–' * i.level} #{i.name}"
21 # }) %>
22 #
23 def nested_set_options(class_or_item, mover = nil)
24 class_or_item = class_or_item.roots if class_or_item.is_a?(Class)
25 items = Array(class_or_item)
26 result = []
27 items.each do |root|
28 result += root.self_and_descendants.map do |i|
29 if mover.nil? || mover.new_record? || mover.move_possible?(i)
30 [yield(i), i.id]
31 end
32 end.compact
33 end
34 result
35 end
36
37 end
38 end
39 end
40end \ No newline at end of file
diff --git a/vendor/plugins/awesome_nested_set/lib/awesome_nested_set/named_scope.rb b/vendor/plugins/awesome_nested_set/lib/awesome_nested_set/named_scope.rb
new file mode 100644
index 0000000..1836498
--- /dev/null
+++ b/vendor/plugins/awesome_nested_set/lib/awesome_nested_set/named_scope.rb
@@ -0,0 +1,140 @@
1# Taken from Rails 2.1
2module CollectiveIdea #:nodoc:
3 module NamedScope #:nodoc:
4 # All subclasses of ActiveRecord::Base have two named_scopes:
5 # * <tt>all</tt>, which is similar to a <tt>find(:all)</tt> query, and
6 # * <tt>scoped</tt>, which allows for the creation of anonymous scopes, on the fly:
7 #
8 # Shirt.scoped(:conditions => {:color => 'red'}).scoped(:include => :washing_instructions)
9 #
10 # These anonymous scopes tend to be useful when procedurally generating complex queries, where passing
11 # intermediate values (scopes) around as first-class objects is convenient.
12 def self.included(base)
13 base.class_eval do
14 extend ClassMethods
15 named_scope :scoped, lambda { |scope| scope }
16 end
17 end
18
19 module ClassMethods #:nodoc:
20 def scopes
21 read_inheritable_attribute(:scopes) || write_inheritable_attribute(:scopes, {})
22 end
23
24 # Adds a class method for retrieving and querying objects. A scope represents a narrowing of a database query,
25 # such as <tt>:conditions => {:color => :red}, :select => 'shirts.*', :include => :washing_instructions</tt>.
26 #
27 # class Shirt < ActiveRecord::Base
28 # named_scope :red, :conditions => {:color => 'red'}
29 # named_scope :dry_clean_only, :joins => :washing_instructions, :conditions => ['washing_instructions.dry_clean_only = ?', true]
30 # end
31 #
32 # The above calls to <tt>named_scope</tt> define class methods <tt>Shirt.red</tt> and <tt>Shirt.dry_clean_only</tt>. <tt>Shirt.red</tt>,
33 # in effect, represents the query <tt>Shirt.find(:all, :conditions => {:color => 'red'})</tt>.
34 #
35 # Unlike Shirt.find(...), however, the object returned by <tt>Shirt.red</tt> is not an Array; it resembles the association object
36 # constructed by a <tt>has_many</tt> declaration. For instance, you can invoke <tt>Shirt.red.find(:first)</tt>, <tt>Shirt.red.count</tt>,
37 # <tt>Shirt.red.find(:all, :conditions => {:size => 'small'})</tt>. Also, just
38 # as with the association objects, name scopes acts like an Array, implementing Enumerable; <tt>Shirt.red.each(&block)</tt>,
39 # <tt>Shirt.red.first</tt>, and <tt>Shirt.red.inject(memo, &block)</tt> all behave as if Shirt.red really were an Array.
40 #
41 # These named scopes are composable. For instance, <tt>Shirt.red.dry_clean_only</tt> will produce all shirts that are both red and dry clean only.
42 # Nested finds and calculations also work with these compositions: <tt>Shirt.red.dry_clean_only.count</tt> returns the number of garments
43 # for which these criteria obtain. Similarly with <tt>Shirt.red.dry_clean_only.average(:thread_count)</tt>.
44 #
45 # All scopes are available as class methods on the ActiveRecord descendent upon which the scopes were defined. But they are also available to
46 # <tt>has_many</tt> associations. If,
47 #
48 # class Person < ActiveRecord::Base
49 # has_many :shirts
50 # end
51 #
52 # then <tt>elton.shirts.red.dry_clean_only</tt> will return all of Elton's red, dry clean
53 # only shirts.
54 #
55 # Named scopes can also be procedural.
56 #
57 # class Shirt < ActiveRecord::Base
58 # named_scope :colored, lambda { |color|
59 # { :conditions => { :color => color } }
60 # }
61 # end
62 #
63 # In this example, <tt>Shirt.colored('puce')</tt> finds all puce shirts.
64 #
65 # Named scopes can also have extensions, just as with <tt>has_many</tt> declarations:
66 #
67 # class Shirt < ActiveRecord::Base
68 # named_scope :red, :conditions => {:color => 'red'} do
69 # def dom_id
70 # 'red_shirts'
71 # end
72 # end
73 # end
74 #
75 #
76 # For testing complex named scopes, you can examine the scoping options using the
77 # <tt>proxy_options</tt> method on the proxy itself.
78 #
79 # class Shirt < ActiveRecord::Base
80 # named_scope :colored, lambda { |color|
81 # { :conditions => { :color => color } }
82 # }
83 # end
84 #
85 # expected_options = { :conditions => { :colored => 'red' } }
86 # assert_equal expected_options, Shirt.colored('red').proxy_options
87 def named_scope(name, options = {}, &block)
88 scopes[name] = lambda do |parent_scope, *args|
89 Scope.new(parent_scope, case options
90 when Hash
91 options
92 when Proc
93 options.call(*args)
94 end, &block)
95 end
96 (class << self; self end).instance_eval do
97 define_method name do |*args|
98 scopes[name].call(self, *args)
99 end
100 end
101 end
102 end
103
104 class Scope #:nodoc:
105 attr_reader :proxy_scope, :proxy_options
106 [].methods.each { |m| delegate m, :to => :proxy_found unless m =~ /(^__|^nil\?|^send|class|extend|find|count|sum|average|maximum|minimum|paginate)/ }
107 delegate :scopes, :with_scope, :to => :proxy_scope
108
109 def initialize(proxy_scope, options, &block)
110 [options[:extend]].flatten.each { |extension| extend extension } if options[:extend]
111 extend Module.new(&block) if block_given?
112 @proxy_scope, @proxy_options = proxy_scope, options.except(:extend)
113 end
114
115 def reload
116 load_found; self
117 end
118
119 protected
120 def proxy_found
121 @found || load_found
122 end
123
124 private
125 def method_missing(method, *args, &block)
126 if scopes.include?(method)
127 scopes[method].call(self, *args)
128 else
129 with_scope :find => proxy_options do
130 proxy_scope.send(method, *args, &block)
131 end
132 end
133 end
134
135 def load_found
136 @found = find(:all)
137 end
138 end
139 end
140end \ No newline at end of file