Halaman

Rabu, 22 Desember 2010

[shared] change rails form error field wrapper

by default rails wrap all fields that contain error data using div with "fieldWithErrors" class

but sometimes we want to change that wrapper for example to use span instead of div.
that can easily be done by overriding field_error_proc class attribute in ActionView::Base

in environment.rb
ActionView::Base.field_error_proc = Proc.new{ |html_tag, instance| "#{html_tag}".html_safe }

file position (for rails 2.3.10):
[ruby_dir]\lib\ruby\gems\1.8\gems\actionpack-2.3.10\lib\action_view\helpers\active_record_helper.rb

[shared] rails render_invalid method

purpose


provide DRY solution for rendering (or redirecting) from controller on invalid request, such as failed validation on create/update

requirements


- jquery js framework
- notifications helper from my earlier post about custom ajax helper:
http://tech.maysora.com/2010/11/rails-custom-ajax-helper-for-jquery.html

codes


application_controller.rb
# render invalid request to show error message depend on request type
# example:
#   render_invalid "Update failed."
#     ajax: show error notification with message "Update failed."
#     html: simply render text "Update failed."
#   render_invalid "Save failed.", :render => {:action => "new"}
#     ajax: show error notification with message "Save failed."
#     html: render action new with "Save failed." flash message
#   render_invalid "Save failed.", :render => {:action => "new"}, :flash => false
#     same as above but without flash for html
#   render_invalid "You can't do that.", :redirect_to => {:action => "index"}, :flash => false, :status => :unauthorized
#     same as above but redirecting to index instead of rendering new for html
#     and using unauthorized(401) status instead of not_acceptable(406)
#   render_invalid "Update failed.", :object => @user, :fields_wrapping => true
#     ajax: show error notification with addition of object's error messages and wrap error fields with div class fieldWithErrors
#     html: same as ajax minus fields wrapping
def render_invalid message="Error.", options={:flash => true}
  options[:status] ||= :not_acceptable
  message += "
#{options[:object].errors.full_messages.join(' ')}
" if options[:object].present? respond_to do |format| format.js do render(:update, :status => options[:status]) do |page| page << notification_error(message) if options[:object].present? && options[:fields_wrapping] page.rjs_clean_error_fields request page.rjs_apply_error_fields options[:object], request end end end format.html do if options[:render].present? flash.now[:error] = message if options[:flash] render options[:render].try(:merge, {:status => options[:status]}) || {:text => message, :status => options[:status]} elsif options[:redirect_to].present? flash[:error] = message if options[:flash] redirect_to options[:redirect_to] else render :text => message, :status => options[:status] end end end end

application_helper.rb (only if you use ajax error fields wrapping)
def rjs_apply_error_fields object, request
  klass_name = object.class.name.underscore
  wrapper = "
" object.errors.each do |attr, msg| attr = attr.split(".") page << "var $fields = $(), $container = $('\##{klass_name}_#{object.id || "new"}');" page << "if(!$container.length) $container = $('form[action=\\'#{request.request_uri}\\']');" page << "if(!$container.length) $container = $(document);" if attr.length > 1 object.send(attr.first).each_with_index do |x,i| page << "$fields = $fields.add('[id^=#{klass_name}_#{attr.first}_attributes][id$=#{attr.last}]:eq(#{i}),label[for^=#{klass_name}_#{attr.first}_attributes][for$=#{attr.last}]:eq(#{i})', $container)" if x.errors.on(attr.last) end else page << "$fields = $('label[for=#{klass_name}_#{attr}],\##{klass_name}_#{attr}', $container);" end page << "if(!$fields.parent('.fieldWithErrors').length)" page << "$fields.wrap('#{wrapper}')" end end def rjs_clean_error_fields request page << "$('.fieldWithErrors.rjs_#{request.request_method}_#{request.request_uri.gsub(/_=\d+($|&)/, "").parameterize} *').unwrap();" end

usage

users_controller.rb (using non ajax form)
#...

def create
  @user = User.new(params[:user])
  unless @user.save
    render_invalid "Failed to register.", :object => @user, :render => {:action => "new"}
  else
    flash[:info] = "Registration success, please check your email to activate."
    redirect_to login_path
  end
end

#...
users_controller.rb (using ajax form)
#...

def update
  @user = current_user
  unless @user.update_attributes(params[:user])
    render_invalid "Profile update failed.", :object => @user, :fields_wrapping => true
  end
end

#...
and update.js.rjs (for success behavior)
page << notification_info "Profile updated."
page.rjs_clean_error_fields request # used for cleaning error fields wrapping
#... other necessary action ...

additional tips

depending on your site style, it might be a good idea to change the div wrapper in rjs_apply_error_fields helper to span. you can also change rails standard error field wrapper for non-ajax form, see this post

[shared] floating point error in js

console.log(0.1+0.2); //-> 0.30000000000000004
console.log(0.1*0.2); //-> 0.020000000000000004

solution:
1. use x.toFixed(n) if you only need 5 or less decimal digit
2. use function from http://www.codingforums.com/showpost.php?p=483962&postcount=9
3. best solution, use js BigDecimal library https://github.com/jhs/bigdecimal.js or http://stz-ida.de/index.php?option=com_content&view=article&id=18&Itemid=32

summary:
use number 1 if decimal precision isn't really important (most likely), or use number 3 with some performance cost

Minggu, 05 Desember 2010

foobar2k headphone surround setup

this guide will add 5.1 speaker simulation using headphone for foobar2k
the result isn't really perfect but it's noticeably increase surround effect and reduce sound inside head feels..

needed files:
- foobar2k http://www.foobar2000.org/download
- foo_input_dts http://www.foobar2000.org/components/view/foo_input_dts
- foo_channel_mixer http://skipyrich.com/wiki/Foobar2000:Channel_Mixer
- nvidia purevideo decoder http://www.nvidia.com/object/dvd_decoder_1.02-223-trial.html

step:
- install nvidia purevideo decoder, use the trial CC and activation code in the site
- install foobar
- copy foo_input_dts and foo_channel_mixer dll to [foobar installation folder]/components/
- run foobar, go to file, preferences, playback, DSP manager
- (optional) add resampler (PPHS) to active DSP, then configure it to 48000Hz Ultra mode
- add channel mixer
- configure channel mixer, general: output channels 6, check L C R RL RR, uncheck LFE, Stereo image width 1.00
- upmix: mode surround, mode surround, center 1.00, volume 2.00, all others 0.00
- add Dolby headphone, configure: dolbyhph.dll to C:\windows\system32\DolbyHph.dll
- (optional) add others DSP to the top of the list if you want, make sure channel mixer then dolby headphone is the last two on the list

other useful components:
- Skip silence
- foo_ui_columns (nice ui)
- foo_uie_lyrics (add lyric, need foo_ui_columns)
- foo_shutdown (auto shutdown/standby/hibernate)
- amip for foobar and amip configurator (add winamp like jump and now playing announce to messengers and irc)

original source: http://www.head-fi.org/forum/thread/447089/5-1-headphone-experience-foobar-configuration-for-all-stereo-music-files

Minggu, 21 November 2010

[shared] rails custom ajax helper for jquery

requirements:
- jrails plugin (https://github.com/aaronchi/jrails)
- working jquery.gritter or other js notification (http://boedesign.com/blog/2009/07/11/growl-for-jquery-gritter/)
- my jquery.extended_helper (http://code.google.com/p/jquery-extendedhelper)

installation:
- add these code in application_helper.rb
# Adding additional options for ajax function
# You can use thisElement to manipulate element that using the function (link/button/form)
def custom_remote_function_options options
  options[:before] ||= ""
  options[:loading] ||= ""
  options[:complete] ||= ""
  options[:success] ||= ""
  options[:before] += "; var thisElement=this"
  options[:failure] ||= "; eval(request.responseText)"

  unless options[:loading_text].blank?
    options[:loading] += "; thisElement.notif = #{notification_loading(options[:loading_text])}"
    options[:complete] += "; #{notification_remove "thisElement.notif"}"
  end
  options
end

# Override rails remote_function using additional options
def custom_remote_function options
  remote_function(custom_remote_function_options(options)).html_safe
end

# Override rails link_to_remote function using additional options
# Disabling the link when ajax loading in progress to avoid double request
def custom_link_to_remote name, options={}, html_options=nil
  options = custom_remote_function_options options
  options[:loading] += "; $(thisElement).setLink(false)"
  options[:complete] += "; $(thisElement).setLink(true)"
  link_to_remote name, options, html_options
end

# Override rails button_to_remote function using additional options
# Disabling the button when ajax loading in progress to avoid double request
def custom_button_to_remote name, options={}, html_options=nil
  options = custom_remote_function_options options
  options[:loading] += "; $(thisElement).setInput(false)"
  options[:complete] += "; $(thisElement).setInput(true)"
  button_to_remote name, options, html_options
end

# Adding additional options for ajax form
# Add :unchange_default => true to prevent successful request changing default form fields value
# Add :disable_link => true to disable all link inside form when loading in progress
def custom_remote_form_options options
  options = custom_remote_function_options options
  options[:loading] += "; $(':input', thisElement).setInput(false);"
  options[:complete] += "; $(':input', thisElement).setInput(true);"
  unless options[:unchange_default]
    options[:success] +=  "; $(':input', thisElement).setDefault();"
  end
  if options[:disable_link]
    options[:loading] += "; $('a', thisElement).setLink(false);"
    options[:complete] += "; $('a', thisElement).setLink(true);"
  end
  options
end

# Override rails remote_form_for using additional options
def custom_remote_form_for record_or_name_or_array, *args, &proc
  args.push custom_remote_form_options(args.extract_options!)
  remote_form_for record_or_name_or_array, *args, &proc
end
alias_method :custom_form_remote_for, :custom_remote_form_for

# Override rails form_remote_tag using additional options
def custom_form_remote_tag options={}, &block
  form_remote_tag custom_remote_form_options(options), &block
end
alias_method :custom_remote_form_tag, :custom_form_remote_tag

def custom_button_to_remote name, value, options = {}
  button_to_remote name, value, custom_remote_form_options(options)
end

# Override rails submit_to_remote using additional options
def custom_submit_to_remote(name, value, options = {})
  submit_to_remote(name, value, custom_remote_form_options(options))
end

def notification_info text, no_escape=false
  text.gsub!(/\n/, '
')
  text = escape_javascript(text) unless no_escape
  "notificationInfo('#{text}')"
end

def notification_loading text, no_escape=false
  text.gsub!(/\n/, '
')
  text = escape_javascript(text) unless no_escape
  "notificationLoading('#{text}')"
end

def notification_error text, no_escape=false
  text.gsub!(/\n/, '
')
  text = escape_javascript(text) unless no_escape
  "notificationError('#{text}')"
end

def notification_remove notification
  "notificationRemove(#{notification})"
end

- add notification code in application.js (sample using jquery.gritter)
function notificationInfo(text){
  return $.gritter.add({text: text.replace(/\n/g, '
')});
}

function notificationError(text){
  return $.gritter.add({text: '' + text.replace(/\n/g, '
') + '',time: 6000});
}

function notificationLoading(text){
  return $.gritter.add({text: text.replace(/\n/g, '
'), image: '/images/gritter-loading.gif', sticky:true})
}

function notificationRemove(notification){
  $.gritter.remove(notification);
}

rails http status code and symbol

Status CodeStatus MessageSymbol
1xx Informational
100Continue:continue
101Switching Protocols:switching_protocols
102Processing:processing
 
2xx Success
200OK:ok
201Created:created
202Accepted:accepted
203Non-Authoritative Information:non_authoritative_information
204No Content:no_content
205Reset Content:reset_content
206Partial Content:partial_content
207Multi-Status:multi_status
226IM Used:im_used
 
3xx Redirection
300Multiple Choices:multiple_choices
301Moved Permanently:moved_permanently
302Found:found
303See Other:see_other
304Not Modified:not_modified
305Use Proxy:use_proxy
307Temporary Redirect:temporary_redirect
 
4xx Client Error
400Bad Request:bad_request
401Unauthorized:unauthorized
402Payment Required:payment_required
403Forbidden:forbidden
404Not Found:not_found
405Method Not Allowed:method_not_allowed
406Not Acceptable:not_acceptable
407Proxy Authentication Required:proxy_authentication_required
408Request Timeout:request_timeout
409Conflict:conflict
410Gone:gone
411Length Required:length_required
412Precondition Failed:precondition_failed
413Request Entity Too Large:request_entity_too_large
414Request-URI Too Long:request_uri_too_long
415Unsupported Media Type:unsupported_media_type
416Requested Range Not Satisfiable:requested_range_not_satisfiable
417Expectation Failed:expectation_failed
422Unprocessable Entity:unprocessable_entity
423Locked:locked
424Failed Dependency:failed_dependency
426Upgrade Required:upgrade_required
 
5xx Server Error
500Internal Server Error:internal_server_error
501Not Implemented:not_implemented
502Bad Gateway:bad_gateway
503Service Unavailable:service_unavailable
504Gateway Timeout:gateway_timeout
505HTTP Version Not Supported:http_version_not_supported
507Insufficient Storage:insufficient_storage
510Not Extended:not_extended

source: http://www.codyfauser.com/2008/7/4/rails-http-status-code-to-symbol-mapping

Rabu, 17 November 2010

[shared] array/hash support for system_settings

https://github.com/realityforge/rails-system-settings

# system_settings/lib/system_setting.rb

class SystemSetting < ActiveRecord::Base
@@data = {}

validates_length_of :name, :in => 1..255
validates_uniqueness_of :name

serialize :value

def self.[](name)
name = name.to_s unless name.is_a? String
return @@data[name] unless @@data[name].nil?
p = SystemSetting.find_by_name(name)
@@data[name] = p ? p.value.freeze : nil
end

def self.[]=(name, value)
name = name.to_s unless name.is_a? String
p = SystemSetting.find_or_initialize_by_name(name)
@@data[name] = p.value = value
p.save!
p.value.freeze
end
end

[shared] Unsigned int support for migration

currently rails only allow signed int for migration, but for best practice it's better to use unsigned int for some cases

you can force migration to unsigned int by doing something like this

t.column :unsigned_value, "integer unsigned"


but that kinda ugly to see.
luckily rob anderton (thewebfellas.com) already created monkey patch to add unsigned support: http://thewebfellas.com/assets/2008/9/30/active_record_unsigned.rb

the patch will add :unsigned option in migration so you can do something like this:

t.integer :unsigned_value, :unsigned => true


furthermore the patch will make default table id and foreign key to unsigned int(11), but make sure to use t.references to create foreign key or simply use :unsigned => true

important things:


- for existing project this patch might break table relation because it will create unsigned int foreign key but the primary key might be signed int from old migration
- some plugin like role_requirement will generate migration that using t.integer for foreign key instead of t.references, you need to change this manually
- the patch will only change mysql adapter, so it will not work for other adapter

[shared] Rails migration integer limit

:limit Numeric Type Column Size Signed Max value Unsigned Max value
1 TINYINT(4) 1 byte 127 255
2 SMALLINT(6) 2 bytes 32767 65535
3 MEDIUMINT(9) 3 bytes 8388607 16777215
4 and others INT(11) 4 bytes 2147483647 4294967295
5..8 BIGINT(20) 8 bytes 9223372036854775807 18446744073709551615

Minggu, 14 November 2010

[shared] rails user role system

https://github.com/timcharper/role_requirement

installation:
- ruby script/generate roles Role User

installation will do:

  • Generates habtm table, creates role.rb with the habtm declaration. Adds declaration in user.rb (scans the code for "class User < ActiveRecord::Base", and puts the new code right after it.



  • Creates an admin user in users.yml, with a role named admin in roles.yml, including a fixture to demonstrate how to relate roles to users in roles_users.yml



  • Modify the user.rb (or corresponding user model) file, add the instance method has_role?



  • Generates RoleRequirementSystem against for the corresponding user model.



  • Generates a migration to make the necessary database changes



  • Scans ApplicationController, inserts the lines "include AuthenticatedSystem", and "include RoleRequirementSystem", if not already included.



  • Scans test_helper.rb and adds "includes RoleRequirementTestHelpers", if not already included.




Usage:
- add require_role "role_name" in controller, similar to before_filter

[shared] rails exception logger

https://github.com/defunkt/exception_logger

installation:
- ruby script/generate exception_migration
- rake db:migrate
- in application_controller.rb add
include ExceptionLoggable
rescue_from Exception, :with => :exception_handler

private

def exception_handler exception
log_exception exception
render "/500.html"
end


the default controller and view using prototype.js instead of jquery so i created my own controller view under admin namespace. the database pretty simple and similar to rails exception notification

for rails 3:
https://github.com/QuBiT/exception_logger

Rabu, 10 November 2010

kaspersky conflicting with tortoise svn

kaspersky, settings, exclusions, add:
- rules for the project folder
- trusted application for:
- - ..\system32\searchprotocolhost.exe
- - ..\system32\searchfilterhost.exe
- - ..\tortoisesvn\bin\tsvncache.exe
- - ..\tortoisesvn\bin\tortoiseproc.exe
- - ..\tortoisesvn\bin\tortoisemerge.exe

other possible cause:
win7 bug prior sp1: http://support.microsoft.com/kb/982927/en-us

Kamis, 04 November 2010

Mysql::Error: query: not connected

MySQL 5.1 client library doesn't play well with Rails

download:
http://instantrails.rubyforge.org/svn/trunk/InstantRails-win/InstantRails/mysql/bin/libmySQL.dll

source:
http://forums.aptana.com/viewtopic.php?f=20&t=7563&p=27407&hilit=libmysql.dll#p27407

Sabtu, 30 Oktober 2010

named scope conditions eager loaded warning

http://jitu-blog.blogspot.com/2009/07/looking-into-rails-namedscope.html

Senin, 25 Oktober 2010

[shared] range overlap check

overlap = !((end2 < start1) || (start2 > end1))

r = Range.find_all(:conditions => ["NOT ((end < ?) || (start > ?))", new_start, new_end])

overlap = r.present?
number_of_overlaps = r.length

Kamis, 21 Oktober 2010

dynamic form with accept_nested_attributes

http://railsforum.com/viewtopic.php?id=28447

Selasa, 19 Oktober 2010

jQuery ExtendedHelper

http://code.google.com/p/jquery-extendedhelper/

jQuery hash merge

merge hash2 to hash1:

$.extend(hash1, hash2);


for recursive merge:

$.extend(true, hash1, hash2);

Senin, 18 Oktober 2010

add javascript files to rails default

environment.rb

# reset:
ActionView::Helpers::AssetTagHelper::JAVASCRIPT_DEFAULT_SOURCES = [
'jquery-1.4.3', 'jquery-ui-1.8.6.custom', 'jrails',
'jquery.extended_helper_2.0', 'date-id', 'jquery.gritter'
]
ActionView::Helpers::AssetTagHelper::reset_javascript_include_default

# adding:
ActionView::Helpers::AssetTagHelper::register_javascript_include_default 'extended_helper', 'date'

Senin, 11 Oktober 2010

[shared] I18n pluralization patch

translate patch



module I18n
module Backend
module Base
def translate(locale, key, options = {})
raise InvalidLocale.new(locale) unless locale
return key.map { |k| translate(locale, k, options) } if key.is_a?(Array)

entry = key && lookup(locale, key, options[:scope], options)

if options.empty?
entry = resolve(locale, key, entry, options)
else
count, default = options.values_at(:count, :default)
values = options.except(*RESERVED_KEYS)
entry = entry.nil? && default ?
default(locale, key, default, options) : resolve(locale, key, entry, options)
end

raise(I18n::MissingTranslationData.new(locale, key, options)) if entry.nil?
entry = entry.dup if entry.is_a?(String)

entry = pluralize(locale, entry, count)
entry = interpolate(locale, entry, values) if values
entry
end
end
end
end


pluralize patch



module I18n
module Backend
class Simple
def pluralize(locale, entry, count)
if !defined?(count) || count.blank?
key = :none
elsif count == 0 && (entry.respond_to?(:has_key?) && entry.has_key?(:zero))
key = :zero
end
key ||= count == 1 ? :one : :other
unless entry.respond_to?(:has_key?) && entry.has_key?(key)
entry
else
entry[key]
end
end
end
end
end

Rabu, 06 Oktober 2010

[shared] Netbeans FUSCK (Frequently Used ShortCut Keys)



  • ctrl+c, ctrl+v, alt+v :
    - Copy, Paste, Display paste history (CopyPasteHistory Plugin required)


  • ctrl+f, ctrl+h :
    - find, replace in file


  • ctrl+shift+f, ctrl+shift+h :
    - find, replace in project


  • alt+shift+f :
    - format codes indentation in file


  • ctrl+shift+up/down arrow
    - copy up/down selection line. very useful combined with alt+shift+arrow


  • alt+shift+up/down/left/right arrow
    - move selection


  • ctrl+space
    - show code completion


  • ctrl+shift+a
    - go to view from controller or to controller from view


  • ctrl+click / ctrl+b
    - go to declaration


  • ctrl+tab, ctrl+shift+tab
    - next/prev tab


  • alt+shift+r
    - open rake task window


  • alt+insert
    - open generate window


  • ctrl+w
    - close document


  • ctrl+shift+w
    - close all documents


  • alt+shift+w
    - [custom shortcut] close other documents

[shared] netbeans useful plugins



  • Bundled subversion client for windows
    - What: self explanatory
    - Install: Tools, Plugins


  • Cucumber features
    - What: support cucumber files and add cucumber functionality
    - Install: http://github.com/QuBiT/cucumber-netbeans-plugin


  • Copy paste history
    - What: add copy (ctrl+c) history functionality, very useful!
    - Install: http://plugins.netbeans.org/PluginPortal/faces/PluginDetailPage.jsp?pluginid=78


  • jVi
    - What: vi/vim editor clone
    - Install: http://sourceforge.net/projects/jvi/files/

[shared] netbeans rails additional code template

Ruby


1. model

#--
# constants
#++

#--
# require
#++

#--
# include
#++

#--
# attr
#++

#--
# extra capabilities
#++

#--
# belongs_to
#++

#--
# has_one
#++

#--
# has_many
#++

#--
# accepts_nested_attributes_for
#++

#--
# validation
#++

#--
# callbacks
#++

#--
# named_scope
#++

#--
# other definition such as alias_method
#++

#--
# public class methods
#++

#--
# public instance methods
#++

private
#--
# private class methods
#++

#--
# private instance methods
#++

protected
#--
# protected class methods
#++

#--
# protected instance methods
#++


2. resource

before_filter :prepare_account, :only => [:show, :edit, :update, :destroy]

# GET new_account_url
def new
# return an HTML form for describing the new account
end

# POST account_url
def create
# create an account
end

# GET account_url
def show
# find and return the account
end

# GET edit_account_url
def edit
# return an HTML form for editing the account
end

# PUT account_url
def update
# find and update the account
unless @account.update_attributes params[:account]
# failure code here..
end
end

# DELETE account_url
def destroy
# delete the account
@account.destroy
end

private

def prepare_account
@account = Account.first
end


3. resources

before_filter :prepare_messages, :only => [:index]
before_filter :prepare_message, :only => [:show, :edit, :update, :destroy]

# GET messages_url
def index
# return all messages
end

# GET new_message_url
def new
# return an HTML form for describing a new message
end

# POST messages_url
def create
# create a new message
end

# GET message_url(:id => 1)
def show
# find and return a specific message
end

# GET edit_message_url(:id => 1)
def edit
# return an HTML form for editing a specific message
end

# PUT message_url(:id => 1)
def update
# find and update a specific message
unless @message.update_attributes params[:message]
# failure code here..
end
end

# DELETE message_url(:id => 1)
def destroy
# delete a specific message
@message.destroy
end

private

def prepare_messages
@messages = Message.all
end

def prepare_message
@message = Message.find params[:id]
end


4. vf

validates_format_of :${1 default="attribute"}


5. vn

validates_numericality_of :${1 default="attribute"}


Ruby HTML (RHTML)


1. script




2. style


Kamis, 30 September 2010

[shared] datejs, impressive js date helpers

http://www.datejs.com/
http://code.google.com/p/datejs/wiki/APIDocumentation

include in rails 2.1+ not always generate single query

"..for some situations, the monster outer join becomes slower than many smaller queries..."

http://akitaonrails.com/2008/05/25/rolling-with-rails-2-1-the-first-full-tutorial-part-2
in Optimized Eager Loading

Kamis, 23 September 2010

[shared] paypal recurring with activemerchant

add this file:
from http://github.com/rayvinly/active_merchant/blob/master/lib/active_merchant/billing/gateways/paypal_express_recurring.rb


read:
- https://cms.paypal.com/us/cgi-bin/?cmd=_render-content&content_ID=developer/e_howto_api_WPRecurringPayments
- and PP_RecurringPaymentsAPI.pdf

Credit Card







allowed credit card type (not all country support all type):
  • Visa
  • MasterCard
  • Discover
  • Amex
  • Maestro
  • Solo


test credit card obtained from:
http://www.darkcoding.net/credit-card-numbers/
you can generate it yourself using MOD 10 algorithm (Luhn formula) or so they said =P
http://www.darkcoding.net/credit-card/luhn-formula/

Error! DPRP is disabled for this merchant ?
- Only US & UK sandbox business accounts can be enabled for WPP
- use preconfigured account using Website Payments Pro
- or read https://www.x.com/docs/DOC-1603

Paypal Account





Additional



ActiveMerchant::Billing::CreditCard patch to support paypal recurring:

libmysql.dll is missing

copy [mysql installation directory]/bin/libmysql.dll to [ruby installation directory]/bin/

Rabu, 22 September 2010

ruby installer development kit

a note to self for devkit problem

error:

"Could not create Makefile due to some reason, probably lack of
necessary libraries and/or headers."

or

"You have to install development tools first."

see:
http://github.com/oneclick/rubyinstaller/wiki/development-kit

Rabu, 04 Agustus 2010

[shared] pac file for developing site using subdomain

saat ini saia mengerjakan site yg menggunakan subdomain tersendiri untuk setiap usernya, karenanya setiap saia meregistrasi user baru saia harus menambahkan line baru di hosts file saia:

127.0.1.1 localhost
127.0.1.2 mysite.local
127.0.1.3 www.mysite.local
127.0.1.5 user1.mysite.local
127.0.1.6 user2.mysite.local
...

hal tersebut cukup merepotkan terutama untuk testing.

sedikit googling saia menemukan site yang menjelaskan penggunaan pac (proxy auto-config) file untuk mempermudah develop subdomain site:
http://www.taylorluk.com/2009/08/hey-pac-man-sup-subdomains

tambahan, isi pac file yg saia gunakan saat ini untuk rails:

function FindProxyForURL(url, host) {
if (shExpMatch(host, "*.local")) {
return "PROXY 127.0.0.1:3000";
}
if (shExpMatch(host, "*.example.com")){
return "PROXY 127.0.0.1:3000";
}
return "DIRECT";
}

[shared] javascript arrayCompact

di RoR saia biasa menggunakan array.compact untuk memastikan di dalam suatu array tidak terdapat elemen nil. masalahnya javascript tidak mempunyai fungsi tersebut, untuk itu saia membuat fungsi kecil untuk melakukan hal td + sedikit tambahan untuk membuang bukan hanya null tetapi jg string kosong atau false:


/* return a copy of array with all null and optionally blank or false elements removed
* example:
* - arrayCompact(['a',null, 0, '', false]) //=> [ 'a', 0, '', false ] (only null removed)
* - arrayCompact(['a',null, 0, '', false], 1) //=> [ 'a', 0, false ] (only null and empty string removed)
* - arrayCompact(['a',null, 0, '', false], 2) //=> [ 'a' ] (all that equal to false removed)
*/
function arrayCompact(array, remove_level){
var new_array = new Array();
for(k in array)
if(typeof(array[k]) != 'undefined' &&
array[k] != null &&
!(remove_level > 0 &&
(typeof(array[k]) == 'string' || remove_level > 1) &&
array[k] == ''))
new_array.push(array[k]);
return new_array;
}


fungsi tersebut menggunakan plain javascript jadi dapat digunakan tanpa js framework apapun

[shared] Simple jQuery function for form debugging

dalam developing site, seringkali saia membuat form dengan elemen2 seperti select tag, radio button, input hidden dll. normalnya user hanya dapat memasukkan value2 yang sudah saia tentukan pada elemen2 tersebut, tp user yg "iseng" bisa saja memasukkan value di luar value2 yg sudah ditentukan td sehingga dapat merusak aplikasi.

buat mencegah saia harus membuat server side validation yang seringkali terlupakan karena pada saat saia melakukan testing saia melakukannya sebagai user normal bukan sebagai user "iseng". hal tersebut membuat saia berpikir untuk membuat script sederhana untuk mempermudah testing.

menggunakan jQuery ternyata script helper nya cukup simple:

function debugInput(){
$('.debug_input').remove();
$(':input:not(textarea,:text:not(.date),:button,:submit,:reset)').each(function(){
$(this).after('
'+this.name+':
').css('border', 'medium dotted gray');
});
$('form').each(function(){
$(this).append('
'+this.id+':
')
});
$('.debug_input').css('opacity', 0.7).draggable();
}


pada saat dijalankan fungsi tersebut akan menambahkan input text normal di tiap elemen2 form sehingga saia dapat dengan mudah memasukkan value2 aneh di select tag dan sejenisnya. selain itu fungsi ini jg menambahkan tombol submit di form.

contoh pemakaian pada site php manual:

Selasa, 25 Mei 2010

accept online payment for users (Part 1a: PayPal Website Payments Standard)

dah lama ga post, kali ini saia mao post artikel yg rasanya bakal lumayan panjang ;) tentang integrasi beberapa payment gateway (paypal, authorize.net, sagepay, google-checkout) buat nerima online payment untuk user (bukan untuk site owner).
jadi intinya site hanya sebagai perantara antara user dengan payment gateway.

ok mulai dari yg paling banyak digunakan: PayPal

persiapan yg dibutuhkan:
1. rails project dengan users (saia asumsikan dsini nama modelnya User)
2. developer account buat paypal (biar bisa nyoba) register di: https://developer.paypal.com/

siapkan model untuk menyimpan data paypal tiap user dan data notifikasi:

ruby script/generate model Paypal
ruby script/generate model Ipn


buat relasi user dengan paypal dan paypal dengan ipn, agar flexible saia menggunakan relasi polymorphic tapi relasi biasa juga dapat digunakan (saia anjurkan relasi paypal dengan ipn polymorphic agar nantinya kita mudah menambahkan gateway lain):

### paypal.rb ###
belongs_to :payable, :polymorphic => true
has_many :ipns, :as => :gateway, :dependent => :destroy


### ipn.rb ###
belongs_to :gateway, :polymorphic => true


### user.rb ###
include PaymentGatewayRelation


### /lib/payment_gateway_relation.rb ###
module PaymentGatewayRelation
def self.included(recipient)
recipient.class_eval do
has_one :paypal, :as => :payable, :dependent => :destroy

def payments
[self.paypal].compact
end

def payment_available?
!self.payments.empty?
end

def payment
self.payments.first
end

def payment= value
return unless value.respond_to? :payable
klass = value.class
self.payments.each{|x| x.destroy if x.class != klass}
self.reload
self.send("#{klass.to_s.underscore}=", value)
end
end
end
end

payment_gateway_relation.rb berisi relasi untuk semua gateway dan method2 untuk membantu akses.
mungkin akan lebih saia perjelas nanti saat gateway lain ditambahkan.

kemudian, tambahkan data paypal user yang dibutuhkan pada migration:

class CreatePaypals < ActiveRecord::Migration
def self.up
create_table :paypals do |t|
t.references :payable, :polymorphic => true
t.string :business, :limit => 70
t.string :return_url
t.timestamps
end
end

def self.down
drop_table :paypals
end
end


class CreateIpns < ActiveRecord::Migration
def self.up
create_table :ipns do |t|
t.references :gateway, :polymorphic => true
t.string :trans_id, :limit => 30
t.string :description, :limit => 50
t.timestamps
end
end

def self.down
drop_table :ipns
end
end

- business: paypal business account (email) dari user.
- return_url: url redirect untuk visitor ke suatu site (bisa di luar site kita) setelah pembayaran selesai
- trans_id: transaction id untuk mencegah duplikasi pembayaran
- description: optional, untuk menyimpan catatan ato suatu referensi

lanjut, di paypal.rb tambahkan attr_accessible dan beberapa validasi untuk keamanan ;)

include UriValidator

alias_attribute :account, :business

attr_accessible :business, :account, :return_url

validates_presence_of :account, :return_url
validates_uniqueness_of :business, :scope => :payable_type, :message => "account has already been used.", :case_sensitive => false
def validate
errors.add(:return_url, 'format invalid.') unless self.return_url.blank? || validate_http(self.return_url)
end

saia menggunakan alias attribute account untuk business agar lebih mudah dimengerti.

hal yang sama di ipn.rb

validates_presence_of :trans_id
validates_uniqueness_of :trans_id, :scope => :gateway_type, :allow_nil => true, :allow_blank => true

attr_accessible :trans_id, :description, :user, :gateway


buat ruby file /lib/uri_validator.rb:

module UriValidator

def validate_http uri
require 'uri'

return false if uri.blank?

uri = "http://" + uri unless uri.index("http") == 0

uri = URI.parse(uri)
if uri.class != URI::HTTP
return false
end

true
rescue URI::InvalidURIError
return false
end
end


fiuh.. urusan model2an beres..
migrate lalu selanjutnya controller dan view..
..
di post berikutnya biar kentang (sbenernya males nulis blog panjang2) =P

Jumat, 16 April 2010

[shared] to_xml_params with attributes from hash

sejauh yang saia tau, rails .to_xml dari hash tidak dapat menghasilkan xml tag dengan attribute selain type.
tetapi saia membutuhkan hal tersebut pada project yang saia kerjakan. karena itu saia membuat method to_xml_params untuk mengubah hash menjadi xml string:

def self.to_xml_params(data)
if data.is_a? Hash
data.collect do |key, value|
tag_attr = []
if key.is_a? Array
tag = key.first
key[1..-1].each do |key_attr|
key_attr.each do |k, v|
tag_attr << "#{k}=\"#{v}\""
end
end
else
tag = key
end
tag = tag.to_s.tr('_', '-')
result = "<#{tag}#{" #{tag_attr.join(' ')}" unless tag_attr.empty?}>"
result << to_xml_params(value)
result << "</#{tag}>"
result
end.join('')
elsif data.is_a? Array
data.inject(''){|result, v|
result << to_xml_params(v)
}
else
data.to_s
end
end


contoh penggunaan:


to_xml_params :a => "b"
# -> b
to_xml_params :a => {:b => "c"}
# -> c
to_xml_params [:a,{:x => :y}] => {:b => "c"}
# -> c
to_xml_params [:a,{:x => :y, :z => "w"}] => {[:b, {:p => "q"}] => "c"}
# -> c
to_xml_params :a => {:b => "c", :d => "e"}
# -> ec
to_xml_params :a => [{:b => "c"}, {:d => "e"}]
# -> ce sama dengan diatas

Selasa, 06 April 2010

[shared] "whenever" - cron simplified

karena beberapa waktu lalu saia menyadari kalau rufus-scheduler bermasalah bila digunakan di passenger maka saia beralih kembali menggunakan cron.
namun cron mengharuskan saia untuk meng-update crontab setiap kali saia ingin menambahkan task baru, karenanya saia mencoba mencari alternatif lain yg reliable dan mudah digunakan..

http://github.com/javan/whenever/

sebagai alternatif rufus-scheduler gem ini cukup mudah untuk digunakan, walaupun sedikit lebih "repot".

saia rasa dokumentasi di github sudah cukup jelas jadi saia hanya akan membahas implementasi yg saia gunakan.

yang menyusahkan dari gem ini, saia diharuskan meng-convert schedule.rb ke cron dengan menggunakan

whenever --update-crontab MyApp

setiap kali saia melakukan perubahan di schedule.rb
dokumentasi di github menunjukkan cara integrasi dengan capistrano, tetapi saia tdk mau direpotkan dengan menginstall gem tambahan, jadi yg saia lakukan hanya menambahkan bbrp baris di script untuk deploying pada staging dan production server:

.... svn up, rsync, etc ....

cd /home/rails/myapp && whenever --update-crontab MyApp --set environment=staging
cd /home/rails/myapp/script && chmod 755 runner

baris 1 untuk meng-update crontab. --set environment digunakan untuk merubah environment dari default production
baris 2 untuk memberi ijin execute pada script runner, karena saia menggunakan model method bukan rake untuk task2 nya

contoh schedule.rb yg saia gunakan:

every 30.minutes do
runner "QueuedEmail.deliver_all"
end

every 10.minutes do
runner "Message.destroy_all_expired"
end

every :day do
runner "ReferData.give_rewards!"
runner "SystemMailer.deliver_scheduler_check"
end

How to report bugs effectively

http://www.chiark.greenend.org.uk/~sgtatham/bugs.html
artikel yg saia pikir sangat berguna, tidak hanya untuk user, tetapi juga untuk developer.

finding a way to reproduce problem is already a half of the debugging progress ^_^

Selasa, 09 Maret 2010

[shared] Branching using TortoiseSVN part 2

ok, ngelanjutin dari part 1, saia akan menjelaskan soal feature branch.

untuk post ini saia tidak akan menjelaskan prosedur untuk branching dan merging karena saia rasa di part 1 sudah cukup jelas.. jadi saia akan lebih membahas tentang konsep dari feature branch.

so, pertanyaannya: apa itu feature branch?
jawab: feature branch sbenarnya sama seperti staging branch. perbedaannya feature branch biasanya dibuat dari staging branch, bukan dari trunk, jadi branch of branch.
feature branch ini dibuat untuk memenuhi salah satu aturan yg saia tulis di bagian tambahan pada part 1:

"Usahakan agar setiap update yang ditambahkan di staging selesai dalam sesedikit mungkin commit"

jadi bila pada suatu project ada feature yang cukup besar dan rumit, sehingga diperkirakan membutuhkan waktu 2 hari atau lebih, sebaiknya membuat feature branch khusus untuk feature tersebut.

Kelebihan menggunakan feature branch:

misalkan klien meminta implementasi feature A di page X yang cukup besar/rumit. nah saat kita sedang ditengah2 mengerjakan feature A tersebut ternyata di page X terdapat bug vital dan harus segera diperbaiki. Namun yang jadi masalah page X tersebut sudah terdapat beberapa bagian dari feature A yang masi dalam pengerjaan, jadi tidak mungkin untuk di-deploy (dapat menyebabkan bug). hal tersebut dapat dihindari apabila feature A dikerjakan dalam feature branch.

kasus lain (masih nyambung sama yg di atas), karena feature A cukup rumit, maka pengerjaan memakan waktu bbrp hari, smentara sebelum selesai tidak dapat di-commit karena akan menyebabkan staging server menjadi rusak. nah lalu misalkan karena suatu sebab tempat pengerjaan feature A tersebut tidak dapat digunakan, maka pengerjaan feature A akan dipaksa berhenti. dengan feature branch commit kode yang belum selesai tidak akan mempengaruhi staging server jadi bila terjadi kasus tadi, kita dapat check-out ulang dan melanjutkan pengerjaan di tempat lain.

Kekurangannya:

- memperbesar penggunaan disk space
- ada kemungkinan conflict antara feature branch dengan staging (hati2 dalam me-resolve conflict)
- memperlama deploying, untuk deploy feature A ke production (trunk), merge harus dilakukan 2x: merge feature_a_branch ke staging, kemudian merge staging ke trunk. karena merge proses yg memakan waktu, hal ini dapat menyusahkan apabila kita hanya perlu men-deploy perbaikan kecil (1-2 baris) untuk feature A ke production. maka dari itu biasanya perbaikan2 bug tetap saia lakukan di staging.

ok sgitu saja soal branching2an, post ini hanya merupakan pendapat dari saia dan apa yang saia gunakan saat ini, jadi mungkin tidak spenuhnya benar, dan pastinya ada cara lain yang lebih baik.

Kamis, 04 Maret 2010

[shared] jQuery serialize only a part of form

baru2 ini di project yang sedang saia kerjakan, saia menemui suatu masalah, intinya saia ingin men-submit suatu bagian dari form yang cukup besar menggunakan ajax untuk mendapatkan nilai yang akan dipakai oleh bagian lain dari form.

secara sederhana kode nya sebagai berikut:

<form>
<input type="text" name="user_name" />
<div id="part1">
<input type="text" name="user_paid_amount" onchange="ajaxRequestToGetPart2ValueBasedOnPart1();" />
<input type="text" name="user_type" onchange="ajaxRequestToGetPart2ValueBasedOnPart1();"/>
</div>
<div id="part2">
<input type="text" name="price_amount" value="" />
</div>
</form>

intinya ketika ada perubahan pada input2 di part1, part2 akan secara otomatis diisi.

namun yang jadi masalah adalah bagaimana cara mengambil value2 dari part1 untuk dikirimkan sebagai parameter..
jquery menyediakan fungsi serialize untuk mengambil semua element dari suatu form (http://api.jquery.com/serialize/), sayangnya hal tersebut tidak dapat dilakukan untuk container selain form, jadi $('#part1').serialize() tidak dapat digunakan..

Solusi 1 - clone(true)

jQuery Serialize a Fieldset
menggunakan clone untuk meng-copy isi div yang kemudian diletakkan pada form yang disembunyikan. form tersebut lalu di-serialize kemudian di-remove.

namun clone merupakan fungsi yang cukup memakan resource.

Solusi 2 - selector

solusi yang ke-2 dengan menggunakan selector :input. kira2 kodenya sebagai berikut:

function serialize(container){
//container: string selector or element of the container
$(':input', container).serialize();
}
serialize('#part1');

lebih simple dan efektif :)

Jumat, 05 Februari 2010

[shared] Directory Traversal in Rails

beberapa saat yang lalu di project yang sedang saia kerjakan, saia menyadari sebuah security hole yang bisa dibilang cukup berbahaya (untungnya sebelum terjadi hal2 yang tidak diinginkan). yang menyebabkan masalah, saia membuat sebuah page yang isinya berasal dari partial berdasarkan params, isi view-nya kurang lebih seperti ini:

# views/frontpages/show.html.erb
<h3>Title</h3>
<%= render :partial => params[:page] || "home" %>

jadi isi dari page tersebut tergantung dari params page. yang jadi masalah ketika user memasukkan suatu url seperti
http://..../frontpages/show?page=%2fadmin%2fusers%2flist
(url dapat dengan mudah dicari menggunakan dictionary attack atau sejenisnya)
maka partial yang di-load akan berasal dari halaman di admin::users.
walaupun besar kemungkinan user tidak akan dapat melihat data di halaman tersebut karena akses tidak melalui controller, tetapi hal tersebut tetap tidak seharusnya diperbolehkan. apalagi terkadang dengan alasan 'malas' atau lainnya kita melakukan pengambilan data tidak di dalam controller tetapi langsung di view atau helper dimana data tersebut digunakan (ini salah satu alasan mengapa pengambilan data harus berada di controller bukan di view)

untuk mengatasi hal tersebut saia memproses params page td sebelum digunakan di view, misalkan:

# /controllers/frontpages_controller.rb
# ...
def show
@partial = (params[:page].blank? ||
params[:page].try('include?', '/')) ?
'home' : params[:page]
end
# ...


# views/frontpages/show.html.erb
<h3>Title</h3>
<%= render :partial => @partial %>

atau lebih baik lagi dengan tidak meng-implementasi partial seperti ini dan membuat action terpisah untuk setiap page.

yah situasi di atas mungkin merupakan hal bodoh yang seharusnya tidak terjadi selama kita mengikuti konvensi dari rails, tp ada kalanya karena berbagai alasan kita melanggar konvensi dan menggunakan implementasi kita sendiri, karenanya sebaiknya kita berpikir 1000x (lebay) sebelum melakukan hal itu.

oh ya directory traversal sendiri bukan hanya dapat digunakan pada situasi seperti di atas, tetapi banyak situasi lain yang bisa dimanfaatkan dengan directory traversal ini. karena itu, berhati-hatilah, inget kata bang napi (halah..)

ok segitu aja, tadinya mao nulis lanjutan dari svn branching tapi ditunda buat next post ^_^

Rabu, 03 Februari 2010

[shared] Branching using TortoiseSVN part 1

skarang saia akan sedikit sharing tentang branching dengan tortoise svn. perlu saia beritahukan, implementasi yang saia gunakan mungkin bukan yang terbaik, karena itu kalo ada yg mempunyai implementasi lebih baik tolong beritahu saia melalui comment ^_^


Buat branch


pertama yg perlu dilakukan buat 3 folder di dalam folder project, yaitu: branches, tags dan trunk

branching -- trunk
-- branches
-- tags


kemudian taruh project anda di dalam folder trunk, folder trunk ini akan digunakan sebagai kode stabil yang digunakan di production server.

branching -- trunk -- app
-- config
-- db
-- dll..
-- branches
-- tags


commit, lalu klik kanan di folder trunk, pilih menu TortoiseSVN>Branch/tag ubah To URL menjadi "../branches/staging", tambahkan message kalo perlu, dan OK.

setelah selesai update folder staging, maka staging akan ditambahkan

branching -- trunk -- app
-- config
-- db
-- dll..
-- branches -- staging -- app
-- config
-- dll..
-- tags

folder staging ini digunakan untuk kode staging server, biasa saia gunakan untuk kode2 yg perlu ditest atau dicek oleh klien sebelum masuk production.


Merge


setelah melakukan banyak update2 di staging, dan klien menyatakan ok untuk suatu update/feature agar di-deploy ke production, kita dapat dengan mudah menggunakan fitur merge.

Perhatian! untuk mempermudah merging, ikuti aturan commit di akhir post ini..

pastikan fitur yg akan di merge sudah di commit, kemudian klik kanan di folder trunk, pilih menu TortoiseSVN>Merge, pilih "Merge a range of revisions", masukkan url staging dalam URL to merge from, dan tentukan revision dari fitur yg akan di-merge, sebaiknya gunakan show log.
dalam log pilih revision yang akan di-merge, revision yang sudah pernah di-merge akan berwarna abu2, next lalu merge
stelah selesai periksa kode yang ditambahkan, bila ada kesalahan dalam memilih revision gunakan revert pada trunk. sebaiknya jangan pernah secara langsung merubah trunk, kalau ada kesalahan dalam kode, tambahkan perbaikan di staging dan merge ke trunk.

commit dan selesai.


Tambahan


beberapa tambahan yang perlu diperhatikan:

  • Hindari meng-edit trunk secara langsung

  • Tambahkan message yang jelas dalam setiap commit di staging

  • Usahakan agar setiap update yang ditambahkan di staging selesai dalam sesedikit mungkin commit, kalau fitur yang ditambahkan cukup besar dan rumit, gunakan feature branch (akan dibahas di post berikutnya)

  • Jangan pernah menambahkan 2 fitur dalam 1 revision, pisahkan dalam revision yang berbeda, akan menyulitkan pada saat memilih revision untuk di-merge nantinya



ok segitu dulu, post berikutnya saia akan membahas feature branches dan tags

Senin, 01 Februari 2010

[shared] nested layout

sesuai dengan yang saia janjikan di post sebelumnya, skarang saia akan membahas soal nested layout.
untuk memudahkan, saia akan mengambil contoh kasus dari post saia sbelumnya, yaitu untuk membuat menu yg berbeda2 tergantung controller yg aktif.


# layouts/application.html.erb
# ...
<div class="menu">
<%= render :partial => "main_menu" %>
</div>
<div class="content">
<%= yield %>
</div>
# ...


diubah menjadi:


# layouts/application.html.erb
# ...
<div class="menu">
<% begin %>
<%= render :partial => "layouts/#{controller.controller_path}_menu" %>
<% rescue ActionView::MissingTemplate %>
<% begin %>
<%= render :partial => "layouts/#{controller.controller_path.sub(/#{controller.controller_name}$/,'default_menu')}" %>
<% rescue ActionView::MissingTemplate %>
<%= render :partial => "main_menu" %>
<% end %>
<% end %>
</div>
<div class="content">
<%= yield %>
</div>
# ...


kode diatas akan mencari layout menu dengan path dan nama controller yg sedang aktif, bila tidak ditemukan default_menu partial dalam path controller aktif akan digunakan, bila tidak ada juga maka main_menu sebagai default akan digunakan. contoh:
  • UsersController akan menggunakan "layouts/_users_menu" || "layouts/_default_menu" || "layouts/_main_menu"
  • Admin::UsersController akan menggunakan "layouts/admin/_users_menu" || "layouts/admin/_default_menu" || "layouts/_main_menu" 
  • Admin::HomeController akan menggunakan "layouts/admin/_home_menu" || "layouts/admin/_default_menu" || "layouts/_main_menu" 
jadi untuk menggunakan menu yang sama pada smua controller untuk admin, buat partial "layouts/admin/_default_menu" dan tentu saja bila dibutuhkan tetap dapat di override dengan "layouts/admin/_[nama_controller]_menu"

dengan menggunakan nested layout, kita cukup menggunakan hanya 1 application layout. walaupun lebih tidak flexible daripada menggunakan content_for yg dapat diatur bergantung pada action. tp menurut saia jauh lebih rapih karena mengurangi kode yg ditulis pada controller.

Rabu, 20 Januari 2010

[shared] Using content_for from controller

biasanya content_for digunakan untuk passing suatu blok text dr view ke layout, contohnya

layout/application.html.erb

# ...
<div class="menu">
<%= yield :menu %>
</div>
<div class="content">
<%= yield %>
</div>
# ...


homepages/show.html.erb

# ...
<% content_for :menu, generate_menu %>
# ...


nah, terkadang dibutuhkan content_for yg dapat dipanggil di controller. misalkan untuk mengurangi pengulangan dengan menggunakan before_filter (dapat jg menggunakan nested layout, akan saia bahas di post lain).
untuk itu setelah saia coba ternyata hal tersebut dapat diselesaikan dengan mudah, hanya dengan mengisi instance variable dengan nama @content_for_#{nama} dengan contoh diatas berarti:

homepages_controller.rb

# ...
@content_for_menu = generate_menu
# ...


Warning: cara tersebut sudah dicoba di rails 2.3.3 tapi ada kmungkinan deprecated untuk versi rails yang lebih baru.. kalo ada cara yg lebih baik tolong kasi tau saia via comment, thanks ^_^

Jumat, 15 Januari 2010

[shared] Simple scheduled emails using rufus-scheduler

Baru2 ini di project yg sedang saia kerjakan, saia diminta untuk mengimplementasikan automated email. Intinya mengirim email secara otomatis pada saat user melakukan sesuatu. Sekilas biasa saja, hanya menggunakan ActionMailer untuk mengirim email pada event tertentu, namun masalahnya ada beberapa email yang dikirim bukan pada saat event dijalankan, ada delay waktu bbrp hari sebelum email dikirim.

Untuk itu saia memutuskan untuk membuat mailer yang dapat dijadwalkan, selain dapat mengirimkan email pada tanggal dan waktu tertentu, email jg dikirimkan secara berkala pada jangka waktu yg ditentukan (asumsi saia akan dibutuhkan nantinya untuk email laporan berkala.)

Buat Scheduler

Pertama, install rufus-scheduler

gem install rufus-scheduler

lalu dalam folder config/initializers buat file ruby baru, misal "task_scheduler.rb"

scheduler = Rufus::Scheduler.start_new

scheduler.every("10m") do
QueuedEmail.deliver_all
end

yang akan menjalankan deliver_all pada model QueuedEmail setiap 10 menit.

Buat Model utk menyimpan jadwal

generate model baru, misal queued_email
migration:

class CreateQueuedEmails < ActiveRecord::Migration
def self.up
create_table :queued_emails do |t|
t.string :from, :limit => 100
t.string :recipients, :limit => 100
t.string :subject, :limit => 100
t.text :content
t.integer :duration
t.datetime :send_at
t.datetime :last_sent_at
t.timestamps
end
end

def self.down
drop_table :queued_emails
end
end

model:

class QueuedEmail < ActiveRecord::Base
validates_presence_of :from
validates_length_of :from, :within => 6..100

validates_presence_of :recipients
validates_length_of :recipients, :within => 6..100

def recurring?
!self.duration.blank?
end

def deliver!
puts "Sending email: #{self.inspect}"
UserMailer.deliver_custom_mail self.attributes
if recurring?
puts "set sent at: #{Time.now.utc}"
self.update_attribute :last_sent_at, Time.now.utc
else
puts "destroy"
self.destroy
end
end

def self.deliver_all
mails = self.all :conditions => ["(send_at <= :current_time) AND (duration IS NULL OR duration = \"\" OR last_sent_at IS NULL OR TIME_TO_SEC(TIMEDIFF(:current_time, last_sent_at)) >= duration)", {:current_time => Time.now.utc}]
mails.each do |mail|
mail.deliver!
end
end
end

cara kerja:
- duration: bila diisi, setelah email dikirim, email tidak akan langsung dihapus, tetapi akan dikirimkan kembali setiap duration detik
- send_at: menentukan tanggal dan waktu email akan dikirim atau dikirim pertama kalinya bila duration ditentukan.
- last_sent_at: tidak perlu diisi, digunakan secara internal untuk email berkala
- lainnya: rasanya sudah cukup jelas, sama seperti actionmailer

Tambahkan custom_mail di mailer 

Saia menggunakan UserMailer < ActionMailer::Base untuk mengirimkan email, tambahkan kode berikut di dalamnya:

def custom_mail(options)
@recipients = options["recipients"]
@from = options["from"]
@subject = options["subject"]
@sent_on = Time.now
@body[:content] = options["content"]
end

dan buat custom_mail.erb di viewnya:

<%= @content %>


Contoh penggunaan

Misalkan untuk mengirimkan survey email 2 hari setelah user register. Tambahkan kode berikut di users_controller create:

# ....
# bila registrasi sukses daftarkan email untuk dikirim setelah 2 hari
QueuedEmail.create(
:send_at => 2.days.since(Time.now.utc),
:from => "\"Site Keren\" ",
:recipients => "#{@user.email}",
:subject => "Survey supaya keren" ,
:content => "Halo #{@user.name}, isi survey berikut supaya kamu makin keren gitu loh ....."
)
# ....

untuk email berkala, tentukan duration dalam detik, misal email yg dikirim tiap hari

:duration => 24.hours


Dan segitu saja, mungkin bukan implementasi yg terbaik, tp paling tidak bisa digunakan hehe..

Kamis, 14 Januari 2010

Hello World!!!

Hello World | Halo Dunia