forme 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,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