no_fly_list 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
 - data/lib/generators/no_fly_list/templates/create_application_tagging_table.rb.erb +15 -5
 - data/lib/generators/no_fly_list/templates/create_tagging_table.rb.erb +4 -2
 - data/lib/generators/no_fly_list/transformer_generator.rb +1 -0
 - data/lib/no_fly_list/application_tag.rb +0 -7
 - data/lib/no_fly_list/application_tagging.rb +0 -6
 - data/lib/no_fly_list/tag_record.rb +2 -2
 - data/lib/no_fly_list/taggable_record/config.rb +60 -0
 - data/lib/no_fly_list/taggable_record/configuration.rb +154 -27
 - data/lib/no_fly_list/taggable_record/query/mysql_strategy.rb +128 -0
 - data/lib/no_fly_list/taggable_record/query/postgresql_strategy.rb +128 -0
 - data/lib/no_fly_list/taggable_record/query/sqlite_strategy.rb +163 -0
 - data/lib/no_fly_list/taggable_record/query.rb +16 -84
 - data/lib/no_fly_list/taggable_record/tag_setup.rb +52 -0
 - data/lib/no_fly_list/taggable_record.rb +55 -4
 - data/lib/no_fly_list/tagging_proxy.rb +91 -14
 - data/lib/no_fly_list/tagging_record.rb +3 -3
 - data/lib/no_fly_list/test_helper.rb +85 -5
 - data/lib/no_fly_list/version.rb +1 -1
 - data/lib/no_fly_list.rb +8 -1
 - metadata +8 -17
 - /data/lib/generators/no_fly_list/templates/{tag_parser.rb → tag_transformer.rb} +0 -0
 
    
        checksums.yaml
    CHANGED
    
    | 
         @@ -1,7 +1,7 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            ---
         
     | 
| 
       2 
2 
     | 
    
         
             
            SHA256:
         
     | 
| 
       3 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       4 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 3 
     | 
    
         
            +
              metadata.gz: fddc142a9f89b7e7fae0c35c5285ffbdf1a68b0164ffcaf889ea70a572297933
         
     | 
| 
      
 4 
     | 
    
         
            +
              data.tar.gz: 6376fad323f9efbfaee941472ba977460dedc5a3a46e994df7a4bef2a8bb7d64
         
     | 
| 
       5 
5 
     | 
    
         
             
            SHA512:
         
     | 
| 
       6 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       7 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 6 
     | 
    
         
            +
              metadata.gz: ec568b5205e74c5d856d07122d6325585c12416c33f65ee2852207fa17288f1676a09d2f0757c70a6f88a091510e9d9060f0cb7446ca8d493f3397ade1bd5020
         
     | 
| 
      
 7 
     | 
    
         
            +
              data.tar.gz: 76b620b9ce76b112229c770a5a2221bdacb25b9ccc5f85bfb95c77623d26dee7114967323f91dd9b5a4f896355ab0b27cbba948abfbd7effc19f4ed1d1156809
         
     | 
| 
         @@ -1,15 +1,25 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version %>
         
     | 
| 
       2 
2 
     | 
    
         
             
              def up
         
     | 
| 
       3 
3 
     | 
    
         
             
                create_table :application_tags do |t|
         
     | 
| 
       4 
     | 
    
         
            -
                  t.string :name
         
     | 
| 
       5 
     | 
    
         
            -
                  t. 
     | 
| 
      
 4 
     | 
    
         
            +
                  t.string :name, null: false, index: { unique: true }
         
     | 
| 
      
 5 
     | 
    
         
            +
                  t.timestamp :created_at, default: -> { 'CURRENT_TIMESTAMP' }, null: false
         
     | 
| 
      
 6 
     | 
    
         
            +
                  t.timestamp :updated_at, default: -> { 'CURRENT_TIMESTAMP' }, null: false
         
     | 
| 
       6 
7 
     | 
    
         
             
                end
         
     | 
| 
       7 
8 
     | 
    
         | 
| 
       8 
9 
     | 
    
         
             
                create_table :application_taggings do |t|
         
     | 
| 
       9 
     | 
    
         
            -
                  t. 
     | 
| 
       10 
     | 
    
         
            -
                  t.foreign_key :application_tags, column: :tag_id
         
     | 
| 
      
 10 
     | 
    
         
            +
                  t.references :tag, null: false, foreign_key: { to_table: :application_tags }
         
     | 
| 
       11 
11 
     | 
    
         
             
                  t.references :taggable, polymorphic: true, null: false
         
     | 
| 
       12 
     | 
    
         
            -
                  t. 
     | 
| 
      
 12 
     | 
    
         
            +
                  t.string :context, null: false
         
     | 
| 
      
 13 
     | 
    
         
            +
                  t.timestamp :created_at, default: -> { 'CURRENT_TIMESTAMP' }, null: false
         
     | 
| 
      
 14 
     | 
    
         
            +
                  t.timestamp :updated_at, default: -> { 'CURRENT_TIMESTAMP' }, null: false
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
                  # Add index for uniqueness and performance
         
     | 
| 
      
 17 
     | 
    
         
            +
                  t.index [:taggable_type, :taggable_id, :context, :tag_id],
         
     | 
| 
      
 18 
     | 
    
         
            +
                          unique: true,
         
     | 
| 
      
 19 
     | 
    
         
            +
                          name: 'index_app_taggings_uniqueness'
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
                  # Add index for faster lookups by context
         
     | 
| 
      
 22 
     | 
    
         
            +
                  t.index [:context]
         
     | 
| 
       13 
23 
     | 
    
         
             
                end
         
     | 
| 
       14 
24 
     | 
    
         
             
              end
         
     | 
| 
       15 
25 
     | 
    
         
             
            end
         
     | 
| 
         @@ -2,14 +2,16 @@ class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version 
     | 
|
| 
       2 
2 
     | 
    
         
             
              def change
         
     | 
| 
       3 
3 
     | 
    
         
             
                create_table :<%= tag_table_name %>, id: :bigint do |t|
         
     | 
| 
       4 
4 
     | 
    
         
             
                  t.string :name, null: false
         
     | 
| 
       5 
     | 
    
         
            -
                  t. 
     | 
| 
      
 5 
     | 
    
         
            +
                  t.timestamp :created_at, default: -> { 'CURRENT_TIMESTAMP' }, null: false
         
     | 
| 
      
 6 
     | 
    
         
            +
                  t.timestamp :updated_at, default: -> { 'CURRENT_TIMESTAMP' }, null: false
         
     | 
| 
       6 
7 
     | 
    
         
             
                end
         
     | 
| 
       7 
8 
     | 
    
         | 
| 
       8 
9 
     | 
    
         
             
                create_table :<%= tagging_table_name %> do |t|
         
     | 
| 
       9 
10 
     | 
    
         
             
                  t.column :taggable_id, :bigint, null: false, index: true # Change to :uuid if you are using UUIDs
         
     | 
| 
       10 
11 
     | 
    
         
             
                  t.column :tag_id, :bigint, null: false, index: true
         
     | 
| 
       11 
12 
     | 
    
         
             
                  t.string :context, null: false
         
     | 
| 
       12 
     | 
    
         
            -
                  t. 
     | 
| 
      
 13 
     | 
    
         
            +
                  t.timestamp :created_at, default: -> { 'CURRENT_TIMESTAMP' }, null: false
         
     | 
| 
      
 14 
     | 
    
         
            +
                  t.timestamp :updated_at, default: -> { 'CURRENT_TIMESTAMP' }, null: false
         
     | 
| 
       13 
15 
     | 
    
         
             
                end
         
     | 
| 
       14 
16 
     | 
    
         | 
| 
       15 
17 
     | 
    
         
             
                add_index :<%= tag_table_name %>, :name, unique: true
         
     | 
| 
         @@ -8,6 +8,7 @@ require 'rails/generators/named_base' 
     | 
|
| 
       8 
8 
     | 
    
         
             
            unless defined?(ApplicationTagTransformer)
         
     | 
| 
       9 
9 
     | 
    
         
             
              module NoFlyList
         
     | 
| 
       10 
10 
     | 
    
         
             
                module Generators
         
     | 
| 
      
 11 
     | 
    
         
            +
                  # bin/rails g no_fly_list:transformer
         
     | 
| 
       11 
12 
     | 
    
         
             
                  class TransformerGenerator < Rails::Generators::Base
         
     | 
| 
       12 
13 
     | 
    
         
             
                    source_root File.expand_path('templates', __dir__)
         
     | 
| 
       13 
14 
     | 
    
         
             
                    def create_tag_transformer_file
         
     | 
| 
         @@ -12,12 +12,5 @@ module NoFlyList 
     | 
|
| 
       12 
12 
     | 
    
         
             
              #   end
         
     | 
| 
       13 
13 
     | 
    
         
             
              module ApplicationTag
         
     | 
| 
       14 
14 
     | 
    
         
             
                extend ActiveSupport::Concern
         
     | 
| 
       15 
     | 
    
         
            -
             
     | 
| 
       16 
     | 
    
         
            -
                included do
         
     | 
| 
       17 
     | 
    
         
            -
                  self.table_name = Rails.configuration.no_fly_list.application_tag_table_name || 'application_tags'
         
     | 
| 
       18 
     | 
    
         
            -
             
     | 
| 
       19 
     | 
    
         
            -
                  has_many :taggings, class_name: 'ApplicationTagging', dependent: :destroy, foreign_key: 'tag_id'
         
     | 
| 
       20 
     | 
    
         
            -
                  has_many :taggables, through: :taggings, source: :taggable
         
     | 
| 
       21 
     | 
    
         
            -
                end
         
     | 
| 
       22 
15 
     | 
    
         
             
              end
         
     | 
| 
       23 
16 
     | 
    
         
             
            end
         
     | 
| 
         @@ -12,11 +12,5 @@ module NoFlyList 
     | 
|
| 
       12 
12 
     | 
    
         
             
              #   end
         
     | 
| 
       13 
13 
     | 
    
         
             
              module ApplicationTagging
         
     | 
| 
       14 
14 
     | 
    
         
             
                extend ActiveSupport::Concern
         
     | 
| 
       15 
     | 
    
         
            -
             
     | 
| 
       16 
     | 
    
         
            -
                included do
         
     | 
| 
       17 
     | 
    
         
            -
                  self.table_name = Rails.configuration.no_fly_list.application_tagging_table_name || 'application_taggings'
         
     | 
| 
       18 
     | 
    
         
            -
                  belongs_to :tag, class_name: 'ApplicationTag', foreign_key: 'tag_id'
         
     | 
| 
       19 
     | 
    
         
            -
                  belongs_to :taggable, polymorphic: true
         
     | 
| 
       20 
     | 
    
         
            -
                end
         
     | 
| 
       21 
15 
     | 
    
         
             
              end
         
     | 
| 
       22 
16 
     | 
    
         
             
            end
         
     | 
| 
         @@ -1,11 +1,11 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            # frozen_string_literal: true
         
     | 
| 
       2 
2 
     | 
    
         | 
| 
       3 
3 
     | 
    
         
             
            module NoFlyList
         
     | 
| 
       4 
     | 
    
         
            -
              # This module provides functionality for a tag table that contains  
     | 
| 
      
 4 
     | 
    
         
            +
              # This module provides functionality for a tag table that contains tags for a model.
         
     | 
| 
       5 
5 
     | 
    
         
             
              #
         
     | 
| 
       6 
6 
     | 
    
         
             
              # @example Usage
         
     | 
| 
       7 
7 
     | 
    
         
             
              #   class User::Tag < ApplicationRecord
         
     | 
| 
       8 
     | 
    
         
            -
              #     include NoFlyList:: 
     | 
| 
      
 8 
     | 
    
         
            +
              #     include NoFlyList::TagRecord
         
     | 
| 
       9 
9 
     | 
    
         
             
              #   end
         
     | 
| 
       10 
10 
     | 
    
         
             
              module TagRecord
         
     | 
| 
       11 
11 
     | 
    
         
             
                extend ActiveSupport::Concern
         
     | 
| 
         @@ -0,0 +1,60 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module NoFlyList
         
     | 
| 
      
 4 
     | 
    
         
            +
              class Config
         
     | 
| 
      
 5 
     | 
    
         
            +
                attr_reader :tag_contexts, :taggable_class, :adapter
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
                def initialize(taggable_class = nil)
         
     | 
| 
      
 8 
     | 
    
         
            +
                  @tag_contexts = {}
         
     | 
| 
      
 9 
     | 
    
         
            +
                  @taggable_class = taggable_class
         
     | 
| 
      
 10 
     | 
    
         
            +
                  @adapter = determine_adapter
         
     | 
| 
      
 11 
     | 
    
         
            +
                end
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
                def add_context(context, options = {})
         
     | 
| 
      
 14 
     | 
    
         
            +
                  context = context.to_sym
         
     | 
| 
      
 15 
     | 
    
         
            +
                  tag_class_name = determine_tag_class_name(options)
         
     | 
| 
      
 16 
     | 
    
         
            +
                  tagging_class_name = determine_tagging_class_name(options)
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                  @tag_contexts[context] = {
         
     | 
| 
      
 19 
     | 
    
         
            +
                    taggable_class: @taggable_class.to_s,
         
     | 
| 
      
 20 
     | 
    
         
            +
                    tag_class_name: tag_class_name,
         
     | 
| 
      
 21 
     | 
    
         
            +
                    tagging_class_name: tagging_class_name,
         
     | 
| 
      
 22 
     | 
    
         
            +
                    transformer: options.fetch(:transformer, ApplicationTagTransformer).to_s,
         
     | 
| 
      
 23 
     | 
    
         
            +
                    polymorphic: options.fetch(:polymorphic, false),
         
     | 
| 
      
 24 
     | 
    
         
            +
                    restrict_to_existing: options.fetch(:restrict_to_existing, false),
         
     | 
| 
      
 25 
     | 
    
         
            +
                    limit: options.fetch(:limit, nil),
         
     | 
| 
      
 26 
     | 
    
         
            +
                    case_sensitive: options.fetch(:case_sensitive, true),
         
     | 
| 
      
 27 
     | 
    
         
            +
                    adapter: @adapter
         
     | 
| 
      
 28 
     | 
    
         
            +
                  }
         
     | 
| 
      
 29 
     | 
    
         
            +
                end
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
                private
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
                def determine_adapter
         
     | 
| 
      
 34 
     | 
    
         
            +
                  case taggable_class.connection.adapter_name.downcase
         
     | 
| 
      
 35 
     | 
    
         
            +
                  when 'postgresql'
         
     | 
| 
      
 36 
     | 
    
         
            +
                    :postgresql
         
     | 
| 
      
 37 
     | 
    
         
            +
                  when 'mysql2'
         
     | 
| 
      
 38 
     | 
    
         
            +
                    :mysql
         
     | 
| 
      
 39 
     | 
    
         
            +
                  else
         
     | 
| 
      
 40 
     | 
    
         
            +
                    :sqlite
         
     | 
| 
      
 41 
     | 
    
         
            +
                  end
         
     | 
| 
      
 42 
     | 
    
         
            +
                end
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
                def determine_tag_class_name(options)
         
     | 
| 
      
 45 
     | 
    
         
            +
                  if options[:polymorphic]
         
     | 
| 
      
 46 
     | 
    
         
            +
                    Rails.application.config.no_fly_list.tag_class_name
         
     | 
| 
      
 47 
     | 
    
         
            +
                  else
         
     | 
| 
      
 48 
     | 
    
         
            +
                    options.fetch(:tag_class_name, "#{@taggable_class}Tag")
         
     | 
| 
      
 49 
     | 
    
         
            +
                  end
         
     | 
| 
      
 50 
     | 
    
         
            +
                end
         
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
                def determine_tagging_class_name(options)
         
     | 
| 
      
 53 
     | 
    
         
            +
                  if options[:polymorphic]
         
     | 
| 
      
 54 
     | 
    
         
            +
                    Rails.application.config.no_fly_list.tagging_class_name
         
     | 
| 
      
 55 
     | 
    
         
            +
                  else
         
     | 
| 
      
 56 
     | 
    
         
            +
                    options.fetch(:tagging_class_name, "#{@taggable_class}::Tagging")
         
     | 
| 
      
 57 
     | 
    
         
            +
                  end
         
     | 
| 
      
 58 
     | 
    
         
            +
                end
         
     | 
| 
      
 59 
     | 
    
         
            +
              end
         
     | 
| 
      
 60 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -2,56 +2,64 @@ 
     | 
|
| 
       2 
2 
     | 
    
         | 
| 
       3 
3 
     | 
    
         
             
            require_relative 'mutation'
         
     | 
| 
       4 
4 
     | 
    
         
             
            require_relative 'query'
         
     | 
| 
      
 5 
     | 
    
         
            +
            require_relative 'tag_setup'
         
     | 
| 
       5 
6 
     | 
    
         | 
| 
       6 
7 
     | 
    
         
             
            module NoFlyList
         
     | 
| 
       7 
8 
     | 
    
         
             
              module TaggableRecord
         
     | 
| 
      
 9 
     | 
    
         
            +
                # Configuration module handles the setup and structure of tagging functionality
         
     | 
| 
      
 10 
     | 
    
         
            +
                # This includes creating necessary classes, setting up associations, and defining
         
     | 
| 
      
 11 
     | 
    
         
            +
                # the interface for tag manipulation
         
     | 
| 
       8 
12 
     | 
    
         
             
                module Configuration
         
     | 
| 
       9 
13 
     | 
    
         
             
                  module_function
         
     | 
| 
       10 
14 
     | 
    
         | 
| 
      
 15 
     | 
    
         
            +
                  # Main entry point for setting up tagging functionality on a model
         
     | 
| 
      
 16 
     | 
    
         
            +
                  # @param taggable_klass [Class] The model class to make taggable
         
     | 
| 
      
 17 
     | 
    
         
            +
                  # @param contexts [Array<Symbol>] The contexts to create tags for (e.g., :tags, :colors)
         
     | 
| 
      
 18 
     | 
    
         
            +
                  # @param options [Hash] Configuration options for tagging behavior
         
     | 
| 
       11 
19 
     | 
    
         
             
                  def setup_tagging(taggable_klass, contexts, options = {})
         
     | 
| 
       12 
20 
     | 
    
         
             
                    contexts.each do |context|
         
     | 
| 
       13 
21 
     | 
    
         
             
                      setup = build_tag_setup(taggable_klass, context, options)
         
     | 
| 
       14 
22 
     | 
    
         
             
                      define_tag_structure(setup)
         
     | 
| 
       15 
23 
     | 
    
         
             
                      define_list_methods(setup)
         
     | 
| 
       16 
     | 
    
         
            -
                      Mutation.define_mutation_methods(setup) 
     | 
| 
       17 
     | 
    
         
            -
                      Query.define_query_methods(setup) 
     | 
| 
      
 24 
     | 
    
         
            +
                      Mutation.define_mutation_methods(setup)
         
     | 
| 
      
 25 
     | 
    
         
            +
                      Query.define_query_methods(setup)
         
     | 
| 
       18 
26 
     | 
    
         
             
                    end
         
     | 
| 
       19 
27 
     | 
    
         
             
                  end
         
     | 
| 
       20 
28 
     | 
    
         | 
| 
      
 29 
     | 
    
         
            +
                  # Creates a new TagSetup instance with the given configuration
         
     | 
| 
       21 
30 
     | 
    
         
             
                  def build_tag_setup(taggable_klass, context, options)
         
     | 
| 
       22 
     | 
    
         
            -
                     
     | 
| 
       23 
     | 
    
         
            -
                      taggable_klass: taggable_klass,
         
     | 
| 
       24 
     | 
    
         
            -
                      context: context,
         
     | 
| 
       25 
     | 
    
         
            -
                      transformer: options.fetch(:transformer, ApplicationTagTransformer),
         
     | 
| 
       26 
     | 
    
         
            -
                      global: options.fetch(:global, false),
         
     | 
| 
       27 
     | 
    
         
            -
                      restrict_to_existing: options.fetch(:restrict_to_existing, false),
         
     | 
| 
       28 
     | 
    
         
            -
                      limit: options.fetch(:limit, nil),
         
     | 
| 
       29 
     | 
    
         
            -
                      tag_class_name: determine_tag_class_name(taggable_klass, options),
         
     | 
| 
       30 
     | 
    
         
            -
                      tagging_class_name: determine_tagging_class_name(taggable_klass, options)
         
     | 
| 
       31 
     | 
    
         
            -
                    )
         
     | 
| 
      
 31 
     | 
    
         
            +
                    TagSetup.new(taggable_klass, context, options)
         
     | 
| 
       32 
32 
     | 
    
         
             
                  end
         
     | 
| 
       33 
33 
     | 
    
         | 
| 
      
 34 
     | 
    
         
            +
                  # Determines the appropriate class name for tags based on configuration
         
     | 
| 
      
 35 
     | 
    
         
            +
                  # For global tags, uses application-wide tag class
         
     | 
| 
      
 36 
     | 
    
         
            +
                  # For local tags, creates model-specific tag classes
         
     | 
| 
       34 
37 
     | 
    
         
             
                  def determine_tag_class_name(taggable_klass, options)
         
     | 
| 
       35 
     | 
    
         
            -
                    if options[: 
     | 
| 
      
 38 
     | 
    
         
            +
                    if options[:polymorphic]
         
     | 
| 
       36 
39 
     | 
    
         
             
                      Rails.application.config.no_fly_list.tag_class_name
         
     | 
| 
       37 
40 
     | 
    
         
             
                    else
         
     | 
| 
       38 
41 
     | 
    
         
             
                      options.fetch(:tag_class_name, "#{taggable_klass.name}Tag")
         
     | 
| 
       39 
42 
     | 
    
         
             
                    end
         
     | 
| 
       40 
43 
     | 
    
         
             
                  end
         
     | 
| 
       41 
44 
     | 
    
         | 
| 
      
 45 
     | 
    
         
            +
                  # Determines the appropriate class name for taggings based on configuration
         
     | 
| 
      
 46 
     | 
    
         
            +
                  # For global tags, uses application-wide tagging class
         
     | 
| 
      
 47 
     | 
    
         
            +
                  # For local tags, creates model-specific tagging classes
         
     | 
| 
       42 
48 
     | 
    
         
             
                  def determine_tagging_class_name(taggable_klass, options)
         
     | 
| 
       43 
     | 
    
         
            -
                    if options[: 
     | 
| 
      
 49 
     | 
    
         
            +
                    if options[:polymorphic]
         
     | 
| 
       44 
50 
     | 
    
         
             
                      Rails.application.config.no_fly_list.tagging_class_name
         
     | 
| 
       45 
51 
     | 
    
         
             
                    else
         
     | 
| 
       46 
52 
     | 
    
         
             
                      options.fetch(:tagging_class_name, "#{taggable_klass.name}::Tagging")
         
     | 
| 
       47 
53 
     | 
    
         
             
                    end
         
     | 
| 
       48 
54 
     | 
    
         
             
                  end
         
     | 
| 
       49 
55 
     | 
    
         | 
| 
      
 56 
     | 
    
         
            +
                  # Sets up the complete tag structure including classes and associations
         
     | 
| 
       50 
57 
     | 
    
         
             
                  def define_tag_structure(setup)
         
     | 
| 
       51 
     | 
    
         
            -
                    define_tag_classes(setup) unless setup. 
     | 
| 
      
 58 
     | 
    
         
            +
                    define_tag_classes(setup) unless setup.polymorphic
         
     | 
| 
       52 
59 
     | 
    
         
             
                    define_tagging_associations(setup)
         
     | 
| 
       53 
60 
     | 
    
         
             
                  end
         
     | 
| 
       54 
61 
     | 
    
         | 
| 
      
 62 
     | 
    
         
            +
                  # Creates the tag and tagging classes for local (non-global) tags
         
     | 
| 
       55 
63 
     | 
    
         
             
                  def define_tag_classes(setup)
         
     | 
| 
       56 
64 
     | 
    
         
             
                    base_class = find_abstract_class(setup.taggable_klass)
         
     | 
| 
       57 
65 
     | 
    
         | 
| 
         @@ -64,40 +72,159 @@ module NoFlyList 
     | 
|
| 
       64 
72 
     | 
    
         
             
                    end
         
     | 
| 
       65 
73 
     | 
    
         
             
                  end
         
     | 
| 
       66 
74 
     | 
    
         | 
| 
      
 75 
     | 
    
         
            +
                  # Creates a new tag class with appropriate configuration
         
     | 
| 
       67 
76 
     | 
    
         
             
                  def create_tag_class(setup, base_class)
         
     | 
| 
       68 
77 
     | 
    
         
             
                    Class.new(base_class) do
         
     | 
| 
       69 
78 
     | 
    
         
             
                      self.table_name = "#{setup.taggable_klass.table_name.singularize}_tags"
         
     | 
| 
       70 
     | 
    
         
            -
             
     | 
| 
       71 
     | 
    
         
            -
                      has_many :taggings, class_name: setup.tagging_class_name, dependent: :destroy
         
     | 
| 
       72 
     | 
    
         
            -
                      has_many :taggables, through: :taggings, source: :taggable, source_type: setup.taggable_klass.name
         
     | 
| 
       73 
79 
     | 
    
         
             
                      include NoFlyList::TagRecord
         
     | 
| 
       74 
80 
     | 
    
         
             
                    end
         
     | 
| 
       75 
81 
     | 
    
         
             
                  end
         
     | 
| 
       76 
82 
     | 
    
         | 
| 
      
 83 
     | 
    
         
            +
                  # Creates a new tagging class with appropriate configuration
         
     | 
| 
       77 
84 
     | 
    
         
             
                  def create_tagging_class(setup, base_class)
         
     | 
| 
      
 85 
     | 
    
         
            +
                    setup.context.to_s.singularize
         
     | 
| 
      
 86 
     | 
    
         
            +
             
     | 
| 
       78 
87 
     | 
    
         
             
                    Class.new(base_class) do
         
     | 
| 
       79 
88 
     | 
    
         
             
                      self.table_name = "#{setup.taggable_klass.table_name.singularize}_taggings"
         
     | 
| 
       80 
89 
     | 
    
         | 
| 
       81 
     | 
    
         
            -
                       
     | 
| 
       82 
     | 
    
         
            -
                      belongs_to :tag, 
     | 
| 
      
 90 
     | 
    
         
            +
                      # Add the basic associations
         
     | 
| 
      
 91 
     | 
    
         
            +
                      belongs_to :tag,
         
     | 
| 
      
 92 
     | 
    
         
            +
                                 class_name: setup.tag_class_name,
         
     | 
| 
      
 93 
     | 
    
         
            +
                                 foreign_key: 'tag_id'
         
     | 
| 
      
 94 
     | 
    
         
            +
             
     | 
| 
      
 95 
     | 
    
         
            +
                      belongs_to :taggable,
         
     | 
| 
      
 96 
     | 
    
         
            +
                                 class_name: setup.taggable_klass.name,
         
     | 
| 
      
 97 
     | 
    
         
            +
                                 foreign_key: 'taggable_id'
         
     | 
| 
      
 98 
     | 
    
         
            +
             
     | 
| 
       83 
99 
     | 
    
         
             
                      include NoFlyList::TaggingRecord
         
     | 
| 
       84 
100 
     | 
    
         
             
                    end
         
     | 
| 
       85 
101 
     | 
    
         
             
                  end
         
     | 
| 
       86 
102 
     | 
    
         | 
| 
      
 103 
     | 
    
         
            +
                  # Sets up all necessary associations between tags, taggings, and the taggable model
         
     | 
| 
       87 
104 
     | 
    
         
             
                  def define_tagging_associations(setup)
         
     | 
| 
       88 
105 
     | 
    
         
             
                    singular_name = setup.context.to_s.singularize
         
     | 
| 
       89 
106 
     | 
    
         | 
| 
       90 
     | 
    
         
            -
                    setup. 
     | 
| 
       91 
     | 
    
         
            -
                       
     | 
| 
      
 107 
     | 
    
         
            +
                    if setup.polymorphic
         
     | 
| 
      
 108 
     | 
    
         
            +
                      setup_polymorphic_tag_associations(setup, singular_name)
         
     | 
| 
      
 109 
     | 
    
         
            +
                    else
         
     | 
| 
      
 110 
     | 
    
         
            +
                      setup_local_tag_associations(setup, singular_name)
         
     | 
| 
      
 111 
     | 
    
         
            +
                    end
         
     | 
| 
      
 112 
     | 
    
         
            +
             
     | 
| 
      
 113 
     | 
    
         
            +
                    setup_taggable_associations(setup, singular_name)
         
     | 
| 
      
 114 
     | 
    
         
            +
                  end
         
     | 
| 
      
 115 
     | 
    
         
            +
             
     | 
| 
      
 116 
     | 
    
         
            +
                  # Sets up associations for polymorphic tags
         
     | 
| 
      
 117 
     | 
    
         
            +
                  def setup_polymorphic_tag_associations(setup, singular_name)
         
     | 
| 
      
 118 
     | 
    
         
            +
                    # Set up the tag model associations
         
     | 
| 
      
 119 
     | 
    
         
            +
                    setup.tag_class_name.constantize.class_eval do
         
     | 
| 
      
 120 
     | 
    
         
            +
                      # Fix: Use 'tagging' for the join association when context is 'tag'
         
     | 
| 
      
 121 
     | 
    
         
            +
                      association_name = (singular_name == 'tag' ? :taggings : :"#{singular_name}_taggings")
         
     | 
| 
      
 122 
     | 
    
         
            +
             
     | 
| 
      
 123 
     | 
    
         
            +
                      has_many association_name,
         
     | 
| 
       92 
124 
     | 
    
         
             
                               -> { where(context: singular_name) },
         
     | 
| 
       93 
125 
     | 
    
         
             
                               class_name: setup.tagging_class_name,
         
     | 
| 
       94 
     | 
    
         
            -
                               foreign_key: ' 
     | 
| 
      
 126 
     | 
    
         
            +
                               foreign_key: 'tag_id',
         
     | 
| 
       95 
127 
     | 
    
         
             
                               dependent: :destroy
         
     | 
| 
       96 
128 
     | 
    
         | 
| 
      
 129 
     | 
    
         
            +
                      # Fix: Use consistent naming for through association
         
     | 
| 
       97 
130 
     | 
    
         
             
                      has_many setup.context,
         
     | 
| 
      
 131 
     | 
    
         
            +
                               through: association_name,
         
     | 
| 
      
 132 
     | 
    
         
            +
                               source: :taggable,
         
     | 
| 
      
 133 
     | 
    
         
            +
                               source_type: setup.taggable_klass.name
         
     | 
| 
      
 134 
     | 
    
         
            +
                    end
         
     | 
| 
      
 135 
     | 
    
         
            +
             
     | 
| 
      
 136 
     | 
    
         
            +
                    # Set up the tagging model with global scope
         
     | 
| 
      
 137 
     | 
    
         
            +
                    setup.tagging_class_name.constantize.class_eval do
         
     | 
| 
      
 138 
     | 
    
         
            +
                      belongs_to :tag,
         
     | 
| 
      
 139 
     | 
    
         
            +
                                 class_name: setup.tag_class_name,
         
     | 
| 
      
 140 
     | 
    
         
            +
                                 foreign_key: 'tag_id'
         
     | 
| 
      
 141 
     | 
    
         
            +
             
     | 
| 
      
 142 
     | 
    
         
            +
                      belongs_to :taggable,
         
     | 
| 
      
 143 
     | 
    
         
            +
                                 polymorphic: true
         
     | 
| 
      
 144 
     | 
    
         
            +
             
     | 
| 
      
 145 
     | 
    
         
            +
                      validates :tag, :taggable, :context, presence: true
         
     | 
| 
      
 146 
     | 
    
         
            +
             
     | 
| 
      
 147 
     | 
    
         
            +
                      # Add scope for specific taggable type
         
     | 
| 
      
 148 
     | 
    
         
            +
                      scope :for_taggable_type, ->(type) { where(taggable_type: type) }
         
     | 
| 
      
 149 
     | 
    
         
            +
             
     | 
| 
      
 150 
     | 
    
         
            +
                      validates :tag_id, uniqueness: {
         
     | 
| 
      
 151 
     | 
    
         
            +
                        scope: %i[taggable_type taggable_id context],
         
     | 
| 
      
 152 
     | 
    
         
            +
                        message: 'has already been tagged on this record in this context'
         
     | 
| 
      
 153 
     | 
    
         
            +
                      }
         
     | 
| 
      
 154 
     | 
    
         
            +
                    end
         
     | 
| 
      
 155 
     | 
    
         
            +
                  end
         
     | 
| 
      
 156 
     | 
    
         
            +
             
     | 
| 
      
 157 
     | 
    
         
            +
                  # Sets up associations for local (non-global) tags
         
     | 
| 
      
 158 
     | 
    
         
            +
                  def setup_local_tag_associations(setup, singular_name)
         
     | 
| 
      
 159 
     | 
    
         
            +
                    # Set up tag class associations
         
     | 
| 
      
 160 
     | 
    
         
            +
                    setup.tag_class_name.constantize.class_eval do
         
     | 
| 
      
 161 
     | 
    
         
            +
                      has_many :"#{singular_name}_taggings",
         
     | 
| 
      
 162 
     | 
    
         
            +
                               -> { where(context: singular_name) },
         
     | 
| 
      
 163 
     | 
    
         
            +
                               class_name: setup.tagging_class_name,
         
     | 
| 
      
 164 
     | 
    
         
            +
                               foreign_key: 'tag_id',
         
     | 
| 
      
 165 
     | 
    
         
            +
                               dependent: :destroy
         
     | 
| 
      
 166 
     | 
    
         
            +
             
     | 
| 
      
 167 
     | 
    
         
            +
                      has_many :"#{singular_name}_taggables",
         
     | 
| 
       98 
168 
     | 
    
         
             
                               through: :"#{singular_name}_taggings",
         
     | 
| 
       99 
     | 
    
         
            -
                               source: : 
     | 
| 
       100 
     | 
    
         
            -
             
     | 
| 
      
 169 
     | 
    
         
            +
                               source: :taggable
         
     | 
| 
      
 170 
     | 
    
         
            +
                    end
         
     | 
| 
      
 171 
     | 
    
         
            +
             
     | 
| 
      
 172 
     | 
    
         
            +
                    # Set up tagging class associations
         
     | 
| 
      
 173 
     | 
    
         
            +
                    setup.tagging_class_name.constantize.class_eval do
         
     | 
| 
      
 174 
     | 
    
         
            +
                      belongs_to :tag,
         
     | 
| 
      
 175 
     | 
    
         
            +
                                 class_name: setup.tag_class_name,
         
     | 
| 
      
 176 
     | 
    
         
            +
                                 foreign_key: 'tag_id'
         
     | 
| 
      
 177 
     | 
    
         
            +
             
     | 
| 
      
 178 
     | 
    
         
            +
                      # For local tags, we use a simple belongs_to without polymorphic
         
     | 
| 
      
 179 
     | 
    
         
            +
                      belongs_to :taggable,
         
     | 
| 
      
 180 
     | 
    
         
            +
                                 class_name: setup.taggable_klass.name,
         
     | 
| 
      
 181 
     | 
    
         
            +
                                 foreign_key: 'taggable_id'
         
     | 
| 
      
 182 
     | 
    
         
            +
             
     | 
| 
      
 183 
     | 
    
         
            +
                      validates :tag, :taggable, :context, presence: true
         
     | 
| 
      
 184 
     | 
    
         
            +
                      validates :tag_id, uniqueness: {
         
     | 
| 
      
 185 
     | 
    
         
            +
                        scope: %i[taggable_id context],
         
     | 
| 
      
 186 
     | 
    
         
            +
                        message: 'has already been tagged on this record in this context'
         
     | 
| 
      
 187 
     | 
    
         
            +
                      }
         
     | 
| 
      
 188 
     | 
    
         
            +
                    end
         
     | 
| 
      
 189 
     | 
    
         
            +
                  end
         
     | 
| 
      
 190 
     | 
    
         
            +
             
     | 
| 
      
 191 
     | 
    
         
            +
                  # Sets up associations on the taggable model
         
     | 
| 
      
 192 
     | 
    
         
            +
                  def setup_taggable_associations(setup, singular_name)
         
     | 
| 
      
 193 
     | 
    
         
            +
                    setup.taggable_klass.class_eval do
         
     | 
| 
      
 194 
     | 
    
         
            +
                      if setup.polymorphic
         
     | 
| 
      
 195 
     | 
    
         
            +
                        # Global tags need polymorphic associations
         
     | 
| 
      
 196 
     | 
    
         
            +
                        has_many :"#{singular_name}_taggings",
         
     | 
| 
      
 197 
     | 
    
         
            +
                                 -> { where(context: singular_name) },
         
     | 
| 
      
 198 
     | 
    
         
            +
                                 class_name: setup.tagging_class_name,
         
     | 
| 
      
 199 
     | 
    
         
            +
                                 foreign_key: 'taggable_id',
         
     | 
| 
      
 200 
     | 
    
         
            +
                                 as: :taggable,
         
     | 
| 
      
 201 
     | 
    
         
            +
                                 dependent: :destroy
         
     | 
| 
      
 202 
     | 
    
         
            +
             
     | 
| 
      
 203 
     | 
    
         
            +
                        has_many setup.context,
         
     | 
| 
      
 204 
     | 
    
         
            +
                                 through: :"#{singular_name}_taggings",
         
     | 
| 
      
 205 
     | 
    
         
            +
                                 source: :tag,
         
     | 
| 
      
 206 
     | 
    
         
            +
                                 class_name: setup.tag_class_name do
         
     | 
| 
      
 207 
     | 
    
         
            +
                          def by_type(taggable_type)
         
     | 
| 
      
 208 
     | 
    
         
            +
                            where(taggings: { taggable_type: taggable_type })
         
     | 
| 
      
 209 
     | 
    
         
            +
                          end
         
     | 
| 
      
 210 
     | 
    
         
            +
             
     | 
| 
      
 211 
     | 
    
         
            +
                          def shared_with(other_taggable)
         
     | 
| 
      
 212 
     | 
    
         
            +
                            where(id: other_taggable.send(proxy_association.name).pluck(:id))
         
     | 
| 
      
 213 
     | 
    
         
            +
                          end
         
     | 
| 
      
 214 
     | 
    
         
            +
                        end
         
     | 
| 
      
 215 
     | 
    
         
            +
                      else
         
     | 
| 
      
 216 
     | 
    
         
            +
                        # Local tags should use simple associations
         
     | 
| 
      
 217 
     | 
    
         
            +
                        has_many :"#{singular_name}_taggings",
         
     | 
| 
      
 218 
     | 
    
         
            +
                                 -> { where(context: singular_name) },
         
     | 
| 
      
 219 
     | 
    
         
            +
                                 class_name: setup.tagging_class_name,
         
     | 
| 
      
 220 
     | 
    
         
            +
                                 foreign_key: 'taggable_id',
         
     | 
| 
      
 221 
     | 
    
         
            +
                                 dependent: :destroy
         
     | 
| 
      
 222 
     | 
    
         
            +
             
     | 
| 
      
 223 
     | 
    
         
            +
                        has_many setup.context,
         
     | 
| 
      
 224 
     | 
    
         
            +
                                 through: :"#{singular_name}_taggings",
         
     | 
| 
      
 225 
     | 
    
         
            +
                                 source: :tag,
         
     | 
| 
      
 226 
     | 
    
         
            +
                                 class_name: setup.tag_class_name
         
     | 
| 
      
 227 
     | 
    
         
            +
                      end
         
     | 
| 
       101 
228 
     | 
    
         
             
                    end
         
     | 
| 
       102 
229 
     | 
    
         
             
                  end
         
     | 
| 
       103 
230 
     | 
    
         | 
| 
         @@ -108,13 +235,13 @@ module NoFlyList 
     | 
|
| 
       108 
235 
     | 
    
         
             
                    # Define helper methods module for this context
         
     | 
| 
       109 
236 
     | 
    
         
             
                    helper_module = Module.new do
         
     | 
| 
       110 
237 
     | 
    
         
             
                      define_method :create_and_set_proxy do |instance_variable_name, setup|
         
     | 
| 
       111 
     | 
    
         
            -
                        tag_model = if setup. 
     | 
| 
      
 238 
     | 
    
         
            +
                        tag_model = if setup.polymorphic
         
     | 
| 
       112 
239 
     | 
    
         
             
                                      setup.tag_class_name.constantize
         
     | 
| 
       113 
240 
     | 
    
         
             
                                    else
         
     | 
| 
       114 
241 
     | 
    
         
             
                                      self.class.const_get("#{self.class.name}Tag")
         
     | 
| 
       115 
242 
     | 
    
         
             
                                    end
         
     | 
| 
       116 
243 
     | 
    
         | 
| 
       117 
     | 
    
         
            -
                        proxy =  
     | 
| 
      
 244 
     | 
    
         
            +
                        proxy = TaggingProxy.new(
         
     | 
| 
       118 
245 
     | 
    
         
             
                          self,
         
     | 
| 
       119 
246 
     | 
    
         
             
                          tag_model,
         
     | 
| 
       120 
247 
     | 
    
         
             
                          setup.context,
         
     | 
| 
         @@ -0,0 +1,128 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module NoFlyList
         
     | 
| 
      
 4 
     | 
    
         
            +
              module TaggableRecord
         
     | 
| 
      
 5 
     | 
    
         
            +
                module Query
         
     | 
| 
      
 6 
     | 
    
         
            +
                  module MysqlStrategy
         
     | 
| 
      
 7 
     | 
    
         
            +
                    extend BaseStrategy
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
                    module_function
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
                    def define_query_methods(setup)
         
     | 
| 
      
 12 
     | 
    
         
            +
                      context = setup.context
         
     | 
| 
      
 13 
     | 
    
         
            +
                      taggable_klass = setup.taggable_klass
         
     | 
| 
      
 14 
     | 
    
         
            +
                      tagging_klass = setup.tagging_class_name.constantize
         
     | 
| 
      
 15 
     | 
    
         
            +
                      tagging_table = tagging_klass.arel_table # Arel
         
     | 
| 
      
 16 
     | 
    
         
            +
                      tag_klass = setup.tag_class_name.constantize
         
     | 
| 
      
 17 
     | 
    
         
            +
                      tag_table = tag_klass.arel_table
         
     | 
| 
      
 18 
     | 
    
         
            +
                      singular_name = context.to_s.singularize
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
                      taggable_klass.class_eval do
         
     | 
| 
      
 21 
     | 
    
         
            +
                        # Find records with any of the specified tags
         
     | 
| 
      
 22 
     | 
    
         
            +
                        scope "with_any_#{context}", lambda { |*tags|
         
     | 
| 
      
 23 
     | 
    
         
            +
                          tags = tags.flatten.compact.uniq
         
     | 
| 
      
 24 
     | 
    
         
            +
                          return none if tags.empty?
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
                          query = Arel::SelectManager.new(self)
         
     | 
| 
      
 27 
     | 
    
         
            +
                                                     .from(table_name)
         
     | 
| 
      
 28 
     | 
    
         
            +
                                                     .project(arel_table[primary_key])
         
     | 
| 
      
 29 
     | 
    
         
            +
                                                     .distinct
         
     | 
| 
      
 30 
     | 
    
         
            +
                                                     .join(tagging_table).on(tagging_table[:taggable_id].eq(arel_table[primary_key]))
         
     | 
| 
      
 31 
     | 
    
         
            +
                                                     .join(tag_table).on(tag_table[:id].eq(tagging_table[:tag_id]))
         
     | 
| 
      
 32 
     | 
    
         
            +
                                                     .where(tagging_table[:context].eq(singular_name))
         
     | 
| 
      
 33 
     | 
    
         
            +
                                                     .where(tag_table[:name].in(tags))
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
                          where(arel_table[primary_key].in(query))
         
     | 
| 
      
 36 
     | 
    
         
            +
                        }
         
     | 
| 
      
 37 
     | 
    
         
            +
             
     | 
| 
      
 38 
     | 
    
         
            +
                        scope "with_all_#{context}", lambda { |*tags|
         
     | 
| 
      
 39 
     | 
    
         
            +
                          tags = tags.flatten.compact.uniq
         
     | 
| 
      
 40 
     | 
    
         
            +
                          return none if tags.empty?
         
     | 
| 
      
 41 
     | 
    
         
            +
             
     | 
| 
      
 42 
     | 
    
         
            +
                          count_function = Arel::Nodes::NamedFunction.new(
         
     | 
| 
      
 43 
     | 
    
         
            +
                            'COUNT',
         
     | 
| 
      
 44 
     | 
    
         
            +
                            [Arel::Nodes::NamedFunction.new('DISTINCT', [tag_table[:name]])]
         
     | 
| 
      
 45 
     | 
    
         
            +
                          )
         
     | 
| 
      
 46 
     | 
    
         
            +
             
     | 
| 
      
 47 
     | 
    
         
            +
                          query = Arel::SelectManager.new(self)
         
     | 
| 
      
 48 
     | 
    
         
            +
                                                     .from(table_name)
         
     | 
| 
      
 49 
     | 
    
         
            +
                                                     .project(arel_table[primary_key])
         
     | 
| 
      
 50 
     | 
    
         
            +
                                                     .join(tagging_table).on(tagging_table[:taggable_id].eq(arel_table[primary_key]))
         
     | 
| 
      
 51 
     | 
    
         
            +
                                                     .join(tag_table).on(tag_table[:id].eq(tagging_table[:tag_id]))
         
     | 
| 
      
 52 
     | 
    
         
            +
                                                     .where(tagging_table[:context].eq(singular_name))
         
     | 
| 
      
 53 
     | 
    
         
            +
                                                     .where(tag_table[:name].in(tags))
         
     | 
| 
      
 54 
     | 
    
         
            +
                                                     .group(arel_table[primary_key])
         
     | 
| 
      
 55 
     | 
    
         
            +
                                                     .having(count_function.eq(tags.size))
         
     | 
| 
      
 56 
     | 
    
         
            +
             
     | 
| 
      
 57 
     | 
    
         
            +
                          where(arel_table[primary_key].in(query))
         
     | 
| 
      
 58 
     | 
    
         
            +
                        }
         
     | 
| 
      
 59 
     | 
    
         
            +
             
     | 
| 
      
 60 
     | 
    
         
            +
                        # Find records without any of the specified tags
         
     | 
| 
      
 61 
     | 
    
         
            +
                        scope "without_any_#{context}", lambda { |*tags|
         
     | 
| 
      
 62 
     | 
    
         
            +
                          tags = tags.flatten.compact.uniq
         
     | 
| 
      
 63 
     | 
    
         
            +
                          return all if tags.empty?
         
     | 
| 
      
 64 
     | 
    
         
            +
             
     | 
| 
      
 65 
     | 
    
         
            +
                          subquery = joins(tagging_table)
         
     | 
| 
      
 66 
     | 
    
         
            +
                                     .where(tagging_table[:context].eq(singular_name))
         
     | 
| 
      
 67 
     | 
    
         
            +
                                     .where(tagging_table.name => { context: singular_name })
         
     | 
| 
      
 68 
     | 
    
         
            +
                                     .where(tag_table.name => { name: tags })
         
     | 
| 
      
 69 
     | 
    
         
            +
                                     .select(primary_key)
         
     | 
| 
      
 70 
     | 
    
         
            +
             
     | 
| 
      
 71 
     | 
    
         
            +
                          where("#{table_name}.#{primary_key} NOT IN (?)", subquery)
         
     | 
| 
      
 72 
     | 
    
         
            +
                        }
         
     | 
| 
      
 73 
     | 
    
         
            +
             
     | 
| 
      
 74 
     | 
    
         
            +
                        # Find records without any tags
         
     | 
| 
      
 75 
     | 
    
         
            +
                        scope "without_#{context}", lambda {
         
     | 
| 
      
 76 
     | 
    
         
            +
                          subquery = if setup.polymorphic
         
     | 
| 
      
 77 
     | 
    
         
            +
                                       setup.tagging_class_name.constantize
         
     | 
| 
      
 78 
     | 
    
         
            +
                                            .where(context: singular_name)
         
     | 
| 
      
 79 
     | 
    
         
            +
                                            .where(taggable_type: name)
         
     | 
| 
      
 80 
     | 
    
         
            +
                                            .select(:taggable_id)
         
     | 
| 
      
 81 
     | 
    
         
            +
                                     else
         
     | 
| 
      
 82 
     | 
    
         
            +
                                       setup.tagging_class_name.constantize
         
     | 
| 
      
 83 
     | 
    
         
            +
                                            .where(context: singular_name)
         
     | 
| 
      
 84 
     | 
    
         
            +
                                            .select(:taggable_id)
         
     | 
| 
      
 85 
     | 
    
         
            +
                                     end
         
     | 
| 
      
 86 
     | 
    
         
            +
             
     | 
| 
      
 87 
     | 
    
         
            +
                          where("#{table_name}.#{primary_key} NOT IN (?)", subquery)
         
     | 
| 
      
 88 
     | 
    
         
            +
                        }
         
     | 
| 
      
 89 
     | 
    
         
            +
             
     | 
| 
      
 90 
     | 
    
         
            +
                        # Find records with exactly these tags
         
     | 
| 
      
 91 
     | 
    
         
            +
                        scope "with_exact_#{context}", lambda { |*tags|
         
     | 
| 
      
 92 
     | 
    
         
            +
                          tags = tags.flatten.compact.uniq
         
     | 
| 
      
 93 
     | 
    
         
            +
             
     | 
| 
      
 94 
     | 
    
         
            +
                          if tags.empty?
         
     | 
| 
      
 95 
     | 
    
         
            +
                            send("without_#{context}")
         
     | 
| 
      
 96 
     | 
    
         
            +
                          else
         
     | 
| 
      
 97 
     | 
    
         
            +
                            Arel::Nodes::NamedFunction.new(
         
     | 
| 
      
 98 
     | 
    
         
            +
                              'COUNT',
         
     | 
| 
      
 99 
     | 
    
         
            +
                              [Arel::Nodes::NamedFunction.new('DISTINCT', [tag_table[:id]])]
         
     | 
| 
      
 100 
     | 
    
         
            +
                            )
         
     | 
| 
      
 101 
     | 
    
         
            +
             
     | 
| 
      
 102 
     | 
    
         
            +
                            # Build the query for records having exactly the tags
         
     | 
| 
      
 103 
     | 
    
         
            +
                            all_tags_query = select(arel_table[primary_key])
         
     | 
| 
      
 104 
     | 
    
         
            +
                                             .joins("INNER JOIN #{tagging_table.name} ON #{tagging_table.name}.taggable_id = #{table_name}.#{primary_key}")
         
     | 
| 
      
 105 
     | 
    
         
            +
                                             .joins("INNER JOIN #{tag_table.name} ON #{tag_table.name}.id = #{tagging_table.name}.tag_id")
         
     | 
| 
      
 106 
     | 
    
         
            +
                                             .where("#{tagging_table.name}.context = ?", context.to_s.singularize)
         
     | 
| 
      
 107 
     | 
    
         
            +
                                             .where("#{tag_table.name}.name IN (?)", tags)
         
     | 
| 
      
 108 
     | 
    
         
            +
                                             .group(arel_table[primary_key])
         
     | 
| 
      
 109 
     | 
    
         
            +
                                             .having("COUNT(DISTINCT #{tag_table.name}.id) = ?", tags.size)
         
     | 
| 
      
 110 
     | 
    
         
            +
             
     | 
| 
      
 111 
     | 
    
         
            +
                            # Build query for records with other tags
         
     | 
| 
      
 112 
     | 
    
         
            +
                            other_tags_query = select(arel_table[primary_key])
         
     | 
| 
      
 113 
     | 
    
         
            +
                                               .joins("INNER JOIN #{tagging_table.name} ON #{tagging_table.name}.taggable_id = #{table_name}.#{primary_key}")
         
     | 
| 
      
 114 
     | 
    
         
            +
                                               .joins("INNER JOIN #{tag_table.name} ON #{tag_table.name}.id = #{tagging_table.name}.tag_id")
         
     | 
| 
      
 115 
     | 
    
         
            +
                                               .where("#{tagging_table.name}.context = ?", context.to_s.singularize)
         
     | 
| 
      
 116 
     | 
    
         
            +
                                               .where("#{tag_table.name}.name NOT IN (?)", tags)
         
     | 
| 
      
 117 
     | 
    
         
            +
             
     | 
| 
      
 118 
     | 
    
         
            +
                            # Combine queries
         
     | 
| 
      
 119 
     | 
    
         
            +
                            where("#{table_name}.#{primary_key} IN (?)", all_tags_query)
         
     | 
| 
      
 120 
     | 
    
         
            +
                              .where("#{table_name}.#{primary_key} NOT IN (?)", other_tags_query)
         
     | 
| 
      
 121 
     | 
    
         
            +
                          end
         
     | 
| 
      
 122 
     | 
    
         
            +
                        }
         
     | 
| 
      
 123 
     | 
    
         
            +
                      end
         
     | 
| 
      
 124 
     | 
    
         
            +
                    end
         
     | 
| 
      
 125 
     | 
    
         
            +
                  end
         
     | 
| 
      
 126 
     | 
    
         
            +
                end
         
     | 
| 
      
 127 
     | 
    
         
            +
              end
         
     | 
| 
      
 128 
     | 
    
         
            +
            end
         
     |