slillibri-acts-as-taggable-on 1.0.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,52 @@
1
+ module ActiveRecord
2
+ module Acts
3
+ module Tagger
4
+ def self.included(base)
5
+ base.extend ClassMethods
6
+ end
7
+
8
+ module ClassMethods
9
+ def acts_as_tagger(opts={})
10
+ has_many :owned_taggings, opts.merge(:as => :tagger, :dependent => :destroy,
11
+ :include => :tag, :class_name => "Tagging")
12
+ has_many :owned_tags, :through => :owned_taggings, :source => :tag, :uniq => true
13
+ include ActiveRecord::Acts::Tagger::InstanceMethods
14
+ extend ActiveRecord::Acts::Tagger::SingletonMethods
15
+ end
16
+
17
+ def is_tagger?
18
+ false
19
+ end
20
+ end
21
+
22
+ module InstanceMethods
23
+ def self.included(base)
24
+ end
25
+
26
+ def tag(taggable, opts={})
27
+ opts.reverse_merge!(:force => true)
28
+
29
+ return false unless taggable.respond_to?(:is_taggable?) && taggable.is_taggable?
30
+ raise "You need to specify a tag context using :on" unless opts.has_key?(:on)
31
+ raise "You need to specify some tags using :with" unless opts.has_key?(:with)
32
+ raise "No context :#{opts[:on]} defined in #{taggable.class.to_s}" unless
33
+ ( opts[:force] || taggable.tag_types.include?(opts[:on]) )
34
+
35
+ taggable.set_tag_list_on(opts[:on].to_s, opts[:with], self)
36
+ taggable.save
37
+ end
38
+
39
+ def is_tagger?
40
+ self.class.is_tagger?
41
+ end
42
+ end
43
+
44
+ module SingletonMethods
45
+ def is_tagger?
46
+ true
47
+ end
48
+ end
49
+
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,23 @@
1
+ class Tag < ActiveRecord::Base
2
+ has_many :taggings
3
+
4
+ validates_presence_of :name
5
+ validates_uniqueness_of :name
6
+
7
+ # LIKE is used for cross-database case-insensitivity
8
+ def self.find_or_create_with_like_by_name(name)
9
+ find(:first, :conditions => ["name LIKE ?", name]) || create(:name => name)
10
+ end
11
+
12
+ def ==(object)
13
+ super || (object.is_a?(Tag) && name == object.name)
14
+ end
15
+
16
+ def to_s
17
+ name
18
+ end
19
+
20
+ def count
21
+ read_attribute(:count).to_i
22
+ end
23
+ end
@@ -0,0 +1,93 @@
1
+ class TagList < Array
2
+ cattr_accessor :delimiter
3
+ self.delimiter = ','
4
+
5
+ def initialize(*args)
6
+ add(*args)
7
+ end
8
+
9
+ attr_accessor :owner
10
+
11
+ # Add tags to the tag_list. Duplicate or blank tags will be ignored.
12
+ #
13
+ # tag_list.add("Fun", "Happy")
14
+ #
15
+ # Use the <tt>:parse</tt> option to add an unparsed tag string.
16
+ #
17
+ # tag_list.add("Fun, Happy", :parse => true)
18
+ def add(*names)
19
+ extract_and_apply_options!(names)
20
+ concat(names)
21
+ clean!
22
+ self
23
+ end
24
+
25
+ # Remove specific tags from the tag_list.
26
+ #
27
+ # tag_list.remove("Sad", "Lonely")
28
+ #
29
+ # Like #add, the <tt>:parse</tt> option can be used to remove multiple tags in a string.
30
+ #
31
+ # tag_list.remove("Sad, Lonely", :parse => true)
32
+ def remove(*names)
33
+ extract_and_apply_options!(names)
34
+ delete_if { |name| names.include?(name) }
35
+ self
36
+ end
37
+
38
+ # Transform the tag_list into a tag string suitable for edting in a form.
39
+ # The tags are joined with <tt>TagList.delimiter</tt> and quoted if necessary.
40
+ #
41
+ # tag_list = TagList.new("Round", "Square,Cube")
42
+ # tag_list.to_s # 'Round, "Square,Cube"'
43
+ def to_s
44
+ clean!
45
+
46
+ map do |name|
47
+ name.include?(delimiter) ? "\"#{name}\"" : name
48
+ end.join(delimiter.ends_with?(" ") ? delimiter : "#{delimiter} ")
49
+ end
50
+
51
+ private
52
+ # Remove whitespace, duplicates, and blanks.
53
+ def clean!
54
+ reject!(&:blank?)
55
+ map!(&:strip)
56
+ uniq!
57
+ end
58
+
59
+ def extract_and_apply_options!(args)
60
+ options = args.last.is_a?(Hash) ? args.pop : {}
61
+ options.assert_valid_keys :parse
62
+
63
+ if options[:parse]
64
+ args.map! { |a| self.class.from(a) }
65
+ end
66
+
67
+ args.flatten!
68
+ end
69
+
70
+ class << self
71
+ # Returns a new TagList using the given tag string.
72
+ #
73
+ # tag_list = TagList.from("One , Two, Three")
74
+ # tag_list # ["One", "Two", "Three"]
75
+ def from(string)
76
+ returning new do |tag_list|
77
+ string = string.to_s.dup
78
+
79
+ # Parse the quoted tags
80
+ string.gsub!(/"(.*?)"\s*#{delimiter}?\s*/) { tag_list << $1; "" }
81
+ string.gsub!(/'(.*?)'\s*#{delimiter}?\s*/) { tag_list << $1; "" }
82
+
83
+ tag_list.add(string.split(delimiter))
84
+ end
85
+ end
86
+
87
+ def from_owner(owner, *tags)
88
+ returning from(*tags) do |taglist|
89
+ taglist.owner = owner
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,6 @@
1
+ class Tagging < ActiveRecord::Base #:nodoc:
2
+ belongs_to :tag
3
+ belongs_to :taggable, :polymorphic => true
4
+ belongs_to :tagger, :polymorphic => true
5
+ validates_presence_of :context
6
+ end
@@ -0,0 +1,11 @@
1
+ module TagsHelper
2
+ # See the README for an example using tag_cloud.
3
+ def tag_cloud(tags, classes)
4
+ max_count = tags.sort_by(&:count).last.count.to_f
5
+
6
+ tags.each do |tag|
7
+ index = ((tag.count / max_count) * (classes.size - 1)).round
8
+ yield tag, classes[index]
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,6 @@
1
+ require 'acts-as-taggable-on'
2
+
3
+ ActiveRecord::Base.send :include, ActiveRecord::Acts::TaggableOn
4
+ ActiveRecord::Base.send :include, ActiveRecord::Acts::Tagger
5
+
6
+ RAILS_DEFAULT_LOGGER.info "** acts_as_taggable_on: initialized properly."
@@ -0,0 +1,165 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe "Acts As Taggable On" do
4
+ it "should provide a class method 'taggable?' that is false for untaggable models" do
5
+ UntaggableModel.should_not be_taggable
6
+ end
7
+
8
+ describe "Taggable Method Generation" do
9
+ before(:each) do
10
+ [TaggableModel, Tag, Tagging, TaggableUser].each(&:delete_all)
11
+ @taggable = TaggableModel.new(:name => "Bob Jones")
12
+ end
13
+
14
+ it "should respond 'true' to taggable?" do
15
+ @taggable.class.should be_taggable
16
+ end
17
+
18
+ it "should create a class attribute for tag types" do
19
+ @taggable.class.should respond_to(:tag_types)
20
+ end
21
+
22
+ it "should generate an association for each tag type" do
23
+ @taggable.should respond_to(:tags, :skills, :languages)
24
+ end
25
+
26
+ it "should generate a cached column checker for each tag type" do
27
+ TaggableModel.should respond_to(:caching_tag_list?, :caching_skill_list?, :caching_language_list?)
28
+ end
29
+
30
+ it "should add tagged_with and tag_counts to singleton" do
31
+ TaggableModel.should respond_to(:find_tagged_with, :tag_counts)
32
+ end
33
+
34
+ it "should add saving of tag lists and cached tag lists to the instance" do
35
+ @taggable.should respond_to(:save_cached_tag_list)
36
+ @taggable.should respond_to(:save_tags)
37
+ end
38
+
39
+ it "should generate a tag_list accessor/setter for each tag type" do
40
+ @taggable.should respond_to(:tag_list, :skill_list, :language_list)
41
+ @taggable.should respond_to(:tag_list=, :skill_list=, :language_list=)
42
+ end
43
+ end
44
+
45
+ describe "Single Table Inheritance" do
46
+ before do
47
+ @taggable = TaggableModel.new(:name => "taggable")
48
+ @inherited_same = InheritingTaggableModel.new(:name => "inherited same")
49
+ @inherited_different = AlteredInheritingTaggableModel.new(:name => "inherited different")
50
+ end
51
+
52
+ it "should pass on tag contexts to STI-inherited models" do
53
+ @inherited_same.should respond_to(:tag_list, :skill_list, :language_list)
54
+ @inherited_different.should respond_to(:tag_list, :skill_list, :language_list)
55
+ end
56
+
57
+ it "should have tag contexts added in altered STI models" do
58
+ @inherited_different.should respond_to(:part_list)
59
+ end
60
+ end
61
+
62
+ describe "Reloading" do
63
+ it "should save a model instantiated by Model.find" do
64
+ taggable = TaggableModel.create!(:name => "Taggable")
65
+ found_taggable = TaggableModel.find(taggable.id)
66
+ found_taggable.save
67
+ end
68
+ end
69
+
70
+ describe "Related Objects" do
71
+ it "should find related objects based on tag names on context" do
72
+ taggable1 = TaggableModel.create!(:name => "Taggable 1")
73
+ taggable2 = TaggableModel.create!(:name => "Taggable 2")
74
+ taggable3 = TaggableModel.create!(:name => "Taggable 3")
75
+
76
+ taggable1.tag_list = "one, two"
77
+ taggable1.save
78
+
79
+ taggable2.tag_list = "three, four"
80
+ taggable2.save
81
+
82
+ taggable3.tag_list = "one, four"
83
+ taggable3.save
84
+
85
+ taggable1.find_related_tags.should include(taggable3)
86
+ taggable1.find_related_tags.should_not include(taggable2)
87
+ end
88
+
89
+ it "should find other related objects based on tag names on context" do
90
+ taggable1 = TaggableModel.create!(:name => "Taggable 1")
91
+ taggable2 = OtherTaggableModel.create!(:name => "Taggable 2")
92
+ taggable3 = OtherTaggableModel.create!(:name => "Taggable 3")
93
+
94
+ taggable1.tag_list = "one, two"
95
+ taggable1.save
96
+
97
+ taggable2.tag_list = "three, four"
98
+ taggable2.save
99
+
100
+ taggable3.tag_list = "one, four"
101
+ taggable3.save
102
+
103
+ taggable1.find_related_tags_for(OtherTaggableModel).should include(taggable3)
104
+ taggable1.find_related_tags_for(OtherTaggableModel).should_not include(taggable2)
105
+ end
106
+
107
+ it "should not include the object itself in the list of related objects" do
108
+ taggable1 = TaggableModel.create!(:name => "Taggable 1")
109
+ taggable2 = TaggableModel.create!(:name => "Taggable 2")
110
+
111
+ taggable1.tag_list = "one"
112
+ taggable1.save
113
+
114
+ taggable2.tag_list = "one, two"
115
+ taggable2.save
116
+
117
+ taggable1.find_related_tags.should include(taggable2)
118
+ taggable1.find_related_tags.should_not include(taggable1)
119
+ end
120
+ end
121
+
122
+ describe 'Tagging Contexts' do
123
+ before(:all) do
124
+ class Array
125
+ def freq
126
+ k=Hash.new(0)
127
+ self.each {|e| k[e]+=1}
128
+ k
129
+ end
130
+ end
131
+ end
132
+
133
+ it 'should eliminate duplicate tagging contexts ' do
134
+ TaggableModel.acts_as_taggable_on(:skills, :skills)
135
+ TaggableModel.tag_types.freq[:skills].should_not == 3
136
+ end
137
+
138
+ it "should not contain embedded/nested arrays" do
139
+ TaggableModel.acts_as_taggable_on([:array], [:array])
140
+ TaggableModel.tag_types.freq[[:array]].should == 0
141
+ end
142
+
143
+ it "should _flatten_ the content of arrays" do
144
+ TaggableModel.acts_as_taggable_on([:array], [:array])
145
+ TaggableModel.tag_types.freq[:array].should == 1
146
+ end
147
+
148
+ it "should not raise an error when passed nil" do
149
+ lambda {
150
+ TaggableModel.acts_as_taggable_on()
151
+ }.should_not raise_error
152
+ end
153
+
154
+ it "should not raise an error when passed [nil]" do
155
+ lambda {
156
+ TaggableModel.acts_as_taggable_on([nil])
157
+ }.should_not raise_error
158
+ end
159
+
160
+ after(:all) do
161
+ class Array; remove_method :freq; end
162
+ end
163
+ end
164
+
165
+ end
@@ -0,0 +1,41 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe TagList do
4
+ before(:each) do
5
+ @tag_list = 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 remove words" do
24
+ @tag_list.remove("awesome")
25
+ @tag_list.include?("awesome").should be_false
26
+ end
27
+
28
+ it "should be able to remove delimited lists of words" do
29
+ @tag_list.remove("awesome, radical", :parse => true)
30
+ @tag_list.should be_empty
31
+ end
32
+
33
+ it "should give a delimited list of words when converted to string" do
34
+ @tag_list.to_s.should == "awesome, radical"
35
+ end
36
+
37
+ it "should quote escape tags with commas in them" do
38
+ @tag_list.add("cool","rad,bodacious")
39
+ @tag_list.to_s.should == "awesome, radical, cool, \"rad,bodacious\""
40
+ end
41
+ end
@@ -0,0 +1,27 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe Tag do
4
+ before(:each) do
5
+ @tag = Tag.new
6
+ @user = TaggableModel.create(:name => "Pablo")
7
+ end
8
+
9
+ it "should require a name" do
10
+ @tag.valid?
11
+ @tag.errors.on(:name).should == "can't be blank"
12
+ @tag.name = "something"
13
+ @tag.valid?
14
+ @tag.errors.on(:name).should be_nil
15
+ end
16
+
17
+ it "should equal a tag with the same name" do
18
+ @tag.name = "awesome"
19
+ new_tag = Tag.new(:name => "awesome")
20
+ new_tag.should == @tag
21
+ end
22
+
23
+ it "should return its name when to_s is called" do
24
+ @tag.name = "cool"
25
+ @tag.to_s.should == "cool"
26
+ end
27
+ end
@@ -0,0 +1,147 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe "Taggable" do
4
+ before(:each) do
5
+ [TaggableModel, Tag, Tagging, TaggableUser].each(&:delete_all)
6
+ @taggable = TaggableModel.new(:name => "Bob Jones")
7
+ end
8
+
9
+ it "should be able to create tags" do
10
+ @taggable.skill_list = "ruby, rails, css"
11
+ @taggable.instance_variable_get("@skill_list").instance_of?(TagList).should be_true
12
+ @taggable.save
13
+
14
+ Tag.find(:all).size.should == 3
15
+ end
16
+
17
+ it "should be able to create tags through the tag list directly" do
18
+ @taggable.tag_list_on(:test).add("hello")
19
+ @taggable.save
20
+ @taggable.reload
21
+ @taggable.tag_list_on(:test).should == ["hello"]
22
+ end
23
+
24
+ it "should differentiate between contexts" do
25
+ @taggable.skill_list = "ruby, rails, css"
26
+ @taggable.tag_list = "ruby, bob, charlie"
27
+ @taggable.save
28
+ @taggable.reload
29
+ @taggable.skill_list.include?("ruby").should be_true
30
+ @taggable.skill_list.include?("bob").should be_false
31
+ end
32
+
33
+ it "should be able to remove tags through list alone" do
34
+ @taggable.skill_list = "ruby, rails, css"
35
+ @taggable.save
36
+ @taggable.reload
37
+ @taggable.should have(3).skills
38
+ @taggable.skill_list = "ruby, rails"
39
+ @taggable.save
40
+ @taggable.reload
41
+ @taggable.should have(2).skills
42
+ end
43
+
44
+ it "should be able to find by tag" do
45
+ @taggable.skill_list = "ruby, rails, css"
46
+ @taggable.save
47
+ TaggableModel.find_tagged_with("ruby").first.should == @taggable
48
+ end
49
+
50
+ it "should be able to find by tag with context" do
51
+ @taggable.skill_list = "ruby, rails, css"
52
+ @taggable.tag_list = "bob, charlie"
53
+ @taggable.save
54
+ TaggableModel.find_tagged_with("ruby").first.should == @taggable
55
+ TaggableModel.find_tagged_with("bob", :on => :skills).first.should_not == @taggable
56
+ TaggableModel.find_tagged_with("bob", :on => :tags).first.should == @taggable
57
+ end
58
+
59
+ it "should be able to use the tagged_with named scope" do
60
+ @taggable.skill_list = "ruby, rails, css"
61
+ @taggable.tag_list = "bob, charlie"
62
+ @taggable.save
63
+ TaggableModel.tagged_with("ruby", {}).first.should == @taggable
64
+ TaggableModel.tagged_with("bob", :on => :skills).first.should_not == @taggable
65
+ TaggableModel.tagged_with("bob", :on => :tags).first.should == @taggable
66
+ end
67
+
68
+ it "should not care about case" do
69
+ bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby")
70
+ frank = TaggableModel.create(:name => "Frank", :tag_list => "Ruby")
71
+
72
+ Tag.find(:all).size.should == 1
73
+ TaggableModel.find_tagged_with("ruby").should == TaggableModel.find_tagged_with("Ruby")
74
+ end
75
+
76
+ it "should be able to get tag counts on model as a whole" do
77
+ bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby, rails, css")
78
+ frank = TaggableModel.create(:name => "Frank", :tag_list => "ruby, rails")
79
+ charlie = TaggableModel.create(:name => "Charlie", :skill_list => "ruby")
80
+ TaggableModel.tag_counts.should_not be_empty
81
+ TaggableModel.skill_counts.should_not be_empty
82
+ end
83
+
84
+ it "should be able to get tag counts on an association" do
85
+ bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby, rails, css")
86
+ frank = TaggableModel.create(:name => "Frank", :tag_list => "ruby, rails")
87
+ charlie = TaggableModel.create(:name => "Charlie", :skill_list => "ruby")
88
+ bob.tag_counts.first.count.should == 2
89
+ charlie.skill_counts.first.count.should == 1
90
+ end
91
+
92
+ it "should be able to set a custom tag context list" do
93
+ bob = TaggableModel.create(:name => "Bob")
94
+ bob.set_tag_list_on(:rotors, "spinning, jumping")
95
+ bob.tag_list_on(:rotors).should == ["spinning","jumping"]
96
+ bob.save
97
+ bob.reload
98
+ bob.tags_on(:rotors).should_not be_empty
99
+ end
100
+
101
+ it "should be able to find tagged on a custom tag context" do
102
+ bob = TaggableModel.create(:name => "Bob")
103
+ bob.set_tag_list_on(:rotors, "spinning, jumping")
104
+ bob.tag_list_on(:rotors).should == ["spinning","jumping"]
105
+ bob.save
106
+ TaggableModel.find_tagged_with("spinning", :on => :rotors).should_not be_empty
107
+ end
108
+
109
+ it "should be able to use named scopes to chain tag finds" do
110
+ bob = TaggableModel.create(:name => "Bob", :tag_list => "fitter, happier, more productive", :skill_list => "ruby, rails, css")
111
+ frank = TaggableModel.create(:name => "Frank", :tag_list => "weaker, depressed, inefficient", :skill_list => "ruby, rails, css")
112
+ steve = TaggableModel.create(:name => 'Steve', :tag_list => 'fitter, happier, more productive', :skill_list => 'c++, java, python')
113
+
114
+ # Let's only find those productive Rails developers
115
+ TaggableModel.tagged_with('rails', :on => :skills).all(:order => 'taggable_models.name').should == [bob, frank]
116
+ TaggableModel.tagged_with('happier', :on => :tags).all(:order => 'taggable_models.name').should == [bob, steve]
117
+ TaggableModel.tagged_with('rails', :on => :skills).tagged_with('happier', :on => :tags).should == [bob]
118
+ end
119
+
120
+ describe "Single Table Inheritance" do
121
+ before do
122
+ [TaggableModel, Tag, Tagging, TaggableUser].each(&:delete_all)
123
+ @taggable = TaggableModel.new(:name => "taggable")
124
+ @inherited_same = InheritingTaggableModel.new(:name => "inherited same")
125
+ @inherited_different = AlteredInheritingTaggableModel.new(:name => "inherited different")
126
+ end
127
+
128
+ it "should be able to save tags for inherited models" do
129
+ @inherited_same.tag_list = "bob, kelso"
130
+ @inherited_same.save
131
+ InheritingTaggableModel.find_tagged_with("bob").first.should == @inherited_same
132
+ end
133
+
134
+ it "should find STI tagged models on the superclass" do
135
+ @inherited_same.tag_list = "bob, kelso"
136
+ @inherited_same.save
137
+ TaggableModel.find_tagged_with("bob").first.should == @inherited_same
138
+ end
139
+
140
+ it "should be able to add on contexts only to some subclasses" do
141
+ @inherited_different.part_list = "fork, spoon"
142
+ @inherited_different.save
143
+ InheritingTaggableModel.find_tagged_with("fork", :on => :parts).should be_empty
144
+ AlteredInheritingTaggableModel.find_tagged_with("fork", :on => :parts).first.should == @inherited_different
145
+ end
146
+ end
147
+ end