module FlagShihTzu::ClassMethods

Public Instance Methods

chained_flags_condition(colmn = DEFAULT_COLUMN_NAME, *args) click to toggle source
# File lib/flag_shih_tzu.rb, line 293
def chained_flags_condition(colmn = DEFAULT_COLUMN_NAME, *args)
  %[(#{flag_full_column_name(table_name, colmn)} in (#{chained_flags_values(colmn, *args).join(",")}))]
end
chained_flags_with(column = DEFAULT_COLUMN_NAME, *args) click to toggle source
# File lib/flag_shih_tzu.rb, line 285
def chained_flags_with(column = DEFAULT_COLUMN_NAME, *args)
  if (ActiveRecord::VERSION::MAJOR >= 3)
    where(chained_flags_condition(column, *args))
  else
    all(conditions: chained_flags_condition(column, *args))
  end
end
check_flag(flag, colmn) click to toggle source
# File lib/flag_shih_tzu.rb, line 257
def check_flag(flag, colmn)
  unless colmn.is_a?(String)
    raise ArgumentError,
          %[Column name "#{colmn}" for flag "#{flag}" is not a string]
  end
  if flag_mapping[colmn].nil? || !flag_mapping[colmn].include?(flag)
    raise ArgumentError,
          %[Invalid flag "#{flag}"]
  end
end
determine_flag_colmn_for(flag) click to toggle source
# File lib/flag_shih_tzu.rb, line 275
def determine_flag_colmn_for(flag)
  return DEFAULT_COLUMN_NAME if flag_mapping.nil?
  flag_mapping.each_pair do |colmn, mapping|
    return colmn if mapping.include?(flag)
  end
  raise NoSuchFlagException.new(
    %[determine_flag_colmn_for: Couldn't determine column for your flags!]
  )
end
flag_keys(colmn = DEFAULT_COLUMN_NAME) click to toggle source
# File lib/flag_shih_tzu.rb, line 297
def flag_keys(colmn = DEFAULT_COLUMN_NAME)
  flag_mapping[colmn].keys
end
has_flags(*args) click to toggle source
# File lib/flag_shih_tzu.rb, line 24
    def has_flags(*args)
      flag_hash, opts = parse_flag_options(*args)
      opts =
        {
          named_scopes: true,
          column: DEFAULT_COLUMN_NAME,
          flag_query_mode: :in_list, # or :bit_operator
          strict: false,
          check_for_column: true
        }.update(opts)
      if !valid_flag_column_name?(opts[:column])
        warn %[FlagShihTzu says: Please use a String to designate column names! I see you here: #{caller.first}]
        opts[:column] = opts[:column].to_s
      end
      colmn = opts[:column]
      if opts[:check_for_column] && (active_record_class? && !check_flag_column(colmn))
        warn(
          %[FlagShihTzu says: Flag column #{colmn} appears to be missing!
To turn off this warning set check_for_column: false in has_flags definition here: #{caller.first}]
        )
        return
      end

      # options are stored in a class level hash and apply per-column
      self.flag_options ||= {}
      flag_options[colmn] = opts

      # the mappings are stored in this class level hash and apply per-column
      self.flag_mapping ||= {}
      # If we already have an instance of the same column in the flag_mapping,
      #   then there is a double definition on a column
      if opts[:strict] && !self.flag_mapping[colmn].nil?
        raise DuplicateFlagColumnException
      end
      flag_mapping[colmn] ||= {}

      # keep track of which flag columns are defined on this class
      self.flag_columns ||= []
      self.flag_columns << colmn

      flag_hash.each do |flag_key, flag_name|
        unless valid_flag_key?(flag_key)
          raise ArgumentError,
                %[has_flags: flag keys should be positive integers, and #{flag_key} is not]
        end
        unless valid_flag_name?(flag_name)
          raise ArgumentError,
                %[has_flags: flag names should be symbols, and #{flag_name} is not]
        end
        # next if method already defined by flag_shih_tzu
        next if flag_mapping[colmn][flag_name] & (1 << (flag_key - 1))
        if method_defined?(flag_name)
          raise ArgumentError,
                %[has_flags: flag name #{flag_name} already defined, please choose different name]
        end

        flag_mapping[colmn][flag_name] = 1 << (flag_key - 1)

        class_eval <<-EVAL, __FILE__, __LINE__ + 1
          def #{flag_name}
            flag_enabled?(:#{flag_name}, "#{colmn}")
          end
          alias :#{flag_name}? :#{flag_name}

          def #{flag_name}=(value)
            FlagShihTzu::TRUE_VALUES.include?(value) ?
              enable_flag(:#{flag_name}, "#{colmn}") :
              disable_flag(:#{flag_name}, "#{colmn}")
          end

          def not_#{flag_name}
            !#{flag_name}
          end
          alias :not_#{flag_name}? :not_#{flag_name}

          def not_#{flag_name}=(value)
            FlagShihTzu::TRUE_VALUES.include?(value) ?
              disable_flag(:#{flag_name}, "#{colmn}") :
              enable_flag(:#{flag_name}, "#{colmn}")
          end

          def #{flag_name}_changed?
            if colmn_changes = changes["#{colmn}"]
              flag_bit = self.class.flag_mapping["#{colmn}"][:#{flag_name}]
              (colmn_changes[0] & flag_bit) != (colmn_changes[1] & flag_bit)
            else
              false
            end
          end

        EVAL

        if active_record_class?
          class_eval <<-EVAL, __FILE__, __LINE__ + 1
            def self.#{flag_name}_condition(options = {})
              sql_condition_for_flag(
                :#{flag_name},
                "#{colmn}",
                true,
                options[:table_alias] || table_name
              )
            end

            def self.not_#{flag_name}_condition
              sql_condition_for_flag(:#{flag_name}, "#{colmn}", false)
            end

            def self.set_#{flag_name}_sql
              sql_set_for_flag(:#{flag_name}, "#{colmn}", true)
            end

            def self.unset_#{flag_name}_sql
              sql_set_for_flag(:#{flag_name}, "#{colmn}", false)
            end

            def self.#{colmn.singularize}_values_for(*flag_names)
              values = []
              flag_names.each do |flag_name|
                if respond_to?(flag_name)
                  values_for_flag = send(:sql_in_for_flag, flag_name, "#{colmn}")
                  values = if values.present?
                    values & values_for_flag
                  else
                    values_for_flag
                  end
                end
              end

              values.sort
            end
          EVAL

          # Define the named scopes if the user wants them and AR supports it
          if flag_options[colmn][:named_scopes]
            if ActiveRecord::VERSION::MAJOR == 2 && respond_to?(:named_scope)
              class_eval <<-EVAL, __FILE__, __LINE__ + 1
                named_scope :#{flag_name}, lambda {
                  { conditions: #{flag_name}_condition }
                }
                named_scope :not_#{flag_name}, lambda {
                  { conditions: not_#{flag_name}_condition }
                }
              EVAL
            elsif respond_to?(:scope)
              # Prevent deprecation notices on Rails 3
              #   when using +named_scope+ instead of +scope+.
              # Prevent deprecation notices on Rails 4
              #   when using +conditions+ instead of +where+.
              class_eval <<-EVAL, __FILE__, __LINE__ + 1
                scope :#{flag_name}, lambda {
                  where(#{flag_name}_condition)
                }
                scope :not_#{flag_name}, lambda {
                  where(not_#{flag_name}_condition)
                }
              EVAL
            end
          end

          if method_defined?(:saved_changes)
            class_eval <<-EVAL, __FILE__, __LINE__ + 1
              def saved_change_to_#{flag_name}?
                if colmn_changes = saved_changes["#{colmn}"]
                  flag_bit = self.class.flag_mapping["#{colmn}"][:#{flag_name}]
                  (colmn_changes[0] & flag_bit) != (colmn_changes[1] & flag_bit)
                else
                  false
                end
              end
            EVAL
          end
        end

        if colmn != DEFAULT_COLUMN_NAME
          class_eval <<-EVAL, __FILE__, __LINE__ + 1

            def all_#{colmn}
              all_flags("#{colmn}")
            end

            def selected_#{colmn}
              selected_flags("#{colmn}")
            end

            def select_all_#{colmn}
              select_all_flags("#{colmn}")
            end

            def unselect_all_#{colmn}
              unselect_all_flags("#{colmn}")
            end

            # useful for a form builder
            def selected_#{colmn}=(chosen_flags)
              unselect_all_flags("#{colmn}")
              return if chosen_flags.nil?
              chosen_flags.each do |selected_flag|
                enable_flag(selected_flag.to_sym, "#{colmn}") if selected_flag.present?
              end
            end

            def has_#{colmn.singularize}?
              not selected_#{colmn}.empty?
            end

            def chained_#{colmn}_with_signature(*args)
              chained_flags_with_signature("#{colmn}", *args)
            end

            def as_#{colmn.singularize}_collection(*args)
              as_flag_collection("#{colmn}", *args)
            end

          EVAL
        end

        # Define bang methods when requested
        if flag_options[colmn][:bang_methods]
          class_eval <<-EVAL, __FILE__, __LINE__ + 1
            def #{flag_name}!
              enable_flag(:#{flag_name}, "#{colmn}")
            end

            def not_#{flag_name}!
              disable_flag(:#{flag_name}, "#{colmn}")
            end
          EVAL
        end

      end

    end
set_flag_sql(flag, value, colmn = nil, custom_table_name = table_name) click to toggle source

Returns SQL statement to enable/disable flag. Automatically determines the correct column.

# File lib/flag_shih_tzu.rb, line 270
def set_flag_sql(flag, value, colmn = nil, custom_table_name = table_name)
  colmn = determine_flag_colmn_for(flag) if colmn.nil?
  sql_set_for_flag(flag, colmn, value, custom_table_name)
end

Private Instance Methods

active_record_class?() click to toggle source
# File lib/flag_shih_tzu.rb, line 462
def active_record_class?
  ancestors.include?(ActiveRecord::Base)
end
chained_flags_values(colmn, *args) click to toggle source
# File lib/flag_shih_tzu.rb, line 321
def chained_flags_values(colmn, *args)
  val = flag_value_range_for_column(colmn).to_a
  args.each do |flag|
    neg = false
    if flag.to_s.match(/^not_/)
      neg = true
      flag = flag.to_s.sub(/^not_/, "").to_sym
    end
    check_flag(flag, colmn)
    flag_values = sql_in_for_flag(flag, colmn)
    if neg
      val = val - flag_values
    else
      val = val & flag_values
    end
  end
  val
end
check_flag_column(colmn, custom_table_name = table_name) click to toggle source
# File lib/flag_shih_tzu.rb, line 356
def check_flag_column(colmn, custom_table_name = table_name)
  # If you aren't using ActiveRecord (eg. you are outside rails)
  #   then do not fail here
  # If you are using ActiveRecord then you only want to check for the
  #   table if the table exists so it won't fail pre-migration
  has_ar = (!!defined?(ActiveRecord) && respond_to?(:descends_from_active_record?))
  # Supposedly Rails 2.3 takes care of this, but this precaution
  #   is needed for backwards compatibility
  has_table = if has_ar
                if ::ActiveRecord::VERSION::MAJOR >= 5
                  connection.data_sources.include?(custom_table_name)
                else
                  connection.tables.include?(custom_table_name)
                end
              else
                true
              end
  if has_table
    found_column = columns.detect { |column| column.name == colmn }
    # If you have not yet run the migration that adds the 'flags' column
    #   then we don't want to fail,
    #   because we need to be able to run the migration
    # If the column is there but is of the wrong type,
    #   then we must fail, because flag_shih_tzu will not work
    if found_column.nil?
      warn(
        %[Error: Column "#{colmn}" doesn't exist on table "#{custom_table_name}". Did you forget to run migrations?]
      )
      return false
    elsif found_column.type != :integer
      raise IncorrectFlagColumnException.new(
        %[Table "#{custom_table_name}" must have an integer column named "#{colmn}" in order to use FlagShihTzu.]
      )
    end
  else
    # ActiveRecord gem may not have loaded yet?
    warn(
      %[FlagShihTzu#has_flags: Table "#{custom_table_name}" doesn't exist.  Have all migrations been run?]
    ) if has_ar
    return false
  end

  true

  # Quietly ignore NoDatabaseErrors - presumably we're being run during, eg, `rails db:create`.
  # NoDatabaseError was only introduced in Rails 4.1, which is why this error-handling is a bit convoluted.
rescue StandardError => e
  if defined?(ActiveRecord::NoDatabaseError) && e.is_a?(ActiveRecord::NoDatabaseError)
    true
  else
    raise
  end
end
flag_full_column_name(table, column) click to toggle source
# File lib/flag_shih_tzu.rb, line 303
def flag_full_column_name(table, column)
  "#{connection.quote_table_name(table)}.#{connection.quote_column_name(column)}"
end
flag_full_column_name_for_assignment(table, column) click to toggle source
# File lib/flag_shih_tzu.rb, line 307
def flag_full_column_name_for_assignment(table, column)
  if (ActiveRecord::VERSION::MAJOR <= 3)
    # If you're trying to do multi-table updates with Rails < 4, sorry - you're out of luck.
    connection.quote_column_name(column)
  else
    connection.quote_table_name_for_assignment(table, column)
  end
end
flag_value_range_for_column(colmn) click to toggle source
# File lib/flag_shih_tzu.rb, line 316
def flag_value_range_for_column(colmn)
  max = flag_mapping[colmn].values.max
  Range.new(0, (2 * max) - 1)
end
named_scope_method() click to toggle source

Returns the correct method to create a named scope. Use to prevent deprecation notices on Rails 3

when using +named_scope+ instead of +scope+.
# File lib/flag_shih_tzu.rb, line 456
def named_scope_method
  # Can't use respond_to because both AR 2 and 3
  #   respond to both +scope+ and +named_scope+.
  ActiveRecord::VERSION::MAJOR == 2 ? :named_scope : :scope
end
parse_flag_options(*args) click to toggle source
# File lib/flag_shih_tzu.rb, line 340
def parse_flag_options(*args)
  options = args.shift
  add_options = if args.size >= 1
                  args.shift
                else
                  options.
                  keys.
                  select { |key| !key.is_a?(Integer) }.
                  inject({}) do |hash, key|
                    hash[key] = options.delete(key)
                    hash
                  end
                end
  [options, add_options]
end
sql_condition_for_flag(flag, colmn, enabled = true, custom_table_name = table_name) click to toggle source
# File lib/flag_shih_tzu.rb, line 410
def sql_condition_for_flag(flag, colmn, enabled = true, custom_table_name = table_name)
  check_flag(flag, colmn)

  if flag_options[colmn][:flag_query_mode] == :bit_operator
    # use & bit operator directly in the SQL query.
    # This has the drawback of not using an index on the flags colum.
    %[(#{flag_full_column_name(custom_table_name, colmn)} & #{flag_mapping[colmn][flag]} = #{enabled ? flag_mapping[colmn][flag] : 0})]
  elsif flag_options[colmn][:flag_query_mode] == :in_list
    # use IN() operator in the SQL query.
    # This has the drawback of becoming a big query
    #   when you have lots of flags.
    neg = enabled ? "" : "not "
    %[(#{flag_full_column_name(custom_table_name, colmn)} #{neg}in (#{sql_in_for_flag(flag, colmn).join(",")}))]
  else
    raise NoSuchFlagQueryModeException
  end
end
sql_in_for_flag(flag, colmn) click to toggle source

returns an array of integers suitable for a SQL IN statement.

# File lib/flag_shih_tzu.rb, line 429
def sql_in_for_flag(flag, colmn)
  val = flag_mapping[colmn][flag]
  flag_value_range_for_column(colmn).select { |bits| bits & val == val }
end
sql_set_for_flag(flag, colmn, enabled = true, custom_table_name = table_name) click to toggle source
# File lib/flag_shih_tzu.rb, line 434
def sql_set_for_flag(flag, colmn, enabled = true, custom_table_name = table_name)
  check_flag(flag, colmn)
  lhs_name = flag_full_column_name_for_assignment(custom_table_name, colmn)
  rhs_name = flag_full_column_name(custom_table_name, colmn)
  "#{lhs_name} = #{rhs_name} #{enabled ? "| " : "& ~" }#{flag_mapping[colmn][flag]}"
end
valid_flag_column_name?(colmn) click to toggle source
# File lib/flag_shih_tzu.rb, line 449
def valid_flag_column_name?(colmn)
  colmn.is_a?(String)
end
valid_flag_key?(flag_key) click to toggle source
# File lib/flag_shih_tzu.rb, line 441
def valid_flag_key?(flag_key)
  flag_key > 0 && flag_key == flag_key.to_i
end
valid_flag_name?(flag_name) click to toggle source
# File lib/flag_shih_tzu.rb, line 445
def valid_flag_name?(flag_name)
  flag_name.is_a?(Symbol)
end