forme 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,89 @@
1
+ require 'forme'
2
+
3
+ module Forme
4
+ module Sinatra # :nodoc:
5
+ # Subclass used when using Forme/Sinatra ERB integration.
6
+ # Handles integrating into the view template so that
7
+ # methods with blocks can inject strings into the output.
8
+ class Form < ::Forme::Form
9
+ # Template output object, where serialized output gets
10
+ # injected.
11
+ attr_reader :output
12
+
13
+ # Set the template output object when initializing.
14
+ def initialize(*)
15
+ super
16
+ @output = @opts[:output]
17
+ end
18
+
19
+ # Serialize the tag and inject it into the output.
20
+ def emit(tag)
21
+ output << tag.to_s
22
+ end
23
+
24
+ # Always return nil, so that use with <%= doesn't cause
25
+ # multiple things to be output.
26
+ def inputs(*a)
27
+ super
28
+ nil
29
+ end
30
+
31
+ # Always return nil, so that use with <%= doesn't cause
32
+ # multiple things to be output.
33
+ def form(*a, &block)
34
+ super
35
+ nil
36
+ end
37
+
38
+ # If a block is provided, inject an opening tag into the
39
+ # output, inject any given children into the output, yield to the
40
+ # block, inject a closing tag into the output, and the return nil
41
+ # so that usage with <%= doesn't cause multiple things to be output.
42
+ # If a block is not given, just return the tag created.
43
+ def tag(type, attr={}, children=[])
44
+ tag = _tag(type, attr, children)
45
+ if block_given?
46
+ emit(serializer.serialize_open(tag)) if serializer.respond_to?(:serialize_open)
47
+ children.each{|c| emit(c)}
48
+ yield self
49
+ emit(serializer.serialize_close(tag)) if serializer.respond_to?(:serialize_close)
50
+ nil
51
+ else
52
+ tag
53
+ end
54
+ end
55
+ end
56
+
57
+ # This is the module used to add the Forme integration
58
+ # to Sinatra. It should be enabled in Sinatra with the
59
+ # following code in your <tt>Sinatra::Base</tt> subclass:
60
+ #
61
+ # helpers Forme::Sinatra::ERB
62
+ module ERB
63
+ # Create a +Form+ object and yield it to the block,
64
+ # injecting the opening form tag before yielding and
65
+ # the closing form tag after yielding.
66
+ #
67
+ # Argument Handling:
68
+ # No args :: Creates a +Form+ object with no options and not associated
69
+ # to an +obj+, and with no attributes in the opening tag.
70
+ # 1 hash arg :: Treated as opening form tag attributes, creating a
71
+ # +Form+ object with no options.
72
+ # 1 non-hash arg :: Treated as the +Form+'s +obj+, with empty options
73
+ # and no attributes in the opening tag.
74
+ # 2 hash args :: First hash is opening attributes, second hash is +Form+
75
+ # options.
76
+ # 1 non-hash arg, 1-2 hash args :: First argument is +Form+'s obj, second is
77
+ # opening attributes, third if provided is
78
+ # +Form+'s options.
79
+ def form(obj=nil, attr={}, opts={}, &block)
80
+ h = {:output=>@_out_buf}
81
+ (obj.is_a?(Hash) ? attr = attr.merge(h) : opts = opts.merge(h))
82
+ Form.form(obj, attr, opts, &block)
83
+ end
84
+ end
85
+
86
+ # Alias of <tt>Forme::Sinatra::ERB</tt>
87
+ Erubis = ERB
88
+ end
89
+ end
@@ -0,0 +1,9 @@
1
+ module Forme
2
+ # Version constant, use <tt>Forme.version</tt> instead.
3
+ VERSION = '0.5.0'.freeze
4
+
5
+ # Returns the version as a frozen string (e.g. '0.1.0')
6
+ def self.version
7
+ VERSION
8
+ end
9
+ end
@@ -0,0 +1,435 @@
1
+ require 'forme'
2
+
3
+ module Sequel # :nodoc:
4
+ module Plugins # :nodoc:
5
+ # This Sequel plugin allows easy use of Forme with Sequel.
6
+ module Forme
7
+ # Exception class raised by the plugin. It's important to
8
+ # note this descends from <tt>Forme::Error</tt> and not
9
+ # <tt>Sequel::Error</tt>, though in practice it's unlikely
10
+ # you will want to rescue these errors.
11
+ class Error < ::Forme::Error
12
+ end
13
+
14
+ # This module extends all <tt>Forme::Form</tt> instances
15
+ # that use a <tt>Sequel::Model</tt> instance as the form's
16
+ # +obj+.
17
+ module SequelForm
18
+ # Stack of objects used by subform. The current +obj+
19
+ # is added to the top of the stack on a call to +subform+,
20
+ # the nested associated object is set as the current +obj+ during the
21
+ # call to +subform+, and when +subform+ returns, the top of the
22
+ # stack is set as the current +obj+.
23
+ attr_accessor :nested_associations
24
+
25
+ # The namespaces that should be added to the id and name
26
+ # attributes for the receiver's inputs. Used as a stack
27
+ # by +subform+.
28
+ attr_accessor :namespaces
29
+
30
+ # Use the post method by default for Sequel forms, unless
31
+ # overridden with the :method attribute.
32
+ def form(attr={}, &block)
33
+ attr = {:method=>:post}.merge(attr)
34
+ attr[:class] = ::Forme.merge_classes(attr[:class], "forme", obj.model.send(:underscore, obj.model.name))
35
+ super(attr, &block)
36
+ end
37
+
38
+ # Call humanize on a string version of the argument if
39
+ # String#humanize exists. Otherwise, do some monkeying
40
+ # with the string manually.
41
+ def humanize(s)
42
+ s = s.to_s
43
+ s.respond_to?(:humanize) ? s.humanize : s.gsub(/_id$/, "").gsub(/_/, " ").capitalize
44
+ end
45
+
46
+ # Handle nested association usage. The +association+ should be a name
47
+ # of the association for the form's +obj+. Inside the block, calls to
48
+ # the +input+ and +inputs+ methods for the receiver treat the associated
49
+ # object as the recevier's +obj+, using name and id attributes that work
50
+ # with the Sequel +nested_attributes+ plugin.
51
+ #
52
+ # The following options are currently supported:
53
+ # :inputs :: Automatically call +inputs+ with the given values. Using
54
+ # this, it is not required to pass a block to the method,
55
+ # though it will still work if you do.
56
+ # :legend :: If :inputs is also used, this is passed to it to override
57
+ # the default :legend used. You can also use a proc as the value,
58
+ # which will called with each associated object (and the position
59
+ # in the associated object already for *_to_many associations),
60
+ # and should return the legend string to use for that object.
61
+ def subform(association, opts={}, &block)
62
+ nested_obj = opts.has_key?(:obj) ? opts[:obj] : obj.send(association)
63
+ ref = obj.class.association_reflection(association)
64
+ multiple = ref.returns_array?
65
+ i = -1
66
+ ins = opts[:inputs]
67
+ Array(nested_obj).each do |no|
68
+ begin
69
+ nested_associations << obj
70
+ namespaces << "#{association}_attributes"
71
+ namespaces << (i+=1) if multiple
72
+ @obj = no
73
+ emit(input(ref.associated_class.primary_key, :type=>:hidden, :label=>nil)) unless no.new?
74
+ if ins
75
+ options = opts.dup
76
+ if options.has_key?(:legend)
77
+ if options[:legend].respond_to?(:call)
78
+ options[:legend] = multiple ? options[:legend].call(no, i) : options[:legend].call(no)
79
+ end
80
+ else
81
+ if multiple
82
+ options[:legend] = humanize("#{obj.model.send(:singularize, association)} ##{i+1}")
83
+ else
84
+ options[:legend] = humanize(association)
85
+ end
86
+ end
87
+ inputs(ins, options, &block)
88
+ else
89
+ yield
90
+ end
91
+ ensure
92
+ @obj = nested_associations.pop
93
+ namespaces.pop if multiple
94
+ namespaces.pop
95
+ end
96
+ end
97
+ nil
98
+ end
99
+
100
+ # Return a unique id attribute for the +field+, handling
101
+ # nested attributes use.
102
+ def namespaced_id(field)
103
+ "#{namespaces.join('_')}_#{field}"
104
+ end
105
+
106
+ # Return a unique name attribute for the +field+, handling nested
107
+ # attribute use. If +multiple+ is true, end the name
108
+ # with [] so that param parsing will treat the name as part of an array.
109
+ def namespaced_name(field, multiple=false)
110
+ root, *nsps = namespaces
111
+ "#{root}#{nsps.map{|n| "[#{n}]"}.join}[#{field}]#{'[]' if multiple}"
112
+ end
113
+ end
114
+
115
+ # Helper class for dealing with Forme/Sequel integration.
116
+ # One instance is created for each call to <tt>Forme::Form#input</tt>
117
+ # for forms associated with <tt>Sequel::Model</tt> objects.
118
+ class SequelInput
119
+ include ::Forme
120
+
121
+ # The name methods that will be tried, in order, to get the
122
+ # text to use for the options in the select input created
123
+ # for associations.
124
+ FORME_NAME_METHODS = [:forme_name, :name, :title, :number]
125
+
126
+ # The <tt>Sequel::Model</tt> object related to the receiver.
127
+ attr_reader :obj
128
+
129
+ # The form related to the receiver.
130
+ attr_reader :form
131
+
132
+ # The field/column name related to the receiver. The type of
133
+ # input created usually depends upon this field.
134
+ attr_reader :field
135
+
136
+ # The options hash related to the receiver.
137
+ attr_reader :opts
138
+
139
+ # Set the +obj+, +form+, +field+, and +opts+ attributes.
140
+ def initialize(obj, form, field, opts)
141
+ @obj, @form, @field, @opts = obj, form, field, opts
142
+ end
143
+
144
+ # Determine which type of input to used based on the +field+.
145
+ # If the field is a column, use the column's type to determine
146
+ # an appropriate field type. If the field is an association,
147
+ # use either a regular or multiple select input (or multiple radios or
148
+ # checkboxes if the related :type option is used). If it's not a
149
+ # column or association, but the object responds to +field+,
150
+ # create a text input. Otherwise, raise an +Error+.
151
+ def input
152
+ opts[:attr] = opts[:attr] ? opts[:attr].dup : {}
153
+ opts[:wrapper_attr] = opts[:wrapper_attr] ? opts[:wrapper_attr].dup : {}
154
+
155
+ if sch = obj.db_schema[field]
156
+ handle_errors(field)
157
+ handle_validations(field)
158
+ meth = :"input_#{sch[:type]}"
159
+ opts[:id] = form.namespaced_id(field) unless opts.has_key?(:id)
160
+ opts[:name] = form.namespaced_name(field) unless opts.has_key?(:name)
161
+ opts[:required] = true if !opts.has_key?(:required) && sch[:allow_null] == false && sch[:type] != :boolean
162
+ handle_label(field)
163
+
164
+ ::Forme.attr_classes(opts[:wrapper_attr], sch[:type])
165
+ ::Forme.attr_classes(opts[:wrapper_attr], "required") if opts[:required]
166
+
167
+ if respond_to?(meth, true)
168
+ send(meth, sch)
169
+ else
170
+ input_other(sch)
171
+ end
172
+ elsif ref = obj.model.association_reflection(field)
173
+ ::Forme.attr_classes(opts[:wrapper_attr], ref[:type])
174
+ meth = :"association_#{ref[:type]}"
175
+ if respond_to?(meth, true)
176
+ send(meth, ref)
177
+ else
178
+ raise Error, "Association type #{ref[:type]} not currently handled for association #{ref[:name]}"
179
+ end
180
+ elsif obj.respond_to?(field)
181
+ opts[:id] = form.namespaced_id(field) unless opts.has_key?(:id)
182
+ opts[:name] = form.namespaced_name(field) unless opts.has_key?(:name)
183
+ handle_label(field)
184
+ input_other({})
185
+ else
186
+ raise Error, "Unrecognized field used: #{field}"
187
+ end
188
+ end
189
+
190
+ private
191
+
192
+ # Create an +Input+ instance associated to the receiver's +form+
193
+ # with the given arguments.
194
+ def _input(*a)
195
+ form._input(*a)
196
+ end
197
+
198
+ # Set the error option correctly if the field contains errors
199
+ def handle_errors(f)
200
+ if e = obj.errors.on(f)
201
+ opts[:error] = e.join(', ')
202
+ end
203
+ end
204
+
205
+ # Set the label option appropriately, adding a * if the field
206
+ # is required.
207
+ def handle_label(f)
208
+ unless opts.has_key?(:label)
209
+ opts[:label] = if opts[:required]
210
+ [humanize(field), form._tag(:abbr, {:title=>'required'}, '*')]
211
+ else
212
+ humanize(field)
213
+ end
214
+ end
215
+ end
216
+
217
+ # Update the attributes and options for any recognized validations
218
+ def handle_validations(f)
219
+ m = obj.model
220
+ if m.respond_to?(:validation_reflections) and (vs = m.validation_reflections[f])
221
+ attr = opts[:attr]
222
+ vs.each do |type, options|
223
+ attr[:placeholder] = options[:placeholder] if options[:placeholder] && !attr.has_key?(:placeholder)
224
+
225
+ case type
226
+ when :format
227
+ attr[:pattern] = options[:with].source unless attr.has_key?(:pattern)
228
+ attr[:title] = options[:title] unless attr.has_key?(:title)
229
+ when :length
230
+ unless attr.has_key?(:maxlength)
231
+ if max =(options[:maximum] || options[:is])
232
+ attr[:maxlength] = max
233
+ elsif (w = options[:within]) && w.is_a?(Range)
234
+ attr[:maxlength] = if w.exclude_end? && w.end.is_a?(Integer)
235
+ w.end - 1
236
+ else
237
+ w.end
238
+ end
239
+ end
240
+ end
241
+ when :numericality
242
+ unless attr.has_key?(:pattern)
243
+ attr[:pattern] = if options[:only_integer]
244
+ "^[+\\-]?\\d+$"
245
+ else
246
+ "^[+\\-]?\\d+(\\.\\d+)?$"
247
+ end
248
+ end
249
+ attr[:title] = options[:title] || "must be a number" unless attr.has_key?(:title)
250
+ end
251
+ end
252
+ end
253
+ end
254
+
255
+ # If the :name_method option is provided, use that as the method.
256
+ # Otherwise, pick the first method in +FORME_NAME_METHODS+ that
257
+ # the associated class implements and use it. If none of the
258
+ # methods are implemented by the associated class, raise an +Error+.
259
+ def forme_name_method(ref)
260
+ if meth = opts.delete(:name_method)
261
+ meth
262
+ else
263
+ meths = FORME_NAME_METHODS & ref.associated_class.instance_methods.map{|s| s.to_sym}
264
+ if meths.empty?
265
+ raise Error, "No suitable name method found for association #{ref[:name]}"
266
+ else
267
+ meths.first
268
+ end
269
+ end
270
+ end
271
+
272
+ # Create a select input made up of options for all entries the object
273
+ # could be associated to, with the one currently associated to being selected.
274
+ # If the :type=>:radio option is used, use multiple radio buttons instead of
275
+ # a select box. For :type=>:radio, you can also provide a :tag_wrapper option
276
+ # used to wrap the individual radio buttons.
277
+ def association_many_to_one(ref)
278
+ key = ref[:key]
279
+ handle_errors(key)
280
+ opts[:name] = form.namespaced_name(key) unless opts.has_key?(:name)
281
+ opts[:value] = obj.send(key) unless opts.has_key?(:value)
282
+ opts[:options] = association_select_options(ref) unless opts.has_key?(:options)
283
+ if opts.delete(:as) == :radio
284
+ handle_label(field)
285
+ label = opts.delete(:label)
286
+ val = opts.delete(:value)
287
+ tag_wrapper = opts.delete(:tag_wrapper) || :default
288
+ wrapper = form.transformer(:wrapper, opts)
289
+ opts.delete(:wrapper)
290
+ radios = opts.delete(:options).map{|l, pk| _input(:radio, opts.merge(:value=>pk, :wrapper=>tag_wrapper, :label=>l, :checked=>(pk == val)))}
291
+ radios.unshift("#{label}: ")
292
+ wrapper ? wrapper.call(radios, _input(:radio, opts)) : radios
293
+ else
294
+ opts[:id] = form.namespaced_id(key) unless opts.has_key?(:id)
295
+ opts[:add_blank] = true if !opts.has_key?(:add_blank)
296
+ opts[:required] = true if !opts.has_key?(:required) && (sch = obj.model.db_schema[key]) && !sch[:allow_null]
297
+ handle_label(field)
298
+ ::Forme.attr_classes(opts[:wrapper_attr], "required") if opts[:required]
299
+ _input(:select, opts)
300
+ end
301
+ end
302
+
303
+ # Create a multiple select input made up of options for all entries the object
304
+ # could be associated to, with all of the ones currently associated to being selected.
305
+ # If the :type=>:checkbox option is used, use multiple checkboxes instead of
306
+ # a multiple select box. For :type=>:checkbox, you can also provide a :tag_wrapper option
307
+ # used to wrap the individual checkboxes.
308
+ def association_one_to_many(ref)
309
+ key = ref[:key]
310
+ klass = ref.associated_class
311
+ pk = klass.primary_key
312
+ field = "#{klass.send(:singularize, ref[:name])}_pks"
313
+ opts[:name] = form.namespaced_name(field, :multiple) unless opts.has_key?(:name)
314
+ opts[:value] = obj.send(ref[:name]).map{|x| x.send(pk)} unless opts.has_key?(:value)
315
+ opts[:options] = association_select_options(ref) unless opts.has_key?(:options)
316
+ handle_label(field)
317
+ if opts.delete(:as) == :checkbox
318
+ label = opts.delete(:label)
319
+ val = opts.delete(:value)
320
+ tag_wrapper = opts.delete(:tag_wrapper) || :default
321
+ wrapper = form.transformer(:wrapper, opts)
322
+ opts.delete(:wrapper)
323
+ cbs = opts.delete(:options).map{|l, pk| _input(:checkbox, opts.merge(:value=>pk, :wrapper=>tag_wrapper, :label=>l, :checked=>val.include?(pk), :no_hidden=>true))}
324
+ cbs.unshift("#{label}: ")
325
+ wrapper ? wrapper.call(cbs, _input(:checkbox, opts)) : cbs
326
+ else
327
+ opts[:id] = form.namespaced_id(field) unless opts.has_key?(:id)
328
+ opts[:multiple] = true
329
+ _input(:select, opts)
330
+ end
331
+ end
332
+ alias association_many_to_many association_one_to_many
333
+
334
+ # Return an array of two element arrays representing the
335
+ # select options that should be created.
336
+ def association_select_options(ref)
337
+ name_method = forme_name_method(ref)
338
+ obj.send(:_apply_association_options, ref, ref.associated_class.dataset).unlimited.all.map{|a| [a.send(name_method), a.pk]}
339
+ end
340
+
341
+ # Delegate to the +form+.
342
+ def humanize(s)
343
+ form.humanize(s)
344
+ end
345
+
346
+ # If the column allows +NULL+ values, use a three-valued select
347
+ # input. If not, use a simple checkbox.
348
+ def input_boolean(sch)
349
+ if sch[:allow_null]
350
+ v = opts[:value] || obj.send(field)
351
+ opts[:value] = (v ? 't' : 'f') unless v.nil?
352
+ opts[:add_blank] = true
353
+ opts[:options] = [['True', 't'], ['False', 'f']]
354
+ _input(:select, opts)
355
+ else
356
+ opts[:checked] = obj.send(field)
357
+ opts[:value] = 't'
358
+ _input(:checkbox, opts)
359
+ end
360
+ end
361
+
362
+ # Use a file type for blobs.
363
+ def input_blob(sch)
364
+ opts[:value] = nil
365
+ standard_input(:file)
366
+ end
367
+
368
+ # Use the text type by default for other cases not handled.
369
+ def input_string(sch)
370
+ if opts[:as] == :textarea
371
+ standard_input(:textarea)
372
+ else
373
+ case field.to_s
374
+ when "password"
375
+ opts[:value] = nil
376
+ standard_input(:password)
377
+ when "email"
378
+ standard_input(:email)
379
+ when "phone", "fax"
380
+ standard_input(:tel)
381
+ when "url", "uri", "website"
382
+ standard_input(:url)
383
+ else
384
+ standard_input(:text)
385
+ end
386
+ end
387
+ end
388
+
389
+ # Use number inputs for integers.
390
+ def input_integer(sch)
391
+ standard_input(:number)
392
+ end
393
+
394
+ # Use date inputs for dates.
395
+ def input_date(sch)
396
+ standard_input(:date)
397
+ end
398
+
399
+ # Use datetime inputs for datetimes.
400
+ def input_datetime(sch)
401
+ standard_input(:datetime)
402
+ end
403
+
404
+ # Use a text input for all other types.
405
+ def input_other(sch)
406
+ standard_input(:text)
407
+ end
408
+
409
+ # Allow overriding the given type using the :type option,
410
+ # and set the :value option to the field value unless it
411
+ # is overridden.
412
+ def standard_input(type)
413
+ type = opts.delete(:type) || type
414
+ opts[:value] = obj.send(field) unless opts.has_key?(:value)
415
+ _input(type, opts)
416
+ end
417
+ end
418
+
419
+ module InstanceMethods
420
+ # Configure the +form+ with support for <tt>Sequel::Model</tt>
421
+ # specific code, such as support for nested attributes.
422
+ def forme_config(form)
423
+ form.extend(SequelForm)
424
+ form.nested_associations = []
425
+ form.namespaces = [model.send(:underscore, model.name)]
426
+ end
427
+
428
+ # Return <tt>Forme::Input</tt> instance based on the given arguments.
429
+ def forme_input(form, field, opts)
430
+ SequelInput.new(self, form, field, opts).input
431
+ end
432
+ end
433
+ end
434
+ end
435
+ end