nano-max-tool 0.0.1
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.
- checksums.yaml +7 -0
- data/acts-as-taggable-on-13.0.0/LICENSE.md +20 -0
- data/acts-as-taggable-on-13.0.0/db/migrate/1_acts_as_taggable_on_migration.rb +33 -0
- data/acts-as-taggable-on-13.0.0/db/migrate/2_add_missing_unique_indices.rb +23 -0
- data/acts-as-taggable-on-13.0.0/db/migrate/3_add_taggings_counter_cache_to_tags.rb +16 -0
- data/acts-as-taggable-on-13.0.0/db/migrate/4_add_missing_taggable_index.rb +12 -0
- data/acts-as-taggable-on-13.0.0/db/migrate/5_change_collation_for_tag_names.rb +12 -0
- data/acts-as-taggable-on-13.0.0/db/migrate/6_add_missing_indexes_on_taggings.rb +24 -0
- data/acts-as-taggable-on-13.0.0/db/migrate/7_add_tenant_to_taggings.rb +13 -0
- data/acts-as-taggable-on-13.0.0/lib/acts-as-taggable-on/default_parser.rb +77 -0
- data/acts-as-taggable-on-13.0.0/lib/acts-as-taggable-on/engine.rb +6 -0
- data/acts-as-taggable-on-13.0.0/lib/acts-as-taggable-on/generic_parser.rb +21 -0
- data/acts-as-taggable-on-13.0.0/lib/acts-as-taggable-on/tag.rb +138 -0
- data/acts-as-taggable-on-13.0.0/lib/acts-as-taggable-on/tag_list.rb +103 -0
- data/acts-as-taggable-on-13.0.0/lib/acts-as-taggable-on/taggable/caching.rb +46 -0
- data/acts-as-taggable-on-13.0.0/lib/acts-as-taggable-on/taggable/collection.rb +220 -0
- data/acts-as-taggable-on-13.0.0/lib/acts-as-taggable-on/taggable/core.rb +333 -0
- data/acts-as-taggable-on-13.0.0/lib/acts-as-taggable-on/taggable/ownership.rb +146 -0
- data/acts-as-taggable-on-13.0.0/lib/acts-as-taggable-on/taggable/related.rb +84 -0
- data/acts-as-taggable-on-13.0.0/lib/acts-as-taggable-on/taggable/tag_list_type.rb +8 -0
- data/acts-as-taggable-on-13.0.0/lib/acts-as-taggable-on/taggable/tagged_with_query/all_tags_query.rb +115 -0
- data/acts-as-taggable-on-13.0.0/lib/acts-as-taggable-on/taggable/tagged_with_query/any_tags_query.rb +74 -0
- data/acts-as-taggable-on-13.0.0/lib/acts-as-taggable-on/taggable/tagged_with_query/exclude_tags_query.rb +85 -0
- data/acts-as-taggable-on-13.0.0/lib/acts-as-taggable-on/taggable/tagged_with_query/query_base.rb +78 -0
- data/acts-as-taggable-on-13.0.0/lib/acts-as-taggable-on/taggable/tagged_with_query.rb +17 -0
- data/acts-as-taggable-on-13.0.0/lib/acts-as-taggable-on/taggable.rb +119 -0
- data/acts-as-taggable-on-13.0.0/lib/acts-as-taggable-on/tagger.rb +85 -0
- data/acts-as-taggable-on-13.0.0/lib/acts-as-taggable-on/tagging.rb +40 -0
- data/acts-as-taggable-on-13.0.0/lib/acts-as-taggable-on/tags_helper.rb +17 -0
- data/acts-as-taggable-on-13.0.0/lib/acts-as-taggable-on/utils.rb +35 -0
- data/acts-as-taggable-on-13.0.0/lib/acts-as-taggable-on/version.rb +5 -0
- data/acts-as-taggable-on-13.0.0/lib/acts-as-taggable-on.rb +118 -0
- data/acts-as-taggable-on-13.0.0/lib/tasks/example/acts-as-taggable-on.rb.example +8 -0
- data/acts-as-taggable-on-13.0.0/lib/tasks/install_initializer.rake +23 -0
- data/acts-as-taggable-on-13.0.0/lib/tasks/tags_collate_utf8.rake +21 -0
- data/nano-max-tool.gemspec +12 -0
- metadata +76 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 12be2e253491b0f5f52de412784a49a4674b2bd890de68f50e9a1674bbb5e7e4
|
|
4
|
+
data.tar.gz: d46a31f512e27ea5bec7e95ac95333673751ef71b491b1fda3d66176413eaec6
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 5b50a2916ee3034e5dc3ae9430c2ee4c1af07620be90475c1d8c8d1e8b18a8413c57c6718b6e7b7e90434477f6f51ba816a6f766d57d827e9325dc8bb34ae46a
|
|
7
|
+
data.tar.gz: f9a2dd2a81b54695b0591c7c1d960b642d982014f2117705eba4c4e67ae4dd307749deaaf8875f7337cb79b5cbbcde7ec48c99e66bd570a461ad88d0a84e83d4
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
__Copyright (c) 2007 Michael Bleigh and Intridea Inc.__
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class ActsAsTaggableOnMigration < ActiveRecord::Migration[6.0]
|
|
4
|
+
def self.up
|
|
5
|
+
create_table ActsAsTaggableOn.tags_table do |t|
|
|
6
|
+
t.string :name
|
|
7
|
+
t.timestamps
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
create_table ActsAsTaggableOn.taggings_table do |t|
|
|
11
|
+
t.references :tag, foreign_key: { to_table: ActsAsTaggableOn.tags_table }
|
|
12
|
+
|
|
13
|
+
# You should make sure that the column created is
|
|
14
|
+
# long enough to store the required class names.
|
|
15
|
+
t.references :taggable, polymorphic: true
|
|
16
|
+
t.references :tagger, polymorphic: true
|
|
17
|
+
|
|
18
|
+
# Limit is created to prevent MySQL error on index
|
|
19
|
+
# length for MyISAM table type: http://bit.ly/vgW2Ql
|
|
20
|
+
t.string :context, limit: 128
|
|
21
|
+
|
|
22
|
+
t.datetime :created_at
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
add_index ActsAsTaggableOn.taggings_table, %i[taggable_id taggable_type context],
|
|
26
|
+
name: 'taggings_taggable_context_idx'
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.down
|
|
30
|
+
drop_table ActsAsTaggableOn.taggings_table
|
|
31
|
+
drop_table ActsAsTaggableOn.tags_table
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AddMissingUniqueIndices < ActiveRecord::Migration[6.0]
|
|
4
|
+
def self.up
|
|
5
|
+
add_index ActsAsTaggableOn.tags_table, :name, unique: true
|
|
6
|
+
|
|
7
|
+
remove_index ActsAsTaggableOn.taggings_table, :tag_id if index_exists?(ActsAsTaggableOn.taggings_table, :tag_id)
|
|
8
|
+
remove_index ActsAsTaggableOn.taggings_table, name: 'taggings_taggable_context_idx'
|
|
9
|
+
add_index ActsAsTaggableOn.taggings_table,
|
|
10
|
+
%i[tag_id taggable_id taggable_type context tagger_id tagger_type],
|
|
11
|
+
unique: true, name: 'taggings_idx'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.down
|
|
15
|
+
remove_index ActsAsTaggableOn.tags_table, :name
|
|
16
|
+
|
|
17
|
+
remove_index ActsAsTaggableOn.taggings_table, name: 'taggings_idx'
|
|
18
|
+
|
|
19
|
+
add_index ActsAsTaggableOn.taggings_table, :tag_id unless index_exists?(ActsAsTaggableOn.taggings_table, :tag_id)
|
|
20
|
+
add_index ActsAsTaggableOn.taggings_table, %i[taggable_id taggable_type context],
|
|
21
|
+
name: 'taggings_taggable_context_idx'
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AddTaggingsCounterCacheToTags < ActiveRecord::Migration[6.0]
|
|
4
|
+
def self.up
|
|
5
|
+
add_column ActsAsTaggableOn.tags_table, :taggings_count, :integer, default: 0
|
|
6
|
+
|
|
7
|
+
ActsAsTaggableOn::Tag.reset_column_information
|
|
8
|
+
ActsAsTaggableOn::Tag.find_each do |tag|
|
|
9
|
+
ActsAsTaggableOn::Tag.reset_counters(tag.id, ActsAsTaggableOn.taggings_table)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.down
|
|
14
|
+
remove_column ActsAsTaggableOn.tags_table, :taggings_count
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AddMissingTaggableIndex < ActiveRecord::Migration[6.0]
|
|
4
|
+
def self.up
|
|
5
|
+
add_index ActsAsTaggableOn.taggings_table, %i[taggable_id taggable_type context],
|
|
6
|
+
name: 'taggings_taggable_context_idx'
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def self.down
|
|
10
|
+
remove_index ActsAsTaggableOn.taggings_table, name: 'taggings_taggable_context_idx'
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# This migration is added to circumvent issue #623 and have special characters
|
|
4
|
+
# work properly
|
|
5
|
+
|
|
6
|
+
class ChangeCollationForTagNames < ActiveRecord::Migration[6.0]
|
|
7
|
+
def up
|
|
8
|
+
if ActsAsTaggableOn::Utils.using_mysql?
|
|
9
|
+
execute("ALTER TABLE #{ActsAsTaggableOn.tags_table} MODIFY name varchar(255) CHARACTER SET utf8 COLLATE utf8_bin;")
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AddMissingIndexesOnTaggings < ActiveRecord::Migration[6.0]
|
|
4
|
+
def change
|
|
5
|
+
add_index ActsAsTaggableOn.taggings_table, :tag_id unless index_exists? ActsAsTaggableOn.taggings_table, :tag_id
|
|
6
|
+
add_index ActsAsTaggableOn.taggings_table, :taggable_id unless index_exists? ActsAsTaggableOn.taggings_table,
|
|
7
|
+
:taggable_id
|
|
8
|
+
add_index ActsAsTaggableOn.taggings_table, :taggable_type unless index_exists? ActsAsTaggableOn.taggings_table,
|
|
9
|
+
:taggable_type
|
|
10
|
+
add_index ActsAsTaggableOn.taggings_table, :tagger_id unless index_exists? ActsAsTaggableOn.taggings_table,
|
|
11
|
+
:tagger_id
|
|
12
|
+
add_index ActsAsTaggableOn.taggings_table, :context unless index_exists? ActsAsTaggableOn.taggings_table, :context
|
|
13
|
+
|
|
14
|
+
unless index_exists? ActsAsTaggableOn.taggings_table, %i[tagger_id tagger_type]
|
|
15
|
+
add_index ActsAsTaggableOn.taggings_table, %i[tagger_id tagger_type]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
unless index_exists? ActsAsTaggableOn.taggings_table, %i[taggable_id taggable_type tagger_id context],
|
|
19
|
+
name: 'taggings_idy'
|
|
20
|
+
add_index ActsAsTaggableOn.taggings_table, %i[taggable_id taggable_type tagger_id context],
|
|
21
|
+
name: 'taggings_idy'
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AddTenantToTaggings < ActiveRecord::Migration[6.0]
|
|
4
|
+
def self.up
|
|
5
|
+
add_column ActsAsTaggableOn.taggings_table, :tenant, :string, limit: 128
|
|
6
|
+
add_index ActsAsTaggableOn.taggings_table, :tenant unless index_exists? ActsAsTaggableOn.taggings_table, :tenant
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def self.down
|
|
10
|
+
remove_index ActsAsTaggableOn.taggings_table, :tenant
|
|
11
|
+
remove_column ActsAsTaggableOn.taggings_table, :tenant
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActsAsTaggableOn
|
|
4
|
+
##
|
|
5
|
+
# Returns a new TagList using the given tag string.
|
|
6
|
+
#
|
|
7
|
+
# Example:
|
|
8
|
+
# tag_list = ActsAsTaggableOn::DefaultParser.parse("One , Two, Three")
|
|
9
|
+
# tag_list # ["One", "Two", "Three"]
|
|
10
|
+
class DefaultParser < GenericParser
|
|
11
|
+
def parse
|
|
12
|
+
string = @tag_list
|
|
13
|
+
|
|
14
|
+
string = string.join(ActsAsTaggableOn.glue) if string.respond_to?(:join)
|
|
15
|
+
TagList.new.tap do |tag_list|
|
|
16
|
+
string = string.to_s.dup
|
|
17
|
+
|
|
18
|
+
string.gsub!(double_quote_pattern) do
|
|
19
|
+
# Append the matched tag to the tag list
|
|
20
|
+
tag_list << Regexp.last_match[2]
|
|
21
|
+
# Return the matched delimiter ($3) to replace the matched items
|
|
22
|
+
''
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
string.gsub!(single_quote_pattern) do
|
|
26
|
+
# Append the matched tag ($2) to the tag list
|
|
27
|
+
tag_list << Regexp.last_match[2]
|
|
28
|
+
# Return an empty string to replace the matched items
|
|
29
|
+
''
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# split the string by the delimiter
|
|
33
|
+
# and add to the tag_list
|
|
34
|
+
tag_list.add(string.split(Regexp.new(delimiter)))
|
|
35
|
+
end
|
|
36
|
+
end
|
|
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.is_a?(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
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActsAsTaggableOn
|
|
4
|
+
##
|
|
5
|
+
# Returns a new TagList using the given tag string.
|
|
6
|
+
#
|
|
7
|
+
# Example:
|
|
8
|
+
# tag_list = ActsAsTaggableOn::GenericParser.new.parse("One , Two, Three")
|
|
9
|
+
# tag_list # ["One", "Two", "Three"]
|
|
10
|
+
class GenericParser
|
|
11
|
+
def initialize(tag_list)
|
|
12
|
+
@tag_list = tag_list
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def parse
|
|
16
|
+
TagList.new.tap do |tag_list|
|
|
17
|
+
tag_list.add @tag_list.split(',').map(&:strip).reject(&:empty?)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActsAsTaggableOn
|
|
4
|
+
class Tag < ActsAsTaggableOn.base_class.constantize
|
|
5
|
+
self.table_name = ActsAsTaggableOn.tags_table
|
|
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?, case_sensitive: true
|
|
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}?", name.to_s])
|
|
29
|
+
else
|
|
30
|
+
where(['LOWER(name) = LOWER(?)', name.to_s.downcase])
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.named_any(list)
|
|
35
|
+
clause = list.map do |tag|
|
|
36
|
+
sanitize_sql_for_named_any(tag)
|
|
37
|
+
end.join(' OR ')
|
|
38
|
+
where(clause)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.named_like(name)
|
|
42
|
+
clause = ["name #{ActsAsTaggableOn::Utils.like_operator} ? ESCAPE '!'",
|
|
43
|
+
"%#{ActsAsTaggableOn::Utils.escape_like(name)}%"]
|
|
44
|
+
where(clause)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.named_like_any(list)
|
|
48
|
+
clause = list.map do |tag|
|
|
49
|
+
sanitize_sql(["name #{ActsAsTaggableOn::Utils.like_operator} ? ESCAPE '!'",
|
|
50
|
+
"%#{ActsAsTaggableOn::Utils.escape_like(tag.to_s)}%"])
|
|
51
|
+
end.join(' OR ')
|
|
52
|
+
where(clause)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.for_context(context)
|
|
56
|
+
joins(:taggings)
|
|
57
|
+
.where(["#{ActsAsTaggableOn.taggings_table}.context = ?", context])
|
|
58
|
+
.select("DISTINCT #{ActsAsTaggableOn.tags_table}.*")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.for_tenant(tenant)
|
|
62
|
+
joins(:taggings)
|
|
63
|
+
.where("#{ActsAsTaggableOn.taggings_table}.tenant = ?", tenant.to_s)
|
|
64
|
+
.select("DISTINCT #{ActsAsTaggableOn.tags_table}.*")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
### CLASS METHODS:
|
|
68
|
+
|
|
69
|
+
def self.find_or_create_with_like_by_name(name)
|
|
70
|
+
if ActsAsTaggableOn.strict_case_match
|
|
71
|
+
find_or_create_all_with_like_by_name([name]).first
|
|
72
|
+
else
|
|
73
|
+
named_like(name).first || create(name: name)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def self.find_or_create_all_with_like_by_name(*list)
|
|
78
|
+
list = Array(list).flatten
|
|
79
|
+
|
|
80
|
+
return [] if list.empty?
|
|
81
|
+
|
|
82
|
+
existing_tags = named_any(list)
|
|
83
|
+
list.map do |tag_name|
|
|
84
|
+
tries ||= 3
|
|
85
|
+
comparable_tag_name = comparable_name(tag_name)
|
|
86
|
+
existing_tag = existing_tags.find { |tag| comparable_name(tag.name) == comparable_tag_name }
|
|
87
|
+
next existing_tag if existing_tag
|
|
88
|
+
|
|
89
|
+
transaction(requires_new: true) { create(name: tag_name) }
|
|
90
|
+
rescue ActiveRecord::RecordNotUnique
|
|
91
|
+
if (tries -= 1).positive?
|
|
92
|
+
existing_tags = named_any(list)
|
|
93
|
+
retry
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
raise DuplicateTagError, "'#{tag_name}' has already been taken"
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
### INSTANCE METHODS:
|
|
101
|
+
|
|
102
|
+
def ==(other)
|
|
103
|
+
super || (other.is_a?(Tag) && name == other.name)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def to_s
|
|
107
|
+
name
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def count
|
|
111
|
+
read_attribute(:count).to_i
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
class << self
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
def comparable_name(str)
|
|
118
|
+
if ActsAsTaggableOn.strict_case_match
|
|
119
|
+
str
|
|
120
|
+
else
|
|
121
|
+
str.to_s.downcase
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def binary
|
|
126
|
+
ActsAsTaggableOn::Utils.using_mysql? ? 'BINARY ' : nil
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def sanitize_sql_for_named_any(tag)
|
|
130
|
+
if ActsAsTaggableOn.strict_case_match
|
|
131
|
+
sanitize_sql(["name = #{binary}?", tag.to_s])
|
|
132
|
+
else
|
|
133
|
+
sanitize_sql(['LOWER(name) = LOWER(?)', tag.to_s.downcase])
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support/core_ext/module/delegation'
|
|
4
|
+
|
|
5
|
+
module ActsAsTaggableOn
|
|
6
|
+
class TagList < Array
|
|
7
|
+
attr_accessor :owner, :parser
|
|
8
|
+
|
|
9
|
+
def initialize(*args)
|
|
10
|
+
@parser = ActsAsTaggableOn.default_parser
|
|
11
|
+
add(*args)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
##
|
|
15
|
+
# Add tags to the tag_list. Duplicate or blank tags will be ignored.
|
|
16
|
+
# Use the <tt>:parse</tt> option to add an unparsed tag string.
|
|
17
|
+
#
|
|
18
|
+
# Example:
|
|
19
|
+
# tag_list.add("Fun", "Happy")
|
|
20
|
+
# tag_list.add("Fun, Happy", :parse => true)
|
|
21
|
+
def add(*names)
|
|
22
|
+
extract_and_apply_options!(names)
|
|
23
|
+
concat(names)
|
|
24
|
+
clean!
|
|
25
|
+
self
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Append---Add the tag to the tag_list. This
|
|
29
|
+
# expression returns the tag_list itself, so several appends
|
|
30
|
+
# may be chained together.
|
|
31
|
+
def <<(obj)
|
|
32
|
+
add(obj)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Concatenation --- Returns a new tag list built by concatenating the
|
|
36
|
+
# two tag lists together to produce a third tag list.
|
|
37
|
+
def +(other)
|
|
38
|
+
TagList.new.add(self).add(other)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Appends the elements of +other_tag_list+ to +self+.
|
|
42
|
+
def concat(other_tag_list)
|
|
43
|
+
super(other_tag_list).send(:clean!)
|
|
44
|
+
self
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
##
|
|
48
|
+
# Remove specific tags from the tag_list.
|
|
49
|
+
# Use the <tt>:parse</tt> option to add an unparsed tag string.
|
|
50
|
+
#
|
|
51
|
+
# Example:
|
|
52
|
+
# tag_list.remove("Sad", "Lonely")
|
|
53
|
+
# tag_list.remove("Sad, Lonely", :parse => true)
|
|
54
|
+
def remove(*names)
|
|
55
|
+
extract_and_apply_options!(names)
|
|
56
|
+
delete_if { |name| names.include?(name) }
|
|
57
|
+
self
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
##
|
|
61
|
+
# Transform the tag_list into a tag string suitable for editing in a form.
|
|
62
|
+
# The tags are joined with <tt>TagList.delimiter</tt> and quoted if necessary.
|
|
63
|
+
#
|
|
64
|
+
# Example:
|
|
65
|
+
# tag_list = TagList.new("Round", "Square,Cube")
|
|
66
|
+
# tag_list.to_s # 'Round, "Square,Cube"'
|
|
67
|
+
def to_s
|
|
68
|
+
tags = frozen? ? dup : self
|
|
69
|
+
tags.send(:clean!)
|
|
70
|
+
|
|
71
|
+
tags.map do |name|
|
|
72
|
+
d = ActsAsTaggableOn.delimiter
|
|
73
|
+
d = Regexp.new d.join('|') if d.is_a? Array
|
|
74
|
+
name.index(d) ? "\"#{name}\"" : name
|
|
75
|
+
end.join(ActsAsTaggableOn.glue)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
# Convert everything to string, remove whitespace, duplicates, and blanks.
|
|
81
|
+
def clean!
|
|
82
|
+
reject!(&:blank?)
|
|
83
|
+
map!(&:to_s)
|
|
84
|
+
map!(&:strip)
|
|
85
|
+
map! { |tag| tag.to_s.downcase } if ActsAsTaggableOn.force_lowercase
|
|
86
|
+
map!(&:parameterize) if ActsAsTaggableOn.force_parameterize
|
|
87
|
+
|
|
88
|
+
ActsAsTaggableOn.strict_case_match ? uniq! : uniq!(&:downcase)
|
|
89
|
+
self
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def extract_and_apply_options!(args)
|
|
93
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
|
94
|
+
options.assert_valid_keys :parse, :parser
|
|
95
|
+
|
|
96
|
+
parser = options[:parser] || @parser
|
|
97
|
+
|
|
98
|
+
args.map! { |a| parser.new(a).parse } if options[:parse] || options[:parser]
|
|
99
|
+
|
|
100
|
+
args.flatten!
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActsAsTaggableOn
|
|
4
|
+
module Taggable
|
|
5
|
+
module Caching
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
included do
|
|
9
|
+
initialize_tags_cache
|
|
10
|
+
before_save :save_cached_tag_list
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
class_methods do
|
|
14
|
+
def initialize_tags_cache
|
|
15
|
+
tag_types.map(&:to_s).each do |tag_type|
|
|
16
|
+
define_singleton_method("caching_#{tag_type.singularize}_list?") do
|
|
17
|
+
caching_tag_list_on?(tag_type)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def acts_as_taggable_on(*args)
|
|
23
|
+
super(*args)
|
|
24
|
+
initialize_tags_cache
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def caching_tag_list_on?(context)
|
|
28
|
+
column_names.include?("cached_#{context.to_s.singularize}_list")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def save_cached_tag_list
|
|
33
|
+
tag_types.map(&:to_s).each do |tag_type|
|
|
34
|
+
next unless self.class.respond_to?("caching_#{tag_type.singularize}_list?")
|
|
35
|
+
if self.class.send("caching_#{tag_type.singularize}_list?") && tag_list_cache_set_on(tag_type)
|
|
36
|
+
list = tag_list_cache_on(tag_type).to_a.flatten.compact.join("#{ActsAsTaggableOn.delimiter} ")
|
|
37
|
+
self["cached_#{tag_type.singularize}_list"] = list
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
true
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|