wideopenspaces-acts-as-taggable-on 1.0.3

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.
@@ -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
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, :counter_cache => :tag_count
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
+ # Need this to get picked up by autotest?
2
+ $:.push(File.join(File.dirname(__FILE__), %w[.. .. rspec]))
3
+
4
+ Autotest.add_discovery do
5
+ "rspec"
6
+ end
data/rails/init.rb ADDED
@@ -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,151 @@
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
+ end
107
+
108
+ describe 'Tagging Contexts' do
109
+ before(:all) do
110
+ class Array
111
+ def freq
112
+ k=Hash.new(0)
113
+ self.each {|e| k[e]+=1}
114
+ k
115
+ end
116
+ end
117
+ end
118
+
119
+ it 'should eliminate duplicate tagging contexts ' do
120
+ TaggableModel.acts_as_taggable_on(:skills, :skills)
121
+ TaggableModel.tag_types.freq[:skills].should_not == 3
122
+ end
123
+
124
+ it "should not contain embedded/nested arrays" do
125
+ TaggableModel.acts_as_taggable_on([:array], [:array])
126
+ TaggableModel.tag_types.freq[[:array]].should == 0
127
+ end
128
+
129
+ it "should _flatten_ the content of arrays" do
130
+ TaggableModel.acts_as_taggable_on([:array], [:array])
131
+ TaggableModel.tag_types.freq[:array].should == 1
132
+ end
133
+
134
+ it "should not raise an error when passed nil" do
135
+ lambda {
136
+ TaggableModel.acts_as_taggable_on()
137
+ }.should_not raise_error
138
+ end
139
+
140
+ it "should not raise an error when passed [nil]" do
141
+ lambda {
142
+ TaggableModel.acts_as_taggable_on([nil])
143
+ }.should_not raise_error
144
+ end
145
+
146
+ after(:all) do
147
+ class Array; remove_method :freq; end
148
+ end
149
+ end
150
+
151
+ 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,25 @@
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.should have(1).errors_on(:name)
11
+ @tag.name = "something"
12
+ @tag.should have(0).errors_on(:name)
13
+ end
14
+
15
+ it "should equal a tag with the same name" do
16
+ @tag.name = "awesome"
17
+ new_tag = Tag.new(:name => "awesome")
18
+ new_tag.should == @tag
19
+ end
20
+
21
+ it "should return its name when to_s is called" do
22
+ @tag.name = "cool"
23
+ @tag.to_s.should == "cool"
24
+ end
25
+ end
@@ -0,0 +1,136 @@
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
+ describe "Single Table Inheritance" do
110
+ before do
111
+ [TaggableModel, Tag, Tagging, TaggableUser].each(&:delete_all)
112
+ @taggable = TaggableModel.new(:name => "taggable")
113
+ @inherited_same = InheritingTaggableModel.new(:name => "inherited same")
114
+ @inherited_different = AlteredInheritingTaggableModel.new(:name => "inherited different")
115
+ end
116
+
117
+ it "should be able to save tags for inherited models" do
118
+ @inherited_same.tag_list = "bob, kelso"
119
+ @inherited_same.save
120
+ InheritingTaggableModel.find_tagged_with("bob").first.should == @inherited_same
121
+ end
122
+
123
+ it "should find STI tagged models on the superclass" do
124
+ @inherited_same.tag_list = "bob, kelso"
125
+ @inherited_same.save
126
+ TaggableModel.find_tagged_with("bob").first.should == @inherited_same
127
+ end
128
+
129
+ it "should be able to add on contexts only to some subclasses" do
130
+ @inherited_different.part_list = "fork, spoon"
131
+ @inherited_different.save
132
+ InheritingTaggableModel.find_tagged_with("fork", :on => :parts).should be_empty
133
+ AlteredInheritingTaggableModel.find_tagged_with("fork", :on => :parts).first.should == @inherited_different
134
+ end
135
+ end
136
+ end