bbenezech-acts-as-taggable-on 0.0.2

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, :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