forme 1.4.0 → 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG +10 -0
- data/README.rdoc +149 -2
- data/Rakefile +1 -1
- data/lib/forme/erb.rb +1 -1
- data/lib/forme/transformers/labeler.rb +1 -1
- data/lib/forme/version.rb +1 -1
- data/lib/sequel/plugins/forme.rb +1 -1
- data/lib/sequel/plugins/forme_i18n.rb +54 -0
- data/lib/sequel/plugins/forme_set.rb +121 -0
- data/spec/all.rb +3 -0
- data/spec/forme_spec.rb +4 -0
- data/spec/sequel_helper.rb +13 -2
- data/spec/sequel_i18n_helper.rb +40 -0
- data/spec/sequel_i18n_plugin_spec.rb +30 -0
- data/spec/sequel_plugin_spec.rb +10 -0
- data/spec/sequel_set_plugin_spec.rb +183 -0
- metadata +22 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fb06104f826067a8f31ba5778bed02029abdbb7a
|
4
|
+
data.tar.gz: 910a345e355680be66a4d049edaa35e8ecca6186
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c829a2fb78d66a347f6ae65ea600aabd80a2ef7373ca0c3ea4894a0409c75c8959df45b096413796f5140590ea8f7fa4b32c0c8b02a5f78f4ce6d7b103abde77
|
7
|
+
data.tar.gz: fd6f0b68a9aadbedcd53d0087e8e5c3ea215c53363834fe9c9e39ea54c728113afc0d6459119cf65bda15355eb2757ac26688cba23266c93179047c16c5cd270
|
data/CHANGELOG
CHANGED
@@ -1,3 +1,13 @@
|
|
1
|
+
=== 1.5.0 (2016-08-09)
|
2
|
+
|
3
|
+
* Add forme_set Sequel plugin, for handling the intake of submitted params based on form fields (jeremyevans)
|
4
|
+
|
5
|
+
* Only add csrf tag in forme/erb support if there is a current session in the environment (jeremyevans)
|
6
|
+
|
7
|
+
* Fix frozen string literal issue in explicit labeler (jeremyevans)
|
8
|
+
|
9
|
+
* Add forme_i18n Sequel plugin, for internalization support (badosu) (#15)
|
10
|
+
|
1
11
|
=== 1.4.0 (2016-02-01)
|
2
12
|
|
3
13
|
* Ignore submit buttons when using the :readonly formatter (jeremyevans)
|
data/README.rdoc
CHANGED
@@ -376,7 +376,6 @@ based on the format, numericality, and length validations.
|
|
376
376
|
|
377
377
|
In addition to the default Forme options, the Sequel support includes, for specific column types,
|
378
378
|
these additional options to the #input method:
|
379
|
-
.
|
380
379
|
|
381
380
|
=== boolean
|
382
381
|
|
@@ -425,7 +424,7 @@ This will create a form similar to:
|
|
425
424
|
For one_to_many and many_to_many associations, you will probably want to use the
|
426
425
|
+association_pks+ plugin that ships with Sequel.
|
427
426
|
|
428
|
-
This supports the pg_array_to_many association type that comes with Sequel's
|
427
|
+
This also supports the pg_array_to_many association type that comes with Sequel's
|
429
428
|
+pg_array_association+ plugin.
|
430
429
|
|
431
430
|
association input options:
|
@@ -509,6 +508,154 @@ subform options:
|
|
509
508
|
:inputs_opts :: When using the :grid option, this allows you to specify options
|
510
509
|
to pass to the table InputsWrapper.
|
511
510
|
|
511
|
+
== Handling form submissions
|
512
|
+
|
513
|
+
The Sequel forme plugin only handles creating the forms, it does not handle processing
|
514
|
+
input submitted via forms. For a form such as:
|
515
|
+
|
516
|
+
Forme.form(Album[1], :action=>'/foo') do |f|
|
517
|
+
f.input :name
|
518
|
+
f.input :copies_sold
|
519
|
+
end
|
520
|
+
|
521
|
+
Input of the form will often be submitted as the following parameter hash (this depends
|
522
|
+
on your web framework, but Rack works this way by default):
|
523
|
+
|
524
|
+
{'album'=>{'name'=>'Rising Force', 'copies_sold'=>'100000'}}
|
525
|
+
|
526
|
+
One way to handle the form submission is to use Sequel::Model#set_fields.
|
527
|
+
|
528
|
+
album = Album[1]
|
529
|
+
album.set_fields(params['album'], %w'name copies_sold')
|
530
|
+
album.save
|
531
|
+
|
532
|
+
Note that you have to specify the parameter names again as the second argument to
|
533
|
+
set_fields.
|
534
|
+
|
535
|
+
Handling Submitted parameters becomes more complex as your forms become more complex.
|
536
|
+
For example, if you are only displaying certain form fields in certain situations:
|
537
|
+
|
538
|
+
album = Album[1]
|
539
|
+
Forme.form(album, :action=>'/foo') do |f|
|
540
|
+
f.input :name
|
541
|
+
f.input :copies_sold if album.released?
|
542
|
+
end
|
543
|
+
|
544
|
+
Then your parameter handling becomes more complex:
|
545
|
+
|
546
|
+
album = Album[1]
|
547
|
+
album.set_fields(params['album'], %w'name')
|
548
|
+
album.set_fields(params['album'], %w'copies_sold') if album.released?
|
549
|
+
album.save
|
550
|
+
|
551
|
+
As you can see, you basically need to recreate the conditionals used when creating
|
552
|
+
the form, so that that the processing of the form submission handles only the
|
553
|
+
inputs that were displayed on the form.
|
554
|
+
|
555
|
+
=== forme_set plugin
|
556
|
+
|
557
|
+
The forme_set plugin is designed to make handling form submissions easier. What it does
|
558
|
+
is record the form fields that are used on the object, and then it uses those fields
|
559
|
+
to handle input submitted for the object. For example:
|
560
|
+
|
561
|
+
album = Album[1]
|
562
|
+
Forme.form(album, :action=>'/foo') do |f|
|
563
|
+
f.input :name
|
564
|
+
f.input :copies_sold if album.released?
|
565
|
+
end
|
566
|
+
album.forme_set(params['album'])
|
567
|
+
|
568
|
+
If the album has been released, and the form would display the name and copies_sold
|
569
|
+
inputs, then forme_set will accept input for both. If the album has not been released,
|
570
|
+
the form will only display the name input, so forme_set will only accept the name input.
|
571
|
+
|
572
|
+
So forme_set offers two advantages over using set_fields:
|
573
|
+
|
574
|
+
1. DRYs up code as you don't have to specify the names twice
|
575
|
+
2. Simplifies complex form submissions by eliminating duplication of conditionals
|
576
|
+
|
577
|
+
==== Validations
|
578
|
+
|
579
|
+
forme_set offers one additional advantage over using set_fields. When dealing with
|
580
|
+
associations, set_fields does not check that the value submitted for an input matches
|
581
|
+
one of the available options displayed on the form. For example, if you have a form such as:
|
582
|
+
|
583
|
+
Forme.form(album, :action=>'/foo') do |f|
|
584
|
+
f.input :name
|
585
|
+
f.input :artist, :dataset=>proc{|ds| ds.where{name > 'M'}}
|
586
|
+
end
|
587
|
+
|
588
|
+
The form will only display artists whose name is greater than 'M'. However, if you process
|
589
|
+
input using:
|
590
|
+
|
591
|
+
album.set_fields(params['album'], %w'name artist_id')
|
592
|
+
|
593
|
+
Then a malicious user can submit an artist_id for an artist whose name is not greater than 'M',
|
594
|
+
and the value will be set. In addition to setting values, forme_set also adds validations
|
595
|
+
that the submitted values for associated objects match one of the options displayed on the form,
|
596
|
+
which can increase security.
|
597
|
+
|
598
|
+
==== Usage
|
599
|
+
|
600
|
+
Because forme_set relies on creating form inputs using the same model instance that will
|
601
|
+
be used for accepting input, using it often requires some code rearranging. If you are
|
602
|
+
storing Forme::Form objects and later using them on forms, it is fairly simple to move the
|
603
|
+
Forme::Forme object creation to a method, that you can call both in the initial display
|
604
|
+
and when processing the input:
|
605
|
+
|
606
|
+
def show_form(album)
|
607
|
+
Forme.form(album, :action=>'/foo') do |f|
|
608
|
+
f.input :name
|
609
|
+
f.input :copies_sold
|
610
|
+
end
|
611
|
+
end
|
612
|
+
|
613
|
+
Then when displaying the form:
|
614
|
+
|
615
|
+
<%= album_form(Album[1]) %>
|
616
|
+
|
617
|
+
and when processing the form's input:
|
618
|
+
|
619
|
+
album = Album[1]
|
620
|
+
album_form(album)
|
621
|
+
album.forme_set(params['album'])
|
622
|
+
|
623
|
+
However, if you use Forme's ERB/Rails template integration (see below), and are inlining
|
624
|
+
forms in your templates, unless you want to extract the Forme::Form creation to methods,
|
625
|
+
you have to basically rerender the template when processing the input. How you do
|
626
|
+
this is specific to the web framework you are using, but is it similar to:
|
627
|
+
|
628
|
+
@album = Album[1]
|
629
|
+
render :template
|
630
|
+
@album.forme_set(params['album'])
|
631
|
+
|
632
|
+
==== Caveats
|
633
|
+
|
634
|
+
forme_set is not perfect, there are ways to use Forme that forme_set will not handle
|
635
|
+
correctly. First, forme_set only works with forms that use model objects, and doesn't
|
636
|
+
handle inputs where the :obj option is provided to change the input.
|
637
|
+
Additionally, forme_set does not currently handle subform/nested_attributes.
|
638
|
+
|
639
|
+
In cases where forme_set does not handle things correctly, you can use forme_parse,
|
640
|
+
which will return metadata that forme_set uses (forme_set calls forme_parse
|
641
|
+
internally). forme_parse returns a hash with the following keys:
|
642
|
+
|
643
|
+
:values :: A hash of values that can be used to update the model, suitable for passing
|
644
|
+
to Sequel::Model#set.
|
645
|
+
:validations :: A hash of values suitable for merging into forme_validations. Used to
|
646
|
+
check that the submitted values for associated objects match one of the
|
647
|
+
options for the input in the form.
|
648
|
+
|
649
|
+
It is possible to use forme_set for the values it can handle, and set other fields manually
|
650
|
+
using set_fields.
|
651
|
+
|
652
|
+
== Other Sequel Plugins
|
653
|
+
|
654
|
+
In addition to the Sequel plugins mentioned above, Forme also ships with additional Sequel
|
655
|
+
plugins:
|
656
|
+
|
657
|
+
forme_i18n :: Handles translations for labels using i18n.
|
658
|
+
|
512
659
|
= ERB Support
|
513
660
|
|
514
661
|
Forme ships with a ERB extension that you can get by <tt>require "forme/erb"</tt> and using
|
data/Rakefile
CHANGED
data/lib/forme/erb.rb
CHANGED
@@ -16,7 +16,7 @@ module Forme
|
|
16
16
|
|
17
17
|
# Add CSRF token tag by default for POST forms
|
18
18
|
add_hidden_tag do |tag|
|
19
|
-
if defined?(::Rack::Csrf) && (form = tag.form) && (env = form.opts[:env]) && tag.attr[:method].to_s.upcase == 'POST'
|
19
|
+
if defined?(::Rack::Csrf) && (form = tag.form) && (env = form.opts[:env]) && env['rack.session'] && tag.attr[:method].to_s.upcase == 'POST'
|
20
20
|
{::Rack::Csrf.field=>::Rack::Csrf.token(env)}
|
21
21
|
end
|
22
22
|
end
|
data/lib/forme/version.rb
CHANGED
data/lib/sequel/plugins/forme.rb
CHANGED
@@ -293,11 +293,11 @@ module Sequel # :nodoc:
|
|
293
293
|
label = klass.send(:singularize, ref[:name])
|
294
294
|
|
295
295
|
field = if ref[:type] == :pg_array_to_many
|
296
|
-
handle_errors(key)
|
297
296
|
key
|
298
297
|
else
|
299
298
|
"#{label}_pks"
|
300
299
|
end
|
300
|
+
handle_errors(field)
|
301
301
|
|
302
302
|
unless opts.has_key?(:key)
|
303
303
|
opts[:array] = true unless opts.has_key?(:array)
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'i18n'
|
2
|
+
require 'sequel/plugins/forme'
|
3
|
+
|
4
|
+
module Sequel # :nodoc:
|
5
|
+
module Plugins # :nodoc:
|
6
|
+
# This Sequel plugin extends Forme usage with Sequel to support I18n
|
7
|
+
module FormeI18n
|
8
|
+
module SequelFormI18n
|
9
|
+
# Checks if there's a translation for the
|
10
|
+
# 'models.<table_name>.<association>' key and merge it to the options
|
11
|
+
# with the :legend key
|
12
|
+
#
|
13
|
+
# Calls the original Sequel::Plugins::Forme::SequelForm method
|
14
|
+
def subform(association, opts={}, &block)
|
15
|
+
i18n_key = "models.#{obj.class.table_name}.#{association}"
|
16
|
+
|
17
|
+
if opts[:legend].nil? && I18n.exists?(i18n_key)
|
18
|
+
opts[:legend] = I18n.t(i18n_key)
|
19
|
+
end
|
20
|
+
|
21
|
+
super
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.apply(model)
|
26
|
+
model.plugin(:forme)
|
27
|
+
end
|
28
|
+
|
29
|
+
module InstanceMethods
|
30
|
+
|
31
|
+
# Includes the SequelFormI18n methods on the original returned class
|
32
|
+
def forme_form_class(base)
|
33
|
+
klass = super
|
34
|
+
klass.send(:include, SequelFormI18n)
|
35
|
+
klass
|
36
|
+
end
|
37
|
+
|
38
|
+
# Checks if there's a translation for the 'models.<table_name>.<field>'
|
39
|
+
# key and merge it to the options with the :label key
|
40
|
+
#
|
41
|
+
# Calls the original Sequel::Plugins::Forme method
|
42
|
+
def forme_input(form, field, opts)
|
43
|
+
i18n_key = "models.#{self.class.table_name}.#{field}"
|
44
|
+
|
45
|
+
if opts[:label].nil? && I18n.exists?(i18n_key)
|
46
|
+
opts[:label] = I18n.t(i18n_key)
|
47
|
+
end
|
48
|
+
|
49
|
+
super
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
module Sequel # :nodoc:
|
4
|
+
module Plugins # :nodoc:
|
5
|
+
# The forme_set plugin makes the model instance keep track of which form
|
6
|
+
# inputs have been added for it. It adds a forme_set method to handle
|
7
|
+
# the intake of submitted data from the form. For more complete control,
|
8
|
+
# it also adds a forme_parse method that returns a hash of information that can be
|
9
|
+
# used to modify and validate the object.
|
10
|
+
module FormeSet
|
11
|
+
SKIP_FORMATTERS = [:disabled, :readonly, ::Forme::Formatter::Disabled, ::Forme::Formatter::ReadOnly]
|
12
|
+
|
13
|
+
# Depend on the forme plugin, as forme_input already needs to be defined.
|
14
|
+
def self.apply(model)
|
15
|
+
model.plugin :forme
|
16
|
+
end
|
17
|
+
|
18
|
+
module InstanceMethods
|
19
|
+
# Hash with column name symbol keys and Forme::SequelInput values
|
20
|
+
def forme_inputs
|
21
|
+
@forme_inputs ||= {}
|
22
|
+
end
|
23
|
+
|
24
|
+
# Hash with column name symbol keys and <tt>[subset, allowed_values]</tt> values. +subset+
|
25
|
+
# is a boolean flag, if true, the uploaded values should be a subset of the allowed values,
|
26
|
+
# otherwise, there should be a single uploaded value that is a member of the allowed values.
|
27
|
+
def forme_validations
|
28
|
+
@forme_validations ||= {}
|
29
|
+
end
|
30
|
+
|
31
|
+
# Keep track of the inputs used.
|
32
|
+
def forme_input(_form, field, _opts)
|
33
|
+
forme_inputs[field] = super
|
34
|
+
end
|
35
|
+
|
36
|
+
# Given the hash of submitted parameters, return a hash containing information on how to
|
37
|
+
# set values in the model based on the inputs used on the related form. Currently, the
|
38
|
+
# hash contains the following information:
|
39
|
+
# :values :: A hash of values that can be used to update the model, suitable for passing
|
40
|
+
# to Sequel::Model#set.
|
41
|
+
# :validations :: A hash of values suitable for merging into forme_validations. Used to
|
42
|
+
# check that the submitted values for associated objects match one of the
|
43
|
+
# options for the input in the form.
|
44
|
+
def forme_parse(params)
|
45
|
+
hash = {}
|
46
|
+
hash_values = hash[:values] = {}
|
47
|
+
validations = hash[:validations] = {}
|
48
|
+
|
49
|
+
forme_inputs.each do |field, input|
|
50
|
+
opts = input.opts
|
51
|
+
next if SKIP_FORMATTERS.include?(opts.fetch(:formatter){input.form.opts[:formatter]})
|
52
|
+
|
53
|
+
if attr = opts[:attr]
|
54
|
+
name = attr[:name] || attr['name']
|
55
|
+
end
|
56
|
+
name ||= opts[:name] || opts[:key] || next
|
57
|
+
|
58
|
+
# Pull out last component of the name if there is one
|
59
|
+
column = (name =~ /\[([^\[\]]+)\]\z/ ? $1 : name)
|
60
|
+
column = column.to_s.sub(/\[\]\z/, '').to_sym
|
61
|
+
|
62
|
+
hash_values[column] = params[column] || params[column.to_s]
|
63
|
+
|
64
|
+
next unless ref = model.association_reflection(field)
|
65
|
+
next unless options = opts[:options]
|
66
|
+
|
67
|
+
values = if opts[:text_method]
|
68
|
+
value_method = opts[:value_method] || opts[:text_method]
|
69
|
+
options.map(&value_method)
|
70
|
+
else
|
71
|
+
options.map{|obj| obj.is_a?(Array) ? obj.last : obj}
|
72
|
+
end
|
73
|
+
|
74
|
+
if ref[:type] == :many_to_one && !opts[:required]
|
75
|
+
values << nil
|
76
|
+
end
|
77
|
+
validations[column] = [ref[:type] != :many_to_one ? :subset : :include, values]
|
78
|
+
end
|
79
|
+
|
80
|
+
hash
|
81
|
+
end
|
82
|
+
|
83
|
+
# Set the values in the object based on the parameters parsed from the form, and add
|
84
|
+
# validations based on the form to ensure that associated objects match form values.
|
85
|
+
def forme_set(params)
|
86
|
+
hash = forme_parse(params)
|
87
|
+
set(hash[:values])
|
88
|
+
unless hash[:validations].empty?
|
89
|
+
forme_validations.merge!(hash[:validations])
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Check associated values to ensure they match one of options in the form.
|
94
|
+
def validate
|
95
|
+
super
|
96
|
+
|
97
|
+
if validations = @forme_validations
|
98
|
+
validations.each do |column, (type, values)|
|
99
|
+
value = send(column)
|
100
|
+
|
101
|
+
valid = case type
|
102
|
+
when :subset
|
103
|
+
# Handle missing value the same as the empty array,
|
104
|
+
# can happen with PostgreSQL array associations
|
105
|
+
!value || (value - values).empty?
|
106
|
+
when :include
|
107
|
+
values.include?(value)
|
108
|
+
else
|
109
|
+
raise Forme::Error, "invalid type used in forme_validations"
|
110
|
+
end
|
111
|
+
|
112
|
+
unless valid
|
113
|
+
errors.add(column, 'invalid value submitted')
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
data/spec/all.rb
ADDED
data/spec/forme_spec.rb
CHANGED
@@ -646,6 +646,10 @@ describe "Forme plain forms" do
|
|
646
646
|
@f.input(:textarea, :labeler=>:explicit, :label=>'bar', :id=>:foo, :label_position=>:after).to_s.must_equal '<textarea id="foo"></textarea><label class="label-after" for="foo">bar</label>'
|
647
647
|
end
|
648
648
|
|
649
|
+
it "inputs handle explicit labels with :key_id" do
|
650
|
+
@f.input(:textarea, :labeler=>:explicit, :label=>'bar', :key=>:bar, :key_id=>:foo).to_s.must_equal '<label class="label-before" for="bar_foo">bar</label><textarea id="bar_foo" name="bar"></textarea>'
|
651
|
+
end
|
652
|
+
|
649
653
|
it "should handle explicit labels with checkboxes" do
|
650
654
|
@f.input(:checkbox, :labeler=>:explicit, :label=>'Foo', :value=>'foo', :name=>'a', :id=>'bar').to_s.must_equal '<input id="bar_hidden" name="a" type="hidden" value="0"/><input id="bar" name="a" type="checkbox" value="foo"/><label class="label-after" for="bar">Foo</label>'
|
651
655
|
end
|
data/spec/sequel_helper.rb
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
require 'rubygems'
|
2
2
|
require 'sequel/no_core_ext'
|
3
3
|
|
4
|
-
|
4
|
+
db_url = defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby' ? 'jdbc:sqlite::memory:' : 'sqlite:/'
|
5
|
+
DB = Sequel.connect(db_url)
|
5
6
|
Sequel.default_timezone = :utc
|
6
7
|
DB.create_table(:artists) do
|
7
8
|
primary_key :id
|
@@ -50,10 +51,13 @@ u = DB[:tags].insert(:name=>'u')
|
|
50
51
|
|
51
52
|
Sequel::Model.plugin :forme
|
52
53
|
class Album < Sequel::Model
|
54
|
+
plugin :association_pks
|
55
|
+
plugin :forme_set
|
56
|
+
|
53
57
|
many_to_one :artist, :order=>:name
|
54
58
|
one_to_one :album_info
|
55
59
|
one_to_many :tracks
|
56
|
-
many_to_many :tags
|
60
|
+
many_to_many :tags, :delay_pks=>:always
|
57
61
|
|
58
62
|
plugin :pg_array_associations
|
59
63
|
pg_array_to_many :atags, :class=>:Tag
|
@@ -65,6 +69,13 @@ class Album < Sequel::Model
|
|
65
69
|
def artist_name
|
66
70
|
artist.name if artist
|
67
71
|
end
|
72
|
+
|
73
|
+
alias foo= name=
|
74
|
+
|
75
|
+
if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby'
|
76
|
+
# Add workaround for no boolean handling in jdbc-sqlite3
|
77
|
+
plugin :typecast_on_load, :gold, :platinum, :release_date, :created_at
|
78
|
+
end
|
68
79
|
end
|
69
80
|
class Artist < Sequel::Model
|
70
81
|
one_to_many :albums
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require File.join(File.dirname(File.expand_path(__FILE__)), 'sequel_helper.rb')
|
2
|
+
|
3
|
+
gem 'i18n', '>= 0.7.0'
|
4
|
+
require 'i18n'
|
5
|
+
I18n.load_path = [File.join(File.dirname(File.expand_path(__FILE__)), 'i18n_helper.yml')]
|
6
|
+
|
7
|
+
DB.create_table(:firms) do
|
8
|
+
primary_key :id
|
9
|
+
String :name
|
10
|
+
end
|
11
|
+
DB.create_table(:invoices) do
|
12
|
+
primary_key :id
|
13
|
+
foreign_key :firm_id, :firms
|
14
|
+
String :name
|
15
|
+
String :summary
|
16
|
+
end
|
17
|
+
DB.create_table(:clients) do
|
18
|
+
primary_key :id
|
19
|
+
foreign_key :firm_id, :firms
|
20
|
+
String :name
|
21
|
+
end
|
22
|
+
|
23
|
+
a = DB[:firms].insert(:name=>'a')
|
24
|
+
b = DB[:invoices].insert(:name=>'b', :firm_id=>a, :summary=>'a brief summary')
|
25
|
+
DB[:clients].insert(:name=>'a great client', :firm_id=>a)
|
26
|
+
|
27
|
+
class Firm < Sequel::Model
|
28
|
+
one_to_many :invoices
|
29
|
+
one_to_many :clients
|
30
|
+
end
|
31
|
+
|
32
|
+
class Invoice < Sequel::Model
|
33
|
+
many_to_one :firm
|
34
|
+
end
|
35
|
+
|
36
|
+
class Client < Sequel::Model
|
37
|
+
many_to_one :firm
|
38
|
+
end
|
39
|
+
|
40
|
+
[Firm, Invoice, Client].each{|c| c.plugin :forme_i18n }
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require File.join(File.dirname(File.expand_path(__FILE__)), 'spec_helper.rb')
|
2
|
+
|
3
|
+
begin
|
4
|
+
require File.join(File.dirname(File.expand_path(__FILE__)), 'sequel_i18n_helper.rb')
|
5
|
+
rescue LoadError
|
6
|
+
warn "unable to load i18n, skipping i18n Sequel plugin spec"
|
7
|
+
else
|
8
|
+
describe "Forme Sequel::Model forms" do
|
9
|
+
before do
|
10
|
+
@ab = Invoice[1]
|
11
|
+
@b = Forme::Form.new(@ab)
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should not change the usual label input if translation is not present" do
|
15
|
+
@b.input(:name).to_s.must_equal '<label>Name: <input id="invoice_name" name="invoice[name]" type="text" value="b"/></label>'
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should use the translation for the label if present" do
|
19
|
+
@b.input(:summary).to_s.must_equal '<label>Brief Description: <input id="invoice_summary" name="invoice[summary]" type="text" value="a brief summary"/></label>'
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should not change the usual legend for the subform if the translation is not present" do
|
23
|
+
Forme.form(Firm[1]){|f| f.subform(:invoices){ f.input(:name) }}.to_s.must_equal "<form class=\"forme firm\" method=\"post\"><input id=\"firm_invoices_attributes_0_id\" name=\"firm[invoices_attributes][0][id]\" type=\"hidden\" value=\"1\"/><fieldset class=\"inputs\"><legend>Invoice #1</legend><label>Name: <input id=\"firm_invoices_attributes_0_name\" name=\"firm[invoices_attributes][0][name]\" type=\"text\" value=\"b\"/></label></fieldset></form>"
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should use the translation for the legend on the subform if present" do
|
27
|
+
Forme.form(Firm[1]){|f| f.subform(:clients){ f.input(:name) }}.to_s.must_equal "<form class=\"forme firm\" method=\"post\"><input id=\"firm_clients_attributes_0_id\" name=\"firm[clients_attributes][0][id]\" type=\"hidden\" value=\"1\"/><fieldset class=\"inputs\"><legend>Clientes</legend><label>Name: <input id=\"firm_clients_attributes_0_name\" name=\"firm[clients_attributes][0][name]\" type=\"text\" value=\"a great client\"/></label></fieldset></form>"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/spec/sequel_plugin_spec.rb
CHANGED
@@ -159,6 +159,16 @@ describe "Forme Sequel::Model forms" do
|
|
159
159
|
@b.input(:artist, :type=>:string, :value=>nil).to_s.must_equal '<label>Artist: <input id="album_artist" name="album[artist]" type="text"/></label>'
|
160
160
|
end
|
161
161
|
|
162
|
+
it "should automatically set :required for many_to_one assocations based on whether the field is required" do
|
163
|
+
begin
|
164
|
+
Album.db_schema[:artist_id][:allow_null] = false
|
165
|
+
@b.input(:artist).to_s.must_equal '<label>Artist<abbr title="required">*</abbr>: <select id="album_artist_id" name="album[artist_id]" required="required"><option selected="selected" value="1">a</option><option value="2">d</option></select></label>'
|
166
|
+
@b.input(:artist, :required=>false).to_s.must_equal '<label>Artist: <select id="album_artist_id" name="album[artist_id]"><option value=""></option><option selected="selected" value="1">a</option><option value="2">d</option></select></label>'
|
167
|
+
ensure
|
168
|
+
Album.db_schema[:artist_id][:allow_null] = true
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
162
172
|
it "should use a required wrapper tag for many_to_one required associations" do
|
163
173
|
@b.input(:artist, :required=>true, :wrapper=>:li).to_s.must_equal '<li class="many_to_one required"><label>Artist<abbr title="required">*</abbr>: <select id="album_artist_id" name="album[artist_id]" required="required"><option selected="selected" value="1">a</option><option value="2">d</option></select></label></li>'
|
164
174
|
end
|
@@ -0,0 +1,183 @@
|
|
1
|
+
require File.join(File.dirname(File.expand_path(__FILE__)), 'spec_helper.rb')
|
2
|
+
require File.join(File.dirname(File.expand_path(__FILE__)), 'sequel_helper.rb')
|
3
|
+
|
4
|
+
describe "Sequel forme_set plugin" do
|
5
|
+
before do
|
6
|
+
@ab = Album.new
|
7
|
+
@f = Forme::Form.new(@ab)
|
8
|
+
end
|
9
|
+
|
10
|
+
it "#forme_set should only set values in the form" do
|
11
|
+
@ab.forme_set(:name=>'Foo')
|
12
|
+
@ab.name.must_equal nil
|
13
|
+
|
14
|
+
@f.input(:name)
|
15
|
+
@ab.forme_set(:name=>'Foo')
|
16
|
+
@ab.name.must_equal 'Foo'
|
17
|
+
|
18
|
+
@ab.forme_set('copies_sold'=>'1')
|
19
|
+
@ab.name.must_equal nil
|
20
|
+
@ab.copies_sold.must_equal nil
|
21
|
+
|
22
|
+
@f.input(:copies_sold)
|
23
|
+
@ab.forme_set('name'=>'Bar', 'copies_sold'=>'1')
|
24
|
+
@ab.name.must_equal 'Bar'
|
25
|
+
@ab.copies_sold.must_equal 1
|
26
|
+
end
|
27
|
+
|
28
|
+
it "#forme_set should handle different ways to specify parameter names" do
|
29
|
+
[{:attr=>{:name=>'foo'}}, {:attr=>{'name'=>:foo}}, {:name=>'foo'}, {:name=>'bar[foo]'}, {:key=>:foo}].each do |opts|
|
30
|
+
@f.input(:name, opts)
|
31
|
+
|
32
|
+
@ab.forme_set(:name=>'Foo')
|
33
|
+
@ab.name.must_equal nil
|
34
|
+
|
35
|
+
@ab.forme_set('foo'=>'Foo')
|
36
|
+
@ab.name.must_equal 'Foo'
|
37
|
+
@ab.forme_inputs.clear
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
it "#forme_set should ignore values where key is explicitly set to nil" do
|
42
|
+
@f.input(:name, :key=>nil)
|
43
|
+
@ab.forme_set(:name=>'Foo')
|
44
|
+
@ab.name.must_equal nil
|
45
|
+
@ab.forme_set(nil=>'Foo')
|
46
|
+
@ab.name.must_equal nil
|
47
|
+
end
|
48
|
+
|
49
|
+
it "#forme_set should skip inputs with disabled/readonly formatter" do
|
50
|
+
@f.input(:name, :formatter=>:disabled)
|
51
|
+
@ab.forme_set(:name=>'Foo')
|
52
|
+
@ab.name.must_equal nil
|
53
|
+
|
54
|
+
@f.input(:name, :formatter=>:readonly)
|
55
|
+
@ab.forme_set(:name=>'Foo')
|
56
|
+
@ab.name.must_equal nil
|
57
|
+
|
58
|
+
@f.input(:name, :formatter=>:default)
|
59
|
+
@ab.forme_set(:name=>'Foo')
|
60
|
+
@ab.name.must_equal 'Foo'
|
61
|
+
end
|
62
|
+
|
63
|
+
it "#forme_set should skip inputs with disabled/readonly formatter" do
|
64
|
+
@f = Forme::Form.new(@ab, :formatter=>:disabled)
|
65
|
+
@f.input(:name)
|
66
|
+
@ab.forme_set(:name=>'Foo')
|
67
|
+
@ab.name.must_equal nil
|
68
|
+
|
69
|
+
@f = Forme::Form.new(@ab, :formatter=>:readonly)
|
70
|
+
@f.input(:name)
|
71
|
+
@ab.forme_set(:name=>'Foo')
|
72
|
+
@ab.name.must_equal nil
|
73
|
+
|
74
|
+
@f.input(:name, :formatter=>:default)
|
75
|
+
@ab.forme_set(:name=>'Foo')
|
76
|
+
@ab.name.must_equal 'Foo'
|
77
|
+
end
|
78
|
+
|
79
|
+
it "#forme_set should handle setting values for associated objects" do
|
80
|
+
@ab.forme_set(:artist_id=>1)
|
81
|
+
@ab.artist_id.must_equal nil
|
82
|
+
|
83
|
+
@f.input(:artist)
|
84
|
+
@ab.forme_set(:artist_id=>'1')
|
85
|
+
@ab.artist_id.must_equal 1
|
86
|
+
|
87
|
+
@ab.forme_set('tag_pks'=>%w'1 2')
|
88
|
+
@ab.artist_id.must_equal nil
|
89
|
+
@ab.tag_pks.must_equal []
|
90
|
+
|
91
|
+
@f.input(:tags)
|
92
|
+
@ab.forme_set('artist_id'=>'1', 'tag_pks'=>%w'1 2')
|
93
|
+
@ab.artist_id.must_equal 1
|
94
|
+
@ab.tag_pks.must_equal [1, 2]
|
95
|
+
end
|
96
|
+
|
97
|
+
it "#forme_set should handle validations for filtered associations" do
|
98
|
+
[
|
99
|
+
[{:dataset=>proc{|ds| ds.exclude(:id=>1)}},
|
100
|
+
{:dataset=>proc{|ds| ds.exclude(:id=>1)}}],
|
101
|
+
[{:options=>Artist.exclude(:id=>1).select_order_map([:name, :id])},
|
102
|
+
{:options=>Tag.exclude(:id=>1).select_order_map(:id), :name=>'tag_pks[]'}],
|
103
|
+
[{:options=>Artist.exclude(:id=>1).all, :text_method=>:name, :value_method=>:id},
|
104
|
+
{:options=>Tag.exclude(:id=>1).all, :text_method=>:name, :value_method=>:id}],
|
105
|
+
].each do |artist_opts, tag_opts|
|
106
|
+
@ab.forme_inputs.clear
|
107
|
+
@f.input(:artist, artist_opts)
|
108
|
+
@f.input(:tags, tag_opts)
|
109
|
+
|
110
|
+
@ab.forme_validations.clear
|
111
|
+
@ab.forme_set('artist_id'=>'1', 'tag_pks'=>%w'1 2')
|
112
|
+
@ab.artist_id.must_equal 1
|
113
|
+
@ab.tag_pks.must_equal [1, 2]
|
114
|
+
|
115
|
+
@ab.valid?.must_equal false
|
116
|
+
@ab.errors[:artist_id].must_equal ['invalid value submitted']
|
117
|
+
@ab.errors[:tag_pks].must_equal ['invalid value submitted']
|
118
|
+
|
119
|
+
@ab.forme_validations.clear
|
120
|
+
@ab.forme_set('artist_id'=>'1', 'tag_pks'=>['2'])
|
121
|
+
@ab.valid?.must_equal false
|
122
|
+
@ab.errors[:artist_id].must_equal ['invalid value submitted']
|
123
|
+
@ab.errors[:tag_pks].must_equal nil
|
124
|
+
|
125
|
+
@ab.forme_validations.clear
|
126
|
+
@ab.forme_set('artist_id'=>'2', 'tag_pks'=>['2'])
|
127
|
+
@ab.valid?.must_equal true
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
it "#forme_set should not require associated values for many_to_one association with select boxes" do
|
132
|
+
@f.input(:artist)
|
133
|
+
@ab.forme_set({})
|
134
|
+
@ab.valid?.must_equal true
|
135
|
+
end
|
136
|
+
|
137
|
+
it "#forme_set should not require associated values for many_to_one association with radio buttons" do
|
138
|
+
@f.input(:artist, :as=>:radio)
|
139
|
+
@ab.forme_set({})
|
140
|
+
@ab.valid?.must_equal true
|
141
|
+
end
|
142
|
+
|
143
|
+
it "#forme_set should require associated values for many_to_one association with select boxes when :required is used" do
|
144
|
+
@f.input(:artist, :required=>true)
|
145
|
+
@ab.forme_set({})
|
146
|
+
@ab.valid?.must_equal false
|
147
|
+
@ab.errors[:artist_id].must_equal ['invalid value submitted']
|
148
|
+
end
|
149
|
+
|
150
|
+
it "#forme_set should require associated values for many_to_one association with radio buttons when :required is used" do
|
151
|
+
@f.input(:artist, :as=>:radio, :required=>true)
|
152
|
+
@ab.forme_set({})
|
153
|
+
@ab.valid?.must_equal false
|
154
|
+
@ab.errors[:artist_id].must_equal ['invalid value submitted']
|
155
|
+
end
|
156
|
+
|
157
|
+
it "#forme_set should handle cases where currently associated values is nil" do
|
158
|
+
@f.input(:tags)
|
159
|
+
@ab.forme_set({:tag_pks=>[1]})
|
160
|
+
def @ab.tag_pks; nil; end
|
161
|
+
@ab.valid?.must_equal true
|
162
|
+
end
|
163
|
+
|
164
|
+
it "#forme_parse should return hash with values and validations" do
|
165
|
+
@f.input(:name)
|
166
|
+
@ab.forme_parse(:name=>'Foo').must_equal(:values=>{:name=>'Foo'}, :validations=>{})
|
167
|
+
|
168
|
+
@f.input(:artist, :dataset=>proc{|ds| ds.exclude(:id=>1)})
|
169
|
+
hash = @ab.forme_parse(:name=>'Foo', 'artist_id'=>'1')
|
170
|
+
hash[:values] = {:name=>'Foo', :artist_id=>'1'}
|
171
|
+
@ab.set(hash[:values])
|
172
|
+
@ab.valid?.must_equal true
|
173
|
+
|
174
|
+
@ab.forme_validations.merge!(hash[:validations])
|
175
|
+
@ab.valid?.must_equal false
|
176
|
+
@ab.errors[:artist_id].must_equal ['invalid value submitted']
|
177
|
+
end
|
178
|
+
|
179
|
+
it "#forme_parse should return hash with values and validations" do
|
180
|
+
@ab.forme_validations[:name] = [:bar, []]
|
181
|
+
proc{@ab.valid?}.must_raise Sequel::Plugins::Forme::Error
|
182
|
+
end
|
183
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: forme
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jeremy Evans
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-08-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: minitest
|
@@ -122,6 +122,20 @@ dependencies:
|
|
122
122
|
- - ">="
|
123
123
|
- !ruby/object:Gem::Version
|
124
124
|
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: i18n
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
125
139
|
description: |
|
126
140
|
Forme is a forms library with the following goals:
|
127
141
|
|
@@ -160,6 +174,9 @@ files:
|
|
160
174
|
- lib/forme/version.rb
|
161
175
|
- lib/roda/plugins/forme.rb
|
162
176
|
- lib/sequel/plugins/forme.rb
|
177
|
+
- lib/sequel/plugins/forme_i18n.rb
|
178
|
+
- lib/sequel/plugins/forme_set.rb
|
179
|
+
- spec/all.rb
|
163
180
|
- spec/bs3_reference_spec.rb
|
164
181
|
- spec/bs3_sequel_plugin_spec.rb
|
165
182
|
- spec/bs3_spec.rb
|
@@ -169,7 +186,10 @@ files:
|
|
169
186
|
- spec/rails_integration_spec.rb
|
170
187
|
- spec/roda_integration_spec.rb
|
171
188
|
- spec/sequel_helper.rb
|
189
|
+
- spec/sequel_i18n_helper.rb
|
190
|
+
- spec/sequel_i18n_plugin_spec.rb
|
172
191
|
- spec/sequel_plugin_spec.rb
|
192
|
+
- spec/sequel_set_plugin_spec.rb
|
173
193
|
- spec/sinatra_integration_spec.rb
|
174
194
|
- spec/spec_helper.rb
|
175
195
|
homepage: http://github.com/jeremyevans/forme
|