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:
parent
b0311257ac
commit
cc62f614e4
|
|
@ -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([
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -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'])
|
||||
|
|
|
|||
|
|
@ -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.'
|
||||
]);
|
||||
});
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue