diff --git a/app/Http/Controllers/V2/Exports/ExportAllProjectDataAsProjectDeveloperController.php b/app/Http/Controllers/V2/Exports/ExportAllProjectDataAsProjectDeveloperController.php index ff37db5dd..8ad92f50e 100644 --- a/app/Http/Controllers/V2/Exports/ExportAllProjectDataAsProjectDeveloperController.php +++ b/app/Http/Controllers/V2/Exports/ExportAllProjectDataAsProjectDeveloperController.php @@ -2,18 +2,16 @@ namespace App\Http\Controllers\V2\Exports; -use App\Exports\V2\EntityExport; use App\Http\Controllers\Controller; +use App\Http\Resources\DelayedJobResource; +use App\Jobs\ExportAllProjectDataAsProjectDeveloperJob; +use App\Models\DelayedJob; use App\Models\V2\Forms\Form; -use App\Models\V2\Nurseries\Nursery; -use App\Models\V2\Nurseries\NurseryReport; use App\Models\V2\Projects\Project; -use App\Models\V2\Projects\ProjectReport; -use App\Models\V2\Sites\Site; -use App\Models\V2\Sites\SiteReport; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Redis; use Illuminate\Support\Str; -use Maatwebsite\Excel\Excel; class ExportAllProjectDataAsProjectDeveloperController extends Controller { @@ -23,124 +21,32 @@ public function __invoke(Request $request, Project $project) $form = $this->getForm(Project::class, $project->framework_key); $this->authorize('export', [Project::class, $form, $project]); - $filename = storage_path('./'.Str::of($project->name)->replace(['/', '\\'], '-') . ' full export - ' . now() . '.zip'); - $zip = new \ZipArchive(); - $zip->open($filename, \ZipArchive::CREATE); + try { + $binary_data = Redis::get('exports:project:'.$project->id); - rescue(function () use ($project, $zip, $form) { - $this->addProjectExports($project, $zip, $form); - }); - rescue(function () use ($project, $zip) { - $this->addProjectReportExports($project, $zip); - }); - rescue(function () use ($project, $zip) { - $this->addSitesExports($project, $zip); - }); - rescue(function () use ($project, $zip) { - $this->addSiteReportsExports($project, $zip); - }); - rescue(function () use ($project, $zip) { - $this->addSiteShapefiles($project, $zip); - }); - rescue(function () use ($project, $zip) { - $this->addNurseriesExports($project, $zip); - }); - - rescue(function () use ($project, $zip) { - $this->addNurseryReportsExports($project, $zip); - }); - - $zip->close(); - - return response()->download($filename)->deleteFileAfterSend(); - } - - private function addSiteReportsExports(Project $project, \ZipArchive $mainZip): void - { - $form = $this->getForm(SiteReport::class, $project->framework_key); - - foreach ($project->sites as $site) { - $filename = 'site reports/'.Str::of($project->name . ' - '.$site->name)->replace(['/', '\\'], '-') . ' - site reports.csv'; - $mainZip - ->addFromString( - $filename, - (new EntityExport(SiteReport::parentId($site->id), $form))->raw(Excel::CSV) + if (! $binary_data) { + $delayedJob = DelayedJob::create(); + $job = new ExportAllProjectDataAsProjectDeveloperJob( + $delayedJob->id, + $form->uuid, + $project->id ); - } - } - - private function addSiteShapefiles(Project $project, \ZipArchive $mainZip): void - { - $shapefilesFolder = 'Sites Shapefiles/'; - $mainZip->addEmptyDir($shapefilesFolder); + dispatch($job); - foreach ($project->sites as $site) { - $filename = $shapefilesFolder . Str::of($site->name)->replace(['/', '\\'], '-') . '.geojson'; - $mainZip->addFromString($filename, $site->boundary_geojson); - } - } + return (new DelayedJobResource($delayedJob))->additional(['message' => "Export for project $project->id is being processed"]); + } else { + $filename = storage_path('./'.Str::of($project->name)->replace(['/', '\\'], '-') . ' full export - ' . now() . '.zip'); + file_put_contents($filename, $binary_data); - private function addNurseryReportsExports(Project $project, \ZipArchive $mainZip): void - { - $form = $this->getForm(NurseryReport::class, $project->framework_key); + return response()->download($filename)->deleteFileAfterSend(); + } + } catch (\Exception $e) { + Log::error('Error during export for single project : ' . $e->getMessage()); - foreach ($project->nurseries as $nursery) { - $filename = 'nursery reports/'.Str::of($project->name . ' - '.$nursery->name)->replace(['/', '\\'], '-') . ' - nursery reports.csv'; - $mainZip - ->addFromString( - $filename, - (new EntityExport(NurseryReport::parentId($nursery->id), $form))->raw(Excel::CSV) - ); + return response()->json(['error' => 'An error occurred during single project export'], 500); } } - private function addProjectExports(Project $project, \ZipArchive $mainZip, Form $form): void - { - $filename = Str::of($project->name)->replace(['/', '\\'], '-') . ' - project establishment data.csv'; - - $mainZip - ->addFromString( - $filename, - (new EntityExport(Project::query()->where('id', $project->id), $form))->raw(Excel::CSV) - ); - } - - private function addProjectReportExports(Project $project, \ZipArchive $mainZip): void - { - $form = $this->getForm(ProjectReport::class, $project->framework_key); - $filename = Str::of($project->name)->replace(['/', '\\'], '-') . ' - project reports.csv'; - - $mainZip - ->addFromString( - $filename, - (new EntityExport($project->reports()->getQuery(), $form))->raw(Excel::CSV) - ); - } - - private function addSitesExports(Project $project, \ZipArchive $mainZip): void - { - $form = $this->getForm(Site::class, $project->framework_key); - $filename = Str::of($project->name)->replace(['/', '\\'], '-') . ' - site establishment data.csv'; - $mainZip - ->addFromString( - $filename, - (new EntityExport($project->sites()->getQuery(), $form))->raw(Excel::CSV) - ); - } - - private function addNurseriesExports(Project $project, \ZipArchive $mainZip): void - { - $form = $this->getForm(Nursery::class, $project->framework_key); - $filename = Str::of($project->name)->replace(['/', '\\'], '-') . ' - nursery establishment data.csv'; - - - $mainZip - ->addFromString( - $filename, - (new EntityExport($project->nurseries()->getQuery(), $form))->raw(Excel::CSV) - ); - } - private function getForm(string $modelClass, string $framework) { return Form::where('model', $modelClass) diff --git a/app/Jobs/ExportAllProjectDataAsProjectDeveloperJob.php b/app/Jobs/ExportAllProjectDataAsProjectDeveloperJob.php new file mode 100644 index 000000000..312015a57 --- /dev/null +++ b/app/Jobs/ExportAllProjectDataAsProjectDeveloperJob.php @@ -0,0 +1,69 @@ +delayed_job_id = $delayed_job_id; + $this->form_uuid = $form_uuid; + $this->project_id = $project_id; + } + + public function handle(ExportAllProjectDataAsProjectDeveloperService $exportAllProjectDataAsProjectDeveloperService) + { + try { + $delayedJob = DelayedJob::findOrFail($this->delayed_job_id); + $project = Project::findOrFail($this->project_id); + $form = Form::isUuid($this->form_uuid)->firstOrFail(); + + $binary_data = $exportAllProjectDataAsProjectDeveloperService->run($form, $project); + Redis::set('exports:project:'.$project->id, $binary_data, 'EX', 7200); + + $delayedJob->update([ + 'status' => DelayedJob::STATUS_SUCCEEDED, + 'payload' => ['message' => 'Total Header Calculation completed'], + 'status_code' => Response::HTTP_OK, + ]); + + } catch (Exception $e) { + Log::error('Error in ExportAllProjectDataAsProjectDeveloperJob: ' . $e->getMessage()); + + DelayedJob::where('id', $this->delayed_job_id)->update([ + 'status' => DelayedJob::STATUS_FAILED, + 'payload' => json_encode(['error' => $e->getMessage()]), + 'status_code' => Response::HTTP_INTERNAL_SERVER_ERROR, + ]); + } + } +} diff --git a/app/Services/ExportAllProjectDataAsProjectDeveloperService.php b/app/Services/ExportAllProjectDataAsProjectDeveloperService.php new file mode 100644 index 000000000..d3e1cc94f --- /dev/null +++ b/app/Services/ExportAllProjectDataAsProjectDeveloperService.php @@ -0,0 +1,145 @@ +name)->replace(['/', '\\'], '-') . ' full export - ' . now() . '.zip'); + + $zip = new \ZipArchive(); + $zip->open($filename, \ZipArchive::CREATE); + + rescue(function () use ($project, $zip, $form) { + $this->addProjectExports($project, $zip, $form); + }); + rescue(function () use ($project, $zip) { + $this->addProjectReportExports($project, $zip); + }); + rescue(function () use ($project, $zip) { + $this->addSitesExports($project, $zip); + }); + rescue(function () use ($project, $zip) { + $this->addSiteReportsExports($project, $zip); + }); + rescue(function () use ($project, $zip) { + $this->addSiteShapefiles($project, $zip); + }); + rescue(function () use ($project, $zip) { + $this->addNurseriesExports($project, $zip); + }); + + rescue(function () use ($project, $zip) { + $this->addNurseryReportsExports($project, $zip); + }); + + $zip->close(); + + return file_get_contents($filename); + } + + private function addSiteReportsExports(Project $project, \ZipArchive $mainZip): void + { + $form = $this->getForm(SiteReport::class, $project->framework_key); + + foreach ($project->sites as $site) { + $filename = 'site reports/'.Str::of($project->name . ' - '.$site->name)->replace(['/', '\\'], '-') . ' - site reports.csv'; + $mainZip + ->addFromString( + $filename, + (new EntityExport(SiteReport::parentId($site->id), $form))->raw(Excel::CSV) + ); + } + } + + private function addSiteShapefiles(Project $project, \ZipArchive $mainZip): void + { + $shapefilesFolder = 'Sites Shapefiles/'; + $mainZip->addEmptyDir($shapefilesFolder); + + foreach ($project->sites as $site) { + $filename = $shapefilesFolder . Str::of($site->name)->replace(['/', '\\'], '-') . '.geojson'; + $mainZip->addFromString($filename, $site->boundary_geojson); + } + } + + private function addNurseryReportsExports(Project $project, \ZipArchive $mainZip): void + { + $form = $this->getForm(NurseryReport::class, $project->framework_key); + + foreach ($project->nurseries as $nursery) { + $filename = 'nursery reports/'.Str::of($project->name . ' - '.$nursery->name)->replace(['/', '\\'], '-') . ' - nursery reports.csv'; + $mainZip + ->addFromString( + $filename, + (new EntityExport(NurseryReport::parentId($nursery->id), $form))->raw(Excel::CSV) + ); + } + } + + private function addProjectExports(Project $project, \ZipArchive $mainZip, Form $form): void + { + $filename = Str::of($project->name)->replace(['/', '\\'], '-') . ' - project establishment data.csv'; + + $mainZip + ->addFromString( + $filename, + (new EntityExport(Project::query()->where('id', $project->id), $form))->raw(Excel::CSV) + ); + } + + private function addProjectReportExports(Project $project, \ZipArchive $mainZip): void + { + $form = $this->getForm(ProjectReport::class, $project->framework_key); + $filename = Str::of($project->name)->replace(['/', '\\'], '-') . ' - project reports.csv'; + + $mainZip + ->addFromString( + $filename, + (new EntityExport($project->reports()->getQuery(), $form))->raw(Excel::CSV) + ); + } + + private function addSitesExports(Project $project, \ZipArchive $mainZip): void + { + $form = $this->getForm(Site::class, $project->framework_key); + $filename = Str::of($project->name)->replace(['/', '\\'], '-') . ' - site establishment data.csv'; + $mainZip + ->addFromString( + $filename, + (new EntityExport($project->sites()->getQuery(), $form))->raw(Excel::CSV) + ); + } + + private function addNurseriesExports(Project $project, \ZipArchive $mainZip): void + { + $form = $this->getForm(Nursery::class, $project->framework_key); + $filename = Str::of($project->name)->replace(['/', '\\'], '-') . ' - nursery establishment data.csv'; + + + $mainZip + ->addFromString( + $filename, + (new EntityExport($project->nurseries()->getQuery(), $form))->raw(Excel::CSV) + ); + } + + private function getForm(string $modelClass, string $framework) + { + return Form::where('model', $modelClass) + ->where('framework_key', $framework) + ->firstOrFail(); + } +} diff --git a/tests/V2/Exports/ExportEntitiesAsProjectDeveloperControllerTest.php b/tests/V2/Exports/ExportEntitiesAsProjectDeveloperControllerTest.php index f7097ee14..85a0e67ba 100644 --- a/tests/V2/Exports/ExportEntitiesAsProjectDeveloperControllerTest.php +++ b/tests/V2/Exports/ExportEntitiesAsProjectDeveloperControllerTest.php @@ -14,6 +14,7 @@ use Carbon\Carbon; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\WithFaker; +use Illuminate\Support\Facades\Redis; use Tests\TestCase; class ExportEntitiesAsProjectDeveloperControllerTest extends TestCase @@ -170,7 +171,7 @@ public function test_an_user_can_export_project_reports_data(string $permission, /** * @dataProvider permissionsDataProvider */ - public function test_an_user_can_export_all_project_data(string $permission, string $fmKey) + public function test_an_user_can_export_all_project_data_gets_a_delayed_job_when_not_cached(string $permission, string $fmKey) { Carbon::setTestNow(now()); @@ -220,6 +221,73 @@ public function test_an_user_can_export_all_project_data(string $permission, str $uri = '/api/v2/projects/' . $project->uuid . '/export'; + $this->actingAs($owner) + ->get($uri) + ->assertJsonStructure([ + 'data' => [ + 'message', + 'job_uuid', + ], + 'message', + ]) + ->assertSuccessful(); + } + + /** + * @dataProvider permissionsDataProvider + */ + public function test_an_user_can_export_all_project_data_when_already_cached(string $permission, string $fmKey) + { + Carbon::setTestNow(now()); + + $organisation = Organisation::factory()->create(); + $owner = User::factory()->create(['organisation_id' => $organisation->id]); + + $project = Project::factory()->create([ + 'framework_key' => $fmKey, + 'organisation_id' => $organisation->id, + ]); + + ProjectReport::factory()->count(5)->create([ + 'framework_key' => $fmKey, + 'project_id' => $project->id, + ]); + + $nurseries = Nursery::factory()->count(5)->create([ + 'framework_key' => $fmKey, + 'project_id' => $project->id, + ]); + + foreach ($nurseries as $nursery) { + NurseryReport::factory()->count(5)->create([ + 'framework_key' => $fmKey, + 'nursery_id' => $nursery->id, + ]); + } + + $sites = Site::factory()->count(5)->create([ + 'framework_key' => $fmKey, + 'project_id' => $project->id, + ]); + + foreach ($sites as $site) { + SiteReport::factory()->count(5)->create([ + 'framework_key' => $fmKey, + 'site_id' => $site->id, + ]); + } + + CustomFormHelper::generateFakeForm('project', $fmKey); + CustomFormHelper::generateFakeForm('project-report', $fmKey); + CustomFormHelper::generateFakeForm('nursery', $fmKey); + CustomFormHelper::generateFakeForm('nursery-report', $fmKey); + CustomFormHelper::generateFakeForm('site', $fmKey); + CustomFormHelper::generateFakeForm('site-report', $fmKey); + + $uri = '/api/v2/projects/' . $project->uuid . '/export'; + + Redis::set('exports:project:'.$project->id, 'some text'); + $this->actingAs($owner) ->get($uri) ->assertDownload($project->name . ' full export - ' . now() . '.zip')