module InheritedResources
module ClassMethods
protected
# Used to overwrite the default assumptions InheritedResources do. Whenever
# this method is called, it should be on the top of your controller, since
# almost other methods depends on the values given to <>defaults.
#
# == Options
#
# * :resource_class - The resource class which by default is guessed
# by the controller name. Defaults to Project in
# ProjectsController.
#
# * :collection_name - The name of the collection instance variable which
# is set on the index action. Defaults to :projects in
# ProjectsController.
#
# * :instance_name - The name of the singular instance variable which
# is set on all actions besides index action. Defaults to
# :project in ProjectsController.
#
# * :route_collection_name - The name of the collection route. Defaults to :collection_name.
#
# * :route_instance_name - The name of the singular route. Defaults to :instance_name.
#
# * :route_prefix - The route prefix which is automically set in namespaced
# controllers. Default to :admin on Admin::ProjectsController.
#
# * :singleton - Tells if this controller is singleton or not.
#
# * :finder - Specifies which method should be called to instantiate the resource.
#
# defaults :project, :finder => :find_by_slug
#
def defaults(options)
raise ArgumentError, 'Class method :defaults expects a hash of options.' unless options.is_a? Hash
options.symbolize_keys!
options.assert_valid_keys(:resource_class, :collection_name, :instance_name,
:class_name, :route_prefix, :route_collection_name,
:route_instance_name, :singleton, :finder)
self.resource_class = options[:resource_class] if options.key?(:resource_class)
self.resource_class = options[:class_name].constantize if options.key?(:class_name)
acts_as_singleton! if options.delete(:singleton)
config = self.resources_configuration[:self]
config[:route_prefix] = options.delete(:route_prefix) if options.key?(:route_prefix)
if options.key?(:resource_class) or options.key?(:class_name)
config[:request_name] = self.resource_class.to_s.underscore.gsub('/', '_')
options.delete(:resource_class) and options.delete(:class_name)
end
options.each do |key, value|
config[key] = value.to_sym
end
create_resources_url_helpers!
end
# Defines wich actions to keep from the inherited controller.
# Syntax is borrowed from resource_controller.
#
# actions :index, :show, :edit
# actions :all, :except => :index
#
def actions(*actions_to_keep)
raise ArgumentError, 'Wrong number of arguments. You have to provide which actions you want to keep.' if actions_to_keep.empty?
options = actions_to_keep.extract_options!
actions_to_remove = Array(options[:except])
actions_to_remove += ACTIONS - actions_to_keep.map { |a| a.to_sym } unless actions_to_keep.first == :all
actions_to_remove.map! { |a| a.to_sym }.uniq!
(instance_methods.map { |m| m.to_sym } & actions_to_remove).each do |action|
undef_method action, "#{action}!"
end
end
# Defines that this controller belongs to another resource.
#
# belongs_to :projects
#
# == Options
#
# * :parent_class - Allows you to specify what is the parent class.
#
# belongs_to :project, :parent_class => AdminProject
#
# * :class_name - Also allows you to specify the parent class, but you should
# give a string. Added for ActiveRecord belongs to compatibility.
#
# * :instance_name - The instance variable name. By default is the name of the association.
#
# belongs_to :project, :instance_name => :my_project
#
# * :finder - Specifies which method should be called to instantiate the parent.
#
# belongs_to :project, :finder => :find_by_title!
#
# This will make your projects be instantiated as:
#
# Project.find_by_title!(params[:project_id])
#
# Instead of:
#
# Project.find(params[:project_id])
#
# * :param - Allows you to specify params key to retrieve the id.
# Default is :association_id, which in this case is :project_id.
#
# * :route_name - Allows you to specify what is the route name in your url
# helper. By default is association name.
#
# * :collection_name - Tell how to retrieve the next collection. Let's
# suppose you have Tasks which belongs to Projects
# which belongs to companies. This will do somewhere
# down the road:
#
# @company.projects
#
# But if you want to retrieve instead:
#
# @company.admin_projects
#
# You supply the collection name.
#
# * :polymorphic - Tell the association is polymorphic.
#
# * :singleton - Tell it's a singleton association.
#
# * :optional - Tell the association is optional (it's a special
# type of polymorphic association)
#
def belongs_to(*symbols, &block)
options = symbols.extract_options!
options.symbolize_keys!
options.assert_valid_keys(:class_name, :parent_class, :instance_name, :param,
:finder, :route_name, :collection_name, :singleton,
:polymorphic, :optional, :shallow)
optional = options.delete(:optional)
shallow = options.delete(:shallow)
singleton = options.delete(:singleton)
polymorphic = options.delete(:polymorphic)
finder = options.delete(:finder)
include BelongsToHelpers if self.parents_symbols.empty?
acts_as_singleton! if singleton
acts_as_polymorphic! if polymorphic || optional
acts_as_shallow! if shallow
raise ArgumentError, 'You have to give me at least one association name.' if symbols.empty?
raise ArgumentError, 'You cannot define multiple associations with options: #{options.keys.inspect} to belongs to.' unless symbols.size == 1 || options.empty?
symbols.each do |symbol|
symbol = symbol.to_sym
if polymorphic || optional
self.parents_symbols << :polymorphic unless self.parents_symbols.include?(:polymorphic)
self.resources_configuration[:polymorphic][:symbols] << symbol
self.resources_configuration[:polymorphic][:optional] ||= optional
else
self.parents_symbols << symbol
end
self.resources_configuration[:self][:shallow] = true if shallow
config = self.resources_configuration[symbol] = {}
config[:parent_class] = options.delete(:parent_class) || begin
class_name = (options.delete(:class_name) || symbol).to_s.pluralize.classify
class_name.constantize
rescue NameError => e
raise unless e.message.include?(class_name)
nil
end
config[:collection_name] = options.delete(:collection_name) || symbol.to_s.pluralize.to_sym
config[:instance_name] = options.delete(:instance_name) || symbol
config[:param] = options.delete(:param) || :"#{symbol}_id"
config[:route_name] = options.delete(:route_name) || symbol
config[:finder] = finder || :find
end
if block_given?
class_eval(&block)
else
create_resources_url_helpers!
end
helper_method :parent, :parent?
end
alias :nested_belongs_to :belongs_to
# A quick method to declare polymorphic belongs to.
#
def polymorphic_belongs_to(*symbols, &block)
options = symbols.extract_options!
options.merge!(:polymorphic => true)
belongs_to(*symbols << options, &block)
end
# A quick method to declare singleton belongs to.
#
def singleton_belongs_to(*symbols, &block)
options = symbols.extract_options!
options.merge!(:singleton => true)
belongs_to(*symbols << options, &block)
end
# A quick method to declare optional belongs to.
#
def optional_belongs_to(*symbols, &block)
options = symbols.extract_options!
options.merge!(:optional => true)
belongs_to(*symbols << options, &block)
end
# Defines custom restful actions by resource or collection basis.
#
# custom_actions :resource => [:delete, :transit], :collection => :search
#
# == Options
#
# * :resource - Allows you to specify resource actions.
# custom_actions :resource => :delete
# This macro creates 'delete' method in controller and defines
# delete_reource_{path,url} helpers. The body of generated 'delete'
# method is same as 'show' method. So you can override it if need
#
# * :collection - Allows you to specify collection actions.
# custom_actions :collection => :search
# This macro creates 'search' method in controller and defines
# search_reources_{path,url} helpers. The body of generated 'search'
# method is same as 'index' method. So you can override it if need
#
def custom_actions(options)
self.resources_configuration[:self][:custom_actions] = options
options.each do | resource_or_collection, actions |
[*actions].each do | action |
create_custom_action(resource_or_collection, action)
end
end
create_resources_url_helpers!
[*options[:resource]].each do | action |
helper_method "#{action}_resource_path", "#{action}_resource_url"
end
[*options[:collection]].each do | action |
helper_method "#{action}_resources_path", "#{action}_resources_url"
end
end
# Defines the role to use when creating or updating resource.
# Makes sense when using rails 3.1 mass assignment conventions
def with_role(role)
self.resources_configuration[:self][:role] = role.try(:to_sym)
end
private
def acts_as_singleton! #:nodoc:
unless self.resources_configuration[:self][:singleton]
self.resources_configuration[:self][:singleton] = true
include SingletonHelpers
actions :all, :except => :index
end
end
def acts_as_polymorphic! #:nodoc:
unless self.parents_symbols.include?(:polymorphic)
include PolymorphicHelpers
helper_method :parent_type, :parent_class
end
end
def acts_as_shallow! #:nodoc:
include BelongsToHelpers
include ShallowHelpers
end
# Initialize resources class accessors and set their default values.
#
def initialize_resources_class_accessors! #:nodoc:
# First priority is the namespaced model, e.g. User::Group
self.resource_class ||= begin
namespaced_class = self.name.sub(/Controller/, '').singularize
namespaced_class.constantize
rescue NameError
nil
end
# Second priority is the top namespace model, e.g. EngineName::Article for EngineName::Admin::ArticlesController
self.resource_class ||= begin
namespaced_classes = self.name.sub(/Controller/, '').split('::')
namespaced_class = [namespaced_classes.first, namespaced_classes.last].join('::').singularize
namespaced_class.constantize
rescue NameError
nil
end
# Third priority the camelcased c, i.e. UserGroup
self.resource_class ||= begin
camelcased_class = self.name.sub(/Controller/, '').gsub('::', '').singularize
camelcased_class.constantize
rescue NameError
nil
end
# Otherwise use the Group class, or fail
self.resource_class ||= begin
class_name = self.controller_name.classify
class_name.constantize
rescue NameError => e
raise unless e.message.include?(class_name)
nil
end
self.parents_symbols = self.parents_symbols.try(:dup) || []
# Initialize resources configuration hash
self.resources_configuration = self.resources_configuration.try(:dup) || {}
self.resources_configuration.each do |key, value|
next unless value.is_a?(Hash) || value.is_a?(Array)
self.resources_configuration[key] = value.dup
end
config = (self.resources_configuration[:self] ||= {})
config[:collection_name] = self.controller_name.to_sym
config[:instance_name] = self.controller_name.singularize.to_sym
config[:route_collection_name] = config[:collection_name]
config[:route_instance_name] = config[:instance_name]
# Deal with namespaced controllers
namespaces = self.controller_path.split('/')[0..-2]
config[:route_prefix] = namespaces.join('_') unless namespaces.empty?
# Deal with default request parameters in namespaced controllers, e.g.
# Forum::Thread#create will properly pick up the request parameter
# which will be forum_thread, and not thread
# Additionally make this work orthogonally with instance_name
config[:request_name] = self.resource_class.to_s.underscore.gsub('/', '_')
# Initialize polymorphic, singleton, scopes and belongs_to parameters
polymorphic = self.resources_configuration[:polymorphic] || { :symbols => [], :optional => false }
polymorphic[:symbols] = polymorphic[:symbols].dup
self.resources_configuration[:polymorphic] = polymorphic
end
def create_custom_action(resource_or_collection, action)
class_eval <<-CUSTOM_ACTION, __FILE__, __LINE__
def #{action}(options={}, &block)
respond_with(*(with_chain(#{resource_or_collection}) << options), &block)
end
alias :#{action}! :#{action}
protected :#{action}!
CUSTOM_ACTION
end
# Hook called on inheritance.
#
def inherited(base) #:nodoc:
super(base)
base.send :initialize_resources_class_accessors!
base.send :create_resources_url_helpers!
end
end
end