Skip to content

“Soft” enum columns for Rails

Here’s a quick and easy way to use enum-style columns implemented directly on an active record model.

=begin rdoc
  Usage: 
  class Box < ActiveRecord::Base
    include Enumify
    enumify :state, :pending, :in_progress, :complete
  end

  box = Box.new
  box.state = :wat # raises ValueNotFound error
  box.state = :in_progress # sets column value to 2
  box.state # returns :in_progress
=end
module Enumify
  class ValueNotFound < Exception; end
  
  def self.included base
    base.send :include, InstanceMethods
    base.send :extend,  ClassMethods
  end
  
  module InstanceMethods
    def read_attribute attr_name
      if enumified? attr_name
        self.class.enumified_attrs[attr_name][super(attr_name)]
      else
        super(attr_name)
      end
    end
    
    def write_attribute attr_name, value
      if enumified? attr_name
        vals = self.class.enumified_attrs[attr_name]
        if vals.index(value).nil?
          raise ValueNotFound, "#{value} is not a valid #{attr_name} value"
        else
          attribute_will_change! attr_name.to_s
          super attr_name, vals.index(value)
        end
      else
        super attr_name, value
      end
    end
    
    def enumified? attr_name
      self.class.enumified?(attr_name)
    end
  end
  
  module ClassMethods
    attr_accessor :enumified_attrs
    
    def enumify attr_name, *values
      @enumified_attrs = {} if @enumified_attrs.nil?
      @enumified_attrs[attr_name] = values
      
      define_method(:"#{attr_name}=") do |value|
        write_attribute(attr_name, value)
      end
      
      define_method(attr_name) do |*args|
        read_attribute(attr_name)
      end
    end
    
    def enumified? attr_name
      @enumified_attrs && @enumified_attrs[attr_name].present?
    end
  end
end

Categories: Code.

Tags: ,

Prestashop Constants

Here is a list of all constants set by a Prestashop installation, and how to get them on any system. Obviously the following PHP script is generic and will work in any PHP framework or app context.


  string(33) "/abs/path/to/ps/backoffice"
  ["PS_ADMIN_DIR"]=>
  string(33) "/abs/path/to/ps/backoffice"
  ["_PS_MODE_DEV_"]=>
  bool(false)
  ["_PS_DEBUG_SQL_"]=>
  bool(false)
  ["_PS_DISPLAY_COMPATIBILITY_WARNING_"]=>
  bool(false)
  ["_PS_DEBUG_PROFILING_"]=>
  bool(false)
  ["_PS_MODE_DEMO_"]=>
  bool(false)
  ["_PS_ROOT_DIR_"]=>
  string(22) "/abs/path/to/ps"
  ["_PS_CLASS_DIR_"]=>
  string(31) "/abs/path/to/ps/classes/"
  ["_PS_CONTROLLER_DIR_"]=>
  string(35) "/abs/path/to/ps/controllers/"
  ["_PS_FRONT_CONTROLLER_DIR_"]=>
  string(41) "/abs/path/to/ps/controllers/front/"
  ["_PS_ADMIN_CONTROLLER_DIR_"]=>
  string(41) "/abs/path/to/ps/controllers/admin/"
  ["_PS_OVERRIDE_DIR_"]=>
  string(32) "/abs/path/to/ps/override/"
  ["_PS_TRANSLATIONS_DIR_"]=>
  string(36) "/abs/path/to/ps/translations/"
  ["_PS_DOWNLOAD_DIR_"]=>
  string(32) "/abs/path/to/ps/download/"
  ["_PS_MAIL_DIR_"]=>
  string(29) "/abs/path/to/ps/mails/"
  ["_PS_PDF_DIR_"]=>
  string(27) "/abs/path/to/ps/pdf/"
  ["_PS_ALL_THEMES_DIR_"]=>
  string(30) "/abs/path/to/ps/themes/"
  ["_PS_IMG_DIR_"]=>
  string(27) "/abs/path/to/ps/img/"
  ["_PS_MODULE_DIR_"]=>
  string(31) "/abs/path/to/ps/modules/"
  ["_PS_CAT_IMG_DIR_"]=>
  string(29) "/abs/path/to/ps/img/c/"
  ["_PS_STORE_IMG_DIR_"]=>
  string(30) "/abs/path/to/ps/img/st/"
  ["_PS_PROD_IMG_DIR_"]=>
  string(29) "/abs/path/to/ps/img/p/"
  ["_PS_SCENE_IMG_DIR_"]=>
  string(34) "/abs/path/to/ps/img/scenes/"
  ["_PS_SCENE_THUMB_IMG_DIR_"]=>
  string(41) "/abs/path/to/ps/img/scenes/thumbs/"
  ["_PS_MANU_IMG_DIR_"]=>
  string(29) "/abs/path/to/ps/img/m/"
  ["_PS_SHIP_IMG_DIR_"]=>
  string(29) "/abs/path/to/ps/img/s/"
  ["_PS_SUPP_IMG_DIR_"]=>
  string(30) "/abs/path/to/ps/img/su/"
  ["_PS_COL_IMG_DIR_"]=>
  string(30) "/abs/path/to/ps/img/co/"
  ["_PS_TMP_IMG_DIR_"]=>
  string(31) "/abs/path/to/ps/img/tmp/"
  ["_PS_UPLOAD_DIR_"]=>
  string(30) "/abs/path/to/ps/upload/"
  ["_PS_TOOL_DIR_"]=>
  string(29) "/abs/path/to/ps/tools/"
  ["_PS_GEOIP_DIR_"]=>
  string(35) "/abs/path/to/ps/tools/geoip/"
  ["_PS_SWIFT_DIR_"]=>
  string(35) "/abs/path/to/ps/tools/swift/"
  ["_PS_GENDERS_DIR_"]=>
  string(35) "/abs/path/to/ps/img/genders/"
  ["_PS_FPDF_PATH_"]=>
  string(34) "/abs/path/to/ps/tools/fpdf/"
  ["_PS_TCPDF_PATH_"]=>
  string(35) "/abs/path/to/ps/tools/tcpdf/"
  ["_PS_TAASC_PATH_"]=>
  string(35) "/abs/path/to/ps/tools/taasc/"
  ["_PS_PEAR_XML_PARSER_PATH_"]=>
  string(45) "/abs/path/to/ps/tools/pear_xml_parser/"
  ["_PS_CACHE_DIR_"]=>
  string(29) "/abs/path/to/ps/cache/"
  ["_PS_BO_ALL_THEMES_DIR_"]=>
  string(41) "/abs/path/to/ps/backoffice/themes/"
  ["_PS_TRANS_PATTERN_"]=>
  string(9) "(.*[^\\])"
  ["_PS_MIN_TIME_GENERATE_PASSWD_"]=>
  string(3) "360"
  ["_PS_MAGIC_QUOTES_GPC_"]=>
  int(0)
  ["_CAN_LOAD_FILES_"]=>
  int(1)
  ["PS_PRODUCT_TAX"]=>
  int(0)
  ["PS_STATE_TAX"]=>
  int(1)
  ["PS_BOTH_TAX"]=>
  int(2)
  ["_PS_PRICE_DISPLAY_PRECISION_"]=>
  int(2)
  ["PS_TAX_EXC"]=>
  int(1)
  ["PS_TAX_INC"]=>
  int(0)
  ["PS_ORDER_PROCESS_STANDARD"]=>
  int(0)
  ["PS_ORDER_PROCESS_OPC"]=>
  int(1)
  ["PS_ROUND_UP"]=>
  int(0)
  ["PS_ROUND_DOWN"]=>
  int(1)
  ["PS_ROUND_HALF"]=>
  int(2)
  ["PS_REGISTRATION_PROCESS_STANDARD"]=>
  int(0)
  ["PS_REGISTRATION_PROCESS_AIO"]=>
  int(1)
  ["PS_CARRIERS_ONLY"]=>
  int(1)
  ["CARRIERS_MODULE"]=>
  int(2)
  ["CARRIERS_MODULE_NEED_RANGE"]=>
  int(3)
  ["PS_CARRIERS_AND_CARRIER_MODULES_NEED_RANGE"]=>
  int(4)
  ["ALL_CARRIERS"]=>
  int(5)
  ["_PS_USE_SQL_SLAVE_"]=>
  int(0)
  ["_PS_ADMIN_PROFILE_"]=>
  int(1)
  ["_STOCK_MOVEMENT_ORDER_REASON_"]=>
  int(3)
  ["_STOCK_MOVEMENT_MISSING_REASON_"]=>
  int(4)
  ["_PS_DEFAULT_CUSTOMER_GROUP_"]=>
  int(1)
  ["_PS_CACHEFS_DIRECTORY_"]=>
  string(37) "/abs/path/to/ps/cache/cachefs/"
  ["_PS_GEOLOCATION_NO_CATALOG_"]=>
  int(0)
  ["_PS_GEOLOCATION_NO_ORDER_"]=>
  int(1)
  ["MIN_PASSWD_LENGTH"]=>
  int(8)
  ["_PS_SMARTY_NO_COMPILE_"]=>
  int(0)
  ["_PS_SMARTY_CHECK_COMPILE_"]=>
  int(1)
  ["_PS_SMARTY_FORCE_COMPILE_"]=>
  int(2)
  ["_PS_SMARTY_CONSOLE_CLOSE_"]=>
  int(0)
  ["_PS_SMARTY_CONSOLE_OPEN_BY_URL_"]=>
  int(1)
  ["_PS_SMARTY_CONSOLE_OPEN_"]=>
  int(2)
  ["_PS_JQUERY_VERSION_"]=>
  string(5) "1.7.2"
  ["_PS_SSL_PORT_"]=>
  int(443)
  ["_DB_SERVER_"]=>
  string(9) "localhost"
  ["_DB_NAME_"]=>
  string(6) "frs_ps"
  ["_DB_USER_"]=>
  string(3) "frs"
  ["_DB_PASSWD_"]=>
  string(16) "PvABZDx8yuMyWEm6"
  ["_DB_PREFIX_"]=>
  string(3) "ps_"
  ["_MYSQL_ENGINE_"]=>
  string(6) "InnoDB"
  ["_PS_CACHING_SYSTEM_"]=>
  string(13) "CacheMemcache"
  ["_PS_CACHE_ENABLED_"]=>
  string(1) "0"
  ["_MEDIA_SERVER_1_"]=>
  string(0) ""
  ["_MEDIA_SERVER_2_"]=>
  string(0) ""
  ["_MEDIA_SERVER_3_"]=>
  string(0) ""
  ["_COOKIE_KEY_"]=>
  string(56) "[...]"
  ["_COOKIE_IV_"]=>
  string(8) "TmdCVbj2"
  ["_PS_CREATION_DATE_"]=>
  string(10) "2014-03-10"
  ["_PS_VERSION_"]=>
  string(7) "1.5.6.2"
  ["_RIJNDAEL_KEY_"]=>
  string(32) "[...]"
  ["_RIJNDAEL_IV_"]=>
  string(24) "[...]=="
  ["_PS_DIRECTORY_"]=>
  string(10) "/"
  ["_THEME_NAME_"]=>
  string(15) "Default"
  ["__PS_BASE_URI__"]=>
  string(10) "/"
  ["_PS_THEME_DIR_"]=>
  string(46) "/abs/path/to/ps/themes/default/"
  ["_THEMES_DIR_"]=>
  string(17) "/themes/"
  ["_THEME_DIR_"]=>
  string(33) "/themes/default/"
  ["_THEME_IMG_DIR_"]=>
  string(37) "/themes/default/img/"
  ["_THEME_CSS_DIR_"]=>
  string(37) "/themes/default/css/"
  ["_THEME_JS_DIR_"]=>
  string(36) "/themes/default/js/"
  ["_PS_THEME_OVERRIDE_DIR_"]=>
  string(55) "/abs/path/to/ps/themes/default/override/"
  ["_PS_THEME_MOBILE_DIR_"]=>
  string(45) "/abs/path/to/ps/themes/default/mobile/"
  ["_THEME_MOBILE_DIR_"]=>
  string(32) "/themes/default/mobile/"
  ["_PS_THEME_MOBILE_OVERRIDE_DIR_"]=>
  string(54) "/abs/path/to/ps/themes/default/mobile/override/"
  ["_THEME_MOBILE_IMG_DIR_"]=>
  string(36) "/themes/default/mobile/img/"
  ["_THEME_MOBILE_CSS_DIR_"]=>
  string(36) "/themes/default/mobile/css/"
  ["_THEME_MOBILE_JS_DIR_"]=>
  string(35) "/themes/default/mobile/js/"
  ["_PS_THEME_TOUCHPAD_DIR_"]=>
  string(55) "/abs/path/to/ps/themes/default/touchpad/"
  ["_THEME_TOUCHPAD_DIR_"]=>
  string(42) "/themes/default/touchpad/"
  ["_THEME_TOUCHPAD_CSS_DIR_"]=>
  string(36) "/themes/default/mobile/css/"
  ["_THEME_TOUCHPAD_JS_DIR_"]=>
  string(35) "/themes/default/mobile/js/"
  ["_PS_IMG_"]=>
  string(14) "/img/"
  ["_PS_ADMIN_IMG_"]=>
  string(20) "/img/admin/"
  ["_PS_TMP_IMG_"]=>
  string(18) "/img/tmp/"
  ["_THEME_CAT_DIR_"]=>
  string(16) "/img/c/"
  ["_THEME_PROD_DIR_"]=>
  string(16) "/img/p/"
  ["_THEME_MANU_DIR_"]=>
  string(16) "/img/m/"
  ["_THEME_SCENE_DIR_"]=>
  string(21) "/img/scenes/"
  ["_THEME_SCENE_THUMB_DIR_"]=>
  string(27) "/img/scenes/thumbs"
  ["_THEME_SUP_DIR_"]=>
  string(17) "/img/su/"
  ["_THEME_SHIP_DIR_"]=>
  string(16) "/img/s/"
  ["_THEME_STORE_DIR_"]=>
  string(17) "/img/st/"
  ["_THEME_LANG_DIR_"]=>
  string(16) "/img/l/"
  ["_THEME_COL_DIR_"]=>
  string(17) "/img/co/"
  ["_THEME_GENDERS_DIR_"]=>
  string(22) "/img/genders/"
  ["_SUPP_DIR_"]=>
  string(17) "/img/su/"
  ["_PS_PROD_IMG_"]=>
  string(16) "/img/p/"
  ["_PS_JS_DIR_"]=>
  string(13) "/js/"
  ["_PS_CSS_DIR_"]=>
  string(14) "/css/"
  ["_THEME_PROD_PIC_DIR_"]=>
  string(17) "/upload/"
  ["_MAIL_DIR_"]=>
  string(16) "/mails/"
  ["_MODULE_DIR_"]=>
  string(18) "/modules/"
  ["_PS_BASE_URL_"]=>
  string(34) "http://www.example.com"
  ["_PS_BASE_URL_SSL_"]=>
  string(35) "https://www.example.com"
  ["_PS_OS_CHEQUE_"]=>
  string(1) "1"
  ["_PS_OS_PAYMENT_"]=>
  string(1) "2"
  ["_PS_OS_PREPARATION_"]=>
  string(1) "3"
  ["_PS_OS_SHIPPING_"]=>
  string(1) "4"
  ["_PS_OS_DELIVERED_"]=>
  string(1) "5"
  ["_PS_OS_CANCELED_"]=>
  string(1) "6"
  ["_PS_OS_REFUND_"]=>
  string(1) "7"
  ["_PS_OS_ERROR_"]=>
  string(1) "8"
  ["_PS_OS_OUTOFSTOCK_"]=>
  string(1) "9"
  ["_PS_OS_BANKWIRE_"]=>
  string(2) "10"
  ["_PS_OS_PAYPAL_"]=>
  string(2) "11"
  ["_PS_OS_WS_PAYMENT_"]=>
  string(2) "12"
  ["_PS_SMARTY_DIR_"]=>
  string(36) "/abs/path/to/ps/tools/smarty/"
  ["DS"]=>
  string(1) "/"
  ["SMARTY_DIR"]=>
  string(36) "/abs/path/to/ps/tools/smarty/"
  ["SMARTY_SYSPLUGINS_DIR"]=>
  string(47) "/abs/path/to/ps/tools/smarty/sysplugins/"
  ["SMARTY_PLUGINS_DIR"]=>
  string(44) "/abs/path/to/ps/tools/smarty/plugins/"
  ["SMARTY_MBSTRING"]=>
  bool(true)
  ["SMARTY_RESOURCE_CHAR_SET"]=>
  string(5) "UTF-8"
  ["SMARTY_RESOURCE_DATE_FORMAT"]=>
  string(9) "%b %e, %Y"
  ["SMARTY_SPL_AUTOLOAD"]=>
  int(0)
}

Categories: Tricks.

Tags: ,

rbcalc rubygem

New gem released – Ruby Bindings for the Bcalc library. More info here.

Categories: Code.

Tags: , , ,

Guitar Tabs for Geeks

I just added an old test app I made a while back on Github. It’s called Tabs (unsurprisingly) and it lets you input small guitar tabs using your keyboard in a -hopefully- intuitive manner. It was built to test a few javascript libraries I had bumped into, and also to experiment with Capybara + Selenium testing. Hence, it is not really meant for public use but I ended up using it quite a bit to share licks and song ideas with bandmates, so here it is!

Categories: Code.

Tags: , ,

Managing Sphinx Real-time indexing in Ruby

The Sphinx Search Server has support for real-time indexing for quite a while now. Real-time indexing means that you do not have to trigger an index update whenever you update searchable data in your application, which makes Sphinx a viable option for apps with big searchable datasets.

The following class implements a basic RT Index interface. This particular example uses Sequel as ORM, but you can easily modify it to use ActiveRecord or whatever else you prefer.

You can hook this class up your models’ after filters, thus ensuring that our search index is always up to date. No cronjobs, no nothing.
Here is an example:

class Artist < ActiveRecord::Base
  after_create :add_to_index
  after_update :update_index
  after_delete :remove_from_index
  
  private
  # make use you include the record pk and any searchable fields
  def add_to_index
    rt_index.insert(:id => id, :name => name)
  end
  
  def update_index
    rt_index.replace(:id => id, :name => name)
  end
  
  def remove_from_index
    rt_index.delete(id)
  end
  
  def rt_index
    RtIndex.new(:artists)
  end
end

And here is the complete class:

require 'sequel'
class RtIndex
  attr_reader :index_name
  
  def initialize index_name
    @index_name = index_name.to_sym
  end
  
  def insert hash
    DB["INSERT INTO #{index_name} (?) VALUES (?)",
      Sequel.lit(hash.keys.join(',')),
      Sequel.lit(hash.values.map {|v| DB.literal(v)}.join(','))
    ].insert
  end
  
  def replace hash
    DB["REPLACE INTO #{index_name} (?) VALUES (?)",
      Sequel.lit(hash.keys.join(',')),
      Sequel.lit(hash.values.map {|v| DB.literal(v)}.join(','))
    ].replace
  end
  
  def delete id
    DB["DELETE FROM #{index_name} WHERE id = ?",id].delete
  end
  
  class << self
    # you want to amend this to load sphinx configuration from wherever you keep it.
    def config
      { "sphinx" => { "host" => "localhost", "port" => 3309 } }
    end
  end
  
  # create a shared mysql connection to sphinx
  DB = Sequel.mysql(:host => config["sphinx"]["host"], :port => config["sphinx"]["rt_port"])
end

Creating reusable ORM-specific extensions is fairly trivial. A class method call such as has_rt_index :name, :description is easy to implement and left as an exercise.

Categories: Code.

Tags: ,

Redis Model

Here is a simple class I have been using more and more recently. It is a very simple Redis-backed Model implementation.
The class itself is minimal, only facilitating CRUD operations and automatic timestamp management.

The class assumes you have a Redis connection initialized in $redis.

require 'time'
require 'json'

# A basic Redis-backed Model implementation.
# Extend it by specifying your desired attr_accessors
# Enable timestamps by calling use_timestamps in your implementation
# Features auto-incremented ids and id namespacing
# Example:
#   class Zorro < RedisModel
#     attr_accessor :sword, :mask
#     use_timestamps
#   end
#   
#   z = Zorro.new(:sword => 'rapier', :mask => 'lame')
#   z.persist!
#   z
#   => #<Zorro @id="1", @sword="rapier", @mask="lame", @updated_at=2013-03-14 18:11:17 +0100, @created_at=2013-03-14 18:11:17 +0100>
#
class RedisModel
  # the only default attribute
  attr_accessor :id
  
  def initialize params = {}
    params.map do |k,v| 
      begin
        send(:"#{k}=",JSON.parse(v))
      rescue JSON::ParserError
        send(:"#{k}=",v)
      end
    end
    
    # typecast some standard attributes
    self.id = self.id.to_i if self.id
    if use_timestamps?
      self.created_at = Time.parse(self.created_at) if self.created_at.is_a?(String)
      self.updated_at = Time.parse(self.updated_at) if self.updated_at.is_a?(String)
    end
  end
  
  def persist!
    is_new = id.nil? or !self.class.exists?(id)
    
    if use_timestamps?
      self.updated_at = Time.now
      self.created_at = Time.now if is_new
    end
    
    self.id = generate_id if is_new
    
    $redis.hmset(namespaced_id, *self.to_array)
    return true
  end
  alias :save! :persist!
  
  def persist
    persist!
    self
  end
  alias :save :persist
  
  def destroy!
    $redis.del(namespaced_id)
  end
  
  def reload
    self.class.load(namespaced_id)
  end
  
  def to_hash
    instance_variables.inject({}) do |h,v|
      value = instance_variable_get(v)
      value = value.is_a?(String) ? value : value.to_json
      h.update v.to_s.gsub('@','') => value
    end
  end
  
  def use_timestamps?
    self.class.instance_variable_get(:@use_timestamps)
  end
  
  def to_array
    to_hash.to_a.flatten
  end
  
  def namespaced_id
    self.class.namespaced_id(self.id)
  end
  
  # useful for testing, but also a general convenience
  include Comparable
  def <=>(other)
    self.to_hash <=> other.to_hash
  end
  
  class << self
    @use_timestamps = false
    
    def namespaced_id(id)
      "#{self}::#{id}"
    end
    
    def all
      $redis.namespace(self)
    end
    
    def exists? id
      $redis.exists(namespaced_id(id))
    end
    
    def load id
      hash = $redis.hgetall(namespaced_id(id))
      hash == {} ? nil : new(hash)
    end
    alias :find :load
    
    def use_timestamps
      @use_timestamps = true
      self.send(:attr_accessor, :created_at)
      self.send(:attr_accessor, :updated_at)
    end
  end
  
protected
  def generate_id
    $redis.incr("_#{self.class}_current_id")
  end
end

Categories: Code.

Tags: ,