acts-as-taggable-on 2.1.0 → 2.3.0
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/.gitignore +3 -0
- data/.travis.yml +1 -2
- data/CHANGELOG +10 -0
- data/README.rdoc +87 -67
- data/acts-as-taggable-on.gemspec +5 -4
- data/lib/acts-as-taggable-on/version.rb +1 -1
- data/lib/acts-as-taggable-on.rb +29 -6
- data/lib/acts_as_taggable_on/acts_as_taggable_on/cache.rb +1 -1
- data/lib/acts_as_taggable_on/acts_as_taggable_on/collection.rb +9 -21
- data/lib/acts_as_taggable_on/acts_as_taggable_on/core.rb +110 -33
- data/lib/acts_as_taggable_on/acts_as_taggable_on/dirty.rb +37 -0
- data/lib/acts_as_taggable_on/acts_as_taggable_on/ownership.rb +33 -12
- data/lib/acts_as_taggable_on/acts_as_taggable_on/related.rb +16 -16
- data/lib/acts_as_taggable_on/tag.rb +14 -18
- data/lib/acts_as_taggable_on/tag_list.rb +13 -12
- data/lib/acts_as_taggable_on/taggable.rb +102 -0
- data/lib/acts_as_taggable_on/{acts_as_tagger.rb → tagger.rb} +3 -3
- data/lib/acts_as_taggable_on/tagging.rb +12 -2
- data/lib/acts_as_taggable_on/tags_helper.rb +2 -2
- data/lib/acts_as_taggable_on/utils.rb +11 -8
- data/lib/generators/acts_as_taggable_on/migration/migration_generator.rb +9 -2
- data/lib/generators/acts_as_taggable_on/migration/templates/active_record/migration.rb +3 -1
- data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +286 -61
- data/spec/acts_as_taggable_on/tag_list_spec.rb +84 -61
- data/spec/acts_as_taggable_on/tag_spec.rb +42 -24
- data/spec/acts_as_taggable_on/taggable_spec.rb +234 -39
- data/spec/acts_as_taggable_on/tagger_spec.rb +36 -15
- data/spec/acts_as_taggable_on/tagging_spec.rb +2 -5
- data/spec/acts_as_taggable_on/tags_helper_spec.rb +16 -0
- data/spec/acts_as_taggable_on/utils_spec.rb +2 -3
- data/spec/database.yml.sample +1 -1
- data/spec/generators/acts_as_taggable_on/migration/migration_generator_spec.rb +22 -0
- data/spec/models.rb +16 -2
- data/spec/schema.rb +12 -0
- data/spec/spec_helper.rb +4 -1
- metadata +98 -125
- data/VERSION +0 -1
- data/generators/acts_as_taggable_on_migration/acts_as_taggable_on_migration_generator.rb +0 -7
- data/generators/acts_as_taggable_on_migration/templates/migration.rb +0 -29
- data/lib/acts_as_taggable_on/acts_as_taggable_on.rb +0 -63
- data/lib/acts_as_taggable_on/compatibility/Gemfile +0 -8
- data/lib/acts_as_taggable_on/compatibility/active_record_backports.rb +0 -21
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
module ActsAsTaggableOn::Taggable
|
|
2
|
-
module Core
|
|
2
|
+
module Core
|
|
3
3
|
def self.included(base)
|
|
4
4
|
base.send :include, ActsAsTaggableOn::Taggable::Core::InstanceMethods
|
|
5
5
|
base.extend ActsAsTaggableOn::Taggable::Core::ClassMethods
|
|
@@ -8,21 +8,32 @@ module ActsAsTaggableOn::Taggable
|
|
|
8
8
|
attr_writer :custom_contexts
|
|
9
9
|
after_save :save_tags
|
|
10
10
|
end
|
|
11
|
-
|
|
11
|
+
|
|
12
12
|
base.initialize_acts_as_taggable_on_core
|
|
13
13
|
end
|
|
14
|
-
|
|
14
|
+
|
|
15
15
|
module ClassMethods
|
|
16
16
|
def initialize_acts_as_taggable_on_core
|
|
17
17
|
tag_types.map(&:to_s).each do |tags_type|
|
|
18
18
|
tag_type = tags_type.to_s.singularize
|
|
19
19
|
context_taggings = "#{tag_type}_taggings".to_sym
|
|
20
20
|
context_tags = tags_type.to_sym
|
|
21
|
-
|
|
21
|
+
taggings_order = (preserve_tag_order? ? "#{ActsAsTaggableOn::Tagging.table_name}.id" : nil)
|
|
22
|
+
|
|
22
23
|
class_eval do
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
has_many
|
|
24
|
+
# when preserving tag order, include order option so that for a 'tags' context
|
|
25
|
+
# the associations tag_taggings & tags are always returned in created order
|
|
26
|
+
has_many context_taggings, :as => :taggable,
|
|
27
|
+
:dependent => :destroy,
|
|
28
|
+
:include => :tag,
|
|
29
|
+
:class_name => "ActsAsTaggableOn::Tagging",
|
|
30
|
+
:conditions => ["#{ActsAsTaggableOn::Tagging.table_name}.context = ?", tags_type],
|
|
31
|
+
:order => taggings_order
|
|
32
|
+
|
|
33
|
+
has_many context_tags, :through => context_taggings,
|
|
34
|
+
:source => :tag,
|
|
35
|
+
:class_name => "ActsAsTaggableOn::Tag",
|
|
36
|
+
:order => taggings_order
|
|
26
37
|
end
|
|
27
38
|
|
|
28
39
|
class_eval %(
|
|
@@ -38,11 +49,11 @@ module ActsAsTaggableOn::Taggable
|
|
|
38
49
|
all_tags_list_on('#{tags_type}')
|
|
39
50
|
end
|
|
40
51
|
)
|
|
41
|
-
end
|
|
52
|
+
end
|
|
42
53
|
end
|
|
43
|
-
|
|
44
|
-
def
|
|
45
|
-
super(*
|
|
54
|
+
|
|
55
|
+
def taggable_on(preserve_tag_order, *tag_types)
|
|
56
|
+
super(preserve_tag_order, *tag_types)
|
|
46
57
|
initialize_acts_as_taggable_on_core
|
|
47
58
|
end
|
|
48
59
|
|
|
@@ -59,12 +70,14 @@ module ActsAsTaggableOn::Taggable
|
|
|
59
70
|
# * <tt>:exclude</tt> - if set to true, return objects that are *NOT* tagged with the specified tags
|
|
60
71
|
# * <tt>:any</tt> - if set to true, return objects that are tagged with *ANY* of the specified tags
|
|
61
72
|
# * <tt>:match_all</tt> - if set to true, return objects that are *ONLY* tagged with the specified tags
|
|
73
|
+
# * <tt>:owned_by</tt> - return objects that are *ONLY* owned by the owner
|
|
62
74
|
#
|
|
63
75
|
# Example:
|
|
64
76
|
# User.tagged_with("awesome", "cool") # Users that are tagged with awesome and cool
|
|
65
77
|
# User.tagged_with("awesome", "cool", :exclude => true) # Users that are not tagged with awesome or cool
|
|
66
78
|
# User.tagged_with("awesome", "cool", :any => true) # Users that are tagged with awesome or cool
|
|
67
79
|
# User.tagged_with("awesome", "cool", :match_all => true) # Users that are tagged with just awesome and cool
|
|
80
|
+
# User.tagged_with("awesome", "cool", :owned_by => foo ) # Users that are tagged with just awesome and cool by 'foo'
|
|
68
81
|
def tagged_with(tags, options = {})
|
|
69
82
|
tag_list = ActsAsTaggableOn::TagList.from(tags)
|
|
70
83
|
empty_result = scoped(:conditions => "1 = 0")
|
|
@@ -75,23 +88,35 @@ module ActsAsTaggableOn::Taggable
|
|
|
75
88
|
conditions = []
|
|
76
89
|
|
|
77
90
|
context = options.delete(:on)
|
|
91
|
+
owned_by = options.delete(:owned_by)
|
|
78
92
|
alias_base_name = undecorated_table_name.gsub('.','_')
|
|
79
93
|
|
|
80
94
|
if options.delete(:exclude)
|
|
81
|
-
|
|
82
|
-
|
|
95
|
+
if options.delete(:wild)
|
|
96
|
+
tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{like_operator} ? ESCAPE '!'", "%#{escape_like(t)}%"]) }.join(" OR ")
|
|
97
|
+
else
|
|
98
|
+
tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{like_operator} ?", t]) }.join(" OR ")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
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)})"
|
|
83
102
|
|
|
84
103
|
elsif options.delete(:any)
|
|
85
104
|
# get tags, drop out if nothing returned (we need at least one)
|
|
86
|
-
|
|
105
|
+
if options.delete(:wild)
|
|
106
|
+
tags = ActsAsTaggableOn::Tag.named_like_any(tag_list)
|
|
107
|
+
else
|
|
108
|
+
tags = ActsAsTaggableOn::Tag.named_any(tag_list)
|
|
109
|
+
end
|
|
110
|
+
|
|
87
111
|
return scoped(:conditions => "1 = 0") unless tags.length > 0
|
|
88
112
|
|
|
89
113
|
# setup taggings alias so we can chain, ex: items_locations_taggings_awesome_cool_123
|
|
90
114
|
# avoid ambiguous column name
|
|
91
115
|
taggings_context = context ? "_#{context}" : ''
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
116
|
+
|
|
117
|
+
taggings_alias = adjust_taggings_alias(
|
|
118
|
+
"#{alias_base_name[0..4]}#{taggings_context[0..6]}_taggings_#{sha_prefix(tags.map(&:name).join('_'))}"
|
|
119
|
+
)
|
|
95
120
|
|
|
96
121
|
tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
|
|
97
122
|
" ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
|
|
@@ -109,9 +134,8 @@ module ActsAsTaggableOn::Taggable
|
|
|
109
134
|
return empty_result unless tags.length == tag_list.length
|
|
110
135
|
|
|
111
136
|
tags.each do |tag|
|
|
112
|
-
prefix = "#{tag.safe_name}_#{rand(1024)}"
|
|
113
137
|
|
|
114
|
-
taggings_alias = "#{alias_base_name}_taggings_#{
|
|
138
|
+
taggings_alias = adjust_taggings_alias("#{alias_base_name[0..11]}_taggings_#{sha_prefix(tag.name)}")
|
|
115
139
|
|
|
116
140
|
tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
|
|
117
141
|
" ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
|
|
@@ -119,11 +143,20 @@ module ActsAsTaggableOn::Taggable
|
|
|
119
143
|
" AND #{taggings_alias}.tag_id = #{tag.id}"
|
|
120
144
|
tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
|
|
121
145
|
|
|
146
|
+
if owned_by
|
|
147
|
+
tagging_join << " AND " +
|
|
148
|
+
sanitize_sql([
|
|
149
|
+
"#{taggings_alias}.tagger_id = ? AND #{taggings_alias}.tagger_type = ?",
|
|
150
|
+
owned_by.id,
|
|
151
|
+
owned_by.class.to_s
|
|
152
|
+
])
|
|
153
|
+
end
|
|
154
|
+
|
|
122
155
|
joins << tagging_join
|
|
123
156
|
end
|
|
124
157
|
end
|
|
125
158
|
|
|
126
|
-
taggings_alias, tags_alias = "#{alias_base_name}_taggings_group", "#{alias_base_name}_tags_group"
|
|
159
|
+
taggings_alias, tags_alias = adjust_taggings_alias("#{alias_base_name}_taggings_group"), "#{alias_base_name}_tags_group"
|
|
127
160
|
|
|
128
161
|
if options.delete(:match_all)
|
|
129
162
|
joins << "LEFT OUTER JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
|
|
@@ -146,8 +179,15 @@ module ActsAsTaggableOn::Taggable
|
|
|
146
179
|
def is_taggable?
|
|
147
180
|
true
|
|
148
181
|
end
|
|
149
|
-
|
|
150
|
-
|
|
182
|
+
|
|
183
|
+
def adjust_taggings_alias(taggings_alias)
|
|
184
|
+
if taggings_alias.size > 75
|
|
185
|
+
taggings_alias = 'taggings_alias_' + Digest::SHA1.hexdigest(taggings_alias)
|
|
186
|
+
end
|
|
187
|
+
taggings_alias
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
151
191
|
module InstanceMethods
|
|
152
192
|
# all column names are necessary for PostgreSQL group clause
|
|
153
193
|
def grouped_column_names_for(object)
|
|
@@ -200,7 +240,7 @@ module ActsAsTaggableOn::Taggable
|
|
|
200
240
|
|
|
201
241
|
opts = ["#{tagging_table_name}.context = ?", context.to_s]
|
|
202
242
|
scope = base_tags.where(opts)
|
|
203
|
-
|
|
243
|
+
|
|
204
244
|
if ActsAsTaggableOn::Tag.using_postgresql?
|
|
205
245
|
group_columns = grouped_column_names_for(ActsAsTaggableOn::Tag)
|
|
206
246
|
scope = scope.order("max(#{tagging_table_name}.created_at)").group(group_columns)
|
|
@@ -214,13 +254,19 @@ module ActsAsTaggableOn::Taggable
|
|
|
214
254
|
##
|
|
215
255
|
# Returns all tags that are not owned of a given context
|
|
216
256
|
def tags_on(context)
|
|
217
|
-
base_tags.where(["#{ActsAsTaggableOn::Tagging.table_name}.context = ? AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_id IS NULL", context.to_s])
|
|
257
|
+
scope = base_tags.where(["#{ActsAsTaggableOn::Tagging.table_name}.context = ? AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_id IS NULL", context.to_s])
|
|
258
|
+
# when preserving tag order, return tags in created order
|
|
259
|
+
# if we added the order to the association this would always apply
|
|
260
|
+
scope = scope.order("#{ActsAsTaggableOn::Tagging.table_name}.id") if self.class.preserve_tag_order?
|
|
261
|
+
scope.all
|
|
218
262
|
end
|
|
219
263
|
|
|
220
264
|
def set_tag_list_on(context, new_list)
|
|
221
265
|
add_custom_context(context)
|
|
222
266
|
|
|
223
267
|
variable_name = "@#{context.to_s.singularize}_list"
|
|
268
|
+
process_dirty_object(context, new_list) unless custom_contexts.include?(context.to_s)
|
|
269
|
+
|
|
224
270
|
instance_variable_set(variable_name, ActsAsTaggableOn::TagList.from(new_list))
|
|
225
271
|
end
|
|
226
272
|
|
|
@@ -228,12 +274,26 @@ module ActsAsTaggableOn::Taggable
|
|
|
228
274
|
custom_contexts + self.class.tag_types.map(&:to_s)
|
|
229
275
|
end
|
|
230
276
|
|
|
277
|
+
def process_dirty_object(context,new_list)
|
|
278
|
+
value = new_list.is_a?(Array) ? new_list.join(', ') : new_list
|
|
279
|
+
attrib = "#{context.to_s.singularize}_list"
|
|
280
|
+
|
|
281
|
+
if changed_attributes.include?(attrib)
|
|
282
|
+
# The attribute already has an unsaved change.
|
|
283
|
+
old = changed_attributes[attrib]
|
|
284
|
+
changed_attributes.delete(attrib) if (old.to_s == value.to_s)
|
|
285
|
+
else
|
|
286
|
+
old = tag_list_on(context).to_s
|
|
287
|
+
changed_attributes[attrib] = old if (old.to_s != value.to_s)
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
231
291
|
def reload(*args)
|
|
232
292
|
self.class.tag_types.each do |context|
|
|
233
293
|
instance_variable_set("@#{context.to_s.singularize}_list", nil)
|
|
234
294
|
instance_variable_set("@all_#{context.to_s.singularize}_list", nil)
|
|
235
295
|
end
|
|
236
|
-
|
|
296
|
+
|
|
237
297
|
super(*args)
|
|
238
298
|
end
|
|
239
299
|
|
|
@@ -241,22 +301,39 @@ module ActsAsTaggableOn::Taggable
|
|
|
241
301
|
tagging_contexts.each do |context|
|
|
242
302
|
next unless tag_list_cache_set_on(context)
|
|
243
303
|
|
|
304
|
+
# List of currently assigned tag names
|
|
244
305
|
tag_list = tag_list_cache_on(context).uniq
|
|
245
306
|
|
|
246
307
|
# Find existing tags or create non-existing tags:
|
|
247
|
-
|
|
308
|
+
tags = ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name(tag_list)
|
|
248
309
|
|
|
310
|
+
# Tag objects for currently assigned tags
|
|
249
311
|
current_tags = tags_on(context)
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
312
|
+
|
|
313
|
+
# Tag maintenance based on whether preserving the created order of tags
|
|
314
|
+
if self.class.preserve_tag_order?
|
|
315
|
+
# First off order the array of tag objects to match the tag list
|
|
316
|
+
# rather than existing tags followed by new tags
|
|
317
|
+
tags = tag_list.map{|l| tags.detect{|t| t.name.downcase == l.downcase}}
|
|
318
|
+
# To preserve tags in the order in which they were added
|
|
319
|
+
# delete all current tags and create new tags if the content or order has changed
|
|
320
|
+
old_tags = (tags == current_tags ? [] : current_tags)
|
|
321
|
+
new_tags = (tags == current_tags ? [] : tags)
|
|
322
|
+
else
|
|
323
|
+
# Delete discarded tags and create new tags
|
|
324
|
+
old_tags = current_tags - tags
|
|
325
|
+
new_tags = tags - current_tags
|
|
326
|
+
end
|
|
327
|
+
|
|
253
328
|
# Find taggings to remove:
|
|
254
|
-
|
|
255
|
-
|
|
329
|
+
if old_tags.present?
|
|
330
|
+
old_taggings = taggings.where(:tagger_type => nil, :tagger_id => nil,
|
|
331
|
+
:context => context.to_s, :tag_id => old_tags).all
|
|
332
|
+
end
|
|
256
333
|
|
|
334
|
+
# Destroy old taggings:
|
|
257
335
|
if old_taggings.present?
|
|
258
|
-
#
|
|
259
|
-
ActsAsTaggableOn::Tagging.destroy_all :id => old_taggings.map(&:id)
|
|
336
|
+
ActsAsTaggableOn::Tagging.destroy_all "#{ActsAsTaggableOn::Tagging.primary_key}".to_sym => old_taggings.map(&:id)
|
|
260
337
|
end
|
|
261
338
|
|
|
262
339
|
# Create new taggings:
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module ActsAsTaggableOn::Taggable
|
|
2
|
+
module Dirty
|
|
3
|
+
def self.included(base)
|
|
4
|
+
base.extend ActsAsTaggableOn::Taggable::Dirty::ClassMethods
|
|
5
|
+
|
|
6
|
+
base.initialize_acts_as_taggable_on_dirty
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
module ClassMethods
|
|
10
|
+
def initialize_acts_as_taggable_on_dirty
|
|
11
|
+
tag_types.map(&:to_s).each do |tags_type|
|
|
12
|
+
tag_type = tags_type.to_s.singularize
|
|
13
|
+
context_tags = tags_type.to_sym
|
|
14
|
+
|
|
15
|
+
class_eval %(
|
|
16
|
+
def #{tag_type}_list_changed?
|
|
17
|
+
changed_attributes.include?("#{tag_type}_list")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def #{tag_type}_list_was
|
|
21
|
+
changed_attributes.include?("#{tag_type}_list") ? changed_attributes["#{tag_type}_list"] : __send__("#{tag_type}_list")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def #{tag_type}_list_change
|
|
25
|
+
[changed_attributes['#{tag_type}_list'], __send__('#{tag_type}_list')] if changed_attributes.include?("#{tag_type}_list")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def #{tag_type}_list_changes
|
|
29
|
+
[changed_attributes['#{tag_type}_list'], __send__('#{tag_type}_list')] if changed_attributes.include?("#{tag_type}_list")
|
|
30
|
+
end
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -31,12 +31,16 @@ module ActsAsTaggableOn::Taggable
|
|
|
31
31
|
module InstanceMethods
|
|
32
32
|
def owner_tags_on(owner, context)
|
|
33
33
|
if owner.nil?
|
|
34
|
-
base_tags.where([%(#{ActsAsTaggableOn::Tagging.table_name}.context = ?), context.to_s])
|
|
34
|
+
scope = base_tags.where([%(#{ActsAsTaggableOn::Tagging.table_name}.context = ?), context.to_s])
|
|
35
35
|
else
|
|
36
|
-
base_tags.where([%(#{ActsAsTaggableOn::Tagging.table_name}.context = ? AND
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
scope = base_tags.where([%(#{ActsAsTaggableOn::Tagging.table_name}.context = ? AND
|
|
37
|
+
#{ActsAsTaggableOn::Tagging.table_name}.tagger_id = ? AND
|
|
38
|
+
#{ActsAsTaggableOn::Tagging.table_name}.tagger_type = ?), context.to_s, owner.id, owner.class.to_s])
|
|
39
39
|
end
|
|
40
|
+
# when preserving tag order, return tags in created order
|
|
41
|
+
# if we added the order to the association this would always apply
|
|
42
|
+
scope = scope.order("#{ActsAsTaggableOn::Tagging.table_name}.id") if self.class.preserve_tag_order?
|
|
43
|
+
scope.all
|
|
40
44
|
end
|
|
41
45
|
|
|
42
46
|
def cached_owned_tag_list_on(context)
|
|
@@ -73,21 +77,38 @@ module ActsAsTaggableOn::Taggable
|
|
|
73
77
|
def save_owned_tags
|
|
74
78
|
tagging_contexts.each do |context|
|
|
75
79
|
cached_owned_tag_list_on(context).each do |owner, tag_list|
|
|
80
|
+
|
|
76
81
|
# Find existing tags or create non-existing tags:
|
|
77
|
-
|
|
82
|
+
tags = ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name(tag_list.uniq)
|
|
78
83
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
84
|
+
# Tag objects for owned tags
|
|
85
|
+
owned_tags = owner_tags_on(owner, context)
|
|
86
|
+
|
|
87
|
+
# Tag maintenance based on whether preserving the created order of tags
|
|
88
|
+
if self.class.preserve_tag_order?
|
|
89
|
+
# First off order the array of tag objects to match the tag list
|
|
90
|
+
# rather than existing tags followed by new tags
|
|
91
|
+
tags = tag_list.uniq.map{|s| tags.detect{|t| t.name.downcase == s.downcase}}
|
|
92
|
+
# To preserve tags in the order in which they were added
|
|
93
|
+
# delete all owned tags and create new tags if the content or order has changed
|
|
94
|
+
old_tags = (tags == owned_tags ? [] : owned_tags)
|
|
95
|
+
new_tags = (tags == owned_tags ? [] : tags)
|
|
96
|
+
else
|
|
97
|
+
# Delete discarded tags and create new tags
|
|
98
|
+
old_tags = owned_tags - tags
|
|
99
|
+
new_tags = tags - owned_tags
|
|
100
|
+
end
|
|
82
101
|
|
|
83
102
|
# Find all taggings that belong to the taggable (self), are owned by the owner,
|
|
84
103
|
# have the correct context, and are removed from the list.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
104
|
+
if old_tags.present?
|
|
105
|
+
old_taggings = ActsAsTaggableOn::Tagging.where(:taggable_id => id, :taggable_type => self.class.base_class.to_s,
|
|
106
|
+
:tagger_type => owner.class.to_s, :tagger_id => owner.id,
|
|
107
|
+
:tag_id => old_tags, :context => context).all
|
|
108
|
+
end
|
|
88
109
|
|
|
110
|
+
# Destroy old taggings:
|
|
89
111
|
if old_taggings.present?
|
|
90
|
-
# Destroy old taggings:
|
|
91
112
|
ActsAsTaggableOn::Tagging.destroy_all(:id => old_taggings.map(&:id))
|
|
92
113
|
end
|
|
93
114
|
|
|
@@ -5,7 +5,7 @@ module ActsAsTaggableOn::Taggable
|
|
|
5
5
|
base.extend ActsAsTaggableOn::Taggable::Related::ClassMethods
|
|
6
6
|
base.initialize_acts_as_taggable_on_related
|
|
7
7
|
end
|
|
8
|
-
|
|
8
|
+
|
|
9
9
|
module ClassMethods
|
|
10
10
|
def initialize_acts_as_taggable_on_related
|
|
11
11
|
tag_types.map(&:to_s).each do |tag_type|
|
|
@@ -20,7 +20,7 @@ module ActsAsTaggableOn::Taggable
|
|
|
20
20
|
end
|
|
21
21
|
)
|
|
22
22
|
end
|
|
23
|
-
|
|
23
|
+
|
|
24
24
|
unless tag_types.empty?
|
|
25
25
|
class_eval %(
|
|
26
26
|
def find_matching_contexts(search_context, result_context, options = {})
|
|
@@ -31,43 +31,43 @@ module ActsAsTaggableOn::Taggable
|
|
|
31
31
|
matching_contexts_for(search_context.to_s, result_context.to_s, klass, options)
|
|
32
32
|
end
|
|
33
33
|
)
|
|
34
|
-
end
|
|
34
|
+
end
|
|
35
35
|
end
|
|
36
|
-
|
|
36
|
+
|
|
37
37
|
def acts_as_taggable_on(*args)
|
|
38
38
|
super(*args)
|
|
39
39
|
initialize_acts_as_taggable_on_related
|
|
40
40
|
end
|
|
41
41
|
end
|
|
42
|
-
|
|
42
|
+
|
|
43
43
|
module InstanceMethods
|
|
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
|
-
exclude_self = "#{klass.table_name}.
|
|
48
|
-
|
|
47
|
+
exclude_self = "#{klass.table_name}.#{klass.primary_key} != #{id} AND" if [self.class.base_class, self.class].include? klass
|
|
48
|
+
|
|
49
49
|
group_columns = ActsAsTaggableOn::Tag.using_postgresql? ? grouped_column_names_for(klass) : "#{klass.table_name}.#{klass.primary_key}"
|
|
50
|
-
|
|
51
|
-
klass.scoped({ :select => "#{klass.table_name}.*, COUNT(#{ActsAsTaggableOn::Tag.table_name}.
|
|
50
|
+
|
|
51
|
+
klass.scoped({ :select => "#{klass.table_name}.*, COUNT(#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}) AS count",
|
|
52
52
|
:from => "#{klass.table_name}, #{ActsAsTaggableOn::Tag.table_name}, #{ActsAsTaggableOn::Tagging.table_name}",
|
|
53
|
-
:conditions => ["#{exclude_self} #{klass.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
54
|
:group => group_columns,
|
|
55
55
|
:order => "count DESC" }.update(options))
|
|
56
56
|
end
|
|
57
|
-
|
|
57
|
+
|
|
58
58
|
def related_tags_for(context, klass, options = {})
|
|
59
59
|
tags_to_find = tags_on(context).collect { |t| t.name }
|
|
60
60
|
|
|
61
|
-
exclude_self = "#{klass.table_name}.
|
|
61
|
+
exclude_self = "#{klass.table_name}.#{klass.primary_key} != #{id} AND" if [self.class.base_class, self.class].include? klass
|
|
62
62
|
|
|
63
|
-
group_columns = ActsAsTaggableOn::Tag.using_postgresql? ? grouped_column_names_for(klass) : "#{klass.table_name}.#{klass.primary_key}"
|
|
63
|
+
group_columns = ActsAsTaggableOn::Tag.using_postgresql? ? grouped_column_names_for(klass) : "#{klass.table_name}.#{klass.primary_key}"
|
|
64
64
|
|
|
65
|
-
klass.scoped({ :select => "#{klass.table_name}.*, COUNT(#{ActsAsTaggableOn::Tag.table_name}.
|
|
65
|
+
klass.scoped({ :select => "#{klass.table_name}.*, COUNT(#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}) AS count",
|
|
66
66
|
:from => "#{klass.table_name}, #{ActsAsTaggableOn::Tag.table_name}, #{ActsAsTaggableOn::Tagging.table_name}",
|
|
67
|
-
:conditions => ["#{exclude_self} #{klass.table_name}.
|
|
67
|
+
: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 (?)", tags_to_find],
|
|
68
68
|
:group => group_columns,
|
|
69
69
|
:order => "count DESC" }.update(options))
|
|
70
70
|
end
|
|
71
71
|
end
|
|
72
72
|
end
|
|
73
|
-
end
|
|
73
|
+
end
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
module ActsAsTaggableOn
|
|
2
2
|
class Tag < ::ActiveRecord::Base
|
|
3
|
-
include ActsAsTaggableOn::ActiveRecord::Backports if ::ActiveRecord::VERSION::MAJOR < 3
|
|
4
3
|
include ActsAsTaggableOn::Utils
|
|
5
|
-
|
|
4
|
+
|
|
6
5
|
attr_accessible :name
|
|
7
6
|
|
|
8
7
|
### ASSOCIATIONS:
|
|
@@ -13,23 +12,24 @@ module ActsAsTaggableOn
|
|
|
13
12
|
|
|
14
13
|
validates_presence_of :name
|
|
15
14
|
validates_uniqueness_of :name
|
|
15
|
+
validates_length_of :name, :maximum => 255
|
|
16
16
|
|
|
17
17
|
### SCOPES:
|
|
18
|
-
|
|
18
|
+
|
|
19
19
|
def self.named(name)
|
|
20
|
-
where(["name
|
|
20
|
+
where(["lower(name) = ?", name.downcase])
|
|
21
21
|
end
|
|
22
|
-
|
|
22
|
+
|
|
23
23
|
def self.named_any(list)
|
|
24
|
-
where(list.map { |tag| sanitize_sql(["name
|
|
24
|
+
where(list.map { |tag| sanitize_sql(["lower(name) = ?", tag.to_s.downcase]) }.join(" OR "))
|
|
25
25
|
end
|
|
26
|
-
|
|
26
|
+
|
|
27
27
|
def self.named_like(name)
|
|
28
|
-
where(["name #{like_operator} ?", "%#{escape_like(name)}%"])
|
|
28
|
+
where(["name #{like_operator} ? ESCAPE '!'", "%#{escape_like(name)}%"])
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
def self.named_like_any(list)
|
|
32
|
-
where(list.map { |tag| sanitize_sql(["name #{like_operator} ?", "%#{escape_like(tag.to_s)}%"]) }.join(" OR "))
|
|
32
|
+
where(list.map { |tag| sanitize_sql(["name #{like_operator} ? ESCAPE '!'", "%#{escape_like(tag.to_s)}%"]) }.join(" OR "))
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
### CLASS METHODS:
|
|
@@ -44,7 +44,7 @@ module ActsAsTaggableOn
|
|
|
44
44
|
return [] if list.empty?
|
|
45
45
|
|
|
46
46
|
existing_tags = Tag.named_any(list).all
|
|
47
|
-
new_tag_names = list.reject do |name|
|
|
47
|
+
new_tag_names = list.reject do |name|
|
|
48
48
|
name = comparable_name(name)
|
|
49
49
|
existing_tags.any? { |tag| comparable_name(tag.name) == name }
|
|
50
50
|
end
|
|
@@ -66,16 +66,12 @@ module ActsAsTaggableOn
|
|
|
66
66
|
def count
|
|
67
67
|
read_attribute(:count).to_i
|
|
68
68
|
end
|
|
69
|
-
|
|
70
|
-
def safe_name
|
|
71
|
-
name.gsub(/[^a-zA-Z0-9]/, '')
|
|
72
|
-
end
|
|
73
|
-
|
|
69
|
+
|
|
74
70
|
class << self
|
|
75
|
-
private
|
|
71
|
+
private
|
|
76
72
|
def comparable_name(str)
|
|
77
|
-
|
|
73
|
+
str.mb_chars.downcase.to_s
|
|
78
74
|
end
|
|
79
75
|
end
|
|
80
76
|
end
|
|
81
|
-
end
|
|
77
|
+
end
|
|
@@ -1,14 +1,13 @@
|
|
|
1
|
+
require 'active_support/core_ext/module/delegation'
|
|
2
|
+
|
|
1
3
|
module ActsAsTaggableOn
|
|
2
4
|
class TagList < Array
|
|
3
|
-
cattr_accessor :delimiter
|
|
4
|
-
self.delimiter = ','
|
|
5
|
-
|
|
6
5
|
attr_accessor :owner
|
|
7
6
|
|
|
8
7
|
def initialize(*args)
|
|
9
8
|
add(*args)
|
|
10
9
|
end
|
|
11
|
-
|
|
10
|
+
|
|
12
11
|
##
|
|
13
12
|
# Returns a new TagList using the given tag string.
|
|
14
13
|
#
|
|
@@ -16,17 +15,16 @@ module ActsAsTaggableOn
|
|
|
16
15
|
# tag_list = TagList.from("One , Two, Three")
|
|
17
16
|
# tag_list # ["One", "Two", "Three"]
|
|
18
17
|
def self.from(string)
|
|
19
|
-
|
|
20
|
-
string = string.join(glue) if string.respond_to?(:join)
|
|
18
|
+
string = string.join(ActsAsTaggableOn.glue) if string.respond_to?(:join)
|
|
21
19
|
|
|
22
20
|
new.tap do |tag_list|
|
|
23
21
|
string = string.to_s.dup
|
|
24
22
|
|
|
25
23
|
# Parse the quoted tags
|
|
26
|
-
string.gsub!(/(\A|#{delimiter})\s*"(.*?)"\s*(#{delimiter}\s*|\z)/) { tag_list << $2; $3 }
|
|
27
|
-
string.gsub!(/(\A|#{delimiter})\s*'(.*?)'\s*(#{delimiter}\s*|\z)/) { tag_list << $2; $3 }
|
|
24
|
+
string.gsub!(/(\A|#{ActsAsTaggableOn.delimiter})\s*"(.*?)"\s*(#{ActsAsTaggableOn.delimiter}\s*|\z)/) { tag_list << $2; $3 }
|
|
25
|
+
string.gsub!(/(\A|#{ActsAsTaggableOn.delimiter})\s*'(.*?)'\s*(#{ActsAsTaggableOn.delimiter}\s*|\z)/) { tag_list << $2; $3 }
|
|
28
26
|
|
|
29
|
-
tag_list.add(string.split(delimiter))
|
|
27
|
+
tag_list.add(string.split(ActsAsTaggableOn.delimiter))
|
|
30
28
|
end
|
|
31
29
|
end
|
|
32
30
|
|
|
@@ -69,16 +67,19 @@ module ActsAsTaggableOn
|
|
|
69
67
|
tags.send(:clean!)
|
|
70
68
|
|
|
71
69
|
tags.map do |name|
|
|
72
|
-
name.include?(delimiter) ? "\"#{name}\"" : name
|
|
73
|
-
end.join(
|
|
70
|
+
name.include?(ActsAsTaggableOn.delimiter) ? "\"#{name}\"" : name
|
|
71
|
+
end.join(ActsAsTaggableOn.glue)
|
|
74
72
|
end
|
|
75
73
|
|
|
76
74
|
private
|
|
77
|
-
|
|
75
|
+
|
|
78
76
|
# Remove whitespace, duplicates, and blanks.
|
|
79
77
|
def clean!
|
|
80
78
|
reject!(&:blank?)
|
|
81
79
|
map!(&:strip)
|
|
80
|
+
map!(&:downcase) if ActsAsTaggableOn.force_lowercase
|
|
81
|
+
map!(&:parameterize) if ActsAsTaggableOn.force_parameterize
|
|
82
|
+
|
|
82
83
|
uniq!
|
|
83
84
|
end
|
|
84
85
|
|