Ruby on Rails

Depends how you interpret things

Category Archives: Rails

Spree Extend: Updating eligibility criteria for any existing promotion rule

As I mentioned in my last post spree-extend, how to create custom promotion rule(s) while working with Spree based e-Commerece website. Last weekend, I faced a situation where I need to update the existing promotion rule to handle some specific condition.

Scenario:
For example, when I select “Product(s)” spree default rule, it gives us the option to choose products manually or choose by product group and its working as expected. Later, my client told me he wants, when he selects by product_group he can actually select some Taxons/Tags so that only the products belongs to that product_group and has taxon(s) listed there are eligible for this promotion.

Solution:
For updating any existing promotion rules in spree there are few generic steps we need to follow,
Step1. Extend the corresponding spree->promotion->rules model class. Here, I created a file app/models/promotion/rules/product_decorator.rb.

Promotion::Rules::Product.class_eval do
end

Step2. Create the attribute accessors/ accessibles for new attributes you needed.

Promotion::Rules::Product.class_eval do
  has_and_belongs_to_many :taxons, :class_name =>"::taxon", :join_table => "taxons_promotion_rules", :foreign_key=>"promotion_rule_id"
  attr_accessible :taxon_ids_string
  //setter and getter methods
  def taxon_ids_string
    self.taxon_ids.join(",")
  end
  def taxon_ids_string=(string)
    self.taxon_ids = string.split(",").map(&:strip)
  end
end

Step3. update the corresponding View file for the same rule in admin/promotions/rules/_product.html.haml or .erb if you are using erb. Add the required code for handling the same. In My case I added one TokenInput object to get multiple taxons selection. Add code something like this… where-ever you think is more suitable for you.

%p{:class=>"field products_rule_taxons"}
  %label
    = "Choose Taxons"
    = taxon_picker_field "#{param_prefix}[tag_ids_string]", promotion_rule.taxon_ids_string

There are 2 different ways of adding this to view file. One, you can write a decorator and tell before/after which object you want to put this code. Second, you can re-write the complete file and do whatever you need. Upto you.

Also, taxon_picker_field is a helper method which I created for creating the tokenInput/tokenizer object. Create the AdminBaseHelper decorator file, if not exists and put the following code or something like that,

Admin::BaseHelper.module_eval do
  def taxon_picker_field(name, value)
    taxons = value == "" ?  [] : Taxon.where("id in ('#{value}')")
    taxon_names_hash = taxons.inject({}){|memo,item| memo[item.id] = item.name; memo}
    %(tag_picker_initializer();).html_safe
  end
end

Step4. Update the eligible? and/or eligible_products methods as per your rule updations. Here in my case I just need to change the eligible_products method. Add the following code in app/models/promotion/rules/product_decorator.rb

  def eligible_products
    if product_group
      return self.taxons.collect(&:products).flatten
    end
    products
  end

Or something like that as per your requirements

And yes, We are good to go and the new rule criteria will be applied from now on-wards.

Good luck, In case of any query, feel free to ask here or email me at skr.ymca@gmail.com

Spree Extend: Creating Custom Promotion Rule

I’m working on an E-commerce website for sometime now. We are using spree as backbone of the project and customizing where and when its required. Spree is quite good and fully functioning E-commerce Rails engine which give you base to build a e-commerce website quickly and with lesser efforts.
One of the main features of any e-commerce website is Promotions/Coupons. Spree also has a built in spree promotion module which is ready to use with predefined rules and adjustment calculators. Being an ADMIN I can define any new promotion rules i.e. first order, user specific, products specific etc. Also I can define the actions like ALL or ANY. You can have a look on spree_promo module at spree_promo and see how it works.
Spree already provides some 4-5 predefined Promotions Rules on the basis of which every order get cross-checked whether some adjustments has to be done or not. Most of the time these rules/calculators are more than enough in general usage, but sometimes we need few very specific CUSTOM rules to be defined. Here I’m going to explain how easy it is to create new spree promotion rules.
Create Promotion Rule:
step1. create a model file app/models/spree/promotion/rules/my_custom_promotion.rb

  module Spree
    class Promotion::Rules::MyCustomPromotion < PromotionRule
      // required associations or preferences if any
      // required attribute accessible if you need to protect some attributes from 
mass assignment
      // match polices and operators
      MATCH_POLICIES = %w(any all) // as per your requirement

      def eligible?(order, options={})
        // This method is the place where every order is being verified whether its 
eligible for any promotional discount
      end
    end
  end

Step2. Add this custom promotion rule to default spree promotion rules list. Write the following code in either application.rb or some initializer

  initializer "spree.promo.register.promotions.rules" do |app|
    app.config.spree.promotions.rules += [Spree::Promotion::Rules::MyCustomPromotion]
  end

Step3. Add the translations for name & description for custom rule as spree use the standard view for all with Translation method t(‘name’) or t(‘description’). write the following code in your config/locales/en.yml(or any other locale file if you are having multilingual products).

 
  en:
    promotion_rule_types:
      product_by_tag:
        name: Products By MyCustomPromotion
        description: Add products from MyCustomPromotion

Step4. create the view partial file for the custom promotion rule. add a file to app/admin/promotions/rules/_my_custom_promotion.html.erb or haml or something else depending on the template engine you are using for views.

And, restart your application server and you are good to go. Now whenever you are going to edit the Promotion, in the promotions rules list you will see you custom rule also available.

NOTE:: if you want some custom price adjustments/calculations as well, then you have to create a new calculator. E.g. If I want to define some flat percentage benefit on my custom promotion we have to create app/models/spree/calculator/flat_percentage.rb

  module Spree
    class Calculator::FlatPercetange < Calculator
      // preferences if there are any.
      // attributes accessibles
      def compute(object)
        // main method where we have to compute the adjustments we have to made.
      end
    end
  end

I hope it helped someone. In case of any query, please feel free to ask here or drop me a mail at skr.ymca@gmail.com

Assets & Database hosting on Amazon/S3

Assets Hosting
Lately I was working on a project, highly data driven. Its a movie social network, means there are lots of assets, Photos, Audios & videos. Thats why we should keep continuous backup for our data on some 3rd party provider or somewhere else, which is easier to manage. We are using Amazon::S3 module for our assets and database backup.

Database Backup:

For database backup on S3 we used a ruby gem named “mysql_s3_backup”, a simple backup script for mysql and s3 with incremental backups. Before starting the backups, Create a YAML config file:
mysql:
# Database name to backup
database: xyz_development
# Mysql user and password to execute commands
user: dbuser
password: paswword
# Path to mysql binaries, like mysql, mysqldump (optional)
bin_path: /usr/bin/
# Path to the binary logs, should match the bin_log option in your my.cnf
bin_log: /var/lib/mysql/binlog/mysql-bin
s3:
# S3 bucket name to backup to
bucket: bucketname
# S3 credentials
access_key_id: XXXXXXXXXXXXXXX
secret_access_key: XXXXXXXXXXXXXXXXXXXXXX

Create a full backup:
mysql_s3_backup -c=your_config.yml full
Create an incremental backup:
mysql_s3_backup -c=your_config.yml inc
Restore the latest backup (applying incremental backups):
mysql_s3_backup -c=your_config.yml restore
Restore a specific backup (NOT applying incremental backups):
mysql_s3_backup -c=your_config.yml restore 20091126112233

We were planning to just keep backup for last 5 copies of the database. So, I did a tweak in the code, and written one wrapper class over the GEM for storing only last 5 backups [in case of full backups only].

require 'mysql_s3_backup'

class MysqlS3Dumper
  attr_accessor :config
  class MysqlS3Backup::Backup
    def full(name=make_new_name)
      lock do
        # When the full backup runs it delete any binary log files that might already exist
        # in the bucket. Otherwise the restore will try to restore them even though they’re
        # older than the full backup.
        @bucket.delete_all @bin_log_prefix
        with_temp_file do |file|
          puts file
          @mysql.dump(file)
          @bucket.store(dump_file_name(name), file)
          @bucket.copy(dump_file_name(name), dump_file_name("latest"))
          @bucket.keep_last_five
        end
      end
    end
  end

  class MysqlS3Backup::Bucket
    def keep_last_five
      puts @name
      puts "coming to bucket"
      all_backups = AWS::S3::Bucket.objects(@name)
      while all_backups.count > 6
        AWS::S3::Bucket.objects(@name).first.delete
        all_backups = AWS::S3::Bucket.objects(@name)
      end
    end
  end

  def initialize
    @config = MysqlS3Backup::Config.from_yaml_file(File.dirname(__FILE__) + "/../config/s3_mysql.yml")
  end

end

Assets Backup:

For assets backup we use S3Backup ruby gem. S3Backup, is a backup tool to local directory to Amazon S3. It uploads local directory to Amazon S3 with compression. If directories isn’t modified after prior backup,those aren’t upload. It can be Cryptnize upload files if password and salt are configured. To use remotebackup,you should prepare backup configuration file by yaml such below:

bucket: “bucket name”
directories:
– “absolute path to directory for backup/restore”
– “iterate directory as you like”
access_key_id: ‘Amazon access_key_id’
secret_access_key: ‘Amazon secret_access_key’
password: ‘password for aes. (optional)’
salt: ‘HexString(16 length) (must when password is specified) ‘
buffer_size: ‘number of byte max 50000000000 (optional default 32000000)’
max_retry_count: ‘number of retry of post if post failed.(optional default 10)’
proxy_host: proxy host address if you use proxy.
proxy_port: proxy port if you use proxy.
proxy_user: login name for proxy server if you use proxy.
proxy_password: login password for proxy server if you use proxy.
log_level: ‘output log level. value is debug or info or warn or error(optional default info)’
temporary: ‘temporary directory path. default(/tmp)
*If directories isn’t specified when restore, it restores all directories in bucket.*

== COMMAND:
=== backup
s3backup [-f configuration file] [-v verbose message] [-l path for log] [-h help]
configuration file path to file written above contents. default is ./backup.yml
verbose display directory tree and difference of anterior backup
path for log defaut starndard output.
help help message

=== restore
s3backup -r [-f configuration file] [-v verbose message] [-l path for log] [-o output dir] [-h help]
configuration file path to file written above contents. default is ./backup.yml
verbose display directory tree and difference of anterior backup
path for log defaut starndard output.
output dir path to directory for restore directory. defaut is ./
help help message

ImdbCelebrity – A new rubygem for parsing celebritydata from www.imdb.com

Last week I was working on my scrapper for getting Imdb Celebrity data for some specific purpose. Then, I searched for some available plugin or gem for the same. More surprisingly I was not able to find something what I’m looking for. Then, one idea stuck in my mind, why not create one of my own. Then I started working on this small rubygem named “imdb_celebrity”. You can see the public repository of the gem on github imdb celebrity. Also gem is hosted on GemCutter as well.

Imdb_celebrity is a ruby-gem which is used for scrapping celebrity pages from www.imdb.com . You can install imdb_celebrity as

  gem install imdb_celebrity

With current initial release we can search a celebrity with name or we can fetch content for a celebrity with given IMDB id or/and name, by specifying which parser we want to use. Right now we support Hpricot and Nokogiri as parser classes.

Usages:

require ‘imdb_celebrity’

** searching a celebrity

imdb_celebs = ImdbCelebrity::Search.new(“Brad Pitt”) imdb_celebs.celebrities

# this will return array of celebrity objects.

Also you can define the ParserClass which u wanna use [ right now you have choice of using either Nokogiri or Hpricot], as

imdb_celebs = ImdbCelebrity::Search.new(“Brad Pitt”, “NokogiriParser”)

#by default it will be HpricotParser imdb_celebs.celebrities

** Fetching data for celebrity celeb = ImdbCelebrity::Celebrity.new(“0000093”, “brad pitt”) # give 7 digit imdbid number

celeb.parser

=> it will give you the type of parser class it using

celeb.public_methods(false)

=> will give you the public methods of ImdbCelebrity::Celebrity class only

celeb.to_s => will return an array of celebrity data items containing name, real_name, biography, nationality, height, url.

Requirements:

* Hpricot gem should be installed.

* Nokogiri gem should be installed.

Enhancing script/console

I Used Rails ./script/console a lot for debugging my rails apps. Every time when I started debugging something, I have to keep my console log[*.log] file to see what corresponding queries being generated to carried out the result I desired.

I found a better solution for that. I enhanced my script/console, which enabled on-screen query logging for my rails app. I made a small change to #{RAILS_ROOT}/script/console.rb file, which it will look to load any additional ruby file resides in #{RAILS_ROOT}/console_script/ if available.

I have also included the script I wanted to load in the first place, I find it quite handy to be able to see what kind of queries my commands are generating.

#updates /script/console.rb

LOAD_HOOK_DIRECTORY = “#{RAILS_ROOT}/console_scripts”

Find.find( LOAD_HOOK_DIRECTORY ) do |filename|
if filename =~ /\.rb$/
puts “Adding #{filename} to load-path”
libs << ” -r #{filename}”
end
end
#add sql_log.rb file to /console_script/
def log_to(stream=STDOUT, colorize=true)
ActiveRecord::Base.logger = Logger.new(stream)
ActiveRecord::Base.clear_active_connections!
ActiveRecord::Base.colorize_logging = colorize
end
log_to

Rails ActiveRecord – has_and_belongs_to_many & Duplicate entry Error

Scenario :

In One of my project, we had  Many to Many association between models. this allowed me to look more deeply into the has_and_belongs_to_many(HABTM)  association.

When we say:

class Movies < ActiveRecord::Base
has_and_belongs_to_many :gallery_photos, :join_table=> "movies_gallery_photos"
end

class GalleryPhoto < ActiveRecord::Base
has_and_belongs_to_many :movies, :join_table=>"movies_gallery_photos"
end

Rails will assume there is a table “movies_gallery_photos” that include foreign keys to the two entities. So this table should be created by the following migration

create_table :movies_gallery_photos, :id => false do |t|
t.integer :movie_id, :null => false
t.integer :gallery_photo_id, :null => false
end

:id=>false will prevent the creation of the default primary key for that table.

as the API documentation say; other attributes in that relation will be loaded with the objects and will be read only, including the :id. So failing to disable the id generation for that table will cause the loaded objects to have and “id” attribute holding the value of the id or the movies_gallery_photos entries instead of the ids of the target entity (movies or gallery_photos here).

Issue :

Having (:id) false helps to over Mysql::Error: Duplicate entry ‘#’ for key #” entry error we faces while adding entries. (movie.gallery_photos << photo). But it leads to creation and availability of duplicate values in the movies_gallery_photos table. i.e.

movies.gallery_photos << photos

will not check for uniqueness of the photos objects.