acts-as-taggable-on 3.4.2 → 4.0.0

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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/.travis.yml +22 -29
  4. data/Appraisals +8 -13
  5. data/CHANGELOG.md +16 -10
  6. data/Gemfile +2 -2
  7. data/README.md +88 -20
  8. data/Rakefile +2 -0
  9. data/UPGRADING.md +2 -7
  10. data/acts-as-taggable-on.gemspec +3 -3
  11. data/db/migrate/2_add_missing_unique_indices.rb +3 -2
  12. data/db/migrate/5_change_collation_for_tag_names.rb +9 -0
  13. data/db/migrate/6_add_missing_indexes.rb +12 -0
  14. data/gemfiles/activerecord_4.0.gemfile +2 -0
  15. data/gemfiles/activerecord_4.1.gemfile +2 -0
  16. data/gemfiles/activerecord_4.2.gemfile +2 -3
  17. data/gemfiles/{activerecord_3.2.gemfile → activerecord_5.0.gemfile} +2 -1
  18. data/lib/acts-as-taggable-on.rb +40 -5
  19. data/lib/acts_as_taggable_on/engine.rb +0 -1
  20. data/lib/acts_as_taggable_on/tag.rb +7 -3
  21. data/lib/acts_as_taggable_on/tag_list.rb +2 -13
  22. data/lib/acts_as_taggable_on/taggable/cache.rb +5 -1
  23. data/lib/acts_as_taggable_on/taggable/collection.rb +6 -3
  24. data/lib/acts_as_taggable_on/taggable/core.rb +21 -16
  25. data/lib/acts_as_taggable_on/taggable/ownership.rb +3 -3
  26. data/lib/acts_as_taggable_on/taggable/related.rb +1 -1
  27. data/lib/acts_as_taggable_on/taggable.rb +2 -2
  28. data/lib/acts_as_taggable_on/tagger.rb +12 -11
  29. data/lib/acts_as_taggable_on/tagging.rb +5 -15
  30. data/lib/acts_as_taggable_on/utils.rb +3 -4
  31. data/lib/acts_as_taggable_on/version.rb +1 -1
  32. data/lib/tasks/tags_collate_utf8.rake +21 -0
  33. data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +10 -1
  34. data/spec/acts_as_taggable_on/acts_as_tagger_spec.rb +1 -1
  35. data/spec/acts_as_taggable_on/caching_spec.rb +28 -0
  36. data/spec/acts_as_taggable_on/related_spec.rb +9 -0
  37. data/spec/acts_as_taggable_on/tag_list_spec.rb +27 -1
  38. data/spec/acts_as_taggable_on/tag_spec.rb +18 -1
  39. data/spec/acts_as_taggable_on/taggable_spec.rb +19 -5
  40. data/spec/acts_as_taggable_on/tagging_spec.rb +64 -10
  41. data/spec/acts_as_taggable_on/utils_spec.rb +7 -0
  42. data/spec/internal/db/schema.rb +7 -3
  43. data/spec/support/database.rb +1 -7
  44. metadata +14 -28
  45. data/gemfiles/activerecord_edge.gemfile +0 -15
  46. data/lib/acts_as_taggable_on/compatibility.rb +0 -35
  47. data/lib/acts_as_taggable_on/tag_list_parser.rb +0 -21
  48. data/spec/acts_as_taggable_on/tag_list_parser_spec.rb +0 -46
@@ -40,6 +40,10 @@ module ActsAsTaggableOn::Taggable
40
40
  end
41
41
  end
42
42
 
43
+ def reset_column_information
44
+ super
45
+ @acts_as_taggable_on_cache_columns = nil
46
+ end
43
47
  end
44
48
  end
45
49
 
@@ -69,7 +73,7 @@ module ActsAsTaggableOn::Taggable
69
73
  tag_types.map(&:to_s).each do |tag_type|
70
74
  if self.class.send("caching_#{tag_type.singularize}_list?")
71
75
  if tag_list_cache_set_on(tag_type)
72
- list = tag_list_cache_on(tag_type).to_a.flatten.compact.join(', ')
76
+ list = tag_list_cache_on(tag_type).to_a.flatten.compact.join("#{ActsAsTaggableOn.delimiter} ")
73
77
  self["cached_#{tag_type.singularize}_list"] = list
74
78
  end
75
79
  end
@@ -138,7 +138,7 @@ module ActsAsTaggableOn::Taggable
138
138
  scoped_ids = pluck(table_name_pkey)
139
139
  tagging_scope = tagging_scope.where("#{ActsAsTaggableOn::Tagging.table_name}.taggable_id IN (?)", scoped_ids)
140
140
  else
141
- tagging_scope = tagging_scope.where("#{ActsAsTaggableOn::Tagging.table_name}.taggable_id IN(#{safe_to_sql(select(table_name_pkey))})")
141
+ tagging_scope = tagging_scope.where("#{ActsAsTaggableOn::Tagging.table_name}.taggable_id IN(#{safe_to_sql(except(:select).select(table_name_pkey))})")
142
142
  end
143
143
 
144
144
  tagging_scope
@@ -169,9 +169,12 @@ module ActsAsTaggableOn::Taggable
169
169
  end
170
170
 
171
171
  module CalculationMethods
172
- def count(column_name=:all)
172
+ # Rails 5 TODO: Remove options argument as soon we remove support to
173
+ # activerecord-deprecated_finders.
174
+ # See https://github.com/rails/rails/blob/master/activerecord/lib/active_record/relation/calculations.rb#L38
175
+ def count(column_name = :all, options = {})
173
176
  # https://github.com/rails/rails/commit/da9b5d4a8435b744fcf278fffd6d7f1e36d4a4f2
174
- super
177
+ ActsAsTaggableOn::Utils.active_record5? ? super(column_name) : super(column_name, options)
175
178
  end
176
179
  end
177
180
  end
@@ -23,18 +23,15 @@ module ActsAsTaggableOn::Taggable
23
23
  class_eval do
24
24
  # when preserving tag order, include order option so that for a 'tags' context
25
25
  # the associations tag_taggings & tags are always returned in created order
26
- has_many_with_taggable_compatibility context_taggings, as: :taggable,
27
- dependent: :destroy,
28
- class_name: 'ActsAsTaggableOn::Tagging',
29
- order: taggings_order,
30
- conditions: {context: tags_type},
31
- include: :tag
32
-
33
- has_many_with_taggable_compatibility context_tags, through: context_taggings,
34
- source: :tag,
35
- class_name: 'ActsAsTaggableOn::Tag',
36
- order: taggings_order
37
-
26
+ has_many context_taggings, -> { includes(:tag).order(taggings_order).where(context: tags_type) },
27
+ as: :taggable,
28
+ class_name: ActsAsTaggableOn::Tagging,
29
+ dependent: :destroy
30
+
31
+ has_many context_tags, -> { order(taggings_order) },
32
+ class_name: ActsAsTaggableOn::Tag,
33
+ through: context_taggings,
34
+ source: :tag
38
35
  end
39
36
 
40
37
  taggable_mixin.class_eval <<-RUBY, __FILE__, __LINE__ + 1
@@ -246,6 +243,14 @@ module ActsAsTaggableOn::Taggable
246
243
  def taggable_mixin
247
244
  @taggable_mixin ||= Module.new
248
245
  end
246
+
247
+ private
248
+
249
+ # Rails 5 has merged sanitize and quote_value
250
+ # See https://github.com/rails/rails/blob/master/activerecord/lib/active_record/sanitization.rb#L10
251
+ def quote_value(value, column = nil)
252
+ ActsAsTaggableOn::Utils.active_record5? ? super(value) : super(value, column)
253
+ end
249
254
  end
250
255
 
251
256
  # all column names are necessary for PostgreSQL group clause
@@ -254,7 +259,7 @@ module ActsAsTaggableOn::Taggable
254
259
  end
255
260
 
256
261
  def custom_contexts
257
- @custom_contexts ||= []
262
+ @custom_contexts ||= taggings.map(&:context).uniq
258
263
  end
259
264
 
260
265
  def is_taggable?
@@ -333,7 +338,7 @@ module ActsAsTaggableOn::Taggable
333
338
  end
334
339
 
335
340
  def tagging_contexts
336
- custom_contexts + self.class.tag_types.map(&:to_s)
341
+ self.class.tag_types.map(&:to_s) + custom_contexts
337
342
  end
338
343
 
339
344
  def process_dirty_object(context, new_list)
@@ -407,7 +412,7 @@ module ActsAsTaggableOn::Taggable
407
412
 
408
413
  # Destroy old taggings:
409
414
  if old_tags.present?
410
- taggings.not_owned.by_context(context).destroy_all(tag_id: old_tags)
415
+ taggings.not_owned.by_context(context).where(tag_id: old_tags).destroy_all
411
416
  end
412
417
 
413
418
  # Create new taggings:
@@ -432,7 +437,7 @@ module ActsAsTaggableOn::Taggable
432
437
  tag_lists = tag_types.map {|tags_type| "#{tags_type.to_s.singularize}_list"}
433
438
  super.delete_if {|attr| tag_lists.include? attr }
434
439
  end
435
-
440
+
436
441
  ##
437
442
  # Override this hook if you wish to subclass {ActsAsTaggableOn::Tag} --
438
443
  # context is provided so that you may conditionally use a Tag subclass
@@ -108,9 +108,9 @@ module ActsAsTaggableOn::Taggable
108
108
 
109
109
  # Find all taggings that belong to the taggable (self), are owned by the owner,
110
110
  # have the correct context, and are removed from the list.
111
- ActsAsTaggableOn::Tagging.destroy_all(taggable_id: id, taggable_type: self.class.base_class.to_s,
112
- tagger_type: owner.class.base_class.to_s, tagger_id: owner.id,
113
- tag_id: old_tags, context: context) if old_tags.present?
111
+ ActsAsTaggableOn::Tagging.where(taggable_id: id, taggable_type: self.class.base_class.to_s,
112
+ tagger_type: owner.class.base_class.to_s, tagger_id: owner.id,
113
+ tag_id: old_tags, context: context).destroy_all if old_tags.present?
114
114
 
115
115
  # Create new taggings:
116
116
  new_tags.each do |tag|
@@ -43,7 +43,7 @@ module ActsAsTaggableOn::Taggable
43
43
  def related_tags_for(context, klass, options = {})
44
44
  tags_to_ignore = Array.wrap(options[:ignore]).map(&:to_s) || []
45
45
  tags_to_find = tags_on(context).map { |t| t.name }.reject { |t| tags_to_ignore.include? t }
46
- related_where(klass, ["#{exclude_self(klass, id)} #{klass.table_name}.#{klass.primary_key} = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = '#{klass.base_class}' AND #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key} AND #{ActsAsTaggableOn::Tag.table_name}.name IN (?)", tags_to_find])
46
+ related_where(klass, ["#{exclude_self(klass, id)} #{klass.table_name}.#{klass.primary_key} = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = '#{klass.base_class}' AND #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key} AND #{ActsAsTaggableOn::Tag.table_name}.name IN (?) AND #{ActsAsTaggableOn::Tagging.table_name}.context = ?", tags_to_find, context])
47
47
  end
48
48
 
49
49
  private
@@ -80,8 +80,8 @@ module ActsAsTaggableOn
80
80
  self.preserve_tag_order = preserve_tag_order
81
81
 
82
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'
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
85
 
86
86
  def self.taggable?
87
87
  true
@@ -15,18 +15,19 @@ module ActsAsTaggableOn
15
15
  # end
16
16
  def acts_as_tagger(opts={})
17
17
  class_eval do
18
- has_many_with_taggable_compatibility :owned_taggings,
19
- opts.merge(
20
- as: :tagger,
21
- dependent: :destroy,
22
- class_name: 'ActsAsTaggableOn::Tagging'
23
- )
18
+ owned_taggings_scope = opts.delete(:scope)
24
19
 
25
- has_many_with_taggable_compatibility :owned_tags,
26
- through: :owned_taggings,
27
- source: :tag,
28
- class_name: 'ActsAsTaggableOn::Tag',
29
- uniq: true
20
+ has_many :owned_taggings, owned_taggings_scope,
21
+ opts.merge(
22
+ as: :tagger,
23
+ class_name: ::ActsAsTaggableOn::Tagging,
24
+ dependent: :destroy
25
+ )
26
+
27
+ has_many :owned_tags, -> { distinct },
28
+ class_name: ::ActsAsTaggableOn::Tag,
29
+ source: :tag,
30
+ through: :owned_taggings
30
31
  end
31
32
 
32
33
  include ActsAsTaggableOn::Tagger::InstanceMethods
@@ -1,25 +1,15 @@
1
1
  module ActsAsTaggableOn
2
2
  class Tagging < ::ActiveRecord::Base #:nodoc:
3
- #TODO, remove from 4.0.0
4
- attr_accessible :tag,
5
- :tag_id,
6
- :context,
7
- :taggable,
8
- :taggable_type,
9
- :taggable_id,
10
- :tagger,
11
- :tagger_type,
12
- :tagger_id if defined?(ActiveModel::MassAssignmentSecurity)
13
-
14
- belongs_to :tag, class_name: 'ActsAsTaggableOn::Tag', counter_cache: ActsAsTaggableOn.tags_counter
3
+ belongs_to :tag, class_name: '::ActsAsTaggableOn::Tag', counter_cache: ActsAsTaggableOn.tags_counter
15
4
  belongs_to :taggable, polymorphic: true
16
- belongs_to :tagger, polymorphic: true
5
+
6
+ belongs_to :tagger, {polymorphic: true}.tap {|o| o.merge!(optional: true) if ActsAsTaggableOn::Utils.active_record5? }
17
7
 
18
8
  scope :owned_by, ->(owner) { where(tagger: owner) }
19
9
  scope :not_owned, -> { where(tagger_id: nil, tagger_type: nil) }
20
10
 
21
- scope :by_contexts, ->(contexts = ['tags']) { where(context: contexts) }
22
- scope :by_context, ->(context= 'tags') { by_contexts(context.to_s) }
11
+ scope :by_contexts, ->(contexts) { where(context: (contexts || 'tags')) }
12
+ scope :by_context, ->(context = 'tags') { by_contexts(context.to_s) }
23
13
 
24
14
  validates_presence_of :context
25
15
  validates_presence_of :tag_id
@@ -13,16 +13,15 @@ module ActsAsTaggableOn
13
13
  end
14
14
 
15
15
  def using_mysql?
16
- #We should probably use regex for mysql to support prehistoric adapters
17
16
  connection && connection.adapter_name == 'Mysql2'
18
17
  end
19
18
 
20
19
  def sha_prefix(string)
21
- Digest::SHA1.hexdigest("#{string}#{rand}")[0..6]
20
+ Digest::SHA1.hexdigest(string)[0..6]
22
21
  end
23
22
 
24
- def active_record4?
25
- ::ActiveRecord::VERSION::MAJOR == 4
23
+ def active_record5?
24
+ ::ActiveRecord::VERSION::MAJOR == 5
26
25
  end
27
26
 
28
27
  def like_operator
@@ -1,4 +1,4 @@
1
1
  module ActsAsTaggableOn
2
- VERSION = '3.4.2'
2
+ VERSION = '4.0.0'
3
3
  end
4
4
 
@@ -0,0 +1,21 @@
1
+ # These rake tasks are to be run by MySql users only, they fix the management of
2
+ # binary-encoded strings for tag 'names'. Issues:
3
+ # https://github.com/mbleigh/acts-as-taggable-on/issues/623
4
+
5
+ namespace :acts_as_taggable_on_engine do
6
+
7
+ namespace :tag_names do
8
+
9
+ desc "Forcing collate of tag names to utf8_bin"
10
+ task :collate_bin => [:environment] do |t, args|
11
+ ActsAsTaggableOn::Configuration.apply_binary_collation(true)
12
+ end
13
+
14
+ desc "Forcing collate of tag names to utf8_general_ci"
15
+ task :collate_ci => [:environment] do |t, args|
16
+ ActsAsTaggableOn::Configuration.apply_binary_collation(false)
17
+ end
18
+
19
+ end
20
+
21
+ end
@@ -154,7 +154,7 @@ describe 'Acts As Taggable On' do
154
154
  describe 'Tagging Contexts' do
155
155
  it 'should eliminate duplicate tagging contexts ' do
156
156
  TaggableModel.acts_as_taggable_on(:skills, :skills)
157
- expect(TaggableModel.tag_types.freq[:skills]).to_not eq(3)
157
+ expect(TaggableModel.tag_types.freq[:skills]).to eq(1)
158
158
  end
159
159
 
160
160
  it 'should not contain embedded/nested arrays' do
@@ -178,6 +178,15 @@ describe 'Acts As Taggable On' do
178
178
  TaggableModel.acts_as_taggable_on([nil])
179
179
  }).to_not raise_error
180
180
  end
181
+
182
+ it 'should include dynamic contexts in tagging_contexts' do
183
+ taggable = TaggableModel.create!(name: 'Dynamic Taggable')
184
+ taggable.set_tag_list_on :colors, 'tag1, tag2, tag3'
185
+ expect(taggable.tagging_contexts).to eq(%w(tags languages skills needs offerings array colors))
186
+ taggable.save
187
+ taggable = TaggableModel.where(name: 'Dynamic Taggable').first
188
+ expect(taggable.tagging_contexts).to eq(%w(tags languages skills needs offerings array colors))
189
+ end
181
190
  end
182
191
 
183
192
  context 'when tagging context ends in an "s" when singular (ex. "status", "glass", etc.)' do
@@ -72,7 +72,7 @@ describe 'acts_as_tagger' do
72
72
  expect(@taggable.tag_list_on(:foo_boo)).to be_empty
73
73
  expect(-> {
74
74
  @tagger.tag(@taggable, with: 'this, and, that', on: :foo_boo, force: false)
75
- }).to raise_error
75
+ }).to raise_error(RuntimeError)
76
76
  end
77
77
 
78
78
  it 'should not create the tag context on-the-fly when the default is over-ridden' do
@@ -69,6 +69,34 @@ describe 'Acts As Taggable On' do
69
69
  @taggable.save!
70
70
  expect(@taggable.tag_list.sort).to eq(%w(awesome epic).sort)
71
71
  end
72
+
73
+ it 'should clear the cache on reset_column_information' do
74
+ CachedModel.column_names
75
+ CachedModel.reset_column_information
76
+ expect(CachedModel.instance_variable_get(:@acts_as_taggable_on_cache_columns)).to eql(nil)
77
+ end
78
+ end
79
+
80
+ describe 'with a custom delimiter' do
81
+ before(:each) do
82
+ @taggable = CachedModel.new(name: 'Bob Jones')
83
+ @another_taggable = OtherCachedModel.new(name: 'John Smith')
84
+ ActsAsTaggableOn.delimiter = ';'
85
+ end
86
+
87
+ after(:all) do
88
+ ActsAsTaggableOn.delimiter = ','
89
+ end
90
+
91
+ it 'should cache tags with custom delimiter' do
92
+ @taggable.update_attributes(tag_list: 'awesome; epic')
93
+ expect(@taggable.tag_list).to eq(['awesome', 'epic'])
94
+ expect(@taggable.cached_tag_list).to eq('awesome; epic')
95
+
96
+ @taggable = CachedModel.find_by_name('Bob Jones')
97
+ expect(@taggable.tag_list).to eq(['awesome', 'epic'])
98
+ expect(@taggable.cached_tag_list).to eq('awesome; epic')
99
+ end
72
100
  end
73
101
 
74
102
  describe 'CachingWithArray' do
@@ -42,6 +42,15 @@ describe 'Acts As Taggable On' do
42
42
  expect(taggable1.find_related_tags_for(OtherTaggableModel)).to_not include(taggable2)
43
43
  end
44
44
 
45
+ it 'should find other related objects based on tags only from particular context' do
46
+ taggable1 = TaggableModel.create!(name: 'Taggable 1',tag_list: 'one, two')
47
+ taggable2 = TaggableModel.create!(name: 'Taggable 2',tag_list: 'three, four', skill_list: 'one, two')
48
+ taggable3 = TaggableModel.create!(name: 'Taggable 3',tag_list: 'one, four')
49
+
50
+ expect(taggable1.find_related_tags).to include(taggable3)
51
+ expect(taggable1.find_related_tags).to_not include(taggable2)
52
+ end
53
+
45
54
 
46
55
  shared_examples "a collection" do
47
56
  it do
@@ -83,6 +83,18 @@ describe ActsAsTaggableOn::TagList do
83
83
  new_tag_list = tag_list.concat(another_tag_list)
84
84
  expect(new_tag_list.class).to eq(ActsAsTaggableOn::TagList)
85
85
  end
86
+
87
+ context 'without duplicates' do
88
+ let(:arr) { ['crazy', 'alien'] }
89
+ let(:another_tag_list) { ActsAsTaggableOn::TagList.new(*arr) }
90
+ it 'adds other list' do
91
+ expect(tag_list.concat(another_tag_list)).to eq(%w[awesome radical crazy alien])
92
+ end
93
+
94
+ it 'adds other array' do
95
+ expect(tag_list.concat(arr)).to eq(%w[awesome radical crazy alien])
96
+ end
97
+ end
86
98
  end
87
99
 
88
100
  describe '#to_s' do
@@ -92,7 +104,7 @@ describe ActsAsTaggableOn::TagList do
92
104
 
93
105
  it 'should be able to call to_s on a frozen tag list' do
94
106
  tag_list.freeze
95
- expect(-> { tag_list.add('cool', 'rad,bodacious') }).to raise_error
107
+ expect(-> { tag_list.add('cool', 'rad,bodacious') }).to raise_error(RuntimeError)
96
108
  expect(-> { tag_list.to_s }).to_not raise_error
97
109
  end
98
110
  end
@@ -114,6 +126,20 @@ describe ActsAsTaggableOn::TagList do
114
126
 
115
127
  ActsAsTaggableOn.force_lowercase = false
116
128
  end
129
+
130
+ it 'should ignore case when removing duplicates if strict_case_match is false' do
131
+ tag_list = ActsAsTaggableOn::TagList.new('Junglist', 'JUNGLIST', 'Junglist', 'Massive', 'MASSIVE', 'MASSIVE')
132
+
133
+ expect(tag_list.to_s).to eq('Junglist, Massive')
134
+ end
135
+
136
+ it 'should not ignore case when removing duplicates if strict_case_match is true' do
137
+ ActsAsTaggableOn.strict_case_match = true
138
+ tag_list = ActsAsTaggableOn::TagList.new('Junglist', 'JUNGLIST', 'Junglist', 'Massive', 'MASSIVE', 'MASSIVE')
139
+
140
+ expect(tag_list.to_s).to eq('Junglist, JUNGLIST, Massive, MASSIVE')
141
+ ActsAsTaggableOn.strict_case_match = false
142
+ end
117
143
  end
118
144
 
119
145
  describe 'custom parser' do
@@ -2,6 +2,7 @@
2
2
  require 'spec_helper'
3
3
  require 'db/migrate/2_add_missing_unique_indices.rb'
4
4
 
5
+
5
6
  shared_examples_for 'without unique index' do
6
7
  prepend_before(:all) { AddMissingUniqueIndices.down }
7
8
  append_after(:all) do
@@ -54,6 +55,21 @@ describe ActsAsTaggableOn::Tag do
54
55
  end
55
56
  end
56
57
 
58
+ describe 'for context' do
59
+ before(:each) do
60
+ @user.skill_list.add('ruby')
61
+ @user.save
62
+ end
63
+
64
+ it 'should return tags that have been used in the given context' do
65
+ expect(ActsAsTaggableOn::Tag.for_context('skills').pluck(:name)).to include('ruby')
66
+ end
67
+
68
+ it 'should not return tags that have been used in other contexts' do
69
+ expect(ActsAsTaggableOn::Tag.for_context('needs').pluck(:name)).to_not include('ruby')
70
+ end
71
+ end
72
+
57
73
  describe 'find or create by name' do
58
74
  before(:each) do
59
75
  @tag.name = 'awesome'
@@ -309,7 +325,7 @@ describe ActsAsTaggableOn::Tag do
309
325
  tag.save!
310
326
  end
311
327
  end
312
-
328
+
313
329
  it 'should find the most popular tags' do
314
330
  expect(ActsAsTaggableOn::Tag.most_used(3).first.name).to eq("golden_syrup")
315
331
  expect(ActsAsTaggableOn::Tag.most_used(3).length).to eq(3)
@@ -320,4 +336,5 @@ describe ActsAsTaggableOn::Tag do
320
336
  expect(ActsAsTaggableOn::Tag.least_used(3).length).to eq(3)
321
337
  end
322
338
  end
339
+
323
340
  end
@@ -119,6 +119,21 @@ describe 'Taggable' do
119
119
  expect(@taggable.tag_counts_on(:tags).length).to eq(2)
120
120
  end
121
121
 
122
+ context 'tag_counts on a collection' do
123
+ context 'a select clause is specified on the collection' do
124
+ it 'should return tag counts without raising an error' do
125
+ expect(TaggableModel.tag_counts_on(:tags)).to be_empty
126
+
127
+ @taggable.tag_list = %w(awesome epic)
128
+ @taggable.save
129
+
130
+ expect {
131
+ expect(TaggableModel.select(:name).tag_counts_on(:tags).length).to eq(2)
132
+ }.not_to raise_error
133
+ end
134
+ end
135
+ end
136
+
122
137
  it 'should have tags_on' do
123
138
  expect(TaggableModel.tags_on(:tags)).to be_empty
124
139
 
@@ -226,8 +241,10 @@ describe 'Taggable' do
226
241
  it "should be able to find a tag using dates" do
227
242
  @taggable.skill_list = "ruby"
228
243
  @taggable.save
244
+ today = Date.today.to_time.utc
245
+ tomorrow = Date.tomorrow.to_time.utc
229
246
 
230
- expect(TaggableModel.tagged_with("ruby", :start_at => Date.today, :end_at => Date.tomorrow).count).to eq(1)
247
+ expect(TaggableModel.tagged_with("ruby", :start_at => today, :end_at => tomorrow).count).to eq(1)
231
248
  end
232
249
 
233
250
  it "shouldn't be able to find a tag outside date range" do
@@ -380,7 +397,6 @@ describe 'Taggable' do
380
397
  # Test specific join syntaxes:
381
398
  frank.untaggable_models.create!
382
399
  expect(TaggableModel.tagged_with('rails').joins(:untaggable_models).all_tag_counts.size).to eq(2)
383
- expect(TaggableModel.tagged_with('rails').joins(untaggable_models: :taggable_model).all_tag_counts.size).to eq(2)
384
400
  expect(TaggableModel.tagged_with('rails').joins([:untaggable_models]).all_tag_counts.size).to eq(2)
385
401
  end
386
402
 
@@ -395,7 +411,6 @@ describe 'Taggable' do
395
411
  # Test specific join syntaxes:
396
412
  frank.untaggable_models.create!
397
413
  expect(TaggableModel.tagged_with('rails').joins(:untaggable_models).all_tags.size).to eq(2)
398
- expect(TaggableModel.tagged_with('rails').joins(untaggable_models: :taggable_model).all_tags.size).to eq(2)
399
414
  expect(TaggableModel.tagged_with('rails').joins([:untaggable_models]).all_tags.size).to eq(2)
400
415
  end
401
416
 
@@ -459,7 +474,7 @@ describe 'Taggable' do
459
474
 
460
475
  expect(TaggableModel.tagged_with(%w(bob tricia), wild: true, any: true).to_a.sort_by { |o| o.id }).to eq([bob, frank, steve])
461
476
  expect(TaggableModel.tagged_with(%w(bob tricia), wild: true, exclude: true).to_a).to eq([jim])
462
- expect(TaggableModel.tagged_with('ji', wild: true, any: true).to_a).to eq([frank, jim])
477
+ expect(TaggableModel.tagged_with('ji', wild: true, any: true).to_a =~ [frank, jim])
463
478
  end
464
479
  end
465
480
 
@@ -743,7 +758,6 @@ describe 'Taggable' do
743
758
 
744
759
  context 'Model.limit(x).tag_counts.sum(:tags_count)' do
745
760
  it 'should not break on Mysql' do
746
- # Activerecord 3.2 return a string
747
761
  expect(TaggableModel.limit(2).tag_counts.sum('tags_count').to_i).to eq(5)
748
762
  end
749
763
  end
@@ -49,15 +49,69 @@ describe ActsAsTaggableOn::Tagging do
49
49
  ActsAsTaggableOn.remove_unused_tags = previous_setting
50
50
  end
51
51
 
52
- pending 'context scopes' do
53
- describe '.by_context'
54
-
55
- describe '.by_contexts'
56
-
57
- describe '.owned_by'
58
-
59
- describe '.not_owned'
60
-
52
+ describe 'context scopes' do
53
+ before do
54
+ @tagging_2 = ActsAsTaggableOn::Tagging.new
55
+ @tagging_3 = ActsAsTaggableOn::Tagging.new
56
+
57
+ @tagger = User.new
58
+ @tagger_2 = User.new
59
+
60
+ @tagging.taggable = TaggableModel.create(name: "Black holes")
61
+ @tagging.tag = ActsAsTaggableOn::Tag.create(name: "Physics")
62
+ @tagging.tagger = @tagger
63
+ @tagging.context = 'Science'
64
+ @tagging.save
65
+
66
+ @tagging_2.taggable = TaggableModel.create(name: "Satellites")
67
+ @tagging_2.tag = ActsAsTaggableOn::Tag.create(name: "Technology")
68
+ @tagging_2.tagger = @tagger_2
69
+ @tagging_2.context = 'Science'
70
+ @tagging_2.save
71
+
72
+ @tagging_3.taggable = TaggableModel.create(name: "Satellites")
73
+ @tagging_3.tag = ActsAsTaggableOn::Tag.create(name: "Engineering")
74
+ @tagging_3.tagger = @tagger_2
75
+ @tagging_3.context = 'Astronomy'
76
+ @tagging_3.save
77
+
78
+ end
79
+
80
+ describe '.owned_by' do
81
+ it "should belong to a specific user" do
82
+ expect(ActsAsTaggableOn::Tagging.owned_by(@tagger).first).to eq(@tagging)
83
+ end
84
+ end
85
+
86
+ describe '.by_context' do
87
+ it "should be found by context" do
88
+ expect(ActsAsTaggableOn::Tagging.by_context('Science').length).to eq(2);
89
+ end
90
+ end
91
+
92
+ describe '.by_contexts' do
93
+ it "should find taggings by contexts" do
94
+ expect(ActsAsTaggableOn::Tagging.by_contexts(['Science', 'Astronomy']).first).to eq(@tagging);
95
+ expect(ActsAsTaggableOn::Tagging.by_contexts(['Science', 'Astronomy']).second).to eq(@tagging_2);
96
+ expect(ActsAsTaggableOn::Tagging.by_contexts(['Science', 'Astronomy']).third).to eq(@tagging_3);
97
+ expect(ActsAsTaggableOn::Tagging.by_contexts(['Science', 'Astronomy']).length).to eq(3);
98
+ end
99
+ end
100
+
101
+ describe '.not_owned' do
102
+ before do
103
+ @tagging_4 = ActsAsTaggableOn::Tagging.new
104
+ @tagging_4.taggable = TaggableModel.create(name: "Gravity")
105
+ @tagging_4.tag = ActsAsTaggableOn::Tag.create(name: "Space")
106
+ @tagging_4.context = "Science"
107
+ @tagging_4.save
108
+ end
109
+
110
+ it "should found the taggings that do not have owner" do
111
+ expect(ActsAsTaggableOn::Tagging.all.length).to eq(4)
112
+ expect(ActsAsTaggableOn::Tagging.not_owned.length).to eq(1)
113
+ expect(ActsAsTaggableOn::Tagging.not_owned.first).to eq(@tagging_4)
114
+ end
115
+ end
61
116
  end
62
-
63
117
  end
@@ -13,4 +13,11 @@ describe ActsAsTaggableOn::Utils do
13
13
  expect(ActsAsTaggableOn::Utils.like_operator).to eq('LIKE')
14
14
  end
15
15
  end
16
+
17
+ describe '#sha_prefix' do
18
+ it 'should return a consistent prefix for a given word' do
19
+ expect(ActsAsTaggableOn::Utils.sha_prefix('kittens')).to eq(ActsAsTaggableOn::Utils.sha_prefix('kittens'))
20
+ expect(ActsAsTaggableOn::Utils.sha_prefix('puppies')).not_to eq(ActsAsTaggableOn::Utils.sha_prefix('kittens'))
21
+ end
22
+ end
16
23
  end
@@ -7,12 +7,15 @@ ActiveRecord::Schema.define version: 0 do
7
7
  add_index 'tags', ['name'], name: 'index_tags_on_name', unique: true
8
8
 
9
9
  create_table :taggings, force: true do |t|
10
- t.references :tag
10
+ t.integer :tag_id
11
11
 
12
12
  # You should make sure that the column created is
13
13
  # long enough to store the required class names.
14
- t.references :taggable, polymorphic: true
15
- t.references :tagger, polymorphic: true
14
+ t.string :taggable_type
15
+ t.integer :taggable_id
16
+
17
+ t.string :tagger_type
18
+ t.integer :tagger_id
16
19
 
17
20
  # Limit is created to prevent MySQL error on index
18
21
  # length for MyISAM table type: http://bit.ly/vgW2Ql
@@ -23,6 +26,7 @@ ActiveRecord::Schema.define version: 0 do
23
26
  add_index 'taggings',
24
27
  ['tag_id', 'taggable_id', 'taggable_type', 'context', 'tagger_id', 'tagger_type'],
25
28
  unique: true, name: 'taggings_idx'
29
+ add_index 'taggings', :tag_id , name: 'index_taggings_on_tag_id'
26
30
 
27
31
  # above copied from
28
32
  # generators/acts_as_taggable_on/migration/migration_generator
@@ -13,13 +13,7 @@ if File.exist?(database_yml)
13
13
  config = ActiveRecord::Base.configurations[db_name]
14
14
 
15
15
  begin
16
- #activerecord 4 uses symbol
17
- #TODO, remove when activerecord 3 support is dropped
18
- if ActsAsTaggableOn::Utils.active_record4?
19
- ActiveRecord::Base.establish_connection(db_name.to_sym)
20
- else
21
- ActiveRecord::Base.establish_connection(db_name)
22
- end
16
+ ActiveRecord::Base.establish_connection(db_name.to_sym)
23
17
  ActiveRecord::Base.connection
24
18
  rescue
25
19
  case db_name