acts-as-taggable-on-13.0.0/0000755000004100000410000000000015103536225015336 5ustar www-datawww-dataacts-as-taggable-on-13.0.0/acts-as-taggable-on.gemspec0000644000004100000410000000634615103536225022425 0ustar www-datawww-data######################################################### # This file has been automatically generated by gem2tgz # ######################################################### # -*- encoding: utf-8 -*- # stub: acts-as-taggable-on 13.0.0 ruby lib Gem::Specification.new do |s| s.name = "acts-as-taggable-on".freeze s.version = "13.0.0".freeze s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version= s.metadata = { "changelog_uri" => "https://github.com/mbleigh/acts-as-taggable-on/blob/master/CHANGELOG.md", "rubygems_mfa_required" => "true" } if s.respond_to? :metadata= s.require_paths = ["lib".freeze] s.authors = ["Michael Bleigh".freeze, "Joost Baaij".freeze] s.date = "1980-01-02" s.description = "With ActsAsTaggableOn, you can tag a single model on several contexts, such as skills, interests, and awards. It also provides other advanced functionality.".freeze s.email = ["michael@intridea.com".freeze, "joost@spacebabies.nl".freeze] s.files = ["LICENSE.md".freeze, "db/migrate/1_acts_as_taggable_on_migration.rb".freeze, "db/migrate/2_add_missing_unique_indices.rb".freeze, "db/migrate/3_add_taggings_counter_cache_to_tags.rb".freeze, "db/migrate/4_add_missing_taggable_index.rb".freeze, "db/migrate/5_change_collation_for_tag_names.rb".freeze, "db/migrate/6_add_missing_indexes_on_taggings.rb".freeze, "db/migrate/7_add_tenant_to_taggings.rb".freeze, "lib/acts-as-taggable-on.rb".freeze, "lib/acts-as-taggable-on/default_parser.rb".freeze, "lib/acts-as-taggable-on/engine.rb".freeze, "lib/acts-as-taggable-on/generic_parser.rb".freeze, "lib/acts-as-taggable-on/tag.rb".freeze, "lib/acts-as-taggable-on/tag_list.rb".freeze, "lib/acts-as-taggable-on/taggable.rb".freeze, "lib/acts-as-taggable-on/taggable/caching.rb".freeze, "lib/acts-as-taggable-on/taggable/collection.rb".freeze, "lib/acts-as-taggable-on/taggable/core.rb".freeze, "lib/acts-as-taggable-on/taggable/ownership.rb".freeze, "lib/acts-as-taggable-on/taggable/related.rb".freeze, "lib/acts-as-taggable-on/taggable/tag_list_type.rb".freeze, "lib/acts-as-taggable-on/taggable/tagged_with_query.rb".freeze, "lib/acts-as-taggable-on/taggable/tagged_with_query/all_tags_query.rb".freeze, "lib/acts-as-taggable-on/taggable/tagged_with_query/any_tags_query.rb".freeze, "lib/acts-as-taggable-on/taggable/tagged_with_query/exclude_tags_query.rb".freeze, "lib/acts-as-taggable-on/taggable/tagged_with_query/query_base.rb".freeze, "lib/acts-as-taggable-on/tagger.rb".freeze, "lib/acts-as-taggable-on/tagging.rb".freeze, "lib/acts-as-taggable-on/tags_helper.rb".freeze, "lib/acts-as-taggable-on/utils.rb".freeze, "lib/acts-as-taggable-on/version.rb".freeze, "lib/tasks/example/acts-as-taggable-on.rb.example".freeze, "lib/tasks/install_initializer.rake".freeze, "lib/tasks/tags_collate_utf8.rake".freeze] s.homepage = "https://github.com/mbleigh/acts-as-taggable-on".freeze s.licenses = ["MIT".freeze] s.required_ruby_version = Gem::Requirement.new(">= 3.1.0".freeze) s.rubygems_version = "3.6.9".freeze s.summary = "Advanced tagging for Rails.".freeze s.specification_version = 4 s.add_runtime_dependency(%q.freeze, [">= 7.1".freeze, "< 8.2".freeze]) s.add_runtime_dependency(%q.freeze, [">= 2.4".freeze, "< 3.0".freeze]) end acts-as-taggable-on-13.0.0/db/0000755000004100000410000000000015103536225015723 5ustar www-datawww-dataacts-as-taggable-on-13.0.0/db/migrate/0000755000004100000410000000000015103536225017353 5ustar www-datawww-dataacts-as-taggable-on-13.0.0/db/migrate/7_add_tenant_to_taggings.rb0000644000004100000410000000067615103536225024625 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-13.0.0/db/migrate/4_add_missing_taggable_index.rb0000644000004100000410000000055515103536225025426 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-13.0.0/db/migrate/6_add_missing_indexes_on_taggings.rb0000644000004100000410000000264415103536225026512 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-13.0.0/db/migrate/2_add_missing_unique_indices.rb0000644000004100000410000000170715103536225025473 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-13.0.0/db/migrate/3_add_taggings_counter_cache_to_tags.rb0000644000004100000410000000073715103536225027146 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-13.0.0/db/migrate/5_change_collation_for_tag_names.rb0000644000004100000410000000060015103536225026275 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-13.0.0/db/migrate/1_acts_as_taggable_on_migration.rb0000644000004100000410000000175415103536225026137 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-13.0.0/lib/0000755000004100000410000000000015103536225016104 5ustar www-datawww-dataacts-as-taggable-on-13.0.0/lib/tasks/0000755000004100000410000000000015103536225017231 5ustar www-datawww-dataacts-as-taggable-on-13.0.0/lib/tasks/install_initializer.rake0000644000004100000410000000103415103536225024144 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-13.0.0/lib/tasks/example/0000755000004100000410000000000015103536225020664 5ustar www-datawww-dataacts-as-taggable-on-13.0.0/lib/tasks/example/acts-as-taggable-on.rb.example0000644000004100000410000000057415103536225026362 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-13.0.0/lib/tasks/tags_collate_utf8.rake0000644000004100000410000000120115103536225023476 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-13.0.0/lib/acts-as-taggable-on/0000755000004100000410000000000015103536225021615 5ustar www-datawww-dataacts-as-taggable-on-13.0.0/lib/acts-as-taggable-on/tagger.rb0000644000004100000410000000465415103536225023424 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-13.0.0/lib/acts-as-taggable-on/tag.rb0000644000004100000410000000724015103536225022720 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}?", name.to_s]) else where(['LOWER(name) = LOWER(?)', name.to_s.downcase]) end end def self.named_any(list) clause = list.map do |tag| sanitize_sql_for_named_any(tag) 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 str.to_s.downcase end end def binary ActsAsTaggableOn::Utils.using_mysql? ? 'BINARY ' : nil end def sanitize_sql_for_named_any(tag) if ActsAsTaggableOn.strict_case_match sanitize_sql(["name = #{binary}?", tag.to_s]) else sanitize_sql(['LOWER(name) = LOWER(?)', tag.to_s.downcase]) end end end end end acts-as-taggable-on-13.0.0/lib/acts-as-taggable-on/tagging.rb0000644000004100000410000000237015103536225023564 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-13.0.0/lib/acts-as-taggable-on/engine.rb0000644000004100000410000000014015103536225023402 0ustar www-datawww-data# frozen_string_literal: true module ActsAsTaggableOn class Engine < Rails::Engine end end acts-as-taggable-on-13.0.0/lib/acts-as-taggable-on/taggable.rb0000644000004100000410000000636615103536225023723 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-13.0.0/lib/acts-as-taggable-on/tags_helper.rb0000644000004100000410000000067215103536225024444 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-13.0.0/lib/acts-as-taggable-on/taggable/0000755000004100000410000000000015103536225023363 5ustar www-datawww-dataacts-as-taggable-on-13.0.0/lib/acts-as-taggable-on/taggable/tag_list_type.rb0000644000004100000410000000021415103536225026554 0ustar www-datawww-data# frozen_string_literal: true module ActsAsTaggableOn module Taggable class TagListType < ActiveModel::Type::Value end end end acts-as-taggable-on-13.0.0/lib/acts-as-taggable-on/taggable/related.rb0000644000004100000410000000743415103536225025340 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-13.0.0/lib/acts-as-taggable-on/taggable/ownership.rb0000644000004100000410000001111415103536225025724 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-13.0.0/lib/acts-as-taggable-on/taggable/tagged_with_query/0000755000004100000410000000000015103536225027076 5ustar www-datawww-dataacts-as-taggable-on-13.0.0/lib/acts-as-taggable-on/taggable/tagged_with_query/any_tags_query.rb0000644000004100000410000000542315103536225032461 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-13.0.0/lib/acts-as-taggable-on/taggable/tagged_with_query/query_base.rb0000644000004100000410000000513015103536225031561 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-13.0.0/lib/acts-as-taggable-on/taggable/tagged_with_query/exclude_tags_query.rb0000644000004100000410000000567315103536225033332 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-13.0.0/lib/acts-as-taggable-on/taggable/tagged_with_query/all_tags_query.rb0000644000004100000410000001035415103536225032441 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-13.0.0/lib/acts-as-taggable-on/taggable/core.rb0000644000004100000410000003166015103536225024646 0ustar www-datawww-data# frozen_string_literal: true 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-13.0.0/lib/acts-as-taggable-on/taggable/collection.rb0000644000004100000410000002377515103536225026061 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-13.0.0/lib/acts-as-taggable-on/taggable/caching.rb0000644000004100000410000000235715103536225025313 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-13.0.0/lib/acts-as-taggable-on/taggable/tagged_with_query.rb0000644000004100000410000000112415103536225027421 0ustar www-datawww-data# frozen_string_literal: true 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-13.0.0/lib/acts-as-taggable-on/utils.rb0000644000004100000410000000150015103536225023276 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-13.0.0/lib/acts-as-taggable-on/generic_parser.rb0000644000004100000410000000074415103536225025137 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-13.0.0/lib/acts-as-taggable-on/tag_list.rb0000644000004100000410000000540715103536225023756 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.to_s.downcase } 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-13.0.0/lib/acts-as-taggable-on/version.rb0000644000004100000410000000012015103536225023620 0ustar www-datawww-data# frozen_string_literal: true module ActsAsTaggableOn VERSION = '13.0.0' end acts-as-taggable-on-13.0.0/lib/acts-as-taggable-on/default_parser.rb0000644000004100000410000000475115103536225025151 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-13.0.0/lib/acts-as-taggable-on.rb0000644000004100000410000000617215103536225022150 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-13.0.0/LICENSE.md0000644000004100000410000000207015103536225016741 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.