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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b392c21e8ea5b18c3b1c125b84c36379b40595bf
4
- data.tar.gz: de4ef22b48745082a9661989dd745824ef75ea07
3
+ metadata.gz: fb06104f826067a8f31ba5778bed02029abdbb7a
4
+ data.tar.gz: 910a345e355680be66a4d049edaa35e8ecca6186
5
5
  SHA512:
6
- metadata.gz: bfc76ba5baa373ce677fe2fc7f44703d2e8783b9752e667a356a454293b11efecabe4b03fab498b040ac7afd3e0bc10a09f463681f5b839fa7620a984ba7bc1b
7
- data.tar.gz: 93b41b112be039b14e2368618e9bc00d8599d7935828fddc27870bdd1426199c58fbc6c63b4085c48368e87c717ed5b0f952d76d684143ef455e1d19854c078f
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)
@@ -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
@@ -50,7 +50,7 @@ end
50
50
 
51
51
  desc "Run specs"
52
52
  task :spec do
53
- sh "#{FileUtils::RUBY} -rubygems -I lib -e 'ARGV.each{|f| require f}' ./spec/*_spec.rb"
53
+ sh "#{FileUtils::RUBY} spec/all.rb"
54
54
  end
55
55
  task :default => :spec
56
56
 
@@ -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
@@ -55,7 +55,7 @@ module Forme
55
55
  namespaces = input.form_opts[:namespace]
56
56
  id = "#{namespaces.join('_')}#{'_' unless namespaces.empty?}#{key}"
57
57
  if key_id = input.opts[:key_id]
58
- id << "_#{key_id.to_s}"
58
+ id += "_#{key_id.to_s}"
59
59
  end
60
60
  end
61
61
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Forme
4
4
  # Version constant, use <tt>Forme.version</tt> instead.
5
- VERSION = '1.4.0'.freeze
5
+ VERSION = '1.5.0'.freeze
6
6
 
7
7
  # Returns the version as a frozen string (e.g. '0.1.0')
8
8
  def self.version
@@ -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
@@ -0,0 +1,3 @@
1
+ require 'rubygems'
2
+ $: << 'lib'
3
+ Dir['./spec/*_spec.rb'].each{|f| require f}
@@ -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
@@ -1,7 +1,8 @@
1
1
  require 'rubygems'
2
2
  require 'sequel/no_core_ext'
3
3
 
4
- DB = Sequel.sqlite
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
@@ -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.0
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-02-02 00:00:00.000000000 Z
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