acts-as-taggable-on 4.0.0.pre → 7.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|