ordered-tags 0.0.1

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,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,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!(/"(.*?)"\s*#{delimiter}?\s*/) { tag_list << $1; "" }
83
+ string.gsub!(/'(.*?)'\s*#{delimiter}?\s*/) { tag_list << $1; "" }
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,8 @@
1
+ class Tagging < ActiveRecord::Base #:nodoc:
2
+ belongs_to :tag
3
+ belongs_to :taggable, :polymorphic => true
4
+ belongs_to :tagger, :polymorphic => true
5
+ validates_presence_of :context
6
+ acts_as_list :scope => 'taggable_id = #{taggable_id} AND taggable_type = \'#{taggable_type}\' AND context = \'#{context}\''
7
+ default_scope :order => 'position ASC'
8
+ 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
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,165 @@
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
+
107
+ it "should not include the object itself in the list of related objects" do
108
+ taggable1 = TaggableModel.create!(:name => "Taggable 1")
109
+ taggable2 = TaggableModel.create!(:name => "Taggable 2")
110
+
111
+ taggable1.tag_list = "one"
112
+ taggable1.save
113
+
114
+ taggable2.tag_list = "one, two"
115
+ taggable2.save
116
+
117
+ taggable1.find_related_tags.should include(taggable2)
118
+ taggable1.find_related_tags.should_not include(taggable1)
119
+ end
120
+ end
121
+
122
+ describe 'Tagging Contexts' do
123
+ before(:all) do
124
+ class Array
125
+ def freq
126
+ k=Hash.new(0)
127
+ self.each {|e| k[e]+=1}
128
+ k
129
+ end
130
+ end
131
+ end
132
+
133
+ it 'should eliminate duplicate tagging contexts ' do
134
+ TaggableModel.acts_as_taggable_on(:skills, :skills)
135
+ TaggableModel.tag_types.freq[:skills].should_not == 3
136
+ end
137
+
138
+ it "should not contain embedded/nested arrays" do
139
+ TaggableModel.acts_as_taggable_on([:array], [:array])
140
+ TaggableModel.tag_types.freq[[:array]].should == 0
141
+ end
142
+
143
+ it "should _flatten_ the content of arrays" do
144
+ TaggableModel.acts_as_taggable_on([:array], [:array])
145
+ TaggableModel.tag_types.freq[:array].should == 1
146
+ end
147
+
148
+ it "should not raise an error when passed nil" do
149
+ lambda {
150
+ TaggableModel.acts_as_taggable_on()
151
+ }.should_not raise_error
152
+ end
153
+
154
+ it "should not raise an error when passed [nil]" do
155
+ lambda {
156
+ TaggableModel.acts_as_taggable_on([nil])
157
+ }.should_not raise_error
158
+ end
159
+
160
+ after(:all) do
161
+ class Array; remove_method :freq; end
162
+ end
163
+ end
164
+
165
+ 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
@@ -0,0 +1,52 @@
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 add an array of words" do
24
+ @tag_list.add(["cool", "wicked"], :parse => true)
25
+ @tag_list.include?("cool").should be_true
26
+ @tag_list.include?("wicked").should be_true
27
+ end
28
+
29
+ it "should be able to remove words" do
30
+ @tag_list.remove("awesome")
31
+ @tag_list.include?("awesome").should be_false
32
+ end
33
+
34
+ it "should be able to remove delimited lists of words" do
35
+ @tag_list.remove("awesome, radical", :parse => true)
36
+ @tag_list.should be_empty
37
+ end
38
+
39
+ it "should be able to remove an array of words" do
40
+ @tag_list.remove(["awesome", "radical"], :parse => true)
41
+ @tag_list.should be_empty
42
+ end
43
+
44
+ it "should give a delimited list of words when converted to string" do
45
+ @tag_list.to_s.should == "awesome, radical"
46
+ end
47
+
48
+ it "should quote escape tags with commas in them" do
49
+ @tag_list.add("cool","rad,bodacious")
50
+ @tag_list.to_s.should == "awesome, radical, cool, \"rad,bodacious\""
51
+ end
52
+ end