Skip to content

Commit

Permalink
Added conditional filters #431 [Marcel]
Browse files Browse the repository at this point in the history
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@354 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
  • Loading branch information
dhh committed Jan 9, 2005
1 parent c836832 commit 677d922
Show file tree
Hide file tree
Showing 3 changed files with 213 additions and 16 deletions.
12 changes: 12 additions & 0 deletions actionpack/CHANGELOG
@@ -1,5 +1,17 @@
*SVN* *SVN*


* Added conditional filters #431 [Marcel]. Example:

class JournalController < ActionController::Base
# only require authentication if the current action is edit or delete
before_filter :authorize, :only_on => [ :edit, :delete ]

private
def authorize
# redirect to login unless authenticated
end
end

* Added authentication framework to protect actions behind a condition and redirect on failure. See ActionController::Authentication for more. * Added authentication framework to protect actions behind a condition and redirect on failure. See ActionController::Authentication for more.


* Added Base#render_nothing as a cleaner way of doing render_text "" when you're not interested in returning anything but an empty response. * Added Base#render_nothing as a cleaner way of doing render_text "" when you're not interested in returning anything but an empty response.
Expand Down
100 changes: 85 additions & 15 deletions actionpack/lib/action_controller/filters.rb
Expand Up @@ -126,18 +126,46 @@ def self.append_features(base)
# report_result # report_result
# end # end
# end # end
#
# == Filter conditions
#
# Filters can be limited to run for only specific actions. This can be expressed either by listing the actions to
# exclude or the actions to include when executing the filter. Available conditions are +:only+ or +:except+, both
# of which accept an arbitrary number of method references. For example:
#
# class Journal < ActionController::Base
# # only require authentication if the current action is edit or delete
# before_filter :authorize, :only => [ :edit, :delete ]
#
# private
# def authorize
# # redirect to login unless authenticated
# end
# end
#
# When setting conditions on inline method (proc) filters the condition must come first and be placed in parenthesis.
#
# class UserPreferences < ActionController::Base
# before_filter(:except => :new) { # some proc ... }
# # ...
# end
#
module ClassMethods module ClassMethods
# The passed <tt>filters</tt> will be appended to the array of filters that's run _before_ actions # The passed <tt>filters</tt> will be appended to the array of filters that's run _before_ actions
# on this controller are performed. # on this controller are performed.
def append_before_filter(*filters, &block) def append_before_filter(*filters, &block)
conditions = extract_conditions!(filters)
filters << block if block_given? filters << block if block_given?
add_action_conditions(filters, conditions)
append_filter_to_chain("before", filters) append_filter_to_chain("before", filters)
end end


# The passed <tt>filters</tt> will be prepended to the array of filters that's run _before_ actions # The passed <tt>filters</tt> will be prepended to the array of filters that's run _before_ actions
# on this controller are performed. # on this controller are performed.
def prepend_before_filter(*filters, &block) def prepend_before_filter(*filters, &block)
conditions = extract_conditions!(filters)
filters << block if block_given? filters << block if block_given?
add_action_conditions(filters, conditions)
prepend_filter_to_chain("before", filters) prepend_filter_to_chain("before", filters)
end end


Expand All @@ -147,14 +175,18 @@ def prepend_before_filter(*filters, &block)
# The passed <tt>filters</tt> will be appended to the array of filters that's run _after_ actions # The passed <tt>filters</tt> will be appended to the array of filters that's run _after_ actions
# on this controller are performed. # on this controller are performed.
def append_after_filter(*filters, &block) def append_after_filter(*filters, &block)
conditions = extract_conditions!(filters)
filters << block if block_given? filters << block if block_given?
add_action_conditions(filters, conditions)
append_filter_to_chain("after", filters) append_filter_to_chain("after", filters)
end end


# The passed <tt>filters</tt> will be prepended to the array of filters that's run _after_ actions # The passed <tt>filters</tt> will be prepended to the array of filters that's run _after_ actions
# on this controller are performed. # on this controller are performed.
def prepend_after_filter(*filters, &block) def prepend_after_filter(*filters, &block)
conditions = extract_conditions!(filters)
filters << block if block_given? filters << block if block_given?
add_action_conditions(filters, conditions)
prepend_filter_to_chain("after", filters) prepend_filter_to_chain("after", filters)
end end


Expand All @@ -169,8 +201,8 @@ def prepend_after_filter(*filters, &block)
# A#before # A#before
# A#after # A#after
# B#after # B#after
def append_around_filter(filters) def append_around_filter(*filters)
for filter in [filters].flatten for filter in filters.flatten
ensure_filter_responds_to_before_and_after(filter) ensure_filter_responds_to_before_and_after(filter)
append_before_filter { |c| filter.before(c) } append_before_filter { |c| filter.before(c) }
prepend_after_filter { |c| filter.after(c) } prepend_after_filter { |c| filter.after(c) }
Expand All @@ -185,8 +217,8 @@ def append_around_filter(filters)
# B#before # B#before
# B#after # B#after
# A#after # A#after
def prepend_around_filter(filters) def prepend_around_filter(*filters)
for filter in [filters].flatten for filter in filters.flatten
ensure_filter_responds_to_before_and_after(filter) ensure_filter_responds_to_before_and_after(filter)
prepend_before_filter { |c| filter.before(c) } prepend_before_filter { |c| filter.before(c) }
append_after_filter { |c| filter.after(c) } append_after_filter { |c| filter.after(c) }
Expand All @@ -206,6 +238,16 @@ def after_filters #:nodoc:
read_inheritable_attribute("after_filters") read_inheritable_attribute("after_filters")
end end


# Returns a mapping between filters and the actions that may run them.
def included_actions #:nodoc:
read_inheritable_attribute("included_actions") || {}
end

# Returns a mapping between filters and actions that may not run them.
def excluded_actions #:nodoc:
read_inheritable_attribute("excluded_actions") || {}
end

private private
def append_filter_to_chain(condition, filters) def append_filter_to_chain(condition, filters)
write_inheritable_array("#{condition}_filters", filters) write_inheritable_array("#{condition}_filters", filters)
Expand All @@ -220,6 +262,22 @@ def ensure_filter_responds_to_before_and_after(filter)
raise ActionControllerError, "Filter object must respond to both before and after" raise ActionControllerError, "Filter object must respond to both before and after"
end end
end end

def extract_conditions!(filters)
return nil unless filters.last.is_a? Hash
filters.pop
end

def add_action_conditions(filters, conditions)
return unless conditions
included, excluded = conditions[:only], conditions[:except]
write_inheritable_hash("included_actions", condition_hash(filters, included)) && return if included
write_inheritable_hash("excluded_actions", condition_hash(filters, excluded)) if excluded
end

def condition_hash(filters, *actions)
filters.inject({}) {|hash, filter| hash.merge(filter => actions.flatten.map {|action| action.to_s})}
end
end end


module InstanceMethods # :nodoc: module InstanceMethods # :nodoc:
Expand Down Expand Up @@ -252,18 +310,21 @@ def after_action #:doc:
private private
def call_filters(filters) def call_filters(filters)
filters.each do |filter| filters.each do |filter|
if Symbol === filter next if action_exempted?(filter)
if self.send(filter) == false then return false end filter_result = case
elsif filter_block?(filter) when filter.is_a?(Symbol)
if filter.call(self) == false then return false end self.send(filter)
elsif filter_class?(filter) when filter_block?(filter)
if filter.filter(self) == false then return false end filter.call(self)
else when filter_class?(filter)
raise( filter.filter(self)
ActionControllerError, else
"Filters need to be either a symbol, proc/method, or class implementing a static filter method" raise(
) ActionControllerError,
"Filters need to be either a symbol, proc/method, or class implementing a static filter method"
)
end end
return false if filter_result == false
end end
end end


Expand All @@ -274,6 +335,15 @@ def filter_block?(filter)
def filter_class?(filter) def filter_class?(filter)
filter.respond_to?("filter") filter.respond_to?("filter")
end end

def action_exempted?(filter)
case
when self.class.included_actions[filter]
!self.class.included_actions[filter].include?(action_name)
when self.class.excluded_actions[filter]
self.class.excluded_actions[filter].include?(action_name)
end
end
end end
end end
end end
117 changes: 116 additions & 1 deletion actionpack/test/controller/filters_test.rb
Expand Up @@ -15,6 +15,74 @@ def ensure_login
end end
end end


class ConditionalFilterController < ActionController::Base
def show
render_text "ran action"
end

def another_action
render_text "ran action"
end

def show_without_filter
render_text "ran action without filter"
end

private
def ensure_login
@ran_filter ||= []
@ran_filter << "ensure_login"
end

def clean_up_tmp
@ran_filter ||= []
@ran_filter << "clean_up_tmp"
end

def rescue_action(e) raise(e) end
end

class ConditionalCollectionFilterController < ConditionalFilterController
before_filter :ensure_login, :except => [ :show_without_filter, :another_action ]
end

class OnlyConditionSymController < ConditionalFilterController
before_filter :ensure_login, :only => :show
end

class ExceptConditionSymController < ConditionalFilterController
before_filter :ensure_login, :except => :show_without_filter
end

class BeforeAndAfterConditionController < ConditionalFilterController
before_filter :ensure_login, :only => :show
after_filter :clean_up_tmp, :only => :show
end

class OnlyConditionProcController < ConditionalFilterController
before_filter(:only => :show) {|c| c.assigns["ran_proc_filter"] = true }
end

class ExceptConditionProcController < ConditionalFilterController
before_filter(:except => :show_without_filter) {|c| c.assigns["ran_proc_filter"] = true }
end

class ConditionalClassFilter
def self.filter(controller) controller.assigns["ran_class_filter"] = true end
end

class OnlyConditionClassController < ConditionalFilterController
before_filter ConditionalClassFilter, :only => :show
end

class ExceptConditionClassController < ConditionalFilterController
before_filter ConditionalClassFilter, :except => :show_without_filter
end

class AnomolousYetValidConditionController < ConditionalFilterController
before_filter(ConditionalClassFilter, :ensure_login, Proc.new {|c| c.assigns["ran_proc_filter1"] = true }, :except => :show_without_filter) { |c| c.assigns["ran_proc_filter2"] = true}
end

class PrependingController < TestController class PrependingController < TestController
prepend_before_filter :wonderful_life prepend_before_filter :wonderful_life


Expand Down Expand Up @@ -126,6 +194,53 @@ def test_running_filters_with_class
assert test_process(AuditController).template.assigns["was_audited"] assert test_process(AuditController).template.assigns["was_audited"]
end end


def test_running_anomolous_yet_valid_condition_filters
response = test_process(AnomolousYetValidConditionController)
assert_equal %w( ensure_login ), response.template.assigns["ran_filter"]
assert response.template.assigns["ran_class_filter"]
assert response.template.assigns["ran_proc_filter1"]
assert response.template.assigns["ran_proc_filter2"]

response = test_process(AnomolousYetValidConditionController, "show_without_filter")
assert_equal nil, response.template.assigns["ran_filter"]
assert !response.template.assigns["ran_class_filter"]
assert !response.template.assigns["ran_proc_filter1"]
assert !response.template.assigns["ran_proc_filter2"]
end

def test_running_collection_condition_filters
assert_equal %w( ensure_login ), test_process(ConditionalCollectionFilterController).template.assigns["ran_filter"]
assert_equal nil, test_process(ConditionalCollectionFilterController, "show_without_filter").template.assigns["ran_filter"]
assert_equal nil, test_process(ConditionalCollectionFilterController, "another_action").template.assigns["ran_filter"]
end

def test_running_only_condition_filters
assert_equal %w( ensure_login ), test_process(OnlyConditionSymController).template.assigns["ran_filter"]
assert_equal nil, test_process(OnlyConditionSymController, "show_without_filter").template.assigns["ran_filter"]

assert test_process(OnlyConditionProcController).template.assigns["ran_proc_filter"]
assert !test_process(OnlyConditionProcController, "show_without_filter").template.assigns["ran_proc_filter"]

assert test_process(OnlyConditionClassController).template.assigns["ran_class_filter"]
assert !test_process(OnlyConditionClassController, "show_without_filter").template.assigns["ran_class_filter"]
end

def test_running_except_condition_filters
assert_equal %w( ensure_login ), test_process(ExceptConditionSymController).template.assigns["ran_filter"]
assert_equal nil, test_process(ExceptConditionSymController, "show_without_filter").template.assigns["ran_filter"]

assert test_process(ExceptConditionProcController).template.assigns["ran_proc_filter"]
assert !test_process(ExceptConditionProcController, "show_without_filter").template.assigns["ran_proc_filter"]

assert test_process(ExceptConditionClassController).template.assigns["ran_class_filter"]
assert !test_process(ExceptConditionClassController, "show_without_filter").template.assigns["ran_class_filter"]
end

def test_running_before_and_after_condition_filters
assert_equal %w( ensure_login clean_up_tmp), test_process(BeforeAndAfterConditionController).template.assigns["ran_filter"]
assert_equal nil, test_process(BeforeAndAfterConditionController, "show_without_filter").template.assigns["ran_filter"]
end

def test_bad_filter def test_bad_filter
assert_raises(ActionController::ActionControllerError) { assert_raises(ActionController::ActionControllerError) {
test_process(BadFilterController) test_process(BadFilterController)
Expand Down Expand Up @@ -156,4 +271,4 @@ def test_process(controller, action = "show")
request.action = action request.action = action
controller.process(request, ActionController::TestResponse.new) controller.process(request, ActionController::TestResponse.new)
end end
end end

0 comments on commit 677d922

Please sign in to comment.