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

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