acts_as_taggable_on 3.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +11 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +9 -0
  5. data/Appraisals +7 -0
  6. data/Gemfile +5 -0
  7. data/Guardfile +5 -0
  8. data/LICENSE.md +20 -0
  9. data/README.md +309 -0
  10. data/Rakefile +13 -0
  11. data/UPGRADING +7 -0
  12. data/acts_as_taggable_on.gemspec +35 -0
  13. data/db/migrate/1_acts_as_taggable_on_migration.rb +30 -0
  14. data/db/migrate/2_add_missing_unique_indices.rb +21 -0
  15. data/gemfiles/rails_3.gemfile +8 -0
  16. data/gemfiles/rails_4.gemfile +8 -0
  17. data/lib/acts_as_taggable_on.rb +61 -0
  18. data/lib/acts_as_taggable_on/acts_as_taggable_on/cache.rb +82 -0
  19. data/lib/acts_as_taggable_on/acts_as_taggable_on/collection.rb +187 -0
  20. data/lib/acts_as_taggable_on/acts_as_taggable_on/compatibility.rb +34 -0
  21. data/lib/acts_as_taggable_on/acts_as_taggable_on/core.rb +394 -0
  22. data/lib/acts_as_taggable_on/acts_as_taggable_on/dirty.rb +37 -0
  23. data/lib/acts_as_taggable_on/acts_as_taggable_on/ownership.rb +135 -0
  24. data/lib/acts_as_taggable_on/acts_as_taggable_on/related.rb +84 -0
  25. data/lib/acts_as_taggable_on/engine.rb +6 -0
  26. data/lib/acts_as_taggable_on/tag.rb +119 -0
  27. data/lib/acts_as_taggable_on/tag_list.rb +101 -0
  28. data/lib/acts_as_taggable_on/taggable.rb +105 -0
  29. data/lib/acts_as_taggable_on/tagger.rb +76 -0
  30. data/lib/acts_as_taggable_on/tagging.rb +34 -0
  31. data/lib/acts_as_taggable_on/tags_helper.rb +15 -0
  32. data/lib/acts_as_taggable_on/utils.rb +34 -0
  33. data/lib/acts_as_taggable_on/version.rb +4 -0
  34. data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +265 -0
  35. data/spec/acts_as_taggable_on/acts_as_tagger_spec.rb +114 -0
  36. data/spec/acts_as_taggable_on/caching_spec.rb +77 -0
  37. data/spec/acts_as_taggable_on/related_spec.rb +143 -0
  38. data/spec/acts_as_taggable_on/single_table_inheritance_spec.rb +187 -0
  39. data/spec/acts_as_taggable_on/tag_list_spec.rb +126 -0
  40. data/spec/acts_as_taggable_on/tag_spec.rb +211 -0
  41. data/spec/acts_as_taggable_on/taggable_spec.rb +623 -0
  42. data/spec/acts_as_taggable_on/tagger_spec.rb +137 -0
  43. data/spec/acts_as_taggable_on/tagging_spec.rb +28 -0
  44. data/spec/acts_as_taggable_on/tags_helper_spec.rb +44 -0
  45. data/spec/acts_as_taggable_on/utils_spec.rb +21 -0
  46. data/spec/bm.rb +52 -0
  47. data/spec/database.yml.sample +19 -0
  48. data/spec/models.rb +58 -0
  49. data/spec/schema.rb +65 -0
  50. data/spec/spec_helper.rb +87 -0
  51. metadata +248 -0
@@ -0,0 +1,105 @@
1
+ module ActsAsTaggableOn
2
+ module Taggable
3
+ def taggable?
4
+ false
5
+ end
6
+
7
+ ##
8
+ # This is an alias for calling <tt>acts_as_taggable_on :tags</tt>.
9
+ #
10
+ # Example:
11
+ # class Book < ActiveRecord::Base
12
+ # acts_as_taggable
13
+ # end
14
+ def acts_as_taggable
15
+ acts_as_taggable_on :tags
16
+ end
17
+
18
+ ##
19
+ # This is an alias for calling <tt>acts_as_ordered_taggable_on :tags</tt>.
20
+ #
21
+ # Example:
22
+ # class Book < ActiveRecord::Base
23
+ # acts_as_ordered_taggable
24
+ # end
25
+ def acts_as_ordered_taggable
26
+ acts_as_ordered_taggable_on :tags
27
+ end
28
+
29
+ ##
30
+ # Make a model taggable on specified contexts.
31
+ #
32
+ # @param [Array] tag_types An array of taggable contexts
33
+ #
34
+ # Example:
35
+ # class User < ActiveRecord::Base
36
+ # acts_as_taggable_on :languages, :skills
37
+ # end
38
+ def acts_as_taggable_on(*tag_types)
39
+ taggable_on(false, tag_types)
40
+ end
41
+
42
+
43
+ ##
44
+ # Make a model taggable on specified contexts
45
+ # and preserves the order in which tags are created
46
+ #
47
+ # @param [Array] tag_types An array of taggable contexts
48
+ #
49
+ # Example:
50
+ # class User < ActiveRecord::Base
51
+ # acts_as_ordered_taggable_on :languages, :skills
52
+ # end
53
+ def acts_as_ordered_taggable_on(*tag_types)
54
+ taggable_on(true, tag_types)
55
+ end
56
+
57
+ private
58
+
59
+ # Make a model taggable on specified contexts
60
+ # and optionally preserves the order in which tags are created
61
+ #
62
+ # Seperate methods used above for backwards compatibility
63
+ # so that the original acts_as_taggable_on method is unaffected
64
+ # as it's not possible to add another arguement to the method
65
+ # without the tag_types being enclosed in square brackets
66
+ #
67
+ # NB: method overridden in core module in order to create tag type
68
+ # associations and methods after this logic has executed
69
+ #
70
+ def taggable_on(preserve_tag_order, *tag_types)
71
+ tag_types = tag_types.to_a.flatten.compact.map(&:to_sym)
72
+
73
+ if taggable?
74
+ self.tag_types = (self.tag_types + tag_types).uniq
75
+ self.preserve_tag_order = preserve_tag_order
76
+ else
77
+ class_attribute :tag_types
78
+ self.tag_types = tag_types
79
+ class_attribute :preserve_tag_order
80
+ self.preserve_tag_order = preserve_tag_order
81
+
82
+ class_eval do
83
+ has_many :taggings, :as => :taggable, :dependent => :destroy, :class_name => "ActsAsTaggableOn::Tagging"
84
+ has_many :base_tags, :through => :taggings, :source => :tag, :class_name => "ActsAsTaggableOn::Tag"
85
+
86
+ def self.taggable?
87
+ true
88
+ end
89
+
90
+ include ActsAsTaggableOn::Utils
91
+ end
92
+ end
93
+
94
+ # each of these add context-specific methods and must be
95
+ # called on each call of taggable_on
96
+ include ActsAsTaggableOn::Taggable::Core
97
+ include ActsAsTaggableOn::Taggable::Collection
98
+ include ActsAsTaggableOn::Taggable::Cache
99
+ include ActsAsTaggableOn::Taggable::Ownership
100
+ include ActsAsTaggableOn::Taggable::Related
101
+ include ActsAsTaggableOn::Taggable::Dirty
102
+ end
103
+
104
+ end
105
+ end
@@ -0,0 +1,76 @@
1
+ module ActsAsTaggableOn
2
+ module Tagger
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ end
6
+
7
+ module ClassMethods
8
+ ##
9
+ # Make a model a tagger. This allows an instance of a model to claim ownership
10
+ # of tags.
11
+ #
12
+ # Example:
13
+ # class User < ActiveRecord::Base
14
+ # acts_as_tagger
15
+ # end
16
+ def acts_as_tagger(opts={})
17
+ class_eval do
18
+ has_many_with_compatibility :owned_taggings,
19
+ opts.merge(
20
+ :as => :tagger,
21
+ :dependent => :destroy,
22
+ :class_name => "ActsAsTaggableOn::Tagging"
23
+ )
24
+
25
+ has_many_with_compatibility :owned_tags,
26
+ :through => :owned_taggings,
27
+ :source => :tag,
28
+ :class_name => "ActsAsTaggableOn::Tag",
29
+ :uniq => true
30
+ end
31
+
32
+ include ActsAsTaggableOn::Tagger::InstanceMethods
33
+ extend ActsAsTaggableOn::Tagger::SingletonMethods
34
+ end
35
+
36
+ def is_tagger?
37
+ false
38
+ end
39
+ end
40
+
41
+ module InstanceMethods
42
+ ##
43
+ # Tag a taggable model with tags that are owned by the tagger.
44
+ #
45
+ # @param taggable The object that will be tagged
46
+ # @param [Hash] options An hash with options. Available options are:
47
+ # * <tt>:with</tt> - The tags that you want to
48
+ # * <tt>:on</tt> - The context on which you want to tag
49
+ #
50
+ # Example:
51
+ # @user.tag(@photo, :with => "paris, normandy", :on => :locations)
52
+ def tag(taggable, opts={})
53
+ opts.reverse_merge!(:force => true)
54
+ skip_save = opts.delete(:skip_save)
55
+ return false unless taggable.respond_to?(:is_taggable?) && taggable.is_taggable?
56
+
57
+ raise "You need to specify a tag context using :on" unless opts.has_key?(:on)
58
+ raise "You need to specify some tags using :with" unless opts.has_key?(:with)
59
+ raise "No context :#{opts[:on]} defined in #{taggable.class.to_s}" unless (opts[:force] || taggable.tag_types.include?(opts[:on]))
60
+
61
+ taggable.set_owner_tag_list_on(self, opts[:on].to_s, opts[:with])
62
+ taggable.save unless skip_save
63
+ end
64
+
65
+ def is_tagger?
66
+ self.class.is_tagger?
67
+ end
68
+ end
69
+
70
+ module SingletonMethods
71
+ def is_tagger?
72
+ true
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,34 @@
1
+ module ActsAsTaggableOn
2
+ class Tagging < ::ActiveRecord::Base #:nodoc:
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 if defined?(ActiveModel::MassAssignmentSecurity)
12
+
13
+ belongs_to :tag, :class_name => 'ActsAsTaggableOn::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
+ after_destroy :remove_unused_tags
23
+
24
+ private
25
+
26
+ def remove_unused_tags
27
+ if ActsAsTaggableOn.remove_unused_tags
28
+ if tag.taggings.count.zero?
29
+ tag.destroy
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,15 @@
1
+ module ActsAsTaggableOn
2
+ module TagsHelper
3
+ # See the README for an example using tag_cloud.
4
+ def tag_cloud(tags, classes)
5
+ return [] if tags.empty?
6
+
7
+ max_count = tags.sort_by(&:count).last.count.to_f
8
+
9
+ tags.each do |tag|
10
+ index = ((tag.count / max_count) * (classes.size - 1))
11
+ yield tag, classes[index.nan? ? 0 : index.round]
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,34 @@
1
+ module ActsAsTaggableOn
2
+ module Utils
3
+ def self.included(base)
4
+
5
+ base.send :include, ActsAsTaggableOn::Utils::OverallMethods
6
+ base.extend ActsAsTaggableOn::Utils::OverallMethods
7
+ end
8
+
9
+ module OverallMethods
10
+ def using_postgresql?
11
+ ::ActiveRecord::Base.connection && ::ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
12
+ end
13
+
14
+ def using_sqlite?
15
+ ::ActiveRecord::Base.connection && ::ActiveRecord::Base.connection.adapter_name == 'SQLite'
16
+ end
17
+
18
+ def sha_prefix(string)
19
+ Digest::SHA1.hexdigest("#{string}#{rand}")[0..6]
20
+ end
21
+
22
+ private
23
+ def like_operator
24
+ using_postgresql? ? 'ILIKE' : 'LIKE'
25
+ end
26
+
27
+ # escape _ and % characters in strings, since these are wildcards in SQL.
28
+ def escape_like(str)
29
+ str.gsub(/[!%_]/){ |x| '!' + x }
30
+ end
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,4 @@
1
+ module ActsAsTaggableOn
2
+ VERSION = '3.0.0.rc1'
3
+ end
4
+
@@ -0,0 +1,265 @@
1
+ # coding: utf-8
2
+ require 'spec_helper'
3
+
4
+ describe "Acts As Taggable On" do
5
+ before(:each) do
6
+ clean_database!
7
+ end
8
+
9
+ it "should provide a class method 'taggable?' that is false for untaggable models" do
10
+ UntaggableModel.should_not be_taggable
11
+ end
12
+
13
+ describe "Taggable Method Generation To Preserve Order" do
14
+ before(:each) do
15
+ clean_database!
16
+ TaggableModel.tag_types = []
17
+ TaggableModel.preserve_tag_order = false
18
+ TaggableModel.acts_as_ordered_taggable_on(:ordered_tags)
19
+ @taggable = TaggableModel.new(:name => "Bob Jones")
20
+ end
21
+
22
+ it "should respond 'true' to preserve_tag_order?" do
23
+ @taggable.class.preserve_tag_order?.should be_true
24
+ end
25
+ end
26
+
27
+ describe "Taggable Method Generation" do
28
+ before(:each) do
29
+ clean_database!
30
+ TaggableModel.tag_types = []
31
+ TaggableModel.acts_as_taggable_on(:tags, :languages, :skills, :needs, :offerings)
32
+ @taggable = TaggableModel.new(:name => "Bob Jones")
33
+ end
34
+
35
+ it "should respond 'true' to taggable?" do
36
+ @taggable.class.should be_taggable
37
+ end
38
+
39
+ it "should create a class attribute for tag types" do
40
+ @taggable.class.should respond_to(:tag_types)
41
+ end
42
+
43
+ it "should create an instance attribute for tag types" do
44
+ @taggable.should respond_to(:tag_types)
45
+ end
46
+
47
+ it "should have all tag types" do
48
+ @taggable.tag_types.should == [:tags, :languages, :skills, :needs, :offerings]
49
+ end
50
+
51
+ it "should create a class attribute for preserve tag order" do
52
+ @taggable.class.should respond_to(:preserve_tag_order?)
53
+ end
54
+
55
+ it "should create an instance attribute for preserve tag order" do
56
+ @taggable.should respond_to(:preserve_tag_order?)
57
+ end
58
+
59
+ it "should respond 'false' to preserve_tag_order?" do
60
+ @taggable.class.preserve_tag_order?.should be_false
61
+ end
62
+
63
+ it "should generate an association for each tag type" do
64
+ @taggable.should respond_to(:tags, :skills, :languages)
65
+ end
66
+
67
+ it "should add tagged_with and tag_counts to singleton" do
68
+ TaggableModel.should respond_to(:tagged_with, :tag_counts)
69
+ end
70
+
71
+ it "should generate a tag_list accessor/setter for each tag type" do
72
+ @taggable.should respond_to(:tag_list, :skill_list, :language_list)
73
+ @taggable.should respond_to(:tag_list=, :skill_list=, :language_list=)
74
+ end
75
+
76
+ it "should generate a tag_list accessor, that includes owned tags, for each tag type" do
77
+ @taggable.should respond_to(:all_tags_list, :all_skills_list, :all_languages_list)
78
+ end
79
+ end
80
+
81
+ describe "Reloading" do
82
+ it "should save a model instantiated by Model.find" do
83
+ taggable = TaggableModel.create!(:name => "Taggable")
84
+ found_taggable = TaggableModel.find(taggable.id)
85
+ found_taggable.save
86
+ end
87
+ end
88
+
89
+ describe "Matching Contexts" do
90
+ it "should find objects with tags of matching contexts" do
91
+ taggable1 = TaggableModel.create!(:name => "Taggable 1")
92
+ taggable2 = TaggableModel.create!(:name => "Taggable 2")
93
+ taggable3 = TaggableModel.create!(:name => "Taggable 3")
94
+
95
+ taggable1.offering_list = "one, two"
96
+ taggable1.save!
97
+
98
+ taggable2.need_list = "one, two"
99
+ taggable2.save!
100
+
101
+ taggable3.offering_list = "one, two"
102
+ taggable3.save!
103
+
104
+ taggable1.find_matching_contexts(:offerings, :needs).should include(taggable2)
105
+ taggable1.find_matching_contexts(:offerings, :needs).should_not include(taggable3)
106
+ end
107
+
108
+ it "should find other related objects with tags of matching contexts" do
109
+ taggable1 = TaggableModel.create!(:name => "Taggable 1")
110
+ taggable2 = OtherTaggableModel.create!(:name => "Taggable 2")
111
+ taggable3 = OtherTaggableModel.create!(:name => "Taggable 3")
112
+
113
+ taggable1.offering_list = "one, two"
114
+ taggable1.save
115
+
116
+ taggable2.need_list = "one, two"
117
+ taggable2.save
118
+
119
+ taggable3.offering_list = "one, two"
120
+ taggable3.save
121
+
122
+ taggable1.find_matching_contexts_for(OtherTaggableModel, :offerings, :needs).should include(taggable2)
123
+ taggable1.find_matching_contexts_for(OtherTaggableModel, :offerings, :needs).should_not include(taggable3)
124
+ end
125
+
126
+ it "should not include the object itself in the list of related objects with tags of matching contexts" do
127
+ taggable1 = TaggableModel.create!(:name => "Taggable 1")
128
+ taggable2 = TaggableModel.create!(:name => "Taggable 2")
129
+
130
+ taggable1.offering_list = "one, two"
131
+ taggable1.need_list = "one, two"
132
+ taggable1.save
133
+
134
+ taggable2.need_list = "one, two"
135
+ taggable2.save
136
+
137
+ taggable1.find_matching_contexts_for(TaggableModel, :offerings, :needs).should include(taggable2)
138
+ taggable1.find_matching_contexts_for(TaggableModel, :offerings, :needs).should_not include(taggable1)
139
+ end
140
+
141
+ end
142
+
143
+ describe 'Tagging Contexts' do
144
+ it 'should eliminate duplicate tagging contexts ' do
145
+ TaggableModel.acts_as_taggable_on(:skills, :skills)
146
+ TaggableModel.tag_types.freq[:skills].should_not == 3
147
+ end
148
+
149
+ it "should not contain embedded/nested arrays" do
150
+ TaggableModel.acts_as_taggable_on([:array], [:array])
151
+ TaggableModel.tag_types.freq[[:array]].should == 0
152
+ end
153
+
154
+ it "should _flatten_ the content of arrays" do
155
+ TaggableModel.acts_as_taggable_on([:array], [:array])
156
+ TaggableModel.tag_types.freq[:array].should == 1
157
+ end
158
+
159
+ it "should not raise an error when passed nil" do
160
+ lambda {
161
+ TaggableModel.acts_as_taggable_on()
162
+ }.should_not raise_error
163
+ end
164
+
165
+ it "should not raise an error when passed [nil]" do
166
+ lambda {
167
+ TaggableModel.acts_as_taggable_on([nil])
168
+ }.should_not raise_error
169
+ end
170
+ end
171
+
172
+ context 'when tagging context ends in an "s" when singular (ex. "status", "glass", etc.)' do
173
+ describe 'caching' do
174
+ before { @taggable = OtherCachedModel.new(:name => "John Smith") }
175
+ subject { @taggable }
176
+
177
+ it { should respond_to(:save_cached_tag_list) }
178
+ its(:cached_language_list) { should be_blank }
179
+ its(:cached_status_list) { should be_blank }
180
+ its(:cached_glass_list) { should be_blank }
181
+
182
+ context 'language taggings cache after update' do
183
+ before { @taggable.update_attributes(:language_list => 'ruby, .net') }
184
+ subject { @taggable }
185
+
186
+ its(:language_list) { should == ['ruby', '.net']}
187
+ its(:cached_language_list) { should == 'ruby, .net' } # passes
188
+ its(:instance_variables) { should include((RUBY_VERSION < '1.9' ? '@language_list' : :@language_list)) }
189
+ end
190
+
191
+ context 'status taggings cache after update' do
192
+ before { @taggable.update_attributes(:status_list => 'happy, married') }
193
+ subject { @taggable }
194
+
195
+ its(:status_list) { should == ['happy', 'married'] }
196
+ its(:cached_status_list) { should == 'happy, married' } # fails
197
+ its(:cached_status_list) { should_not == '' } # fails, is blank
198
+ its(:instance_variables) { should include((RUBY_VERSION < '1.9' ? '@status_list' : :@status_list)) }
199
+ its(:instance_variables) { should_not include((RUBY_VERSION < '1.9' ? '@statu_list' : :@statu_list)) } # fails, note: one "s"
200
+
201
+ end
202
+
203
+ context 'glass taggings cache after update' do
204
+ before do
205
+ @taggable.update_attributes(:glass_list => 'rectangle, aviator')
206
+ end
207
+
208
+ subject { @taggable }
209
+ its(:glass_list) { should == ['rectangle', 'aviator'] }
210
+ its(:cached_glass_list) { should == 'rectangle, aviator' } # fails
211
+ its(:cached_glass_list) { should_not == '' } # fails, is blank
212
+ if RUBY_VERSION < '1.9'
213
+ its(:instance_variables) { should include('@glass_list') }
214
+ its(:instance_variables) { should_not include('@glas_list') } # fails, note: one "s"
215
+ else
216
+ its(:instance_variables) { should include(:@glass_list) }
217
+ its(:instance_variables) { should_not include(:@glas_list) } # fails, note: one "s"
218
+ end
219
+
220
+ end
221
+ end
222
+ end
223
+
224
+ describe "taggings" do
225
+ before(:each) do
226
+ @taggable = TaggableModel.new(:name => "Art Kram")
227
+ end
228
+
229
+ it 'should return [] taggings' do
230
+ @taggable.taggings.should == []
231
+ end
232
+ end
233
+
234
+ describe "@@remove_unused_tags" do
235
+ before do
236
+ @taggable = TaggableModel.create(:name => "Bob Jones")
237
+ @tag = ActsAsTaggableOn::Tag.create(:name => "awesome")
238
+
239
+ @tagging = ActsAsTaggableOn::Tagging.create(:taggable => @taggable, :tag => @tag, :context => 'tags')
240
+ end
241
+
242
+ context "if set to true" do
243
+ before do
244
+ ActsAsTaggableOn.remove_unused_tags = true
245
+ end
246
+
247
+ it "should remove unused tags after removing taggings" do
248
+ @tagging.destroy
249
+ ActsAsTaggableOn::Tag.find_by_name("awesome").should be_nil
250
+ end
251
+ end
252
+
253
+ context "if set to false" do
254
+ before do
255
+ ActsAsTaggableOn.remove_unused_tags = false
256
+ end
257
+
258
+ it "should not remove unused tags after removing taggings" do
259
+ @tagging.destroy
260
+ ActsAsTaggableOn::Tag.find_by_name("awesome").should == @tag
261
+ end
262
+ end
263
+ end
264
+
265
+ end