diff options
Diffstat (limited to 'actionpack/lib/action_controller/abstract')
8 files changed, 337 insertions, 113 deletions
diff --git a/actionpack/lib/action_controller/abstract/base.rb b/actionpack/lib/action_controller/abstract/base.rb index ade7719cc0..87083a4d79 100644 --- a/actionpack/lib/action_controller/abstract/base.rb +++ b/actionpack/lib/action_controller/abstract/base.rb @@ -1,41 +1,115 @@ +require 'active_support/core_ext/module/attr_internal' + module AbstractController + class Error < StandardError; end + + class DoubleRenderError < Error + DEFAULT_MESSAGE = "Render and/or redirect were called multiple times in this action. Please note that you may only call render OR redirect, and at most once per action. Also note that neither redirect nor render terminate execution of the action, so if you want to exit an action after redirecting, you need to do something like \"redirect_to(...) and return\"." + + def initialize(message = nil) + super(message || DEFAULT_MESSAGE) + end + end + class Base - attr_internal :response_body attr_internal :response_obj attr_internal :action_name - - def self.process(action) - new.process(action) - end - - def self.inherited(klass) + + class << self + attr_reader :abstract + + def abstract! + @abstract = true + end + + alias_method :abstract?, :abstract + + def inherited(klass) + ::AbstractController::Base.subclasses << klass.to_s + super + end + + def subclasses + @subclasses ||= [] + end + + def internal_methods + controller = self + controller = controller.superclass until controller.abstract? + controller.public_instance_methods(true) + end + + def process(action) + new.process(action.to_s) + end + + def hidden_actions + [] + end + + def action_methods + @action_methods ||= + # All public instance methods of this class, including ancestors + public_instance_methods(true).map { |m| m.to_s }.to_set - + # Except for public instance methods of Base and its ancestors + internal_methods.map { |m| m.to_s } + + # Be sure to include shadowed public instance methods of this class + public_instance_methods(false).map { |m| m.to_s } - + # And always exclude explicitly hidden actions + hidden_actions + end end - + + abstract! + def initialize self.response_obj = {} end - - def process(action_name) - unless respond_to_action?(action_name) - raise ActionNotFound, "The action '#{action_name}' could not be found" + + def process(action) + @_action_name = action_name = action.to_s + + unless action_name = method_for_action(action_name) + raise ActionNotFound, "The action '#{action}' could not be found" end - - @_action_name = action_name - process_action - self.response_obj[:body] = self.response_body + + process_action(action_name) self end - + private - - def process_action - respond_to?(action_name) ? send(action_name) : send(:action_missing, action_name) + def action_methods + self.class.action_methods + end + + def action_method?(action) + action_methods.include?(action) end - - def respond_to_action?(action_name) - respond_to?(action_name) || respond_to?(:action_missing, true) + + # It is possible for respond_to?(action_name) to be false and + # respond_to?(:action_missing) to be false if respond_to_action? + # is overridden in a subclass. For instance, ActionController::Base + # overrides it to include the case where a template matching the + # action_name is found. + def process_action(method_name) + send_action(method_name) + end + + alias send_action send + + def _handle_action_missing + action_missing(@_action_name) + end + + # Override this to change the conditions that will raise an + # ActionNotFound error. If you accept a difference case, + # you must handle it by also overriding process_action and + # handling the case. + def method_for_action(action_name) + if action_method?(action_name) then action_name + elsif respond_to?(:action_missing, true) then "_handle_action_missing" + end end - end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_controller/abstract/benchmarker.rb b/actionpack/lib/action_controller/abstract/benchmarker.rb new file mode 100644 index 0000000000..6999329144 --- /dev/null +++ b/actionpack/lib/action_controller/abstract/benchmarker.rb @@ -0,0 +1,28 @@ +module AbstractController + module Benchmarker + extend ActiveSupport::Concern + + depends_on Logger + + module ClassMethods + def benchmark(title, log_level = ::Logger::DEBUG, use_silence = true) + if logger && logger.level >= log_level + result = nil + ms = Benchmark.ms { result = use_silence ? silence { yield } : yield } + logger.add(log_level, "#{title} (#{('%.1f' % ms)}ms)") + result + else + yield + end + end + + # Silences the logger for the duration of the block. + def silence + old_logger_level, logger.level = logger.level, ::Logger::ERROR if logger + yield + ensure + logger.level = old_logger_level if logger + end + end + end +end diff --git a/actionpack/lib/action_controller/abstract/callbacks.rb b/actionpack/lib/action_controller/abstract/callbacks.rb index c8b509081c..6c67315c58 100644 --- a/actionpack/lib/action_controller/abstract/callbacks.rb +++ b/actionpack/lib/action_controller/abstract/callbacks.rb @@ -1,28 +1,31 @@ module AbstractController module Callbacks - setup do - include ActiveSupport::NewCallbacks - define_callbacks :process_action + extend ActiveSupport::Concern + + depends_on ActiveSupport::NewCallbacks + + included do + define_callbacks :process_action, "response_body" end - - def process_action - _run_process_action_callbacks(action_name) do + + def process_action(method_name) + _run_process_action_callbacks(method_name) do super end end - + module ClassMethods def _normalize_callback_options(options) if only = options[:only] - only = Array(only).map {|o| "action_name == :#{o}"}.join(" || ") + only = Array(only).map {|o| "action_name == '#{o}'"}.join(" || ") options[:per_key] = {:if => only} end if except = options[:except] - except = Array(except).map {|e| "action_name == :#{e}"}.join(" || ") + except = Array(except).map {|e| "action_name == '#{e}'"}.join(" || ") options[:per_key] = {:unless => except} end end - + [:before, :after, :around].each do |filter| class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 def #{filter}_filter(*names, &blk) @@ -33,8 +36,28 @@ module AbstractController process_action_callback(:#{filter}, name, options) end end + + def prepend_#{filter}_filter(*names, &blk) + options = names.last.is_a?(Hash) ? names.pop : {} + _normalize_callback_options(options) + names.push(blk) if block_given? + names.each do |name| + process_action_callback(:#{filter}, name, options.merge(:prepend => true)) + end + end + + def skip_#{filter}_filter(*names, &blk) + options = names.last.is_a?(Hash) ? names.pop : {} + _normalize_callback_options(options) + names.push(blk) if block_given? + names.each do |name| + skip_process_action_callback(:#{filter}, name, options) + end + end + + alias_method :append_#{filter}_filter, :#{filter}_filter RUBY_EVAL end end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_controller/abstract/exceptions.rb b/actionpack/lib/action_controller/abstract/exceptions.rb index ec4680629b..2f6c55f068 100644 --- a/actionpack/lib/action_controller/abstract/exceptions.rb +++ b/actionpack/lib/action_controller/abstract/exceptions.rb @@ -1,3 +1,3 @@ module AbstractController - class ActionNotFound < StandardError ; end -end
\ No newline at end of file + class ActionNotFound < StandardError; end +end diff --git a/actionpack/lib/action_controller/abstract/helpers.rb b/actionpack/lib/action_controller/abstract/helpers.rb index 1f0b38417b..43832f1e2d 100644 --- a/actionpack/lib/action_controller/abstract/helpers.rb +++ b/actionpack/lib/action_controller/abstract/helpers.rb @@ -1,19 +1,14 @@ module AbstractController module Helpers + extend ActiveSupport::Concern + depends_on Renderer - - setup do + + included do extlib_inheritable_accessor :master_helper_module self.master_helper_module = Module.new end - - # def self.included(klass) - # klass.class_eval do - # extlib_inheritable_accessor :master_helper_module - # self.master_helper_module = Module.new - # end - # end - + def _action_view @_action_view ||= begin av = super @@ -21,19 +16,38 @@ module AbstractController av end end - + module ClassMethods def inherited(klass) klass.master_helper_module = Module.new klass.master_helper_module.__send__ :include, master_helper_module - + super end - + + # Makes all the (instance) methods in the helper module available to templates rendered through this controller. + # See ActionView::Helpers (link:classes/ActionView/Helpers.html) for more about making your own helper modules + # available to the templates. def add_template_helper(mod) master_helper_module.module_eval { include mod } end - + + # Declare a controller method as a helper. For example, the following + # makes the +current_user+ controller method available to the view: + # class ApplicationController < ActionController::Base + # helper_method :current_user, :logged_in? + # + # def current_user + # @current_user ||= User.find_by_id(session[:user]) + # end + # + # def logged_in? + # current_user != nil + # end + # end + # + # In a view: + # <% if logged_in? -%>Welcome, <%= current_user.name %><% end -%> def helper_method(*meths) meths.flatten.each do |meth| master_helper_module.class_eval <<-ruby_eval, __FILE__, __LINE__ + 1 @@ -43,17 +57,16 @@ module AbstractController ruby_eval end end - - def helper(*args, &blk) + + def helper(*args, &block) args.flatten.each do |arg| case arg when Module add_template_helper(arg) end end - master_helper_module.module_eval(&blk) if block_given? + master_helper_module.module_eval(&block) if block_given? end end - end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_controller/abstract/layouts.rb b/actionpack/lib/action_controller/abstract/layouts.rb index 478b301a26..b3f21f7b22 100644 --- a/actionpack/lib/action_controller/abstract/layouts.rb +++ b/actionpack/lib/action_controller/abstract/layouts.rb @@ -1,25 +1,34 @@ module AbstractController module Layouts - + extend ActiveSupport::Concern + depends_on Renderer - + + included do + extlib_inheritable_accessor :_layout_conditions + self._layout_conditions = {} + end + module ClassMethods - def layout(layout) + def layout(layout, conditions = {}) unless [String, Symbol, FalseClass, NilClass].include?(layout.class) raise ArgumentError, "Layouts must be specified as a String, Symbol, false, or nil" end - + + conditions.each {|k, v| conditions[k] = Array(v).map {|a| a.to_s} } + self._layout_conditions = conditions + @_layout = layout || false # Converts nil to false _write_layout_method end - + def _implied_layout_name name.underscore end - + # Takes the specified layout and creates a _layout method to be called # by _default_layout - # + # # If the specified layout is a: # String:: return the string # Symbol:: call the method specified by the symbol @@ -30,15 +39,15 @@ module AbstractController def _write_layout_method case @_layout when String - self.class_eval %{def _layout() #{@_layout.inspect} end} + self.class_eval %{def _layout(details) #{@_layout.inspect} end} when Symbol - self.class_eval %{def _layout() #{@_layout} end} + self.class_eval %{def _layout(details) #{@_layout} end} when false - self.class_eval %{def _layout() end} + self.class_eval %{def _layout(details) end} else self.class_eval %{ - def _layout - if view_paths.find_by_parts?("#{_implied_layout_name}", formats, "layouts") + def _layout(details) + if view_paths.find_by_parts?("#{_implied_layout_name}", details, "layouts") "#{_implied_layout_name}" else super @@ -48,35 +57,51 @@ module AbstractController end end end - - def _render_template(template, options) - _action_view._render_template_with_layout(template, options[:_layout]) - end - + private - - def _layout() end # This will be overwritten - - def _layout_for_name(name) + # This will be overwritten + def _layout(details) + end + + # :api: plugin + # ==== + # Override this to mutate the inbound layout name + def _layout_for_name(name, details = {:formats => formats}) unless [String, FalseClass, NilClass].include?(name.class) raise ArgumentError, "String, false, or nil expected; you passed #{name.inspect}" end - - name && view_paths.find_by_parts(name, formats, "layouts") + + name && view_paths.find_by_parts(name, details, _layout_prefix(name)) end - - def _default_layout(require_layout = false) - if require_layout && !_layout - raise ArgumentError, + + # TODO: Decide if this is the best hook point for the feature + def _layout_prefix(name) + "layouts" + end + + def _default_layout(require_layout = false, details = {:formats => formats}) + if require_layout && _action_has_layout? && !_layout(details) + raise ArgumentError, "There was no default layout for #{self.class} in #{view_paths.inspect}" end - + begin - layout = _layout_for_name(_layout) + _layout_for_name(_layout(details), details) if _action_has_layout? rescue NameError => e - raise NoMethodError, + raise NoMethodError, "You specified #{@_layout.inspect} as the layout, but no such method was found" end end + + def _action_has_layout? + conditions = _layout_conditions + if only = conditions[:only] + only.include?(action_name) + elsif except = conditions[:except] + !except.include?(action_name) + else + true + end + end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_controller/abstract/logger.rb b/actionpack/lib/action_controller/abstract/logger.rb index 4117369bd4..d6fa843485 100644 --- a/actionpack/lib/action_controller/abstract/logger.rb +++ b/actionpack/lib/action_controller/abstract/logger.rb @@ -1,7 +1,45 @@ +require 'active_support/core_ext/class/attribute_accessors' +require 'active_support/core_ext/logger' + module AbstractController module Logger - setup do + extend ActiveSupport::Concern + + class DelayedLog + def initialize(&blk) + @blk = blk + end + + def to_s + @blk.call + end + alias to_str to_s + end + + included do cattr_accessor :logger end + + def process(action) + ret = super + + if logger + log = DelayedLog.new do + "\n\nProcessing #{self.class.name}\##{action_name} " \ + "to #{request.formats} " \ + "(for #{request_origin}) [#{request.method.to_s.upcase}]" + end + + logger.info(log) + end + + ret + end + + def request_origin + # this *needs* to be cached! + # otherwise you'd get different results if calling it more than once + @request_origin ||= "#{request.remote_ip} at #{Time.now.to_s(:db)}" + end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_controller/abstract/renderer.rb b/actionpack/lib/action_controller/abstract/renderer.rb index a86eef889e..cd3e87d861 100644 --- a/actionpack/lib/action_controller/abstract/renderer.rb +++ b/actionpack/lib/action_controller/abstract/renderer.rb @@ -2,52 +2,63 @@ require "action_controller/abstract/logger" module AbstractController module Renderer + extend ActiveSupport::Concern + depends_on AbstractController::Logger - - setup do + + included do attr_internal :formats - + extlib_inheritable_accessor :_view_paths - + self._view_paths ||= ActionView::PathSet.new end - + def _action_view - @_action_view ||= ActionView::Base.new(self.class.view_paths, {}, self) + @_action_view ||= ActionView::Base.new(self.class.view_paths, {}, self) end - - def render(options = {}) - self.response_body = render_to_body(options) + + def render(*args) + if response_body + raise AbstractController::DoubleRenderError, "OMG" + end + + self.response_body = render_to_body(*args) end - + # Raw rendering of a template to a Rack-compatible body. # ==== # @option _prefix<String> The template's path prefix # @option _layout<String> The relative path to the layout template to use - # + # # :api: plugin def render_to_body(options = {}) - name = options[:_template_name] || action_name - - template = options[:_template] || view_paths.find_by_parts(name.to_s, formats, options[:_prefix]) - _render_template(template, options) + # TODO: Refactor so we can just use the normal template logic for this + if options[:_partial_object] + _action_view._render_partial_from_controller(options) + else + _determine_template(options) + _render_template(options) + end end # Raw rendering of a template to a string. # ==== # @option _prefix<String> The template's path prefix # @option _layout<String> The relative path to the layout template to use - # + # # :api: plugin def render_to_string(options = {}) AbstractController::Renderer.body_to_s(render_to_body(options)) end - def _render_template(template, options) - _action_view._render_template_with_layout(template) + def _render_template(options) + _action_view._render_template_from_controller(options[:_template], options[:_layout], options, options[:_partial]) + end + + def view_paths() + _view_paths end - - def view_paths() _view_paths end # Return a string representation of a Rack-compatible response body. def self.body_to_s(body) @@ -61,16 +72,28 @@ module AbstractController end end + private + def _determine_template(options) + name = (options[:_template_name] || action_name).to_s + + options[:_template] ||= view_paths.find_by_parts( + name, { :formats => formats }, options[:_prefix], options[:_partial] + ) + end + module ClassMethods - def append_view_path(path) self.view_paths << path end - + + def prepend_view_path(path) + self.view_paths.unshift(path) + end + def view_paths self._view_paths end - + def view_paths=(paths) self._view_paths = paths.is_a?(ActionView::PathSet) ? paths : ActionView::Base.process_view_paths(paths) |