citrusbyte-is_taggable 0.85
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.
- data/CHANGELOG +30 -0
- data/MIT-LICENSE +21 -0
- data/README +243 -0
- data/generators/is_taggable_migration/is_taggable_migration_generator.rb +7 -0
- data/generators/is_taggable_migration/templates/migration.rb +24 -0
- data/init.rb +1 -0
- data/lib/is_taggable/is_taggable.rb +354 -0
- data/lib/is_taggable/is_tagger.rb +49 -0
- data/lib/is_taggable/tag_list.rb +107 -0
- data/lib/is_taggable/tagging.rb +31 -0
- data/lib/is_taggable/tags_helper.rb +11 -0
- data/lib/is_taggable.rb +6 -0
- data/rails/init.rb +6 -0
- data/spec/is_taggable/is_taggable_spec.rb +170 -0
- data/spec/is_taggable/tag_list_spec.rb +67 -0
- data/spec/is_taggable/taggable_spec.rb +136 -0
- data/spec/is_taggable/tagger_spec.rb +18 -0
- data/spec/is_taggable/tagging_spec.rb +42 -0
- data/spec/schema.rb +30 -0
- data/spec/spec_helper.rb +36 -0
- data/uninstall.rb +1 -0
- metadata +79 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module ActiveRecord
|
|
2
|
+
module Is
|
|
3
|
+
module Tagger
|
|
4
|
+
def self.included(base)
|
|
5
|
+
base.extend ClassMethods
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
module ClassMethods
|
|
9
|
+
def is_tagger(opts={})
|
|
10
|
+
has_many :owned_taggings, opts.merge(:as => :tagger, :dependent => :destroy, :class_name => "Tagging")
|
|
11
|
+
include ActiveRecord::Is::Tagger::InstanceMethods
|
|
12
|
+
extend ActiveRecord::Is::Tagger::SingletonMethods
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def is_tagger?
|
|
16
|
+
false
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
module InstanceMethods
|
|
21
|
+
def self.included(base)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def tag(taggable, opts={})
|
|
25
|
+
opts.reverse_merge!(:force => true)
|
|
26
|
+
|
|
27
|
+
return false unless taggable.respond_to?(:is_taggable?) && taggable.is_taggable?
|
|
28
|
+
raise "You need to specify a tag context using :on" unless opts.has_key?(:on)
|
|
29
|
+
raise "You need to specify some tags using :with" unless opts.has_key?(:with)
|
|
30
|
+
raise "No context :#{opts[:on]} defined in #{taggable.class.to_s}" unless (opts[:force] || taggable.tag_types.include?(opts[:on]))
|
|
31
|
+
|
|
32
|
+
taggable.set_tag_list_on(opts[:on].to_s, opts[:with], self)
|
|
33
|
+
taggable.save
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def is_tagger?
|
|
37
|
+
self.class.is_tagger?
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
module SingletonMethods
|
|
42
|
+
def is_tagger?
|
|
43
|
+
true
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
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
|
+
def normalized
|
|
52
|
+
TagList.new(collect{ |tag| self.class.normalize(tag) })
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
# Remove whitespace, duplicates, and blanks.
|
|
57
|
+
def clean!
|
|
58
|
+
reject!(&:blank?)
|
|
59
|
+
map!(&:strip)
|
|
60
|
+
uniq!
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def extract_and_apply_options!(args)
|
|
64
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
|
65
|
+
options.assert_valid_keys :parse
|
|
66
|
+
|
|
67
|
+
if options[:parse]
|
|
68
|
+
args.map! { |a| self.class.from(a) }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
args.flatten!
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
class << self
|
|
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
|
+
returning new do |tag_list|
|
|
81
|
+
string = string.to_s.dup
|
|
82
|
+
|
|
83
|
+
# Parse the quoted tags
|
|
84
|
+
string.gsub!(/"(.*?)"\s*#{delimiter}?\s*/) { tag_list << $1; "" }
|
|
85
|
+
string.gsub!(/'(.*?)'\s*#{delimiter}?\s*/) { tag_list << $1; "" }
|
|
86
|
+
|
|
87
|
+
tag_list.add(string.split(delimiter))
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def from_owner(owner, *tags)
|
|
92
|
+
returning from(*tags) do |taglist|
|
|
93
|
+
taglist.owner = owner
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def new_from_owner(owner, *tags)
|
|
98
|
+
returning new(*tags) do |taglist|
|
|
99
|
+
taglist.owner = owner
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def normalize(tag)
|
|
104
|
+
tag.gsub(/\s+/, ' ').to_ascii.downcase.gsub(/[^a-z0-9\-\s]/, '')
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
class Tagging < ActiveRecord::Base #:nodoc:
|
|
2
|
+
belongs_to :taggable, :polymorphic => true
|
|
3
|
+
belongs_to :tagger, :polymorphic => true
|
|
4
|
+
|
|
5
|
+
# these validations are useless since we don't save tags this way...
|
|
6
|
+
validates_presence_of :context
|
|
7
|
+
validates_presence_of :tag
|
|
8
|
+
validates_presence_of :normalized
|
|
9
|
+
validates_uniqueness_of :normalized, :scope => [ :context, :taggable_type, :taggable_id ]
|
|
10
|
+
|
|
11
|
+
def tag=(tag)
|
|
12
|
+
self[:tag] = tag
|
|
13
|
+
self[:normalized] = TagList.normalize(tag)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def context
|
|
17
|
+
self[:context] ? self[:context].to_sym : nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def to_s
|
|
21
|
+
tag
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def count
|
|
25
|
+
read_attribute(:count).to_i
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def save
|
|
29
|
+
raise "Taggings are protected from being created individually, please use the tagging methods on Taggable objects"
|
|
30
|
+
end
|
|
31
|
+
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/lib/is_taggable.rb
ADDED
data/rails/init.rb
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
|
2
|
+
|
|
3
|
+
describe "Is Taggable" 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, 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
|
+
|
|
44
|
+
it "should generate a tags accessor/setter for each tag type" do
|
|
45
|
+
@taggable.should respond_to(:tags, :skills, :languages)
|
|
46
|
+
@taggable.should respond_to(:tags=, :skills=, :languages=)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
describe "Single Table Inheritance" do
|
|
52
|
+
before do
|
|
53
|
+
@taggable = TaggableModel.new(:name => "taggable")
|
|
54
|
+
@inherited_same = InheritingTaggableModel.new(:name => "inherited same")
|
|
55
|
+
@inherited_different = AlteredInheritingTaggableModel.new(:name => "inherited different")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it "should pass on tag contexts to STI-inherited models" do
|
|
59
|
+
@inherited_same.should respond_to(:tag_list, :skill_list, :language_list)
|
|
60
|
+
@inherited_different.should respond_to(:tag_list, :skill_list, :language_list)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it "should have tag contexts added in altered STI models" do
|
|
64
|
+
@inherited_different.should respond_to(:part_list)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
describe "Reloading" do
|
|
69
|
+
it "should save a model instantiated by Model.find" do
|
|
70
|
+
taggable = TaggableModel.create!(:name => "Taggable")
|
|
71
|
+
found_taggable = TaggableModel.find(taggable.id)
|
|
72
|
+
found_taggable.save
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
describe "Related Objects" do
|
|
77
|
+
it "should find related objects based on tag names on context" do
|
|
78
|
+
taggable1 = TaggableModel.create!(:name => "Taggable 1")
|
|
79
|
+
taggable2 = TaggableModel.create!(:name => "Taggable 2")
|
|
80
|
+
taggable3 = TaggableModel.create!(:name => "Taggable 3")
|
|
81
|
+
|
|
82
|
+
taggable1.tag_list = "one, two"
|
|
83
|
+
taggable1.save
|
|
84
|
+
|
|
85
|
+
taggable2.tag_list = "three, four"
|
|
86
|
+
taggable2.save
|
|
87
|
+
|
|
88
|
+
taggable3.tag_list = "one, four"
|
|
89
|
+
taggable3.save
|
|
90
|
+
|
|
91
|
+
taggable1.find_related_tags.should include(taggable3)
|
|
92
|
+
taggable1.find_related_tags.should_not include(taggable2)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it "should find other related objects based on tag names on context" do
|
|
96
|
+
taggable1 = TaggableModel.create!(:name => "Taggable 1")
|
|
97
|
+
taggable2 = OtherTaggableModel.create!(:name => "Taggable 2")
|
|
98
|
+
taggable3 = OtherTaggableModel.create!(:name => "Taggable 3")
|
|
99
|
+
|
|
100
|
+
taggable1.tag_list = "one, two"
|
|
101
|
+
taggable1.save
|
|
102
|
+
|
|
103
|
+
taggable2.tag_list = "three, four"
|
|
104
|
+
taggable2.save
|
|
105
|
+
|
|
106
|
+
taggable3.tag_list = "one, four"
|
|
107
|
+
taggable3.save
|
|
108
|
+
|
|
109
|
+
taggable1.find_related_tags_for(OtherTaggableModel).should include(taggable3)
|
|
110
|
+
taggable1.find_related_tags_for(OtherTaggableModel).should_not include(taggable2)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
describe "when setting tags" do
|
|
115
|
+
before :each do
|
|
116
|
+
@taggable = TaggableModel.create!(:name => 'foo')
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it "should be settable through through tag_list=" do
|
|
120
|
+
@taggable.tag_list = 'foo, bar, baz'
|
|
121
|
+
@taggable.save
|
|
122
|
+
@taggable.reload.tags.should include('foo', 'bar', 'baz')
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it "should be settable through through tags=" do
|
|
126
|
+
@taggable.tags = %w(foo bar baz)
|
|
127
|
+
@taggable.save
|
|
128
|
+
@taggable.reload.tags.should include('foo', 'bar', 'baz')
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
describe 'Tagging Contexts' do
|
|
133
|
+
before(:all) do
|
|
134
|
+
class Array
|
|
135
|
+
def freq
|
|
136
|
+
k=Hash.new(0)
|
|
137
|
+
self.each {|e| k[e]+=1}
|
|
138
|
+
k
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it 'should eliminate duplicate tagging contexts ' do
|
|
144
|
+
TaggableModel.is_taggable(:skills, :skills)
|
|
145
|
+
TaggableModel.tag_types.freq[:skills].should_not == 3
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it "should not contain embedded/nested arrays" do
|
|
149
|
+
TaggableModel.is_taggable([:array], [:array])
|
|
150
|
+
TaggableModel.tag_types.freq[[:array]].should == 0
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
it "should _flatten_ the content of arrays" do
|
|
154
|
+
TaggableModel.is_taggable([:array], [:array])
|
|
155
|
+
TaggableModel.tag_types.freq[:array].should == 1
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
it "should not raise an error when passed [nil]" do
|
|
160
|
+
lambda {
|
|
161
|
+
TaggableModel.is_taggable([nil])
|
|
162
|
+
}.should_not raise_error
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
after(:all) do
|
|
166
|
+
class Array; remove_method :freq; end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
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 remove words" do
|
|
24
|
+
@tag_list.remove("awesome")
|
|
25
|
+
@tag_list.include?("awesome").should be_false
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it "should be able to remove delimited lists of words" do
|
|
29
|
+
@tag_list.remove("awesome, radical", :parse => true)
|
|
30
|
+
@tag_list.should be_empty
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "should give a delimited list of words when converted to string" do
|
|
34
|
+
@tag_list.to_s.should == "awesome, radical"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "should quote escape tags with commas in them" do
|
|
38
|
+
@tag_list.add("cool","rad,bodacious")
|
|
39
|
+
@tag_list.to_s.should == "awesome, radical, cool, \"rad,bodacious\""
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
describe "normalization" do
|
|
43
|
+
def normalized_tags(tag_list)
|
|
44
|
+
TagList.from(tag_list).normalized.to_s
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it "should lower case all tags" do
|
|
48
|
+
normalized_tags("COol, BeANs").should eql('cool, beans')
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it "should replace accented characters" do
|
|
52
|
+
normalized_tags("CÕÖl, BÈÄñs").should eql('cool, beans')
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it "should compress whitespace" do
|
|
56
|
+
normalized_tags(" c o o l, b e an s ").should eql('c o o l, b e an s')
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it "should strip 'special' characters" do
|
|
60
|
+
normalized_tags('c%!#{*!@\&oo!@l}, #*!#**)#(!@#)bea<>><>}{}:":":ns').should eql('cool, beans')
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it "should replace ÀÁÂÃÄÅÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝàáâãäåçèéêëìíîïñòóôõöøùúûüýÆæ" do
|
|
64
|
+
normalized_tags('ÀÁÂÃÄÅÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝàáâãäåçèéêëìíîïñòóôõöøùúûüýÆæ').should eql('aaaaaaceeeeiiiidnoooooxouuuuyaaaaaaceeeeiiiinoooooouuuuyaeae')
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
|
2
|
+
|
|
3
|
+
describe "Taggable" do
|
|
4
|
+
before(:each) do
|
|
5
|
+
[TaggableModel, Tagging, TaggableUser].each(&:delete_all)
|
|
6
|
+
@taggable = TaggableModel.new(:name => "Bob Jones")
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
it "should be able to create tags" do
|
|
10
|
+
@taggable.skill_list = "ruby, rails, css"
|
|
11
|
+
@taggable.instance_variable_get("@skill_list").instance_of?(TagList).should be_true
|
|
12
|
+
@taggable.save
|
|
13
|
+
|
|
14
|
+
Tagging.find(:all).size.should == 3
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "should be able to create tags through the tag list directly" do
|
|
18
|
+
@taggable.tag_list_on(:test).add("hello")
|
|
19
|
+
@taggable.save
|
|
20
|
+
@taggable.reload
|
|
21
|
+
@taggable.tag_list_on(:test).should == ["hello"]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it "should differentiate between contexts" do
|
|
25
|
+
@taggable.skill_list = "ruby, rails, css"
|
|
26
|
+
@taggable.tag_list = "ruby, bob, charlie"
|
|
27
|
+
@taggable.save
|
|
28
|
+
@taggable.reload
|
|
29
|
+
@taggable.skill_list.include?("ruby").should be_true
|
|
30
|
+
@taggable.skill_list.include?("bob").should be_false
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "should be able to remove tags through list alone" do
|
|
34
|
+
@taggable.skill_list = "ruby, rails, css"
|
|
35
|
+
@taggable.save
|
|
36
|
+
@taggable.reload
|
|
37
|
+
@taggable.should have(3).skills
|
|
38
|
+
@taggable.skill_list = "ruby, rails"
|
|
39
|
+
@taggable.save
|
|
40
|
+
@taggable.reload
|
|
41
|
+
@taggable.should have(2).skills
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it "should be able to find by tag" do
|
|
45
|
+
@taggable.skill_list = "ruby, rails, css"
|
|
46
|
+
@taggable.save
|
|
47
|
+
TaggableModel.find_tagged_with("ruby").first.should == @taggable
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it "should be able to find by tag with context" do
|
|
51
|
+
@taggable.skill_list = "ruby, rails, css"
|
|
52
|
+
@taggable.tag_list = "bob, charlie"
|
|
53
|
+
@taggable.save
|
|
54
|
+
TaggableModel.find_tagged_with("ruby").first.should == @taggable
|
|
55
|
+
TaggableModel.find_tagged_with("bob", :on => :skills).first.should_not == @taggable
|
|
56
|
+
TaggableModel.find_tagged_with("bob", :on => :tags).first.should == @taggable
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it "should be able to use the tagged_with named scope" do
|
|
60
|
+
@taggable.skill_list = "ruby, rails, css"
|
|
61
|
+
@taggable.tag_list = "bob, charlie"
|
|
62
|
+
@taggable.save
|
|
63
|
+
TaggableModel.tagged_with("ruby", {}).first.should == @taggable
|
|
64
|
+
TaggableModel.tagged_with("bob", :on => :skills).first.should_not == @taggable
|
|
65
|
+
TaggableModel.tagged_with("bob", :on => :tags).first.should == @taggable
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it "should not care about case" do
|
|
69
|
+
bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby")
|
|
70
|
+
frank = TaggableModel.create(:name => "Frank", :tag_list => "Ruby")
|
|
71
|
+
|
|
72
|
+
Tagging.find(:all).size.should == 2
|
|
73
|
+
TaggableModel.find_tagged_with("ruby").should == TaggableModel.find_tagged_with("Ruby")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it "should be able to get tag counts on model as a whole" do
|
|
77
|
+
bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby, rails, css")
|
|
78
|
+
frank = TaggableModel.create(:name => "Frank", :tag_list => "ruby, rails")
|
|
79
|
+
charlie = TaggableModel.create(:name => "Charlie", :skill_list => "ruby")
|
|
80
|
+
TaggableModel.tag_counts.should_not be_empty
|
|
81
|
+
TaggableModel.skill_counts.should_not be_empty
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it "should be able to get tag counts on an association" do
|
|
85
|
+
bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby, rails, css")
|
|
86
|
+
frank = TaggableModel.create(:name => "Frank", :tag_list => "ruby, rails")
|
|
87
|
+
charlie = TaggableModel.create(:name => "Charlie", :skill_list => "ruby")
|
|
88
|
+
bob.tag_counts.collect{ |t| t.count }.sort.should eql([1, 2, 2])
|
|
89
|
+
charlie.skill_counts.collect{ |t| t.count }.sort.should eql([1])
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it "should be able to set a custom tag context list" do
|
|
93
|
+
bob = TaggableModel.create(:name => "Bob")
|
|
94
|
+
bob.set_tag_list_on(:rotors, "spinning, jumping")
|
|
95
|
+
bob.tag_list_on(:rotors).should == ["spinning","jumping"]
|
|
96
|
+
bob.save
|
|
97
|
+
bob.reload
|
|
98
|
+
bob.taggings_on(:rotors).should_not be_empty
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it "should be able to find tagged on a custom tag context" do
|
|
102
|
+
bob = TaggableModel.create(:name => "Bob")
|
|
103
|
+
bob.set_tag_list_on(:rotors, "spinning, jumping")
|
|
104
|
+
bob.tag_list_on(:rotors).should == ["spinning","jumping"]
|
|
105
|
+
bob.save
|
|
106
|
+
TaggableModel.find_tagged_with("spinning", :on => :rotors).should_not be_empty
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
describe "Single Table Inheritance" do
|
|
110
|
+
before do
|
|
111
|
+
[TaggableModel, Tagging, TaggableUser].each(&:delete_all)
|
|
112
|
+
@taggable = TaggableModel.new(:name => "taggable")
|
|
113
|
+
@inherited_same = InheritingTaggableModel.new(:name => "inherited same")
|
|
114
|
+
@inherited_different = AlteredInheritingTaggableModel.new(:name => "inherited different")
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
it "should be able to save tags for inherited models" do
|
|
118
|
+
@inherited_same.tag_list = "bob, kelso"
|
|
119
|
+
@inherited_same.save
|
|
120
|
+
InheritingTaggableModel.find_tagged_with("bob").first.should == @inherited_same
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it "should find STI tagged models on the superclass" do
|
|
124
|
+
@inherited_same.tag_list = "bob, kelso"
|
|
125
|
+
@inherited_same.save
|
|
126
|
+
TaggableModel.find_tagged_with("bob").first.should == @inherited_same
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it "should be able to add on contexts only to some subclasses" do
|
|
130
|
+
@inherited_different.part_list = "fork, spoon"
|
|
131
|
+
@inherited_different.save
|
|
132
|
+
InheritingTaggableModel.find_tagged_with("fork", :on => :parts).should be_empty
|
|
133
|
+
AlteredInheritingTaggableModel.find_tagged_with("fork", :on => :parts).first.should == @inherited_different
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
|
2
|
+
|
|
3
|
+
describe "Tagger" do
|
|
4
|
+
before(:each) do
|
|
5
|
+
[TaggableModel, Tagging, TaggableUser].each(&:delete_all)
|
|
6
|
+
@user = TaggableUser.new
|
|
7
|
+
@taggable = TaggableModel.new(:name => "Bob Jones")
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
it "should have taggings" do
|
|
11
|
+
@user.tag(@taggable, :with=>'ruby,scheme', :on=>:tags)
|
|
12
|
+
@user.owned_taggings.size == 2
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it "is tagger" do
|
|
16
|
+
@user.is_tagger?.should(be_true)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
|
2
|
+
|
|
3
|
+
describe Tagging do
|
|
4
|
+
before(:each) do
|
|
5
|
+
@tagging = Tagging.new
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
it "should require a tag" do
|
|
9
|
+
@tagging.valid?
|
|
10
|
+
@tagging.should have(1).errors_on(:tag)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it "should require a normalized version of the tag" do
|
|
14
|
+
@tagging.valid?
|
|
15
|
+
@tagging.should have(1).errors_on(:normalized)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it "should be valid with a tag" do
|
|
19
|
+
@tagging.tag = "something"
|
|
20
|
+
@tagging.valid?
|
|
21
|
+
@tagging.should have(0).errors_on(:tag)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it "should return its name when to_s is called" do
|
|
25
|
+
@tagging.tag = "cool"
|
|
26
|
+
@tagging.to_s.should == "cool"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "should raise if you try to save one even when it's valid" do
|
|
30
|
+
@tagging.tag = 'foo'
|
|
31
|
+
lambda { @tagging.save }.should raise_error
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "should raise on create" do
|
|
35
|
+
lambda { Tagging.create :tag => 'foo' }.should raise_error
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it "should raise on update" do
|
|
39
|
+
@taggable = TaggableModel.create!(:name => "Bob Jones", :tag_list => 'bar')
|
|
40
|
+
lambda { @taggable.reload.taggings.first.update_attributes(:tag => 'foo') }.should raise_error
|
|
41
|
+
end
|
|
42
|
+
end
|
data/spec/schema.rb
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
ActiveRecord::Schema.define :version => 0 do
|
|
2
|
+
create_table "taggings", :force => true do |t|
|
|
3
|
+
t.string "tag"
|
|
4
|
+
t.string "normalized"
|
|
5
|
+
t.string "context"
|
|
6
|
+
t.integer "taggable_id", :limit => 11
|
|
7
|
+
t.string "taggable_type"
|
|
8
|
+
t.datetime "created_at"
|
|
9
|
+
t.integer "tagger_id", :limit => 11
|
|
10
|
+
t.string "tagger_type"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
add_index "taggings", ["tag"], :name => "index_taggings_on_tag"
|
|
14
|
+
add_index "taggings", ["taggable_id", "taggable_type", "context"], :name => "index_taggings_on_taggable_id_and_taggable_type_and_context"
|
|
15
|
+
add_index "taggings", ["taggable_id", "taggable_type", "context", "normalized"], :name => "index_taggings_on_taggable_and_context_and_normalized", :uniq => true
|
|
16
|
+
|
|
17
|
+
create_table :taggable_models, :force => true do |t|
|
|
18
|
+
t.column :name, :string
|
|
19
|
+
t.column :type, :string
|
|
20
|
+
#t.column :cached_tag_list, :string
|
|
21
|
+
end
|
|
22
|
+
create_table :taggable_users, :force => true do |t|
|
|
23
|
+
t.column :name, :string
|
|
24
|
+
end
|
|
25
|
+
create_table :other_taggable_models, :force => true do |t|
|
|
26
|
+
t.column :name, :string
|
|
27
|
+
t.column :type, :string
|
|
28
|
+
#t.column :cached_tag_list, :string
|
|
29
|
+
end
|
|
30
|
+
end
|