acts-as-taggable-on 2.0.0.pre5 → 3.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (102) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/{spec/spec.opts → .rspec} +0 -0
  4. data/.travis.yml +40 -0
  5. data/Appraisals +16 -0
  6. data/CHANGELOG.md +208 -0
  7. data/CONTRIBUTING.md +44 -0
  8. data/Gemfile +10 -5
  9. data/Guardfile +5 -0
  10. data/{MIT-LICENSE → LICENSE.md} +1 -1
  11. data/README.md +477 -0
  12. data/Rakefile +14 -52
  13. data/UPGRADING.md +8 -0
  14. data/acts-as-taggable-on.gemspec +36 -0
  15. data/{lib/generators/acts_as_taggable_on/migration/templates/active_record/migration.rb → db/migrate/1_acts_as_taggable_on_migration.rb} +5 -3
  16. data/db/migrate/2_add_missing_unique_indices.rb +19 -0
  17. data/db/migrate/3_add_taggings_counter_cache_to_tags.rb +14 -0
  18. data/db/migrate/4_add_missing_taggable_index.rb +9 -0
  19. data/db/migrate/5_change_collation_for_tag_names.rb +9 -0
  20. data/gemfiles/activerecord_3.2.gemfile +15 -0
  21. data/gemfiles/activerecord_4.0.gemfile +15 -0
  22. data/gemfiles/activerecord_4.1.gemfile +15 -0
  23. data/gemfiles/activerecord_4.2.gemfile +16 -0
  24. data/lib/acts-as-taggable-on.rb +117 -22
  25. data/lib/acts_as_taggable_on/compatibility.rb +35 -0
  26. data/lib/acts_as_taggable_on/default_parser.rb +79 -0
  27. data/lib/acts_as_taggable_on/engine.rb +5 -0
  28. data/lib/acts_as_taggable_on/generic_parser.rb +19 -0
  29. data/lib/acts_as_taggable_on/tag.rb +137 -61
  30. data/lib/acts_as_taggable_on/tag_list.rb +96 -75
  31. data/lib/acts_as_taggable_on/tag_list_parser.rb +21 -0
  32. data/lib/acts_as_taggable_on/taggable/cache.rb +86 -0
  33. data/lib/acts_as_taggable_on/taggable/collection.rb +178 -0
  34. data/lib/acts_as_taggable_on/taggable/core.rb +459 -0
  35. data/lib/acts_as_taggable_on/taggable/dirty.rb +36 -0
  36. data/lib/acts_as_taggable_on/taggable/ownership.rb +125 -0
  37. data/lib/acts_as_taggable_on/taggable/related.rb +71 -0
  38. data/lib/acts_as_taggable_on/taggable.rb +102 -0
  39. data/lib/acts_as_taggable_on/tagger.rb +88 -0
  40. data/lib/acts_as_taggable_on/tagging.rb +38 -18
  41. data/lib/acts_as_taggable_on/tags_helper.rb +12 -14
  42. data/lib/acts_as_taggable_on/utils.rb +38 -0
  43. data/lib/acts_as_taggable_on/version.rb +4 -0
  44. data/lib/acts_as_taggable_on.rb +6 -0
  45. data/lib/tasks/tags_collate_utf8.rake +21 -0
  46. data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +205 -195
  47. data/spec/acts_as_taggable_on/acts_as_tagger_spec.rb +79 -81
  48. data/spec/acts_as_taggable_on/caching_spec.rb +83 -0
  49. data/spec/acts_as_taggable_on/default_parser_spec.rb +47 -0
  50. data/spec/acts_as_taggable_on/generic_parser_spec.rb +14 -0
  51. data/spec/acts_as_taggable_on/related_spec.rb +99 -0
  52. data/spec/acts_as_taggable_on/single_table_inheritance_spec.rb +211 -0
  53. data/spec/acts_as_taggable_on/tag_list_parser_spec.rb +46 -0
  54. data/spec/acts_as_taggable_on/tag_list_spec.rb +142 -62
  55. data/spec/acts_as_taggable_on/tag_spec.rb +274 -64
  56. data/spec/acts_as_taggable_on/taggable/dirty_spec.rb +127 -0
  57. data/spec/acts_as_taggable_on/taggable_spec.rb +704 -181
  58. data/spec/acts_as_taggable_on/tagger_spec.rb +134 -56
  59. data/spec/acts_as_taggable_on/tagging_spec.rb +54 -22
  60. data/spec/acts_as_taggable_on/tags_helper_spec.rb +39 -22
  61. data/spec/acts_as_taggable_on/utils_spec.rb +23 -0
  62. data/spec/internal/app/models/altered_inheriting_taggable_model.rb +3 -0
  63. data/spec/internal/app/models/cached_model.rb +3 -0
  64. data/spec/internal/app/models/cached_model_with_array.rb +5 -0
  65. data/spec/internal/app/models/company.rb +15 -0
  66. data/spec/internal/app/models/inheriting_taggable_model.rb +2 -0
  67. data/spec/internal/app/models/market.rb +2 -0
  68. data/spec/internal/app/models/models.rb +90 -0
  69. data/spec/internal/app/models/non_standard_id_taggable_model.rb +8 -0
  70. data/spec/internal/app/models/ordered_taggable_model.rb +4 -0
  71. data/spec/internal/app/models/other_cached_model.rb +3 -0
  72. data/spec/internal/app/models/other_taggable_model.rb +4 -0
  73. data/spec/internal/app/models/student.rb +2 -0
  74. data/spec/internal/app/models/taggable_model.rb +13 -0
  75. data/spec/internal/app/models/untaggable_model.rb +3 -0
  76. data/spec/internal/app/models/user.rb +3 -0
  77. data/spec/internal/config/database.yml.sample +19 -0
  78. data/spec/internal/db/schema.rb +97 -0
  79. data/spec/spec_helper.rb +12 -38
  80. data/spec/support/0-helpers.rb +32 -0
  81. data/spec/support/array.rb +9 -0
  82. data/spec/support/database.rb +42 -0
  83. data/spec/support/database_cleaner.rb +21 -0
  84. metadata +268 -73
  85. data/CHANGELOG +0 -25
  86. data/README.rdoc +0 -212
  87. data/VERSION +0 -1
  88. data/lib/acts_as_taggable_on/acts_as_taggable_on/cache.rb +0 -56
  89. data/lib/acts_as_taggable_on/acts_as_taggable_on/collection.rb +0 -97
  90. data/lib/acts_as_taggable_on/acts_as_taggable_on/core.rb +0 -220
  91. data/lib/acts_as_taggable_on/acts_as_taggable_on/dirty.rb +0 -29
  92. data/lib/acts_as_taggable_on/acts_as_taggable_on/ownership.rb +0 -101
  93. data/lib/acts_as_taggable_on/acts_as_taggable_on/related.rb +0 -64
  94. data/lib/acts_as_taggable_on/acts_as_taggable_on.rb +0 -41
  95. data/lib/acts_as_taggable_on/acts_as_tagger.rb +0 -47
  96. data/lib/acts_as_taggable_on/compatibility/Gemfile +0 -6
  97. data/lib/acts_as_taggable_on/compatibility/active_record_backports.rb +0 -17
  98. data/lib/generators/acts_as_taggable_on/migration/migration_generator.rb +0 -31
  99. data/rails/init.rb +0 -1
  100. data/spec/bm.rb +0 -52
  101. data/spec/models.rb +0 -36
  102. data/spec/schema.rb +0 -42
@@ -0,0 +1,15 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", :github => "rails/rails", :branch => "4-0-stable"
6
+
7
+ group :local_development do
8
+ gem "guard"
9
+ gem "guard-rspec"
10
+ gem "appraisal"
11
+ gem "rake"
12
+ gem "byebug", :platform => :mri_21
13
+ end
14
+
15
+ gemspec :path => "../"
@@ -0,0 +1,15 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", :github => "rails/rails", :branch => "4-1-stable"
6
+
7
+ group :local_development do
8
+ gem "guard"
9
+ gem "guard-rspec"
10
+ gem "appraisal"
11
+ gem "rake"
12
+ gem "byebug", :platform => :mri_21
13
+ end
14
+
15
+ gemspec :path => "../"
@@ -0,0 +1,16 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "railties", :github => "rails/rails", :branch => "4-2-stable"
6
+ gem "activerecord", :github => "rails/rails", :branch => "4-2-stable"
7
+
8
+ group :local_development do
9
+ gem "guard"
10
+ gem "guard-rspec"
11
+ gem "appraisal"
12
+ gem "rake"
13
+ gem "byebug", :platform => :mri_21
14
+ end
15
+
16
+ gemspec :path => "../"
@@ -1,30 +1,125 @@
1
- require "active_record"
2
- require "action_view"
1
+ require 'active_record'
2
+ require 'active_record/version'
3
+ require 'active_support/core_ext/module'
3
4
 
4
- $LOAD_PATH.unshift(File.dirname(__FILE__))
5
+ require_relative 'acts_as_taggable_on/engine' if defined?(Rails)
5
6
 
6
- require "acts_as_taggable_on/compatibility/active_record_backports" if ActiveRecord::VERSION::MAJOR < 3
7
+ require 'digest/sha1'
7
8
 
8
- require "acts_as_taggable_on/acts_as_taggable_on"
9
- require "acts_as_taggable_on/acts_as_taggable_on/core"
10
- require "acts_as_taggable_on/acts_as_taggable_on/collection"
11
- require "acts_as_taggable_on/acts_as_taggable_on/cache"
12
- require "acts_as_taggable_on/acts_as_taggable_on/ownership"
13
- require "acts_as_taggable_on/acts_as_taggable_on/related"
9
+ module ActsAsTaggableOn
10
+ extend ActiveSupport::Autoload
14
11
 
15
- require "acts_as_taggable_on/acts_as_tagger"
16
- require "acts_as_taggable_on/tag"
17
- require "acts_as_taggable_on/tag_list"
18
- require "acts_as_taggable_on/tags_helper"
19
- require "acts_as_taggable_on/tagging"
12
+ autoload :Tag
13
+ autoload :TagList
14
+ autoload :GenericParser
15
+ autoload :DefaultParser
16
+ autoload :TagListParser
17
+ autoload :Taggable
18
+ autoload :Tagger
19
+ autoload :Tagging
20
+ autoload :TagsHelper
21
+ autoload :VERSION
20
22
 
21
- $LOAD_PATH.shift
23
+ autoload_under 'taggable' do
24
+ autoload :Cache
25
+ autoload :Collection
26
+ autoload :Core
27
+ autoload :Dirty
28
+ autoload :Ownership
29
+ autoload :Related
30
+ end
22
31
 
23
- if defined?(ActiveRecord::Base)
24
- ActiveRecord::Base.extend ActsAsTaggableOn::Taggable
25
- ActiveRecord::Base.send :include, ActsAsTaggableOn::Tagger
32
+ autoload :Utils
33
+ autoload :Compatibility
34
+
35
+
36
+ class DuplicateTagError < StandardError
37
+ end
38
+
39
+ def self.setup
40
+ @configuration ||= Configuration.new
41
+ yield @configuration if block_given?
42
+ end
43
+
44
+ def self.method_missing(method_name, *args, &block)
45
+ @configuration.respond_to?(method_name) ?
46
+ @configuration.send(method_name, *args, &block) : super
47
+ end
48
+
49
+ def self.respond_to?(method_name, include_private=false)
50
+ @configuration.respond_to? method_name
51
+ end
52
+
53
+ def self.glue
54
+ setting = @configuration.delimiter
55
+ delimiter = setting.kind_of?(Array) ? setting[0] : setting
56
+ delimiter.ends_with?(' ') ? delimiter : "#{delimiter} "
57
+ end
58
+
59
+ class Configuration
60
+ attr_accessor :delimiter, :force_lowercase, :force_parameterize,
61
+ :strict_case_match, :remove_unused_tags, :default_parser,
62
+ :tags_counter
63
+
64
+ def initialize
65
+ @delimiter = ','
66
+ @force_lowercase = false
67
+ @force_parameterize = false
68
+ @strict_case_match = false
69
+ @remove_unused_tags = false
70
+ @tags_counter = true
71
+ @default_parser = DefaultParser
72
+ @force_binary_collation = false
73
+ end
74
+
75
+ def strict_case_match=(force_cs)
76
+ if @force_binary_collation == false
77
+ @strict_case_match = force_cs
78
+ end
79
+ end
80
+
81
+ def delimiter=(string)
82
+ ActiveRecord::Base.logger.warn <<WARNING
83
+ ActsAsTaggableOn.delimiter is deprecated \
84
+ and will be removed from v4.0+, use \
85
+ a ActsAsTaggableOn.default_parser instead
86
+ WARNING
87
+ @delimiter = string
88
+ end
89
+
90
+ def force_binary_collation=(force_bin)
91
+ if Utils.using_mysql?
92
+ if force_bin == true
93
+ Configuration.apply_binary_collation(true)
94
+ @force_binary_collation = true
95
+ @strict_case_match = true
96
+ else
97
+ Configuration.apply_binary_collation(false)
98
+ @force_binary_collation = false
99
+ end
100
+ end
101
+ end
102
+
103
+ def self.apply_binary_collation(bincoll)
104
+ if Utils.using_mysql?
105
+ coll = 'utf8_general_ci'
106
+ if bincoll == true
107
+ coll = 'utf8_bin'
108
+ end
109
+ ActiveRecord::Migration.execute("ALTER TABLE tags MODIFY name varchar(255) CHARACTER SET utf8 COLLATE #{coll};")
110
+ end
111
+ end
112
+
113
+ end
114
+
115
+ setup
26
116
  end
27
117
 
28
- if defined?(ActionView::Base)
29
- ActionView::Base.send :include, TagsHelper
30
- end
118
+ ActiveSupport.on_load(:active_record) do
119
+ extend ActsAsTaggableOn::Compatibility
120
+ extend ActsAsTaggableOn::Taggable
121
+ include ActsAsTaggableOn::Tagger
122
+ end
123
+ ActiveSupport.on_load(:action_view) do
124
+ include ActsAsTaggableOn::TagsHelper
125
+ end
@@ -0,0 +1,35 @@
1
+ module ActsAsTaggableOn::Compatibility
2
+ def has_many_with_taggable_compatibility(name, options = {}, &extention)
3
+ if ActsAsTaggableOn::Utils.active_record4?
4
+ scope, opts = build_taggable_scope_and_options(options)
5
+ has_many(name, scope, opts, &extention)
6
+ else
7
+ has_many(name, options, &extention)
8
+ end
9
+ end
10
+
11
+ def build_taggable_scope_and_options(opts)
12
+ scope_opts, opts = parse_taggable_options(opts)
13
+
14
+ unless scope_opts.empty?
15
+ scope = -> {
16
+ scope_opts.inject(self) { |result, hash| result.send(*hash) }
17
+ }
18
+ return [scope, opts]
19
+ end
20
+
21
+ [nil, opts]
22
+ end
23
+
24
+ def parse_taggable_options(opts)
25
+ scope_opts = {}
26
+ [:order, :having, :select, :group, :limit, :offset, :readonly].each do |o|
27
+ scope_opts[o] = opts.delete o if opts[o]
28
+ end
29
+ scope_opts[:where] = opts.delete :conditions if opts[:conditions]
30
+ scope_opts[:joins] = opts.delete :include if opts [:include]
31
+ scope_opts[:distinct] = opts.delete :uniq if opts[:uniq]
32
+
33
+ [scope_opts, opts]
34
+ end
35
+ end
@@ -0,0 +1,79 @@
1
+ module ActsAsTaggableOn
2
+ ##
3
+ # Returns a new TagList using the given tag string.
4
+ #
5
+ # Example:
6
+ # tag_list = ActsAsTaggableOn::DefaultParser.parse("One , Two, Three")
7
+ # tag_list # ["One", "Two", "Three"]
8
+ class DefaultParser < GenericParser
9
+
10
+ def parse
11
+ string = @tag_list
12
+
13
+ string = string.join(ActsAsTaggableOn.glue) if string.respond_to?(:join)
14
+ TagList.new.tap do |tag_list|
15
+ string = string.to_s.dup
16
+
17
+ string.gsub!(double_quote_pattern) {
18
+ # Append the matched tag to the tag list
19
+ tag_list << Regexp.last_match[2]
20
+ # Return the matched delimiter ($3) to replace the matched items
21
+ ''
22
+ }
23
+
24
+ string.gsub!(single_quote_pattern) {
25
+ # Append the matched tag ($2) to the tag list
26
+ tag_list << Regexp.last_match[2]
27
+ # Return an empty string to replace the matched items
28
+ ''
29
+ }
30
+
31
+ # split the string by the delimiter
32
+ # and add to the tag_list
33
+ tag_list.add(string.split(Regexp.new delimiter))
34
+ end
35
+ end
36
+
37
+
38
+ # private
39
+ def delimiter
40
+ # Parse the quoted tags
41
+ d = ActsAsTaggableOn.delimiter
42
+ # Separate multiple delimiters by bitwise operator
43
+ d = d.join('|') if d.kind_of?(Array)
44
+ d
45
+ end
46
+
47
+ # ( # Tag start delimiter ($1)
48
+ # \A | # Either string start or
49
+ # #{delimiter} # a delimiter
50
+ # )
51
+ # \s*" # quote (") optionally preceded by whitespace
52
+ # (.*?) # Tag ($2)
53
+ # "\s* # quote (") optionally followed by whitespace
54
+ # (?= # Tag end delimiter (not consumed; is zero-length lookahead)
55
+ # #{delimiter}\s* | # Either a delimiter optionally followed by whitespace or
56
+ # \z # string end
57
+ # )
58
+ def double_quote_pattern
59
+ /(\A|#{delimiter})\s*"(.*?)"\s*(?=#{delimiter}\s*|\z)/
60
+ end
61
+
62
+ # ( # Tag start delimiter ($1)
63
+ # \A | # Either string start or
64
+ # #{delimiter} # a delimiter
65
+ # )
66
+ # \s*' # quote (') optionally preceded by whitespace
67
+ # (.*?) # Tag ($2)
68
+ # '\s* # quote (') optionally followed by whitespace
69
+ # (?= # Tag end delimiter (not consumed; is zero-length lookahead)
70
+ # #{delimiter}\s* | d # Either a delimiter optionally followed by whitespace or
71
+ # \z # string end
72
+ # )
73
+ def single_quote_pattern
74
+ /(\A|#{delimiter})\s*'(.*?)'\s*(?=#{delimiter}\s*|\z)/
75
+ end
76
+
77
+ end
78
+
79
+ end
@@ -0,0 +1,5 @@
1
+ require 'rails/engine'
2
+ module ActsAsTaggableOn
3
+ class Engine < Rails::Engine
4
+ end
5
+ end
@@ -0,0 +1,19 @@
1
+ module ActsAsTaggableOn
2
+ ##
3
+ # Returns a new TagList using the given tag string.
4
+ #
5
+ # Example:
6
+ # tag_list = ActsAsTaggableOn::GenericParser.new.parse("One , Two, Three")
7
+ # tag_list # ["One", "Two", "Three"]
8
+ class GenericParser
9
+ def initialize(tag_list)
10
+ @tag_list = tag_list
11
+ end
12
+
13
+ def parse
14
+ TagList.new.tap do |tag_list|
15
+ tag_list.add @tag_list.split(',').map(&:strip).reject(&:empty?)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -1,65 +1,141 @@
1
- class Tag < ActiveRecord::Base
2
- include ActsAsTaggableOn::ActiveRecord::Backports if ActiveRecord::VERSION::MAJOR < 3
3
-
4
- attr_accessible :name
1
+ # encoding: utf-8
2
+ module ActsAsTaggableOn
3
+ class Tag < ::ActiveRecord::Base
5
4
 
6
- ### ASSOCIATIONS:
5
+ attr_accessible :name if defined?(ActiveModel::MassAssignmentSecurity)
7
6
 
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)
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.downcase == name.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
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
+ scope :most_used, ->(limit = 20) { order('taggings_count desc').limit(limit) }
24
+ scope :least_used, ->(limit = 20) { order('taggings_count asc').limit(limit) }
25
+
26
+ def self.named(name)
27
+ if ActsAsTaggableOn.strict_case_match
28
+ where(["name = #{binary}?", as_8bit_ascii(name)])
29
+ else
30
+ where(['LOWER(name) = LOWER(?)', as_8bit_ascii(unicode_downcase(name))])
31
+ end
32
+ end
33
+
34
+ def self.named_any(list)
35
+ clause = list.map { |tag|
36
+ sanitize_sql_for_named_any(tag).force_encoding('BINARY')
37
+ }.join(' OR ')
38
+ where(clause)
39
+ end
40
+
41
+ def self.named_like(name)
42
+ clause = ["name #{ActsAsTaggableOn::Utils.like_operator} ? ESCAPE '!'", "%#{ActsAsTaggableOn::Utils.escape_like(name)}%"]
43
+ where(clause)
44
+ end
45
+
46
+ def self.named_like_any(list)
47
+ clause = list.map { |tag|
48
+ sanitize_sql(["name #{ActsAsTaggableOn::Utils.like_operator} ? ESCAPE '!'", "%#{ActsAsTaggableOn::Utils.escape_like(tag.to_s)}%"])
49
+ }.join(' OR ')
50
+ where(clause)
51
+ end
52
+
53
+ ### CLASS METHODS:
54
+
55
+ def self.find_or_create_with_like_by_name(name)
56
+ if ActsAsTaggableOn.strict_case_match
57
+ self.find_or_create_all_with_like_by_name([name]).first
58
+ else
59
+ named_like(name).first || create(name: name)
60
+ end
61
+ end
62
+
63
+ def self.find_or_create_all_with_like_by_name(*list)
64
+ list = Array(list).flatten
65
+
66
+ return [] if list.empty?
67
+
68
+ existing_tags = named_any(list)
69
+
70
+ list.map do |tag_name|
71
+ comparable_tag_name = comparable_name(tag_name)
72
+ existing_tag = existing_tags.find { |tag| comparable_name(tag.name) == comparable_tag_name }
73
+ begin
74
+ existing_tag || create(name: tag_name)
75
+ rescue ActiveRecord::RecordNotUnique
76
+ # Postgres aborts the current transaction with
77
+ # PG::InFailedSqlTransaction: ERROR: current transaction is aborted, commands ignored until end of transaction block
78
+ # so we have to rollback this transaction
79
+ raise DuplicateTagError.new("'#{tag_name}' has already been taken")
80
+ end
81
+ end
82
+ end
83
+
84
+ ### INSTANCE METHODS:
85
+
86
+ def ==(object)
87
+ super || (object.is_a?(Tag) && name == object.name)
88
+ end
89
+
90
+ def to_s
91
+ name
92
+ end
93
+
94
+ def count
95
+ read_attribute(:count).to_i
96
+ end
97
+
98
+ class << self
99
+
100
+
101
+
102
+ private
103
+
104
+ def comparable_name(str)
105
+ if ActsAsTaggableOn.strict_case_match
106
+ str
107
+ else
108
+ unicode_downcase(str.to_s)
109
+ end
110
+ end
111
+
112
+ def binary
113
+ ActsAsTaggableOn::Utils.using_mysql? ? 'BINARY ' : nil
114
+ end
115
+
116
+ def unicode_downcase(string)
117
+ if ActiveSupport::Multibyte::Unicode.respond_to?(:downcase)
118
+ ActiveSupport::Multibyte::Unicode.downcase(string)
119
+ else
120
+ ActiveSupport::Multibyte::Chars.new(string).downcase.to_s
121
+ end
122
+ end
123
+
124
+ def as_8bit_ascii(string)
125
+ if defined?(Encoding)
126
+ string.to_s.dup.force_encoding('BINARY')
127
+ else
128
+ string.to_s.mb_chars
129
+ end
130
+ end
131
+
132
+ def sanitize_sql_for_named_any(tag)
133
+ if ActsAsTaggableOn.strict_case_match
134
+ sanitize_sql(["name = #{binary}?", as_8bit_ascii(tag)])
135
+ else
136
+ sanitize_sql(['LOWER(name) = LOWER(?)', as_8bit_ascii(unicode_downcase(tag))])
137
+ end
138
+ end
139
+ end
59
140
  end
60
-
61
- def count
62
- read_attribute(:count).to_i
63
- end
64
-
65
141
  end