forme 1.4.0 → 1.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.
- 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
|