acts-as-taggable-on 2.0.0 → 3.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 (86) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.travis.yml +23 -0
  4. data/Appraisals +20 -0
  5. data/CHANGELOG.md +112 -0
  6. data/CONTRIBUTING.md +44 -0
  7. data/Gemfile +9 -5
  8. data/Guardfile +5 -0
  9. data/{MIT-LICENSE → LICENSE.md} +1 -1
  10. data/README.md +385 -0
  11. data/Rakefile +13 -53
  12. data/UPGRADING.md +13 -0
  13. data/acts-as-taggable-on.gemspec +38 -0
  14. data/{lib/generators/acts_as_taggable_on/migration/templates/active_record/migration.rb → db/migrate/1_acts_as_taggable_on_migration.rb} +5 -3
  15. data/db/migrate/2_add_missing_unique_indices.rb +19 -0
  16. data/db/migrate/3_add_taggings_counter_cache_to_tags.rb +14 -0
  17. data/gemfiles/activerecord_3.2.gemfile +15 -0
  18. data/gemfiles/activerecord_4.0.gemfile +15 -0
  19. data/gemfiles/activerecord_4.1.gemfile +15 -0
  20. data/gemfiles/activerecord_edge.gemfile +16 -0
  21. data/lib/acts-as-taggable-on.rb +65 -22
  22. data/lib/acts_as_taggable_on/acts_as_taggable_on/cache.rb +53 -24
  23. data/lib/acts_as_taggable_on/acts_as_taggable_on/collection.rb +120 -42
  24. data/lib/acts_as_taggable_on/acts_as_taggable_on/compatibility.rb +35 -0
  25. data/lib/acts_as_taggable_on/acts_as_taggable_on/core.rb +309 -124
  26. data/lib/acts_as_taggable_on/acts_as_taggable_on/dirty.rb +36 -0
  27. data/lib/acts_as_taggable_on/acts_as_taggable_on/ownership.rb +96 -72
  28. data/lib/acts_as_taggable_on/acts_as_taggable_on/related.rb +43 -36
  29. data/lib/acts_as_taggable_on/engine.rb +5 -0
  30. data/lib/acts_as_taggable_on/tag.rb +133 -63
  31. data/lib/acts_as_taggable_on/tag_list.rb +162 -81
  32. data/lib/acts_as_taggable_on/taggable.rb +101 -0
  33. data/lib/acts_as_taggable_on/{acts_as_tagger.rb → tagger.rb} +34 -13
  34. data/lib/acts_as_taggable_on/tagging.rb +28 -18
  35. data/lib/acts_as_taggable_on/tags_helper.rb +12 -14
  36. data/lib/acts_as_taggable_on/utils.rb +62 -0
  37. data/lib/acts_as_taggable_on/version.rb +4 -0
  38. data/lib/acts_as_taggable_on.rb +6 -0
  39. data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +189 -195
  40. data/spec/acts_as_taggable_on/acts_as_tagger_spec.rb +78 -81
  41. data/spec/acts_as_taggable_on/caching_spec.rb +76 -0
  42. data/spec/acts_as_taggable_on/related_spec.rb +89 -0
  43. data/spec/acts_as_taggable_on/single_table_inheritance_spec.rb +210 -0
  44. data/spec/acts_as_taggable_on/tag_list_spec.rb +149 -61
  45. data/spec/acts_as_taggable_on/tag_spec.rb +245 -65
  46. data/spec/acts_as_taggable_on/taggable_spec.rb +776 -187
  47. data/spec/acts_as_taggable_on/tagger_spec.rb +119 -70
  48. data/spec/acts_as_taggable_on/tagging_spec.rb +17 -21
  49. data/spec/acts_as_taggable_on/tags_helper_spec.rb +38 -22
  50. data/spec/acts_as_taggable_on/utils_spec.rb +15 -0
  51. data/spec/internal/app/models/altered_inheriting_taggable_model.rb +3 -0
  52. data/spec/internal/app/models/cached_model.rb +3 -0
  53. data/spec/internal/app/models/cached_model_with_array.rb +5 -0
  54. data/spec/internal/app/models/company.rb +15 -0
  55. data/spec/internal/app/models/inheriting_taggable_model.rb +2 -0
  56. data/spec/internal/app/models/market.rb +2 -0
  57. data/spec/internal/app/models/models.rb +90 -0
  58. data/spec/internal/app/models/non_standard_id_taggable_model.rb +8 -0
  59. data/spec/internal/app/models/ordered_taggable_model.rb +4 -0
  60. data/spec/internal/app/models/other_cached_model.rb +3 -0
  61. data/spec/internal/app/models/other_taggable_model.rb +4 -0
  62. data/spec/internal/app/models/student.rb +2 -0
  63. data/spec/internal/app/models/taggable_model.rb +13 -0
  64. data/spec/internal/app/models/untaggable_model.rb +3 -0
  65. data/spec/internal/app/models/user.rb +3 -0
  66. data/spec/internal/config/database.yml.sample +19 -0
  67. data/spec/internal/db/schema.rb +97 -0
  68. data/spec/schema.rb +57 -18
  69. data/spec/spec_helper.rb +12 -47
  70. data/spec/support/array.rb +9 -0
  71. data/spec/support/database.rb +42 -0
  72. data/spec/support/database_cleaner.rb +21 -0
  73. metadata +286 -63
  74. data/CHANGELOG +0 -25
  75. data/README.rdoc +0 -221
  76. data/VERSION +0 -1
  77. data/generators/acts_as_taggable_on_migration/acts_as_taggable_on_migration_generator.rb +0 -7
  78. data/generators/acts_as_taggable_on_migration/templates/migration.rb +0 -29
  79. data/lib/acts_as_taggable_on/acts_as_taggable_on.rb +0 -53
  80. data/lib/acts_as_taggable_on/compatibility/Gemfile +0 -6
  81. data/lib/acts_as_taggable_on/compatibility/active_record_backports.rb +0 -17
  82. data/lib/generators/acts_as_taggable_on/migration/migration_generator.rb +0 -31
  83. data/rails/init.rb +0 -1
  84. data/spec/bm.rb +0 -52
  85. data/spec/models.rb +0 -30
  86. /data/{spec/spec.opts → .rspec} +0 -0
@@ -1,101 +1,125 @@
1
1
  module ActsAsTaggableOn::Taggable
2
2
  module Ownership
3
3
  def self.included(base)
4
- base.send :include, ActsAsTaggableOn::Taggable::Ownership::InstanceMethods
5
4
  base.extend ActsAsTaggableOn::Taggable::Ownership::ClassMethods
6
-
5
+
7
6
  base.class_eval do
8
- after_save :save_owned_tags
7
+ after_save :save_owned_tags
9
8
  end
10
-
9
+
11
10
  base.initialize_acts_as_taggable_on_ownership
12
11
  end
13
-
12
+
14
13
  module ClassMethods
15
14
  def acts_as_taggable_on(*args)
16
15
  initialize_acts_as_taggable_on_ownership
17
16
  super(*args)
18
17
  end
19
-
20
- def initialize_acts_as_taggable_on_ownership
18
+
19
+ def initialize_acts_as_taggable_on_ownership
21
20
  tag_types.map(&:to_s).each do |tag_type|
22
- class_eval %(
21
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
23
22
  def #{tag_type}_from(owner)
24
23
  owner_tag_list_on(owner, '#{tag_type}')
25
- end
26
- )
27
- end
24
+ end
25
+ RUBY
26
+ end
28
27
  end
29
28
  end
30
-
31
- module InstanceMethods
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
36
- end
37
29
 
38
- def cached_owned_tag_list_on(context)
39
- variable_name = "@owned_#{context}_list"
40
- cache = instance_variable_get(variable_name) || instance_variable_set(variable_name, {})
30
+ def owner_tags_on(owner, context)
31
+ if owner.nil?
32
+ scope = base_tags.where([%(#{ActsAsTaggableOn::Tagging.table_name}.context = ?), context.to_s])
33
+ else
34
+ scope = base_tags.where([%(#{ActsAsTaggableOn::Tagging.table_name}.context = ? AND
35
+ #{ActsAsTaggableOn::Tagging.table_name}.tagger_id = ? AND
36
+ #{ActsAsTaggableOn::Tagging.table_name}.tagger_type = ?), context.to_s, owner.id, owner.class.base_class.to_s])
41
37
  end
42
-
43
- def owner_tag_list_on(owner, context)
44
- add_custom_context(context)
45
-
46
- cache = cached_owned_tag_list_on(context)
47
- cache.delete_if { |key, value| key.id == owner.id && key.class == owner.class }
48
-
49
- cache[owner] ||= TagList.new(*owner_tags_on(owner, context).map(&:name))
50
- end
51
-
52
- def set_owner_tag_list_on(owner, context, new_list)
53
- add_custom_context(context)
54
-
55
- cache = cached_owned_tag_list_on(context)
56
- cache.delete_if { |key, value| key.id == owner.id && key.class == owner.class }
57
-
58
- cache[owner] = TagList.from(new_list)
38
+
39
+ # when preserving tag order, return tags in created order
40
+ # if we added the order to the association this would always apply
41
+ if self.class.preserve_tag_order?
42
+ scope.order("#{ActsAsTaggableOn::Tagging.table_name}.id")
43
+ else
44
+ scope
59
45
  end
60
-
61
- def reload
62
- self.class.tag_types.each do |context|
63
- instance_variable_set("@owned_#{context}_list", nil)
64
- end
65
-
66
- super
46
+ end
47
+
48
+ def cached_owned_tag_list_on(context)
49
+ variable_name = "@owned_#{context}_list"
50
+ (instance_variable_defined?(variable_name) && instance_variable_get(variable_name)) || instance_variable_set(variable_name, {})
51
+ end
52
+
53
+ def owner_tag_list_on(owner, context)
54
+ add_custom_context(context)
55
+
56
+ cache = cached_owned_tag_list_on(context)
57
+
58
+ cache[owner] ||= ActsAsTaggableOn::TagList.new(*owner_tags_on(owner, context).map(&:name))
59
+ end
60
+
61
+ def set_owner_tag_list_on(owner, context, new_list)
62
+ add_custom_context(context)
63
+
64
+ cache = cached_owned_tag_list_on(context)
65
+
66
+ cache[owner] = ActsAsTaggableOn::TagList.from(new_list)
67
+ end
68
+
69
+ def reload(*args)
70
+ self.class.tag_types.each do |context|
71
+ instance_variable_set("@owned_#{context}_list", nil)
67
72
  end
68
-
69
- def save_owned_tags
70
- tagging_contexts.each do |context|
71
- cached_owned_tag_list_on(context).each do |owner, tag_list|
72
- # Find existing tags or create non-existing tags:
73
- tag_list = Tag.find_or_create_all_with_like_by_name(tag_list.uniq)
74
-
75
- owned_tags = owner_tags_on(owner, context)
76
- old_tags = owned_tags - tag_list
77
- new_tags = tag_list - owned_tags
78
-
79
- # Find all taggings that belong to the taggable (self), are owned by the owner,
80
- # 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
84
-
85
- if old_taggings.present?
86
- # Destroy old taggings:
87
- Tagging.destroy_all(:id => old_taggings.map(&:id))
88
- end
89
73
 
90
- # Create new taggings:
91
- new_tags.each do |tag|
92
- taggings.create!(:tag_id => tag.id, :context => context.to_s, :tagger => owner, :taggable => self)
74
+ super(*args)
75
+ end
76
+
77
+ def save_owned_tags
78
+ tagging_contexts.each do |context|
79
+ cached_owned_tag_list_on(context).each do |owner, tag_list|
80
+
81
+ # Find existing tags or create non-existing tags:
82
+ tags = find_or_create_tags_from_list_with_context(tag_list.uniq, context)
83
+
84
+ # Tag objects for owned tags
85
+ owned_tags = owner_tags_on(owner, context)
86
+
87
+ # Tag maintenance based on whether preserving the created order of tags
88
+ if self.class.preserve_tag_order?
89
+ old_tags, new_tags = owned_tags - tags, tags - owned_tags
90
+
91
+ shared_tags = owned_tags & tags
92
+
93
+ if shared_tags.any? && tags[0...shared_tags.size] != shared_tags
94
+ index = shared_tags.each_with_index { |_, i| break i unless shared_tags[i] == tags[i] }
95
+
96
+ # Update arrays of tag objects
97
+ old_tags |= owned_tags.from(index)
98
+ new_tags |= owned_tags.from(index) & shared_tags
99
+
100
+ # Order the array of tag objects to match the tag list
101
+ new_tags = tags.map { |t| new_tags.find { |n| n.name.downcase == t.name.downcase } }.compact
93
102
  end
103
+ else
104
+ # Delete discarded tags and create new tags
105
+ old_tags = owned_tags - tags
106
+ new_tags = tags - owned_tags
107
+ end
108
+
109
+ # Find all taggings that belong to the taggable (self), are owned by the owner,
110
+ # have the correct context, and are removed from the list.
111
+ ActsAsTaggableOn::Tagging.destroy_all(taggable_id: id, taggable_type: self.class.base_class.to_s,
112
+ tagger_type: owner.class.base_class.to_s, tagger_id: owner.id,
113
+ tag_id: old_tags, context: context) if old_tags.present?
114
+
115
+ # Create new taggings:
116
+ new_tags.each do |tag|
117
+ taggings.create!(tag_id: tag.id, context: context.to_s, tagger: owner, taggable: self)
94
118
  end
95
119
  end
96
-
97
- true
98
120
  end
121
+
122
+ true
99
123
  end
100
124
  end
101
- end
125
+ end
@@ -1,14 +1,14 @@
1
1
  module ActsAsTaggableOn::Taggable
2
2
  module Related
3
3
  def self.included(base)
4
- base.send :include, ActsAsTaggableOn::Taggable::Related::InstanceMethods
5
4
  base.extend ActsAsTaggableOn::Taggable::Related::ClassMethods
5
+ base.initialize_acts_as_taggable_on_related
6
6
  end
7
-
7
+
8
8
  module ClassMethods
9
9
  def initialize_acts_as_taggable_on_related
10
10
  tag_types.map(&:to_s).each do |tag_type|
11
- class_eval %(
11
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
12
12
  def find_related_#{tag_type}(options = {})
13
13
  related_tags_for('#{tag_type}', self.class, options)
14
14
  end
@@ -17,48 +17,55 @@ module ActsAsTaggableOn::Taggable
17
17
  def find_related_#{tag_type}_for(klass, options = {})
18
18
  related_tags_for('#{tag_type}', klass, options)
19
19
  end
20
-
21
- def find_matching_contexts(search_context, result_context, options = {})
22
- matching_contexts_for(search_context.to_s, result_context.to_s, self.class, options)
23
- end
24
-
25
- def find_matching_contexts_for(klass, search_context, result_context, options = {})
26
- matching_contexts_for(search_context.to_s, result_context.to_s, klass, options)
27
- end
28
- )
29
- end
20
+ RUBY
21
+ end
30
22
  end
31
-
23
+
32
24
  def acts_as_taggable_on(*args)
33
25
  super(*args)
34
26
  initialize_acts_as_taggable_on_related
35
27
  end
36
28
  end
37
-
38
- module InstanceMethods
39
- def matching_contexts_for(search_context, result_context, klass, options = {})
40
- tags_to_find = tags_on(search_context).collect { |t| t.name }
41
29
 
42
- exclude_self = "#{klass.table_name}.id != #{id} AND" if self.class == klass
43
-
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),
48
- :order => "count DESC" }.update(options))
49
- end
50
-
51
- def related_tags_for(context, klass, options = {})
52
- tags_to_find = tags_on(context).collect { |t| t.name }
30
+ def find_matching_contexts(search_context, result_context, options = {})
31
+ matching_contexts_for(search_context.to_s, result_context.to_s, self.class, options)
32
+ end
33
+
34
+ def find_matching_contexts_for(klass, search_context, result_context, options = {})
35
+ matching_contexts_for(search_context.to_s, result_context.to_s, klass, options)
36
+ end
37
+
38
+ def matching_contexts_for(search_context, result_context, klass, options = {})
39
+ tags_to_find = tags_on(search_context).map { |t| t.name }
40
+ related_where(klass, ["#{exclude_self(klass, id)} #{klass.table_name}.#{klass.primary_key} = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = '#{klass.base_class}' 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])
41
+ end
42
+
43
+ def related_tags_for(context, klass, options = {})
44
+ tags_to_ignore = Array.wrap(options.delete(:ignore)).map(&:to_s) || []
45
+ tags_to_find = tags_on(context).map { |t| t.name }.reject { |t| tags_to_ignore.include? t }
46
+ related_where(klass, ["#{exclude_self(klass, id)} #{klass.table_name}.#{klass.primary_key} = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = '#{klass.base_class}' 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])
47
+ end
53
48
 
54
- exclude_self = "#{klass.table_name}.id != #{id} AND" if self.class == klass
49
+ private
55
50
 
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),
60
- :order => "count DESC" }.update(options))
51
+ def exclude_self(klass, id)
52
+ "#{klass.table_name}.#{klass.primary_key} != #{id} AND" if [self.class.base_class, self.class].include? klass
53
+ end
54
+
55
+ def group_columns(klass)
56
+ if ActsAsTaggableOn::Utils.using_postgresql?
57
+ grouped_column_names_for(klass)
58
+ else
59
+ "#{klass.table_name}.#{klass.primary_key}"
61
60
  end
62
61
  end
62
+
63
+ def related_where(klass, conditions)
64
+ klass.select("#{klass.table_name}.*, COUNT(#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}) AS count")
65
+ .from("#{klass.table_name}, #{ActsAsTaggableOn::Tag.table_name}, #{ActsAsTaggableOn::Tagging.table_name}")
66
+ .group(group_columns(klass))
67
+ .order('count DESC')
68
+ .where(conditions)
69
+ end
63
70
  end
64
- end
71
+ end
@@ -0,0 +1,5 @@
1
+ require 'rails/engine'
2
+ module ActsAsTaggableOn
3
+ class Engine < Rails::Engine
4
+ end
5
+ end
@@ -1,65 +1,135 @@
1
- class Tag < ActiveRecord::Base
2
- include ActsAsTaggableOn::ActiveRecord::Backports if ActiveRecord::VERSION::MAJOR < 3
3
-
4
- attr_accessible :name
5
-
6
- ### ASSOCIATIONS:
7
-
8
- has_many :taggings, :dependent => :destroy
9
-
10
- ### VALIDATIONS:
11
-
12
- validates_presence_of :name
13
- validates_uniqueness_of :name
14
-
15
- ### SCOPES:
16
-
17
- def self.named(name)
18
- where(["name LIKE ?", name])
19
- end
20
-
21
- def self.named_any(list)
22
- where(list.map { |tag| sanitize_sql(["name LIKE ?", tag.to_s]) }.join(" OR "))
23
- end
24
-
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)
1
+ # coding: utf-8
2
+ module ActsAsTaggableOn
3
+ class Tag < ::ActiveRecord::Base
4
+
5
+ attr_accessible :name if defined?(ActiveModel::MassAssignmentSecurity)
6
+
7
+ ### ASSOCIATIONS:
8
+
9
+ has_many :taggings, dependent: :destroy, class_name: 'ActsAsTaggableOn::Tagging'
10
+
11
+ ### VALIDATIONS:
12
+
13
+ validates_presence_of :name
14
+ validates_uniqueness_of :name, if: :validates_name_uniqueness?
15
+ validates_length_of :name, maximum: 255
16
+
17
+ # monkey patch this method if don't need name uniqueness validation
18
+ def validates_name_uniqueness?
19
+ true
20
+ end
21
+
22
+ ### SCOPES:
23
+
24
+ def self.named(name)
25
+ if ActsAsTaggableOn.strict_case_match
26
+ where(["name = #{binary}?", as_8bit_ascii(name)])
27
+ else
28
+ where(['LOWER(name) = LOWER(?)', as_8bit_ascii(unicode_downcase(name))])
29
+ end
30
+ end
31
+
32
+ def self.named_any(list)
33
+ if ActsAsTaggableOn.strict_case_match
34
+ clause = list.map { |tag|
35
+ sanitize_sql(["name = #{binary}?", as_8bit_ascii(tag)])
36
+ }.join(' OR ')
37
+ where(clause)
38
+ else
39
+ clause = list.map { |tag|
40
+ sanitize_sql(['LOWER(name) = LOWER(?)', as_8bit_ascii(unicode_downcase(tag))])
41
+ }.join(' OR ')
42
+ where(clause)
43
+ end
44
+ end
45
+
46
+ def self.named_like(name)
47
+ clause = ["name #{ActsAsTaggableOn::Utils.like_operator} ? ESCAPE '!'", "%#{ActsAsTaggableOn::Utils.escape_like(name)}%"]
48
+ where(clause)
49
+ end
50
+
51
+ def self.named_like_any(list)
52
+ clause = list.map { |tag|
53
+ sanitize_sql(["name #{ActsAsTaggableOn::Utils.like_operator} ? ESCAPE '!'", "%#{ActsAsTaggableOn::Utils.escape_like(tag.to_s)}%"])
54
+ }.join(' OR ')
55
+ where(clause)
56
+ end
57
+
58
+ ### CLASS METHODS:
59
+
60
+ def self.find_or_create_with_like_by_name(name)
61
+ if ActsAsTaggableOn.strict_case_match
62
+ self.find_or_create_all_with_like_by_name([name]).first
63
+ else
64
+ named_like(name).first || create(name: name)
65
+ end
66
+ end
67
+
68
+ def self.find_or_create_all_with_like_by_name(*list)
69
+ list = Array(list).flatten
70
+
71
+ return [] if list.empty?
72
+
73
+ existing_tags = named_any(list)
74
+
75
+ list.map do |tag_name|
76
+ comparable_tag_name = comparable_name(tag_name)
77
+ existing_tag = existing_tags.find { |tag| comparable_name(tag.name) == comparable_tag_name }
78
+ begin
79
+ existing_tag || create(name: tag_name)
80
+ rescue ActiveRecord::RecordNotUnique
81
+ # Postgres aborts the current transaction with
82
+ # PG::InFailedSqlTransaction: ERROR: current transaction is aborted, commands ignored until end of transaction block
83
+ # so we have to rollback this transaction
84
+ raise DuplicateTagError.new("'#{tag_name}' has already been taken")
85
+ end
86
+ end
87
+ end
88
+
89
+ ### INSTANCE METHODS:
90
+
91
+ def ==(object)
92
+ super || (object.is_a?(Tag) && name == object.name)
93
+ end
94
+
95
+ def to_s
96
+ name
97
+ end
98
+
99
+ def count
100
+ read_attribute(:count).to_i
101
+ end
102
+
103
+ class << self
104
+ private
105
+
106
+ def comparable_name(str)
107
+ if ActsAsTaggableOn.strict_case_match
108
+ str
109
+ else
110
+ unicode_downcase(str.to_s)
111
+ end
112
+ end
113
+
114
+ def binary
115
+ ActsAsTaggableOn::Utils.using_mysql? ? 'BINARY ' : nil
116
+ end
117
+
118
+ def unicode_downcase(string)
119
+ if ActiveSupport::Multibyte::Unicode.respond_to?(:downcase)
120
+ ActiveSupport::Multibyte::Unicode.downcase(string)
121
+ else
122
+ ActiveSupport::Multibyte::Chars.new(string).downcase.to_s
123
+ end
124
+ end
125
+
126
+ def as_8bit_ascii(string)
127
+ if defined?(Encoding)
128
+ string.to_s.dup.force_encoding('BINARY')
129
+ else
130
+ string.to_s.mb_chars
131
+ end
132
+ end
133
+ end
37
134
  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)
55
- end
56
-
57
- def to_s
58
- name
59
- end
60
-
61
- def count
62
- read_attribute(:count).to_i
63
- end
64
-
65
135
  end