From fcd1ff28a26519588b0a3dddd1b81f6670426ff4 Mon Sep 17 00:00:00 2001
From: Ches Martin <ches@whiskeyandgrits.net>
Date: Fri, 27 Sep 2013 02:34:42 +0700
Subject: [PATCH 1/2] Hook to support STI subclasses of Tag in save_tags

One use case I have is that a custom Tag class is "followable". Because
`Core#save_tags` is a monstrous method, I was dirtily hacking around it
on an override using `with_scope` around `super` -- this eliminates the
need for such a trick.
---
 .../acts_as_taggable_on/core.rb               | 28 ++++++++++++++++++-
 lib/acts_as_taggable_on/tag.rb                |  4 +--
 .../single_table_inheritance_spec.rb          | 20 +++++++++++++
 spec/acts_as_taggable_on/taggable_spec.rb     |  5 +++-
 spec/internal/app/models/models.rb            | 19 +++++++++++++
 spec/internal/db/schema.rb                    |  5 ++++
 6 files changed, 77 insertions(+), 4 deletions(-)

diff --git a/lib/acts_as_taggable_on/acts_as_taggable_on/core.rb b/lib/acts_as_taggable_on/acts_as_taggable_on/core.rb
index 84dbd81f9..f88cc8797 100644
--- a/lib/acts_as_taggable_on/acts_as_taggable_on/core.rb
+++ b/lib/acts_as_taggable_on/acts_as_taggable_on/core.rb
@@ -352,7 +352,7 @@ def save_tags
         tag_list = tag_list_cache_on(context).uniq
 
         # Find existing tags or create non-existing tags:
-        tags = load_tags(tag_list)
+        tags = find_or_create_tags_from_list_with_context(tag_list, context)
 
         # Tag objects for currently assigned tags
         current_tags = tags_on(context)
@@ -394,5 +394,31 @@ def save_tags
 
       true
     end
+
+    private
+
+    ##
+    # Override this hook if you wish to subclass {ActsAsTaggableOn::Tag} --
+    # context is provided so that you may conditionally use a Tag subclass
+    # only for some contexts.
+    #
+    # @example Custom Tag class for one context
+    #   class Company < ActiveRecord::Base
+    #     acts_as_taggable_on :markets, :locations
+    #
+    #     def find_or_create_tags_from_list_with_context(tag_list, context)
+    #       if context.to_sym == :markets
+    #         MarketTag.find_or_create_all_with_like_by_name(tag_list)
+    #       else
+    #         super
+    #       end
+    #     end
+    #
+    # @param [Array<String>] tag_list Tags to find or create
+    # @param [Symbol] context The tag context for the tag_list
+    def find_or_create_tags_from_list_with_context(tag_list, context)
+      load_tags(tag_list)
+    end
   end
 end
+
diff --git a/lib/acts_as_taggable_on/tag.rb b/lib/acts_as_taggable_on/tag.rb
index 9cd64226e..2cbfe3328 100644
--- a/lib/acts_as_taggable_on/tag.rb
+++ b/lib/acts_as_taggable_on/tag.rb
@@ -71,13 +71,13 @@ def self.find_or_create_all_with_like_by_name(*list)
 
       return [] if list.empty?
 
-      existing_tags = Tag.named_any(list)
+      existing_tags = named_any(list)
 
       list.map do |tag_name|
         comparable_tag_name = comparable_name(tag_name)
         existing_tag = existing_tags.find { |tag| comparable_name(tag.name) == comparable_tag_name }
         begin
-          existing_tag || Tag.create(name: tag_name)
+          existing_tag || create(name: tag_name)
         rescue ActiveRecord::RecordNotUnique
           # Postgres aborts the current transaction with
           # PG::InFailedSqlTransaction: ERROR:  current transaction is aborted, commands ignored until end of transaction block
diff --git a/spec/acts_as_taggable_on/single_table_inheritance_spec.rb b/spec/acts_as_taggable_on/single_table_inheritance_spec.rb
index 7d619e2fe..e8c3df231 100644
--- a/spec/acts_as_taggable_on/single_table_inheritance_spec.rb
+++ b/spec/acts_as_taggable_on/single_table_inheritance_spec.rb
@@ -185,4 +185,24 @@
     end
   end
 
+  describe 'a subclass of Tag' do
+    let(:company) { Company.new(:name => 'Dewey, Cheatham & Howe') }
+
+    subject { Market.create! :name => 'finance' }
+
+    its(:type) { should eql 'Market' }
+
+    it 'sets STI type through string list' do
+      company.market_list = 'law, accounting'
+      company.save!
+      expect(Market.count).to eq(2)
+    end
+
+    it 'does not interfere with a normal Tag context on the same model' do
+      company.location_list = 'cambridge'
+      company.save!
+      ActsAsTaggableOn::Tag.where(name: 'cambridge', type: nil).should_not be_empty
+    end
+  end
 end
+
diff --git a/spec/acts_as_taggable_on/taggable_spec.rb b/spec/acts_as_taggable_on/taggable_spec.rb
index 426c27989..a064222a5 100644
--- a/spec/acts_as_taggable_on/taggable_spec.rb
+++ b/spec/acts_as_taggable_on/taggable_spec.rb
@@ -525,7 +525,10 @@
 
   describe 'grouped_column_names_for method' do
     it 'should return all column names joined for Tag GROUP clause' do
-      expect(@taggable.grouped_column_names_for(ActsAsTaggableOn::Tag)).to eq('tags.id, tags.name, tags.taggings_count')
+      # NOTE: type column supports an STI Tag subclass in the test suite, though
+      # isn't included by default in the migration generator
+      expect(@taggable.grouped_column_names_for(ActsAsTaggableOn::Tag)).
+        to eq('tags.id, tags.name, tags.taggings_count, tags.type')
     end
 
     it 'should return all column names joined for TaggableModel GROUP clause' do
diff --git a/spec/internal/app/models/models.rb b/spec/internal/app/models/models.rb
index 51b4b01ae..06bb0c08f 100644
--- a/spec/internal/app/models/models.rb
+++ b/spec/internal/app/models/models.rb
@@ -32,6 +32,25 @@ class AlteredInheritingTaggableModel < TaggableModel
   acts_as_taggable_on :parts
 end
 
+class Market < ActsAsTaggableOn::Tag
+end
+
+class Company < ActiveRecord::Base
+  acts_as_taggable_on :locations, :markets
+
+  has_many :markets, :through => :market_taggings, :source => :tag
+
+  private
+
+  def find_or_create_tags_from_list_with_context(tag_list, context)
+    if context.to_sym == :markets
+      Market.find_or_create_all_with_like_by_name(tag_list)
+    else
+      super
+    end
+  end
+end
+
 class User < ActiveRecord::Base
   acts_as_tagger
 end
diff --git a/spec/internal/db/schema.rb b/spec/internal/db/schema.rb
index 4c3d78427..821aa3df2 100644
--- a/spec/internal/db/schema.rb
+++ b/spec/internal/db/schema.rb
@@ -2,6 +2,7 @@
   create_table :tags, force: true do |t|
     t.string :name
     t.integer :taggings_count, default: 0
+    t.string :type
   end
   add_index 'tags', ['name'], name: 'index_tags_on_name', unique: true
 
@@ -55,6 +56,10 @@
     t.column :cached_glass_list, :string
   end
 
+  create_table :companies, force: true do |t|
+    t.column :name, :string
+  end
+
   create_table :users, force: true do |t|
     t.column :name, :string
   end

From d5889dd7ec48cd4aa791e65d3ca9d9a74b679696 Mon Sep 17 00:00:00 2001
From: Ches Martin <ches@whiskeyandgrits.net>
Date: Fri, 27 Sep 2013 03:19:46 +0700
Subject: [PATCH 2/2] Apply Tag STI hook for owned taggings

---
 lib/acts_as_taggable_on/acts_as_taggable_on/ownership.rb  | 2 +-
 spec/acts_as_taggable_on/single_table_inheritance_spec.rb | 7 +++++++
 2 files changed, 8 insertions(+), 1 deletion(-)

diff --git a/lib/acts_as_taggable_on/acts_as_taggable_on/ownership.rb b/lib/acts_as_taggable_on/acts_as_taggable_on/ownership.rb
index 6a6b157dc..41f2ebfb1 100644
--- a/lib/acts_as_taggable_on/acts_as_taggable_on/ownership.rb
+++ b/lib/acts_as_taggable_on/acts_as_taggable_on/ownership.rb
@@ -79,7 +79,7 @@ def save_owned_tags
         cached_owned_tag_list_on(context).each do |owner, tag_list|
 
           # Find existing tags or create non-existing tags:
-          tags = ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name(tag_list.uniq)
+          tags = find_or_create_tags_from_list_with_context(tag_list.uniq, context)
 
           # Tag objects for owned tags
           owned_tags = owner_tags_on(owner, context)
diff --git a/spec/acts_as_taggable_on/single_table_inheritance_spec.rb b/spec/acts_as_taggable_on/single_table_inheritance_spec.rb
index e8c3df231..a15909209 100644
--- a/spec/acts_as_taggable_on/single_table_inheritance_spec.rb
+++ b/spec/acts_as_taggable_on/single_table_inheritance_spec.rb
@@ -187,6 +187,7 @@
 
   describe 'a subclass of Tag' do
     let(:company) { Company.new(:name => 'Dewey, Cheatham & Howe') }
+    let(:user) { User.create! }
 
     subject { Market.create! :name => 'finance' }
 
@@ -203,6 +204,12 @@
       company.save!
       ActsAsTaggableOn::Tag.where(name: 'cambridge', type: nil).should_not be_empty
     end
+
+    it 'is returned with proper type through ownership' do
+      user.tag(company, :with => 'ripoffs, rackets', :on => :markets)
+      tags = company.owner_tags_on(user, :markets)
+      tags.all? { |tag| tag.is_a? Market }.should be_truthy
+    end
   end
 end