module Hobo module ModelController include Hobo::Controller DONT_PAGINATE_FORMATS = [ Mime::CSV, Mime::YAML, Mime::JSON, Mime::XML, Mime::ATOM, Mime::RSS ] WILL_PAGINATE_OPTIONS = [ :page, :per_page, :total_entries, :count, :finder ] READ_ONLY_ACTIONS = [:index, :show] WRITE_ONLY_ACTIONS = [:create, :update, :destroy] FORM_ACTIONS = [:new, :edit] class << self def included(base) base.class_eval do @auto_actions ||= {} inheriting_cattr_reader :web_methods => [], :show_actions => [], :index_actions => [], :owner_actions => {} extend ClassMethods helper_method :model, :current_user before_filter :set_no_cache_headers rescue_from ActiveRecord::RecordNotFound, :with => :not_found rescue_from Hobo::PermissionDeniedError, :with => :permission_denied rescue_from Hobo::Lifecycles::LifecycleKeyError, :with => :permission_denied alias_method_chain :render, :hobo_model end register_controller(base) Hobo::Controller.included_in_class(base) end end def self.register_controller(controller) @controller_names ||= Set.new @controller_names << controller.name end def self.all_controllers(subsite=nil, force=false) # Load every controller in app/controllers/... @controllers_loaded ||= {} if force || !@controllers_loaded[subsite] dir = "#{RAILS_ROOT}/app/controllers#{'/' + subsite if subsite}" Dir.entries(dir).each do |f| if f =~ /^[a-zA-Z_][a-zA-Z0-9_]*_controller\.rb$/ name = f.remove(/.rb$/).camelize name = "#{subsite.camelize}::#{name}" if subsite name.constantize end end @controllers_loaded[subsite] = true end # ...but only return the ones that registered themselves names = (@controller_names || []).select { |n| subsite ? n =~ /^#{subsite.camelize}::/ : n !~ /::/ } names.map do |name| name.safe_constantize || (@controller_names.delete name; nil) end.compact end module ClassMethods attr_writer :model def model_name name.demodulize.remove(/Controller$/).singularize end def model @model ||= model_name.constantize end def autocomplete(*args, &block) options = args.extract_options! name = args.first || model.name_attribute field = options.delete(:field) || name if block index_action "complete_#{name}", &block else index_action "complete_#{name}" do hobo_completions field, model, options end end end def web_method(web_name, options={}, &block) web_methods << web_name.to_sym method = options.delete(:method) || web_name got_block = block_given? define_method web_name do # Make sure we have a copy of the options - it is being mutated somewhere opts = options.dup self.this = find_instance(opts) raise Hobo::PermissionDeniedError unless @this.method_callable_by?(current_user, method) if got_block this.with_acting_user(current_user) { instance_eval(&block) } else @this.send(method) end hobo_ajax_response unless performed? end end def auto_actions(*args) options = args.extract_options! @auto_actions = case args.first when :all then available_auto_actions when :write_only then available_auto_write_actions + args.rest when :read_only then available_auto_read_actions + args.rest when :lifecycle then available_auto_lifecycle_actions + args.rest else args end except = Array(options[:except]) except_actions = except.map do |arg| case arg when :lifecycle then available_auto_lifecycle_actions else arg end end @auto_actions -= except_actions.flatten def_auto_actions end def def_auto_actions self.class_eval do def index; hobo_index end if include_action?(:index) def show; hobo_show end if include_action?(:show) def new; hobo_new end if include_action?(:new) def create; hobo_create end if include_action?(:create) def edit; hobo_show end if include_action?(:edit) def update; hobo_update end if include_action?(:update) def destroy; hobo_destroy end if include_action?(:destroy) def completions; hobo_completions end if include_action?(:completions) def reorder; hobo_reorder end if include_action?(:reorder) end def_lifecycle_actions end def def_auto_action(name, &block) define_method name, &block if !method_defined?(name) && include_action?(name) end def def_lifecycle_actions if model.has_lifecycle? model::Lifecycle.publishable_creators.each do |creator| name = creator.name def_auto_action name do creator_page_action name end def_auto_action "do_#{name}" do do_creator_action name end end model::Lifecycle.publishable_transitions.each do |transition| name = transition.name def_auto_action name do transition_page_action name end def_auto_action "do_#{name}" do do_transition_action name end end end end def show_action(*names, &block) options = names.extract_options! show_actions.concat(names) for name in names if block define_method(name, &block) else define_method(name) { hobo_show options.dup } end end end def index_action(*names, &block) options = names.extract_options! index_actions.concat(names) for name in names if block define_method(name, &block) else if scope = options.delete(:scope) if scope.is_a?(Symbol) define_method(name) { hobo_index model.send(scope), options.dup } else define_method(name) { hobo_index scope, options.dup } end else define_method(name) { hobo_index options.dup } end end end end def creator_page_action(name, options={}, &block) define_method(name) do creator_page_action name, options, &block end end def do_creator_action(name, options={}, &block) define_method("do_#{name}") do do_creator_action name, options, &block end end def transtion_page_action(name, options={}, &block) define_method(name) do transtion_page_action name, options, &block end end def do_transition_action(name, options={}, &block) define_method("do_#{name}") do do_transition_action name, options, &block end end def auto_actions_for(owner, actions) name = model.reflections[owner].macro == :has_many ? owner.to_s.singularize : owner owner_actions[owner] ||= [] Array(actions).each do |action| case action when :new define_method("new_for_#{name}") { hobo_new_for owner } when :index define_method("index_for_#{name}") { hobo_index_for owner } when :create define_method("create_for_#{name}") { hobo_create_for owner } else raise ArgumentError, "Invalid owner action: #{action}" end owner_actions[owner] << action end end def include_action?(name) name.to_sym.in?(@auto_actions) end def available_auto_actions (available_auto_read_actions + available_auto_write_actions + FORM_ACTIONS + available_auto_lifecycle_actions).uniq end def available_auto_read_actions READ_ONLY_ACTIONS end def available_auto_write_actions if model.method_defined?("position_column") WRITE_ONLY_ACTIONS + [:reorder] else WRITE_ONLY_ACTIONS end end def available_auto_lifecycle_actions # For each creator/transition there are two possible # actions. e.g. for signup, 'signup' would be routed to # GET users/signup, and would show the form, while 'do_signup' # would be routed to POST /users/signup) if model.has_lifecycle? (model::Lifecycle.publishable_creators.map { |c| [c.name, "do_#{c.name}"] } + model::Lifecycle.publishable_transitions.map { |t| [t.name, "do_#{t.name}"] }).flatten.*.to_sym else [] end end end # of ClassMethods protected def parse_sort_param(*sort_fields) _, desc, field = *params[:sort]._?.match(/^(-)?([a-z_]+(?:\.[a-z_]+)?)$/) if field if field.in?(sort_fields.*.to_s) @sort_field = field @sort_direction = desc ? "desc" : "asc" [@sort_field, @sort_direction] end end end # --- Action implementation helpers --- # def find_instance(options={}) model.user_find(current_user, params[:id], options) do |record| yield record if block_given? end end def invalid?; !valid?; end def valid?; this.errors.empty?; end def re_render_form(default_action=nil) if params[:page_path] @invalid_record = this controller, view = Controller.controller_and_view_for(params[:page_path]) view = default_action if view == Dryml::EMPTY_PAGE # Hack fix for Bug 477. See also bug 489. if self.class.name == "#{controller.camelize}Controller" && view == "index" params['action'] = 'index' self.action_name = 'index' index else render :template => "#{controller}/#{view}" end else render :action => default_action end end def destination_after_submit(record=this, destroyed=false) after_submit = params[:after_submit] # The after_submit post parameter takes priority (after_submit == "stay-here" ? url_for_page_path : after_submit) || # Then try the record's show page (!destroyed && object_url(@this)) || # Then the show page of the 'owning' object if there is one object_url(owning_object) || # Last try - the index page for this model object_url(@this.class) || # Give up home_page end def owning_object method = @this.class.view_hints.parent method ? @this.send(method) : nil end def url_for_page_path controller, view = Controller.controller_and_view_for(params[:page_path]) url_for :controller => controller, :action => view end # TODO: Get rid of this joke of an idea that fails miserably if you open another browser window. def previous_page_path session[:previous_page_path] end def redirect_after_submit(*args) options = args.extract_options! o = options[:redirect] if o url = if o.is_a?(Symbol) object_url(this, o) elsif o.is_a?(String) || o.is_a?(Hash) o else object_url(*Array(o)) end redirect_to url else redirect_to destination_after_submit(*args) end end def response_block(&b) if b respond_to do |format| if b.arity == 1 yield format else format.html { yield } end end performed? end end def request_requires_pagination? request.format.not_in?(DONT_PAGINATE_FORMATS) && model.view_hints.paginate? end def find_or_paginate(finder, options) options = options.reverse_merge(:paginate => request_requires_pagination?) do_pagination = options.delete(:paginate) && finder.respond_to?(:paginate) options[:order] = :default unless options[:order] || finder.send(:scope, :find)._?[:order] if do_pagination options.reverse_merge!(:page => params[:page] || 1) finder.paginate(options) else finder.all(options.except(*WILL_PAGINATE_OPTIONS)) end end def find_owner_and_association(owner_association) owner_name = name_of_auto_action_for(owner_association) refl = model.reflections[owner_association] id = params["#{owner_name}_id"] owner = refl.klass.find(id) instance_variable_set("@#{owner_association}", owner) [owner, owner.send(model.reverse_reflection(owner_association).name)] end def name_of_auto_action_for(owner_association) model.reflections[owner_association].macro == :has_many ? owner_association.to_s.singularize : owner_association end # --- Action implementations --- # def hobo_index(*args, &b) options = args.extract_options! finder = args.first || model self.this = find_or_paginate(finder, options) response_block(&b) end def hobo_index_for(owner, *args, &b) options = args.extract_options! owner, association = find_owner_and_association(owner) finder = args.first || association self.this = find_or_paginate(finder, options) response_block(&b) end def hobo_show(*args, &b) options = args.extract_options! self.this ||= args.first || find_instance(options) response_block(&b) end def hobo_new(record=nil, &b) self.this = record || model.user_new(current_user) response_block(&b) end def hobo_new_for(owner, record=nil, &b) owner, association = find_owner_and_association(owner) self.this = record || association.user_new(current_user) response_block(&b) end def hobo_create(*args, &b) options = args.extract_options! attributes = options[:attributes] || attribute_parameters || {} if self.this ||= args.first this.user_update_attributes(current_user, attributes) else self.this = new_for_create(attributes) this.save end create_response(:new, options, &b) end def hobo_create_for(owner_association, *args, &b) options = args.extract_options! owner, association = find_owner_and_association(owner_association) attributes = options[:attributes] || attribute_parameters || {} if self.this ||= args.first this.user_update_attributes(current_user, attributes) else self.this = association.new(attributes) this.save end create_response(:"new_for_#{name_of_auto_action_for(owner_association)}", options, &b) end def attribute_parameters params[(this ? this.class : model).name.underscore] end def new_for_create(attributes = {}) type_param = subtype_for_create create_model = type_param ? type_param.constantize : model create_model.user_new(current_user, attributes) end def subtype_for_create model.has_inheritance_column? && (t = params['type']) && t.in?(model.send(:subclasses).*.name) and t end def flash_notice(message) flash[:notice] = message unless request.xhr? end def create_response(new_action, options={}, &b) flash_notice (ht( :"#{@this.class.name.pluralize.underscore}.messages.create.success", :default=>["The #{@this.class.name.titleize.downcase} was created successfully"])) if valid? response_block(&b) or if valid? respond_to do |wants| wants.html { redirect_after_submit(options) } wants.js { hobo_ajax_response || render(:nothing => true) } end else respond_to do |wants| # errors is used by the translation helper, ht, below. errors = this.errors.full_messages.join("\n") wants.html { re_render_form(new_action) } wants.js { render(:status => 500, :text => ht( :"#{this.class.name.pluralize.underscore}.messages.create.error", :errors=>errors,:default=>["Couldn't create the #{this.class.name.titleize.downcase}.\n #{errors}"]) )} end end end def hobo_update(*args, &b) options = args.extract_options! self.this ||= args.first || find_instance changes = options[:attributes] || attribute_parameters or raise RuntimeError, ht(:"hobo.messages.update.no_attribute_error", :default=>["No update specified in params"]) this.user_update_attributes(current_user, changes) # Ensure current_user isn't out of date @current_user = @this if @this == current_user in_place_edit_field = changes.keys.first if changes.size == 1 && params[:render] update_response(in_place_edit_field, options, &b) end def update_response(in_place_edit_field=nil, options={}, &b) flash_notice (ht(:"#{@this.class.name.pluralize.underscore}.messages.update.success", :default=>["Changes to the #{@this.class.name.titleize.downcase} were saved"])) if valid? response_block(&b) or if valid? respond_to do |wants| wants.html do redirect_after_submit options end wants.js do if in_place_edit_field # Decreasingly hacky support for the scriptaculous in-place-editor new_val = call_dryml_tag("view", :field => in_place_edit_field, :no_wrapper => true) hobo_ajax_response(this, :new_field_value => new_val) else hobo_ajax_response(this) end # Maybe no ajax requests were made render :nothing => true unless performed? end end else respond_to do |wants| # errors is used by the translation helper, ht, below. errors = @this.errors.full_messages.join("\n") wants.html { re_render_form(:edit) } wants.js { render(:status => 500, :text => ht(:"#{@this.class.name.pluralize.underscore}.messages.update.error",:default=>["There was a problem with that change.\n#{errors}"], :errors=>errors) ) } end end end def hobo_destroy(*args, &b) options = args.extract_options! self.this ||= args.first || find_instance this.user_destroy(current_user) flash_notice ht( :"#{model.name.pluralize.underscore}.messages.destroy.success", :default=>["The #{model.name.titleize.downcase} was deleted"]) destroy_response(options, &b) end def destroy_response(options={}, &b) response_block(&b) or respond_to do |wants| wants.html { redirect_after_submit(this, true, options) } wants.js { hobo_ajax_response || render(:nothing => true) } end end # --- Lifecycle Actions --- # def creator_page_action(name, options={}, &b) self.this ||= model.new this.exempt_from_edit_checks = true @creator = model::Lifecycle.creator(name) raise Hobo::PermissionDeniedError unless @creator.allowed?(current_user) response_block &b end def do_creator_action(name, options={}, &b) @creator = model::Lifecycle.creator(name) self.this = @creator.run!(current_user, attribute_parameters) response_block(&b) or if valid? respond_to do |wants| wants.html { redirect_after_submit(options) } wants.js { hobo_ajax_response || render(:nothing => true) } end else this.exempt_from_edit_checks = true respond_to do |wants| # errors is used by the translation helper, ht, below. errors = this.errors.full_messages.join("\n") wants.html { re_render_form(name) } wants.js { render(:status => 500, :text => ht(:"#{@this.class.name.pluralize.underscore}.messages.creator.error", :default=>["Couldn't do creator #{name}.\n#{errors}"], :name=>name, :errors=>errors) )} end end end def prepare_transition(name, options) key = options.delete(:key) || params[:key] # we don't use find_instance here, as it fails for key_holder transitions on objects that Guest can't view record = model.find(params[:id]) record.exempt_from_edit_checks = true record.lifecycle.provided_key = key self.this = record this.lifecycle.find_transition(name, current_user) or raise Hobo::PermissionDeniedError end def transition_page_action(name, options={}, &b) @transition = prepare_transition(name, options) response_block &b end def do_transition_action(name, *args, &b) options = args.extract_options! @transition = prepare_transition(name, options) @transition.run!(this, current_user, attribute_parameters) response_block(&b) or if valid? respond_to do |wants| wants.html { redirect_after_submit(options) } wants.js { hobo_ajax_response || render(:nothing => true) } end else respond_to do |wants| # errors is used by the translation helper, ht, below. errors = this.errors.full_messages.join("\n") wants.html { re_render_form(name) } wants.js { render(:status => 500, :text => ht(:"#{@this.class.name.pluralize.underscore}.messages.transition.error", :default=>["Couldn't do transition #{name}.\n#{errors}"], :name=>name, :errors=>errors) )} end end end # --- Miscelaneous Actions --- # def hobo_completions(attribute, finder, options={}) options = options.reverse_merge(:limit => 10, :param => :query, :query_scope => "#{attribute}_contains") finder = finder.limit(options[:limit]) unless finder.send(:scope, :find, :limit) begin finder = finder.send(options[:query_scope], params[options[:param]]) items = finder.find(:all).select { |r| r.viewable_by?(current_user) } rescue TypeError # must be a list of methods instead items = [] options[:query_scope].each do |qscope| finder2 = finder.send(qscope, params[options[:param]]) items += finder2.find(:all).select { |r| r.viewable_by?(current_user) } end end render :text => "" end def hobo_reorder ordering = params["#{model.name.underscore}_ordering"] if ordering ordering.each_with_index do |id, position| model.find(id).user_update_attributes(current_user, :position => position+1) end hobo_ajax_response || render(:nothing => true) else render :nothing => true end end # --- Response helpers --- # def permission_denied(error) self.this = true # Otherwise this gets sent user_view logger.info "Hobo: Permission Denied!" @permission_error = error if self.class.superclass.method_defined?("permission_denied") super else respond_to do |wants| wants.html do if render_tag("permission-denied-page", { }, :status => 403) # job done else render :text => ht(:"hobo.messages.permission_denied", :default=>["Permission Denied"]), :status => 403 end end wants.js do render :text => ht(:"hobo.messages.permission_denied", :default=>["Permission Denied"]), :status => 403 end end end end def this @this ||= (instance_variable_get("@#{model.name.underscore}") || instance_variable_get("@#{model.name.underscore.pluralize}")) end def this=(object) ivar = if object.is_a?(Array) (object.try.member_class || model).name.underscore.pluralize else object.class.name.underscore end @this = instance_variable_set("@#{ivar}", object) end def dryml_context this end def render_with_hobo_model(*args, &block) options = args.extract_options! self.this = options[:object] if options[:object] # this causes more problems than it solves, and Tom says it's not supposed to be here # this.user_view(current_user) if this && this.respond_to?(:user_view) render_without_hobo_model(*args + [options], &block) end # --- filters --- # def set_no_cache_headers headers["Pragma"] = "no-cache" #headers["Cache-Control"] = ["must-revalidate", "no-cache", "no-store"] #headers["Cache-Control"] = "no-cache" headers["Cache-Control"] = "no-store" headers["Expires"] ='0' end # --- end filters --- # public def model self.class.model end end end