make_taggable 0.6.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +47 -0
- data/.gitignore +13 -0
- data/.rspec +3 -0
- data/.standard.yml +18 -0
- data/.standard_todo.yml +5 -0
- data/.travis.yml +36 -0
- data/Appraisals +11 -0
- data/CHANGELOG.md +0 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/CONTRIBUTING.md +57 -0
- data/Gemfile +16 -0
- data/LICENSE.md +20 -0
- data/LICENSE.txt +21 -0
- data/README.md +478 -0
- data/Rakefile +7 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/db/migrate/1_create_make_taggable_tags.rb +10 -0
- data/db/migrate/2_create_make_taggable_taggings.rb +12 -0
- data/db/migrate/3_add_index_to_tags.rb +5 -0
- data/db/migrate/4_add_index_to_taggings.rb +12 -0
- data/gemfiles/rails_5.gemfile +9 -0
- data/gemfiles/rails_6.gemfile +9 -0
- data/gemfiles/rails_master.gemfile +9 -0
- data/lib/make_taggable.rb +134 -0
- data/lib/make_taggable/default_parser.rb +75 -0
- data/lib/make_taggable/engine.rb +4 -0
- data/lib/make_taggable/generic_parser.rb +19 -0
- data/lib/make_taggable/tag.rb +131 -0
- data/lib/make_taggable/tag_list.rb +102 -0
- data/lib/make_taggable/taggable.rb +100 -0
- data/lib/make_taggable/taggable/cache.rb +90 -0
- data/lib/make_taggable/taggable/collection.rb +183 -0
- data/lib/make_taggable/taggable/core.rb +323 -0
- data/lib/make_taggable/taggable/ownership.rb +137 -0
- data/lib/make_taggable/taggable/related.rb +71 -0
- data/lib/make_taggable/taggable/tag_list_type.rb +4 -0
- data/lib/make_taggable/taggable/tagged_with_query.rb +16 -0
- data/lib/make_taggable/taggable/tagged_with_query/all_tags_query.rb +111 -0
- data/lib/make_taggable/taggable/tagged_with_query/any_tags_query.rb +68 -0
- data/lib/make_taggable/taggable/tagged_with_query/exclude_tags_query.rb +81 -0
- data/lib/make_taggable/taggable/tagged_with_query/query_base.rb +61 -0
- data/lib/make_taggable/tagger.rb +89 -0
- data/lib/make_taggable/tagging.rb +32 -0
- data/lib/make_taggable/tags_helper.rb +15 -0
- data/lib/make_taggable/utils.rb +34 -0
- data/lib/make_taggable/version.rb +4 -0
- data/lib/tasks/tags_collate_utf8.rake +17 -0
- data/make_taggable.gemspec +26 -0
- data/spec/dummy/README.md +24 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/app/assets/config/manifest.js +2 -0
- data/spec/dummy/app/channels/application_cable/channel.rb +4 -0
- data/spec/dummy/app/channels/application_cable/connection.rb +4 -0
- data/spec/dummy/app/controllers/application_controller.rb +2 -0
- data/spec/dummy/app/controllers/concerns/.keep +0 -0
- data/spec/dummy/app/jobs/application_job.rb +7 -0
- data/spec/dummy/app/mailers/application_mailer.rb +4 -0
- data/spec/dummy/app/models/altered_inheriting_taggable_model.rb +5 -0
- data/spec/dummy/app/models/application_record.rb +3 -0
- data/spec/dummy/app/models/cached_model.rb +3 -0
- data/spec/dummy/app/models/cached_model_with_array.rb +11 -0
- data/spec/dummy/app/models/columns_override_model.rb +5 -0
- data/spec/dummy/app/models/company.rb +15 -0
- data/spec/dummy/app/models/concerns/.keep +0 -0
- data/spec/dummy/app/models/inheriting_taggable_model.rb +4 -0
- data/spec/dummy/app/models/market.rb +2 -0
- data/spec/dummy/app/models/non_standard_id_taggable_model.rb +8 -0
- data/spec/dummy/app/models/ordered_taggable_model.rb +4 -0
- data/spec/dummy/app/models/other_cached_model.rb +3 -0
- data/spec/dummy/app/models/other_taggable_model.rb +4 -0
- data/spec/dummy/app/models/student.rb +4 -0
- data/spec/dummy/app/models/taggable_model.rb +14 -0
- data/spec/dummy/app/models/untaggable_model.rb +3 -0
- data/spec/dummy/app/models/user.rb +3 -0
- data/spec/dummy/app/views/layouts/mailer.html.erb +13 -0
- data/spec/dummy/app/views/layouts/mailer.text.erb +1 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/bin/setup +33 -0
- data/spec/dummy/config.ru +5 -0
- data/spec/dummy/config/application.rb +19 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/cable.yml +10 -0
- data/spec/dummy/config/credentials.yml.enc +1 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +52 -0
- data/spec/dummy/config/environments/production.rb +105 -0
- data/spec/dummy/config/environments/test.rb +49 -0
- data/spec/dummy/config/initializers/application_controller_renderer.rb +8 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/cors.rb +16 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/inflections.rb +16 -0
- data/spec/dummy/config/initializers/mime_types.rb +4 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +33 -0
- data/spec/dummy/config/master.key +1 -0
- data/spec/dummy/config/puma.rb +38 -0
- data/spec/dummy/config/routes.rb +3 -0
- data/spec/dummy/config/spring.rb +6 -0
- data/spec/dummy/config/storage.yml +34 -0
- data/spec/dummy/db/migrate/20201119220853_create_taggable_models.rb +8 -0
- data/spec/dummy/db/migrate/20201119221037_create_columns_override_models.rb +9 -0
- data/spec/dummy/db/migrate/20201119221121_create_non_standard_id_taggable_models.rb +8 -0
- data/spec/dummy/db/migrate/20201119221228_create_untaggable_models.rb +8 -0
- data/spec/dummy/db/migrate/20201119221247_create_cached_models.rb +9 -0
- data/spec/dummy/db/migrate/20201119221314_create_other_cached_models.rb +11 -0
- data/spec/dummy/db/migrate/20201119221343_create_companies.rb +7 -0
- data/spec/dummy/db/migrate/20201119221416_create_users.rb +7 -0
- data/spec/dummy/db/migrate/20201119221434_create_other_taggable_models.rb +8 -0
- data/spec/dummy/db/migrate/20201119221507_create_ordered_taggable_models.rb +8 -0
- data/spec/dummy/db/migrate/20201119221530_create_cache_methods_injected_models.rb +7 -0
- data/spec/dummy/db/migrate/20201119221629_create_other_cached_with_array_models.rb +11 -0
- data/spec/dummy/db/migrate/20201119221746_create_taggable_model_with_jsons.rb +9 -0
- data/spec/dummy/db/migrate/20201119222429_create_make_taggable_tags.make_taggable_engine.rb +11 -0
- data/spec/dummy/db/migrate/20201119222430_create_make_taggable_taggings.make_taggable_engine.rb +13 -0
- data/spec/dummy/db/migrate/20201119222431_add_index_to_tags.make_taggable_engine.rb +6 -0
- data/spec/dummy/db/migrate/20201119222432_add_index_to_taggings.make_taggable_engine.rb +13 -0
- data/spec/dummy/db/schema.rb +117 -0
- data/spec/dummy/db/seeds.rb +7 -0
- data/spec/dummy/lib/tasks/.keep +0 -0
- data/spec/dummy/log/.keep +0 -0
- data/spec/dummy/public/robots.txt +1 -0
- data/spec/dummy/storage/.keep +0 -0
- data/spec/dummy/test/channels/application_cable/connection_test.rb +11 -0
- data/spec/dummy/test/controllers/.keep +0 -0
- data/spec/dummy/test/fixtures/.keep +0 -0
- data/spec/dummy/test/fixtures/files/.keep +0 -0
- data/spec/dummy/test/integration/.keep +0 -0
- data/spec/dummy/test/mailers/.keep +0 -0
- data/spec/dummy/test/models/.keep +0 -0
- data/spec/dummy/test/test_helper.rb +13 -0
- data/spec/dummy/vendor/.keep +0 -0
- data/spec/make_taggable/acts_as_tagger_spec.rb +112 -0
- data/spec/make_taggable/caching_spec.rb +123 -0
- data/spec/make_taggable/default_parser_spec.rb +45 -0
- data/spec/make_taggable/dirty_spec.rb +140 -0
- data/spec/make_taggable/generic_parser_spec.rb +13 -0
- data/spec/make_taggable/make_taggable_spec.rb +260 -0
- data/spec/make_taggable/related_spec.rb +93 -0
- data/spec/make_taggable/single_table_inheritance_spec.rb +220 -0
- data/spec/make_taggable/tag_list_spec.rb +169 -0
- data/spec/make_taggable/tag_spec.rb +297 -0
- data/spec/make_taggable/taggable_spec.rb +804 -0
- data/spec/make_taggable/tagger_spec.rb +149 -0
- data/spec/make_taggable/tagging_spec.rb +115 -0
- data/spec/make_taggable/tags_helper_spec.rb +43 -0
- data/spec/make_taggable/utils_spec.rb +22 -0
- data/spec/make_taggable_spec.rb +5 -0
- data/spec/spec_helper.rb +18 -0
- data/spec/support/array.rb +9 -0
- data/spec/support/helpers.rb +31 -0
- metadata +391 -0
data/Rakefile
ADDED
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,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,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,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,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
|