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.
@@ -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 #{like_operator} ? ESCAPE '!'", escape_like(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 #{like_operator} ? ESCAPE '!'", escape_like(tag.to_s)]) }.join(" OR "))
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
- RUBY_VERSION >= "1.9" ? str.downcase : str.mb_chars.downcase
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
- glue = delimiter.ends_with?(" ") ? delimiter : "#{delimiter} "
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(delimiter.ends_with?(" ") ? delimiter : "#{delimiter} ")
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
- tag_types = tag_types.to_a.flatten.compact.map(&:to_sym)
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
- if taggable?
73
+ if taggable?
31
74
  self.tag_types = (self.tag_types + tag_types).uniq
32
- else
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
- class_eval do
37
- has_many :taggings, :as => :taggable, :dependent => :destroy, :include => :tag, :class_name => "ActsAsTaggableOn::Tagging"
38
- has_many :base_tags, :through => :taggings, :source => :tag, :class_name => "ActsAsTaggableOn::Tag"
86
+ def self.taggable?
87
+ true
88
+ end
39
89
 
40
- def self.taggable?
41
- true
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
- end
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?
@@ -24,7 +24,7 @@ module ActsAsTaggableOn
24
24
  private
25
25
 
26
26
  def remove_unused_tags
27
- if Tag.remove_unused
27
+ if ActsAsTaggableOn.remove_unused_tags
28
28
  if tag.taggings.count.zero?
29
29
  tag.destroy
30
30
  end
@@ -16,7 +16,7 @@ module ActsAsTaggableOn
16
16
  end
17
17
 
18
18
  def sha_prefix(string)
19
- Digest::SHA1.hexdigest(string + Time.now.to_s)[0..6]
19
+ Digest::SHA1.hexdigest("#{string}#{rand}")[0..6]
20
20
  end
21
21
 
22
22
  private
@@ -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
- before(:each) do
5
- @tag_list = ActsAsTaggableOn::TagList.new("awesome","radical")
6
- end
7
-
8
- it "should be an array" do
9
- @tag_list.is_a?(Array).should be_true
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
- it "should give a delimited list of words when converted to string" do
57
- @tag_list.to_s.should == "awesome, radical"
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
- it "should quote escape tags with commas in them" do
61
- @tag_list.add("cool","rad,bodacious")
62
- @tag_list.to_s.should == "awesome, radical, cool, \"rad,bodacious\""
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
- it "#from should return empty array if empty array is passed" do
66
- ActsAsTaggableOn::TagList.from([]).should == []
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
- it "should be able to call to_s on a frozen tag list" do
70
- @tag_list.freeze
71
- lambda { @tag_list.add("cool","rad,bodacious") }.should raise_error
72
- lambda { @tag_list.to_s }.should_not raise_error
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