diff --git a/lib/action_controller/station.rb b/lib/action_controller/station.rb index 0b37ef2..f34bd16 100644 --- a/lib/action_controller/station.rb +++ b/lib/action_controller/station.rb @@ -47,16 +47,29 @@ def model_class self.class.model_class end - # Obtains a given ActiveRecord from parameters. + # Obtains a given ActiveRecord instance from parameters. + # + # Returns the first of records_from_path. Same options. + def record_from_path(options = {}) + records_from_path(options).first + end + + # Obtains all ActiveRecord record in parameters. + # + # Given this URI: + # /projects/1/tasks/2/posts/3 + # + # records_from_path #=> [ Project-1, Task-2 ] + # # Options: # * acts_as: the ActiveRecord model must acts_as the given symbol. # acts_as => :container - def record_from_path(options = {}) + def records_from_path(options = {}) acts_as_module = "ActiveRecord::#{ options[:acts_as].to_s.classify }".constantize if options[:acts_as] candidates = params.keys.select{ |k| k[-3..-1] == '_id' } - candidates.each do |candidate_key| + candidates.map { |candidate_key| # Filter keys that correspond to classes begin candidate_class = candidate_key[0..-4].to_sym.to_class @@ -73,32 +86,38 @@ def record_from_path(options = {}) # Find record begin - record = candidate_class.find_with_param(params[candidate_key]) - instance_variable_set("@#{ options[:acts_as] }", record) if options[:acts_as] - instance_variable_set("@#{ candidate_class.to_s.underscore }", record) - return record + candidate_class.find_with_param(params[candidate_key]) rescue ::ActiveRecord::RecordNotFound next end - end - - nil + }.compact end + # An instance modeling site configuration and stage def current_site @current_site ||= Site.current end - # Find current Container using path from the request - def path_container - @path_container ||= record_from_path(:acts_as => :container) + # Find all Containers in the path, using records_from_path + # + # Options: + # ancestors:: include containers's containers + # type:: the class of the searched containers + def path_containers(options = {}) + @path_containers ||= records_from_path(:acts_as => :container) + + candidates = options[:ancestors] ? + @path_containers.map{ |c| c.container_and_ancestors }.flatten.uniq : + @path_containers.dup + + filter_type(candidates, options[:type]) end # Find current Container using path from the request # - # Note that in StationResouces this method is redefined priorizing the resource - def current_container - path_container + # Uses path_containers. Same options. + def path_container(options = {}) + path_containers(options).first end # Must find a Container @@ -106,18 +125,32 @@ def current_container # Calls path_container to figure out from params. If unsuccesful, # raises ActiveRecord::RecordNotFound # - def path_container! - path_container || raise(ActiveRecord::RecordNotFound) + def path_container!(options = {}) + path_container(options) || raise(ActiveRecord::RecordNotFound) end + # Find current Container using path from the request + # + # Note that in StationResouces this method is redefined looking also at the resource + # + # Options: + # type:: the class the container should be + # path_ancestors:: include the ancestors of the resources in the path + def current_container(options = {}) + options[:ancestors] = options.delete(:path_ancestors) + + path_container(options) + end # Must find a Container # # Calls current_container to figure out from params. If unsuccesful, # raises ActiveRecord::RecordNotFound + # + # Takes the same options as current_container # - def current_container! - current_container || raise(ActiveRecord::RecordNotFound) + def current_container!(options = {}) + current_container(options) || raise(ActiveRecord::RecordNotFound) end protected @@ -142,5 +175,15 @@ def set_params_from_raw_post(content = controller_name.singularize.to_sym) params[content][:media] ||= file params[content][:public_read] ||= true end + + private + + def filter_type(candidates, type) #:nodoc: + return candidates.dup unless type + + type = type.to_sym.to_class + + candidates.select{ |c| c.is_a?(type) } + end end end diff --git a/lib/action_controller/station_resources.rb b/lib/action_controller/station_resources.rb index c7ba38d..1b8ec70 100644 --- a/lib/action_controller/station_resources.rb +++ b/lib/action_controller/station_resources.rb @@ -2,6 +2,9 @@ module ActionController #:nodoc: # Controller methods for Resources # module StationResources + class ContainerError < ::StandardError #:nodoc: + end + class << self def included(base) #:nodoc: base.send :include, ActionController::Station unless base.ancestors.include?(ActionController::Station) @@ -11,6 +14,9 @@ def included(base) #:nodoc: alias_method controller_name.singularize, :resource # alias_method :article, :resource helper_method controller_name.singularize # helper_method :article end # end + + base.send :rescue_from, ContainerError, :with => :container_error + base.send :include, ActionController::Authorization unless base.ancestors.include?(ActionController::Authorization) end end @@ -258,18 +264,52 @@ def resources model_class.roots.in(path_container).column_sort(params[:order], params[:direction]).paginate(:page => params[:page], :conditions => @conditions) end - # Find current Container prioritizing the resource (if it exists), or its container. + # Search in the resource's containers and in the path for current Container. + # + # We start with the following assumptions: + # * Resources have a unique branch of nested containers in the containers tree + # * If we find containers of the same type in both branches (resource containers + # and path), they must be the same. If they aren't we assume a routing error (409 Conflict). # - # Defaults to path_container - def current_container - @current_container ||= - resource && ( resource.class.acts_as?(:container) && resource || - resource.respond_to?(:container) && resource.container ) || - path_container + # Options: + # type:: the class of the container + def current_container(options = {}) + find_current_container(options) end private + def find_current_container(options) #:nodoc: + rc = resource_container(options) + + path_options = options.dup + path_options[:ancestors] = path_options.delete(:path_ancestors) + pc = path_container(path_options) + + if rc.present? && pc.present? && + rc != pc + raise ContainerError + end + + rc || pc + end + + # Gets a container from the resource containers + def resource_container(options = {}) #:nodoc: + return nil unless resource.present? + + @resource_container_candidates ||= + Array( resource.class.acts_as?(:container) ? + resource.container_and_ancestors : + resource.class.acts_as?(:content) ? + resource.container.try(:container_and_ancestors) : + nil ).compact + + candidates = filter_type(@resource_container_candidates, options[:type]) + + candidates.first + end + # Redirect here after create if everythig went well def after_create_with_success redirect_to @resource @@ -304,5 +344,10 @@ def after_destroy_with_success def after_destroy_with_errors redirect_to(request.referer || [ path_container, model_class.new ]) end + + def container_error(e) #:nodoc: + render :text => 'Container route conflicts with resource container', + :status => 409 + end end end diff --git a/lib/active_record/container.rb b/lib/active_record/container.rb index 8c1f10a..faf7192 100644 --- a/lib/active_record/container.rb +++ b/lib/active_record/container.rb @@ -50,6 +50,14 @@ def contents(options = {}) ActiveRecord::Content::Inquirer.all(options, container_options) end + + # A list of all the nested containers of this Container, including self, + # sorted by closeness + def container_and_ancestors + ca = respond_to?(:container) && container.try(:container_and_ancestors) || nil + + (Array(self) + Array(ca)).compact + end end end end