acts-as-taggable-on 3.5.0 → 8.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.github/workflows/spec.yml +95 -0
- data/.gitignore +2 -0
- data/Appraisals +12 -9
- data/CHANGELOG.md +212 -70
- data/CONTRIBUTING.md +13 -0
- data/Gemfile +2 -1
- data/README.md +128 -25
- 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 +15 -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 +21 -0
- 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 +22 -14
- data/lib/acts_as_taggable_on/engine.rb +0 -1
- data/lib/acts_as_taggable_on/tag.rb +27 -23
- data/lib/acts_as_taggable_on/tag_list.rb +3 -13
- data/lib/acts_as_taggable_on/taggable/cache.rb +39 -35
- data/lib/acts_as_taggable_on/taggable/collection.rb +14 -9
- data/lib/acts_as_taggable_on/taggable/core.rb +59 -184
- data/lib/acts_as_taggable_on/taggable/ownership.rb +19 -8
- 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/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/taggable/tagged_with_query.rb +16 -0
- data/lib/acts_as_taggable_on/taggable.rb +18 -1
- data/lib/acts_as_taggable_on/tagger.rb +12 -11
- data/lib/acts_as_taggable_on/tagging.rb +9 -14
- data/lib/acts_as_taggable_on/utils.rb +4 -5
- data/lib/acts_as_taggable_on/version.rb +1 -2
- data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +14 -13
- data/spec/acts_as_taggable_on/acts_as_tagger_spec.rb +1 -1
- data/spec/acts_as_taggable_on/caching_spec.rb +55 -9
- 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_list_spec.rb +27 -1
- data/spec/acts_as_taggable_on/tag_spec.rb +31 -1
- data/spec/acts_as_taggable_on/taggable_spec.rb +40 -19
- data/spec/acts_as_taggable_on/tagger_spec.rb +2 -2
- data/spec/acts_as_taggable_on/tagging_spec.rb +87 -7
- 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 +23 -7
- data/spec/spec_helper.rb +0 -1
- data/spec/support/database.rb +5 -11
- metadata +27 -75
- data/.travis.yml +0 -40
- data/UPGRADING.md +0 -8
- data/gemfiles/activerecord_3.2.gemfile +0 -15
- data/gemfiles/activerecord_4.0.gemfile +0 -15
- data/gemfiles/activerecord_4.1.gemfile +0 -15
- data/gemfiles/activerecord_4.2.gemfile +0 -16
- data/lib/acts_as_taggable_on/compatibility.rb +0 -35
- data/lib/acts_as_taggable_on/tag_list_parser.rb +0 -21
- data/lib/acts_as_taggable_on/taggable/dirty.rb +0 -36
- data/spec/acts_as_taggable_on/tag_list_parser_spec.rb +0 -46
- data/spec/internal/app/models/models.rb +0 -90
@@ -1,8 +1,7 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
module ActsAsTaggableOn
|
3
3
|
class Tag < ::ActiveRecord::Base
|
4
|
-
|
5
|
-
attr_accessible :name if defined?(ActiveModel::MassAssignmentSecurity)
|
4
|
+
self.table_name = ActsAsTaggableOn.tags_table
|
6
5
|
|
7
6
|
### ASSOCIATIONS:
|
8
7
|
|
@@ -11,7 +10,7 @@ module ActsAsTaggableOn
|
|
11
10
|
### VALIDATIONS:
|
12
11
|
|
13
12
|
validates_presence_of :name
|
14
|
-
validates_uniqueness_of :name, if: :validates_name_uniqueness
|
13
|
+
validates_uniqueness_of :name, if: :validates_name_uniqueness?, case_sensitive: true
|
15
14
|
validates_length_of :name, maximum: 255
|
16
15
|
|
17
16
|
# monkey patch this method if don't need name uniqueness validation
|
@@ -50,6 +49,18 @@ module ActsAsTaggableOn
|
|
50
49
|
where(clause)
|
51
50
|
end
|
52
51
|
|
52
|
+
def self.for_context(context)
|
53
|
+
joins(:taggings).
|
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}.*")
|
62
|
+
end
|
63
|
+
|
53
64
|
### CLASS METHODS:
|
54
65
|
|
55
66
|
def self.find_or_create_with_like_by_name(name)
|
@@ -66,16 +77,19 @@ module ActsAsTaggableOn
|
|
66
77
|
return [] if list.empty?
|
67
78
|
|
68
79
|
existing_tags = named_any(list)
|
69
|
-
|
70
80
|
list.map do |tag_name|
|
71
|
-
comparable_tag_name = comparable_name(tag_name)
|
72
|
-
existing_tag = existing_tags.find { |tag| comparable_name(tag.name) == comparable_tag_name }
|
73
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 }
|
74
85
|
existing_tag || create(name: tag_name)
|
75
86
|
rescue ActiveRecord::RecordNotUnique
|
76
|
-
|
77
|
-
|
78
|
-
|
87
|
+
if (tries -= 1).positive?
|
88
|
+
ActiveRecord::Base.connection.execute 'ROLLBACK'
|
89
|
+
existing_tags = named_any(list)
|
90
|
+
retry
|
91
|
+
end
|
92
|
+
|
79
93
|
raise DuplicateTagError.new("'#{tag_name}' has already been taken")
|
80
94
|
end
|
81
95
|
end
|
@@ -97,8 +111,6 @@ module ActsAsTaggableOn
|
|
97
111
|
|
98
112
|
class << self
|
99
113
|
|
100
|
-
|
101
|
-
|
102
114
|
private
|
103
115
|
|
104
116
|
def comparable_name(str)
|
@@ -113,20 +125,12 @@ module ActsAsTaggableOn
|
|
113
125
|
ActsAsTaggableOn::Utils.using_mysql? ? 'BINARY ' : nil
|
114
126
|
end
|
115
127
|
|
116
|
-
def
|
117
|
-
|
118
|
-
ActiveSupport::Multibyte::Unicode.downcase(string)
|
119
|
-
else
|
120
|
-
ActiveSupport::Multibyte::Chars.new(string).downcase.to_s
|
121
|
-
end
|
128
|
+
def as_8bit_ascii(string)
|
129
|
+
string.to_s.mb_chars
|
122
130
|
end
|
123
131
|
|
124
|
-
def
|
125
|
-
|
126
|
-
string.to_s.dup.force_encoding('BINARY')
|
127
|
-
else
|
128
|
-
string.to_s.mb_chars
|
129
|
-
end
|
132
|
+
def unicode_downcase(string)
|
133
|
+
as_8bit_ascii(string).downcase
|
130
134
|
end
|
131
135
|
|
132
136
|
def sanitize_sql_for_named_any(tag)
|
@@ -41,6 +41,7 @@ module ActsAsTaggableOn
|
|
41
41
|
# Appends the elements of +other_tag_list+ to +self+.
|
42
42
|
def concat(other_tag_list)
|
43
43
|
super(other_tag_list).send(:clean!)
|
44
|
+
self
|
44
45
|
end
|
45
46
|
|
46
47
|
##
|
@@ -84,7 +85,8 @@ module ActsAsTaggableOn
|
|
84
85
|
map! { |tag| tag.mb_chars.downcase.to_s } if ActsAsTaggableOn.force_lowercase
|
85
86
|
map!(&:parameterize) if ActsAsTaggableOn.force_parameterize
|
86
87
|
|
87
|
-
uniq!
|
88
|
+
ActsAsTaggableOn.strict_case_match ? uniq! : uniq!{ |tag| tag.downcase }
|
89
|
+
self
|
88
90
|
end
|
89
91
|
|
90
92
|
|
@@ -99,18 +101,6 @@ module ActsAsTaggableOn
|
|
99
101
|
args.flatten!
|
100
102
|
end
|
101
103
|
|
102
|
-
|
103
|
-
## DEPRECATED
|
104
|
-
def self.from(string)
|
105
|
-
ActiveRecord::Base.logger.warn <<WARNING
|
106
|
-
ActsAsTaggableOn::TagList.from is deprecated \
|
107
|
-
and will be removed from v4.0+, use \
|
108
|
-
ActsAsTaggableOn::DefaultParser.new instead
|
109
|
-
WARNING
|
110
|
-
@parser.new(string).parse
|
111
|
-
end
|
112
|
-
|
113
|
-
|
114
104
|
end
|
115
105
|
end
|
116
106
|
|
@@ -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
|
|
@@ -73,7 +77,7 @@ module ActsAsTaggableOn::Taggable
|
|
73
77
|
tag_types.map(&:to_s).each do |tag_type|
|
74
78
|
if self.class.send("caching_#{tag_type.singularize}_list?")
|
75
79
|
if tag_list_cache_set_on(tag_type)
|
76
|
-
list = tag_list_cache_on(tag_type).to_a.flatten.compact.join(
|
80
|
+
list = tag_list_cache_on(tag_type).to_a.flatten.compact.join("#{ActsAsTaggableOn.delimiter} ")
|
77
81
|
self["cached_#{tag_type.singularize}_list"] = list
|
78
82
|
end
|
79
83
|
end
|
@@ -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
|
|
@@ -138,7 +140,7 @@ module ActsAsTaggableOn::Taggable
|
|
138
140
|
scoped_ids = pluck(table_name_pkey)
|
139
141
|
tagging_scope = tagging_scope.where("#{ActsAsTaggableOn::Tagging.table_name}.taggable_id IN (?)", scoped_ids)
|
140
142
|
else
|
141
|
-
tagging_scope = tagging_scope.where("#{ActsAsTaggableOn::Tagging.table_name}.taggable_id IN(#{safe_to_sql(select(table_name_pkey))})")
|
143
|
+
tagging_scope = tagging_scope.where("#{ActsAsTaggableOn::Tagging.table_name}.taggable_id IN(#{safe_to_sql(except(:select).select(table_name_pkey))})")
|
142
144
|
end
|
143
145
|
|
144
146
|
tagging_scope
|
@@ -169,9 +171,12 @@ module ActsAsTaggableOn::Taggable
|
|
169
171
|
end
|
170
172
|
|
171
173
|
module CalculationMethods
|
172
|
-
|
174
|
+
# Rails 5 TODO: Remove options argument as soon we remove support to
|
175
|
+
# activerecord-deprecated_finders.
|
176
|
+
# See https://github.com/rails/rails/blob/master/activerecord/lib/active_record/relation/calculations.rb#L38
|
177
|
+
def count(column_name = :all, options = {})
|
173
178
|
# https://github.com/rails/rails/commit/da9b5d4a8435b744fcf278fffd6d7f1e36d4a4f2
|
174
|
-
super
|
179
|
+
super(column_name)
|
175
180
|
end
|
176
181
|
end
|
177
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
|
|
@@ -23,18 +27,19 @@ module ActsAsTaggableOn::Taggable
|
|
23
27
|
class_eval do
|
24
28
|
# when preserving tag order, include order option so that for a 'tags' context
|
25
29
|
# the associations tag_taggings & tags are always returned in created order
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
30
|
+
has_many context_taggings, -> { includes(:tag).order(taggings_order).where(context: tags_type) },
|
31
|
+
as: :taggable,
|
32
|
+
class_name: 'ActsAsTaggableOn::Tagging',
|
33
|
+
dependent: :destroy,
|
34
|
+
after_add: :dirtify_tag_list,
|
35
|
+
after_remove: :dirtify_tag_list
|
36
|
+
|
37
|
+
has_many context_tags, -> { order(taggings_order) },
|
38
|
+
class_name: 'ActsAsTaggableOn::Tag',
|
39
|
+
through: context_taggings,
|
40
|
+
source: :tag
|
41
|
+
|
42
|
+
attribute "#{tags_type.singularize}_list".to_sym, ActsAsTaggableOn::Taggable::TagListType.new
|
38
43
|
end
|
39
44
|
|
40
45
|
taggable_mixin.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
@@ -43,12 +48,30 @@ module ActsAsTaggableOn::Taggable
|
|
43
48
|
end
|
44
49
|
|
45
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
|
+
|
46
64
|
set_tag_list_on('#{tags_type}', new_tags)
|
47
65
|
end
|
48
66
|
|
49
67
|
def all_#{tags_type}_list
|
50
68
|
all_tags_list_on('#{tags_type}')
|
51
69
|
end
|
70
|
+
|
71
|
+
private
|
72
|
+
def dirtify_tag_list(tagging)
|
73
|
+
attribute_will_change! tagging.context.singularize+"_list"
|
74
|
+
end
|
52
75
|
RUBY
|
53
76
|
end
|
54
77
|
end
|
@@ -87,162 +110,16 @@ module ActsAsTaggableOn::Taggable
|
|
87
110
|
def tagged_with(tags, options = {})
|
88
111
|
tag_list = ActsAsTaggableOn.default_parser.new(tags).parse
|
89
112
|
options = options.dup
|
90
|
-
empty_result = where('1 = 0')
|
91
|
-
|
92
|
-
return empty_result if tag_list.empty?
|
93
|
-
|
94
|
-
joins = []
|
95
|
-
conditions = []
|
96
|
-
having = []
|
97
|
-
select_clause = []
|
98
|
-
order_by = []
|
99
|
-
|
100
|
-
context = options.delete(:on)
|
101
|
-
owned_by = options.delete(:owned_by)
|
102
|
-
alias_base_name = undecorated_table_name.gsub('.', '_')
|
103
|
-
# FIXME use ActiveRecord's connection quote_column_name
|
104
|
-
quote = ActsAsTaggableOn::Utils.using_postgresql? ? '"' : ''
|
105
|
-
|
106
|
-
if options.delete(:exclude)
|
107
|
-
if options.delete(:wild)
|
108
|
-
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 ')
|
109
|
-
else
|
110
|
-
tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{ActsAsTaggableOn::Utils.like_operator} ?", t]) }.join(' OR ')
|
111
|
-
end
|
112
|
-
|
113
|
-
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)})"
|
114
|
-
|
115
|
-
if owned_by
|
116
|
-
joins << "JOIN #{ActsAsTaggableOn::Tagging.table_name}" +
|
117
|
-
" ON #{ActsAsTaggableOn::Tagging.table_name}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" +
|
118
|
-
" AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = #{quote_value(base_class.name, nil)}" +
|
119
|
-
" AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_id = #{quote_value(owned_by.id, nil)}" +
|
120
|
-
" AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_type = #{quote_value(owned_by.class.base_class.to_s, nil)}"
|
121
|
-
|
122
|
-
joins << " AND " + sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
|
123
|
-
joins << " AND " + sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
|
124
|
-
end
|
125
|
-
|
126
|
-
elsif any = options.delete(:any)
|
127
|
-
# get tags, drop out if nothing returned (we need at least one)
|
128
|
-
tags = if options.delete(:wild)
|
129
|
-
ActsAsTaggableOn::Tag.named_like_any(tag_list)
|
130
|
-
else
|
131
|
-
ActsAsTaggableOn::Tag.named_any(tag_list)
|
132
|
-
end
|
133
|
-
|
134
|
-
return empty_result if tags.length == 0
|
135
|
-
|
136
|
-
# setup taggings alias so we can chain, ex: items_locations_taggings_awesome_cool_123
|
137
|
-
# avoid ambiguous column name
|
138
|
-
taggings_context = context ? "_#{context}" : ''
|
139
|
-
|
140
|
-
taggings_alias = adjust_taggings_alias(
|
141
|
-
"#{alias_base_name[0..4]}#{taggings_context[0..6]}_taggings_#{ActsAsTaggableOn::Utils.sha_prefix(tags.map(&:name).join('_'))}"
|
142
|
-
)
|
143
|
-
|
144
|
-
tagging_cond = "#{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
|
145
|
-
" WHERE #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" +
|
146
|
-
" AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name, nil)}"
|
147
|
-
|
148
|
-
tagging_cond << " AND " + sanitize_sql(["#{taggings_alias}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
|
149
|
-
tagging_cond << " AND " + sanitize_sql(["#{taggings_alias}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
|
150
|
-
|
151
|
-
tagging_cond << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
|
152
|
-
|
153
|
-
# don't need to sanitize sql, map all ids and join with OR logic
|
154
|
-
tag_ids = tags.map { |t| quote_value(t.id, nil) }.join(', ')
|
155
|
-
tagging_cond << " AND #{taggings_alias}.tag_id in (#{tag_ids})"
|
156
|
-
select_clause << " #{table_name}.*" unless context and tag_types.one?
|
157
|
-
|
158
|
-
if owned_by
|
159
|
-
tagging_cond << ' AND ' +
|
160
|
-
sanitize_sql([
|
161
|
-
"#{taggings_alias}.tagger_id = ? AND #{taggings_alias}.tagger_type = ?",
|
162
|
-
owned_by.id,
|
163
|
-
owned_by.class.base_class.to_s
|
164
|
-
])
|
165
|
-
end
|
166
|
-
|
167
|
-
conditions << "EXISTS (SELECT 1 FROM #{tagging_cond})"
|
168
|
-
if options.delete(:order_by_matching_tag_count)
|
169
|
-
order_by << "(SELECT count(*) FROM #{tagging_cond}) desc"
|
170
|
-
end
|
171
|
-
else
|
172
|
-
tags = ActsAsTaggableOn::Tag.named_any(tag_list)
|
173
|
-
|
174
|
-
return empty_result unless tags.length == tag_list.length
|
175
|
-
|
176
|
-
tags.each do |tag|
|
177
|
-
taggings_alias = adjust_taggings_alias("#{alias_base_name[0..11]}_taggings_#{ActsAsTaggableOn::Utils.sha_prefix(tag.name)}")
|
178
|
-
tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" \
|
179
|
-
" ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" +
|
180
|
-
" AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name, nil)}" +
|
181
|
-
" AND #{taggings_alias}.tag_id = #{quote_value(tag.id, nil)}"
|
182
113
|
|
183
|
-
|
184
|
-
tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
|
114
|
+
return none if tag_list.empty?
|
185
115
|
|
186
|
-
|
187
|
-
|
188
|
-
if owned_by
|
189
|
-
tagging_join << ' AND ' +
|
190
|
-
sanitize_sql([
|
191
|
-
"#{taggings_alias}.tagger_id = ? AND #{taggings_alias}.tagger_type = ?",
|
192
|
-
owned_by.id,
|
193
|
-
owned_by.class.base_class.to_s
|
194
|
-
])
|
195
|
-
end
|
196
|
-
|
197
|
-
joins << tagging_join
|
198
|
-
end
|
199
|
-
end
|
200
|
-
|
201
|
-
group ||= [] # Rails interprets this as a no-op in the group() call below
|
202
|
-
if options.delete(:order_by_matching_tag_count)
|
203
|
-
select_clause << "#{table_name}.*, COUNT(#{taggings_alias}.tag_id) AS #{taggings_alias}_count"
|
204
|
-
group_columns = ActsAsTaggableOn::Utils.using_postgresql? ? grouped_column_names_for(self) : "#{table_name}.#{primary_key}"
|
205
|
-
group = group_columns
|
206
|
-
order_by << "#{taggings_alias}_count DESC"
|
207
|
-
|
208
|
-
elsif options.delete(:match_all)
|
209
|
-
taggings_alias, _ = adjust_taggings_alias("#{alias_base_name}_taggings_group"), "#{alias_base_name}_tags_group"
|
210
|
-
joins << "LEFT OUTER JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" \
|
211
|
-
" ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" \
|
212
|
-
" AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name, nil)}"
|
213
|
-
|
214
|
-
joins << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
|
215
|
-
joins << " AND " + sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
|
216
|
-
joins << " AND " + sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
|
217
|
-
|
218
|
-
group_columns = ActsAsTaggableOn::Utils.using_postgresql? ? grouped_column_names_for(self) : "#{table_name}.#{primary_key}"
|
219
|
-
group = group_columns
|
220
|
-
having = "COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
|
221
|
-
end
|
222
|
-
|
223
|
-
order_by << options[:order] if options[:order].present?
|
224
|
-
|
225
|
-
query = self
|
226
|
-
query = self.select(select_clause.join(',')) unless select_clause.empty?
|
227
|
-
query.joins(joins.join(' '))
|
228
|
-
.where(conditions.join(' AND '))
|
229
|
-
.group(group)
|
230
|
-
.having(having)
|
231
|
-
.order(order_by.join(', '))
|
232
|
-
.readonly(false)
|
116
|
+
::ActsAsTaggableOn::Taggable::TaggedWithQuery.build(self, ActsAsTaggableOn::Tag, ActsAsTaggableOn::Tagging, tag_list, options)
|
233
117
|
end
|
234
118
|
|
235
119
|
def is_taggable?
|
236
120
|
true
|
237
121
|
end
|
238
122
|
|
239
|
-
def adjust_taggings_alias(taggings_alias)
|
240
|
-
if taggings_alias.size > 75
|
241
|
-
taggings_alias = 'taggings_alias_' + Digest::SHA1.hexdigest(taggings_alias)
|
242
|
-
end
|
243
|
-
taggings_alias
|
244
|
-
end
|
245
|
-
|
246
123
|
def taggable_mixin
|
247
124
|
@taggable_mixin ||= Module.new
|
248
125
|
end
|
@@ -254,7 +131,7 @@ module ActsAsTaggableOn::Taggable
|
|
254
131
|
end
|
255
132
|
|
256
133
|
def custom_contexts
|
257
|
-
@custom_contexts ||=
|
134
|
+
@custom_contexts ||= taggings.map(&:context).uniq
|
258
135
|
end
|
259
136
|
|
260
137
|
def is_taggable?
|
@@ -278,7 +155,7 @@ module ActsAsTaggableOn::Taggable
|
|
278
155
|
variable_name = "@#{context.to_s.singularize}_list"
|
279
156
|
if instance_variable_get(variable_name)
|
280
157
|
instance_variable_get(variable_name)
|
281
|
-
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)
|
282
159
|
instance_variable_set(variable_name, ActsAsTaggableOn.default_parser.new(cached_tag_list_on(context)).parse)
|
283
160
|
else
|
284
161
|
instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(tags_on(context).map(&:name)))
|
@@ -307,7 +184,7 @@ module ActsAsTaggableOn::Taggable
|
|
307
184
|
|
308
185
|
if ActsAsTaggableOn::Utils.using_postgresql?
|
309
186
|
group_columns = grouped_column_names_for(ActsAsTaggableOn::Tag)
|
310
|
-
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)
|
311
188
|
else
|
312
189
|
scope.group("#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}")
|
313
190
|
end.to_a
|
@@ -327,30 +204,19 @@ module ActsAsTaggableOn::Taggable
|
|
327
204
|
add_custom_context(context)
|
328
205
|
|
329
206
|
variable_name = "@#{context.to_s.singularize}_list"
|
330
|
-
process_dirty_object(context, new_list) unless custom_contexts.include?(context.to_s)
|
331
207
|
|
332
|
-
|
208
|
+
parsed_new_list = ActsAsTaggableOn.default_parser.new(new_list).parse
|
209
|
+
|
210
|
+
instance_variable_set(variable_name, parsed_new_list)
|
333
211
|
end
|
334
212
|
|
335
213
|
def tagging_contexts
|
336
|
-
|
214
|
+
self.class.tag_types.map(&:to_s) + custom_contexts
|
337
215
|
end
|
338
216
|
|
339
|
-
def
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
if changed_attributes.include?(attrib)
|
344
|
-
# The attribute already has an unsaved change.
|
345
|
-
old = changed_attributes[attrib]
|
346
|
-
@changed_attributes.delete(attrib) if old.to_s == value.to_s
|
347
|
-
else
|
348
|
-
old = tag_list_on(context)
|
349
|
-
if self.class.preserve_tag_order
|
350
|
-
@changed_attributes[attrib] = old if old.to_s != value.to_s
|
351
|
-
else
|
352
|
-
@changed_attributes[attrib] = old.to_s if old.sort != ActsAsTaggableOn.default_parser.new(value).parse.sort
|
353
|
-
end
|
217
|
+
def taggable_tenant
|
218
|
+
if self.class.tenant_column
|
219
|
+
public_send(self.class.tenant_column)
|
354
220
|
end
|
355
221
|
end
|
356
222
|
|
@@ -407,12 +273,16 @@ module ActsAsTaggableOn::Taggable
|
|
407
273
|
|
408
274
|
# Destroy old taggings:
|
409
275
|
if old_tags.present?
|
410
|
-
taggings.not_owned.by_context(context).
|
276
|
+
taggings.not_owned.by_context(context).where(tag_id: old_tags).destroy_all
|
411
277
|
end
|
412
278
|
|
413
279
|
# Create new taggings:
|
414
280
|
new_tags.each do |tag|
|
415
|
-
|
281
|
+
if taggable_tenant
|
282
|
+
taggings.create!(tag_id: tag.id, context: context.to_s, taggable: self, tenant: taggable_tenant)
|
283
|
+
else
|
284
|
+
taggings.create!(tag_id: tag.id, context: context.to_s, taggable: self)
|
285
|
+
end
|
416
286
|
end
|
417
287
|
end
|
418
288
|
|
@@ -421,6 +291,10 @@ module ActsAsTaggableOn::Taggable
|
|
421
291
|
|
422
292
|
private
|
423
293
|
|
294
|
+
def ensure_included_cache_methods!
|
295
|
+
self.class.columns
|
296
|
+
end
|
297
|
+
|
424
298
|
# Filters the tag lists from the attribute names.
|
425
299
|
def attributes_for_update(attribute_names)
|
426
300
|
tag_lists = tag_types.map {|tags_type| "#{tags_type.to_s.singularize}_list"}
|
@@ -432,7 +306,7 @@ module ActsAsTaggableOn::Taggable
|
|
432
306
|
tag_lists = tag_types.map {|tags_type| "#{tags_type.to_s.singularize}_list"}
|
433
307
|
super.delete_if {|attr| tag_lists.include? attr }
|
434
308
|
end
|
435
|
-
|
309
|
+
|
436
310
|
##
|
437
311
|
# Override this hook if you wish to subclass {ActsAsTaggableOn::Tag} --
|
438
312
|
# context is provided so that you may conditionally use a Tag subclass
|
@@ -457,3 +331,4 @@ module ActsAsTaggableOn::Taggable
|
|
457
331
|
end
|
458
332
|
end
|
459
333
|
end
|
334
|
+
|