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.
Files changed (46) hide show
  1. data/.gitignore +8 -0
  2. data/.rspec +2 -0
  3. data/.travis.yml +9 -0
  4. data/CHANGELOG +10 -0
  5. data/Gemfile +2 -9
  6. data/Guardfile +5 -0
  7. data/README.rdoc +47 -63
  8. data/Rakefile +9 -55
  9. data/acts-as-taggable-on.gemspec +28 -0
  10. data/lib/acts-as-taggable-on/version.rb +4 -0
  11. data/lib/acts-as-taggable-on.rb +7 -3
  12. data/lib/acts_as_taggable_on/acts_as_taggable_on/cache.rb +4 -4
  13. data/lib/acts_as_taggable_on/acts_as_taggable_on/collection.rb +38 -43
  14. data/lib/acts_as_taggable_on/acts_as_taggable_on/core.rb +81 -30
  15. data/lib/acts_as_taggable_on/acts_as_taggable_on/ownership.rb +7 -3
  16. data/lib/acts_as_taggable_on/acts_as_taggable_on/related.rb +23 -15
  17. data/lib/acts_as_taggable_on/tag.rb +21 -12
  18. data/lib/acts_as_taggable_on/{acts_as_taggable_on.rb → taggable.rb} +6 -5
  19. data/lib/acts_as_taggable_on/tagging.rb +12 -2
  20. data/lib/acts_as_taggable_on/tags_helper.rb +2 -2
  21. data/lib/acts_as_taggable_on/utils.rb +34 -0
  22. data/lib/generators/acts_as_taggable_on/migration/migration_generator.rb +9 -2
  23. data/lib/generators/acts_as_taggable_on/migration/templates/active_record/migration.rb +3 -1
  24. data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +242 -54
  25. data/spec/acts_as_taggable_on/tag_list_spec.rb +4 -0
  26. data/spec/acts_as_taggable_on/tag_spec.rb +52 -13
  27. data/spec/acts_as_taggable_on/taggable_spec.rb +131 -35
  28. data/spec/acts_as_taggable_on/tagger_spec.rb +14 -0
  29. data/spec/acts_as_taggable_on/tagging_spec.rb +2 -5
  30. data/spec/acts_as_taggable_on/tags_helper_spec.rb +16 -0
  31. data/spec/acts_as_taggable_on/utils_spec.rb +21 -0
  32. data/spec/database.yml.sample +4 -2
  33. data/spec/generators/acts_as_taggable_on/migration/migration_generator_spec.rb +22 -0
  34. data/spec/models.rb +14 -1
  35. data/spec/schema.rb +13 -0
  36. data/spec/spec_helper.rb +27 -6
  37. data/uninstall.rb +1 -0
  38. metadata +136 -51
  39. data/VERSION +0 -1
  40. data/generators/acts_as_taggable_on_migration/acts_as_taggable_on_migration_generator.rb +0 -7
  41. data/generators/acts_as_taggable_on_migration/templates/migration.rb +0 -29
  42. data/lib/acts_as_taggable_on/compatibility/Gemfile +0 -8
  43. data/lib/acts_as_taggable_on/compatibility/active_record_backports.rb +0 -17
  44. data/lib/acts_as_taggable_on/compatibility/postgresql.rb +0 -44
  45. data/spec/database.yml +0 -17
  46. /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
- has_many context_taggings, :as => :taggable, :dependent => :destroy, :include => :tag, :class_name => "ActsAsTaggableOn::Tagging",
24
- :conditions => ["#{ActsAsTaggableOn::Tagging.table_name}.tagger_id IS NULL AND #{ActsAsTaggableOn::Tagging.table_name}.context = ?", tags_type]
25
- has_many context_tags, :through => context_taggings, :source => :tag, :class_name => "ActsAsTaggableOn::Tag"
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 {} if tag_list.empty?
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
- tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name LIKE ?", t]) }.join(" OR ")
80
- 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}.id AND (#{tags_conditions}) WHERE #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = #{quote_value(base_class.name)})"
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
- tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name LIKE ?", t]) }.join(" OR ")
84
- conditions << "#{table_name}.#{primary_key} 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}.id AND (#{tags_conditions}) WHERE #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = #{quote_value(base_class.name)})"
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 scoped(:conditions => "1 = 0") unless tags.length == tag_list.length
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 = "#{table_name}_taggings_#{prefix}"
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 = "#{table_name}_taggings_group", "#{table_name}_tags_group"
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(:joins => joins.join(" "),
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 = ["#{tagging_table_name}.context = ?", context.to_s]
180
- base_tags.where(opts).order("max(#{tagging_table_name}.created_at)").group("#{tag_table_name}.id, #{tag_table_name}.name").all
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 :id => old_taggings.map(&:id)
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
- base_tags.where([%(#{ActsAsTaggableOn::Tagging.table_name}.context = ? AND
34
- #{ActsAsTaggableOn::Tagging.table_name}.tagger_id = ? AND
35
- #{ActsAsTaggableOn::Tagging.table_name}.tagger_type = ?), context.to_s, owner.id, owner.class.to_s]).all
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}.id != #{id} AND" if self.class == klass
44
-
45
- klass.scoped({ :select => "#{klass.table_name}.*, COUNT(#{ActsAsTaggableOn::Tag.table_name}.id) AS count",
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}.id = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = '#{klass.to_s}' AND #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.id AND #{ActsAsTaggableOn::Tag.table_name}.name IN (?) AND #{ActsAsTaggableOn::Tagging.table_name}.context = ?", tags_to_find, result_context],
48
- :group => grouped_column_names_for(klass),
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}.id != #{id} AND" if self.class == klass
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}.id) AS count",
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}.id = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = '#{klass.to_s}' AND #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.id AND #{ActsAsTaggableOn::Tag.table_name}.name IN (?)", tags_to_find],
60
- :group => grouped_column_names_for(klass),
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::ActiveRecord::Backports if ::ActiveRecord::VERSION::MAJOR < 3
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 { |name| existing_tags.any? { |tag| tag.name.mb_chars.downcase == name.mb_chars.downcase } }
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 like_operator
69
- connection.adapter_name == 'PostgreSQL' ? 'ILIKE' : 'LIKE'
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
- write_inheritable_attribute(:tag_types, (self.tag_types + tag_types).uniq)
31
+ self.tag_types = (self.tag_types + tag_types).uniq
32
32
  else
33
- write_inheritable_attribute(:tag_types, tag_types)
34
- class_inheritable_reader(:tag_types)
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)).round
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(path)
22
- Time.now.utc.strftime("%Y%m%d%H%M%S")
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
- t.string :context
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