acts-as-taggable-on 2.3.3 → 2.4.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.
- checksums.yaml +7 -0
- data/.gitignore +3 -3
- data/Appraisals +7 -0
- data/Gemfile +3 -1
- data/{MIT-LICENSE → LICENSE.md} +1 -1
- data/README.md +303 -0
- data/Rakefile +2 -2
- data/acts-as-taggable-on.gemspec +25 -18
- data/gemfiles/rails_3.gemfile +8 -0
- data/gemfiles/rails_4.gemfile +8 -0
- data/lib/acts-as-taggable-on.rb +5 -0
- data/lib/acts_as_taggable_on/acts_as_taggable_on/cache.rb +2 -2
- data/lib/acts_as_taggable_on/acts_as_taggable_on/collection.rb +64 -7
- data/lib/acts_as_taggable_on/acts_as_taggable_on/compatibility.rb +34 -0
- data/lib/acts_as_taggable_on/acts_as_taggable_on/core.rb +86 -55
- data/lib/acts_as_taggable_on/acts_as_taggable_on/dirty.rb +3 -3
- data/lib/acts_as_taggable_on/acts_as_taggable_on/ownership.rb +24 -15
- data/lib/acts_as_taggable_on/acts_as_taggable_on/related.rb +32 -21
- data/lib/acts_as_taggable_on/tag.rb +37 -15
- data/lib/acts_as_taggable_on/tag_list.rb +2 -2
- data/lib/acts_as_taggable_on/taggable.rb +10 -7
- data/lib/acts_as_taggable_on/tagger.rb +12 -3
- data/lib/acts_as_taggable_on/tagging.rb +2 -2
- data/lib/acts_as_taggable_on/tags_helper.rb +0 -2
- data/lib/{acts-as-taggable-on → acts_as_taggable_on}/version.rb +1 -1
- data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +1 -183
- data/spec/acts_as_taggable_on/acts_as_tagger_spec.rb +8 -8
- data/spec/acts_as_taggable_on/related_spec.rb +143 -0
- data/spec/acts_as_taggable_on/single_table_inheritance_spec.rb +187 -0
- data/spec/acts_as_taggable_on/tag_list_spec.rb +4 -4
- data/spec/acts_as_taggable_on/tag_spec.rb +61 -3
- data/spec/acts_as_taggable_on/taggable_spec.rb +178 -98
- data/spec/acts_as_taggable_on/tagger_spec.rb +32 -33
- data/spec/acts_as_taggable_on/tagging_spec.rb +1 -1
- data/spec/acts_as_taggable_on/tags_helper_spec.rb +2 -2
- data/spec/acts_as_taggable_on/utils_spec.rb +2 -2
- data/spec/models.rb +8 -2
- data/spec/schema.rb +1 -1
- data/spec/spec_helper.rb +7 -4
- metadata +101 -56
- data/CHANGELOG +0 -35
- data/README.rdoc +0 -244
- data/rails/init.rb +0 -1
- data/uninstall.rb +0 -1
@@ -13,30 +13,33 @@ module ActsAsTaggableOn::Taggable
|
|
13
13
|
end
|
14
14
|
|
15
15
|
module ClassMethods
|
16
|
+
|
16
17
|
def initialize_acts_as_taggable_on_core
|
18
|
+
include taggable_mixin
|
17
19
|
tag_types.map(&:to_s).each do |tags_type|
|
18
20
|
tag_type = tags_type.to_s.singularize
|
19
21
|
context_taggings = "#{tag_type}_taggings".to_sym
|
20
22
|
context_tags = tags_type.to_sym
|
21
|
-
taggings_order = (preserve_tag_order? ? "#{ActsAsTaggableOn::Tagging.table_name}.id" :
|
22
|
-
|
23
|
+
taggings_order = (preserve_tag_order? ? "#{ActsAsTaggableOn::Tagging.table_name}.id" : [])
|
24
|
+
|
23
25
|
class_eval do
|
24
26
|
# when preserving tag order, include order option so that for a 'tags' context
|
25
27
|
# the associations tag_taggings & tags are always returned in created order
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
28
|
+
has_many_with_compatibility context_taggings, :as => :taggable,
|
29
|
+
:dependent => :destroy,
|
30
|
+
:class_name => "ActsAsTaggableOn::Tagging",
|
31
|
+
:order => taggings_order,
|
32
|
+
:conditions => ["#{ActsAsTaggableOn::Tagging.table_name}.context = (?)", tags_type],
|
33
|
+
:include => :tag
|
34
|
+
|
35
|
+
has_many_with_compatibility context_tags, :through => context_taggings,
|
36
|
+
:source => :tag,
|
37
|
+
:class_name => "ActsAsTaggableOn::Tag",
|
38
|
+
:order => taggings_order
|
39
|
+
|
37
40
|
end
|
38
41
|
|
39
|
-
class_eval
|
42
|
+
taggable_mixin.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
40
43
|
def #{tag_type}_list
|
41
44
|
tag_list_on('#{tags_type}')
|
42
45
|
end
|
@@ -48,7 +51,7 @@ module ActsAsTaggableOn::Taggable
|
|
48
51
|
def all_#{tags_type}_list
|
49
52
|
all_tags_list_on('#{tags_type}')
|
50
53
|
end
|
51
|
-
|
54
|
+
RUBY
|
52
55
|
end
|
53
56
|
end
|
54
57
|
|
@@ -56,7 +59,7 @@ module ActsAsTaggableOn::Taggable
|
|
56
59
|
super(preserve_tag_order, *tag_types)
|
57
60
|
initialize_acts_as_taggable_on_core
|
58
61
|
end
|
59
|
-
|
62
|
+
|
60
63
|
# all column names are necessary for PostgreSQL group clause
|
61
64
|
def grouped_column_names_for(object)
|
62
65
|
object.column_names.map { |column| "#{object.table_name}.#{column}" }.join(", ")
|
@@ -80,12 +83,14 @@ module ActsAsTaggableOn::Taggable
|
|
80
83
|
# User.tagged_with("awesome", "cool", :owned_by => foo ) # Users that are tagged with just awesome and cool by 'foo'
|
81
84
|
def tagged_with(tags, options = {})
|
82
85
|
tag_list = ActsAsTaggableOn::TagList.from(tags)
|
83
|
-
empty_result =
|
86
|
+
empty_result = where("1 = 0")
|
84
87
|
|
85
88
|
return empty_result if tag_list.empty?
|
86
89
|
|
87
90
|
joins = []
|
88
91
|
conditions = []
|
92
|
+
having = []
|
93
|
+
select_clause = []
|
89
94
|
|
90
95
|
context = options.delete(:on)
|
91
96
|
owned_by = options.delete(:owned_by)
|
@@ -101,15 +106,23 @@ module ActsAsTaggableOn::Taggable
|
|
101
106
|
|
102
107
|
conditions << "#{table_name}.#{primary_key} NOT IN (SELECT #{ActsAsTaggableOn::Tagging.table_name}.taggable_id FROM #{ActsAsTaggableOn::Tagging.table_name} JOIN #{ActsAsTaggableOn::Tag.table_name} ON #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key} AND (#{tags_conditions}) WHERE #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = #{quote_value(base_class.name)})"
|
103
108
|
|
109
|
+
if owned_by
|
110
|
+
joins << "JOIN #{ActsAsTaggableOn::Tagging.table_name}" +
|
111
|
+
" ON #{ActsAsTaggableOn::Tagging.table_name}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" +
|
112
|
+
" AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = #{quote_value(base_class.name)}" +
|
113
|
+
" AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_id = #{owned_by.id}" +
|
114
|
+
" AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_type = #{quote_value(owned_by.class.base_class.to_s)}"
|
115
|
+
end
|
116
|
+
|
104
117
|
elsif options.delete(:any)
|
105
118
|
# get tags, drop out if nothing returned (we need at least one)
|
106
|
-
if options.delete(:wild)
|
107
|
-
|
119
|
+
tags = if options.delete(:wild)
|
120
|
+
ActsAsTaggableOn::Tag.named_like_any(tag_list)
|
108
121
|
else
|
109
|
-
|
122
|
+
ActsAsTaggableOn::Tag.named_any(tag_list)
|
110
123
|
end
|
111
124
|
|
112
|
-
return
|
125
|
+
return empty_result unless tags.length > 0
|
113
126
|
|
114
127
|
# setup taggings alias so we can chain, ex: items_locations_taggings_awesome_cool_123
|
115
128
|
# avoid ambiguous column name
|
@@ -128,29 +141,37 @@ module ActsAsTaggableOn::Taggable
|
|
128
141
|
conditions << tags.map { |t| "#{taggings_alias}.tag_id = #{t.id}" }.join(" OR ")
|
129
142
|
select_clause = "DISTINCT #{table_name}.*" unless context and tag_types.one?
|
130
143
|
|
131
|
-
|
144
|
+
if owned_by
|
145
|
+
tagging_join << " AND " +
|
146
|
+
sanitize_sql([
|
147
|
+
"#{taggings_alias}.tagger_id = ? AND #{taggings_alias}.tagger_type = ?",
|
148
|
+
owned_by.id,
|
149
|
+
owned_by.class.base_class.to_s
|
150
|
+
])
|
151
|
+
end
|
132
152
|
|
153
|
+
joins << tagging_join
|
133
154
|
else
|
134
155
|
tags = ActsAsTaggableOn::Tag.named_any(tag_list)
|
156
|
+
|
135
157
|
return empty_result unless tags.length == tag_list.length
|
136
158
|
|
137
159
|
tags.each do |tag|
|
138
|
-
|
139
160
|
taggings_alias = adjust_taggings_alias("#{alias_base_name[0..11]}_taggings_#{sha_prefix(tag.name)}")
|
140
|
-
|
141
161
|
tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
|
142
162
|
" ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" +
|
143
163
|
" AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}" +
|
144
164
|
" AND #{taggings_alias}.tag_id = #{tag.id}"
|
165
|
+
|
145
166
|
tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
|
146
167
|
|
147
168
|
if owned_by
|
148
169
|
tagging_join << " AND " +
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
170
|
+
sanitize_sql([
|
171
|
+
"#{taggings_alias}.tagger_id = ? AND #{taggings_alias}.tagger_type = ?",
|
172
|
+
owned_by.id,
|
173
|
+
owned_by.class.base_class.to_s
|
174
|
+
])
|
154
175
|
end
|
155
176
|
|
156
177
|
joins << tagging_join
|
@@ -170,13 +191,13 @@ module ActsAsTaggableOn::Taggable
|
|
170
191
|
having = "COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
|
171
192
|
end
|
172
193
|
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
194
|
+
select(select_clause) \
|
195
|
+
.joins(joins.join(" ")) \
|
196
|
+
.where(conditions.join(" AND ")) \
|
197
|
+
.group(group) \
|
198
|
+
.having(having) \
|
199
|
+
.order(options[:order]) \
|
200
|
+
.readonly(false)
|
180
201
|
end
|
181
202
|
|
182
203
|
def is_taggable?
|
@@ -189,6 +210,10 @@ module ActsAsTaggableOn::Taggable
|
|
189
210
|
end
|
190
211
|
taggings_alias
|
191
212
|
end
|
213
|
+
|
214
|
+
def taggable_mixin
|
215
|
+
@taggable_mixin ||= Module.new
|
216
|
+
end
|
192
217
|
end
|
193
218
|
|
194
219
|
module InstanceMethods
|
@@ -215,12 +240,13 @@ module ActsAsTaggableOn::Taggable
|
|
215
240
|
|
216
241
|
def tag_list_cache_set_on(context)
|
217
242
|
variable_name = "@#{context.to_s.singularize}_list"
|
218
|
-
!instance_variable_get(variable_name).nil?
|
243
|
+
instance_variable_defined?(variable_name) && !instance_variable_get(variable_name).nil?
|
219
244
|
end
|
220
245
|
|
221
246
|
def tag_list_cache_on(context)
|
222
247
|
variable_name = "@#{context.to_s.singularize}_list"
|
223
|
-
instance_variable_get(variable_name)
|
248
|
+
return instance_variable_get(variable_name) if instance_variable_defined?(variable_name) && instance_variable_get(variable_name)
|
249
|
+
instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(tags_on(context).map(&:name)))
|
224
250
|
end
|
225
251
|
|
226
252
|
def tag_list_on(context)
|
@@ -230,7 +256,7 @@ module ActsAsTaggableOn::Taggable
|
|
230
256
|
|
231
257
|
def all_tags_list_on(context)
|
232
258
|
variable_name = "@all_#{context.to_s.singularize}_list"
|
233
|
-
return instance_variable_get(variable_name) if instance_variable_get(variable_name)
|
259
|
+
return instance_variable_get(variable_name) if instance_variable_defined?(variable_name) && instance_variable_get(variable_name)
|
234
260
|
|
235
261
|
instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(all_tags_on(context).map(&:name)).freeze)
|
236
262
|
end
|
@@ -246,12 +272,10 @@ module ActsAsTaggableOn::Taggable
|
|
246
272
|
|
247
273
|
if ActsAsTaggableOn::Tag.using_postgresql?
|
248
274
|
group_columns = grouped_column_names_for(ActsAsTaggableOn::Tag)
|
249
|
-
scope
|
275
|
+
scope.order("max(#{tagging_table_name}.created_at)").group(group_columns)
|
250
276
|
else
|
251
|
-
scope
|
252
|
-
end
|
253
|
-
|
254
|
-
scope.all
|
277
|
+
scope.group("#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}")
|
278
|
+
end.to_a
|
255
279
|
end
|
256
280
|
|
257
281
|
##
|
@@ -261,7 +285,7 @@ module ActsAsTaggableOn::Taggable
|
|
261
285
|
# when preserving tag order, return tags in created order
|
262
286
|
# if we added the order to the association this would always apply
|
263
287
|
scope = scope.order("#{ActsAsTaggableOn::Tagging.table_name}.id") if self.class.preserve_tag_order?
|
264
|
-
scope
|
288
|
+
scope
|
265
289
|
end
|
266
290
|
|
267
291
|
def set_tag_list_on(context, new_list)
|
@@ -303,7 +327,6 @@ module ActsAsTaggableOn::Taggable
|
|
303
327
|
def save_tags
|
304
328
|
tagging_contexts.each do |context|
|
305
329
|
next unless tag_list_cache_set_on(context)
|
306
|
-
|
307
330
|
# List of currently assigned tag names
|
308
331
|
tag_list = tag_list_cache_on(context).uniq
|
309
332
|
|
@@ -315,13 +338,22 @@ module ActsAsTaggableOn::Taggable
|
|
315
338
|
|
316
339
|
# Tag maintenance based on whether preserving the created order of tags
|
317
340
|
if self.class.preserve_tag_order?
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
341
|
+
old_tags, new_tags = current_tags - tags, tags - current_tags
|
342
|
+
|
343
|
+
shared_tags = current_tags & tags
|
344
|
+
|
345
|
+
if shared_tags.any? && tags[0...shared_tags.size] != shared_tags
|
346
|
+
index = shared_tags.each_with_index { |_, i| break i unless shared_tags[i] == tags[i] }
|
347
|
+
|
348
|
+
# Update arrays of tag objects
|
349
|
+
old_tags |= current_tags[index...current_tags.size]
|
350
|
+
new_tags |= current_tags[index...current_tags.size] & shared_tags
|
351
|
+
|
352
|
+
# Order the array of tag objects to match the tag list
|
353
|
+
new_tags = tags.map do |t|
|
354
|
+
new_tags.find { |n| n.name.downcase == t.name.downcase }
|
355
|
+
end.compact
|
356
|
+
end
|
325
357
|
else
|
326
358
|
# Delete discarded tags and create new tags
|
327
359
|
old_tags = current_tags - tags
|
@@ -330,8 +362,7 @@ module ActsAsTaggableOn::Taggable
|
|
330
362
|
|
331
363
|
# Find taggings to remove:
|
332
364
|
if old_tags.present?
|
333
|
-
old_taggings = taggings.where(:tagger_type => nil, :tagger_id => nil,
|
334
|
-
:context => context.to_s, :tag_id => old_tags).all
|
365
|
+
old_taggings = taggings.where(:tagger_type => nil, :tagger_id => nil, :context => context.to_s, :tag_id => old_tags)
|
335
366
|
end
|
336
367
|
|
337
368
|
# Destroy old taggings:
|
@@ -12,7 +12,7 @@ module ActsAsTaggableOn::Taggable
|
|
12
12
|
tag_type = tags_type.to_s.singularize
|
13
13
|
context_tags = tags_type.to_sym
|
14
14
|
|
15
|
-
class_eval
|
15
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
16
16
|
def #{tag_type}_list_changed?
|
17
17
|
changed_attributes.include?("#{tag_type}_list")
|
18
18
|
end
|
@@ -28,10 +28,10 @@ module ActsAsTaggableOn::Taggable
|
|
28
28
|
def #{tag_type}_list_changes
|
29
29
|
[changed_attributes['#{tag_type}_list'], __send__('#{tag_type}_list')] if changed_attributes.include?("#{tag_type}_list")
|
30
30
|
end
|
31
|
-
|
31
|
+
RUBY
|
32
32
|
|
33
33
|
end
|
34
34
|
end
|
35
35
|
end
|
36
36
|
end
|
37
|
-
end
|
37
|
+
end
|
@@ -19,11 +19,11 @@ module ActsAsTaggableOn::Taggable
|
|
19
19
|
|
20
20
|
def initialize_acts_as_taggable_on_ownership
|
21
21
|
tag_types.map(&:to_s).each do |tag_type|
|
22
|
-
class_eval
|
22
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
23
23
|
def #{tag_type}_from(owner)
|
24
24
|
owner_tag_list_on(owner, '#{tag_type}')
|
25
25
|
end
|
26
|
-
|
26
|
+
RUBY
|
27
27
|
end
|
28
28
|
end
|
29
29
|
end
|
@@ -37,22 +37,25 @@ module ActsAsTaggableOn::Taggable
|
|
37
37
|
#{ActsAsTaggableOn::Tagging.table_name}.tagger_id = ? AND
|
38
38
|
#{ActsAsTaggableOn::Tagging.table_name}.tagger_type = ?), context.to_s, owner.id, owner.class.base_class.to_s])
|
39
39
|
end
|
40
|
+
|
40
41
|
# when preserving tag order, return tags in created order
|
41
42
|
# if we added the order to the association this would always apply
|
42
|
-
|
43
|
-
|
43
|
+
if self.class.preserve_tag_order?
|
44
|
+
scope.order("#{ActsAsTaggableOn::Tagging.table_name}.id")
|
45
|
+
else
|
46
|
+
scope
|
47
|
+
end
|
44
48
|
end
|
45
49
|
|
46
50
|
def cached_owned_tag_list_on(context)
|
47
51
|
variable_name = "@owned_#{context}_list"
|
48
|
-
cache = instance_variable_get(variable_name) || instance_variable_set(variable_name, {})
|
52
|
+
cache = (instance_variable_defined?(variable_name) && instance_variable_get(variable_name)) || instance_variable_set(variable_name, {})
|
49
53
|
end
|
50
54
|
|
51
55
|
def owner_tag_list_on(owner, context)
|
52
56
|
add_custom_context(context)
|
53
57
|
|
54
58
|
cache = cached_owned_tag_list_on(context)
|
55
|
-
cache.delete_if { |key, value| key.id == owner.id && key.class == owner.class }
|
56
59
|
|
57
60
|
cache[owner] ||= ActsAsTaggableOn::TagList.new(*owner_tags_on(owner, context).map(&:name))
|
58
61
|
end
|
@@ -61,7 +64,6 @@ module ActsAsTaggableOn::Taggable
|
|
61
64
|
add_custom_context(context)
|
62
65
|
|
63
66
|
cache = cached_owned_tag_list_on(context)
|
64
|
-
cache.delete_if { |key, value| key.id == owner.id && key.class == owner.class }
|
65
67
|
|
66
68
|
cache[owner] = ActsAsTaggableOn::TagList.from(new_list)
|
67
69
|
end
|
@@ -86,13 +88,20 @@ module ActsAsTaggableOn::Taggable
|
|
86
88
|
|
87
89
|
# Tag maintenance based on whether preserving the created order of tags
|
88
90
|
if self.class.preserve_tag_order?
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
91
|
+
old_tags, new_tags = owned_tags - tags, tags - owned_tags
|
92
|
+
|
93
|
+
shared_tags = owned_tags & tags
|
94
|
+
|
95
|
+
if shared_tags.any? && tags[0...shared_tags.size] != shared_tags
|
96
|
+
index = shared_tags.each_with_index { |_, i| break i unless shared_tags[i] == tags[i] }
|
97
|
+
|
98
|
+
# Update arrays of tag objects
|
99
|
+
old_tags |= owned_tags.from(index)
|
100
|
+
new_tags |= owned_tags.from(index) & shared_tags
|
101
|
+
|
102
|
+
# Order the array of tag objects to match the tag list
|
103
|
+
new_tags = tags.map { |t| new_tags.find { |n| n.name.downcase == t.name.downcase } }.compact
|
104
|
+
end
|
96
105
|
else
|
97
106
|
# Delete discarded tags and create new tags
|
98
107
|
old_tags = owned_tags - tags
|
@@ -104,7 +113,7 @@ module ActsAsTaggableOn::Taggable
|
|
104
113
|
if old_tags.present?
|
105
114
|
old_taggings = ActsAsTaggableOn::Tagging.where(:taggable_id => id, :taggable_type => self.class.base_class.to_s,
|
106
115
|
:tagger_type => owner.class.base_class.to_s, :tagger_id => owner.id,
|
107
|
-
:tag_id => old_tags, :context => context)
|
116
|
+
:tag_id => old_tags, :context => context)
|
108
117
|
end
|
109
118
|
|
110
119
|
# Destroy old taggings:
|
@@ -9,7 +9,7 @@ module ActsAsTaggableOn::Taggable
|
|
9
9
|
module ClassMethods
|
10
10
|
def initialize_acts_as_taggable_on_related
|
11
11
|
tag_types.map(&:to_s).each do |tag_type|
|
12
|
-
class_eval
|
12
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
13
13
|
def find_related_#{tag_type}(options = {})
|
14
14
|
related_tags_for('#{tag_type}', self.class, options)
|
15
15
|
end
|
@@ -18,11 +18,11 @@ module ActsAsTaggableOn::Taggable
|
|
18
18
|
def find_related_#{tag_type}_for(klass, options = {})
|
19
19
|
related_tags_for('#{tag_type}', klass, options)
|
20
20
|
end
|
21
|
-
|
21
|
+
RUBY
|
22
22
|
end
|
23
23
|
|
24
24
|
unless tag_types.empty?
|
25
|
-
class_eval
|
25
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
26
26
|
def find_matching_contexts(search_context, result_context, options = {})
|
27
27
|
matching_contexts_for(search_context.to_s, result_context.to_s, self.class, options)
|
28
28
|
end
|
@@ -30,7 +30,7 @@ module ActsAsTaggableOn::Taggable
|
|
30
30
|
def find_matching_contexts_for(klass, search_context, result_context, options = {})
|
31
31
|
matching_contexts_for(search_context.to_s, result_context.to_s, klass, options)
|
32
32
|
end
|
33
|
-
|
33
|
+
RUBY
|
34
34
|
end
|
35
35
|
end
|
36
36
|
|
@@ -44,29 +44,40 @@ module ActsAsTaggableOn::Taggable
|
|
44
44
|
def matching_contexts_for(search_context, result_context, klass, options = {})
|
45
45
|
tags_to_find = tags_on(search_context).collect { |t| t.name }
|
46
46
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
:from => "#{klass.table_name}, #{ActsAsTaggableOn::Tag.table_name}, #{ActsAsTaggableOn::Tagging.table_name}",
|
53
|
-
:conditions => ["#{exclude_self} #{klass.table_name}.#{klass.primary_key} = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = '#{klass.base_class.to_s}' AND #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key} AND #{ActsAsTaggableOn::Tag.table_name}.name IN (?) AND #{ActsAsTaggableOn::Tagging.table_name}.context = ?", tags_to_find, result_context],
|
54
|
-
:group => group_columns,
|
55
|
-
:order => "count DESC" }.update(options))
|
47
|
+
klass.select("#{klass.table_name}.*, COUNT(#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}) AS count") \
|
48
|
+
.from("#{klass.table_name}, #{ActsAsTaggableOn::Tag.table_name}, #{ActsAsTaggableOn::Tagging.table_name}") \
|
49
|
+
.where(["#{exclude_self(klass, id)} #{klass.table_name}.#{klass.primary_key} = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = '#{klass.base_class.to_s}' AND #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key} AND #{ActsAsTaggableOn::Tag.table_name}.name IN (?) AND #{ActsAsTaggableOn::Tagging.table_name}.context = ?", tags_to_find, result_context]) \
|
50
|
+
.group(group_columns(klass)) \
|
51
|
+
.order("count DESC")
|
56
52
|
end
|
57
53
|
|
58
54
|
def related_tags_for(context, klass, options = {})
|
59
|
-
|
55
|
+
tags_to_ignore = Array.wrap(options.delete(:ignore)).map(&:to_s) || []
|
56
|
+
tags_to_find = tags_on(context).collect { |t| t.name }.reject { |t| tags_to_ignore.include? t }
|
60
57
|
|
61
|
-
|
58
|
+
klass.select("#{klass.table_name}.*, COUNT(#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}) AS count") \
|
59
|
+
.from("#{klass.table_name}, #{ActsAsTaggableOn::Tag.table_name}, #{ActsAsTaggableOn::Tagging.table_name}") \
|
60
|
+
.where(["#{exclude_self(klass, id)} #{klass.table_name}.#{klass.primary_key} = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = '#{klass.base_class.to_s}' AND #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key} AND #{ActsAsTaggableOn::Tag.table_name}.name IN (?)", tags_to_find]) \
|
61
|
+
.group(group_columns(klass)) \
|
62
|
+
.order("count DESC")
|
63
|
+
end
|
62
64
|
|
63
|
-
|
65
|
+
private
|
66
|
+
|
67
|
+
def exclude_self(klass, id)
|
68
|
+
if [self.class.base_class, self.class].include? klass
|
69
|
+
"#{klass.table_name}.#{klass.primary_key} != #{id} AND"
|
70
|
+
else
|
71
|
+
nil
|
72
|
+
end
|
73
|
+
end
|
64
74
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
75
|
+
def group_columns(klass)
|
76
|
+
if ActsAsTaggableOn::Tag.using_postgresql?
|
77
|
+
grouped_column_names_for(klass)
|
78
|
+
else
|
79
|
+
"#{klass.table_name}.#{klass.primary_key}"
|
80
|
+
end
|
70
81
|
end
|
71
82
|
end
|
72
83
|
end
|
@@ -2,7 +2,7 @@ module ActsAsTaggableOn
|
|
2
2
|
class Tag < ::ActiveRecord::Base
|
3
3
|
include ActsAsTaggableOn::Utils
|
4
4
|
|
5
|
-
attr_accessible :name
|
5
|
+
attr_accessible :name if defined?(ActiveModel::MassAssignmentSecurity)
|
6
6
|
|
7
7
|
### ASSOCIATIONS:
|
8
8
|
|
@@ -11,17 +11,30 @@ module ActsAsTaggableOn
|
|
11
11
|
### VALIDATIONS:
|
12
12
|
|
13
13
|
validates_presence_of :name
|
14
|
-
validates_uniqueness_of :name
|
14
|
+
validates_uniqueness_of :name, :if => :validates_name_uniqueness?
|
15
15
|
validates_length_of :name, :maximum => 255
|
16
16
|
|
17
|
+
# monkey patch this method if don't need name uniqueness validation
|
18
|
+
def validates_name_uniqueness?
|
19
|
+
true
|
20
|
+
end
|
21
|
+
|
17
22
|
### SCOPES:
|
18
23
|
|
19
24
|
def self.named(name)
|
20
|
-
|
25
|
+
if ActsAsTaggableOn.strict_case_match
|
26
|
+
where(["name = #{binary}?", name])
|
27
|
+
else
|
28
|
+
where(["lower(name) = ?", name.downcase])
|
29
|
+
end
|
21
30
|
end
|
22
31
|
|
23
32
|
def self.named_any(list)
|
24
|
-
|
33
|
+
if ActsAsTaggableOn.strict_case_match
|
34
|
+
where(list.map { |tag| sanitize_sql(["name = #{binary}?", tag.to_s.mb_chars]) }.join(" OR "))
|
35
|
+
else
|
36
|
+
where(list.map { |tag| sanitize_sql(["lower(name) = ?", tag.to_s.mb_chars.downcase]) }.join(" OR "))
|
37
|
+
end
|
25
38
|
end
|
26
39
|
|
27
40
|
def self.named_like(name)
|
@@ -35,7 +48,11 @@ module ActsAsTaggableOn
|
|
35
48
|
### CLASS METHODS:
|
36
49
|
|
37
50
|
def self.find_or_create_with_like_by_name(name)
|
38
|
-
|
51
|
+
if (ActsAsTaggableOn.strict_case_match)
|
52
|
+
self.find_or_create_all_with_like_by_name([name]).first
|
53
|
+
else
|
54
|
+
named_like(name).first || create(:name => name)
|
55
|
+
end
|
39
56
|
end
|
40
57
|
|
41
58
|
def self.find_or_create_all_with_like_by_name(*list)
|
@@ -43,14 +60,14 @@ module ActsAsTaggableOn
|
|
43
60
|
|
44
61
|
return [] if list.empty?
|
45
62
|
|
46
|
-
existing_tags = Tag.named_any(list)
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
created_tags = new_tag_names.map { |name| Tag.create(:name => name) }
|
63
|
+
existing_tags = Tag.named_any(list)
|
64
|
+
|
65
|
+
list.map do |tag_name|
|
66
|
+
comparable_tag_name = comparable_name(tag_name)
|
67
|
+
existing_tag = existing_tags.find { |tag| comparable_name(tag.name) == comparable_tag_name }
|
52
68
|
|
53
|
-
|
69
|
+
existing_tag || Tag.create(:name => tag_name)
|
70
|
+
end
|
54
71
|
end
|
55
72
|
|
56
73
|
### INSTANCE METHODS:
|
@@ -69,9 +86,14 @@ module ActsAsTaggableOn
|
|
69
86
|
|
70
87
|
class << self
|
71
88
|
private
|
72
|
-
|
73
|
-
|
74
|
-
|
89
|
+
|
90
|
+
def comparable_name(str)
|
91
|
+
str.mb_chars.downcase.to_s
|
92
|
+
end
|
93
|
+
|
94
|
+
def binary
|
95
|
+
/mysql/ === ActiveRecord::Base.connection_config[:adapter] ? "BINARY " : nil
|
96
|
+
end
|
75
97
|
end
|
76
98
|
end
|
77
99
|
end
|
@@ -58,7 +58,7 @@ module ActsAsTaggableOn
|
|
58
58
|
end
|
59
59
|
|
60
60
|
##
|
61
|
-
# Transform the tag_list into a tag string suitable for
|
61
|
+
# Transform the tag_list into a tag string suitable for editing in a form.
|
62
62
|
# The tags are joined with <tt>TagList.delimiter</tt> and quoted if necessary.
|
63
63
|
#
|
64
64
|
# Example:
|
@@ -81,7 +81,7 @@ module ActsAsTaggableOn
|
|
81
81
|
def clean!
|
82
82
|
reject!(&:blank?)
|
83
83
|
map!(&:strip)
|
84
|
-
map!
|
84
|
+
map!{ |tag| tag.mb_chars.downcase.to_s } if ActsAsTaggableOn.force_lowercase
|
85
85
|
map!(&:parameterize) if ActsAsTaggableOn.force_parameterize
|
86
86
|
|
87
87
|
uniq!
|
@@ -80,7 +80,7 @@ module ActsAsTaggableOn
|
|
80
80
|
self.preserve_tag_order = preserve_tag_order
|
81
81
|
|
82
82
|
class_eval do
|
83
|
-
has_many :taggings, :as => :taggable, :dependent => :destroy, :
|
83
|
+
has_many :taggings, :as => :taggable, :dependent => :destroy, :class_name => "ActsAsTaggableOn::Tagging"
|
84
84
|
has_many :base_tags, :through => :taggings, :source => :tag, :class_name => "ActsAsTaggableOn::Tag"
|
85
85
|
|
86
86
|
def self.taggable?
|
@@ -88,14 +88,17 @@ module ActsAsTaggableOn
|
|
88
88
|
end
|
89
89
|
|
90
90
|
include ActsAsTaggableOn::Utils
|
91
|
-
include ActsAsTaggableOn::Taggable::Core
|
92
|
-
include ActsAsTaggableOn::Taggable::Collection
|
93
|
-
include ActsAsTaggableOn::Taggable::Cache
|
94
|
-
include ActsAsTaggableOn::Taggable::Ownership
|
95
|
-
include ActsAsTaggableOn::Taggable::Related
|
96
|
-
include ActsAsTaggableOn::Taggable::Dirty
|
97
91
|
end
|
98
92
|
end
|
93
|
+
|
94
|
+
# each of these add context-specific methods and must be
|
95
|
+
# called on each call of taggable_on
|
96
|
+
include ActsAsTaggableOn::Taggable::Core
|
97
|
+
include ActsAsTaggableOn::Taggable::Collection
|
98
|
+
include ActsAsTaggableOn::Taggable::Cache
|
99
|
+
include ActsAsTaggableOn::Taggable::Ownership
|
100
|
+
include ActsAsTaggableOn::Taggable::Related
|
101
|
+
include ActsAsTaggableOn::Taggable::Dirty
|
99
102
|
end
|
100
103
|
|
101
104
|
end
|
@@ -15,9 +15,18 @@ module ActsAsTaggableOn
|
|
15
15
|
# end
|
16
16
|
def acts_as_tagger(opts={})
|
17
17
|
class_eval do
|
18
|
-
|
19
|
-
|
20
|
-
|
18
|
+
has_many_with_compatibility :owned_taggings,
|
19
|
+
opts.merge(
|
20
|
+
:as => :tagger,
|
21
|
+
:dependent => :destroy,
|
22
|
+
:class_name => "ActsAsTaggableOn::Tagging"
|
23
|
+
)
|
24
|
+
|
25
|
+
has_many_with_compatibility :owned_tags,
|
26
|
+
:through => :owned_taggings,
|
27
|
+
:source => :tag,
|
28
|
+
:class_name => "ActsAsTaggableOn::Tag",
|
29
|
+
:uniq => true
|
21
30
|
end
|
22
31
|
|
23
32
|
include ActsAsTaggableOn::Tagger::InstanceMethods
|