acts-as-taggable-on 4.0.0.pre → 7.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/.gitignore +1 -1
- data/.travis.yml +28 -15
- data/Appraisals +12 -10
- data/CHANGELOG.md +200 -71
- data/CONTRIBUTING.md +13 -0
- data/Gemfile +1 -1
- data/README.md +68 -28
- 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/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 +17 -23
- data/lib/acts_as_taggable_on/tag_list.rb +1 -0
- data/lib/acts_as_taggable_on/taggable.rb +0 -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 +41 -181
- 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 +6 -3
- 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/taggable_spec.rb +16 -13
- data/spec/acts_as_taggable_on/tagger_spec.rb +2 -2
- 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 +1 -0
- data/spec/internal/app/models/user.rb +1 -1
- data/spec/internal/db/schema.rb +14 -5
- data/spec/spec_helper.rb +0 -1
- data/spec/support/database.rb +4 -4
- metadata +30 -61
- 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,8 @@ 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}.*")
|
55
56
|
end
|
56
57
|
|
57
58
|
### CLASS METHODS:
|
@@ -70,16 +71,19 @@ module ActsAsTaggableOn
|
|
70
71
|
return [] if list.empty?
|
71
72
|
|
72
73
|
existing_tags = named_any(list)
|
73
|
-
|
74
74
|
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
75
|
begin
|
76
|
+
tries ||= 3
|
77
|
+
comparable_tag_name = comparable_name(tag_name)
|
78
|
+
existing_tag = existing_tags.find { |tag| comparable_name(tag.name) == comparable_tag_name }
|
78
79
|
existing_tag || create(name: tag_name)
|
79
80
|
rescue ActiveRecord::RecordNotUnique
|
80
|
-
|
81
|
-
|
82
|
-
|
81
|
+
if (tries -= 1).positive?
|
82
|
+
ActiveRecord::Base.connection.execute 'ROLLBACK'
|
83
|
+
existing_tags = named_any(list)
|
84
|
+
retry
|
85
|
+
end
|
86
|
+
|
83
87
|
raise DuplicateTagError.new("'#{tag_name}' has already been taken")
|
84
88
|
end
|
85
89
|
end
|
@@ -101,8 +105,6 @@ module ActsAsTaggableOn
|
|
101
105
|
|
102
106
|
class << self
|
103
107
|
|
104
|
-
|
105
|
-
|
106
108
|
private
|
107
109
|
|
108
110
|
def comparable_name(str)
|
@@ -117,20 +119,12 @@ module ActsAsTaggableOn
|
|
117
119
|
ActsAsTaggableOn::Utils.using_mysql? ? 'BINARY ' : nil
|
118
120
|
end
|
119
121
|
|
120
|
-
def
|
121
|
-
|
122
|
-
ActiveSupport::Multibyte::Unicode.downcase(string)
|
123
|
-
else
|
124
|
-
ActiveSupport::Multibyte::Chars.new(string).downcase.to_s
|
125
|
-
end
|
122
|
+
def as_8bit_ascii(string)
|
123
|
+
string.to_s.mb_chars
|
126
124
|
end
|
127
125
|
|
128
|
-
def
|
129
|
-
|
130
|
-
string.to_s.dup.force_encoding('BINARY')
|
131
|
-
else
|
132
|
-
string.to_s.mb_chars
|
133
|
-
end
|
126
|
+
def unicode_downcase(string)
|
127
|
+
as_8bit_ascii(string).downcase
|
134
128
|
end
|
135
129
|
|
136
130
|
def sanitize_sql_for_named_any(tag)
|
@@ -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
|
-
|
183
|
-
tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
|
184
|
-
|
185
|
-
if owned_by
|
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
113
|
|
194
|
-
|
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?
|
114
|
+
return none if tag_list.empty?
|
221
115
|
|
222
|
-
|
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,33 +204,16 @@ 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 process_dirty_object(context, new_list)
|
345
|
-
value = new_list.is_a?(Array) ? ActsAsTaggableOn::TagList.new(new_list) : new_list
|
346
|
-
attrib = "#{context.to_s.singularize}_list"
|
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
|
359
|
-
end
|
360
|
-
end
|
361
|
-
|
362
217
|
def reload(*args)
|
363
218
|
self.class.tag_types.each do |context|
|
364
219
|
instance_variable_set("@#{context.to_s.singularize}_list", nil)
|
@@ -426,6 +281,10 @@ module ActsAsTaggableOn::Taggable
|
|
426
281
|
|
427
282
|
private
|
428
283
|
|
284
|
+
def ensure_included_cache_methods!
|
285
|
+
self.class.columns
|
286
|
+
end
|
287
|
+
|
429
288
|
# Filters the tag lists from the attribute names.
|
430
289
|
def attributes_for_update(attribute_names)
|
431
290
|
tag_lists = tag_types.map {|tags_type| "#{tags_type.to_s.singularize}_list"}
|
@@ -462,3 +321,4 @@ module ActsAsTaggableOn::Taggable
|
|
462
321
|
end
|
463
322
|
end
|
464
323
|
end
|
324
|
+
|