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 <julien@nahum.net>
This commit is contained in:
Chirag Chhatrala 2025-01-07 19:34:02 +05:30 committed by GitHub
parent b0311257ac
commit cc62f614e4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 164 additions and 14 deletions

View File

@ -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([

View File

@ -0,0 +1,38 @@
<?php
namespace App\Http\Requests;
use App\Models\Forms\Form;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Request;
class FormSubmissionExportRequest extends FormRequest
{
public Form $form;
public function __construct(Request $request)
{
$this->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));
}
}],
];
}
}

View File

@ -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'])

View File

@ -0,0 +1,98 @@
<?php
use App\Models\User;
use Laravel\Sanctum\Sanctum;
use Tests\Helpers\FormSubmissionDataFactory;
it('can export form submissions with selected columns', function () {
$user = $this->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.'
]);
});

View File

@ -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)