taxonomy 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/MIT-LICENSE +20 -0
- data/README.rdoc +28 -0
- data/Rakefile +31 -0
- data/lib/generators/taxonomy/migration/migration_generator.rb +39 -0
- data/lib/generators/taxonomy/migration/templates/active_record/migration.rb +36 -0
- data/lib/tasks/taxonomy_tasks.rake +4 -0
- data/lib/taxonomy.rb +25 -0
- data/lib/taxonomy/group_helper.rb +12 -0
- data/lib/taxonomy/has_tagger.rb +52 -0
- data/lib/taxonomy/has_taxonomy.rb +502 -0
- data/lib/taxonomy/tag.rb +485 -0
- data/lib/taxonomy/tag_list.rb +97 -0
- data/lib/taxonomy/tagging.rb +12 -0
- data/lib/taxonomy/tags_helper.rb +13 -0
- data/lib/taxonomy/version.rb +3 -0
- data/spec/dummy/Rakefile +7 -0
- data/spec/dummy/app/controllers/application_controller.rb +3 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/models/altered_inheriting_taggable_model.rb +3 -0
- data/spec/dummy/app/models/inheriting_taggable_model.rb +2 -0
- data/spec/dummy/app/models/other_taggable_model.rb +4 -0
- data/spec/dummy/app/models/post.rb +2 -0
- data/spec/dummy/app/models/taggable_model.rb +6 -0
- data/spec/dummy/app/models/taggable_user.rb +3 -0
- data/spec/dummy/app/models/treed_model.rb +3 -0
- data/spec/dummy/app/models/untaggable_model.rb +2 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/config/application.rb +45 -0
- data/spec/dummy/config/boot.rb +10 -0
- data/spec/dummy/config/database.yml +19 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +30 -0
- data/spec/dummy/config/environments/production.rb +60 -0
- data/spec/dummy/config/environments/test.rb +39 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/inflections.rb +10 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/secret_token.rb +7 -0
- data/spec/dummy/config/initializers/session_store.rb +8 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +5 -0
- data/spec/dummy/config/routes.rb +58 -0
- data/spec/dummy/db/migrate/20111221004133_create_posts.rb +8 -0
- data/spec/dummy/db/migrate/20111221023928_taxonomy_migration.rb +35 -0
- data/spec/dummy/db/migrate/20111221024100_create_bulk.rb +18 -0
- data/spec/dummy/db/schema.rb +65 -0
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/log/test.log +100915 -0
- data/spec/dummy/public/404.html +26 -0
- data/spec/dummy/public/422.html +26 -0
- data/spec/dummy/public/500.html +26 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/script/rails +6 -0
- data/spec/factories/posts.rb +6 -0
- data/spec/generators/taxonomy/migration/migration_generator_spec.rb +22 -0
- data/spec/models/post_spec.rb +5 -0
- data/spec/spec_helper.rb +30 -0
- data/spec/taxonomy/group_helper_spec.rb +21 -0
- data/spec/taxonomy/has_tagger_spec.rb +113 -0
- data/spec/taxonomy/has_taxonomy_spec.rb +226 -0
- data/spec/taxonomy/tag_list_spec.rb +70 -0
- data/spec/taxonomy/tag_spec.rb +462 -0
- data/spec/taxonomy/taggable_spec.rb +262 -0
- data/spec/taxonomy/tagger_spec.rb +40 -0
- data/spec/taxonomy/tagging_spec.rb +25 -0
- data/spec/taxonomy/tags_helper_spec.rb +29 -0
- metadata +225 -0
data/lib/taxonomy/tag.rb
ADDED
@@ -0,0 +1,485 @@
|
|
1
|
+
require 'iconv' # for slug generation, this should go away
|
2
|
+
class Tag < ActiveRecord::Base
|
3
|
+
attr_accessible :name, :context
|
4
|
+
attr_accessor :skip_before_destroy
|
5
|
+
|
6
|
+
### ASSOCIATIONS:
|
7
|
+
has_many :taggings, :dependent => :destroy
|
8
|
+
|
9
|
+
### VALIDATIONS:
|
10
|
+
validates :name, :presence => true, :uniqueness => {:scope => :context}
|
11
|
+
validates :slug, :presence => true, :uniqueness => {:scope => :context}
|
12
|
+
|
13
|
+
before_validation :permalize
|
14
|
+
before_validation :strip_name
|
15
|
+
before_create :set_default_left_and_right
|
16
|
+
before_save :store_new_parent
|
17
|
+
after_save :move_to_new_parent
|
18
|
+
before_destroy :destroy_descendants
|
19
|
+
|
20
|
+
belongs_to :parent, :class_name => self.base_class.to_s,
|
21
|
+
:foreign_key => Taxonomy.nested_set_options[:parent_column]
|
22
|
+
has_many :children, :class_name => self.base_class.to_s,
|
23
|
+
:foreign_key => Taxonomy.nested_set_options[:parent_column], :order => Taxonomy.nested_set_options[:left_column]
|
24
|
+
|
25
|
+
# no assignment to structure fields
|
26
|
+
[Taxonomy.nested_set_options[:left_column], Taxonomy.nested_set_options[:right_column]].each do |column|
|
27
|
+
module_eval <<-"end_eval", __FILE__, __LINE__
|
28
|
+
def #{column}=(x)
|
29
|
+
raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{column}: it's an internal field handled by nested set code, use move_to_* methods instead."
|
30
|
+
end
|
31
|
+
end_eval
|
32
|
+
end
|
33
|
+
|
34
|
+
define_callbacks("before_move", "after_move")
|
35
|
+
|
36
|
+
### NESTED SCOPES
|
37
|
+
|
38
|
+
# calling parent_column_name in a where cause makes migrations explode in beta4, probably an AREL bug
|
39
|
+
# use :conditions for now
|
40
|
+
# scope :roots, where(parent_column_name => nil).order(quote_column_name(Taxonomy.nested_set_options[:left_column]))
|
41
|
+
scope :roots, where(Taxonomy.nested_set_options[:parent_column] => nil).order(Taxonomy.nested_set_options[:left_column])
|
42
|
+
scope :leaves, where("#{Taxonomy.nested_set_options[:right_column]} - #{Taxonomy.nested_set_options[:left_column]} = 1").order(Taxonomy.nested_set_options[:left_column])
|
43
|
+
|
44
|
+
### TAG SCOPES:
|
45
|
+
|
46
|
+
scope :named, lambda { |context, name|
|
47
|
+
where("context = ? AND name LIKE ?", context, name)
|
48
|
+
}
|
49
|
+
scope :named_any, lambda { |context, list|
|
50
|
+
where(list.map { |tag| sanitize_sql(["name LIKE ?", tag.to_s]) }.join(" OR ")).where(sanitize_sql(["(context = ?)", context]))
|
51
|
+
}
|
52
|
+
scope :named_like, lambda { |context, name|
|
53
|
+
where("name LIKE ?", "%#{name}%")
|
54
|
+
}
|
55
|
+
scope :named_like_any, lambda { |context, list|
|
56
|
+
where(list.map { |tag| sanitize_sql(["name LIKE ?", "%#{tag.to_s}%"]) }.join(" OR ")).where(sanitize_sql(["(context = ?)", context]))
|
57
|
+
}
|
58
|
+
|
59
|
+
### CLASS METHODS:
|
60
|
+
|
61
|
+
def self.root
|
62
|
+
roots.first
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.find_context_with_slug!(context, slug)
|
66
|
+
ret = self.where(:context => context, :slug => slug).first
|
67
|
+
raise ActiveRecord::RecordNotFound if ret.nil?
|
68
|
+
ret
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.find_or_create_with_like_by_name(context, name)
|
72
|
+
named_like(context, name).first || create(:context => "#{context.singularize.to_s}", :name => name)
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.find_or_create_all_with_like_by_name(context, *list)
|
76
|
+
list = [list].flatten
|
77
|
+
|
78
|
+
return [] if list.empty?
|
79
|
+
|
80
|
+
existing_tags = Tag.named_any(context, list).all
|
81
|
+
new_tag_names = list.reject { |name| existing_tags.any? { |tag| tag.name.downcase == name.downcase } }
|
82
|
+
created_tags = new_tag_names.map { |name| Tag.create(:context => "#{context.singularize.to_s}", :name => name) }
|
83
|
+
|
84
|
+
existing_tags + created_tags
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.valid?
|
88
|
+
left_and_rights_valid? && no_duplicates_for_columns? && all_roots_valid?
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.treed_taggings_for(context, options = {})
|
92
|
+
self.where("tags.context" => context.to_s.singularize)
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.left_and_rights_valid?
|
96
|
+
self.base_class.joins("LEFT OUTER JOIN #{quoted_table_name} AS parent ON " +
|
97
|
+
"#{quoted_table_name}.#{connection.quote_column_name(Taxonomy.nested_set_options[:parent_column])} = parent.#{primary_key}").where(
|
98
|
+
"#{quoted_table_name}.#{connection.quote_column_name(Taxonomy.nested_set_options[:left_column])} IS NULL OR " +
|
99
|
+
"#{quoted_table_name}.#{connection.quote_column_name(Taxonomy.nested_set_options[:right_column])} IS NULL OR " +
|
100
|
+
"#{quoted_table_name}.#{connection.quote_column_name(Taxonomy.nested_set_options[:left_column])} >= " +
|
101
|
+
"#{quoted_table_name}.#{connection.quote_column_name(Taxonomy.nested_set_options[:right_column])} OR " +
|
102
|
+
"(#{quoted_table_name}.#{connection.quote_column_name(Taxonomy.nested_set_options[:parent_column])} IS NOT NULL AND " +
|
103
|
+
"(#{quoted_table_name}.#{connection.quote_column_name(Taxonomy.nested_set_options[:left_column])} <= parent.#{connection.quote_column_name(Taxonomy.nested_set_options[:left_column])} OR " +
|
104
|
+
"#{quoted_table_name}.#{connection.quote_column_name(Taxonomy.nested_set_options[:right_column])} >= parent.#{connection.quote_column_name(Taxonomy.nested_set_options[:right_column])}))"
|
105
|
+
).count == 0
|
106
|
+
end
|
107
|
+
|
108
|
+
def self.no_duplicates_for_columns?
|
109
|
+
[connection.quote_column_name(Taxonomy.nested_set_options[:left_column]), connection.quote_column_name(Taxonomy.nested_set_options[:right_column])].all? do |column|
|
110
|
+
# No duplicates
|
111
|
+
self.base_class.select("#{column}, COUNT(#{column})").group("#{column} HAVING COUNT(#{column}) > 1").first.nil?
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def self.all_roots_valid?
|
116
|
+
left = right = 0
|
117
|
+
roots.all? do |root|
|
118
|
+
g_returning(root.left > left && root.right > right) do
|
119
|
+
left = root.left
|
120
|
+
right = root.right
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
# Rebuilds the left & rights if unset or invalid. Also very useful for converting from acts_as_tree.
|
126
|
+
def self.rebuild!
|
127
|
+
# Don't rebuild a valid tree.
|
128
|
+
return true if valid?
|
129
|
+
|
130
|
+
scope = lambda{|node|}
|
131
|
+
if Taxonomy.nested_set_options[:scope]
|
132
|
+
scope = lambda{|node|
|
133
|
+
scope_column_names.inject(""){|str, column_name|
|
134
|
+
str << "AND #{connection.quote_column_name(column_name)} = #{connection.quote(node.send(column_name.to_sym))} "
|
135
|
+
}
|
136
|
+
}
|
137
|
+
end
|
138
|
+
indices = {}
|
139
|
+
|
140
|
+
set_left_and_rights = lambda do |node|
|
141
|
+
# set left
|
142
|
+
node[Taxonomy.nested_set_options[:left_column]] = indices[scope.call(node)] += 1
|
143
|
+
# find
|
144
|
+
find(:all, :conditions => ["#{connection.quote_column_name(Taxonomy.nested_set_options[:parent_column])} = ? #{scope.call(node)}", node], :order => "#{connection.quote_column_name(Taxonomy.nested_set_options[:left_column])}, #{connection.quote_column_name(Taxonomy.nested_set_options[:right_column])}, id").each{|n| set_left_and_rights.call(n) }
|
145
|
+
# set right
|
146
|
+
node[Taxonomy.nested_set_options[:right_column]] = indices[scope.call(node)] += 1
|
147
|
+
node.save!
|
148
|
+
end
|
149
|
+
|
150
|
+
# Find root node(s)
|
151
|
+
root_nodes = find(:all, :conditions => "#{connection.quote_column_name(Taxonomy.nested_set_options[:parent_column])} IS NULL", :order => "#{connection.quote_column_name(Taxonomy.nested_set_options[:left_column])}, #{connection.quote_column_name(Taxonomy.nested_set_options[:right_column])}, id").each do |root_node|
|
152
|
+
# setup index for this scope
|
153
|
+
indices[scope.call(root_node)] ||= 0
|
154
|
+
set_left_and_rights.call(root_node)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
### INSTANCE METHODS:
|
159
|
+
|
160
|
+
def ==(object)
|
161
|
+
super || (object.is_a?(Tag) && name == object.name)
|
162
|
+
end
|
163
|
+
|
164
|
+
def to_s
|
165
|
+
name
|
166
|
+
end
|
167
|
+
|
168
|
+
def count
|
169
|
+
read_attribute(:count).to_i
|
170
|
+
end
|
171
|
+
|
172
|
+
### INSTANCE METHODS FOR MOVING ITEMS IN NESTED SET
|
173
|
+
|
174
|
+
# Returns true if this is a root node
|
175
|
+
def root?
|
176
|
+
parent_id.nil?
|
177
|
+
end
|
178
|
+
# Value of the parent column
|
179
|
+
def parent_id
|
180
|
+
self[Taxonomy.nested_set_options[:parent_column]]
|
181
|
+
end
|
182
|
+
|
183
|
+
# Value of the left column
|
184
|
+
def left
|
185
|
+
self[Taxonomy.nested_set_options[:left_column]]
|
186
|
+
end
|
187
|
+
|
188
|
+
# Value of the right column
|
189
|
+
def right
|
190
|
+
self[Taxonomy.nested_set_options[:right_column]]
|
191
|
+
end
|
192
|
+
|
193
|
+
# Returns the level of this object in the tree
|
194
|
+
# root level is 0
|
195
|
+
def level
|
196
|
+
parent_id.nil? ? 0 : ancestors.count
|
197
|
+
end
|
198
|
+
|
199
|
+
def leaf?
|
200
|
+
!new_record? && right - left == 1
|
201
|
+
end
|
202
|
+
|
203
|
+
# Returns true is this is a child node
|
204
|
+
def child?
|
205
|
+
!parent_id.nil?
|
206
|
+
end
|
207
|
+
|
208
|
+
# Returns the array of all parents and self
|
209
|
+
def self_and_ancestors
|
210
|
+
self.reload
|
211
|
+
nested_set_scope.where("#{self.class.quoted_table_name}.#{connection.quote_column_name(Taxonomy.nested_set_options[:left_column])} <= ? AND #{self.class.quoted_table_name}.#{connection.quote_column_name(Taxonomy.nested_set_options[:right_column])} >= ?", left, right)
|
212
|
+
end
|
213
|
+
|
214
|
+
# Returns a set of itself and all of its nested children
|
215
|
+
def self_and_descendants
|
216
|
+
self.reload
|
217
|
+
nested_set_scope.where("#{self.class.quoted_table_name}.#{connection.quote_column_name(Taxonomy.nested_set_options[:left_column])} >= ? AND #{self.class.quoted_table_name}.#{connection.quote_column_name(Taxonomy.nested_set_options[:right_column])} <= ?", left, right)
|
218
|
+
end
|
219
|
+
|
220
|
+
# Returns the scope of all children of the parent, including self
|
221
|
+
def self_and_siblings
|
222
|
+
# Rails 3, but not really. scoped.where(Taxonomy.nested_set_options[:parent_column] => parent_id)
|
223
|
+
nested_set_scope.where(["#{Taxonomy.nested_set_options[:parent_column]} == #{parent_id}"])
|
224
|
+
end
|
225
|
+
|
226
|
+
# Check if other model is in the same scope
|
227
|
+
def same_scope?(other)
|
228
|
+
Array(Taxonomy.nested_set_options[:scope]).all? do |attr|
|
229
|
+
self.send(attr) == other.send(attr)
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
# Returns a set of all of its nested children which do not have children
|
234
|
+
def leaves
|
235
|
+
descendants.where "#{self.class.quoted_table_name}.#{connection.quote_column_name(Taxonomy.nested_set_options[:right_column])} - #{self.class.quoted_table_name}.#{connection.quote_column_name(Taxonomy.nested_set_options[:left_column])} = 1"
|
236
|
+
end
|
237
|
+
|
238
|
+
# Returns a set of all of its children and nested children
|
239
|
+
def descendants
|
240
|
+
without_self(self_and_descendants)
|
241
|
+
end
|
242
|
+
|
243
|
+
# Returns an array of all parents
|
244
|
+
def ancestors
|
245
|
+
without_self(self_and_ancestors)
|
246
|
+
end
|
247
|
+
|
248
|
+
# Returns the array of all children of the parent, except self
|
249
|
+
def siblings
|
250
|
+
without_self(self_and_siblings)
|
251
|
+
end
|
252
|
+
|
253
|
+
# Move the node to the child of another node (you can pass id only)
|
254
|
+
def move_to_child_of(node)
|
255
|
+
move_to node, :child
|
256
|
+
end
|
257
|
+
|
258
|
+
# Find the first sibling to the left
|
259
|
+
def left_sibling
|
260
|
+
siblings.where("#{self.class.quoted_table_name}.#{connection.quote_column_name(Taxonomy.nested_set_options[:left_column])} < ?", left).order(
|
261
|
+
"#{self.class.quoted_table_name}.#{connection.quote_column_name(Taxonomy.nested_set_options[:left_column])} DESC").first
|
262
|
+
end
|
263
|
+
|
264
|
+
# Find the first sibling to the right
|
265
|
+
def right_sibling
|
266
|
+
siblings.where("#{self.class.quoted_table_name}.#{connection.quote_column_name(Taxonomy.nested_set_options[:left_column])} > ?", left).first
|
267
|
+
end
|
268
|
+
|
269
|
+
# Move the node to the left of another node (you can pass id only)
|
270
|
+
def move_to_left_of(node)
|
271
|
+
move_to node, :left
|
272
|
+
end
|
273
|
+
# Move the node to the left of another node (you can pass id only)
|
274
|
+
def move_to_right_of(node)
|
275
|
+
move_to node, :right
|
276
|
+
end
|
277
|
+
# Shorthand method for finding the left sibling and moving to the left of it.
|
278
|
+
def move_left
|
279
|
+
move_to_left_of left_sibling
|
280
|
+
end
|
281
|
+
# Shorthand method for finding the right sibling and moving to the right of it.
|
282
|
+
def move_right
|
283
|
+
move_to_right_of right_sibling
|
284
|
+
end
|
285
|
+
|
286
|
+
# Move the node to root nodes
|
287
|
+
def move_to_root
|
288
|
+
move_to nil, :root
|
289
|
+
end
|
290
|
+
|
291
|
+
def is_descendant_of?(other)
|
292
|
+
other.reload
|
293
|
+
other.left < self.left && self.left < other.right && same_scope?(other)
|
294
|
+
end
|
295
|
+
|
296
|
+
def is_or_is_descendant_of?(other)
|
297
|
+
other.reload
|
298
|
+
other.left <= self.left && self.left < other.right && same_scope?(other)
|
299
|
+
end
|
300
|
+
|
301
|
+
def is_ancestor_of?(other)
|
302
|
+
self.reload
|
303
|
+
self.left < other.left && other.left < self.right && same_scope?(other)
|
304
|
+
end
|
305
|
+
|
306
|
+
def is_or_is_ancestor_of?(other)
|
307
|
+
self.reload
|
308
|
+
self.left <= other.left && other.left < self.right && same_scope?(other)
|
309
|
+
end
|
310
|
+
|
311
|
+
|
312
|
+
def move_possible?(target)
|
313
|
+
self != target && # Can't target self
|
314
|
+
same_scope?(target) && # can't be in different scopes
|
315
|
+
# !(left..right).include?(target.left..target.right) # this needs tested more
|
316
|
+
# detect impossible move
|
317
|
+
!((left <= target.left && right >= target.left) or (left <= target.right && right >= target.right))
|
318
|
+
end
|
319
|
+
|
320
|
+
protected
|
321
|
+
|
322
|
+
def permalize
|
323
|
+
if (changed.include?(self.name) && !changed.include?(:slug)) || self.slug.nil? || self.slug.blank?
|
324
|
+
s = Iconv.iconv('ascii//ignore//translit', 'utf-8', self.name).to_s
|
325
|
+
s.gsub!(/\'/, '') # remove '
|
326
|
+
s.gsub!(/\W+/, ' ') # all non-word chars to spaces
|
327
|
+
s.strip! # ohh la la
|
328
|
+
s.downcase! #
|
329
|
+
s.gsub!(/\ +/, '-') # spaces to dashes, preferred separator char everywhere
|
330
|
+
# self.send("#{self.sluggable_conf[:slug_column]}=", s)
|
331
|
+
write_attribute(:slug, s)
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
def strip_name
|
336
|
+
self.name.strip! if self.name.present?
|
337
|
+
end
|
338
|
+
|
339
|
+
def without_self(s)
|
340
|
+
s.where("#{self.class.quoted_table_name}.#{self.class.primary_key} != ?", self)
|
341
|
+
end
|
342
|
+
|
343
|
+
# on creation, set automatically lft and rgt to the end of the tree
|
344
|
+
def set_default_left_and_right
|
345
|
+
maxright = nested_set_scope.maximum(Taxonomy.nested_set_options[:right_column]) || 0
|
346
|
+
# adds the new node to the right of all existing nodes
|
347
|
+
self[Taxonomy.nested_set_options[:left_column]] = maxright + 1
|
348
|
+
self[Taxonomy.nested_set_options[:right_column]] = maxright + 2
|
349
|
+
end
|
350
|
+
|
351
|
+
def store_new_parent
|
352
|
+
@move_to_new_parent_id = send("#{Taxonomy.nested_set_options[:parent_column]}_changed?") ? parent_id : false
|
353
|
+
true # force callback to return true
|
354
|
+
end
|
355
|
+
|
356
|
+
def move_to_new_parent
|
357
|
+
if @move_to_new_parent_id.nil?
|
358
|
+
move_to_root
|
359
|
+
elsif @move_to_new_parent_id
|
360
|
+
move_to_child_of(@move_to_new_parent_id)
|
361
|
+
end
|
362
|
+
end
|
363
|
+
|
364
|
+
# reload left, right, and parent
|
365
|
+
def reload_nested_set
|
366
|
+
reload(:select => "#{connection.quote_column_name(Taxonomy.nested_set_options[:left_column])}, " +
|
367
|
+
"#{connection.quote_column_name(Taxonomy.nested_set_options[:right_column])}, #{connection.quote_column_name(Taxonomy.nested_set_options[:parent_column])}")
|
368
|
+
end
|
369
|
+
|
370
|
+
# All nested set queries should use this nested_set_scope, which performs finds on
|
371
|
+
# the base ActiveRecord class, using the :scope declared in the acts_as_nested_set
|
372
|
+
# declaration.
|
373
|
+
def nested_set_scope
|
374
|
+
self.class.base_class.scoped.order(connection.quote_column_name(Taxonomy.nested_set_options[:left_column])) # options
|
375
|
+
end
|
376
|
+
|
377
|
+
# Prunes a branch off of the tree, shifting all of the elements on the right
|
378
|
+
# back to the left so the counts still work.
|
379
|
+
def destroy_descendants
|
380
|
+
return if right.nil? || left.nil? || skip_before_destroy
|
381
|
+
|
382
|
+
self.class.base_class.transaction do
|
383
|
+
if Taxonomy.nested_set_options[:dependent] == :destroy
|
384
|
+
descendants.each do |model|
|
385
|
+
model.skip_before_destroy = true
|
386
|
+
model.destroy
|
387
|
+
end
|
388
|
+
else
|
389
|
+
nested_set_scope.delete_all(
|
390
|
+
["#{connection.quote_column_name(Taxonomy.nested_set_options[:left_column])} > ? AND #{connection.quote_column_name(Taxonomy.nested_set_options[:right_column])} < ?",
|
391
|
+
left, right]
|
392
|
+
)
|
393
|
+
end
|
394
|
+
|
395
|
+
# update lefts and rights for remaining nodes
|
396
|
+
diff = right - left + 1
|
397
|
+
nested_set_scope.update_all(
|
398
|
+
["#{connection.quote_column_name(Taxonomy.nested_set_options[:left_column])} = (#{connection.quote_column_name(Taxonomy.nested_set_options[:left_column])} - ?)", diff],
|
399
|
+
["#{connection.quote_column_name(Taxonomy.nested_set_options[:left_column])} > ?", right]
|
400
|
+
)
|
401
|
+
nested_set_scope.update_all(
|
402
|
+
["#{connection.quote_column_name(Taxonomy.nested_set_options[:right_column])} = (#{connection.quote_column_name(Taxonomy.nested_set_options[:right_column])} - ?)", diff],
|
403
|
+
["#{connection.quote_column_name(Taxonomy.nested_set_options[:right_column])} > ?", right]
|
404
|
+
)
|
405
|
+
|
406
|
+
# Don't allow multiple calls to destroy to corrupt the set
|
407
|
+
self.skip_before_destroy = true
|
408
|
+
end
|
409
|
+
end
|
410
|
+
|
411
|
+
def move_to(target, position)
|
412
|
+
raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if self.new_record?
|
413
|
+
return if run_callbacks(:before_move) == false
|
414
|
+
transaction do
|
415
|
+
if target.is_a? self.class.base_class
|
416
|
+
target.reload_nested_set
|
417
|
+
elsif position != :root
|
418
|
+
# load object if node is not an object
|
419
|
+
target = nested_set_scope.find(target)
|
420
|
+
end
|
421
|
+
self.reload_nested_set
|
422
|
+
|
423
|
+
unless position == :root || move_possible?(target)
|
424
|
+
raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
|
425
|
+
end
|
426
|
+
|
427
|
+
bound = case position
|
428
|
+
when :child; target[Taxonomy.nested_set_options[:right_column]]
|
429
|
+
when :left; target[Taxonomy.nested_set_options[:left_column]]
|
430
|
+
when :right; target[Taxonomy.nested_set_options[:right_column]] + 1
|
431
|
+
when :root; 1
|
432
|
+
else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
|
433
|
+
end
|
434
|
+
|
435
|
+
if bound > self[Taxonomy.nested_set_options[:right_column]]
|
436
|
+
bound = bound - 1
|
437
|
+
other_bound = self[Taxonomy.nested_set_options[:right_column]] + 1
|
438
|
+
else
|
439
|
+
other_bound = self[Taxonomy.nested_set_options[:left_column]] - 1
|
440
|
+
end
|
441
|
+
|
442
|
+
# there would be no change
|
443
|
+
return if bound == self[Taxonomy.nested_set_options[:right_column]] || bound == self[Taxonomy.nested_set_options[:left_column]]
|
444
|
+
|
445
|
+
# we have defined the boundaries of two non-overlapping intervals,
|
446
|
+
# so sorting puts both the intervals and their boundaries in order
|
447
|
+
a, b, c, d = [self[Taxonomy.nested_set_options[:left_column]], self[Taxonomy.nested_set_options[:right_column]], bound, other_bound].sort
|
448
|
+
|
449
|
+
new_parent = case position
|
450
|
+
when :child; target.id
|
451
|
+
when :root; nil
|
452
|
+
else target[Taxonomy.nested_set_options[:parent_column]]
|
453
|
+
end
|
454
|
+
|
455
|
+
self.class.base_class.update_all([
|
456
|
+
"#{connection.quote_column_name(Taxonomy.nested_set_options[:left_column])} = CASE " +
|
457
|
+
"WHEN #{connection.quote_column_name(Taxonomy.nested_set_options[:left_column])} BETWEEN :a AND :b " +
|
458
|
+
"THEN #{connection.quote_column_name(Taxonomy.nested_set_options[:left_column])} + :d - :b " +
|
459
|
+
"WHEN #{connection.quote_column_name(Taxonomy.nested_set_options[:left_column])} BETWEEN :c AND :d " +
|
460
|
+
"THEN #{connection.quote_column_name(Taxonomy.nested_set_options[:left_column])} + :a - :c " +
|
461
|
+
"ELSE #{connection.quote_column_name(Taxonomy.nested_set_options[:left_column])} END, " +
|
462
|
+
"#{connection.quote_column_name(Taxonomy.nested_set_options[:right_column])} = CASE " +
|
463
|
+
"WHEN #{connection.quote_column_name(Taxonomy.nested_set_options[:right_column])} BETWEEN :a AND :b " +
|
464
|
+
"THEN #{connection.quote_column_name(Taxonomy.nested_set_options[:right_column])} + :d - :b " +
|
465
|
+
"WHEN #{connection.quote_column_name(Taxonomy.nested_set_options[:right_column])} BETWEEN :c AND :d " +
|
466
|
+
"THEN #{connection.quote_column_name(Taxonomy.nested_set_options[:right_column])} + :a - :c " +
|
467
|
+
"ELSE #{connection.quote_column_name(Taxonomy.nested_set_options[:right_column])} END, " +
|
468
|
+
"#{connection.quote_column_name(Taxonomy.nested_set_options[:parent_column])} = CASE " +
|
469
|
+
"WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " +
|
470
|
+
"ELSE #{connection.quote_column_name(Taxonomy.nested_set_options[:parent_column])} END",
|
471
|
+
{:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent}
|
472
|
+
], nested_set_scope.where_values)
|
473
|
+
end
|
474
|
+
target.reload_nested_set if target
|
475
|
+
self.reload_nested_set
|
476
|
+
run_callbacks(:after_move)
|
477
|
+
end
|
478
|
+
|
479
|
+
private
|
480
|
+
def self.g_returning(value)
|
481
|
+
yield(value)
|
482
|
+
value
|
483
|
+
end
|
484
|
+
|
485
|
+
end
|