class AutoForme::Action

  1. lib/autoforme/action.rb
Superclass: Object

Represents an action on a model in response to a web request.

Constants

ALL_SUPPORTED_ACTIONS = %w'new create show edit update delete destroy browse search mtm_edit mtm_update association_links autocomplete'.freeze  

Array of strings for all action types currently supported

NON_IDEMPOTENT_TYPES = NORMALIZED_ACTION_MAP = {:create=>:new, :update=>:edit, :destroy=>:delete, :mtm_update=>:mtm_edit}  

Map of regular type symbols to normalized type symbols

TITLE_MAP = {:new=>'New', :show=>'Show', :edit=>'Edit', :delete=>'Delete', :browse=>'Browse', :search=>'Search', :mtm_edit=>'Many To Many Edit'}  

Map of type symbols to HTML titles

Attributes

model [R]

The AutoForme::Model instance related to the current action

normalized_type [R]

The normalized type symbol related to the current action, so that paired actions such as new and create both use :new.

output_filename [R]

The filename to use for file output

output_type [R]

The type of output, currently nil for normal output, and 'csv' for csv output

params_association [R]

An association symbol for the current related association.

request [R]

The AutoForme::Request instance related to the current action

title [R]

An string suitable for use as the HTML title on the displayed page

type [R]

The type symbols related to the current action (e.g. :new, :create).

Public Class methods

new (model, request)

Creates a new action for the model and request. This action is not usable unless supported? is called first and it returns true.

[show source]
# File lib/autoforme/action.rb, line 43
def initialize(model, request)
  @model = model
  @request = request
end

Public Instance methods

base_url_for (page)

A path for the given page tied to the framework, but not the current model. Used for linking to other models in the same framework.

[show source]
# File lib/autoforme/action.rb, line 110
def base_url_for(page)
  "#{request.path}#{model.framework.prefix}/#{page}"
end
column_label_for (type, request, model, column)

The label to use for the given column.

[show source]
# File lib/autoforme/action.rb, line 173
def column_label_for(type, request, model, column)
  unless label = model.column_options_for(type, request, column)[:label]
    label = humanize(column)
  end
  label
end
column_options_for (type, request, obj, column)

The options to use for the given column, which will be passed to Forme::Form#input.

[show source]
# File lib/autoforme/action.rb, line 160
def column_options_for(type, request, obj, column)
  opts = model.column_options_for(type, request, column)
  if opts[:class] == 'autoforme_autocomplete'
    if type == :show
      opts[:value] = model.column_value(type, request, obj, column)
    elsif key = obj.send(model.association_key(column))
      opts[:value] = "#{key} - #{model.column_value(type, request, obj, column)}"
    end
  end
  opts
end
csv (meth)

Return a string in CSV format with the results

[show source]
# File lib/autoforme/action.rb, line 181
def csv(meth)
  @output_type = 'csv'
  @output_filename = "#{model.link.downcase}_#{normalized_type}.csv"
  columns = model.columns_for(type, request)
  headers = columns.map{|column| column_label_for(type, request, model, column)}
  EnumCSV.csv(model.send(meth, normalized_type, request, :all_results=>true), :headers=>headers) do |obj|
    columns.map{|column| model.column_value(type, request, obj, column)}
  end
end
edit_page (obj)

The page to use when editing the object.

[show source]
# File lib/autoforme/action.rb, line 340
def edit_page(obj)
  page do
    t = String.new
    t << Forme.form(obj, form_attributes(:action=>url_for("update/#{model.primary_key_value(obj)}")), form_opts) do |f|
      model.columns_for(:edit, request).each do |column|
        col_opts = column_options_for(:edit, request, obj, column)
        if html = model.edit_html_for(obj, column, :edit, request)
          col_opts = col_opts.merge(:html=>html)
        end
        f.input(column, col_opts)
      end
      f.button(:value=>'Update', :class=>'btn btn-primary')
    end.to_s
    if model.supported_action?(:delete, request)
      t << Forme.form(form_attributes(:action=>url_for("delete/#{model.primary_key_value(obj)}")), form_opts) do |f|
        f.button(:value=>'Delete', :class=>'btn btn-danger')
      end.to_s
    end
    t << association_links(obj)
  end
end
form_attributes (attrs)

Merge the model's form attributes into the given form attributes, yielding the attributes to use for the form.

[show source]
# File lib/autoforme/action.rb, line 238
def form_attributes(attrs)
  attrs.merge(model.form_attributes_for(type, request))
end
form_opts ()

Options to use for the form. If the form uses POST, automatically adds the CSRF token.

[show source]
# File lib/autoforme/action.rb, line 227
def form_opts
  opts = model.form_options_for(type, request).dup
  hidden_tags = opts[:hidden_tags] = []
  if csrf = request.csrf_token_hash
    hidden_tags << lambda{|tag| csrf if tag.attr[:method].to_s.upcase == 'POST'}
  end
  opts
end
h (s)

Convert input to a string adn HTML escape it.

[show source]
# File lib/autoforme/action.rb, line 98
def h(s)
  Rack::Utils.escape_html(s.to_s)
end
handle ()

Handle the current action, returning an HTML string containing the page content, or redirecting.

[show source]
# File lib/autoforme/action.rb, line 148
def handle
  model.before_action_hook(type, request)
  send("handle_#{type}")
end
handle_autocomplete ()

Handle autocomplete action by returning a string with one line per model object.

[show source]
# File lib/autoforme/action.rb, line 537
def handle_autocomplete
  unless (query = request.params['q'].to_s).empty?
    model.autocomplete(:type=>@subtype, :request=>request, :association=>params_association, :query=>query, :exclude=>request.params['exclude']).join("\n")
  end
end
handle_browse ()

Handle browse action by showing a table containing model objects.

[show source]
# File lib/autoforme/action.rb, line 435
def handle_browse
  if request.id == 'csv'
    csv(:browse)
  else
    table_page(*model.browse(type, request))
  end
end
handle_create ()

Handle the create action by creating a new model object.

[show source]
# File lib/autoforme/action.rb, line 266
def handle_create
  obj = model.new(nil, request)
  model.set_fields(obj, :new, request, model_params)
  model.hook(:before_create, request, obj)
  if model.save(obj)
    model.hook(:after_create, request, obj)
    request.set_flash_notice("Created #{model.class_name}")
    redirect(:new, obj)
  else
    request.set_flash_now_error("Error Creating #{model.class_name}")
    new_page(obj)
  end
end
handle_delete ()

Handle the edit action by showing a list page if there is no model object selected, or a confirmation screen if there is one.

[show source]
# File lib/autoforme/action.rb, line 389
def handle_delete
  if request.id
    handle_show
  else
    list_page(:delete, :form=>{:action=>url_for('delete')})
  end
end
handle_destroy ()

Handle the destroy action by destroying the model object.

[show source]
# File lib/autoforme/action.rb, line 398
def handle_destroy
  obj = model.with_pk(normalized_type, request, request.id)
  model.hook(:before_destroy, request, obj)
  model.destroy(obj)
  model.hook(:after_destroy, request, obj)
  request.set_flash_notice("Deleted #{model.class_name}")
  redirect(:delete, obj)
end
handle_edit ()

Handle the edit action by showing a list page if there is no model object selected, or the edit page if there is one.

[show source]
# File lib/autoforme/action.rb, line 363
def handle_edit
  if request.id
    obj = model.with_pk(normalized_type, request, request.id)
    model.hook(:before_edit, request, obj)
    edit_page(obj)
  else
    list_page(:edit)
  end
end
handle_mtm_edit ()

Handle the mtm_edit action by showing a list page if there is no model object selected, a list of associations for that model if there is a model object but no association selected, or a mtm_edit form if there is a model object and association selected.

[show source]
# File lib/autoforme/action.rb, line 468
def handle_mtm_edit
  if request.id
    obj = model.with_pk(:edit, request, request.id)
    unless assoc = params_association
      options = model.mtm_association_select_options(request)
      if options.length == 1
        assoc = options.first
      end
    end
    if assoc
      page do
        t = String.new
        t << "<h2>Edit #{humanize(assoc)} for #{h model.object_display_name(type, request, obj)}</h2>"
        t << Forme.form(obj, form_attributes(:action=>url_for("mtm_update/#{model.primary_key_value(obj)}?association=#{assoc}")), form_opts) do |f|
          opts = model.column_options_for(:mtm_edit, request, assoc)
          add_opts = opts[:add] ? opts.merge(opts.delete(:add)) : opts
          remove_opts = opts[:remove] ? opts.merge(opts.delete(:remove)) : opts
          add_opts = {:name=>'add[]', :id=>'add', :label=>'Associate With'}.merge(add_opts)
          if model.association_autocomplete?(assoc, request)
            f.input(assoc, {:type=>'text', :class=>'autoforme_autocomplete', :attr=>{'data-type'=>'association', 'data-column'=>assoc, 'data-exclude'=>model.primary_key_value(obj)}, :value=>''}.merge(add_opts))
          else
            f.input(assoc, {:dataset=>model.unassociated_mtm_objects(request, assoc, obj)}.merge(add_opts))
          end
          f.input(assoc, {:name=>'remove[]', :id=>'remove', :label=>'Disassociate From', :dataset=>model.associated_mtm_objects(request, assoc, obj), :value=>[]}.merge(remove_opts))
          f.button(:value=>'Update', :class=>'btn btn-primary')
        end.to_s
      end
    else
      page do
        Forme.form(form_attributes(:action=>"mtm_edit/#{model.primary_key_value(obj)}"), form_opts) do |f|
          f.input(:select, :options=>options, :name=>'association', :id=>'association', :label=>'Association', :add_blank=>true)
          f.button(:value=>'Edit', :class=>'btn btn-primary')
        end
      end
    end
  else
    list_page(:edit, :form=>{})
  end
end
handle_mtm_update ()

Handle mtm_update action by updating the related many to many association. For ajax requests, return an HTML fragment to update the page, otherwise redirect to the appropriate form.

[show source]
# File lib/autoforme/action.rb, line 510
def handle_mtm_update
  obj = model.with_pk(:edit, request, request.id)
  assoc = params_association
  assoc_obj = model.mtm_update(request, assoc, obj, request.params['add'], request.params['remove'])
  request.set_flash_notice("Updated #{assoc} association for #{model.class_name}") unless request.xhr?
  if request.xhr?
    if request.params['add']
      @type = :edit
      mtm_edit_remove(assoc, model.associated_model_class(assoc), obj, assoc_obj)
    else
      "<option value=\"#{model.primary_key_value(assoc_obj)}\">#{model.associated_object_display_name(assoc, request, assoc_obj)}</option>"
    end
  elsif request.params['redir'] == 'edit'
    redirect(:edit, obj)
  else
    redirect(:mtm_edit, obj)
  end
end
handle_new ()

Handle the new action by always showing the new form.

[show source]
# File lib/autoforme/action.rb, line 259
def handle_new
  obj = model.new(request.params[model.link], request)
  model.hook(:before_new, request, obj)
  new_page(obj)
end
handle_show ()

Handle the show action by showing a list page if there is no model object selected, or the show page if there is one.

[show source]
# File lib/autoforme/action.rb, line 331
def handle_show
  if request.id
    show_page(model.with_pk(normalized_type, request, request.id))
  else
    list_page(:show)
  end
end
handle_update ()

Handle the update action by updating the current model object.

[show source]
# File lib/autoforme/action.rb, line 374
def handle_update
  obj = model.with_pk(normalized_type, request, request.id)
  model.set_fields(obj, :edit, request, model_params)
  model.hook(:before_update, request, obj)
  if model.save(obj)
    model.hook(:after_update, request, obj)
    request.set_flash_notice("Updated #{model.class_name}")
    redirect(:edit, obj)
  else
    request.set_flash_now_error("Error Updating #{model.class_name}")
    edit_page(obj)
  end
end
humanize (string)

Convert the given object into a suitable human readable string.

[show source]
# File lib/autoforme/action.rb, line 154
def humanize(string)
  string = string.to_s
  string.respond_to?(:humanize) ? string.humanize : string.gsub(/_/, " ").capitalize
end
inline_mtm_edit_forms (obj)

HTML fragment used for the inline mtm_edit forms on the edit page.

[show source]
# File lib/autoforme/action.rb, line 635
def inline_mtm_edit_forms(obj)
  assocs = model.inline_mtm_assocs(request)
  return if assocs.empty?

  t = String.new
  t << "<div class='inline_mtm_add_associations'>"
  assocs.each do |assoc|
    form_attr = form_attributes(:action=>url_for("mtm_update/#{model.primary_key_value(obj)}?association=#{assoc}&redir=edit"), :class => 'mtm_add_associations', 'data-remove' => "##{assoc}_remove_list")
    t << Forme.form(obj, form_attr, form_opts) do |f|
      opts = model.column_options_for(:mtm_edit, request, assoc)
      add_opts = opts[:add] ? opts.merge(opts.delete(:add)) : opts.dup
      add_opts = {:name=>'add[]', :id=>"add_#{assoc}"}.merge(add_opts)
      if model.association_autocomplete?(assoc, request)
        f.input(assoc, {:type=>'text', :class=>'autoforme_autocomplete', :attr=>{'data-type'=>'association', 'data-column'=>assoc, 'data-exclude'=>model.primary_key_value(obj)}, :value=>''}.merge(add_opts))
      else
        f.input(assoc, {:dataset=>model.unassociated_mtm_objects(request, assoc, obj), :multiple=>false, :add_blank=>true}.merge(add_opts))
      end
      f.button(:value=>'Add', :class=>'btn btn-xs btn-primary')
    end.to_s
  end
  t << "</div>"
  t << "<div class='inline_mtm_remove_associations'><ul>"
  assocs.each do |assoc|
    mc = model.associated_model_class(assoc)
    t << "<li>"
    t << association_class_link(mc, assoc)
    t << "<ul id='#{assoc}_remove_list'>"
    obj.send(assoc).each do |assoc_obj|
      t << mtm_edit_remove(assoc, mc, obj, assoc_obj)
    end
    t << "</ul></li>"
  end
  t << "</ul></div>"
end
list_page (type, opts={})

Shared page used by show, edit, and delete actions that shows a list of available model objects (or an autocompleting box), and allows the user to choose one to act on.

[show source]
# File lib/autoforme/action.rb, line 282
def list_page(type, opts={})
  page do
    form_attr = form_attributes(opts[:form] || {:action=>url_for(type)})
    Forme.form(form_attr, form_opts) do |f|
      input_opts = {:name=>'id', :id=>'id', :label=>model.class_name}
      if model.autocomplete_options_for(type, request)
        input_type = :text
        input_opts.merge!(:class=>'autoforme_autocomplete', :attr=>{'data-type'=>type})
      else
        input_type = :select
        input_opts.merge!(:options=>model.select_options(type, request), :add_blank=>true)
      end
      f.input(input_type, input_opts)
      f.button(:value=>type.to_s.capitalize, :class=>"btn btn-#{type == :delete ? 'danger' : 'primary'}")
    end
  end
end
model_params ()

Get request parameters for the model. Used when retrieving form values that use namespacing.

[show source]
# File lib/autoforme/action.rb, line 104
def model_params
  request.params[model.params_name]
end
mtm_edit_remove (assoc, mc, obj, assoc_obj)

Line item containing form to remove the currently associated object.

[show source]
# File lib/autoforme/action.rb, line 671
def mtm_edit_remove(assoc, mc, obj, assoc_obj)
  t = String.new
  t << "<li>"
  t << association_link(mc, assoc_obj)
  form_attr = form_attributes(:action=>url_for("mtm_update/#{model.primary_key_value(obj)}?association=#{assoc}&remove%5b%5d=#{model.primary_key_value(assoc_obj)}&redir=edit"), :method=>'post', :class => 'mtm_remove_associations', 'data-add'=>"#add_#{assoc}")
  t << Forme.form(form_attr, form_opts) do |f|
    f.button(:value=>'Remove', :class=>'btn btn-xs btn-danger')
  end.to_s
  t << "</li>"
end
new_page (obj, opts={})

HTML content used for the new action

[show source]
# File lib/autoforme/action.rb, line 243
def new_page(obj, opts={})
  page do
    Forme.form(obj, form_attributes(:action=>url_for("create")), form_opts) do |f|
      model.columns_for(:new, request).each do |column|
        col_opts = column_options_for(:new, request, obj, column)
        if html = model.edit_html_for(obj, column, :new, request)
          col_opts = col_opts.merge(:html=>html)
        end
        f.input(column, col_opts)
      end
      f.button(:value=>'Create', :class=>'btn btn-primary')
    end
  end
end
page ()

Yields and wraps the returned data in a header and footer for the page.

[show source]
# File lib/autoforme/action.rb, line 216
def page
  html = String.new
  html << (model.page_header_for(type, request) || tabs)
  html << "<div id='autoforme_content' data-url='#{url_for('')}'>"
  html << yield.to_s
  html << "</div>"
  html << model.page_footer_for(type, request).to_s
  html
end
redirect (type, obj)

Redirect to a page based on the type of action and the given object.

[show source]
# File lib/autoforme/action.rb, line 125
def redirect(type, obj)
  if redir = model.redirect_for
    path = redir.call(obj, type, request)
  end

  unless path
    path = case type
    when :new, :delete
      type.to_s
    when :edit
      "edit/#{model.primary_key_value(obj)}"
    when :mtm_edit
      "mtm_edit/#{model.primary_key_value(obj)}?association=#{params_association}"
    end
    path = url_for(path)
  end

  request.redirect(path)
  nil
end
show_page (obj)

The page to use when displaying an object, always used as a confirmation screen when deleting an object.

[show source]
# File lib/autoforme/action.rb, line 301
def show_page(obj)
  page do
    t = String.new
    f = Forme::Form.new(obj, :formatter=>:readonly, :wrapper=>:trtd, :labeler=>:explicit)
    t << "<table class=\"#{model.table_class_for(:show, request)}\">"
    model.columns_for(type, request).each do |column|
      col_opts = column_options_for(type, request, obj, column)
      if html = model.show_html_for(obj, column, type, request)
        col_opts = col_opts.merge(:html=>html)
      end
      t << f.input(column, col_opts).to_s
    end
    t << '</table>'
    if type == :show && model.supported_action?(:edit, request)
      t << Forme.form(form_attributes(:action=>url_for("edit/#{model.primary_key_value(obj)}")), form_opts) do |f1|
        f1.button(:value=>'Edit', :class=>'btn btn-primary')
      end.to_s
    end
    if type == :delete
      t << Forme.form(form_attributes(:action=>url_for("destroy/#{model.primary_key_value(obj)}"), :method=>:post), form_opts) do |f1|
        f1.button(:value=>'Delete', :class=>'btn btn-danger')
      end.to_s
    else
      t << association_links(obj)
    end
    t
  end
end
subtype ()

The subtype of request, used for #association_links and autocomplete actions.

[show source]
# File lib/autoforme/action.rb, line 120
def subtype
  ((t = request.params['type']) && ALL_SUPPORTED_ACTIONS.include?(t) && t.to_sym) || :edit
end
supported? ()

Return true if the action is supported, and false otherwise. An action may not be supported if the type is not one of the supported types, or if an non-idemponent request is issued with get instead of post, or potentionally other reasons.

As a side-effect, this sets up additional state related to the request.

[show source]
# File lib/autoforme/action.rb, line 54
def supported?
  return false unless ALL_SUPPORTED_ACTIONS.include?(request.action_type)
  type = @type = request.action_type.to_sym
  @normalized_type = NORMALIZED_ACTION_MAP.fetch(type, type)
  return false if NON_IDEMPOTENT_TYPES[type] && !request.post?

  case type
  when :mtm_edit
    return false unless model.supported_action?(type, request)
    if request.id && (assoc = request.params['association'])
      return false unless model.supported_mtm_edit?(assoc, request)
      @params_association = assoc.to_sym
    end

    @title = "#{model.class_name} - #{TITLE_MAP[type]}"
  when :mtm_update
    return false unless request.id && (assoc = request.params['association']) && model.supported_mtm_update?(assoc, request)
    @params_association = assoc.to_sym
  when :association_links
    @subtype = subtype
    return false unless model.supported_action?(@subtype, request)
  when :autocomplete
    if assoc = request.id
      return false unless model.association?(assoc)
      @params_association = assoc.to_sym
      @subtype = :association
      return false unless associated_class = model.associated_model_class(@params_association)
      return false unless associated_class.autocomplete_options_for(@subtype, request)
    else
      @subtype = subtype
      return false unless model.autocomplete_options_for(@subtype, request)
    end
  else
    return false unless model.supported_action?(normalized_type, request)

    if title = TITLE_MAP[type]
      @title = "#{model.class_name} - #{title}"
    end
  end

  true
end
tab_name (type)

The name to give the tab for the given type.

[show source]
# File lib/autoforme/action.rb, line 204
def tab_name(type)
  case type
  when :browse
    model.class_name
  when :mtm_edit
    'MTM'
  else
    type.to_s.capitalize
  end
end
table_page (next_page, objs)

Show page used for browse/search pages.

[show source]
# File lib/autoforme/action.rb, line 428
def table_page(next_page, objs)
  page do
    Table.new(self, objs).to_s << table_pager(normalized_type, next_page)
  end
end
table_pager (type, next_page)

HTML fragment for the table pager, showing links to next page or previous page for browse/search forms.

[show source]
# File lib/autoforme/action.rb, line 408
def table_pager(type, next_page)
  html = String.new
  html << '<ul class="pager">'
  page = request.id.to_i
  if page > 1
    html << "<li><a href=\"#{url_for("#{type}/#{page-1}?#{h request.query_string}")}\">Previous</a></li>"
  else
    html << '<li class="disabled"><a href="#">Previous</a></li>'
  end
  if next_page
    page = 1 if page < 1
    html << "<li><a href=\"#{url_for("#{type}/#{page+1}?#{h request.query_string}")}\">Next</a></li>"
  else
    html << '<li class="disabled"><a href="#">Next</a></li>'
  end
  html << "</ul>"
  html << "<p><a href=\"#{url_for("#{type}/csv?#{h request.query_string}")}\">CSV Format</a></p>"
end
tabs ()

HTML fragment for the default page header, which uses tabs for each supported action.

[show source]
# File lib/autoforme/action.rb, line 192
def tabs
  content = String.new
  content << '<ul class="nav nav-tabs">'
  Model::DEFAULT_SUPPORTED_ACTIONS.each do |action_type|
    if model.supported_action?(action_type, request)
      content << "<li class=\"#{'active' if type == action_type}\"><a href=\"#{url_for(action_type)}\">#{tab_name(action_type)}</a></li>"
    end
  end
  content << '</ul>'
end
url_for (page)

A path for the given page for the same model.

[show source]
# File lib/autoforme/action.rb, line 115
def url_for(page)
  base_url_for("#{model.link}/#{page}")
end