acts-as-taggable-on 2.0.6 → 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 +10 -0
- data/.rspec +2 -0
- data/.travis.yml +9 -0
- data/CHANGELOG +10 -0
- data/Gemfile +2 -9
- data/Guardfile +5 -0
- data/README.rdoc +89 -66
- data/Rakefile +9 -55
- data/acts-as-taggable-on.gemspec +28 -0
- data/lib/acts-as-taggable-on/version.rb +4 -0
- data/lib/acts-as-taggable-on.rb +33 -4
- data/lib/acts_as_taggable_on/acts_as_taggable_on/cache.rb +4 -4
- data/lib/acts_as_taggable_on/acts_as_taggable_on/collection.rb +38 -43
- data/lib/acts_as_taggable_on/acts_as_taggable_on/core.rb +146 -38
- 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 +36 -11
- data/lib/acts_as_taggable_on/acts_as_taggable_on/related.rb +23 -15
- data/lib/acts_as_taggable_on/tag.rb +16 -13
- 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 +34 -0
- 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 +300 -54
- data/spec/acts_as_taggable_on/tag_list_spec.rb +84 -61
- data/spec/acts_as_taggable_on/tag_spec.rb +51 -13
- data/spec/acts_as_taggable_on/taggable_spec.rb +261 -34
- 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 +21 -0
- data/spec/database.yml.sample +4 -2
- data/spec/generators/acts_as_taggable_on/migration/migration_generator_spec.rb +22 -0
- data/spec/models.rb +19 -1
- data/spec/schema.rb +18 -0
- data/spec/spec_helper.rb +30 -7
- data/uninstall.rb +1 -0
- metadata +137 -51
- 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 -53
- data/lib/acts_as_taggable_on/compatibility/Gemfile +0 -8
- data/lib/acts_as_taggable_on/compatibility/active_record_backports.rb +0 -17
- data/lib/acts_as_taggable_on/compatibility/postgresql.rb +0 -44
- data/spec/database.yml +0 -17
@@ -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,39 +70,72 @@ 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)
|
83
|
+
empty_result = scoped(:conditions => "1 = 0")
|
70
84
|
|
71
|
-
return
|
85
|
+
return empty_result if tag_list.empty?
|
72
86
|
|
73
87
|
joins = []
|
74
88
|
conditions = []
|
75
89
|
|
76
90
|
context = options.delete(:on)
|
91
|
+
owned_by = options.delete(:owned_by)
|
92
|
+
alias_base_name = undecorated_table_name.gsub('.','_')
|
77
93
|
|
78
94
|
if options.delete(:exclude)
|
79
|
-
|
80
|
-
|
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)})"
|
81
102
|
|
82
103
|
elsif options.delete(:any)
|
83
|
-
|
84
|
-
|
104
|
+
# get tags, drop out if nothing returned (we need at least one)
|
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
|
+
|
111
|
+
return scoped(:conditions => "1 = 0") unless tags.length > 0
|
112
|
+
|
113
|
+
# setup taggings alias so we can chain, ex: items_locations_taggings_awesome_cool_123
|
114
|
+
# avoid ambiguous column name
|
115
|
+
taggings_context = context ? "_#{context}" : ''
|
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
|
+
)
|
120
|
+
|
121
|
+
tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
|
122
|
+
" ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
|
123
|
+
" AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}"
|
124
|
+
tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
|
125
|
+
|
126
|
+
# don't need to sanitize sql, map all ids and join with OR logic
|
127
|
+
conditions << tags.map { |t| "#{taggings_alias}.tag_id = #{t.id}" }.join(" OR ")
|
128
|
+
select_clause = "DISTINCT #{table_name}.*" unless context and tag_types.one?
|
129
|
+
|
130
|
+
joins << tagging_join
|
85
131
|
|
86
132
|
else
|
87
133
|
tags = ActsAsTaggableOn::Tag.named_any(tag_list)
|
88
|
-
return
|
134
|
+
return empty_result unless tags.length == tag_list.length
|
89
135
|
|
90
136
|
tags.each do |tag|
|
91
|
-
safe_tag = tag.name.gsub(/[^a-zA-Z0-9]/, '')
|
92
|
-
prefix = "#{safe_tag}_#{rand(1024)}"
|
93
137
|
|
94
|
-
taggings_alias = "#{
|
138
|
+
taggings_alias = adjust_taggings_alias("#{alias_base_name[0..11]}_taggings_#{sha_prefix(tag.name)}")
|
95
139
|
|
96
140
|
tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
|
97
141
|
" ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
|
@@ -99,22 +143,33 @@ module ActsAsTaggableOn::Taggable
|
|
99
143
|
" AND #{taggings_alias}.tag_id = #{tag.id}"
|
100
144
|
tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
|
101
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
|
+
|
102
155
|
joins << tagging_join
|
103
156
|
end
|
104
157
|
end
|
105
158
|
|
106
|
-
taggings_alias, tags_alias = "#{
|
159
|
+
taggings_alias, tags_alias = adjust_taggings_alias("#{alias_base_name}_taggings_group"), "#{alias_base_name}_tags_group"
|
107
160
|
|
108
161
|
if options.delete(:match_all)
|
109
162
|
joins << "LEFT OUTER JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
|
110
163
|
" ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
|
111
164
|
" AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}"
|
112
165
|
|
113
|
-
group = "#{grouped_column_names_for(self)} HAVING COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
|
114
|
-
end
|
115
166
|
|
167
|
+
group_columns = ActsAsTaggableOn::Tag.using_postgresql? ? grouped_column_names_for(self) : "#{table_name}.#{primary_key}"
|
168
|
+
group = "#{group_columns} HAVING COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
|
169
|
+
end
|
116
170
|
|
117
|
-
scoped(:
|
171
|
+
scoped(:select => select_clause,
|
172
|
+
:joins => joins.join(" "),
|
118
173
|
:group => group,
|
119
174
|
:conditions => conditions.join(" AND "),
|
120
175
|
:order => options[:order],
|
@@ -124,8 +179,15 @@ module ActsAsTaggableOn::Taggable
|
|
124
179
|
def is_taggable?
|
125
180
|
true
|
126
181
|
end
|
127
|
-
|
128
|
-
|
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
|
+
|
129
191
|
module InstanceMethods
|
130
192
|
# all column names are necessary for PostgreSQL group clause
|
131
193
|
def grouped_column_names_for(object)
|
@@ -176,20 +238,35 @@ module ActsAsTaggableOn::Taggable
|
|
176
238
|
tag_table_name = ActsAsTaggableOn::Tag.table_name
|
177
239
|
tagging_table_name = ActsAsTaggableOn::Tagging.table_name
|
178
240
|
|
179
|
-
opts
|
180
|
-
base_tags.where(opts)
|
241
|
+
opts = ["#{tagging_table_name}.context = ?", context.to_s]
|
242
|
+
scope = base_tags.where(opts)
|
243
|
+
|
244
|
+
if ActsAsTaggableOn::Tag.using_postgresql?
|
245
|
+
group_columns = grouped_column_names_for(ActsAsTaggableOn::Tag)
|
246
|
+
scope = scope.order("max(#{tagging_table_name}.created_at)").group(group_columns)
|
247
|
+
else
|
248
|
+
scope = scope.group("#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}")
|
249
|
+
end
|
250
|
+
|
251
|
+
scope.all
|
181
252
|
end
|
182
253
|
|
183
254
|
##
|
184
255
|
# Returns all tags that are not owned of a given context
|
185
256
|
def tags_on(context)
|
186
|
-
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
|
187
262
|
end
|
188
263
|
|
189
264
|
def set_tag_list_on(context, new_list)
|
190
265
|
add_custom_context(context)
|
191
266
|
|
192
267
|
variable_name = "@#{context.to_s.singularize}_list"
|
268
|
+
process_dirty_object(context, new_list) unless custom_contexts.include?(context.to_s)
|
269
|
+
|
193
270
|
instance_variable_set(variable_name, ActsAsTaggableOn::TagList.from(new_list))
|
194
271
|
end
|
195
272
|
|
@@ -197,12 +274,26 @@ module ActsAsTaggableOn::Taggable
|
|
197
274
|
custom_contexts + self.class.tag_types.map(&:to_s)
|
198
275
|
end
|
199
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
|
+
|
200
291
|
def reload(*args)
|
201
292
|
self.class.tag_types.each do |context|
|
202
293
|
instance_variable_set("@#{context.to_s.singularize}_list", nil)
|
203
294
|
instance_variable_set("@all_#{context.to_s.singularize}_list", nil)
|
204
295
|
end
|
205
|
-
|
296
|
+
|
206
297
|
super(*args)
|
207
298
|
end
|
208
299
|
|
@@ -210,22 +301,39 @@ module ActsAsTaggableOn::Taggable
|
|
210
301
|
tagging_contexts.each do |context|
|
211
302
|
next unless tag_list_cache_set_on(context)
|
212
303
|
|
304
|
+
# List of currently assigned tag names
|
213
305
|
tag_list = tag_list_cache_on(context).uniq
|
214
306
|
|
215
307
|
# Find existing tags or create non-existing tags:
|
216
|
-
|
308
|
+
tags = ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name(tag_list)
|
217
309
|
|
310
|
+
# Tag objects for currently assigned tags
|
218
311
|
current_tags = tags_on(context)
|
219
|
-
|
220
|
-
|
221
|
-
|
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
|
+
|
222
328
|
# Find taggings to remove:
|
223
|
-
|
224
|
-
|
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
|
225
333
|
|
334
|
+
# Destroy old taggings:
|
226
335
|
if old_taggings.present?
|
227
|
-
#
|
228
|
-
ActsAsTaggableOn::Tagging.destroy_all :id => old_taggings.map(&:id)
|
336
|
+
ActsAsTaggableOn::Tagging.destroy_all "#{ActsAsTaggableOn::Tagging.primary_key}".to_sym => old_taggings.map(&:id)
|
229
337
|
end
|
230
338
|
|
231
339
|
# Create new taggings:
|
@@ -238,4 +346,4 @@ module ActsAsTaggableOn::Taggable
|
|
238
346
|
end
|
239
347
|
end
|
240
348
|
end
|
241
|
-
end
|
349
|
+
end
|
@@ -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
|
@@ -30,9 +30,17 @@ module ActsAsTaggableOn::Taggable
|
|
30
30
|
|
31
31
|
module InstanceMethods
|
32
32
|
def owner_tags_on(owner, context)
|
33
|
-
|
34
|
-
|
35
|
-
|
33
|
+
if owner.nil?
|
34
|
+
scope = base_tags.where([%(#{ActsAsTaggableOn::Tagging.table_name}.context = ?), context.to_s])
|
35
|
+
else
|
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
|
+
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
|
36
44
|
end
|
37
45
|
|
38
46
|
def cached_owned_tag_list_on(context)
|
@@ -69,21 +77,38 @@ module ActsAsTaggableOn::Taggable
|
|
69
77
|
def save_owned_tags
|
70
78
|
tagging_contexts.each do |context|
|
71
79
|
cached_owned_tag_list_on(context).each do |owner, tag_list|
|
80
|
+
|
72
81
|
# Find existing tags or create non-existing tags:
|
73
|
-
|
82
|
+
tags = ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name(tag_list.uniq)
|
74
83
|
|
75
|
-
|
76
|
-
|
77
|
-
|
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
|
78
101
|
|
79
102
|
# Find all taggings that belong to the taggable (self), are owned by the owner,
|
80
103
|
# have the correct context, and are removed from the list.
|
81
|
-
|
82
|
-
|
83
|
-
|
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
|
84
109
|
|
110
|
+
# Destroy old taggings:
|
85
111
|
if old_taggings.present?
|
86
|
-
# Destroy old taggings:
|
87
112
|
ActsAsTaggableOn::Tagging.destroy_all(:id => old_taggings.map(&:id))
|
88
113
|
end
|
89
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|
|
@@ -18,7 +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
|
+
)
|
22
|
+
end
|
21
23
|
|
24
|
+
unless tag_types.empty?
|
25
|
+
class_eval %(
|
22
26
|
def find_matching_contexts(search_context, result_context, options = {})
|
23
27
|
matching_contexts_for(search_context.to_s, result_context.to_s, self.class, options)
|
24
28
|
end
|
@@ -27,39 +31,43 @@ module ActsAsTaggableOn::Taggable
|
|
27
31
|
matching_contexts_for(search_context.to_s, result_context.to_s, klass, options)
|
28
32
|
end
|
29
33
|
)
|
30
|
-
end
|
34
|
+
end
|
31
35
|
end
|
32
|
-
|
36
|
+
|
33
37
|
def acts_as_taggable_on(*args)
|
34
38
|
super(*args)
|
35
39
|
initialize_acts_as_taggable_on_related
|
36
40
|
end
|
37
41
|
end
|
38
|
-
|
42
|
+
|
39
43
|
module InstanceMethods
|
40
44
|
def matching_contexts_for(search_context, result_context, klass, options = {})
|
41
45
|
tags_to_find = tags_on(search_context).collect { |t| t.name }
|
42
46
|
|
43
|
-
exclude_self = "#{klass.table_name}.
|
44
|
-
|
45
|
-
|
47
|
+
exclude_self = "#{klass.table_name}.#{klass.primary_key} != #{id} AND" if [self.class.base_class, self.class].include? klass
|
48
|
+
|
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}.#{ActsAsTaggableOn::Tag.primary_key}) AS count",
|
46
52
|
:from => "#{klass.table_name}, #{ActsAsTaggableOn::Tag.table_name}, #{ActsAsTaggableOn::Tagging.table_name}",
|
47
|
-
:conditions => ["#{exclude_self} #{klass.table_name}.
|
48
|
-
:group =>
|
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,
|
49
55
|
:order => "count DESC" }.update(options))
|
50
56
|
end
|
51
|
-
|
57
|
+
|
52
58
|
def related_tags_for(context, klass, options = {})
|
53
59
|
tags_to_find = tags_on(context).collect { |t| t.name }
|
54
60
|
|
55
|
-
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
|
+
|
63
|
+
group_columns = ActsAsTaggableOn::Tag.using_postgresql? ? grouped_column_names_for(klass) : "#{klass.table_name}.#{klass.primary_key}"
|
56
64
|
|
57
|
-
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",
|
58
66
|
:from => "#{klass.table_name}, #{ActsAsTaggableOn::Tag.table_name}, #{ActsAsTaggableOn::Tagging.table_name}",
|
59
|
-
:conditions => ["#{exclude_self} #{klass.table_name}.
|
60
|
-
:group =>
|
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
|
+
:group => group_columns,
|
61
69
|
:order => "count DESC" }.update(options))
|
62
70
|
end
|
63
71
|
end
|
64
72
|
end
|
65
|
-
end
|
73
|
+
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
module ActsAsTaggableOn
|
2
2
|
class Tag < ::ActiveRecord::Base
|
3
|
-
include ActsAsTaggableOn::
|
4
|
-
|
3
|
+
include ActsAsTaggableOn::Utils
|
4
|
+
|
5
5
|
attr_accessible :name
|
6
6
|
|
7
7
|
### ASSOCIATIONS:
|
@@ -12,23 +12,24 @@ module ActsAsTaggableOn
|
|
12
12
|
|
13
13
|
validates_presence_of :name
|
14
14
|
validates_uniqueness_of :name
|
15
|
+
validates_length_of :name, :maximum => 255
|
15
16
|
|
16
17
|
### SCOPES:
|
17
18
|
|
18
19
|
def self.named(name)
|
19
|
-
where(["name
|
20
|
+
where(["lower(name) = ?", name.downcase])
|
20
21
|
end
|
21
|
-
|
22
|
+
|
22
23
|
def self.named_any(list)
|
23
|
-
where(list.map { |tag| sanitize_sql(["name
|
24
|
+
where(list.map { |tag| sanitize_sql(["lower(name) = ?", tag.to_s.downcase]) }.join(" OR "))
|
24
25
|
end
|
25
|
-
|
26
|
+
|
26
27
|
def self.named_like(name)
|
27
|
-
where(["name #{like_operator} ?", "%#{name}%"])
|
28
|
+
where(["name #{like_operator} ? ESCAPE '!'", "%#{escape_like(name)}%"])
|
28
29
|
end
|
29
30
|
|
30
31
|
def self.named_like_any(list)
|
31
|
-
where(list.map { |tag| sanitize_sql(["name #{like_operator} ?", "%#{tag.to_s}%"]) }.join(" OR "))
|
32
|
+
where(list.map { |tag| sanitize_sql(["name #{like_operator} ? ESCAPE '!'", "%#{escape_like(tag.to_s)}%"]) }.join(" OR "))
|
32
33
|
end
|
33
34
|
|
34
35
|
### CLASS METHODS:
|
@@ -43,7 +44,10 @@ module ActsAsTaggableOn
|
|
43
44
|
return [] if list.empty?
|
44
45
|
|
45
46
|
existing_tags = Tag.named_any(list).all
|
46
|
-
new_tag_names = list.reject
|
47
|
+
new_tag_names = list.reject do |name|
|
48
|
+
name = comparable_name(name)
|
49
|
+
existing_tags.any? { |tag| comparable_name(tag.name) == name }
|
50
|
+
end
|
47
51
|
created_tags = new_tag_names.map { |name| Tag.create(:name => name) }
|
48
52
|
|
49
53
|
existing_tags + created_tags
|
@@ -65,10 +69,9 @@ module ActsAsTaggableOn
|
|
65
69
|
|
66
70
|
class << self
|
67
71
|
private
|
68
|
-
def
|
69
|
-
|
72
|
+
def comparable_name(str)
|
73
|
+
str.mb_chars.downcase.to_s
|
70
74
|
end
|
71
75
|
end
|
72
|
-
|
73
76
|
end
|
74
|
-
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
|
|