acts-as-taggable-on 2.0.6 → 2.2.2
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 +8 -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 +47 -63
- 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 +7 -3
- 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 +81 -30
- data/lib/acts_as_taggable_on/acts_as_taggable_on/ownership.rb +7 -3
- data/lib/acts_as_taggable_on/acts_as_taggable_on/related.rb +23 -15
- data/lib/acts_as_taggable_on/tag.rb +21 -12
- data/lib/acts_as_taggable_on/{acts_as_taggable_on.rb → taggable.rb} +6 -5
- 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 +242 -54
- data/spec/acts_as_taggable_on/tag_list_spec.rb +4 -0
- data/spec/acts_as_taggable_on/tag_spec.rb +52 -13
- data/spec/acts_as_taggable_on/taggable_spec.rb +131 -35
- data/spec/acts_as_taggable_on/tagger_spec.rb +14 -0
- 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 +14 -1
- data/spec/schema.rb +13 -0
- data/spec/spec_helper.rb +27 -6
- data/uninstall.rb +1 -0
- metadata +136 -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/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
- /data/lib/acts_as_taggable_on/{acts_as_tagger.rb → tagger.rb} +0 -0
@@ -8,10 +8,10 @@ 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|
|
@@ -20,9 +20,9 @@ module ActsAsTaggableOn::Taggable
|
|
20
20
|
context_tags = tags_type.to_sym
|
21
21
|
|
22
22
|
class_eval do
|
23
|
-
|
24
|
-
|
25
|
-
|
23
|
+
has_many context_taggings, :as => :taggable, :dependent => :destroy, :include => :tag, :class_name => "ActsAsTaggableOn::Tagging",
|
24
|
+
:conditions => ["#{ActsAsTaggableOn::Tagging.table_name}.context = ?", tags_type]
|
25
|
+
has_many context_tags, :through => context_taggings, :source => :tag, :class_name => "ActsAsTaggableOn::Tag"
|
26
26
|
end
|
27
27
|
|
28
28
|
class_eval %(
|
@@ -38,14 +38,14 @@ module ActsAsTaggableOn::Taggable
|
|
38
38
|
all_tags_list_on('#{tags_type}')
|
39
39
|
end
|
40
40
|
)
|
41
|
-
end
|
41
|
+
end
|
42
42
|
end
|
43
|
-
|
43
|
+
|
44
44
|
def acts_as_taggable_on(*args)
|
45
45
|
super(*args)
|
46
46
|
initialize_acts_as_taggable_on_core
|
47
47
|
end
|
48
|
-
|
48
|
+
|
49
49
|
# all column names are necessary for PostgreSQL group clause
|
50
50
|
def grouped_column_names_for(object)
|
51
51
|
object.column_names.map { |column| "#{object.table_name}.#{column}" }.join(", ")
|
@@ -59,39 +59,70 @@ module ActsAsTaggableOn::Taggable
|
|
59
59
|
# * <tt>:exclude</tt> - if set to true, return objects that are *NOT* tagged with the specified tags
|
60
60
|
# * <tt>:any</tt> - if set to true, return objects that are tagged with *ANY* of the specified tags
|
61
61
|
# * <tt>:match_all</tt> - if set to true, return objects that are *ONLY* tagged with the specified tags
|
62
|
+
# * <tt>:owned_by</tt> - return objects that are *ONLY* owned by the owner
|
62
63
|
#
|
63
64
|
# Example:
|
64
65
|
# User.tagged_with("awesome", "cool") # Users that are tagged with awesome and cool
|
65
66
|
# User.tagged_with("awesome", "cool", :exclude => true) # Users that are not tagged with awesome or cool
|
66
67
|
# User.tagged_with("awesome", "cool", :any => true) # Users that are tagged with awesome or cool
|
67
68
|
# User.tagged_with("awesome", "cool", :match_all => true) # Users that are tagged with just awesome and cool
|
69
|
+
# User.tagged_with("awesome", "cool", :owned_by => foo ) # Users that are tagged with just awesome and cool by 'foo'
|
68
70
|
def tagged_with(tags, options = {})
|
69
71
|
tag_list = ActsAsTaggableOn::TagList.from(tags)
|
72
|
+
empty_result = scoped(:conditions => "1 = 0")
|
70
73
|
|
71
|
-
return
|
74
|
+
return empty_result if tag_list.empty?
|
72
75
|
|
73
76
|
joins = []
|
74
77
|
conditions = []
|
75
78
|
|
76
79
|
context = options.delete(:on)
|
80
|
+
owned_by = options.delete(:owned_by)
|
81
|
+
alias_base_name = undecorated_table_name.gsub('.','_')
|
77
82
|
|
78
83
|
if options.delete(:exclude)
|
79
|
-
|
80
|
-
|
84
|
+
if options.delete(:wild)
|
85
|
+
tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{like_operator} ? ESCAPE '!'", "%#{escape_like(t)}%"]) }.join(" OR ")
|
86
|
+
else
|
87
|
+
tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{like_operator} ?", t]) }.join(" OR ")
|
88
|
+
end
|
89
|
+
|
90
|
+
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
91
|
|
82
92
|
elsif options.delete(:any)
|
83
|
-
|
84
|
-
|
93
|
+
# get tags, drop out if nothing returned (we need at least one)
|
94
|
+
if options.delete(:wild)
|
95
|
+
tags = ActsAsTaggableOn::Tag.named_like_any(tag_list)
|
96
|
+
else
|
97
|
+
tags = ActsAsTaggableOn::Tag.named_any(tag_list)
|
98
|
+
end
|
99
|
+
|
100
|
+
return scoped(:conditions => "1 = 0") unless tags.length > 0
|
101
|
+
|
102
|
+
# setup taggings alias so we can chain, ex: items_locations_taggings_awesome_cool_123
|
103
|
+
# avoid ambiguous column name
|
104
|
+
taggings_context = context ? "_#{context}" : ''
|
105
|
+
|
106
|
+
taggings_alias = "#{alias_base_name[0..4]}#{taggings_context[0..6]}_taggings_#{sha_prefix(tags.map(&:safe_name).join('_'))}"
|
107
|
+
|
108
|
+
tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
|
109
|
+
" ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
|
110
|
+
" AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}"
|
111
|
+
tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
|
112
|
+
|
113
|
+
# don't need to sanitize sql, map all ids and join with OR logic
|
114
|
+
conditions << tags.map { |t| "#{taggings_alias}.tag_id = #{t.id}" }.join(" OR ")
|
115
|
+
select_clause = "DISTINCT #{table_name}.*" unless context and tag_types.one?
|
116
|
+
|
117
|
+
joins << tagging_join
|
85
118
|
|
86
119
|
else
|
87
|
-
tags = ActsAsTaggableOn::Tag.named_any(tag_list)
|
88
|
-
return
|
120
|
+
tags = ActsAsTaggableOn::Tag.named_any(tag_list)
|
121
|
+
return empty_result unless tags.length == tag_list.length
|
89
122
|
|
90
123
|
tags.each do |tag|
|
91
|
-
safe_tag = tag.name.gsub(/[^a-zA-Z0-9]/, '')
|
92
|
-
prefix = "#{safe_tag}_#{rand(1024)}"
|
93
124
|
|
94
|
-
taggings_alias = "#{
|
125
|
+
taggings_alias = "#{alias_base_name[0..11]}_taggings_#{sha_prefix(tag.safe_name)}"
|
95
126
|
|
96
127
|
tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
|
97
128
|
" ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
|
@@ -99,22 +130,33 @@ module ActsAsTaggableOn::Taggable
|
|
99
130
|
" AND #{taggings_alias}.tag_id = #{tag.id}"
|
100
131
|
tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
|
101
132
|
|
133
|
+
if owned_by
|
134
|
+
tagging_join << " AND " +
|
135
|
+
sanitize_sql([
|
136
|
+
"#{taggings_alias}.tagger_id = ? AND #{taggings_alias}.tagger_type = ?",
|
137
|
+
owned_by.id,
|
138
|
+
owned_by.class.to_s
|
139
|
+
])
|
140
|
+
end
|
141
|
+
|
102
142
|
joins << tagging_join
|
103
143
|
end
|
104
144
|
end
|
105
145
|
|
106
|
-
taggings_alias, tags_alias = "#{
|
146
|
+
taggings_alias, tags_alias = "#{alias_base_name}_taggings_group", "#{alias_base_name}_tags_group"
|
107
147
|
|
108
148
|
if options.delete(:match_all)
|
109
149
|
joins << "LEFT OUTER JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
|
110
150
|
" ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
|
111
151
|
" AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}"
|
112
152
|
|
113
|
-
group = "#{grouped_column_names_for(self)} HAVING COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
|
114
|
-
end
|
115
153
|
|
154
|
+
group_columns = ActsAsTaggableOn::Tag.using_postgresql? ? grouped_column_names_for(self) : "#{table_name}.#{primary_key}"
|
155
|
+
group = "#{group_columns} HAVING COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
|
156
|
+
end
|
116
157
|
|
117
|
-
scoped(:
|
158
|
+
scoped(:select => select_clause,
|
159
|
+
:joins => joins.join(" "),
|
118
160
|
:group => group,
|
119
161
|
:conditions => conditions.join(" AND "),
|
120
162
|
:order => options[:order],
|
@@ -124,8 +166,8 @@ module ActsAsTaggableOn::Taggable
|
|
124
166
|
def is_taggable?
|
125
167
|
true
|
126
168
|
end
|
127
|
-
end
|
128
|
-
|
169
|
+
end
|
170
|
+
|
129
171
|
module InstanceMethods
|
130
172
|
# all column names are necessary for PostgreSQL group clause
|
131
173
|
def grouped_column_names_for(object)
|
@@ -176,8 +218,17 @@ module ActsAsTaggableOn::Taggable
|
|
176
218
|
tag_table_name = ActsAsTaggableOn::Tag.table_name
|
177
219
|
tagging_table_name = ActsAsTaggableOn::Tagging.table_name
|
178
220
|
|
179
|
-
opts
|
180
|
-
base_tags.where(opts)
|
221
|
+
opts = ["#{tagging_table_name}.context = ?", context.to_s]
|
222
|
+
scope = base_tags.where(opts)
|
223
|
+
|
224
|
+
if ActsAsTaggableOn::Tag.using_postgresql?
|
225
|
+
group_columns = grouped_column_names_for(ActsAsTaggableOn::Tag)
|
226
|
+
scope = scope.order("max(#{tagging_table_name}.created_at)").group(group_columns)
|
227
|
+
else
|
228
|
+
scope = scope.group("#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}")
|
229
|
+
end
|
230
|
+
|
231
|
+
scope.all
|
181
232
|
end
|
182
233
|
|
183
234
|
##
|
@@ -202,7 +253,7 @@ module ActsAsTaggableOn::Taggable
|
|
202
253
|
instance_variable_set("@#{context.to_s.singularize}_list", nil)
|
203
254
|
instance_variable_set("@all_#{context.to_s.singularize}_list", nil)
|
204
255
|
end
|
205
|
-
|
256
|
+
|
206
257
|
super(*args)
|
207
258
|
end
|
208
259
|
|
@@ -218,14 +269,14 @@ module ActsAsTaggableOn::Taggable
|
|
218
269
|
current_tags = tags_on(context)
|
219
270
|
old_tags = current_tags - tag_list
|
220
271
|
new_tags = tag_list - current_tags
|
221
|
-
|
272
|
+
|
222
273
|
# Find taggings to remove:
|
223
274
|
old_taggings = taggings.where(:tagger_type => nil, :tagger_id => nil,
|
224
275
|
:context => context.to_s, :tag_id => old_tags).all
|
225
276
|
|
226
277
|
if old_taggings.present?
|
227
278
|
# Destroy old taggings:
|
228
|
-
ActsAsTaggableOn::Tagging.destroy_all
|
279
|
+
ActsAsTaggableOn::Tagging.destroy_all "#{ActsAsTaggableOn::Tagging.primary_key}".to_sym => old_taggings.map(&:id)
|
229
280
|
end
|
230
281
|
|
231
282
|
# Create new taggings:
|
@@ -238,4 +289,4 @@ module ActsAsTaggableOn::Taggable
|
|
238
289
|
end
|
239
290
|
end
|
240
291
|
end
|
241
|
-
end
|
292
|
+
end
|
@@ -30,9 +30,13 @@ 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
|
+
base_tags.where([%(#{ActsAsTaggableOn::Tagging.table_name}.context = ?), context.to_s]).all
|
35
|
+
else
|
36
|
+
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]).all
|
39
|
+
end
|
36
40
|
end
|
37
41
|
|
38
42
|
def cached_owned_tag_list_on(context)
|
@@ -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,10 @@
|
|
1
1
|
module ActsAsTaggableOn
|
2
2
|
class Tag < ::ActiveRecord::Base
|
3
|
-
include ActsAsTaggableOn::
|
4
|
-
|
3
|
+
include ActsAsTaggableOn::Utils
|
4
|
+
|
5
|
+
cattr_accessor :remove_unused
|
6
|
+
self.remove_unused = false
|
7
|
+
|
5
8
|
attr_accessible :name
|
6
9
|
|
7
10
|
### ASSOCIATIONS:
|
@@ -16,19 +19,19 @@ module ActsAsTaggableOn
|
|
16
19
|
### SCOPES:
|
17
20
|
|
18
21
|
def self.named(name)
|
19
|
-
where(["name #{like_operator} ?", name])
|
22
|
+
where(["name #{like_operator} ? ESCAPE '!'", escape_like(name)])
|
20
23
|
end
|
21
|
-
|
24
|
+
|
22
25
|
def self.named_any(list)
|
23
|
-
where(list.map { |tag| sanitize_sql(["name #{like_operator} ?", tag.to_s]) }.join(" OR "))
|
26
|
+
where(list.map { |tag| sanitize_sql(["name #{like_operator} ? ESCAPE '!'", escape_like(tag.to_s)]) }.join(" OR "))
|
24
27
|
end
|
25
|
-
|
28
|
+
|
26
29
|
def self.named_like(name)
|
27
|
-
where(["name #{like_operator} ?", "%#{name}%"])
|
30
|
+
where(["name #{like_operator} ? ESCAPE '!'", "%#{escape_like(name)}%"])
|
28
31
|
end
|
29
32
|
|
30
33
|
def self.named_like_any(list)
|
31
|
-
where(list.map { |tag| sanitize_sql(["name #{like_operator} ?", "%#{tag.to_s}%"]) }.join(" OR "))
|
34
|
+
where(list.map { |tag| sanitize_sql(["name #{like_operator} ? ESCAPE '!'", "%#{escape_like(tag.to_s)}%"]) }.join(" OR "))
|
32
35
|
end
|
33
36
|
|
34
37
|
### CLASS METHODS:
|
@@ -43,7 +46,10 @@ module ActsAsTaggableOn
|
|
43
46
|
return [] if list.empty?
|
44
47
|
|
45
48
|
existing_tags = Tag.named_any(list).all
|
46
|
-
new_tag_names = list.reject
|
49
|
+
new_tag_names = list.reject do |name|
|
50
|
+
name = comparable_name(name)
|
51
|
+
existing_tags.any? { |tag| comparable_name(tag.name) == name }
|
52
|
+
end
|
47
53
|
created_tags = new_tag_names.map { |name| Tag.create(:name => name) }
|
48
54
|
|
49
55
|
existing_tags + created_tags
|
@@ -63,12 +69,15 @@ module ActsAsTaggableOn
|
|
63
69
|
read_attribute(:count).to_i
|
64
70
|
end
|
65
71
|
|
72
|
+
def safe_name
|
73
|
+
name.gsub(/[^a-zA-Z0-9]/, '')
|
74
|
+
end
|
75
|
+
|
66
76
|
class << self
|
67
77
|
private
|
68
|
-
def
|
69
|
-
|
78
|
+
def comparable_name(str)
|
79
|
+
RUBY_VERSION >= "1.9" ? str.downcase : str.mb_chars.downcase
|
70
80
|
end
|
71
81
|
end
|
72
|
-
|
73
82
|
end
|
74
83
|
end
|
@@ -28,11 +28,11 @@ module ActsAsTaggableOn
|
|
28
28
|
tag_types = tag_types.to_a.flatten.compact.map(&:to_sym)
|
29
29
|
|
30
30
|
if taggable?
|
31
|
-
|
31
|
+
self.tag_types = (self.tag_types + tag_types).uniq
|
32
32
|
else
|
33
|
-
|
34
|
-
|
35
|
-
|
33
|
+
class_attribute :tag_types
|
34
|
+
self.tag_types = tag_types
|
35
|
+
|
36
36
|
class_eval do
|
37
37
|
has_many :taggings, :as => :taggable, :dependent => :destroy, :include => :tag, :class_name => "ActsAsTaggableOn::Tagging"
|
38
38
|
has_many :base_tags, :through => :taggings, :source => :tag, :class_name => "ActsAsTaggableOn::Tag"
|
@@ -40,7 +40,8 @@ module ActsAsTaggableOn
|
|
40
40
|
def self.taggable?
|
41
41
|
true
|
42
42
|
end
|
43
|
-
|
43
|
+
|
44
|
+
include ActsAsTaggableOn::Utils
|
44
45
|
include ActsAsTaggableOn::Taggable::Core
|
45
46
|
include ActsAsTaggableOn::Taggable::Collection
|
46
47
|
include ActsAsTaggableOn::Taggable::Cache
|
@@ -1,7 +1,5 @@
|
|
1
1
|
module ActsAsTaggableOn
|
2
2
|
class Tagging < ::ActiveRecord::Base #:nodoc:
|
3
|
-
include ActsAsTaggableOn::ActiveRecord::Backports if ::ActiveRecord::VERSION::MAJOR < 3
|
4
|
-
|
5
3
|
attr_accessible :tag,
|
6
4
|
:tag_id,
|
7
5
|
:context,
|
@@ -20,5 +18,17 @@ module ActsAsTaggableOn
|
|
20
18
|
validates_presence_of :tag_id
|
21
19
|
|
22
20
|
validates_uniqueness_of :tag_id, :scope => [ :taggable_type, :taggable_id, :context, :tagger_id, :tagger_type ]
|
21
|
+
|
22
|
+
after_destroy :remove_unused_tags
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def remove_unused_tags
|
27
|
+
if Tag.remove_unused
|
28
|
+
if tag.taggings.count.zero?
|
29
|
+
tag.destroy
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
23
33
|
end
|
24
34
|
end
|
@@ -9,8 +9,8 @@ module ActsAsTaggableOn
|
|
9
9
|
max_count = tags.sort_by(&:count).last.count.to_f
|
10
10
|
|
11
11
|
tags.each do |tag|
|
12
|
-
index = ((tag.count / max_count) * (classes.size - 1))
|
13
|
-
yield tag, classes[index]
|
12
|
+
index = ((tag.count / max_count) * (classes.size - 1))
|
13
|
+
yield tag, classes[index.nan? ? 0 : index.round]
|
14
14
|
end
|
15
15
|
end
|
16
16
|
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module ActsAsTaggableOn
|
2
|
+
module Utils
|
3
|
+
def self.included(base)
|
4
|
+
|
5
|
+
base.send :include, ActsAsTaggableOn::Utils::OverallMethods
|
6
|
+
base.extend ActsAsTaggableOn::Utils::OverallMethods
|
7
|
+
end
|
8
|
+
|
9
|
+
module OverallMethods
|
10
|
+
def using_postgresql?
|
11
|
+
::ActiveRecord::Base.connection && ::ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
|
12
|
+
end
|
13
|
+
|
14
|
+
def using_sqlite?
|
15
|
+
::ActiveRecord::Base.connection && ::ActiveRecord::Base.connection.adapter_name == 'SQLite'
|
16
|
+
end
|
17
|
+
|
18
|
+
def sha_prefix(string)
|
19
|
+
Digest::SHA1.hexdigest(string + Time.now.to_s)[0..6]
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
def like_operator
|
24
|
+
using_postgresql? ? 'ILIKE' : 'LIKE'
|
25
|
+
end
|
26
|
+
|
27
|
+
# escape _ and % characters in strings, since these are wildcards in SQL.
|
28
|
+
def escape_like(str)
|
29
|
+
str.gsub(/[!%_]/){ |x| '!' + x }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'rails/generators'
|
1
2
|
require 'rails/generators/migration'
|
2
3
|
|
3
4
|
module ActsAsTaggableOn
|
@@ -18,8 +19,14 @@ module ActsAsTaggableOn
|
|
18
19
|
[:active_record].include? orm
|
19
20
|
end
|
20
21
|
|
21
|
-
def self.next_migration_number(
|
22
|
-
|
22
|
+
def self.next_migration_number(dirname)
|
23
|
+
if ActiveRecord::Base.timestamped_migrations
|
24
|
+
migration_number = Time.now.utc.strftime("%Y%m%d%H%M%S").to_i
|
25
|
+
migration_number += 1
|
26
|
+
migration_number.to_s
|
27
|
+
else
|
28
|
+
"%.3d" % (current_migration_number(dirname) + 1)
|
29
|
+
end
|
23
30
|
end
|
24
31
|
|
25
32
|
def create_migration_file
|
@@ -12,7 +12,9 @@ class ActsAsTaggableOnMigration < ActiveRecord::Migration
|
|
12
12
|
t.references :taggable, :polymorphic => true
|
13
13
|
t.references :tagger, :polymorphic => true
|
14
14
|
|
15
|
-
|
15
|
+
# limit is created to prevent mysql error o index lenght for myisam table type.
|
16
|
+
# http://bit.ly/vgW2Ql
|
17
|
+
t.string :context, :limit => 128
|
16
18
|
|
17
19
|
t.datetime :created_at
|
18
20
|
end
|