acts-as-taggable-on 3.1.1 → 3.2.2
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/.gitignore +1 -1
- data/.travis.yml +9 -7
- data/Appraisals +13 -8
- data/CHANGELOG.md +27 -7
- data/Gemfile +1 -2
- data/README.md +36 -19
- data/Rakefile +5 -17
- data/UPGRADING.md +6 -0
- data/acts-as-taggable-on.gemspec +15 -15
- data/db/migrate/1_acts_as_taggable_on_migration.rb +3 -3
- data/db/migrate/2_add_missing_unique_indices.rb +3 -5
- data/db/migrate/3_add_taggings_counter_cache_to_tags.rb +1 -1
- data/gemfiles/activerecord_3.2.gemfile +15 -0
- data/gemfiles/activerecord_4.0.gemfile +15 -0
- data/gemfiles/activerecord_4.1.gemfile +15 -0
- data/gemfiles/activerecord_edge.gemfile +16 -0
- data/lib/acts-as-taggable-on.rb +23 -21
- data/lib/acts_as_taggable_on/acts_as_taggable_on/cache.rb +2 -4
- data/lib/acts_as_taggable_on/acts_as_taggable_on/collection.rb +29 -20
- data/lib/acts_as_taggable_on/acts_as_taggable_on/compatibility.rb +11 -10
- data/lib/acts_as_taggable_on/acts_as_taggable_on/core.rb +103 -85
- data/lib/acts_as_taggable_on/acts_as_taggable_on/ownership.rb +5 -12
- data/lib/acts_as_taggable_on/acts_as_taggable_on/related.rb +11 -11
- data/lib/acts_as_taggable_on/engine.rb +0 -1
- data/lib/acts_as_taggable_on/tag.rb +24 -19
- data/lib/acts_as_taggable_on/tag_list.rb +97 -22
- data/lib/acts_as_taggable_on/taggable.rb +26 -30
- data/lib/acts_as_taggable_on/tagger.rb +30 -18
- data/lib/acts_as_taggable_on/tagging.rb +7 -8
- data/lib/acts_as_taggable_on/tags_helper.rb +1 -1
- data/lib/acts_as_taggable_on/utils.rb +57 -35
- data/lib/acts_as_taggable_on/version.rb +1 -1
- data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +133 -138
- data/spec/acts_as_taggable_on/acts_as_tagger_spec.rb +55 -58
- data/spec/acts_as_taggable_on/caching_spec.rb +34 -35
- data/spec/acts_as_taggable_on/related_spec.rb +59 -113
- data/spec/acts_as_taggable_on/single_table_inheritance_spec.rb +118 -95
- data/spec/acts_as_taggable_on/tag_list_spec.rb +89 -57
- data/spec/acts_as_taggable_on/tag_spec.rb +129 -116
- data/spec/acts_as_taggable_on/taggable_spec.rb +539 -352
- data/spec/acts_as_taggable_on/tagger_spec.rb +81 -78
- data/spec/acts_as_taggable_on/tagging_spec.rb +13 -14
- data/spec/acts_as_taggable_on/tags_helper_spec.rb +25 -25
- data/spec/acts_as_taggable_on/utils_spec.rb +7 -13
- data/spec/internal/app/models/altered_inheriting_taggable_model.rb +3 -0
- data/spec/internal/app/models/cached_model.rb +3 -0
- data/spec/internal/app/models/cached_model_with_array.rb +5 -0
- data/spec/internal/app/models/company.rb +15 -0
- data/spec/internal/app/models/inheriting_taggable_model.rb +2 -0
- data/spec/internal/app/models/market.rb +2 -0
- data/spec/{models.rb → internal/app/models/models.rb} +34 -2
- data/spec/internal/app/models/non_standard_id_taggable_model.rb +8 -0
- data/spec/internal/app/models/ordered_taggable_model.rb +4 -0
- data/spec/internal/app/models/other_cached_model.rb +3 -0
- data/spec/internal/app/models/other_taggable_model.rb +4 -0
- data/spec/internal/app/models/student.rb +2 -0
- data/spec/internal/app/models/taggable_model.rb +13 -0
- data/spec/internal/app/models/untaggable_model.rb +3 -0
- data/spec/internal/app/models/user.rb +3 -0
- data/spec/{database.yml.sample → internal/config/database.yml.sample} +2 -2
- data/spec/internal/db/schema.rb +97 -0
- data/spec/schema.rb +11 -0
- data/spec/spec_helper.rb +9 -62
- data/spec/support/array.rb +9 -0
- data/spec/support/database.rb +42 -0
- data/spec/support/database_cleaner.rb +21 -0
- metadata +105 -43
- data/gemfiles/rails_3.2.gemfile +0 -7
- data/gemfiles/rails_4.0.gemfile +0 -7
- data/gemfiles/rails_4.1.gemfile +0 -7
- data/gemfiles/rails_edge.gemfile +0 -7
- data/spec/bm.rb +0 -52
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
# coding: utf-8
|
|
2
2
|
module ActsAsTaggableOn
|
|
3
3
|
class Tag < ::ActiveRecord::Base
|
|
4
|
-
extend ActsAsTaggableOn::Utils
|
|
5
4
|
|
|
6
5
|
attr_accessible :name if defined?(ActiveModel::MassAssignmentSecurity)
|
|
7
6
|
|
|
8
7
|
### ASSOCIATIONS:
|
|
9
8
|
|
|
10
|
-
has_many :taggings, :
|
|
9
|
+
has_many :taggings, dependent: :destroy, class_name: 'ActsAsTaggableOn::Tagging'
|
|
11
10
|
|
|
12
11
|
### VALIDATIONS:
|
|
13
12
|
|
|
14
13
|
validates_presence_of :name
|
|
15
|
-
validates_uniqueness_of :name, :
|
|
16
|
-
validates_length_of :name, :
|
|
14
|
+
validates_uniqueness_of :name, if: :validates_name_uniqueness?
|
|
15
|
+
validates_length_of :name, maximum: 255
|
|
17
16
|
|
|
18
17
|
# monkey patch this method if don't need name uniqueness validation
|
|
19
18
|
def validates_name_uniqueness?
|
|
@@ -26,7 +25,7 @@ module ActsAsTaggableOn
|
|
|
26
25
|
if ActsAsTaggableOn.strict_case_match
|
|
27
26
|
where(["name = #{binary}?", as_8bit_ascii(name)])
|
|
28
27
|
else
|
|
29
|
-
where([
|
|
28
|
+
where(['LOWER(name) = LOWER(?)', as_8bit_ascii(unicode_downcase(name))])
|
|
30
29
|
end
|
|
31
30
|
end
|
|
32
31
|
|
|
@@ -34,35 +33,35 @@ module ActsAsTaggableOn
|
|
|
34
33
|
if ActsAsTaggableOn.strict_case_match
|
|
35
34
|
clause = list.map { |tag|
|
|
36
35
|
sanitize_sql(["name = #{binary}?", as_8bit_ascii(tag)])
|
|
37
|
-
}.join(
|
|
36
|
+
}.join(' OR ')
|
|
38
37
|
where(clause)
|
|
39
38
|
else
|
|
40
39
|
clause = list.map { |tag|
|
|
41
|
-
sanitize_sql([
|
|
42
|
-
}.join(
|
|
40
|
+
sanitize_sql(['LOWER(name) = LOWER(?)', as_8bit_ascii(unicode_downcase(tag))])
|
|
41
|
+
}.join(' OR ')
|
|
43
42
|
where(clause)
|
|
44
43
|
end
|
|
45
44
|
end
|
|
46
45
|
|
|
47
46
|
def self.named_like(name)
|
|
48
|
-
clause = ["name #{like_operator} ? ESCAPE '!'", "%#{escape_like(name)}%"]
|
|
47
|
+
clause = ["name #{ActsAsTaggableOn::Utils.like_operator} ? ESCAPE '!'", "%#{ActsAsTaggableOn::Utils.escape_like(name)}%"]
|
|
49
48
|
where(clause)
|
|
50
49
|
end
|
|
51
50
|
|
|
52
51
|
def self.named_like_any(list)
|
|
53
52
|
clause = list.map { |tag|
|
|
54
|
-
sanitize_sql(["name #{like_operator} ? ESCAPE '!'", "%#{escape_like(tag.to_s)}%"])
|
|
55
|
-
}.join(
|
|
53
|
+
sanitize_sql(["name #{ActsAsTaggableOn::Utils.like_operator} ? ESCAPE '!'", "%#{ActsAsTaggableOn::Utils.escape_like(tag.to_s)}%"])
|
|
54
|
+
}.join(' OR ')
|
|
56
55
|
where(clause)
|
|
57
56
|
end
|
|
58
57
|
|
|
59
58
|
### CLASS METHODS:
|
|
60
59
|
|
|
61
60
|
def self.find_or_create_with_like_by_name(name)
|
|
62
|
-
if
|
|
61
|
+
if ActsAsTaggableOn.strict_case_match
|
|
63
62
|
self.find_or_create_all_with_like_by_name([name]).first
|
|
64
63
|
else
|
|
65
|
-
named_like(name).first || create(:
|
|
64
|
+
named_like(name).first || create(name: name)
|
|
66
65
|
end
|
|
67
66
|
end
|
|
68
67
|
|
|
@@ -71,13 +70,19 @@ module ActsAsTaggableOn
|
|
|
71
70
|
|
|
72
71
|
return [] if list.empty?
|
|
73
72
|
|
|
74
|
-
existing_tags =
|
|
73
|
+
existing_tags = named_any(list)
|
|
75
74
|
|
|
76
75
|
list.map do |tag_name|
|
|
77
76
|
comparable_tag_name = comparable_name(tag_name)
|
|
78
|
-
existing_tag = existing_tags.
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
existing_tag = existing_tags.find { |tag| comparable_name(tag.name) == comparable_tag_name }
|
|
78
|
+
begin
|
|
79
|
+
existing_tag || create(name: tag_name)
|
|
80
|
+
rescue ActiveRecord::RecordNotUnique
|
|
81
|
+
# Postgres aborts the current transaction with
|
|
82
|
+
# PG::InFailedSqlTransaction: ERROR: current transaction is aborted, commands ignored until end of transaction block
|
|
83
|
+
# so we have to rollback this transaction
|
|
84
|
+
raise DuplicateTagError.new("'#{tag_name}' has already been taken")
|
|
85
|
+
end
|
|
81
86
|
end
|
|
82
87
|
end
|
|
83
88
|
|
|
@@ -107,14 +112,14 @@ module ActsAsTaggableOn
|
|
|
107
112
|
end
|
|
108
113
|
|
|
109
114
|
def binary
|
|
110
|
-
using_mysql? ?
|
|
115
|
+
ActsAsTaggableOn::Utils.using_mysql? ? 'BINARY ' : nil
|
|
111
116
|
end
|
|
112
117
|
|
|
113
118
|
def unicode_downcase(string)
|
|
114
119
|
if ActiveSupport::Multibyte::Unicode.respond_to?(:downcase)
|
|
115
120
|
ActiveSupport::Multibyte::Unicode.downcase(string)
|
|
116
121
|
else
|
|
117
|
-
ActiveSupport::Multibyte::Chars.new(string).downcase.to_s
|
|
122
|
+
ActiveSupport::Multibyte::Chars.new(string).downcase.to_s
|
|
118
123
|
end
|
|
119
124
|
end
|
|
120
125
|
|
|
@@ -8,28 +8,82 @@ module ActsAsTaggableOn
|
|
|
8
8
|
add(*args)
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
22
44
|
|
|
45
|
+
def delimiter
|
|
23
46
|
# Parse the quoted tags
|
|
24
47
|
d = ActsAsTaggableOn.delimiter
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
string.gsub!(/(\A|#{d})\s*'(.*?)'\s*(#{d}\s*|\z)/) { tag_list << $2; $3 }
|
|
48
|
+
# Separate multiple delimiters by bitwise operator
|
|
49
|
+
d = d.join('|') if d.kind_of?(Array)
|
|
28
50
|
|
|
29
|
-
|
|
51
|
+
d
|
|
30
52
|
end
|
|
31
|
-
end
|
|
32
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
|
|
33
87
|
##
|
|
34
88
|
# Add tags to the tag_list. Duplicate or blank tags will be ignored.
|
|
35
89
|
# Use the <tt>:parse</tt> option to add an unparsed tag string.
|
|
@@ -44,6 +98,24 @@ module ActsAsTaggableOn
|
|
|
44
98
|
self
|
|
45
99
|
end
|
|
46
100
|
|
|
101
|
+
# Append---Add the tag to the tag_list. This
|
|
102
|
+
# expression returns the tag_list itself, so several appends
|
|
103
|
+
# may be chained together.
|
|
104
|
+
def <<(obj)
|
|
105
|
+
add(obj)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Concatenation --- Returns a new tag list built by concatenating the
|
|
109
|
+
# two tag lists together to produce a third tag list.
|
|
110
|
+
def +(other_tag_list)
|
|
111
|
+
TagList.new.add(self).add(other_tag_list)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Appends the elements of +other_tag_list+ to +self+.
|
|
115
|
+
def concat(other_tag_list)
|
|
116
|
+
super(other_tag_list).send(:clean!)
|
|
117
|
+
end
|
|
118
|
+
|
|
47
119
|
##
|
|
48
120
|
# Remove specific tags from the tag_list.
|
|
49
121
|
# Use the <tt>:parse</tt> option to add an unparsed tag string.
|
|
@@ -70,32 +142,35 @@ module ActsAsTaggableOn
|
|
|
70
142
|
|
|
71
143
|
tags.map do |name|
|
|
72
144
|
d = ActsAsTaggableOn.delimiter
|
|
73
|
-
d = Regexp.new d.join(
|
|
145
|
+
d = Regexp.new d.join('|') if d.kind_of? Array
|
|
74
146
|
name.index(d) ? "\"#{name}\"" : name
|
|
75
147
|
end.join(ActsAsTaggableOn.glue)
|
|
76
148
|
end
|
|
77
149
|
|
|
78
150
|
private
|
|
79
151
|
|
|
80
|
-
#
|
|
152
|
+
# Convert everything to string, remove whitespace, duplicates, and blanks.
|
|
81
153
|
def clean!
|
|
82
154
|
reject!(&:blank?)
|
|
155
|
+
map!(&:to_s)
|
|
83
156
|
map!(&:strip)
|
|
84
|
-
map!{ |tag| tag.mb_chars.downcase.to_s } if ActsAsTaggableOn.force_lowercase
|
|
157
|
+
map! { |tag| tag.mb_chars.downcase.to_s } if ActsAsTaggableOn.force_lowercase
|
|
85
158
|
map!(&:parameterize) if ActsAsTaggableOn.force_parameterize
|
|
86
159
|
|
|
87
160
|
uniq!
|
|
88
161
|
end
|
|
89
162
|
|
|
163
|
+
|
|
90
164
|
def extract_and_apply_options!(args)
|
|
91
165
|
options = args.last.is_a?(Hash) ? args.pop : {}
|
|
92
166
|
options.assert_valid_keys :parse
|
|
93
167
|
|
|
94
|
-
if options[:parse]
|
|
95
|
-
args.map! { |a| self.class.from(a) }
|
|
96
|
-
end
|
|
168
|
+
args.map! { |a| self.class.from(a) } if options[:parse]
|
|
97
169
|
|
|
98
170
|
args.flatten!
|
|
99
171
|
end
|
|
172
|
+
|
|
173
|
+
|
|
100
174
|
end
|
|
101
175
|
end
|
|
176
|
+
|
|
@@ -39,7 +39,6 @@ module ActsAsTaggableOn
|
|
|
39
39
|
taggable_on(false, tag_types)
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
-
|
|
43
42
|
##
|
|
44
43
|
# Make a model taggable on specified contexts
|
|
45
44
|
# and preserves the order in which tags are created
|
|
@@ -59,47 +58,44 @@ module ActsAsTaggableOn
|
|
|
59
58
|
# Make a model taggable on specified contexts
|
|
60
59
|
# and optionally preserves the order in which tags are created
|
|
61
60
|
#
|
|
62
|
-
#
|
|
61
|
+
# Separate methods used above for backwards compatibility
|
|
63
62
|
# so that the original acts_as_taggable_on method is unaffected
|
|
64
|
-
# as it's not possible to add another
|
|
63
|
+
# as it's not possible to add another argument to the method
|
|
65
64
|
# without the tag_types being enclosed in square brackets
|
|
66
65
|
#
|
|
67
66
|
# NB: method overridden in core module in order to create tag type
|
|
68
67
|
# associations and methods after this logic has executed
|
|
69
68
|
#
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if taggable?
|
|
74
|
-
self.tag_types = (self.tag_types + tag_types).uniq
|
|
75
|
-
self.preserve_tag_order = preserve_tag_order
|
|
76
|
-
else
|
|
77
|
-
class_attribute :tag_types
|
|
78
|
-
self.tag_types = tag_types
|
|
79
|
-
class_attribute :preserve_tag_order
|
|
80
|
-
self.preserve_tag_order = preserve_tag_order
|
|
69
|
+
def taggable_on(preserve_tag_order, *tag_types)
|
|
70
|
+
tag_types = tag_types.to_a.flatten.compact.map(&:to_sym)
|
|
81
71
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
72
|
+
if taggable?
|
|
73
|
+
self.tag_types = (self.tag_types + tag_types).uniq
|
|
74
|
+
self.preserve_tag_order = preserve_tag_order
|
|
75
|
+
else
|
|
76
|
+
class_attribute :tag_types
|
|
77
|
+
self.tag_types = tag_types
|
|
78
|
+
class_attribute :preserve_tag_order
|
|
79
|
+
self.preserve_tag_order = preserve_tag_order
|
|
85
80
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
81
|
+
class_eval do
|
|
82
|
+
has_many :taggings, as: :taggable, dependent: :destroy, class_name: 'ActsAsTaggableOn::Tagging'
|
|
83
|
+
has_many :base_tags, through: :taggings, source: :tag, class_name: 'ActsAsTaggableOn::Tag'
|
|
89
84
|
|
|
90
|
-
|
|
85
|
+
def self.taggable?
|
|
86
|
+
true
|
|
91
87
|
end
|
|
92
88
|
end
|
|
93
|
-
|
|
94
|
-
# each of these add context-specific methods and must be
|
|
95
|
-
# called on each call of taggable_on
|
|
96
|
-
include ActsAsTaggableOn::Taggable::Core
|
|
97
|
-
include ActsAsTaggableOn::Taggable::Collection
|
|
98
|
-
include ActsAsTaggableOn::Taggable::Cache
|
|
99
|
-
include ActsAsTaggableOn::Taggable::Ownership
|
|
100
|
-
include ActsAsTaggableOn::Taggable::Related
|
|
101
|
-
include ActsAsTaggableOn::Taggable::Dirty
|
|
102
89
|
end
|
|
103
90
|
|
|
91
|
+
# each of these add context-specific methods and must be
|
|
92
|
+
# called on each call of taggable_on
|
|
93
|
+
include ActsAsTaggableOn::Taggable::Core
|
|
94
|
+
include ActsAsTaggableOn::Taggable::Collection
|
|
95
|
+
include ActsAsTaggableOn::Taggable::Cache
|
|
96
|
+
include ActsAsTaggableOn::Taggable::Ownership
|
|
97
|
+
include ActsAsTaggableOn::Taggable::Related
|
|
98
|
+
include ActsAsTaggableOn::Taggable::Dirty
|
|
99
|
+
end
|
|
104
100
|
end
|
|
105
101
|
end
|
|
@@ -15,27 +15,31 @@ module ActsAsTaggableOn
|
|
|
15
15
|
# end
|
|
16
16
|
def acts_as_tagger(opts={})
|
|
17
17
|
class_eval do
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
has_many_with_taggable_compatibility :owned_taggings,
|
|
19
|
+
opts.merge(
|
|
20
|
+
as: :tagger,
|
|
21
|
+
dependent: :destroy,
|
|
22
|
+
class_name: 'ActsAsTaggableOn::Tagging'
|
|
23
|
+
)
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
has_many_with_taggable_compatibility :owned_tags,
|
|
26
|
+
through: :owned_taggings,
|
|
27
|
+
source: :tag,
|
|
28
|
+
class_name: 'ActsAsTaggableOn::Tag',
|
|
29
|
+
uniq: true
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
include ActsAsTaggableOn::Tagger::InstanceMethods
|
|
33
33
|
extend ActsAsTaggableOn::Tagger::SingletonMethods
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
-
def
|
|
36
|
+
def tagger?
|
|
37
37
|
false
|
|
38
38
|
end
|
|
39
|
+
|
|
40
|
+
def is_tagger?
|
|
41
|
+
tagger?
|
|
42
|
+
end
|
|
39
43
|
end
|
|
40
44
|
|
|
41
45
|
module InstanceMethods
|
|
@@ -50,27 +54,35 @@ module ActsAsTaggableOn
|
|
|
50
54
|
# Example:
|
|
51
55
|
# @user.tag(@photo, :with => "paris, normandy", :on => :locations)
|
|
52
56
|
def tag(taggable, opts={})
|
|
53
|
-
opts.reverse_merge!(:
|
|
57
|
+
opts.reverse_merge!(force: true)
|
|
54
58
|
skip_save = opts.delete(:skip_save)
|
|
55
59
|
return false unless taggable.respond_to?(:is_taggable?) && taggable.is_taggable?
|
|
56
60
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
61
|
+
fail 'You need to specify a tag context using :on' unless opts.key?(:on)
|
|
62
|
+
fail 'You need to specify some tags using :with' unless opts.key?(:with)
|
|
63
|
+
fail "No context :#{opts[:on]} defined in #{taggable.class}" unless opts[:force] || taggable.tag_types.include?(opts[:on])
|
|
60
64
|
|
|
61
65
|
taggable.set_owner_tag_list_on(self, opts[:on].to_s, opts[:with])
|
|
62
66
|
taggable.save unless skip_save
|
|
63
67
|
end
|
|
64
68
|
|
|
65
|
-
def
|
|
69
|
+
def tagger?
|
|
66
70
|
self.class.is_tagger?
|
|
67
71
|
end
|
|
72
|
+
|
|
73
|
+
def is_tagger?
|
|
74
|
+
tagger?
|
|
75
|
+
end
|
|
68
76
|
end
|
|
69
77
|
|
|
70
78
|
module SingletonMethods
|
|
71
|
-
def
|
|
79
|
+
def tagger?
|
|
72
80
|
true
|
|
73
81
|
end
|
|
82
|
+
|
|
83
|
+
def is_tagger?
|
|
84
|
+
tagger?
|
|
85
|
+
end
|
|
74
86
|
end
|
|
75
87
|
end
|
|
76
88
|
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
module ActsAsTaggableOn
|
|
2
2
|
class Tagging < ::ActiveRecord::Base #:nodoc:
|
|
3
|
+
#TODO, remove from 4.0.0
|
|
3
4
|
attr_accessible :tag,
|
|
4
5
|
:tag_id,
|
|
5
6
|
:context,
|
|
@@ -9,14 +10,15 @@ module ActsAsTaggableOn
|
|
|
9
10
|
:tagger,
|
|
10
11
|
:tagger_type,
|
|
11
12
|
:tagger_id if defined?(ActiveModel::MassAssignmentSecurity)
|
|
12
|
-
|
|
13
|
-
belongs_to :
|
|
14
|
-
belongs_to :
|
|
13
|
+
|
|
14
|
+
belongs_to :tag, class_name: 'ActsAsTaggableOn::Tag' , counter_cache: true
|
|
15
|
+
belongs_to :taggable, polymorphic: true
|
|
16
|
+
belongs_to :tagger, polymorphic: true
|
|
15
17
|
|
|
16
18
|
validates_presence_of :context
|
|
17
19
|
validates_presence_of :tag_id
|
|
18
20
|
|
|
19
|
-
validates_uniqueness_of :tag_id, :
|
|
21
|
+
validates_uniqueness_of :tag_id, scope: [:taggable_type, :taggable_id, :context, :tagger_id, :tagger_type]
|
|
20
22
|
|
|
21
23
|
after_destroy :remove_unused_tags
|
|
22
24
|
|
|
@@ -24,10 +26,7 @@ module ActsAsTaggableOn
|
|
|
24
26
|
|
|
25
27
|
def remove_unused_tags
|
|
26
28
|
if ActsAsTaggableOn.remove_unused_tags
|
|
27
|
-
|
|
28
|
-
if tag.taggings.count.zero?
|
|
29
|
-
tag.destroy
|
|
30
|
-
end
|
|
29
|
+
tag.destroy if tag.taggings_count.zero?
|
|
31
30
|
end
|
|
32
31
|
end
|
|
33
32
|
end
|
|
@@ -1,40 +1,62 @@
|
|
|
1
1
|
module ActsAsTaggableOn
|
|
2
2
|
module Utils
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
3
|
+
class << self
|
|
4
|
+
# Use ActsAsTaggableOn::Tag connection
|
|
5
|
+
def connection
|
|
6
|
+
ActsAsTaggableOn::Tag.connection
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def using_postgresql?
|
|
10
|
+
connection && connection.adapter_name == 'PostgreSQL'
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def postgresql_version
|
|
14
|
+
if using_postgresql?
|
|
15
|
+
connection.execute('SHOW SERVER_VERSION').first['server_version'].to_f
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def postgresql_support_json?
|
|
20
|
+
postgresql_version >= 9.2
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def using_sqlite?
|
|
24
|
+
connection && connection.adapter_name == 'SQLite'
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def using_mysql?
|
|
28
|
+
#We should probably use regex for mysql to support prehistoric adapters
|
|
29
|
+
connection && connection.adapter_name == 'Mysql2'
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def using_case_insensitive_collation?
|
|
33
|
+
using_mysql? && connection.collation =~ /_ci\Z/
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def supports_concurrency?
|
|
37
|
+
!using_sqlite?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def sha_prefix(string)
|
|
41
|
+
Digest::SHA1.hexdigest("#{string}#{rand}")[0..6]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def active_record4?
|
|
45
|
+
::ActiveRecord::VERSION::MAJOR == 4
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def active_record42?
|
|
49
|
+
active_record4? && ::ActiveRecord::VERSION::MINOR >= 2
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def like_operator
|
|
53
|
+
using_postgresql? ? 'ILIKE' : 'LIKE'
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# escape _ and % characters in strings, since these are wildcards in SQL.
|
|
57
|
+
def escape_like(str)
|
|
58
|
+
str.gsub(/[!%_]/) { |x| '!' + x }
|
|
59
|
+
end
|
|
38
60
|
end
|
|
39
61
|
end
|
|
40
62
|
end
|