make_taggable 0.6.3

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 (156) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +47 -0
  3. data/.gitignore +13 -0
  4. data/.rspec +3 -0
  5. data/.standard.yml +18 -0
  6. data/.standard_todo.yml +5 -0
  7. data/.travis.yml +36 -0
  8. data/Appraisals +11 -0
  9. data/CHANGELOG.md +0 -0
  10. data/CODE_OF_CONDUCT.md +74 -0
  11. data/CONTRIBUTING.md +57 -0
  12. data/Gemfile +16 -0
  13. data/LICENSE.md +20 -0
  14. data/LICENSE.txt +21 -0
  15. data/README.md +478 -0
  16. data/Rakefile +7 -0
  17. data/bin/console +14 -0
  18. data/bin/setup +8 -0
  19. data/db/migrate/1_create_make_taggable_tags.rb +10 -0
  20. data/db/migrate/2_create_make_taggable_taggings.rb +12 -0
  21. data/db/migrate/3_add_index_to_tags.rb +5 -0
  22. data/db/migrate/4_add_index_to_taggings.rb +12 -0
  23. data/gemfiles/rails_5.gemfile +9 -0
  24. data/gemfiles/rails_6.gemfile +9 -0
  25. data/gemfiles/rails_master.gemfile +9 -0
  26. data/lib/make_taggable.rb +134 -0
  27. data/lib/make_taggable/default_parser.rb +75 -0
  28. data/lib/make_taggable/engine.rb +4 -0
  29. data/lib/make_taggable/generic_parser.rb +19 -0
  30. data/lib/make_taggable/tag.rb +131 -0
  31. data/lib/make_taggable/tag_list.rb +102 -0
  32. data/lib/make_taggable/taggable.rb +100 -0
  33. data/lib/make_taggable/taggable/cache.rb +90 -0
  34. data/lib/make_taggable/taggable/collection.rb +183 -0
  35. data/lib/make_taggable/taggable/core.rb +323 -0
  36. data/lib/make_taggable/taggable/ownership.rb +137 -0
  37. data/lib/make_taggable/taggable/related.rb +71 -0
  38. data/lib/make_taggable/taggable/tag_list_type.rb +4 -0
  39. data/lib/make_taggable/taggable/tagged_with_query.rb +16 -0
  40. data/lib/make_taggable/taggable/tagged_with_query/all_tags_query.rb +111 -0
  41. data/lib/make_taggable/taggable/tagged_with_query/any_tags_query.rb +68 -0
  42. data/lib/make_taggable/taggable/tagged_with_query/exclude_tags_query.rb +81 -0
  43. data/lib/make_taggable/taggable/tagged_with_query/query_base.rb +61 -0
  44. data/lib/make_taggable/tagger.rb +89 -0
  45. data/lib/make_taggable/tagging.rb +32 -0
  46. data/lib/make_taggable/tags_helper.rb +15 -0
  47. data/lib/make_taggable/utils.rb +34 -0
  48. data/lib/make_taggable/version.rb +4 -0
  49. data/lib/tasks/tags_collate_utf8.rake +17 -0
  50. data/make_taggable.gemspec +26 -0
  51. data/spec/dummy/README.md +24 -0
  52. data/spec/dummy/Rakefile +6 -0
  53. data/spec/dummy/app/assets/config/manifest.js +2 -0
  54. data/spec/dummy/app/channels/application_cable/channel.rb +4 -0
  55. data/spec/dummy/app/channels/application_cable/connection.rb +4 -0
  56. data/spec/dummy/app/controllers/application_controller.rb +2 -0
  57. data/spec/dummy/app/controllers/concerns/.keep +0 -0
  58. data/spec/dummy/app/jobs/application_job.rb +7 -0
  59. data/spec/dummy/app/mailers/application_mailer.rb +4 -0
  60. data/spec/dummy/app/models/altered_inheriting_taggable_model.rb +5 -0
  61. data/spec/dummy/app/models/application_record.rb +3 -0
  62. data/spec/dummy/app/models/cached_model.rb +3 -0
  63. data/spec/dummy/app/models/cached_model_with_array.rb +11 -0
  64. data/spec/dummy/app/models/columns_override_model.rb +5 -0
  65. data/spec/dummy/app/models/company.rb +15 -0
  66. data/spec/dummy/app/models/concerns/.keep +0 -0
  67. data/spec/dummy/app/models/inheriting_taggable_model.rb +4 -0
  68. data/spec/dummy/app/models/market.rb +2 -0
  69. data/spec/dummy/app/models/non_standard_id_taggable_model.rb +8 -0
  70. data/spec/dummy/app/models/ordered_taggable_model.rb +4 -0
  71. data/spec/dummy/app/models/other_cached_model.rb +3 -0
  72. data/spec/dummy/app/models/other_taggable_model.rb +4 -0
  73. data/spec/dummy/app/models/student.rb +4 -0
  74. data/spec/dummy/app/models/taggable_model.rb +14 -0
  75. data/spec/dummy/app/models/untaggable_model.rb +3 -0
  76. data/spec/dummy/app/models/user.rb +3 -0
  77. data/spec/dummy/app/views/layouts/mailer.html.erb +13 -0
  78. data/spec/dummy/app/views/layouts/mailer.text.erb +1 -0
  79. data/spec/dummy/bin/rails +4 -0
  80. data/spec/dummy/bin/rake +4 -0
  81. data/spec/dummy/bin/setup +33 -0
  82. data/spec/dummy/config.ru +5 -0
  83. data/spec/dummy/config/application.rb +19 -0
  84. data/spec/dummy/config/boot.rb +5 -0
  85. data/spec/dummy/config/cable.yml +10 -0
  86. data/spec/dummy/config/credentials.yml.enc +1 -0
  87. data/spec/dummy/config/database.yml +25 -0
  88. data/spec/dummy/config/environment.rb +5 -0
  89. data/spec/dummy/config/environments/development.rb +52 -0
  90. data/spec/dummy/config/environments/production.rb +105 -0
  91. data/spec/dummy/config/environments/test.rb +49 -0
  92. data/spec/dummy/config/initializers/application_controller_renderer.rb +8 -0
  93. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  94. data/spec/dummy/config/initializers/cors.rb +16 -0
  95. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  96. data/spec/dummy/config/initializers/inflections.rb +16 -0
  97. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  98. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  99. data/spec/dummy/config/locales/en.yml +33 -0
  100. data/spec/dummy/config/master.key +1 -0
  101. data/spec/dummy/config/puma.rb +38 -0
  102. data/spec/dummy/config/routes.rb +3 -0
  103. data/spec/dummy/config/spring.rb +6 -0
  104. data/spec/dummy/config/storage.yml +34 -0
  105. data/spec/dummy/db/migrate/20201119220853_create_taggable_models.rb +8 -0
  106. data/spec/dummy/db/migrate/20201119221037_create_columns_override_models.rb +9 -0
  107. data/spec/dummy/db/migrate/20201119221121_create_non_standard_id_taggable_models.rb +8 -0
  108. data/spec/dummy/db/migrate/20201119221228_create_untaggable_models.rb +8 -0
  109. data/spec/dummy/db/migrate/20201119221247_create_cached_models.rb +9 -0
  110. data/spec/dummy/db/migrate/20201119221314_create_other_cached_models.rb +11 -0
  111. data/spec/dummy/db/migrate/20201119221343_create_companies.rb +7 -0
  112. data/spec/dummy/db/migrate/20201119221416_create_users.rb +7 -0
  113. data/spec/dummy/db/migrate/20201119221434_create_other_taggable_models.rb +8 -0
  114. data/spec/dummy/db/migrate/20201119221507_create_ordered_taggable_models.rb +8 -0
  115. data/spec/dummy/db/migrate/20201119221530_create_cache_methods_injected_models.rb +7 -0
  116. data/spec/dummy/db/migrate/20201119221629_create_other_cached_with_array_models.rb +11 -0
  117. data/spec/dummy/db/migrate/20201119221746_create_taggable_model_with_jsons.rb +9 -0
  118. data/spec/dummy/db/migrate/20201119222429_create_make_taggable_tags.make_taggable_engine.rb +11 -0
  119. data/spec/dummy/db/migrate/20201119222430_create_make_taggable_taggings.make_taggable_engine.rb +13 -0
  120. data/spec/dummy/db/migrate/20201119222431_add_index_to_tags.make_taggable_engine.rb +6 -0
  121. data/spec/dummy/db/migrate/20201119222432_add_index_to_taggings.make_taggable_engine.rb +13 -0
  122. data/spec/dummy/db/schema.rb +117 -0
  123. data/spec/dummy/db/seeds.rb +7 -0
  124. data/spec/dummy/lib/tasks/.keep +0 -0
  125. data/spec/dummy/log/.keep +0 -0
  126. data/spec/dummy/public/robots.txt +1 -0
  127. data/spec/dummy/storage/.keep +0 -0
  128. data/spec/dummy/test/channels/application_cable/connection_test.rb +11 -0
  129. data/spec/dummy/test/controllers/.keep +0 -0
  130. data/spec/dummy/test/fixtures/.keep +0 -0
  131. data/spec/dummy/test/fixtures/files/.keep +0 -0
  132. data/spec/dummy/test/integration/.keep +0 -0
  133. data/spec/dummy/test/mailers/.keep +0 -0
  134. data/spec/dummy/test/models/.keep +0 -0
  135. data/spec/dummy/test/test_helper.rb +13 -0
  136. data/spec/dummy/vendor/.keep +0 -0
  137. data/spec/make_taggable/acts_as_tagger_spec.rb +112 -0
  138. data/spec/make_taggable/caching_spec.rb +123 -0
  139. data/spec/make_taggable/default_parser_spec.rb +45 -0
  140. data/spec/make_taggable/dirty_spec.rb +140 -0
  141. data/spec/make_taggable/generic_parser_spec.rb +13 -0
  142. data/spec/make_taggable/make_taggable_spec.rb +260 -0
  143. data/spec/make_taggable/related_spec.rb +93 -0
  144. data/spec/make_taggable/single_table_inheritance_spec.rb +220 -0
  145. data/spec/make_taggable/tag_list_spec.rb +169 -0
  146. data/spec/make_taggable/tag_spec.rb +297 -0
  147. data/spec/make_taggable/taggable_spec.rb +804 -0
  148. data/spec/make_taggable/tagger_spec.rb +149 -0
  149. data/spec/make_taggable/tagging_spec.rb +115 -0
  150. data/spec/make_taggable/tags_helper_spec.rb +43 -0
  151. data/spec/make_taggable/utils_spec.rb +22 -0
  152. data/spec/make_taggable_spec.rb +5 -0
  153. data/spec/spec_helper.rb +18 -0
  154. data/spec/support/array.rb +9 -0
  155. data/spec/support/helpers.rb +31 -0
  156. metadata +391 -0
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
4
+
5
+ require "rspec/core/rake_task"
6
+ RSpec::Core::RakeTask.new(:spec)
7
+ task default: :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "make_taggable"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,10 @@
1
+ class CreateMakeTaggableTags < ActiveRecord::Migration[5.2]
2
+ def change
3
+ create_table MakeTaggable.tags_table do |t|
4
+ t.string :name
5
+ t.integer :taggings_count, default: 0
6
+
7
+ t.timestamps
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,12 @@
1
+ class CreateMakeTaggableTaggings < ActiveRecord::Migration[5.2]
2
+ def change
3
+ create_table MakeTaggable.taggings_table do |t|
4
+ t.references :tag, foreign_key: {to_table: MakeTaggable.tags_table}
5
+ t.references :taggable, polymorphic: true
6
+ t.references :tagger, polymorphic: true
7
+ t.string :context, limit: 128
8
+
9
+ t.timestamps
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,5 @@
1
+ class AddIndexToTags < ActiveRecord::Migration[5.2]
2
+ def change
3
+ add_index MakeTaggable.tags_table, :name, unique: true
4
+ end
5
+ end
@@ -0,0 +1,12 @@
1
+ class AddIndexToTaggings < ActiveRecord::Migration[5.2]
2
+ def change
3
+ add_index MakeTaggable.taggings_table, :taggable_id
4
+ add_index MakeTaggable.taggings_table, :tagger_id
5
+ add_index MakeTaggable.taggings_table, :taggable_type
6
+ add_index MakeTaggable.taggings_table, :context
7
+ add_index MakeTaggable.taggings_table, [:tagger_id, :tagger_type]
8
+ add_index MakeTaggable.taggings_table, [:tag_id, :taggable_id, :taggable_type, :context, :tagger_id, :tagger_type], unique: true, name: "taggings_idx"
9
+ add_index MakeTaggable.taggings_table, [:taggable_id, :taggable_type, :context], name: "taggings_taggable_context_idx"
10
+ add_index MakeTaggable.taggings_table, [:taggable_id, :taggable_type, :tagger_id, :context], name: "taggings_idy"
11
+ end
12
+ end
@@ -0,0 +1,9 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "byebug", group: [:development, :test]
6
+ gem "listen", group: [:development, :test]
7
+ gem "rails", "~> 5.2.0"
8
+
9
+ gemspec path: "../"
@@ -0,0 +1,9 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "byebug", group: [:development, :test]
6
+ gem "listen", group: [:development, :test]
7
+ gem "rails", "~> 6.0.0"
8
+
9
+ gemspec path: "../"
@@ -0,0 +1,9 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "byebug", group: [:development, :test]
6
+ gem "listen", group: [:development, :test]
7
+ gem "rails", git: "https://github.com/rails/rails.git"
8
+
9
+ gemspec path: "../"
@@ -0,0 +1,134 @@
1
+ require "make_taggable/version"
2
+ require "active_record"
3
+ require "active_record/version"
4
+ require "active_support/core_ext/module"
5
+
6
+ begin
7
+ require "rails/engine"
8
+ require "make_taggable/engine"
9
+ rescue LoadError
10
+ end
11
+
12
+ require "digest/sha1"
13
+
14
+ module MakeTaggable
15
+ extend ActiveSupport::Autoload
16
+
17
+ autoload :Tag
18
+ autoload :TagList
19
+ autoload :GenericParser
20
+ autoload :DefaultParser
21
+ autoload :Taggable
22
+ autoload :Tagger
23
+ autoload :Tagging
24
+ autoload :TagsHelper
25
+ autoload :VERSION
26
+
27
+ autoload_under "taggable" do
28
+ autoload :Cache
29
+ autoload :Collection
30
+ autoload :Core
31
+ autoload :Dirty
32
+ autoload :Ownership
33
+ autoload :Related
34
+ autoload :TagListType
35
+ end
36
+
37
+ autoload :Utils
38
+ autoload :Compatibility
39
+
40
+ class DuplicateTagError < StandardError
41
+ end
42
+
43
+ def self.setup
44
+ @configuration ||= Configuration.new
45
+ yield @configuration if block_given?
46
+ end
47
+
48
+ def self.method_missing(method_name, *args, &block)
49
+ if @configuration.respond_to?(method_name)
50
+ @configuration.send(method_name, *args, &block)
51
+ else
52
+ super
53
+ end
54
+ end
55
+
56
+ def self.respond_to_missing?(method_name, include_private = false)
57
+ @configuration.respond_to? method_name
58
+ end
59
+
60
+ def self.glue
61
+ setting = @configuration.delimiter
62
+ delimiter = setting.is_a?(Array) ? setting[0] : setting
63
+ delimiter.ends_with?(" ") ? delimiter : "#{delimiter} "
64
+ end
65
+
66
+ class Configuration
67
+ attr_accessor :force_lowercase, :force_parameterize,
68
+ :remove_unused_tags, :default_parser,
69
+ :tags_counter, :tags_table,
70
+ :taggings_table
71
+ attr_reader :delimiter, :strict_case_match
72
+
73
+ def initialize
74
+ @delimiter = ","
75
+ @force_lowercase = false
76
+ @force_parameterize = false
77
+ @strict_case_match = false
78
+ @remove_unused_tags = false
79
+ @tags_counter = true
80
+ @default_parser = DefaultParser
81
+ @force_binary_collation = false
82
+ @tags_table = :tags
83
+ @taggings_table = :taggings
84
+ end
85
+
86
+ def strict_case_match=(force_cs)
87
+ @strict_case_match = force_cs unless @force_binary_collation
88
+ end
89
+
90
+ def delimiter=(string)
91
+ ActiveRecord::Base.logger.warn <<~WARNING
92
+ MakeTaggable.delimiter is deprecated \
93
+ and will be removed from v4.0+, use \
94
+ a MakeTaggable.default_parser instead
95
+ WARNING
96
+ @delimiter = string
97
+ end
98
+
99
+ def force_binary_collation=(force_bin)
100
+ if Utils.using_mysql?
101
+ if force_bin
102
+ Configuration.apply_binary_collation(true)
103
+ @force_binary_collation = true
104
+ @strict_case_match = true
105
+ else
106
+ Configuration.apply_binary_collation(false)
107
+ @force_binary_collation = false
108
+ end
109
+ end
110
+ end
111
+
112
+ def self.apply_binary_collation(bincoll)
113
+ if Utils.using_mysql?
114
+ coll = "utf8_general_ci"
115
+ coll = "utf8_bin" if bincoll
116
+ begin
117
+ ActiveRecord::Migration.execute("ALTER TABLE #{Tag.table_name} MODIFY name varchar(255) CHARACTER SET utf8 COLLATE #{coll};")
118
+ rescue => e
119
+ puts "Trapping #{e.class}: collation parameter ignored while migrating for the first time."
120
+ end
121
+ end
122
+ end
123
+ end
124
+ setup
125
+ end
126
+
127
+ ActiveSupport.on_load(:active_record) do
128
+ extend MakeTaggable::Taggable
129
+ include MakeTaggable::Tagger
130
+ end
131
+
132
+ ActiveSupport.on_load(:action_view) do
133
+ include MakeTaggable::TagsHelper
134
+ end
@@ -0,0 +1,75 @@
1
+ module MakeTaggable
2
+ ##
3
+ # Returns a new TagList using the given tag string.
4
+ #
5
+ # Example:
6
+ # tag_list = MakeTaggable::DefaultParser.parse("One , Two, Three")
7
+ # tag_list # ["One", "Two", "Three"]
8
+ class DefaultParser < GenericParser
9
+ def parse
10
+ string = @tag_list
11
+
12
+ string = string.join(MakeTaggable.glue) if string.respond_to?(:join)
13
+ TagList.new.tap do |tag_list|
14
+ string = string.to_s.dup
15
+
16
+ string.gsub!(double_quote_pattern) do
17
+ # Append the matched tag to the tag list
18
+ tag_list << Regexp.last_match[2]
19
+ # Return the matched delimiter ($3) to replace the matched items
20
+ ""
21
+ end
22
+
23
+ string.gsub!(single_quote_pattern) do
24
+ # Append the matched tag ($2) to the tag list
25
+ tag_list << Regexp.last_match[2]
26
+ # Return an empty string to replace the matched items
27
+ ""
28
+ end
29
+
30
+ # split the string by the delimiter
31
+ # and add to the tag_list
32
+ tag_list.add(string.split(Regexp.new(delimiter)))
33
+ end
34
+ end
35
+
36
+ # private
37
+ def delimiter
38
+ # Parse the quoted tags
39
+ d = MakeTaggable.delimiter
40
+ # Separate multiple delimiters by bitwise operator
41
+ d = d.join("|") if d.is_a?(Array)
42
+ d
43
+ end
44
+
45
+ # ( # Tag start delimiter ($1)
46
+ # \A | # Either string start or
47
+ # #{delimiter} # a delimiter
48
+ # )
49
+ # \s*" # quote (") optionally preceded by whitespace
50
+ # (.*?) # Tag ($2)
51
+ # "\s* # quote (") optionally followed by whitespace
52
+ # (?= # Tag end delimiter (not consumed; is zero-length lookahead)
53
+ # #{delimiter}\s* | # Either a delimiter optionally followed by whitespace or
54
+ # \z # string end
55
+ # )
56
+ def double_quote_pattern
57
+ /(\A|#{delimiter})\s*"(.*?)"\s*(?=#{delimiter}\s*|\z)/
58
+ end
59
+
60
+ # ( # Tag start delimiter ($1)
61
+ # \A | # Either string start or
62
+ # #{delimiter} # a delimiter
63
+ # )
64
+ # \s*' # quote (') optionally preceded by whitespace
65
+ # (.*?) # Tag ($2)
66
+ # '\s* # quote (') optionally followed by whitespace
67
+ # (?= # Tag end delimiter (not consumed; is zero-length lookahead)
68
+ # #{delimiter}\s* | d # Either a delimiter optionally followed by whitespace or
69
+ # \z # string end
70
+ # )
71
+ def single_quote_pattern
72
+ /(\A|#{delimiter})\s*'(.*?)'\s*(?=#{delimiter}\s*|\z)/
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,4 @@
1
+ module MakeTaggable
2
+ class Engine < Rails::Engine
3
+ end
4
+ end
@@ -0,0 +1,19 @@
1
+ module MakeTaggable
2
+ ##
3
+ # Returns a new TagList using the given tag string.
4
+ #
5
+ # Example:
6
+ # tag_list = MakeTaggable::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
@@ -0,0 +1,131 @@
1
+ module MakeTaggable
2
+ class Tag < ::ActiveRecord::Base
3
+ self.table_name = MakeTaggable.tags_table
4
+
5
+ ### ASSOCIATIONS:
6
+ has_many :taggings, dependent: :destroy, class_name: "::MakeTaggable::Tagging"
7
+
8
+ ### VALIDATIONS:
9
+ validates_presence_of :name
10
+ validates_uniqueness_of :name, if: :validates_name_uniqueness?
11
+ validates_length_of :name, maximum: 255
12
+
13
+ # Monkey patch this method if don't need name uniqueness validation
14
+ def validates_name_uniqueness?
15
+ true
16
+ end
17
+
18
+ ### SCOPES:
19
+ scope :most_used, ->(limit = 20) { order("taggings_count desc").limit(limit) }
20
+ scope :least_used, ->(limit = 20) { order("taggings_count asc").limit(limit) }
21
+
22
+ def self.named(name)
23
+ if MakeTaggable.strict_case_match
24
+ where(["name = #{binary}?", as_8bit_ascii(name)])
25
+ else
26
+ where(["LOWER(name) = LOWER(?)", as_8bit_ascii(unicode_downcase(name))])
27
+ end
28
+ end
29
+
30
+ def self.named_any(list)
31
+ clause = list.map { |tag|
32
+ sanitize_sql_for_named_any(tag).force_encoding("BINARY")
33
+ }.join(" OR ")
34
+ where(clause)
35
+ end
36
+
37
+ def self.named_like(name)
38
+ clause = ["name #{MakeTaggable::Utils.like_operator} ? ESCAPE '!'", "%#{MakeTaggable::Utils.escape_like(name)}%"]
39
+ where(clause)
40
+ end
41
+
42
+ def self.named_like_any(list)
43
+ clause = list.map { |tag|
44
+ sanitize_sql(["name #{MakeTaggable::Utils.like_operator} ? ESCAPE '!'", "%#{MakeTaggable::Utils.escape_like(tag.to_s)}%"])
45
+ }.join(" OR ")
46
+ where(clause)
47
+ end
48
+
49
+ def self.for_context(context)
50
+ joins(:taggings)
51
+ .where(["#{MakeTaggable.taggings_table}.context = ?", context])
52
+ .select("DISTINCT #{MakeTaggable.tags_table}.*")
53
+ end
54
+
55
+ ### CLASS METHODS:
56
+ def self.find_or_create_with_like_by_name(name)
57
+ if MakeTaggable.strict_case_match
58
+ find_or_create_all_with_like_by_name([name]).first
59
+ else
60
+ named_like(name).first || create(name: name)
61
+ end
62
+ end
63
+
64
+ def self.find_or_create_all_with_like_by_name(*list)
65
+ list = Array(list).flatten
66
+
67
+ return [] if list.empty?
68
+
69
+ existing_tags = named_any(list)
70
+ list.map do |tag_name|
71
+ tries ||= 3
72
+ comparable_tag_name = comparable_name(tag_name)
73
+ existing_tag = existing_tags.find { |tag| comparable_name(tag.name) == comparable_tag_name }
74
+ existing_tag || create(name: tag_name)
75
+ rescue ActiveRecord::RecordNotUnique
76
+ if (tries -= 1).positive?
77
+ ActiveRecord::Base.connection.execute "ROLLBACK"
78
+ existing_tags = named_any(list)
79
+ retry
80
+ end
81
+
82
+ raise DuplicateTagError.new("'#{tag_name}' has already been taken")
83
+ end
84
+ end
85
+
86
+ ### INSTANCE METHODS:
87
+ def ==(other)
88
+ super || (other.is_a?(Tag) && name == other.name)
89
+ end
90
+
91
+ def to_s
92
+ name
93
+ end
94
+
95
+ def count
96
+ read_attribute(:count).to_i
97
+ end
98
+
99
+ class << self
100
+ private
101
+
102
+ def comparable_name(str)
103
+ if MakeTaggable.strict_case_match
104
+ str
105
+ else
106
+ unicode_downcase(str.to_s)
107
+ end
108
+ end
109
+
110
+ def binary
111
+ MakeTaggable::Utils.using_mysql? ? "BINARY " : nil
112
+ end
113
+
114
+ def as_8bit_ascii(string)
115
+ string.to_s.mb_chars
116
+ end
117
+
118
+ def unicode_downcase(string)
119
+ as_8bit_ascii(string).downcase
120
+ end
121
+
122
+ def sanitize_sql_for_named_any(tag)
123
+ if MakeTaggable.strict_case_match
124
+ sanitize_sql(["name = #{binary}?", as_8bit_ascii(tag)])
125
+ else
126
+ sanitize_sql(["LOWER(name) = LOWER(?)", as_8bit_ascii(unicode_downcase(tag))])
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end