acts-as-taggable-on-11.0.0/ 0000755 0000041 0000041 00000000000 14704600021 015324 5 ustar www-data www-data acts-as-taggable-on-11.0.0/.gitignore 0000644 0000041 0000041 00000000174 14704600021 017316 0 ustar www-data www-data *.log
*.sqlite3
/pkg/*
.bundle
.ruby-version
spec/internal/config/database.yml
tmp*.sw?
*.sw?
tmp
*.gem
*.lock
*.iml
/.idea
acts-as-taggable-on-11.0.0/acts-as-taggable-on.gemspec 0000644 0000041 0000041 00000002374 14704600021 022410 0 ustar www-data www-data # coding: utf-8
require_relative 'lib/acts-as-taggable-on/version'
Gem::Specification.new do |gem|
gem.name = 'acts-as-taggable-on'
gem.version = ActsAsTaggableOn::VERSION
gem.authors = ['Michael Bleigh', 'Joost Baaij']
gem.email = %w(michael@intridea.com joost@spacebabies.nl)
gem.description = %q{With ActsAsTaggableOn, you can tag a single model on several contexts, such as skills, interests, and awards. It also provides other advanced functionality.}
gem.summary = 'Advanced tagging for Rails.'
gem.homepage = 'https://github.com/mbleigh/acts-as-taggable-on'
gem.license = 'MIT'
gem.files = `git ls-files`.split($/)
gem.test_files = gem.files.grep(%r{^spec/})
gem.require_paths = ['lib']
gem.required_ruby_version = '>= 3.0.0'
if File.exist?('UPGRADING.md')
gem.post_install_message = File.read('UPGRADING.md')
end
gem.add_runtime_dependency 'activerecord', '>= 7.0', '< 8.0'
gem.add_runtime_dependency 'zeitwerk', '>= 2.4', '< 3.0'
gem.add_development_dependency 'rspec-rails'
gem.add_development_dependency 'rspec-its'
gem.add_development_dependency 'rspec'
gem.add_development_dependency 'barrier'
gem.add_development_dependency 'database_cleaner'
end
acts-as-taggable-on-11.0.0/CONTRIBUTING.md 0000644 0000041 0000041 00000005214 14704600021 017557 0 ustar www-data www-data
**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*
- [How to contribute:](#how-to-contribute)
- [Bug reports / Issues](#bug-reports--issues)
- [Code](#code)
- [Commit Messages](#commit-messages)
- [About Pull Requests (PR's)](#about-pull-requests-prs)
- [Documentation](#documentation)
# How to contribute:
## Bug reports / Issues
* Is something broken or not working as expected? Check for an existing issue or [create a new one](https://github.com/mbleigh/acts-as-taggable-on/issues/new)
* IMPORTANT: Include the version of the gem, if you've install from git, what Ruby and Rails you are running, etc.
## Code
1. [Fork and clone the repo](https://help.github.com/articles/fork-a-repo)
2. Install the gem dependencies: `bundle install`
3. Make the changes you want and back them up with tests.
* [Run the tests](https://github.com/mbleigh/acts-as-taggable-on#testing) (`bundle exec rake spec`)
4. Update the CHANGELOG.md file with your changes and give yourself credit
5. Commit and create a pull request with details as to what has been changed and why
* Use well-described, small (atomic) commits.
* Include links to any relevant github issues.
* *Don't* change the VERSION file.
6. Extra Credit: [Confirm it runs and tests pass on the rubies specified in the Github Actions config](.github/workflows/spec.yml). I will otherwise confirm it runs on these.
How I handle pull requests:
* If the tests pass and the pull request looks good, I will merge it.
* If the pull request needs to be changed,
* you can change it by updating the branch you generated the pull request from
* either by adding more commits, or
* by force pushing to it
* I can make any changes myself and manually merge the code in.
### Commit Messages
* [A Note About Git Commit Messages](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)
* [http://stopwritingramblingcommitmessages.com/](http://stopwritingramblingcommitmessages.com/)
* [ThoughtBot style guide](https://github.com/thoughtbot/guides/tree/main/git)
### About Pull Requests (PR's)
* [All Your Open Source Code Are Belong To Us](http://www.benjaminfleischer.com/2013/07/30/all-your-open-source-code-are-belong-to-us/)
* [Using Pull Requests](https://help.github.com/articles/using-pull-requests)
* [Github pull requests made easy](https://www.element84.com/blog/github-pull-requests-made-easy)
## Documentation
* Update the wiki
acts-as-taggable-on-11.0.0/db/ 0000755 0000041 0000041 00000000000 14704600021 015711 5 ustar www-data www-data acts-as-taggable-on-11.0.0/db/migrate/ 0000755 0000041 0000041 00000000000 14704600021 017341 5 ustar www-data www-data acts-as-taggable-on-11.0.0/db/migrate/7_add_tenant_to_taggings.rb 0000644 0000041 0000041 00000000676 14704600021 024613 0 ustar www-data www-data # frozen_string_literal: true
class AddTenantToTaggings < ActiveRecord::Migration[6.0]
def self.up
add_column ActsAsTaggableOn.taggings_table, :tenant, :string, limit: 128
add_index ActsAsTaggableOn.taggings_table, :tenant unless index_exists? ActsAsTaggableOn.taggings_table, :tenant
end
def self.down
remove_index ActsAsTaggableOn.taggings_table, :tenant
remove_column ActsAsTaggableOn.taggings_table, :tenant
end
end
acts-as-taggable-on-11.0.0/db/migrate/4_add_missing_taggable_index.rb 0000644 0000041 0000041 00000000555 14704600021 025414 0 ustar www-data www-data # frozen_string_literal: true
class AddMissingTaggableIndex < ActiveRecord::Migration[6.0]
def self.up
add_index ActsAsTaggableOn.taggings_table, %i[taggable_id taggable_type context],
name: 'taggings_taggable_context_idx'
end
def self.down
remove_index ActsAsTaggableOn.taggings_table, name: 'taggings_taggable_context_idx'
end
end
acts-as-taggable-on-11.0.0/db/migrate/6_add_missing_indexes_on_taggings.rb 0000644 0000041 0000041 00000002644 14704600021 026500 0 ustar www-data www-data # frozen_string_literal: true
class AddMissingIndexesOnTaggings < ActiveRecord::Migration[6.0]
def change
add_index ActsAsTaggableOn.taggings_table, :tag_id unless index_exists? ActsAsTaggableOn.taggings_table, :tag_id
add_index ActsAsTaggableOn.taggings_table, :taggable_id unless index_exists? ActsAsTaggableOn.taggings_table,
:taggable_id
add_index ActsAsTaggableOn.taggings_table, :taggable_type unless index_exists? ActsAsTaggableOn.taggings_table,
:taggable_type
add_index ActsAsTaggableOn.taggings_table, :tagger_id unless index_exists? ActsAsTaggableOn.taggings_table,
:tagger_id
add_index ActsAsTaggableOn.taggings_table, :context unless index_exists? ActsAsTaggableOn.taggings_table, :context
unless index_exists? ActsAsTaggableOn.taggings_table, %i[tagger_id tagger_type]
add_index ActsAsTaggableOn.taggings_table, %i[tagger_id tagger_type]
end
unless index_exists? ActsAsTaggableOn.taggings_table, %i[taggable_id taggable_type tagger_id context],
name: 'taggings_idy'
add_index ActsAsTaggableOn.taggings_table, %i[taggable_id taggable_type tagger_id context],
name: 'taggings_idy'
end
end
end
acts-as-taggable-on-11.0.0/db/migrate/2_add_missing_unique_indices.rb 0000644 0000041 0000041 00000001707 14704600021 025461 0 ustar www-data www-data # frozen_string_literal: true
class AddMissingUniqueIndices < ActiveRecord::Migration[6.0]
def self.up
add_index ActsAsTaggableOn.tags_table, :name, unique: true
remove_index ActsAsTaggableOn.taggings_table, :tag_id if index_exists?(ActsAsTaggableOn.taggings_table, :tag_id)
remove_index ActsAsTaggableOn.taggings_table, name: 'taggings_taggable_context_idx'
add_index ActsAsTaggableOn.taggings_table,
%i[tag_id taggable_id taggable_type context tagger_id tagger_type],
unique: true, name: 'taggings_idx'
end
def self.down
remove_index ActsAsTaggableOn.tags_table, :name
remove_index ActsAsTaggableOn.taggings_table, name: 'taggings_idx'
add_index ActsAsTaggableOn.taggings_table, :tag_id unless index_exists?(ActsAsTaggableOn.taggings_table, :tag_id)
add_index ActsAsTaggableOn.taggings_table, %i[taggable_id taggable_type context],
name: 'taggings_taggable_context_idx'
end
end
acts-as-taggable-on-11.0.0/db/migrate/3_add_taggings_counter_cache_to_tags.rb 0000644 0000041 0000041 00000000737 14704600021 027134 0 ustar www-data www-data # frozen_string_literal: true
class AddTaggingsCounterCacheToTags < ActiveRecord::Migration[6.0]
def self.up
add_column ActsAsTaggableOn.tags_table, :taggings_count, :integer, default: 0
ActsAsTaggableOn::Tag.reset_column_information
ActsAsTaggableOn::Tag.find_each do |tag|
ActsAsTaggableOn::Tag.reset_counters(tag.id, ActsAsTaggableOn.taggings_table)
end
end
def self.down
remove_column ActsAsTaggableOn.tags_table, :taggings_count
end
end
acts-as-taggable-on-11.0.0/db/migrate/5_change_collation_for_tag_names.rb 0000644 0000041 0000041 00000000600 14704600021 026263 0 ustar www-data www-data # frozen_string_literal: true
# This migration is added to circumvent issue #623 and have special characters
# work properly
class ChangeCollationForTagNames < ActiveRecord::Migration[6.0]
def up
if ActsAsTaggableOn::Utils.using_mysql?
execute("ALTER TABLE #{ActsAsTaggableOn.tags_table} MODIFY name varchar(255) CHARACTER SET utf8 COLLATE utf8_bin;")
end
end
end
acts-as-taggable-on-11.0.0/db/migrate/1_acts_as_taggable_on_migration.rb 0000644 0000041 0000041 00000001754 14704600021 026125 0 ustar www-data www-data # frozen_string_literal: true
class ActsAsTaggableOnMigration < ActiveRecord::Migration[6.0]
def self.up
create_table ActsAsTaggableOn.tags_table do |t|
t.string :name
t.timestamps
end
create_table ActsAsTaggableOn.taggings_table do |t|
t.references :tag, foreign_key: { to_table: ActsAsTaggableOn.tags_table }
# You should make sure that the column created is
# long enough to store the required class names.
t.references :taggable, polymorphic: true
t.references :tagger, polymorphic: true
# Limit is created to prevent MySQL error on index
# length for MyISAM table type: http://bit.ly/vgW2Ql
t.string :context, limit: 128
t.datetime :created_at
end
add_index ActsAsTaggableOn.taggings_table, %i[taggable_id taggable_type context],
name: 'taggings_taggable_context_idx'
end
def self.down
drop_table ActsAsTaggableOn.taggings_table
drop_table ActsAsTaggableOn.tags_table
end
end
acts-as-taggable-on-11.0.0/.github/ 0000755 0000041 0000041 00000000000 14704600021 016664 5 ustar www-data www-data acts-as-taggable-on-11.0.0/.github/workflows/ 0000755 0000041 0000041 00000000000 14704600021 020721 5 ustar www-data www-data acts-as-taggable-on-11.0.0/.github/workflows/spec.yml 0000644 0000041 0000041 00000003777 14704600021 022414 0 ustar www-data www-data name: spec
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
continue-on-error: ${{ matrix.ruby == 'head' }}
env:
DB: ${{ matrix.db }}
BUNDLE_GEMFILE: ${{ matrix.gemfile }}
strategy:
fail-fast: false
matrix:
ruby:
- 3.3
- 3.2
- 3.1
gemfile:
- gemfiles/activerecord_7.2.gemfile
- gemfiles/activerecord_7.1.gemfile
- gemfiles/activerecord_7.0.gemfile
db:
- mysql
- postgresql
- sqlite3
include:
- ruby: truffleruby-head
db: postgresql
gemfile: gemfiles/activerecord_7.0.gemfile
- ruby: truffleruby-head
db: postgresql
gemfile: gemfiles/activerecord_7.1.gemfile
- ruby: truffleruby-head
db: postgresql
gemfile: gemfiles/activerecord_7.2.gemfile
services:
postgres:
image: postgres:10
env:
POSTGRES_USER: postgres
POSTGRES_DB: acts_as_taggable_on
POSTGRES_PASSWORD: postgres
ports: ['5432:5432']
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
mysql:
image: mysql:8
env:
MYSQL_ALLOW_EMPTY_PASSWORD: true
ports: ['3306:3306']
options: >-
--health-cmd "mysqladmin ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
- name: Create MySQL test database with utf8mb4 charset
if: ${{ matrix.db == 'mysql' }}
run: |
mysql -uroot --host=127.0.0.1 -e "CREATE DATABASE acts_as_taggable_on CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci"
- name: Build and test with Rake
run: |
bundle exec rake
acts-as-taggable-on-11.0.0/Guardfile 0000644 0000041 0000041 00000000240 14704600021 017145 0 ustar www-data www-data guard 'rspec' do
watch(%r{^spec/.+_spec\.rb})
watch(%r{^lib/(.+)\.rb}) { |m| "spec/lib/#{m[1]}_spec.rb" }
watch('spec/spec_helper.rb') { "spec" }
end
acts-as-taggable-on-11.0.0/lib/ 0000755 0000041 0000041 00000000000 14704600021 016072 5 ustar www-data www-data acts-as-taggable-on-11.0.0/lib/tasks/ 0000755 0000041 0000041 00000000000 14704600021 017217 5 ustar www-data www-data acts-as-taggable-on-11.0.0/lib/tasks/install_initializer.rake 0000644 0000041 0000041 00000001034 14704600021 024132 0 ustar www-data www-data namespace :acts_as_taggable_on do
namespace :sharded_db do
desc "Install initializer setting custom base class"
task :install_initializer => [:environment, "config/initializers/foo"] do
source = File.join(
Gem.loaded_specs["acts-as-taggable-on"].full_gem_path,
"lib",
"tasks",
"examples",
"acts-as-taggable-on.rb.example"
)
destination = "config/initializers/acts-as-taggable-on.rb"
cp source, destination
end
directory "config/initializers"
end
end
acts-as-taggable-on-11.0.0/lib/tasks/example/ 0000755 0000041 0000041 00000000000 14704600021 020652 5 ustar www-data www-data acts-as-taggable-on-11.0.0/lib/tasks/example/acts-as-taggable-on.rb.example 0000644 0000041 0000041 00000000574 14704600021 026350 0 ustar www-data www-data ActsAsTaggableOn.setup do |config|
# This works because the classes where the base class is a concern, Tag and Tagging
# are autoloaded, and won't be started until after the initializers run. The value
# must be a String, as the Rails Zeitwerk autoloader will not allow models to be
# referenced at initialization time.
#
# config.base_class = 'ApplicationRecord'
end
acts-as-taggable-on-11.0.0/lib/tasks/tags_collate_utf8.rake 0000644 0000041 0000041 00000001201 14704600021 023464 0 ustar www-data www-data # These rake tasks are to be run by MySql users only, they fix the management of
# binary-encoded strings for tag 'names'. Issues:
# https://github.com/mbleigh/acts-as-taggable-on/issues/623
namespace :acts_as_taggable_on_engine do
namespace :tag_names do
desc "Forcing collate of tag names to utf8_bin"
task :collate_bin => [:environment] do |t, args|
ActsAsTaggableOn::Configuration.apply_binary_collation(true)
end
desc "Forcing collate of tag names to utf8_general_ci"
task :collate_ci => [:environment] do |t, args|
ActsAsTaggableOn::Configuration.apply_binary_collation(false)
end
end
end
acts-as-taggable-on-11.0.0/lib/acts-as-taggable-on/ 0000755 0000041 0000041 00000000000 14704600021 021603 5 ustar www-data www-data acts-as-taggable-on-11.0.0/lib/acts-as-taggable-on/tagger.rb 0000644 0000041 0000041 00000004654 14704600021 023412 0 ustar www-data www-data # frozen_string_literal: true
module ActsAsTaggableOn
module Tagger
extend ActiveSupport::Concern
class_methods do
##
# Make a model a tagger. This allows an instance of a model to claim ownership
# of tags.
#
# Example:
# class User < ActiveRecord::Base
# acts_as_tagger
# end
def acts_as_tagger(opts = {})
class_eval do
owned_taggings_scope = opts.delete(:scope)
has_many :owned_taggings, owned_taggings_scope,
**opts.merge(
as: :tagger,
class_name: '::ActsAsTaggableOn::Tagging',
dependent: :destroy
)
has_many :owned_tags, -> { distinct },
class_name: '::ActsAsTaggableOn::Tag',
source: :tag,
through: :owned_taggings
end
include ActsAsTaggableOn::Tagger::InstanceMethods
extend ActsAsTaggableOn::Tagger::SingletonMethods
end
def tagger?
false
end
alias is_tagger? tagger?
end
module InstanceMethods
##
# Tag a taggable model with tags that are owned by the tagger.
#
# @param taggable The object that will be tagged
# @param [Hash] options An hash with options. Available options are:
# * :with - The tags that you want to
# * :on - The context on which you want to tag
#
# Example:
# @user.tag(@photo, :with => "paris, normandy", :on => :locations)
def tag(taggable, opts = {})
opts.reverse_merge!(force: true)
skip_save = opts.delete(:skip_save)
return false unless taggable.respond_to?(:is_taggable?) && taggable.is_taggable?
raise 'You need to specify a tag context using :on' unless opts.key?(:on)
raise 'You need to specify some tags using :with' unless opts.key?(:with)
unless opts[:force] || taggable.tag_types.include?(opts[:on])
raise "No context :#{opts[:on]} defined in #{taggable.class}"
end
taggable.set_owner_tag_list_on(self, opts[:on].to_s, opts[:with])
taggable.save unless skip_save
end
def tagger?
self.class.is_tagger?
end
alias is_tagger? tagger?
end
module SingletonMethods
def tagger?
true
end
alias is_tagger? tagger?
end
end
end
acts-as-taggable-on-11.0.0/lib/acts-as-taggable-on/tag.rb 0000644 0000041 0000041 00000007631 14704600021 022712 0 ustar www-data www-data # frozen_string_literal: true
module ActsAsTaggableOn
class Tag < ActsAsTaggableOn.base_class.constantize
self.table_name = ActsAsTaggableOn.tags_table
### ASSOCIATIONS:
has_many :taggings, dependent: :destroy, class_name: '::ActsAsTaggableOn::Tagging'
### VALIDATIONS:
validates_presence_of :name
validates_uniqueness_of :name, if: :validates_name_uniqueness?, case_sensitive: true
validates_length_of :name, maximum: 255
# monkey patch this method if don't need name uniqueness validation
def validates_name_uniqueness?
true
end
### SCOPES:
scope :most_used, ->(limit = 20) { order('taggings_count desc').limit(limit) }
scope :least_used, ->(limit = 20) { order('taggings_count asc').limit(limit) }
def self.named(name)
if ActsAsTaggableOn.strict_case_match
where(["name = #{binary}?", as_8bit_ascii(name)])
else
where(['LOWER(name) = LOWER(?)', as_8bit_ascii(unicode_downcase(name))])
end
end
def self.named_any(list)
clause = list.map do |tag|
sanitize_sql_for_named_any(tag).force_encoding('BINARY')
end.join(' OR ')
where(clause)
end
def self.named_like(name)
clause = ["name #{ActsAsTaggableOn::Utils.like_operator} ? ESCAPE '!'",
"%#{ActsAsTaggableOn::Utils.escape_like(name)}%"]
where(clause)
end
def self.named_like_any(list)
clause = list.map do |tag|
sanitize_sql(["name #{ActsAsTaggableOn::Utils.like_operator} ? ESCAPE '!'",
"%#{ActsAsTaggableOn::Utils.escape_like(tag.to_s)}%"])
end.join(' OR ')
where(clause)
end
def self.for_context(context)
joins(:taggings)
.where(["#{ActsAsTaggableOn.taggings_table}.context = ?", context])
.select("DISTINCT #{ActsAsTaggableOn.tags_table}.*")
end
def self.for_tenant(tenant)
joins(:taggings)
.where("#{ActsAsTaggableOn.taggings_table}.tenant = ?", tenant.to_s)
.select("DISTINCT #{ActsAsTaggableOn.tags_table}.*")
end
### CLASS METHODS:
def self.find_or_create_with_like_by_name(name)
if ActsAsTaggableOn.strict_case_match
find_or_create_all_with_like_by_name([name]).first
else
named_like(name).first || create(name: name)
end
end
def self.find_or_create_all_with_like_by_name(*list)
list = Array(list).flatten
return [] if list.empty?
existing_tags = named_any(list)
list.map do |tag_name|
tries ||= 3
comparable_tag_name = comparable_name(tag_name)
existing_tag = existing_tags.find { |tag| comparable_name(tag.name) == comparable_tag_name }
next existing_tag if existing_tag
transaction(requires_new: true) { create(name: tag_name) }
rescue ActiveRecord::RecordNotUnique
if (tries -= 1).positive?
existing_tags = named_any(list)
retry
end
raise DuplicateTagError, "'#{tag_name}' has already been taken"
end
end
### INSTANCE METHODS:
def ==(other)
super || (other.is_a?(Tag) && name == other.name)
end
def to_s
name
end
def count
read_attribute(:count).to_i
end
class << self
private
def comparable_name(str)
if ActsAsTaggableOn.strict_case_match
str
else
unicode_downcase(str.to_s)
end
end
def binary
ActsAsTaggableOn::Utils.using_mysql? ? 'BINARY ' : nil
end
def as_8bit_ascii(string)
string.to_s.mb_chars
end
def unicode_downcase(string)
as_8bit_ascii(string).downcase
end
def sanitize_sql_for_named_any(tag)
if ActsAsTaggableOn.strict_case_match
sanitize_sql(["name = #{binary}?", as_8bit_ascii(tag)])
else
sanitize_sql(['LOWER(name) = LOWER(?)', as_8bit_ascii(unicode_downcase(tag))])
end
end
end
end
end
acts-as-taggable-on-11.0.0/lib/acts-as-taggable-on/tagging.rb 0000644 0000041 0000041 00000002370 14704600021 023552 0 ustar www-data www-data # frozen_string_literal: true
module ActsAsTaggableOn
class Tagging < ActsAsTaggableOn.base_class.constantize # :nodoc:
self.table_name = ActsAsTaggableOn.taggings_table
DEFAULT_CONTEXT = 'tags'
belongs_to :tag, class_name: '::ActsAsTaggableOn::Tag', counter_cache: ActsAsTaggableOn.tags_counter
belongs_to :taggable, polymorphic: true
belongs_to :tagger, polymorphic: true, optional: true
scope :owned_by, ->(owner) { where(tagger: owner) }
scope :not_owned, -> { where(tagger_id: nil, tagger_type: nil) }
scope :by_contexts, ->(contexts) { where(context: (contexts || DEFAULT_CONTEXT)) }
scope :by_context, ->(context = DEFAULT_CONTEXT) { by_contexts(context.to_s) }
scope :by_tenant, ->(tenant) { where(tenant: tenant) }
validates_presence_of :context
validates_presence_of :tag_id
validates_uniqueness_of :tag_id, scope: %i[taggable_type taggable_id context tagger_id tagger_type]
after_destroy :remove_unused_tags
private
def remove_unused_tags
if ActsAsTaggableOn.remove_unused_tags
if ActsAsTaggableOn.tags_counter
tag.destroy if tag.reload.taggings_count.zero?
elsif tag.reload.taggings.none?
tag.destroy
end
end
end
end
end
acts-as-taggable-on-11.0.0/lib/acts-as-taggable-on/engine.rb 0000644 0000041 0000041 00000000140 14704600021 023370 0 ustar www-data www-data # frozen_string_literal: true
module ActsAsTaggableOn
class Engine < Rails::Engine
end
end
acts-as-taggable-on-11.0.0/lib/acts-as-taggable-on/taggable.rb 0000644 0000041 0000041 00000006366 14704600021 023711 0 ustar www-data www-data # frozen_string_literal: true
module ActsAsTaggableOn
module Taggable
def taggable?
false
end
##
# This is an alias for calling acts_as_taggable_on :tags.
#
# Example:
# class Book < ActiveRecord::Base
# acts_as_taggable
# end
def acts_as_taggable
acts_as_taggable_on :tags
end
##
# This is an alias for calling acts_as_ordered_taggable_on :tags.
#
# Example:
# class Book < ActiveRecord::Base
# acts_as_ordered_taggable
# end
def acts_as_ordered_taggable
acts_as_ordered_taggable_on :tags
end
##
# Make a model taggable on specified contexts.
#
# @param [Array] tag_types An array of taggable contexts
#
# Example:
# class User < ActiveRecord::Base
# acts_as_taggable_on :languages, :skills
# end
def acts_as_taggable_on(*tag_types)
taggable_on(false, tag_types)
end
##
# Make a model taggable on specified contexts
# and preserves the order in which tags are created
#
# @param [Array] tag_types An array of taggable contexts
#
# Example:
# class User < ActiveRecord::Base
# acts_as_ordered_taggable_on :languages, :skills
# end
def acts_as_ordered_taggable_on(*tag_types)
taggable_on(true, tag_types)
end
def acts_as_taggable_tenant(tenant)
if taggable?
else
class_attribute :tenant_column
end
self.tenant_column = tenant
# each of these add context-specific methods and must be
# called on each call of taggable_on
include Core
include Collection
include Caching
include Ownership
include Related
end
private
# Make a model taggable on specified contexts
# and optionally preserves the order in which tags are created
#
# Separate methods used above for backwards compatibility
# so that the original acts_as_taggable_on method is unaffected
# as it's not possible to add another argument to the method
# without the tag_types being enclosed in square brackets
#
# NB: method overridden in core module in order to create tag type
# associations and methods after this logic has executed
#
def taggable_on(preserve_tag_order, *tag_types)
tag_types = tag_types.to_a.flatten.compact.map(&:to_sym)
if taggable?
self.tag_types = (self.tag_types + tag_types).uniq
self.preserve_tag_order = preserve_tag_order
else
class_eval do
class_attribute :tag_types
class_attribute :preserve_tag_order
class_attribute :tenant_column
self.tag_types = tag_types
self.preserve_tag_order = preserve_tag_order
has_many :taggings, as: :taggable, dependent: :destroy, class_name: '::ActsAsTaggableOn::Tagging'
has_many :base_tags, through: :taggings, source: :tag, class_name: '::ActsAsTaggableOn::Tag'
def self.taggable?
true
end
end
end
# each of these add context-specific methods and must be
# called on each call of taggable_on
include Core
include Collection
include Caching
include Ownership
include Related
end
end
end
acts-as-taggable-on-11.0.0/lib/acts-as-taggable-on/tags_helper.rb 0000644 0000041 0000041 00000000672 14704600021 024432 0 ustar www-data www-data # frozen_string_literal: true
module ActsAsTaggableOn
module TagsHelper
# See the wiki for an example using tag_cloud.
def tag_cloud(tags, classes)
return [] if tags.empty?
max_count = tags.max_by(&:taggings_count).taggings_count.to_f
tags.each do |tag|
index = ((tag.taggings_count / max_count) * (classes.size - 1))
yield tag, classes[index.nan? ? 0 : index.round]
end
end
end
end
acts-as-taggable-on-11.0.0/lib/acts-as-taggable-on/taggable/ 0000755 0000041 0000041 00000000000 14704600021 023351 5 ustar www-data www-data acts-as-taggable-on-11.0.0/lib/acts-as-taggable-on/taggable/tag_list_type.rb 0000644 0000041 0000041 00000000214 14704600021 026542 0 ustar www-data www-data # frozen_string_literal: true
module ActsAsTaggableOn
module Taggable
class TagListType < ActiveModel::Type::Value
end
end
end
acts-as-taggable-on-11.0.0/lib/acts-as-taggable-on/taggable/related.rb 0000644 0000041 0000041 00000007434 14704600021 025326 0 ustar www-data www-data # frozen_string_literal: true
module ActsAsTaggableOn
module Taggable
module Related
def self.included(base)
base.extend ActsAsTaggableOn::Taggable::Related::ClassMethods
base.initialize_acts_as_taggable_on_related
end
module ClassMethods
def initialize_acts_as_taggable_on_related
tag_types.map(&:to_s).each do |tag_type|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
def find_related_#{tag_type}(options = {})
related_tags_for('#{tag_type}', self.class, options)
end
alias_method :find_related_on_#{tag_type}, :find_related_#{tag_type}
def find_related_#{tag_type}_for(klass, options = {})
related_tags_for('#{tag_type}', klass, options)
end
RUBY
end
end
def acts_as_taggable_on(*args)
super(*args)
initialize_acts_as_taggable_on_related
end
end
def find_matching_contexts(search_context, result_context, options = {})
matching_contexts_for(search_context.to_s, result_context.to_s, self.class, options)
end
def find_matching_contexts_for(klass, search_context, result_context, options = {})
matching_contexts_for(search_context.to_s, result_context.to_s, klass, options)
end
def matching_contexts_for(search_context, result_context, klass, _options = {})
tags_to_find = tags_on(search_context).map(&:name)
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, result_context
])
end
def related_tags_for(context, klass, options = {})
tags_to_ignore = Array.wrap(options[:ignore]).map(&:to_s) || []
tags_to_find = tags_on(context).map(&:name).reject { |t| tags_to_ignore.include? t }
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
])
end
private
def exclude_self(klass, id)
"#{klass.arel_table[klass.primary_key].not_eq(id).to_sql} AND" if [self.class.base_class,
self.class].include? klass
end
def group_columns(klass)
if ActsAsTaggableOn::Utils.using_postgresql?
grouped_column_names_for(klass)
else
"#{klass.table_name}.#{klass.primary_key}"
end
end
def related_where(klass, conditions)
klass.select("#{klass.table_name}.*, COUNT(#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}) AS count")
.from("#{klass.table_name}, #{ActsAsTaggableOn::Tag.table_name}, #{ActsAsTaggableOn::Tagging.table_name}")
.group(group_columns(klass))
.order('count DESC')
.where(conditions)
end
end
end
end
acts-as-taggable-on-11.0.0/lib/acts-as-taggable-on/taggable/ownership.rb 0000644 0000041 0000041 00000011114 14704600021 025712 0 ustar www-data www-data # frozen_string_literal: true
module ActsAsTaggableOn
module Taggable
module Ownership
extend ActiveSupport::Concern
included do
after_save :save_owned_tags
initialize_acts_as_taggable_on_ownership
end
class_methods do
def acts_as_taggable_on(*args)
initialize_acts_as_taggable_on_ownership
super(*args)
end
def initialize_acts_as_taggable_on_ownership
tag_types.map(&:to_s).each do |tag_type|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{tag_type}_from(owner)
owner_tag_list_on(owner, '#{tag_type}')
end
RUBY
end
end
end
def owner_tags(owner)
scope = if owner.nil?
base_tags
else
base_tags.where(
ActsAsTaggableOn::Tagging.table_name.to_s => {
tagger_id: owner.id,
tagger_type: owner.class.base_class.to_s
}
)
end
# when preserving tag order, return tags in created order
# if we added the order to the association this would always apply
if self.class.preserve_tag_order?
scope.order("#{ActsAsTaggableOn::Tagging.table_name}.id")
else
scope
end
end
def owner_tags_on(owner, context)
owner_tags(owner).where(
ActsAsTaggableOn::Tagging.table_name.to_s => {
context: context
}
)
end
def cached_owned_tag_list_on(context)
variable_name = "@owned_#{context}_list"
(instance_variable_defined?(variable_name) && instance_variable_get(variable_name)) || instance_variable_set(
variable_name, {}
)
end
def owner_tag_list_on(owner, context)
add_custom_context(context)
cache = cached_owned_tag_list_on(context)
cache[owner] ||= ActsAsTaggableOn::TagList.new(*owner_tags_on(owner, context).map(&:name))
end
def set_owner_tag_list_on(owner, context, new_list)
add_custom_context(context)
cache = cached_owned_tag_list_on(context)
cache[owner] = ActsAsTaggableOn.default_parser.new(new_list).parse
end
def reload(*args)
self.class.tag_types.each do |context|
instance_variable_set("@owned_#{context}_list", nil)
end
super(*args)
end
def save_owned_tags
tagging_contexts.each do |context|
cached_owned_tag_list_on(context).each do |owner, tag_list|
# Find existing tags or create non-existing tags:
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).to_a
# Tag maintenance based on whether preserving the created order of tags
old_tags = owned_tags - tags
new_tags = tags - owned_tags
if self.class.preserve_tag_order?
shared_tags = owned_tags & tags
if shared_tags.any? && tags[0...shared_tags.size] != shared_tags
index = shared_tags.each_with_index do |_, i|
break i unless shared_tags[i] == tags[i]
end
# Update arrays of tag objects
old_tags |= owned_tags.from(index)
new_tags |= owned_tags.from(index) & shared_tags
# Order the array of tag objects to match the tag list
new_tags = tags.map do |t|
new_tags.find do |n|
n.name.downcase == t.name.downcase
end
end.compact
end
else
# Delete discarded tags and create new tags
end
# Find all taggings that belong to the taggable (self), are owned by the owner,
# have the correct context, and are removed from the list.
if old_tags.present?
ActsAsTaggableOn::Tagging.where(taggable_id: id, taggable_type: self.class.base_class.to_s,
tagger_type: owner.class.base_class.to_s, tagger_id: owner.id,
tag_id: old_tags, context: context).destroy_all
end
# Create new taggings:
new_tags.each do |tag|
taggings.create!(tag_id: tag.id, context: context.to_s, tagger: owner, taggable: self)
end
end
end
true
end
end
end
end
acts-as-taggable-on-11.0.0/lib/acts-as-taggable-on/taggable/tagged_with_query/ 0000755 0000041 0000041 00000000000 14704600021 027064 5 ustar www-data www-data acts-as-taggable-on-11.0.0/lib/acts-as-taggable-on/taggable/tagged_with_query/any_tags_query.rb 0000644 0000041 0000041 00000005423 14704600021 032447 0 ustar www-data www-data # frozen_string_literal: true
module ActsAsTaggableOn
module Taggable
module TaggedWithQuery
class AnyTagsQuery < QueryBase
def build
taggable_model.select(all_fields)
.where(model_has_at_least_one_tag)
.order(Arel.sql(order_conditions))
.readonly(false)
end
private
def all_fields
taggable_arel_table[Arel.star]
end
def model_has_at_least_one_tag
tagging_arel_table.project(Arel.star).where(at_least_one_tag).exists
end
def at_least_one_tag
exists_contition = tagging_arel_table[:taggable_id].eq(taggable_arel_table[taggable_model.primary_key])
.and(tagging_arel_table[:taggable_type].eq(taggable_model.base_class.name))
.and(
tagging_arel_table[:tag_id].in(
tag_arel_table.project(tag_arel_table[:id]).where(tags_match_type)
)
)
if options[:start_at].present?
exists_contition = exists_contition.and(tagging_arel_table[:created_at].gteq(options[:start_at]))
end
if options[:end_at].present?
exists_contition = exists_contition.and(tagging_arel_table[:created_at].lteq(options[:end_at]))
end
if options[:on].present?
exists_contition = exists_contition.and(tagging_arel_table[:context].eq(options[:on]))
end
if (owner = options[:owned_by]).present?
exists_contition = exists_contition.and(tagging_arel_table[:tagger_id].eq(owner.id))
.and(tagging_arel_table[:tagger_type].eq(owner.class.base_class.to_s))
end
exists_contition
end
def order_conditions
order_by = []
if options[:order_by_matching_tag_count].present?
order_by << "(SELECT count(*) FROM #{tagging_model.table_name} WHERE #{at_least_one_tag.to_sql}) desc"
end
order_by << options[:order] if options[:order].present?
order_by.join(', ')
end
def alias_name(tag_list)
alias_base_name = taggable_model.base_class.name.downcase
taggings_context = options[:on] ? "_#{options[:on]}" : ''
adjust_taggings_alias(
"#{alias_base_name[0..4]}#{taggings_context[0..6]}_taggings_#{ActsAsTaggableOn::Utils.sha_prefix(tag_list.join('_'))}"
)
end
end
end
end
end
acts-as-taggable-on-11.0.0/lib/acts-as-taggable-on/taggable/tagged_with_query/query_base.rb 0000644 0000041 0000041 00000005130 14704600021 031547 0 ustar www-data www-data # frozen_string_literal: true
module ActsAsTaggableOn
module Taggable
module TaggedWithQuery
class QueryBase
def initialize(taggable_model, tag_model, tagging_model, tag_list, options)
@taggable_model = taggable_model
@tag_model = tag_model
@tagging_model = tagging_model
@tag_list = tag_list
@options = options
end
private
attr_reader :taggable_model, :tag_model, :tagging_model, :tag_list, :options
def taggable_arel_table
@taggable_arel_table ||= taggable_model.arel_table
end
def tag_arel_table
@tag_arel_table ||= tag_model.arel_table
end
def tagging_arel_table
@tagging_arel_table ||= tagging_model.arel_table
end
def tag_match_type(tag)
matches_attribute = tag_arel_table[:name]
matches_attribute = matches_attribute.lower unless ActsAsTaggableOn.strict_case_match
if options[:wild].present?
matches_attribute.matches(wildcard_escaped_tag(tag), '!', ActsAsTaggableOn.strict_case_match)
else
matches_attribute.matches(escaped_tag(tag), '!', ActsAsTaggableOn.strict_case_match)
end
end
def tags_match_type
matches_attribute = tag_arel_table[:name]
matches_attribute = matches_attribute.lower unless ActsAsTaggableOn.strict_case_match
if options[:wild].present?
matches_attribute.matches_any(tag_list.map do |tag|
wildcard_escaped_tag(tag)
end, '!', ActsAsTaggableOn.strict_case_match)
else
matches_attribute.matches_any(tag_list.map do |tag|
escaped_tag(tag).to_s
end, '!', ActsAsTaggableOn.strict_case_match)
end
end
def escaped_tag(tag)
tag = tag.downcase unless ActsAsTaggableOn.strict_case_match
ActsAsTaggableOn::Utils.escape_like(tag)
end
def wildcard_escaped_tag(tag)
case options[:wild]
when :suffix then "#{escaped_tag(tag)}%"
when :prefix then "%#{escaped_tag(tag)}"
when true then "%#{escaped_tag(tag)}%"
else escaped_tag(tag)
end
end
def adjust_taggings_alias(taggings_alias)
taggings_alias = "taggings_alias_#{Digest::SHA1.hexdigest(taggings_alias)}" if taggings_alias.size > 75
taggings_alias
end
end
end
end
end
acts-as-taggable-on-11.0.0/lib/acts-as-taggable-on/taggable/tagged_with_query/exclude_tags_query.rb 0000644 0000041 0000041 00000005673 14704600021 033320 0 ustar www-data www-data # frozen_string_literal: true
module ActsAsTaggableOn
module Taggable
module TaggedWithQuery
class ExcludeTagsQuery < QueryBase
def build
taggable_model.joins(owning_to_tagger)
.where(tags_not_in_list)
.having(tags_that_matches_count)
.readonly(false)
end
private
def tags_not_in_list
taggable_arel_table[:id].not_in(
tagging_arel_table
.project(tagging_arel_table[:taggable_id])
.join(tag_arel_table)
.on(
tagging_arel_table[:tag_id].eq(tag_arel_table[:id])
.and(tagging_arel_table[:taggable_type].eq(taggable_model.base_class.name))
.and(tags_match_type)
)
)
# FIXME: missing time scope, this is also missing in the original implementation
end
def owning_to_tagger
return [] if options[:owned_by].blank?
owner = options[:owned_by]
arel_join = taggable_arel_table
.join(tagging_arel_table)
.on(
tagging_arel_table[:tagger_id].eq(owner.id)
.and(tagging_arel_table[:tagger_type].eq(owner.class.base_class.to_s))
.and(tagging_arel_table[:taggable_id].eq(taggable_arel_table[taggable_model.primary_key]))
.and(tagging_arel_table[:taggable_type].eq(taggable_model.base_class.name))
)
if options[:match_all].present?
arel_join = arel_join
.join(tagging_arel_table, Arel::Nodes::OuterJoin)
.on(
match_all_on_conditions
)
end
arel_join.join_sources
end
def match_all_on_conditions
on_condition = tagging_arel_table[:taggable_id].eq(taggable_arel_table[taggable_model.primary_key])
.and(tagging_arel_table[:taggable_type].eq(taggable_model.base_class.name))
if options[:start_at].present?
on_condition = on_condition.and(tagging_arel_table[:created_at].gteq(options[:start_at]))
end
if options[:end_at].present?
on_condition = on_condition.and(tagging_arel_table[:created_at].lteq(options[:end_at]))
end
on_condition = on_condition.and(tagging_arel_table[:context].eq(options[:on])) if options[:on].present?
on_condition
end
def tags_that_matches_count
return [] if options[:match_all].blank?
taggable_model.find_by_sql(tag_arel_table.project(Arel.star.count).where(tags_match_type).to_sql)
tagging_arel_table[:taggable_id].count.eq(
tag_arel_table.project(Arel.star.count).where(tags_match_type)
)
end
end
end
end
end
acts-as-taggable-on-11.0.0/lib/acts-as-taggable-on/taggable/tagged_with_query/all_tags_query.rb 0000644 0000041 0000041 00000010354 14704600021 032427 0 ustar www-data www-data # frozen_string_literal: true
module ActsAsTaggableOn
module Taggable
module TaggedWithQuery
class AllTagsQuery < QueryBase
def build
taggable_model.joins(each_tag_in_list)
.group(by_taggable)
.having(tags_that_matches_count)
.order(order_conditions)
.readonly(false)
end
private
def each_tag_in_list
arel_join = taggable_arel_table
tag_list.each do |tag|
tagging_alias = tagging_arel_table.alias(tagging_alias(tag))
arel_join = arel_join
.join(tagging_alias)
.on(on_conditions(tag, tagging_alias))
end
if options[:match_all].present?
arel_join = arel_join
.join(tagging_arel_table, Arel::Nodes::OuterJoin)
.on(
match_all_on_conditions
)
end
arel_join.join_sources
end
def on_conditions(tag, tagging_alias)
on_condition = tagging_alias[:taggable_id].eq(taggable_arel_table[taggable_model.primary_key])
.and(tagging_alias[:taggable_type].eq(taggable_model.base_class.name))
.and(
tagging_alias[:tag_id].in(
tag_arel_table.project(tag_arel_table[:id]).where(tag_match_type(tag))
)
)
if options[:start_at].present?
on_condition = on_condition.and(tagging_alias[:created_at].gteq(options[:start_at]))
end
if options[:end_at].present?
on_condition = on_condition.and(tagging_alias[:created_at].lteq(options[:end_at]))
end
on_condition = on_condition.and(tagging_alias[:context].eq(options[:on])) if options[:on].present?
if (owner = options[:owned_by]).present?
on_condition = on_condition.and(tagging_alias[:tagger_id].eq(owner.id))
.and(tagging_alias[:tagger_type].eq(owner.class.base_class.to_s))
end
on_condition
end
def match_all_on_conditions
on_condition = tagging_arel_table[:taggable_id].eq(taggable_arel_table[taggable_model.primary_key])
.and(tagging_arel_table[:taggable_type].eq(taggable_model.base_class.name))
if options[:start_at].present?
on_condition = on_condition.and(tagging_arel_table[:created_at].gteq(options[:start_at]))
end
if options[:end_at].present?
on_condition = on_condition.and(tagging_arel_table[:created_at].lteq(options[:end_at]))
end
on_condition = on_condition.and(tagging_arel_table[:context].eq(options[:on])) if options[:on].present?
on_condition
end
def by_taggable
return [] if options[:match_all].blank?
taggable_arel_table[taggable_model.primary_key]
end
def tags_that_matches_count
return [] if options[:match_all].blank?
taggable_model.find_by_sql(tag_arel_table.project(Arel.star.count).where(tags_match_type).to_sql)
tagging_arel_table[:taggable_id].count.eq(
tag_arel_table.project(Arel.star.count).where(tags_match_type)
)
end
def order_conditions
order_by = []
if options[:order_by_matching_tag_count].present? && options[:match_all].blank?
order_by << tagging_arel_table.project(tagging_arel_table[Arel.star].count.as('taggings_count')).order('taggings_count DESC').to_sql
end
order_by << options[:order] if options[:order].present?
order_by.join(', ')
end
def tagging_alias(tag)
alias_base_name = taggable_model.base_class.name.downcase
adjust_taggings_alias("#{alias_base_name[0..11]}_taggings_#{ActsAsTaggableOn::Utils.sha_prefix(tag)}")
end
end
end
end
end
acts-as-taggable-on-11.0.0/lib/acts-as-taggable-on/taggable/core.rb 0000644 0000041 0000041 00000031767 14704600021 024644 0 ustar www-data www-data # frozen_string_literal: true
require_relative 'tagged_with_query'
require_relative 'tag_list_type'
module ActsAsTaggableOn
module Taggable
module Core
extend ActiveSupport::Concern
included do
attr_writer :custom_contexts
after_save :save_tags
initialize_acts_as_taggable_on_core
end
class_methods do
def initialize_acts_as_taggable_on_core
include taggable_mixin
tag_types.map(&:to_s).each do |tags_type|
tag_type = tags_type.to_s.singularize
context_taggings = "#{tag_type}_taggings".to_sym
context_tags = tags_type.to_sym
taggings_order = (preserve_tag_order? ? "#{ActsAsTaggableOn::Tagging.table_name}.id" : [])
class_eval do
# when preserving tag order, include order option so that for a 'tags' context
# the associations tag_taggings & tags are always returned in created order
has_many context_taggings, -> { includes(:tag).order(taggings_order).where(context: tags_type) },
as: :taggable,
class_name: 'ActsAsTaggableOn::Tagging',
dependent: :destroy,
after_add: :dirtify_tag_list,
after_remove: :dirtify_tag_list
has_many context_tags, -> { order(taggings_order) },
class_name: 'ActsAsTaggableOn::Tag',
through: context_taggings,
source: :tag
attribute "#{tags_type.singularize}_list".to_sym, ActsAsTaggableOn::Taggable::TagListType.new
end
taggable_mixin.class_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{tag_type}_list
tag_list_on('#{tags_type}')
end
def #{tag_type}_list=(new_tags)
parsed_new_list = ActsAsTaggableOn.default_parser.new(new_tags).parse
if self.class.preserve_tag_order? || (parsed_new_list.sort != #{tag_type}_list.sort)
unless #{tag_type}_list_changed?
@attributes["#{tag_type}_list"] = ActiveModel::Attribute.from_user("#{tag_type}_list", #{tag_type}_list, ActsAsTaggableOn::Taggable::TagListType.new)
end
write_attribute("#{tag_type}_list", parsed_new_list)
end
set_tag_list_on('#{tags_type}', new_tags)
end
def all_#{tags_type}_list
all_tags_list_on('#{tags_type}')
end
private
def dirtify_tag_list(tagging)
attribute_will_change! tagging.context.singularize+"_list"
end
RUBY
end
end
def taggable_on(preserve_tag_order, *tag_types)
super(preserve_tag_order, *tag_types)
initialize_acts_as_taggable_on_core
end
# all column names are necessary for PostgreSQL group clause
def grouped_column_names_for(object)
object.column_names.map { |column| "#{object.table_name}.#{column}" }.join(', ')
end
##
# Return a scope of objects that are tagged with the specified tags.
#
# @param tags The tags that we want to query for
# @param [Hash] options A hash of options to alter you query:
# * :exclude - if set to true, return objects that are *NOT* tagged with the specified tags
# * :any - if set to true, return objects that are tagged with *ANY* of the specified tags
# * :order_by_matching_tag_count - if set to true and used with :any, sort by objects matching the most tags, descending
# * :match_all - if set to true, return objects that are *ONLY* tagged with the specified tags
# * :owned_by - return objects that are *ONLY* owned by the owner
# * :start_at - Restrict the tags to those created after a certain time
# * :end_at - Restrict the tags to those created before a certain time
#
# Example:
# User.tagged_with(["awesome", "cool"]) # Users that are tagged with awesome and cool
# User.tagged_with(["awesome", "cool"], :exclude => true) # Users that are not tagged with awesome or cool
# User.tagged_with(["awesome", "cool"], :any => true) # Users that are tagged with awesome or cool
# User.tagged_with(["awesome", "cool"], :any => true, :order_by_matching_tag_count => true) # Sort by users who match the most tags, descending
# User.tagged_with(["awesome", "cool"], :match_all => true) # Users that are tagged with just awesome and cool
# User.tagged_with(["awesome", "cool"], :owned_by => foo ) # Users that are tagged with just awesome and cool by 'foo'
# User.tagged_with(["awesome", "cool"], :owned_by => foo, :start_at => Date.today ) # Users that are tagged with just awesome, cool by 'foo' and starting today
def tagged_with(tags, options = {})
tag_list = ActsAsTaggableOn.default_parser.new(tags).parse
options = options.dup
return none if tag_list.empty?
::ActsAsTaggableOn::Taggable::TaggedWithQuery.build(self, ActsAsTaggableOn::Tag, ActsAsTaggableOn::Tagging,
tag_list, options)
end
def is_taggable?
true
end
def taggable_mixin
@taggable_mixin ||= Module.new
end
end
# all column names are necessary for PostgreSQL group clause
def grouped_column_names_for(object)
self.class.grouped_column_names_for(object)
end
def custom_contexts
@custom_contexts ||= taggings.map(&:context).uniq
end
def is_taggable?
self.class.is_taggable?
end
def add_custom_context(value)
unless custom_contexts.include?(value.to_s) || self.class.tag_types.map(&:to_s).include?(value.to_s)
custom_contexts << value.to_s
end
end
def cached_tag_list_on(context)
self["cached_#{context.to_s.singularize}_list"]
end
def tag_list_cache_set_on(context)
variable_name = "@#{context.to_s.singularize}_list"
instance_variable_defined?(variable_name) && instance_variable_get(variable_name)
end
def tag_list_cache_on(context)
variable_name = "@#{context.to_s.singularize}_list"
if instance_variable_get(variable_name)
instance_variable_get(variable_name)
elsif cached_tag_list_on(context) && ensure_included_cache_methods! && self.class.caching_tag_list_on?(context)
instance_variable_set(variable_name, ActsAsTaggableOn.default_parser.new(cached_tag_list_on(context)).parse)
else
instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(tags_on(context).map(&:name)))
end
end
def tag_list_on(context)
add_custom_context(context)
tag_list_cache_on(context)
end
def all_tags_list_on(context)
variable_name = "@all_#{context.to_s.singularize}_list"
if instance_variable_defined?(variable_name) && instance_variable_get(variable_name)
return instance_variable_get(variable_name)
end
instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(all_tags_on(context).map(&:name)).freeze)
end
##
# Returns all tags of a given context
def all_tags_on(context)
tagging_table_name = ActsAsTaggableOn::Tagging.table_name
opts = ["#{tagging_table_name}.context = ?", context.to_s]
scope = base_tags.where(opts)
if ActsAsTaggableOn::Utils.using_postgresql?
group_columns = grouped_column_names_for(ActsAsTaggableOn::Tag)
scope.order(Arel.sql("max(#{tagging_table_name}.created_at)")).group(group_columns)
else
scope.group("#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}")
end.to_a
end
##
# Returns all tags that are not owned of a given context
def tags_on(context)
scope = base_tags.where([
"#{ActsAsTaggableOn::Tagging.table_name}.context = ? AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_id IS NULL", context.to_s
])
# when preserving tag order, return tags in created order
# if we added the order to the association this would always apply
scope = scope.order("#{ActsAsTaggableOn::Tagging.table_name}.id") if self.class.preserve_tag_order?
scope
end
def set_tag_list_on(context, new_list)
add_custom_context(context)
variable_name = "@#{context.to_s.singularize}_list"
parsed_new_list = ActsAsTaggableOn.default_parser.new(new_list).parse
instance_variable_set(variable_name, parsed_new_list)
end
def tagging_contexts
self.class.tag_types.map(&:to_s) + custom_contexts
end
def taggable_tenant
public_send(self.class.tenant_column) if self.class.tenant_column
end
def reload(*args)
self.class.tag_types.each do |context|
instance_variable_set("@#{context.to_s.singularize}_list", nil)
instance_variable_set("@all_#{context.to_s.singularize}_list", nil)
end
super(*args)
end
##
# Find existing tags or create non-existing tags
def load_tags(tag_list)
ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name(tag_list)
end
def save_tags
tagging_contexts.each do |context|
next unless tag_list_cache_set_on(context)
# List of currently assigned tag names
tag_list = tag_list_cache_on(context).uniq
# Find existing tags or create non-existing tags:
tags = find_or_create_tags_from_list_with_context(tag_list, context)
# Tag objects for currently assigned tags
current_tags = tags_on(context)
# Tag maintenance based on whether preserving the created order of tags
old_tags = current_tags - tags
new_tags = tags - current_tags
if self.class.preserve_tag_order?
shared_tags = current_tags & tags
if shared_tags.any? && tags[0...shared_tags.size] != shared_tags
index = shared_tags.each_with_index do |_, i|
break i unless shared_tags[i] == tags[i]
end
# Update arrays of tag objects
old_tags |= current_tags[index...current_tags.size]
new_tags |= current_tags[index...current_tags.size] & shared_tags
# Order the array of tag objects to match the tag list
new_tags = tags.map do |t|
new_tags.find { |n| n.name.downcase == t.name.downcase }
end.compact
end
else
# Delete discarded tags and create new tags
end
# Destroy old taggings:
taggings.not_owned.by_context(context).where(tag_id: old_tags).destroy_all if old_tags.present?
# Create new taggings:
new_tags.each do |tag|
if taggable_tenant
taggings.create!(tag_id: tag.id, context: context.to_s, taggable: self, tenant: taggable_tenant)
else
taggings.create!(tag_id: tag.id, context: context.to_s, taggable: self)
end
end
end
true
end
private
def ensure_included_cache_methods!
self.class.columns
end
# Filters the tag lists from the attribute names.
def attributes_for_update(attribute_names)
tag_lists = tag_types.map { |tags_type| "#{tags_type.to_s.singularize}_list" }
super.delete_if { |attr| tag_lists.include? attr }
end
# Filters the tag lists from the attribute names.
def attributes_for_create(attribute_names)
tag_lists = tag_types.map { |tags_type| "#{tags_type.to_s.singularize}_list" }
super.delete_if { |attr| tag_lists.include? attr }
end
##
# 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] 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
end
acts-as-taggable-on-11.0.0/lib/acts-as-taggable-on/taggable/collection.rb 0000644 0000041 0000041 00000023775 14704600021 026047 0 ustar www-data www-data # frozen_string_literal: true
module ActsAsTaggableOn
module Taggable
module Collection
extend ActiveSupport::Concern
included do
initialize_acts_as_taggable_on_collection
end
class_methods do
def initialize_acts_as_taggable_on_collection
tag_types.map(&:to_s).each do |tag_type|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
def self.#{tag_type.singularize}_counts(options={})
tag_counts_on('#{tag_type}', options)
end
def #{tag_type.singularize}_counts(options = {})
tag_counts_on('#{tag_type}', options)
end
def top_#{tag_type}(limit = 10)
tag_counts_on('#{tag_type}', order: 'count desc', limit: limit.to_i)
end
def self.top_#{tag_type}(limit = 10)
tag_counts_on('#{tag_type}', order: 'count desc', limit: limit.to_i)
end
RUBY
end
end
def acts_as_taggable_on(*args)
super(*args)
initialize_acts_as_taggable_on_collection
end
def tag_counts_on(context, options = {})
all_tag_counts(options.merge({ on: context.to_s }))
end
def tags_on(context, options = {})
all_tags(options.merge({ on: context.to_s }))
end
##
# Calculate the tag names.
# To be used when you don't need tag counts and want to avoid the taggable joins.
#
# @param [Hash] options Options:
# * :start_at - Restrict the tags to those created after a certain time
# * :end_at - Restrict the tags to those created before a certain time
# * :conditions - A piece of SQL conditions to add to the query. Note we don't join the taggable objects for performance reasons.
# * :limit - The maximum number of tags to return
# * :order - A piece of SQL to order by. Eg 'tags.count desc' or 'taggings.created_at desc'
# * :on - Scope the find to only include a certain context
def all_tags(options = {})
options = options.dup
options.assert_valid_keys :start_at, :end_at, :conditions, :order, :limit, :on
## Generate conditions:
options[:conditions] = sanitize_sql(options[:conditions]) if options[:conditions]
## Generate scope:
tagging_scope = ActsAsTaggableOn::Tagging.select("#{ActsAsTaggableOn::Tagging.table_name}.tag_id")
tag_scope = ActsAsTaggableOn::Tag.select("#{ActsAsTaggableOn::Tag.table_name}.*").order(options[:order]).limit(options[:limit])
# Joins and conditions
tagging_conditions(options).each { |condition| tagging_scope = tagging_scope.where(condition) }
tag_scope = tag_scope.where(options[:conditions])
group_columns = "#{ActsAsTaggableOn::Tagging.table_name}.tag_id"
# Append the current scope to the scope, because we can't use scope(:find) in RoR 3.0 anymore:
tagging_scope = generate_tagging_scope_in_clause(tagging_scope, table_name, primary_key).group(group_columns)
tag_scope_joins(tag_scope, tagging_scope)
end
##
# Calculate the tag counts for all tags.
#
# @param [Hash] options Options:
# * :start_at - Restrict the tags to those created after a certain time
# * :end_at - Restrict the tags to those created before a certain time
# * :conditions - A piece of SQL conditions to add to the query
# * :limit - The maximum number of tags to return
# * :order - A piece of SQL to order by. Eg 'tags.count desc' or 'taggings.created_at desc'
# * :at_least - Exclude tags with a frequency less than the given value
# * :at_most - Exclude tags with a frequency greater than the given value
# * :on - Scope the find to only include a certain context
def all_tag_counts(options = {})
options = options.dup
options.assert_valid_keys :start_at, :end_at, :conditions, :at_least, :at_most, :order, :limit, :on, :id
## Generate conditions:
options[:conditions] = sanitize_sql(options[:conditions]) if options[:conditions]
## Generate scope:
tagging_scope = ActsAsTaggableOn::Tagging.select("#{ActsAsTaggableOn::Tagging.table_name}.tag_id, COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) AS tags_count")
tag_scope = ActsAsTaggableOn::Tag.select("#{ActsAsTaggableOn::Tag.table_name}.*, #{ActsAsTaggableOn::Tagging.table_name}.tags_count AS count").order(options[:order]).limit(options[:limit])
# Current model is STI descendant, so add type checking to the join condition
unless descends_from_active_record?
taggable_join = "INNER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id"
taggable_join = taggable_join + " AND #{table_name}.#{inheritance_column} = '#{name}'"
tagging_scope = tagging_scope.joins(taggable_join)
end
# Conditions
tagging_conditions(options).each { |condition| tagging_scope = tagging_scope.where(condition) }
tag_scope = tag_scope.where(options[:conditions])
# GROUP BY and HAVING clauses:
having = ["COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) > 0"]
if options[:at_least]
having.push sanitize_sql(["COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) >= ?",
options.delete(:at_least)])
end
if options[:at_most]
having.push sanitize_sql(["COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) <= ?",
options.delete(:at_most)])
end
having = having.compact.join(' AND ')
group_columns = "#{ActsAsTaggableOn::Tagging.table_name}.tag_id"
unless options[:id]
# Append the current scope to the scope, because we can't use scope(:find) in RoR 3.0 anymore:
tagging_scope = generate_tagging_scope_in_clause(tagging_scope, table_name, primary_key)
end
tagging_scope = tagging_scope.group(group_columns).having(having)
tag_scope_joins(tag_scope, tagging_scope)
end
def safe_to_sql(relation)
if connection.respond_to?(:unprepared_statement)
connection.unprepared_statement do
relation.to_sql
end
else
relation.to_sql
end
end
private
def generate_tagging_scope_in_clause(tagging_scope, table_name, primary_key)
table_name_pkey = "#{table_name}.#{primary_key}"
if ActsAsTaggableOn::Utils.using_mysql?
# See https://github.com/mbleigh/acts-as-taggable-on/pull/457 for details
scoped_ids = pluck(table_name_pkey)
tagging_scope = tagging_scope.where("#{ActsAsTaggableOn::Tagging.table_name}.taggable_id IN (?)",
scoped_ids)
else
tagging_scope = tagging_scope.where("#{ActsAsTaggableOn::Tagging.table_name}.taggable_id IN(#{safe_to_sql(except(:select).select(table_name_pkey))})")
end
tagging_scope
end
def tagging_conditions(options)
tagging_conditions = []
if options[:end_at]
tagging_conditions.push sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at <= ?",
options.delete(:end_at)])
end
if options[:start_at]
tagging_conditions.push sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at >= ?",
options.delete(:start_at)])
end
taggable_conditions = sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.taggable_type = ?",
base_class.name])
if options[:on]
taggable_conditions << sanitize_sql([" AND #{ActsAsTaggableOn::Tagging.table_name}.context = ?",
options.delete(:on).to_s])
end
if options[:id]
taggable_conditions << if options[:id].is_a? Array
sanitize_sql([" AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_id IN (?)",
options[:id]])
else
sanitize_sql([" AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_id = ?",
options[:id]])
end
end
tagging_conditions.push taggable_conditions
tagging_conditions
end
def tag_scope_joins(tag_scope, tagging_scope)
tag_scope = tag_scope.joins("JOIN (#{safe_to_sql(tagging_scope)}) AS #{ActsAsTaggableOn::Tagging.table_name} ON #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.id")
tag_scope.extending(CalculationMethods)
end
end
def tag_counts_on(context, options = {})
self.class.tag_counts_on(context, options.merge(id: id))
end
module CalculationMethods
# Rails 5 TODO: Remove options argument as soon we remove support to
# activerecord-deprecated_finders.
# See https://github.com/rails/rails/blob/master/activerecord/lib/active_record/relation/calculations.rb#L38
def count(column_name = :all, _options = {})
# https://github.com/rails/rails/commit/da9b5d4a8435b744fcf278fffd6d7f1e36d4a4f2
super(column_name)
end
end
end
end
end
acts-as-taggable-on-11.0.0/lib/acts-as-taggable-on/taggable/caching.rb 0000644 0000041 0000041 00000002357 14704600021 025301 0 ustar www-data www-data # frozen_string_literal: true
module ActsAsTaggableOn
module Taggable
module Caching
extend ActiveSupport::Concern
included do
initialize_tags_cache
before_save :save_cached_tag_list
end
class_methods do
def initialize_tags_cache
tag_types.map(&:to_s).each do |tag_type|
define_singleton_method("caching_#{tag_type.singularize}_list?") do
caching_tag_list_on?(tag_type)
end
end
end
def acts_as_taggable_on(*args)
super(*args)
initialize_tags_cache
end
def caching_tag_list_on?(context)
column_names.include?("cached_#{context.to_s.singularize}_list")
end
end
def save_cached_tag_list
tag_types.map(&:to_s).each do |tag_type|
next unless self.class.respond_to?("caching_#{tag_type.singularize}_list?")
if self.class.send("caching_#{tag_type.singularize}_list?") && tag_list_cache_set_on(tag_type)
list = tag_list_cache_on(tag_type).to_a.flatten.compact.join("#{ActsAsTaggableOn.delimiter} ")
self["cached_#{tag_type.singularize}_list"] = list
end
end
true
end
end
end
end
acts-as-taggable-on-11.0.0/lib/acts-as-taggable-on/taggable/tagged_with_query.rb 0000644 0000041 0000041 00000001445 14704600021 027415 0 ustar www-data www-data # frozen_string_literal: true
require_relative 'tagged_with_query/query_base'
require_relative 'tagged_with_query/exclude_tags_query'
require_relative 'tagged_with_query/any_tags_query'
require_relative 'tagged_with_query/all_tags_query'
module ActsAsTaggableOn
module Taggable
module TaggedWithQuery
def self.build(taggable_model, tag_model, tagging_model, tag_list, options)
if options[:exclude].present?
ExcludeTagsQuery.new(taggable_model, tag_model, tagging_model, tag_list, options).build
elsif options[:any].present?
AnyTagsQuery.new(taggable_model, tag_model, tagging_model, tag_list, options).build
else
AllTagsQuery.new(taggable_model, tag_model, tagging_model, tag_list, options).build
end
end
end
end
end
acts-as-taggable-on-11.0.0/lib/acts-as-taggable-on/utils.rb 0000644 0000041 0000041 00000001500 14704600021 023264 0 ustar www-data www-data # frozen_string_literal: true
# This module is deprecated and will be removed in the incoming versions
module ActsAsTaggableOn
module Utils
class << self
# Use ActsAsTaggableOn::Tag connection
def connection
ActsAsTaggableOn::Tag.connection
end
def using_postgresql?
connection && %w[PostgreSQL PostGIS].include?(connection.adapter_name)
end
def using_mysql?
connection && connection.adapter_name == 'Mysql2'
end
def sha_prefix(string)
Digest::SHA1.hexdigest(string)[0..6]
end
def like_operator
using_postgresql? ? 'ILIKE' : 'LIKE'
end
# escape _ and % characters in strings, since these are wildcards in SQL.
def escape_like(str)
str.gsub(/[!%_]/) { |x| "!#{x}" }
end
end
end
end
acts-as-taggable-on-11.0.0/lib/acts-as-taggable-on/generic_parser.rb 0000644 0000041 0000041 00000000744 14704600021 025125 0 ustar www-data www-data # frozen_string_literal: true
module ActsAsTaggableOn
##
# Returns a new TagList using the given tag string.
#
# Example:
# tag_list = ActsAsTaggableOn::GenericParser.new.parse("One , Two, Three")
# tag_list # ["One", "Two", "Three"]
class GenericParser
def initialize(tag_list)
@tag_list = tag_list
end
def parse
TagList.new.tap do |tag_list|
tag_list.add @tag_list.split(',').map(&:strip).reject(&:empty?)
end
end
end
end
acts-as-taggable-on-11.0.0/lib/acts-as-taggable-on/tag_list.rb 0000644 0000041 0000041 00000005420 14704600021 023737 0 ustar www-data www-data # frozen_string_literal: true
require 'active_support/core_ext/module/delegation'
module ActsAsTaggableOn
class TagList < Array
attr_accessor :owner, :parser
def initialize(*args)
@parser = ActsAsTaggableOn.default_parser
add(*args)
end
##
# Add tags to the tag_list. Duplicate or blank tags will be ignored.
# Use the :parse option to add an unparsed tag string.
#
# Example:
# tag_list.add("Fun", "Happy")
# tag_list.add("Fun, Happy", :parse => true)
def add(*names)
extract_and_apply_options!(names)
concat(names)
clean!
self
end
# Append---Add the tag to the tag_list. This
# expression returns the tag_list itself, so several appends
# may be chained together.
def <<(obj)
add(obj)
end
# Concatenation --- Returns a new tag list built by concatenating the
# two tag lists together to produce a third tag list.
def +(other)
TagList.new.add(self).add(other)
end
# Appends the elements of +other_tag_list+ to +self+.
def concat(other_tag_list)
super(other_tag_list).send(:clean!)
self
end
##
# Remove specific tags from the tag_list.
# Use the :parse option to add an unparsed tag string.
#
# Example:
# tag_list.remove("Sad", "Lonely")
# tag_list.remove("Sad, Lonely", :parse => true)
def remove(*names)
extract_and_apply_options!(names)
delete_if { |name| names.include?(name) }
self
end
##
# Transform the tag_list into a tag string suitable for editing in a form.
# The tags are joined with TagList.delimiter and quoted if necessary.
#
# Example:
# tag_list = TagList.new("Round", "Square,Cube")
# tag_list.to_s # 'Round, "Square,Cube"'
def to_s
tags = frozen? ? dup : self
tags.send(:clean!)
tags.map do |name|
d = ActsAsTaggableOn.delimiter
d = Regexp.new d.join('|') if d.is_a? Array
name.index(d) ? "\"#{name}\"" : name
end.join(ActsAsTaggableOn.glue)
end
private
# Convert everything to string, remove whitespace, duplicates, and blanks.
def clean!
reject!(&:blank?)
map!(&:to_s)
map!(&:strip)
map! { |tag| tag.mb_chars.downcase.to_s } if ActsAsTaggableOn.force_lowercase
map!(&:parameterize) if ActsAsTaggableOn.force_parameterize
ActsAsTaggableOn.strict_case_match ? uniq! : uniq!(&:downcase)
self
end
def extract_and_apply_options!(args)
options = args.last.is_a?(Hash) ? args.pop : {}
options.assert_valid_keys :parse, :parser
parser = options[:parser] || @parser
args.map! { |a| parser.new(a).parse } if options[:parse] || options[:parser]
args.flatten!
end
end
end
acts-as-taggable-on-11.0.0/lib/acts-as-taggable-on/version.rb 0000644 0000041 0000041 00000000120 14704600021 023606 0 ustar www-data www-data # frozen_string_literal: true
module ActsAsTaggableOn
VERSION = '11.0.0'
end
acts-as-taggable-on-11.0.0/lib/acts-as-taggable-on/default_parser.rb 0000644 0000041 0000041 00000004751 14704600021 025137 0 ustar www-data www-data # frozen_string_literal: true
module ActsAsTaggableOn
##
# Returns a new TagList using the given tag string.
#
# Example:
# tag_list = ActsAsTaggableOn::DefaultParser.parse("One , Two, Three")
# tag_list # ["One", "Two", "Three"]
class DefaultParser < GenericParser
def parse
string = @tag_list
string = string.join(ActsAsTaggableOn.glue) if string.respond_to?(:join)
TagList.new.tap do |tag_list|
string = string.to_s.dup
string.gsub!(double_quote_pattern) do
# Append the matched tag to the tag list
tag_list << Regexp.last_match[2]
# Return the matched delimiter ($3) to replace the matched items
''
end
string.gsub!(single_quote_pattern) do
# Append the matched tag ($2) to the tag list
tag_list << Regexp.last_match[2]
# Return an empty string to replace the matched items
''
end
# split the string by the delimiter
# and add to the tag_list
tag_list.add(string.split(Regexp.new(delimiter)))
end
end
# private
def delimiter
# Parse the quoted tags
d = ActsAsTaggableOn.delimiter
# Separate multiple delimiters by bitwise operator
d = d.join('|') if d.is_a?(Array)
d
end
# ( # Tag start delimiter ($1)
# \A | # Either string start or
# #{delimiter} # a delimiter
# )
# \s*" # quote (") optionally preceded by whitespace
# (.*?) # Tag ($2)
# "\s* # quote (") optionally followed by whitespace
# (?= # Tag end delimiter (not consumed; is zero-length lookahead)
# #{delimiter}\s* | # Either a delimiter optionally followed by whitespace or
# \z # string end
# )
def double_quote_pattern
/(\A|#{delimiter})\s*"(.*?)"\s*(?=#{delimiter}\s*|\z)/
end
# ( # Tag start delimiter ($1)
# \A | # Either string start or
# #{delimiter} # a delimiter
# )
# \s*' # quote (') optionally preceded by whitespace
# (.*?) # Tag ($2)
# '\s* # quote (') optionally followed by whitespace
# (?= # Tag end delimiter (not consumed; is zero-length lookahead)
# #{delimiter}\s* | d # Either a delimiter optionally followed by whitespace or
# \z # string end
# )
def single_quote_pattern
/(\A|#{delimiter})\s*'(.*?)'\s*(?=#{delimiter}\s*|\z)/
end
end
end
acts-as-taggable-on-11.0.0/lib/acts-as-taggable-on.rb 0000644 0000041 0000041 00000006174 14704600021 022140 0 ustar www-data www-data require 'active_record'
require 'active_record/version'
require 'active_support/core_ext/module'
require 'zeitwerk'
loader = Zeitwerk::Loader.for_gem
loader.inflector.inflect "acts-as-taggable-on" => "ActsAsTaggableOn"
loader.setup
begin
require 'rails/engine'
require 'acts-as-taggable-on/engine'
rescue LoadError
end
require 'digest/sha1'
module ActsAsTaggableOn
class DuplicateTagError < StandardError
end
def self.setup
@configuration ||= Configuration.new
yield @configuration if block_given?
end
def self.method_missing(method_name, *args, &block)
@configuration.respond_to?(method_name) ?
@configuration.send(method_name, *args, &block) : super
end
def self.respond_to?(method_name, include_private=false)
@configuration.respond_to? method_name
end
def self.glue
setting = @configuration.delimiter
delimiter = setting.kind_of?(Array) ? setting[0] : setting
delimiter.end_with?(' ') ? delimiter : "#{delimiter} "
end
class Configuration
attr_accessor :force_lowercase, :force_parameterize,
:remove_unused_tags, :default_parser,
:tags_counter, :tags_table,
:taggings_table
attr_reader :delimiter, :strict_case_match, :base_class
def initialize
@delimiter = ','
@force_lowercase = false
@force_parameterize = false
@strict_case_match = false
@remove_unused_tags = false
@tags_counter = true
@default_parser = DefaultParser
@force_binary_collation = false
@tags_table = :tags
@taggings_table = :taggings
@base_class = '::ActiveRecord::Base'
end
def strict_case_match=(force_cs)
@strict_case_match = force_cs unless @force_binary_collation
end
def delimiter=(string)
ActiveRecord::Base.logger.warn < e
puts "Trapping #{e.class}: collation parameter ignored while migrating for the first time."
end
end
end
def base_class=(base_class)
raise "base_class must be a String" unless base_class.is_a?(String)
@base_class = base_class
end
end
setup
end
ActiveSupport.on_load(:active_record) do
extend ActsAsTaggableOn::Taggable
include ActsAsTaggableOn::Tagger
end
ActiveSupport.on_load(:action_view) do
include ActsAsTaggableOn::TagsHelper
end
acts-as-taggable-on-11.0.0/Appraisals 0000644 0000041 0000041 00000000646 14704600021 017354 0 ustar www-data www-data # frozen_string_literal: true
appraise 'activerecord-7.0' do
gem 'activerecord', '~> 7.0.1'
gem 'pg'
gem 'sqlite3', '~> 1.4'
gem 'mysql2', '~> 0.5'
end
appraise 'activerecord-7.1' do
gem 'activerecord', '~> 7.1.0'
gem 'pg'
gem 'sqlite3', '~> 1.4'
gem 'mysql2', '~> 0.5'
end
appraise 'activerecord-7.2' do
gem 'activerecord', '~> 7.2.0'
gem 'pg'
gem 'sqlite3', '~> 1.4'
gem 'mysql2', '~> 0.5'
end acts-as-taggable-on-11.0.0/spec/ 0000755 0000041 0000041 00000000000 14704600021 016256 5 ustar www-data www-data acts-as-taggable-on-11.0.0/spec/acts_as_taggable_on/ 0000755 0000041 0000041 00000000000 14704600021 022215 5 ustar www-data www-data acts-as-taggable-on-11.0.0/spec/acts_as_taggable_on/single_table_inheritance_spec.rb 0000644 0000041 0000041 00000022401 14704600021 030554 0 ustar www-data www-data # -*- encoding : utf-8 -*-
require 'spec_helper'
describe 'Single Table Inheritance' do
let(:taggable) { TaggableModel.new(name: 'taggable model') }
let(:inheriting_model) { InheritingTaggableModel.new(name: 'Inheriting Taggable Model') }
let(:altered_inheriting) { AlteredInheritingTaggableModel.new(name: 'Altered Inheriting Model') }
1.upto(4) do |n|
let(:"inheriting_#{n}") { InheritingTaggableModel.new(name: "Inheriting Model #{n}") }
end
let(:student) { Student.create! }
describe 'tag contexts' do
it 'should pass on to STI-inherited models' do
expect(inheriting_model).to respond_to(:tag_list, :skill_list, :language_list)
expect(altered_inheriting).to respond_to(:tag_list, :skill_list, :language_list)
end
it 'should pass on to altered STI models' do
expect(altered_inheriting).to respond_to(:part_list)
end
end
context 'matching contexts' do
before do
inheriting_1.offering_list = 'one, two'
inheriting_1.need_list = 'one, two'
inheriting_1.save!
inheriting_2.need_list = 'one, two'
inheriting_2.save!
inheriting_3.offering_list = 'one, two'
inheriting_3.save!
inheriting_4.tag_list = 'one, two, three, four'
inheriting_4.save!
taggable.need_list = 'one, two'
taggable.save!
end
it 'should find objects with tags of matching contexts' do
expect(inheriting_1.find_matching_contexts(:offerings, :needs)).to include(inheriting_2)
expect(inheriting_1.find_matching_contexts(:offerings, :needs)).to_not include(inheriting_3)
expect(inheriting_1.find_matching_contexts(:offerings, :needs)).to_not include(inheriting_4)
expect(inheriting_1.find_matching_contexts(:offerings, :needs)).to_not include(taggable)
expect(inheriting_1.find_matching_contexts_for(TaggableModel, :offerings, :needs)).to include(inheriting_2)
expect(inheriting_1.find_matching_contexts_for(TaggableModel, :offerings, :needs)).to_not include(inheriting_3)
expect(inheriting_1.find_matching_contexts_for(TaggableModel, :offerings, :needs)).to_not include(inheriting_4)
expect(inheriting_1.find_matching_contexts_for(TaggableModel, :offerings, :needs)).to include(taggable)
end
it 'should not include the object itself in the list of related objects with tags of matching contexts' do
expect(inheriting_1.find_matching_contexts(:offerings, :needs)).to_not include(inheriting_1)
expect(inheriting_1.find_matching_contexts_for(InheritingTaggableModel, :offerings, :needs)).to_not include(inheriting_1)
expect(inheriting_1.find_matching_contexts_for(TaggableModel, :offerings, :needs)).to_not include(inheriting_1)
end
end
context 'find related tags' do
before do
inheriting_1.tag_list = 'one, two'
inheriting_1.save
inheriting_2.tag_list = 'three, four'
inheriting_2.save
inheriting_3.tag_list = 'one, four'
inheriting_3.save
taggable.tag_list = 'one, two, three, four'
taggable.save
end
it 'should find related objects based on tag names on context' do
expect(inheriting_1.find_related_tags).to include(inheriting_3)
expect(inheriting_1.find_related_tags).to_not include(inheriting_2)
expect(inheriting_1.find_related_tags).to_not include(taggable)
expect(inheriting_1.find_related_tags_for(TaggableModel)).to include(inheriting_3)
expect(inheriting_1.find_related_tags_for(TaggableModel)).to_not include(inheriting_2)
expect(inheriting_1.find_related_tags_for(TaggableModel)).to include(taggable)
end
it 'should not include the object itself in the list of related objects' do
expect(inheriting_1.find_related_tags).to_not include(inheriting_1)
expect(inheriting_1.find_related_tags_for(InheritingTaggableModel)).to_not include(inheriting_1)
expect(inheriting_1.find_related_tags_for(TaggableModel)).to_not include(inheriting_1)
end
end
describe 'tag list' do
before do
@inherited_same = InheritingTaggableModel.new(name: 'inherited same')
@inherited_different = AlteredInheritingTaggableModel.new(name: 'inherited different')
end
#TODO, shared example
it 'should be able to save tags for inherited models' do
inheriting_model.tag_list = 'bob, kelso'
inheriting_model.save
expect(InheritingTaggableModel.tagged_with('bob').first).to eq(inheriting_model)
end
it 'should find STI tagged models on the superclass' do
inheriting_model.tag_list = 'bob, kelso'
inheriting_model.save
expect(TaggableModel.tagged_with('bob').first).to eq(inheriting_model)
end
it 'should be able to add on contexts only to some subclasses' do
altered_inheriting.part_list = 'fork, spoon'
altered_inheriting.save
expect(InheritingTaggableModel.tagged_with('fork', on: :parts)).to be_empty
expect(AlteredInheritingTaggableModel.tagged_with('fork', on: :parts).first).to eq(altered_inheriting)
end
it 'should have different tag_counts_on for inherited models' do
inheriting_model.tag_list = 'bob, kelso'
inheriting_model.save!
altered_inheriting.tag_list = 'fork, spoon'
altered_inheriting.save!
expect(InheritingTaggableModel.tag_counts_on(:tags, order: "#{ActsAsTaggableOn.tags_table}.id").map(&:name)).to eq(%w(bob kelso))
expect(AlteredInheritingTaggableModel.tag_counts_on(:tags, order: "#{ActsAsTaggableOn.tags_table}.id").map(&:name)).to eq(%w(fork spoon))
expect(TaggableModel.tag_counts_on(:tags, order: "#{ActsAsTaggableOn.tags_table}.id").map(&:name)).to eq(%w(bob kelso fork spoon))
end
it 'should have different tags_on for inherited models' do
inheriting_model.tag_list = 'bob, kelso'
inheriting_model.save!
altered_inheriting.tag_list = 'fork, spoon'
altered_inheriting.save!
expect(InheritingTaggableModel.tags_on(:tags, order: "#{ActsAsTaggableOn.tags_table}.id").map(&:name)).to eq(%w(bob kelso))
expect(AlteredInheritingTaggableModel.tags_on(:tags, order: "#{ActsAsTaggableOn.tags_table}.id").map(&:name)).to eq(%w(fork spoon))
expect(TaggableModel.tags_on(:tags, order: "#{ActsAsTaggableOn.tags_table}.id").map(&:name)).to eq(%w(bob kelso fork spoon))
end
it 'should store same tag without validation conflict' do
taggable.tag_list = 'one'
taggable.save!
inheriting_model.tag_list = 'one'
inheriting_model.save!
inheriting_model.update! name: 'foo'
end
it "should only join with taggable's table to check type for inherited models" do
expect(TaggableModel.tag_counts_on(:tags).to_sql).to_not match /INNER JOIN taggable_models ON/
expect(InheritingTaggableModel.tag_counts_on(:tags).to_sql).to match /INNER JOIN taggable_models ON/
end
end
describe 'ownership' do
it 'should have taggings' do
student.tag(taggable, with: 'ruby,scheme', on: :tags)
expect(student.owned_taggings.count).to eq(2)
end
it 'should have tags' do
student.tag(taggable, with: 'ruby,scheme', on: :tags)
expect(student.owned_tags.count).to eq(2)
end
it 'should return tags for the inheriting tagger' do
student.tag(taggable, with: 'ruby, scheme', on: :tags)
expect(taggable.tags_from(student)).to eq(%w(ruby scheme))
end
it 'returns all owner tags on the taggable' do
student.tag(taggable, with: 'ruby, scheme', on: :tags)
student.tag(taggable, with: 'skill_one', on: :skills)
student.tag(taggable, with: 'english, spanish', on: :language)
expect(taggable.owner_tags(student).count).to eq(5)
expect(taggable.owner_tags(student).sort == %w(english ruby scheme skill_one spanish))
end
it 'returns owner tags on the tagger' do
student.tag(taggable, with: 'ruby, scheme', on: :tags)
expect(taggable.owner_tags_on(student, :tags).count).to eq(2)
end
it 'returns owner tags on the taggable for an array of contexts' do
student.tag(taggable, with: 'ruby, scheme', on: :tags)
student.tag(taggable, with: 'skill_one, skill_two', on: :skills)
expect(taggable.owner_tags_on(student, [:tags, :skills]).count).to eq(4)
expect(taggable.owner_tags_on(student, [:tags, :skills]).sort == %w(ruby scheme skill_one skill_two))
end
it 'should scope objects returned by tagged_with by owners' do
student.tag(taggable, with: 'ruby, scheme', on: :tags)
expect(TaggableModel.tagged_with(%w(ruby scheme), owned_by: student).count).to eq(1)
end
end
describe 'a subclass of Tag' do
let(:company) { Company.new(:name => 'Dewey, Cheatham & Howe') }
let(:user) { User.create! }
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!
expect(ActsAsTaggableOn::Tag.where(name: 'cambridge', type: nil)).to_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)
expect(tags.all? { |tag| tag.is_a? Market }).to be_truthy
end
end
end
acts-as-taggable-on-11.0.0/spec/acts_as_taggable_on/tagging_spec.rb 0000644 0000041 0000041 00000013424 14704600021 025200 0 ustar www-data www-data # -*- encoding : utf-8 -*-
require 'spec_helper'
describe ActsAsTaggableOn::Tagging do
before(:each) do
@tagging = ActsAsTaggableOn::Tagging.new
end
it 'should not be valid with a invalid tag' do
@tagging.taggable = TaggableModel.create(name: 'Bob Jones')
@tagging.tag = ActsAsTaggableOn::Tag.new(name: '')
@tagging.context = 'tags'
expect(@tagging).to_not be_valid
expect(@tagging.errors[:tag_id]).to eq(['can\'t be blank'])
end
it 'should not create duplicate taggings' do
@taggable = TaggableModel.create(name: 'Bob Jones')
@tag = ActsAsTaggableOn::Tag.create(name: 'awesome')
expect {
2.times { ActsAsTaggableOn::Tagging.create(taggable: @taggable, tag: @tag, context: 'tags') }
}.to change(ActsAsTaggableOn::Tagging, :count).by(1)
end
it 'should not delete tags of other records' do
6.times { TaggableModel.create(name: 'Bob Jones', tag_list: 'very, serious, bug') }
expect(ActsAsTaggableOn::Tag.count).to eq(3)
taggable = TaggableModel.first
taggable.tag_list = 'bug'
taggable.save
expect(taggable.tag_list).to eq(['bug'])
another_taggable = TaggableModel.where('id != ?', taggable.id).sample
expect(another_taggable.tag_list.sort).to eq(%w(very serious bug).sort)
end
it 'should destroy unused tags after tagging destroyed' do
previous_setting = ActsAsTaggableOn.remove_unused_tags
ActsAsTaggableOn.remove_unused_tags = true
ActsAsTaggableOn::Tag.destroy_all
@taggable = TaggableModel.create(name: 'Bob Jones')
@taggable.update_attribute :tag_list, 'aaa,bbb,ccc'
@taggable.update_attribute :tag_list, ''
expect(ActsAsTaggableOn::Tag.count).to eql(0)
ActsAsTaggableOn.remove_unused_tags = previous_setting
end
it 'should destroy unused tags after tagging destroyed when not using tags_counter' do
remove_unused_tags_previous_setting = ActsAsTaggableOn.remove_unused_tags
tags_counter_previous_setting = ActsAsTaggableOn.tags_counter
ActsAsTaggableOn.remove_unused_tags = true
ActsAsTaggableOn.tags_counter = false
ActsAsTaggableOn::Tag.destroy_all
@taggable = TaggableModel.create(name: 'Bob Jones')
@taggable.update_attribute :tag_list, 'aaa,bbb,ccc'
@taggable.update_attribute :tag_list, ''
expect(ActsAsTaggableOn::Tag.count).to eql(0)
ActsAsTaggableOn.remove_unused_tags = remove_unused_tags_previous_setting
ActsAsTaggableOn.tags_counter = tags_counter_previous_setting
end
describe 'context scopes' do
before do
@tagging_2 = ActsAsTaggableOn::Tagging.new
@tagging_3 = ActsAsTaggableOn::Tagging.new
@tagger = User.new
@tagger_2 = User.new
@tagging.taggable = TaggableModel.create(name: "Black holes")
@tagging.tag = ActsAsTaggableOn::Tag.create(name: "Physics")
@tagging.tagger = @tagger
@tagging.context = 'Science'
@tagging.tenant = 'account1'
@tagging.save
@tagging_2.taggable = TaggableModel.create(name: "Satellites")
@tagging_2.tag = ActsAsTaggableOn::Tag.create(name: "Technology")
@tagging_2.tagger = @tagger_2
@tagging_2.context = 'Science'
@tagging_2.tenant = 'account1'
@tagging_2.save
@tagging_3.taggable = TaggableModel.create(name: "Satellites")
@tagging_3.tag = ActsAsTaggableOn::Tag.create(name: "Engineering")
@tagging_3.tagger = @tagger_2
@tagging_3.context = 'Astronomy'
@tagging_3.save
end
describe '.owned_by' do
it "should belong to a specific user" do
expect(ActsAsTaggableOn::Tagging.owned_by(@tagger).first).to eq(@tagging)
end
end
describe '.by_context' do
it "should be found by context" do
expect(ActsAsTaggableOn::Tagging.by_context('Science').length).to eq(2);
end
end
describe '.by_contexts' do
it "should find taggings by contexts" do
expect(ActsAsTaggableOn::Tagging.by_contexts(['Science', 'Astronomy']).first).to eq(@tagging);
expect(ActsAsTaggableOn::Tagging.by_contexts(['Science', 'Astronomy']).second).to eq(@tagging_2);
expect(ActsAsTaggableOn::Tagging.by_contexts(['Science', 'Astronomy']).third).to eq(@tagging_3);
expect(ActsAsTaggableOn::Tagging.by_contexts(['Science', 'Astronomy']).length).to eq(3);
end
end
describe '.by_tenant' do
it "should find taggings by tenant" do
expect(ActsAsTaggableOn::Tagging.by_tenant('account1').length).to eq(2);
expect(ActsAsTaggableOn::Tagging.by_tenant('account1').first).to eq(@tagging);
expect(ActsAsTaggableOn::Tagging.by_tenant('account1').second).to eq(@tagging_2);
end
end
describe '.not_owned' do
before do
@tagging_4 = ActsAsTaggableOn::Tagging.new
@tagging_4.taggable = TaggableModel.create(name: "Gravity")
@tagging_4.tag = ActsAsTaggableOn::Tag.create(name: "Space")
@tagging_4.context = "Science"
@tagging_4.save
end
it "should found the taggings that do not have owner" do
expect(ActsAsTaggableOn::Tagging.all.length).to eq(4)
expect(ActsAsTaggableOn::Tagging.not_owned.length).to eq(1)
expect(ActsAsTaggableOn::Tagging.not_owned.first).to eq(@tagging_4)
end
end
end
describe 'base_class' do
before do
class Foo < ActiveRecord::Base; end
end
context "default" do
it "inherits from ActiveRecord::Base" do
expect(ActsAsTaggableOn::Tagging.ancestors).to include(ActiveRecord::Base)
expect(ActsAsTaggableOn::Tagging.ancestors).to_not include(Foo)
end
end
context "custom" do
it "inherits from custom class" do
ActsAsTaggableOn.base_class = 'Foo'
hide_const("ActsAsTaggableOn::Tagging")
load("lib/acts-as-taggable-on/tagging.rb")
expect(ActsAsTaggableOn::Tagging.ancestors).to include(Foo)
end
end
end
end
acts-as-taggable-on-11.0.0/spec/acts_as_taggable_on/default_parser_spec.rb 0000644 0000041 0000041 00000003153 14704600021 026556 0 ustar www-data www-data # -*- encoding : utf-8 -*-
require 'spec_helper'
describe ActsAsTaggableOn::DefaultParser do
it '#parse should return empty array if empty array is passed' do
parser = ActsAsTaggableOn::DefaultParser.new([])
expect(parser.parse).to be_empty
end
describe 'Multiple Delimiter' do
before do
@old_delimiter = ActsAsTaggableOn.delimiter
end
after do
ActsAsTaggableOn.delimiter = @old_delimiter
end
it 'should separate tags by delimiters' do
ActsAsTaggableOn.delimiter = [',', ' ', '\|']
parser = ActsAsTaggableOn::DefaultParser.new('cool, data|I have')
expect(parser.parse.to_s).to eq('cool, data, I, have')
end
it 'should escape quote' do
ActsAsTaggableOn.delimiter = [',', ' ', '\|']
parser = ActsAsTaggableOn::DefaultParser.new("'I have'|cool, data")
expect(parser.parse.to_s).to eq('"I have", cool, data')
parser = ActsAsTaggableOn::DefaultParser.new('"I, have"|cool, data')
expect(parser.parse.to_s).to eq('"I, have", cool, data')
end
it 'should work for utf8 delimiter and long delimiter' do
ActsAsTaggableOn.delimiter = [',', '的', '可能是']
parser = ActsAsTaggableOn::DefaultParser.new('我的东西可能是不见了,还好有备份')
expect(parser.parse.to_s).to eq('我, 东西, 不见了, 还好有备份')
end
it 'should work for multiple quoted tags' do
ActsAsTaggableOn.delimiter = [',']
parser = ActsAsTaggableOn::DefaultParser.new('"Ruby Monsters","eat Katzenzungen"')
expect(parser.parse.to_s).to eq('Ruby Monsters, eat Katzenzungen')
end
end
end
acts-as-taggable-on-11.0.0/spec/acts_as_taggable_on/tagger_spec.rb 0000644 0000041 0000041 00000012262 14704600021 025030 0 ustar www-data www-data # -*- encoding : utf-8 -*-
require 'spec_helper'
describe 'Tagger' do
before(:each) do
@user = User.create
@taggable = TaggableModel.create(name: 'Bob Jones')
end
it 'should have taggings' do
@user.tag(@taggable, with: 'ruby,scheme', on: :tags)
expect(@user.owned_taggings.size).to eq(2)
end
it 'should have tags' do
@user.tag(@taggable, with: 'ruby,scheme', on: :tags)
expect(@user.owned_tags.size).to eq(2)
end
it 'should scope objects returned by tagged_with by owners' do
@taggable2 = TaggableModel.create(name: 'Jim Jones')
@taggable3 = TaggableModel.create(name: 'Jane Doe')
@user2 = User.new
@user.tag(@taggable, with: 'ruby, scheme', on: :tags)
@user2.tag(@taggable2, with: 'ruby, scheme', on: :tags)
@user2.tag(@taggable3, with: 'ruby, scheme', on: :tags)
expect(TaggableModel.tagged_with(%w(ruby scheme), owned_by: @user).count).to eq(1)
expect(TaggableModel.tagged_with(%w(ruby scheme), owned_by: @user2).count).to eq(2)
end
it 'only returns objects tagged by owned_by when any is true' do
@user2 = User.new
@taggable2 = TaggableModel.create(name: 'Jim Jones')
@taggable3 = TaggableModel.create(name: 'Jane Doe')
@user.tag(@taggable, with: 'ruby', on: :tags)
@user.tag(@taggable2, with: 'java', on: :tags)
@user2.tag(@taggable3, with: 'ruby', on: :tags)
tags = TaggableModel.tagged_with(%w(ruby java), owned_by: @user, any: true)
expect(tags).to include(@taggable, @taggable2)
expect(tags.size).to eq(2)
end
it 'only returns objects tagged by owned_by when exclude is true' do
@user2 = User.new
@taggable2 = TaggableModel.create(name: 'Jim Jones')
@taggable3 = TaggableModel.create(name: 'Jane Doe')
@user.tag(@taggable, with: 'ruby', on: :tags)
@user.tag(@taggable2, with: 'java', on: :tags)
@user2.tag(@taggable3, with: 'java', on: :tags)
tags = TaggableModel.tagged_with(%w(ruby), owned_by: @user, exclude: true)
expect(tags).to eq([@taggable2])
end
it 'should not overlap tags from different taggers' do
@user2 = User.new
expect {
@user.tag(@taggable, with: 'ruby, scheme', on: :tags)
@user2.tag(@taggable, with: 'java, python, lisp, ruby', on: :tags)
}.to change(ActsAsTaggableOn::Tagging, :count).by(6)
[@user, @user2, @taggable].each(&:reload)
expect(@user.owned_tags.map(&:name).sort).to eq(%w(ruby scheme).sort)
expect(@user2.owned_tags.map(&:name).sort).to eq(%w(java python lisp ruby).sort)
expect(@taggable.tags_from(@user).sort).to eq(%w(ruby scheme).sort)
expect(@taggable.tags_from(@user2).sort).to eq(%w(java lisp python ruby).sort)
expect(@taggable.all_tags_list.sort).to eq(%w(ruby scheme java python lisp).sort)
expect(@taggable.all_tags_on(:tags).size).to eq(5)
end
it 'should not lose tags from different taggers' do
@user2 = User.create
@user2.tag(@taggable, with: 'java, python, lisp, ruby', on: :tags)
@user.tag(@taggable, with: 'ruby, scheme', on: :tags)
expect {
@user2.tag(@taggable, with: 'java, python, lisp', on: :tags)
}.to change(ActsAsTaggableOn::Tagging, :count).by(-1)
[@user, @user2, @taggable].each(&:reload)
expect(@taggable.tags_from(@user).sort).to eq(%w(ruby scheme).sort)
expect(@taggable.tags_from(@user2).sort).to eq(%w(java python lisp).sort)
expect(@taggable.all_tags_list.sort).to eq(%w(ruby scheme java python lisp).sort)
expect(@taggable.all_tags_on(:tags).length).to eq(5)
end
it 'should not lose tags' do
@user2 = User.create
@user.tag(@taggable, with: 'awesome', on: :tags)
@user2.tag(@taggable, with: 'awesome, epic', on: :tags)
expect {
@user2.tag(@taggable, with: 'epic', on: :tags)
}.to change(ActsAsTaggableOn::Tagging, :count).by(-1)
@taggable.reload
expect(@taggable.all_tags_list).to include('awesome')
expect(@taggable.all_tags_list).to include('epic')
end
it 'should not lose tags' do
@taggable.update(tag_list: 'ruby')
@user.tag(@taggable, with: 'ruby, scheme', on: :tags)
[@taggable, @user].each(&:reload)
expect(@taggable.tag_list).to eq(%w(ruby))
expect(@taggable.all_tags_list.sort).to eq(%w(ruby scheme).sort)
expect {
@taggable.update(tag_list: '')
}.to change(ActsAsTaggableOn::Tagging, :count).by(-1)
expect(@taggable.tag_list).to be_empty
expect(@taggable.all_tags_list.sort).to eq(%w(ruby scheme).sort)
end
it 'is tagger' do
expect(@user.is_tagger?).to be_truthy
end
it 'should skip save if skip_save is passed as option' do
expect(-> {
@user.tag(@taggable, with: 'epic', on: :tags, skip_save: true)
}).to_not change(ActsAsTaggableOn::Tagging, :count)
end
it 'should change tags order in ordered taggable' do
@ordered_taggable = OrderedTaggableModel.create name: 'hey!'
@user.tag @ordered_taggable, with: 'tag, tag1', on: :tags
expect(@ordered_taggable.reload.tags_from(@user)).to eq(['tag', 'tag1'])
@user.tag @ordered_taggable, with: 'tag2, tag1', on: :tags
expect(@ordered_taggable.reload.tags_from(@user)).to eq(['tag2', 'tag1'])
@user.tag @ordered_taggable, with: 'tag1, tag2', on: :tags
expect(@ordered_taggable.reload.tags_from(@user)).to eq(['tag1', 'tag2'])
end
end
acts-as-taggable-on-11.0.0/spec/acts_as_taggable_on/tag_list_spec.rb 0000644 0000041 0000041 00000013376 14704600021 025374 0 ustar www-data www-data # -*- encoding : utf-8 -*-
require 'spec_helper'
describe ActsAsTaggableOn::TagList do
let(:tag_list) { ActsAsTaggableOn::TagList.new('awesome', 'radical') }
let(:another_tag_list) { ActsAsTaggableOn::TagList.new('awesome','crazy', 'alien') }
it { should be_kind_of Array }
describe '#add' do
it 'should be able to be add a new tag word' do
tag_list.add('cool')
expect(tag_list.include?('cool')).to be_truthy
end
it 'should be able to add delimited lists of words' do
tag_list.add('cool, wicked', parse: true)
expect(tag_list).to include('cool', 'wicked')
end
it 'should be able to add delimited list of words with quoted delimiters' do
tag_list.add("'cool, wicked', \"really cool, really wicked\"", parse: true)
expect(tag_list).to include('cool, wicked', 'really cool, really wicked')
end
it 'should be able to handle other uses of quotation marks correctly' do
tag_list.add("john's cool car, mary's wicked toy", parse: true)
expect(tag_list).to include("john's cool car", "mary's wicked toy")
end
it 'should be able to add an array of words' do
tag_list.add(%w(cool wicked), parse: true)
expect(tag_list).to include('cool', 'wicked')
end
it 'should quote escape tags with commas in them' do
tag_list.add('cool', 'rad,bodacious')
expect(tag_list.to_s).to eq("awesome, radical, cool, \"rad,bodacious\"")
end
end
describe '#remove' do
it 'should be able to remove words' do
tag_list.remove('awesome')
expect(tag_list).to_not include('awesome')
end
it 'should be able to remove delimited lists of words' do
tag_list.remove('awesome, radical', parse: true)
expect(tag_list).to be_empty
end
it 'should be able to remove an array of words' do
tag_list.remove(%w(awesome radical), parse: true)
expect(tag_list).to be_empty
end
end
describe '#+' do
it 'should not have duplicate tags' do
new_tag_list = tag_list + another_tag_list
expect(tag_list).to eq(%w[awesome radical])
expect(another_tag_list).to eq(%w[awesome crazy alien])
expect(new_tag_list).to eq(%w[awesome radical crazy alien])
end
it 'should have class : ActsAsTaggableOn::TagList' do
new_tag_list = tag_list + another_tag_list
expect(new_tag_list.class).to eq(ActsAsTaggableOn::TagList)
end
end
describe '#concat' do
it 'should not have duplicate tags' do
expect(tag_list.concat(another_tag_list)).to eq(%w[awesome radical crazy alien])
end
it 'should have class : ActsAsTaggableOn::TagList' do
new_tag_list = tag_list.concat(another_tag_list)
expect(new_tag_list.class).to eq(ActsAsTaggableOn::TagList)
end
context 'without duplicates' do
let(:arr) { ['crazy', 'alien'] }
let(:another_tag_list) { ActsAsTaggableOn::TagList.new(*arr) }
it 'adds other list' do
expect(tag_list.concat(another_tag_list)).to eq(%w[awesome radical crazy alien])
end
it 'adds other array' do
expect(tag_list.concat(arr)).to eq(%w[awesome radical crazy alien])
end
end
end
describe '#to_s' do
it 'should give a delimited list of words when converted to string' do
expect(tag_list.to_s).to eq('awesome, radical')
end
it 'should be able to call to_s on a frozen tag list' do
tag_list.freeze
expect { tag_list.add('cool', 'rad,bodacious') }.to raise_error(RuntimeError)
expect { tag_list.to_s }.to_not raise_error
end
end
describe 'cleaning' do
it 'should parameterize if force_parameterize is set to true' do
ActsAsTaggableOn.force_parameterize = true
tag_list = ActsAsTaggableOn::TagList.new('awesome()', 'radical)(cc')
expect(tag_list.to_s).to eq('awesome, radical-cc')
ActsAsTaggableOn.force_parameterize = false
end
it 'should lowercase if force_lowercase is set to true' do
ActsAsTaggableOn.force_lowercase = true
tag_list = ActsAsTaggableOn::TagList.new('aweSomE', 'RaDicaL', 'Entrée')
expect(tag_list.to_s).to eq('awesome, radical, entrée')
ActsAsTaggableOn.force_lowercase = false
end
it 'should ignore case when removing duplicates if strict_case_match is false' do
tag_list = ActsAsTaggableOn::TagList.new('Junglist', 'JUNGLIST', 'Junglist', 'Massive', 'MASSIVE', 'MASSIVE')
expect(tag_list.to_s).to eq('Junglist, Massive')
end
it 'should not ignore case when removing duplicates if strict_case_match is true' do
ActsAsTaggableOn.strict_case_match = true
tag_list = ActsAsTaggableOn::TagList.new('Junglist', 'JUNGLIST', 'Junglist', 'Massive', 'MASSIVE', 'MASSIVE')
expect(tag_list.to_s).to eq('Junglist, JUNGLIST, Massive, MASSIVE')
ActsAsTaggableOn.strict_case_match = false
end
end
describe 'custom parser' do
let(:parser) { double(parse: %w(cool wicked)) }
let(:parser_class) { stub_const('MyParser', Class) }
it 'should use a the default parser if none is set as parameter' do
allow(ActsAsTaggableOn.default_parser).to receive(:new).and_return(parser)
ActsAsTaggableOn::TagList.new('cool, wicked', parse: true)
expect(parser).to have_received(:parse)
end
it 'should use the custom parser passed as parameter' do
allow(parser_class).to receive(:new).and_return(parser)
ActsAsTaggableOn::TagList.new('cool, wicked', parser: parser_class)
expect(parser).to have_received(:parse)
end
it 'should use the parser set as attribute' do
allow(parser_class).to receive(:new).with('new, tag').and_return(parser)
tag_list = ActsAsTaggableOn::TagList.new('example')
tag_list.parser = parser_class
tag_list.add('new, tag', parse: true)
expect(parser).to have_received(:parse)
end
end
end
acts-as-taggable-on-11.0.0/spec/acts_as_taggable_on/utils_spec.rb 0000644 0000041 0000041 00000001637 14704600021 024723 0 ustar www-data www-data # -*- encoding : utf-8 -*-
require 'spec_helper'
describe ActsAsTaggableOn::Utils do
describe '#like_operator' do
it 'should return \'ILIKE\' when the adapter is PostgreSQL' do
allow(ActsAsTaggableOn::Utils.connection).to receive(:adapter_name) { 'PostgreSQL' }
expect(ActsAsTaggableOn::Utils.like_operator).to eq('ILIKE')
end
it 'should return \'LIKE\' when the adapter is not PostgreSQL' do
allow(ActsAsTaggableOn::Utils.connection).to receive(:adapter_name) { 'MySQL' }
expect(ActsAsTaggableOn::Utils.like_operator).to eq('LIKE')
end
end
describe '#sha_prefix' do
it 'should return a consistent prefix for a given word' do
expect(ActsAsTaggableOn::Utils.sha_prefix('kittens')).to eq(ActsAsTaggableOn::Utils.sha_prefix('kittens'))
expect(ActsAsTaggableOn::Utils.sha_prefix('puppies')).not_to eq(ActsAsTaggableOn::Utils.sha_prefix('kittens'))
end
end
end
acts-as-taggable-on-11.0.0/spec/acts_as_taggable_on/taggable_spec.rb 0000644 0000041 0000041 00000102012 14704600021 025316 0 ustar www-data www-data # encoding: utf-8
require 'spec_helper'
describe 'Taggable To Preserve Order' do
before(:each) do
@taggable = OrderedTaggableModel.new(name: 'Bob Jones')
end
it 'should have tag associations' do
[:tags, :colours].each do |type|
expect(@taggable.respond_to?(type)).to be_truthy
expect(@taggable.respond_to?("#{type.to_s.singularize}_taggings")).to be_truthy
end
end
it 'should have tag methods' do
[:tags, :colours].each do |type|
expect(@taggable.respond_to?("#{type.to_s.singularize}_list")).to be_truthy
expect(@taggable.respond_to?("#{type.to_s.singularize}_list=")).to be_truthy
expect(@taggable.respond_to?("all_#{type}_list")).to be_truthy
end
end
it 'should return tag list in the order the tags were created' do
# create
@taggable.tag_list = 'rails, ruby, css'
expect(@taggable.instance_variable_get('@tag_list').instance_of?(ActsAsTaggableOn::TagList)).to be_truthy
expect{
@taggable.save
}.to change(ActsAsTaggableOn::Tag, :count).by(3)
@taggable.reload
expect(@taggable.tag_list).to eq(%w(rails ruby css))
# update
@taggable.tag_list = 'pow, ruby, rails'
@taggable.save
@taggable.reload
expect(@taggable.tag_list).to eq(%w(pow ruby rails))
# update with no change
@taggable.tag_list = 'pow, ruby, rails'
@taggable.save
@taggable.reload
expect(@taggable.tag_list).to eq(%w(pow ruby rails))
# update to clear tags
@taggable.tag_list = ''
@taggable.save
@taggable.reload
expect(@taggable.tag_list).to be_empty
end
it 'should return tag objects in the order the tags were created' do
# create
@taggable.tag_list = 'pow, ruby, rails'
expect(@taggable.instance_variable_get('@tag_list').instance_of?(ActsAsTaggableOn::TagList)).to be_truthy
expect {
@taggable.save
}.to change(ActsAsTaggableOn::Tag, :count).by(3)
@taggable.reload
expect(@taggable.tags.map { |t| t.name }).to eq(%w(pow ruby rails))
# update
@taggable.tag_list = 'rails, ruby, css, pow'
@taggable.save
@taggable.reload
expect(@taggable.tags.map { |t| t.name }).to eq(%w(rails ruby css pow))
end
it 'should return tag objects in tagging id order' do
# create
@taggable.tag_list = 'pow, ruby, rails'
@taggable.save
@taggable.reload
ids = @taggable.tags.map { |t| t.taggings.first.id }
expect(ids).to eq(ids.sort)
# update
@taggable.tag_list = 'rails, ruby, css, pow'
@taggable.save
@taggable.reload
ids = @taggable.tags.map { |t| t.taggings.first.id }
expect(ids).to eq(ids.sort)
end
end
describe 'Taggable' do
before(:each) do
@taggable = TaggableModel.new(name: 'Bob Jones')
@taggables = [@taggable, TaggableModel.new(name: 'John Doe')]
end
it 'should have tag types' do
[:tags, :languages, :skills, :needs, :offerings].each do |type|
expect(TaggableModel.tag_types).to include type
end
expect(@taggable.tag_types).to eq(TaggableModel.tag_types)
end
it 'should have tenant column' do
expect(TaggableModel.tenant_column).to eq(:tenant_id)
end
it 'should have tag_counts_on' do
expect(TaggableModel.tag_counts_on(:tags)).to be_empty
@taggable.tag_list = %w(awesome epic)
@taggable.save
expect(TaggableModel.tag_counts_on(:tags).length).to eq(2)
expect(@taggable.tag_counts_on(:tags).length).to eq(2)
end
context 'tag_counts on a collection' do
context 'a select clause is specified on the collection' do
it 'should return tag counts without raising an error' do
expect(TaggableModel.tag_counts_on(:tags)).to be_empty
@taggable.tag_list = %w(awesome epic)
@taggable.save
expect {
expect(TaggableModel.select(:name).tag_counts_on(:tags).length).to eq(2)
}.not_to raise_error
end
end
end
it 'should have tags_on' do
expect(TaggableModel.tags_on(:tags)).to be_empty
@taggable.tag_list = %w(awesome epic)
@taggable.save
expect(TaggableModel.tags_on(:tags).length).to eq(2)
expect(@taggable.tags_on(:tags).length).to eq(2)
end
it 'should return [] right after create' do
blank_taggable = TaggableModel.new(name: 'Bob Jones')
expect(blank_taggable.tag_list).to be_empty
end
it 'should be able to create tags' do
@taggable.skill_list = 'ruby, rails, css'
expect(@taggable.instance_variable_get('@skill_list').instance_of?(ActsAsTaggableOn::TagList)).to be_truthy
expect{
@taggable.save
}.to change(ActsAsTaggableOn::Tag, :count).by(3)
@taggable.reload
expect(@taggable.skill_list.sort).to eq(%w(ruby rails css).sort)
end
it 'should be able to create tags through the tag list directly' do
@taggable.tag_list_on(:test).add('hello')
expect(@taggable.tag_list_cache_on(:test)).to_not be_empty
expect(@taggable.tag_list_on(:test)).to eq(['hello'])
@taggable.save
@taggable.save_tags
@taggable.reload
expect(@taggable.tag_list_on(:test)).to eq(['hello'])
end
it 'should differentiate between contexts' do
@taggable.skill_list = 'ruby, rails, css'
@taggable.tag_list = 'ruby, bob, charlie'
@taggable.save
@taggable.reload
expect(@taggable.skill_list).to include('ruby')
expect(@taggable.skill_list).to_not include('bob')
end
it 'should be able to remove tags through list alone' do
@taggable.skill_list = 'ruby, rails, css'
@taggable.save
@taggable.reload
expect(@taggable.skills.count).to eq(3)
@taggable.skill_list = 'ruby, rails'
@taggable.save
@taggable.reload
expect(@taggable.skills.count).to eq(2)
end
it 'should be able to select taggables by subset of tags using ActiveRelation methods' do
@taggables[0].tag_list = 'bob'
@taggables[1].tag_list = 'charlie'
@taggables[0].skill_list = 'ruby'
@taggables[1].skill_list = 'css'
@taggables.each { |taggable| taggable.save }
@found_taggables_by_tag = TaggableModel.joins(:tags).where(ActsAsTaggableOn.tags_table => {name: ['bob']})
@found_taggables_by_skill = TaggableModel.joins(:skills).where(ActsAsTaggableOn.tags_table => {name: ['ruby']})
expect(@found_taggables_by_tag).to include @taggables[0]
expect(@found_taggables_by_tag).to_not include @taggables[1]
expect(@found_taggables_by_skill).to include @taggables[0]
expect(@found_taggables_by_skill).to_not include @taggables[1]
end
it 'should be able to find by tag' do
@taggable.skill_list = 'ruby, rails, css'
@taggable.save
expect(TaggableModel.tagged_with('ruby').first).to eq(@taggable)
end
it 'should be able to get a count with find by tag when using a group by' do
@taggable.skill_list = 'ruby'
@taggable.save
expect(TaggableModel.tagged_with('ruby').group(:created_at).count.count).to eq(1)
end
it 'can be used as scope' do
@taggable.skill_list = 'ruby'
@taggable.save
untaggable_model = @taggable.untaggable_models.create!(name:'foobar')
scope_tag = TaggableModel.tagged_with('ruby', any: 'distinct', order: 'taggable_models.name asc')
expect(UntaggableModel.joins(:taggable_model).merge(scope_tag).except(:select)).to eq([untaggable_model])
end
it "shouldn't generate a query with DISTINCT by default" do
@taggable.skill_list = 'ruby, rails, css'
@taggable.save
expect(TaggableModel.tagged_with('ruby').to_sql).to_not match /DISTINCT/
end
it "should be able to find a tag using dates" do
@taggable.skill_list = "ruby"
@taggable.save
today = Date.today.to_time.utc
tomorrow = Date.tomorrow.to_time.utc
expect(TaggableModel.tagged_with("ruby", :start_at => today, :end_at => tomorrow).count).to eq(1)
end
it "shouldn't be able to find a tag outside date range" do
@taggable.skill_list = "ruby"
@taggable.save
expect(TaggableModel.tagged_with("ruby", :start_at => Date.today - 2.days, :end_at => Date.today - 1.day).count).to eq(0)
end
it 'should be able to find by tag with context' do
@taggable.skill_list = 'ruby, rails, css, julia'
@taggable.tag_list = 'bob, charlie, julia'
@taggable.save
expect(TaggableModel.tagged_with('ruby').first).to eq(@taggable)
expect(TaggableModel.tagged_with('ruby, css').first).to eq(@taggable)
expect(TaggableModel.tagged_with('bob', on: :skills).first).to_not eq(@taggable)
expect(TaggableModel.tagged_with('bob', on: :tags).first).to eq(@taggable)
expect(TaggableModel.tagged_with('julia', on: :skills).size).to eq(1)
expect(TaggableModel.tagged_with('julia', on: :tags).size).to eq(1)
expect(TaggableModel.tagged_with('julia', on: nil).size).to eq(2)
end
it 'should not care about case' do
TaggableModel.create(name: 'Bob', tag_list: 'ruby')
TaggableModel.create(name: 'Frank', tag_list: 'Ruby')
expect(ActsAsTaggableOn::Tag.all.size).to eq(1)
expect(TaggableModel.tagged_with('ruby').to_a).to eq(TaggableModel.tagged_with('Ruby').to_a)
end
it 'should be able to find by tags with other joins in the query' do
@taggable.skill_list = 'ruby, rails, css'
@taggable.tag_list = 'bob, charlie'
@taggable.save
expect(TaggableModel.tagged_with(['bob', 'css'], :any => true).to_a).to eq([@taggable])
bob = TaggableModel.create(:name => 'Bob', :tag_list => 'ruby, rails, css')
frank = TaggableModel.create(:name => 'Frank', :tag_list => 'ruby, rails')
charlie = TaggableModel.create(:name => 'Charlie', :skill_list => 'ruby, java')
# Test for explicit distinct in select
bob.untaggable_models.create!
frank.untaggable_models.create!
charlie.untaggable_models.create!
expect(TaggableModel.select('distinct(taggable_models.id), taggable_models.*').joins(:untaggable_models).tagged_with(['css', 'java'], :any => true).to_a.sort).to eq([bob, charlie].sort)
expect(TaggableModel.select('distinct(taggable_models.id), taggable_models.*').joins(:untaggable_models).tagged_with(['rails', 'ruby'], :any => false).to_a.sort).to eq([bob, frank].sort)
end
it 'should not care about case for unicode names', unless: using_sqlite? do
ActsAsTaggableOn.strict_case_match = false
TaggableModel.create(name: 'Anya', tag_list: 'ПРИВЕТ')
TaggableModel.create(name: 'Igor', tag_list: 'привет')
TaggableModel.create(name: 'Katia', tag_list: 'ПРИВЕТ')
expect(ActsAsTaggableOn::Tag.all.size).to eq(1)
expect(TaggableModel.tagged_with('привет').to_a).to eq(TaggableModel.tagged_with('ПРИВЕТ').to_a)
end
context 'should be able to create and find tags in languages without capitalization :' do
ActsAsTaggableOn.strict_case_match = false
{
japanese: {name: 'Chihiro', tag_list: '日本の'},
hebrew: {name: 'Salim', tag_list: 'עברית'},
chinese: {name: 'Ieie', tag_list: '中国的'},
arabic: {name: 'Yasser', tag_list: 'العربية'},
emo: {name: 'Emo', tag_list: '✏'}
}.each do |language, values|
it language do
TaggableModel.create(values)
expect(TaggableModel.tagged_with(values[:tag_list]).count).to eq(1)
end
end
end
it 'should be able to get tag counts on model as a whole' do
TaggableModel.create(name: 'Bob', tag_list: 'ruby, rails, css')
TaggableModel.create(name: 'Frank', tag_list: 'ruby, rails')
TaggableModel.create(name: 'Charlie', skill_list: 'ruby')
expect(TaggableModel.tag_counts).to_not be_empty
expect(TaggableModel.skill_counts).to_not be_empty
end
it 'should be able to get all tag counts on model as whole' do
TaggableModel.create(name: 'Bob', tag_list: 'ruby, rails, css')
TaggableModel.create(name: 'Frank', tag_list: 'ruby, rails')
TaggableModel.create(name: 'Charlie', skill_list: 'ruby')
expect(TaggableModel.all_tag_counts).to_not be_empty
expect(TaggableModel.all_tag_counts(order: "#{ActsAsTaggableOn.tags_table}.id").first.count).to eq(3) # ruby
end
it 'should be able to get all tags on model as whole' do
TaggableModel.create(name: 'Bob', tag_list: 'ruby, rails, css')
TaggableModel.create(name: 'Frank', tag_list: 'ruby, rails')
TaggableModel.create(name: 'Charlie', skill_list: 'ruby')
expect(TaggableModel.all_tags).to_not be_empty
expect(TaggableModel.all_tags(order: "#{ActsAsTaggableOn.tags_table}.id").first.name).to eq('ruby')
end
it 'should be able to use named scopes to chain tag finds by any tags by context' do
bob = TaggableModel.create(name: 'Bob', need_list: 'rails', offering_list: 'c++')
TaggableModel.create(name: 'Frank', need_list: 'css', offering_list: 'css')
TaggableModel.create(name: 'Steve', need_list: 'c++', offering_list: 'java')
# Let's only find those who need rails or css and are offering c++ or java
expect(TaggableModel.tagged_with(['rails, css'], on: :needs, any: true).tagged_with(['c++', 'java'], on: :offerings, any: true).to_a).to eq([bob])
end
it 'should not return read-only records' do
TaggableModel.create(name: 'Bob', tag_list: 'ruby, rails, css')
expect(TaggableModel.tagged_with('ruby').first).to_not be_readonly
end
it 'should be able to get scoped tag counts' do
TaggableModel.create(name: 'Bob', tag_list: 'ruby, rails, css')
TaggableModel.create(name: 'Frank', tag_list: 'ruby, rails')
TaggableModel.create(name: 'Charlie', skill_list: 'ruby')
expect(TaggableModel.tagged_with('ruby').tag_counts(order: "#{ActsAsTaggableOn.tags_table}.id").first.count).to eq(2) # ruby
expect(TaggableModel.tagged_with('ruby').skill_counts.first.count).to eq(1) # ruby
end
it 'should be able to get all scoped tag counts' do
TaggableModel.create(name: 'Bob', tag_list: 'ruby, rails, css')
TaggableModel.create(name: 'Frank', tag_list: 'ruby, rails')
TaggableModel.create(name: 'Charlie', skill_list: 'ruby')
expect(TaggableModel.tagged_with('ruby').all_tag_counts(order: "#{ActsAsTaggableOn.tags_table}.id").first.count).to eq(3) # ruby
end
it 'should be able to get all scoped tags' do
TaggableModel.create(name: 'Bob', tag_list: 'ruby, rails, css')
TaggableModel.create(name: 'Frank', tag_list: 'ruby, rails')
TaggableModel.create(name: 'Charlie', skill_list: 'ruby')
expect(TaggableModel.tagged_with('ruby').all_tags(order: "#{ActsAsTaggableOn.tags_table}.id").first.name).to eq('ruby')
end
it 'should only return tag counts for the available scope' do
frank = TaggableModel.create(name: 'Frank', tag_list: 'ruby, rails')
TaggableModel.create(name: 'Bob', tag_list: 'ruby, rails, css')
TaggableModel.create(name: 'Charlie', skill_list: 'ruby, java')
expect(TaggableModel.tagged_with('rails').all_tag_counts.size).to eq(3)
expect(TaggableModel.tagged_with('rails').all_tag_counts.any? { |tag| tag.name == 'java' }).to be_falsy
# Test specific join syntaxes:
frank.untaggable_models.create!
expect(TaggableModel.tagged_with('rails').joins(:untaggable_models).all_tag_counts.size).to eq(2)
expect(TaggableModel.tagged_with('rails').joins([:untaggable_models]).all_tag_counts.size).to eq(2)
end
it 'should only return tags for the available scope' do
frank = TaggableModel.create(name: 'Frank', tag_list: 'ruby, rails')
TaggableModel.create(name: 'Bob', tag_list: 'ruby, rails, css')
TaggableModel.create(name: 'Charlie', skill_list: 'ruby, java')
expect(TaggableModel.tagged_with('rails').all_tags.count).to eq(3)
expect(TaggableModel.tagged_with('rails').all_tags.any? { |tag| tag.name == 'java' }).to be_falsy
# Test specific join syntaxes:
frank.untaggable_models.create!
expect(TaggableModel.tagged_with('rails').joins(:untaggable_models).all_tags.size).to eq(2)
expect(TaggableModel.tagged_with('rails').joins([:untaggable_models]).all_tags.size).to eq(2)
end
it 'should be able to set a custom tag context list' do
bob = TaggableModel.create(name: 'Bob')
bob.set_tag_list_on(:rotors, 'spinning, jumping')
expect(bob.tag_list_on(:rotors)).to eq(%w(spinning jumping))
bob.save
bob.reload
expect(bob.tags_on(:rotors)).to_not be_empty
end
it 'should be able to find tagged' do
bob = TaggableModel.create(name: 'Bob', tag_list: 'fitter, happier, more productive', skill_list: 'ruby, rails, css')
frank = TaggableModel.create(name: 'Frank', tag_list: 'weaker, depressed, inefficient', skill_list: 'ruby, rails, css')
steve = TaggableModel.create(name: 'Steve', tag_list: 'fitter, happier, more productive', skill_list: 'c++, java, ruby')
expect(TaggableModel.tagged_with('ruby', order: 'taggable_models.name').to_a).to eq([bob, frank, steve])
expect(TaggableModel.tagged_with('ruby, rails', order: 'taggable_models.name').to_a).to eq([bob, frank])
expect(TaggableModel.tagged_with(%w(ruby rails), order: 'taggable_models.name').to_a).to eq([bob, frank])
end
it 'should be able to find tagged with quotation marks' do
bob = TaggableModel.create(name: 'Bob', tag_list: "fitter, happier, more productive, 'I love the ,comma,'")
expect(TaggableModel.tagged_with("'I love the ,comma,'")).to include(bob)
end
it 'should be able to find tagged with invalid tags' do
bob = TaggableModel.create(name: 'Bob', tag_list: 'fitter, happier, more productive')
expect(TaggableModel.tagged_with('sad, happier')).to_not include(bob)
end
it 'should be able to find tagged with any tag' do
bob = TaggableModel.create(name: 'Bob', tag_list: 'fitter, happier, more productive', skill_list: 'ruby, rails, css')
frank = TaggableModel.create(name: 'Frank', tag_list: 'weaker, depressed, inefficient', skill_list: 'ruby, rails, css')
steve = TaggableModel.create(name: 'Steve', tag_list: 'fitter, happier, more productive', skill_list: 'c++, java, ruby')
expect(TaggableModel.tagged_with(%w(ruby java), order: 'taggable_models.name', any: true).to_a).to eq([bob, frank, steve])
expect(TaggableModel.tagged_with(%w(c++ fitter), order: 'taggable_models.name', any: true).to_a).to eq([bob, steve])
expect(TaggableModel.tagged_with(%w(depressed css), order: 'taggable_models.name', any: true).to_a).to eq([bob, frank])
end
it 'should be able to order by number of matching tags when matching any' do
bob = TaggableModel.create(name: 'Bob', tag_list: 'fitter, happier, more productive', skill_list: 'ruby, rails, css')
frank = TaggableModel.create(name: 'Frank', tag_list: 'weaker, depressed, inefficient', skill_list: 'ruby, rails, css')
steve = TaggableModel.create(name: 'Steve', tag_list: 'fitter, happier, more productive', skill_list: 'c++, java, ruby')
expect(TaggableModel.tagged_with(%w(ruby java), any: true, order_by_matching_tag_count: true, order: 'taggable_models.name').to_a).to eq([steve, bob, frank])
expect(TaggableModel.tagged_with(%w(c++ fitter), any: true, order_by_matching_tag_count: true, order: 'taggable_models.name').to_a).to eq([steve, bob])
expect(TaggableModel.tagged_with(%w(depressed css), any: true, order_by_matching_tag_count: true, order: 'taggable_models.name').to_a).to eq([frank, bob])
expect(TaggableModel.tagged_with(['fitter', 'happier', 'more productive', 'c++', 'java', 'ruby'], any: true, order_by_matching_tag_count: true, order: 'taggable_models.name').to_a).to eq([steve, bob, frank])
expect(TaggableModel.tagged_with(%w(c++ java ruby fitter), any: true, order_by_matching_tag_count: true, order: 'taggable_models.name').to_a).to eq([steve, bob, frank])
end
context 'wild: true' do
it 'should use params as wildcards' do
bob = TaggableModel.create(name: 'Bob', tag_list: 'bob, tricia')
frank = TaggableModel.create(name: 'Frank', tag_list: 'bobby, jim')
steve = TaggableModel.create(name: 'Steve', tag_list: 'john, patricia')
jim = TaggableModel.create(name: 'Jim', tag_list: 'jim, steve')
expect(TaggableModel.tagged_with(%w(bob tricia), wild: true, any: true).to_a.sort_by { |o| o.id }).to eq([bob, frank, steve])
expect(TaggableModel.tagged_with(%w(bob tricia), wild: :prefix, any: true).to_a.sort_by { |o| o.id }).to eq([bob, steve])
expect(TaggableModel.tagged_with(%w(bob tricia), wild: :suffix, any: true).to_a.sort_by { |o| o.id }).to eq([bob, frank])
expect(TaggableModel.tagged_with(%w(cia), wild: :prefix, any: true).to_a.sort_by { |o| o.id }).to eq([bob, steve])
expect(TaggableModel.tagged_with(%w(j), wild: :suffix, any: true).to_a.sort_by { |o| o.id }).to eq([frank, steve, jim])
expect(TaggableModel.tagged_with(%w(bob tricia), wild: true, exclude: true).to_a).to eq([jim])
expect(TaggableModel.tagged_with('ji', wild: true, any: true).to_a).to match_array([frank, jim])
end
end
it 'should be able to find tagged on a custom tag context' do
bob = TaggableModel.create(name: 'Bob')
bob.set_tag_list_on(:rotors, 'spinning, jumping')
expect(bob.tag_list_on(:rotors)).to eq(%w(spinning jumping))
bob.save
expect(TaggableModel.tagged_with('spinning', on: :rotors).to_a).to eq([bob])
end
it 'should be able to use named scopes to chain tag finds' do
bob = TaggableModel.create(name: 'Bob', tag_list: 'fitter, happier, more productive', skill_list: 'ruby, rails, css')
frank = TaggableModel.create(name: 'Frank', tag_list: 'weaker, depressed, inefficient', skill_list: 'ruby, rails, css')
steve = TaggableModel.create(name: 'Steve', tag_list: 'fitter, happier, more productive', skill_list: 'c++, java, python')
# Let's only find those productive Rails developers
expect(TaggableModel.tagged_with('rails', on: :skills, order: 'taggable_models.name').to_a).to eq([bob, frank])
expect(TaggableModel.tagged_with('happier', on: :tags, order: 'taggable_models.name').to_a).to eq([bob, steve])
expect(TaggableModel.tagged_with('rails', on: :skills).tagged_with('happier', on: :tags).to_a).to eq([bob])
expect(TaggableModel.tagged_with('rails').tagged_with('happier', on: :tags).to_a).to eq([bob])
end
it 'should be able to find tagged with only the matching tags' do
TaggableModel.create(name: 'Bob', tag_list: 'lazy, happier')
TaggableModel.create(name: 'Frank', tag_list: 'fitter, happier, inefficient')
steve = TaggableModel.create(name: 'Steve', tag_list: 'fitter, happier')
expect(TaggableModel.tagged_with('fitter, happier', match_all: true).to_a).to eq([steve])
end
it 'should be able to find tagged with only the matching tags for a context' do
TaggableModel.create(name: 'Bob', tag_list: 'lazy, happier', skill_list: 'ruby, rails, css')
frank = TaggableModel.create(name: 'Frank', tag_list: 'fitter, happier, inefficient', skill_list: 'css')
TaggableModel.create(name: 'Steve', tag_list: 'fitter, happier', skill_list: 'ruby, rails, css')
expect(TaggableModel.tagged_with('css', on: :skills, match_all: true).to_a).to eq([frank])
end
it 'should be able to find tagged with some excluded tags' do
TaggableModel.create(name: 'Bob', tag_list: 'happier, lazy')
frank = TaggableModel.create(name: 'Frank', tag_list: 'happier')
steve = TaggableModel.create(name: 'Steve', tag_list: 'happier')
expect(TaggableModel.tagged_with('lazy', exclude: true)).to include(frank, steve)
expect(TaggableModel.tagged_with('lazy', exclude: true).size).to eq(2)
end
it 'should return an empty scope for empty tags' do
['', ' ', nil, []].each do |tag|
expect(TaggableModel.tagged_with(tag)).to be_empty
end
end
it 'should options key not be deleted' do
options = {:exclude => true}
TaggableModel.tagged_with("foo", options)
expect(options).to eq({:exclude => true})
end
it 'should not delete tags if not updated' do
model = TaggableModel.create(name: 'foo', tag_list: 'ruby, rails, programming')
model.update(name: 'bar')
model.reload
expect(model.tag_list.sort).to eq(%w(ruby rails programming).sort)
end
context 'Duplicates' do
context 'should not create duplicate taggings' do
let(:bob) { TaggableModel.create(name: 'Bob') }
context 'case sensitive' do
it '#add' do
expect {
bob.tag_list.add 'happier'
bob.tag_list.add 'happier'
bob.tag_list.add 'happier', 'rich', 'funny'
bob.save
}.to change(ActsAsTaggableOn::Tagging, :count).by(3)
end
it '#<<' do
expect {
bob.tag_list << 'social'
bob.tag_list << 'social'
bob.tag_list << 'social' << 'wow'
bob.save
}.to change(ActsAsTaggableOn::Tagging, :count).by(2)
end
it 'unicode' do
expect {
bob.tag_list.add 'ПРИВЕТ'
bob.tag_list.add 'ПРИВЕТ'
bob.tag_list.add 'ПРИВЕТ', 'ПРИВЕТ'
bob.save
}.to change(ActsAsTaggableOn::Tagging, :count).by(1)
end
it '#=' do
expect {
bob.tag_list = ['Happy', 'Happy']
bob.save
}.to change(ActsAsTaggableOn::Tagging, :count).by(1)
end
end
context 'case insensitive' do
before(:all) { ActsAsTaggableOn.force_lowercase = true }
after(:all) { ActsAsTaggableOn.force_lowercase = false }
it '#<<' do
expect {
bob.tag_list << 'Alone'
bob.tag_list << 'AloNe'
bob.tag_list << 'ALONE' << 'In The dark'
bob.save
}.to change(ActsAsTaggableOn::Tagging, :count).by(2)
end
it '#add' do
expect {
bob.tag_list.add 'forever'
bob.tag_list.add 'ForEver'
bob.tag_list.add 'FOREVER', 'ALONE'
bob.save
}.to change(ActsAsTaggableOn::Tagging, :count).by(2)
end
it 'unicode' do
expect {
bob.tag_list.add 'ПРИВЕТ'
bob.tag_list.add 'привет', 'Привет'
bob.save
}.to change(ActsAsTaggableOn::Tagging, :count).by(1)
end
it '#=' do
expect {
bob.tag_list = ['Happy', 'HAPPY']
bob.save
}.to change(ActsAsTaggableOn::Tagging, :count).by(1)
end
end
end
it 'should not duplicate tags' do
connor = TaggableModel.new(name: 'Connor', tag_list: 'There, can, be, only, one')
allow(ActsAsTaggableOn::Tag).to receive(:create).and_call_original
expect(ActsAsTaggableOn::Tag).to receive(:create).with(name: 'can') do
# Simulate concurrent tag creation
ActsAsTaggableOn::Tag.new(name: 'can').save!
raise ActiveRecord::RecordNotUnique
end
expect(ActsAsTaggableOn::Tag).to receive(:create).with(name: 'be') do
# Simulate concurrent tag creation
ActsAsTaggableOn::Tag.new(name: 'be').save!
raise ActiveRecord::RecordNotUnique
end
expect { connor.save! }.to change(ActsAsTaggableOn::Tag, :count).by(5)
%w[There can only be one].each do |tag|
expect(TaggableModel.tagged_with(tag).count).to eq(1)
end
end
end
describe 'Associations' do
before(:each) do
@taggable = TaggableModel.create(tag_list: 'awesome, epic')
end
it 'should not remove tags when creating associated objects' do
@taggable.untaggable_models.create!
@taggable.reload
expect(@taggable.tag_list.size).to eq(2)
end
end
describe 'grouped_column_names_for method' do
it 'should return all column names joined for Tag GROUP clause' do
# 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("#{ActsAsTaggableOn.tags_table}.id, #{ActsAsTaggableOn.tags_table}.name, #{ActsAsTaggableOn.tags_table}.taggings_count, #{ActsAsTaggableOn.tags_table}.type")
end
it 'should return all column names joined for TaggableModel GROUP clause' do
expect(@taggable.grouped_column_names_for(TaggableModel)).to eq('taggable_models.id, taggable_models.name, taggable_models.type, taggable_models.tenant_id')
end
it 'should return all column names joined for NonStandardIdTaggableModel GROUP clause' do
expect(@taggable.grouped_column_names_for(TaggableModel)).to eq("taggable_models.#{TaggableModel.primary_key}, taggable_models.name, taggable_models.type, taggable_models.tenant_id")
end
end
describe 'NonStandardIdTaggable' do
before(:each) do
@taggable = NonStandardIdTaggableModel.new(name: 'Bob Jones')
@taggables = [@taggable, NonStandardIdTaggableModel.new(name: 'John Doe')]
end
it 'should have tag types' do
[:tags, :languages, :skills, :needs, :offerings].each do |type|
expect(NonStandardIdTaggableModel.tag_types).to include type
end
expect(@taggable.tag_types).to eq(NonStandardIdTaggableModel.tag_types)
end
it 'should have tag_counts_on' do
expect(NonStandardIdTaggableModel.tag_counts_on(:tags)).to be_empty
@taggable.tag_list = %w(awesome epic)
@taggable.save
expect(NonStandardIdTaggableModel.tag_counts_on(:tags).length).to eq(2)
expect(@taggable.tag_counts_on(:tags).length).to eq(2)
end
it 'should have tags_on' do
expect(NonStandardIdTaggableModel.tags_on(:tags)).to be_empty
@taggable.tag_list = %w(awesome epic)
@taggable.save
expect(NonStandardIdTaggableModel.tags_on(:tags).length).to eq(2)
expect(@taggable.tags_on(:tags).length).to eq(2)
end
it 'should be able to create tags' do
@taggable.skill_list = 'ruby, rails, css'
expect(@taggable.instance_variable_get('@skill_list').instance_of?(ActsAsTaggableOn::TagList)).to be_truthy
expect {
@taggable.save
}.to change(ActsAsTaggableOn::Tag, :count).by(3)
@taggable.reload
expect(@taggable.skill_list.sort).to eq(%w(ruby rails css).sort)
end
it 'should be able to create tags through the tag list directly' do
@taggable.tag_list_on(:test).add('hello')
expect(@taggable.tag_list_cache_on(:test)).to_not be_empty
expect(@taggable.tag_list_on(:test)).to eq(['hello'])
@taggable.save
@taggable.save_tags
@taggable.reload
expect(@taggable.tag_list_on(:test)).to eq(['hello'])
end
end
describe 'Autogenerated methods' do
it 'should be overridable' do
expect(TaggableModel.create(tag_list: 'woo').tag_list_submethod_called).to be_truthy
end
end
# See https://github.com/mbleigh/acts-as-taggable-on/pull/457 for details
context 'tag_counts and aggreating scopes, compatibility with MySQL ' do
before(:each) do
TaggableModel.new(:name => 'Barb Jones').tap { |t| t.tag_list = %w(awesome fun) }.save
TaggableModel.new(:name => 'John Doe').tap { |t| t.tag_list = %w(cool fun hella) }.save
TaggableModel.new(:name => 'Jo Doe').tap { |t| t.tag_list = %w(curious young naive sharp) }.save
TaggableModel.all.each { |t| t.save }
end
context 'Model.limit(x).tag_counts.sum(:tags_count)' do
it 'should not break on Mysql' do
expect(TaggableModel.limit(2).tag_counts.sum('tags_count').to_i).to eq(5)
end
end
context 'regression prevention, just making sure these esoteric queries still work' do
context 'Model.tag_counts.limit(x)' do
it 'should limit the tag objects (not very useful, of course)' do
array_of_tag_counts = TaggableModel.tag_counts.limit(2)
expect(array_of_tag_counts.count).to eq(2)
end
end
context 'Model.tag_counts.sum(:tags_count)' do
it 'should limit the total tags used' do
expect(TaggableModel.tag_counts.sum(:tags_count).to_i).to eq(9)
end
end
context 'Model.tag_counts.limit(2).sum(:tags_count)' do
it 'limit should have no effect; this is just a sanity check' do
expect(TaggableModel.tag_counts.limit(2).sum(:tags_count).to_i).to eq(9)
end
end
end
end
end
describe 'Taggable model with json columns', if: postgresql_support_json? do
before(:each) do
@taggable = TaggableModelWithJson.new(:name => 'Bob Jones')
@taggables = [@taggable, TaggableModelWithJson.new(:name => 'John Doe')]
end
it 'should be able to find by tag with context' do
@taggable.skill_list = 'ruby, rails, css'
@taggable.tag_list = 'bob, charlie'
@taggable.save
expect(TaggableModelWithJson.tagged_with('ruby').first).to eq(@taggable)
expect(TaggableModelWithJson.tagged_with('ruby, css').first).to eq(@taggable)
expect(TaggableModelWithJson.tagged_with('bob', :on => :skills).first).to_not eq(@taggable)
expect(TaggableModelWithJson.tagged_with('bob', :on => :tags).first).to eq(@taggable)
end
it 'should be able to find tagged with any tag' do
bob = TaggableModelWithJson.create(:name => 'Bob', :tag_list => 'fitter, happier, more productive', :skill_list => 'ruby, rails, css')
frank = TaggableModelWithJson.create(:name => 'Frank', :tag_list => 'weaker, depressed, inefficient', :skill_list => 'ruby, rails, css')
steve = TaggableModelWithJson.create(:name => 'Steve', :tag_list => 'fitter, happier, more productive', :skill_list => 'c++, java, ruby')
expect(TaggableModelWithJson.tagged_with(%w(ruby java), :order => 'taggable_model_with_jsons.name', :any => true).to_a).to eq([bob, frank, steve])
expect(TaggableModelWithJson.tagged_with(%w(c++ fitter), :order => 'taggable_model_with_jsons.name', :any => true).to_a).to eq([bob, steve])
expect(TaggableModelWithJson.tagged_with(%w(depressed css), :order => 'taggable_model_with_jsons.name', :any => true).to_a).to eq([bob, frank])
end
end
acts-as-taggable-on-11.0.0/spec/acts_as_taggable_on/dirty_spec.rb 0000644 0000041 0000041 00000010205 14704600021 024705 0 ustar www-data www-data # -*- encoding : utf-8 -*-
require 'spec_helper'
describe 'Dirty behavior of taggable objects' do
context 'with un-contexted tags' do
before(:each) do
@taggable = TaggableModel.create(tag_list: 'awesome, epic')
end
context 'when tag_list changed' do
before(:each) do
expect(@taggable.changes).to be_empty
@taggable.tag_list = 'one'
end
it 'should show changes of dirty object' do
expect(@taggable.changes).to eq({'tag_list' => [['awesome', 'epic'], ['one']]})
end
it 'should show changes of freshly initialized dirty object' do
taggable = TaggableModel.find(@taggable.id)
taggable.tag_list = 'one'
expect(taggable.changes).to eq({'tag_list' => [['awesome', 'epic'], ['one']]})
end
if Rails.version >= "5.1"
it 'flags tag_list as changed' do
expect(@taggable.will_save_change_to_tag_list?).to be_truthy
end
end
it 'preserves original value' do
expect(@taggable.tag_list_was).to eq(['awesome', 'epic'])
end
it 'shows what the change was' do
expect(@taggable.tag_list_change).to eq([['awesome', 'epic'], ['one']])
end
context 'without order' do
it 'should not mark attribute if order change ' do
taggable = TaggableModel.create(name: 'Dirty Harry', tag_list: %w(d c b a))
taggable.tag_list = %w(a b c d)
expect(taggable.tag_list_changed?).to be_falsey
end
end
context 'with order' do
it 'should mark attribute if order change' do
taggable = OrderedTaggableModel.create(name: 'Clean Harry', tag_list: 'd,c,b,a')
taggable.save
taggable.tag_list = %w(a b c d)
expect(taggable.tag_list_changed?).to be_truthy
end
end
end
context 'when tag_list is the same' do
before(:each) do
@taggable.tag_list = 'awesome, epic'
end
it 'is not flagged as changed' do
expect(@taggable.tag_list_changed?).to be_falsy
end
it 'does not show any changes to the taggable item' do
expect(@taggable.changes).to be_empty
end
context "and using a delimiter different from a ','" do
before do
@old_delimiter = ActsAsTaggableOn.delimiter
ActsAsTaggableOn.delimiter = ';'
end
after do
ActsAsTaggableOn.delimiter = @old_delimiter
end
it 'does not show any changes to the taggable item when using array assignments' do
@taggable.tag_list = %w(awesome epic)
expect(@taggable.changes).to be_empty
end
end
end
end
context 'with context tags' do
before(:each) do
@taggable = TaggableModel.create('language_list' => 'awesome, epic')
end
context 'when language_list changed' do
before(:each) do
expect(@taggable.changes).to be_empty
@taggable.language_list = 'one'
end
it 'should show changes of dirty object' do
expect(@taggable.changes).to eq({'language_list' => [['awesome', 'epic'], ['one']]})
end
it 'flags language_list as changed' do
expect(@taggable.language_list_changed?).to be_truthy
end
it 'preserves original value' do
expect(@taggable.language_list_was).to eq(['awesome', 'epic'])
end
it 'shows what the change was' do
expect(@taggable.language_list_change).to eq([['awesome', 'epic'], ['one']])
end
end
context 'when language_list is the same' do
before(:each) do
@taggable.language_list = 'awesome, epic'
end
it 'is not flagged as changed' do
expect(@taggable.language_list_changed?).to be_falsy
end
it 'does not show any changes to the taggable item' do
expect(@taggable.changes).to be_empty
end
end
context 'when language_list changed by association' do
let(:tag) { ActsAsTaggableOn::Tag.new(name: 'one') }
it 'flags language_list as changed' do
expect(@taggable.changes).to be_empty
@taggable.languages << tag
expect(@taggable.language_list_changed?).to be_truthy
end
end
end
end
acts-as-taggable-on-11.0.0/spec/acts_as_taggable_on/caching_spec.rb 0000644 0000041 0000041 00000010032 14704600021 025144 0 ustar www-data www-data # -*- encoding : utf-8 -*-
require 'spec_helper'
describe 'Acts As Taggable On' do
describe 'Caching' do
before(:each) do
@taggable = CachedModel.new(name: 'Bob Jones')
@another_taggable = OtherCachedModel.new(name: 'John Smith')
end
it 'should add saving of tag lists and cached tag lists to the instance' do
expect(@taggable).to respond_to(:save_cached_tag_list)
expect(@another_taggable).to respond_to(:save_cached_tag_list)
expect(@taggable).to respond_to(:save_tags)
end
it 'should generate a cached column checker for each tag type' do
expect(CachedModel).to respond_to(:caching_tag_list?)
expect(OtherCachedModel).to respond_to(:caching_language_list?)
end
it 'should not have cached tags' do
expect(@taggable.cached_tag_list).to be_blank
expect(@another_taggable.cached_language_list).to be_blank
end
it 'should cache tags' do
@taggable.update(tag_list: 'awesome, epic')
expect(@taggable.cached_tag_list).to eq('awesome, epic')
@another_taggable.update(language_list: 'ruby, .net')
expect(@another_taggable.cached_language_list).to eq('ruby, .net')
end
it 'should keep the cache' do
@taggable.update(tag_list: 'awesome, epic')
@taggable = CachedModel.find(@taggable.id)
@taggable.save!
expect(@taggable.cached_tag_list).to eq('awesome, epic')
end
it 'should update the cache' do
@taggable.update(tag_list: 'awesome, epic')
@taggable.update(tag_list: 'awesome')
expect(@taggable.cached_tag_list).to eq('awesome')
end
it 'should remove the cache' do
@taggable.update(tag_list: 'awesome, epic')
@taggable.update(tag_list: '')
expect(@taggable.cached_tag_list).to be_blank
end
it 'should have a tag list' do
@taggable.update(tag_list: 'awesome, epic')
@taggable = CachedModel.find(@taggable.id)
expect(@taggable.tag_list.sort).to eq(%w(awesome epic).sort)
end
it 'should keep the tag list' do
@taggable.update(tag_list: 'awesome, epic')
@taggable = CachedModel.find(@taggable.id)
@taggable.save!
expect(@taggable.tag_list.sort).to eq(%w(awesome epic).sort)
end
it 'should clear the cache on reset_column_information' do
CachedModel.column_names
CachedModel.reset_column_information
expect(CachedModel.instance_variable_get(:@acts_as_taggable_on_cache_columns)).to eql(nil)
end
it 'should not override a user-defined columns method' do
expect(ColumnsOverrideModel.columns.map(&:name)).not_to include('ignored_column')
ColumnsOverrideModel.acts_as_taggable
expect(ColumnsOverrideModel.columns.map(&:name)).not_to include('ignored_column')
end
end
describe 'with a custom delimiter' do
before(:each) do
@taggable = CachedModel.new(name: 'Bob Jones')
@another_taggable = OtherCachedModel.new(name: 'John Smith')
ActsAsTaggableOn.delimiter = ';'
end
after(:all) do
ActsAsTaggableOn.delimiter = ','
end
it 'should cache tags with custom delimiter' do
@taggable.update(tag_list: 'awesome; epic')
expect(@taggable.tag_list).to eq(['awesome', 'epic'])
expect(@taggable.cached_tag_list).to eq('awesome; epic')
@taggable = CachedModel.find_by_name('Bob Jones')
expect(@taggable.tag_list).to eq(['awesome', 'epic'])
expect(@taggable.cached_tag_list).to eq('awesome; epic')
end
end
describe 'Cache methods initialization on new models' do
before(:all) do
ActiveRecord::Base.connection.execute(
'INSERT INTO cache_methods_injected_models (cached_tag_list) VALUES (\'ciao\')'
)
class CacheMethodsInjectedModel < ActiveRecord::Base
acts_as_taggable
end
end
after(:all) { Object.send(:remove_const, :CacheMethodsInjectedModel) }
it 'cached_tag_list_on? get injected correctly' do
expect do
CacheMethodsInjectedModel.first.tag_list
end.not_to raise_error
end
end
describe 'CachingWithArray' do
pending '#TODO'
end
end
acts-as-taggable-on-11.0.0/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb 0000644 0000041 0000041 00000022767 14704600021 027531 0 ustar www-data www-data # -*- encoding : utf-8 -*-
require 'spec_helper'
describe 'Acts As Taggable On' do
it "should provide a class method 'taggable?' that is false for untaggable models" do
expect(UntaggableModel).to_not be_taggable
end
describe 'Taggable Method Generation To Preserve Order' do
before(:each) do
TaggableModel.tag_types = []
TaggableModel.preserve_tag_order = false
TaggableModel.acts_as_ordered_taggable_on(:ordered_tags)
@taggable = TaggableModel.new(name: 'Bob Jones')
end
it "should respond 'true' to preserve_tag_order?" do
expect(@taggable.class.preserve_tag_order?).to be_truthy
end
end
describe 'Taggable Method Generation' do
before(:each) do
TaggableModel.tag_types = []
TaggableModel.acts_as_taggable_on(:tags, :languages, :skills, :needs, :offerings)
@taggable = TaggableModel.new(name: 'Bob Jones')
end
it "should respond 'true' to taggable?" do
expect(@taggable.class).to be_taggable
end
it 'should create a class attribute for tag types' do
expect(@taggable.class).to respond_to(:tag_types)
end
it 'should create an instance attribute for tag types' do
expect(@taggable).to respond_to(:tag_types)
end
it 'should have all tag types' do
expect(@taggable.tag_types).to eq([:tags, :languages, :skills, :needs, :offerings])
end
it 'should create a class attribute for preserve tag order' do
expect(@taggable.class).to respond_to(:preserve_tag_order?)
end
it 'should create an instance attribute for preserve tag order' do
expect(@taggable).to respond_to(:preserve_tag_order?)
end
it "should respond 'false' to preserve_tag_order?" do
expect(@taggable.class.preserve_tag_order?).to be_falsy
end
it 'should generate an association for each tag type' do
expect(@taggable).to respond_to(:tags, :skills, :languages)
end
it 'should add tagged_with and tag_counts to singleton' do
expect(TaggableModel).to respond_to(:tagged_with, :tag_counts)
end
it 'should generate a tag_list accessor/setter for each tag type' do
expect(@taggable).to respond_to(:tag_list, :skill_list, :language_list)
expect(@taggable).to respond_to(:tag_list=, :skill_list=, :language_list=)
end
it 'should generate a tag_list accessor, that includes owned tags, for each tag type' do
expect(@taggable).to respond_to(:all_tags_list, :all_skills_list, :all_languages_list)
end
end
describe 'Matching Contexts' do
it 'should find objects with tags of matching contexts' do
taggable1 = TaggableModel.create!(name: 'Taggable 1')
taggable2 = TaggableModel.create!(name: 'Taggable 2')
taggable3 = TaggableModel.create!(name: 'Taggable 3')
taggable1.offering_list = 'one, two'
taggable1.save!
taggable2.need_list = 'one, two'
taggable2.save!
taggable3.offering_list = 'one, two'
taggable3.save!
expect(taggable1.find_matching_contexts(:offerings, :needs)).to include(taggable2)
expect(taggable1.find_matching_contexts(:offerings, :needs)).to_not include(taggable3)
end
it 'should find other related objects with tags of matching contexts' do
taggable1 = TaggableModel.create!(name: 'Taggable 1')
taggable2 = OtherTaggableModel.create!(name: 'Taggable 2')
taggable3 = OtherTaggableModel.create!(name: 'Taggable 3')
taggable1.offering_list = 'one, two'
taggable1.save
taggable2.need_list = 'one, two'
taggable2.save
taggable3.offering_list = 'one, two'
taggable3.save
expect(taggable1.find_matching_contexts_for(OtherTaggableModel, :offerings, :needs)).to include(taggable2)
expect(taggable1.find_matching_contexts_for(OtherTaggableModel, :offerings, :needs)).to_not include(taggable3)
end
it 'should not include the object itself in the list of related objects with tags of matching contexts' do
taggable1 = TaggableModel.create!(name: 'Taggable 1')
taggable2 = TaggableModel.create!(name: 'Taggable 2')
taggable1.offering_list = 'one, two'
taggable1.need_list = 'one, two'
taggable1.save
taggable2.need_list = 'one, two'
taggable2.save
expect(taggable1.find_matching_contexts_for(TaggableModel, :offerings, :needs)).to include(taggable2)
expect(taggable1.find_matching_contexts_for(TaggableModel, :offerings, :needs)).to_not include(taggable1)
end
it 'should ensure joins to multiple taggings maintain their contexts when aliasing' do
taggable1 = TaggableModel.create!(name: 'Taggable 1')
taggable1.offering_list = 'one'
taggable1.need_list = 'two'
taggable1.save
column = TaggableModel.connection.quote_column_name("context")
offer_alias = TaggableModel.connection.quote_table_name(ActsAsTaggableOn.taggings_table)
need_alias = TaggableModel.connection.quote_table_name("need_taggings_taggable_models_join")
expect(TaggableModel.joins(:offerings, :needs).to_sql).to include "#{offer_alias}.#{column}"
expect(TaggableModel.joins(:offerings, :needs).to_sql).to include "#{need_alias}.#{column}"
end
end
describe 'Tagging Contexts' do
it 'should eliminate duplicate tagging contexts ' do
TaggableModel.acts_as_taggable_on(:skills, :skills)
expect(TaggableModel.tag_types.freq[:skills]).to eq(1)
end
it 'should not contain embedded/nested arrays' do
TaggableModel.acts_as_taggable_on([:array], [:array])
expect(TaggableModel.tag_types.freq[[:array]]).to eq(0)
end
it 'should _flatten_ the content of arrays' do
TaggableModel.acts_as_taggable_on([:array], [:array])
expect(TaggableModel.tag_types.freq[:array]).to eq(1)
end
it 'should not raise an error when passed nil' do
expect(-> {
TaggableModel.acts_as_taggable_on
}).to_not raise_error
end
it 'should not raise an error when passed [nil]' do
expect(-> {
TaggableModel.acts_as_taggable_on([nil])
}).to_not raise_error
end
it 'should include dynamic contexts in tagging_contexts' do
taggable = TaggableModel.create!(name: 'Dynamic Taggable')
taggable.set_tag_list_on :colors, 'tag1, tag2, tag3'
expect(taggable.tagging_contexts).to eq(%w(tags languages skills needs offerings array colors))
taggable.save
taggable = TaggableModel.where(name: 'Dynamic Taggable').first
expect(taggable.tagging_contexts).to eq(%w(tags languages skills needs offerings array colors))
end
end
context 'when tagging context ends in an "s" when singular (ex. "status", "glass", etc.)' do
describe 'caching' do
before { @taggable = OtherCachedModel.new(name: 'John Smith') }
subject { @taggable }
it { should respond_to(:save_cached_tag_list) }
its(:cached_language_list) { should be_blank }
its(:cached_status_list) { should be_blank }
its(:cached_glass_list) { should be_blank }
context 'language taggings cache after update' do
before { @taggable.update(language_list: 'ruby, .net') }
subject { @taggable }
its(:language_list) { should == ['ruby', '.net']}
its(:cached_language_list) { should == 'ruby, .net' } # passes
its(:instance_variables) { should include(:@language_list) }
end
context 'status taggings cache after update' do
before { @taggable.update(status_list: 'happy, married') }
subject { @taggable }
its(:status_list) { should == ['happy', 'married'] }
its(:cached_status_list) { should == 'happy, married' } # fails
its(:cached_status_list) { should_not == '' } # fails, is blank
its(:instance_variables) { should include(:@status_list) }
its(:instance_variables) { should_not include(:@statu_list) } # fails, note: one "s"
end
context 'glass taggings cache after update' do
before do
@taggable.update(glass_list: 'rectangle, aviator')
end
subject { @taggable }
its(:glass_list) { should == ['rectangle', 'aviator'] }
its(:cached_glass_list) { should == 'rectangle, aviator' } # fails
its(:cached_glass_list) { should_not == '' } # fails, is blank
its(:instance_variables) { should include(:@glass_list) }
its(:instance_variables) { should_not include(:@glas_list) } # fails, note: one "s"
end
end
end
describe 'taggings' do
before(:each) do
@taggable = TaggableModel.new(name: 'Art Kram')
end
it 'should return no taggings' do
expect(@taggable.taggings).to be_empty
end
end
describe '@@remove_unused_tags' do
before do
@taggable = TaggableModel.create(name: 'Bob Jones')
@tag = ActsAsTaggableOn::Tag.create(name: 'awesome')
@tagging = ActsAsTaggableOn::Tagging.create(taggable: @taggable, tag: @tag, context: 'tags')
end
context 'if set to true' do
before do
ActsAsTaggableOn.remove_unused_tags = true
end
it 'should remove unused tags after removing taggings' do
@tagging.destroy
expect(ActsAsTaggableOn::Tag.find_by_name('awesome')).to be_nil
end
end
context 'if set to false' do
before do
ActsAsTaggableOn.remove_unused_tags = false
end
it 'should not remove unused tags after removing taggings' do
@tagging.destroy
expect(ActsAsTaggableOn::Tag.find_by_name('awesome')).to eq(@tag)
end
end
end
end
acts-as-taggable-on-11.0.0/spec/acts_as_taggable_on/acts_as_tagger_spec.rb 0000644 0000041 0000041 00000007374 14704600021 026535 0 ustar www-data www-data # -*- encoding : utf-8 -*-
require 'spec_helper'
describe 'acts_as_tagger' do
describe 'Tagger Method Generation' do
before(:each) do
@tagger = User.new
end
it 'should add #is_tagger? query method to the class-side' do
expect(User).to respond_to(:is_tagger?)
end
it 'should return true from the class-side #is_tagger?' do
expect(User.is_tagger?).to be_truthy
end
it 'should return false from the base #is_tagger?' do
expect(ActiveRecord::Base.is_tagger?).to be_falsy
end
it 'should add #is_tagger? query method to the singleton' do
expect(@tagger).to respond_to(:is_tagger?)
end
it 'should add #tag method on the instance-side' do
expect(@tagger).to respond_to(:tag)
end
it 'should generate an association for #owned_taggings and #owned_tags' do
expect(@tagger).to respond_to(:owned_taggings, :owned_tags)
end
end
describe '#tag' do
context 'when called with a non-existent tag context' do
before(:each) do
@tagger = User.new
@taggable = TaggableModel.new(name: 'Richard Prior')
end
it 'should by default not throw an exception ' do
expect(@taggable.tag_list_on(:foo)).to be_empty
expect(-> {
@tagger.tag(@taggable, with: 'this, and, that', on: :foo)
}).to_not raise_error
end
it 'should by default create the tag context on-the-fly' do
expect(@taggable.tag_list_on(:here_ond_now)).to be_empty
@tagger.tag(@taggable, with: 'that', on: :here_ond_now)
expect(@taggable.tag_list_on(:here_ond_now)).to_not include('that')
expect(@taggable.all_tags_list_on(:here_ond_now)).to include('that')
end
it 'should show all the tag list when both public and owned tags exist' do
@taggable.tag_list = 'ruby, python'
@tagger.tag(@taggable, with: 'java, lisp', on: :tags)
expect(@taggable.all_tags_on(:tags).map(&:name).sort).to eq(%w(ruby python java lisp).sort)
end
it 'should not add owned tags to the common list' do
@taggable.tag_list = 'ruby, python'
@tagger.tag(@taggable, with: 'java, lisp', on: :tags)
expect(@taggable.tag_list).to eq(%w(ruby python))
@tagger.tag(@taggable, with: '', on: :tags)
expect(@taggable.tag_list).to eq(%w(ruby python))
end
it 'should throw an exception when the default is over-ridden' do
expect(@taggable.tag_list_on(:foo_boo)).to be_empty
expect {
@tagger.tag(@taggable, with: 'this, and, that', on: :foo_boo, force: false)
}.to raise_error(RuntimeError)
end
it 'should not create the tag context on-the-fly when the default is over-ridden' do
expect(@taggable.tag_list_on(:foo_boo)).to be_empty
@tagger.tag(@taggable, with: 'this, and, that', on: :foo_boo, force: false) rescue
expect(@taggable.tag_list_on(:foo_boo)).to be_empty
end
end
describe "when called by multiple tagger's" do
before(:each) do
@user_x = User.create(name: 'User X')
@user_y = User.create(name: 'User Y')
@taggable = TaggableModel.create(name: 'acts_as_taggable_on', tag_list: 'plugin')
@user_x.tag(@taggable, with: 'ruby, rails', on: :tags)
@user_y.tag(@taggable, with: 'ruby, plugin', on: :tags)
@user_y.tag(@taggable, with: '', on: :tags)
@user_y.tag(@taggable, with: '', on: :tags)
end
it 'should delete owned tags' do
expect(@user_y.owned_tags).to be_empty
end
it 'should not delete other taggers tags' do
expect(@user_x.owned_tags.count).to eq(2)
end
it 'should not delete original tags' do
expect(@taggable.all_tags_list_on(:tags)).to include('plugin')
end
end
end
end
acts-as-taggable-on-11.0.0/spec/acts_as_taggable_on/related_spec.rb 0000644 0000041 0000041 00000010453 14704600021 025177 0 ustar www-data www-data # -*- encoding : utf-8 -*-
require 'spec_helper'
describe 'Acts As Taggable On' do
describe 'Related Objects' do
#TODO, shared example
it 'should find related objects based on tag names on context' do
taggable1 = TaggableModel.create!(name: 'Taggable 1',tag_list: 'one, two')
taggable2 = TaggableModel.create!(name: 'Taggable 2',tag_list: 'three, four')
taggable3 = TaggableModel.create!(name: 'Taggable 3',tag_list: 'one, four')
expect(taggable1.find_related_tags).to include(taggable3)
expect(taggable1.find_related_tags).to_not include(taggable2)
end
it 'finds related tags for ordered taggable on' do
taggable1 = OrderedTaggableModel.create!(name: 'Taggable 1',colour_list: 'one, two')
taggable2 = OrderedTaggableModel.create!(name: 'Taggable 2',colour_list: 'three, four')
taggable3 = OrderedTaggableModel.create!(name: 'Taggable 3',colour_list: 'one, four')
expect(taggable1.find_related_colours).to include(taggable3)
expect(taggable1.find_related_colours).to_not include(taggable2)
end
it 'should find related objects based on tag names on context - non standard id' do
taggable1 = NonStandardIdTaggableModel.create!(name: 'Taggable 1',tag_list: 'one, two')
taggable2 = NonStandardIdTaggableModel.create!(name: 'Taggable 2',tag_list: 'three, four')
taggable3 = NonStandardIdTaggableModel.create!(name: 'Taggable 3',tag_list: 'one, four')
expect(taggable1.find_related_tags).to include(taggable3)
expect(taggable1.find_related_tags).to_not include(taggable2)
end
it 'should find other related objects based on tag names on context' do
taggable1 = TaggableModel.create!(name: 'Taggable 1',tag_list: 'one, two')
taggable2 = OtherTaggableModel.create!(name: 'Taggable 2',tag_list: 'three, four')
taggable3 = OtherTaggableModel.create!(name: 'Taggable 3',tag_list: 'one, four')
expect(taggable1.find_related_tags_for(OtherTaggableModel)).to include(taggable3)
expect(taggable1.find_related_tags_for(OtherTaggableModel)).to_not include(taggable2)
end
it 'should find other related objects based on tags only from particular context' do
taggable1 = TaggableModel.create!(name: 'Taggable 1',tag_list: 'one, two')
taggable2 = TaggableModel.create!(name: 'Taggable 2',tag_list: 'three, four', skill_list: 'one, two')
taggable3 = TaggableModel.create!(name: 'Taggable 3',tag_list: 'one, four')
expect(taggable1.find_related_tags).to include(taggable3)
expect(taggable1.find_related_tags).to_not include(taggable2)
end
shared_examples "a collection" do
it do
taggable1 = described_class.create!(name: 'Taggable 1', tag_list: 'one')
taggable2 = described_class.create!(name: 'Taggable 2', tag_list: 'one, two')
expect(taggable1.find_related_tags).to include(taggable2)
expect(taggable1.find_related_tags).to_not include(taggable1)
end
end
# it 'should not include the object itself in the list of related objects' do
describe TaggableModel do
it_behaves_like "a collection"
end
# it 'should not include the object itself in the list of related objects - non standard id' do
describe NonStandardIdTaggableModel do
it_behaves_like "a collection"
end
context 'Ignored Tags' do
let(:taggable1) { TaggableModel.create!(name: 'Taggable 1', tag_list: 'one, two, four') }
let(:taggable2) { TaggableModel.create!(name: 'Taggable 2', tag_list: 'two, three') }
let(:taggable3) { TaggableModel.create!(name: 'Taggable 3', tag_list: 'one, three') }
it 'should not include ignored tags in related search' do
expect(taggable1.find_related_tags(ignore: 'two')).to_not include(taggable2)
expect(taggable1.find_related_tags(ignore: 'two')).to include(taggable3)
end
it 'should accept array of ignored tags' do
taggable4 = TaggableModel.create!(name: 'Taggable 4', tag_list: 'four')
expect(taggable1.find_related_tags(ignore: ['two', 'four'])).to_not include(taggable2)
expect(taggable1.find_related_tags(ignore: ['two', 'four'])).to_not include(taggable4)
end
it 'should accept symbols as ignored tags' do
expect(taggable1.find_related_tags(ignore: :two)).to_not include(taggable2)
end
end
end
end
acts-as-taggable-on-11.0.0/spec/acts_as_taggable_on/generic_parser_spec.rb 0000644 0000041 0000041 00000000704 14704600021 026545 0 ustar www-data www-data # -*- encoding : utf-8 -*-
require 'spec_helper'
describe ActsAsTaggableOn::GenericParser do
it '#parse should return empty array if empty tag string is passed' do
tag_list = ActsAsTaggableOn::GenericParser.new('')
expect(tag_list.parse).to be_empty
end
it '#parse should separate tags by comma' do
tag_list = ActsAsTaggableOn::GenericParser.new('cool,data,,I,have')
expect(tag_list.parse).to eq(%w(cool data I have))
end
end
acts-as-taggable-on-11.0.0/spec/acts_as_taggable_on/tag_spec.rb 0000644 0000041 0000041 00000030514 14704600021 024332 0 ustar www-data www-data # -*- encoding : utf-8 -*-
require 'spec_helper'
require 'db/migrate/2_add_missing_unique_indices.rb'
shared_examples_for 'without unique index' do
prepend_before(:all) { AddMissingUniqueIndices.down }
append_after(:all) do
ActsAsTaggableOn::Tag.delete_all
AddMissingUniqueIndices.up
end
end
describe ActsAsTaggableOn::Tag do
before(:each) do
@tag = ActsAsTaggableOn::Tag.new
@user = TaggableModel.create(name: 'Pablo', tenant_id: 100)
end
describe 'named like any' do
context 'case insensitive collation and unique index on tag name', if: using_case_insensitive_collation? do
before(:each) do
ActsAsTaggableOn::Tag.create(name: 'Awesome')
ActsAsTaggableOn::Tag.create(name: 'epic')
end
it 'should find both tags' do
expect(ActsAsTaggableOn::Tag.named_like_any(%w(awesome epic)).count).to eq(2)
end
end
context 'case insensitive collation without indexes or case sensitive collation with indexes' do
if using_case_insensitive_collation?
include_context 'without unique index'
end
before(:each) do
ActsAsTaggableOn::Tag.create(name: 'Awesome')
ActsAsTaggableOn::Tag.create(name: 'awesome')
ActsAsTaggableOn::Tag.create(name: 'epic')
end
it 'should find both tags' do
expect(ActsAsTaggableOn::Tag.named_like_any(%w(awesome epic)).count).to eq(3)
end
end
end
describe 'named any' do
context 'with some special characters combinations', if: using_mysql? do
it 'should not raise an invalid encoding exception' do
expect{ActsAsTaggableOn::Tag.named_any(["holä", "hol'ä"])}.not_to raise_error
end
end
end
describe 'for context' do
before(:each) do
@user.skill_list.add('ruby')
@user.save
end
it 'should return tags that have been used in the given context' do
expect(ActsAsTaggableOn::Tag.for_context('skills').pluck(:name)).to include('ruby')
end
it 'should not return tags that have been used in other contexts' do
expect(ActsAsTaggableOn::Tag.for_context('needs').pluck(:name)).to_not include('ruby')
end
end
describe 'for tenant' do
before(:each) do
@user.skill_list.add('ruby')
@user.save
end
it 'should return tags for the tenant' do
expect(ActsAsTaggableOn::Tag.for_tenant('100').pluck(:name)).to include('ruby')
end
it 'should not return tags for other tenants' do
expect(ActsAsTaggableOn::Tag.for_tenant('200').pluck(:name)).to_not include('ruby')
end
end
describe 'find or create by name' do
before(:each) do
@tag.name = 'awesome'
@tag.save
end
it 'should find by name' do
expect(ActsAsTaggableOn::Tag.find_or_create_with_like_by_name('awesome')).to eq(@tag)
end
it 'should find by name case insensitive' do
expect(ActsAsTaggableOn::Tag.find_or_create_with_like_by_name('AWESOME')).to eq(@tag)
end
it 'should create by name' do
expect {
ActsAsTaggableOn::Tag.find_or_create_with_like_by_name('epic')
}.to change(ActsAsTaggableOn::Tag, :count).by(1)
end
end
describe 'find or create by unicode name', unless: using_sqlite? do
before(:each) do
@tag.name = 'привет'
@tag.save
end
it 'should find by name' do
expect(ActsAsTaggableOn::Tag.find_or_create_with_like_by_name('привет')).to eq(@tag)
end
it 'should find by name case insensitive' do
expect(ActsAsTaggableOn::Tag.find_or_create_with_like_by_name('ПРИВЕТ')).to eq(@tag)
end
it 'should find by name accent insensitive', if: using_case_insensitive_collation? do
@tag.name = 'inupiat'
@tag.save
expect(ActsAsTaggableOn::Tag.find_or_create_with_like_by_name('Iñupiat')).to eq(@tag)
end
end
describe 'find or create all by any name' do
before(:each) do
@tag.name = 'awesome'
@tag.save
end
it 'should find by name' do
expect(ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name('awesome')).to eq([@tag])
end
it 'should find by name case insensitive' do
expect(ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name('AWESOME')).to eq([@tag])
end
context 'case sensitive' do
if using_case_insensitive_collation?
include_context 'without unique index'
end
it 'should find by name case sensitive' do
ActsAsTaggableOn.strict_case_match = true
expect {
ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name('AWESOME')
}.to change(ActsAsTaggableOn::Tag, :count).by(1)
end
end
it 'should create by name' do
expect {
ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name('epic')
}.to change(ActsAsTaggableOn::Tag, :count).by(1)
end
context 'case sensitive' do
if using_case_insensitive_collation?
include_context 'without unique index'
end
it 'should find or create by name case sensitive' do
ActsAsTaggableOn.strict_case_match = true
expect {
expect(ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name('AWESOME', 'awesome').map(&:name)).to eq(%w(AWESOME awesome))
}.to change(ActsAsTaggableOn::Tag, :count).by(1)
end
end
it 'should find or create by name' do
expect {
expect(ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name('awesome', 'epic').map(&:name)).to eq(%w(awesome epic))
}.to change(ActsAsTaggableOn::Tag, :count).by(1)
end
it 'should return an empty array if no tags are specified' do
expect(ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name([])).to be_empty
end
context 'another tag is created concurrently', :database_cleaner_delete, if: supports_concurrency? do
it 'retries and finds tag if tag with same name created concurrently' do
tag_name = 'super'
expect(ActsAsTaggableOn::Tag).to receive(:create).with(name: tag_name) do
# Simulate concurrent tag creation
Thread.new do
ActsAsTaggableOn::Tag.new(name: tag_name).save!
end.join
raise ActiveRecord::RecordNotUnique
end
expect {
ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name(tag_name)
}.to change(ActsAsTaggableOn::Tag, :count).by(1)
end
end
end
it 'should require a name' do
@tag.valid?
#TODO, we should find another way to check this
expect(@tag.errors[:name]).to eq(["can't be blank"])
@tag.name = 'something'
@tag.valid?
expect(@tag.errors[:name]).to be_empty
end
it 'should limit the name length to 255 or less characters' do
@tag.name = 'fgkgnkkgjymkypbuozmwwghblmzpqfsgjasflblywhgkwndnkzeifalfcpeaeqychjuuowlacmuidnnrkprgpcpybarbkrmziqihcrxirlokhnzfvmtzixgvhlxzncyywficpraxfnjptxxhkqmvicbcdcynkjvziefqzyndxkjmsjlvyvbwraklbalykyxoliqdlreeykuphdtmzfdwpphmrqvwvqffojkqhlzvinqajsxbszyvrqqyzusxranr'
@tag.valid?
#TODO, we should find another way to check this
expect(@tag.errors[:name]).to eq(['is too long (maximum is 255 characters)'])
@tag.name = 'fgkgnkkgjymkypbuozmwwghblmzpqfsgjasflblywhgkwndnkzeifalfcpeaeqychjuuowlacmuidnnrkprgpcpybarbkrmziqihcrxirlokhnzfvmtzixgvhlxzncyywficpraxfnjptxxhkqmvicbcdcynkjvziefqzyndxkjmsjlvyvbwraklbalykyxoliqdlreeykuphdtmzfdwpphmrqvwvqffojkqhlzvinqajsxbszyvrqqyzusxran'
@tag.valid?
expect(@tag.errors[:name]).to be_empty
end
it 'should equal a tag with the same name' do
@tag.name = 'awesome'
new_tag = ActsAsTaggableOn::Tag.new(name: 'awesome')
expect(new_tag).to eq(@tag)
end
it 'should return its name when to_s is called' do
@tag.name = 'cool'
expect(@tag.to_s).to eq('cool')
end
it 'have named_scope named(something)' do
@tag.name = 'cool'
@tag.save!
expect(ActsAsTaggableOn::Tag.named('cool')).to include(@tag)
end
it 'have named_scope named_like(something)' do
@tag.name = 'cool'
@tag.save!
@another_tag = ActsAsTaggableOn::Tag.create!(name: 'coolip')
expect(ActsAsTaggableOn::Tag.named_like('cool')).to include(@tag, @another_tag)
end
describe 'escape wildcard symbols in like requests' do
before(:each) do
@tag.name = 'cool'
@tag.save
@another_tag = ActsAsTaggableOn::Tag.create!(name: 'coo%')
@another_tag2 = ActsAsTaggableOn::Tag.create!(name: 'coolish')
end
it "return escaped result when '%' char present in tag" do
expect(ActsAsTaggableOn::Tag.named_like('coo%')).to_not include(@tag)
expect(ActsAsTaggableOn::Tag.named_like('coo%')).to include(@another_tag)
end
end
describe 'when using strict_case_match' do
before do
ActsAsTaggableOn.strict_case_match = true
@tag.name = 'awesome'
@tag.save!
end
after do
ActsAsTaggableOn.strict_case_match = false
end
it 'should find by name' do
expect(ActsAsTaggableOn::Tag.find_or_create_with_like_by_name('awesome')).to eq(@tag)
end
context 'case sensitive' do
if using_case_insensitive_collation?
include_context 'without unique index'
end
it 'should find by name case sensitively' do
expect {
ActsAsTaggableOn::Tag.find_or_create_with_like_by_name('AWESOME')
}.to change(ActsAsTaggableOn::Tag, :count)
expect(ActsAsTaggableOn::Tag.last.name).to eq('AWESOME')
end
end
context 'case sensitive' do
if using_case_insensitive_collation?
include_context 'without unique index'
end
it 'should have a named_scope named(something) that matches exactly' do
uppercase_tag = ActsAsTaggableOn::Tag.create(name: 'Cool')
@tag.name = 'cool'
@tag.save!
expect(ActsAsTaggableOn::Tag.named('cool')).to include(@tag)
expect(ActsAsTaggableOn::Tag.named('cool')).to_not include(uppercase_tag)
end
end
it 'should not change encoding' do
name = "\u3042"
original_encoding = name.encoding
record = ActsAsTaggableOn::Tag.find_or_create_with_like_by_name(name)
record.reload
expect(record.name.encoding).to eq(original_encoding)
end
context 'named any with some special characters combinations', if: using_mysql? do
it 'should not raise an invalid encoding exception' do
expect{ActsAsTaggableOn::Tag.named_any(["holä", "hol'ä"])}.not_to raise_error
end
end
end
describe 'name uniqeness validation' do
let(:duplicate_tag) { ActsAsTaggableOn::Tag.new(name: 'ror') }
before { ActsAsTaggableOn::Tag.create(name: 'ror') }
context "when don't need unique names" do
include_context 'without unique index'
it 'should not run uniqueness validation' do
allow(duplicate_tag).to receive(:validates_name_uniqueness?) { false }
duplicate_tag.save
expect(duplicate_tag).to be_persisted
end
end
context 'when do need unique names' do
it 'should run uniqueness validation' do
expect(duplicate_tag).to_not be_valid
end
it 'add error to name' do
duplicate_tag.save
expect(duplicate_tag.errors.size).to eq(1)
expect(duplicate_tag.errors.messages[:name]).to include('has already been taken')
end
end
end
describe 'popular tags' do
before do
%w(sports rails linux tennis golden_syrup).each_with_index do |t, i|
tag = ActsAsTaggableOn::Tag.new(name: t)
tag.taggings_count = i
tag.save!
end
end
it 'should find the most popular tags' do
expect(ActsAsTaggableOn::Tag.most_used(3).first.name).to eq("golden_syrup")
expect(ActsAsTaggableOn::Tag.most_used(3).length).to eq(3)
end
it 'should find the least popular tags' do
expect(ActsAsTaggableOn::Tag.least_used(3).first.name).to eq("sports")
expect(ActsAsTaggableOn::Tag.least_used(3).length).to eq(3)
end
end
describe 'base_class' do
before do
class Foo < ActiveRecord::Base; end
end
context "default" do
it "inherits from ActiveRecord::Base" do
expect(ActsAsTaggableOn::Tag.ancestors).to include(ActiveRecord::Base)
expect(ActsAsTaggableOn::Tag.ancestors).to_not include(Foo)
end
end
context "custom" do
it "inherits from custom class" do
ActsAsTaggableOn.base_class = 'Foo'
hide_const("ActsAsTaggableOn::Tag")
load("lib/acts-as-taggable-on/tag.rb")
expect(ActsAsTaggableOn::Tag.ancestors).to include(Foo)
end
end
end
end
acts-as-taggable-on-11.0.0/spec/acts_as_taggable_on/tags_helper_spec.rb 0000644 0000041 0000041 00000002430 14704600021 026050 0 ustar www-data www-data # -*- encoding : utf-8 -*-
require 'spec_helper'
describe ActsAsTaggableOn::TagsHelper do
before(:each) do
@bob = TaggableModel.create(name: 'Bob Jones', language_list: 'ruby, php')
@tom = TaggableModel.create(name: 'Tom Marley', language_list: 'ruby, java')
@eve = TaggableModel.create(name: 'Eve Nodd', language_list: 'ruby, c++')
@helper =
class Helper
include ActsAsTaggableOn::TagsHelper
end.new
end
it 'should yield the proper css classes' do
tags = {}
@helper.tag_cloud(TaggableModel.tag_counts_on(:languages), %w(sucky awesome)) do |tag, css_class|
tags[tag.name] = css_class
end
expect(tags['ruby']).to eq('awesome')
expect(tags['java']).to eq('sucky')
expect(tags['c++']).to eq('sucky')
expect(tags['php']).to eq('sucky')
end
it 'should handle tags with zero counts (build for empty)' do
ActsAsTaggableOn::Tag.create(name: 'php')
ActsAsTaggableOn::Tag.create(name: 'java')
ActsAsTaggableOn::Tag.create(name: 'c++')
tags = {}
@helper.tag_cloud(ActsAsTaggableOn::Tag.all, %w(sucky awesome)) do |tag, css_class|
tags[tag.name] = css_class
end
expect(tags['java']).to eq('sucky')
expect(tags['c++']).to eq('sucky')
expect(tags['php']).to eq('sucky')
end
end
acts-as-taggable-on-11.0.0/spec/spec_helper.rb 0000644 0000041 0000041 00000000755 14704600021 021103 0 ustar www-data www-data begin
require 'byebug'
rescue LoadError
end
$LOAD_PATH << '.' unless $LOAD_PATH.include?('.')
$LOAD_PATH.unshift(File.expand_path('../../lib', __FILE__))
require 'logger'
require File.expand_path('../../lib/acts-as-taggable-on', __FILE__)
I18n.enforce_available_locales = true
require 'rails'
require 'rspec/its'
require 'barrier'
require 'database_cleaner'
Dir['./spec/support/**/*.rb'].sort.each { |f| require f }
RSpec.configure do |config|
config.raise_errors_for_deprecations!
end
acts-as-taggable-on-11.0.0/spec/support/ 0000755 0000041 0000041 00000000000 14704600021 017772 5 ustar www-data www-data acts-as-taggable-on-11.0.0/spec/support/0-helpers.rb 0000644 0000041 0000041 00000001215 14704600021 022115 0 ustar www-data www-data def using_sqlite?
ActsAsTaggableOn::Utils.connection && ActsAsTaggableOn::Utils.connection.adapter_name == 'SQLite'
end
def supports_concurrency?
!using_sqlite?
end
def using_postgresql?
ActsAsTaggableOn::Utils.using_postgresql?
end
def postgresql_version
if using_postgresql?
ActsAsTaggableOn::Utils.connection.execute('SHOW SERVER_VERSION').first['server_version'].to_f
else
0.0
end
end
def postgresql_support_json?
postgresql_version >= 9.2
end
def using_mysql?
ActsAsTaggableOn::Utils.using_mysql?
end
def using_case_insensitive_collation?
using_mysql? && ActsAsTaggableOn::Utils.connection.collation =~ /_ci\Z/
end
acts-as-taggable-on-11.0.0/spec/support/database.rb 0000644 0000041 0000041 00000003226 14704600021 022066 0 ustar www-data www-data # frozen_string_literal: true
# set adapter to use, default is sqlite3
# to use an alternative adapter run => rake spec DB='postgresql'
db_name = ENV['DB'] || 'sqlite3'
database_yml = File.expand_path('../internal/config/database.yml', __dir__)
unless File.exist?(database_yml)
raise "Please create #{database_yml} first to configure your database. Take a look at: #{database_yml}.sample"
end
ActiveRecord::Base.configurations = YAML.load_file(database_yml)
ActiveRecord::Base.logger = Logger.new(File.join(File.dirname(__FILE__), '../debug.log'))
ActiveRecord::Base.logger.level = ENV['CI'] ? ::Logger::ERROR : ::Logger::DEBUG
ActiveRecord::Migration.verbose = false
ActiveRecord.default_timezone = :utc
config = ActiveRecord::Base.configurations.configs_for(env_name: db_name)
begin
ActiveRecord::Base.establish_connection(db_name.to_sym)
ActiveRecord::Base.connection
rescue StandardError
case db_name
when /(mysql)/
ActiveRecord::Base.establish_connection(config.merge('database' => nil))
ActiveRecord::Base.connection.create_database(config['database'],
{ charset: 'utf8', collation: 'utf8_unicode_ci' })
when 'postgresql'
ActiveRecord::Base.establish_connection(config.merge('database' => 'postgres',
'schema_search_path' => 'public'))
ActiveRecord::Base.connection.create_database(config['database'], config.merge('encoding' => 'utf8'))
end
ActiveRecord::Base.establish_connection(config)
end
require "#{File.dirname(__FILE__)}/../internal/db/schema.rb"
Dir["#{File.dirname(__dir__)}/internal/app/models/*.rb"].each { |f| require f }
acts-as-taggable-on-11.0.0/spec/support/database_cleaner.rb 0000644 0000041 0000041 00000000713 14704600021 023555 0 ustar www-data www-data RSpec.configure do |config|
config.before(:suite) do
DatabaseCleaner.clean_with(:truncation)
DatabaseCleaner.strategy = :transaction
DatabaseCleaner.clean
end
config.before(:each, :database_cleaner_delete) do
DatabaseCleaner.strategy = :truncation
end
config.after(:suite) do
DatabaseCleaner.clean
end
config.before(:each) do
DatabaseCleaner.start
end
config.after(:each) do
DatabaseCleaner.clean
end
end
acts-as-taggable-on-11.0.0/spec/support/array.rb 0000644 0000041 0000041 00000000200 14704600021 021425 0 ustar www-data www-data unless [].respond_to?(:freq)
class Array
def freq
k=Hash.new(0)
each { |e| k[e]+=1 }
k
end
end
end acts-as-taggable-on-11.0.0/spec/internal/ 0000755 0000041 0000041 00000000000 14704600021 020072 5 ustar www-data www-data acts-as-taggable-on-11.0.0/spec/internal/db/ 0000755 0000041 0000041 00000000000 14704600021 020457 5 ustar www-data www-data acts-as-taggable-on-11.0.0/spec/internal/db/schema.rb 0000644 0000041 0000041 00000006300 14704600021 022243 0 ustar www-data www-data ActiveRecord::Schema.define version: 0 do
create_table ActsAsTaggableOn.tags_table, force: true do |t|
t.string :name
t.integer :taggings_count, default: 0
t.string :type
end
add_index ActsAsTaggableOn.tags_table, ['name'], name: 'index_tags_on_name', unique: true
create_table ActsAsTaggableOn.taggings_table, force: true do |t|
t.integer :tag_id
# You should make sure that the column created is
# long enough to store the required class names.
t.string :taggable_type
t.integer :taggable_id
t.string :tagger_type
t.integer :tagger_id
# Limit is created to prevent MySQL error on index
# length for MyISAM table type: http://bit.ly/vgW2Ql
t.string :context, limit: 128
t.string :tenant , limit: 128
t.datetime :created_at
end
add_index ActsAsTaggableOn.taggings_table,
['tag_id', 'taggable_id', 'taggable_type', 'context', 'tagger_id', 'tagger_type'],
unique: true, name: 'taggings_idx'
add_index ActsAsTaggableOn.taggings_table, :tag_id , name: 'index_taggings_on_tag_id'
# above copied from
# generators/acts_as_taggable_on/migration/migration_generator
create_table :taggable_models, force: true do |t|
t.column :name, :string
t.column :type, :string
t.column :tenant_id, :integer
end
create_table :columns_override_models, force: true do |t|
t.column :name, :string
t.column :type, :string
t.column :ignored_column, :string
end
create_table :non_standard_id_taggable_models, primary_key: 'an_id', force: true do |t|
t.column :name, :string
t.column :type, :string
end
create_table :untaggable_models, force: true do |t|
t.column :taggable_model_id, :integer
t.column :name, :string
end
create_table :cached_models, force: true do |t|
t.column :name, :string
t.column :type, :string
t.column :cached_tag_list, :string
end
create_table :other_cached_models, force: true do |t|
t.column :name, :string
t.column :type, :string
t.column :cached_language_list, :string
t.column :cached_status_list, :string
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
create_table :other_taggable_models, force: true do |t|
t.column :name, :string
t.column :type, :string
end
create_table :ordered_taggable_models, force: true do |t|
t.column :name, :string
t.column :type, :string
end
create_table :cache_methods_injected_models, force: true do |t|
t.column :cached_tag_list, :string
end
# Special cases for postgresql
if using_postgresql?
create_table :other_cached_with_array_models, force: true do |t|
t.column :name, :string
t.column :type, :string
t.column :cached_language_list, :string, array: true
t.column :cached_status_list, :string, array: true
t.column :cached_glass_list, :string, array: true
end
if postgresql_support_json?
create_table :taggable_model_with_jsons, :force => true do |t|
t.column :name, :string
t.column :type, :string
t.column :opts, :json
end
end
end
end
acts-as-taggable-on-11.0.0/spec/internal/app/ 0000755 0000041 0000041 00000000000 14704600021 020652 5 ustar www-data www-data acts-as-taggable-on-11.0.0/spec/internal/app/models/ 0000755 0000041 0000041 00000000000 14704600021 022135 5 ustar www-data www-data acts-as-taggable-on-11.0.0/spec/internal/app/models/columns_override_model.rb 0000644 0000041 0000041 00000000201 14704600021 027212 0 ustar www-data www-data class ColumnsOverrideModel < ActiveRecord::Base
def self.columns
super.reject { |c| c.name == 'ignored_column' }
end
end
acts-as-taggable-on-11.0.0/spec/internal/app/models/cached_model_with_array.rb 0000644 0000041 0000041 00000000400 14704600021 027274 0 ustar www-data www-data if using_postgresql?
class CachedModelWithArray < ActiveRecord::Base
acts_as_taggable
end
if postgresql_support_json?
class TaggableModelWithJson < ActiveRecord::Base
acts_as_taggable
acts_as_taggable_on :skills
end
end
end
acts-as-taggable-on-11.0.0/spec/internal/app/models/other_cached_model.rb 0000644 0000041 0000041 00000000146 14704600021 026253 0 ustar www-data www-data class OtherCachedModel < ActiveRecord::Base
acts_as_taggable_on :languages, :statuses, :glasses
end
acts-as-taggable-on-11.0.0/spec/internal/app/models/untaggable_model.rb 0000644 0000041 0000041 00000000114 14704600021 025747 0 ustar www-data www-data class UntaggableModel < ActiveRecord::Base
belongs_to :taggable_model
end
acts-as-taggable-on-11.0.0/spec/internal/app/models/company.rb 0000644 0000041 0000041 00000000547 14704600021 024136 0 ustar www-data www-data 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
acts-as-taggable-on-11.0.0/spec/internal/app/models/altered_inheriting_taggable_model.rb 0000644 0000041 0000041 00000000171 14704600021 031327 0 ustar www-data www-data require_relative 'taggable_model'
class AlteredInheritingTaggableModel < TaggableModel
acts_as_taggable_on :parts
end
acts-as-taggable-on-11.0.0/spec/internal/app/models/user.rb 0000644 0000041 0000041 00000000065 14704600021 023441 0 ustar www-data www-data class User < ActiveRecord::Base
acts_as_tagger
end
acts-as-taggable-on-11.0.0/spec/internal/app/models/cached_model.rb 0000644 0000041 0000041 00000000076 14704600021 025054 0 ustar www-data www-data class CachedModel < ActiveRecord::Base
acts_as_taggable
end
acts-as-taggable-on-11.0.0/spec/internal/app/models/market.rb 0000644 0000041 0000041 00000000051 14704600021 023741 0 ustar www-data www-data class Market < ActsAsTaggableOn::Tag
end
acts-as-taggable-on-11.0.0/spec/internal/app/models/inheriting_taggable_model.rb 0000644 0000041 0000041 00000000125 14704600021 027626 0 ustar www-data www-data require_relative 'taggable_model'
class InheritingTaggableModel < TaggableModel
end
acts-as-taggable-on-11.0.0/spec/internal/app/models/taggable_model.rb 0000644 0000041 0000041 00000000540 14704600021 025407 0 ustar www-data www-data class TaggableModel < ActiveRecord::Base
acts_as_taggable
acts_as_taggable_on :languages
acts_as_taggable_on :skills
acts_as_taggable_on :needs, :offerings
acts_as_taggable_tenant :tenant_id
has_many :untaggable_models
attr_reader :tag_list_submethod_called
def tag_list=(v)
@tag_list_submethod_called = true
super
end
end
acts-as-taggable-on-11.0.0/spec/internal/app/models/other_taggable_model.rb 0000644 0000041 0000041 00000000203 14704600021 026604 0 ustar www-data www-data class OtherTaggableModel < ActiveRecord::Base
acts_as_taggable_on :tags, :languages
acts_as_taggable_on :needs, :offerings
end
acts-as-taggable-on-11.0.0/spec/internal/app/models/non_standard_id_taggable_model.rb 0000644 0000041 0000041 00000000357 14704600021 030623 0 ustar www-data www-data class NonStandardIdTaggableModel < ActiveRecord::Base
self.primary_key = :an_id
acts_as_taggable
acts_as_taggable_on :languages
acts_as_taggable_on :skills
acts_as_taggable_on :needs, :offerings
has_many :untaggable_models
end
acts-as-taggable-on-11.0.0/spec/internal/app/models/ordered_taggable_model.rb 0000644 0000041 0000041 00000000166 14704600021 027117 0 ustar www-data www-data class OrderedTaggableModel < ActiveRecord::Base
acts_as_ordered_taggable
acts_as_ordered_taggable_on :colours
end
acts-as-taggable-on-11.0.0/spec/internal/app/models/student.rb 0000644 0000041 0000041 00000000062 14704600021 024146 0 ustar www-data www-data require_relative 'user'
class Student < User
end
acts-as-taggable-on-11.0.0/spec/internal/config/ 0000755 0000041 0000041 00000000000 14704600021 021337 5 ustar www-data www-data acts-as-taggable-on-11.0.0/spec/internal/config/database.yml.sample 0000644 0000041 0000041 00000000516 14704600021 025110 0 ustar www-data www-data sqlite3:
adapter: sqlite3
database: ':memory:'
mysql:
adapter: mysql2
host: 127.0.0.1
username: root
password:
database: acts_as_taggable_on
encoding: utf8
postgresql:
# Needs to be given as a URL to force connection via TCP
url: postgresql://postgres:postgres@127.0.0.1:5432/acts_as_taggable_on?encoding=utf8
acts-as-taggable-on-11.0.0/gemfiles/ 0000755 0000041 0000041 00000000000 14704600021 017117 5 ustar www-data www-data acts-as-taggable-on-11.0.0/gemfiles/activerecord_7.0.gemfile 0000644 0000041 0000041 00000000467 14704600021 023516 0 ustar www-data www-data # This file was generated by Appraisal
source "https://rubygems.org"
gem "activerecord", "~> 7.0.1"
gem "pg"
gem "sqlite3", "~> 1.4"
gem "mysql2", "~> 0.5"
group :local_development do
gem "guard"
gem "guard-rspec"
gem "appraisal"
gem "rake"
gem "byebug", platforms: [:mri]
end
gemspec path: "../"
acts-as-taggable-on-11.0.0/gemfiles/activerecord_7.2.gemfile 0000644 0000041 0000041 00000000467 14704600021 023520 0 ustar www-data www-data # This file was generated by Appraisal
source "https://rubygems.org"
gem "activerecord", "~> 7.2.0"
gem "pg"
gem "sqlite3", "~> 1.4"
gem "mysql2", "~> 0.5"
group :local_development do
gem "guard"
gem "guard-rspec"
gem "appraisal"
gem "rake"
gem "byebug", platforms: [:mri]
end
gemspec path: "../"
acts-as-taggable-on-11.0.0/gemfiles/activerecord_7.1.gemfile 0000644 0000041 0000041 00000000467 14704600021 023517 0 ustar www-data www-data # This file was generated by Appraisal
source "https://rubygems.org"
gem "activerecord", "~> 7.1.0"
gem "pg"
gem "sqlite3", "~> 1.4"
gem "mysql2", "~> 0.5"
group :local_development do
gem "guard"
gem "guard-rspec"
gem "appraisal"
gem "rake"
gem "byebug", platforms: [:mri]
end
gemspec path: "../"
acts-as-taggable-on-11.0.0/.rspec 0000644 0000041 0000041 00000000025 14704600021 016436 0 ustar www-data www-data --colour
--backtrace
acts-as-taggable-on-11.0.0/Rakefile 0000644 0000041 0000041 00000000730 14704600021 016771 0 ustar www-data www-data require 'rubygems'
require 'bundler/setup'
import "./lib/tasks/tags_collate_utf8.rake"
desc 'Default: run specs'
task default: :spec
desc 'Copy sample spec database.yml over if not exists'
task :copy_db_config do
cp 'spec/internal/config/database.yml.sample', 'spec/internal/config/database.yml'
end
task spec: [:copy_db_config]
require 'rspec/core/rake_task'
RSpec::Core::RakeTask.new do |t|
t.pattern = 'spec/**/*_spec.rb'
end
Bundler::GemHelper.install_tasks
acts-as-taggable-on-11.0.0/LICENSE.md 0000644 0000041 0000041 00000002070 14704600021 016727 0 ustar www-data www-data __Copyright (c) 2007 Michael Bleigh and Intridea Inc.__
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
acts-as-taggable-on-11.0.0/Gemfile 0000644 0000041 0000041 00000000253 14704600021 016617 0 ustar www-data www-data source 'https://rubygems.org'
gemspec
group :local_development do
gem 'guard'
gem 'guard-rspec'
gem 'appraisal'
gem 'rake'
gem 'byebug', platforms: [:mri]
end
acts-as-taggable-on-11.0.0/docker-compose.yml 0000644 0000041 0000041 00000000543 14704600021 020763 0 ustar www-data www-data version: '3.9'
services:
postgres:
image: postgres:10
environment:
POSTGRES_USER: postgres
POSTGRES_DB: acts_as_taggable_on
POSTGRES_PASSWORD: postgres
ports: ['5432:5432']
mysql:
image: mysql:8
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: true
MYSQL_DATABASE: acts_as_taggable_on
ports: ['3306:3306'] acts-as-taggable-on-11.0.0/README.md 0000644 0000041 0000041 00000043712 14704600021 016612 0 ustar www-data www-data
**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*
- [ActsAsTaggableOn](#actsastaggableon)
- [Installation](#installation)
- [Post Installation](#post-installation)
- [For MySql users](#for-mysql-users)
- [Usage](#usage)
- [Finding most or least used tags](#finding-most-or-least-used-tags)
- [Finding Tagged Objects](#finding-tagged-objects)
- [Relationships](#relationships)
- [Dynamic Tag Contexts](#dynamic-tag-contexts)
- [Tag Parsers](#tag-parsers)
- [Tag Ownership](#tag-ownership)
- [Working with Owned Tags](#working-with-owned-tags)
- [Adding owned tags](#adding-owned-tags)
- [Removing owned tags](#removing-owned-tags)
- [Dirty objects](#dirty-objects)
- [Tag cloud calculations](#tag-cloud-calculations)
- [Configuration](#configuration)
- [Upgrading](#upgrading)
- [Contributors](#contributors)
- [Compatibility](#compatibility)
- [Testing](#testing)
- [License](#license)
# ActsAsTaggableOn
[](https://gitter.im/mbleigh/acts-as-taggable-on?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[](http://badge.fury.io/rb/acts-as-taggable-on)
[](https://github.com/mbleigh/acts-as-taggable-on/actions)
[](https://codeclimate.com/github/mbleigh/acts-as-taggable-on)
[](http://inch-ci.org/github/mbleigh/acts-as-taggable-on)
[](https://hakiri.io/github/mbleigh/acts-as-taggable-on/master)
This plugin was originally based on Acts as Taggable on Steroids by Jonathan Viney.
It has evolved substantially since that point, but all credit goes to him for the
initial tagging functionality that so many people have used.
For instance, in a social network, a user might have tags that are called skills,
interests, sports, and more. There is no real way to differentiate between tags and
so an implementation of this type is not possible with acts as taggable on steroids.
Enter Acts as Taggable On. Rather than tying functionality to a specific keyword
(namely `tags`), acts as taggable on allows you to specify an arbitrary number of
tag "contexts" that can be used locally or in combination in the same way steroids
was used.
## Installation
To use it, add it to your Gemfile:
```ruby
gem 'acts-as-taggable-on'
```
and bundle:
```shell
bundle
```
#### Post Installation
Install migrations
```shell
# For the latest versions :
rake acts_as_taggable_on_engine:install:migrations
```
Review the generated migrations then migrate :
```shell
rake db:migrate
```
If you do not wish or need to support multi-tenancy, the migration for `add_tenant_to_taggings` is optional and can be discarded safely.
#### For MySql users
You can circumvent at any time the problem of special characters [issue 623](https://github.com/mbleigh/acts-as-taggable-on/issues/623) by setting in an initializer file:
```ruby
ActsAsTaggableOn.force_binary_collation = true
```
Or by running this rake task:
```shell
rake acts_as_taggable_on_engine:tag_names:collate_bin
```
See the Configuration section for more details, and a general note valid for older
version of the gem.
## Usage
Setup
```ruby
class User < ActiveRecord::Base
acts_as_taggable_on :tags
acts_as_taggable_on :skills, :interests #You can also configure multiple tag types per model
end
class UsersController < ApplicationController
def user_params
params.require(:user).permit(:name, :tag_list) ## Rails 4 strong params usage
end
end
@user = User.new(:name => "Bobby")
```
Add and remove a single tag
```ruby
@user.tag_list.add("awesome") # add a single tag. alias for <<
@user.tag_list.remove("awesome") # remove a single tag
@user.save # save to persist tag_list
```
Add and remove multiple tags in an array
```ruby
@user.tag_list.add("awesome", "slick")
@user.tag_list.remove("awesome", "slick")
@user.save
```
You can also add and remove tags in format of String. This would
be convenient in some cases such as handling tag input param in a String.
Pay attention you need to add `parse: true` as option in this case.
You may also want to take a look at delimiter in the string. The default
is comma `,` so you don't need to do anything here. However, if you made
a change on delimiter setting, make sure the string will match. See
[configuration](#configuration) for more about delimiter.
```ruby
@user.tag_list.add("awesome, slick", parse: true)
@user.tag_list.remove("awesome, slick", parse: true)
```
You can also add and remove tags by direct assignment. Note this will
remove existing tags so use it with attention.
```ruby
@user.tag_list = "awesome, slick, hefty"
@user.save
@user.reload
@user.tags
=> [#,
#,
#]
```
With the defined context in model, you have multiple new methods at disposal
to manage and view the tags in the context. For example, with `:skill` context
these methods are added to the model: `skill_list`(and `skill_list.add`, `skill_list.remove`
`skill_list=`), `skills`(plural), `skill_counts`.
```ruby
@user.skill_list = "joking, clowning, boxing"
@user.save
@user.reload
@user.skills
=> [#,
#,
#]
@user.skill_list.add("coding")
@user.skill_list
# => ["joking", "clowning", "boxing", "coding"]
@another_user = User.new(:name => "Alice")
@another_user.skill_list.add("clowning")
@another_user.save
User.skill_counts
=> [#,
#,
#]
```
To preserve the order in which tags are created use `acts_as_ordered_taggable`:
```ruby
class User < ActiveRecord::Base
# Alias for acts_as_ordered_taggable_on :tags
acts_as_ordered_taggable
acts_as_ordered_taggable_on :skills, :interests
end
@user = User.new(:name => "Bobby")
@user.tag_list = "east, south"
@user.save
@user.tag_list = "north, east, south, west"
@user.save
@user.reload
@user.tag_list # => ["north", "east", "south", "west"]
```
### Finding most or least used tags
You can find the most or least used tags by using:
```ruby
ActsAsTaggableOn::Tag.most_used
ActsAsTaggableOn::Tag.least_used
```
You can also filter the results by passing the method a limit, however the default limit is 20.
```ruby
ActsAsTaggableOn::Tag.most_used(10)
ActsAsTaggableOn::Tag.least_used(10)
```
### Finding Tagged Objects
Acts As Taggable On uses scopes to create an association for tags.
This way you can mix and match to filter down your results.
```ruby
class User < ActiveRecord::Base
acts_as_taggable_on :tags, :skills
scope :by_join_date, order("created_at DESC")
end
User.tagged_with("awesome").by_join_date
User.tagged_with("awesome").by_join_date.paginate(:page => params[:page], :per_page => 20)
# Find users that matches all given tags:
# NOTE: This only matches users that have the exact set of specified tags. If a user has additional tags, they are not returned.
User.tagged_with(["awesome", "cool"], :match_all => true)
# Find users with any of the specified tags:
User.tagged_with(["awesome", "cool"], :any => true)
# Find users that have not been tagged with awesome or cool:
User.tagged_with(["awesome", "cool"], :exclude => true)
# Find users with any of the tags based on context:
User.tagged_with(['awesome', 'cool'], :on => :tags, :any => true).tagged_with(['smart', 'shy'], :on => :skills, :any => true)
```
#### Wildcard tag search
You now have the following options for prefix, suffix and containment search, along with `:any` or `:exclude` option.
Use `wild: :suffix` to place a wildcard at the end of the tag. It will be looking for `awesome%` and `cool%` in SQL.
Use `wild: :prefix` to place a wildcard at the beginning of the tag. It will be looking for `%awesome` and `%cool` in SQL.
Use `wild: true` to place a wildcard both at the beginning and the end of the tag. It will be looking for `%awesome%` and `%cool%` in SQL.
__Tip:__ `User.tagged_with([])` or `User.tagged_with('')` will return `[]`, an empty set of records.
### Relationships
You can find objects of the same type based on similar tags on certain contexts.
Also, objects will be returned in descending order based on the total number of
matched tags.
```ruby
@bobby = User.find_by_name("Bobby")
@bobby.skill_list # => ["jogging", "diving"]
@frankie = User.find_by_name("Frankie")
@frankie.skill_list # => ["hacking"]
@tom = User.find_by_name("Tom")
@tom.skill_list # => ["hacking", "jogging", "diving"]
@tom.find_related_skills # => [, ]
@bobby.find_related_skills # => []
@frankie.find_related_skills # => []
```
### Dynamic Tag Contexts
In addition to the generated tag contexts in the definition, it is also possible
to allow for dynamic tag contexts (this could be user generated tag contexts!)
```ruby
@user = User.new(:name => "Bobby")
@user.set_tag_list_on(:customs, "same, as, tag, list")
@user.tag_list_on(:customs) # => ["same", "as", "tag", "list"]
@user.save
@user.tags_on(:customs) # => [,...]
@user.tag_counts_on(:customs)
User.tagged_with("same", :on => :customs) # => [@user]
```
### Finding tags based on context
You can find tags for a specific context by using the ```for_context``` scope:
```ruby
ActsAsTaggableOn::Tag.for_context(:tags)
ActsAsTaggableOn::Tag.for_context(:skills)
```
### Tag Parsers
If you want to change how tags are parsed, you can define your own implementation:
```ruby
class MyParser < ActsAsTaggableOn::GenericParser
def parse
ActsAsTaggableOn::TagList.new.tap do |tag_list|
tag_list.add @tag_list.split('|')
end
end
end
```
Now you can use this parser, passing it as parameter:
```ruby
@user = User.new(:name => "Bobby")
@user.tag_list = "east, south"
@user.tag_list.add("north|west", parser: MyParser)
@user.tag_list # => ["north", "east", "south", "west"]
# Or also:
@user.tag_list.parser = MyParser
@user.tag_list.add("north|west")
@user.tag_list # => ["north", "east", "south", "west"]
```
Or change it globally:
```ruby
ActsAsTaggableOn.default_parser = MyParser
@user = User.new(:name => "Bobby")
@user.tag_list = "east|south"
@user.tag_list # => ["east", "south"]
```
### Tag Ownership
Tags can have owners:
```ruby
class User < ActiveRecord::Base
acts_as_tagger
end
class Photo < ActiveRecord::Base
acts_as_taggable_on :locations
end
@some_user.tag(@some_photo, :with => "paris, normandy", :on => :locations)
@some_user.owned_taggings
@some_user.owned_tags
Photo.tagged_with("paris", :on => :locations, :owned_by => @some_user)
@some_photo.locations_from(@some_user) # => ["paris", "normandy"]
@some_photo.owner_tags_on(@some_user, :locations) # => [#...]
@some_photo.owner_tags_on(nil, :locations) # => Ownerships equivalent to saying @some_photo.locations
@some_user.tag(@some_photo, :with => "paris, normandy", :on => :locations, :skip_save => true) #won't save @some_photo object
```
#### Working with Owned Tags
Note that `tag_list` only returns tags whose taggings do not have an owner. Continuing from the above example:
```ruby
@some_photo.tag_list # => []
```
To retrieve all tags of an object (regardless of ownership) or if only one owner can tag the object, use `all_tags_list`.
##### Adding owned tags
Note that **owned tags** are added all at once, in the form of ***comma separated tags*** in string.
Also, when you try to add **owned tags** again, it simply overwrites the previous set of **owned tags**.
So to append tags in previously existing **owned tags** list, go as follows:
```ruby
def add_owned_tag
@some_item = Item.find(params[:id])
owned_tag_list = @some_item.all_tags_list - @some_item.tag_list
owned_tag_list += [(params[:tag])]
@tag_owner.tag(@some_item, :with => stringify(owned_tag_list), :on => :tags)
@some_item.save
end
def stringify(tag_list)
tag_list.inject('') { |memo, tag| memo += (tag + ',') }[0..-1]
end
```
##### Removing owned tags
Similarly as above, removing will be as follows:
```ruby
def remove_owned_tag
@some_item = Item.find(params[:id])
owned_tag_list = @some_item.all_tags_list - @some_item.tag_list
owned_tag_list -= [(params[:tag])]
@tag_owner.tag(@some_item, :with => stringify(owned_tag_list), :on => :tags)
@some_item.save
end
```
### Tag Tenancy
Tags support multi-tenancy. This is useful for applications where a Tag belongs to a scoped set of models:
```ruby
class Account < ActiveRecord::Base
has_many :photos
end
class User < ActiveRecord::Base
belongs_to :account
acts_as_taggable_on :tags
acts_as_taggable_tenant :account_id
end
@user1.tag_list = ["foo", "bar"] # these taggings will automatically have the tenant saved
@user2.tag_list = ["bar", "baz"]
ActsAsTaggableOn::Tag.for_tenant(@user1.account.id) # returns Tag models for "foo" and "bar", but not "baz"
```
### Dirty objects
```ruby
@bobby = User.find_by_name("Bobby")
@bobby.skill_list # => ["jogging", "diving"]
@bobby.skill_list_changed? #=> false
@bobby.changes #=> {}
@bobby.skill_list = "swimming"
@bobby.changes.should == {"skill_list"=>["jogging, diving", ["swimming"]]}
@bobby.skill_list_changed? #=> true
@bobby.skill_list_change.should == ["jogging, diving", ["swimming"]]
```
### Tag cloud calculations
To construct tag clouds, the frequency of each tag needs to be calculated.
Because we specified `acts_as_taggable_on` on the `User` class, we can
get a calculation of all the tag counts by using `User.tag_counts_on(:customs)`. But what if we wanted a tag count for
a single user's posts? To achieve this we call tag_counts on the association:
```ruby
User.find(:first).posts.tag_counts_on(:tags)
```
A helper is included to assist with generating tag clouds.
Here is an example that generates a tag cloud.
Helper:
```ruby
module PostsHelper
include ActsAsTaggableOn::TagsHelper
end
```
Controller:
```ruby
class PostController < ApplicationController
def tag_cloud
@tags = Post.tag_counts_on(:tags)
end
end
```
View:
```erb
<% tag_cloud(@tags, %w(css1 css2 css3 css4)) do |tag, css_class| %>
<%= link_to tag.name, { :action => :tag, :id => tag.name }, :class => css_class %>
<% end %>
```
CSS:
```css
.css1 { font-size: 1.0em; }
.css2 { font-size: 1.2em; }
.css3 { font-size: 1.4em; }
.css4 { font-size: 1.6em; }
```
## Configuration
If you would like to remove unused tag objects after removing taggings, add:
```ruby
ActsAsTaggableOn.remove_unused_tags = true
```
If you want force tags to be saved downcased:
```ruby
ActsAsTaggableOn.force_lowercase = true
```
If you want tags to be saved parametrized (you can redefine to_param as well):
```ruby
ActsAsTaggableOn.force_parameterize = true
```
If you would like tags to be case-sensitive and not use LIKE queries for creation:
```ruby
ActsAsTaggableOn.strict_case_match = true
```
If you would like to have an exact match covering special characters with MySql:
```ruby
ActsAsTaggableOn.force_binary_collation = true
```
If you would like to specify table names:
```ruby
ActsAsTaggableOn.tags_table = 'aato_tags'
ActsAsTaggableOn.taggings_table = 'aato_taggings'
```
If you want to change the default delimiter (it defaults to ','). You can also pass in an array of delimiters such as ([',', '|']):
```ruby
ActsAsTaggableOn.delimiter = ','
```
*NOTE 1: SQLite by default can't upcase or downcase multibyte characters, resulting in unwanted behavior. Load the SQLite ICU extension for proper handle of such characters. [See docs](http://www.sqlite.org/src/artifact?ci=trunk&filename=ext/icu/README.txt)*
*NOTE 2: the option `force_binary_collation` is strongest than `strict_case_match` and when
set to true, the `strict_case_match` is ignored.
To roughly apply the `force_binary_collation` behaviour with a version of the gem <= 3.4.4, execute the following commands in the MySql console:*
```shell
USE my_wonderful_app_db;
ALTER TABLE tags MODIFY name VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_bin;
```
#### Upgrading
see [UPGRADING](UPGRADING.md)
## Contributors
We have a long list of valued contributors. [Check them all](https://github.com/mbleigh/acts-as-taggable-on/contributors)
## Compatibility
Versions 2.x are compatible with Ruby 1.8.7+ and Rails 3.
Versions 2.4.1 and up are compatible with Rails 4 too (thanks to arabonradar and cwoodcox).
Versions >= 3.x are compatible with Ruby 1.9.3+ and Rails 3 and 4.
Versions >= 4.x are compatible with Ruby 2.0.0+ and Rails 4 and 5.
Versions >= 7.x are compatible with Ruby 2.3.7+ and Rails 5 and 6.
Versions >= 8.x are compatible with Ruby 2.3.7+ and Rails 5 and 6.
Versions >= 9.x are compatible with Ruby 2.5.0 and Rails 6 and 7.
For an up-to-date roadmap, see https://github.com/mbleigh/acts-as-taggable-on/milestones
## Testing
Acts As Taggable On uses RSpec for its test coverage. Inside the gem
directory, you can run the specs with:
```shell
bundle
rake spec
```
You can run all the tests across all the Rails versions by running `rake appraise`.
If you'd also like to [run the tests across all rubies and databases as configured for Github Actions, install and run `wwtd`](https://github.com/grosser/wwtd).
## License
See [LICENSE](https://github.com/mbleigh/acts-as-taggable-on/blob/master/LICENSE.md)
acts-as-taggable-on-11.0.0/CHANGELOG.md 0000644 0000041 0000041 00000060467 14704600021 017152 0 ustar www-data www-data Changes are below categorized as follows:
- Breaking Changes
- Features
- Fixes
- Performance
- Misc
- Documentation
Each change should fall into categories that would affect whether the release is major (breaking changes), minor (new behavior), or patch (bug fix). See [semver](http://semver.org/) and [pessimistic versioning](http://guides.rubygems.org/patterns/#pessimistic_version_constraint).
As such, _Breaking Changes_ are major. _Features_ would map to either major or minor. _Fixes_, _Performance_, and _Misc_ are either minor or patch, the difference being kind of fuzzy for the purposes of history. Adding _Documentation_ (including tests) would be patch level.
### [v11.0.0) / 2024-08-23](https://github.com/mbleigh/acts-as-taggable-on/compare/v10.0.0...v11.0.0)
- Removed support for Ruby 2.7
- Removed support for Rails 6.1
- Added support for Ruby 3.2 and 3.3
- Added support for Rails 7.2
- Remove legacy code
- Renamed gem folder from acts_as_taggable_on to acts-as-taggable-on
- Removed legacy autoloading and replaced it with zeitwerk
### [v10.0.0) / 2023-10-15](https://github.com/mbleigh/acts-as-taggable-on/compare/v9.0.1...v10.0.0)
* Features
* [@glampr Add support for prefix and suffix searches alongside previously supported containment (wildcard) searches](https://github.com/mbleigh/acts-as-taggable-on/pull/1082)
* [@donquxiote Add support for horizontally sharded databases](https://github.com/mbleigh/acts-as-taggable-on/pull/1079)
* [aovertus Remove restriction around ActiveRecord 7.x versions allowing support until next major is released](https://github.com/mbleigh/acts-as-taggable-on/pull/1110)
### [v9.0.1) / 2022-01-07](https://github.com/mbleigh/acts-as-taggable-on/compare/v9.0.0..v9.0.1)
* Fixes
* Fix migration that generate default index
### [v9.0.0) / 2022-01-04](https://github.com/mbleigh/acts-as-taggable-on/compare/v8.1.0...v9.0.0)
* Fixes
* Support activerecord-7.0.0
* Support postgis adapter
* Fix migration syntax
* Features
* [@moliver-hemasystems Add support for a list of taggable IDs in tagging conditions](https://github.com/mbleigh/acts-as-taggable-on/pull/1053)
* Misc
* Add docker-compose.yml for local development
### [v8.1.0) / 2021-06-19](https://github.com/mbleigh/acts-as-taggable-on/compare/v8.0.0...v8.1.0)
* Fixes
* [@ngouy Fix rollbackable tenant migrations](https://github.com/mbleigh/acts-as-taggable-on/pull/1038)
* [@ngouy Fix gem conflict with already existing tenant model](https://github.com/mbleigh/acts-as-taggable-on/pull/1037)
### [v8.0.0) / 2021-06-07](https://github.com/mbleigh/acts-as-taggable-on/compare/v7.0.0...v8.0.0)
* Features
* [@lunaru Support tenants for taggings](https://github.com/mbleigh/acts-as-taggable-on/pull/1000)
* Fixes
* [@gr-eg Use none? instead of count.zero?](https://github.com/mbleigh/acts-as-taggable-on/pull/1030)
### [v7.0.0) / 2020-12-31](https://github.com/mbleigh/acts-as-taggable-on/compare/v6.5.0...v7.0.0)
* Features
* [@kvokka Rails 6.1 support](https://github.com/mbleigh/acts-as-taggable-on/pull/1013)
* Fixes
* [@nbulaj Add support for Ruby 2.7 and it's kwargs](https://github.com/mbleigh/acts-as-taggable-on/pull/999)
* [@Andythurlow @endorfin case sensitivity fix for tagged_with](https://github.com/mbleigh/acts-as-taggable-on/pull/965)
### [6.5.0 / 2019-11-07](https://github.com/mbleigh/acts-as-taggable-on/compare/v6.0.0...v6.5.0)
* Features
* [@mizukami234 @junmoka Make table names configurable](https://github.com/mbleigh/acts-as-taggable-on/pull/910)
* [@damianlegawiec Rails 6.0.0.beta1 support](https://github.com/mbleigh/acts-as-taggable-on/pull/937)
* Fixes
* [@tonyta Avoid overriding user-defined columns cache methods](https://github.com/mbleigh/acts-as-taggable-on/pull/911)
* [@hengwoon tags_count only need to join on the taggable's table if using STI](https://github.com/mbleigh/acts-as-taggable-on/pull/904)
* [@bduran82 Avoid unnecessary queries when finding or creating tags](https://github.com/mbleigh/acts-as-taggable-on/pull/839)
* [@iiwo simplify relation options syntax](https://github.com/mbleigh/acts-as-taggable-on/pull/940)
* Misc
* [@gssbzn Remove legacy code for an empty query and replace it with ` ActiveRecord::none`](https://github.com/mbleigh/acts-as-taggable-on/pull/906)
* [@iiwo remove unneeded spec case](https://github.com/mbleigh/acts-as-taggable-on/pull/941)
* Documentation
* [@tonyta Cleanup CHANGELOG.md formatting and references](https://github.com/mbleigh/acts-as-taggable-on/pull/913)
### [6.0.0 / 2018-06-19](https://github.com/mbleigh/acts-as-taggable-on/compare/v5.0.0...v6.0.0)
* Breaking Changes
* [@Fodoj Drop support for Rails 4.2](https://github.com/mbleigh/acts-as-taggable-on/pull/887)
* Features
* [@CalvertYang Add support for uuid primary keys](https://github.com/mbleigh/acts-as-taggable-on/pull/898)
* [@Fodoj Support Rails 5.2](https://github.com/mbleigh/acts-as-taggable-on/pull/887)
* Fixes
* [@tekniklr matches_attribute was not being used in tag_match_type](https://github.com/mbleigh/acts-as-taggable-on/issues/869)
### [5.0.0 / 2017-05-18](https://github.com/mbleigh/acts-as-taggable-on/compare/v4.0.0...v5.0.0)
* Breaking Changes
* [@seuros Drop support for old version of ActiveRecord and Ruby and prepare rel](https://github.com/mbleigh/acts-as-taggable-on/pull/828)
* Features
* [@rbritom Tagged with rewrite](https://github.com/mbleigh/acts-as-taggable-on/pull/829)
* [@fearenales Due to database collisions, retry finding or creating a tag](https://github.com/mbleigh/acts-as-taggable-on/pull/809)
* [@brilyuhns Add owner_tags method to taggable](https://github.com/mbleigh/acts-as-taggable-on/pull/771)
* [@brilyuhns upport array of contexts in owner_tags_on method](https://github.com/mbleigh/acts-as-taggable-on/pull/771)
* [@brilyuhns Add specs for owner_tags_on and owner_tags methods](https://github.com/mbleigh/acts-as-taggable-on/pull/771)
* Fixes
* [@rbritom bump ruby versions for travis](https://github.com/mbleigh/acts-as-taggable-on/pull/825)
* [@mnrk Fixed Rails 5.1 deprecation message, has_many needs String value for](https://github.com/mbleigh/acts-as-taggable-on/pull/813)
* [@ProGM ProGM Adding a test to demonstrate the bug](https://github.com/mbleigh/acts-as-taggable-on/pull/806)
* [@ProGM ProGM Ensure that `caching_tag_list_on?` is injected before using it](https://github.com/mbleigh/acts-as-taggable-on/pull/806)
* [@ProGM ProGM Fix insert query for postgresql. Move schema definition in schema.rb](https://github.com/mbleigh/acts-as-taggable-on/pull/806)
* [@amatsuda assigned but unused variable - any](https://github.com/mbleigh/acts-as-taggable-on/pull/787)
* [@gmcnaughton Fix incorrect call of 'self.class' on methods which are already class](https://github.com/mbleigh/acts-as-taggable-on/pull/782)
* [@gmcnaughton Fixed #712 (incompatibility with ActiveRecord::Sanitization#quoted_id)](https://github.com/mbleigh/acts-as-taggable-on/pull/782)
* [@arpitchauhan Guard against indexes already existing](https://github.com/mbleigh/acts-as-taggable-on/pull/779)
* [@arpitchauhan Rename migration to avoid conflicts](https://github.com/mbleigh/acts-as-taggable-on/pull/774)
* [@lukeasrodgers "Bugfix `TagList#concat` with non-duplicates."](https://github.com/mbleigh/acts-as-taggable-on/pull/729)
* [@fabn Revert "Added missed indexes."](https://github.com/mbleigh/acts-as-taggable-on/pull/709)
* Documentation
* [@logicminds Adds a table of contents to the readme and contributing files](https://github.com/mbleigh/acts-as-taggable-on/pull/803)
* [@ashishg-qburst Fix typo in README](https://github.com/mbleigh/acts-as-taggable-on/pull/800)
* [@praveenangyan Update README.md](https://github.com/mbleigh/acts-as-taggable-on/pull/798)
* [@colemerrick update finding tagged objects in readme](https://github.com/mbleigh/acts-as-taggable-on/pull/794)
* [@jaredbeck Help people upgrade to 4.0.0](https://github.com/mbleigh/acts-as-taggable-on/pull/784)
* [@vasinov Update README.md](https://github.com/mbleigh/acts-as-taggable-on/pull/776)
### [4.0.0 / 2016-08-08](https://github.com/mbleigh/acts-as-taggable-on/compare/v3.5.0...v4.0.0)
* Breaking Changes
* [@krzysiek1507 drop support for Ruby < 2](https://github.com/mbleigh/acts-as-taggable-on/pull/758)
* [@krzysiek1507 drop support for Rails < 4](https://github.com/mbleigh/acts-as-taggable-on/pull/757)
* Features
* [@jessieay Rails 5](https://github.com/mbleigh/acts-as-taggable-on/pull/763)
* Fixes
* [@rikettsie #623 collation parameter is ignored if it generates an exception](https://github.com/mbleigh/acts-as-taggable-on/pull/650)
* [@bwvoss References working parser in deprecation warning](https://github.com/mbleigh/acts-as-taggable-on/pull/659)
* [@jh125486 Updated tagging_contexts to include dynamic contexts](https://github.com/mbleigh/acts-as-taggable-on/pull/660)
* [@jh125486 Fixed wildcard test (postgres returning rows with unexpected order)](https://github.com/mbleigh/acts-as-taggable-on/pull/660)
* [@FlowerWrong Add rails 5.0.0 alpha support, not hack rails <5](https://github.com/mbleigh/acts-as-taggable-on/pull/673)
* [@ryanfox1985 Added missed indexes.](https://github.com/mbleigh/acts-as-taggable-on/pull/682)
* [@zapnap scope tags to specific tagging](https://github.com/mbleigh/acts-as-taggable-on/pull/697)
* [@amatsuda method redefined](https://github.com/mbleigh/acts-as-taggable-on/pull/715)
* [@klacointe Rails 5: Tagger is optional in Tagging relation](https://github.com/mbleigh/acts-as-taggable-on/pull/720)
* [@mark-jacobs Update clean! method to use case insensitive uniq! when strict_case_match false](https://github.com/mbleigh/acts-as-taggable-on/commit/90c86994b70a399b8b1cbc0ae88835e14d6aadfc)
* [@lukeasrodgers BugFix flackey time test](https://github.com/mbleigh/acts-as-taggable-on/pull/727)
* [@pcupueran Add rspec tests for context scopes for tagging_spec](https://github.com/mbleigh/acts-as-taggable-on/pull/740)
* [@emerson-h Remove existing selects from relation](https://github.com/mbleigh/acts-as-taggable-on/pull/743)
* [@keerthisiv fix issue with custom delimiter](https://github.com/mbleigh/acts-as-taggable-on/pull/748)
* [@priyank-gupta specify tag table name for mysql collation query](https://github.com/mbleigh/acts-as-taggable-on/pull/760)
* [@seuros Remove warning messages](https://github.com/mbleigh/acts-as-taggable-on/commit/cda08c764b07a18b8582b948d1c5b3910a376965)
* [@rbritom Fix migration, #references already adds index](https://github.com/mbleigh/acts-as-taggable-on/commit/95f743010954b6b738a6e8c17315112c878f7a81)
* [@rbritom Fix deprecation warning](https://github.com/mbleigh/acts-as-taggable-on/commit/62e4a6fa74ae3faed615683cd3ad5b5cdacf5c96)
* [@rbritom fix scope array arguments](https://github.com/mbleigh/acts-as-taggable-on/commit/a415a8d6367b2e91bd7e363589135f953929b8cc)
* [@seuros Remove more deprecations](https://github.com/mbleigh/acts-as-taggable-on/commit/05794170f64f8bf250b34d2d594e368721009278)
* [@lukeasrodgers Bugfix `TagList#concat` with non-duplicates.](https://github.com/mbleigh/acts-as-taggable-on/commit/2c6214f0ddf8c6440ab81eec04d1fbf9d97c8826)
* [@seuros clean! should return self.](https://github.com/mbleigh/acts-as-taggable-on/commit/c739422f56f8ff37e3f321235e74997422a1c980)
* [@rbritom re-enable appraisals](https://github.com/mbleigh/acts-as-taggable-on/commit/0ca1f1c5b059699c683a28b522e86a3d5cd7639e)
* [@rbritom remove index conditionally on up method.](https://github.com/mbleigh/acts-as-taggable-on/commit/9cc580e7f88164634eb10c8826e5b30ea0e00544)
* [@rbritom add index on down method . ](https://github.com/mbleigh/acts-as-taggable-on/pull/767)
* [@rbritom remove index conditionally on up method](https://github.com/mbleigh/acts-as-taggable-on/commit/9cc580e7f88164634eb10c8826e5b30ea0e00544)
* Documentation
* [@logicminds Adds table of contents using doctoc utility](https://github.com/mbleigh/acts-as-taggable-on/pull/803)
* [@jamesprior Changing ActsAsTaggable to ActsAsTaggableOn](https://github.com/mbleigh/acts-as-taggable-on/pull/637)
* [@markgandolfo Update README.md](https://github.com/mbleigh/acts-as-taggable-on/pull/645))
* [@snowblink Update release date for 3.5.0](https://github.com/mbleigh/acts-as-taggable-on/pull/647)
* [@AlexVPopov Update README.md](https://github.com/mbleigh/acts-as-taggable-on/pull/671)
* [@schnmudgal README.md, Improve documentation for Tag Ownership](https://github.com/mbleigh/acts-as-taggable-on/pull/706)
### [3.5.0 / 2015-03-03](https://github.com/mbleigh/acts-as-taggable-on/compare/v3.4.4...v3.5.0)
* Fixes
* [@rikettsie Fixed collation for MySql via rake rule or config parameter](https://github.com/mbleigh/acts-as-taggable-on/pull/634)
* Misc
* [@pcupueran Add rspec test for tagging_spec completeness]()
### [3.4.4 / 2015-02-11](https://github.com/mbleigh/acts-as-taggable-on/compare/v3.4.3...v3.4.4)
* Fixes
* [@d4rky-pl Add context constraint to find_related_* methods](https://github.com/mbleigh/acts-as-taggable-on/pull/629)
### [3.4.3 / 2014-09-26](https://github.com/mbleigh/acts-as-taggable-on/compare/v3.4.2...v3.4.3)
* Fixes
* [@warp clears column cache on reset_column_information resolves](https://github.com/mbleigh/acts-as-taggable-on/issues/621)
### [3.4.2 / 2014-09-26](https://github.com/mbleigh/acts-as-taggable-on/compare/v3.4.1...v3.4.2)
* Fixes
* [@stiff fixed tagged_with :any in postgresql](https://github.com/mbleigh/acts-as-taggable-on/pull/570)
* [@jerefrer fixed encoding in mysql](https://github.com/mbleigh/acts-as-taggable-on/pull/588)
* [@markedmondson Ensure taggings context aliases are maintained when joining multiple taggables](https://github.com/mbleigh/acts-as-taggable-on/pull/589)
### [3.4.1 / 2014-09-01](https://github.com/mbleigh/acts-as-taggable-on/compare/v3.4.0...v3.4.1)
* Fixes
* [@konukhov fix owned ordered taggable bug](https://github.com/mbleigh/acts-as-taggable-on/pull/585)
### [3.4.0 / 2014-08-29](https://github.com/mbleigh/acts-as-taggable-on/compare/v3.3.0...v3.4.0)
* Features
* [@ProGM Support for custom parsers for tags](https://github.com/mbleigh/acts-as-taggable-on/pull/579)
* [@lolaodelola #577 Popular feature](https://github.com/mbleigh/acts-as-taggable-on/pull/577)
* Fixes
* [@twalpole Update for rails edge (4.2)](https://github.com/mbleigh/acts-as-taggable-on/pull/583)
* Performance
* [@ashanbrown #584 Use pluck instead of select](https://github.com/mbleigh/acts-as-taggable-on/pull/584)
### [3.3.0 / 2014-07-08](https://github.com/mbleigh/acts-as-taggable-on/compare/v3.2.6...v3.3.0)
* Features
* [@felipeclopes #488 Support for `start_at` and `end_at` restrictions when selecting tags](https://github.com/mbleigh/acts-as-taggable-on/pull/488)
* Fixes
* [@tonytonyjan #560 Fix for `ActsAsTaggableOn.remove_unused_tags` doesn't work](https://github.com/mbleigh/acts-as-taggable-on/pull/560)
* [@TheLarkInn #555 Fix for `tag_cloud` helper to generate correct css tags](https://github.com/mbleigh/acts-as-taggable-on/pull/555)
* Performance
* [@pcai #556 Add back taggables index in the taggins table](https://github.com/mbleigh/acts-as-taggable-on/pull/556)
### [3.2.6 / 2014-05-28](https://github.com/mbleigh/acts-as-taggable-on/compare/v3.2.5...v3.2.6)
* Fixes
* [@seuros #548 Fix dirty marking when tags are not ordered](https://github.com/mbleigh/acts-as-taggable-on/issues/548)
* Misc
* [@seuros Remove actionpack dependency](https://github.com/mbleigh/acts-as-taggable-on/commit/5d20e0486c892fbe21af42fdcd79d0b6ebe87ed4)
* [@seuros #547 Add tests for update_attributes](https://github.com/mbleigh/acts-as-taggable-on/issues/547)
### [3.2.5 / 2014-05-25](https://github.com/mbleigh/acts-as-taggable-on/compare/v3.2.4...v3.2.5)
* Fixes
* [@seuros #546 Fix autoload bug. Now require engine file instead of autoloading it](https://github.com/mbleigh/acts-as-taggable-on/issues/546)
### [3.2.4 / 2014-05-24](https://github.com/mbleigh/acts-as-taggable-on/compare/v3.2.3...v3.2.4)
* Fixes
* [@seuros #544 Fix incorrect query generation related to `GROUP BY` SQL statement](https://github.com/mbleigh/acts-as-taggable-on/issues/544)
* Misc
* [@seuros #545 Remove `ammeter` development dependency](https://github.com/mbleigh/acts-as-taggable-on/pull/545)
* [@seuros #545 Deprecate `TagList.from` in favor of `TagListParser.parse`](https://github.com/mbleigh/acts-as-taggable-on/pull/545)
* [@seuros #543 Introduce lazy loading](https://github.com/mbleigh/acts-as-taggable-on/pull/543)
* [@seuros #541 Deprecate ActsAsTaggableOn::Utils](https://github.com/mbleigh/acts-as-taggable-on/pull/541)
### [3.2.3 / 2014-05-16](https://github.com/mbleigh/acts-as-taggable-on/compare/v3.2.2...v3.2.3)
* Fixes
* [@seuros #540 Fix for tags removal (it was affecting all records with the same tag)](https://github.com/mbleigh/acts-as-taggable-on/pull/540)
* [@akicho8 #535 Fix for `options` Hash passed to methods from being deleted by those methods](https://github.com/mbleigh/acts-as-taggable-on/pull/535)
### [3.2.2 / 2014-05-07](https://github.com/mbleigh/acts-as-taggable-on/compare/v3.2.1...v3.2.2)
* Breaking Changes
* [@seuros #526 Taggable models are not extended with ActsAsTaggableOn::Utils anymore](https://github.com/mbleigh/acts-as-taggable-on/pull/526)
* Fixes
* [@seuros #536 Add explicit conversion of tags to strings (when assigning tags)](https://github.com/mbleigh/acts-as-taggable-on/pull/536)
* Misc
* [@seuros #526 Delete outdated benchmark script](https://github.com/mbleigh/acts-as-taggable-on/pull/526)
* [@seuros #525 Fix tests so that they pass with MySQL](https://github.com/mbleigh/acts-as-taggable-on/pull/525)
### [3.2.1 / 2014-05-06](https://github.com/mbleigh/acts-as-taggable-on/compare/v3.2.0...v3.2.1)
* Misc
* [@seuros #523 Run tests loading only ActiveRecord (without the full Rails stack)](https://github.com/mbleigh/acts-as-taggable-on/pull/523)
* [@seuros #523 Remove activesupport dependency](https://github.com/mbleigh/acts-as-taggable-on/pull/523)
* [@seuros #523 Introduce database_cleaner in specs](https://github.com/mbleigh/acts-as-taggable-on/pull/523)
* [@seuros #520 Tag_list cleanup](https://github.com/mbleigh/acts-as-taggable-on/pull/520)
### [3.2.0 / 2014-05-01](https://github.com/mbleigh/acts-as-taggable-on/compare/v3.1.1...v3.2.0)
* Breaking Changes
* ActsAsTaggableOn::Tag is not extend with ActsAsTaggableOn::Utils anymore
* Features
* [@ches #413 Hook to support STI subclasses of Tag in save_tags](https://github.com/mbleigh/acts-as-taggable-on/pull/413)
* Fixes
* [@jdelStrother #515 Rename Compatibility methods to reduce chance of conflicts](https://github.com/mbleigh/acts-as-taggable-on/pull/515)
* [@seuros #512 fix for << method](https://github.com/mbleigh/acts-as-taggable-on/pull/512)
* [@sonots #510 fix IN subquery error for mysql](https://github.com/mbleigh/acts-as-taggable-on/pull/510)
* [@jonseaberg #499 fix for race condition when multiple processes try to add the same tag](https://github.com/mbleigh/acts-as-taggable-on/pull/499)
* [@leklund #496 Fix for distinct and postgresql json columns errors](https://github.com/mbleigh/acts-as-taggable-on/pull/496)
* [@thatbettina & @plexus #394 Multiple quoted tags](https://github.com/mbleigh/acts-as-taggable-on/pull/394)
* Performance
* Misc
* [@seuros #511 Rspec 3](https://github.com/mbleigh/acts-as-taggable-on/pull/511)
### [3.1.0 / 2014-03-31](https://github.com/mbleigh/acts-as-taggable-on/compare/v3.0.1...v3.1.0)
* Fixes
* [@mikehale #487 Match_all respects context](https://github.com/mbleigh/acts-as-taggable-on/pull/487)
* Performance
* [@dgilperez #390 Add taggings counter cache](https://github.com/mbleigh/acts-as-taggable-on/pull/390)
* Misc
* [@jonseaberg Add missing indexes to schema used in specs #474](https://github.com/mbleigh/acts-as-taggable-on/pull/474)
* [@seuros Specify Ruby >= 1.9.3 required in gemspec](https://github.com/mbleigh/acts-as-taggable-on/pull/502)
* [@kiasaki Add missing quotes to code example](https://github.com/mbleigh/acts-as-taggable-on/pull/501)
### [3.1.0.rc1 / 2014-02-26](https://github.com/mbleigh/acts-as-taggable-on/compare/v3.0.1...v3.1.0.rc1)
* Features
* [@Burkazoid #467 Add :order_by_matching_tag_count option](https://github.com/mbleigh/acts-as-taggable-on/pull/469)
* Fixes
* [@rafael #406 Dirty attributes not correctly derived](https://github.com/mbleigh/acts-as-taggable-on/pull/406)
* [@BenZhang #440 Did not respect strict_case_match](https://github.com/mbleigh/acts-as-taggable-on/pull/440)
* [@znz #456 Fix breaking encoding of tag](https://github.com/mbleigh/acts-as-taggable-on/pull/456)
* [@rgould #417 Let '.count' work when tagged_with is accompanied by a group clause](https://github.com/mbleigh/acts-as-taggable-on/pull/417)
* [@developer88 #461 Move 'Distinct' out of select string and use .uniq instead](https://github.com/mbleigh/acts-as-taggable-on/pull/461)
* [@gerard-leijdekkers #473 Fixed down migration index name](https://github.com/mbleigh/acts-as-taggable-on/pull/473)
* [@leo-souza #498 Use database's lower function for case-insensitive match](https://github.com/mbleigh/acts-as-taggable-on/pull/498)
* Misc
* [@billychan #463 Thread safe support](https://github.com/mbleigh/acts-as-taggable-on/pull/463)
* [@billychan #386 Add parse:true instructions to README](https://github.com/mbleigh/acts-as-taggable-on/pull/386)
* [@seuros #449 Improve README/UPGRADING/post install docs](https://github.com/mbleigh/acts-as-taggable-on/pull/449)
* [@seuros #452 Remove I18n deprecation warning in specs](https://github.com/mbleigh/acts-as-taggable-on/pull/452)
* [@seuros #453 Test against Ruby 2.1 on Travis CI](https://github.com/mbleigh/acts-as-taggable-on/pull/453)
* [@takashi #454 Clarify example in docs](https://github.com/mbleigh/acts-as-taggable-on/pull/454)
### [3.0.1 / 2014-01-08](https://github.com/mbleigh/acts-as-taggable-on/compare/v3.0.0...v3.0.1)
* Fixes
* [@rafael #406 Dirty attributes not correctly derived](https://github.com/mbleigh/acts-as-taggable-on/pull/406)
* [@BenZhang #440 Did not respect strict_case_match](https://github.com/mbleigh/acts-as-taggable-on/pull/440)
* [@znz #456 Fix breaking encoding of tag](https://github.com/mbleigh/acts-as-taggable-on/pull/456)
* Misc
* [@billychan #386 Add parse:true instructions to README](https://github.com/mbleigh/acts-as-taggable-on/pull/386)
* [@seuros #449 Improve README/UPGRADING/post install docs](https://github.com/mbleigh/acts-as-taggable-on/pull/449)
* [@seuros #452 Remove I18n deprecation warning in specs](https://github.com/mbleigh/acts-as-taggable-on/pull/452)
* [@seuros #453 Test against Ruby 2.1 on Travis CI](https://github.com/mbleigh/acts-as-taggable-on/pull/453)
* [@takashi #454 Clarify example in docs](https://github.com/mbleigh/acts-as-taggable-on/pull/454)
### [3.0.0 / 2014-01-01](https://github.com/mbleigh/acts-as-taggable-on/compare/v2.4.1...v3.0.0)
* Breaking Changes
* No longer supports Ruby 1.8.
* Features
* Supports Rails 4.1.
* Misc (TODO: expand)
* [@zquestz #359](https://github.com/mbleigh/acts-as-taggable-on/pull/359)
* [@rsl #367](https://github.com/mbleigh/acts-as-taggable-on/pull/367)
* [@ktdreyer #383](https://github.com/mbleigh/acts-as-taggable-on/pull/383)
* [@cwoodcox #346](https://github.com/mbleigh/acts-as-taggable-on/pull/346)
* [@mrb #421](https://github.com/mbleigh/acts-as-taggable-on/pull/421)
* [@bf4 #430](https://github.com/mbleigh/acts-as-taggable-on/pull/430)
* [@sanemat #368](https://github.com/mbleigh/acts-as-taggable-on/pull/368)
* [@bf4 #343](https://github.com/mbleigh/acts-as-taggable-on/pull/343)
* [@marclennox #429](https://github.com/mbleigh/acts-as-taggable-on/pull/429)
* [@shekibobo #403](https://github.com/mbleigh/acts-as-taggable-on/pull/403)
* [@ches @ktdreyer #410](https://github.com/mbleigh/acts-as-taggable-on/pull/410)
* [@makaroni4 #371](https://github.com/mbleigh/acts-as-taggable-on/pull/371)
* [kenzai @davidstosik @awt #431](https://github.com/mbleigh/acts-as-taggable-on/pull/431)
* [@bf4 @joelcogen @shekibobo @aaronchi #438](https://github.com/mbleigh/acts-as-taggable-on/pull/438)
* [@seuros #442](https://github.com/mbleigh/acts-as-taggable-on/pull/442)
* [@bf4 #445](https://github.com/mbleigh/acts-as-taggable-on/pull/445)
* [@eagletmt #446](https://github.com/mbleigh/acts-as-taggable-on/pull/446)
### 3.0.0.rc2 [changes](https://github.com/mbleigh/acts-as-taggable-on/compare/fork-v3.0.0.rc1...fork-v3.0.0.rc2)
### 3.0.0.rc1 [changes](https://github.com/mbleigh/acts-as-taggable-on/compare/v2.4.1...fork-v3.0.0.rc1)
### [2.4.1 / 2013-05-07](https://github.com/mbleigh/acts-as-taggable-on/compare/v2.4.0...v2.4.1)
* Features
* Fixes
* Misc