acts-as-taggable-on-11.0.0/0000755000004100000410000000000014704600021015324 5ustar www-datawww-dataacts-as-taggable-on-11.0.0/.gitignore0000644000004100000410000000017414704600021017316 0ustar www-datawww-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.gemspec0000644000004100000410000000237414704600021022410 0ustar www-datawww-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.md0000644000004100000410000000521414704600021017557 0ustar www-datawww-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/0000755000004100000410000000000014704600021015711 5ustar www-datawww-dataacts-as-taggable-on-11.0.0/db/migrate/0000755000004100000410000000000014704600021017341 5ustar www-datawww-dataacts-as-taggable-on-11.0.0/db/migrate/7_add_tenant_to_taggings.rb0000644000004100000410000000067614704600021024613 0ustar www-datawww-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.rb0000644000004100000410000000055514704600021025414 0ustar www-datawww-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.rb0000644000004100000410000000264414704600021026500 0ustar www-datawww-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.rb0000644000004100000410000000170714704600021025461 0ustar www-datawww-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.rb0000644000004100000410000000073714704600021027134 0ustar www-datawww-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.rb0000644000004100000410000000060014704600021026263 0ustar www-datawww-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.rb0000644000004100000410000000175414704600021026125 0ustar www-datawww-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/0000755000004100000410000000000014704600021016664 5ustar www-datawww-dataacts-as-taggable-on-11.0.0/.github/workflows/0000755000004100000410000000000014704600021020721 5ustar www-datawww-dataacts-as-taggable-on-11.0.0/.github/workflows/spec.yml0000644000004100000410000000377714704600021022414 0ustar www-datawww-dataname: 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/Guardfile0000644000004100000410000000024014704600021017145 0ustar www-datawww-dataguard '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/0000755000004100000410000000000014704600021016072 5ustar www-datawww-dataacts-as-taggable-on-11.0.0/lib/tasks/0000755000004100000410000000000014704600021017217 5ustar www-datawww-dataacts-as-taggable-on-11.0.0/lib/tasks/install_initializer.rake0000644000004100000410000000103414704600021024132 0ustar www-datawww-datanamespace :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/0000755000004100000410000000000014704600021020652 5ustar www-datawww-dataacts-as-taggable-on-11.0.0/lib/tasks/example/acts-as-taggable-on.rb.example0000644000004100000410000000057414704600021026350 0ustar www-datawww-dataActsAsTaggableOn.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.rake0000644000004100000410000000120114704600021023464 0ustar www-datawww-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/0000755000004100000410000000000014704600021021603 5ustar www-datawww-dataacts-as-taggable-on-11.0.0/lib/acts-as-taggable-on/tagger.rb0000644000004100000410000000465414704600021023412 0ustar www-datawww-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.rb0000644000004100000410000000763114704600021022712 0ustar www-datawww-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.rb0000644000004100000410000000237014704600021023552 0ustar www-datawww-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.rb0000644000004100000410000000014014704600021023370 0ustar www-datawww-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.rb0000644000004100000410000000636614704600021023711 0ustar www-datawww-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.rb0000644000004100000410000000067214704600021024432 0ustar www-datawww-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/0000755000004100000410000000000014704600021023351 5ustar www-datawww-dataacts-as-taggable-on-11.0.0/lib/acts-as-taggable-on/taggable/tag_list_type.rb0000644000004100000410000000021414704600021026542 0ustar www-datawww-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.rb0000644000004100000410000000743414704600021025326 0ustar www-datawww-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.rb0000644000004100000410000001111414704600021025712 0ustar www-datawww-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/0000755000004100000410000000000014704600021027064 5ustar www-datawww-dataacts-as-taggable-on-11.0.0/lib/acts-as-taggable-on/taggable/tagged_with_query/any_tags_query.rb0000644000004100000410000000542314704600021032447 0ustar www-datawww-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.rb0000644000004100000410000000513014704600021031547 0ustar www-datawww-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.rb0000644000004100000410000000567314704600021033320 0ustar www-datawww-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.rb0000644000004100000410000001035414704600021032427 0ustar www-datawww-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.rb0000644000004100000410000003176714704600021024644 0ustar www-datawww-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.rb0000644000004100000410000002377514704600021026047 0ustar www-datawww-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.rb0000644000004100000410000000235714704600021025301 0ustar www-datawww-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.rb0000644000004100000410000000144514704600021027415 0ustar www-datawww-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.rb0000644000004100000410000000150014704600021023264 0ustar www-datawww-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.rb0000644000004100000410000000074414704600021025125 0ustar www-datawww-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.rb0000644000004100000410000000542014704600021023737 0ustar www-datawww-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.rb0000644000004100000410000000012014704600021023606 0ustar www-datawww-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.rb0000644000004100000410000000475114704600021025137 0ustar www-datawww-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.rb0000644000004100000410000000617414704600021022140 0ustar www-datawww-datarequire '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/Appraisals0000644000004100000410000000064614704600021017354 0ustar www-datawww-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' endacts-as-taggable-on-11.0.0/spec/0000755000004100000410000000000014704600021016256 5ustar www-datawww-dataacts-as-taggable-on-11.0.0/spec/acts_as_taggable_on/0000755000004100000410000000000014704600021022215 5ustar www-datawww-dataacts-as-taggable-on-11.0.0/spec/acts_as_taggable_on/single_table_inheritance_spec.rb0000644000004100000410000002240114704600021030554 0ustar www-datawww-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.rb0000644000004100000410000001342414704600021025200 0ustar www-datawww-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.rb0000644000004100000410000000315314704600021026556 0ustar www-datawww-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.rb0000644000004100000410000001226214704600021025030 0ustar www-datawww-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.rb0000644000004100000410000001337614704600021025374 0ustar www-datawww-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.rb0000644000004100000410000000163714704600021024723 0ustar www-datawww-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.rb0000644000004100000410000010201214704600021025316 0ustar www-datawww-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.rb0000644000004100000410000001020514704600021024705 0ustar www-datawww-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.rb0000644000004100000410000001003214704600021025144 0ustar www-datawww-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.rb0000644000004100000410000002276714704600021027531 0ustar www-datawww-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.rb0000644000004100000410000000737414704600021026535 0ustar www-datawww-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.rb0000644000004100000410000001045314704600021025177 0ustar www-datawww-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.rb0000644000004100000410000000070414704600021026545 0ustar www-datawww-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.rb0000644000004100000410000003051414704600021024332 0ustar www-datawww-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.rb0000644000004100000410000000243014704600021026050 0ustar www-datawww-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.rb0000644000004100000410000000075514704600021021103 0ustar www-datawww-databegin 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/0000755000004100000410000000000014704600021017772 5ustar www-datawww-dataacts-as-taggable-on-11.0.0/spec/support/0-helpers.rb0000644000004100000410000000121514704600021022115 0ustar www-datawww-datadef 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.rb0000644000004100000410000000322614704600021022066 0ustar www-datawww-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.rb0000644000004100000410000000071314704600021023555 0ustar www-datawww-dataRSpec.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.rb0000644000004100000410000000020014704600021021425 0ustar www-datawww-dataunless [].respond_to?(:freq) class Array def freq k=Hash.new(0) each { |e| k[e]+=1 } k end end endacts-as-taggable-on-11.0.0/spec/internal/0000755000004100000410000000000014704600021020072 5ustar www-datawww-dataacts-as-taggable-on-11.0.0/spec/internal/db/0000755000004100000410000000000014704600021020457 5ustar www-datawww-dataacts-as-taggable-on-11.0.0/spec/internal/db/schema.rb0000644000004100000410000000630014704600021022243 0ustar www-datawww-dataActiveRecord::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/0000755000004100000410000000000014704600021020652 5ustar www-datawww-dataacts-as-taggable-on-11.0.0/spec/internal/app/models/0000755000004100000410000000000014704600021022135 5ustar www-datawww-dataacts-as-taggable-on-11.0.0/spec/internal/app/models/columns_override_model.rb0000644000004100000410000000020114704600021027212 0ustar www-datawww-dataclass 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.rb0000644000004100000410000000040014704600021027274 0ustar www-datawww-dataif 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.rb0000644000004100000410000000014614704600021026253 0ustar www-datawww-dataclass OtherCachedModel < ActiveRecord::Base acts_as_taggable_on :languages, :statuses, :glasses end acts-as-taggable-on-11.0.0/spec/internal/app/models/untaggable_model.rb0000644000004100000410000000011414704600021025747 0ustar www-datawww-dataclass UntaggableModel < ActiveRecord::Base belongs_to :taggable_model end acts-as-taggable-on-11.0.0/spec/internal/app/models/company.rb0000644000004100000410000000054714704600021024136 0ustar www-datawww-dataclass 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.rb0000644000004100000410000000017114704600021031327 0ustar www-datawww-datarequire_relative 'taggable_model' class AlteredInheritingTaggableModel < TaggableModel acts_as_taggable_on :parts end acts-as-taggable-on-11.0.0/spec/internal/app/models/user.rb0000644000004100000410000000006514704600021023441 0ustar www-datawww-dataclass User < ActiveRecord::Base acts_as_tagger end acts-as-taggable-on-11.0.0/spec/internal/app/models/cached_model.rb0000644000004100000410000000007614704600021025054 0ustar www-datawww-dataclass CachedModel < ActiveRecord::Base acts_as_taggable end acts-as-taggable-on-11.0.0/spec/internal/app/models/market.rb0000644000004100000410000000005114704600021023741 0ustar www-datawww-dataclass Market < ActsAsTaggableOn::Tag end acts-as-taggable-on-11.0.0/spec/internal/app/models/inheriting_taggable_model.rb0000644000004100000410000000012514704600021027626 0ustar www-datawww-datarequire_relative 'taggable_model' class InheritingTaggableModel < TaggableModel end acts-as-taggable-on-11.0.0/spec/internal/app/models/taggable_model.rb0000644000004100000410000000054014704600021025407 0ustar www-datawww-dataclass 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.rb0000644000004100000410000000020314704600021026604 0ustar www-datawww-dataclass 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.rb0000644000004100000410000000035714704600021030623 0ustar www-datawww-dataclass 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.rb0000644000004100000410000000016614704600021027117 0ustar www-datawww-dataclass 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.rb0000644000004100000410000000006214704600021024146 0ustar www-datawww-datarequire_relative 'user' class Student < User end acts-as-taggable-on-11.0.0/spec/internal/config/0000755000004100000410000000000014704600021021337 5ustar www-datawww-dataacts-as-taggable-on-11.0.0/spec/internal/config/database.yml.sample0000644000004100000410000000051614704600021025110 0ustar www-datawww-datasqlite3: 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/0000755000004100000410000000000014704600021017117 5ustar www-datawww-dataacts-as-taggable-on-11.0.0/gemfiles/activerecord_7.0.gemfile0000644000004100000410000000046714704600021023516 0ustar www-datawww-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.gemfile0000644000004100000410000000046714704600021023520 0ustar www-datawww-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.gemfile0000644000004100000410000000046714704600021023517 0ustar www-datawww-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/.rspec0000644000004100000410000000002514704600021016436 0ustar www-datawww-data--colour --backtrace acts-as-taggable-on-11.0.0/Rakefile0000644000004100000410000000073014704600021016771 0ustar www-datawww-datarequire '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.md0000644000004100000410000000207014704600021016727 0ustar www-datawww-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/Gemfile0000644000004100000410000000025314704600021016617 0ustar www-datawww-datasource '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.yml0000644000004100000410000000054314704600021020763 0ustar www-datawww-dataversion: '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.md0000644000004100000410000004371214704600021016612 0ustar www-datawww-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 [![Join the chat at https://gitter.im/mbleigh/acts-as-taggable-on](https://badges.gitter.im/mbleigh/acts-as-taggable-on.svg)](https://gitter.im/mbleigh/acts-as-taggable-on?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Gem Version](https://badge.fury.io/rb/acts-as-taggable-on.svg)](http://badge.fury.io/rb/acts-as-taggable-on) [![Build Status](https://github.com/mbleigh/acts-as-taggable-on/workflows/spec/badge.svg)](https://github.com/mbleigh/acts-as-taggable-on/actions) [![Code Climate](https://codeclimate.com/github/mbleigh/acts-as-taggable-on.svg)](https://codeclimate.com/github/mbleigh/acts-as-taggable-on) [![Inline docs](http://inch-ci.org/github/mbleigh/acts-as-taggable-on.svg)](http://inch-ci.org/github/mbleigh/acts-as-taggable-on) [![Security](https://hakiri.io/github/mbleigh/acts-as-taggable-on/master.svg)](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.md0000644000004100000410000006046714704600021017152 0ustar www-datawww-dataChanges 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