acts-as-taggable-on 2.0.0 → 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 +7 -0
- data/.gitignore +11 -0
- data/.travis.yml +23 -0
- data/Appraisals +20 -0
- data/CHANGELOG.md +112 -0
- data/CONTRIBUTING.md +44 -0
- data/Gemfile +9 -5
- data/Guardfile +5 -0
- data/{MIT-LICENSE → LICENSE.md} +1 -1
- data/README.md +385 -0
- data/Rakefile +13 -53
- data/UPGRADING.md +13 -0
- data/acts-as-taggable-on.gemspec +38 -0
- data/{lib/generators/acts_as_taggable_on/migration/templates/active_record/migration.rb → db/migrate/1_acts_as_taggable_on_migration.rb} +5 -3
- data/db/migrate/2_add_missing_unique_indices.rb +19 -0
- data/db/migrate/3_add_taggings_counter_cache_to_tags.rb +14 -0
- 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 +65 -22
- data/lib/acts_as_taggable_on/acts_as_taggable_on/cache.rb +53 -24
- data/lib/acts_as_taggable_on/acts_as_taggable_on/collection.rb +120 -42
- data/lib/acts_as_taggable_on/acts_as_taggable_on/compatibility.rb +35 -0
- data/lib/acts_as_taggable_on/acts_as_taggable_on/core.rb +309 -124
- data/lib/acts_as_taggable_on/acts_as_taggable_on/dirty.rb +36 -0
- data/lib/acts_as_taggable_on/acts_as_taggable_on/ownership.rb +96 -72
- data/lib/acts_as_taggable_on/acts_as_taggable_on/related.rb +43 -36
- data/lib/acts_as_taggable_on/engine.rb +5 -0
- data/lib/acts_as_taggable_on/tag.rb +133 -63
- data/lib/acts_as_taggable_on/tag_list.rb +162 -81
- data/lib/acts_as_taggable_on/taggable.rb +101 -0
- data/lib/acts_as_taggable_on/{acts_as_tagger.rb → tagger.rb} +34 -13
- data/lib/acts_as_taggable_on/tagging.rb +28 -18
- data/lib/acts_as_taggable_on/tags_helper.rb +12 -14
- data/lib/acts_as_taggable_on/utils.rb +62 -0
- data/lib/acts_as_taggable_on/version.rb +4 -0
- data/lib/acts_as_taggable_on.rb +6 -0
- data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +189 -195
- data/spec/acts_as_taggable_on/acts_as_tagger_spec.rb +78 -81
- data/spec/acts_as_taggable_on/caching_spec.rb +76 -0
- data/spec/acts_as_taggable_on/related_spec.rb +89 -0
- data/spec/acts_as_taggable_on/single_table_inheritance_spec.rb +210 -0
- data/spec/acts_as_taggable_on/tag_list_spec.rb +149 -61
- data/spec/acts_as_taggable_on/tag_spec.rb +245 -65
- data/spec/acts_as_taggable_on/taggable_spec.rb +776 -187
- data/spec/acts_as_taggable_on/tagger_spec.rb +119 -70
- data/spec/acts_as_taggable_on/tagging_spec.rb +17 -21
- data/spec/acts_as_taggable_on/tags_helper_spec.rb +38 -22
- data/spec/acts_as_taggable_on/utils_spec.rb +15 -0
- 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/internal/app/models/models.rb +90 -0
- 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/internal/config/database.yml.sample +19 -0
- data/spec/internal/db/schema.rb +97 -0
- data/spec/schema.rb +57 -18
- data/spec/spec_helper.rb +12 -47
- data/spec/support/array.rb +9 -0
- data/spec/support/database.rb +42 -0
- data/spec/support/database_cleaner.rb +21 -0
- metadata +286 -63
- data/CHANGELOG +0 -25
- data/README.rdoc +0 -221
- data/VERSION +0 -1
- data/generators/acts_as_taggable_on_migration/acts_as_taggable_on_migration_generator.rb +0 -7
- data/generators/acts_as_taggable_on_migration/templates/migration.rb +0 -29
- data/lib/acts_as_taggable_on/acts_as_taggable_on.rb +0 -53
- data/lib/acts_as_taggable_on/compatibility/Gemfile +0 -6
- data/lib/acts_as_taggable_on/compatibility/active_record_backports.rb +0 -17
- data/lib/generators/acts_as_taggable_on/migration/migration_generator.rb +0 -31
- data/rails/init.rb +0 -1
- data/spec/bm.rb +0 -52
- data/spec/models.rb +0 -30
- /data/{spec/spec.opts → .rspec} +0 -0
|
@@ -1,95 +1,176 @@
|
|
|
1
|
-
|
|
1
|
+
require 'active_support/core_ext/module/delegation'
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
module ActsAsTaggableOn
|
|
4
|
+
class TagList < Array
|
|
5
|
+
attr_accessor :owner
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
def initialize(*args)
|
|
8
|
+
add(*args)
|
|
9
|
+
end
|
|
10
|
+
|
|
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
|
|
7
85
|
|
|
8
|
-
def initialize(*args)
|
|
9
|
-
add(*args)
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
##
|
|
13
|
-
# Returns a new TagList using the given tag string.
|
|
14
|
-
#
|
|
15
|
-
# Example:
|
|
16
|
-
# tag_list = TagList.from("One , Two, Three")
|
|
17
|
-
# tag_list # ["One", "Two", "Three"]
|
|
18
|
-
def self.from(string)
|
|
19
|
-
string = string.join(", ") if string.respond_to?(:join)
|
|
20
|
-
|
|
21
|
-
new.tap do |tag_list|
|
|
22
|
-
string = string.to_s.dup
|
|
23
|
-
|
|
24
|
-
# Parse the quoted tags
|
|
25
|
-
string.gsub!(/(\A|#{delimiter})\s*"(.*?)"\s*(#{delimiter}\s*|\z)/) { tag_list << $2; $3 }
|
|
26
|
-
string.gsub!(/(\A|#{delimiter})\s*'(.*?)'\s*(#{delimiter}\s*|\z)/) { tag_list << $2; $3 }
|
|
27
|
-
|
|
28
|
-
tag_list.add(string.split(delimiter))
|
|
29
86
|
end
|
|
30
|
-
|
|
87
|
+
##
|
|
88
|
+
# Add tags to the tag_list. Duplicate or blank tags will be ignored.
|
|
89
|
+
# Use the <tt>:parse</tt> option to add an unparsed tag string.
|
|
90
|
+
#
|
|
91
|
+
# Example:
|
|
92
|
+
# tag_list.add("Fun", "Happy")
|
|
93
|
+
# tag_list.add("Fun, Happy", :parse => true)
|
|
94
|
+
def add(*names)
|
|
95
|
+
extract_and_apply_options!(names)
|
|
96
|
+
concat(names)
|
|
97
|
+
clean!
|
|
98
|
+
self
|
|
99
|
+
end
|
|
31
100
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
# tag_list.add("Fun, Happy", :parse => true)
|
|
39
|
-
def add(*names)
|
|
40
|
-
extract_and_apply_options!(names)
|
|
41
|
-
concat(names)
|
|
42
|
-
clean!
|
|
43
|
-
self
|
|
44
|
-
end
|
|
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
|
|
45
107
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
# tag_list.remove("Sad", "Lonely")
|
|
52
|
-
# tag_list.remove("Sad, Lonely", :parse => true)
|
|
53
|
-
def remove(*names)
|
|
54
|
-
extract_and_apply_options!(names)
|
|
55
|
-
delete_if { |name| names.include?(name) }
|
|
56
|
-
self
|
|
57
|
-
end
|
|
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
|
|
58
113
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
# Example:
|
|
64
|
-
# tag_list = TagList.new("Round", "Square,Cube")
|
|
65
|
-
# tag_list.to_s # 'Round, "Square,Cube"'
|
|
66
|
-
def to_s
|
|
67
|
-
tags = frozen? ? self.dup : self
|
|
68
|
-
tags.send(:clean!)
|
|
69
|
-
|
|
70
|
-
tags.map do |name|
|
|
71
|
-
name.include?(delimiter) ? "\"#{name}\"" : name
|
|
72
|
-
end.join(delimiter.ends_with?(" ") ? delimiter : "#{delimiter} ")
|
|
73
|
-
end
|
|
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
|
|
74
118
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
119
|
+
##
|
|
120
|
+
# Remove specific tags from the tag_list.
|
|
121
|
+
# Use the <tt>:parse</tt> option to add an unparsed tag string.
|
|
122
|
+
#
|
|
123
|
+
# Example:
|
|
124
|
+
# tag_list.remove("Sad", "Lonely")
|
|
125
|
+
# tag_list.remove("Sad, Lonely", :parse => true)
|
|
126
|
+
def remove(*names)
|
|
127
|
+
extract_and_apply_options!(names)
|
|
128
|
+
delete_if { |name| names.include?(name) }
|
|
129
|
+
self
|
|
130
|
+
end
|
|
83
131
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
132
|
+
##
|
|
133
|
+
# Transform the tag_list into a tag string suitable for editing in a form.
|
|
134
|
+
# The tags are joined with <tt>TagList.delimiter</tt> and quoted if necessary.
|
|
135
|
+
#
|
|
136
|
+
# Example:
|
|
137
|
+
# tag_list = TagList.new("Round", "Square,Cube")
|
|
138
|
+
# tag_list.to_s # 'Round, "Square,Cube"'
|
|
139
|
+
def to_s
|
|
140
|
+
tags = frozen? ? self.dup : self
|
|
141
|
+
tags.send(:clean!)
|
|
87
142
|
|
|
88
|
-
|
|
89
|
-
|
|
143
|
+
tags.map do |name|
|
|
144
|
+
d = ActsAsTaggableOn.delimiter
|
|
145
|
+
d = Regexp.new d.join('|') if d.kind_of? Array
|
|
146
|
+
name.index(d) ? "\"#{name}\"" : name
|
|
147
|
+
end.join(ActsAsTaggableOn.glue)
|
|
90
148
|
end
|
|
91
149
|
|
|
92
|
-
|
|
93
|
-
|
|
150
|
+
private
|
|
151
|
+
|
|
152
|
+
# Convert everything to string, remove whitespace, duplicates, and blanks.
|
|
153
|
+
def clean!
|
|
154
|
+
reject!(&:blank?)
|
|
155
|
+
map!(&:to_s)
|
|
156
|
+
map!(&:strip)
|
|
157
|
+
map! { |tag| tag.mb_chars.downcase.to_s } if ActsAsTaggableOn.force_lowercase
|
|
158
|
+
map!(&:parameterize) if ActsAsTaggableOn.force_parameterize
|
|
159
|
+
|
|
160
|
+
uniq!
|
|
161
|
+
end
|
|
94
162
|
|
|
163
|
+
|
|
164
|
+
def extract_and_apply_options!(args)
|
|
165
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
|
166
|
+
options.assert_valid_keys :parse
|
|
167
|
+
|
|
168
|
+
args.map! { |a| self.class.from(a) } if options[:parse]
|
|
169
|
+
|
|
170
|
+
args.flatten!
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
end
|
|
95
175
|
end
|
|
176
|
+
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
module ActsAsTaggableOn
|
|
2
|
+
module Taggable
|
|
3
|
+
def taggable?
|
|
4
|
+
false
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
##
|
|
8
|
+
# This is an alias for calling <tt>acts_as_taggable_on :tags</tt>.
|
|
9
|
+
#
|
|
10
|
+
# Example:
|
|
11
|
+
# class Book < ActiveRecord::Base
|
|
12
|
+
# acts_as_taggable
|
|
13
|
+
# end
|
|
14
|
+
def acts_as_taggable
|
|
15
|
+
acts_as_taggable_on :tags
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
##
|
|
19
|
+
# This is an alias for calling <tt>acts_as_ordered_taggable_on :tags</tt>.
|
|
20
|
+
#
|
|
21
|
+
# Example:
|
|
22
|
+
# class Book < ActiveRecord::Base
|
|
23
|
+
# acts_as_ordered_taggable
|
|
24
|
+
# end
|
|
25
|
+
def acts_as_ordered_taggable
|
|
26
|
+
acts_as_ordered_taggable_on :tags
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
##
|
|
30
|
+
# Make a model taggable on specified contexts.
|
|
31
|
+
#
|
|
32
|
+
# @param [Array] tag_types An array of taggable contexts
|
|
33
|
+
#
|
|
34
|
+
# Example:
|
|
35
|
+
# class User < ActiveRecord::Base
|
|
36
|
+
# acts_as_taggable_on :languages, :skills
|
|
37
|
+
# end
|
|
38
|
+
def acts_as_taggable_on(*tag_types)
|
|
39
|
+
taggable_on(false, tag_types)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
##
|
|
43
|
+
# Make a model taggable on specified contexts
|
|
44
|
+
# and preserves the order in which tags are created
|
|
45
|
+
#
|
|
46
|
+
# @param [Array] tag_types An array of taggable contexts
|
|
47
|
+
#
|
|
48
|
+
# Example:
|
|
49
|
+
# class User < ActiveRecord::Base
|
|
50
|
+
# acts_as_ordered_taggable_on :languages, :skills
|
|
51
|
+
# end
|
|
52
|
+
def acts_as_ordered_taggable_on(*tag_types)
|
|
53
|
+
taggable_on(true, tag_types)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
# Make a model taggable on specified contexts
|
|
59
|
+
# and optionally preserves the order in which tags are created
|
|
60
|
+
#
|
|
61
|
+
# Separate methods used above for backwards compatibility
|
|
62
|
+
# so that the original acts_as_taggable_on method is unaffected
|
|
63
|
+
# as it's not possible to add another argument to the method
|
|
64
|
+
# without the tag_types being enclosed in square brackets
|
|
65
|
+
#
|
|
66
|
+
# NB: method overridden in core module in order to create tag type
|
|
67
|
+
# associations and methods after this logic has executed
|
|
68
|
+
#
|
|
69
|
+
def taggable_on(preserve_tag_order, *tag_types)
|
|
70
|
+
tag_types = tag_types.to_a.flatten.compact.map(&:to_sym)
|
|
71
|
+
|
|
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
|
|
80
|
+
|
|
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'
|
|
84
|
+
|
|
85
|
+
def self.taggable?
|
|
86
|
+
true
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
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
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -15,23 +15,36 @@ module ActsAsTaggableOn
|
|
|
15
15
|
# end
|
|
16
16
|
def acts_as_tagger(opts={})
|
|
17
17
|
class_eval do
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
has_many_with_taggable_compatibility :owned_taggings,
|
|
19
|
+
opts.merge(
|
|
20
|
+
as: :tagger,
|
|
21
|
+
dependent: :destroy,
|
|
22
|
+
class_name: 'ActsAsTaggableOn::Tagging'
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
has_many_with_taggable_compatibility :owned_tags,
|
|
26
|
+
through: :owned_taggings,
|
|
27
|
+
source: :tag,
|
|
28
|
+
class_name: 'ActsAsTaggableOn::Tag',
|
|
29
|
+
uniq: true
|
|
21
30
|
end
|
|
22
31
|
|
|
23
32
|
include ActsAsTaggableOn::Tagger::InstanceMethods
|
|
24
33
|
extend ActsAsTaggableOn::Tagger::SingletonMethods
|
|
25
34
|
end
|
|
26
35
|
|
|
27
|
-
def
|
|
36
|
+
def tagger?
|
|
28
37
|
false
|
|
29
38
|
end
|
|
39
|
+
|
|
40
|
+
def is_tagger?
|
|
41
|
+
tagger?
|
|
42
|
+
end
|
|
30
43
|
end
|
|
31
44
|
|
|
32
45
|
module InstanceMethods
|
|
33
46
|
##
|
|
34
|
-
# Tag a taggable model with tags that are owned by the tagger.
|
|
47
|
+
# Tag a taggable model with tags that are owned by the tagger.
|
|
35
48
|
#
|
|
36
49
|
# @param taggable The object that will be tagged
|
|
37
50
|
# @param [Hash] options An hash with options. Available options are:
|
|
@@ -41,27 +54,35 @@ module ActsAsTaggableOn
|
|
|
41
54
|
# Example:
|
|
42
55
|
# @user.tag(@photo, :with => "paris, normandy", :on => :locations)
|
|
43
56
|
def tag(taggable, opts={})
|
|
44
|
-
opts.reverse_merge!(:
|
|
45
|
-
|
|
57
|
+
opts.reverse_merge!(force: true)
|
|
58
|
+
skip_save = opts.delete(:skip_save)
|
|
46
59
|
return false unless taggable.respond_to?(:is_taggable?) && taggable.is_taggable?
|
|
47
60
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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])
|
|
51
64
|
|
|
52
65
|
taggable.set_owner_tag_list_on(self, opts[:on].to_s, opts[:with])
|
|
53
|
-
taggable.save
|
|
66
|
+
taggable.save unless skip_save
|
|
54
67
|
end
|
|
55
68
|
|
|
56
|
-
def
|
|
69
|
+
def tagger?
|
|
57
70
|
self.class.is_tagger?
|
|
58
71
|
end
|
|
72
|
+
|
|
73
|
+
def is_tagger?
|
|
74
|
+
tagger?
|
|
75
|
+
end
|
|
59
76
|
end
|
|
60
77
|
|
|
61
78
|
module SingletonMethods
|
|
62
|
-
def
|
|
79
|
+
def tagger?
|
|
63
80
|
true
|
|
64
81
|
end
|
|
82
|
+
|
|
83
|
+
def is_tagger?
|
|
84
|
+
tagger?
|
|
85
|
+
end
|
|
65
86
|
end
|
|
66
87
|
end
|
|
67
88
|
end
|
|
@@ -1,23 +1,33 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
module ActsAsTaggableOn
|
|
2
|
+
class Tagging < ::ActiveRecord::Base #:nodoc:
|
|
3
|
+
#TODO, remove from 4.0.0
|
|
4
|
+
attr_accessible :tag,
|
|
5
|
+
:tag_id,
|
|
6
|
+
:context,
|
|
7
|
+
:taggable,
|
|
8
|
+
:taggable_type,
|
|
9
|
+
:taggable_id,
|
|
10
|
+
:tagger,
|
|
11
|
+
:tagger_type,
|
|
12
|
+
:tagger_id if defined?(ActiveModel::MassAssignmentSecurity)
|
|
3
13
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
:taggable,
|
|
8
|
-
:taggable_type,
|
|
9
|
-
:taggable_id,
|
|
10
|
-
:tagger,
|
|
11
|
-
:tagger_type,
|
|
12
|
-
:tagger_id
|
|
14
|
+
belongs_to :tag, class_name: 'ActsAsTaggableOn::Tag' , counter_cache: true
|
|
15
|
+
belongs_to :taggable, polymorphic: true
|
|
16
|
+
belongs_to :tagger, polymorphic: true
|
|
13
17
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
belongs_to :tagger, :polymorphic => true
|
|
18
|
+
validates_presence_of :context
|
|
19
|
+
validates_presence_of :tag_id
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
validates_presence_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
|
|
|
23
|
-
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def remove_unused_tags
|
|
28
|
+
if ActsAsTaggableOn.remove_unused_tags
|
|
29
|
+
tag.destroy if tag.taggings_count.zero?
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -1,17 +1,15 @@
|
|
|
1
|
-
module
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
yield tag, classes[index]
|
|
1
|
+
module ActsAsTaggableOn
|
|
2
|
+
module TagsHelper
|
|
3
|
+
# See the wiki for an example using tag_cloud.
|
|
4
|
+
def tag_cloud(tags, classes)
|
|
5
|
+
return [] if tags.empty?
|
|
6
|
+
|
|
7
|
+
max_count = tags.sort_by(&:count).last.count.to_f
|
|
8
|
+
|
|
9
|
+
tags.each do |tag|
|
|
10
|
+
index = ((tag.count / max_count) * (classes.size - 1))
|
|
11
|
+
yield tag, classes[index.nan? ? 0 : index.round]
|
|
12
|
+
end
|
|
14
13
|
end
|
|
15
14
|
end
|
|
16
|
-
|
|
17
15
|
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
module ActsAsTaggableOn
|
|
2
|
+
module Utils
|
|
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
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|