Skip to content

Tags cannot be removed with tag_ids after calling as_json #1155

Description

@iberianpig

Summary

After calling the as_json method on a model, attempting to remove tags using label_ids = [] does not work - the tags remain attached to the model.

Environment

  • acts-as-taggable-on: v6.0.0 ~ v12.0.0
  • Rails: 5-2-stable ~ 8.0-stable
  • Ruby: 2.7.8 ~ 3.4.1

Steps to Reproduce

  1. Create a model with custom tag class (Label)
  2. Set up acts_as_taggable_on :labels
  3. Add tags using label_ids
  4. Call as_json method
  5. Try to remove tags using label_ids = []

Reproduction Code

  • Save following code as issue_cannot_save_tags_after_as_json.rb in repository root.
  • $ ruby issue_cannot_save_tags_after_as_json.rb
# frozen_string_literal: true

begin
  require "bundler/inline"
rescue LoadError => e
  $stderr.puts "Bundler version 1.10 or later is required. Please update your Bundler"
  raise e
end
gemfile = File.read(File.expand_path("Gemfile", __dir__)).strip
gemfile(true) do
  source "https://rubygems.org"

  git_source(:github) { |repo| "https://github.com/#{repo}.git" }
  gem "logger"

  # gem "rails", github: "rails/rails", branch: "5-1-stable"
  # gem "rails", github: "rails/rails", branch: "5-2-stable"
  # gem "rails", github: "rails/rails", branch: "6-0-stable"
  # gem "rails", github: "rails/rails", branch: "6-1-stable"
  # gem "rails", github: "rails/rails", branch: "7-0-stable"
  # gem "rails", github: "rails/rails", branch: "7-1-stable"
  # gem "sqlite3", '~> 1.4.4'
  
  gem "rails", github: "rails/rails", branch: "8-0-stable"
  gem "sqlite3", '~> 2.1.0'
  gem 'debug'
  gem 'pry-byebug'

  gem 'stringio'

  gem 'acts-as-taggable-on', path: '.'
end

require "active_record"
require "minitest/autorun"
require "logger"

# Database connection setup
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.logger = Logger.new(STDOUT)

# Run acts-as-taggable-on migrations
ActiveRecord::Schema.define do
  # Create acts-as-taggable-on tables
  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

  create_table :taggings, force: true do |t|
    t.integer :tag_id

    t.string :taggable_type
    t.integer :taggable_id

    t.string :tagger_type
    t.integer :tagger_id

    t.string :context, limit: 128

    t.string :tenant , limit: 128

    t.datetime :created_at
  end

  add_index :taggings,
            ['tag_id', 'taggable_id', 'taggable_type', 'context', 'tagger_id', 'tagger_type'],
            unique: true, name: 'taggings_idx'
  add_index :taggings, :tag_id , name: 'index_taggings_on_tag_id'

  # Test model tables
  create_table :articles, force: true do |t|
    t.string :title
    t.text :content
    t.timestamps

    t.text :cached_label_list
  end

end


# Model definitions
class Article < ActiveRecord::Base
  acts_as_taggable_on :labels

  # # Adding this fixes the issue
  # def as_json(options = {})
  #   # Exclude label_list
  #   super(options.merge(except: :label_list))
  # end
end

class Label < ActsAsTaggableOn::Tag
end

# Test class
class ActsAsTaggableOnTest < Minitest::Test

  def test_update_article_with_empty_labels
    article = Article.create!(title: "Test Title", content: "Test Content")

    # Create labels
    label1 = Label.find_or_create_by!(name: "Label1")
    label2 = Label.find_or_create_by!(name: "Label2")

    # Update article (set labels)
    article.update!(
      label_ids: [label1.id, label2.id]
    )

    article = Article.find(article.id)

    article.reload

    # ↓ Removing this makes the test pass
    article.as_json # Accesses article.label_list, causing INSERT/DELETE operations in after_save

    # label_ids immediately performs INSERT/DELETE on labels + then enters after_save
    article.update!(label_ids: [])

    article.reload

    assert_empty article.label_list
    # ActsAsTaggableOnTest#test_update_article_with_empty_labels [issue_cannot_save_tags_after_as_json.rb:126]:
    # Expected ["Label1", "Label2"] to be empty.
  end
end

Expected Behavior

Tags should be completely removed when setting label_ids = []

Actual Behavior

After calling as_json, tags are not removed when using label_ids = []

Workarounds

Exclude label_list from as_json

class Article < ActiveRecord::Base
  acts_as_taggable_on :labels
  
  def as_json(options = {})
    super(options.merge(except: :label_list))
  end
end

Analysis

  • The as_json method accesses label_list, which modifies the internal state
  • Subsequent updates using label_ids cause INSERT/DELETE operations in the after_save callback to malfunction
  • Using label_list works correctly

Additional Information

See the attached issue_cannot_save_tags_after_as_json.rb for the complete reproduction test code.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions