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 +5 -5
- data/CHANGELOG +24 -0
- data/MIT-LICENSE +1 -1
- data/README.rdoc +65 -5
- data/lib/autoforme/action.rb +18 -14
- data/lib/autoforme/frameworks/rails.rb +1 -1
- data/lib/autoforme/frameworks/roda.rb +32 -2
- data/lib/autoforme/frameworks/sinatra.rb +1 -1
- data/lib/autoforme/model.rb +1 -1
- data/lib/autoforme/models/sequel.rb +29 -6
- data/lib/autoforme/version.rb +15 -1
- data/spec/associations_spec.rb +44 -0
- data/spec/basic_spec.rb +15 -3
- data/spec/mtm_spec.rb +112 -0
- data/spec/rails_spec_helper.rb +38 -3
- data/spec/roda_spec_helper.rb +28 -9
- data/spec/spec_helper.rb +4 -2
- metadata +23 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 6f9fb399d144c4bfc7eed6e2a4f7497cab7bc25b8409d555c4f67fb91bad9bb7
|
4
|
+
data.tar.gz: bffb9114ef162400c639abb1ee17b7eb42ced481294d152e2603711c01752274
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
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
|
-
|
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
|
-
|
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
|
data/lib/autoforme/action.rb
CHANGED
@@ -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[
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
|
data/lib/autoforme/model.rb
CHANGED
@@ -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.
|
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
|
183
|
+
ds = ds.where(S.ilike(S.qualify(model.table_name, c), "%#{ds.escape_like(v)}%"))
|
172
184
|
else
|
173
|
-
|
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
|
-
|
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
|
data/lib/autoforme/version.rb
CHANGED
@@ -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 =
|
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
|
data/spec/associations_spec.rb
CHANGED
@@ -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&"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&"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
|
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
|
data/spec/rails_spec_helper.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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 =
|
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
|
data/spec/roda_spec_helper.rb
CHANGED
@@ -4,8 +4,7 @@ require 'autoforme'
|
|
4
4
|
require 'rack/csrf'
|
5
5
|
|
6
6
|
begin
|
7
|
-
require '
|
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><%=
|
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><%=
|
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
|
-
|
34
|
-
|
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.
|
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:
|
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
|
-
|
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
|