acts-as-taggable-on 4.0.0 → 8.0.0
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 +5 -5
- data/.github/workflows/spec.yml +95 -0
- data/.gitignore +1 -1
- data/Appraisals +12 -10
- data/CHANGELOG.md +206 -71
- data/CONTRIBUTING.md +13 -0
- data/Gemfile +1 -1
- data/README.md +79 -13
- data/acts-as-taggable-on.gemspec +2 -6
- data/db/migrate/1_acts_as_taggable_on_migration.rb +14 -8
- data/db/migrate/2_add_missing_unique_indices.rb +14 -9
- data/db/migrate/3_add_taggings_counter_cache_to_tags.rb +9 -4
- data/db/migrate/4_add_missing_taggable_index.rb +8 -3
- data/db/migrate/5_change_collation_for_tag_names.rb +7 -2
- data/db/migrate/6_add_missing_indexes_on_taggings.rb +22 -0
- data/db/migrate/7_add_tenant_to_taggings.rb +16 -0
- data/gemfiles/activerecord_5.0.gemfile +11 -5
- data/gemfiles/activerecord_5.1.gemfile +21 -0
- data/gemfiles/activerecord_5.2.gemfile +21 -0
- data/gemfiles/activerecord_6.0.gemfile +21 -0
- data/gemfiles/activerecord_6.1.gemfile +23 -0
- data/lib/acts-as-taggable-on.rb +6 -2
- data/lib/acts_as_taggable_on/tag.rb +23 -23
- data/lib/acts_as_taggable_on/tag_list.rb +1 -0
- data/lib/acts_as_taggable_on/taggable.rb +18 -1
- data/lib/acts_as_taggable_on/taggable/cache.rb +38 -34
- data/lib/acts_as_taggable_on/taggable/collection.rb +9 -7
- data/lib/acts_as_taggable_on/taggable/core.rb +49 -179
- data/lib/acts_as_taggable_on/taggable/ownership.rb +16 -5
- data/lib/acts_as_taggable_on/taggable/related.rb +1 -1
- data/lib/acts_as_taggable_on/taggable/tag_list_type.rb +4 -0
- data/lib/acts_as_taggable_on/taggable/tagged_with_query.rb +16 -0
- data/lib/acts_as_taggable_on/taggable/tagged_with_query/all_tags_query.rb +111 -0
- data/lib/acts_as_taggable_on/taggable/tagged_with_query/any_tags_query.rb +70 -0
- data/lib/acts_as_taggable_on/taggable/tagged_with_query/exclude_tags_query.rb +82 -0
- data/lib/acts_as_taggable_on/taggable/tagged_with_query/query_base.rb +61 -0
- data/lib/acts_as_taggable_on/tagger.rb +3 -3
- data/lib/acts_as_taggable_on/tagging.rb +9 -4
- data/lib/acts_as_taggable_on/utils.rb +4 -4
- data/lib/acts_as_taggable_on/version.rb +1 -2
- data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +4 -12
- data/spec/acts_as_taggable_on/caching_spec.rb +34 -10
- data/spec/acts_as_taggable_on/{taggable/dirty_spec.rb → dirty_spec.rb} +28 -13
- data/spec/acts_as_taggable_on/single_table_inheritance_spec.rb +28 -8
- data/spec/acts_as_taggable_on/tag_spec.rb +16 -1
- data/spec/acts_as_taggable_on/taggable_spec.rb +22 -15
- data/spec/acts_as_taggable_on/tagger_spec.rb +2 -2
- data/spec/acts_as_taggable_on/tagging_spec.rb +26 -0
- data/spec/internal/app/models/altered_inheriting_taggable_model.rb +2 -0
- data/spec/internal/app/models/cached_model_with_array.rb +6 -0
- data/spec/internal/app/models/columns_override_model.rb +5 -0
- data/spec/internal/app/models/company.rb +1 -1
- data/spec/internal/app/models/inheriting_taggable_model.rb +2 -0
- data/spec/internal/app/models/market.rb +1 -1
- data/spec/internal/app/models/non_standard_id_taggable_model.rb +1 -1
- data/spec/internal/app/models/student.rb +2 -0
- data/spec/internal/app/models/taggable_model.rb +3 -0
- data/spec/internal/app/models/user.rb +1 -1
- data/spec/internal/config/database.yml.sample +4 -8
- data/spec/internal/db/schema.rb +17 -5
- data/spec/spec_helper.rb +0 -1
- data/spec/support/database.rb +4 -4
- metadata +29 -68
- data/.travis.yml +0 -36
- data/UPGRADING.md +0 -8
- data/db/migrate/6_add_missing_indexes.rb +0 -12
- data/gemfiles/activerecord_4.0.gemfile +0 -16
- data/gemfiles/activerecord_4.1.gemfile +0 -16
- data/gemfiles/activerecord_4.2.gemfile +0 -15
- data/lib/acts_as_taggable_on/taggable/dirty.rb +0 -36
- data/spec/internal/app/models/models.rb +0 -90
@@ -0,0 +1,21 @@
|
|
1
|
+
source "https://rubygems.org"
|
2
|
+
|
3
|
+
gem "activerecord", "~> 5.2.0"
|
4
|
+
case ENV["DB"]
|
5
|
+
when "postgresql"
|
6
|
+
gem 'pg'
|
7
|
+
when "mysql"
|
8
|
+
gem 'mysql2', '~> 0.3'
|
9
|
+
else
|
10
|
+
gem "sqlite3", "~> 1.3", "< 1.4"
|
11
|
+
end
|
12
|
+
|
13
|
+
group :local_development do
|
14
|
+
gem "guard"
|
15
|
+
gem "guard-rspec"
|
16
|
+
gem "appraisal"
|
17
|
+
gem "rake"
|
18
|
+
gem "byebug", platforms: [:mri]
|
19
|
+
end
|
20
|
+
|
21
|
+
gemspec path: "../"
|
@@ -0,0 +1,21 @@
|
|
1
|
+
source "https://rubygems.org"
|
2
|
+
|
3
|
+
gem "activerecord", "~> 6.0.0"
|
4
|
+
case ENV["DB"]
|
5
|
+
when "postgresql"
|
6
|
+
gem 'pg'
|
7
|
+
when "mysql"
|
8
|
+
gem 'mysql2', '~> 0.4'
|
9
|
+
else
|
10
|
+
gem 'sqlite3'
|
11
|
+
end
|
12
|
+
|
13
|
+
group :local_development do
|
14
|
+
gem "guard"
|
15
|
+
gem "guard-rspec"
|
16
|
+
gem "appraisal"
|
17
|
+
gem "rake"
|
18
|
+
gem "byebug", platforms: [:mri]
|
19
|
+
end
|
20
|
+
|
21
|
+
gemspec path: "../"
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# This file was generated by Appraisal
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
gem "activerecord", "~> 6.1.0"
|
6
|
+
case ENV["DB"]
|
7
|
+
when "postgresql"
|
8
|
+
gem 'pg'
|
9
|
+
when "mysql"
|
10
|
+
gem 'mysql2', '~> 0.5'
|
11
|
+
else
|
12
|
+
gem 'sqlite3'
|
13
|
+
end
|
14
|
+
|
15
|
+
group :local_development do
|
16
|
+
gem "guard"
|
17
|
+
gem "guard-rspec"
|
18
|
+
gem "appraisal"
|
19
|
+
gem "rake"
|
20
|
+
gem "byebug", platforms: [:mri]
|
21
|
+
end
|
22
|
+
|
23
|
+
gemspec path: "../"
|
data/lib/acts-as-taggable-on.rb
CHANGED
@@ -31,6 +31,7 @@ module ActsAsTaggableOn
|
|
31
31
|
autoload :Dirty
|
32
32
|
autoload :Ownership
|
33
33
|
autoload :Related
|
34
|
+
autoload :TagListType
|
34
35
|
end
|
35
36
|
|
36
37
|
autoload :Utils
|
@@ -57,13 +58,14 @@ module ActsAsTaggableOn
|
|
57
58
|
def self.glue
|
58
59
|
setting = @configuration.delimiter
|
59
60
|
delimiter = setting.kind_of?(Array) ? setting[0] : setting
|
60
|
-
delimiter.
|
61
|
+
delimiter.end_with?(' ') ? delimiter : "#{delimiter} "
|
61
62
|
end
|
62
63
|
|
63
64
|
class Configuration
|
64
65
|
attr_accessor :force_lowercase, :force_parameterize,
|
65
66
|
:remove_unused_tags, :default_parser,
|
66
|
-
:tags_counter
|
67
|
+
:tags_counter, :tags_table,
|
68
|
+
:taggings_table
|
67
69
|
attr_reader :delimiter, :strict_case_match
|
68
70
|
|
69
71
|
def initialize
|
@@ -75,6 +77,8 @@ module ActsAsTaggableOn
|
|
75
77
|
@tags_counter = true
|
76
78
|
@default_parser = DefaultParser
|
77
79
|
@force_binary_collation = false
|
80
|
+
@tags_table = :tags
|
81
|
+
@taggings_table = :taggings
|
78
82
|
end
|
79
83
|
|
80
84
|
def strict_case_match=(force_cs)
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
module ActsAsTaggableOn
|
3
3
|
class Tag < ::ActiveRecord::Base
|
4
|
+
self.table_name = ActsAsTaggableOn.tags_table
|
4
5
|
|
5
6
|
### ASSOCIATIONS:
|
6
7
|
|
@@ -9,7 +10,7 @@ module ActsAsTaggableOn
|
|
9
10
|
### VALIDATIONS:
|
10
11
|
|
11
12
|
validates_presence_of :name
|
12
|
-
validates_uniqueness_of :name, if: :validates_name_uniqueness
|
13
|
+
validates_uniqueness_of :name, if: :validates_name_uniqueness?, case_sensitive: true
|
13
14
|
validates_length_of :name, maximum: 255
|
14
15
|
|
15
16
|
# monkey patch this method if don't need name uniqueness validation
|
@@ -50,8 +51,14 @@ module ActsAsTaggableOn
|
|
50
51
|
|
51
52
|
def self.for_context(context)
|
52
53
|
joins(:taggings).
|
53
|
-
where(["
|
54
|
-
select("DISTINCT
|
54
|
+
where(["#{ActsAsTaggableOn.taggings_table}.context = ?", context]).
|
55
|
+
select("DISTINCT #{ActsAsTaggableOn.tags_table}.*")
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.for_tenant(tenant)
|
59
|
+
joins(:taggings).
|
60
|
+
where("#{ActsAsTaggableOn.taggings_table}.tenant = ?", tenant.to_s).
|
61
|
+
select("DISTINCT #{ActsAsTaggableOn.tags_table}.*")
|
55
62
|
end
|
56
63
|
|
57
64
|
### CLASS METHODS:
|
@@ -70,16 +77,19 @@ module ActsAsTaggableOn
|
|
70
77
|
return [] if list.empty?
|
71
78
|
|
72
79
|
existing_tags = named_any(list)
|
73
|
-
|
74
80
|
list.map do |tag_name|
|
75
|
-
comparable_tag_name = comparable_name(tag_name)
|
76
|
-
existing_tag = existing_tags.find { |tag| comparable_name(tag.name) == comparable_tag_name }
|
77
81
|
begin
|
82
|
+
tries ||= 3
|
83
|
+
comparable_tag_name = comparable_name(tag_name)
|
84
|
+
existing_tag = existing_tags.find { |tag| comparable_name(tag.name) == comparable_tag_name }
|
78
85
|
existing_tag || create(name: tag_name)
|
79
86
|
rescue ActiveRecord::RecordNotUnique
|
80
|
-
|
81
|
-
|
82
|
-
|
87
|
+
if (tries -= 1).positive?
|
88
|
+
ActiveRecord::Base.connection.execute 'ROLLBACK'
|
89
|
+
existing_tags = named_any(list)
|
90
|
+
retry
|
91
|
+
end
|
92
|
+
|
83
93
|
raise DuplicateTagError.new("'#{tag_name}' has already been taken")
|
84
94
|
end
|
85
95
|
end
|
@@ -101,8 +111,6 @@ module ActsAsTaggableOn
|
|
101
111
|
|
102
112
|
class << self
|
103
113
|
|
104
|
-
|
105
|
-
|
106
114
|
private
|
107
115
|
|
108
116
|
def comparable_name(str)
|
@@ -117,20 +125,12 @@ module ActsAsTaggableOn
|
|
117
125
|
ActsAsTaggableOn::Utils.using_mysql? ? 'BINARY ' : nil
|
118
126
|
end
|
119
127
|
|
120
|
-
def
|
121
|
-
|
122
|
-
ActiveSupport::Multibyte::Unicode.downcase(string)
|
123
|
-
else
|
124
|
-
ActiveSupport::Multibyte::Chars.new(string).downcase.to_s
|
125
|
-
end
|
128
|
+
def as_8bit_ascii(string)
|
129
|
+
string.to_s.mb_chars
|
126
130
|
end
|
127
131
|
|
128
|
-
def
|
129
|
-
|
130
|
-
string.to_s.dup.force_encoding('BINARY')
|
131
|
-
else
|
132
|
-
string.to_s.mb_chars
|
133
|
-
end
|
132
|
+
def unicode_downcase(string)
|
133
|
+
as_8bit_ascii(string).downcase
|
134
134
|
end
|
135
135
|
|
136
136
|
def sanitize_sql_for_named_any(tag)
|
@@ -54,6 +54,23 @@ module ActsAsTaggableOn
|
|
54
54
|
taggable_on(true, tag_types)
|
55
55
|
end
|
56
56
|
|
57
|
+
def acts_as_taggable_tenant(tenant)
|
58
|
+
if taggable?
|
59
|
+
self.tenant_column = tenant
|
60
|
+
else
|
61
|
+
class_attribute :tenant_column
|
62
|
+
self.tenant_column = tenant
|
63
|
+
end
|
64
|
+
|
65
|
+
# each of these add context-specific methods and must be
|
66
|
+
# called on each call of taggable_on
|
67
|
+
include Core
|
68
|
+
include Collection
|
69
|
+
include Cache
|
70
|
+
include Ownership
|
71
|
+
include Related
|
72
|
+
end
|
73
|
+
|
57
74
|
private
|
58
75
|
|
59
76
|
# Make a model taggable on specified contexts
|
@@ -78,6 +95,7 @@ module ActsAsTaggableOn
|
|
78
95
|
self.tag_types = tag_types
|
79
96
|
class_attribute :preserve_tag_order
|
80
97
|
self.preserve_tag_order = preserve_tag_order
|
98
|
+
class_attribute :tenant_column
|
81
99
|
|
82
100
|
class_eval do
|
83
101
|
has_many :taggings, as: :taggable, dependent: :destroy, class_name: '::ActsAsTaggableOn::Tagging'
|
@@ -96,7 +114,6 @@ module ActsAsTaggableOn
|
|
96
114
|
include Cache
|
97
115
|
include Ownership
|
98
116
|
include Related
|
99
|
-
include Dirty
|
100
117
|
end
|
101
118
|
end
|
102
119
|
end
|
@@ -3,47 +3,51 @@ module ActsAsTaggableOn::Taggable
|
|
3
3
|
def self.included(base)
|
4
4
|
# When included, conditionally adds tag caching methods when the model
|
5
5
|
# has any "cached_#{tag_type}_list" column
|
6
|
-
base.
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
6
|
+
base.extend Columns
|
7
|
+
end
|
8
|
+
|
9
|
+
module Columns
|
10
|
+
# ActiveRecord::Base.columns makes a database connection and caches the
|
11
|
+
# calculated columns hash for the record as @columns. Since we don't
|
12
|
+
# want to add caching methods until we confirm the presence of a
|
13
|
+
# caching column, and we don't want to force opening a database
|
14
|
+
# connection when the class is loaded, here we intercept and cache
|
15
|
+
# the call to :columns as @acts_as_taggable_on_cache_columns
|
16
|
+
# to mimic the underlying behavior. While processing this first
|
17
|
+
# call to columns, we do the caching column check and dynamically add
|
18
|
+
# the class and instance methods
|
19
|
+
# FIXME: this method cannot compile in rubinius
|
20
|
+
def columns
|
21
|
+
@acts_as_taggable_on_cache_columns ||= begin
|
22
|
+
db_columns = super
|
23
|
+
_add_tags_caching_methods if _has_tags_cache_columns?(db_columns)
|
24
|
+
db_columns
|
13
25
|
end
|
26
|
+
end
|
14
27
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
28
|
+
def reset_column_information
|
29
|
+
super
|
30
|
+
@acts_as_taggable_on_cache_columns = nil
|
31
|
+
end
|
19
32
|
|
20
|
-
|
33
|
+
private
|
21
34
|
|
22
|
-
|
35
|
+
# @private
|
36
|
+
def _has_tags_cache_columns?(db_columns)
|
37
|
+
db_column_names = db_columns.map(&:name)
|
38
|
+
tag_types.any? do |context|
|
39
|
+
db_column_names.include?("cached_#{context.to_s.singularize}_list")
|
23
40
|
end
|
41
|
+
end
|
24
42
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
# connection when the class is loaded, here we intercept and cache
|
30
|
-
# the call to :columns as @acts_as_taggable_on_cache_columns
|
31
|
-
# to mimic the underlying behavior. While processing this first
|
32
|
-
# call to columns, we do the caching column check and dynamically add
|
33
|
-
# the class and instance methods
|
34
|
-
# FIXME: this method cannot compile in rubinius
|
35
|
-
def columns
|
36
|
-
@acts_as_taggable_on_cache_columns ||= begin
|
37
|
-
db_columns = super
|
38
|
-
_add_tags_caching_methods if _has_tags_cache_columns?(db_columns)
|
39
|
-
db_columns
|
40
|
-
end
|
41
|
-
end
|
43
|
+
# @private
|
44
|
+
def _add_tags_caching_methods
|
45
|
+
send :include, ActsAsTaggableOn::Taggable::Cache::InstanceMethods
|
46
|
+
extend ActsAsTaggableOn::Taggable::Cache::ClassMethods
|
42
47
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
end
|
48
|
+
before_save :save_cached_tag_list
|
49
|
+
|
50
|
+
initialize_tags_cache
|
47
51
|
end
|
48
52
|
end
|
49
53
|
|
@@ -94,16 +94,18 @@ module ActsAsTaggableOn::Taggable
|
|
94
94
|
## Generate conditions:
|
95
95
|
options[:conditions] = sanitize_sql(options[:conditions]) if options[:conditions]
|
96
96
|
|
97
|
-
## Generate joins:
|
98
|
-
taggable_join = "INNER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id"
|
99
|
-
taggable_join << " AND #{table_name}.#{inheritance_column} = '#{name}'" unless descends_from_active_record? # Current model is STI descendant, so add type checking to the join condition
|
100
|
-
|
101
97
|
## Generate scope:
|
102
98
|
tagging_scope = ActsAsTaggableOn::Tagging.select("#{ActsAsTaggableOn::Tagging.table_name}.tag_id, COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) AS tags_count")
|
103
99
|
tag_scope = ActsAsTaggableOn::Tag.select("#{ActsAsTaggableOn::Tag.table_name}.*, #{ActsAsTaggableOn::Tagging.table_name}.tags_count AS count").order(options[:order]).limit(options[:limit])
|
104
100
|
|
105
|
-
#
|
106
|
-
|
101
|
+
# Current model is STI descendant, so add type checking to the join condition
|
102
|
+
unless descends_from_active_record?
|
103
|
+
taggable_join = "INNER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id"
|
104
|
+
taggable_join << " AND #{table_name}.#{inheritance_column} = '#{name}'"
|
105
|
+
tagging_scope = tagging_scope.joins(taggable_join)
|
106
|
+
end
|
107
|
+
|
108
|
+
# Conditions
|
107
109
|
tagging_conditions(options).each { |condition| tagging_scope = tagging_scope.where(condition) }
|
108
110
|
tag_scope = tag_scope.where(options[:conditions])
|
109
111
|
|
@@ -174,7 +176,7 @@ module ActsAsTaggableOn::Taggable
|
|
174
176
|
# See https://github.com/rails/rails/blob/master/activerecord/lib/active_record/relation/calculations.rb#L38
|
175
177
|
def count(column_name = :all, options = {})
|
176
178
|
# https://github.com/rails/rails/commit/da9b5d4a8435b744fcf278fffd6d7f1e36d4a4f2
|
177
|
-
|
179
|
+
super(column_name)
|
178
180
|
end
|
179
181
|
end
|
180
182
|
end
|
@@ -1,5 +1,9 @@
|
|
1
|
+
require_relative 'tagged_with_query'
|
2
|
+
require_relative 'tag_list_type'
|
3
|
+
|
1
4
|
module ActsAsTaggableOn::Taggable
|
2
5
|
module Core
|
6
|
+
|
3
7
|
def self.included(base)
|
4
8
|
base.extend ActsAsTaggableOn::Taggable::Core::ClassMethods
|
5
9
|
|
@@ -25,13 +29,17 @@ module ActsAsTaggableOn::Taggable
|
|
25
29
|
# the associations tag_taggings & tags are always returned in created order
|
26
30
|
has_many context_taggings, -> { includes(:tag).order(taggings_order).where(context: tags_type) },
|
27
31
|
as: :taggable,
|
28
|
-
class_name: ActsAsTaggableOn::Tagging,
|
29
|
-
dependent: :destroy
|
32
|
+
class_name: 'ActsAsTaggableOn::Tagging',
|
33
|
+
dependent: :destroy,
|
34
|
+
after_add: :dirtify_tag_list,
|
35
|
+
after_remove: :dirtify_tag_list
|
30
36
|
|
31
37
|
has_many context_tags, -> { order(taggings_order) },
|
32
|
-
class_name: ActsAsTaggableOn::Tag,
|
38
|
+
class_name: 'ActsAsTaggableOn::Tag',
|
33
39
|
through: context_taggings,
|
34
40
|
source: :tag
|
41
|
+
|
42
|
+
attribute "#{tags_type.singularize}_list".to_sym, ActsAsTaggableOn::Taggable::TagListType.new
|
35
43
|
end
|
36
44
|
|
37
45
|
taggable_mixin.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
@@ -40,12 +48,30 @@ module ActsAsTaggableOn::Taggable
|
|
40
48
|
end
|
41
49
|
|
42
50
|
def #{tag_type}_list=(new_tags)
|
51
|
+
parsed_new_list = ActsAsTaggableOn.default_parser.new(new_tags).parse
|
52
|
+
|
53
|
+
if self.class.preserve_tag_order? || (parsed_new_list.sort != #{tag_type}_list.sort)
|
54
|
+
if ActsAsTaggableOn::Utils.legacy_activerecord?
|
55
|
+
set_attribute_was("#{tag_type}_list", #{tag_type}_list)
|
56
|
+
else
|
57
|
+
unless #{tag_type}_list_changed?
|
58
|
+
@attributes["#{tag_type}_list"] = ActiveModel::Attribute.from_user("#{tag_type}_list", #{tag_type}_list, ActsAsTaggableOn::Taggable::TagListType.new)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
write_attribute("#{tag_type}_list", parsed_new_list)
|
62
|
+
end
|
63
|
+
|
43
64
|
set_tag_list_on('#{tags_type}', new_tags)
|
44
65
|
end
|
45
66
|
|
46
67
|
def all_#{tags_type}_list
|
47
68
|
all_tags_list_on('#{tags_type}')
|
48
69
|
end
|
70
|
+
|
71
|
+
private
|
72
|
+
def dirtify_tag_list(tagging)
|
73
|
+
attribute_will_change! tagging.context.singularize+"_list"
|
74
|
+
end
|
49
75
|
RUBY
|
50
76
|
end
|
51
77
|
end
|
@@ -84,173 +110,19 @@ module ActsAsTaggableOn::Taggable
|
|
84
110
|
def tagged_with(tags, options = {})
|
85
111
|
tag_list = ActsAsTaggableOn.default_parser.new(tags).parse
|
86
112
|
options = options.dup
|
87
|
-
empty_result = where('1 = 0')
|
88
|
-
|
89
|
-
return empty_result if tag_list.empty?
|
90
|
-
|
91
|
-
joins = []
|
92
|
-
conditions = []
|
93
|
-
having = []
|
94
|
-
select_clause = []
|
95
|
-
order_by = []
|
96
|
-
|
97
|
-
context = options.delete(:on)
|
98
|
-
owned_by = options.delete(:owned_by)
|
99
|
-
alias_base_name = undecorated_table_name.gsub('.', '_')
|
100
|
-
# FIXME use ActiveRecord's connection quote_column_name
|
101
|
-
quote = ActsAsTaggableOn::Utils.using_postgresql? ? '"' : ''
|
102
|
-
|
103
|
-
if options.delete(:exclude)
|
104
|
-
if options.delete(:wild)
|
105
|
-
tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{ActsAsTaggableOn::Utils.like_operator} ? ESCAPE '!'", "%#{ActsAsTaggableOn::Utils.escape_like(t)}%"]) }.join(' OR ')
|
106
|
-
else
|
107
|
-
tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{ActsAsTaggableOn::Utils.like_operator} ?", t]) }.join(' OR ')
|
108
|
-
end
|
109
|
-
|
110
|
-
conditions << "#{table_name}.#{primary_key} NOT IN (SELECT #{ActsAsTaggableOn::Tagging.table_name}.taggable_id FROM #{ActsAsTaggableOn::Tagging.table_name} JOIN #{ActsAsTaggableOn::Tag.table_name} ON #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key} AND (#{tags_conditions}) WHERE #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = #{quote_value(base_class.name, nil)})"
|
111
|
-
|
112
|
-
if owned_by
|
113
|
-
joins << "JOIN #{ActsAsTaggableOn::Tagging.table_name}" +
|
114
|
-
" ON #{ActsAsTaggableOn::Tagging.table_name}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" +
|
115
|
-
" AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = #{quote_value(base_class.name, nil)}" +
|
116
|
-
" AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_id = #{quote_value(owned_by.id, nil)}" +
|
117
|
-
" AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_type = #{quote_value(owned_by.class.base_class.to_s, nil)}"
|
118
|
-
|
119
|
-
joins << " AND " + sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
|
120
|
-
joins << " AND " + sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
|
121
|
-
end
|
122
|
-
|
123
|
-
elsif any = options.delete(:any)
|
124
|
-
# get tags, drop out if nothing returned (we need at least one)
|
125
|
-
tags = if options.delete(:wild)
|
126
|
-
ActsAsTaggableOn::Tag.named_like_any(tag_list)
|
127
|
-
else
|
128
|
-
ActsAsTaggableOn::Tag.named_any(tag_list)
|
129
|
-
end
|
130
|
-
|
131
|
-
return empty_result if tags.length == 0
|
132
|
-
|
133
|
-
# setup taggings alias so we can chain, ex: items_locations_taggings_awesome_cool_123
|
134
|
-
# avoid ambiguous column name
|
135
|
-
taggings_context = context ? "_#{context}" : ''
|
136
|
-
|
137
|
-
taggings_alias = adjust_taggings_alias(
|
138
|
-
"#{alias_base_name[0..4]}#{taggings_context[0..6]}_taggings_#{ActsAsTaggableOn::Utils.sha_prefix(tags.map(&:name).join('_'))}"
|
139
|
-
)
|
140
|
-
|
141
|
-
tagging_cond = "#{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
|
142
|
-
" WHERE #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" +
|
143
|
-
" AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name, nil)}"
|
144
|
-
|
145
|
-
tagging_cond << " AND " + sanitize_sql(["#{taggings_alias}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
|
146
|
-
tagging_cond << " AND " + sanitize_sql(["#{taggings_alias}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
|
147
|
-
|
148
|
-
tagging_cond << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
|
149
|
-
|
150
|
-
# don't need to sanitize sql, map all ids and join with OR logic
|
151
|
-
tag_ids = tags.map { |t| quote_value(t.id, nil) }.join(', ')
|
152
|
-
tagging_cond << " AND #{taggings_alias}.tag_id in (#{tag_ids})"
|
153
|
-
select_clause << " #{table_name}.*" unless context and tag_types.one?
|
154
|
-
|
155
|
-
if owned_by
|
156
|
-
tagging_cond << ' AND ' +
|
157
|
-
sanitize_sql([
|
158
|
-
"#{taggings_alias}.tagger_id = ? AND #{taggings_alias}.tagger_type = ?",
|
159
|
-
owned_by.id,
|
160
|
-
owned_by.class.base_class.to_s
|
161
|
-
])
|
162
|
-
end
|
163
|
-
|
164
|
-
conditions << "EXISTS (SELECT 1 FROM #{tagging_cond})"
|
165
|
-
if options.delete(:order_by_matching_tag_count)
|
166
|
-
order_by << "(SELECT count(*) FROM #{tagging_cond}) desc"
|
167
|
-
end
|
168
|
-
else
|
169
|
-
tags = ActsAsTaggableOn::Tag.named_any(tag_list)
|
170
|
-
|
171
|
-
return empty_result unless tags.length == tag_list.length
|
172
|
-
|
173
|
-
tags.each do |tag|
|
174
|
-
taggings_alias = adjust_taggings_alias("#{alias_base_name[0..11]}_taggings_#{ActsAsTaggableOn::Utils.sha_prefix(tag.name)}")
|
175
|
-
tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" \
|
176
|
-
" ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" +
|
177
|
-
" AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name, nil)}" +
|
178
|
-
" AND #{taggings_alias}.tag_id = #{quote_value(tag.id, nil)}"
|
179
|
-
|
180
|
-
tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
|
181
|
-
tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
|
182
113
|
|
183
|
-
|
114
|
+
return none if tag_list.empty?
|
184
115
|
|
185
|
-
|
186
|
-
tagging_join << ' AND ' +
|
187
|
-
sanitize_sql([
|
188
|
-
"#{taggings_alias}.tagger_id = ? AND #{taggings_alias}.tagger_type = ?",
|
189
|
-
owned_by.id,
|
190
|
-
owned_by.class.base_class.to_s
|
191
|
-
])
|
192
|
-
end
|
193
|
-
|
194
|
-
joins << tagging_join
|
195
|
-
end
|
196
|
-
end
|
197
|
-
|
198
|
-
group ||= [] # Rails interprets this as a no-op in the group() call below
|
199
|
-
if options.delete(:order_by_matching_tag_count)
|
200
|
-
select_clause << "#{table_name}.*, COUNT(#{taggings_alias}.tag_id) AS #{taggings_alias}_count"
|
201
|
-
group_columns = ActsAsTaggableOn::Utils.using_postgresql? ? grouped_column_names_for(self) : "#{table_name}.#{primary_key}"
|
202
|
-
group = group_columns
|
203
|
-
order_by << "#{taggings_alias}_count DESC"
|
204
|
-
|
205
|
-
elsif options.delete(:match_all)
|
206
|
-
taggings_alias, _ = adjust_taggings_alias("#{alias_base_name}_taggings_group"), "#{alias_base_name}_tags_group"
|
207
|
-
joins << "LEFT OUTER JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" \
|
208
|
-
" ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" \
|
209
|
-
" AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name, nil)}"
|
210
|
-
|
211
|
-
joins << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
|
212
|
-
joins << " AND " + sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
|
213
|
-
joins << " AND " + sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
|
214
|
-
|
215
|
-
group_columns = ActsAsTaggableOn::Utils.using_postgresql? ? grouped_column_names_for(self) : "#{table_name}.#{primary_key}"
|
216
|
-
group = group_columns
|
217
|
-
having = "COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
|
218
|
-
end
|
219
|
-
|
220
|
-
order_by << options[:order] if options[:order].present?
|
221
|
-
|
222
|
-
query = self
|
223
|
-
query = self.select(select_clause.join(',')) unless select_clause.empty?
|
224
|
-
query.joins(joins.join(' '))
|
225
|
-
.where(conditions.join(' AND '))
|
226
|
-
.group(group)
|
227
|
-
.having(having)
|
228
|
-
.order(order_by.join(', '))
|
229
|
-
.readonly(false)
|
116
|
+
::ActsAsTaggableOn::Taggable::TaggedWithQuery.build(self, ActsAsTaggableOn::Tag, ActsAsTaggableOn::Tagging, tag_list, options)
|
230
117
|
end
|
231
118
|
|
232
119
|
def is_taggable?
|
233
120
|
true
|
234
121
|
end
|
235
122
|
|
236
|
-
def adjust_taggings_alias(taggings_alias)
|
237
|
-
if taggings_alias.size > 75
|
238
|
-
taggings_alias = 'taggings_alias_' + Digest::SHA1.hexdigest(taggings_alias)
|
239
|
-
end
|
240
|
-
taggings_alias
|
241
|
-
end
|
242
|
-
|
243
123
|
def taggable_mixin
|
244
124
|
@taggable_mixin ||= Module.new
|
245
125
|
end
|
246
|
-
|
247
|
-
private
|
248
|
-
|
249
|
-
# Rails 5 has merged sanitize and quote_value
|
250
|
-
# See https://github.com/rails/rails/blob/master/activerecord/lib/active_record/sanitization.rb#L10
|
251
|
-
def quote_value(value, column = nil)
|
252
|
-
ActsAsTaggableOn::Utils.active_record5? ? super(value) : super(value, column)
|
253
|
-
end
|
254
126
|
end
|
255
127
|
|
256
128
|
# all column names are necessary for PostgreSQL group clause
|
@@ -283,7 +155,7 @@ module ActsAsTaggableOn::Taggable
|
|
283
155
|
variable_name = "@#{context.to_s.singularize}_list"
|
284
156
|
if instance_variable_get(variable_name)
|
285
157
|
instance_variable_get(variable_name)
|
286
|
-
elsif cached_tag_list_on(context) && self.class.caching_tag_list_on?(context)
|
158
|
+
elsif cached_tag_list_on(context) && ensure_included_cache_methods! && self.class.caching_tag_list_on?(context)
|
287
159
|
instance_variable_set(variable_name, ActsAsTaggableOn.default_parser.new(cached_tag_list_on(context)).parse)
|
288
160
|
else
|
289
161
|
instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(tags_on(context).map(&:name)))
|
@@ -312,7 +184,7 @@ module ActsAsTaggableOn::Taggable
|
|
312
184
|
|
313
185
|
if ActsAsTaggableOn::Utils.using_postgresql?
|
314
186
|
group_columns = grouped_column_names_for(ActsAsTaggableOn::Tag)
|
315
|
-
scope.order("max(#{tagging_table_name}.created_at)").group(group_columns)
|
187
|
+
scope.order(Arel.sql("max(#{tagging_table_name}.created_at)")).group(group_columns)
|
316
188
|
else
|
317
189
|
scope.group("#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}")
|
318
190
|
end.to_a
|
@@ -332,30 +204,19 @@ module ActsAsTaggableOn::Taggable
|
|
332
204
|
add_custom_context(context)
|
333
205
|
|
334
206
|
variable_name = "@#{context.to_s.singularize}_list"
|
335
|
-
process_dirty_object(context, new_list) unless custom_contexts.include?(context.to_s)
|
336
207
|
|
337
|
-
|
208
|
+
parsed_new_list = ActsAsTaggableOn.default_parser.new(new_list).parse
|
209
|
+
|
210
|
+
instance_variable_set(variable_name, parsed_new_list)
|
338
211
|
end
|
339
212
|
|
340
213
|
def tagging_contexts
|
341
214
|
self.class.tag_types.map(&:to_s) + custom_contexts
|
342
215
|
end
|
343
216
|
|
344
|
-
def
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
if changed_attributes.include?(attrib)
|
349
|
-
# The attribute already has an unsaved change.
|
350
|
-
old = changed_attributes[attrib]
|
351
|
-
@changed_attributes.delete(attrib) if old.to_s == value.to_s
|
352
|
-
else
|
353
|
-
old = tag_list_on(context)
|
354
|
-
if self.class.preserve_tag_order
|
355
|
-
@changed_attributes[attrib] = old if old.to_s != value.to_s
|
356
|
-
else
|
357
|
-
@changed_attributes[attrib] = old.to_s if old.sort != ActsAsTaggableOn.default_parser.new(value).parse.sort
|
358
|
-
end
|
217
|
+
def tenant
|
218
|
+
if self.class.tenant_column
|
219
|
+
read_attribute(self.class.tenant_column)
|
359
220
|
end
|
360
221
|
end
|
361
222
|
|
@@ -417,7 +278,11 @@ module ActsAsTaggableOn::Taggable
|
|
417
278
|
|
418
279
|
# Create new taggings:
|
419
280
|
new_tags.each do |tag|
|
420
|
-
|
281
|
+
if tenant
|
282
|
+
taggings.create!(tag_id: tag.id, context: context.to_s, taggable: self, tenant: tenant)
|
283
|
+
else
|
284
|
+
taggings.create!(tag_id: tag.id, context: context.to_s, taggable: self)
|
285
|
+
end
|
421
286
|
end
|
422
287
|
end
|
423
288
|
|
@@ -426,6 +291,10 @@ module ActsAsTaggableOn::Taggable
|
|
426
291
|
|
427
292
|
private
|
428
293
|
|
294
|
+
def ensure_included_cache_methods!
|
295
|
+
self.class.columns
|
296
|
+
end
|
297
|
+
|
429
298
|
# Filters the tag lists from the attribute names.
|
430
299
|
def attributes_for_update(attribute_names)
|
431
300
|
tag_lists = tag_types.map {|tags_type| "#{tags_type.to_s.singularize}_list"}
|
@@ -462,3 +331,4 @@ module ActsAsTaggableOn::Taggable
|
|
462
331
|
end
|
463
332
|
end
|
464
333
|
end
|
334
|
+
|