Skip to content

Commit cc62f61

Browse files
Enhance Form Submission Export Functionality (#657)
* Enhance Form Submission Export Functionality * Validate new param 'columns' * Form submission export request as seprate class with validation * Test case for export --------- Co-authored-by: Julien Nahum <[email protected]>
1 parent b031125 commit cc62f61

File tree

5 files changed

+164
-14
lines changed

5 files changed

+164
-14
lines changed

api/app/Http/Controllers/Forms/FormSubmissionController.php

+19-10
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use App\Exports\FormSubmissionExport;
66
use App\Http\Controllers\Controller;
77
use App\Http\Requests\AnswerFormRequest;
8+
use App\Http\Requests\FormSubmissionExportRequest;
89
use App\Http\Resources\FormSubmissionResource;
910
use App\Jobs\Form\StoreFormSubmissionJob;
1011
use App\Models\Forms\Form;
@@ -46,38 +47,46 @@ public function update(AnswerFormRequest $request, $id, $submissionId)
4647
]);
4748
}
4849

49-
public function export(string $id)
50+
public function export(FormSubmissionExportRequest $request, string $id)
5051
{
51-
$form = Form::findOrFail((int) $id);
52+
$form = $request->form;
5253
$this->authorize('view', $form);
5354

5455
$allRows = [];
56+
$displayColumns = collect($request->columns)->filter(fn ($value, $key) => $value === true)->toArray();
5557
foreach ($form->submissions->toArray() as $row) {
5658
$formatter = (new FormSubmissionFormatter($form, $row['data']))
5759
->outputStringsOnly()
5860
->setEmptyForNoValue()
5961
->showRemovedFields()
6062
->showHiddenFields()
6163
->useSignedUrlForFiles();
62-
$allRows[] = [
63-
'id' => Hashids::encode($row['id']),
64-
'created_at' => date('Y-m-d H:i', strtotime($row['created_at'])),
65-
...$formatter->getCleanKeyValue(),
66-
];
64+
$formattedData = $formatter->getCleanKeyValue();
65+
$filteredData = ['id' => Hashids::encode($row['id'])];
66+
foreach ($displayColumns as $column => $value) {
67+
$key = collect($formattedData)->keys()->first(fn ($key) => str_contains($key, $column));
68+
if ($key) {
69+
$filteredData[$key] = $formattedData[$key];
70+
}
71+
}
72+
if (isset($displayColumns['created_at'])) {
73+
$filteredData['created_at'] = date('Y-m-d H:i', strtotime($row['created_at']));
74+
}
75+
$allRows[] = $filteredData;
6776
}
6877
$csvExport = (new FormSubmissionExport($allRows));
6978

7079
return Excel::download(
7180
$csvExport,
72-
$form->slug.'-submission-data.csv',
81+
$form->slug . '-submission-data.csv',
7382
\Maatwebsite\Excel\Excel::CSV
7483
);
7584
}
7685

7786
public function submissionFile($id, $fileName)
7887
{
79-
$fileName = Str::of(PublicFormController::FILE_UPLOAD_PATH)->replace('?', $id).'/'
80-
.urldecode($fileName);
88+
$fileName = Str::of(PublicFormController::FILE_UPLOAD_PATH)->replace('?', $id) . '/'
89+
. urldecode($fileName);
8190

8291
if (! Storage::exists($fileName)) {
8392
return $this->error([
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
namespace App\Http\Requests;
4+
5+
use App\Models\Forms\Form;
6+
use Illuminate\Foundation\Http\FormRequest;
7+
use Illuminate\Http\Request;
8+
9+
class FormSubmissionExportRequest extends FormRequest
10+
{
11+
public Form $form;
12+
13+
public function __construct(Request $request)
14+
{
15+
$this->form = Form::findOrFail($request->route('id'));
16+
}
17+
18+
public function rules()
19+
{
20+
$validColumns = collect(array_merge(
21+
$this->form->properties,
22+
$this->form->removed_properties ?? []
23+
))->pluck('id')->toArray();
24+
$validColumns[] = 'created_at';
25+
26+
return [
27+
'columns' => 'required|array',
28+
'columns.*' => ['boolean', 'required'],
29+
'columns' => [function ($attribute, $value, $fail) use ($validColumns) {
30+
$submittedColumns = array_keys($value);
31+
$invalidColumns = array_diff($submittedColumns, $validColumns);
32+
if (!empty($invalidColumns)) {
33+
$fail('The columns contain invalid values: ' . implode(', ', $invalidColumns));
34+
}
35+
}],
36+
];
37+
}
38+
}

api/routes/api.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@
161161

162162
Route::get('/{id}/submissions', [FormSubmissionController::class, 'submissions'])->name('submissions');
163163
Route::put('/{id}/submissions/{submission_id}', [FormSubmissionController::class, 'update'])->name('submissions.update')->middleware([ResolveFormMiddleware::class]);
164-
Route::get('/{id}/submissions/export', [FormSubmissionController::class, 'export'])->name('submissions.export');
164+
Route::post('/{id}/submissions/export', [FormSubmissionController::class, 'export'])->name('submissions.export');
165165
Route::get('/{id}/submissions/file/{filename}', [FormSubmissionController::class, 'submissionFile'])
166166
->middleware('signed')
167167
->withoutMiddleware(['auth:api'])
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<?php
2+
3+
use App\Models\User;
4+
use Laravel\Sanctum\Sanctum;
5+
use Tests\Helpers\FormSubmissionDataFactory;
6+
7+
it('can export form submissions with selected columns', function () {
8+
$user = $this->actingAsProUser();
9+
$workspace = $this->createUserWorkspace($user);
10+
$form = $this->createForm($user, $workspace, [
11+
'properties' => [
12+
[
13+
'id' => 'name_field',
14+
'name' => 'Name',
15+
'type' => 'text',
16+
'required' => true,
17+
],
18+
[
19+
'id' => 'email_field',
20+
'name' => 'Email',
21+
'type' => 'email',
22+
'required' => true,
23+
]
24+
]
25+
]);
26+
27+
// Create some submissions
28+
$submissions = [
29+
['name_field' => 'John Doe', 'email_field' => '[email protected]'],
30+
['name_field' => 'Jane Smith', 'email_field' => '[email protected]']
31+
];
32+
33+
foreach ($submissions as $submission) {
34+
$formData = FormSubmissionDataFactory::generateSubmissionData($form, $submission);
35+
$this->postJson(route('forms.answer', $form->slug), $formData)
36+
->assertSuccessful();
37+
}
38+
39+
// Test export with selected columns
40+
$response = $this->postJson(route('open.forms.submissions.export', [
41+
'id' => $form->id,
42+
'columns' => [
43+
'name_field' => true,
44+
'email_field' => true,
45+
'created_at' => true
46+
]
47+
]));
48+
49+
$response->assertSuccessful()
50+
->assertHeader('content-disposition', 'attachment; filename=' . $form->slug . '-submission-data.csv');
51+
});
52+
53+
it('cannot export form submissions with invalid columns', function () {
54+
$user = $this->actingAsProUser();
55+
$workspace = $this->createUserWorkspace($user);
56+
$form = $this->createForm($user, $workspace, [
57+
'properties' => [
58+
[
59+
'id' => 'name_field',
60+
'name' => 'Name',
61+
'type' => 'text',
62+
'required' => true,
63+
]
64+
]
65+
]);
66+
67+
$response = $this->postJson(route('open.forms.submissions.export', [
68+
'id' => $form->id,
69+
'columns' => [
70+
'invalid_field' => true,
71+
'name_field' => true
72+
]
73+
]));
74+
75+
$response->assertStatus(422)
76+
->assertJsonValidationErrors(['columns']);
77+
});
78+
79+
it('cannot export form submissions from another user form', function () {
80+
$user = User::factory()->create();
81+
$user2 = User::factory()->create();
82+
$workspace = createUserWorkspace($user2);
83+
84+
$form = createForm($user, $workspace);
85+
86+
Sanctum::actingAs($user2);
87+
88+
$response = $this->postJson(route('open.forms.submissions.export', [
89+
'id' => $form->id,
90+
'columns' => [
91+
'name_field' => true
92+
]
93+
]));
94+
95+
$response->assertJson([
96+
'message' => 'Unauthenticated.'
97+
]);
98+
});

client/components/open/forms/components/FormSubmissions.vue

+8-3
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ export default {
213213
if (!this.form) {
214214
return ''
215215
}
216-
return this.runtimeConfig.public.apiBase + '/open/forms/' + this.form.id + '/submissions/export'
216+
return this.runtimeConfig.public.apiBase + 'open/forms/' + this.form.id + '/submissions/export'
217217
},
218218
isLoading() {
219219
return this.recordStore.loading
@@ -333,8 +333,13 @@ export default {
333333
return
334334
}
335335
this.exportLoading = true
336-
opnFetch(this.exportUrl, {responseType: "blob"})
337-
.then(blob => {
336+
opnFetch(this.exportUrl, {
337+
responseType: "blob",
338+
method: "POST",
339+
body: {
340+
columns: this.displayColumns
341+
}
342+
}).then(blob => {
338343
const filename = `${this.form.slug}-${Date.now()}-submissions.csv`
339344
const a = document.createElement("a")
340345
document.body.appendChild(a)

0 commit comments

Comments
 (0)