autoforme 0.5.0

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