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