autoforme 1.7.0 → 1.10.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
- SHA1:
3
- metadata.gz: 83fda57cc1135158108ed30103403c23d0600ccf
4
- data.tar.gz: 42e3f1e0bae263ea3e5babc767cb3492bf2f5efe
2
+ SHA256:
3
+ metadata.gz: 6f9fb399d144c4bfc7eed6e2a4f7497cab7bc25b8409d555c4f67fb91bad9bb7
4
+ data.tar.gz: bffb9114ef162400c639abb1ee17b7eb42ced481294d152e2603711c01752274
5
5
  SHA512:
6
- metadata.gz: 0d716b34aec6a9b8a7e85e6c3928a029240ff1959b3e94358cdbcd974164f34a6a68206d698977d5cdfedbf253b6973ae04713574693f20d18c6933647e7c47f
7
- data.tar.gz: fc8efa80a2e498952c10c1052b9eccee17d20a5d488e7416537e3eae89b76f6505cddf3058be80580f139e959f5d3a9f82c2c8bbfb411b172f7783e704b5f0c0
6
+ metadata.gz: 15cd700c5edc91d860dcec0209dae57dd3db48ebe2bc6cf66f19b9d786d7a51b23f909ec80a0a2333af8d9c42c165690dd6710c26ebfadfbfa93c9d17618976b
7
+ data.tar.gz: 0c4a42c4e34e398209f98e85303558ef95447eb6a1e8f6351a182f214a1dab7b754f86aca8d81d35ba32d5890a09fa417a28c7e8212a2284df5d265c776533e8
data/CHANGELOG CHANGED
@@ -1,3 +1,27 @@
1
+ === 1.10.0 (2021-08-27)
2
+
3
+ * Do not consider read_only many_to_many associations to be editable (jeremyevans)
4
+
5
+ * Ignore unique constraint violations when adding associated objects in mtm_update (jeremyevans)
6
+
7
+ * Handle search fields that cannot be typecast correctly by returning no results (jeremyevans)
8
+
9
+ === 1.9.1 (2019-07-22)
10
+
11
+ * [SECURITY] Escape object display name when displaying association links (adam12)
12
+
13
+ === 1.9.0 (2018-07-18)
14
+
15
+ * Add support for using flash string keys in the Roda support, to work with Roda's sessions plugin (jeremyevans)
16
+
17
+ * Show correct page title on error pages (jeremyevans)
18
+
19
+ === 1.8.0 (2018-06-11)
20
+
21
+ * Add support for Roda route_csrf plugin for request-specific CSRF tokens (jeremyevans)
22
+
23
+ * Default to size of 10 for select multiple inputs (jeremyevans)
24
+
1
25
  === 1.7.0 (2017-10-27)
2
26
 
3
27
  * Respect Model#forme_namespace method for parameter names (adam12, jeremyevans) (#9)
data/MIT-LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2013-2017 Jeremy Evans
1
+ Copyright (c) 2013-2021 Jeremy Evans
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining a copy
4
4
  of this software and associated documentation files (the "Software"), to
data/README.rdoc CHANGED
@@ -22,8 +22,7 @@ flexible in terms of configuration.
22
22
  Demo Site :: http://autoforme-demo.jeremyevans.net
23
23
  RDoc :: http://autoforme.jeremyevans.net
24
24
  Source :: https://github.com/jeremyevans/autoforme
25
- IRC :: irc://irc.freenode.net/forme
26
- Google Group :: https://groups.google.com/forum/#!forum/ruby-forme
25
+ Discussion Forum :: https://github.com/jeremyevans/autoforme/discussions
27
26
  Bug Tracker :: https://github.com/jeremyevans/autoforme/issues
28
27
 
29
28
  = Features
@@ -161,12 +160,13 @@ These options are related to displayed output:
161
160
 
162
161
  form_attributes :: Hash of attributes to use for any form tags
163
162
  form_options :: Hash of Forme::Form options to pass for any forms created
164
- class_display_name :: The string to use when referring to the model class
163
+ class_display_name :: The string to use on pages when referring to the model class.
164
+ This defaults to the full class name.
165
165
  display_name :: The string to use when referring to a model instance. Can either be a symbol
166
166
  representing an instance method call, or a Proc called with the model object,
167
167
  the model object and type symbol, or the model object, type symbol, and request,
168
168
  depending on the arity of the Proc.
169
- link_name :: The string to use in links for the class
169
+ link_name :: The string to use in links for the class. This defaults to +class_display_name+.
170
170
  edit_html :: The html to use for a particular object edit field. Should be a proc that takes the
171
171
  model object, column symbol, type symbol, and request and returns the html to use.
172
172
  page_footer :: Override the default footer used for pages
@@ -197,7 +197,7 @@ symbol and request).
197
197
 
198
198
  Additionally, AutoForme.for accepts a :prefix option that controls where the forms are mounted:
199
199
 
200
- AutoForm.for(:sinatra, self, :prefix=>'/path/to') do
200
+ AutoForme.for(:sinatra, self, :prefix=>'/path/to') do
201
201
  model Artist
202
202
  end
203
203
 
@@ -238,6 +238,66 @@ use AutoForme in Rails development mode. The best way to handle it is to call
238
238
  +AutoForme.for+ in the related controller file, and have an initializer reference
239
239
  the controller class, causing the controller file to be loaded.
240
240
 
241
+ = Roda
242
+
243
+ Because Roda uses a routing tree, unlike Rails and Sinatra, with Roda you need to
244
+ dispatch to the autoforme routes at the point in the routing tree where you want
245
+ to mount them. Additionally, the Roda support offers a Roda plugin for easier
246
+ configuration.
247
+
248
+ To mount the autoforme routes in the root of the application, you could do:
249
+
250
+ class App < Roda
251
+ plugin :autoforme do
252
+ model Artist
253
+ end
254
+
255
+ route do
256
+ # rest of routing tree
257
+ autoforme
258
+ end
259
+ end
260
+
261
+ To mount the routes in a subpath:
262
+
263
+ class App < Roda
264
+ plugin :autoforme do
265
+ model Artist
266
+ end
267
+
268
+ route do
269
+ r.on "admin" do
270
+ autoforme
271
+ end
272
+
273
+ # rest of routing tree
274
+ end
275
+ end
276
+
277
+ To handle multiple autoforme configurations, mounted at different subpaths:
278
+
279
+ class App < Roda
280
+ plugin :autoforme
281
+
282
+ autoforme(name: 'artists')
283
+ model Artist
284
+ end
285
+ autoforme(name: 'albums')
286
+ model Album
287
+ end
288
+
289
+ route do
290
+ r.on "artists" do
291
+ autoforme('artists')
292
+ end
293
+ r.on "albums" do
294
+ autoforme('albums')
295
+ end
296
+
297
+ # rest of routing tree
298
+ end
299
+ end
300
+
241
301
  = TODO
242
302
 
243
303
  * capybara-webkit tests for ajax behavior
@@ -87,7 +87,7 @@ module AutoForme
87
87
  else
88
88
  return false unless model.supported_action?(normalized_type, request)
89
89
 
90
- if title = TITLE_MAP[type]
90
+ if title = TITLE_MAP[@normalized_type]
91
91
  @title = "#{model.class_name} - #{title}"
92
92
  end
93
93
  end
@@ -225,11 +225,11 @@ module AutoForme
225
225
  end
226
226
 
227
227
  # Options to use for the form. If the form uses POST, automatically adds the CSRF token.
228
- def form_opts
228
+ def form_opts(action=nil)
229
229
  opts = model.form_options_for(type, request).dup
230
230
  hidden_tags = opts[:hidden_tags] = []
231
- if csrf = request.csrf_token_hash
232
- hidden_tags << lambda{|tag| csrf if tag.attr[:method].to_s.upcase == 'POST'}
231
+ if csrf = request.csrf_token_hash(action)
232
+ hidden_tags << lambda{|tag| csrf if (tag.attr[:method] || tag.attr['method']).to_s.upcase == 'POST'}
233
233
  end
234
234
  opts
235
235
  end
@@ -243,7 +243,8 @@ module AutoForme
243
243
  # HTML content used for the new action
244
244
  def new_page(obj, opts={})
245
245
  page do
246
- Forme.form(obj, form_attributes(:action=>url_for("create")), form_opts) do |f|
246
+ form_attr = form_attributes(:action=>url_for("create"))
247
+ Forme.form(obj, form_attr, form_opts(form_attr[:action])) do |f|
247
248
  model.columns_for(:new, request).each do |column|
248
249
  col_opts = column_options_for(:new, request, obj, column)
249
250
  if html = model.edit_html_for(obj, column, :new, request)
@@ -318,7 +319,8 @@ module AutoForme
318
319
  end.to_s
319
320
  end
320
321
  if type == :delete
321
- t << Forme.form(form_attributes(:action=>url_for("destroy/#{model.primary_key_value(obj)}"), :method=>:post), form_opts) do |f1|
322
+ form_attr = form_attributes(:action=>url_for("destroy/#{model.primary_key_value(obj)}"), :method=>:post)
323
+ t << Forme.form(form_attr, form_opts(form_attr[:action])) do |f1|
322
324
  f1.button(:value=>'Delete', :class=>'btn btn-danger')
323
325
  end.to_s
324
326
  else
@@ -341,7 +343,8 @@ module AutoForme
341
343
  def edit_page(obj)
342
344
  page do
343
345
  t = String.new
344
- t << Forme.form(obj, form_attributes(:action=>url_for("update/#{model.primary_key_value(obj)}")), form_opts) do |f|
346
+ form_attr = form_attributes(:action=>url_for("update/#{model.primary_key_value(obj)}"))
347
+ t << Forme.form(obj, form_attr, form_opts(form_attr[:action])) do |f|
345
348
  model.columns_for(:edit, request).each do |column|
346
349
  col_opts = column_options_for(:edit, request, obj, column)
347
350
  if html = model.edit_html_for(obj, column, :edit, request)
@@ -479,7 +482,8 @@ module AutoForme
479
482
  page do
480
483
  t = String.new
481
484
  t << "<h2>Edit #{humanize(assoc)} for #{h model.object_display_name(type, request, obj)}</h2>"
482
- t << Forme.form(obj, form_attributes(:action=>url_for("mtm_update/#{model.primary_key_value(obj)}?association=#{assoc}")), form_opts) do |f|
485
+ form_attr = form_attributes(:action=>url_for("mtm_update/#{model.primary_key_value(obj)}?association=#{assoc}"))
486
+ t << Forme.form(obj, form_attr, form_opts(form_attr[:action])) do |f|
483
487
  opts = model.column_options_for(:mtm_edit, request, assoc)
484
488
  add_opts = opts[:add] ? opts.merge(opts.delete(:add)) : opts
485
489
  remove_opts = opts[:remove] ? opts.merge(opts.delete(:remove)) : opts
@@ -487,9 +491,9 @@ module AutoForme
487
491
  if model.association_autocomplete?(assoc, request)
488
492
  f.input(assoc, {:type=>'text', :class=>'autoforme_autocomplete', :attr=>{'data-type'=>'association', 'data-column'=>assoc, 'data-exclude'=>model.primary_key_value(obj)}, :value=>''}.merge(add_opts))
489
493
  else
490
- f.input(assoc, {:dataset=>model.unassociated_mtm_objects(request, assoc, obj)}.merge(add_opts))
494
+ f.input(assoc, {:dataset=>model.unassociated_mtm_objects(request, assoc, obj), :size=>10}.merge(add_opts))
491
495
  end
492
- f.input(assoc, {:name=>'remove[]', :id=>'remove', :label=>'Disassociate From', :dataset=>model.associated_mtm_objects(request, assoc, obj), :value=>[]}.merge(remove_opts))
496
+ f.input(assoc, {:name=>'remove[]', :id=>'remove', :label=>'Disassociate From', :dataset=>model.associated_mtm_objects(request, assoc, obj), :value=>[], :size=>10}.merge(remove_opts))
493
497
  f.button(:value=>'Update', :class=>'btn btn-primary')
494
498
  end.to_s
495
499
  end
@@ -622,13 +626,13 @@ module AutoForme
622
626
  # page.
623
627
  def association_link(mc, assoc_obj)
624
628
  if mc
625
- t = mc.object_display_name(:association, request, assoc_obj)
629
+ t = h(mc.object_display_name(:association, request, assoc_obj))
626
630
  if mc.supported_action?(type, request)
627
631
  t = "<a href=\"#{base_url_for("#{mc.link}/#{type}/#{mc.primary_key_value(assoc_obj)}")}\">#{t}</a>"
628
632
  end
629
633
  t
630
634
  else
631
- model.default_object_display_name(assoc_obj)
635
+ h(model.default_object_display_name(assoc_obj))
632
636
  end
633
637
  end
634
638
 
@@ -641,7 +645,7 @@ module AutoForme
641
645
  t << "<div class='inline_mtm_add_associations'>"
642
646
  assocs.each do |assoc|
643
647
  form_attr = form_attributes(:action=>url_for("mtm_update/#{model.primary_key_value(obj)}?association=#{assoc}&redir=edit"), :class => 'mtm_add_associations', 'data-remove' => "##{assoc}_remove_list")
644
- t << Forme.form(obj, form_attr, form_opts) do |f|
648
+ t << Forme.form(obj, form_attr, form_opts(form_attr[:action])) do |f|
645
649
  opts = model.column_options_for(:mtm_edit, request, assoc)
646
650
  add_opts = opts[:add] ? opts.merge(opts.delete(:add)) : opts.dup
647
651
  add_opts = {:name=>'add[]', :id=>"add_#{assoc}"}.merge(add_opts)
@@ -674,7 +678,7 @@ module AutoForme
674
678
  t << "<li>"
675
679
  t << association_link(mc, assoc_obj)
676
680
  form_attr = form_attributes(:action=>url_for("mtm_update/#{model.primary_key_value(obj)}?association=#{assoc}&remove%5b%5d=#{model.primary_key_value(assoc_obj)}&redir=edit"), :method=>'post', :class => 'mtm_remove_associations', 'data-add'=>"#add_#{assoc}")
677
- t << Forme.form(form_attr, form_opts) do |f|
681
+ t << Forme.form(form_attr, form_opts(form_attr[:action])) do |f|
678
682
  f.button(:value=>'Remove', :class=>'btn btn-xs btn-danger')
679
683
  end.to_s
680
684
  t << "</li>"
@@ -29,7 +29,7 @@ module AutoForme
29
29
  end
30
30
 
31
31
  # Use Rails's form_authenticity_token for CSRF protection.
32
- def csrf_token_hash
32
+ def csrf_token_hash(action=nil)
33
33
  vc = @controller.view_context
34
34
  {vc.request_forgery_protection_token.to_s=>vc.form_authenticity_token} if vc.protect_against_forgery?
35
35
  end
@@ -37,9 +37,39 @@ module AutoForme
37
37
  @env['HTTP_X_REQUESTED_WITH'] =~ /XMLHttpRequest/i
38
38
  end
39
39
 
40
+ # Set the flash at notice level when redirecting, so it shows
41
+ # up on the redirected page.
42
+ def set_flash_notice(message)
43
+ @controller.flash[flash_symbol_keys? ? :notice : 'notice'] = message
44
+ end
45
+
46
+ # Set the current flash at error level, used when displaying
47
+ # pages when there is an error.
48
+ def set_flash_now_error(message)
49
+ @controller.flash.now[flash_symbol_keys? ? :error : 'error'] = message
50
+ end
51
+
40
52
  # Use Rack::Csrf for csrf protection if it is defined.
41
- def csrf_token_hash
42
- {::Rack::Csrf.field=>::Rack::Csrf.token(@env)} if defined?(::Rack::Csrf)
53
+ def csrf_token_hash(action=nil)
54
+ if @controller.respond_to?(:check_csrf!)
55
+ # Using route_csrf plugin
56
+ # :nocov:
57
+ token = if @controller.use_request_specific_csrf_tokens?
58
+ @controller.csrf_token(@controller.csrf_path(action))
59
+ else
60
+ @controller.csrf_token
61
+ end
62
+ {@controller.csrf_field=>token}
63
+ # :nocov:
64
+ elsif defined?(::Rack::Csrf)
65
+ {::Rack::Csrf.field=>::Rack::Csrf.token(@env)}
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ def flash_symbol_keys?
72
+ !@controller.opts[:sessions_convert_symbols]
43
73
  end
44
74
  end
45
75
 
@@ -29,7 +29,7 @@ module AutoForme
29
29
  end
30
30
 
31
31
  # Use Rack::Csrf for csrf protection if it is defined.
32
- def csrf_token_hash
32
+ def csrf_token_hash(action=nil)
33
33
  {::Rack::Csrf.field=>::Rack::Csrf.token(@env)} if defined?(::Rack::Csrf)
34
34
  end
35
35
  end
@@ -389,7 +389,7 @@ module AutoForme
389
389
 
390
390
  def normalize_mtm_associations(assocs)
391
391
  if assocs == :all
392
- mtm_association_names
392
+ editable_mtm_association_names
393
393
  else
394
394
  Array(assocs)
395
395
  end
@@ -98,14 +98,26 @@ module AutoForme
98
98
  ref[:keys].zip(ref[:primary_keys].map{|k| obj.send(k)})
99
99
  end
100
100
 
101
+ # Array of many to many association name strings for editable
102
+ # many to many associations.
103
+ def editable_mtm_association_names
104
+ association_names([:many_to_many]) do |r|
105
+ model.method_defined?(r.add_method) && model.method_defined?(r.remove_method)
106
+ end
107
+ end
108
+
101
109
  # Array of many to many association name strings.
102
110
  def mtm_association_names
103
111
  association_names([:many_to_many])
104
112
  end
105
113
 
106
- # Array of association name strings for given association types
114
+ # Array of association name strings for given association types. If a block is
115
+ # given, only include associations where the block returns truthy.
107
116
  def association_names(types=SUPPORTED_ASSOCIATION_TYPES)
108
- model.all_association_reflections.select{|r| types.include?(r[:type])}.map{|r| r[:name]}.sort_by(&:to_s)
117
+ model.all_association_reflections.
118
+ select{|r| types.include?(r[:type]) && (!block_given? || yield(r))}.
119
+ map{|r| r[:name]}.
120
+ sort_by(&:to_s)
109
121
  end
110
122
 
111
123
  # Save the object, returning the object if successful, or nil if not.
@@ -158,7 +170,7 @@ module AutoForme
158
170
  params = request.params
159
171
  ds = apply_associated_eager(:search, request, all_dataset_for(type, request))
160
172
  columns_for(:search_form, request).each do |c|
161
- if (v = params[c.to_s]) && !v.empty?
173
+ if (v = params[c.to_s]) && !(v = v.to_s).empty?
162
174
  if association?(c)
163
175
  ref = model.association_reflection(c)
164
176
  ads = ref.associated_dataset
@@ -168,9 +180,16 @@ module AutoForme
168
180
  primary_key = S.qualify(ref.associated_class.table_name, ref.primary_key)
169
181
  ds = ds.where(S.qualify(model.table_name, ref[:key])=>ads.where(primary_key=>v).select(primary_key))
170
182
  elsif column_type(c) == :string
171
- ds = ds.where(S.ilike(S.qualify(model.table_name, c), "%#{ds.escape_like(v.to_s)}%"))
183
+ ds = ds.where(S.ilike(S.qualify(model.table_name, c), "%#{ds.escape_like(v)}%"))
172
184
  else
173
- ds = ds.where(S.qualify(model.table_name, c)=>model.db.typecast_value(column_type(c), v))
185
+ begin
186
+ typecasted_value = model.db.typecast_value(column_type(c), v)
187
+ rescue S::InvalidValue
188
+ ds = ds.where(false)
189
+ break
190
+ else
191
+ ds = ds.where(S.qualify(model.table_name, c)=>typecasted_value)
192
+ end
174
193
  end
175
194
  end
176
195
  end
@@ -293,7 +312,11 @@ module AutoForme
293
312
  ids.each do |id|
294
313
  next if id.to_s.empty?
295
314
  ret = assoc_class ? assoc_class.with_pk(:association, request, id) : obj.send(:_apply_association_options, ref, ref.associated_class.dataset.clone).with_pk!(id)
296
- obj.send(meth, ret)
315
+ begin
316
+ model.db.transaction(:savepoint=>true){obj.send(meth, ret)}
317
+ rescue S::UniqueConstraintViolation
318
+ # Already added, safe to ignore
319
+ end
297
320
  end
298
321
  end
299
322
  end
@@ -1,8 +1,22 @@
1
1
  # frozen-string-literal: true
2
2
 
3
3
  module AutoForme
4
+ # The major version of AutoForme, updated only for major changes that are
5
+ # likely to require modification to apps using AutoForme.
6
+ MAJOR = 1
7
+
8
+ # The minor version of AutoForme, updated for new feature releases of AutoForme.
9
+ MINOR = 10
10
+
11
+ # The patch version of AutoForme, updated only for bug fixes from the last
12
+ # feature release.
13
+ TINY = 0
14
+
4
15
  # Version constant, use <tt>AutoForme.version</tt> instead.
5
- VERSION = '1.7.0'.freeze
16
+ VERSION = "#{MAJOR}.#{MINOR}.#{TINY}".freeze
17
+
18
+ # The full version of AutoForme as a number (1.8.0 => 10800)
19
+ VERSION_NUMBER = MAJOR*10000 + MINOR*100 + TINY
6
20
 
7
21
  # Returns the version as a frozen string (e.g. '0.1.0')
8
22
  def self.version
@@ -51,6 +51,50 @@ describe AutoForme do
51
51
  page.all('td').map{|s| s.text}.must_equal ["Album1b", "Artist2", "Show", "Edit", "Delete"]
52
52
  end
53
53
 
54
+ it "should escape display names in association links" do
55
+ app_setup do
56
+ model Artist
57
+ model Album do
58
+ columns [:name, :artist]
59
+ end
60
+ association_links :all
61
+ end
62
+
63
+ visit("/Artist/new")
64
+ fill_in 'Name', :with=>'Art&"ist2'
65
+ click_button 'Create'
66
+
67
+ visit("/Album/new")
68
+ fill_in 'Name', :with=>'Album1'
69
+ select 'Art&"ist2'
70
+ click_button 'Create'
71
+
72
+ click_link 'Edit'
73
+ select 'Album1'
74
+ click_button 'Edit'
75
+ page.html.must_match(%r{- <a href="/Artist/edit/\d+">Art&amp;&quot;ist2})
76
+ end
77
+
78
+ it "should escape display names in association links" do
79
+ app_setup do
80
+ model Album do
81
+ columns [:name, :artist]
82
+ end
83
+ association_links :all
84
+ end
85
+
86
+ Artist.create(:name=>'Art&"ist2')
87
+ visit("/Album/new")
88
+ fill_in 'Name', :with=>'Album1'
89
+ select 'Art&"ist2'
90
+ click_button 'Create'
91
+
92
+ click_link 'Edit'
93
+ select 'Album1'
94
+ click_button 'Edit'
95
+ page.html.must_include("- Art&amp;&quot;ist2")
96
+ end
97
+
54
98
  it "should use text boxes for associated objects on new/edit/search forms if associated model uses autocompleting" do
55
99
  app_setup do
56
100
  model Artist do
data/spec/basic_spec.rb CHANGED
@@ -573,7 +573,7 @@ describe AutoForme do
573
573
  page.all('td').map{|s| s.text}.must_equal []
574
574
  end
575
575
 
576
- it "should correct handle validation errors" do
576
+ it "should correctly handle validation errors" do
577
577
  app_setup(Artist)
578
578
  Artist.send(:define_method, :validate) do
579
579
  errors.add(:name, "bad name") if name == 'Foo'
@@ -583,6 +583,7 @@ describe AutoForme do
583
583
  page.title.must_equal 'Artist - New'
584
584
  fill_in 'Name', :with=>'Foo'
585
585
  click_button 'Create'
586
+ page.title.must_equal 'Artist - New'
586
587
  page.html.must_include 'Error Creating Artist'
587
588
  page.html.must_include 'bad name'
588
589
  fill_in 'Name', :with=>'TestArtistNew'
@@ -597,6 +598,7 @@ describe AutoForme do
597
598
  click_button 'Edit'
598
599
  fill_in 'Name', :with=>'Foo'
599
600
  click_button 'Update'
601
+ page.title.must_equal 'Artist - Edit'
600
602
  page.html.must_include 'Error Updating Artist'
601
603
  page.html.must_include 'bad name'
602
604
  fill_in 'Name', :with=>'TestArtistUpdate'
@@ -952,19 +954,29 @@ describe AutoForme do
952
954
  after(:all) do
953
955
  Object.send(:remove_const, :Artist)
954
956
  end
955
-
956
- it "should display decimals in float format in tables" do
957
+ before do
957
958
  app_setup(Artist)
958
959
  visit("/Artist/new")
959
960
  page.title.must_equal 'Artist - New'
960
961
  fill_in 'Num', :with=>'1.01'
961
962
  click_button 'Create'
963
+ visit("/Artist/browse")
964
+ end
965
+
966
+ it "should display decimals in float format in tables" do
962
967
  click_link 'Artist'
963
968
  page.all('tr td:first-child').map{|s| s.text}.must_equal %w'1.01'
964
969
  click_link 'Search'
965
970
  click_button 'Search'
966
971
  page.all('tr td:first-child').map{|s| s.text}.must_equal %w'1.01'
967
972
  end
973
+
974
+ it "should treat invalid search fields as returning no results" do
975
+ click_link 'Search'
976
+ fill_in 'Num', :with=>'3/3/2020'
977
+ click_button 'Search'
978
+ page.all('tr td:first-child').map{|s| s.text}.must_equal []
979
+ end
968
980
  end
969
981
 
970
982
  describe AutoForme do
data/spec/mtm_spec.rb CHANGED
@@ -507,3 +507,115 @@ describe AutoForme do
507
507
  page.all('select')[1].all('option').map{|s| s.text}.must_equal ["Album2", "Album3"]
508
508
  end
509
509
  end
510
+
511
+ describe AutoForme do
512
+ before(:all) do
513
+ db_setup(:artists=>[[:name, :string]], :albums=>[[:name, :string]], :albums_artists=>proc{column :album_id, :integer, :table=>:albums; column :artist_id, :integer, :table=>:artists; primary_key [:album_id, :artist_id]})
514
+ model_setup(:Artist=>[:artists, [[:many_to_many, :albums]]], :Album=>[:albums, [[:many_to_many, :artists]]])
515
+ end
516
+ after(:all) do
517
+ Object.send(:remove_const, :Album)
518
+ Object.send(:remove_const, :Artist)
519
+ end
520
+
521
+ it "should handle unique constraint violation errors when adding associated objects" do
522
+ app_setup do
523
+ model Artist do
524
+ mtm_associations :albums
525
+ end
526
+ model Album
527
+ end
528
+
529
+ artist = Artist.create(:name=>'Artist1')
530
+ album = Album.create(:name=>'Album1')
531
+
532
+ visit("/Artist/mtm_edit")
533
+ page.title.must_equal 'Artist - Many To Many Edit'
534
+ select("Artist1")
535
+ click_button "Edit"
536
+
537
+ find('h2').text.must_equal 'Edit Albums for Artist1'
538
+ page.all('select')[0].all('option').map{|s| s.text}.must_equal ["Album1"]
539
+ page.all('select')[1].all('option').map{|s| s.text}.must_equal []
540
+ select("Album1", :from=>"Associate With")
541
+ artist.add_album(album)
542
+ click_button "Update"
543
+ page.html.must_include 'Updated albums association for Artist'
544
+ Artist.first.albums.map{|x| x.name}.must_equal %w'Album1'
545
+ end
546
+
547
+ it "should handle unique constraint violation errors when adding associated objects" do
548
+ app_setup do
549
+ model Artist do
550
+ mtm_associations :albums
551
+ end
552
+ model Album
553
+ end
554
+
555
+ artist = Artist.create(:name=>'Artist1')
556
+ album = Album.create(:name=>'Album1')
557
+ artist.add_album(album)
558
+
559
+ visit("/Artist/mtm_edit")
560
+ page.title.must_equal 'Artist - Many To Many Edit'
561
+ select("Artist1")
562
+ click_button "Edit"
563
+
564
+ find('h2').text.must_equal 'Edit Albums for Artist1'
565
+ page.all('select')[0].all('option').map{|s| s.text}.must_equal []
566
+ page.all('select')[1].all('option').map{|s| s.text}.must_equal ["Album1"]
567
+ select("Album1", :from=>"Disassociate From")
568
+ artist.remove_album(album)
569
+ click_button "Update"
570
+ page.html.must_include 'Updated albums association for Artist'
571
+ Artist.first.albums.map{|x| x.name}.must_equal []
572
+ end
573
+ end
574
+
575
+ describe AutoForme do
576
+ before(:all) do
577
+ db_setup(:artists=>[[:name, :string]], :albums=>[[:name, :string]], :albums_artists=>[[:album_id, :integer, {:table=>:albums}], [:artist_id, :integer, {:table=>:artists}]])
578
+ model_setup(:Artist=>[:artists, [[:many_to_many, :albums, :read_only=>true]]], :Album=>[:albums, [[:many_to_many, :artists, :read_only=>true]]])
579
+ end
580
+ after(:all) do
581
+ Object.send(:remove_const, :Album)
582
+ Object.send(:remove_const, :Artist)
583
+ end
584
+
585
+ it "should not automatically setup mtm support for read-only associations" do
586
+ app_setup do
587
+ model Artist do
588
+ mtm_associations :all
589
+ association_links :all
590
+ end
591
+ model Album do
592
+ mtm_associations :all
593
+ association_links :all
594
+ end
595
+ end
596
+
597
+ visit("/Artist/new")
598
+ page.html.wont_include 'MTM'
599
+ fill_in 'Name', :with=>'Artist1'
600
+ click_button 'Create'
601
+ click_link 'Edit'
602
+ select 'Artist1'
603
+ click_button 'Edit'
604
+ page.html.must_include 'Albums'
605
+ page.html.wont_include '>associate<'
606
+ visit("/Artist/mtm_edit")
607
+ page.html.must_include 'Unhandled Request'
608
+
609
+ visit("/Album/new")
610
+ page.html.wont_include 'MTM'
611
+ fill_in 'Name', :with=>'Album1'
612
+ click_button 'Create'
613
+ click_link 'Edit'
614
+ select 'Album1'
615
+ click_button 'Edit'
616
+ page.html.must_include 'Artists'
617
+ page.html.wont_include '>associate<'
618
+ visit("/Album/mtm_edit")
619
+ page.html.must_include 'Unhandled Request'
620
+ end
621
+ end
@@ -1,10 +1,19 @@
1
1
  require 'rubygems'
2
+ require 'rails'
2
3
  require 'action_controller/railtie'
3
4
  require 'autoforme'
4
5
 
5
6
  class AutoFormeSpec::App
7
+ class << self
8
+ # Workaround for action_view railtie deleting the finalizer
9
+ attr_accessor :av_finalizer
10
+ end
11
+
6
12
  def self.autoforme(klass=nil, opts={}, &block)
7
13
  sc = Class.new(Rails::Application)
14
+ def sc.name
15
+ "AutoForme Test"
16
+ end
8
17
  framework = nil
9
18
  sc.class_eval do
10
19
  controller = Class.new(ActionController::Base)
@@ -13,7 +22,7 @@ class AutoFormeSpec::App
13
22
  resolver = Class.new(ActionView::Resolver)
14
23
  resolver.class_eval do
15
24
  template = ActionView::Template
16
- t = [template.new(<<HTML, "layout", template.handler_for_extension(:erb), {:virtual_path=>'layout', :format=>'erb', :updated_at=>Time.now})]
25
+ code = (<<HTML)
17
26
  <!DOCTYPE html>
18
27
  <html>
19
28
  <head><title><%= @autoforme_action.title if @autoforme_action %></title></head>
@@ -27,6 +36,11 @@ class AutoFormeSpec::App
27
36
  <%= yield %>
28
37
  </body></html>"
29
38
  HTML
39
+ if Rails.version > '6'
40
+ t = [template.new(code, "layout", template.handler_for_extension(:erb), :virtual_path=>'layout', :format=>'erb', :locals=>[])]
41
+ else
42
+ t = [template.new(code, "layout", template.handler_for_extension(:erb), :virtual_path=>'layout', :format=>'erb', :updated_at=>Time.now)]
43
+ end
30
44
 
31
45
  define_method(:find_templates){|*args| t}
32
46
  end
@@ -50,14 +64,27 @@ HTML
50
64
  end
51
65
  end
52
66
 
53
- config.secret_token = routes.append do
67
+ st = routes.append do
54
68
  get 'session/set', :controller=>'autoforme', :action=>'session_set'
55
69
  end.inspect
70
+ config.secret_token = st if Rails.respond_to?(:version) && Rails.version < '5.2'
71
+ config.hosts << "www.example.com" if config.respond_to?(:hosts)
56
72
  config.active_support.deprecation = :stderr
57
73
  config.middleware.delete(ActionDispatch::ShowExceptions)
58
74
  config.middleware.delete(Rack::Lock)
59
- config.secret_key_base = 'foo'
75
+ config.secret_key_base = st*15
60
76
  config.eager_load = true
77
+ if Rails.version.start_with?('4')
78
+ # Work around issue in backported openssl environments where
79
+ # secret is 64 bytes intead of 32 bytes
80
+ require 'active_support/message_encryptor'
81
+ ActiveSupport::MessageEncryptor.send :prepend, Module.new {
82
+ def initialize(secret, *signature_key_or_options)
83
+ secret = secret[0, 32]
84
+ super
85
+ end
86
+ }
87
+ end
61
88
  if Rails.version > '4.2'
62
89
  config.action_dispatch.cookies_serializer = :json
63
90
  end
@@ -66,6 +93,14 @@ HTML
66
93
  ActionDispatch::Routing::RouteSet::Dispatcher.class_eval do
67
94
  define_method(:controller){|_| controller}
68
95
  end
96
+ config.session_store :cookie_store, :key=>'_autoforme_test_session'
97
+ end
98
+ if Rails.version > '6'
99
+ if AutoFormeSpec::App.av_finalizer
100
+ config.action_view.finalize_compiled_template_methods = AutoFormeSpec::App.av_finalizer
101
+ else
102
+ AutoFormeSpec::App.av_finalizer = config.action_view.finalize_compiled_template_methods
103
+ end
69
104
  end
70
105
  initialize!
71
106
  end
@@ -4,8 +4,7 @@ require 'autoforme'
4
4
  require 'rack/csrf'
5
5
 
6
6
  begin
7
- require 'erubis'
8
- require 'tilt/erubis'
7
+ require 'tilt/erubi'
9
8
  rescue LoadError
10
9
  require 'tilt/erb'
11
10
  end
@@ -14,31 +13,49 @@ class AutoFormeSpec::App < Roda
14
13
  opts[:unsupported_block_result] = :raise
15
14
  opts[:unsupported_matcher] = :raise
16
15
  opts[:verbatim_string_matcher] = true
16
+ opts[:check_dynamic_arity] = opts[:check_arity] = :warn
17
17
 
18
18
  LAYOUT = <<HTML
19
19
  <!DOCTYPE html>
20
20
  <html>
21
21
  <head><title><%= @autoforme_action.title if @autoforme_action %></title></head>
22
22
  <body>
23
- <% if flash[:notice] %>
24
- <div class="alert alert-success"><p><%= flash[:notice] %></p></div>
23
+ <% if notice = opts[:sessions_convert_symbols] ? flash['notice'] : flash[:notice] %>
24
+ <div class="alert alert-success"><p><%= notice %></p></div>
25
25
  <% end %>
26
- <% if flash[:error] %>
27
- <div class="alert alert-error"><p><%= flash[:error] %></p></div>
26
+ <% if error = opts[:sessions_convert_symbols] ? flash['error'] : flash[:error] %>
27
+ <div class="alert alert-error"><p><%= error %></p></div>
28
28
  <% end %>
29
29
  <%= yield %>
30
30
  </body></html>"
31
31
  HTML
32
32
 
33
- use Rack::Session::Cookie, :secret => '1'
34
- use Rack::Csrf
33
+ plugin :flash
34
+
35
+ if defined?(Roda::RodaVersionNumber) && Roda::RodaVersionNumber >= 30100
36
+ if ENV['RODA_ROUTE_CSRF'] == '0'
37
+ require 'roda/session_middleware'
38
+ opts[:sessions_convert_symbols] = true
39
+ use RodaSessionMiddleware, :secret=>SecureRandom.random_bytes(64)
40
+ else
41
+ ENV['RODA_ROUTE_CSRF'] ||= '1'
42
+ plugin :sessions, :secret=>SecureRandom.random_bytes(64)
43
+ end
44
+ else
45
+ use Rack::Session::Cookie, :secret => '1'
46
+ end
47
+
48
+ if ENV['RODA_ROUTE_CSRF'].to_i > 0
49
+ plugin :route_csrf, :require_request_specific_tokens=>ENV['RODA_ROUTE_CSRF'] == '1'
50
+ else
51
+ use Rack::Csrf
52
+ end
35
53
 
36
54
  template_opts = {:default_encoding=>nil}
37
55
  plugin :render, :layout=>{:inline=>LAYOUT}, :template_opts=>template_opts, :opts=>template_opts
38
56
  plugin :not_found do
39
57
  'Unhandled Request'
40
58
  end
41
- plugin :flash
42
59
 
43
60
  def self.autoforme(klass=nil, opts={}, &block)
44
61
  sc = Class.new(self)
@@ -54,6 +71,8 @@ HTML
54
71
  end
55
72
 
56
73
  route do |r|
74
+ check_csrf! if ENV['RODA_ROUTE_CSRF'].to_i > 0
75
+
57
76
  r.get 'session/set' do
58
77
  session.merge!(r.params)
59
78
  ''
data/spec/spec_helper.rb CHANGED
@@ -24,13 +24,15 @@ require "./spec/#{ENV['FRAMEWORK']}_spec_helper"
24
24
  require 'capybara'
25
25
  require 'capybara/dsl'
26
26
  require 'rack/test'
27
+
28
+ ENV['MT_NO_PLUGINS'] = '1' # Work around stupid autoloading of plugins
27
29
  gem 'minitest'
28
- require 'minitest/autorun'
30
+ require 'minitest/global_expectations/autorun'
29
31
  require 'minitest/hooks/default'
30
32
 
31
33
  if ENV['WARNING']
32
34
  require 'warning'
33
- Warning.ignore([:missing_ivar, :fixnum])
35
+ Warning.ignore([:missing_ivar, :fixnum, :not_reached])
34
36
  end
35
37
 
36
38
  require './spec/sequel_spec_helper'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: autoforme
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.7.0
4
+ version: 1.10.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: 2017-10-27 00:00:00.000000000 Z
11
+ date: 2021-08-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: forme
@@ -94,6 +94,20 @@ dependencies:
94
94
  - - ">="
95
95
  - !ruby/object:Gem::Version
96
96
  version: 1.1.0
97
+ - !ruby/object:Gem::Dependency
98
+ name: minitest-global_expectations
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
97
111
  - !ruby/object:Gem::Dependency
98
112
  name: capybara
99
113
  requirement: !ruby/object:Gem::Requirement
@@ -243,7 +257,12 @@ files:
243
257
  homepage: http://github.com/jeremyevans/autoforme
244
258
  licenses:
245
259
  - MIT
246
- metadata: {}
260
+ metadata:
261
+ bug_tracker_uri: https://github.com/jeremyevans/autoforme/issues
262
+ changelog_uri: http://autoforme.jeremyevans.net/files/CHANGELOG.html
263
+ documentation_uri: http://autoforme.jeremyevans.net
264
+ mailing_list_uri: https://github.com/jeremyevans/autoforme/discussions
265
+ source_code_uri: https://github.com/jeremyevans/autoforme
247
266
  post_install_message:
248
267
  rdoc_options:
249
268
  - "--quiet"
@@ -266,8 +285,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
266
285
  - !ruby/object:Gem::Version
267
286
  version: '0'
268
287
  requirements: []
269
- rubyforge_project:
270
- rubygems_version: 2.6.13
288
+ rubygems_version: 3.2.22
271
289
  signing_key:
272
290
  specification_version: 4
273
291
  summary: Web Administrative Console for Roda/Sinatra/Rails and Sequel::Model