autoforme 1.7.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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
|