acts-as-taggable-on 3.2.1 → 3.4.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 +4 -4
- data/.travis.yml +21 -1
- data/Appraisals +8 -6
- data/CHANGELOG.md +99 -10
- data/README.md +78 -11
- data/acts-as-taggable-on.gemspec +2 -4
- data/db/migrate/4_add_missing_taggable_index.rb +9 -0
- data/gemfiles/activerecord_3.2.gemfile +0 -1
- data/gemfiles/activerecord_4.0.gemfile +1 -2
- data/gemfiles/activerecord_4.1.gemfile +1 -2
- data/gemfiles/activerecord_4.2.gemfile +16 -0
- data/gemfiles/activerecord_edge.gemfile +0 -1
- data/lib/acts-as-taggable-on.rb +40 -21
- data/lib/acts_as_taggable_on/default_parser.rb +79 -0
- data/lib/acts_as_taggable_on/generic_parser.rb +19 -0
- data/lib/acts_as_taggable_on/tag.rb +6 -1
- data/lib/acts_as_taggable_on/tag_list.rb +20 -79
- data/lib/acts_as_taggable_on/tag_list_parser.rb +21 -0
- data/lib/acts_as_taggable_on/{acts_as_taggable_on → taggable}/cache.rb +1 -0
- data/lib/acts_as_taggable_on/{acts_as_taggable_on → taggable}/collection.rb +3 -1
- data/lib/acts_as_taggable_on/{acts_as_taggable_on → taggable}/core.rb +87 -51
- data/lib/acts_as_taggable_on/{acts_as_taggable_on → taggable}/ownership.rb +1 -1
- data/lib/acts_as_taggable_on/{acts_as_taggable_on → taggable}/related.rb +6 -6
- data/lib/acts_as_taggable_on/taggable.rb +7 -8
- data/lib/acts_as_taggable_on/tagging.rb +7 -1
- data/lib/acts_as_taggable_on/tags_helper.rb +2 -2
- data/lib/acts_as_taggable_on/utils.rb +26 -50
- data/lib/acts_as_taggable_on/version.rb +1 -1
- data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +1 -1
- data/spec/acts_as_taggable_on/acts_as_tagger_spec.rb +1 -0
- data/spec/acts_as_taggable_on/caching_spec.rb +1 -0
- data/spec/acts_as_taggable_on/default_parser_spec.rb +47 -0
- data/spec/acts_as_taggable_on/generic_parser_spec.rb +14 -0
- data/spec/acts_as_taggable_on/related_spec.rb +1 -0
- data/spec/acts_as_taggable_on/single_table_inheritance_spec.rb +1 -0
- data/spec/acts_as_taggable_on/tag_list_parser_spec.rb +46 -0
- data/spec/acts_as_taggable_on/tag_list_spec.rb +23 -31
- data/spec/acts_as_taggable_on/tag_spec.rb +54 -38
- data/spec/acts_as_taggable_on/taggable/dirty_spec.rb +127 -0
- data/spec/acts_as_taggable_on/taggable_spec.rb +84 -164
- data/spec/acts_as_taggable_on/tagger_spec.rb +1 -1
- data/spec/acts_as_taggable_on/tagging_spec.rb +37 -1
- data/spec/acts_as_taggable_on/tags_helper_spec.rb +1 -0
- data/spec/acts_as_taggable_on/utils_spec.rb +6 -11
- data/spec/internal/app/models/cached_model_with_array.rb +1 -1
- data/spec/internal/app/models/models.rb +2 -2
- data/spec/internal/db/schema.rb +2 -2
- data/spec/spec_helper.rb +0 -1
- data/spec/support/0-helpers.rb +32 -0
- data/spec/support/database_cleaner.rb +4 -0
- metadata +29 -52
- data/spec/bm.rb +0 -52
- data/spec/schema.rb +0 -82
- /data/lib/acts_as_taggable_on/{acts_as_taggable_on/compatibility.rb → compatibility.rb} +0 -0
- /data/lib/acts_as_taggable_on/{acts_as_taggable_on → taggable}/dirty.rb +0 -0
|
@@ -1,89 +1,16 @@
|
|
|
1
|
+
|
|
1
2
|
require 'active_support/core_ext/module/delegation'
|
|
2
3
|
|
|
3
4
|
module ActsAsTaggableOn
|
|
4
5
|
class TagList < Array
|
|
5
6
|
attr_accessor :owner
|
|
7
|
+
attr_accessor :parser
|
|
6
8
|
|
|
7
9
|
def initialize(*args)
|
|
10
|
+
@parser = ActsAsTaggableOn.default_parser
|
|
8
11
|
add(*args)
|
|
9
12
|
end
|
|
10
13
|
|
|
11
|
-
class << self
|
|
12
|
-
##
|
|
13
|
-
# Returns a new TagList using the given tag string.
|
|
14
|
-
#
|
|
15
|
-
# Example:
|
|
16
|
-
# tag_list = ActsAsTaggableOn::TagList.from("One , Two, Three")
|
|
17
|
-
# tag_list # ["One", "Two", "Three"]
|
|
18
|
-
def from(string)
|
|
19
|
-
string = string.join(ActsAsTaggableOn.glue) if string.respond_to?(:join)
|
|
20
|
-
|
|
21
|
-
new.tap do |tag_list|
|
|
22
|
-
string = string.to_s.dup
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
string.gsub!(double_quote_pattern) {
|
|
26
|
-
# Append the matched tag to the tag list
|
|
27
|
-
tag_list << Regexp.last_match[2]
|
|
28
|
-
# Return the matched delimiter ($3) to replace the matched items
|
|
29
|
-
''
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
string.gsub!(single_quote_pattern) {
|
|
33
|
-
# Append the matched tag ($2) to the tag list
|
|
34
|
-
tag_list << Regexp.last_match[2]
|
|
35
|
-
# Return an empty string to replace the matched items
|
|
36
|
-
''
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
# split the string by the delimiter
|
|
40
|
-
# and add to the tag_list
|
|
41
|
-
tag_list.add(string.split(Regexp.new delimiter))
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def delimiter
|
|
46
|
-
# Parse the quoted tags
|
|
47
|
-
d = ActsAsTaggableOn.delimiter
|
|
48
|
-
# Separate multiple delimiters by bitwise operator
|
|
49
|
-
d = d.join('|') if d.kind_of?(Array)
|
|
50
|
-
|
|
51
|
-
d
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def single_quote_pattern
|
|
55
|
-
%r{
|
|
56
|
-
( # Tag start delimiter ($1)
|
|
57
|
-
\A | # Either string start or
|
|
58
|
-
#{delimiter} # a delimiter
|
|
59
|
-
)
|
|
60
|
-
\s*' # quote (') optionally preceded by whitespace
|
|
61
|
-
(.*?) # Tag ($2)
|
|
62
|
-
'\s* # quote (') optionally followed by whitespace
|
|
63
|
-
(?= # Tag end delimiter (not consumed; is zero-length lookahead)
|
|
64
|
-
#{delimiter}\s* | # Either a delimiter optionally followed by whitespace or
|
|
65
|
-
\z # string end
|
|
66
|
-
)
|
|
67
|
-
}x
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def double_quote_pattern
|
|
71
|
-
%r{
|
|
72
|
-
( # Tag start delimiter ($1)
|
|
73
|
-
\A | # Either string start or
|
|
74
|
-
#{delimiter} # a delimiter
|
|
75
|
-
)
|
|
76
|
-
\s*" # quote (") optionally preceded by whitespace
|
|
77
|
-
(.*?) # Tag ($2)
|
|
78
|
-
"\s* # quote (") optionally followed by whitespace
|
|
79
|
-
(?= # Tag end delimiter (not consumed; is zero-length lookahead)
|
|
80
|
-
#{delimiter}\s* | # Either a delimiter optionally followed by whitespace or
|
|
81
|
-
\z # string end
|
|
82
|
-
)
|
|
83
|
-
}x
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
end
|
|
87
14
|
##
|
|
88
15
|
# Add tags to the tag_list. Duplicate or blank tags will be ignored.
|
|
89
16
|
# Use the <tt>:parse</tt> option to add an unparsed tag string.
|
|
@@ -149,9 +76,10 @@ module ActsAsTaggableOn
|
|
|
149
76
|
|
|
150
77
|
private
|
|
151
78
|
|
|
152
|
-
#
|
|
79
|
+
# Convert everything to string, remove whitespace, duplicates, and blanks.
|
|
153
80
|
def clean!
|
|
154
81
|
reject!(&:blank?)
|
|
82
|
+
map!(&:to_s)
|
|
155
83
|
map!(&:strip)
|
|
156
84
|
map! { |tag| tag.mb_chars.downcase.to_s } if ActsAsTaggableOn.force_lowercase
|
|
157
85
|
map!(&:parameterize) if ActsAsTaggableOn.force_parameterize
|
|
@@ -162,14 +90,27 @@ module ActsAsTaggableOn
|
|
|
162
90
|
|
|
163
91
|
def extract_and_apply_options!(args)
|
|
164
92
|
options = args.last.is_a?(Hash) ? args.pop : {}
|
|
165
|
-
options.assert_valid_keys :parse
|
|
93
|
+
options.assert_valid_keys :parse, :parser
|
|
166
94
|
|
|
167
|
-
|
|
95
|
+
parser = options[:parser] ? options[:parser] : @parser
|
|
96
|
+
|
|
97
|
+
args.map! { |a| parser.new(a).parse } if options[:parse] || options[:parser]
|
|
168
98
|
|
|
169
99
|
args.flatten!
|
|
170
100
|
end
|
|
171
101
|
|
|
172
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
|
+
|
|
173
114
|
end
|
|
174
115
|
end
|
|
175
116
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module ActsAsTaggableOn
|
|
2
|
+
##
|
|
3
|
+
# Returns a new TagList using the given tag string.
|
|
4
|
+
#
|
|
5
|
+
# Example:
|
|
6
|
+
# tag_list = ActsAsTaggableOn::TagListParser.parse("One , Two, Three")
|
|
7
|
+
# tag_list # ["One", "Two", "Three"]
|
|
8
|
+
module TagListParser
|
|
9
|
+
class << self
|
|
10
|
+
## DEPRECATED
|
|
11
|
+
def parse(string)
|
|
12
|
+
ActiveRecord::Base.logger.warn <<WARNING
|
|
13
|
+
ActsAsTaggableOn::TagListParser.parse is deprecated \
|
|
14
|
+
and will be removed from v4.0+, use \
|
|
15
|
+
ActsAsTaggableOn::TagListParser.new instead
|
|
16
|
+
WARNING
|
|
17
|
+
DefaultParser.new(string).parse
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -31,6 +31,7 @@ module ActsAsTaggableOn::Taggable
|
|
|
31
31
|
# to mimic the underlying behavior. While processing this first
|
|
32
32
|
# call to columns, we do the caching column check and dynamically add
|
|
33
33
|
# the class and instance methods
|
|
34
|
+
# FIXME: this method cannot compile in rubinius
|
|
34
35
|
def columns
|
|
35
36
|
@acts_as_taggable_on_cache_columns ||= begin
|
|
36
37
|
db_columns = super
|
|
@@ -53,6 +53,7 @@ module ActsAsTaggableOn::Taggable
|
|
|
53
53
|
# * :order - A piece of SQL to order by. Eg 'tags.count desc' or 'taggings.created_at desc'
|
|
54
54
|
# * :on - Scope the find to only include a certain context
|
|
55
55
|
def all_tags(options = {})
|
|
56
|
+
options = options.dup
|
|
56
57
|
options.assert_valid_keys :start_at, :end_at, :conditions, :order, :limit, :on
|
|
57
58
|
|
|
58
59
|
## Generate conditions:
|
|
@@ -87,6 +88,7 @@ module ActsAsTaggableOn::Taggable
|
|
|
87
88
|
# * :at_most - Exclude tags with a frequency greater than the given value
|
|
88
89
|
# * :on - Scope the find to only include a certain context
|
|
89
90
|
def all_tag_counts(options = {})
|
|
91
|
+
options = options.dup
|
|
90
92
|
options.assert_valid_keys :start_at, :end_at, :conditions, :at_least, :at_most, :order, :limit, :on, :id
|
|
91
93
|
|
|
92
94
|
## Generate conditions:
|
|
@@ -133,7 +135,7 @@ module ActsAsTaggableOn::Taggable
|
|
|
133
135
|
table_name_pkey = "#{table_name}.#{primary_key}"
|
|
134
136
|
if ActsAsTaggableOn::Utils.using_mysql?
|
|
135
137
|
# See https://github.com/mbleigh/acts-as-taggable-on/pull/457 for details
|
|
136
|
-
scoped_ids =
|
|
138
|
+
scoped_ids = pluck(table_name_pkey)
|
|
137
139
|
tagging_scope = tagging_scope.where("#{ActsAsTaggableOn::Tagging.table_name}.taggable_id IN (?)", scoped_ids)
|
|
138
140
|
else
|
|
139
141
|
tagging_scope = tagging_scope.where("#{ActsAsTaggableOn::Tagging.table_name}.taggable_id IN(#{safe_to_sql(select(table_name_pkey))})")
|
|
@@ -24,16 +24,16 @@ module ActsAsTaggableOn::Taggable
|
|
|
24
24
|
# when preserving tag order, include order option so that for a 'tags' context
|
|
25
25
|
# the associations tag_taggings & tags are always returned in created order
|
|
26
26
|
has_many_with_taggable_compatibility context_taggings, as: :taggable,
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
dependent: :destroy,
|
|
28
|
+
class_name: 'ActsAsTaggableOn::Tagging',
|
|
29
|
+
order: taggings_order,
|
|
30
|
+
conditions: ["#{ActsAsTaggableOn::Tagging.table_name}.context = (?)", tags_type],
|
|
31
|
+
include: :tag
|
|
32
32
|
|
|
33
33
|
has_many_with_taggable_compatibility context_tags, through: context_taggings,
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
source: :tag,
|
|
35
|
+
class_name: 'ActsAsTaggableOn::Tag',
|
|
36
|
+
order: taggings_order
|
|
37
37
|
|
|
38
38
|
end
|
|
39
39
|
|
|
@@ -73,16 +73,20 @@ module ActsAsTaggableOn::Taggable
|
|
|
73
73
|
# * <tt>:order_by_matching_tag_count</tt> - if set to true and used with :any, sort by objects matching the most tags, descending
|
|
74
74
|
# * <tt>:match_all</tt> - if set to true, return objects that are *ONLY* tagged with the specified tags
|
|
75
75
|
# * <tt>:owned_by</tt> - return objects that are *ONLY* owned by the owner
|
|
76
|
+
# * <tt>:start_at</tt> - Restrict the tags to those created after a certain time
|
|
77
|
+
# * <tt>:end_at</tt> - Restrict the tags to those created before a certain time
|
|
76
78
|
#
|
|
77
79
|
# Example:
|
|
78
|
-
# User.tagged_with("awesome", "cool") # Users that are tagged with awesome and cool
|
|
79
|
-
# User.tagged_with("awesome", "cool", :exclude => true) # Users that are not tagged with awesome or cool
|
|
80
|
-
# User.tagged_with("awesome", "cool", :any => true) # Users that are tagged with awesome or cool
|
|
81
|
-
# User.tagged_with("awesome", "cool", :any => true, :order_by_matching_tag_count => true) # Sort by users who match the most tags, descending
|
|
82
|
-
# User.tagged_with("awesome", "cool", :match_all => true) # Users that are tagged with just awesome and cool
|
|
83
|
-
# User.tagged_with("awesome", "cool", :owned_by => foo ) # Users that are tagged with just awesome and cool by 'foo'
|
|
80
|
+
# User.tagged_with(["awesome", "cool"]) # Users that are tagged with awesome and cool
|
|
81
|
+
# User.tagged_with(["awesome", "cool"], :exclude => true) # Users that are not tagged with awesome or cool
|
|
82
|
+
# User.tagged_with(["awesome", "cool"], :any => true) # Users that are tagged with awesome or cool
|
|
83
|
+
# User.tagged_with(["awesome", "cool"], :any => true, :order_by_matching_tag_count => true) # Sort by users who match the most tags, descending
|
|
84
|
+
# User.tagged_with(["awesome", "cool"], :match_all => true) # Users that are tagged with just awesome and cool
|
|
85
|
+
# User.tagged_with(["awesome", "cool"], :owned_by => foo ) # Users that are tagged with just awesome and cool by 'foo'
|
|
86
|
+
# User.tagged_with(["awesome", "cool"], :owned_by => foo, :start_at => Date.today ) # Users that are tagged with just awesome, cool by 'foo' and starting today
|
|
84
87
|
def tagged_with(tags, options = {})
|
|
85
|
-
tag_list = ActsAsTaggableOn
|
|
88
|
+
tag_list = ActsAsTaggableOn.default_parser.new(tags).parse
|
|
89
|
+
options = options.dup
|
|
86
90
|
empty_result = where('1 = 0')
|
|
87
91
|
|
|
88
92
|
return empty_result if tag_list.empty?
|
|
@@ -108,14 +112,17 @@ module ActsAsTaggableOn::Taggable
|
|
|
108
112
|
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)})"
|
|
109
113
|
|
|
110
114
|
if owned_by
|
|
111
|
-
joins <<
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
115
|
+
joins << "JOIN #{ActsAsTaggableOn::Tagging.table_name}" +
|
|
116
|
+
" ON #{ActsAsTaggableOn::Tagging.table_name}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" +
|
|
117
|
+
" AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = #{quote_value(base_class.name, nil)}" +
|
|
118
|
+
" AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_id = #{quote_value(owned_by.id, nil)}" +
|
|
119
|
+
" AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_type = #{quote_value(owned_by.class.base_class.to_s, nil)}"
|
|
120
|
+
|
|
121
|
+
joins << " AND " + sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
|
|
122
|
+
joins << " AND " + sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
|
|
116
123
|
end
|
|
117
124
|
|
|
118
|
-
elsif options.delete(:any)
|
|
125
|
+
elsif any = options.delete(:any)
|
|
119
126
|
# get tags, drop out if nothing returned (we need at least one)
|
|
120
127
|
tags = if options.delete(:wild)
|
|
121
128
|
ActsAsTaggableOn::Tag.named_like_any(tag_list)
|
|
@@ -130,49 +137,59 @@ module ActsAsTaggableOn::Taggable
|
|
|
130
137
|
taggings_context = context ? "_#{context}" : ''
|
|
131
138
|
|
|
132
139
|
taggings_alias = adjust_taggings_alias(
|
|
133
|
-
"#{alias_base_name[0..4]}#{taggings_context[0..6]}_taggings_#{sha_prefix(tags.map(&:name).join('_'))}"
|
|
140
|
+
"#{alias_base_name[0..4]}#{taggings_context[0..6]}_taggings_#{ActsAsTaggableOn::Utils.sha_prefix(tags.map(&:name).join('_'))}"
|
|
134
141
|
)
|
|
135
142
|
|
|
136
|
-
tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}"
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
143
|
+
tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
|
|
144
|
+
" ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" +
|
|
145
|
+
" AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name, nil)}"
|
|
146
|
+
|
|
147
|
+
tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
|
|
148
|
+
tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
|
|
149
|
+
|
|
150
|
+
tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
|
|
140
151
|
|
|
141
152
|
# don't need to sanitize sql, map all ids and join with OR logic
|
|
142
153
|
conditions << tags.map { |t| "#{taggings_alias}.tag_id = #{quote_value(t.id, nil)}" }.join(' OR ')
|
|
143
|
-
select_clause
|
|
154
|
+
select_clause << " #{table_name}.*" unless context and tag_types.one?
|
|
144
155
|
|
|
145
156
|
if owned_by
|
|
146
157
|
tagging_join << ' AND ' +
|
|
147
158
|
sanitize_sql([
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
159
|
+
"#{taggings_alias}.tagger_id = ? AND #{taggings_alias}.tagger_type = ?",
|
|
160
|
+
owned_by.id,
|
|
161
|
+
owned_by.class.base_class.to_s
|
|
151
162
|
])
|
|
152
163
|
end
|
|
153
164
|
|
|
154
165
|
joins << tagging_join
|
|
155
|
-
|
|
166
|
+
unless any == 'distinct' # Fix issue #544
|
|
167
|
+
group = "#{table_name}.#{primary_key}"
|
|
168
|
+
select_clause << group
|
|
169
|
+
end
|
|
156
170
|
else
|
|
157
171
|
tags = ActsAsTaggableOn::Tag.named_any(tag_list)
|
|
158
172
|
|
|
159
173
|
return empty_result unless tags.length == tag_list.length
|
|
160
174
|
|
|
161
175
|
tags.each do |tag|
|
|
162
|
-
taggings_alias = adjust_taggings_alias("#{alias_base_name[0..11]}_taggings_#{sha_prefix(tag.name)}")
|
|
176
|
+
taggings_alias = adjust_taggings_alias("#{alias_base_name[0..11]}_taggings_#{ActsAsTaggableOn::Utils.sha_prefix(tag.name)}")
|
|
163
177
|
tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" \
|
|
164
178
|
" ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" +
|
|
165
179
|
" AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name, nil)}" +
|
|
166
180
|
" AND #{taggings_alias}.tag_id = #{quote_value(tag.id, nil)}"
|
|
167
181
|
|
|
168
|
-
tagging_join <<
|
|
182
|
+
tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
|
|
183
|
+
tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
|
|
184
|
+
|
|
185
|
+
tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
|
|
169
186
|
|
|
170
187
|
if owned_by
|
|
171
188
|
tagging_join << ' AND ' +
|
|
172
189
|
sanitize_sql([
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
190
|
+
"#{taggings_alias}.tagger_id = ? AND #{taggings_alias}.tagger_type = ?",
|
|
191
|
+
owned_by.id,
|
|
192
|
+
owned_by.class.base_class.to_s
|
|
176
193
|
])
|
|
177
194
|
end
|
|
178
195
|
|
|
@@ -182,7 +199,7 @@ module ActsAsTaggableOn::Taggable
|
|
|
182
199
|
|
|
183
200
|
group ||= [] # Rails interprets this as a no-op in the group() call below
|
|
184
201
|
if options.delete(:order_by_matching_tag_count)
|
|
185
|
-
select_clause
|
|
202
|
+
select_clause << "#{table_name}.*, COUNT(#{taggings_alias}.tag_id) AS #{taggings_alias}_count"
|
|
186
203
|
group_columns = ActsAsTaggableOn::Utils.using_postgresql? ? grouped_column_names_for(self) : "#{table_name}.#{primary_key}"
|
|
187
204
|
group = group_columns
|
|
188
205
|
order_by << "#{taggings_alias}_count DESC"
|
|
@@ -193,7 +210,9 @@ module ActsAsTaggableOn::Taggable
|
|
|
193
210
|
" ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" \
|
|
194
211
|
" AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name, nil)}"
|
|
195
212
|
|
|
196
|
-
joins <<
|
|
213
|
+
joins << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
|
|
214
|
+
joins << " AND " + sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
|
|
215
|
+
joins << " AND " + sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
|
|
197
216
|
|
|
198
217
|
group_columns = ActsAsTaggableOn::Utils.using_postgresql? ? grouped_column_names_for(self) : "#{table_name}.#{primary_key}"
|
|
199
218
|
group = group_columns
|
|
@@ -202,13 +221,14 @@ module ActsAsTaggableOn::Taggable
|
|
|
202
221
|
|
|
203
222
|
order_by << options[:order] if options[:order].present?
|
|
204
223
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
224
|
+
query = self
|
|
225
|
+
query = self.select(select_clause.join(',')) unless select_clause.empty?
|
|
226
|
+
query.joins(joins.join(' '))
|
|
227
|
+
.where(conditions.join(' AND '))
|
|
228
|
+
.group(group)
|
|
229
|
+
.having(having)
|
|
230
|
+
.order(order_by.join(', '))
|
|
231
|
+
.readonly(false)
|
|
212
232
|
end
|
|
213
233
|
|
|
214
234
|
def is_taggable?
|
|
@@ -258,7 +278,7 @@ module ActsAsTaggableOn::Taggable
|
|
|
258
278
|
if instance_variable_get(variable_name)
|
|
259
279
|
instance_variable_get(variable_name)
|
|
260
280
|
elsif cached_tag_list_on(context) && self.class.caching_tag_list_on?(context)
|
|
261
|
-
instance_variable_set(variable_name, ActsAsTaggableOn
|
|
281
|
+
instance_variable_set(variable_name, ActsAsTaggableOn.default_parser.new(cached_tag_list_on(context)).parse)
|
|
262
282
|
else
|
|
263
283
|
instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(tags_on(context).map(&:name)))
|
|
264
284
|
end
|
|
@@ -308,7 +328,7 @@ module ActsAsTaggableOn::Taggable
|
|
|
308
328
|
variable_name = "@#{context.to_s.singularize}_list"
|
|
309
329
|
process_dirty_object(context, new_list) unless custom_contexts.include?(context.to_s)
|
|
310
330
|
|
|
311
|
-
instance_variable_set(variable_name, ActsAsTaggableOn
|
|
331
|
+
instance_variable_set(variable_name, ActsAsTaggableOn.default_parser.new(new_list).parse)
|
|
312
332
|
end
|
|
313
333
|
|
|
314
334
|
def tagging_contexts
|
|
@@ -322,10 +342,14 @@ module ActsAsTaggableOn::Taggable
|
|
|
322
342
|
if changed_attributes.include?(attrib)
|
|
323
343
|
# The attribute already has an unsaved change.
|
|
324
344
|
old = changed_attributes[attrib]
|
|
325
|
-
changed_attributes.delete(attrib) if old.to_s == value.to_s
|
|
345
|
+
@changed_attributes.delete(attrib) if old.to_s == value.to_s
|
|
326
346
|
else
|
|
327
|
-
old = tag_list_on(context)
|
|
328
|
-
|
|
347
|
+
old = tag_list_on(context)
|
|
348
|
+
if self.class.preserve_tag_order
|
|
349
|
+
@changed_attributes[attrib] = old if old.to_s != value.to_s
|
|
350
|
+
else
|
|
351
|
+
@changed_attributes[attrib] = old.to_s if old.sort != ActsAsTaggableOn.default_parser.new(value).parse.sort
|
|
352
|
+
end
|
|
329
353
|
end
|
|
330
354
|
end
|
|
331
355
|
|
|
@@ -382,7 +406,7 @@ module ActsAsTaggableOn::Taggable
|
|
|
382
406
|
|
|
383
407
|
# Destroy old taggings:
|
|
384
408
|
if old_tags.present?
|
|
385
|
-
|
|
409
|
+
taggings.not_owned.by_context(context).destroy_all(tag_id: old_tags)
|
|
386
410
|
end
|
|
387
411
|
|
|
388
412
|
# Create new taggings:
|
|
@@ -396,6 +420,18 @@ module ActsAsTaggableOn::Taggable
|
|
|
396
420
|
|
|
397
421
|
private
|
|
398
422
|
|
|
423
|
+
# Filters the tag lists from the attribute names.
|
|
424
|
+
def attributes_for_update(attribute_names)
|
|
425
|
+
tag_lists = tag_types.map {|tags_type| "#{tags_type.to_s.singularize}_list"}
|
|
426
|
+
super.delete_if {|attr| tag_lists.include? attr }
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
# Filters the tag lists from the attribute names.
|
|
430
|
+
def attributes_for_create(attribute_names)
|
|
431
|
+
tag_lists = tag_types.map {|tags_type| "#{tags_type.to_s.singularize}_list"}
|
|
432
|
+
super.delete_if {|attr| tag_lists.include? attr }
|
|
433
|
+
end
|
|
434
|
+
|
|
399
435
|
##
|
|
400
436
|
# Override this hook if you wish to subclass {ActsAsTaggableOn::Tag} --
|
|
401
437
|
# context is provided so that you may conditionally use a Tag subclass
|
|
@@ -41,7 +41,7 @@ module ActsAsTaggableOn::Taggable
|
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def related_tags_for(context, klass, options = {})
|
|
44
|
-
tags_to_ignore = Array.wrap(options
|
|
44
|
+
tags_to_ignore = Array.wrap(options[:ignore]).map(&:to_s) || []
|
|
45
45
|
tags_to_find = tags_on(context).map { |t| t.name }.reject { |t| tags_to_ignore.include? t }
|
|
46
46
|
related_where(klass, ["#{exclude_self(klass, id)} #{klass.table_name}.#{klass.primary_key} = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = '#{klass.base_class}' AND #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key} AND #{ActsAsTaggableOn::Tag.table_name}.name IN (?)", tags_to_find])
|
|
47
47
|
end
|
|
@@ -61,11 +61,11 @@ module ActsAsTaggableOn::Taggable
|
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
def related_where(klass, conditions)
|
|
64
|
-
klass.select("#{klass.table_name}.*, COUNT(#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}) AS count")
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
klass.select("#{klass.table_name}.*, COUNT(#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}) AS count")
|
|
65
|
+
.from("#{klass.table_name}, #{ActsAsTaggableOn::Tag.table_name}, #{ActsAsTaggableOn::Tagging.table_name}")
|
|
66
|
+
.group(group_columns(klass))
|
|
67
|
+
.order('count DESC')
|
|
68
|
+
.where(conditions)
|
|
69
69
|
end
|
|
70
70
|
end
|
|
71
71
|
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
module ActsAsTaggableOn
|
|
2
2
|
module Taggable
|
|
3
|
+
|
|
3
4
|
def taggable?
|
|
4
5
|
false
|
|
5
6
|
end
|
|
@@ -85,19 +86,17 @@ module ActsAsTaggableOn
|
|
|
85
86
|
def self.taggable?
|
|
86
87
|
true
|
|
87
88
|
end
|
|
88
|
-
|
|
89
|
-
extend ActsAsTaggableOn::Utils
|
|
90
89
|
end
|
|
91
90
|
end
|
|
92
91
|
|
|
93
92
|
# each of these add context-specific methods and must be
|
|
94
93
|
# called on each call of taggable_on
|
|
95
|
-
include
|
|
96
|
-
include
|
|
97
|
-
include
|
|
98
|
-
include
|
|
99
|
-
include
|
|
100
|
-
include
|
|
94
|
+
include Core
|
|
95
|
+
include Collection
|
|
96
|
+
include Cache
|
|
97
|
+
include Ownership
|
|
98
|
+
include Related
|
|
99
|
+
include Dirty
|
|
101
100
|
end
|
|
102
101
|
end
|
|
103
102
|
end
|
|
@@ -15,6 +15,12 @@ module ActsAsTaggableOn
|
|
|
15
15
|
belongs_to :taggable, polymorphic: true
|
|
16
16
|
belongs_to :tagger, polymorphic: true
|
|
17
17
|
|
|
18
|
+
scope :owned_by, ->(owner) { where(tagger: owner) }
|
|
19
|
+
scope :not_owned, -> { where(tagger_id: nil, tagger_type: nil) }
|
|
20
|
+
|
|
21
|
+
scope :by_contexts, ->(contexts = ['tags']) { where(context: contexts) }
|
|
22
|
+
scope :by_context, ->(context= 'tags') { by_contexts(context.to_s) }
|
|
23
|
+
|
|
18
24
|
validates_presence_of :context
|
|
19
25
|
validates_presence_of :tag_id
|
|
20
26
|
|
|
@@ -26,7 +32,7 @@ module ActsAsTaggableOn
|
|
|
26
32
|
|
|
27
33
|
def remove_unused_tags
|
|
28
34
|
if ActsAsTaggableOn.remove_unused_tags
|
|
29
|
-
tag.destroy if tag.taggings_count.zero?
|
|
35
|
+
tag.destroy if tag.reload.taggings_count.zero?
|
|
30
36
|
end
|
|
31
37
|
end
|
|
32
38
|
end
|
|
@@ -4,10 +4,10 @@ module ActsAsTaggableOn
|
|
|
4
4
|
def tag_cloud(tags, classes)
|
|
5
5
|
return [] if tags.empty?
|
|
6
6
|
|
|
7
|
-
max_count = tags.sort_by(&:
|
|
7
|
+
max_count = tags.sort_by(&:taggings_count).last.taggings_count.to_f
|
|
8
8
|
|
|
9
9
|
tags.each do |tag|
|
|
10
|
-
index = ((tag.
|
|
10
|
+
index = ((tag.taggings_count / max_count) * (classes.size - 1))
|
|
11
11
|
yield tag, classes[index.nan? ? 0 : index.round]
|
|
12
12
|
end
|
|
13
13
|
end
|
|
@@ -1,62 +1,38 @@
|
|
|
1
|
+
# This module is deprecated and will be removed in the incoming versions
|
|
2
|
+
|
|
1
3
|
module ActsAsTaggableOn
|
|
2
4
|
module Utils
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
ActsAsTaggableOn::Tag.connection
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def using_postgresql?
|
|
11
|
-
connection && connection.adapter_name == 'PostgreSQL'
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def postgresql_version
|
|
15
|
-
if using_postgresql?
|
|
16
|
-
connection.execute("SHOW SERVER_VERSION").first["server_version"].to_f
|
|
5
|
+
class << self
|
|
6
|
+
# Use ActsAsTaggableOn::Tag connection
|
|
7
|
+
def connection
|
|
8
|
+
ActsAsTaggableOn::Tag.connection
|
|
17
9
|
end
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def postgresql_support_json?
|
|
21
|
-
postgresql_version >= 9.2
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def using_sqlite?
|
|
25
|
-
connection && connection.adapter_name == 'SQLite'
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def using_mysql?
|
|
29
|
-
#We should probably use regex for mysql to support prehistoric adapters
|
|
30
|
-
connection && connection.adapter_name == 'Mysql2'
|
|
31
|
-
end
|
|
32
10
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def supports_concurrency?
|
|
38
|
-
!using_sqlite?
|
|
39
|
-
end
|
|
11
|
+
def using_postgresql?
|
|
12
|
+
connection && connection.adapter_name == 'PostgreSQL'
|
|
13
|
+
end
|
|
40
14
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
15
|
+
def using_mysql?
|
|
16
|
+
#We should probably use regex for mysql to support prehistoric adapters
|
|
17
|
+
connection && connection.adapter_name == 'Mysql2'
|
|
18
|
+
end
|
|
44
19
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
20
|
+
def sha_prefix(string)
|
|
21
|
+
Digest::SHA1.hexdigest("#{string}#{rand}")[0..6]
|
|
22
|
+
end
|
|
48
23
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
24
|
+
def active_record4?
|
|
25
|
+
::ActiveRecord::VERSION::MAJOR == 4
|
|
26
|
+
end
|
|
52
27
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
28
|
+
def like_operator
|
|
29
|
+
using_postgresql? ? 'ILIKE' : 'LIKE'
|
|
30
|
+
end
|
|
56
31
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
32
|
+
# escape _ and % characters in strings, since these are wildcards in SQL.
|
|
33
|
+
def escape_like(str)
|
|
34
|
+
str.gsub(/[!%_]/) { |x| '!' + x }
|
|
35
|
+
end
|
|
60
36
|
end
|
|
61
37
|
end
|
|
62
38
|
end
|