module Cms module Behaviors # The Rendering Behavior allows a model to be rendered within a view. # The key methods are the instance methods perform_render, render and # inline_options. From within a view or a helper, you can render a # renderable object by calling the perform_render and passing the controller # object to it. # # When perform_render is called, it will first call the render instance method # of the renderable object. This is very similar to a controller action. # The purpose of this method is to setup instance varaibles to be used by the # renderable's view. # # After the render method is called, it checks to see if there is a inline_options # instance method on the renderable object. If so, it calls this and it expects # this to return a Hash that will be passed to render. This expects there to be # an inline option, so this is the way to do inline rendering. # # Assuming there is no inline_options method, it will look for a template in the # view path at cms/pluralized_class_name/render. So if the Renderable class is # Article, the template should be at cms/articles/render. It uses the same # format and template engine options as regular views, to the file name should # be render.html.erb. # module Rendering def self.included(model) model.extend(MacroMethods) end module MacroMethods def renderable? false end def is_renderable(options={}) @instance_variable_name_for_view = options[:instance_variable_name_for_view] extend ClassMethods include InstanceMethods # I'm not pleased with the need to include all of the these rails helpers onto every 'renderable' content item # It's likely to lead to unfortunate side effects. # Need to determine how this can be simplified. # Required to make the calls to add Rails Core controllers work include ActiveSupport::Configurable # Include all the core rails helpers include ActionController::Helpers include ActionController::RequestForgeryProtection helper ApplicationHelper attr_accessor :controller delegate :params, :session, :request, :flash, :to => :controller end end end module ClassMethods def renderable? true end # This will be the used as the name of instance variable # that will be available in the view. The default value is "@renderable" def instance_variable_name_for_view @instance_variable_name_for_view ||= "@renderable" end def helper_path "app/helpers/#{name.underscore}_helper.rb" end def helper_class "Cms::#{name}Helper".constantize end # This is where the path to the template. The default is based on the class # of the renderable, so if you have an Article that is renderable, # the template will be "articles/render" def template_path "#{name.underscore.pluralize}/render" end # Instance variables that will not be copied from the renderable to the view def ivars_to_ignore ['@controller', '@_already_rendered'] end end module InstanceMethods # Returns the Mercury editor type for a given attribute # @param [Symbol] method (i.e. :name, :content, etc) # @return [Hash] def editor_info(method) column = self.class.columns_hash[method.to_s] if column.type == :text {:element => 'div', :region => 'full'} else {:element => 'span', :region => 'simple'} end end def prepare_to_render(controller) # Give this renderable a reference to the controller @controller = controller copy_instance_variables_from_controller! # This gives the view a reference to this object instance_variable_set(self.class.instance_variable_name_for_view, self) # This is like a controller action # We will call it if you have defined a render method # but if you haven't we won't render if should_render_self? end def perform_render(controller) return "Exception: #{@render_exception}" if @render_exception unless @controller # We haven't prepared to render. This should only happen when logged in, as we don't want # errors to bubble up and prevent the page being edited in that case. prepare_to_render(controller) end if self.respond_to?(:deleted) && self.deleted logger.error "Attempting to render deleted object: #{self.inspect}" msg = (edit_mode? ? %Q[
This #{self.class.name} has been deleted. Please remove this container from the page
] : '') return msg end # Create, Instantiate and Initialize the view action_view = Cms::ViewContext.new(@controller, assigns_for_view) add_content_helpers_if_defined(action_view) # Determine if this content should render from a file system template or inline (i.e. database based template) if respond_to?(:inline_options) && self.inline_options && self.inline_options.has_key?(:inline) options = self.inline_options locals = {} action_view.render(options, locals) else action_view.render(:file => self.class.template_path) end end def render_exception=(exception) @render_exception = exception end # Determines if a block should have its 'render' method called when it's rendered within a page. def should_render_self? # Reason to exist: This was added to work around the fact that Rails 3 AbstractController::Helpers defines its own # render method, which was conflicted with block's render methods. public_methods(false).include?(:render) end protected # Add the helper class for this particular object, if its defined # This is mostly for portlets, and should not fail if no helper is defined. def add_content_helpers_if_defined(action_view) begin helper_module = self.class.helper_class action_view.extend(helper_module) rescue NameError => error logger.debug "No helper class named '#{error.missing_name}' was found. This isn't necessarily an error as '#{self.class}' doesn't NEED a separate helper.'" end end def copy_instance_variables_from_controller! if @controller.respond_to?(:instance_variables_for_rendering) @controller.instance_variables_for_rendering.each do |iv| #logger.info "Copying #{iv} => #{@controller.instance_variable_get(iv).inspect}" instance_variable_set(iv, @controller.instance_variable_get(iv)) end end end def assigns_for_view (instance_variables - self.class.ivars_to_ignore).inject({}) do |h, k| h[k[1..-1]] = instance_variable_get(k) h end end end end end