autoforme 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,629 @@
1
+ module AutoForme
2
+ # Represents an action on a model in response to a web request.
3
+ class Action
4
+ # The AutoForme::Model instance related to the current action
5
+ attr_reader :model
6
+
7
+ # The normalized type symbol related to the current action, so that paired actions
8
+ # such as new and create both use :new.
9
+ attr_reader :normalized_type
10
+
11
+ # An association symbol for the current related association.
12
+ attr_reader :params_association
13
+
14
+ # The AutoForme::Request instance related to the current action
15
+ attr_reader :request
16
+
17
+ # An string suitable for use as the HTML title on the displayed page
18
+ attr_reader :title
19
+
20
+ # The type symbols related to the current action (e.g. :new, :create).
21
+ attr_reader :type
22
+
23
+ # Array of strings for all action types currently supported
24
+ ALL_SUPPORTED_ACTIONS = %w'new create show edit update delete destroy browse search mtm_edit mtm_update association_links autocomplete'.freeze
25
+
26
+ # Map of regular type symbols to normalized type symbols
27
+ NORMALIZED_ACTION_MAP = {:create=>:new, :update=>:edit, :destroy=>:delete, :mtm_update=>:mtm_edit}
28
+
29
+ # Map of type symbols to HTML titles
30
+ TITLE_MAP = {:new=>'New', :show=>'Show', :edit=>'Edit', :delete=>'Delete', :browse=>'Browse', :search=>'Search', :mtm_edit=>'Many To Many Edit'}
31
+
32
+ # Creates a new action for the model and request. This action is not
33
+ # usable unless supported? is called first and it returns true.
34
+ def initialize(model, request)
35
+ @model = model
36
+ @request = request
37
+ end
38
+
39
+ # Return true if the action is supported, and false otherwise. An action
40
+ # may not be supported if the type is not one of the supported types, or
41
+ # if an non-idemponent request is issued with get instead of post, or
42
+ # potentionally other reasons.
43
+ #
44
+ # As a side-effect, this sets up additional state related to the request.
45
+ def supported?
46
+ return false unless idempotent? || request.post?
47
+ return false unless ALL_SUPPORTED_ACTIONS.include?(request.action_type)
48
+
49
+ @type = request.action_type.to_sym
50
+ @normalized_type = NORMALIZED_ACTION_MAP.fetch(@type, @type)
51
+
52
+ case type
53
+ when :mtm_edit
54
+ return false unless model.supported_action?(type, request)
55
+ if request.id && (assoc = request.params['association'])
56
+ return false unless model.supported_mtm_edit?(assoc, request)
57
+ @params_association = assoc.to_sym
58
+ end
59
+
60
+ @title = "#{model.class_name} - #{TITLE_MAP[type]}"
61
+ when :mtm_update
62
+ return false unless request.id && (assoc = request.params['association']) && model.supported_mtm_update?(assoc, request)
63
+ @params_association = assoc.to_sym
64
+ when :association_links
65
+ @subtype = subtype
66
+ return false unless model.supported_action?(@subtype, request)
67
+ when :autocomplete
68
+ if assoc = request.id
69
+ return false unless model.association?(assoc)
70
+ @params_association = assoc.to_sym
71
+ end
72
+ @subtype = subtype
73
+ return false unless model.autocomplete_options_for(@subtype, request)
74
+ else
75
+ return false unless model.supported_action?(normalized_type, request)
76
+
77
+ if title = TITLE_MAP[type]
78
+ @title = "#{model.class_name} - #{title}"
79
+ end
80
+ end
81
+
82
+ true
83
+ end
84
+
85
+ # Convert input to a string adn HTML escape it.
86
+ def h(s)
87
+ Rack::Utils.escape_html(s.to_s)
88
+ end
89
+
90
+ # Return whether the current action is an idempotent action.
91
+ def idempotent?
92
+ type == normalized_type
93
+ end
94
+
95
+ # Get request parameters for the model. Used when retrieving form
96
+ # values that use namespacing.
97
+ def model_params
98
+ request.params[model.params_name]
99
+ end
100
+
101
+ # A path for the given page tied to the framework, but not the current model.
102
+ # Used for linking to other models in the same framework.
103
+ def base_url_for(page)
104
+ "#{request.path}#{model.framework.prefix}/#{page}"
105
+ end
106
+
107
+ # A path for the given page for the same model.
108
+ def url_for(page)
109
+ base_url_for("#{model.link}/#{page}")
110
+ end
111
+
112
+ # The subtype of request, used for association_links and autocomplete actions.
113
+ def subtype
114
+ ((t = request.params['type']) && ALL_SUPPORTED_ACTIONS.include?(t) && t.to_sym) || :edit
115
+ end
116
+
117
+ # Redirect to a page based on the type of action and the given object.
118
+ def redirect(type, obj)
119
+ if redir = model.redirect_for
120
+ path = redir.call(obj, type, request)
121
+ end
122
+
123
+ unless path
124
+ path = case type
125
+ when :new, :delete
126
+ type.to_s
127
+ when :edit
128
+ "edit/#{model.primary_key_value(obj)}"
129
+ when :mtm_edit
130
+ "mtm_edit/#{model.primary_key_value(obj)}?association=#{params_association}"
131
+ else
132
+ raise Error, "Unhandled redirect type: #{type.inspect}"
133
+ end
134
+ path = url_for(path)
135
+ end
136
+
137
+ request.redirect(path)
138
+ nil
139
+ end
140
+
141
+ # Handle the current action, returning an HTML string containing the page content,
142
+ # or redirecting.
143
+ def handle
144
+ model.before_action_hook(type, request)
145
+ send("handle_#{type}")
146
+ end
147
+
148
+ # Convert the given object into a suitable human readable string.
149
+ def humanize(string)
150
+ string = string.to_s
151
+ string.respond_to?(:humanize) ? string.humanize : string.gsub(/_/, " ").capitalize
152
+ end
153
+
154
+ # The options to use for the given column, which will be passed to Forme::Form#input.
155
+ def column_options_for(type, request, obj, column)
156
+ opts = model.column_options_for(type, request, column)
157
+ if opts[:class] == 'autoforme_autocomplete'
158
+ if type == :show
159
+ opts[:value] = model.column_value(type, request, obj, column)
160
+ elsif key = obj.send(model.association_key(column))
161
+ opts[:value] = "#{key} - #{model.column_value(type, request, obj, column)}"
162
+ end
163
+ end
164
+ opts
165
+ end
166
+
167
+ # The label to use for the given column.
168
+ def column_label_for(type, request, model, column)
169
+ unless label = model.column_options_for(type, request, column)[:label]
170
+ label = humanize(column)
171
+ end
172
+ label
173
+ end
174
+
175
+ # HTML fragment for the default page header, which uses tabs for each supported action.
176
+ def tabs
177
+ content = '<ul class="nav nav-tabs">'
178
+ Model::DEFAULT_SUPPORTED_ACTIONS.each do |action_type|
179
+ if model.supported_action?(action_type, request)
180
+ content << "<li class=\"#{'active' if type == action_type}\"><a href=\"#{url_for(action_type)}\">#{tab_name(action_type)}</a></li>"
181
+ end
182
+ end
183
+ content << '</ul>'
184
+ end
185
+
186
+ # The name to give the tab for the given type.
187
+ def tab_name(type)
188
+ case type
189
+ when :browse
190
+ model.class_name
191
+ when :mtm_edit
192
+ 'MTM'
193
+ else
194
+ type.to_s.capitalize
195
+ end
196
+ end
197
+
198
+ # Yields and wraps the returned data in a header and footer for the page.
199
+ def page
200
+ html = ''
201
+ html << (model.page_header_for(type, request) || tabs)
202
+ html << "<div id='autoforme_content' data-url='#{url_for('')}'>"
203
+ html << yield.to_s
204
+ html << "</div>"
205
+ html << model.page_footer_for(type, request).to_s
206
+ html
207
+ end
208
+
209
+ # Options to use for the form. If the form uses POST, automatically adds the CSRF token.
210
+ def form_opts
211
+ opts = model.form_options_for(type, request).dup
212
+ hidden_tags = opts[:hidden_tags] = []
213
+ if csrf = request.csrf_token_hash
214
+ hidden_tags << lambda{|tag| csrf if tag.attr[:method].to_s.upcase == 'POST'}
215
+ end
216
+ opts
217
+ end
218
+
219
+ # Merge the model's form attributes into the given form attributes, yielding the
220
+ # attributes to use for the form.
221
+ def form_attributes(attrs)
222
+ attrs.merge(model.form_attributes_for(type, request))
223
+ end
224
+
225
+ # HTML content used for the new action
226
+ def new_page(obj, opts={})
227
+ page do
228
+ Forme.form(obj, form_attributes(:action=>url_for("create")), form_opts) do |f|
229
+ model.columns_for(:new, request).each do |column|
230
+ f.input(column, column_options_for(:new, request, obj, column))
231
+ end
232
+ f.button(:value=>'Create', :class=>'btn btn-primary')
233
+ end
234
+ end
235
+ end
236
+
237
+ # Handle the new action by always showing the new form.
238
+ def handle_new
239
+ obj = model.new(request.params[model.link], request)
240
+ model.hook(:before_new, request, obj)
241
+ new_page(obj)
242
+ end
243
+
244
+ # Handle the create action by creating a new model object.
245
+ def handle_create
246
+ obj = model.new(nil, request)
247
+ model.set_fields(obj, :new, request, model_params)
248
+ model.hook(:before_create, request, obj)
249
+ if model.save(obj)
250
+ model.hook(:after_create, request, obj)
251
+ request.set_flash_notice("Created #{model.class_name}")
252
+ redirect(:new, obj)
253
+ else
254
+ request.set_flash_now_error("Error Creating #{model.class_name}")
255
+ new_page(obj)
256
+ end
257
+ end
258
+
259
+ # Shared page used by show, edit, and delete actions that shows a list of available
260
+ # model objects (or an autocompleting box), and allows the user to choose one to act on.
261
+ def list_page(type, opts={})
262
+ page do
263
+ form_attr = form_attributes(opts[:form] || {:action=>url_for(type)})
264
+ Forme.form(form_attr, form_opts) do |f|
265
+ input_opts = {:name=>'id', :id=>'id', :label=>model.class_name}
266
+ if model.autocomplete_options_for(type, request)
267
+ input_type = :text
268
+ input_opts.merge!(:class=>'autoforme_autocomplete', :attr=>{'data-type'=>type})
269
+ else
270
+ input_type = :select
271
+ input_opts.merge!(:options=>model.select_options(type, request), :add_blank=>true)
272
+ end
273
+ f.input(input_type, input_opts)
274
+ f.button(:value=>type.to_s.capitalize, :class=>"btn btn-#{type == :delete ? 'danger' : 'primary'}")
275
+ end
276
+ end
277
+ end
278
+
279
+ # The page to use when displaying an object, always used as a confirmation screen when deleting an object.
280
+ def show_page(obj)
281
+ page do
282
+ t = ''
283
+ f = Forme::Form.new(obj, :formatter=>:readonly, :wrapper=>:trtd)
284
+ t << "<table class=\"#{model.table_class_for(:show, request)}\">"
285
+ model.columns_for(type, request).each do |column|
286
+ t << f.input(column, column_options_for(:show, request, obj, column)).to_s
287
+ end
288
+ t << '</table>'
289
+ if type == :show && model.supported_action?(:edit, request)
290
+ t << Forme.form(form_attributes(:action=>url_for("edit/#{model.primary_key_value(obj)}")), form_opts) do |f|
291
+ f.button(:value=>'Edit', :class=>'btn btn-primary')
292
+ end.to_s
293
+ end
294
+ if type == :delete
295
+ t << Forme.form(form_attributes(:action=>url_for("destroy/#{model.primary_key_value(obj)}"), :method=>:post), form_opts) do |f|
296
+ f.button(:value=>'Delete', :class=>'btn btn-danger')
297
+ end.to_s
298
+ else
299
+ t << association_links(obj)
300
+ end
301
+ t
302
+ end
303
+ end
304
+
305
+ # Handle the show action by showing a list page if there is no model object selected, or the show page if there is one.
306
+ def handle_show
307
+ if request.id
308
+ show_page(model.with_pk(normalized_type, request, request.id))
309
+ else
310
+ list_page(:show)
311
+ end
312
+ end
313
+
314
+ # The page to use when editing the object.
315
+ def edit_page(obj)
316
+ page do
317
+ t = Forme.form(obj, form_attributes(:action=>url_for("update/#{model.primary_key_value(obj)}")), form_opts) do |f|
318
+ model.columns_for(:edit, request).each do |column|
319
+ f.input(column, column_options_for(:edit, request, obj, column))
320
+ end
321
+ f.button(:value=>'Update', :class=>'btn btn-primary')
322
+ end.to_s
323
+ if model.supported_action?(:delete, request)
324
+ t << Forme.form(form_attributes(:action=>url_for("delete/#{model.primary_key_value(obj)}")), form_opts) do |f|
325
+ f.button(:value=>'Delete', :class=>'btn btn-danger')
326
+ end.to_s
327
+ end
328
+ t << association_links(obj)
329
+ end
330
+ end
331
+
332
+ # Handle the edit action by showing a list page if there is no model object selected, or the edit page if there is one.
333
+ def handle_edit
334
+ if request.id
335
+ obj = model.with_pk(normalized_type, request, request.id)
336
+ model.hook(:before_edit, request, obj)
337
+ edit_page(obj)
338
+ else
339
+ list_page(:edit)
340
+ end
341
+ end
342
+
343
+ # Handle the update action by updating the current model object.
344
+ def handle_update
345
+ obj = model.with_pk(normalized_type, request, request.id)
346
+ model.set_fields(obj, :edit, request, model_params)
347
+ model.hook(:before_update, request, obj)
348
+ if model.save(obj)
349
+ model.hook(:after_update, request, obj)
350
+ request.set_flash_notice("Updated #{model.class_name}")
351
+ redirect(:edit, obj)
352
+ else
353
+ request.set_flash_now_error("Error Updating #{model.class_name}")
354
+ edit_page(obj)
355
+ end
356
+ end
357
+
358
+ # Handle the edit action by showing a list page if there is no model object selected, or a confirmation screen if there is one.
359
+ def handle_delete
360
+ if request.id
361
+ handle_show
362
+ else
363
+ list_page(:delete, :form=>{:action=>url_for('delete')})
364
+ end
365
+ end
366
+
367
+ # Handle the destroy action by destroying the model object.
368
+ def handle_destroy
369
+ obj = model.with_pk(normalized_type, request, request.id)
370
+ model.hook(:before_destroy, request, obj)
371
+ model.destroy(obj)
372
+ model.hook(:after_destroy, request, obj)
373
+ request.set_flash_notice("Deleted #{model.class_name}")
374
+ redirect(:delete, obj)
375
+ end
376
+
377
+ # HTML fragment for the table pager, showing links to next page or previous page for browse/search forms.
378
+ def table_pager(type, next_page)
379
+ html = '<ul class="pager">'
380
+ page = request.id.to_i
381
+ if page > 1
382
+ html << "<li><a href=\"#{url_for("#{type}/#{page-1}?#{h request.query_string}")}\">Previous</a></li>"
383
+ else
384
+ html << '<li class="disabled"><a href="#">Previous</a></li>'
385
+ end
386
+ if next_page
387
+ page = 1 if page < 1
388
+ html << "<li><a href=\"#{url_for("#{type}/#{page+1}?#{h request.query_string}")}\">Next</a></li>"
389
+ else
390
+ html << '<li class="disabled"><a href="#">Next</a></li>'
391
+ end
392
+ html << "</ul>"
393
+ end
394
+
395
+ # Show page used for browse/search pages.
396
+ def table_page(next_page, objs)
397
+ page do
398
+ Table.new(self, objs).to_s << table_pager(normalized_type, next_page)
399
+ end
400
+ end
401
+
402
+ # Handle browse action by showing a table containing model objects.
403
+ def handle_browse
404
+ table_page(*model.browse(type, request))
405
+ end
406
+
407
+ # Handle browse action by showing a search form if no page is selected, or the correct page of search results
408
+ # if there is a page selected.
409
+ def handle_search
410
+ if request.id
411
+ table_page(*model.search_results(normalized_type, request))
412
+ else
413
+ page do
414
+ Forme.form(model.new(nil, request), form_attributes(:action=>url_for("search/1"), :method=>:get), form_opts) do |f|
415
+ model.columns_for(:search_form, request).each do |column|
416
+ f.input(column, {:name=>column, :id=>column}.merge(column_options_for(:search_form, request, f.obj, column)))
417
+ end
418
+ f.button(:value=>'Search', :class=>'btn btn-primary')
419
+ end
420
+ end
421
+ end
422
+ end
423
+
424
+ # Handle the mtm_edit action by showing a list page if there is no model object selected, a list of associations for that model
425
+ # if there is a model object but no association selected, or a mtm_edit form if there is a model object and association selected.
426
+ def handle_mtm_edit
427
+ if id = request.id
428
+ obj = model.with_pk(:edit, request, request.id)
429
+ if assoc = params_association
430
+ page do
431
+ Forme.form(obj, form_attributes(:action=>url_for("mtm_update/#{model.primary_key_value(obj)}?association=#{assoc}")), form_opts) do |f|
432
+ opts = model.column_options_for(:mtm_edit, request, assoc)
433
+ add_opts = opts[:add] ? opts.merge(opts.delete(:add)) : opts
434
+ remove_opts = opts[:remove] ? opts.merge(opts.delete(:remove)) : opts
435
+ add_opts = {:name=>'add[]', :id=>'add', :label=>'Associate With'}.merge(add_opts)
436
+ if model.association_autocomplete?(assoc, request)
437
+ 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))
438
+ else
439
+ f.input(assoc, {:dataset=>model.unassociated_mtm_objects(request, assoc, obj)}.merge(add_opts))
440
+ end
441
+ f.input(assoc, {:name=>'remove[]', :id=>'remove', :label=>'Disassociate From', :dataset=>model.associated_mtm_objects(request, assoc, obj), :value=>[]}.merge(remove_opts))
442
+ f.button(:value=>'Update', :class=>'btn btn-primary')
443
+ end
444
+ end
445
+ else
446
+ page do
447
+ Forme.form(form_attributes(:action=>"mtm_edit/#{model.primary_key_value(obj)}"), form_opts) do |f|
448
+ f.input(:select, :options=>model.mtm_association_select_options(request), :name=>'association', :id=>'association', :label=>'Association')
449
+ f.button(:value=>'Edit', :class=>'btn btn-primary')
450
+ end
451
+ end
452
+ end
453
+ else
454
+ list_page(:edit, :form=>{})
455
+ end
456
+ end
457
+
458
+ # Handle mtm_update action by updating the related many to many association. For ajax requests,
459
+ # return an HTML fragment to update the page, otherwise redirect to the appropriate form.
460
+ def handle_mtm_update
461
+ obj = model.with_pk(:edit, request, request.id)
462
+ assoc = params_association
463
+ assoc_obj = model.mtm_update(request, assoc, obj, request.params['add'], request.params['remove'])
464
+ request.set_flash_notice("Updated #{assoc} association for #{model.class_name}") unless request.xhr?
465
+ if request.xhr?
466
+ if add = request.params['add']
467
+ @type = :edit
468
+ mtm_edit_remove(assoc, model.associated_model_class(assoc), obj, assoc_obj)
469
+ else
470
+ "<option value=\"#{model.primary_key_value(assoc_obj)}\">#{model.associated_object_display_name(assoc, request, assoc_obj)}</option>"
471
+ end
472
+ elsif request.params['redir'] == 'edit'
473
+ redirect(:edit, obj)
474
+ else
475
+ redirect(:mtm_edit, obj)
476
+ end
477
+ end
478
+
479
+ # Handle association_links action by returning an HTML fragment of association links.
480
+ def handle_association_links
481
+ @type = @normalized_type = @subtype
482
+ obj = model.with_pk(@type, request, request.id)
483
+ association_links(obj)
484
+ end
485
+
486
+ # Handle autocomplete action by returning a string with one line per model object.
487
+ def handle_autocomplete
488
+ unless (query = request.params['q'].to_s).empty?
489
+ model.autocomplete(:type=>@subtype, :request=>request, :association=>params_association, :query=>query, :exclude=>request.params['exclude']).join("\n")
490
+ end
491
+ end
492
+
493
+ # HTML fragment containing the association links for the given object, or a link to lazily load them
494
+ # if configured. Also contains the inline mtm_edit forms when editing.
495
+ def association_links(obj)
496
+ if model.lazy_load_association_links?(type, request) && normalized_type != :association_links && request.params['associations'] != 'show'
497
+ "<div id='lazy_load_association_links' data-object='#{model.primary_key_value(obj)}' data-type='#{type}'><a href=\"#{url_for("#{type}/#{model.primary_key_value(obj)}?associations=show")}\">Show Associations</a></div>"
498
+ elsif type == :show
499
+ association_link_list(obj).to_s
500
+ else
501
+ "#{inline_mtm_edit_forms(obj)}#{association_link_list(obj)}"
502
+ end
503
+ end
504
+
505
+ # HTML fragment for the list of association links, allowing quick access to associated models and objects.
506
+ def association_link_list(obj)
507
+ assocs = model.association_links_for(type, request)
508
+ return if assocs.empty?
509
+ read_only = type == :show
510
+ t = '<h3 class="associated_records_header">Associated Records</h3>'
511
+ t << "<ul class='association_links'>\n"
512
+ assocs.each do |assoc|
513
+ mc = model.associated_model_class(assoc)
514
+ t << "<li>"
515
+ t << association_class_link(mc, assoc)
516
+ t << "\n "
517
+
518
+ case model.association_type(assoc)
519
+ when :one
520
+ if assoc_obj = obj.send(assoc)
521
+ t << " - "
522
+ t << association_link(mc, assoc_obj)
523
+ end
524
+ assoc_objs = []
525
+ when :edit
526
+ if !read_only && model.supported_mtm_edit?(assoc.to_s, request)
527
+ t << "(<a href=\"#{url_for("mtm_edit/#{model.primary_key_value(obj)}?association=#{assoc}")}\">associate</a>)"
528
+ end
529
+ assoc_objs = obj.send(assoc)
530
+ when :new
531
+ if !read_only && mc && mc.supported_action?(:new, request)
532
+ params = model.associated_new_column_values(obj, assoc).map do |col, value|
533
+ "#{mc.link}%5b#{col}%5d=#{value}"
534
+ end
535
+ t << "(<a href=\"#{base_url_for("#{mc.link}/new?#{params.join('&amp;')}")}\">create</a>)"
536
+ end
537
+ assoc_objs = obj.send(assoc)
538
+ else
539
+ assoc_objs = []
540
+ end
541
+
542
+ unless assoc_objs.empty?
543
+ t << "<ul>\n"
544
+ assoc_objs.each do |assoc_obj|
545
+ t << "<li>"
546
+ t << association_link(mc, assoc_obj)
547
+ t << "</li>"
548
+ end
549
+ t << "</ul>"
550
+ end
551
+
552
+ t << "</li>"
553
+ end
554
+ t << "</ul>"
555
+ end
556
+
557
+ # If the framework contains the associated model class and that supports browsing,
558
+ # return a link to the associated browse page, otherwise, just return the name.
559
+ def association_class_link(mc, assoc)
560
+ assoc_name = humanize(assoc)
561
+ if mc && mc.supported_action?(:browse, request)
562
+ "<a href=\"#{base_url_for("#{mc.link}/browse")}\">#{assoc_name}</a>"
563
+ else
564
+ assoc_name
565
+ end
566
+ end
567
+
568
+ # For the given associated object, if the framework contains the associated model class,
569
+ # and that supports the type of action we are doing, return a link to the associated action
570
+ # page.
571
+ def association_link(mc, assoc_obj)
572
+ if mc
573
+ t = mc.object_display_name(:association, request, assoc_obj)
574
+ if mc.supported_action?(type, request)
575
+ t = "<a href=\"#{base_url_for("#{mc.link}/#{type}/#{mc.primary_key_value(assoc_obj)}")}\">#{t}</a>"
576
+ end
577
+ t
578
+ else
579
+ model.default_object_display_name(assoc_obj)
580
+ end
581
+ end
582
+
583
+ # HTML fragment used for the inline mtm_edit forms on the edit page.
584
+ def inline_mtm_edit_forms(obj)
585
+ assocs = model.inline_mtm_assocs(request)
586
+ return if assocs.empty?
587
+
588
+ t = "<div class='inline_mtm_add_associations'>"
589
+ assocs.each do |assoc|
590
+ form_attr = form_attributes(:action=>url_for("mtm_update/#{model.primary_key_value(obj)}?association=#{assoc}&amp;redir=edit"), :class => 'mtm_add_associations', 'data-remove' => "##{assoc}_remove_list")
591
+ t << Forme.form(obj, form_attr, form_opts) do |f|
592
+ opts = model.column_options_for(:mtm_edit, request, assoc)
593
+ add_opts = opts[:add] ? opts.merge(opts.delete(:add)) : opts.dup
594
+ add_opts = {:name=>'add[]', :id=>"add_#{assoc}"}.merge(add_opts)
595
+ if model.association_autocomplete?(assoc, request)
596
+ 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))
597
+ else
598
+ f.input(assoc, {:dataset=>model.unassociated_mtm_objects(request, assoc, obj), :multiple=>false, :add_blank=>true}.merge(add_opts))
599
+ end
600
+ f.button(:value=>'Add', :class=>'btn btn-mini btn-primary')
601
+ end.to_s
602
+ end
603
+ t << "</div>"
604
+ t << "<div class='inline_mtm_remove_associations'><ul>"
605
+ assocs.each do |assoc|
606
+ mc = model.associated_model_class(assoc)
607
+ t << "<li>"
608
+ t << association_class_link(mc, assoc)
609
+ t << "<ul id='#{assoc}_remove_list'>"
610
+ obj.send(assoc).each do |assoc_obj|
611
+ t << mtm_edit_remove(assoc, mc, obj, assoc_obj)
612
+ end
613
+ t << "</ul></li>"
614
+ end
615
+ t << "</ul></div>"
616
+ end
617
+
618
+ # Line item containing form to remove the currently associated object.
619
+ def mtm_edit_remove(assoc, mc, obj, assoc_obj)
620
+ t = "<li>"
621
+ t << association_link(mc, assoc_obj)
622
+ form_attr = form_attributes(:action=>url_for("mtm_update/#{model.primary_key_value(obj)}?association=#{assoc}&amp;remove%5b%5d=#{model.primary_key_value(assoc_obj)}&amp;redir=edit"), :method=>'post', :class => 'mtm_remove_associations', 'data-add'=>"#add_#{assoc}")
623
+ t << Forme.form(form_attr, form_opts) do |f|
624
+ f.button(:value=>'Remove', :class=>'btn btn-mini btn-danger')
625
+ end.to_s
626
+ t << "</li>"
627
+ end
628
+ end
629
+ end