acts-as-taggable-on 1.0.13 → 2.0.0.pre1

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.
@@ -1,26 +1,53 @@
1
1
  class Tag < ActiveRecord::Base
2
+
3
+ attr_accessible :name
4
+
5
+ ### ASSOCIATIONS:
6
+
2
7
  has_many :taggings, :dependent => :destroy
3
-
8
+
9
+ ### VALIDATIONS:
10
+
4
11
  validates_presence_of :name
5
12
  validates_uniqueness_of :name
6
-
7
- named_scope :named, lambda { |name| { :conditions => ["name = ?", name] } }
8
- named_scope :named_like, lambda { |name| { :conditions => ["name LIKE ?", "%#{name}%"] } }
9
-
10
- # LIKE is used for cross-database case-insensitivity
13
+
14
+ ### NAMED SCOPES:
15
+
16
+ scope :named, lambda { |name| { :conditions => ["name LIKE ?", name] } }
17
+ scope :named_any, lambda { |list| { :conditions => list.map { |tag| sanitize_sql(["name LIKE ?", tag.to_s]) }.join(" OR ") } }
18
+ scope :named_like, lambda { |name| { :conditions => ["name LIKE ?", "%#{name}%"] } }
19
+ scope :named_like_any, lambda { |list| { :conditions => list.map { |tag| sanitize_sql(["name LIKE ?", "%#{tag.to_s}%"]) }.join(" OR ") } }
20
+
21
+ ### CLASS METHODS:
22
+
11
23
  def self.find_or_create_with_like_by_name(name)
12
- find(:first, :conditions => ["name LIKE ?", name]) || create(:name => name)
24
+ named_like(name).first || create(:name => name)
13
25
  end
14
-
26
+
27
+ def self.find_or_create_all_with_like_by_name(*list)
28
+ list = [list].flatten
29
+
30
+ return [] if list.empty?
31
+
32
+ existing_tags = Tag.named_any(list).all
33
+ new_tag_names = list.reject { |name| existing_tags.any? { |tag| tag.name.downcase == name.downcase } }
34
+ created_tags = new_tag_names.map { |name| Tag.create(:name => name) }
35
+
36
+ existing_tags + created_tags
37
+ end
38
+
39
+ ### INSTANCE METHODS:
40
+
15
41
  def ==(object)
16
42
  super || (object.is_a?(Tag) && name == object.name)
17
43
  end
18
-
44
+
19
45
  def to_s
20
46
  name
21
47
  end
22
-
48
+
23
49
  def count
24
50
  read_attribute(:count).to_i
25
51
  end
52
+
26
53
  end
@@ -1,17 +1,19 @@
1
1
  class TagList < Array
2
+
2
3
  cattr_accessor :delimiter
4
+
3
5
  self.delimiter = ','
4
-
6
+
5
7
  def initialize(*args)
6
8
  add(*args)
7
9
  end
8
-
10
+
9
11
  attr_accessor :owner
10
-
12
+
11
13
  # Add tags to the tag_list. Duplicate or blank tags will be ignored.
12
14
  #
13
15
  # tag_list.add("Fun", "Happy")
14
- #
16
+ #
15
17
  # Use the <tt>:parse</tt> option to add an unparsed tag string.
16
18
  #
17
19
  # tag_list.add("Fun, Happy", :parse => true)
@@ -21,33 +23,34 @@ class TagList < Array
21
23
  clean!
22
24
  self
23
25
  end
24
-
26
+
25
27
  # Remove specific tags from the tag_list.
26
- #
28
+ #
27
29
  # tag_list.remove("Sad", "Lonely")
28
30
  #
29
31
  # Like #add, the <tt>:parse</tt> option can be used to remove multiple tags in a string.
30
- #
32
+ #
31
33
  # tag_list.remove("Sad, Lonely", :parse => true)
32
34
  def remove(*names)
33
35
  extract_and_apply_options!(names)
34
36
  delete_if { |name| names.include?(name) }
35
37
  self
36
38
  end
37
-
39
+
38
40
  # Transform the tag_list into a tag string suitable for edting in a form.
39
41
  # The tags are joined with <tt>TagList.delimiter</tt> and quoted if necessary.
40
42
  #
41
43
  # tag_list = TagList.new("Round", "Square,Cube")
42
44
  # tag_list.to_s # 'Round, "Square,Cube"'
43
45
  def to_s
44
- clean!
45
-
46
- map do |name|
46
+ tags = frozen? ? self.dup : self
47
+ tags.send(:clean!)
48
+
49
+ tags.map do |name|
47
50
  name.include?(delimiter) ? "\"#{name}\"" : name
48
51
  end.join(delimiter.ends_with?(" ") ? delimiter : "#{delimiter} ")
49
52
  end
50
-
53
+
51
54
  private
52
55
  # Remove whitespace, duplicates, and blanks.
53
56
  def clean!
@@ -55,41 +58,38 @@ class TagList < Array
55
58
  map!(&:strip)
56
59
  uniq!
57
60
  end
58
-
61
+
59
62
  def extract_and_apply_options!(args)
60
63
  options = args.last.is_a?(Hash) ? args.pop : {}
61
64
  options.assert_valid_keys :parse
62
-
65
+
63
66
  if options[:parse]
64
67
  args.map! { |a| self.class.from(a) }
65
68
  end
66
-
69
+
67
70
  args.flatten!
68
71
  end
69
-
72
+
70
73
  class << self
74
+
71
75
  # Returns a new TagList using the given tag string.
72
- #
76
+ #
73
77
  # tag_list = TagList.from("One , Two, Three")
74
78
  # tag_list # ["One", "Two", "Three"]
75
79
  def from(string)
76
80
  string = string.join(", ") if string.respond_to?(:join)
77
81
 
78
- returning new do |tag_list|
82
+ new.tap do |tag_list|
79
83
  string = string.to_s.dup
80
-
84
+
81
85
  # Parse the quoted tags
82
- string.gsub!(/"(.*?)"\s*#{delimiter}?\s*/) { tag_list << $1; "" }
83
- string.gsub!(/'(.*?)'\s*#{delimiter}?\s*/) { tag_list << $1; "" }
84
-
86
+ string.gsub!(/(\A|#{delimiter})\s*"(.*?)"\s*(#{delimiter}\s*|\z)/) { tag_list << $2; $3 }
87
+ string.gsub!(/(\A|#{delimiter})\s*'(.*?)'\s*(#{delimiter}\s*|\z)/) { tag_list << $2; $3 }
88
+
85
89
  tag_list.add(string.split(delimiter))
86
90
  end
87
91
  end
88
-
89
- def from_owner(owner, *tags)
90
- returning from(*tags) do |taglist|
91
- taglist.owner = owner
92
- end
93
- end
92
+
94
93
  end
95
- end
94
+
95
+ end
@@ -1,8 +1,22 @@
1
1
  class Tagging < ActiveRecord::Base #:nodoc:
2
+
3
+ attr_accessible :tag,
4
+ :tag_id,
5
+ :context,
6
+ :taggable,
7
+ :taggable_type,
8
+ :taggable_id,
9
+ :tagger,
10
+ :tagger_type,
11
+ :tagger_id
12
+
2
13
  belongs_to :tag
3
14
  belongs_to :taggable, :polymorphic => true
4
- belongs_to :tagger, :polymorphic => true
5
-
15
+ belongs_to :tagger, :polymorphic => true
16
+
6
17
  validates_presence_of :context
7
18
  validates_presence_of :tag_id
19
+
20
+ validates_uniqueness_of :tag_id, :scope => [ :taggable_type, :taggable_id, :context, :tagger_id, :tagger_type ]
21
+
8
22
  end
@@ -1,11 +1,17 @@
1
1
  module TagsHelper
2
+
2
3
  # See the README for an example using tag_cloud.
3
4
  def tag_cloud(tags, classes)
5
+ tags = tags.all if tags.is_a? ActiveRecord::Relation
6
+
7
+ return [] if tags.empty?
8
+
4
9
  max_count = tags.sort_by(&:count).last.count.to_f
5
-
10
+
6
11
  tags.each do |tag|
7
12
  index = ((tag.count / max_count) * (classes.size - 1)).round
8
13
  yield tag, classes[index]
9
14
  end
10
15
  end
11
- end
16
+
17
+ end
@@ -0,0 +1,31 @@
1
+ require 'rails/generators/migration'
2
+
3
+ module ActsAsTaggableOn
4
+ class MigrationGenerator < Rails::Generators::Base
5
+ include Rails::Generators::Migration
6
+
7
+ desc "Generates migration for Tag and Tagging models"
8
+
9
+ def self.orm
10
+ Rails::Generators.options[:rails][:orm]
11
+ end
12
+
13
+ def self.source_root
14
+ File.join(File.dirname(__FILE__), 'templates', orm)
15
+ end
16
+
17
+ def self.orm_has_migration?
18
+ [:active_record].include? orm
19
+ end
20
+
21
+ def self.next_migration_number(path)
22
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
23
+ end
24
+
25
+ def create_migration_file
26
+ if self.class.orm_has_migration?
27
+ migration_template 'migration.rb', 'db/migrate/acts_as_taggable_on_migration'
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,28 @@
1
+ class ActsAsTaggableOnMigration < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :tags do |t|
4
+ t.string :name
5
+ end
6
+
7
+ create_table :taggings do |t|
8
+ t.references :tag
9
+
10
+ # You should make sure that the column created is
11
+ # long enough to store the required class names.
12
+ t.references :taggable, :polymorphic => true
13
+ t.references :tagger, :polymorphic => true
14
+
15
+ t.string :context
16
+
17
+ t.datetime :created_at
18
+ end
19
+
20
+ add_index :taggings, :tag_id
21
+ add_index :taggings, [:taggable_id, :taggable_type, :context]
22
+ end
23
+
24
+ def self.down
25
+ drop_table :taggings
26
+ drop_table :tags
27
+ end
28
+ end
@@ -1,6 +1,10 @@
1
1
  require File.dirname(__FILE__) + '/../spec_helper'
2
2
 
3
3
  describe "Acts As Taggable On" do
4
+ before(:each) do
5
+ clean_database!
6
+ end
7
+
4
8
  it "should provide a class method 'taggable?' that is false for untaggable models" do
5
9
  UntaggableModel.should_not be_taggable
6
10
  end
@@ -18,7 +22,7 @@ describe "Acts As Taggable On" do
18
22
  it "should create a class attribute for tag types" do
19
23
  @taggable.class.should respond_to(:tag_types)
20
24
  end
21
-
25
+
22
26
  it "should create an instance attribute for tag types" do
23
27
  @taggable.should respond_to(:tag_types)
24
28
  end
@@ -32,7 +36,7 @@ describe "Acts As Taggable On" do
32
36
  end
33
37
 
34
38
  it "should add tagged_with and tag_counts to singleton" do
35
- TaggableModel.should respond_to(:find_tagged_with, :tag_counts)
39
+ TaggableModel.should respond_to(:tagged_with, :tag_counts)
36
40
  end
37
41
 
38
42
  it "should add saving of tag lists and cached tag lists to the instance" do
@@ -176,16 +180,6 @@ describe "Acts As Taggable On" do
176
180
  end
177
181
 
178
182
  describe 'Tagging Contexts' do
179
- before(:all) do
180
- class Array
181
- def freq
182
- k=Hash.new(0)
183
- self.each {|e| k[e]+=1}
184
- k
185
- end
186
- end
187
- end
188
-
189
183
  it 'should eliminate duplicate tagging contexts ' do
190
184
  TaggableModel.acts_as_taggable_on(:skills, :skills)
191
185
  TaggableModel.tag_types.freq[:skills].should_not == 3
@@ -212,10 +206,6 @@ describe "Acts As Taggable On" do
212
206
  TaggableModel.acts_as_taggable_on([nil])
213
207
  }.should_not raise_error
214
208
  end
215
-
216
- after(:all) do
217
- class Array; remove_method :freq; end
218
- end
219
209
  end
220
210
 
221
- end
211
+ end
@@ -1,8 +1,11 @@
1
1
  require File.dirname(__FILE__) + '/../spec_helper'
2
2
 
3
3
  describe "acts_as_tagger" do
4
+ before(:each) do
5
+ clean_database!
6
+ end
7
+
4
8
  context "Tagger Method Generation" do
5
-
6
9
  before(:each) do
7
10
  @tagger = TaggableUser.new()
8
11
  end
@@ -48,8 +51,22 @@ describe "acts_as_tagger" do
48
51
 
49
52
  it 'should by default create the tag context on-the-fly' do
50
53
  @taggable.tag_list_on(:here_ond_now).should be_empty
51
- @tagger.tag(@taggable, :with=>'that', :on=>:here_ond_now)
52
- @taggable.tag_list_on(:here_ond_now).should include('that')
54
+ @tagger.tag(@taggable, :with=>'that', :on => :here_ond_now)
55
+ @taggable.tag_list_on(:here_ond_now).should_not include('that')
56
+ @taggable.all_tags_list_on(:here_ond_now).should include('that')
57
+ end
58
+
59
+ it "should show all the tag list when both public and owned tags exist" do
60
+ @taggable.tag_list = 'ruby, python'
61
+ @tagger.tag(@taggable, :with => 'java, lisp', :on => :tags)
62
+ @taggable.all_tags_on(:tags).map(&:name).sort.should == %w(ruby python java lisp).sort
63
+ end
64
+
65
+ it "should not add owned tags to the common list" do
66
+ @taggable.tag_list = 'ruby, python'
67
+ @tagger.tag(@taggable, :with => 'java, lisp', :on => :foo)
68
+ @tagger.tag(@taggable, :with => '', :on => :foo)
69
+ @taggable.tag_list.should == %w(ruby python)
53
70
  end
54
71
 
55
72
  it "should throw an exception when the default is over-ridden" do
@@ -64,9 +81,33 @@ describe "acts_as_tagger" do
64
81
  @tagger.tag(@taggable, :with=>'this, and, that', :on=>:foo_boo, :force=>false) rescue
65
82
  @taggable.tag_list_on(:foo_boo).should be_empty
66
83
  end
84
+ end
85
+
86
+ context "when called by multiple tagger's" do
87
+ before(:each) do
88
+ @user_x = TaggableUser.create(:name => "User X")
89
+ @user_y = TaggableUser.create(:name => "User Y")
90
+ @taggable = TaggableModel.create(:name => 'acts_as_taggable_on', :tag_list => 'plugin')
91
+
92
+ @user_x.tag(@taggable, :with => 'ruby, rails', :on => :tags)
93
+ @user_y.tag(@taggable, :with => 'ruby, plugin', :on => :tags)
67
94
 
95
+ @user_y.tag(@taggable, :with => '', :on => :tags)
96
+ @user_y.tag(@taggable, :with => '', :on => :tags)
97
+ end
98
+
99
+ it "should delete owned tags" do
100
+ @user_y.owned_tags.should == []
101
+ end
102
+
103
+ it "should not delete other taggers tags" do
104
+ @user_x.owned_tags.should have(2).items
105
+ end
106
+
107
+ it "should not delete original tags" do
108
+ @taggable.all_tags_list_on(:tags).should include('plugin')
109
+ end
68
110
  end
69
-
70
111
  end
71
112
 
72
113
  end
@@ -1,6 +1,9 @@
1
1
  require File.dirname(__FILE__) + '/../spec_helper'
2
2
 
3
3
  describe "Group Helper" do
4
+ before(:each) do
5
+ clean_database!
6
+ end
4
7
 
5
8
  describe "grouped_column_names_for method" do
6
9
  before(:each) do
@@ -20,6 +20,18 @@ describe TagList do
20
20
  @tag_list.include?("wicked").should be_true
21
21
  end
22
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
+
23
35
  it "should be able to add an array of words" do
24
36
  @tag_list.add(["cool", "wicked"], :parse => true)
25
37
  @tag_list.include?("cool").should be_true
@@ -49,4 +61,10 @@ describe TagList do
49
61
  @tag_list.add("cool","rad,bodacious")
50
62
  @tag_list.to_s.should == "awesome, radical, cool, \"rad,bodacious\""
51
63
  end
64
+
65
+ it "should be able to call to_s on a frozen tag list" do
66
+ @tag_list.freeze
67
+ lambda { @tag_list.add("cool","rad,bodacious") }.should raise_error
68
+ lambda { @tag_list.to_s }.should_not raise_error
69
+ end
52
70
  end
@@ -2,25 +2,36 @@ require File.dirname(__FILE__) + '/../spec_helper'
2
2
 
3
3
  describe Tag do
4
4
  before(:each) do
5
+ clean_database!
5
6
  @tag = Tag.new
6
- @user = TaggableModel.create(:name => "Pablo")
7
- Tag.delete_all
7
+ @user = TaggableModel.create(:name => "Pablo")
8
8
  end
9
-
9
+
10
+ describe "named like any" do
11
+ before(:each) do
12
+ Tag.create(:name => "awesome")
13
+ Tag.create(:name => "epic")
14
+ end
15
+
16
+ it "should find both tags" do
17
+ Tag.named_like_any(["awesome", "epic"]).should have(2).items
18
+ end
19
+ end
20
+
10
21
  describe "find or create by name" do
11
22
  before(:each) do
12
23
  @tag.name = "awesome"
13
24
  @tag.save
14
25
  end
15
-
26
+
16
27
  it "should find by name" do
17
28
  Tag.find_or_create_with_like_by_name("awesome").should == @tag
18
29
  end
19
-
30
+
20
31
  it "should find by name case insensitive" do
21
32
  Tag.find_or_create_with_like_by_name("AWESOME").should == @tag
22
33
  end
23
-
34
+
24
35
  it "should create by name" do
25
36
  lambda {
26
37
  Tag.find_or_create_with_like_by_name("epic")
@@ -28,35 +39,66 @@ describe Tag do
28
39
  end
29
40
  end
30
41
 
42
+ describe "find or create all by any name" do
43
+ before(:each) do
44
+ @tag.name = "awesome"
45
+ @tag.save
46
+ end
47
+
48
+ it "should find by name" do
49
+ Tag.find_or_create_all_with_like_by_name("awesome").should == [@tag]
50
+ end
51
+
52
+ it "should find by name case insensitive" do
53
+ Tag.find_or_create_all_with_like_by_name("AWESOME").should == [@tag]
54
+ end
55
+
56
+ it "should create by name" do
57
+ lambda {
58
+ Tag.find_or_create_all_with_like_by_name("epic")
59
+ }.should change(Tag, :count).by(1)
60
+ end
61
+
62
+ it "should find or create by name" do
63
+ lambda {
64
+ Tag.find_or_create_all_with_like_by_name("awesome", "epic").map(&:name).should == ["awesome", "epic"]
65
+ }.should change(Tag, :count).by(1)
66
+ end
67
+
68
+ it "should return an empty array if no tags are specified" do
69
+ Tag.find_or_create_all_with_like_by_name([]).should == []
70
+ end
71
+ end
72
+
31
73
  it "should require a name" do
32
74
  @tag.valid?
33
- @tag.errors.on(:name).should == "can't be blank"
75
+ @tag.errors[:name].should == ["can't be blank"]
34
76
  @tag.name = "something"
35
77
  @tag.valid?
36
- @tag.errors.on(:name).should be_nil
78
+ @tag.errors[:name].should == []
37
79
  end
38
-
80
+
39
81
  it "should equal a tag with the same name" do
40
82
  @tag.name = "awesome"
41
83
  new_tag = Tag.new(:name => "awesome")
42
84
  new_tag.should == @tag
43
85
  end
44
-
86
+
45
87
  it "should return its name when to_s is called" do
46
88
  @tag.name = "cool"
47
89
  @tag.to_s.should == "cool"
48
90
  end
49
-
91
+
50
92
  it "have named_scope named(something)" do
51
93
  @tag.name = "cool"
52
94
  @tag.save!
53
95
  Tag.named('cool').should include(@tag)
54
96
  end
55
-
97
+
56
98
  it "have named_scope named_like(something)" do
57
99
  @tag.name = "cool"
58
100
  @tag.save!
59
101
  @another_tag = Tag.create!(:name => "coolip")
60
102
  Tag.named_like('cool').should include(@tag, @another_tag)
61
103
  end
62
- end
104
+ end