require 'active_record'
module Multiup
module Acts #:nodoc:
module Slugable #:nodoc:
def self.append_features(base)
super
base.extend(ClassMethods)
end
module ClassMethods
# Generates a URL slug based on provided fields and adds after_validation callbacks.
#
# class Page < ActiveRecord::Base
# acts_as_slugable :source_column => :title, :target_column => :url_slug, :scope => :parent
# end
#
# Configuration options:
# * source_column - specifies the column name used to generate the URL slug
# * slug_column - specifies the column name used to store the URL slug
# * scope - Given a symbol, it'll attach "_id" and use that as the foreign key
# restriction. It's also possible to give it an entire string that is interpolated if
# you need a tighter scope than just a foreign key.
def acts_as_slugable(options = {})
configuration = { :source_column => 'name', :slug_column => 'url_slug', :scope => nil}
configuration.update(options) if options.is_a?(Hash)
configuration[:scope] = "#{configuration[:scope]}_id".intern if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/
if configuration[:scope].is_a?(Symbol)
scope_condition_method = %(
def slug_scope_condition
if #{configuration[:scope].to_s}.nil?
"#{configuration[:scope].to_s} IS NULL"
else
"#{configuration[:scope].to_s} = \#{#{configuration[:scope].to_s}}"
end
end
)
elsif configuration[:scope].nil?
scope_condition_method = "def slug_scope_condition() \"1 = 1\" end"
else
scope_condition_method = "def slug_scope_condition() \"#{configuration[:scope]}\" end"
end
class_eval <<-EOV
include Multiup::Acts::Slugable::InstanceMethods
def acts_as_slugable_class
::#{self.name}
end
def source_column
"#{configuration[:source_column]}"
end
def slug_column
"#{configuration[:slug_column]}"
end
#{scope_condition_method}
after_validation :create_slug
EOV
end
end
# Adds instance methods.
module InstanceMethods
private
# URL slug creation logic
#
# The steps are roughly as follows
# 1. If the record hasn't passed its validations, exit immediately
# 2. If the source_column is empty, exit immediately (no error is thrown - this should be checked with your own validation)
# 3. If the url_slug is already set we have nothing to do, otherwise
# a. Strip out punctuation
# b. Replace unusable characters with dashes
# c. Clean up any doubled up dashes
# d. Check if the slug is unique and, if not, append a number until it is
# e. Save the URL slug
def create_slug
return if self.errors.length > 0
if self[source_column].nil? or self[source_column].empty?
return
end
if self[slug_column].to_s.empty?
test_string = self[source_column]
#strip out common punctuation
proposed_slug = test_string.strip.downcase.gsub(/[\'\"\#\$\,\.\!\?\%\@\(\)]+/, '')
#replace ampersand chars with 'and'
proposed_slug = proposed_slug.gsub(/&/, 'and')
#replace non-word chars with dashes
proposed_slug = proposed_slug.gsub(/[\W^-_]+/, '-')
#remove double dashes
proposed_slug = proposed_slug.gsub(/\-{2}/, '-')
suffix = ""
existing = true
acts_as_slugable_class.transaction do
while existing != nil
# look for records with the same url slug and increment a counter until we find a unique slug
existing = acts_as_slugable_class.find(:first, :conditions => ["#{slug_column} = ? and #{slug_scope_condition}", proposed_slug + suffix])
if existing
if suffix.empty?
suffix = "-0"
else
suffix.succ!
end
end
end
end # end of transaction
self[slug_column] = proposed_slug + suffix
end
end
end
end
end
end
ActiveRecord::Base.class_eval do
include Multiup::Acts::Slugable
end