bbenezech-acts-as-taggable-on 0.0.2

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,12 @@
1
+ module ActiveRecord
2
+ module Acts
3
+ module TaggableOn
4
+ module GroupHelper
5
+ # all column names are necessary for PostgreSQL group clause
6
+ def grouped_column_names_for(object)
7
+ object.column_names.map { |column| "#{object.table_name}.#{column}" }.join(", ")
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,27 @@
1
+ class Tag < ActiveRecord::Base
2
+ has_many :taggings, :dependent => :destroy
3
+
4
+ validates_presence_of :name
5
+ 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
+ named_scope :named_like_any, lambda { |list| { :conditions => list.map { |tag| sanitize_sql(["name LIKE ?", tag.to_s]) }.join(" OR ") } }
10
+
11
+ # LIKE is used for cross-database case-insensitivity
12
+ def self.find_or_create_with_like_by_name(name)
13
+ find(:first, :conditions => ["name LIKE ?", name]) || create(:name => name)
14
+ end
15
+
16
+ def ==(object)
17
+ super || (object.is_a?(Tag) && name == object.name)
18
+ end
19
+
20
+ def to_s
21
+ name
22
+ end
23
+
24
+ def count
25
+ read_attribute(:count).to_i
26
+ end
27
+ end
@@ -0,0 +1,95 @@
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
+ string = string.join(", ") if string.respond_to?(:join)
77
+
78
+ returning new do |tag_list|
79
+ string = string.to_s.dup
80
+
81
+ # Parse the quoted tags
82
+ string.gsub!(/(\A|#{delimiter})\s*"(.*?)"\s*(#{delimiter}\s*|\z)/) { tag_list << $2; $3 }
83
+ string.gsub!(/(\A|#{delimiter})\s*'(.*?)'\s*(#{delimiter}\s*|\z)/) { tag_list << $2; $3 }
84
+
85
+ tag_list.add(string.split(delimiter))
86
+ end
87
+ end
88
+
89
+ def from_owner(owner, *tags)
90
+ returning from(*tags) do |taglist|
91
+ taglist.owner = owner
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,11 @@
1
+ class Tagging < ActiveRecord::Base #:nodoc:
2
+ belongs_to :tag
3
+ belongs_to :taggable, :polymorphic => true
4
+ belongs_to :tagger, :polymorphic => true
5
+
6
+ validates_presence_of :context
7
+ acts_as_list :scope => 'taggable_id = #{taggable_id} AND taggable_type = \'#{taggable_type}\' AND context = \'#{context}\''
8
+ default_scope :order => 'position'
9
+ validates_presence_of :tag_id
10
+ validates_uniqueness_of :tag_id, :scope => [:taggable_type, :taggable_id, :context]
11
+ end
@@ -0,0 +1,13 @@
1
+ module TagsHelper
2
+ # See the README for an example using tag_cloud.
3
+ def tag_cloud(tags, classes)
4
+ return [] if tags.empty?
5
+
6
+ max_count = tags.sort_by(&:count).last.count.to_f
7
+
8
+ tags.each do |tag|
9
+ index = ((tag.count / max_count) * (classes.size - 1)).round
10
+ yield tag, classes[index]
11
+ end
12
+ end
13
+ end
data/rails/init.rb ADDED
@@ -0,0 +1,5 @@
1
+ require 'acts-as-taggable-on'
2
+
3
+ ActiveRecord::Base.send :include, ActiveRecord::Acts::TaggableOn
4
+ ActiveRecord::Base.send :include, ActiveRecord::Acts::Tagger
5
+ ActionView::Base.send :include, TagsHelper if defined?(ActionView::Base)
@@ -0,0 +1,207 @@
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 create an instance attribute for tag types" do
23
+ @taggable.should respond_to(:tag_types)
24
+ end
25
+
26
+ it "should generate an association for each tag type" do
27
+ @taggable.should respond_to(:tags, :skills, :languages)
28
+ end
29
+
30
+ it "should generate a cached column checker for each tag type" do
31
+ TaggableModel.should respond_to(:caching_tag_list?, :caching_skill_list?, :caching_language_list?)
32
+ end
33
+
34
+ it "should add tagged_with and tag_counts to singleton" do
35
+ TaggableModel.should respond_to(:find_tagged_with, :tag_counts)
36
+ end
37
+
38
+ it "should add saving of tag lists and cached tag lists to the instance" do
39
+ @taggable.should respond_to(:save_cached_tag_list)
40
+ @taggable.should respond_to(:save_tags)
41
+ end
42
+
43
+ it "should generate a tag_list accessor/setter for each tag type" do
44
+ @taggable.should respond_to(:tag_list, :skill_list, :language_list)
45
+ @taggable.should respond_to(:tag_list=, :skill_list=, :language_list=)
46
+ end
47
+ end
48
+
49
+ describe "Single Table Inheritance" do
50
+ before do
51
+ @taggable = TaggableModel.new(:name => "taggable")
52
+ @inherited_same = InheritingTaggableModel.new(:name => "inherited same")
53
+ @inherited_different = AlteredInheritingTaggableModel.new(:name => "inherited different")
54
+ end
55
+
56
+ it "should pass on tag contexts to STI-inherited models" do
57
+ @inherited_same.should respond_to(:tag_list, :skill_list, :language_list)
58
+ @inherited_different.should respond_to(:tag_list, :skill_list, :language_list)
59
+ end
60
+
61
+ it "should have tag contexts added in altered STI models" do
62
+ @inherited_different.should respond_to(:part_list)
63
+ end
64
+ end
65
+
66
+ describe "Reloading" do
67
+ it "should save a model instantiated by Model.find" do
68
+ taggable = TaggableModel.create!(:name => "Taggable")
69
+ found_taggable = TaggableModel.find(taggable.id)
70
+ found_taggable.save
71
+ end
72
+ end
73
+
74
+ describe "Related Objects" do
75
+ it "should find related objects based on tag names on context" do
76
+ taggable1 = TaggableModel.create!(:name => "Taggable 1")
77
+ taggable2 = TaggableModel.create!(:name => "Taggable 2")
78
+ taggable3 = TaggableModel.create!(:name => "Taggable 3")
79
+
80
+ taggable1.tag_list = "one, two"
81
+ taggable1.save
82
+
83
+ taggable2.tag_list = "three, four"
84
+ taggable2.save
85
+
86
+ taggable3.tag_list = "one, four"
87
+ taggable3.save
88
+
89
+ taggable1.find_related_tags.should include(taggable3)
90
+ taggable1.find_related_tags.should_not include(taggable2)
91
+ end
92
+
93
+ it "should find other related objects based on tag names on context" do
94
+ taggable1 = TaggableModel.create!(:name => "Taggable 1")
95
+ taggable2 = OtherTaggableModel.create!(:name => "Taggable 2")
96
+ taggable3 = OtherTaggableModel.create!(:name => "Taggable 3")
97
+
98
+ taggable1.tag_list = "one, two"
99
+ taggable1.save
100
+
101
+ taggable2.tag_list = "three, four"
102
+ taggable2.save
103
+
104
+ taggable3.tag_list = "one, four"
105
+ taggable3.save
106
+
107
+ taggable1.find_related_tags_for(OtherTaggableModel).should include(taggable3)
108
+ taggable1.find_related_tags_for(OtherTaggableModel).should_not include(taggable2)
109
+ end
110
+
111
+ it "should not include the object itself in the list of related objects" do
112
+ taggable1 = TaggableModel.create!(:name => "Taggable 1")
113
+ taggable2 = TaggableModel.create!(:name => "Taggable 2")
114
+
115
+ taggable1.tag_list = "one"
116
+ taggable1.save
117
+
118
+ taggable2.tag_list = "one, two"
119
+ taggable2.save
120
+
121
+ taggable1.find_related_tags.should include(taggable2)
122
+ taggable1.find_related_tags.should_not include(taggable1)
123
+ end
124
+ end
125
+
126
+ describe "Matching Contexts" do
127
+ it "should find objects with tags of matching contexts" do
128
+ taggable1 = TaggableModel.create!(:name => "Taggable 1")
129
+ taggable2 = TaggableModel.create!(:name => "Taggable 2")
130
+ taggable3 = TaggableModel.create!(:name => "Taggable 3")
131
+
132
+ taggable1.offering_list = "one, two"
133
+ taggable1.save!
134
+
135
+ taggable2.need_list = "one, two"
136
+ taggable2.save!
137
+
138
+ taggable3.offering_list = "one, two"
139
+ taggable3.save!
140
+
141
+ taggable1.find_matching_contexts(:offerings, :needs).should include(taggable2)
142
+ taggable1.find_matching_contexts(:offerings, :needs).should_not include(taggable3)
143
+ end
144
+
145
+ it "should find other related objects with tags of matching contexts" do
146
+ taggable1 = TaggableModel.create!(:name => "Taggable 1")
147
+ taggable2 = OtherTaggableModel.create!(:name => "Taggable 2")
148
+ taggable3 = OtherTaggableModel.create!(:name => "Taggable 3")
149
+
150
+ taggable1.offering_list = "one, two"
151
+ taggable1.save
152
+
153
+ taggable2.need_list = "one, two"
154
+ taggable2.save
155
+
156
+ taggable3.offering_list = "one, two"
157
+ taggable3.save
158
+
159
+ taggable1.find_matching_contexts_for(OtherTaggableModel, :offerings, :needs).should include(taggable2)
160
+ taggable1.find_matching_contexts_for(OtherTaggableModel, :offerings, :needs).should_not include(taggable3)
161
+ end
162
+
163
+ it "should not include the object itself in the list of related objects" do
164
+ taggable1 = TaggableModel.create!(:name => "Taggable 1")
165
+ taggable2 = TaggableModel.create!(:name => "Taggable 2")
166
+
167
+ taggable1.tag_list = "one"
168
+ taggable1.save
169
+
170
+ taggable2.tag_list = "one, two"
171
+ taggable2.save
172
+
173
+ taggable1.find_related_tags.should include(taggable2)
174
+ taggable1.find_related_tags.should_not include(taggable1)
175
+ end
176
+ end
177
+
178
+ describe 'Tagging Contexts' do
179
+ it 'should eliminate duplicate tagging contexts ' do
180
+ TaggableModel.acts_as_taggable_on(:skills, :skills)
181
+ TaggableModel.tag_types.freq[:skills].should_not == 3
182
+ end
183
+
184
+ it "should not contain embedded/nested arrays" do
185
+ TaggableModel.acts_as_taggable_on([:array], [:array])
186
+ TaggableModel.tag_types.freq[[:array]].should == 0
187
+ end
188
+
189
+ it "should _flatten_ the content of arrays" do
190
+ TaggableModel.acts_as_taggable_on([:array], [:array])
191
+ TaggableModel.tag_types.freq[:array].should == 1
192
+ end
193
+
194
+ it "should not raise an error when passed nil" do
195
+ lambda {
196
+ TaggableModel.acts_as_taggable_on()
197
+ }.should_not raise_error
198
+ end
199
+
200
+ it "should not raise an error when passed [nil]" do
201
+ lambda {
202
+ TaggableModel.acts_as_taggable_on([nil])
203
+ }.should_not raise_error
204
+ end
205
+ end
206
+
207
+ end
@@ -0,0 +1,72 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe "acts_as_tagger" do
4
+ context "Tagger Method Generation" do
5
+
6
+ before(:each) do
7
+ @tagger = TaggableUser.new()
8
+ end
9
+
10
+ it "should add #is_tagger? query method to the class-side" do
11
+ TaggableUser.should respond_to(:is_tagger?)
12
+ end
13
+
14
+ it "should return true from the class-side #is_tagger?" do
15
+ TaggableUser.is_tagger?.should be_true
16
+ end
17
+
18
+ it "should return false from the base #is_tagger?" do
19
+ ActiveRecord::Base.is_tagger?.should be_false
20
+ end
21
+
22
+ it "should add #is_tagger? query method to the singleton" do
23
+ @tagger.should respond_to(:is_tagger?)
24
+ end
25
+
26
+ it "should add #tag method on the instance-side" do
27
+ @tagger.should respond_to(:tag)
28
+ end
29
+
30
+ it "should generate an association for #owned_taggings and #owned_tags" do
31
+ @tagger.should respond_to(:owned_taggings, :owned_tags)
32
+ end
33
+ end
34
+
35
+ describe "#tag" do
36
+ context 'when called with a non-existent tag context' do
37
+ before(:each) do
38
+ @tagger = TaggableUser.new()
39
+ @taggable = TaggableModel.new(:name=>"Richard Prior")
40
+ end
41
+
42
+ it "should by default not throw an exception " do
43
+ @taggable.tag_list_on(:foo).should be_empty
44
+ lambda {
45
+ @tagger.tag(@taggable, :with=>'this, and, that', :on=>:foo)
46
+ }.should_not raise_error
47
+ end
48
+
49
+ it 'should by default create the tag context on-the-fly' do
50
+ @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')
53
+ end
54
+
55
+ it "should throw an exception when the default is over-ridden" do
56
+ @taggable.tag_list_on(:foo_boo).should be_empty
57
+ lambda {
58
+ @tagger.tag(@taggable, :with=>'this, and, that', :on=>:foo_boo, :force=>false)
59
+ }.should raise_error
60
+ end
61
+
62
+ it "should not create the tag context on-the-fly when the default is over-ridden" do
63
+ @taggable.tag_list_on(:foo_boo).should be_empty
64
+ @tagger.tag(@taggable, :with=>'this, and, that', :on=>:foo_boo, :force=>false) rescue
65
+ @taggable.tag_list_on(:foo_boo).should be_empty
66
+ end
67
+
68
+ end
69
+
70
+ end
71
+
72
+ end