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: ,