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.
- checksums.yaml +7 -0
- data/CHANGELOG +3 -0
- data/MIT-LICENSE +18 -0
- data/README.rdoc +226 -0
- data/Rakefile +79 -0
- data/autoforme.js +77 -0
- data/lib/autoforme/action.rb +629 -0
- data/lib/autoforme/framework.rb +145 -0
- data/lib/autoforme/frameworks/rails.rb +80 -0
- data/lib/autoforme/frameworks/sinatra.rb +59 -0
- data/lib/autoforme/model.rb +377 -0
- data/lib/autoforme/models/sequel.rb +344 -0
- data/lib/autoforme/opts_attributes.rb +29 -0
- data/lib/autoforme/request.rb +53 -0
- data/lib/autoforme/table.rb +70 -0
- data/lib/autoforme/version.rb +9 -0
- data/lib/autoforme.rb +57 -0
- data/spec/associations_spec.rb +505 -0
- data/spec/basic_spec.rb +661 -0
- data/spec/mtm_spec.rb +333 -0
- data/spec/rails_spec_helper.rb +75 -0
- data/spec/sequel_spec_helper.rb +44 -0
- data/spec/sinatra_spec_helper.rb +53 -0
- data/spec/spec_helper.rb +51 -0
- data/spec/unit_spec.rb +449 -0
- metadata +129 -0
@@ -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('&')}")}\">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}&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}&remove%5b%5d=#{model.primary_key_value(assoc_obj)}&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
|