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
 
| 
         @@ -0,0 +1,128 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module NoFlyList
         
     | 
| 
      
 4 
     | 
    
         
            +
              module TaggableRecord
         
     | 
| 
      
 5 
     | 
    
         
            +
                module Query
         
     | 
| 
      
 6 
     | 
    
         
            +
                  module PostgresqlStrategy
         
     | 
| 
      
 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
         
     | 
| 
      
 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.not(primary_key => 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.not(primary_key => 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
         
     | 
| 
         @@ -0,0 +1,163 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module NoFlyList
         
     | 
| 
      
 4 
     | 
    
         
            +
              module TaggableRecord
         
     | 
| 
      
 5 
     | 
    
         
            +
                module Query
         
     | 
| 
      
 6 
     | 
    
         
            +
                  module SqliteStrategy
         
     | 
| 
      
 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
         
     | 
| 
      
 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 
     | 
    
         
            +
                          # Build dynamic joins
         
     | 
| 
      
 66 
     | 
    
         
            +
                          tagged_ids = distinct
         
     | 
| 
      
 67 
     | 
    
         
            +
                                       .joins("INNER JOIN #{tagging_table} ON #{tagging_table}.taggable_id = #{table_name}.id")
         
     | 
| 
      
 68 
     | 
    
         
            +
                                       .joins("INNER JOIN #{context} ON #{context}.id = #{tagging_table}.tag_id")
         
     | 
| 
      
 69 
     | 
    
         
            +
                                       .where("#{context}.name IN (?)", tags)
         
     | 
| 
      
 70 
     | 
    
         
            +
                                       .pluck("#{table_name}.id")
         
     | 
| 
      
 71 
     | 
    
         
            +
             
     | 
| 
      
 72 
     | 
    
         
            +
                          # Handle empty tagged_ids explicitly for SQLite compatibility
         
     | 
| 
      
 73 
     | 
    
         
            +
                          where("#{table_name}.id NOT IN (?)", tagged_ids.present? ? tagged_ids : [-1])
         
     | 
| 
      
 74 
     | 
    
         
            +
                        }
         
     | 
| 
      
 75 
     | 
    
         
            +
             
     | 
| 
      
 76 
     | 
    
         
            +
                        # Find records without any tags
         
     | 
| 
      
 77 
     | 
    
         
            +
                        scope "without_#{context}", lambda {
         
     | 
| 
      
 78 
     | 
    
         
            +
                          subquery = if setup.polymorphic
         
     | 
| 
      
 79 
     | 
    
         
            +
                                       setup.tagging_class_name.constantize
         
     | 
| 
      
 80 
     | 
    
         
            +
                                            .where(context: singular_name, taggable_type: name)
         
     | 
| 
      
 81 
     | 
    
         
            +
                                            .select(:taggable_id)
         
     | 
| 
      
 82 
     | 
    
         
            +
                                     else
         
     | 
| 
      
 83 
     | 
    
         
            +
                                       setup.tagging_class_name.constantize
         
     | 
| 
      
 84 
     | 
    
         
            +
                                            .where(context: singular_name)
         
     | 
| 
      
 85 
     | 
    
         
            +
                                            .select(:taggable_id)
         
     | 
| 
      
 86 
     | 
    
         
            +
                                     end
         
     | 
| 
      
 87 
     | 
    
         
            +
                          where('id 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 = ?", singular_name)
         
     | 
| 
      
 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 = ?", singular_name)
         
     | 
| 
      
 116 
     | 
    
         
            +
                                               .where("#{tag_table.name}.name NOT IN (?)", tags)
         
     | 
| 
      
 117 
     | 
    
         
            +
             
     | 
| 
      
 118 
     | 
    
         
            +
                            # Combine queries using subqueries
         
     | 
| 
      
 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 
     | 
    
         
            +
             
     | 
| 
      
 124 
     | 
    
         
            +
                        # Add tag counts
         
     | 
| 
      
 125 
     | 
    
         
            +
                        # Find records with exactly these tags
         
     | 
| 
      
 126 
     | 
    
         
            +
                        scope "with_exact_#{context}", lambda { |*tags|
         
     | 
| 
      
 127 
     | 
    
         
            +
                          tags = tags.flatten.compact.uniq
         
     | 
| 
      
 128 
     | 
    
         
            +
             
     | 
| 
      
 129 
     | 
    
         
            +
                          if tags.empty?
         
     | 
| 
      
 130 
     | 
    
         
            +
                            send("without_#{context}")
         
     | 
| 
      
 131 
     | 
    
         
            +
                          else
         
     | 
| 
      
 132 
     | 
    
         
            +
                            Arel::Nodes::NamedFunction.new(
         
     | 
| 
      
 133 
     | 
    
         
            +
                              'COUNT',
         
     | 
| 
      
 134 
     | 
    
         
            +
                              [Arel::Nodes::NamedFunction.new('DISTINCT', [tag_table[:id]])]
         
     | 
| 
      
 135 
     | 
    
         
            +
                            )
         
     | 
| 
      
 136 
     | 
    
         
            +
             
     | 
| 
      
 137 
     | 
    
         
            +
                            # Build the query for records having exactly the tags
         
     | 
| 
      
 138 
     | 
    
         
            +
                            all_tags_query = select(arel_table[primary_key])
         
     | 
| 
      
 139 
     | 
    
         
            +
                                             .joins("INNER JOIN #{tagging_table.name} ON #{tagging_table.name}.taggable_id = #{table_name}.#{primary_key}")
         
     | 
| 
      
 140 
     | 
    
         
            +
                                             .joins("INNER JOIN #{tag_table.name} ON #{tag_table.name}.id = #{tagging_table.name}.tag_id")
         
     | 
| 
      
 141 
     | 
    
         
            +
                                             .where("#{tagging_table.name}.context = ?", context.to_s.singularize)
         
     | 
| 
      
 142 
     | 
    
         
            +
                                             .where("#{tag_table.name}.name IN (?)", tags)
         
     | 
| 
      
 143 
     | 
    
         
            +
                                             .group(arel_table[primary_key])
         
     | 
| 
      
 144 
     | 
    
         
            +
                                             .having("COUNT(DISTINCT #{tag_table.name}.id) = ?", tags.size)
         
     | 
| 
      
 145 
     | 
    
         
            +
             
     | 
| 
      
 146 
     | 
    
         
            +
                            # Build query for records with other tags
         
     | 
| 
      
 147 
     | 
    
         
            +
                            other_tags_query = select(arel_table[primary_key])
         
     | 
| 
      
 148 
     | 
    
         
            +
                                               .joins("INNER JOIN #{tagging_table.name} ON #{tagging_table.name}.taggable_id = #{table_name}.#{primary_key}")
         
     | 
| 
      
 149 
     | 
    
         
            +
                                               .joins("INNER JOIN #{tag_table.name} ON #{tag_table.name}.id = #{tagging_table.name}.tag_id")
         
     | 
| 
      
 150 
     | 
    
         
            +
                                               .where("#{tagging_table.name}.context = ?", context.to_s.singularize)
         
     | 
| 
      
 151 
     | 
    
         
            +
                                               .where("#{tag_table.name}.name NOT IN (?)", tags)
         
     | 
| 
      
 152 
     | 
    
         
            +
             
     | 
| 
      
 153 
     | 
    
         
            +
                            # Combine queries
         
     | 
| 
      
 154 
     | 
    
         
            +
                            where("#{table_name}.#{primary_key} IN (?)", all_tags_query)
         
     | 
| 
      
 155 
     | 
    
         
            +
                              .where("#{table_name}.#{primary_key} NOT IN (?)", other_tags_query)
         
     | 
| 
      
 156 
     | 
    
         
            +
                          end
         
     | 
| 
      
 157 
     | 
    
         
            +
                        }
         
     | 
| 
      
 158 
     | 
    
         
            +
                      end
         
     | 
| 
      
 159 
     | 
    
         
            +
                    end
         
     | 
| 
      
 160 
     | 
    
         
            +
                  end
         
     | 
| 
      
 161 
     | 
    
         
            +
                end
         
     | 
| 
      
 162 
     | 
    
         
            +
              end
         
     | 
| 
      
 163 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -6,93 +6,25 @@ module NoFlyList 
     | 
|
| 
       6 
6 
     | 
    
         
             
                  module_function
         
     | 
| 
       7 
7 
     | 
    
         | 
| 
       8 
8 
     | 
    
         
             
                  def define_query_methods(setup)
         
     | 
| 
       9 
     | 
    
         
            -
                     
     | 
| 
       10 
     | 
    
         
            -
                     
     | 
| 
       11 
     | 
    
         
            -
             
     | 
| 
       12 
     | 
    
         
            -
             
     | 
| 
       13 
     | 
    
         
            -
             
     | 
| 
       14 
     | 
    
         
            -
             
     | 
| 
       15 
     | 
    
         
            -
                       
     | 
| 
       16 
     | 
    
         
            -
             
     | 
| 
       17 
     | 
    
         
            -
             
     | 
| 
       18 
     | 
    
         
            -
             
     | 
| 
       19 
     | 
    
         
            -
                        joins(:"#{singular_name}_taggings")
         
     | 
| 
       20 
     | 
    
         
            -
                          .joins(context)
         
     | 
| 
       21 
     | 
    
         
            -
                          .where("#{singular_name}_taggings": { context: singular_name })
         
     | 
| 
       22 
     | 
    
         
            -
                          .where(context => { name: tags })
         
     | 
| 
       23 
     | 
    
         
            -
                          .distinct
         
     | 
| 
       24 
     | 
    
         
            -
                      }
         
     | 
| 
       25 
     | 
    
         
            -
             
     | 
| 
       26 
     | 
    
         
            -
                      # Find records without any tags
         
     | 
| 
       27 
     | 
    
         
            -
                      scope "without_#{context}", lambda {
         
     | 
| 
       28 
     | 
    
         
            -
                        where.not(
         
     | 
| 
       29 
     | 
    
         
            -
                          id: setup.tagging_class_name.constantize.where(context: singular_name).select(:taggable_id)
         
     | 
| 
       30 
     | 
    
         
            -
                        )
         
     | 
| 
       31 
     | 
    
         
            -
                      }
         
     | 
| 
       32 
     | 
    
         
            -
             
     | 
| 
       33 
     | 
    
         
            -
                      # Find records with all specified tags
         
     | 
| 
       34 
     | 
    
         
            -
                      scope "with_all_#{context}", lambda { |*tags|
         
     | 
| 
       35 
     | 
    
         
            -
                        tags = tags.flatten.compact.uniq
         
     | 
| 
       36 
     | 
    
         
            -
                        return none if tags.empty?
         
     | 
| 
       37 
     | 
    
         
            -
             
     | 
| 
       38 
     | 
    
         
            -
                        tag_count = tags.size
         
     | 
| 
       39 
     | 
    
         
            -
                        joins(:"#{singular_name}_taggings")
         
     | 
| 
       40 
     | 
    
         
            -
                          .joins(context)
         
     | 
| 
       41 
     | 
    
         
            -
                          .where("#{singular_name}_taggings": { context: singular_name })
         
     | 
| 
       42 
     | 
    
         
            -
                          .where(context => { name: tags })
         
     | 
| 
       43 
     | 
    
         
            -
                          .group(:id)
         
     | 
| 
       44 
     | 
    
         
            -
                          .having("COUNT(DISTINCT #{context}.name) = ?", tag_count)
         
     | 
| 
       45 
     | 
    
         
            -
                      }
         
     | 
| 
       46 
     | 
    
         
            -
             
     | 
| 
       47 
     | 
    
         
            -
                      # Find records without specific tags
         
     | 
| 
       48 
     | 
    
         
            -
                      scope "without_any_#{context}", lambda { |*tags|
         
     | 
| 
       49 
     | 
    
         
            -
                        tags = tags.flatten.compact.uniq
         
     | 
| 
       50 
     | 
    
         
            -
                        return all if tags.empty?
         
     | 
| 
       51 
     | 
    
         
            -
             
     | 
| 
       52 
     | 
    
         
            -
                        where.not(
         
     | 
| 
       53 
     | 
    
         
            -
                          id: joins(:"#{singular_name}_taggings")
         
     | 
| 
       54 
     | 
    
         
            -
                                .joins(context)
         
     | 
| 
       55 
     | 
    
         
            -
                                .where("#{singular_name}_taggings": { context: singular_name })
         
     | 
| 
       56 
     | 
    
         
            -
                                .where(context => { name: tags })
         
     | 
| 
       57 
     | 
    
         
            -
                                .select(:id)
         
     | 
| 
       58 
     | 
    
         
            -
                        )
         
     | 
| 
       59 
     | 
    
         
            -
                      }
         
     | 
| 
       60 
     | 
    
         
            -
             
     | 
| 
       61 
     | 
    
         
            -
                      # Find records with exactly these tags
         
     | 
| 
       62 
     | 
    
         
            -
                      scope "with_exact_#{context}", lambda { |*tags|
         
     | 
| 
       63 
     | 
    
         
            -
                        tags = tags.flatten.compact.uniq
         
     | 
| 
      
 9 
     | 
    
         
            +
                    case setup.adapter
         
     | 
| 
      
 10 
     | 
    
         
            +
                    when :postgresql
         
     | 
| 
      
 11 
     | 
    
         
            +
                      PostgresqlStrategy.define_query_methods(setup)
         
     | 
| 
      
 12 
     | 
    
         
            +
                    when :mysql
         
     | 
| 
      
 13 
     | 
    
         
            +
                      MysqlStrategy.define_query_methods(setup)
         
     | 
| 
      
 14 
     | 
    
         
            +
                    else
         
     | 
| 
      
 15 
     | 
    
         
            +
                      SqliteStrategy.define_query_methods(setup)
         
     | 
| 
      
 16 
     | 
    
         
            +
                    end
         
     | 
| 
      
 17 
     | 
    
         
            +
                  end
         
     | 
| 
       64 
18 
     | 
    
         | 
| 
       65 
     | 
    
         
            -
             
     | 
| 
       66 
     | 
    
         
            -
             
     | 
| 
       67 
     | 
    
         
            -
                        else
         
     | 
| 
       68 
     | 
    
         
            -
                          # Get records with the exact count of specified tags
         
     | 
| 
       69 
     | 
    
         
            -
                          having_exact_tags =
         
     | 
| 
       70 
     | 
    
         
            -
                            joins(:"#{singular_name}_taggings")
         
     | 
| 
       71 
     | 
    
         
            -
                            .joins(context)
         
     | 
| 
       72 
     | 
    
         
            -
                            .where("#{singular_name}_taggings": { context: singular_name })
         
     | 
| 
       73 
     | 
    
         
            -
                            .where(context => { name: tags })
         
     | 
| 
       74 
     | 
    
         
            -
                            .group(:id)
         
     | 
| 
       75 
     | 
    
         
            -
                            .having("COUNT(DISTINCT #{context}.name) = ?", tags.size)
         
     | 
| 
       76 
     | 
    
         
            -
                            .select(:id)
         
     | 
| 
      
 19 
     | 
    
         
            +
                  module BaseStrategy
         
     | 
| 
      
 20 
     | 
    
         
            +
                    module_function
         
     | 
| 
       77 
21 
     | 
    
         | 
| 
       78 
     | 
    
         
            -
             
     | 
| 
       79 
     | 
    
         
            -
             
     | 
| 
       80 
     | 
    
         
            -
             
     | 
| 
       81 
     | 
    
         
            -
                                  .joins(context)
         
     | 
| 
       82 
     | 
    
         
            -
                                  .where("#{singular_name}_taggings": { context: singular_name })
         
     | 
| 
       83 
     | 
    
         
            -
                                  .where.not(context => { name: tags })
         
     | 
| 
       84 
     | 
    
         
            -
                                  .select(:id)
         
     | 
| 
       85 
     | 
    
         
            -
                          )
         
     | 
| 
       86 
     | 
    
         
            -
                        end
         
     | 
| 
       87 
     | 
    
         
            -
                      }
         
     | 
| 
      
 22 
     | 
    
         
            +
                    def case_insensitive_where(table, column, values)
         
     | 
| 
      
 23 
     | 
    
         
            +
                      raise NotImplementedError
         
     | 
| 
      
 24 
     | 
    
         
            +
                    end
         
     | 
| 
       88 
25 
     | 
    
         | 
| 
       89 
     | 
    
         
            -
             
     | 
| 
       90 
     | 
    
         
            -
                       
     | 
| 
       91 
     | 
    
         
            -
                        left_joins(:"#{singular_name}_taggings")
         
     | 
| 
       92 
     | 
    
         
            -
                          .where("#{singular_name}_taggings": { context: singular_name })
         
     | 
| 
       93 
     | 
    
         
            -
                          .group(:id)
         
     | 
| 
       94 
     | 
    
         
            -
                          .select("#{table_name}.*, COUNT(DISTINCT #{singular_name}_taggings.id) as #{context}_count")
         
     | 
| 
       95 
     | 
    
         
            -
                      }
         
     | 
| 
      
 26 
     | 
    
         
            +
                    def define_query_methods(setup)
         
     | 
| 
      
 27 
     | 
    
         
            +
                      raise NotImplementedError
         
     | 
| 
       96 
28 
     | 
    
         
             
                    end
         
     | 
| 
       97 
29 
     | 
    
         
             
                  end
         
     | 
| 
       98 
30 
     | 
    
         
             
                end
         
     | 
| 
         @@ -0,0 +1,52 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module NoFlyList
         
     | 
| 
      
 4 
     | 
    
         
            +
              module TaggableRecord
         
     | 
| 
      
 5 
     | 
    
         
            +
                class TagSetup
         
     | 
| 
      
 6 
     | 
    
         
            +
                  attr_reader :taggable_klass, :context, :transformer, :polymorphic,
         
     | 
| 
      
 7 
     | 
    
         
            +
                              :restrict_to_existing, :limit,
         
     | 
| 
      
 8 
     | 
    
         
            +
                              :tag_class_name, :tagging_class_name, :adapter
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
                  def initialize(taggable_klass, context, options = {})
         
     | 
| 
      
 11 
     | 
    
         
            +
                    @taggable_klass = taggable_klass
         
     | 
| 
      
 12 
     | 
    
         
            +
                    @context = context
         
     | 
| 
      
 13 
     | 
    
         
            +
                    @transformer = options.fetch(:transformer, ApplicationTagTransformer)
         
     | 
| 
      
 14 
     | 
    
         
            +
                    @polymorphic = options.fetch(:polymorphic, false)
         
     | 
| 
      
 15 
     | 
    
         
            +
                    @restrict_to_existing = options.fetch(:restrict_to_existing, false)
         
     | 
| 
      
 16 
     | 
    
         
            +
                    @limit = options.fetch(:limit, nil)
         
     | 
| 
      
 17 
     | 
    
         
            +
                    @tag_class_name = determine_tag_class_name(taggable_klass, options)
         
     | 
| 
      
 18 
     | 
    
         
            +
                    @tagging_class_name = determine_tagging_class_name(taggable_klass, options)
         
     | 
| 
      
 19 
     | 
    
         
            +
                    @adapter = determine_adapter
         
     | 
| 
      
 20 
     | 
    
         
            +
                  end
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
                  private
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
                  def determine_adapter
         
     | 
| 
      
 25 
     | 
    
         
            +
                    case ActiveRecord::Base.connection.adapter_name.downcase
         
     | 
| 
      
 26 
     | 
    
         
            +
                    when 'postgresql'
         
     | 
| 
      
 27 
     | 
    
         
            +
                      :postgresql
         
     | 
| 
      
 28 
     | 
    
         
            +
                    when 'mysql2'
         
     | 
| 
      
 29 
     | 
    
         
            +
                      :mysql
         
     | 
| 
      
 30 
     | 
    
         
            +
                    else
         
     | 
| 
      
 31 
     | 
    
         
            +
                      :sqlite
         
     | 
| 
      
 32 
     | 
    
         
            +
                    end
         
     | 
| 
      
 33 
     | 
    
         
            +
                  end
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
                  def determine_tag_class_name(taggable_klass, options)
         
     | 
| 
      
 36 
     | 
    
         
            +
                    if options[:polymorphic]
         
     | 
| 
      
 37 
     | 
    
         
            +
                      Rails.application.config.no_fly_list.tag_class_name
         
     | 
| 
      
 38 
     | 
    
         
            +
                    else
         
     | 
| 
      
 39 
     | 
    
         
            +
                      options.fetch(:tag_class_name, "#{taggable_klass.name}Tag")
         
     | 
| 
      
 40 
     | 
    
         
            +
                    end
         
     | 
| 
      
 41 
     | 
    
         
            +
                  end
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
                  def determine_tagging_class_name(taggable_klass, options)
         
     | 
| 
      
 44 
     | 
    
         
            +
                    if options[:polymorphic]
         
     | 
| 
      
 45 
     | 
    
         
            +
                      Rails.application.config.no_fly_list.tagging_class_name
         
     | 
| 
      
 46 
     | 
    
         
            +
                    else
         
     | 
| 
      
 47 
     | 
    
         
            +
                      options.fetch(:tagging_class_name, "#{taggable_klass.name}::Tagging")
         
     | 
| 
      
 48 
     | 
    
         
            +
                    end
         
     | 
| 
      
 49 
     | 
    
         
            +
                  end
         
     | 
| 
      
 50 
     | 
    
         
            +
                end
         
     | 
| 
      
 51 
     | 
    
         
            +
              end
         
     | 
| 
      
 52 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -5,22 +5,73 @@ module NoFlyList 
     | 
|
| 
       5 
5 
     | 
    
         
             
                extend ActiveSupport::Concern
         
     | 
| 
       6 
6 
     | 
    
         | 
| 
       7 
7 
     | 
    
         
             
                included do
         
     | 
| 
      
 8 
     | 
    
         
            +
                  class_attribute :_no_fly_list, instance_writer: false
         
     | 
| 
      
 9 
     | 
    
         
            +
                  self._no_fly_list = Config.new self
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
       8 
11 
     | 
    
         
             
                  before_save :save_tag_proxies
         
     | 
| 
      
 12 
     | 
    
         
            +
                  before_validation :validate_tag_proxies
         
     | 
| 
       9 
13 
     | 
    
         
             
                end
         
     | 
| 
       10 
14 
     | 
    
         | 
| 
       11 
15 
     | 
    
         
             
                private
         
     | 
| 
       12 
16 
     | 
    
         | 
| 
      
 17 
     | 
    
         
            +
                def validate_tag_proxies
         
     | 
| 
      
 18 
     | 
    
         
            +
                  return true if @validating_proxies
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
                  @validating_proxies = true
         
     | 
| 
      
 21 
     | 
    
         
            +
                  begin
         
     | 
| 
      
 22 
     | 
    
         
            +
                    instance_variables.each do |var|
         
     | 
| 
      
 23 
     | 
    
         
            +
                      next unless var.to_s.match?(/_list_proxy$/)
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
                      proxy = instance_variable_get(var)
         
     | 
| 
      
 26 
     | 
    
         
            +
                      next if proxy.nil? || proxy.valid?
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
                      proxy.errors.each do |error|
         
     | 
| 
      
 29 
     | 
    
         
            +
                        errors.add(:base, error.message)
         
     | 
| 
      
 30 
     | 
    
         
            +
                      end
         
     | 
| 
      
 31 
     | 
    
         
            +
                      return false
         
     | 
| 
      
 32 
     | 
    
         
            +
                    end
         
     | 
| 
      
 33 
     | 
    
         
            +
                    true
         
     | 
| 
      
 34 
     | 
    
         
            +
                  ensure
         
     | 
| 
      
 35 
     | 
    
         
            +
                    @validating_proxies = false
         
     | 
| 
      
 36 
     | 
    
         
            +
                  end
         
     | 
| 
      
 37 
     | 
    
         
            +
                end
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
       13 
39 
     | 
    
         
             
                def save_tag_proxies
         
     | 
| 
       14 
     | 
    
         
            -
                   
     | 
| 
       15 
     | 
    
         
            -
             
     | 
| 
      
 40 
     | 
    
         
            +
                  return true if @saving_proxies
         
     | 
| 
      
 41 
     | 
    
         
            +
             
     | 
| 
      
 42 
     | 
    
         
            +
                  @saving_proxies = true
         
     | 
| 
      
 43 
     | 
    
         
            +
                  begin
         
     | 
| 
      
 44 
     | 
    
         
            +
                    instance_variables.each do |var|
         
     | 
| 
      
 45 
     | 
    
         
            +
                      next unless var.to_s.match?(/_list_proxy$/)
         
     | 
| 
       16 
46 
     | 
    
         | 
| 
       17 
     | 
    
         
            -
             
     | 
| 
       18 
     | 
    
         
            -
             
     | 
| 
      
 47 
     | 
    
         
            +
                      proxy = instance_variable_get(var)
         
     | 
| 
      
 48 
     | 
    
         
            +
                      next if proxy.nil?
         
     | 
| 
      
 49 
     | 
    
         
            +
                      return false unless proxy.save
         
     | 
| 
      
 50 
     | 
    
         
            +
                    end
         
     | 
| 
      
 51 
     | 
    
         
            +
                    true
         
     | 
| 
      
 52 
     | 
    
         
            +
                  ensure
         
     | 
| 
      
 53 
     | 
    
         
            +
                    @saving_proxies = false
         
     | 
| 
       19 
54 
     | 
    
         
             
                  end
         
     | 
| 
       20 
55 
     | 
    
         
             
                end
         
     | 
| 
       21 
56 
     | 
    
         | 
| 
      
 57 
     | 
    
         
            +
                def no_fly_list_config
         
     | 
| 
      
 58 
     | 
    
         
            +
                  self.class._no_fly_list
         
     | 
| 
      
 59 
     | 
    
         
            +
                end
         
     | 
| 
      
 60 
     | 
    
         
            +
             
     | 
| 
      
 61 
     | 
    
         
            +
                def tag_contexts
         
     | 
| 
      
 62 
     | 
    
         
            +
                  no_fly_list_config.tag_contexts
         
     | 
| 
      
 63 
     | 
    
         
            +
                end
         
     | 
| 
      
 64 
     | 
    
         
            +
             
     | 
| 
      
 65 
     | 
    
         
            +
                def options_for_context(context)
         
     | 
| 
      
 66 
     | 
    
         
            +
                  tag_contexts[context.to_sym]
         
     | 
| 
      
 67 
     | 
    
         
            +
                end
         
     | 
| 
      
 68 
     | 
    
         
            +
             
     | 
| 
       22 
69 
     | 
    
         
             
                class_methods do
         
     | 
| 
       23 
70 
     | 
    
         
             
                  def has_tags(*contexts, **options)
         
     | 
| 
      
 71 
     | 
    
         
            +
                    contexts.each do |context|
         
     | 
| 
      
 72 
     | 
    
         
            +
                      _no_fly_list.add_context(context, options)
         
     | 
| 
      
 73 
     | 
    
         
            +
                    end
         
     | 
| 
      
 74 
     | 
    
         
            +
             
     | 
| 
       24 
75 
     | 
    
         
             
                    Configuration.setup_tagging(self, contexts, options)
         
     | 
| 
       25 
76 
     | 
    
         
             
                  end
         
     | 
| 
       26 
77 
     | 
    
         
             
                end
         
     | 
| 
         @@ -57,19 +57,48 @@ module NoFlyList 
     | 
|
| 
       57 
57 
     | 
    
         
             
                # @return [Boolean] true if the proxy is valid
         
     | 
| 
       58 
58 
     | 
    
         
             
                def save
         
     | 
| 
       59 
59 
     | 
    
         
             
                  return true unless @pending_changes.any?
         
     | 
| 
      
 60 
     | 
    
         
            +
                  return false unless valid?
         
     | 
| 
      
 61 
     | 
    
         
            +
             
     | 
| 
      
 62 
     | 
    
         
            +
                  # Prevent recursive validation
         
     | 
| 
      
 63 
     | 
    
         
            +
                  @saving = true
         
     | 
| 
      
 64 
     | 
    
         
            +
                  begin
         
     | 
| 
      
 65 
     | 
    
         
            +
                    model.class.transaction do
         
     | 
| 
      
 66 
     | 
    
         
            +
                      # Always save parent first if needed
         
     | 
| 
      
 67 
     | 
    
         
            +
                      if model.new_record? && !model.save
         
     | 
| 
      
 68 
     | 
    
         
            +
                        errors.add(:base, 'Failed to save parent record')
         
     | 
| 
      
 69 
     | 
    
         
            +
                        raise ActiveRecord::Rollback
         
     | 
| 
      
 70 
     | 
    
         
            +
                      end
         
     | 
| 
      
 71 
     | 
    
         
            +
             
     | 
| 
      
 72 
     | 
    
         
            +
                      # Clear existing tags
         
     | 
| 
      
 73 
     | 
    
         
            +
                      model.send(context_taggings).delete_all
         
     | 
| 
       60 
74 
     | 
    
         | 
| 
       61 
     | 
    
         
            -
             
     | 
| 
       62 
     | 
    
         
            -
                    @model.transaction do
         
     | 
| 
       63 
     | 
    
         
            -
                      @model.send(@context.to_s).destroy_all
         
     | 
| 
      
 75 
     | 
    
         
            +
                      # Create new tags
         
     | 
| 
       64 
76 
     | 
    
         
             
                      @pending_changes.each do |tag_name|
         
     | 
| 
       65 
77 
     | 
    
         
             
                        tag = find_or_create_tag(tag_name)
         
     | 
| 
       66 
     | 
    
         
            -
                         
     | 
| 
      
 78 
     | 
    
         
            +
                        next unless tag
         
     | 
| 
      
 79 
     | 
    
         
            +
             
     | 
| 
      
 80 
     | 
    
         
            +
                        attributes = {
         
     | 
| 
      
 81 
     | 
    
         
            +
                          tag: tag,
         
     | 
| 
      
 82 
     | 
    
         
            +
                          context: @context.to_s.singularize
         
     | 
| 
      
 83 
     | 
    
         
            +
                        }
         
     | 
| 
      
 84 
     | 
    
         
            +
             
     | 
| 
      
 85 
     | 
    
         
            +
                        if setup[:polymorphic]
         
     | 
| 
      
 86 
     | 
    
         
            +
                          attributes[:taggable_type] = model.class.name
         
     | 
| 
      
 87 
     | 
    
         
            +
                          attributes[:taggable_id] = model.id
         
     | 
| 
      
 88 
     | 
    
         
            +
                        end
         
     | 
| 
      
 89 
     | 
    
         
            +
             
     | 
| 
      
 90 
     | 
    
         
            +
                        # Use create! to ensure we catch any errors
         
     | 
| 
      
 91 
     | 
    
         
            +
                        model.send(context_taggings).create!(attributes)
         
     | 
| 
       67 
92 
     | 
    
         
             
                      end
         
     | 
| 
       68 
93 
     | 
    
         
             
                    end
         
     | 
| 
      
 94 
     | 
    
         
            +
             
     | 
| 
       69 
95 
     | 
    
         
             
                    refresh_from_database
         
     | 
| 
       70 
96 
     | 
    
         
             
                    true
         
     | 
| 
       71 
     | 
    
         
            -
                   
     | 
| 
      
 97 
     | 
    
         
            +
                  rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved => e
         
     | 
| 
      
 98 
     | 
    
         
            +
                    errors.add(:base, e.message)
         
     | 
| 
       72 
99 
     | 
    
         
             
                    false
         
     | 
| 
      
 100 
     | 
    
         
            +
                  ensure
         
     | 
| 
      
 101 
     | 
    
         
            +
                    @saving = false
         
     | 
| 
       73 
102 
     | 
    
         
             
                  end
         
     | 
| 
       74 
103 
     | 
    
         
             
                end
         
     | 
| 
       75 
104 
     | 
    
         | 
| 
         @@ -175,14 +204,6 @@ module NoFlyList 
     | 
|
| 
       175 
204 
     | 
    
         
             
                  current_list
         
     | 
| 
       176 
205 
     | 
    
         
             
                end
         
     | 
| 
       177 
206 
     | 
    
         | 
| 
       178 
     | 
    
         
            -
                def current_list
         
     | 
| 
       179 
     | 
    
         
            -
                  if @pending_changes.any?
         
     | 
| 
       180 
     | 
    
         
            -
                    @pending_changes
         
     | 
| 
       181 
     | 
    
         
            -
                  else
         
     | 
| 
       182 
     | 
    
         
            -
                    @model.send(@context.to_s).pluck(:name)
         
     | 
| 
       183 
     | 
    
         
            -
                  end
         
     | 
| 
       184 
     | 
    
         
            -
                end
         
     | 
| 
       185 
     | 
    
         
            -
             
     | 
| 
       186 
207 
     | 
    
         
             
                def refresh_from_database
         
     | 
| 
       187 
208 
     | 
    
         
             
                  @pending_changes = []
         
     | 
| 
       188 
209 
     | 
    
         
             
                end
         
     | 
| 
         @@ -198,7 +219,9 @@ module NoFlyList 
     | 
|
| 
       198 
219 
     | 
    
         
             
                  return unless @restrict_to_existing
         
     | 
| 
       199 
220 
     | 
    
         
             
                  return if @pending_changes.empty?
         
     | 
| 
       200 
221 
     | 
    
         | 
| 
       201 
     | 
    
         
            -
                   
     | 
| 
      
 222 
     | 
    
         
            +
                  # Transform tags to lowercase for comparison
         
     | 
| 
      
 223 
     | 
    
         
            +
                  normalized_changes = @pending_changes.map(&:downcase)
         
     | 
| 
      
 224 
     | 
    
         
            +
                  existing_tags = @tag_model.where('LOWER(name) IN (?)', normalized_changes).pluck(:name)
         
     | 
| 
       202 
225 
     | 
    
         
             
                  missing_tags = @pending_changes - existing_tags
         
     | 
| 
       203 
226 
     | 
    
         | 
| 
       204 
227 
     | 
    
         
             
                  return unless missing_tags.any?
         
     | 
| 
         @@ -206,6 +229,17 @@ module NoFlyList 
     | 
|
| 
       206 
229 
     | 
    
         
             
                  errors.add(:base, "The following tags do not exist: #{missing_tags.join(', ')}")
         
     | 
| 
       207 
230 
     | 
    
         
             
                end
         
     | 
| 
       208 
231 
     | 
    
         | 
| 
      
 232 
     | 
    
         
            +
                def context_taggings
         
     | 
| 
      
 233 
     | 
    
         
            +
                  @context_taggings ||= "#{@context.to_s.singularize}_taggings"
         
     | 
| 
      
 234 
     | 
    
         
            +
                end
         
     | 
| 
      
 235 
     | 
    
         
            +
             
     | 
| 
      
 236 
     | 
    
         
            +
                def setup
         
     | 
| 
      
 237 
     | 
    
         
            +
                  @setup ||= begin
         
     | 
| 
      
 238 
     | 
    
         
            +
                    context = @context.to_sym
         
     | 
| 
      
 239 
     | 
    
         
            +
                    @model.class._no_fly_list.tag_contexts[context]
         
     | 
| 
      
 240 
     | 
    
         
            +
                  end
         
     | 
| 
      
 241 
     | 
    
         
            +
                end
         
     | 
| 
      
 242 
     | 
    
         
            +
             
     | 
| 
       209 
243 
     | 
    
         
             
                def find_or_create_tag(tag_name)
         
     | 
| 
       210 
244 
     | 
    
         
             
                  if @restrict_to_existing
         
     | 
| 
       211 
245 
     | 
    
         
             
                    @tag_model.find_by(name: tag_name)
         
     | 
| 
         @@ -214,6 +248,49 @@ module NoFlyList 
     | 
|
| 
       214 
248 
     | 
    
         
             
                  end
         
     | 
| 
       215 
249 
     | 
    
         
             
                end
         
     | 
| 
       216 
250 
     | 
    
         | 
| 
      
 251 
     | 
    
         
            +
                def save_changes
         
     | 
| 
      
 252 
     | 
    
         
            +
                  # Clear existing tags
         
     | 
| 
      
 253 
     | 
    
         
            +
                  model.send(context_taggings).delete_all
         
     | 
| 
      
 254 
     | 
    
         
            +
             
     | 
| 
      
 255 
     | 
    
         
            +
                  # Create new tags
         
     | 
| 
      
 256 
     | 
    
         
            +
                  @pending_changes.each do |tag_name|
         
     | 
| 
      
 257 
     | 
    
         
            +
                    tag = find_or_create_tag(tag_name)
         
     | 
| 
      
 258 
     | 
    
         
            +
                    next unless tag
         
     | 
| 
      
 259 
     | 
    
         
            +
             
     | 
| 
      
 260 
     | 
    
         
            +
                    attributes = {
         
     | 
| 
      
 261 
     | 
    
         
            +
                      tag: tag,
         
     | 
| 
      
 262 
     | 
    
         
            +
                      context: @context.to_s.singularize
         
     | 
| 
      
 263 
     | 
    
         
            +
                    }
         
     | 
| 
      
 264 
     | 
    
         
            +
             
     | 
| 
      
 265 
     | 
    
         
            +
                    # Add polymorphic attributes for polymorphic tags
         
     | 
| 
      
 266 
     | 
    
         
            +
                    if setup[:polymorphic]
         
     | 
| 
      
 267 
     | 
    
         
            +
                      attributes[:taggable_type] = model.class.name
         
     | 
| 
      
 268 
     | 
    
         
            +
                      attributes[:taggable_id] = model.id
         
     | 
| 
      
 269 
     | 
    
         
            +
                    end
         
     | 
| 
      
 270 
     | 
    
         
            +
             
     | 
| 
      
 271 
     | 
    
         
            +
                    # Use create! to ensure we catch any errors
         
     | 
| 
      
 272 
     | 
    
         
            +
                    model.send(context_taggings).create!(attributes)
         
     | 
| 
      
 273 
     | 
    
         
            +
                  end
         
     | 
| 
      
 274 
     | 
    
         
            +
             
     | 
| 
      
 275 
     | 
    
         
            +
                  refresh_from_database
         
     | 
| 
      
 276 
     | 
    
         
            +
                  true
         
     | 
| 
      
 277 
     | 
    
         
            +
                end
         
     | 
| 
      
 278 
     | 
    
         
            +
             
     | 
| 
      
 279 
     | 
    
         
            +
                def current_list
         
     | 
| 
      
 280 
     | 
    
         
            +
                  if @pending_changes.any?
         
     | 
| 
      
 281 
     | 
    
         
            +
                    @pending_changes
         
     | 
| 
      
 282 
     | 
    
         
            +
                  elsif setup[:polymorphic]
         
     | 
| 
      
 283 
     | 
    
         
            +
                    tagging_table = setup[:tagging_class_name].tableize
         
     | 
| 
      
 284 
     | 
    
         
            +
                    @model.send(@context.to_s)
         
     | 
| 
      
 285 
     | 
    
         
            +
                          .joins("INNER JOIN #{tagging_table} ON #{tagging_table}.tag_id = tags.id")
         
     | 
| 
      
 286 
     | 
    
         
            +
                          .where("#{tagging_table}.taggable_type = ? AND #{tagging_table}.taggable_id = ?",
         
     | 
| 
      
 287 
     | 
    
         
            +
                                 @model.class.name, @model.id)
         
     | 
| 
      
 288 
     | 
    
         
            +
                          .pluck(:name)
         
     | 
| 
      
 289 
     | 
    
         
            +
                  else
         
     | 
| 
      
 290 
     | 
    
         
            +
                    @model.send(@context.to_s).pluck(:name)
         
     | 
| 
      
 291 
     | 
    
         
            +
                  end
         
     | 
| 
      
 292 
     | 
    
         
            +
                end
         
     | 
| 
      
 293 
     | 
    
         
            +
             
     | 
| 
       217 
294 
     | 
    
         
             
                def limit_reached?
         
     | 
| 
       218 
295 
     | 
    
         
             
                  @limit && current_list.size >= @limit
         
     | 
| 
       219 
296 
     | 
    
         
             
                end
         
     |