autoforme 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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