acts-as-taggable-on 2.2.2 → 2.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|