acts-as-taggable-on 2.2.2 → 2.3.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.
- data/.gitignore +3 -1
- data/README.rdoc +54 -15
- data/acts-as-taggable-on.gemspec +1 -1
- data/lib/acts-as-taggable-on.rb +26 -1
- data/lib/acts-as-taggable-on/version.rb +1 -1
- data/lib/acts_as_taggable_on/acts_as_taggable_on/core.rb +80 -23
- data/lib/acts_as_taggable_on/acts_as_taggable_on/dirty.rb +37 -0
- data/lib/acts_as_taggable_on/acts_as_taggable_on/ownership.rb +33 -12
- data/lib/acts_as_taggable_on/tag.rb +5 -11
- data/lib/acts_as_taggable_on/tag_list.rb +13 -12
- data/lib/acts_as_taggable_on/taggable.rb +64 -16
- data/lib/acts_as_taggable_on/tagger.rb +3 -3
- data/lib/acts_as_taggable_on/tagging.rb +1 -1
- data/lib/acts_as_taggable_on/utils.rb +1 -1
- data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +58 -0
- data/spec/acts_as_taggable_on/tag_list_spec.rb +84 -65
- data/spec/acts_as_taggable_on/tag_spec.rb +29 -30
- data/spec/acts_as_taggable_on/taggable_spec.rb +137 -6
- data/spec/acts_as_taggable_on/tagger_spec.rb +23 -16
- data/spec/models.rb +5 -0
- data/spec/schema.rb +5 -0
- data/spec/spec_helper.rb +3 -1
- metadata +21 -20
@@ -2,9 +2,6 @@ module ActsAsTaggableOn
|
|
2
2
|
class Tag < ::ActiveRecord::Base
|
3
3
|
include ActsAsTaggableOn::Utils
|
4
4
|
|
5
|
-
cattr_accessor :remove_unused
|
6
|
-
self.remove_unused = false
|
7
|
-
|
8
5
|
attr_accessible :name
|
9
6
|
|
10
7
|
### ASSOCIATIONS:
|
@@ -15,15 +12,16 @@ module ActsAsTaggableOn
|
|
15
12
|
|
16
13
|
validates_presence_of :name
|
17
14
|
validates_uniqueness_of :name
|
15
|
+
validates_length_of :name, :maximum => 255
|
18
16
|
|
19
17
|
### SCOPES:
|
20
18
|
|
21
19
|
def self.named(name)
|
22
|
-
where(["name
|
20
|
+
where(["lower(name) = ?", name.downcase])
|
23
21
|
end
|
24
22
|
|
25
23
|
def self.named_any(list)
|
26
|
-
where(list.map { |tag| sanitize_sql(["name
|
24
|
+
where(list.map { |tag| sanitize_sql(["lower(name) = ?", tag.to_s.downcase]) }.join(" OR "))
|
27
25
|
end
|
28
26
|
|
29
27
|
def self.named_like(name)
|
@@ -69,15 +67,11 @@ module ActsAsTaggableOn
|
|
69
67
|
read_attribute(:count).to_i
|
70
68
|
end
|
71
69
|
|
72
|
-
def safe_name
|
73
|
-
name.gsub(/[^a-zA-Z0-9]/, '')
|
74
|
-
end
|
75
|
-
|
76
70
|
class << self
|
77
71
|
private
|
78
72
|
def comparable_name(str)
|
79
|
-
|
73
|
+
str.mb_chars.downcase.to_s
|
80
74
|
end
|
81
75
|
end
|
82
76
|
end
|
83
|
-
end
|
77
|
+
end
|
@@ -1,14 +1,13 @@
|
|
1
|
+
require 'active_support/core_ext/module/delegation'
|
2
|
+
|
1
3
|
module ActsAsTaggableOn
|
2
4
|
class TagList < Array
|
3
|
-
cattr_accessor :delimiter
|
4
|
-
self.delimiter = ','
|
5
|
-
|
6
5
|
attr_accessor :owner
|
7
6
|
|
8
7
|
def initialize(*args)
|
9
8
|
add(*args)
|
10
9
|
end
|
11
|
-
|
10
|
+
|
12
11
|
##
|
13
12
|
# Returns a new TagList using the given tag string.
|
14
13
|
#
|
@@ -16,17 +15,16 @@ module ActsAsTaggableOn
|
|
16
15
|
# tag_list = TagList.from("One , Two, Three")
|
17
16
|
# tag_list # ["One", "Two", "Three"]
|
18
17
|
def self.from(string)
|
19
|
-
|
20
|
-
string = string.join(glue) if string.respond_to?(:join)
|
18
|
+
string = string.join(ActsAsTaggableOn.glue) if string.respond_to?(:join)
|
21
19
|
|
22
20
|
new.tap do |tag_list|
|
23
21
|
string = string.to_s.dup
|
24
22
|
|
25
23
|
# Parse the quoted tags
|
26
|
-
string.gsub!(/(\A|#{delimiter})\s*"(.*?)"\s*(#{delimiter}\s*|\z)/) { tag_list << $2; $3 }
|
27
|
-
string.gsub!(/(\A|#{delimiter})\s*'(.*?)'\s*(#{delimiter}\s*|\z)/) { tag_list << $2; $3 }
|
24
|
+
string.gsub!(/(\A|#{ActsAsTaggableOn.delimiter})\s*"(.*?)"\s*(#{ActsAsTaggableOn.delimiter}\s*|\z)/) { tag_list << $2; $3 }
|
25
|
+
string.gsub!(/(\A|#{ActsAsTaggableOn.delimiter})\s*'(.*?)'\s*(#{ActsAsTaggableOn.delimiter}\s*|\z)/) { tag_list << $2; $3 }
|
28
26
|
|
29
|
-
tag_list.add(string.split(delimiter))
|
27
|
+
tag_list.add(string.split(ActsAsTaggableOn.delimiter))
|
30
28
|
end
|
31
29
|
end
|
32
30
|
|
@@ -69,16 +67,19 @@ module ActsAsTaggableOn
|
|
69
67
|
tags.send(:clean!)
|
70
68
|
|
71
69
|
tags.map do |name|
|
72
|
-
name.include?(delimiter) ? "\"#{name}\"" : name
|
73
|
-
end.join(
|
70
|
+
name.include?(ActsAsTaggableOn.delimiter) ? "\"#{name}\"" : name
|
71
|
+
end.join(ActsAsTaggableOn.glue)
|
74
72
|
end
|
75
73
|
|
76
74
|
private
|
77
|
-
|
75
|
+
|
78
76
|
# Remove whitespace, duplicates, and blanks.
|
79
77
|
def clean!
|
80
78
|
reject!(&:blank?)
|
81
79
|
map!(&:strip)
|
80
|
+
map!(&:downcase) if ActsAsTaggableOn.force_lowercase
|
81
|
+
map!(&:parameterize) if ActsAsTaggableOn.force_parameterize
|
82
|
+
|
82
83
|
uniq!
|
83
84
|
end
|
84
85
|
|
@@ -15,6 +15,17 @@ module ActsAsTaggableOn
|
|
15
15
|
acts_as_taggable_on :tags
|
16
16
|
end
|
17
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
|
+
|
18
29
|
##
|
19
30
|
# Make a model taggable on specified contexts.
|
20
31
|
#
|
@@ -25,30 +36,67 @@ module ActsAsTaggableOn
|
|
25
36
|
# acts_as_taggable_on :languages, :skills
|
26
37
|
# end
|
27
38
|
def acts_as_taggable_on(*tag_types)
|
28
|
-
|
39
|
+
taggable_on(false, tag_types)
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
##
|
44
|
+
# Make a model taggable on specified contexts
|
45
|
+
# and preserves the order in which tags are created
|
46
|
+
#
|
47
|
+
# @param [Array] tag_types An array of taggable contexts
|
48
|
+
#
|
49
|
+
# Example:
|
50
|
+
# class User < ActiveRecord::Base
|
51
|
+
# acts_as_ordered_taggable_on :languages, :skills
|
52
|
+
# end
|
53
|
+
def acts_as_ordered_taggable_on(*tag_types)
|
54
|
+
taggable_on(true, tag_types)
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
# Make a model taggable on specified contexts
|
60
|
+
# and optionally preserves the order in which tags are created
|
61
|
+
#
|
62
|
+
# Seperate methods used above for backwards compatibility
|
63
|
+
# so that the original acts_as_taggable_on method is unaffected
|
64
|
+
# as it's not possible to add another arguement to the method
|
65
|
+
# without the tag_types being enclosed in square brackets
|
66
|
+
#
|
67
|
+
# NB: method overridden in core module in order to create tag type
|
68
|
+
# associations and methods after this logic has executed
|
69
|
+
#
|
70
|
+
def taggable_on(preserve_tag_order, *tag_types)
|
71
|
+
tag_types = tag_types.to_a.flatten.compact.map(&:to_sym)
|
29
72
|
|
30
|
-
|
73
|
+
if taggable?
|
31
74
|
self.tag_types = (self.tag_types + tag_types).uniq
|
32
|
-
|
75
|
+
self.preserve_tag_order = preserve_tag_order
|
76
|
+
else
|
33
77
|
class_attribute :tag_types
|
34
78
|
self.tag_types = tag_types
|
79
|
+
class_attribute :preserve_tag_order
|
80
|
+
self.preserve_tag_order = preserve_tag_order
|
81
|
+
|
82
|
+
class_eval do
|
83
|
+
has_many :taggings, :as => :taggable, :dependent => :destroy, :include => :tag, :class_name => "ActsAsTaggableOn::Tagging"
|
84
|
+
has_many :base_tags, :through => :taggings, :source => :tag, :class_name => "ActsAsTaggableOn::Tag"
|
35
85
|
|
36
|
-
|
37
|
-
|
38
|
-
|
86
|
+
def self.taggable?
|
87
|
+
true
|
88
|
+
end
|
39
89
|
|
40
|
-
|
41
|
-
|
90
|
+
include ActsAsTaggableOn::Utils
|
91
|
+
include ActsAsTaggableOn::Taggable::Core
|
92
|
+
include ActsAsTaggableOn::Taggable::Collection
|
93
|
+
include ActsAsTaggableOn::Taggable::Cache
|
94
|
+
include ActsAsTaggableOn::Taggable::Ownership
|
95
|
+
include ActsAsTaggableOn::Taggable::Related
|
96
|
+
include ActsAsTaggableOn::Taggable::Dirty
|
42
97
|
end
|
43
|
-
|
44
|
-
include ActsAsTaggableOn::Utils
|
45
|
-
include ActsAsTaggableOn::Taggable::Core
|
46
|
-
include ActsAsTaggableOn::Taggable::Collection
|
47
|
-
include ActsAsTaggableOn::Taggable::Cache
|
48
|
-
include ActsAsTaggableOn::Taggable::Ownership
|
49
|
-
include ActsAsTaggableOn::Taggable::Related
|
50
98
|
end
|
51
99
|
end
|
52
|
-
|
100
|
+
|
53
101
|
end
|
54
102
|
end
|
@@ -31,7 +31,7 @@ module ActsAsTaggableOn
|
|
31
31
|
|
32
32
|
module InstanceMethods
|
33
33
|
##
|
34
|
-
# Tag a taggable model with tags that are owned by the tagger.
|
34
|
+
# Tag a taggable model with tags that are owned by the tagger.
|
35
35
|
#
|
36
36
|
# @param taggable The object that will be tagged
|
37
37
|
# @param [Hash] options An hash with options. Available options are:
|
@@ -42,7 +42,7 @@ module ActsAsTaggableOn
|
|
42
42
|
# @user.tag(@photo, :with => "paris, normandy", :on => :locations)
|
43
43
|
def tag(taggable, opts={})
|
44
44
|
opts.reverse_merge!(:force => true)
|
45
|
-
|
45
|
+
skip_save = opts.delete(:skip_save)
|
46
46
|
return false unless taggable.respond_to?(:is_taggable?) && taggable.is_taggable?
|
47
47
|
|
48
48
|
raise "You need to specify a tag context using :on" unless opts.has_key?(:on)
|
@@ -50,7 +50,7 @@ module ActsAsTaggableOn
|
|
50
50
|
raise "No context :#{opts[:on]} defined in #{taggable.class.to_s}" unless (opts[:force] || taggable.tag_types.include?(opts[:on]))
|
51
51
|
|
52
52
|
taggable.set_owner_tag_list_on(self, opts[:on].to_s, opts[:with])
|
53
|
-
taggable.save
|
53
|
+
taggable.save unless skip_save
|
54
54
|
end
|
55
55
|
|
56
56
|
def is_tagger?
|
@@ -8,6 +8,20 @@ describe "Acts As Taggable On" do
|
|
8
8
|
it "should provide a class method 'taggable?' that is false for untaggable models" do
|
9
9
|
UntaggableModel.should_not be_taggable
|
10
10
|
end
|
11
|
+
|
12
|
+
describe "Taggable Method Generation To Preserve Order" do
|
13
|
+
before(:each) do
|
14
|
+
clean_database!
|
15
|
+
TaggableModel.tag_types = []
|
16
|
+
TaggableModel.preserve_tag_order = false
|
17
|
+
TaggableModel.acts_as_ordered_taggable_on(:ordered_tags)
|
18
|
+
@taggable = TaggableModel.new(:name => "Bob Jones")
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should respond 'true' to preserve_tag_order?" do
|
22
|
+
@taggable.class.preserve_tag_order?.should be_true
|
23
|
+
end
|
24
|
+
end
|
11
25
|
|
12
26
|
describe "Taggable Method Generation" do
|
13
27
|
before(:each) do
|
@@ -32,6 +46,18 @@ describe "Acts As Taggable On" do
|
|
32
46
|
it "should have all tag types" do
|
33
47
|
@taggable.tag_types.should == [:tags, :languages, :skills, :needs, :offerings]
|
34
48
|
end
|
49
|
+
|
50
|
+
it "should create a class attribute for preserve tag order" do
|
51
|
+
@taggable.class.should respond_to(:preserve_tag_order?)
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should create an instance attribute for preserve tag order" do
|
55
|
+
@taggable.should respond_to(:preserve_tag_order?)
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should respond 'false' to preserve_tag_order?" do
|
59
|
+
@taggable.class.preserve_tag_order?.should be_false
|
60
|
+
end
|
35
61
|
|
36
62
|
it "should generate an association for each tag type" do
|
37
63
|
@taggable.should respond_to(:tags, :skills, :languages)
|
@@ -453,4 +479,36 @@ describe "Acts As Taggable On" do
|
|
453
479
|
@taggable.taggings.should == []
|
454
480
|
end
|
455
481
|
end
|
482
|
+
|
483
|
+
describe "@@remove_unused_tags" do
|
484
|
+
before do
|
485
|
+
@taggable = TaggableModel.create(:name => "Bob Jones")
|
486
|
+
@tag = ActsAsTaggableOn::Tag.create(:name => "awesome")
|
487
|
+
|
488
|
+
@tagging = ActsAsTaggableOn::Tagging.create(:taggable => @taggable, :tag => @tag, :context => 'tags')
|
489
|
+
end
|
490
|
+
|
491
|
+
context "if set to true" do
|
492
|
+
before do
|
493
|
+
ActsAsTaggableOn.remove_unused_tags = true
|
494
|
+
end
|
495
|
+
|
496
|
+
it "should remove unused tags after removing taggings" do
|
497
|
+
@tagging.destroy
|
498
|
+
ActsAsTaggableOn::Tag.find_by_name("awesome").should be_nil
|
499
|
+
end
|
500
|
+
end
|
501
|
+
|
502
|
+
context "if set to false" do
|
503
|
+
before do
|
504
|
+
ActsAsTaggableOn.remove_unused_tags = false
|
505
|
+
end
|
506
|
+
|
507
|
+
it "should not remove unused tags after removing taggings" do
|
508
|
+
@tagging.destroy
|
509
|
+
ActsAsTaggableOn::Tag.find_by_name("awesome").should == @tag
|
510
|
+
end
|
511
|
+
end
|
512
|
+
end
|
513
|
+
|
456
514
|
end
|
@@ -1,74 +1,93 @@
|
|
1
1
|
require File.expand_path('../../spec_helper', __FILE__)
|
2
2
|
|
3
3
|
describe ActsAsTaggableOn::TagList do
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
it "should
|
9
|
-
|
10
|
-
end
|
11
|
-
|
12
|
-
it "should be able to be add a new tag word" do
|
13
|
-
@tag_list.add("cool")
|
14
|
-
@tag_list.include?("cool").should be_true
|
15
|
-
end
|
16
|
-
|
17
|
-
it "should be able to add delimited lists of words" do
|
18
|
-
@tag_list.add("cool, wicked", :parse => true)
|
19
|
-
@tag_list.include?("cool").should be_true
|
20
|
-
@tag_list.include?("wicked").should be_true
|
21
|
-
end
|
22
|
-
|
23
|
-
it "should be able to add delimited list of words with quoted delimiters" do
|
24
|
-
@tag_list.add("'cool, wicked', \"really cool, really wicked\"", :parse => true)
|
25
|
-
@tag_list.include?("cool, wicked").should be_true
|
26
|
-
@tag_list.include?("really cool, really wicked").should be_true
|
27
|
-
end
|
28
|
-
|
29
|
-
it "should be able to handle other uses of quotation marks correctly" do
|
30
|
-
@tag_list.add("john's cool car, mary's wicked toy", :parse => true)
|
31
|
-
@tag_list.include?("john's cool car").should be_true
|
32
|
-
@tag_list.include?("mary's wicked toy").should be_true
|
33
|
-
end
|
34
|
-
|
35
|
-
it "should be able to add an array of words" do
|
36
|
-
@tag_list.add(["cool", "wicked"], :parse => true)
|
37
|
-
@tag_list.include?("cool").should be_true
|
38
|
-
@tag_list.include?("wicked").should be_true
|
39
|
-
end
|
40
|
-
|
41
|
-
it "should be able to remove words" do
|
42
|
-
@tag_list.remove("awesome")
|
43
|
-
@tag_list.include?("awesome").should be_false
|
44
|
-
end
|
45
|
-
|
46
|
-
it "should be able to remove delimited lists of words" do
|
47
|
-
@tag_list.remove("awesome, radical", :parse => true)
|
48
|
-
@tag_list.should be_empty
|
49
|
-
end
|
50
|
-
|
51
|
-
it "should be able to remove an array of words" do
|
52
|
-
@tag_list.remove(["awesome", "radical"], :parse => true)
|
53
|
-
@tag_list.should be_empty
|
4
|
+
let(:tag_list) { ActsAsTaggableOn::TagList.new("awesome","radical") }
|
5
|
+
|
6
|
+
it { should be_kind_of Array }
|
7
|
+
|
8
|
+
it "#from should return empty array if empty array is passed" do
|
9
|
+
ActsAsTaggableOn::TagList.from([]).should be_empty
|
54
10
|
end
|
55
|
-
|
56
|
-
|
57
|
-
|
11
|
+
|
12
|
+
describe "#add" do
|
13
|
+
it "should be able to be add a new tag word" do
|
14
|
+
tag_list.add("cool")
|
15
|
+
tag_list.include?("cool").should be_true
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should be able to add delimited lists of words" do
|
19
|
+
tag_list.add("cool, wicked", :parse => true)
|
20
|
+
tag_list.should include("cool", "wicked")
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should be able to add delimited list of words with quoted delimiters" do
|
24
|
+
tag_list.add("'cool, wicked', \"really cool, really wicked\"", :parse => true)
|
25
|
+
tag_list.should include("cool, wicked", "really cool, really wicked")
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should be able to handle other uses of quotation marks correctly" do
|
29
|
+
tag_list.add("john's cool car, mary's wicked toy", :parse => true)
|
30
|
+
tag_list.should include("john's cool car", "mary's wicked toy")
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should be able to add an array of words" do
|
34
|
+
tag_list.add(["cool", "wicked"], :parse => true)
|
35
|
+
tag_list.should include("cool", "wicked")
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should quote escape tags with commas in them" do
|
39
|
+
tag_list.add("cool","rad,bodacious")
|
40
|
+
tag_list.to_s.should == "awesome, radical, cool, \"rad,bodacious\""
|
41
|
+
end
|
42
|
+
|
58
43
|
end
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
44
|
+
|
45
|
+
describe "#remove" do
|
46
|
+
it "should be able to remove words" do
|
47
|
+
tag_list.remove("awesome")
|
48
|
+
tag_list.should_not include("awesome")
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should be able to remove delimited lists of words" do
|
52
|
+
tag_list.remove("awesome, radical", :parse => true)
|
53
|
+
tag_list.should be_empty
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should be able to remove an array of words" do
|
57
|
+
tag_list.remove(["awesome", "radical"], :parse => true)
|
58
|
+
tag_list.should be_empty
|
59
|
+
end
|
63
60
|
end
|
64
|
-
|
65
|
-
|
66
|
-
|
61
|
+
|
62
|
+
describe "#to_s" do
|
63
|
+
it "should give a delimited list of words when converted to string" do
|
64
|
+
tag_list.to_s.should == "awesome, radical"
|
65
|
+
end
|
66
|
+
|
67
|
+
it "should be able to call to_s on a frozen tag list" do
|
68
|
+
tag_list.freeze
|
69
|
+
lambda { tag_list.add("cool","rad,bodacious") }.should raise_error
|
70
|
+
lambda { tag_list.to_s }.should_not raise_error
|
71
|
+
end
|
67
72
|
end
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
+
|
74
|
+
describe "cleaning" do
|
75
|
+
it "should parameterize if force_parameterize is set to true" do
|
76
|
+
ActsAsTaggableOn.force_parameterize = true
|
77
|
+
tag_list = ActsAsTaggableOn::TagList.new("awesome()","radical)(cc")
|
78
|
+
|
79
|
+
tag_list.to_s.should == "awesome, radical-cc"
|
80
|
+
ActsAsTaggableOn.force_parameterize = false
|
81
|
+
end
|
82
|
+
|
83
|
+
it "should lowercase if force_lowercase is set to true" do
|
84
|
+
ActsAsTaggableOn.force_lowercase = true
|
85
|
+
|
86
|
+
tag_list = ActsAsTaggableOn::TagList.new("aweSomE","RaDicaL")
|
87
|
+
tag_list.to_s.should == "awesome, radical"
|
88
|
+
|
89
|
+
ActsAsTaggableOn.force_lowercase = false
|
90
|
+
end
|
91
|
+
|
73
92
|
end
|
74
93
|
end
|