# encoding: UTF-8
module Stringex
module ActsAsUrl # :nodoc:
def self.included(base)
base.extend ClassMethods
end
class Configuration
attr_accessor :allow_slash, :allow_duplicates, :attribute_to_urlify, :duplicate_count_separator,
:exclude, :only_when_blank, :scope_for_url, :sync_url, :url_attribute, :url_limit
def initialize(klass, options = {})
self.allow_slash = options[:allow_slash]
self.allow_duplicates = options[:allow_duplicates]
self.attribute_to_urlify = options[:attribute]
self.duplicate_count_separator = options[:duplicate_count_separator] || "-"
self.exclude = options[:exclude] || []
self.only_when_blank = options[:only_when_blank]
self.scope_for_url = options[:scope]
self.sync_url = options[:sync_url]
self.url_attribute = options[:url_attribute] || "url"
self.url_limit = options[:limit]
end
def get_base_url!(instance)
base_url = instance.send(url_attribute)
if base_url.blank? || !only_when_blank
root = instance.send(attribute_to_urlify).to_s
base_url = root.to_url(:allow_slash => allow_slash, :limit => url_limit, :exclude => exclude)
end
instance.instance_variable_set "@acts_as_url_base_url", base_url
end
def get_conditions!(instance)
conditions = ["#{url_attribute} LIKE ?", instance.instance_variable_get("@acts_as_url_base_url") + '%']
unless instance.new_record?
conditions.first << " and id != ?"
conditions << instance.id
end
if scope_for_url
conditions.first << " and #{scope_for_url} = ?"
conditions << instance.send(scope_for_url)
end
conditions
end
def handle_duplicate_urls!(instance)
unless allow_duplicates
base_url = instance.instance_variable_get("@acts_as_url_base_url")
url_owners = instance.class.unscoped.find(:all, :conditions => get_conditions!(instance))
if url_owners.any?{|owner| owner.send(url_attribute) == base_url}
separator = duplicate_count_separator
n = 1
while url_owners.any?{|owner| owner.send(url_attribute) == "#{base_url}#{separator}#{n}"}
n = n.succ
end
instance.send :write_attribute, url_attribute, "#{base_url}#{separator}#{n}"
end
end
end
end
module ClassMethods # :doc:
# Creates a callback to automatically create an url-friendly representation
# of the attribute argument. Example:
#
# acts_as_url :title
#
# will use the string contents of the title attribute
# to create the permalink. Note: you can also use a non-database-backed
# method to supply the string contents for the permalink. Just use that method's name
# as the argument as you would an attribute.
#
# The default attribute acts_as_url uses to save the permalink is url
# but this can be changed in the options hash. Available options are:
#
# :allow_slash:: If true, allow the generated url to contain slashes. Default is false[y].
# :allow_duplicates:: If true, allow duplicate urls instead of appending numbers to
# differentiate between urls. Default is false[y].
# :duplicate_count_separator:: String to use when forcing unique urls from non-unique strings.
# Default is "-".
# :exclude_list:: List of complete strings that should not be transformed by acts_as_url.
# Default is empty.
# :only_when_blank:: If true, the url generation will only happen when :url_attribute is
# blank. Default is false[y] (meaning url generation will happen always).
# :scope:: The name of model attribute to scope unique urls to. There is no default here.
# :sync_url:: If set to true, the url field will be updated when changes are made to the
# attribute it is based on. Default is false[y].
# :url_attribute:: The name of the attribute to use for storing the generated url string.
# Default is :url.
# :url_limit:: The maximum size a generated url should be. Note: this does not
# include the characters needed to enforce uniqueness on duplicate urls.
# Default is nil.
def acts_as_url(attribute, options = {})
cattr_accessor :acts_as_url_configuration
options[:attribute] = attribute
self.acts_as_url_configuration = ActsAsUrl::Configuration.new(self, options)
if acts_as_url_configuration.sync_url
before_validation(:ensure_unique_url)
else
if defined?(ActiveModel::Callbacks)
before_validation(:ensure_unique_url, :on => :create)
else
before_validation_on_create(:ensure_unique_url)
end
end
class_eval <<-"END"
def #{acts_as_url_configuration.url_attribute}
if !new_record? && errors[acts_as_url_configuration.attribute_to_urlify].present?
self.class.find(id).send(acts_as_url_configuration.url_attribute)
else
read_attribute(acts_as_url_configuration.url_attribute)
end
end
END
end
# Initialize the url fields for the records that need it. Designed for people who add
# acts_as_url support once there's already development/production data they'd
# like to keep around.
#
# Note: This method can get very expensive, very fast. If you're planning on using this
# on a large selection, you will get much better results writing your own version with
# using pagination.
def initialize_urls
find_each(:conditions => {acts_as_url_configuration.url_attribute => nil}) do |instance|
instance.send :ensure_unique_url
instance.save
end
end
end
private
def ensure_unique_url
# Just to save some typing
config = acts_as_url_configuration
url_attribute = config.url_attribute
config.get_base_url! self
write_attribute url_attribute, @acts_as_url_base_url
config.handle_duplicate_urls!(self) unless config.allow_duplicates
end
end
end