inline_forms 8.1.47 → 8.1.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +3 -0
- data/CHANGELOG.md +149 -0
- data/Gemfile +3 -0
- data/README.rdoc +47 -0
- data/app/assets/stylesheets/inline_forms/inline_forms.scss +34 -0
- data/app/controllers/concerns/file_slots_concern.rb +211 -0
- data/app/controllers/inline_forms/file_trash_controller.rb +115 -0
- data/app/controllers/inline_forms_controller.rb +82 -16
- data/app/helpers/inline_forms_file_trash_helper.rb +131 -0
- data/app/helpers/inline_forms_helper.rb +5 -1
- data/app/models/concerns/inline_forms/stored_files.rb +350 -0
- data/app/models/inline_forms/current.rb +8 -0
- data/app/models/inline_forms/file_trash_sweep.rb +37 -0
- data/app/models/inline_forms/trashed_file.rb +205 -0
- data/app/views/inline_forms/_file_slot.html.erb +39 -0
- data/app/views/inline_forms/_header.html.erb +3 -0
- data/app/views/inline_forms/_model_top_bar.html.erb +3 -4
- data/app/views/inline_forms/_show.html.erb +6 -0
- data/app/views/inline_forms/_versions.html.erb +4 -4
- data/app/views/inline_forms/_versions_file_entry.html.erb +36 -0
- data/app/views/inline_forms/_versions_list.html.erb +11 -2
- data/app/views/inline_forms/file_field_frame.html.erb +22 -0
- data/app/views/inline_forms/file_trash/index.html.erb +112 -0
- data/app/views/inline_forms/file_trash_panel.html.erb +90 -0
- data/app/views/layouts/inline_forms.html.erb +5 -1
- data/lib/generators/inline_forms/file_trash_generator.rb +36 -0
- data/lib/generators/inline_forms/templates/create_inline_forms_file_trash.rb.erb +41 -0
- data/lib/generators/inline_forms_addto_generator.rb +47 -0
- data/lib/generators/inline_forms_generator.rb +19 -2
- data/lib/generators/templates/model.erb +4 -0
- data/lib/inline_forms/form_element_from_callee.rb +25 -0
- data/lib/inline_forms/form_elements/simple_file_field_helper.rb +30 -2
- data/lib/inline_forms/gem_files.rb +5 -0
- data/lib/inline_forms/version.rb +1 -1
- data/lib/inline_forms.rb +55 -0
- data/lib/locales/inline_forms.en.yml +87 -0
- data/lib/locales/inline_forms.nl.yml +87 -3
- data/lib/tasks/inline_forms_files.rake +61 -0
- data/test/dummy/app/controllers/application_controller.rb +5 -0
- data/test/dummy/app/controllers/documents_controller.rb +4 -0
- data/test/dummy/app/controllers/dossiers_controller.rb +4 -0
- data/test/dummy/app/models/ability.rb +15 -0
- data/test/dummy/app/models/document.rb +33 -0
- data/test/dummy/app/models/dossier.rb +38 -0
- data/test/dummy/config/application.rb +4 -3
- data/test/dummy/config/routes.rb +13 -0
- data/test/inline_forms_addto_generator_test.rb +47 -0
- data/test/inline_forms_generator_test.rb +24 -0
- data/test/integration/field_authorization_test.rb +131 -0
- data/test/integration/file_slots_test.rb +574 -0
- data/test/integration/layout_title_test.rb +56 -0
- data/test/integration/model_top_bar_test.rb +9 -0
- data/test/integration/revert_skipped_columns_test.rb +59 -0
- data/test/integration_test_helper.rb +62 -0
- metadata +34 -3
- data/.claude/settings.local.json +0 -9
- data/CLAUDE.md +0 -68
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../integration_test_helper"
|
|
4
|
+
|
|
5
|
+
# Declared file slots (InlineForms::StoredFiles) and the file trash:
|
|
6
|
+
# upload / replace / remove / restore / purge, permissions per action, the
|
|
7
|
+
# per-record panel, versions history, revert interplay, the retention sweep
|
|
8
|
+
# and the global trash page. Dummy model: Document (test/dummy).
|
|
9
|
+
class FileSlotsTest < InlineFormsIntegrationTestCase
|
|
10
|
+
PLAN = "%PDF-1.4 the plan \x00\x01".b
|
|
11
|
+
OTHER = "%PDF-1.4 another file \x02\x03".b
|
|
12
|
+
|
|
13
|
+
setup do
|
|
14
|
+
@doc = Document.create!(title: "Dossier A")
|
|
15
|
+
@frame = "document_#{@doc.id}_filename"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def upload(bytes, name = "plan.pdf", type = "application/pdf")
|
|
19
|
+
Rack::Test::UploadedFile.new(StringIO.new(bytes), type, original_filename: name)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def stream_headers(frame)
|
|
23
|
+
frame_headers(frame).merge("Accept" => "text/vnd.turbo-stream.html")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def put_file(doc, attribute, value)
|
|
27
|
+
frame = "document_#{doc.id}_#{attribute}"
|
|
28
|
+
patch document_path(doc, attribute: attribute, form_element: "simple_file_field", update: frame),
|
|
29
|
+
params: { attribute => value }, headers: frame_headers(frame)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def with_file(bytes = PLAN, name = "plan.pdf")
|
|
33
|
+
@doc.update!(filename: upload(bytes, name))
|
|
34
|
+
@doc.reload
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def trash
|
|
38
|
+
InlineForms::TrashedFile.trash_entries
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# ---- declaration ------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
test "slot columns are skipped by PaperTrail on the model only" do
|
|
44
|
+
skip_list = Document.paper_trail_options[:skip]
|
|
45
|
+
%w[filename data content_type plan_filename plan_data plan_content_type].each do |column|
|
|
46
|
+
assert_includes skip_list, column
|
|
47
|
+
end
|
|
48
|
+
refute_includes Widget.paper_trail_options[:skip], "data",
|
|
49
|
+
"the inherited ApplicationRecord options must not be mutated"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
test "default column names follow the attribute" do
|
|
53
|
+
assert_equal({ data: "data", content_type: "content_type" }, Document.inline_forms_file_slot(:filename))
|
|
54
|
+
assert_equal({ data: "plan_data", content_type: "plan_content_type" }, Document.inline_forms_file_slot(:plan_filename))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
test "a hand-written setter for a declared slot raises outside production" do
|
|
58
|
+
assert_raises(InlineForms::StoredFiles::HandWrittenSetterError) do
|
|
59
|
+
Class.new(ApplicationRecord) do
|
|
60
|
+
self.table_name = "documents"
|
|
61
|
+
include InlineForms::StoredFiles
|
|
62
|
+
inline_forms_file :filename
|
|
63
|
+
def filename=(value); end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
test "has_paper_trail after the macro still skips the slot columns" do
|
|
69
|
+
klass = Class.new(ActiveRecord::Base) do
|
|
70
|
+
self.table_name = "documents"
|
|
71
|
+
include InlineForms::StoredFiles
|
|
72
|
+
inline_forms_file :filename
|
|
73
|
+
has_paper_trail
|
|
74
|
+
end
|
|
75
|
+
assert_includes klass.paper_trail_options[:skip], "data"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# ---- upload / replace ---------------------------------------------------
|
|
79
|
+
|
|
80
|
+
test "uploading into an empty slot stores bytes and logs an upload event" do
|
|
81
|
+
put_file @doc, "filename", upload(PLAN, "my plan (v2).pdf")
|
|
82
|
+
|
|
83
|
+
assert_response :success
|
|
84
|
+
@doc.reload
|
|
85
|
+
assert_equal PLAN, @doc.data
|
|
86
|
+
assert_equal "my_plan__v2_.pdf", @doc.filename
|
|
87
|
+
assert_equal "application/pdf", @doc.content_type
|
|
88
|
+
event = InlineForms::TrashedFile.events.sole
|
|
89
|
+
assert_equal "filename", event.attribute_name
|
|
90
|
+
assert_nil event.data
|
|
91
|
+
assert_equal 1, event.trashed_by_id
|
|
92
|
+
assert_empty trash
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
test "replacing moves the old bytes to the trash" do
|
|
96
|
+
with_file
|
|
97
|
+
put_file @doc, "filename", upload(OTHER, "other.pdf")
|
|
98
|
+
|
|
99
|
+
assert_response :success
|
|
100
|
+
assert_equal OTHER, @doc.reload.data
|
|
101
|
+
entry = trash.sole
|
|
102
|
+
assert_equal "replaced", entry.reason
|
|
103
|
+
assert_equal PLAN, entry.file_data
|
|
104
|
+
assert_equal "plan.pdf", entry.filename
|
|
105
|
+
assert_equal Digest::SHA256.hexdigest(PLAN), entry.checksum
|
|
106
|
+
assert_in_delta 30.days.from_now, entry.purge_after, 5
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
test "re-uploading identical bytes trashes nothing" do
|
|
110
|
+
with_file
|
|
111
|
+
put_file @doc, "filename", upload(PLAN, "plan.pdf")
|
|
112
|
+
|
|
113
|
+
assert_response :success
|
|
114
|
+
assert_empty trash
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
test "replace needs :replace_file; an empty slot only needs :update" do
|
|
118
|
+
Ability.restrictions = -> { cannot :replace_file, Document }
|
|
119
|
+
put_file @doc, "plan_filename", upload(PLAN)
|
|
120
|
+
assert_response :success
|
|
121
|
+
|
|
122
|
+
with_file
|
|
123
|
+
put_file @doc, "filename", upload(OTHER, "other.pdf")
|
|
124
|
+
assert_response :forbidden
|
|
125
|
+
assert_equal PLAN, @doc.reload.data
|
|
126
|
+
assert_empty trash
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
test "edit of an occupied slot needs :replace_file too" do
|
|
130
|
+
with_file
|
|
131
|
+
Ability.restrictions = -> { cannot :replace_file, Document }
|
|
132
|
+
get edit_document_path(@doc, attribute: "filename", form_element: "simple_file_field", update: @frame),
|
|
133
|
+
headers: frame_headers(@frame)
|
|
134
|
+
assert_response :forbidden
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
test "a crafted string for a slot is refused as 'no file chosen'" do
|
|
138
|
+
with_file
|
|
139
|
+
put_file @doc, "filename", "evil.exe"
|
|
140
|
+
|
|
141
|
+
assert_response :success
|
|
142
|
+
assert_includes response.body, "no file chosen"
|
|
143
|
+
@doc.reload
|
|
144
|
+
assert_equal "plan.pdf", @doc.filename
|
|
145
|
+
assert_equal PLAN, @doc.data
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
test "an upload above max_file_size is a validation error" do
|
|
149
|
+
InlineForms.max_file_size = 10
|
|
150
|
+
put_file @doc, "filename", upload(PLAN)
|
|
151
|
+
|
|
152
|
+
assert_includes response.body, "is too large"
|
|
153
|
+
assert_nil @doc.reload.data
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
test "a failed save does not leave a trash row behind" do
|
|
157
|
+
with_file
|
|
158
|
+
Document.where(id: @doc.id).update_all(title: nil) # an unrelated invalid column
|
|
159
|
+
put_file @doc, "filename", upload(OTHER, "other.pdf")
|
|
160
|
+
|
|
161
|
+
assert_includes response.body, "can"
|
|
162
|
+
assert_equal PLAN, @doc.reload.data
|
|
163
|
+
assert_empty trash
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# ---- show ---------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
test "a present slot renders download, replace and remove; an empty one the plus link" do
|
|
169
|
+
with_file
|
|
170
|
+
get document_path(@doc, attribute: "filename", form_element: "simple_file_field", update: @frame),
|
|
171
|
+
headers: frame_headers(@frame)
|
|
172
|
+
|
|
173
|
+
assert_response :success
|
|
174
|
+
assert_includes response.body, download_file_document_path(@doc, attribute: "filename")
|
|
175
|
+
assert_includes response.body, remove_file_document_path(@doc, attribute: "filename")
|
|
176
|
+
assert_includes response.body, "fi-pencil"
|
|
177
|
+
assert_includes response.body, "data-turbo-confirm"
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
test "remove and replace icons follow the permissions" do
|
|
181
|
+
with_file
|
|
182
|
+
Ability.restrictions = -> { cannot [ :remove_file, :replace_file ], Document }
|
|
183
|
+
get document_path(@doc, attribute: "filename", form_element: "simple_file_field", update: @frame),
|
|
184
|
+
headers: frame_headers(@frame)
|
|
185
|
+
|
|
186
|
+
refute_includes response.body, "remove_file"
|
|
187
|
+
refute_includes response.body, "fi-pencil"
|
|
188
|
+
assert_includes response.body, "download_file"
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
test "the record shows a trash panel frame and file events in versions" do
|
|
192
|
+
with_file
|
|
193
|
+
@doc.inline_forms_remove_file!(:filename, by: 1)
|
|
194
|
+
row = "document_#{@doc.id}"
|
|
195
|
+
get document_path(@doc, update: row), headers: frame_headers(row)
|
|
196
|
+
|
|
197
|
+
assert_response :success
|
|
198
|
+
assert_includes response.body, %(<turbo-frame id="document_#{@doc.id}_file_trash">)
|
|
199
|
+
assert_includes response.body, "Removed files (1)"
|
|
200
|
+
assert_includes response.body, "Versions (3)", "create version + upload event + removal event"
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# ---- remove / restore / purge -----------------------------------------
|
|
204
|
+
|
|
205
|
+
test "remove moves the file to the trash and offers undo" do
|
|
206
|
+
with_file
|
|
207
|
+
post remove_file_document_path(@doc, attribute: "filename", update: @frame), headers: stream_headers(@frame)
|
|
208
|
+
|
|
209
|
+
assert_response :success
|
|
210
|
+
@doc.reload
|
|
211
|
+
assert_nil @doc.filename
|
|
212
|
+
assert_nil @doc.data
|
|
213
|
+
entry = trash.sole
|
|
214
|
+
assert_equal "removed", entry.reason
|
|
215
|
+
assert_equal PLAN, entry.file_data
|
|
216
|
+
assert_includes response.body, "plan.pdf removed"
|
|
217
|
+
assert_includes response.body, "plan.pdf removed"
|
|
218
|
+
assert_equal 1, response.body.scan("trash_id=#{entry.id}").size, "one undo link: the notice's, not also the plus link's"
|
|
219
|
+
assert_includes response.body, %(target="document_#{@doc.id}_file_trash")
|
|
220
|
+
assert_includes response.body, %(target="document_#{@doc.id}_versions")
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
test "removing an empty slot twice creates no empty trash row" do
|
|
224
|
+
post remove_file_document_path(@doc, attribute: "filename", update: @frame), headers: stream_headers(@frame)
|
|
225
|
+
assert_response :success
|
|
226
|
+
assert_empty trash
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
test "remove works on a record that fails unrelated validations" do
|
|
230
|
+
with_file
|
|
231
|
+
Document.where(id: @doc.id).update_all(title: nil)
|
|
232
|
+
post remove_file_document_path(@doc, attribute: "filename", update: @frame), headers: stream_headers(@frame)
|
|
233
|
+
|
|
234
|
+
assert_response :success
|
|
235
|
+
assert_nil @doc.reload.data
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
test "remove needs :remove_file" do
|
|
239
|
+
with_file
|
|
240
|
+
Ability.restrictions = -> { cannot :remove_file, Document }
|
|
241
|
+
post remove_file_document_path(@doc, attribute: "filename", update: @frame), headers: stream_headers(@frame)
|
|
242
|
+
|
|
243
|
+
assert_response :forbidden
|
|
244
|
+
assert_equal PLAN, @doc.reload.data
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
test "an undeclared attribute is refused" do
|
|
248
|
+
post remove_file_document_path(@doc, attribute: "title", update: @frame), headers: stream_headers(@frame)
|
|
249
|
+
assert_response :bad_request
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
test "restore puts the bytes back and empties the trash row" do
|
|
253
|
+
with_file
|
|
254
|
+
entry = @doc.inline_forms_remove_file!(:filename, by: 1)
|
|
255
|
+
post restore_file_document_path(@doc, attribute: "filename", trash_id: entry.id, update: @frame),
|
|
256
|
+
headers: stream_headers(@frame)
|
|
257
|
+
|
|
258
|
+
assert_response :success
|
|
259
|
+
@doc.reload
|
|
260
|
+
assert_equal PLAN, @doc.data
|
|
261
|
+
assert_equal "plan.pdf", @doc.filename
|
|
262
|
+
entry.reload
|
|
263
|
+
assert entry.restored?
|
|
264
|
+
assert_nil entry.file_data
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
test "restore into an occupied slot swaps: the current file goes to the trash" do
|
|
268
|
+
with_file
|
|
269
|
+
entry = @doc.inline_forms_remove_file!(:filename, by: 1)
|
|
270
|
+
with_file(OTHER, "other.pdf")
|
|
271
|
+
post restore_file_document_path(@doc, attribute: "filename", trash_id: entry.id, update: @frame),
|
|
272
|
+
headers: stream_headers(@frame)
|
|
273
|
+
|
|
274
|
+
assert_equal PLAN, @doc.reload.data
|
|
275
|
+
swapped = trash.where(reason: "replaced").sole
|
|
276
|
+
assert_equal OTHER, swapped.file_data
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
test "restore twice is a no-op the second time" do
|
|
280
|
+
with_file
|
|
281
|
+
entry = @doc.inline_forms_remove_file!(:filename, by: 1)
|
|
282
|
+
2.times do
|
|
283
|
+
post restore_file_document_path(@doc, attribute: "filename", trash_id: entry.id, update: @frame),
|
|
284
|
+
headers: stream_headers(@frame)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
assert_includes response.body, "can no longer be restored"
|
|
288
|
+
assert_equal 1, trash.count
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
test "restore needs :restore_file, and :update for an empty slot" do
|
|
292
|
+
with_file
|
|
293
|
+
entry = @doc.inline_forms_remove_file!(:filename, by: 1)
|
|
294
|
+
Ability.restrictions = -> { cannot :update, Document, [ :filename ] }
|
|
295
|
+
post restore_file_document_path(@doc, attribute: "filename", trash_id: entry.id, update: @frame),
|
|
296
|
+
headers: stream_headers(@frame)
|
|
297
|
+
|
|
298
|
+
assert_response :forbidden
|
|
299
|
+
assert_nil @doc.reload.data
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
test "a trash entry of another record is not found" do
|
|
303
|
+
other = Document.create!(title: "Dossier B", filename: upload(PLAN))
|
|
304
|
+
entry = other.inline_forms_remove_file!(:filename, by: 1)
|
|
305
|
+
post restore_file_document_path(@doc, attribute: "filename", trash_id: entry.id, update: @frame),
|
|
306
|
+
headers: stream_headers(@frame)
|
|
307
|
+
|
|
308
|
+
assert_response :not_found
|
|
309
|
+
assert entry.reload.trashed?
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
test "purge deletes the bytes and keeps the tombstone" do
|
|
313
|
+
with_file
|
|
314
|
+
entry = @doc.inline_forms_remove_file!(:filename, by: 1)
|
|
315
|
+
post purge_file_document_path(@doc, attribute: "filename", trash_id: entry.id, update: @frame),
|
|
316
|
+
headers: stream_headers(@frame)
|
|
317
|
+
|
|
318
|
+
assert_response :success
|
|
319
|
+
entry.reload
|
|
320
|
+
assert entry.purged?
|
|
321
|
+
assert_nil entry.file_data
|
|
322
|
+
assert_equal "plan.pdf", entry.filename, "no redaction by default"
|
|
323
|
+
assert_equal Digest::SHA256.hexdigest(PLAN), entry.checksum
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
test "purge with redaction blanks the filename" do
|
|
327
|
+
InlineForms.file_trash_redact_on_purge = true
|
|
328
|
+
with_file
|
|
329
|
+
entry = @doc.inline_forms_remove_file!(:filename, by: 1)
|
|
330
|
+
assert entry.purge!(by: 1)
|
|
331
|
+
|
|
332
|
+
assert_nil entry.reload.filename
|
|
333
|
+
assert entry.redacted?
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
test "purge needs :purge_file" do
|
|
337
|
+
with_file
|
|
338
|
+
entry = @doc.inline_forms_remove_file!(:filename, by: 1)
|
|
339
|
+
Ability.restrictions = -> { cannot :purge_file, Document }
|
|
340
|
+
post purge_file_document_path(@doc, attribute: "filename", trash_id: entry.id, update: @frame),
|
|
341
|
+
headers: stream_headers(@frame)
|
|
342
|
+
|
|
343
|
+
assert_response :forbidden
|
|
344
|
+
assert entry.reload.trashed?
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
test "a purged entry can be neither restored nor purged again" do
|
|
348
|
+
with_file
|
|
349
|
+
entry = @doc.inline_forms_remove_file!(:filename, by: 1)
|
|
350
|
+
assert entry.purge!(by: 1)
|
|
351
|
+
|
|
352
|
+
refute entry.purge!(by: 1)
|
|
353
|
+
assert_raises(InlineForms::TrashedFile::NotRestorable) { entry.restore!(by: 1) }
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# ---- downloads --------------------------------------------------------
|
|
357
|
+
|
|
358
|
+
test "download_file sends the bytes as an attachment, never cached" do
|
|
359
|
+
with_file
|
|
360
|
+
get download_file_document_path(@doc, attribute: "filename")
|
|
361
|
+
|
|
362
|
+
assert_response :success
|
|
363
|
+
assert_equal PLAN, response.body.b
|
|
364
|
+
assert_match(/attachment/, response.headers["Content-Disposition"])
|
|
365
|
+
assert_equal "no-store", response.headers["Cache-Control"]
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
test "download_file of an empty slot is a 404, not an empty file" do
|
|
369
|
+
get download_file_document_path(@doc, attribute: "filename")
|
|
370
|
+
assert_response :not_found
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
test "download_file needs :read on the attribute" do
|
|
374
|
+
with_file
|
|
375
|
+
Ability.restrictions = -> { cannot :read, Document, [ :filename ] }
|
|
376
|
+
get download_file_document_path(@doc, attribute: "filename")
|
|
377
|
+
assert_response :forbidden
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
test "a trashed file downloads as octet-stream attachment until purged" do
|
|
381
|
+
with_file(PLAN, "page.html")
|
|
382
|
+
entry = @doc.inline_forms_remove_file!(:filename, by: 1)
|
|
383
|
+
get trashed_file_document_path(@doc, attribute: "filename", trash_id: entry.id)
|
|
384
|
+
|
|
385
|
+
assert_response :success
|
|
386
|
+
assert_equal PLAN, response.body.b
|
|
387
|
+
assert_equal "application/octet-stream", response.media_type
|
|
388
|
+
assert_match(/attachment/, response.headers["Content-Disposition"])
|
|
389
|
+
|
|
390
|
+
entry.purge!(by: 1)
|
|
391
|
+
get trashed_file_document_path(@doc, attribute: "filename", trash_id: entry.id)
|
|
392
|
+
assert_response :not_found
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
test "downloading a trashed file needs :download_trashed_file" do
|
|
396
|
+
with_file
|
|
397
|
+
entry = @doc.inline_forms_remove_file!(:filename, by: 1)
|
|
398
|
+
Ability.restrictions = -> { cannot :download_trashed_file, Document }
|
|
399
|
+
get trashed_file_document_path(@doc, attribute: "filename", trash_id: entry.id)
|
|
400
|
+
assert_response :forbidden
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
# ---- panel --------------------------------------------------------------
|
|
404
|
+
|
|
405
|
+
test "the open panel lists entries with actions; closed shows the count" do
|
|
406
|
+
with_file
|
|
407
|
+
@doc.inline_forms_remove_file!(:filename, by: 1)
|
|
408
|
+
panel = "document_#{@doc.id}_file_trash"
|
|
409
|
+
get file_trash_document_path(@doc, update: panel, slots: "filename,plan_filename"), headers: frame_headers(panel)
|
|
410
|
+
|
|
411
|
+
assert_response :success
|
|
412
|
+
assert_includes response.body, "plan.pdf"
|
|
413
|
+
assert_includes response.body, "delete permanently"
|
|
414
|
+
assert_includes response.body, "restorable until"
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
test "the panel needs :read_file_trash" do
|
|
418
|
+
Ability.restrictions = -> { cannot :read_file_trash, Document }
|
|
419
|
+
panel = "document_#{@doc.id}_file_trash"
|
|
420
|
+
get file_trash_document_path(@doc, update: panel), headers: frame_headers(panel)
|
|
421
|
+
assert_response :forbidden
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
# ---- destroy / revert --------------------------------------------------
|
|
425
|
+
|
|
426
|
+
test "destroying a record trashes its files; undoing the destroy brings them back" do
|
|
427
|
+
Document.require_filename = true
|
|
428
|
+
with_file
|
|
429
|
+
@doc.update!(plan_filename: upload(OTHER, "plan2.pdf"))
|
|
430
|
+
row = "document_#{@doc.id}"
|
|
431
|
+
delete document_path(@doc, update: row), headers: frame_headers(row)
|
|
432
|
+
assert_equal 2, trash.where(reason: "record_destroyed").count
|
|
433
|
+
|
|
434
|
+
version = PaperTrail::Version.where(item_type: "Document", item_id: @doc.id, event: "destroy").last
|
|
435
|
+
post revert_document_path(version.id, update: "documents_list"), headers: stream_headers("documents_list")
|
|
436
|
+
|
|
437
|
+
assert_response :success
|
|
438
|
+
restored = Document.find(@doc.id)
|
|
439
|
+
assert_equal PLAN, restored.data
|
|
440
|
+
assert_equal "plan.pdf", restored.filename
|
|
441
|
+
assert_equal OTHER, restored.plan_data
|
|
442
|
+
assert trash.where(reason: "record_destroyed").all?(&:restored?)
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
test "undoing a destroy after a purge brings the record back without that file" do
|
|
446
|
+
with_file
|
|
447
|
+
row = "document_#{@doc.id}"
|
|
448
|
+
delete document_path(@doc, update: row), headers: frame_headers(row)
|
|
449
|
+
trash.sole.purge!(by: 1)
|
|
450
|
+
|
|
451
|
+
version = PaperTrail::Version.where(item_type: "Document", item_id: @doc.id, event: "destroy").last
|
|
452
|
+
post revert_document_path(version.id, update: "documents_list"), headers: stream_headers("documents_list")
|
|
453
|
+
|
|
454
|
+
restored = Document.find(@doc.id)
|
|
455
|
+
assert_nil restored.filename
|
|
456
|
+
assert_nil restored.data
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
test "reverting an update never touches a slot, even from an old version carrying a filename" do
|
|
460
|
+
with_file
|
|
461
|
+
@doc.update!(title: "Renamed")
|
|
462
|
+
version = PaperTrail::Version.where(item_type: "Document", item_id: @doc.id, event: "update").last
|
|
463
|
+
# A pre-adoption version row still has the slot columns in it.
|
|
464
|
+
version.update_columns(object: version.object.sub("title: Dossier A", "title: Dossier A\nfilename: old.pdf"))
|
|
465
|
+
|
|
466
|
+
post revert_document_path(version.id, update: "document_#{@doc.id}"), headers: stream_headers("document_#{@doc.id}")
|
|
467
|
+
|
|
468
|
+
assert_response :success
|
|
469
|
+
@doc.reload
|
|
470
|
+
assert_equal "Dossier A", @doc.title
|
|
471
|
+
assert_equal "plan.pdf", @doc.filename
|
|
472
|
+
assert_equal PLAN, @doc.data
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
# ---- retention ----------------------------------------------------------
|
|
476
|
+
|
|
477
|
+
test "an expired entry is no longer restorable and the sweep purges it" do
|
|
478
|
+
with_file
|
|
479
|
+
entry = @doc.inline_forms_remove_file!(:filename, by: 1)
|
|
480
|
+
entry.update_columns(purge_after: 1.minute.ago)
|
|
481
|
+
|
|
482
|
+
assert_raises(InlineForms::TrashedFile::NotRestorable) { entry.reload.restore!(by: 1) }
|
|
483
|
+
assert_equal 1, InlineForms::FileTrashSweep.run!
|
|
484
|
+
assert entry.reload.purged?
|
|
485
|
+
assert_nil entry.purged_by_id
|
|
486
|
+
refute InlineForms::FileTrashSweep.overdue?
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
test "retention nil never expires" do
|
|
490
|
+
InlineForms.file_trash_retention = nil
|
|
491
|
+
with_file
|
|
492
|
+
entry = @doc.inline_forms_remove_file!(:filename, by: 1)
|
|
493
|
+
assert_nil entry.purge_after
|
|
494
|
+
assert_equal 0, InlineForms::TrashedFile.purge_expired!
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
test "the sweep is overdue when it never ran" do
|
|
498
|
+
assert InlineForms::FileTrashSweep.overdue?
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
# ---- global page ----------------------------------------------------------
|
|
502
|
+
|
|
503
|
+
test "the global page lists trashed files and bulk-purges selected ones" do
|
|
504
|
+
with_file
|
|
505
|
+
entry = @doc.inline_forms_remove_file!(:filename, by: 1)
|
|
506
|
+
get inline_forms_file_trash_path
|
|
507
|
+
|
|
508
|
+
assert_response :success
|
|
509
|
+
assert_includes response.body, "plan.pdf"
|
|
510
|
+
|
|
511
|
+
post inline_forms_file_trash_purge_path, params: { ids: [ entry.id ] }
|
|
512
|
+
assert_response :redirect
|
|
513
|
+
assert entry.reload.purged?
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
test "the global page offers bulk purge only when a listed row is purgeable" do
|
|
517
|
+
with_file
|
|
518
|
+
entry = @doc.inline_forms_remove_file!(:filename, by: 1)
|
|
519
|
+
get inline_forms_file_trash_path(state: "trashed")
|
|
520
|
+
assert_includes response.body, %(id="trash_entry_#{entry.id}")
|
|
521
|
+
assert_includes response.body, I18n.t("inline_forms.files.global.purge_selected")
|
|
522
|
+
|
|
523
|
+
entry.restore!(by: 1)
|
|
524
|
+
get inline_forms_file_trash_path(state: "restored")
|
|
525
|
+
assert_includes response.body, "plan.pdf"
|
|
526
|
+
refute_includes response.body, %(name="ids[]")
|
|
527
|
+
refute_includes response.body, I18n.t("inline_forms.files.global.purge_selected")
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
test "the global page restores and downloads" do
|
|
531
|
+
with_file
|
|
532
|
+
entry = @doc.inline_forms_remove_file!(:filename, by: 1)
|
|
533
|
+
get inline_forms_file_trash_download_path(entry)
|
|
534
|
+
assert_equal PLAN, response.body.b
|
|
535
|
+
|
|
536
|
+
post inline_forms_file_trash_restore_path(entry)
|
|
537
|
+
assert_response :redirect
|
|
538
|
+
assert_equal PLAN, @doc.reload.data
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
test "the global page purges files of deleted records" do
|
|
542
|
+
with_file
|
|
543
|
+
row = "document_#{@doc.id}"
|
|
544
|
+
delete document_path(@doc, update: row), headers: frame_headers(row)
|
|
545
|
+
entry = trash.sole
|
|
546
|
+
|
|
547
|
+
post inline_forms_file_trash_purge_path, params: { ids: [ entry.id ] }
|
|
548
|
+
assert entry.reload.purged?
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
test "the global page needs :read_file_trash on :all" do
|
|
552
|
+
Ability.restrictions = lambda do
|
|
553
|
+
cannot :manage, :all
|
|
554
|
+
can :read, :all
|
|
555
|
+
can :read_file_trash, Document
|
|
556
|
+
end
|
|
557
|
+
get inline_forms_file_trash_path
|
|
558
|
+
assert_response :forbidden
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
test "a bulk purge is all or nothing when one row is not allowed" do
|
|
562
|
+
with_file
|
|
563
|
+
mine = @doc.inline_forms_remove_file!(:filename, by: 1)
|
|
564
|
+
@doc.update!(plan_filename: upload(OTHER, "x.pdf"))
|
|
565
|
+
theirs = @doc.inline_forms_remove_file!(:plan_filename, by: 1)
|
|
566
|
+
Ability.restrictions = -> { cannot :purge_file, Document, [ :plan_filename ] }
|
|
567
|
+
|
|
568
|
+
post inline_forms_file_trash_purge_path, params: { ids: [ mine.id, theirs.id ] }
|
|
569
|
+
|
|
570
|
+
assert_response :forbidden
|
|
571
|
+
assert mine.reload.trashed?
|
|
572
|
+
assert theirs.reload.trashed?
|
|
573
|
+
end
|
|
574
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../integration_test_helper"
|
|
4
|
+
|
|
5
|
+
# layouts/inline_forms <title>: t('application_name') used to render the
|
|
6
|
+
# html_safe <span class="translation_missing"> into <title> when the host had
|
|
7
|
+
# no translation (every page of a fresh example app). It now falls back to
|
|
8
|
+
# ApplicationHelper#application_name ("Dummy" here) and is always plain text.
|
|
9
|
+
class LayoutTitleTest < InlineFormsIntegrationTestCase
|
|
10
|
+
teardown do
|
|
11
|
+
I18n.backend.reload!
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def page_title
|
|
15
|
+
response.body[%r{<title>(.*?)</title>}m, 1]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
test "title falls back to the application_name helper without a translation" do
|
|
19
|
+
get widgets_path
|
|
20
|
+
|
|
21
|
+
assert_response :success
|
|
22
|
+
assert_equal "Dummy v#{InlineForms::VERSION}", page_title
|
|
23
|
+
refute_includes page_title, "translation_missing"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
test "title uses the application_name translation when the host defines one" do
|
|
27
|
+
I18n.backend.store_translations(:en, application_name: "Widget Works")
|
|
28
|
+
|
|
29
|
+
get widgets_path
|
|
30
|
+
|
|
31
|
+
assert_response :success
|
|
32
|
+
assert_equal "Widget Works v#{InlineForms::VERSION}", page_title
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
test "title in nl does not show the gem's old placeholder" do
|
|
36
|
+
I18n.with_locale(:nl) do
|
|
37
|
+
original = I18n.default_locale
|
|
38
|
+
I18n.default_locale = :nl
|
|
39
|
+
get widgets_path
|
|
40
|
+
ensure
|
|
41
|
+
I18n.default_locale = original
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
assert_response :success
|
|
45
|
+
assert_equal "Dummy v#{InlineForms::VERSION}", page_title
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
test "markup in the translation is escaped, never rendered into the title" do
|
|
49
|
+
I18n.backend.store_translations(:en, application_name: "<b>Bold</b>")
|
|
50
|
+
|
|
51
|
+
get widgets_path
|
|
52
|
+
|
|
53
|
+
assert_response :success
|
|
54
|
+
assert_equal "<b>Bold</b> v#{InlineForms::VERSION}", page_title
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -44,6 +44,15 @@ class ModelTopBarTest < InlineFormsIntegrationTestCase
|
|
|
44
44
|
refute_includes response.body, "Beta"
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
+
test "no schema GUI link in the engine top bar, even with the schema routes drawn" do
|
|
48
|
+
assert respond_to?(:inline_forms_schema_new_path), "precondition: the dummy draws the schema routes"
|
|
49
|
+
get widgets_path
|
|
50
|
+
|
|
51
|
+
assert_response :success
|
|
52
|
+
refute_includes response.body, "+ field"
|
|
53
|
+
refute_includes response.body, %(href="#{inline_forms_schema_new_path}")
|
|
54
|
+
end
|
|
55
|
+
|
|
47
56
|
test "non-model page with no @Klass renders title only, no new-record link" do
|
|
48
57
|
get stats_path
|
|
49
58
|
|