acts_as_taggable_on 3.0.0.rc1

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