diff --git a/api/app/Http/Controllers/Forms/FormSubmissionController.php b/api/app/Http/Controllers/Forms/FormSubmissionController.php index 18ef9c3f..8a15bc12 100644 --- a/api/app/Http/Controllers/Forms/FormSubmissionController.php +++ b/api/app/Http/Controllers/Forms/FormSubmissionController.php @@ -5,6 +5,7 @@ namespace App\Http\Controllers\Forms; use App\Exports\FormSubmissionExport; use App\Http\Controllers\Controller; use App\Http\Requests\AnswerFormRequest; +use App\Http\Requests\FormSubmissionExportRequest; use App\Http\Resources\FormSubmissionResource; use App\Jobs\Form\StoreFormSubmissionJob; use App\Models\Forms\Form; @@ -46,12 +47,13 @@ class FormSubmissionController extends Controller ]); } - public function export(string $id) + public function export(FormSubmissionExportRequest $request, string $id) { - $form = Form::findOrFail((int) $id); + $form = $request->form; $this->authorize('view', $form); $allRows = []; + $displayColumns = collect($request->columns)->filter(fn ($value, $key) => $value === true)->toArray(); foreach ($form->submissions->toArray() as $row) { $formatter = (new FormSubmissionFormatter($form, $row['data'])) ->outputStringsOnly() @@ -59,25 +61,32 @@ class FormSubmissionController extends Controller ->showRemovedFields() ->showHiddenFields() ->useSignedUrlForFiles(); - $allRows[] = [ - 'id' => Hashids::encode($row['id']), - 'created_at' => date('Y-m-d H:i', strtotime($row['created_at'])), - ...$formatter->getCleanKeyValue(), - ]; + $formattedData = $formatter->getCleanKeyValue(); + $filteredData = ['id' => Hashids::encode($row['id'])]; + foreach ($displayColumns as $column => $value) { + $key = collect($formattedData)->keys()->first(fn ($key) => str_contains($key, $column)); + if ($key) { + $filteredData[$key] = $formattedData[$key]; + } + } + if (isset($displayColumns['created_at'])) { + $filteredData['created_at'] = date('Y-m-d H:i', strtotime($row['created_at'])); + } + $allRows[] = $filteredData; } $csvExport = (new FormSubmissionExport($allRows)); return Excel::download( $csvExport, - $form->slug.'-submission-data.csv', + $form->slug . '-submission-data.csv', \Maatwebsite\Excel\Excel::CSV ); } public function submissionFile($id, $fileName) { - $fileName = Str::of(PublicFormController::FILE_UPLOAD_PATH)->replace('?', $id).'/' - .urldecode($fileName); + $fileName = Str::of(PublicFormController::FILE_UPLOAD_PATH)->replace('?', $id) . '/' + . urldecode($fileName); if (! Storage::exists($fileName)) { return $this->error([ diff --git a/api/app/Http/Requests/FormSubmissionExportRequest.php b/api/app/Http/Requests/FormSubmissionExportRequest.php new file mode 100644 index 00000000..24fda167 --- /dev/null +++ b/api/app/Http/Requests/FormSubmissionExportRequest.php @@ -0,0 +1,38 @@ +form = Form::findOrFail($request->route('id')); + } + + public function rules() + { + $validColumns = collect(array_merge( + $this->form->properties, + $this->form->removed_properties ?? [] + ))->pluck('id')->toArray(); + $validColumns[] = 'created_at'; + + return [ + 'columns' => 'required|array', + 'columns.*' => ['boolean', 'required'], + 'columns' => [function ($attribute, $value, $fail) use ($validColumns) { + $submittedColumns = array_keys($value); + $invalidColumns = array_diff($submittedColumns, $validColumns); + if (!empty($invalidColumns)) { + $fail('The columns contain invalid values: ' . implode(', ', $invalidColumns)); + } + }], + ]; + } +} diff --git a/api/routes/api.php b/api/routes/api.php index d60f860b..e0a49ec3 100644 --- a/api/routes/api.php +++ b/api/routes/api.php @@ -161,7 +161,7 @@ Route::group(['middleware' => 'auth:api'], function () { Route::get('/{id}/submissions', [FormSubmissionController::class, 'submissions'])->name('submissions'); Route::put('/{id}/submissions/{submission_id}', [FormSubmissionController::class, 'update'])->name('submissions.update')->middleware([ResolveFormMiddleware::class]); - Route::get('/{id}/submissions/export', [FormSubmissionController::class, 'export'])->name('submissions.export'); + Route::post('/{id}/submissions/export', [FormSubmissionController::class, 'export'])->name('submissions.export'); Route::get('/{id}/submissions/file/{filename}', [FormSubmissionController::class, 'submissionFile']) ->middleware('signed') ->withoutMiddleware(['auth:api']) diff --git a/api/tests/Feature/Forms/FormSubmissionExportTest.php b/api/tests/Feature/Forms/FormSubmissionExportTest.php new file mode 100644 index 00000000..c2f1241a --- /dev/null +++ b/api/tests/Feature/Forms/FormSubmissionExportTest.php @@ -0,0 +1,98 @@ +actingAsProUser(); + $workspace = $this->createUserWorkspace($user); + $form = $this->createForm($user, $workspace, [ + 'properties' => [ + [ + 'id' => 'name_field', + 'name' => 'Name', + 'type' => 'text', + 'required' => true, + ], + [ + 'id' => 'email_field', + 'name' => 'Email', + 'type' => 'email', + 'required' => true, + ] + ] + ]); + + // Create some submissions + $submissions = [ + ['name_field' => 'John Doe', 'email_field' => 'john@example.com'], + ['name_field' => 'Jane Smith', 'email_field' => 'jane@example.com'] + ]; + + foreach ($submissions as $submission) { + $formData = FormSubmissionDataFactory::generateSubmissionData($form, $submission); + $this->postJson(route('forms.answer', $form->slug), $formData) + ->assertSuccessful(); + } + + // Test export with selected columns + $response = $this->postJson(route('open.forms.submissions.export', [ + 'id' => $form->id, + 'columns' => [ + 'name_field' => true, + 'email_field' => true, + 'created_at' => true + ] + ])); + + $response->assertSuccessful() + ->assertHeader('content-disposition', 'attachment; filename=' . $form->slug . '-submission-data.csv'); +}); + +it('cannot export form submissions with invalid columns', function () { + $user = $this->actingAsProUser(); + $workspace = $this->createUserWorkspace($user); + $form = $this->createForm($user, $workspace, [ + 'properties' => [ + [ + 'id' => 'name_field', + 'name' => 'Name', + 'type' => 'text', + 'required' => true, + ] + ] + ]); + + $response = $this->postJson(route('open.forms.submissions.export', [ + 'id' => $form->id, + 'columns' => [ + 'invalid_field' => true, + 'name_field' => true + ] + ])); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['columns']); +}); + +it('cannot export form submissions from another user form', function () { + $user = User::factory()->create(); + $user2 = User::factory()->create(); + $workspace = createUserWorkspace($user2); + + $form = createForm($user, $workspace); + + Sanctum::actingAs($user2); + + $response = $this->postJson(route('open.forms.submissions.export', [ + 'id' => $form->id, + 'columns' => [ + 'name_field' => true + ] + ])); + + $response->assertJson([ + 'message' => 'Unauthenticated.' + ]); +}); diff --git a/client/components/open/forms/components/FormSubmissions.vue b/client/components/open/forms/components/FormSubmissions.vue index 600be353..d44f0bba 100644 --- a/client/components/open/forms/components/FormSubmissions.vue +++ b/client/components/open/forms/components/FormSubmissions.vue @@ -213,7 +213,7 @@ export default { if (!this.form) { return '' } - return this.runtimeConfig.public.apiBase + '/open/forms/' + this.form.id + '/submissions/export' + return this.runtimeConfig.public.apiBase + 'open/forms/' + this.form.id + '/submissions/export' }, isLoading() { return this.recordStore.loading @@ -333,8 +333,13 @@ export default { return } this.exportLoading = true - opnFetch(this.exportUrl, {responseType: "blob"}) - .then(blob => { + opnFetch(this.exportUrl, { + responseType: "blob", + method: "POST", + body: { + columns: this.displayColumns + } + }).then(blob => { const filename = `${this.form.slug}-${Date.now()}-submissions.csv` const a = document.createElement("a") document.body.appendChild(a)