acts-as-taggable-on-simonwh 2.0.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,60 @@
1
+ module ActiveRecord
2
+ module Acts
3
+ module Tagger
4
+
5
+ def self.included(base)
6
+ base.extend ClassMethods
7
+ end
8
+
9
+ module ClassMethods
10
+
11
+ def acts_as_tagger(opts={})
12
+ has_many :owned_taggings, opts.merge(:as => :tagger, :dependent => :destroy,
13
+ :include => :tag, :class_name => "Tagging")
14
+ has_many :owned_tags, :through => :owned_taggings, :source => :tag, :uniq => true
15
+
16
+ include ActiveRecord::Acts::Tagger::InstanceMethods
17
+ extend ActiveRecord::Acts::Tagger::SingletonMethods
18
+ end
19
+
20
+ def is_tagger?
21
+ false
22
+ end
23
+
24
+ end
25
+
26
+ module InstanceMethods
27
+
28
+ def self.included(base)
29
+ end
30
+
31
+ def tag(taggable, opts={})
32
+ opts.reverse_merge!(:force => true)
33
+
34
+ return false unless taggable.respond_to?(:is_taggable?) && taggable.is_taggable?
35
+
36
+ raise "You need to specify a tag context using :on" unless opts.has_key?(:on)
37
+ raise "You need to specify some tags using :with" unless opts.has_key?(:with)
38
+ raise "No context :#{opts[:on]} defined in #{taggable.class.to_s}" unless (opts[:force] || taggable.tag_types.include?(opts[:on]))
39
+
40
+ taggable.set_tag_list_on(opts[:on].to_s, opts[:with], self)
41
+ taggable.save
42
+ end
43
+
44
+ def is_tagger?
45
+ self.class.is_tagger?
46
+ end
47
+
48
+ end
49
+
50
+ module SingletonMethods
51
+
52
+ def is_tagger?
53
+ true
54
+ end
55
+
56
+ end
57
+
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,14 @@
1
+ module ActiveRecord
2
+ module Acts
3
+ module TaggableOn
4
+ module GroupHelper
5
+
6
+ # all column names are necessary for PostgreSQL group clause
7
+ def grouped_column_names_for(object)
8
+ object.column_names.map { |column| "#{object.table_name}.#{column}" }.join(", ")
9
+ end
10
+
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,53 @@
1
+ class Tag < ActiveRecord::Base
2
+
3
+ attr_accessible :name
4
+
5
+ ### ASSOCIATIONS:
6
+
7
+ has_many :taggings, :dependent => :destroy
8
+
9
+ ### VALIDATIONS:
10
+
11
+ validates_presence_of :name
12
+ validates_uniqueness_of :name
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
+
23
+ def self.find_or_create_with_like_by_name(name)
24
+ named_like(name).first || create(:name => name)
25
+ end
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
+
41
+ def ==(object)
42
+ super || (object.is_a?(Tag) && name == object.name)
43
+ end
44
+
45
+ def to_s
46
+ name
47
+ end
48
+
49
+ def count
50
+ read_attribute(:count).to_i
51
+ end
52
+
53
+ end
@@ -0,0 +1,95 @@
1
+ class TagList < Array
2
+
3
+ cattr_accessor :delimiter
4
+
5
+ self.delimiter = ','
6
+
7
+ def initialize(*args)
8
+ add(*args)
9
+ end
10
+
11
+ attr_accessor :owner
12
+
13
+ # Add tags to the tag_list. Duplicate or blank tags will be ignored.
14
+ #
15
+ # tag_list.add("Fun", "Happy")
16
+ #
17
+ # Use the <tt>:parse</tt> option to add an unparsed tag string.
18
+ #
19
+ # tag_list.add("Fun, Happy", :parse => true)
20
+ def add(*names)
21
+ extract_and_apply_options!(names)
22
+ concat(names)
23
+ clean!
24
+ self
25
+ end
26
+
27
+ # Remove specific tags from the tag_list.
28
+ #
29
+ # tag_list.remove("Sad", "Lonely")
30
+ #
31
+ # Like #add, the <tt>:parse</tt> option can be used to remove multiple tags in a string.
32
+ #
33
+ # tag_list.remove("Sad, Lonely", :parse => true)
34
+ def remove(*names)
35
+ extract_and_apply_options!(names)
36
+ delete_if { |name| names.include?(name) }
37
+ self
38
+ end
39
+
40
+ # Transform the tag_list into a tag string suitable for edting in a form.
41
+ # The tags are joined with <tt>TagList.delimiter</tt> and quoted if necessary.
42
+ #
43
+ # tag_list = TagList.new("Round", "Square,Cube")
44
+ # tag_list.to_s # 'Round, "Square,Cube"'
45
+ def to_s
46
+ tags = frozen? ? self.dup : self
47
+ tags.send(:clean!)
48
+
49
+ tags.map do |name|
50
+ name.include?(delimiter) ? "\"#{name}\"" : name
51
+ end.join(delimiter.ends_with?(" ") ? delimiter : "#{delimiter} ")
52
+ end
53
+
54
+ private
55
+ # Remove whitespace, duplicates, and blanks.
56
+ def clean!
57
+ reject!(&:blank?)
58
+ map!(&:strip)
59
+ uniq!
60
+ end
61
+
62
+ def extract_and_apply_options!(args)
63
+ options = args.last.is_a?(Hash) ? args.pop : {}
64
+ options.assert_valid_keys :parse
65
+
66
+ if options[:parse]
67
+ args.map! { |a| self.class.from(a) }
68
+ end
69
+
70
+ args.flatten!
71
+ end
72
+
73
+ class << self
74
+
75
+ # Returns a new TagList using the given tag string.
76
+ #
77
+ # tag_list = TagList.from("One , Two, Three")
78
+ # tag_list # ["One", "Two", "Three"]
79
+ def from(string)
80
+ string = string.join(", ") if string.respond_to?(:join)
81
+
82
+ new.tap do |tag_list|
83
+ string = string.to_s.dup
84
+
85
+ # Parse the quoted tags
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
+
89
+ tag_list.add(string.split(delimiter))
90
+ end
91
+ end
92
+
93
+ end
94
+
95
+ end
@@ -0,0 +1,22 @@
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
+
13
+ belongs_to :tag
14
+ belongs_to :taggable, :polymorphic => true
15
+ belongs_to :tagger, :polymorphic => true
16
+
17
+ validates_presence_of :context
18
+ validates_presence_of :tag_id
19
+
20
+ validates_uniqueness_of :tag_id, :scope => [ :taggable_type, :taggable_id, :context, :tagger_id, :tagger_type ]
21
+
22
+ end
@@ -0,0 +1,17 @@
1
+ module TagsHelper
2
+
3
+ # See the README for an example using tag_cloud.
4
+ def tag_cloud(tags, classes)
5
+ tags = tags.all if tags.is_a? ActiveRecord::Relation
6
+
7
+ return [] if tags.empty?
8
+
9
+ max_count = tags.sort_by(&:count).last.count.to_f
10
+
11
+ tags.each do |tag|
12
+ index = ((tag.count / max_count) * (classes.size - 1)).round
13
+ yield tag, classes[index]
14
+ end
15
+ 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.to_s)
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
@@ -0,0 +1,211 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe "Acts As Taggable On" do
4
+ before(:each) do
5
+ clean_database!
6
+ end
7
+
8
+ it "should provide a class method 'taggable?' that is false for untaggable models" do
9
+ UntaggableModel.should_not be_taggable
10
+ end
11
+
12
+ describe "Taggable Method Generation" do
13
+ before(:each) do
14
+ [TaggableModel, Tag, Tagging, TaggableUser].each(&:delete_all)
15
+ @taggable = TaggableModel.new(:name => "Bob Jones")
16
+ end
17
+
18
+ it "should respond 'true' to taggable?" do
19
+ @taggable.class.should be_taggable
20
+ end
21
+
22
+ it "should create a class attribute for tag types" do
23
+ @taggable.class.should respond_to(:tag_types)
24
+ end
25
+
26
+ it "should create an instance attribute for tag types" do
27
+ @taggable.should respond_to(:tag_types)
28
+ end
29
+
30
+ it "should generate an association for each tag type" do
31
+ @taggable.should respond_to(:tags, :skills, :languages)
32
+ end
33
+
34
+ it "should generate a cached column checker for each tag type" do
35
+ TaggableModel.should respond_to(:caching_tag_list?, :caching_skill_list?, :caching_language_list?)
36
+ end
37
+
38
+ it "should add tagged_with and tag_counts to singleton" do
39
+ TaggableModel.should respond_to(:tagged_with, :tag_counts)
40
+ end
41
+
42
+ it "should add saving of tag lists and cached tag lists to the instance" do
43
+ @taggable.should respond_to(:save_cached_tag_list)
44
+ @taggable.should respond_to(:save_tags)
45
+ end
46
+
47
+ it "should generate a tag_list accessor/setter for each tag type" do
48
+ @taggable.should respond_to(:tag_list, :skill_list, :language_list)
49
+ @taggable.should respond_to(:tag_list=, :skill_list=, :language_list=)
50
+ end
51
+ end
52
+
53
+ describe "Single Table Inheritance" do
54
+ before do
55
+ @taggable = TaggableModel.new(:name => "taggable")
56
+ @inherited_same = InheritingTaggableModel.new(:name => "inherited same")
57
+ @inherited_different = AlteredInheritingTaggableModel.new(:name => "inherited different")
58
+ end
59
+
60
+ it "should pass on tag contexts to STI-inherited models" do
61
+ @inherited_same.should respond_to(:tag_list, :skill_list, :language_list)
62
+ @inherited_different.should respond_to(:tag_list, :skill_list, :language_list)
63
+ end
64
+
65
+ it "should have tag contexts added in altered STI models" do
66
+ @inherited_different.should respond_to(:part_list)
67
+ end
68
+ end
69
+
70
+ describe "Reloading" do
71
+ it "should save a model instantiated by Model.find" do
72
+ taggable = TaggableModel.create!(:name => "Taggable")
73
+ found_taggable = TaggableModel.find(taggable.id)
74
+ found_taggable.save
75
+ end
76
+ end
77
+
78
+ describe "Related Objects" do
79
+ it "should find related objects based on tag names on context" do
80
+ taggable1 = TaggableModel.create!(:name => "Taggable 1")
81
+ taggable2 = TaggableModel.create!(:name => "Taggable 2")
82
+ taggable3 = TaggableModel.create!(:name => "Taggable 3")
83
+
84
+ taggable1.tag_list = "one, two"
85
+ taggable1.save
86
+
87
+ taggable2.tag_list = "three, four"
88
+ taggable2.save
89
+
90
+ taggable3.tag_list = "one, four"
91
+ taggable3.save
92
+
93
+ taggable1.find_related_tags.should include(taggable3)
94
+ taggable1.find_related_tags.should_not include(taggable2)
95
+ end
96
+
97
+ it "should find other related objects based on tag names on context" do
98
+ taggable1 = TaggableModel.create!(:name => "Taggable 1")
99
+ taggable2 = OtherTaggableModel.create!(:name => "Taggable 2")
100
+ taggable3 = OtherTaggableModel.create!(:name => "Taggable 3")
101
+
102
+ taggable1.tag_list = "one, two"
103
+ taggable1.save
104
+
105
+ taggable2.tag_list = "three, four"
106
+ taggable2.save
107
+
108
+ taggable3.tag_list = "one, four"
109
+ taggable3.save
110
+
111
+ taggable1.find_related_tags_for(OtherTaggableModel).should include(taggable3)
112
+ taggable1.find_related_tags_for(OtherTaggableModel).should_not include(taggable2)
113
+ end
114
+
115
+ it "should not include the object itself in the list of related objects" do
116
+ taggable1 = TaggableModel.create!(:name => "Taggable 1")
117
+ taggable2 = TaggableModel.create!(:name => "Taggable 2")
118
+
119
+ taggable1.tag_list = "one"
120
+ taggable1.save
121
+
122
+ taggable2.tag_list = "one, two"
123
+ taggable2.save
124
+
125
+ taggable1.find_related_tags.should include(taggable2)
126
+ taggable1.find_related_tags.should_not include(taggable1)
127
+ end
128
+ end
129
+
130
+ describe "Matching Contexts" do
131
+ it "should find objects with tags of matching contexts" do
132
+ taggable1 = TaggableModel.create!(:name => "Taggable 1")
133
+ taggable2 = TaggableModel.create!(:name => "Taggable 2")
134
+ taggable3 = TaggableModel.create!(:name => "Taggable 3")
135
+
136
+ taggable1.offering_list = "one, two"
137
+ taggable1.save!
138
+
139
+ taggable2.need_list = "one, two"
140
+ taggable2.save!
141
+
142
+ taggable3.offering_list = "one, two"
143
+ taggable3.save!
144
+
145
+ taggable1.find_matching_contexts(:offerings, :needs).should include(taggable2)
146
+ taggable1.find_matching_contexts(:offerings, :needs).should_not include(taggable3)
147
+ end
148
+
149
+ it "should find other related objects with tags of matching contexts" do
150
+ taggable1 = TaggableModel.create!(:name => "Taggable 1")
151
+ taggable2 = OtherTaggableModel.create!(:name => "Taggable 2")
152
+ taggable3 = OtherTaggableModel.create!(:name => "Taggable 3")
153
+
154
+ taggable1.offering_list = "one, two"
155
+ taggable1.save
156
+
157
+ taggable2.need_list = "one, two"
158
+ taggable2.save
159
+
160
+ taggable3.offering_list = "one, two"
161
+ taggable3.save
162
+
163
+ taggable1.find_matching_contexts_for(OtherTaggableModel, :offerings, :needs).should include(taggable2)
164
+ taggable1.find_matching_contexts_for(OtherTaggableModel, :offerings, :needs).should_not include(taggable3)
165
+ end
166
+
167
+ it "should not include the object itself in the list of related objects" do
168
+ taggable1 = TaggableModel.create!(:name => "Taggable 1")
169
+ taggable2 = TaggableModel.create!(:name => "Taggable 2")
170
+
171
+ taggable1.tag_list = "one"
172
+ taggable1.save
173
+
174
+ taggable2.tag_list = "one, two"
175
+ taggable2.save
176
+
177
+ taggable1.find_related_tags.should include(taggable2)
178
+ taggable1.find_related_tags.should_not include(taggable1)
179
+ end
180
+ end
181
+
182
+ describe 'Tagging Contexts' do
183
+ it 'should eliminate duplicate tagging contexts ' do
184
+ TaggableModel.acts_as_taggable_on(:skills, :skills)
185
+ TaggableModel.tag_types.freq[:skills].should_not == 3
186
+ end
187
+
188
+ it "should not contain embedded/nested arrays" do
189
+ TaggableModel.acts_as_taggable_on([:array], [:array])
190
+ TaggableModel.tag_types.freq[[:array]].should == 0
191
+ end
192
+
193
+ it "should _flatten_ the content of arrays" do
194
+ TaggableModel.acts_as_taggable_on([:array], [:array])
195
+ TaggableModel.tag_types.freq[:array].should == 1
196
+ end
197
+
198
+ it "should not raise an error when passed nil" do
199
+ lambda {
200
+ TaggableModel.acts_as_taggable_on()
201
+ }.should_not raise_error
202
+ end
203
+
204
+ it "should not raise an error when passed [nil]" do
205
+ lambda {
206
+ TaggableModel.acts_as_taggable_on([nil])
207
+ }.should_not raise_error
208
+ end
209
+ end
210
+
211
+ end