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
44 def initialize(model, request)
45   @model = model
46   @request = request
47 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
109 def base_url_for(page)
110   "#{request.path}#{model.framework.prefix}/#{page}"
111 end
column_label_for(type, request, model, column)

The label to use for the given column.

[show source]
    # File lib/autoforme/action.rb
171 def column_label_for(type, request, model, column)
172   unless label = model.column_options_for(type, request, column)[:label]
173     label = humanize(column)
174   end
175   label
176 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
158 def column_options_for(type, request, obj, column)
159   opts = model.column_options_for(type, request, column)
160   if opts[:class] == 'autoforme_autocomplete'
161     if type == :show
162       opts[:value] = model.column_value(type, request, obj, column)
163     elsif key = obj.send(model.association_key(column))
164       opts[:value] = "#{key} - #{model.column_value(type, request, obj, column)}"
165     end
166   end
167   opts
168 end
csv(meth)

Return a string in CSV format with the results

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

The page to use when editing the object.

[show source]
    # File lib/autoforme/action.rb
345 def edit_page(obj)
346   page do
347     t = String.new
348     form_attr = form_attributes(:action=>url_for("update/#{model.primary_key_value(obj)}"))
349     t << Forme.form(obj, form_attr, form_opts(form_attr[:action])) do |f|
350       model.columns_for(:edit, request).each do |column|
351         col_opts = column_options_for(:edit, request, obj, column)
352         if html = model.edit_html_for(obj, column, :edit, request)
353           col_opts = col_opts.merge(:html=>html)
354         end
355         f.input(column, col_opts)
356       end
357       f.button(:value=>'Update', :class=>'btn btn-primary')
358     end
359     if model.supported_action?(:delete, request)
360       t << Forme.form(form_attributes(:action=>url_for("delete/#{model.primary_key_value(obj)}")), form_opts) do |f|
361         f.button(:value=>'Delete', :class=>'btn btn-danger')
362       end
363     end
364     t << association_links(obj)
365   end
366 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
241 def form_attributes(attrs)
242   attrs.merge(model.form_attributes_for(type, request))
243 end
form_opts(action=nil)

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

[show source]
    # File lib/autoforme/action.rb
225 def form_opts(action=nil)
226   opts = model.form_options_for(type, request).dup
227 
228   opts[:_before_post] = lambda do |form|
229     if csrf_hash = request.csrf_token_hash(action)
230       csrf_hash.each do |name, value|
231         form.tag(:input, :type=>:hidden, :name=>name, :value=>value)
232       end
233     end
234   end
235 
236   opts
237 end
h(s)

Convert input to a string adn HTML escape it.

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

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

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

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

[show source]
    # File lib/autoforme/action.rb
565 def handle_autocomplete
566   if (query = request.params['q'].to_s).empty?
567     ''
568   else
569     model.autocomplete(:type=>@subtype, :request=>request, :association=>params_association, :query=>query, :exclude=>request.params['exclude']).join("\n")
570   end
571 end
handle_browse()

Handle browse action by showing a table containing model objects.

[show source]
    # File lib/autoforme/action.rb
462 def handle_browse
463   if request.id == 'csv'
464     csv(:browse)
465   else
466     table_page(*model.browse(type, request))
467   end
468 end
handle_create()

Handle the create action by creating a new model object.

[show source]
    # File lib/autoforme/action.rb
270 def handle_create
271   obj = model.new(nil, request)
272   model.set_fields(obj, :new, request, model_params)
273   model.hook(:before_create, request, obj)
274   if model.save(obj)
275     model.hook(:after_create, request, obj)
276     request.set_flash_notice("Created #{model.class_name}")
277     redirect(:new, obj)
278   else
279     request.set_flash_now_error("Error Creating #{model.class_name}")
280     new_page(obj)
281   end
282 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
395 def handle_delete
396   if request.id
397     handle_show
398   else
399     list_page(:delete, :form=>{:action=>url_for('delete')})
400   end
401 end
handle_destroy()

Handle the destroy action by destroying the model object.

[show source]
    # File lib/autoforme/action.rb
404 def handle_destroy
405   obj = model.with_pk(normalized_type, request, request.id)
406   model.hook(:before_destroy, request, obj)
407   model.destroy(obj)
408   model.hook(:after_destroy, request, obj)
409   request.set_flash_notice("Deleted #{model.class_name}")
410   redirect(:delete, obj)
411 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
369 def handle_edit
370   if request.id
371     obj = model.with_pk(normalized_type, request, request.id)
372     model.hook(:before_edit, request, obj)
373     edit_page(obj)
374   else
375     list_page(:edit)
376   end
377 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
495 def handle_mtm_edit
496   if request.id
497     obj = model.with_pk(:edit, request, request.id)
498     unless assoc = params_association
499       options = model.mtm_association_select_options(request)
500       if options.length == 1
501         assoc = options.first
502       end
503     end
504     if assoc
505       page do
506         t = String.new
507         t << "<h2>Edit #{humanize(assoc)} for #{h model.object_display_name(type, request, obj)}</h2>"
508         form_attr = form_attributes(:action=>url_for("mtm_update/#{model.primary_key_value(obj)}?association=#{assoc}"))
509         t << Forme.form(obj, form_attr, form_opts(form_attr[:action])) do |f|
510           opts = model.column_options_for(:mtm_edit, request, assoc)
511           add_opts = opts[:add] ? opts.merge(opts.delete(:add)) : opts
512           remove_opts = opts[:remove] ? opts.merge(opts.delete(:remove)) : opts
513           add_opts = {:name=>'add[]', :id=>'add', :label=>'Associate With'}.merge!(add_opts)
514           if model.association_autocomplete?(assoc, request)
515             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))
516           else
517             f.input(assoc, {:dataset=>model.unassociated_mtm_objects(request, assoc, obj), :size=>10}.merge!(add_opts))
518           end
519           f.input(assoc, {:name=>'remove[]', :id=>'remove', :label=>'Disassociate From', :dataset=>model.associated_mtm_objects(request, assoc, obj), :value=>[], :size=>10}.merge!(remove_opts))
520           f.button(:value=>'Update', :class=>'btn btn-primary')
521         end
522       end
523     else
524       page do
525         Forme.form(form_attributes(:action=>"mtm_edit/#{model.primary_key_value(obj)}"), form_opts) do |f|
526           f.input(:select, :options=>options, :name=>'association', :id=>'association', :label=>'Association', :add_blank=>true)
527           f.button(:value=>'Edit', :class=>'btn btn-primary')
528         end
529       end
530     end
531   else
532     list_page(:edit, :form=>{})
533   end
534 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
538 def handle_mtm_update
539   obj = model.with_pk(:edit, request, request.id)
540   assoc = params_association
541   assoc_obj = model.mtm_update(request, assoc, obj, request.params['add'], request.params['remove'])
542   request.set_flash_notice("Updated #{assoc} association for #{model.class_name}") unless request.xhr?
543   if request.xhr?
544     if request.params['add']
545       @type = :edit
546       mtm_edit_remove(assoc, model.associated_model_class(assoc), obj, assoc_obj)
547     else
548       "<option value=\"#{model.primary_key_value(assoc_obj)}\">#{model.associated_object_display_name(assoc, request, assoc_obj)}</option>"
549     end
550   elsif request.params['redir'] == 'edit'
551     redirect(:edit, obj)
552   else
553     redirect(:mtm_edit, obj)
554   end
555 end
handle_new()

Handle the new action by always showing the new form.

[show source]
    # File lib/autoforme/action.rb
263 def handle_new
264   obj = model.new(request.params[model.link], request)
265   model.hook(:before_new, request, obj)
266   new_page(obj)
267 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
336 def handle_show
337   if request.id
338     show_page(model.with_pk(normalized_type, request, request.id))
339   else
340     list_page(:show)
341   end
342 end
handle_update()

Handle the update action by updating the current model object.

[show source]
    # File lib/autoforme/action.rb
380 def handle_update
381   obj = model.with_pk(normalized_type, request, request.id)
382   model.set_fields(obj, :edit, request, model_params)
383   model.hook(:before_update, request, obj)
384   if model.save(obj)
385     model.hook(:after_update, request, obj)
386     request.set_flash_notice("Updated #{model.class_name}")
387     redirect(:edit, obj)
388   else
389     request.set_flash_now_error("Error Updating #{model.class_name}")
390     edit_page(obj)
391   end
392 end
humanize(string)

Convert the given object into a suitable human readable string.

[show source]
    # File lib/autoforme/action.rb
152 def humanize(string)
153   string = string.to_s
154   string.respond_to?(:humanize) ? string.humanize : string.gsub(/_/, " ").capitalize
155 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
665 def inline_mtm_edit_forms(obj)
666   assocs = model.inline_mtm_assocs(request)
667   return if assocs.empty?
668 
669   t = String.new
670   t << "<div class='inline_mtm_add_associations'>"
671   assocs.each do |assoc|
672     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")
673     t << Forme.form(obj, form_attr, form_opts(form_attr[:action])) do |f|
674       opts = model.column_options_for(:mtm_edit, request, assoc)
675       add_opts = opts[:add] ? opts.merge(opts.delete(:add)) : opts.dup
676       add_opts = {:name=>'add[]', :id=>"add_#{assoc}"}.merge!(add_opts)
677       if model.association_autocomplete?(assoc, request)
678         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))
679       else
680         f.input(assoc, {:dataset=>model.unassociated_mtm_objects(request, assoc, obj), :multiple=>false, :add_blank=>true}.merge!(add_opts))
681       end
682       f.button(:value=>'Add', :class=>'btn btn-xs btn-primary')
683     end
684   end
685   t << "</div>"
686   t << "<div class='inline_mtm_remove_associations'><ul>"
687   assocs.each do |assoc|
688     mc = model.associated_model_class(assoc)
689     t << "<li>"
690     t << association_class_link(mc, assoc)
691     t << "<ul id='#{assoc}_remove_list'>"
692     obj.send(assoc).each do |assoc_obj|
693       t << mtm_edit_remove(assoc, mc, obj, assoc_obj)
694     end
695     t << "</ul></li>"
696   end
697   t << "</ul></div>"
698 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
286 def list_page(type, opts={})
287   page do
288     form_attr = form_attributes(opts[:form] || {:action=>url_for(type)})
289     Forme.form(form_attr, form_opts) do |f|
290       input_opts = {:name=>'id', :id=>'id', :label=>model.class_name}
291       if model.autocomplete_options_for(type, request)
292         input_type = :text
293         input_opts.merge!(:class=>'autoforme_autocomplete', :attr=>{'data-type'=>type})
294       else
295         input_type = :select
296         input_opts.merge!(:options=>model.select_options(type, request), :add_blank=>true)
297       end
298       f.input(input_type, input_opts)
299       f.button(:value=>type.to_s.capitalize, :class=>"btn btn-#{type == :delete ? 'danger' : 'primary'}")
300     end
301   end
302 end
model_params()

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

[show source]
    # File lib/autoforme/action.rb
103 def model_params
104   request.params[model.params_name]
105 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
701 def mtm_edit_remove(assoc, mc, obj, assoc_obj)
702   t = String.new
703   t << "<li>"
704   t << association_link(mc, assoc_obj)
705   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}")
706   t << Forme.form(form_attr, form_opts(form_attr[:action])) do |f|
707     f.button(:value=>'Remove', :class=>'btn btn-xs btn-danger')
708   end
709   t << "</li>"
710 end
new_page(obj, opts={})

HTML content used for the new action

[show source]
    # File lib/autoforme/action.rb
246 def new_page(obj, opts={})
247   page do
248     form_attr = form_attributes(:action=>url_for("create"))
249     Forme.form(obj, form_attr, form_opts(form_attr[:action])) do |f|
250       model.columns_for(:new, request).each do |column|
251         col_opts = column_options_for(:new, request, obj, column)
252         if html = model.edit_html_for(obj, column, :new, request)
253           col_opts = col_opts.merge(:html=>html)
254         end
255         f.input(column, col_opts)
256       end
257       f.button(:value=>'Create', :class=>'btn btn-primary')
258     end
259   end
260 end
page()

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

[show source]
    # File lib/autoforme/action.rb
214 def page
215   html = String.new
216   html << (model.page_header_for(type, request) || tabs)
217   html << "<div id='autoforme_content' data-url='#{url_for('')}'>"
218   html << yield.to_s
219   html << "</div>"
220   html << model.page_footer_for(type, request).to_s
221   html
222 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
124 def redirect(type, obj)
125   if redir = model.redirect_for
126     path = redir.call(obj, type, request)
127   end
128 
129   unless path
130     path = case type
131     when :new, :delete
132       type.to_s
133     when :edit
134       "edit/#{model.primary_key_value(obj)}"
135     else # when :mtm_edit
136       "mtm_edit/#{model.primary_key_value(obj)}?association=#{params_association}"
137     end
138     path = url_for(path)
139   end
140 
141   request.redirect(path)
142 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
305 def show_page(obj)
306   page do
307     t = String.new
308     f = Forme::Form.new(obj, :formatter=>:readonly, :wrapper=>:trtd, :labeler=>:explicit)
309     t << "<table class=\"#{model.table_class_for(:show, request)}\">"
310     model.columns_for(type, request).each do |column|
311       col_opts = column_options_for(type, request, obj, column)
312       if html = model.show_html_for(obj, column, type, request)
313         col_opts = col_opts.merge(:html=>html)
314       end
315       t << f.input(column, col_opts)
316     end
317     t << '</table>'
318     if type == :show && model.supported_action?(:edit, request)
319       t << Forme.form(form_attributes(:action=>url_for("edit/#{model.primary_key_value(obj)}")), form_opts) do |f1|
320         f1.button(:value=>'Edit', :class=>'btn btn-primary')
321       end
322     end
323     if type == :delete
324       form_attr = form_attributes(:action=>url_for("destroy/#{model.primary_key_value(obj)}"), :method=>:post)
325       t << Forme.form(form_attr, form_opts(form_attr[:action])) do |f1|
326         f1.button(:value=>'Delete', :class=>'btn btn-danger')
327       end
328     else
329       t << association_links(obj)
330     end
331     t
332   end
333 end
subtype()

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

[show source]
    # File lib/autoforme/action.rb
119 def subtype
120   ((t = request.params['type']) && ALL_SUPPORTED_ACTIONS.include?(t) && t.to_sym) || :edit
121 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
55 def supported?
56   return false unless ALL_SUPPORTED_ACTIONS.include?(request.action_type)
57   type = @type = request.action_type.to_sym
58   @normalized_type = NORMALIZED_ACTION_MAP.fetch(type, type)
59   return false if NON_IDEMPOTENT_TYPES[type] && !request.post?
60 
61   case type
62   when :mtm_edit
63     return false unless model.supported_action?(type, request)
64     if request.id && (assoc = request.params['association'])
65       return false unless model.supported_mtm_edit?(assoc, request)
66       @params_association = assoc.to_sym
67     end
68 
69     @title = "#{model.class_name} - #{TITLE_MAP[type]}"
70   when :mtm_update
71     return false unless request.id && (assoc = request.params['association']) && model.supported_mtm_update?(assoc, request)
72     @params_association = assoc.to_sym
73   when :association_links
74     @subtype = subtype
75     return false unless model.supported_action?(@subtype, request)
76   when :autocomplete
77     if assoc = request.id
78       return false unless model.association?(assoc)
79       @params_association = assoc.to_sym
80       @subtype = :association
81       return false unless associated_class = model.associated_model_class(@params_association)
82       return false unless associated_class.autocomplete_options_for(@subtype, request)
83     else
84       @subtype = subtype
85       return false unless model.autocomplete_options_for(@subtype, request)
86     end
87   else
88     return false unless model.supported_action?(normalized_type, request)
89 
90     @title = "#{model.class_name} - #{TITLE_MAP[@normalized_type]}"
91   end
92 
93   true
94 end
tab_name(type)

The name to give the tab for the given type.

[show source]
    # File lib/autoforme/action.rb
202 def tab_name(type)
203   case type
204   when :browse
205     model.class_name
206   when :mtm_edit
207     'MTM'
208   else
209     type.to_s.capitalize
210   end
211 end
table_page(next_page, objs)

Show page used for browse/search pages.

[show source]
    # File lib/autoforme/action.rb
455 def table_page(next_page, objs)
456   page do
457     Table.new(self, objs).to_s << table_pager(normalized_type, next_page)
458   end
459 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
414 def table_pager(type, next_page)
415   html = String.new
416   html << '<ul class="pager">'
417   qs = next_qs = h request.query_string
418 
419   if next_page.is_a?(Array)
420     # Filter pagination, next_page contains the values for the last record of current page
421     no_previous = true
422     if next_page.empty?
423       next_page = nil
424     else
425       next_qs = qs.dup
426       params = request.params.dup
427       next_page = next_page.map(&:to_s)
428       next_page = next_page[0] if next_page.length == 1
429       params["_after"] = next_page
430       next_qs = h Rack::Utils.build_nested_query(params)
431       following = prev = "0"
432     end
433   else
434     # Offset pagination, request id contains current page
435     page = request.id.to_i
436     page = 1 if page < 1
437     no_previous = page <= 1
438     prev = page-1
439     following = page+1
440   end
441 
442   unless no_previous
443     html << "<li><a href=\"#{url_for("#{type}/#{prev}?#{qs}")}\">Previous</a></li>"
444   end
445 
446   if next_page
447     html << "<li><a href=\"#{url_for("#{type}/#{following}?#{next_qs}")}\">Next</a></li>"
448   end
449 
450   html << "</ul>"
451   html << "<p><a href=\"#{url_for("#{type}/csv?#{qs}")}\">CSV Format</a></p>"
452 end
tabs()

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

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

A path for the given page for the same model.

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