acts-as-taggable-on 2.0.0 → 2.1.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.
Files changed (41) hide show
  1. data/.gitignore +7 -0
  2. data/.travis.yml +10 -0
  3. data/Gemfile +2 -5
  4. data/Guardfile +5 -0
  5. data/README.rdoc +19 -16
  6. data/Rakefile +9 -55
  7. data/VERSION +1 -1
  8. data/acts-as-taggable-on.gemspec +27 -0
  9. data/lib/acts-as-taggable-on/version.rb +4 -0
  10. data/lib/acts-as-taggable-on.rb +8 -2
  11. data/lib/acts_as_taggable_on/acts_as_taggable_on/cache.rb +6 -6
  12. data/lib/acts_as_taggable_on/acts_as_taggable_on/collection.rb +72 -31
  13. data/lib/acts_as_taggable_on/acts_as_taggable_on/core.rb +67 -32
  14. data/lib/acts_as_taggable_on/acts_as_taggable_on/ownership.rb +16 -12
  15. data/lib/acts_as_taggable_on/acts_as_taggable_on/related.rb +18 -9
  16. data/lib/acts_as_taggable_on/acts_as_taggable_on.rb +16 -6
  17. data/lib/acts_as_taggable_on/acts_as_tagger.rb +2 -2
  18. data/lib/acts_as_taggable_on/compatibility/Gemfile +3 -1
  19. data/lib/acts_as_taggable_on/compatibility/active_record_backports.rb +5 -1
  20. data/lib/acts_as_taggable_on/tag.rb +73 -57
  21. data/lib/acts_as_taggable_on/tag_list.rb +79 -78
  22. data/lib/acts_as_taggable_on/tagging.rb +19 -18
  23. data/lib/acts_as_taggable_on/tags_helper.rb +12 -12
  24. data/lib/acts_as_taggable_on/utils.rb +31 -0
  25. data/lib/generators/acts_as_taggable_on/migration/migration_generator.rb +3 -2
  26. data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +25 -2
  27. data/spec/acts_as_taggable_on/acts_as_tagger_spec.rb +3 -3
  28. data/spec/acts_as_taggable_on/tag_list_spec.rb +3 -3
  29. data/spec/acts_as_taggable_on/tag_spec.rb +41 -21
  30. data/spec/acts_as_taggable_on/taggable_spec.rb +69 -12
  31. data/spec/acts_as_taggable_on/tagger_spec.rb +5 -5
  32. data/spec/acts_as_taggable_on/tagging_spec.rb +7 -7
  33. data/spec/acts_as_taggable_on/tags_helper_spec.rb +3 -3
  34. data/spec/acts_as_taggable_on/utils_spec.rb +22 -0
  35. data/spec/database.yml.sample +19 -0
  36. data/spec/models.rb +5 -0
  37. data/spec/schema.rb +6 -0
  38. data/spec/spec_helper.rb +60 -33
  39. data/uninstall.rb +1 -0
  40. metadata +130 -15
  41. /data/{spec/spec.opts → .rspec} +0 -0
@@ -1,5 +1,5 @@
1
1
  module ActsAsTaggableOn::Taggable
2
- module Core
2
+ module Core
3
3
  def self.included(base)
4
4
  base.send :include, ActsAsTaggableOn::Taggable::Core::InstanceMethods
5
5
  base.extend ActsAsTaggableOn::Taggable::Core::ClassMethods
@@ -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 => "Tagging",
24
- :conditions => ['#{Tagging.table_name}.tagger_id IS NULL AND #{Tagging.table_name}.context = ?', tags_type]
25
- has_many context_tags, :through => context_taggings, :source => :tag
23
+ has_many context_taggings, :as => :taggable, :dependent => :destroy, :include => :tag, :class_name => "ActsAsTaggableOn::Tagging",
24
+ :conditions => ["#{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.id AND #{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 %(
@@ -66,34 +66,54 @@ module ActsAsTaggableOn::Taggable
66
66
  # User.tagged_with("awesome", "cool", :any => true) # Users that are tagged with awesome or cool
67
67
  # User.tagged_with("awesome", "cool", :match_all => true) # Users that are tagged with just awesome and cool
68
68
  def tagged_with(tags, options = {})
69
- tag_list = TagList.from(tags)
69
+ tag_list = ActsAsTaggableOn::TagList.from(tags)
70
+ empty_result = scoped(:conditions => "1 = 0")
70
71
 
71
- return {} if tag_list.empty?
72
+ return empty_result if tag_list.empty?
72
73
 
73
74
  joins = []
74
75
  conditions = []
75
76
 
76
77
  context = options.delete(:on)
78
+ alias_base_name = undecorated_table_name.gsub('.','_')
77
79
 
78
80
  if options.delete(:exclude)
79
- tags_conditions = tag_list.map { |t| sanitize_sql(["#{Tag.table_name}.name LIKE ?", t]) }.join(" OR ")
80
- conditions << "#{table_name}.#{primary_key} NOT IN (SELECT #{Tagging.table_name}.taggable_id FROM #{Tagging.table_name} JOIN #{Tag.table_name} ON #{Tagging.table_name}.tag_id = #{Tag.table_name}.id AND (#{tags_conditions}) WHERE #{Tagging.table_name}.taggable_type = #{quote_value(base_class.name)})"
81
+ tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{like_operator} ?", t]) }.join(" OR ")
82
+ 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)})"
81
83
 
82
84
  elsif options.delete(:any)
83
- tags_conditions = tag_list.map { |t| sanitize_sql(["#{Tag.table_name}.name LIKE ?", t]) }.join(" OR ")
84
- conditions << "#{table_name}.#{primary_key} IN (SELECT #{Tagging.table_name}.taggable_id FROM #{Tagging.table_name} JOIN #{Tag.table_name} ON #{Tagging.table_name}.tag_id = #{Tag.table_name}.id AND (#{tags_conditions}) WHERE #{Tagging.table_name}.taggable_type = #{quote_value(base_class.name)})"
85
+ # get tags, drop out if nothing returned (we need at least one)
86
+ tags = ActsAsTaggableOn::Tag.named_any(tag_list)
87
+ return scoped(:conditions => "1 = 0") unless tags.length > 0
88
+
89
+ # setup taggings alias so we can chain, ex: items_locations_taggings_awesome_cool_123
90
+ # avoid ambiguous column name
91
+ taggings_context = context ? "_#{context}" : ''
92
+
93
+ #TODO: fix alias to be smaller
94
+ taggings_alias = "#{alias_base_name}#{taggings_context}_taggings_#{tags.map(&:safe_name).join('_')}_#{rand(1024)}"
95
+
96
+ tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
97
+ " ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
98
+ " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}"
99
+ tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
100
+
101
+ # don't need to sanitize sql, map all ids and join with OR logic
102
+ conditions << tags.map { |t| "#{taggings_alias}.tag_id = #{t.id}" }.join(" OR ")
103
+ select_clause = "DISTINCT #{table_name}.*" unless context and tag_types.one?
104
+
105
+ joins << tagging_join
85
106
 
86
107
  else
87
- tags = Tag.named_any(tag_list)
88
- return where("1 = 0") unless tags.length == tag_list.length
108
+ tags = ActsAsTaggableOn::Tag.named_any(tag_list)
109
+ return empty_result unless tags.length == tag_list.length
89
110
 
90
111
  tags.each do |tag|
91
- safe_tag = tag.name.gsub(/[^a-zA-Z0-9]/, '')
92
- prefix = "#{safe_tag}_#{rand(1024)}"
112
+ prefix = "#{tag.safe_name}_#{rand(1024)}"
93
113
 
94
- taggings_alias = "#{table_name}_taggings_#{prefix}"
114
+ taggings_alias = "#{alias_base_name}_taggings_#{prefix}"
95
115
 
96
- tagging_join = "JOIN #{Tagging.table_name} #{taggings_alias}" +
116
+ tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
97
117
  " ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
98
118
  " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}" +
99
119
  " AND #{taggings_alias}.tag_id = #{tag.id}"
@@ -103,20 +123,23 @@ module ActsAsTaggableOn::Taggable
103
123
  end
104
124
  end
105
125
 
106
- taggings_alias, tags_alias = "#{table_name}_taggings_group", "#{table_name}_tags_group"
126
+ taggings_alias, tags_alias = "#{alias_base_name}_taggings_group", "#{alias_base_name}_tags_group"
107
127
 
108
128
  if options.delete(:match_all)
109
- joins << "LEFT OUTER JOIN #{Tagging.table_name} #{taggings_alias}" +
129
+ joins << "LEFT OUTER JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
110
130
  " ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
111
131
  " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}"
112
132
 
113
- group = "#{grouped_column_names_for(self)} HAVING COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
114
- end
115
133
 
134
+ group_columns = ActsAsTaggableOn::Tag.using_postgresql? ? grouped_column_names_for(self) : "#{table_name}.#{primary_key}"
135
+ group = "#{group_columns} HAVING COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
136
+ end
116
137
 
117
- scoped(:joins => joins.join(" "),
138
+ scoped(:select => select_clause,
139
+ :joins => joins.join(" "),
118
140
  :group => group,
119
141
  :conditions => conditions.join(" AND "),
142
+ :order => options[:order],
120
143
  :readonly => false)
121
144
  end
122
145
 
@@ -154,7 +177,7 @@ module ActsAsTaggableOn::Taggable
154
177
 
155
178
  def tag_list_cache_on(context)
156
179
  variable_name = "@#{context.to_s.singularize}_list"
157
- instance_variable_get(variable_name) || instance_variable_set(variable_name, TagList.new(tags_on(context).map(&:name)))
180
+ instance_variable_get(variable_name) || instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(tags_on(context).map(&:name)))
158
181
  end
159
182
 
160
183
  def tag_list_on(context)
@@ -166,40 +189,52 @@ module ActsAsTaggableOn::Taggable
166
189
  variable_name = "@all_#{context.to_s.singularize}_list"
167
190
  return instance_variable_get(variable_name) if instance_variable_get(variable_name)
168
191
 
169
- instance_variable_set(variable_name, TagList.new(all_tags_on(context).map(&:name)).freeze)
192
+ instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(all_tags_on(context).map(&:name)).freeze)
170
193
  end
171
194
 
172
195
  ##
173
196
  # Returns all tags of a given context
174
197
  def all_tags_on(context)
175
- opts = ["#{Tagging.table_name}.context = ?", context.to_s]
176
- base_tags.where(opts).order("#{Tagging.table_name}.created_at").group("#{Tagging.table_name}.tag_id").all
198
+ tag_table_name = ActsAsTaggableOn::Tag.table_name
199
+ tagging_table_name = ActsAsTaggableOn::Tagging.table_name
200
+
201
+ opts = ["#{tagging_table_name}.context = ?", context.to_s]
202
+ scope = base_tags.where(opts)
203
+
204
+ if ActsAsTaggableOn::Tag.using_postgresql?
205
+ group_columns = grouped_column_names_for(ActsAsTaggableOn::Tag)
206
+ scope = scope.order("max(#{tagging_table_name}.created_at)").group(group_columns)
207
+ else
208
+ scope = scope.group("#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}")
209
+ end
210
+
211
+ scope.all
177
212
  end
178
213
 
179
214
  ##
180
215
  # Returns all tags that are not owned of a given context
181
216
  def tags_on(context)
182
- base_tags.where(["#{Tagging.table_name}.context = ? AND #{Tagging.table_name}.tagger_id IS NULL", context.to_s]).all
217
+ base_tags.where(["#{ActsAsTaggableOn::Tagging.table_name}.context = ? AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_id IS NULL", context.to_s]).all
183
218
  end
184
219
 
185
220
  def set_tag_list_on(context, new_list)
186
221
  add_custom_context(context)
187
222
 
188
223
  variable_name = "@#{context.to_s.singularize}_list"
189
- instance_variable_set(variable_name, TagList.from(new_list))
224
+ instance_variable_set(variable_name, ActsAsTaggableOn::TagList.from(new_list))
190
225
  end
191
226
 
192
227
  def tagging_contexts
193
228
  custom_contexts + self.class.tag_types.map(&:to_s)
194
229
  end
195
230
 
196
- def reload
231
+ def reload(*args)
197
232
  self.class.tag_types.each do |context|
198
233
  instance_variable_set("@#{context.to_s.singularize}_list", nil)
199
234
  instance_variable_set("@all_#{context.to_s.singularize}_list", nil)
200
235
  end
201
236
 
202
- super
237
+ super(*args)
203
238
  end
204
239
 
205
240
  def save_tags
@@ -209,7 +244,7 @@ module ActsAsTaggableOn::Taggable
209
244
  tag_list = tag_list_cache_on(context).uniq
210
245
 
211
246
  # Find existing tags or create non-existing tags:
212
- tag_list = Tag.find_or_create_all_with_like_by_name(tag_list)
247
+ tag_list = ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name(tag_list)
213
248
 
214
249
  current_tags = tags_on(context)
215
250
  old_tags = current_tags - tag_list
@@ -221,7 +256,7 @@ module ActsAsTaggableOn::Taggable
221
256
 
222
257
  if old_taggings.present?
223
258
  # Destroy old taggings:
224
- Tagging.destroy_all :id => old_taggings.map(&:id)
259
+ ActsAsTaggableOn::Tagging.destroy_all :id => old_taggings.map(&:id)
225
260
  end
226
261
 
227
262
  # Create new taggings:
@@ -234,4 +269,4 @@ module ActsAsTaggableOn::Taggable
234
269
  end
235
270
  end
236
271
  end
237
- end
272
+ 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([%(#{Tagging.table_name}.context = ? AND
34
- #{Tagging.table_name}.tagger_id = ? AND
35
- #{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)
@@ -46,7 +50,7 @@ module ActsAsTaggableOn::Taggable
46
50
  cache = cached_owned_tag_list_on(context)
47
51
  cache.delete_if { |key, value| key.id == owner.id && key.class == owner.class }
48
52
 
49
- cache[owner] ||= TagList.new(*owner_tags_on(owner, context).map(&:name))
53
+ cache[owner] ||= ActsAsTaggableOn::TagList.new(*owner_tags_on(owner, context).map(&:name))
50
54
  end
51
55
 
52
56
  def set_owner_tag_list_on(owner, context, new_list)
@@ -55,22 +59,22 @@ module ActsAsTaggableOn::Taggable
55
59
  cache = cached_owned_tag_list_on(context)
56
60
  cache.delete_if { |key, value| key.id == owner.id && key.class == owner.class }
57
61
 
58
- cache[owner] = TagList.from(new_list)
62
+ cache[owner] = ActsAsTaggableOn::TagList.from(new_list)
59
63
  end
60
64
 
61
- def reload
65
+ def reload(*args)
62
66
  self.class.tag_types.each do |context|
63
67
  instance_variable_set("@owned_#{context}_list", nil)
64
68
  end
65
69
 
66
- super
70
+ super(*args)
67
71
  end
68
72
 
69
73
  def save_owned_tags
70
74
  tagging_contexts.each do |context|
71
75
  cached_owned_tag_list_on(context).each do |owner, tag_list|
72
76
  # Find existing tags or create non-existing tags:
73
- tag_list = Tag.find_or_create_all_with_like_by_name(tag_list.uniq)
77
+ tag_list = ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name(tag_list.uniq)
74
78
 
75
79
  owned_tags = owner_tags_on(owner, context)
76
80
  old_tags = owned_tags - tag_list
@@ -78,13 +82,13 @@ module ActsAsTaggableOn::Taggable
78
82
 
79
83
  # Find all taggings that belong to the taggable (self), are owned by the owner,
80
84
  # have the correct context, and are removed from the list.
81
- old_taggings = Tagging.where(:taggable_id => id, :taggable_type => self.class.base_class.to_s,
82
- :tagger_type => owner.class.to_s, :tagger_id => owner.id,
83
- :tag_id => old_tags, :context => context).all
85
+ old_taggings = ActsAsTaggableOn::Tagging.where(:taggable_id => id, :taggable_type => self.class.base_class.to_s,
86
+ :tagger_type => owner.class.to_s, :tagger_id => owner.id,
87
+ :tag_id => old_tags, :context => context).all
84
88
 
85
89
  if old_taggings.present?
86
90
  # Destroy old taggings:
87
- Tagging.destroy_all(:id => old_taggings.map(&:id))
91
+ ActsAsTaggableOn::Tagging.destroy_all(:id => old_taggings.map(&:id))
88
92
  end
89
93
 
90
94
  # Create new taggings:
@@ -3,6 +3,7 @@ module ActsAsTaggableOn::Taggable
3
3
  def self.included(base)
4
4
  base.send :include, ActsAsTaggableOn::Taggable::Related::InstanceMethods
5
5
  base.extend ActsAsTaggableOn::Taggable::Related::ClassMethods
6
+ base.initialize_acts_as_taggable_on_related
6
7
  end
7
8
 
8
9
  module ClassMethods
@@ -17,7 +18,11 @@ module ActsAsTaggableOn::Taggable
17
18
  def find_related_#{tag_type}_for(klass, options = {})
18
19
  related_tags_for('#{tag_type}', klass, options)
19
20
  end
20
-
21
+ )
22
+ end
23
+
24
+ unless tag_types.empty?
25
+ class_eval %(
21
26
  def find_matching_contexts(search_context, result_context, options = {})
22
27
  matching_contexts_for(search_context.to_s, result_context.to_s, self.class, options)
23
28
  end
@@ -41,10 +46,12 @@ module ActsAsTaggableOn::Taggable
41
46
 
42
47
  exclude_self = "#{klass.table_name}.id != #{id} AND" if self.class == klass
43
48
 
44
- klass.scoped({ :select => "#{klass.table_name}.*, COUNT(#{Tag.table_name}.id) AS count",
45
- :from => "#{klass.table_name}, #{Tag.table_name}, #{Tagging.table_name}",
46
- :conditions => ["#{exclude_self} #{klass.table_name}.id = #{Tagging.table_name}.taggable_id AND #{Tagging.table_name}.taggable_type = '#{klass.to_s}' AND #{Tagging.table_name}.tag_id = #{Tag.table_name}.id AND #{Tag.table_name}.name IN (?) AND #{Tagging.table_name}.context = ?", tags_to_find, result_context],
47
- :group => grouped_column_names_for(klass),
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}.id) AS count",
52
+ :from => "#{klass.table_name}, #{ActsAsTaggableOn::Tag.table_name}, #{ActsAsTaggableOn::Tagging.table_name}",
53
+ :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],
54
+ :group => group_columns,
48
55
  :order => "count DESC" }.update(options))
49
56
  end
50
57
 
@@ -53,10 +60,12 @@ module ActsAsTaggableOn::Taggable
53
60
 
54
61
  exclude_self = "#{klass.table_name}.id != #{id} AND" if self.class == klass
55
62
 
56
- klass.scoped({ :select => "#{klass.table_name}.*, COUNT(#{Tag.table_name}.id) AS count",
57
- :from => "#{klass.table_name}, #{Tag.table_name}, #{Tagging.table_name}",
58
- :conditions => ["#{exclude_self} #{klass.table_name}.id = #{Tagging.table_name}.taggable_id AND #{Tagging.table_name}.taggable_type = '#{klass.to_s}' AND #{Tagging.table_name}.tag_id = #{Tag.table_name}.id AND #{Tag.table_name}.name IN (?)", tags_to_find],
59
- :group => grouped_column_names_for(klass),
63
+ group_columns = ActsAsTaggableOn::Tag.using_postgresql? ? grouped_column_names_for(klass) : "#{klass.table_name}.#{klass.primary_key}"
64
+
65
+ klass.scoped({ :select => "#{klass.table_name}.*, COUNT(#{ActsAsTaggableOn::Tag.table_name}.id) AS count",
66
+ :from => "#{klass.table_name}, #{ActsAsTaggableOn::Tag.table_name}, #{ActsAsTaggableOn::Tagging.table_name}",
67
+ :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],
68
+ :group => group_columns,
60
69
  :order => "count DESC" }.update(options))
61
70
  end
62
71
  end
@@ -28,19 +28,29 @@ 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
+ if RAILS_3
32
+ self.tag_types = (self.tag_types + tag_types).uniq
33
+ else
34
+ write_inheritable_attribute(:tag_types, (self.tag_types + tag_types).uniq)
35
+ end
32
36
  else
33
- write_inheritable_attribute(:tag_types, tag_types)
34
- class_inheritable_reader(:tag_types)
37
+ if RAILS_3
38
+ class_attribute :tag_types
39
+ self.tag_types = tag_types
40
+ else
41
+ write_inheritable_attribute(:tag_types, tag_types)
42
+ class_inheritable_reader(:tag_types)
43
+ end
35
44
 
36
45
  class_eval do
37
- has_many :taggings, :as => :taggable, :dependent => :destroy, :include => :tag
38
- has_many :base_tags, :class_name => "Tag", :through => :taggings, :source => :tag
46
+ has_many :taggings, :as => :taggable, :dependent => :destroy, :include => :tag, :class_name => "ActsAsTaggableOn::Tagging"
47
+ has_many :base_tags, :through => :taggings, :source => :tag, :class_name => "ActsAsTaggableOn::Tag"
39
48
 
40
49
  def self.taggable?
41
50
  true
42
51
  end
43
-
52
+
53
+ include ActsAsTaggableOn::Utils
44
54
  include ActsAsTaggableOn::Taggable::Core
45
55
  include ActsAsTaggableOn::Taggable::Collection
46
56
  include ActsAsTaggableOn::Taggable::Cache
@@ -16,8 +16,8 @@ module ActsAsTaggableOn
16
16
  def acts_as_tagger(opts={})
17
17
  class_eval do
18
18
  has_many :owned_taggings, opts.merge(:as => :tagger, :dependent => :destroy,
19
- :include => :tag, :class_name => "Tagging")
20
- has_many :owned_tags, :through => :owned_taggings, :source => :tag, :uniq => true
19
+ :include => :tag, :class_name => "ActsAsTaggableOn::Tagging")
20
+ has_many :owned_tags, :through => :owned_taggings, :source => :tag, :uniq => true, :class_name => "ActsAsTaggableOn::Tag"
21
21
  end
22
22
 
23
23
  include ActsAsTaggableOn::Tagger::InstanceMethods
@@ -3,4 +3,6 @@ source :gemcutter
3
3
  # Rails 2.3
4
4
  gem 'rails', '2.3.5'
5
5
  gem 'rspec', '1.3.0', :require => 'spec'
6
- gem 'sqlite3-ruby', '1.2.5', :require => 'sqlite3'
6
+ gem 'sqlite3-ruby', '1.2.5', :require => 'sqlite3'
7
+ gem 'mysql2', '~> 0.2.7'
8
+ gem 'pg'
@@ -9,7 +9,11 @@ module ActsAsTaggableOn
9
9
  named_scope :order, lambda { |order| { :order => order } }
10
10
  named_scope :select, lambda { |select| { :select => select } }
11
11
  named_scope :limit, lambda { |limit| { :limit => limit } }
12
- named_scope :readonly, lambda { |readonly| { :readonly => readonly } }
12
+ named_scope :readonly, lambda { |readonly| { :readonly => readonly } }
13
+
14
+ def self.to_sql
15
+ construct_finder_sql({})
16
+ end
13
17
  end
14
18
  end
15
19
  end
@@ -1,65 +1,81 @@
1
- class Tag < ActiveRecord::Base
2
- include ActsAsTaggableOn::ActiveRecord::Backports if ActiveRecord::VERSION::MAJOR < 3
3
-
4
- attr_accessible :name
5
-
6
- ### ASSOCIATIONS:
1
+ module ActsAsTaggableOn
2
+ class Tag < ::ActiveRecord::Base
3
+ include ActsAsTaggableOn::ActiveRecord::Backports if ::ActiveRecord::VERSION::MAJOR < 3
4
+ include ActsAsTaggableOn::Utils
5
+
6
+ attr_accessible :name
7
7
 
8
- has_many :taggings, :dependent => :destroy
8
+ ### ASSOCIATIONS:
9
9
 
10
- ### VALIDATIONS:
10
+ has_many :taggings, :dependent => :destroy, :class_name => 'ActsAsTaggableOn::Tagging'
11
11
 
12
- validates_presence_of :name
13
- validates_uniqueness_of :name
12
+ ### VALIDATIONS:
14
13
 
15
- ### SCOPES:
14
+ validates_presence_of :name
15
+ validates_uniqueness_of :name
16
16
 
17
- def self.named(name)
18
- where(["name LIKE ?", name])
19
- end
17
+ ### SCOPES:
18
+
19
+ def self.named(name)
20
+ where(["name #{like_operator} ?", escape_like(name)])
21
+ end
20
22
 
21
- def self.named_any(list)
22
- where(list.map { |tag| sanitize_sql(["name LIKE ?", tag.to_s]) }.join(" OR "))
23
- end
23
+ def self.named_any(list)
24
+ where(list.map { |tag| sanitize_sql(["name #{like_operator} ?", escape_like(tag.to_s)]) }.join(" OR "))
25
+ end
24
26
 
25
- def self.named_like(name)
26
- where(["name LIKE ?", "%#{name}%"])
27
- end
28
-
29
- def self.named_like_any(list)
30
- where(list.map { |tag| sanitize_sql(["name LIKE ?", "%#{tag.to_s}%"]) }.join(" OR "))
31
- end
32
-
33
- ### CLASS METHODS:
34
-
35
- def self.find_or_create_with_like_by_name(name)
36
- named_like(name).first || create(:name => name)
37
- end
38
-
39
- def self.find_or_create_all_with_like_by_name(*list)
40
- list = [list].flatten
41
-
42
- return [] if list.empty?
43
-
44
- existing_tags = Tag.named_any(list).all
45
- new_tag_names = list.reject { |name| existing_tags.any? { |tag| tag.name.mb_chars.downcase == name.mb_chars.downcase } }
46
- created_tags = new_tag_names.map { |name| Tag.create(:name => name) }
47
-
48
- existing_tags + created_tags
49
- end
50
-
51
- ### INSTANCE METHODS:
52
-
53
- def ==(object)
54
- super || (object.is_a?(Tag) && name == object.name)
27
+ def self.named_like(name)
28
+ where(["name #{like_operator} ?", "%#{escape_like(name)}%"])
29
+ end
30
+
31
+ def self.named_like_any(list)
32
+ where(list.map { |tag| sanitize_sql(["name #{like_operator} ?", "%#{escape_like(tag.to_s)}%"]) }.join(" OR "))
33
+ end
34
+
35
+ ### CLASS METHODS:
36
+
37
+ def self.find_or_create_with_like_by_name(name)
38
+ named_like(name).first || create(:name => name)
39
+ end
40
+
41
+ def self.find_or_create_all_with_like_by_name(*list)
42
+ list = [list].flatten
43
+
44
+ return [] if list.empty?
45
+
46
+ existing_tags = Tag.named_any(list).all
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
51
+ created_tags = new_tag_names.map { |name| Tag.create(:name => name) }
52
+
53
+ existing_tags + created_tags
54
+ end
55
+
56
+ ### INSTANCE METHODS:
57
+
58
+ def ==(object)
59
+ super || (object.is_a?(Tag) && name == object.name)
60
+ end
61
+
62
+ def to_s
63
+ name
64
+ end
65
+
66
+ def count
67
+ read_attribute(:count).to_i
68
+ end
69
+
70
+ def safe_name
71
+ name.gsub(/[^a-zA-Z0-9]/, '')
72
+ end
73
+
74
+ class << self
75
+ private
76
+ def comparable_name(str)
77
+ RUBY_VERSION >= "1.9" ? str.downcase : str.mb_chars.downcase
78
+ end
79
+ end
55
80
  end
56
-
57
- def to_s
58
- name
59
- end
60
-
61
- def count
62
- read_attribute(:count).to_i
63
- end
64
-
65
- end
81
+ end