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,145 @@
1
+ module AutoForme
2
+ # The Framework class contains forms for a set of models, tied to web
3
+ # framework controller.
4
+ class Framework
5
+ extend OptsAttributes
6
+
7
+ # See Autoforme.for.
8
+ def self.for(type, controller, opts={}, &block)
9
+ AutoForme.framework_class_for(type).setup(controller, opts, &block)
10
+ end
11
+
12
+ # Setup a new framework class.
13
+ def self.setup(controller, opts, &block)
14
+ f = new(controller, opts)
15
+ f.instance_exec(&block)
16
+ f
17
+ end
18
+
19
+ # The web framework controller tied to this framework.
20
+ attr_reader :controller
21
+
22
+ # A map of link names to AutoForme::Model classes for this Framework.
23
+ attr_reader :models
24
+
25
+ # A map of underlying model classes to AutoForme::Model classes for this Framework.
26
+ attr_reader :model_classes
27
+
28
+ # The configuration options related to this framework.
29
+ attr_reader :opts
30
+
31
+ # The path prefix that this framework is mounted at
32
+ attr_reader :prefix
33
+
34
+ opts_attribute :after_create, :after_destroy, :after_update, :association_links,
35
+ :autocomplete_options, :before_action, :before_create, :before_destroy,
36
+ :before_edit, :before_new, :before_update, :column_options,
37
+ :columns, :display_name, :filter, :form_attributes, :form_options,
38
+ :inline_mtm_associations, :lazy_load_association_links,
39
+ :model_type, :mtm_associations, :order, :page_footer, :page_header, :per_page,
40
+ :redirect, :supported_actions, :table_class
41
+
42
+ def initialize(controller, opts={})
43
+ @controller = controller
44
+ @opts = opts.dup
45
+ @prefix = @opts[:prefix]
46
+ @models = {}
47
+ @model_classes = {}
48
+ end
49
+
50
+ def supported_actions_for(model, request)
51
+ handle_proc(supported_actions, model, request)
52
+ end
53
+
54
+ def table_class_for(model, type, request)
55
+ handle_proc(table_class, model, type, request)
56
+ end
57
+
58
+ def limit_for(model, type, request)
59
+ handle_proc(per_page, model, type, request)
60
+ end
61
+
62
+ def columns_for(model, type, request)
63
+ handle_proc(columns, model, type, request)
64
+ end
65
+
66
+ def mtm_associations_for(model, request)
67
+ handle_proc(mtm_associations, model, request)
68
+ end
69
+
70
+ def inline_mtm_associations_for(model, request)
71
+ handle_proc(inline_mtm_associations, model, request)
72
+ end
73
+
74
+ def order_for(model, type, request)
75
+ handle_proc(order, model, type, request)
76
+ end
77
+
78
+ def filter_for(model)
79
+ handle_proc(filter, model)
80
+ end
81
+
82
+ def redirect_for(model)
83
+ handle_proc(redirect, model)
84
+ end
85
+
86
+ def display_name_for(model)
87
+ handle_proc(display_name, model)
88
+ end
89
+
90
+ def form_attributes_for(model, type, request)
91
+ handle_proc(form_attributes, model, type, request) || {}
92
+ end
93
+
94
+ def form_options_for(model, type, request)
95
+ handle_proc(form_options, model, type, request) || {}
96
+ end
97
+
98
+ def page_footer_for(model, type, request)
99
+ handle_proc(page_footer, model, type, request)
100
+ end
101
+
102
+ def page_header_for(model, type, request)
103
+ handle_proc(page_header, model, type, request)
104
+ end
105
+
106
+ def lazy_load_association_links?(model, type, request)
107
+ handle_proc(lazy_load_association_links, model, type, request)
108
+ end
109
+
110
+ def autocomplete_options_for(model, type, request)
111
+ handle_proc(autocomplete_options, model, type, request)
112
+ end
113
+
114
+ def association_links_for(model, type, request)
115
+ handle_proc(association_links, model, type, request)
116
+ end
117
+
118
+ # Add a new model to the existing framework.
119
+ def model(model_class, &block)
120
+ model = Model.for(self, model_type, model_class, &block)
121
+ @model_classes[model.model] = model
122
+ @models[model.link] = model
123
+ end
124
+
125
+ # Return the action related to the given request, if such an
126
+ # action is supported.
127
+ def action_for(request)
128
+ if model = @models[request.model]
129
+ action = Action.new(model, request)
130
+ action if action.supported?
131
+ end
132
+ end
133
+
134
+ private
135
+
136
+ def handle_proc(v, *a)
137
+ case v
138
+ when Proc, Method
139
+ v.call(*a)
140
+ else
141
+ v
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,80 @@
1
+ module AutoForme
2
+ module Frameworks
3
+ class Rails < AutoForme::Framework
4
+ class Request < AutoForme::Request
5
+ def initialize(request)
6
+ @controller = request
7
+ @params = request.params
8
+ @session = request.session
9
+ @env = request.env
10
+ @method = @env['REQUEST_METHOD']
11
+ @model = @params['autoforme_model']
12
+ @action_type = @params['autoforme_action']
13
+ @path = @env['SCRIPT_NAME']
14
+ @id = @params['id']
15
+ end
16
+
17
+ # Implement redirects in the Rails support using throw/catch, similar to
18
+ # how they are natively implemented in Sinatra.
19
+ def redirect(path)
20
+ throw :redirect, path
21
+ end
22
+
23
+ # Whether the request is an asynchronous request
24
+ def xhr?
25
+ @controller.request.xhr?
26
+ end
27
+
28
+ # Use Rails's form_authenticity_token for CSRF protection.
29
+ def csrf_token_hash
30
+ vc = @controller.view_context
31
+ {vc.request_forgery_protection_token.to_s=>vc.form_authenticity_token} if vc.protect_against_forgery?
32
+ end
33
+ end
34
+
35
+ # After setting up the framework, add a route for the framework to Rails, so that
36
+ # requests are correctly routed.
37
+ def self.setup(controller, opts, &block)
38
+ f = super
39
+ f.setup_routes
40
+ f
41
+ end
42
+
43
+ # Define an autoforme method in the controller which handles the actions.
44
+ def initialize(*)
45
+ super
46
+ framework = self
47
+ @controller.send(:define_method, :autoforme) do
48
+ if @autoforme_action = framework.action_for(Request.new(self))
49
+ if redirect = catch(:redirect){@autoforme_text = @autoforme_action.handle; nil}
50
+ redirect_to redirect
51
+ else
52
+ render :inline=>"<%=raw @autoforme_text %>", :layout=>!@autoforme_action.request.xhr?
53
+ end
54
+ else
55
+ render :text=>'Unhandled Request', :status=>404
56
+ end
57
+ end
58
+ end
59
+
60
+ ALL_SUPPORTED_ACTIONS_REGEXP = Regexp.union(AutoForme::Action::ALL_SUPPORTED_ACTIONS.map{|x| /#{Regexp.escape(x)}/})
61
+
62
+ # Add a route for the framework to Rails routing.
63
+ def setup_routes
64
+ if prefix
65
+ pre = prefix.to_s[1..-1] + '/'
66
+ end
67
+ model_regexp = Regexp.union(models.keys.map{|m| Regexp.escape(m)})
68
+ controller = @controller.name.sub(/Controller\z/, '').underscore
69
+ ::Rails.application.routes.prepend do
70
+ match "#{pre}:autoforme_model/:autoforme_action(/:id)" , :controller=>controller, :action=>'autoforme', :via=>[:get, :post],
71
+ :constraints=>{:autoforme_model=>model_regexp, :autoforme_action=>ALL_SUPPORTED_ACTIONS_REGEXP}
72
+ end
73
+ ::Rails.application.reload_routes!
74
+ end
75
+
76
+ end
77
+ end
78
+
79
+ register_framework(:rails, Frameworks::Rails)
80
+ end
@@ -0,0 +1,59 @@
1
+ module AutoForme
2
+ module Frameworks
3
+ class Sinatra < AutoForme::Framework
4
+ class Request < AutoForme::Request
5
+ def initialize(controller)
6
+ @controller = controller
7
+ @request = controller.request
8
+ @params = controller.params
9
+ @session = controller.session
10
+ captures = @params[:captures] || []
11
+ @env = @request.env
12
+ @method = @env['REQUEST_METHOD']
13
+ @model = captures[0]
14
+ @action_type = captures[1]
15
+ @path = @env['SCRIPT_NAME']
16
+ @id = @params[:id] || captures[2]
17
+ end
18
+
19
+ # Redirect to the given path
20
+ def redirect(path)
21
+ controller.redirect(path)
22
+ end
23
+
24
+ # Whether the request is an asynchronous request
25
+ def xhr?
26
+ @env['HTTP_X_REQUESTED_WITH'] =~ /XMLHttpRequest/i
27
+ end
28
+
29
+ # Use Rack::Csrf for csrf protection if it is defined.
30
+ def csrf_token_hash
31
+ {::Rack::Csrf.field=>::Rack::Csrf.token(@env)} if defined?(::Rack::Csrf)
32
+ end
33
+ end
34
+
35
+ # Add get and post routes when creating the framework. These routes can potentially
36
+ # match other routes, but in that case use pass to try the next route.
37
+ def initialize(*)
38
+ super
39
+ framework = self
40
+ block = lambda do
41
+ if @autoforme_action = framework.action_for(Request.new(self))
42
+ @autoforme_text = @autoforme_action.handle
43
+ opts = {}
44
+ opts[:layout] = false if @autoforme_action.request.xhr?
45
+ erb "<%= @autoforme_text %>", opts
46
+ else
47
+ pass
48
+ end
49
+ end
50
+
51
+ prefix = Regexp.escape(framework.prefix) if framework.prefix
52
+ @controller.get %r{\A#{prefix}/(\w+)/(\w+)(?:/(\w+))?\z}, &block
53
+ @controller.post %r{\A#{prefix}/(\w+)/(\w+)(?:/(\w+))?\z}, &block
54
+ end
55
+ end
56
+ end
57
+
58
+ register_framework(:sinatra, Frameworks::Sinatra)
59
+ end
@@ -0,0 +1,377 @@
1
+ module AutoForme
2
+ # Wraps a specific model class
3
+ class Model
4
+ # Array of supported autocomplete types
5
+ AUTOCOMPLETE_TYPES = [:show, :edit, :delete, :association, :mtm_edit].freeze
6
+
7
+ # The default number of records to show on each browse/search results pages
8
+ DEFAULT_LIMIT = 25
9
+
10
+ # The default table class to use for browse/search results pages
11
+ DEFAULT_TABLE_CLASS = "table table-bordered table-striped"
12
+
13
+ # The default supported actions for models.
14
+ DEFAULT_SUPPORTED_ACTIONS = [:browse, :new, :show, :edit, :delete, :search, :mtm_edit]
15
+
16
+ extend OptsAttributes
17
+
18
+ # Create a new instance for the given model type and underlying model class
19
+ # tied to the given framework.
20
+ def self.for(framework, type, model_class, &block)
21
+ model = AutoForme.model_class_for(type).new(model_class, framework)
22
+ model.instance_exec(&block) if block
23
+ model
24
+ end
25
+
26
+ # The AutoForme::Framework class tied to the current model
27
+ attr_reader :framework
28
+
29
+ # The underlying model class for the current model
30
+ attr_reader :model
31
+
32
+ # The options for the given model.
33
+ attr_reader :opts
34
+
35
+ opts_attribute :after_create, :after_destroy, :after_update, :association_links,
36
+ :autocomplete_options, :before_action, :before_create, :before_destroy,
37
+ :before_edit, :before_new, :before_update, :class_display_name,
38
+ :column_options, :columns, :display_name, :eager, :eager_graph,
39
+ :filter, :form_attributes, :form_options,
40
+ :inline_mtm_associations, :lazy_load_association_links, :link_name, :mtm_associations,
41
+ :order, :page_footer, :page_header, :per_page,
42
+ :redirect, :supported_actions, :table_class
43
+
44
+ def initialize(model, framework)
45
+ @model = model
46
+ @framework = framework
47
+ @opts = {}
48
+ end
49
+
50
+ # Whether the given type of action is supported for this model.
51
+ def supported_action?(type, request)
52
+ (handle_proc(supported_actions || framework.supported_actions_for(model, request), request) || DEFAULT_SUPPORTED_ACTIONS).include?(type)
53
+ end
54
+
55
+ # An array of many to many association symbols to handle via a separate mtm_edit page.
56
+ def mtm_association_select_options(request)
57
+ normalize_mtm_associations(handle_proc(mtm_associations || framework.mtm_associations_for(model, request), request))
58
+ end
59
+
60
+ # Whether an mtm_edit can be displayed for the given association
61
+ def supported_mtm_edit?(assoc, request)
62
+ mtm_association_select_options(request).map{|x| x.to_s}.include?(assoc)
63
+ end
64
+
65
+ # Whether an mtm_update can occur for the given association
66
+ def supported_mtm_update?(assoc, request)
67
+ supported_mtm_edit?(assoc, request) || inline_mtm_assocs(request).map{|x| x.to_s}.include?(assoc)
68
+ end
69
+
70
+ # An array of many to many association symbols to handle inline on the edit forms.
71
+ def inline_mtm_assocs(request)
72
+ normalize_mtm_associations(handle_proc(inline_mtm_associations || framework.inline_mtm_associations_for(model, request), request))
73
+ end
74
+
75
+ def columns_for(type, request)
76
+ handle_proc(columns || framework.columns_for(model, type, request), type, request) || default_columns
77
+ end
78
+
79
+ # The options to use for the given column and request. Instead of the model options overriding the framework
80
+ # options, they are merged together.
81
+ def column_options_for(type, request, column)
82
+ framework_opts = case framework_opts = framework.column_options
83
+ when Proc, Method
84
+ framework_opts.call(model, column, type, request) || {}
85
+ else
86
+ extract_column_options(framework_opts, column, type, request)
87
+ end
88
+
89
+ model_opts = case model_opts = column_options
90
+ when Proc, Method
91
+ model_opts.call(column, type, request) || {}
92
+ else
93
+ extract_column_options(model_opts, column, type, request)
94
+ end
95
+
96
+ opts = framework_opts.merge(model_opts).dup
97
+
98
+ if association?(column) && associated_model = associated_model_class(column)
99
+ if associated_model.autocomplete_options_for(:association, request) && !opts[:as] && association_type(column) == :one
100
+ opts[:type] = 'text'
101
+ opts[:class] = 'autoforme_autocomplete'
102
+ opts[:attr] = {'data-column'=>column, 'data-type'=>type}
103
+ opts[:name] = form_param_name(column)
104
+ else
105
+ unless opts[:name_method]
106
+ opts[:name_method] = lambda{|obj| associated_model.object_display_name(:association, request, obj)}
107
+ end
108
+
109
+ case type
110
+ when :edit, :new, :search_form
111
+ unless opts[:options] || opts[:dataset]
112
+ opts[:dataset] = lambda{|ds| associated_model.apply_dataset_options(:association, request, ds)}
113
+ end
114
+ end
115
+ end
116
+ end
117
+
118
+ case type
119
+ when :show, :search_form
120
+ opts[:required] = false unless opts.has_key?(:required)
121
+ if type == :search_form && opts[:as] == :textarea
122
+ opts.delete(:as)
123
+ end
124
+ end
125
+
126
+ opts
127
+ end
128
+
129
+ def order_for(type, request)
130
+ handle_proc(order || framework.order_for(model, type, request), type, request)
131
+ end
132
+
133
+ def eager_for(type, request)
134
+ handle_proc(eager, type, request)
135
+ end
136
+
137
+ def eager_graph_for(type, request)
138
+ handle_proc(eager_graph, type, request)
139
+ end
140
+
141
+ def filter_for
142
+ filter || framework.filter_for(model)
143
+ end
144
+
145
+ def redirect_for
146
+ redirect || framework.redirect_for(model)
147
+ end
148
+
149
+ def form_attributes_for(type, request)
150
+ framework.form_attributes_for(model, type, request).merge(handle_proc(form_attributes, type, request) || {})
151
+ end
152
+
153
+ def form_options_for(type, request)
154
+ framework.form_options_for(model, type, request).merge(handle_proc(form_options, type, request) || {})
155
+ end
156
+
157
+ def page_footer_for(type, request)
158
+ handle_proc(page_footer || framework.page_footer_for(model, type, request), type, request)
159
+ end
160
+
161
+ def page_header_for(type, request)
162
+ handle_proc(page_header || framework.page_header_for(model, type, request), type, request)
163
+ end
164
+
165
+ def table_class_for(type, request)
166
+ handle_proc(table_class || framework.table_class_for(model, type, request), type, request) || DEFAULT_TABLE_CLASS
167
+ end
168
+
169
+ def limit_for(type, request)
170
+ handle_proc(per_page || framework.limit_for(model, type, request), type, request) || DEFAULT_LIMIT
171
+ end
172
+
173
+ def display_name_for
174
+ display_name || framework.display_name_for(model)
175
+ end
176
+
177
+ def association_links_for(type, request)
178
+ case v = handle_proc(association_links || framework.association_links_for(model, type, request), type, request)
179
+ when nil
180
+ []
181
+ when Array
182
+ v
183
+ when :all
184
+ association_names
185
+ when :all_except_mtm
186
+ association_names - mtm_association_names
187
+ else
188
+ [v]
189
+ end
190
+ end
191
+
192
+ # Whether to lazy load association links for this model.
193
+ def lazy_load_association_links?(type, request)
194
+ v = handle_proc(lazy_load_association_links, type, request)
195
+ v = framework.lazy_load_association_links?(model, type, request) if v.nil?
196
+ v || false
197
+ end
198
+
199
+ def autocomplete_options_for(type, request)
200
+ return unless AUTOCOMPLETE_TYPES.include?(type)
201
+ framework_opts = framework.autocomplete_options_for(model, type, request)
202
+ model_opts = handle_proc(autocomplete_options, type, request)
203
+ if model_opts
204
+ (framework_opts || {}).merge(model_opts)
205
+ end
206
+ end
207
+
208
+ # The name to display to the user for this model.
209
+ def class_name
210
+ class_display_name || model.name
211
+ end
212
+
213
+ # The name to use in links for this model. Also affects where this model is mounted at.
214
+ def link
215
+ link_name || class_name
216
+ end
217
+
218
+ # The AutoForme::Model instance associated to the given association.
219
+ def associated_model_class(assoc)
220
+ framework.model_classes[associated_class(assoc)]
221
+ end
222
+
223
+ # The column value to display for the given object and column.
224
+ def column_value(type, request, obj, column)
225
+ return unless v = obj.send(column)
226
+ if association?(column)
227
+ opts = column_options_for(type, request, column)
228
+ case nm = opts[:name_method]
229
+ when Symbol, String
230
+ v = v.send(nm)
231
+ when nil
232
+ else
233
+ v = nm.call(v)
234
+ end
235
+ end
236
+ if v.is_a?(base_class)
237
+ v = default_object_display_name(v)
238
+ end
239
+ v
240
+ end
241
+
242
+ # Destroy the given object, deleting it from the database.
243
+ def destroy(obj)
244
+ obj.destroy
245
+ end
246
+
247
+ # Run framework and model before_action hooks with type symbol and request.
248
+ def before_action_hook(type, request)
249
+ if v = framework.before_action
250
+ v.call(type, request)
251
+ end
252
+ if v = before_action
253
+ v.call(type, request)
254
+ end
255
+ end
256
+
257
+ # Run given hooks with the related object and request.
258
+ def hook(type, request, obj)
259
+ if type.to_s =~ /before/
260
+ if v = framework.send(type)
261
+ v.call(obj, request)
262
+ end
263
+ if v = send(type)
264
+ v.call(obj, request)
265
+ end
266
+ else
267
+ if v = send(type)
268
+ v.call(obj, request)
269
+ end
270
+ if v = framework.send(type)
271
+ v.call(obj, request)
272
+ end
273
+ end
274
+ end
275
+
276
+ # Create a new instance of the underlying model, setting
277
+ # defaults based on the params given.
278
+ def new(params, request)
279
+ obj = @model.new
280
+ if params
281
+ columns_for(:new, request).each do |col|
282
+ if association?(col)
283
+ col = association_key(col)
284
+ end
285
+ if v = params[col]
286
+ obj.send("#{col}=", v)
287
+ end
288
+ end
289
+ end
290
+ obj
291
+ end
292
+
293
+ # An array of pairs for the select options to return for the given type.
294
+ def select_options(type, request, opts={})
295
+ case nm = opts[:name_method]
296
+ when Symbol, String
297
+ all_rows_for(type, request).map{|obj| [obj.send(nm), primary_key_value(obj)]}
298
+ when nil
299
+ all_rows_for(type, request).map{|obj| [object_display_name(type, request, obj), primary_key_value(obj)]}
300
+ else
301
+ all_rows_for(type, request).map{|obj| [nm.call(obj), primary_key_value(obj)]}
302
+ end
303
+ end
304
+
305
+ # A human readable string representing the object.
306
+ def object_display_name(type, request, obj)
307
+ apply_name_method(display_name_for, obj, type, request)
308
+ end
309
+
310
+ # A human reable string for the associated object.
311
+ def associated_object_display_name(assoc, request, obj)
312
+ apply_name_method(column_options_for(:mtm_edit, request, assoc)[:name_method], obj, :mtm_edit, request)
313
+ end
314
+
315
+ # A fallback for the display name for the object if none is configured.
316
+ def default_object_display_name(obj)
317
+ if obj.respond_to?(:forme_name)
318
+ obj.forme_name
319
+ elsif obj.respond_to?(:name)
320
+ obj.name
321
+ else
322
+ primary_key_value(obj)
323
+ end
324
+ end
325
+
326
+ private
327
+
328
+ def apply_name_method(nm, obj, type, request)
329
+ case nm
330
+ when Symbol
331
+ obj.send(nm)
332
+ when Proc, Method
333
+ case nm.arity
334
+ when 3
335
+ nm.call(obj, type, request)
336
+ when 2
337
+ nm.call(obj, type)
338
+ else
339
+ nm.call(obj)
340
+ end
341
+ when nil
342
+ default_object_display_name(obj)
343
+ else
344
+ raise Error, "invalid name method: #{nm.inspect}"
345
+ end
346
+ end
347
+
348
+ def extract_column_options(opts, column, type, request)
349
+ return {} unless opts
350
+ case opts = opts[column]
351
+ when Proc, Method
352
+ opts.call(type, request)
353
+ when nil
354
+ {}
355
+ else
356
+ opts
357
+ end
358
+ end
359
+
360
+ def handle_proc(v, *a)
361
+ case v
362
+ when Proc, Method
363
+ v.call(*a)
364
+ else
365
+ v
366
+ end
367
+ end
368
+
369
+ def normalize_mtm_associations(assocs)
370
+ if assocs == :all
371
+ mtm_association_names
372
+ else
373
+ Array(assocs)
374
+ end
375
+ end
376
+ end
377
+ end