make_taggable 0.6.3

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