acts-as-taggable-on 2.0.6 → 2.2.2

Sign up to get free protection for your applications and to get access to all the features.
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