From 4cd025f36e1c6edb2e7354882b0ccc1c78ba80bf Mon Sep 17 00:00:00 2001 From: Salim Kanoun Date: Sun, 12 Nov 2023 15:54:24 +0100 Subject: [PATCH 01/72] refac delete commands --- GaelO2/app/Console/Commands/DeleteStudy.php | 128 +++--------------- .../Commands/GaelODeleteRessourcesService.php | 118 ++++++++++++++++ 2 files changed, 138 insertions(+), 108 deletions(-) create mode 100644 GaelO2/app/Console/Commands/GaelODeleteRessourcesService.php diff --git a/GaelO2/app/Console/Commands/DeleteStudy.php b/GaelO2/app/Console/Commands/DeleteStudy.php index 60ab75ea2..2e64b4743 100644 --- a/GaelO2/app/Console/Commands/DeleteStudy.php +++ b/GaelO2/app/Console/Commands/DeleteStudy.php @@ -4,18 +4,11 @@ use App\GaelO\Services\OrthancService; use App\Models\DicomSeries; -use App\Models\DicomStudy; -use App\Models\Documentation; -use App\Models\Patient; -use App\Models\Review; -use App\Models\ReviewStatus; -use App\Models\Role; use App\Models\Study; -use App\Models\Tracker; use App\Models\Visit; -use App\Models\VisitGroup; use Exception; use Illuminate\Console\Command; +use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Storage; @@ -23,16 +16,8 @@ class DeleteStudy extends Command { private Study $study; - private Patient $patient; private Visit $visit; - private ReviewStatus $reviewStatus; - private DicomStudy $dicomStudy; private DicomSeries $dicomSeries; - private Tracker $tracker; - private Documentation $documentation; - private Role $role; - private VisitGroup $visitGroup; - private Review $review; private OrthancService $orthancService; /** * The name and signature of the console command. @@ -55,29 +40,13 @@ class DeleteStudy extends Command */ public function handle( Study $study, - VisitGroup $visitGroup, Visit $visit, - Patient $patient, - DicomStudy $dicomStudy, DicomSeries $dicomSeries, - Role $role, - Tracker $tracker, - Documentation $documentation, - Review $review, - ReviewStatus $reviewStatus, - OrthancService $orthancService) - { + OrthancService $orthancService + ) { $this->study = $study; - $this->visitGroup = $visitGroup; $this->visit = $visit; - $this->patient = $patient; - $this->dicomStudy = $dicomStudy; $this->dicomSeries = $dicomSeries; - $this->tracker = $tracker; - $this->documentation = $documentation; - $this->role = $role; - $this->review = $review; - $this->reviewStatus = $reviewStatus; $this->orthancService = $orthancService; $this->orthancService->setOrthancServer(true); @@ -106,9 +75,10 @@ public function handle( if ($this->confirm('Warning : This CANNOT be undone, do you wish to continue?')) { - $this->deleteDocumentation($studyEntity->name); - $this->deleteRoles($studyEntity->name); - $this->deleteTracker($studyEntity->name); + $gaeloDeleteRessourceService = App::make(GaelODeleteRessourcesService::class); + $gaeloDeleteRessourceService->deleteDocumentation($studyEntity->name); + $gaeloDeleteRessourceService->deleteRoles($studyEntity->name); + $gaeloDeleteRessourceService->deleteTracker($studyEntity->name); //Get Visit ID of Original Study $originalStudy = $studyEntity->ancillary_of ?? $studyEntity->name; @@ -118,10 +88,9 @@ public function handle( return $visit['id']; }, $visits->toArray()); - - $this->deleteReviews($visitIds, $studyName); - $this->deleteReviewStatus($visitIds, $studyName); - + $gaeloDeleteRessourceService->deleteReviews($visitIds, $studyName); + $gaeloDeleteRessourceService->deleteReviewStatus($visitIds, $studyName); + if ($studyEntity['ancillary_of'] === null) { $dicomSeries = $this->getDicomSeriesOfVisits($visitIds); @@ -137,27 +106,26 @@ public function handle( $dicomSeries ); - - $this->deleteDicomsSeries($visitIds); - $this->deleteDicomsStudies($visitIds); - $this->deleteVisits($visitIds); - $this->deleteVisitGroupAndVisitType($studyName); - $this->deletePatient($studyName); + $gaeloDeleteRessourceService->deleteDicomsSeries($visitIds); + $gaeloDeleteRessourceService->deleteDicomsStudies($visitIds); + $gaeloDeleteRessourceService->deleteVisits($visitIds); + $gaeloDeleteRessourceService->deleteVisitGroupAndVisitType($studyName); + $gaeloDeleteRessourceService->deletePatient($studyName); } $studyEntity->forceDelete(); - if($this->option('deleteDicom') && $this->confirm('Found '.sizeOf($orthancIdArray).' series to delete, do you want to continue ?')){ - foreach($orthancIdArray as $seriesOrthancId){ - try{ + if ($this->option('deleteDicom') && $this->confirm('Found ' . sizeOf($orthancIdArray) . ' series to delete, do you want to continue ?')) { + foreach ($orthancIdArray as $seriesOrthancId) { + try { $this->orthancService->deleteFromOrthanc('series', $seriesOrthancId); - }catch(Exception $e){ + } catch (Exception $e) { Log::error($e->getMessage()); } } } - if($this->option('deleteAssociatedFile')&& $this->confirm('Going to delete associated file, do you want to continue ?')){ + if ($this->option('deleteAssociatedFile') && $this->confirm('Going to delete associated file, do you want to continue ?')) { Storage::deleteDirectory($studyName); } @@ -183,60 +151,4 @@ private function getVisitsOfStudy(string $studyName) })->get(); } - private function deleteDocumentation(string $studyName) - { - $this->documentation->where('study_name', $studyName)->withTrashed()->forceDelete(); - } - - private function deleteRoles(string $studyName) - { - $this->role->where('study_name', $studyName)->delete(); - } - - private function deleteTracker(string $studyName) - { - $this->tracker->where('study_name', $studyName)->delete(); - } - - private function deleteDicomsStudies(array $visitId) - { - return $this->dicomStudy->whereIn('visit_id', $visitId)->withTrashed()->forceDelete(); - } - - private function deleteDicomsSeries(array $visitId) - { - return $this->dicomSeries->whereHas('dicomStudy', function ($query) use ($visitId) { - $query->whereIn('visit_id', $visitId)->withTrashed(); - })->withTrashed()->forceDelete(); - } - - - private function deleteVisitGroupAndVisitType(string $studyName) - { - $visitGroups = $this->visitGroup->where('study_name', $studyName)->get(); - foreach ($visitGroups as $visitGroup) { - $visitGroup->visitTypes()->delete(); - $visitGroup->delete(); - } - } - - private function deletePatient(string $studyName) - { - $this->patient->where('study_name', $studyName)->delete(); - } - - private function deleteReviews(array $visitIds, string $studyName) - { - $this->review->where('study_name', $studyName)->whereIn('visit_id', $visitIds)->withTrashed()->forceDelete(); - } - - private function deleteReviewStatus(array $visitIds, String $studyName) - { - $this->reviewStatus->where('study_name', $studyName)->whereIn('visit_id', $visitIds)->delete(); - } - - private function deleteVisits(array $visitIds) - { - $this->visit->whereIn('id', $visitIds)->withTrashed()->forceDelete(); - } } diff --git a/GaelO2/app/Console/Commands/GaelODeleteRessourcesService.php b/GaelO2/app/Console/Commands/GaelODeleteRessourcesService.php new file mode 100644 index 000000000..70f6152eb --- /dev/null +++ b/GaelO2/app/Console/Commands/GaelODeleteRessourcesService.php @@ -0,0 +1,118 @@ +study = $study; + $this->visitGroup = $visitGroup; + $this->visit = $visit; + $this->patient = $patient; + $this->dicomStudy = $dicomStudy; + $this->dicomSeries = $dicomSeries; + $this->tracker = $tracker; + $this->documentation = $documentation; + $this->role = $role; + $this->review = $review; + $this->reviewStatus = $reviewStatus; + } + + public function deleteDocumentation(string $studyName) + { + $this->documentation->where('study_name', $studyName)->withTrashed()->forceDelete(); + } + + public function deleteRoles(string $studyName) + { + $this->role->where('study_name', $studyName)->delete(); + } + + public function deleteTracker(string $studyName) + { + $this->tracker->where('study_name', $studyName)->delete(); + } + + public function deleteDicomsStudies(array $visitId) + { + return $this->dicomStudy->whereIn('visit_id', $visitId)->withTrashed()->forceDelete(); + } + + public function deleteDicomsSeries(array $visitId) + { + return $this->dicomSeries->whereHas('dicomStudy', function ($query) use ($visitId) { + $query->whereIn('visit_id', $visitId)->withTrashed(); + })->withTrashed()->forceDelete(); + } + + public function deletePatient(string $studyName) + { + $this->patient->where('study_name', $studyName)->delete(); + } + + public function deleteReviews(array $visitIds, string $studyName) + { + $this->review->where('study_name', $studyName)->whereIn('visit_id', $visitIds)->withTrashed()->forceDelete(); + } + + public function deleteReviewStatus(array $visitIds, String $studyName) + { + $this->reviewStatus->where('study_name', $studyName)->whereIn('visit_id', $visitIds)->delete(); + } + + public function deleteVisits(array $visitIds) + { + $this->visit->whereIn('id', $visitIds)->withTrashed()->forceDelete(); + } + + public function deleteVisitGroupAndVisitType(string $studyName) + { + $visitGroups = $this->visitGroup->where('study_name', $studyName)->get(); + foreach ($visitGroups as $visitGroup) { + $visitGroup->visitTypes()->delete(); + $visitGroup->delete(); + } + } +} From 43a8c0a08aa0e68f52044241513f0077b44f396a Mon Sep 17 00:00:00 2001 From: Salim Kanoun Date: Mon, 13 Nov 2023 09:14:09 +0100 Subject: [PATCH 02/72] starting remove old visits, tested delete study --- .../app/Console/Commands/DeleteOldVisits.php | 150 ++++++++++++++++++ GaelO2/app/Console/Commands/DeleteStudy.php | 42 ++--- ...hp => GaelODeleteRessourcesRepository.php} | 12 +- GaelO2/app/Providers/AdapterProvider.php | 2 + 4 files changed, 182 insertions(+), 24 deletions(-) create mode 100644 GaelO2/app/Console/Commands/DeleteOldVisits.php rename GaelO2/app/Console/Commands/{GaelODeleteRessourcesService.php => GaelODeleteRessourcesRepository.php} (93%) diff --git a/GaelO2/app/Console/Commands/DeleteOldVisits.php b/GaelO2/app/Console/Commands/DeleteOldVisits.php new file mode 100644 index 000000000..5b4b844b2 --- /dev/null +++ b/GaelO2/app/Console/Commands/DeleteOldVisits.php @@ -0,0 +1,150 @@ +study = $study; + $this->visit = $visit; + $this->dicomSeries = $dicomSeries; + $this->orthancService = $orthancService; + $this->gaelODeleteRessourcesRepository = $gaelODeleteRessourcesRepository; + $this->orthancService->setOrthancServer(true); + + $studyName = $this->argument('studyName'); + $studyNameConfirmation = $this->ask('Warning : Please confirm study Name'); + + if ($studyName !== $studyNameConfirmation) { + $this->error('Wrong study name, terminating'); + return 0; + } + + $studyEntity = $this->study->withTrashed()->findOrFail($studyName); + + //Check that no ancillary study is remaining on this study + $ancilariesStudies = $this->study->where('ancillary_of', $studyName)->get(); + if ($ancilariesStudies->count() > 0) { + $this->error('Delete all ancilaries studies first'); + return 0; + } + + if($studyEntity['ancillary_of']){ + $this->error('Cannot be used for an ancillary study'); + return 0; + } + + if ($this->confirm('Warning : This CANNOT be undone, do you wish to continue?')) { + + //TODO delete tracker doit etre specifique au visites supprimées + //$this->gaelODeleteRessourcesRepository->deleteTracker($studyEntity->name); + + //Get Visit ID of Original Study + //TODO Doit tenir compte de l'interval de temps depuis la creation + $visits = $this->getVisitsOfStudy($studyName); + + $visitIds = array_map(function ($visit) { + return $visit['id']; + }, $visits->toArray()); + + $this->gaelODeleteRessourcesRepository->deleteReviews($visitIds, $studyName); + $this->gaelODeleteRessourcesRepository->deleteReviewStatus($visitIds, $studyName); + + $dicomSeries = $this->getDicomSeriesOfVisits($visitIds); + $orthancIdArray = array_map(function ($seriesId) { + return $seriesId['orthanc_id']; + }, $dicomSeries); + + $this->line(implode(" ", $orthancIdArray)); + + $this->table( + ['orthanc_id'], + $dicomSeries + ); + + $this->gaelODeleteRessourcesRepository->deleteDicomsSeries($visitIds); + $this->gaelODeleteRessourcesRepository->deleteDicomsStudies($visitIds); + $this->gaelODeleteRessourcesRepository->deleteVisits($visitIds); + //TODO Doit supprimer les patients que si il ne reste pas de visites... + //$this->gaelODeleteRessourcesRepository->deletePatient($studyName); + + + if ($this->option('deleteDicom') && $this->confirm('Found ' . sizeOf($orthancIdArray) . ' series to delete, do you want to continue ?')) { + foreach ($orthancIdArray as $seriesOrthancId) { + try { + $this->info('Deleting '.$seriesOrthancId); + $this->orthancService->deleteFromOrthanc('series', $seriesOrthancId); + } catch (Exception $e) { + Log::error($e->getMessage()); + } + } + } + + if ($this->option('deleteAssociatedFile') && $this->confirm('Going to delete associated file, do you want to continue ?')) { + //TODO doit supprimer que les associated file des visites deleted + //Storage::deleteDirectory($studyName); + } + + $this->info('The command was successful !'); + } + + return 0; + } + + private function getDicomSeriesOfVisits(array $visitIds) + { + return $this->dicomSeries + ->whereHas('dicomStudy', function ($query) use ($visitIds) { + $query->whereIn('visit_id', $visitIds)->withTrashed(); + })->select('orthanc_id')->get()->toArray(); + } + + private function getVisitsOfStudy(string $studyName) + { + return $this->visit->withTrashed()->with(['visitType', 'patient']) + ->whereHas('patient', function ($query) use ($studyName) { + $query->where('study_name', $studyName); + })->get(); + } + +} diff --git a/GaelO2/app/Console/Commands/DeleteStudy.php b/GaelO2/app/Console/Commands/DeleteStudy.php index 2e64b4743..ea4a0100a 100644 --- a/GaelO2/app/Console/Commands/DeleteStudy.php +++ b/GaelO2/app/Console/Commands/DeleteStudy.php @@ -8,7 +8,6 @@ use App\Models\Visit; use Exception; use Illuminate\Console\Command; -use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Storage; @@ -19,6 +18,7 @@ class DeleteStudy extends Command private Visit $visit; private DicomSeries $dicomSeries; private OrthancService $orthancService; + private GaelODeleteRessourcesRepository $gaelODeleteRessourcesRepository; /** * The name and signature of the console command. * @@ -42,12 +42,14 @@ public function handle( Study $study, Visit $visit, DicomSeries $dicomSeries, - OrthancService $orthancService + OrthancService $orthancService, + GaelODeleteRessourcesRepository $gaelODeleteRessourcesRepository ) { $this->study = $study; $this->visit = $visit; $this->dicomSeries = $dicomSeries; $this->orthancService = $orthancService; + $this->gaelODeleteRessourcesRepository = $gaelODeleteRessourcesRepository; $this->orthancService->setOrthancServer(true); $studyName = $this->argument('studyName'); @@ -75,10 +77,9 @@ public function handle( if ($this->confirm('Warning : This CANNOT be undone, do you wish to continue?')) { - $gaeloDeleteRessourceService = App::make(GaelODeleteRessourcesService::class); - $gaeloDeleteRessourceService->deleteDocumentation($studyEntity->name); - $gaeloDeleteRessourceService->deleteRoles($studyEntity->name); - $gaeloDeleteRessourceService->deleteTracker($studyEntity->name); + $this->gaelODeleteRessourcesRepository->deleteDocumentation($studyEntity->name); + $this->gaelODeleteRessourcesRepository->deleteRoles($studyEntity->name); + $this->gaelODeleteRessourcesRepository->deleteTracker($studyEntity->name); //Get Visit ID of Original Study $originalStudy = $studyEntity->ancillary_of ?? $studyEntity->name; @@ -88,8 +89,8 @@ public function handle( return $visit['id']; }, $visits->toArray()); - $gaeloDeleteRessourceService->deleteReviews($visitIds, $studyName); - $gaeloDeleteRessourceService->deleteReviewStatus($visitIds, $studyName); + $this->gaelODeleteRessourcesRepository->deleteReviews($visitIds, $studyName); + $this->gaelODeleteRessourcesRepository->deleteReviewStatus($visitIds, $studyName); if ($studyEntity['ancillary_of'] === null) { @@ -98,26 +99,30 @@ public function handle( $orthancIdArray = array_map(function ($seriesId) { return $seriesId['orthanc_id']; }, $dicomSeries); - + $this->line(implode(" ", $orthancIdArray)); - + $this->table( ['orthanc_id'], $dicomSeries ); - $gaeloDeleteRessourceService->deleteDicomsSeries($visitIds); - $gaeloDeleteRessourceService->deleteDicomsStudies($visitIds); - $gaeloDeleteRessourceService->deleteVisits($visitIds); - $gaeloDeleteRessourceService->deleteVisitGroupAndVisitType($studyName); - $gaeloDeleteRessourceService->deletePatient($studyName); + $this->gaelODeleteRessourcesRepository->deleteDicomsSeries($visitIds); + $this->gaelODeleteRessourcesRepository->deleteDicomsStudies($visitIds); + $this->gaelODeleteRessourcesRepository->deleteVisits($visitIds); + $this->gaelODeleteRessourcesRepository->deleteVisitGroupAndVisitType($studyName); + $this->gaelODeleteRessourcesRepository->deletePatient($studyName); } - $studyEntity->forceDelete(); + $this->gaelODeleteRessourcesRepository->deleteStudy($studyName); - if ($this->option('deleteDicom') && $this->confirm('Found ' . sizeOf($orthancIdArray) . ' series to delete, do you want to continue ?')) { + $confirmDeleteDicom = $this->confirm('Found ' . sizeOf($orthancIdArray) . ' series to delete, do you want to continue ?'); + $confirmDeleteAssociatedFiles = $this->confirm('Going to delete associated file, do you want to continue ?'); + + if ($this->option('deleteDicom') && $confirmDeleteDicom) { foreach ($orthancIdArray as $seriesOrthancId) { try { + $this->info('Deleting ' . $seriesOrthancId); $this->orthancService->deleteFromOrthanc('series', $seriesOrthancId); } catch (Exception $e) { Log::error($e->getMessage()); @@ -125,7 +130,7 @@ public function handle( } } - if ($this->option('deleteAssociatedFile') && $this->confirm('Going to delete associated file, do you want to continue ?')) { + if ($this->option('deleteAssociatedFile') && $confirmDeleteAssociatedFiles) { Storage::deleteDirectory($studyName); } @@ -150,5 +155,4 @@ private function getVisitsOfStudy(string $studyName) $query->where('study_name', $studyName); })->get(); } - } diff --git a/GaelO2/app/Console/Commands/GaelODeleteRessourcesService.php b/GaelO2/app/Console/Commands/GaelODeleteRessourcesRepository.php similarity index 93% rename from GaelO2/app/Console/Commands/GaelODeleteRessourcesService.php rename to GaelO2/app/Console/Commands/GaelODeleteRessourcesRepository.php index 70f6152eb..def1ba600 100644 --- a/GaelO2/app/Console/Commands/GaelODeleteRessourcesService.php +++ b/GaelO2/app/Console/Commands/GaelODeleteRessourcesRepository.php @@ -2,9 +2,6 @@ namespace App\Console\Commands; -use App\GaelO\Entities\StudyEntity; -use App\GaelO\Exceptions\GaelOException; -use App\GaelO\Services\OrthancService; use App\Models\DicomSeries; use App\Models\DicomStudy; use App\Models\Documentation; @@ -17,7 +14,7 @@ use App\Models\Visit; use App\Models\VisitGroup; -class GaelODeleteRessourcesService +class GaelODeleteRessourcesRepository { private Study $study; @@ -31,7 +28,6 @@ class GaelODeleteRessourcesService private Role $role; private VisitGroup $visitGroup; private Review $review; - private OrthancService $orthancService; public function __construct( Study $study, @@ -102,6 +98,12 @@ public function deleteReviewStatus(array $visitIds, String $studyName) $this->reviewStatus->where('study_name', $studyName)->whereIn('visit_id', $visitIds)->delete(); } + public function deleteStudy(string $studyName) + { + $studyEntity = $this->study->withTrashed()->findOrFail($studyName); + $studyEntity->forceDelete(); + } + public function deleteVisits(array $visitIds) { $this->visit->whereIn('id', $visitIds)->withTrashed()->forceDelete(); diff --git a/GaelO2/app/Providers/AdapterProvider.php b/GaelO2/app/Providers/AdapterProvider.php index 72c6c919f..c9d11ab6d 100644 --- a/GaelO2/app/Providers/AdapterProvider.php +++ b/GaelO2/app/Providers/AdapterProvider.php @@ -2,6 +2,7 @@ namespace App\Providers; +use App\Console\Commands\GaelODeleteRessourcesRepository; use App\GaelO\Adapters\DatabaseDumperAdapter; use App\GaelO\Adapters\FrameworkAdapter; use App\GaelO\Adapters\HttpClientAdapter; @@ -34,6 +35,7 @@ public function register() $this->app->bind(PhoneNumberInterface::class, PhoneNumberAdapter::class); $this->app->bind(JobInterface::class, JobAdapter::class); $this->app->bind(PdfInterface::class, PdfAdapter::class); + $this->app->bind(GaelODeleteRessourcesRepository::class, GaelODeleteRessourcesRepository::class); } /** From e0bcfb482bbcd2fde12d87fb9eb61b8a4c6e5b46 Mon Sep 17 00:00:00 2001 From: Salim Kanoun Date: Tue, 14 Nov 2023 00:23:31 +0100 Subject: [PATCH 03/72] WIP --- .../app/Console/Commands/DeleteOldVisits.php | 39 ++++++------------- .../GaelODeleteRessourcesRepository.php | 10 +++++ 2 files changed, 21 insertions(+), 28 deletions(-) diff --git a/GaelO2/app/Console/Commands/DeleteOldVisits.php b/GaelO2/app/Console/Commands/DeleteOldVisits.php index 5b4b844b2..3a1b70771 100644 --- a/GaelO2/app/Console/Commands/DeleteOldVisits.php +++ b/GaelO2/app/Console/Commands/DeleteOldVisits.php @@ -9,7 +9,6 @@ use Exception; use Illuminate\Console\Command; use Illuminate\Support\Facades\Log; -use Illuminate\Support\Facades\Storage; class DeleteOldVisits extends Command { @@ -61,7 +60,7 @@ public function handle( } $studyEntity = $this->study->withTrashed()->findOrFail($studyName); - + //Check that no ancillary study is remaining on this study $ancilariesStudies = $this->study->where('ancillary_of', $studyName)->get(); if ($ancilariesStudies->count() > 0) { @@ -69,19 +68,15 @@ public function handle( return 0; } - if($studyEntity['ancillary_of']){ + if ($studyEntity['ancillary_of']) { $this->error('Cannot be used for an ancillary study'); return 0; } if ($this->confirm('Warning : This CANNOT be undone, do you wish to continue?')) { - //TODO delete tracker doit etre specifique au visites supprimées - //$this->gaelODeleteRessourcesRepository->deleteTracker($studyEntity->name); - - //Get Visit ID of Original Study - //TODO Doit tenir compte de l'interval de temps depuis la creation - $visits = $this->getVisitsOfStudy($studyName); + //Get visits created more than 5 day + $visits = $this->getOlderVisitsOfStudy($studyName, date('Y.m.d', strtotime("-5 days"))); $visitIds = array_map(function ($visit) { return $visit['id']; @@ -89,30 +84,24 @@ public function handle( $this->gaelODeleteRessourcesRepository->deleteReviews($visitIds, $studyName); $this->gaelODeleteRessourcesRepository->deleteReviewStatus($visitIds, $studyName); + $this->gaelODeleteRessourcesRepository->deleteTrackerOfVisits($visitIds, $studyEntity->name); $dicomSeries = $this->getDicomSeriesOfVisits($visitIds); $orthancIdArray = array_map(function ($seriesId) { return $seriesId['orthanc_id']; }, $dicomSeries); - $this->line(implode(" ", $orthancIdArray)); - - $this->table( - ['orthanc_id'], - $dicomSeries - ); - $this->gaelODeleteRessourcesRepository->deleteDicomsSeries($visitIds); $this->gaelODeleteRessourcesRepository->deleteDicomsStudies($visitIds); $this->gaelODeleteRessourcesRepository->deleteVisits($visitIds); - //TODO Doit supprimer les patients que si il ne reste pas de visites... - //$this->gaelODeleteRessourcesRepository->deletePatient($studyName); - + //Remove patients with no visits + $this->gaelODeleteRessourcesRepository->deletePatientsWithNoVisits($studyName); + if ($this->option('deleteDicom') && $this->confirm('Found ' . sizeOf($orthancIdArray) . ' series to delete, do you want to continue ?')) { foreach ($orthancIdArray as $seriesOrthancId) { try { - $this->info('Deleting '.$seriesOrthancId); + $this->info('Deleting ' . $seriesOrthancId); $this->orthancService->deleteFromOrthanc('series', $seriesOrthancId); } catch (Exception $e) { Log::error($e->getMessage()); @@ -120,11 +109,6 @@ public function handle( } } - if ($this->option('deleteAssociatedFile') && $this->confirm('Going to delete associated file, do you want to continue ?')) { - //TODO doit supprimer que les associated file des visites deleted - //Storage::deleteDirectory($studyName); - } - $this->info('The command was successful !'); } @@ -139,12 +123,11 @@ private function getDicomSeriesOfVisits(array $visitIds) })->select('orthanc_id')->get()->toArray(); } - private function getVisitsOfStudy(string $studyName) + private function getOlderVisitsOfStudy(string $studyName, string $datelimit) { - return $this->visit->withTrashed()->with(['visitType', 'patient']) + return $this->visit->withTrashed()->whereDate('creation_date', '<=', $datelimit)->with(['visitType', 'patient']) ->whereHas('patient', function ($query) use ($studyName) { $query->where('study_name', $studyName); })->get(); } - } diff --git a/GaelO2/app/Console/Commands/GaelODeleteRessourcesRepository.php b/GaelO2/app/Console/Commands/GaelODeleteRessourcesRepository.php index def1ba600..4cdc4ccdd 100644 --- a/GaelO2/app/Console/Commands/GaelODeleteRessourcesRepository.php +++ b/GaelO2/app/Console/Commands/GaelODeleteRessourcesRepository.php @@ -71,6 +71,11 @@ public function deleteTracker(string $studyName) $this->tracker->where('study_name', $studyName)->delete(); } + public function deleteTrackerOfVisits(array $visitIds, string $studyName) + { + $this->tracker->where('study_name', $studyName)->whereIn('visit_id', $visitIds)->delete(); + } + public function deleteDicomsStudies(array $visitId) { return $this->dicomStudy->whereIn('visit_id', $visitId)->withTrashed()->forceDelete(); @@ -88,6 +93,11 @@ public function deletePatient(string $studyName) $this->patient->where('study_name', $studyName)->delete(); } + public function deletePatientsWithNoVisits(string $studyName) + { + $this->patient->where('study_name', $studyName)->doesntHave('visits')->delete(); + } + public function deleteReviews(array $visitIds, string $studyName) { $this->review->where('study_name', $studyName)->whereIn('visit_id', $visitIds)->withTrashed()->forceDelete(); From 1318c73ca72fd51cf89fb33a03d3b80a9722edfc Mon Sep 17 00:00:00 2001 From: salim kanoun Date: Thu, 28 Dec 2023 17:50:37 +0100 Subject: [PATCH 04/72] reading as stream to be tested --- GaelO2/app/GaelO/Adapters/FrameworkAdapter.php | 9 +++++++-- .../app/GaelO/Interfaces/Adapters/FrameworkInterface.php | 2 +- GaelO2/app/GaelO/Util.php | 7 +++++-- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/GaelO2/app/GaelO/Adapters/FrameworkAdapter.php b/GaelO2/app/GaelO/Adapters/FrameworkAdapter.php index 03d8b6c92..3109a0f43 100644 --- a/GaelO2/app/GaelO/Adapters/FrameworkAdapter.php +++ b/GaelO2/app/GaelO/Adapters/FrameworkAdapter.php @@ -46,9 +46,14 @@ public static function deleteFile(string $path): void Storage::delete($path); } - public static function getFile(string $path): string + public static function getFile(string $path, bool $asStream = false) { - $file = Storage::get($path); + if($asStream){ + $file = Storage::readStream($path); + }else{ + $file = Storage::get($path); + } + if($file === null){ throw new GaelOException("File not found in storage"); } diff --git a/GaelO2/app/GaelO/Interfaces/Adapters/FrameworkInterface.php b/GaelO2/app/GaelO/Interfaces/Adapters/FrameworkInterface.php index 2d9e224b0..ebd19dae1 100644 --- a/GaelO2/app/GaelO/Interfaces/Adapters/FrameworkInterface.php +++ b/GaelO2/app/GaelO/Interfaces/Adapters/FrameworkInterface.php @@ -21,7 +21,7 @@ public static function storeFile(string $path, $contents): void; public static function deleteFile(string $path): void; - public static function getFile(string $path): string; + public static function getFile(string $path, bool $asStream) :mixed; public static function sendResetPasswordLink(string $email): bool; diff --git a/GaelO2/app/GaelO/Util.php b/GaelO2/app/GaelO/Util.php index f33d287ae..5e9b1fc3b 100644 --- a/GaelO2/app/GaelO/Util.php +++ b/GaelO2/app/GaelO/Util.php @@ -52,8 +52,11 @@ public static function addStoredFilesInZip(ZipArchive $zip, ?string $path) foreach ($files as $file) { // Add current file to archive - $fileContent = FrameworkAdapter::getFile($file); - $zip->addFromString($file, $fileContent); + $tempraryFile = tmpfile(); + $tempraryFilePath = stream_get_meta_data($tempraryFile)['uri']; + $fileContent = FrameworkAdapter::getFile($file, true); + stream_copy_to_stream($fileContent, $tempraryFile); + $zip->addFile($tempraryFilePath, $file); } } From 1d877004dbb3b2a1cef32e139163673263a6e317 Mon Sep 17 00:00:00 2001 From: salim kanoun Date: Thu, 28 Dec 2023 17:55:18 +0100 Subject: [PATCH 05/72] fixes --- GaelO2/app/GaelO/Adapters/FrameworkAdapter.php | 10 +++++----- .../GaelO/Interfaces/Adapters/FrameworkInterface.php | 2 +- GaelO2/app/GaelO/Util.php | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/GaelO2/app/GaelO/Adapters/FrameworkAdapter.php b/GaelO2/app/GaelO/Adapters/FrameworkAdapter.php index 3109a0f43..72737fd25 100644 --- a/GaelO2/app/GaelO/Adapters/FrameworkAdapter.php +++ b/GaelO2/app/GaelO/Adapters/FrameworkAdapter.php @@ -46,15 +46,15 @@ public static function deleteFile(string $path): void Storage::delete($path); } - public static function getFile(string $path, bool $asStream = false) + public static function getFile(string $path, bool $asStream = false): mixed { - if($asStream){ + if ($asStream) { $file = Storage::readStream($path); - }else{ + } else { $file = Storage::get($path); } - - if($file === null){ + + if ($file === null) { throw new GaelOException("File not found in storage"); } return $file; diff --git a/GaelO2/app/GaelO/Interfaces/Adapters/FrameworkInterface.php b/GaelO2/app/GaelO/Interfaces/Adapters/FrameworkInterface.php index ebd19dae1..4c7471ce1 100644 --- a/GaelO2/app/GaelO/Interfaces/Adapters/FrameworkInterface.php +++ b/GaelO2/app/GaelO/Interfaces/Adapters/FrameworkInterface.php @@ -21,7 +21,7 @@ public static function storeFile(string $path, $contents): void; public static function deleteFile(string $path): void; - public static function getFile(string $path, bool $asStream) :mixed; + public static function getFile(string $path, bool $asStream): mixed; public static function sendResetPasswordLink(string $email): bool; diff --git a/GaelO2/app/GaelO/Util.php b/GaelO2/app/GaelO/Util.php index 5e9b1fc3b..a5bac2193 100644 --- a/GaelO2/app/GaelO/Util.php +++ b/GaelO2/app/GaelO/Util.php @@ -13,7 +13,7 @@ class Util { - public static function fillObject(array $dataToExtract, object $dataToFill) :void + public static function fillObject(array $dataToExtract, object $dataToFill): void { //Get Expected properties awaited in DTO Request $reflect = new ReflectionClass($dataToFill); @@ -53,7 +53,7 @@ public static function addStoredFilesInZip(ZipArchive $zip, ?string $path) foreach ($files as $file) { // Add current file to archive $tempraryFile = tmpfile(); - $tempraryFilePath = stream_get_meta_data($tempraryFile)['uri']; + $tempraryFilePath = stream_get_meta_data($tempraryFile)['uri']; $fileContent = FrameworkAdapter::getFile($file, true); stream_copy_to_stream($fileContent, $tempraryFile); $zip->addFile($tempraryFilePath, $file); @@ -134,7 +134,7 @@ public static function isSemanticVersioning(string $version): bool return preg_match('/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)?$/', $version); } - public static function isUrlSafeString(string $value) :bool + public static function isUrlSafeString(string $value): bool { return preg_match('/^[a-zA-Z0-9_-]*$/', $value); } From d54556361502d97dec00736fbd0f5fa54b239924 Mon Sep 17 00:00:00 2001 From: salim kanoun Date: Thu, 28 Dec 2023 18:30:44 +0100 Subject: [PATCH 06/72] fix download of large file --- .../app/GaelO/Services/ExportStudyService.php | 3 +-- .../ExportDatabase/ExportDatabase.php | 3 +-- GaelO2/app/GaelO/Util.php | 19 +++++++++++++------ 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/GaelO2/app/GaelO/Services/ExportStudyService.php b/GaelO2/app/GaelO/Services/ExportStudyService.php index 313dbf417..69a92f50b 100644 --- a/GaelO2/app/GaelO/Services/ExportStudyService.php +++ b/GaelO2/app/GaelO/Services/ExportStudyService.php @@ -311,8 +311,7 @@ public function exportAssociatedFiles(): void //Add a file to create zip $zip->addFromString('Readme', 'Folder Containing associated files to study'); //send stored file for this study - Util::addStoredFilesInZip($zip, $this->studyName); - $zip->close(); + Util::addStoredFilesInZipAndClose($zip, $this->studyName); $exporFileResult = new ExportFileResults(); $exporFileResult->addExportFile(ExportDataResults::EXPORT_TYPE_ZIP, $tempZip); diff --git a/GaelO2/app/GaelO/UseCases/ExportDatabase/ExportDatabase.php b/GaelO2/app/GaelO/UseCases/ExportDatabase/ExportDatabase.php index c28de6fef..8468fd694 100644 --- a/GaelO2/app/GaelO/UseCases/ExportDatabase/ExportDatabase.php +++ b/GaelO2/app/GaelO/UseCases/ExportDatabase/ExportDatabase.php @@ -38,8 +38,7 @@ public function execute(ExportDatabaseRequest $exportDatabaseRequest, ExportData $date = Date('Ymd_His'); $zip->addFile($filePathSql, "export_database_$date.sql"); - Util::addStoredFilesInZip($zip, null); - $zip->close(); + Util::addStoredFilesInZipAndClose($zip, null); //Unlick after lock released by zip close unlink($filePathSql); diff --git a/GaelO2/app/GaelO/Util.php b/GaelO2/app/GaelO/Util.php index a5bac2193..913d97053 100644 --- a/GaelO2/app/GaelO/Util.php +++ b/GaelO2/app/GaelO/Util.php @@ -5,6 +5,7 @@ use App\GaelO\Adapters\FrameworkAdapter; use Carbon\Carbon; use FilesystemIterator; +use Illuminate\Support\Facades\Log; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; use ReflectionClass; @@ -45,19 +46,25 @@ public static function camelCaseToSnakeCase(string $string, string $us = "_"): s )); } - public static function addStoredFilesInZip(ZipArchive $zip, ?string $path) + public static function addStoredFilesInZipAndClose(ZipArchive $zip, ?string $path) { $files = FrameworkAdapter::getStoredFiles($path); - + $temporaryFilesToDelete=[]; foreach ($files as $file) { - // Add current file to archive - $tempraryFile = tmpfile(); - $tempraryFilePath = stream_get_meta_data($tempraryFile)['uri']; + // Add current file to archive using data as stream to prevent running out memory for large files + $tempraryFilePath = tempnam(ini_get('upload_tmp_dir'), 'TMPEXP_'); + $temporaryFilesToDelete[] = $tempraryFilePath; $fileContent = FrameworkAdapter::getFile($file, true); - stream_copy_to_stream($fileContent, $tempraryFile); + stream_copy_to_stream($fileContent, fopen($tempraryFilePath, 'w')); $zip->addFile($tempraryFilePath, $file); } + //Close to build the zip as the operation is async + $zip->close(); + //Delete all temporary files + foreach($temporaryFilesToDelete as $temp){ + unlink($temp); + } } public static function recursiveDirectoryDelete(string $directory) From 23626774e84daa8810da41dc9dbc294a86adf706 Mon Sep 17 00:00:00 2001 From: salim kanoun Date: Fri, 29 Dec 2023 10:07:27 +0100 Subject: [PATCH 07/72] remove unneded import --- GaelO2/app/GaelO/Util.php | 1 - 1 file changed, 1 deletion(-) diff --git a/GaelO2/app/GaelO/Util.php b/GaelO2/app/GaelO/Util.php index 913d97053..fad808ffc 100644 --- a/GaelO2/app/GaelO/Util.php +++ b/GaelO2/app/GaelO/Util.php @@ -5,7 +5,6 @@ use App\GaelO\Adapters\FrameworkAdapter; use Carbon\Carbon; use FilesystemIterator; -use Illuminate\Support\Facades\Log; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; use ReflectionClass; From f095f3de3897e0fdb11dbe649741b5424865d26d Mon Sep 17 00:00:00 2001 From: salim kanoun Date: Fri, 29 Dec 2023 10:25:16 +0100 Subject: [PATCH 08/72] max execution time for export apis --- GaelO2/app/GaelO/UseCases/ExportDatabase/ExportDatabase.php | 3 ++- .../app/GaelO/UseCases/ExportStudyData/ExportStudyData.php | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/GaelO2/app/GaelO/UseCases/ExportDatabase/ExportDatabase.php b/GaelO2/app/GaelO/UseCases/ExportDatabase/ExportDatabase.php index 8468fd694..111ef573c 100644 --- a/GaelO2/app/GaelO/UseCases/ExportDatabase/ExportDatabase.php +++ b/GaelO2/app/GaelO/UseCases/ExportDatabase/ExportDatabase.php @@ -27,7 +27,8 @@ public function execute(ExportDatabaseRequest $exportDatabaseRequest, ExportData try { $this->checkAuthorization($exportDatabaseRequest->currentUserId); - + //Operation might be long, set max execution time to 30 minutes + set_time_limit(1800); $zip = new ZipArchive(); $tempZip = tempnam(ini_get('upload_tmp_dir'), 'TMPZIPDB_'); $zip->open($tempZip, ZipArchive::OVERWRITE); diff --git a/GaelO2/app/GaelO/UseCases/ExportStudyData/ExportStudyData.php b/GaelO2/app/GaelO/UseCases/ExportStudyData/ExportStudyData.php index 06a8b3cbd..ab2258534 100644 --- a/GaelO2/app/GaelO/UseCases/ExportStudyData/ExportStudyData.php +++ b/GaelO2/app/GaelO/UseCases/ExportStudyData/ExportStudyData.php @@ -30,10 +30,11 @@ public function execute(ExportStudyDataRequest $exportStudyDataRequest, ExportSt $this->checkAuthorization($exportStudyDataRequest->currentUserId, $studyName); - $this->exportStudyService->setStudyName($studyName); + //Operation might be long, set max execution time to 30 minutes + set_time_limit(1800); + $this->exportStudyService->setStudyName($studyName); $this->exportStudyService->exportAll(); - $exportResults = $this->exportStudyService->getExportStudyResult(); $exportStudyDataResponse->zipFile = $exportResults->getResultsAsZip(); From 302e1fa5c1b934d666b744054b9960828aa2d6a7 Mon Sep 17 00:00:00 2001 From: Salim Kanoun Date: Sat, 13 Jan 2024 17:57:33 +0100 Subject: [PATCH 09/72] migrate qc report to processing backend --- .../GaelOProcessingService.php | 13 ++++++ .../TmtvProcessingService.php | 16 ++------ GaelO2/app/GaelO/Services/OrthancService.php | 10 +++++ GaelO2/app/Jobs/JobQcReport.php | 4 +- GaelO2/app/Jobs/QcReport/SeriesReport.php | 41 +++++++++++-------- 5 files changed, 54 insertions(+), 30 deletions(-) diff --git a/GaelO2/app/GaelO/Services/GaelOProcessingService/GaelOProcessingService.php b/GaelO2/app/GaelO/Services/GaelOProcessingService/GaelOProcessingService.php index 00393f282..2d07f6882 100644 --- a/GaelO2/app/GaelO/Services/GaelOProcessingService/GaelOProcessingService.php +++ b/GaelO2/app/GaelO/Services/GaelOProcessingService/GaelOProcessingService.php @@ -49,6 +49,9 @@ public function executeInference(string $modelName, array $payload) return $request->getJsonBody(); } + /** + * Return gif + */ public function createMIPForSeries(string $seriesId, array $payload = []): string { $downloadedFilePath = tempnam(ini_get('upload_tmp_dir'), 'TMP_Inference_'); @@ -57,6 +60,16 @@ public function createMIPForSeries(string $seriesId, array $payload = []): strin return $downloadedFilePath; } + /** + * return png + */ + public function createMosaicForSeries(string $seriesId, array $payload = ["min" => null, "max" => null, "cols" => 5, "nbImages" => 20, "width" => 512, "height" => 512, "orientation"=>"LPI"]): string + { + $downloadedFilePath = tempnam(ini_get('upload_tmp_dir'), 'TMP_Inference_'); + $this->httpClientInterface->requestStreamResponseToFile('POST', "/series/" . $seriesId . "/mosaic", $downloadedFilePath, ['content-Type' => 'application/json'], $payload); + return $downloadedFilePath; + } + public function getNiftiMask(string $maskId): string { $downloadedFilePath = tempnam(ini_get('upload_tmp_dir'), 'TMP_Inference_'); diff --git a/GaelO2/app/GaelO/Services/GaelOProcessingService/TmtvProcessingService.php b/GaelO2/app/GaelO/Services/GaelOProcessingService/TmtvProcessingService.php index a94cb6d33..c63265676 100644 --- a/GaelO2/app/GaelO/Services/GaelOProcessingService/TmtvProcessingService.php +++ b/GaelO2/app/GaelO/Services/GaelOProcessingService/TmtvProcessingService.php @@ -33,8 +33,10 @@ public function __construct( public function runInference(): MaskProcessingService { - $this->sendDicomToProcessing($this->ptOrthancSeriesId); - $this->sendDicomToProcessing($this->ctOrthancSeriesId); + $this->orthancService->sendDicomToProcessing($this->ptOrthancSeriesId, $this->gaelOProcessingService); + $this->addCreatedRessource('dicoms', $this->ptOrthancSeriesId); + $this->orthancService->sendDicomToProcessing($this->ctOrthancSeriesId, $this->gaelOProcessingService); + $this->addCreatedRessource('dicoms', $this->ptOrthancSeriesId); $idPT = $this->gaelOProcessingService->createSeriesFromOrthanc($this->ptOrthancSeriesId, true, true); $this->addCreatedRessource('series', $idPT); @@ -56,16 +58,6 @@ public function runInference(): MaskProcessingService } - - protected function sendDicomToProcessing(string $orthancSeriesIdPt) - { - $temporaryZipDicom = tempnam(ini_get('upload_tmp_dir'), 'TMP_Inference_'); - $this->orthancService->getZipStreamToFile([$orthancSeriesIdPt], $temporaryZipDicom); - $this->gaelOProcessingService->createDicom($temporaryZipDicom); - $this->addCreatedRessource('dicoms', $orthancSeriesIdPt); - unlink($temporaryZipDicom); - } - public function loadPetAndCtSeriesOrthancIdsFromVisit($visitId): void { $dicomStudyEntity = $this->dicomStudyRepositoryInterface->getDicomsDataFromVisit($visitId, false, false); diff --git a/GaelO2/app/GaelO/Services/OrthancService.php b/GaelO2/app/GaelO/Services/OrthancService.php index 904b4ebaa..bfd0e9b64 100644 --- a/GaelO2/app/GaelO/Services/OrthancService.php +++ b/GaelO2/app/GaelO/Services/OrthancService.php @@ -8,6 +8,7 @@ use App\GaelO\Exceptions\GaelOException; use App\GaelO\Interfaces\Adapters\FrameworkInterface; use App\GaelO\Interfaces\Adapters\HttpClientInterface; +use App\GaelO\Services\GaelOProcessingService\GaelOProcessingService; use App\GaelO\Services\StoreObjects\OrthancMetaData; use App\GaelO\Services\StoreObjects\TagAnon; use App\GaelO\Services\StoreObjects\OrthancStudy; @@ -472,4 +473,13 @@ public function getInstancePreview(string $instanceOrthancID): string $this->httpClientInterface->requestStreamResponseToFile('GET', '/instances/' . $instanceOrthancID . '/preview?returnUnsupportedImage', $downloadedFilePath, []); return $downloadedFilePath; } + + public function sendDicomToProcessing(string $orthancSeriesIdPt, GaelOProcessingService $gaelOProcessingService) + { + $temporaryZipDicom = tempnam(ini_get('upload_tmp_dir'), 'TMP_Inference_'); + $this->getZipStreamToFile([$orthancSeriesIdPt], $temporaryZipDicom); + $gaelOProcessingService->createDicom($temporaryZipDicom); + unlink($temporaryZipDicom); + } + } diff --git a/GaelO2/app/Jobs/JobQcReport.php b/GaelO2/app/Jobs/JobQcReport.php index 4ac8f2ef0..231d20b09 100644 --- a/GaelO2/app/Jobs/JobQcReport.php +++ b/GaelO2/app/Jobs/JobQcReport.php @@ -9,6 +9,7 @@ use App\GaelO\Interfaces\Repositories\ReviewRepositoryInterface; use App\GaelO\Interfaces\Repositories\UserRepositoryInterface; use App\GaelO\Interfaces\Repositories\VisitRepositoryInterface; +use App\GaelO\Services\GaelOProcessingService\GaelOProcessingService; use App\GaelO\Services\MailServices; use App\GaelO\Services\OrthancService; use App\Jobs\QcReport\InstanceReport; @@ -57,6 +58,7 @@ public function handle( DicomStudyRepositoryInterface $dicomStudyRepositoryInterface, MailServices $mailServices, OrthancService $orthancService, + GaelOProcessingService $gaelOProcessingService, ReviewRepositoryInterface $reviewRepositoryInterface ) { $this->orthancService = $orthancService; @@ -115,7 +117,7 @@ public function handle( $seriesReport->fillData($seriesSharedTags); $seriesReport->setInstanceReport($instanceReport); - $seriesReport->loadSeriesPreview($this->orthancService); + $seriesReport->loadSeriesPreview($this->orthancService, $gaelOProcessingService); $seriesReports[] = $seriesReport; } catch (Throwable $t) { diff --git a/GaelO2/app/Jobs/QcReport/SeriesReport.php b/GaelO2/app/Jobs/QcReport/SeriesReport.php index 05a835524..6195f6e8b 100644 --- a/GaelO2/app/Jobs/QcReport/SeriesReport.php +++ b/GaelO2/app/Jobs/QcReport/SeriesReport.php @@ -4,6 +4,7 @@ use App\GaelO\DicomUtils; use App\GaelO\Exceptions\GaelOException; +use App\GaelO\Services\GaelOProcessingService\GaelOProcessingService; use App\GaelO\Services\OrthancService; use App\GaelO\Services\StoreObjects\OrthancMetaData; use Throwable; @@ -136,29 +137,35 @@ private function getPreviewType(): ImageType return ImageType::DEFAULT; } - public function loadSeriesPreview(OrthancService $orthancService): void + public function loadSeriesPreview(OrthancService $orthancService, GaelOProcessingService $gaelOProcessingService): void { $imageType = $this->getPreviewType(); $imagePath = []; + try { - switch ($imageType) { - case ImageType::MIP: - //Mosaic for now as mip need significant computation and memory backend - $imagePath[] = $orthancService->getMosaic('series', $this->seriesOrthancId); - break; - case ImageType::MOSAIC: - $imagePath[] = $orthancService->getMosaic('series', $this->seriesOrthancId); - break; - case ImageType::MULTIFRAME: - $imagePath = array_map(function ($instanceOrthancId) use ($orthancService) { - return $orthancService->getMosaic('instances', $instanceOrthancId); - }, $this->orthancInstanceIds); - break; - case ImageType::DEFAULT: - $imagePath[] = $orthancService->getInstancePreview($this->orthancInstanceIds[0]); - break; + if ($imageType === ImageType::DEFAULT) { + $imagePath[] = $orthancService->getInstancePreview($this->orthancInstanceIds[0]); + } else { + $isPet = $this->modality == 'PT'; + $payload = $isPet ? ['min' => 0, 'max' => 5] : []; + $orthancService->sendDicomToProcessing($this->seriesOrthancId, $gaelOProcessingService); + $processingSeriesId = $gaelOProcessingService->createSeriesFromOrthanc($this->seriesOrthancId, $isPet, $isPet); + switch ($imageType) { + case ImageType::MIP: + //Mosaic for now as mip need significant computation and memory backend + $imagePath[] = $gaelOProcessingService->createMIPForSeries($processingSeriesId, $payload); + break; + case ImageType::MOSAIC: + $imagePath[] = $gaelOProcessingService->createMosaicForSeries($processingSeriesId, $payload); + break; + case ImageType::MULTIFRAME: + $imagePath = array_map(function ($instanceOrthancId) use ($gaelOProcessingService) { + return $gaelOProcessingService->createMosaicForSeries('instances', $instanceOrthancId); + }, $this->orthancInstanceIds); + break; + } } } catch (Throwable $t) { } From bec47caaa4666d4c12ea4849b35c5d02977393e2 Mon Sep 17 00:00:00 2001 From: Salim Kanoun Date: Sat, 13 Jan 2024 18:06:53 +0100 Subject: [PATCH 10/72] migrate qc to processing server --- GaelO2/app/Jobs/JobQcReport.php | 2 +- GaelO2/app/Jobs/QcReport/SeriesReport.php | 3 ++- laravel-worker.conf | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/GaelO2/app/Jobs/JobQcReport.php b/GaelO2/app/Jobs/JobQcReport.php index 231d20b09..85925f06d 100644 --- a/GaelO2/app/Jobs/JobQcReport.php +++ b/GaelO2/app/Jobs/JobQcReport.php @@ -42,7 +42,7 @@ class JobQcReport implements ShouldQueue, ShouldBeUnique */ public function __construct(int $visitId) { - $this->onQueue('auto-qc'); + $this->onQueue('processing'); $this->visitId = $visitId; } diff --git a/GaelO2/app/Jobs/QcReport/SeriesReport.php b/GaelO2/app/Jobs/QcReport/SeriesReport.php index 6195f6e8b..d91ad3dfc 100644 --- a/GaelO2/app/Jobs/QcReport/SeriesReport.php +++ b/GaelO2/app/Jobs/QcReport/SeriesReport.php @@ -143,7 +143,6 @@ public function loadSeriesPreview(OrthancService $orthancService, GaelOProcessin $imagePath = []; - try { if ($imageType === ImageType::DEFAULT) { $imagePath[] = $orthancService->getInstancePreview($this->orthancInstanceIds[0]); @@ -166,6 +165,8 @@ public function loadSeriesPreview(OrthancService $orthancService, GaelOProcessin }, $this->orthancInstanceIds); break; } + $gaelOProcessingService->deleteRessource('series', $processingSeriesId); + $gaelOProcessingService->deleteRessource('dicoms', $this->seriesOrthancId); } } catch (Throwable $t) { } diff --git a/laravel-worker.conf b/laravel-worker.conf index a0bfadccb..ad2fdccd3 100644 --- a/laravel-worker.conf +++ b/laravel-worker.conf @@ -12,7 +12,7 @@ serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL for a unix socket [program:laravel-worker] process_name=%(program_name)s_%(process_num)02d -command=php /var/www/html/artisan queue:work --tries=3 --sleep=3 --max-time=3600 --queue=default,auto-qc,processing +command=php /var/www/html/artisan queue:work --tries=3 --sleep=3 --max-time=3600 --queue=default,processing autostart=true autorestart=true stopasgroup=true From 00c86119c2f42516ee2f30530cc0a824b7b53b3f Mon Sep 17 00:00:00 2001 From: Salim Kanoun Date: Mon, 15 Jan 2024 00:34:15 +0100 Subject: [PATCH 11/72] qc report using file caching --- GaelO2/.env.example | 2 + .../app/GaelO/Adapters/AzureCacheAdapter.php | 100 ++++++++ .../app/GaelO/Adapters/FileCacheAdapter.php | 28 +++ .../Interfaces/Adapters/CacheInterface.php | 10 + .../app/GaelO/Services/FileCacheService.php | 45 ++++ .../GaelOProcessingService.php | 10 +- GaelO2/app/GaelO/Services/MailServices.php | 14 +- .../Services/StoreObjects/OrthancMetaData.php | 5 + .../GetDicomSeriesMetadata.php | 70 ++++++ .../GetDicomSeriesMetadataRequest.php | 10 + .../GetDicomSeriesMetadataResponse.php | 10 + .../GetDicomSeriesPreview.php | 75 ++++++ .../GetDicomSeriesPreviewRequest.php | 11 + .../GetDicomSeriesPreviewResponse.php | 11 + .../GetDicomStudyMetadata.php | 70 ++++++ .../GetDicomStudyMetadataRequest.php | 10 + .../GetDicomStudyMetadataResponse.php | 10 + .../views/mails/mail_qc_report.blade.php | 113 +-------- .../mails/mail_qc_report_buttons.blade.php | 217 +++++++++++------- ...mail_qc_report_investigator_form.blade.php | 102 -------- .../mails/mail_qc_report_series.blade.php | 151 ------------ .../mails/mail_qc_report_study.blade.php | 214 ----------------- .../views/mails/mjml/qc_report_buttons.mjml | 7 +- .../mjml/qc_report_investigator_form.mjml | 36 --- .../views/mails/mjml/qc_report_series.mjml | 49 ---- .../views/mails/mjml/qc_report_study.mjml | 65 ------ .../app/Http/Controllers/DicomController.php | 52 +++++ .../app/Http/Controllers/ReviewController.php | 1 - GaelO2/app/Jobs/JobQcReport.php | 46 ++-- GaelO2/app/Jobs/QcReport/SeriesReport.php | 22 +- GaelO2/app/Jobs/QcReport/VisitReport.php | 8 - GaelO2/app/Providers/AdapterProvider.php | 16 +- GaelO2/config/cache.php | 8 + GaelO2/routes/api.php | 4 +- .../Feature/TestDicoms/DicomMetadataTest.php | 77 +++++++ .../TestAdapters/FileCacheAdapterTest.php | 39 ++++ README.md | 3 - 37 files changed, 845 insertions(+), 876 deletions(-) create mode 100644 GaelO2/app/GaelO/Adapters/AzureCacheAdapter.php create mode 100644 GaelO2/app/GaelO/Adapters/FileCacheAdapter.php create mode 100644 GaelO2/app/GaelO/Interfaces/Adapters/CacheInterface.php create mode 100644 GaelO2/app/GaelO/Services/FileCacheService.php create mode 100644 GaelO2/app/GaelO/UseCases/GetDicomSeriesMetadata/GetDicomSeriesMetadata.php create mode 100644 GaelO2/app/GaelO/UseCases/GetDicomSeriesMetadata/GetDicomSeriesMetadataRequest.php create mode 100644 GaelO2/app/GaelO/UseCases/GetDicomSeriesMetadata/GetDicomSeriesMetadataResponse.php create mode 100644 GaelO2/app/GaelO/UseCases/GetDicomSeriesPreview/GetDicomSeriesPreview.php create mode 100644 GaelO2/app/GaelO/UseCases/GetDicomSeriesPreview/GetDicomSeriesPreviewRequest.php create mode 100644 GaelO2/app/GaelO/UseCases/GetDicomSeriesPreview/GetDicomSeriesPreviewResponse.php create mode 100644 GaelO2/app/GaelO/UseCases/GetDicomStudyMetadata/GetDicomStudyMetadata.php create mode 100644 GaelO2/app/GaelO/UseCases/GetDicomStudyMetadata/GetDicomStudyMetadataRequest.php create mode 100644 GaelO2/app/GaelO/UseCases/GetDicomStudyMetadata/GetDicomStudyMetadataResponse.php delete mode 100644 GaelO2/app/GaelO/views/mails/mail_qc_report_investigator_form.blade.php delete mode 100644 GaelO2/app/GaelO/views/mails/mail_qc_report_series.blade.php delete mode 100644 GaelO2/app/GaelO/views/mails/mail_qc_report_study.blade.php delete mode 100644 GaelO2/app/GaelO/views/mails/mjml/qc_report_investigator_form.mjml delete mode 100644 GaelO2/app/GaelO/views/mails/mjml/qc_report_series.mjml delete mode 100644 GaelO2/app/GaelO/views/mails/mjml/qc_report_study.mjml create mode 100644 GaelO2/tests/Feature/TestDicoms/DicomMetadataTest.php create mode 100644 GaelO2/tests/Unit/TestAdapters/FileCacheAdapterTest.php diff --git a/GaelO2/.env.example b/GaelO2/.env.example index caddf52d1..f61ba626c 100644 --- a/GaelO2/.env.example +++ b/GaelO2/.env.example @@ -40,6 +40,8 @@ FILESYSTEM_DISK='local' AZURE_BLOB_DSN='' AZURE_CONTAINER_NAME='' AZURE_BLOB_PREFIX='' +AZURE_FILE_CACHE_CONTAINER_NAME='' +FILE_CACHE_DRIVER='local' BROADCAST_DRIVER=log CACHE_DRIVER=file diff --git a/GaelO2/app/GaelO/Adapters/AzureCacheAdapter.php b/GaelO2/app/GaelO/Adapters/AzureCacheAdapter.php new file mode 100644 index 000000000..d2c23feec --- /dev/null +++ b/GaelO2/app/GaelO/Adapters/AzureCacheAdapter.php @@ -0,0 +1,100 @@ +fileSystem = $fileSystem; + } + + private function getPath(string $key) + { + $hash = sha1($key); + return $this->rootDirectory . '/'. $hash; + } + + public function get($key) + { + $path = $this->getPath($key); + + if ($this->fileSystem->fileExists($path)) { + throw new GaelONotFoundException("File doesn't exist in azure cache"); + } + + $this->fileSystem->read($path); + } + + public function many(array $keys) + { + $answer = []; + foreach ($keys as $key) { + $answer[$key] = $this->get($key); + } + return $answer; + } + + public function put($key, $value, $seconds) + { + $path = $this->getPath($key); + + $this->fileSystem->write($path, $value); + return true; + } + + public function putMany(array $values, $seconds) + { + foreach ($values as $key => $value) { + $this->put($key, $value, $seconds); + } + return true; + } + + public function increment($key, $value = 1) + { + throw new GaelOException('No implementation for increment in Azure Cache'); + } + + public function decrement($key, $value = 1) + { + throw new GaelOException('No implementation for decrement in Azure Cache'); + } + + public function forever($key, $value) + { + $this->put($key, $value, 0); + } + + public function forget($key) + { + $path = $this->getPath($key); + $this->fileSystem->delete($path); + } + + public function flush() + { + $files = $this->fileSystem->listContents($this->rootDirectory); + foreach ($files as $file) { + $this->fileSystem->delete($file->path()); + } + } + + public function getPrefix() + { + return ''; + } +} diff --git a/GaelO2/app/GaelO/Adapters/FileCacheAdapter.php b/GaelO2/app/GaelO/Adapters/FileCacheAdapter.php new file mode 100644 index 000000000..1ae6d75f2 --- /dev/null +++ b/GaelO2/app/GaelO/Adapters/FileCacheAdapter.php @@ -0,0 +1,28 @@ +get($key); + } + + public function store(string $key, $value): bool + { + Cache::store(Config::get('cache.file-cache'))->put($key, $value); + return true; + } + + public function delete(string $key): bool + { + Cache::store(Config::get('cache.file-cache'))->forget($key); + return true; + } +} diff --git a/GaelO2/app/GaelO/Interfaces/Adapters/CacheInterface.php b/GaelO2/app/GaelO/Interfaces/Adapters/CacheInterface.php new file mode 100644 index 000000000..3af5e7181 --- /dev/null +++ b/GaelO2/app/GaelO/Interfaces/Adapters/CacheInterface.php @@ -0,0 +1,10 @@ +fileCacheAdapter = $fileCacheAdapter; + } + + public function getSeriesPreview(string $seriesInstanceUID, int $index) + { + return $this->fileCacheAdapter->get('preview-' . $seriesInstanceUID . '-' . $index); + } + + public function storeSeriesPreview(string $seriesInstanceUID, int $index, $value) + { + return $this->fileCacheAdapter->store('preview-' . $seriesInstanceUID . '-' . $index, $value); + } + + public function deleteSeriesPreview(string $seriesInstanceUID, int $index) + { + return $this->fileCacheAdapter->delete('preview-' . $seriesInstanceUID . '-' . $index); + } + + public function storeDicomMetadata(string $uid, $value) + { + return $this->fileCacheAdapter->store('metadata-' . $uid, $value); + } + + public function getDicomMetadata(string $uid) + { + return $this->fileCacheAdapter->get('metadata-' . $uid); + } + + public function deleteDicomMetadata(string $uid) + { + return $this->fileCacheAdapter->delete('metadata-' . $uid); + } +} diff --git a/GaelO2/app/GaelO/Services/GaelOProcessingService/GaelOProcessingService.php b/GaelO2/app/GaelO/Services/GaelOProcessingService/GaelOProcessingService.php index 2d07f6882..129d3dc2a 100644 --- a/GaelO2/app/GaelO/Services/GaelOProcessingService/GaelOProcessingService.php +++ b/GaelO2/app/GaelO/Services/GaelOProcessingService/GaelOProcessingService.php @@ -52,11 +52,11 @@ public function executeInference(string $modelName, array $payload) /** * Return gif */ - public function createMIPForSeries(string $seriesId, array $payload = []): string + public function createMIPForSeries(string $seriesId, array $payload = ['orientation' => 'LPI']): string { $downloadedFilePath = tempnam(ini_get('upload_tmp_dir'), 'TMP_Inference_'); - $this->httpClientInterface->requestStreamResponseToFile('POST', "/series/" . $seriesId . "/mip", $downloadedFilePath, ['content-Type' => 'application/json'], $payload); + $this->httpClientInterface->requestStreamResponseToFile('POST', "/series/" . $seriesId . "/mip", $downloadedFilePath, ['Content-Type' => 'application/json'], $payload); return $downloadedFilePath; } @@ -66,7 +66,7 @@ public function createMIPForSeries(string $seriesId, array $payload = []): strin public function createMosaicForSeries(string $seriesId, array $payload = ["min" => null, "max" => null, "cols" => 5, "nbImages" => 20, "width" => 512, "height" => 512, "orientation"=>"LPI"]): string { $downloadedFilePath = tempnam(ini_get('upload_tmp_dir'), 'TMP_Inference_'); - $this->httpClientInterface->requestStreamResponseToFile('POST', "/series/" . $seriesId . "/mosaic", $downloadedFilePath, ['content-Type' => 'application/json'], $payload); + $this->httpClientInterface->requestStreamResponseToFile('POST', "/series/" . $seriesId . "/mosaic", $downloadedFilePath, ['Content-Type' => 'application/json'], $payload); return $downloadedFilePath; } @@ -74,7 +74,7 @@ public function getNiftiMask(string $maskId): string { $downloadedFilePath = tempnam(ini_get('upload_tmp_dir'), 'TMP_Inference_'); - $this->httpClientInterface->requestStreamResponseToFile('GET', "/masks/" . $maskId . "/file", $downloadedFilePath, ['content-Type' => 'application/json'], []); + $this->httpClientInterface->requestStreamResponseToFile('GET', "/masks/" . $maskId . "/file", $downloadedFilePath, ['Content-Type' => 'application/json'], []); return $downloadedFilePath; } @@ -82,7 +82,7 @@ public function getNiftiSeries(string $imageId): string { $downloadedFilePath = tempnam(ini_get('upload_tmp_dir'), 'TMP_Inference_'); - $this->httpClientInterface->requestStreamResponseToFile('GET', "/series/" . $imageId . "/file", $downloadedFilePath, ['content-Type' => 'application/json'], []); + $this->httpClientInterface->requestStreamResponseToFile('GET', "/series/" . $imageId . "/file", $downloadedFilePath, ['Content-Type' => 'application/json'], []); return $downloadedFilePath; } diff --git a/GaelO2/app/GaelO/Services/MailServices.php b/GaelO2/app/GaelO/Services/MailServices.php index d01915dcd..f5d3a7823 100644 --- a/GaelO2/app/GaelO/Services/MailServices.php +++ b/GaelO2/app/GaelO/Services/MailServices.php @@ -590,24 +590,14 @@ public function sendMagicLink(int $targetedUserId, string $studyName, string $ur $this->mailInterface->send(); } - public function sendQcReport(string $studyName, string $visitType, string $patientCode, array $studyInfo, array $seriesInfo, string $magicLinkAccepted, string $magicLinkRefused, string $controllerEmail) + public function sendQcReport(string $studyName, string $visitType, string $patientCode, string $magicLink, string $controllerEmail) { - $isVisitDateExpected = true; - - if (isset($studyInfo['visitDate']) && isset($studyInfo['studyDetails']['Study Date'])) { - $isVisitDateExpected = ($studyInfo['visitDate'] === $studyInfo['studyDetails']['Study Date']); - } - $parameters = [ 'study' => $studyName, 'visitType' => $visitType, 'patientCode' => $patientCode, - 'studyInfo' => $studyInfo, - 'seriesInfo' => $seriesInfo, - 'warningVisitDate' => $isVisitDateExpected === false, - 'magicLinkAccepted' => $magicLinkAccepted, - 'magicLinkRefused' => $magicLinkRefused + 'magicLink' => $magicLink, ]; $this->mailInterface->setTo([$controllerEmail]); diff --git a/GaelO2/app/GaelO/Services/StoreObjects/OrthancMetaData.php b/GaelO2/app/GaelO/Services/StoreObjects/OrthancMetaData.php index ff126fb0c..a3d78899a 100644 --- a/GaelO2/app/GaelO/Services/StoreObjects/OrthancMetaData.php +++ b/GaelO2/app/GaelO/Services/StoreObjects/OrthancMetaData.php @@ -260,4 +260,9 @@ public function getImageID() : ?string { return $this->getMetaDataValueFromCode('0054,0400'); } + + public function getSeriesInstanceUID() : ?string + { + return $this->getMetaDataValueFromCode('0020,000e'); + } } diff --git a/GaelO2/app/GaelO/UseCases/GetDicomSeriesMetadata/GetDicomSeriesMetadata.php b/GaelO2/app/GaelO/UseCases/GetDicomSeriesMetadata/GetDicomSeriesMetadata.php new file mode 100644 index 000000000..1c6af849b --- /dev/null +++ b/GaelO2/app/GaelO/UseCases/GetDicomSeriesMetadata/GetDicomSeriesMetadata.php @@ -0,0 +1,70 @@ +authorizationVisitService = $authorizationVisitService; + $this->fileCacheService = $fileCacheService; + $this->dicomSeriesRepository = $dicomSeriesRepository; + $this->visitRepositoryInterface = $visitRepositoryInterface; + } + + public function execute(GetDicomSeriesMetadataRequest $getDicomSeriesMetadataRequest, GetDicomSeriesMetadataResponse $getDicomSeriesMetadataResponse) + { + + try { + $curentUserId = $getDicomSeriesMetadataRequest->currentUserId; + $role = $getDicomSeriesMetadataRequest->role; + $seriesInstanceUID = $getDicomSeriesMetadataRequest->seriesInstanceUID; + + $seriesData = $this->dicomSeriesRepository->getSeries($seriesInstanceUID, false); + $visitId = $seriesData['dicom_study']['visit_id']; + + $visitContext = $this->visitRepositoryInterface->getVisitContext($visitId); + $studyName = $visitContext['patient']['study_name']; + + $this->checkAuthorization($curentUserId, $role, $visitId, $studyName, $visitContext); + + $studyMetadata = $this->fileCacheService->getDicomMetadata($seriesInstanceUID); + + $getDicomSeriesMetadataResponse->body = json_decode($studyMetadata); + $getDicomSeriesMetadataResponse->status = 200; + $getDicomSeriesMetadataResponse->statusText = 'OK'; + } catch (AbstractGaelOException $e) { + $getDicomSeriesMetadataResponse->body = $e->getErrorBody(); + $getDicomSeriesMetadataResponse->status = $e->statusCode; + $getDicomSeriesMetadataResponse->statusText = $e->statusText; + } catch (Exception $e) { + throw $e; + } + } + + private function checkAuthorization(int $userId, string $role, int $visitId, string $studyName, array $visitContext): void + { + $this->authorizationVisitService->setUserId($userId); + $this->authorizationVisitService->setVisitId($visitId); + $this->authorizationVisitService->setStudyName($studyName); + $this->authorizationVisitService->setVisitContext($visitContext); + + if (!$this->authorizationVisitService->isVisitAllowed($role)) { + throw new GaelOForbiddenException(); + } + } +} diff --git a/GaelO2/app/GaelO/UseCases/GetDicomSeriesMetadata/GetDicomSeriesMetadataRequest.php b/GaelO2/app/GaelO/UseCases/GetDicomSeriesMetadata/GetDicomSeriesMetadataRequest.php new file mode 100644 index 000000000..7bfc1ebb6 --- /dev/null +++ b/GaelO2/app/GaelO/UseCases/GetDicomSeriesMetadata/GetDicomSeriesMetadataRequest.php @@ -0,0 +1,10 @@ +authorizationVisitService = $authorizationVisitService; + $this->fileCacheService = $fileCacheService; + $this->dicomSeriesRepository = $dicomSeriesRepository; + $this->visitRepositoryInterface = $visitRepositoryInterface; + } + + public function execute(GetDicomSeriesPreviewRequest $getDicomSeriesPreviewRequest, GetDicomSeriesPreviewResponse $getDicomSeriesPreviewResponse) + { + + try { + $curentUserId = $getDicomSeriesPreviewRequest->currentUserId; + $role = $getDicomSeriesPreviewRequest->role; + $seriesInstanceUID = $getDicomSeriesPreviewRequest->seriesInstanceUID; + $index = $getDicomSeriesPreviewRequest->index; + + $seriesData = $this->dicomSeriesRepository->getSeries($seriesInstanceUID, false); + $visitId = $seriesData['dicom_study']['visit_id']; + + $visitContext = $this->visitRepositoryInterface->getVisitContext($visitId); + $studyName = $visitContext['patient']['study_name']; + + $this->checkAuthorization($curentUserId, $role, $visitId, $studyName, $visitContext); + + $previewData = $this->fileCacheService->getSeriesPreview($seriesInstanceUID, $index); + $finfo = finfo_open(); + $mimeType = finfo_buffer($finfo, $previewData, FILEINFO_MIME_TYPE); + finfo_close($finfo); + + $getDicomSeriesPreviewResponse->body = $previewData; + $getDicomSeriesPreviewResponse->status = 200; + $getDicomSeriesPreviewResponse->statusText = 'OK'; + $getDicomSeriesPreviewResponse->contentType = $mimeType; + } catch (AbstractGaelOException $e) { + $getDicomSeriesPreviewResponse->body = $e->getErrorBody(); + $getDicomSeriesPreviewResponse->status = $e->statusCode; + $getDicomSeriesPreviewResponse->statusText = $e->statusText; + } catch (Exception $e) { + throw $e; + } + } + + private function checkAuthorization(int $userId, string $role, int $visitId, string $studyName, array $visitContext): void + { + $this->authorizationVisitService->setUserId($userId); + $this->authorizationVisitService->setVisitId($visitId); + $this->authorizationVisitService->setStudyName($studyName); + $this->authorizationVisitService->setVisitContext($visitContext); + + if (!$this->authorizationVisitService->isVisitAllowed($role)) { + throw new GaelOForbiddenException(); + } + } +} diff --git a/GaelO2/app/GaelO/UseCases/GetDicomSeriesPreview/GetDicomSeriesPreviewRequest.php b/GaelO2/app/GaelO/UseCases/GetDicomSeriesPreview/GetDicomSeriesPreviewRequest.php new file mode 100644 index 000000000..93e94ee22 --- /dev/null +++ b/GaelO2/app/GaelO/UseCases/GetDicomSeriesPreview/GetDicomSeriesPreviewRequest.php @@ -0,0 +1,11 @@ +authorizationVisitService = $authorizationVisitService; + $this->fileCacheService = $fileCacheService; + $this->dicomStudyRepositoryInterface = $dicomStudyRepositoryInterface; + $this->visitRepositoryInterface = $visitRepositoryInterface; + } + + public function execute(GetDicomStudyMetadataRequest $getDicomStudyMetadataRequest, GetDicomStudyMetadataResponse $getDicomStudyMetadataResponse) + { + + try { + $curentUserId = $getDicomStudyMetadataRequest->currentUserId; + $role = $getDicomStudyMetadataRequest->role; + $studyInstanceUID = $getDicomStudyMetadataRequest->studyInstanceUID; + + $studyData = $this->dicomStudyRepositoryInterface->getDicomStudy($studyInstanceUID, false); + $visitId = $studyData['visit_id']; + + $visitContext = $this->visitRepositoryInterface->getVisitContext($visitId); + $studyName = $visitContext['patient']['study_name']; + + $this->checkAuthorization($curentUserId, $role, $visitId, $studyName, $visitContext); + + $studyMetadata = $this->fileCacheService->getDicomMetadata($studyInstanceUID); + + $getDicomStudyMetadataResponse->body = json_decode($studyMetadata); + $getDicomStudyMetadataResponse->status = 200; + $getDicomStudyMetadataResponse->statusText = 'OK'; + } catch (AbstractGaelOException $e) { + $getDicomStudyMetadataResponse->body = $e->getErrorBody(); + $getDicomStudyMetadataResponse->status = $e->statusCode; + $getDicomStudyMetadataResponse->statusText = $e->statusText; + } catch (Exception $e) { + throw $e; + } + } + + private function checkAuthorization(int $userId, string $role, int $visitId, string $studyName, array $visitContext): void + { + $this->authorizationVisitService->setUserId($userId); + $this->authorizationVisitService->setVisitId($visitId); + $this->authorizationVisitService->setStudyName($studyName); + $this->authorizationVisitService->setVisitContext($visitContext); + + if (!$this->authorizationVisitService->isVisitAllowed($role)) { + throw new GaelOForbiddenException(); + } + } +} diff --git a/GaelO2/app/GaelO/UseCases/GetDicomStudyMetadata/GetDicomStudyMetadataRequest.php b/GaelO2/app/GaelO/UseCases/GetDicomStudyMetadata/GetDicomStudyMetadataRequest.php new file mode 100644 index 000000000..c0a2be9e4 --- /dev/null +++ b/GaelO2/app/GaelO/UseCases/GetDicomStudyMetadata/GetDicomStudyMetadataRequest.php @@ -0,0 +1,10 @@ + - - - - Quality Control Report - - - - - - - - - - - - - - - - - @include('mails.mail_qc_report_study', ['studyInfo' => $studyInfo]) - @foreach ($seriesInfo as $series) - @include('mails.mail_qc_report_series', ['seriesInfo' => $series]) - @endforeach - @if ($studyInfo['investigatorForm'] != null) - @include('mails.mail_qc_report_investigator_form', ['studyInfo' => $studyInfo]) - @endif + The QC report for the following visit has been prepared on the platform:
+ Study : {{ $study }}
+ Patient Code : {{ $patientCode }}
+ Uploaded visit : {{ $visitType }}
@include('mails.mail_qc_report_buttons') - - @endsection diff --git a/GaelO2/app/GaelO/views/mails/mail_qc_report_buttons.blade.php b/GaelO2/app/GaelO/views/mails/mail_qc_report_buttons.blade.php index 8ecd2114f..e96853e4b 100644 --- a/GaelO2/app/GaelO/views/mails/mail_qc_report_buttons.blade.php +++ b/GaelO2/app/GaelO/views/mails/mail_qc_report_buttons.blade.php @@ -1,84 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + +
- -
- - - - + + +
- -
- - - - - - -
- - - - - - -
- It's all good ! -
-
-
- -
- - - - - - -
- - - - - - -
- See in GaelO -
-
-
- + +
+ + + + + +
+ +
+ + + + - - -
+ + + + + + +
+ Access QC +
-
- -
- - - - - - -
- -
-
- +
+
+ +
+
+ +
+ + + + + + +
+ +
+
+
+ + + diff --git a/GaelO2/app/GaelO/views/mails/mail_qc_report_investigator_form.blade.php b/GaelO2/app/GaelO/views/mails/mail_qc_report_investigator_form.blade.php deleted file mode 100644 index 9a89e7843..000000000 --- a/GaelO2/app/GaelO/views/mails/mail_qc_report_investigator_form.blade.php +++ /dev/null @@ -1,102 +0,0 @@ -
- -
- - - - - - -
- -
- - - - - - -
-
-

Investigator form

-
-
-
- -
-
- -
- - - - - - -
- -
- - - - - - -
- - @foreach ($studyInfo['investigatorForm'] as $key => $value) - - - - - @endforeach -
{{ $key }} - {{ var_export($value) }}
-
-
- -
-
- -
- - - - - - -
- -
- - - - - - -
-

-

- -
-
- -
-
- -
diff --git a/GaelO2/app/GaelO/views/mails/mail_qc_report_series.blade.php b/GaelO2/app/GaelO/views/mails/mail_qc_report_series.blade.php deleted file mode 100644 index a1b9d3e35..000000000 --- a/GaelO2/app/GaelO/views/mails/mail_qc_report_series.blade.php +++ /dev/null @@ -1,151 +0,0 @@ -
- -
- - - - - - -
- -
- - - - - - -
-
-

- {{ $series['Series Description'] }} -

-
-
-
- -
-
- -
- - - - - - -
- - @foreach ($series['image_path'] as $path) - -
- - - - - - -
- - - - - - -
- series preview -
-
-
- - @endforeach - -
-
- -
- - - - - - -
- -
- - - - - - -
- - @foreach ($series as $key => $value) - @if ($value != null && $key !== 'image_path') - - - - - @endif - @endforeach -
{{ $key }} - {{ $value }}
-
-
- -
-
- -
- - - - - - -
- -
- - - - - - -
-

-

- -
-
- -
-
- -
diff --git a/GaelO2/app/GaelO/views/mails/mail_qc_report_study.blade.php b/GaelO2/app/GaelO/views/mails/mail_qc_report_study.blade.php deleted file mode 100644 index 997d557ee..000000000 --- a/GaelO2/app/GaelO/views/mails/mail_qc_report_study.blade.php +++ /dev/null @@ -1,214 +0,0 @@ -
- -
- - - - - - -
- -
- - - - - - - - - -
-
- Visit {{ $studyInfo['visitName'] }} of the patient - {{ $studyInfo['patientCode'] }} from the {{ $studyInfo['studyName'] }} - study has been uploaded.
-
-
- Registation Date : {{ $studyInfo['registrationDate'] }}
Visit Date - Expected After : {{ $studyInfo['minVisitDate'] }}
Visit Date - Expected Before : {{ $studyInfo['maxVisitDate'] }}
Visit Date: - {{ $studyInfo['visitDate'] }}
-
-
- -
-
- -
- - - - - - -
- -
- - - - - - -
-

-

- -
-
- -
-
- -
- - - - - - -
- -
- - - - - - -
-
-

Study info

-
-
-
- -
-
- -
- - - - - - -
- -
- - - - - - -
- - @foreach ($studyInfo['studyDetails'] as $key => $value) - @if ($value != null) - - - - - @endif - @endforeach -
{{ $key }} - {{ $value }}
-
-
- -
-
- - @if ($warningVisitDate) - -
- - - - - - -
- -
- - - - - - -
-
- Warning : DICOM Study Date and GaelO Visit Date are different -
-
-
- -
-
- - @endif - -
- - - - - - -
- -
- - - - - - -
-

-

- -
-
- -
-
- -
diff --git a/GaelO2/app/GaelO/views/mails/mjml/qc_report_buttons.mjml b/GaelO2/app/GaelO/views/mails/mjml/qc_report_buttons.mjml index 9fe41d50e..35db83cd8 100644 --- a/GaelO2/app/GaelO/views/mails/mjml/qc_report_buttons.mjml +++ b/GaelO2/app/GaelO/views/mails/mjml/qc_report_buttons.mjml @@ -7,11 +7,8 @@ - - It's all good ! - - - See in GaelO + + Access QC diff --git a/GaelO2/app/GaelO/views/mails/mjml/qc_report_investigator_form.mjml b/GaelO2/app/GaelO/views/mails/mjml/qc_report_investigator_form.mjml deleted file mode 100644 index 7d0055e40..000000000 --- a/GaelO2/app/GaelO/views/mails/mjml/qc_report_investigator_form.mjml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - -

- Investigator form -

-
-
-
- - - - @foreach($studyInfo['investigatorForm'] as $key => $value) - - {{ $key }} - {{ var_export($value) }} - - @endforeach - - - - - - - - -
-
\ No newline at end of file diff --git a/GaelO2/app/GaelO/views/mails/mjml/qc_report_series.mjml b/GaelO2/app/GaelO/views/mails/mjml/qc_report_series.mjml deleted file mode 100644 index b3a90f20a..000000000 --- a/GaelO2/app/GaelO/views/mails/mjml/qc_report_series.mjml +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - - -

- {{$series['Series Description']}} -

-
-
-
- - - @foreach($series['image_path'] as $path) - - - - - - @endforeach - - - - - - @foreach($series as $key => $value) - @if ($value != null && $key !== 'image_path') - - {{ $key }} - {{ $value }} - - @endif - @endforeach - - - - - - - - -
-
\ No newline at end of file diff --git a/GaelO2/app/GaelO/views/mails/mjml/qc_report_study.mjml b/GaelO2/app/GaelO/views/mails/mjml/qc_report_study.mjml deleted file mode 100644 index 374410e92..000000000 --- a/GaelO2/app/GaelO/views/mails/mjml/qc_report_study.mjml +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - - - - Visit {{$studyInfo['visitName']}} of the patient {{$studyInfo['patientCode']}} from the {{$studyInfo['studyName']}} study has been uploaded. - - Registation Date : {{$studyInfo['registrationDate']}}
- Visit Date Expected After : {{$studyInfo['minVisitDate']}}
- Visit Date Expected Before : {{$studyInfo['maxVisitDate']}}
- Visit Date: {{$studyInfo['visitDate']}} -
-
-
- - - - - - - - -

Study info

-
-
-
- - - - @foreach($studyInfo['studyDetails'] as $key => $value) - @if ($value != null) - - {{ $key }} - {{ $value }} - - @endif - @endforeach - - - - - @if($warningVisitDate) - - - - - Warning : DICOM Study Date and GaelO Visit Date are different - - - - - @endif - - - - - - -
-
\ No newline at end of file diff --git a/GaelO2/app/Http/Controllers/DicomController.php b/GaelO2/app/Http/Controllers/DicomController.php index e5263151e..5f7114e87 100644 --- a/GaelO2/app/Http/Controllers/DicomController.php +++ b/GaelO2/app/Http/Controllers/DicomController.php @@ -8,12 +8,21 @@ use App\GaelO\UseCases\GetDicoms\GetDicoms; use App\GaelO\UseCases\GetDicoms\GetDicomsRequest; use App\GaelO\UseCases\GetDicoms\GetDicomsResponse; +use App\GaelO\UseCases\GetDicomSeriesMetadata\GetDicomSeriesMetadata; +use App\GaelO\UseCases\GetDicomSeriesMetadata\GetDicomSeriesMetadataRequest; +use App\GaelO\UseCases\GetDicomSeriesMetadata\GetDicomSeriesMetadataResponse; +use App\GaelO\UseCases\GetDicomSeriesPreview\GetDicomSeriesPreview; +use App\GaelO\UseCases\GetDicomSeriesPreview\GetDicomSeriesPreviewRequest; +use App\GaelO\UseCases\GetDicomSeriesPreview\GetDicomSeriesPreviewResponse; use App\GaelO\UseCases\GetDicomsFile\GetDicomsFile; use App\GaelO\UseCases\GetDicomsFile\GetDicomsFileRequest; use App\GaelO\UseCases\GetDicomsFile\GetDicomsFileResponse; use App\GaelO\UseCases\GetDicomsFileSupervisor\GetDicomsFileSupervisor; use App\GaelO\UseCases\GetDicomsFileSupervisor\GetDicomsFileSupervisorRequest; use App\GaelO\UseCases\GetDicomsFileSupervisor\GetDicomsFileSupervisorResponse; +use App\GaelO\UseCases\GetDicomStudyMetadata\GetDicomStudyMetadata; +use App\GaelO\UseCases\GetDicomStudyMetadata\GetDicomStudyMetadataRequest; +use App\GaelO\UseCases\GetDicomStudyMetadata\GetDicomStudyMetadataResponse; use App\GaelO\UseCases\GetNiftiFileSupervisor\GetNiftiFileSupervisor; use App\GaelO\UseCases\GetNiftiFileSupervisor\GetNiftiFileSupervisorRequest; use App\GaelO\UseCases\GetNiftiFileSupervisor\GetNiftiFileSupervisorResponse; @@ -157,4 +166,47 @@ public function getNiftiSeries(Request $request, GetNiftiFileSupervisor $getNift ->setStatusCode($getNiftiFileSupervisorResponse->status, $getNiftiFileSupervisorResponse->statusText); } } + + public function getStudyMetadata(Request $request, GetDicomStudyMetadata $getDicomStudyMetadata, GetDicomStudyMetadataRequest $getDicomStudyMetadataRequest, GetDicomStudyMetadataResponse $getDicomStudyMetadataResponse, string $studyInstanceUID) + { + $currentUser = Auth::user(); + $queryParam = $request->query(); + + $getDicomStudyMetadataRequest->studyInstanceUID = $studyInstanceUID; + $getDicomStudyMetadataRequest->role = $queryParam['role']; + $getDicomStudyMetadataRequest->currentUserId = $currentUser['id']; + + $getDicomStudyMetadata->execute($getDicomStudyMetadataRequest, $getDicomStudyMetadataResponse); + + return $this->getJsonResponse($getDicomStudyMetadataResponse->body, $getDicomStudyMetadataResponse->status, $getDicomStudyMetadataResponse->statusText); + } + + public function getSeriesMetadata(Request $request, GetDicomSeriesMetadata $getDicomSeriesMetadata, GetDicomSeriesMetadataRequest $getDicomSeriesMetadataRequest, GetDicomSeriesMetadataResponse $getDicomSeriesMetadataResponse, string $seriesInstanceUID) + { + $currentUser = Auth::user(); + $queryParam = $request->query(); + + $getDicomSeriesMetadataRequest->seriesInstanceUID = $seriesInstanceUID; + $getDicomSeriesMetadataRequest->role = $queryParam['role']; + $getDicomSeriesMetadataRequest->currentUserId = $currentUser['id']; + + $getDicomSeriesMetadata->execute($getDicomSeriesMetadataRequest, $getDicomSeriesMetadataResponse); + + return $this->getJsonResponse($getDicomSeriesMetadataResponse->body, $getDicomSeriesMetadataResponse->status, $getDicomSeriesMetadataResponse->statusText); + } + + public function getSeriesPreview(Request $request, GetDicomSeriesPreview $getDicomSeriesPreview, GetDicomSeriesPreviewRequest $getDicomSeriesPreviewRequest, GetDicomSeriesPreviewResponse $getDicomSeriesPreviewResponse, string $seriesInstanceUID, int $index) + { + $currentUser = Auth::user(); + $queryParam = $request->query(); + + $getDicomSeriesPreviewRequest->role = $queryParam['role']; + $getDicomSeriesPreviewRequest->seriesInstanceUID = $seriesInstanceUID; + $getDicomSeriesPreviewRequest->index = $index; + $getDicomSeriesPreviewRequest->currentUserId = $currentUser['id']; + + $getDicomSeriesPreview->execute($getDicomSeriesPreviewRequest, $getDicomSeriesPreviewResponse); + + return response($getDicomSeriesPreviewResponse->body, $getDicomSeriesPreviewResponse->status)->header('Content-Type', $getDicomSeriesPreviewResponse->contentType); + } } diff --git a/GaelO2/app/Http/Controllers/ReviewController.php b/GaelO2/app/Http/Controllers/ReviewController.php index 91d9c72ed..2e744af31 100644 --- a/GaelO2/app/Http/Controllers/ReviewController.php +++ b/GaelO2/app/Http/Controllers/ReviewController.php @@ -56,7 +56,6 @@ use App\GaelO\Util; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Storage; class ReviewController extends Controller diff --git a/GaelO2/app/Jobs/JobQcReport.php b/GaelO2/app/Jobs/JobQcReport.php index 85925f06d..5de45c59f 100644 --- a/GaelO2/app/Jobs/JobQcReport.php +++ b/GaelO2/app/Jobs/JobQcReport.php @@ -3,12 +3,11 @@ namespace App\Jobs; use App\GaelO\Constants\Constants; -use App\GaelO\Constants\Enums\InvestigatorFormStateEnum; use App\GaelO\Interfaces\Adapters\FrameworkInterface; use App\GaelO\Interfaces\Repositories\DicomStudyRepositoryInterface; -use App\GaelO\Interfaces\Repositories\ReviewRepositoryInterface; use App\GaelO\Interfaces\Repositories\UserRepositoryInterface; use App\GaelO\Interfaces\Repositories\VisitRepositoryInterface; +use App\GaelO\Services\FileCacheService; use App\GaelO\Services\GaelOProcessingService\GaelOProcessingService; use App\GaelO\Services\MailServices; use App\GaelO\Services\OrthancService; @@ -29,7 +28,6 @@ class JobQcReport implements ShouldQueue, ShouldBeUnique { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; private int $visitId; - private OrthancService $orthancService; public $failOnTimeout = true; public $timeout = 300; @@ -53,16 +51,15 @@ public function __construct(int $visitId) */ public function handle( FrameworkInterface $frameworkInterface, + FileCacheService $fileCacheService, UserRepositoryInterface $userRepositoryInterface, VisitRepositoryInterface $visitRepositoryInterface, DicomStudyRepositoryInterface $dicomStudyRepositoryInterface, MailServices $mailServices, OrthancService $orthancService, GaelOProcessingService $gaelOProcessingService, - ReviewRepositoryInterface $reviewRepositoryInterface ) { - $this->orthancService = $orthancService; - $this->orthancService->setOrthancServer(true); + $orthancService->setOrthancServer(true); $visitReport = new VisitReport(); @@ -72,7 +69,6 @@ public function handle( $visitId = $visitEntity['id']; $visitType = $visitEntity['visit_type']['name']; $patientCode = $visitEntity['patient']['code']; - $stateInvestigatorForm = $visitEntity['state_investigator_form']; $visitDate = $visitEntity['visit_date']; $registrationDate = $visitEntity['patient']['registration_date']; @@ -95,20 +91,15 @@ public function handle( $visitReport->setMinMaxVisitDate($formattedMinVisitDate, $formattedMaxVisitDate); } - if ($stateInvestigatorForm !== InvestigatorFormStateEnum::NOT_NEEDED->value) { - $reviewEntity = $reviewRepositoryInterface->getInvestigatorForm($this->visitId, false); - $visitReport->setInvestigatorForm($reviewEntity['review_data']); - } - $seriesReports = []; foreach ($dicomStudyEntity[0]['dicom_series'] as $series) { try { - $seriesSharedTags = $this->orthancService->getSharedTags($series['orthanc_id']); - $seriesDetails = $this->orthancService->getOrthancRessourcesDetails(Constants::ORTHANC_SERIES_LEVEL, $series['orthanc_id']); + $seriesSharedTags = $orthancService->getSharedTags($series['orthanc_id']); + $seriesDetails = $orthancService->getOrthancRessourcesDetails(Constants::ORTHANC_SERIES_LEVEL, $series['orthanc_id']); //Needed for radiopharmaceutical data (need first instance metadata to access it) - $instanceTags = $this->orthancService->getInstanceTags($seriesDetails['Instances'][0]); + $instanceTags = $orthancService->getInstanceTags($seriesDetails['Instances'][0]); $instanceReport = new InstanceReport(); $instanceReport->fillData($instanceTags); @@ -117,7 +108,7 @@ public function handle( $seriesReport->fillData($seriesSharedTags); $seriesReport->setInstanceReport($instanceReport); - $seriesReport->loadSeriesPreview($this->orthancService, $gaelOProcessingService); + $seriesReport->loadSeriesPreview($orthancService, $gaelOProcessingService); $seriesReports[] = $seriesReport; } catch (Throwable $t) { @@ -128,11 +119,19 @@ public function handle( $visitReport->setSeriesReports($seriesReports); $seriesReports = $visitReport->getSeriesReports(); - $seriesInfo = array_map(function (SeriesReport $seriesReport) { - return $seriesReport->toArray(); - }, $seriesReports); + foreach ($seriesReports as $seriesReport) { + $seriesInstanceUID = $seriesReport->getSeriesInstanceUID(); + $previews = $seriesReport->getPreviews(); + for ($i = 0; $i < sizeof($previews); $i++) { + $fileCacheService->storeSeriesPreview($seriesInstanceUID, $i, file_get_contents($previews[$i])); + } + $fileCacheService->storeDicomMetadata($seriesInstanceUID, json_encode($seriesReport->toArray())); + } + $studyInfo = $visitReport->toArray(); + $studyInstanceUID = $dicomStudyEntity[0]['study_uid']; + $fileCacheService->storeDicomMetadata($studyInstanceUID, json_encode($studyInfo)); $controllerUsers = $userRepositoryInterface->getUsersByRolesInStudy($studyName, Constants::ROLE_CONTROLLER); @@ -140,16 +139,13 @@ public function handle( $redirectLink = '/magic-link-tools/auto-qc'; $queryParams = [ 'visitId' => $visitId, - 'accepted' => 'true', 'studyName' => $studyName ]; - $magicLinkAccepted = $frameworkInterface->createMagicLink($user['id'], $redirectLink, $queryParams); - $queryParams['accepted'] = 'false'; - $magicLinkRefused = $frameworkInterface->createMagicLink($user['id'], $redirectLink, $queryParams); - $mailServices->sendQcReport($studyName, $visitType, $patientCode, $studyInfo, $seriesInfo, $magicLinkAccepted, $magicLinkRefused, $user['email']); + $magicLink = $frameworkInterface->createMagicLink($user['id'], $redirectLink, $queryParams); + $mailServices->sendQcReport($studyName, $visitType, $patientCode, $magicLink, $user['email']); } - //Once all email emited remove preview file to avoid dangling temporary files + //Once job finished remove preview file to avoid dangling temporary files foreach ($seriesReports as $seriesReport) { $seriesReport->deletePreviewImages(); } diff --git a/GaelO2/app/Jobs/QcReport/SeriesReport.php b/GaelO2/app/Jobs/QcReport/SeriesReport.php index d91ad3dfc..2baee7dd1 100644 --- a/GaelO2/app/Jobs/QcReport/SeriesReport.php +++ b/GaelO2/app/Jobs/QcReport/SeriesReport.php @@ -33,6 +33,7 @@ class SeriesReport private $protocolName; private $patientWeight; private $patientHeight; + private $seriesInstanceUID; private array $previewImagePath = []; private array $orthancInstanceIds; private string $seriesOrthancId; @@ -54,11 +55,6 @@ public function getNumberOfInstances(): int return sizeof($this->orthancInstanceIds); } - public function addPreviewImagePath(?string $path) - { - $this->previewImagePath[] = $path; - } - public function deletePreviewImages() { foreach ($this->previewImagePath as $imagePath) { @@ -88,6 +84,7 @@ public function fillData(OrthancMetaData $sharedTags) $this->matrixSize = $sharedTags->getMatrixSize(); $this->patientPosition = $sharedTags->getPatientPosition(); $this->patientOrientation = $sharedTags->getImageOrientation(); + $this->seriesInstanceUID = $sharedTags->getSeriesInstanceUID(); if ($this->modality == 'MR') { $this->scanningSequence = $sharedTags->getScanningSequence(); @@ -148,12 +145,11 @@ public function loadSeriesPreview(OrthancService $orthancService, GaelOProcessin $imagePath[] = $orthancService->getInstancePreview($this->orthancInstanceIds[0]); } else { $isPet = $this->modality == 'PT'; - $payload = $isPet ? ['min' => 0, 'max' => 5] : []; + $payload = $isPet ? ['min' => 0, 'max' => 5, 'orientation' => 'LPI'] : ['orientation' => 'LPI']; $orthancService->sendDicomToProcessing($this->seriesOrthancId, $gaelOProcessingService); $processingSeriesId = $gaelOProcessingService->createSeriesFromOrthanc($this->seriesOrthancId, $isPet, $isPet); switch ($imageType) { case ImageType::MIP: - //Mosaic for now as mip need significant computation and memory backend $imagePath[] = $gaelOProcessingService->createMIPForSeries($processingSeriesId, $payload); break; case ImageType::MOSAIC: @@ -174,6 +170,16 @@ public function loadSeriesPreview(OrthancService $orthancService, GaelOProcessin $this->previewImagePath = $imagePath; } + public function getSeriesInstanceUID(): string + { + return $this->seriesInstanceUID; + } + + public function getPreviews(): array + { + return $this->previewImagePath; + } + public function toArray() { @@ -203,7 +209,7 @@ public function toArray() 'Protocol Name' => $this->protocolName ?? null, 'Patient weight (kg)' => $this->patientWeight ?? null, 'Patient height (m)' => $this->patientHeight ?? null, - 'image_path' => $this->previewImagePath, + 'Previews' => sizeof($this->previewImagePath), ...$instanceData ]; } diff --git a/GaelO2/app/Jobs/QcReport/VisitReport.php b/GaelO2/app/Jobs/QcReport/VisitReport.php index dc519a443..2f88956b3 100644 --- a/GaelO2/app/Jobs/QcReport/VisitReport.php +++ b/GaelO2/app/Jobs/QcReport/VisitReport.php @@ -7,12 +7,10 @@ class VisitReport private string $studyName; private string $visitName; private string $patientCode; - private string $visitDate; private string $minVisitDate; private string $maxVisitDate; private string $registrationDate; - private array $investigatorForm = []; private array $seriesReports = []; @@ -46,11 +44,6 @@ public function setVisitDate(string $visitDate) $this->visitDate = $visitDate; } - public function setInvestigatorForm(array $investigatorForm) - { - $this->investigatorForm = $investigatorForm; - } - public function setRegistrationDate(string $registrationDate) { $this->registrationDate = $registrationDate; @@ -72,7 +65,6 @@ public function toArray() }, $this->seriesReports)), 'Number Of Series' => sizeof($this->seriesReports) ], - 'investigatorForm' => $this->investigatorForm, 'studyName' => $this->studyName, 'visitName' => $this->visitName, 'patientCode' => $this->patientCode, diff --git a/GaelO2/app/Providers/AdapterProvider.php b/GaelO2/app/Providers/AdapterProvider.php index 1fc3e72d0..5e3c13253 100644 --- a/GaelO2/app/Providers/AdapterProvider.php +++ b/GaelO2/app/Providers/AdapterProvider.php @@ -2,6 +2,7 @@ namespace App\Providers; +use App\GaelO\Adapters\AzureCacheAdapter; use App\GaelO\Adapters\DatabaseDumperAdapter; use App\GaelO\Adapters\FrameworkAdapter; use App\GaelO\Adapters\HttpClientAdapter; @@ -18,7 +19,7 @@ use App\GaelO\Interfaces\Adapters\PhoneNumberInterface; use Illuminate\Filesystem\FilesystemAdapter; use Illuminate\Foundation\Application; -use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Storage; use Illuminate\Support\ServiceProvider; use League\Flysystem\AzureBlobStorage\AzureBlobStorageAdapter; @@ -60,5 +61,18 @@ public function boot() return new FilesystemAdapter(new Filesystem($adapter, $config),$adapter, $config); }); + + Cache::extend('azure', function($app, $config){ + $client = BlobRestProxy::createBlobService($config['dsn']); + $adapter = new AzureBlobStorageAdapter( + $client, + $config['container'], + $config['prefix'], + ); + + $fileSystem = new Filesystem($adapter, $config); + + return Cache::repository(new AzureCacheAdapter($fileSystem)); + }); } } diff --git a/GaelO2/config/cache.php b/GaelO2/config/cache.php index 4f41fdf96..93132cfb7 100644 --- a/GaelO2/config/cache.php +++ b/GaelO2/config/cache.php @@ -19,6 +19,7 @@ */ 'default' => env('CACHE_DRIVER', 'file'), + 'file-cache' => env('FILE_CACHE_DRIVER', 'file'), /* |-------------------------------------------------------------------------- @@ -86,6 +87,13 @@ 'endpoint' => env('DYNAMODB_ENDPOINT'), ], + 'azure' => [ + 'driver' => 'azure', + 'dsn' =>env('AZURE_BLOB_DSN'), + 'container'=>env('AZURE_FILE_CACHE_CONTAINER_NAME'), + 'prefix' => env('AZURE_BLOB_PREFIX', '') + ] + ], /* diff --git a/GaelO2/routes/api.php b/GaelO2/routes/api.php index b57ed25df..277dd908f 100644 --- a/GaelO2/routes/api.php +++ b/GaelO2/routes/api.php @@ -163,7 +163,9 @@ Route::post('dicom-series/{seriesInstanceUID}/activate', [DicomController::class, 'reactivateSeries']); Route::post('dicom-study/{studyInstanceUID}/activate', [DicomController::class, 'reactivateStudy']); Route::get('visits/{id}/dicoms', [DicomController::class, 'getVisitDicoms']); - + Route::get('dicom-series/{seriesInstanceUID}/previews/{index}', [DicomController::class, 'getSeriesPreview']); + Route::get('dicom-series/{seriesInstanceUID}/metadata', [DicomController::class, 'getSeriesMetadata']); + Route::get('dicom-studies/{studyInstanceUID}/metadata', [DicomController::class, 'getStudyMetadata']); //Ask Unlock route Route::post('visits/{id}/ask-unlock', [AskUnlockController::class, 'askUnlock']); diff --git a/GaelO2/tests/Feature/TestDicoms/DicomMetadataTest.php b/GaelO2/tests/Feature/TestDicoms/DicomMetadataTest.php new file mode 100644 index 000000000..04f792d94 --- /dev/null +++ b/GaelO2/tests/Feature/TestDicoms/DicomMetadataTest.php @@ -0,0 +1,77 @@ +artisan('db:seed'); + $this->dicomSeries = DicomSeries::factory()->create(); + $this->studyName = $this->dicomSeries->dicomStudy->visit->patient->study_name; + $fileCacheAdapter = new FileCacheService(new FileCacheAdapter()); + $fileCacheAdapter->storeDicomMetadata($this->dicomSeries->dicomStudy->study_uid, json_encode(['study' => 'value'])); + $fileCacheAdapter->storeDicomMetadata($this->dicomSeries->series_uid, json_encode(['series' => 'value2'])); + $fileCacheAdapter->storeSeriesPreview($this->dicomSeries->series_uid, 0, 'plaintext'); + } + + public function testGetDicomStudiesMetadata() + { + $userId = AuthorizationTools::actAsAdmin(false); + AuthorizationTools::addRoleToUser($userId, Constants::ROLE_SUPERVISOR, $this->studyName); + + $response = $this->get('api/dicom-studies/' . $this->dicomSeries->dicomStudy->study_uid . '/metadata?role=Supervisor&studyName=' . $this->studyName); + $response->assertStatus(200); + $response->assertJsonIsObject(); + } + + public function testGetDicomStudiesMetadataShouldFailNoRole() + { + AuthorizationTools::actAsAdmin(false); + + $response = $this->get('api/dicom-studies/' . $this->dicomSeries->dicomStudy->study_uid . '/metadata?role=Supervisor&studyName=' . $this->studyName); + $response->assertStatus(403); + } + + public function testGetDicomSeriesMetadata() + { + $userId = AuthorizationTools::actAsAdmin(false); + AuthorizationTools::addRoleToUser($userId, Constants::ROLE_SUPERVISOR, $this->studyName); + + $response = $this->get('api/dicom-series/' . $this->dicomSeries->series_uid . '/metadata?role=Supervisor&studyName=' . $this->studyName); + $response->assertStatus(200); + $response->assertJsonIsObject(); + } + + public function testGetDicomSeriesMetadataShouldFailNoRole() + { + AuthorizationTools::actAsAdmin(false); + + $response = $this->get('api/dicom-series/' . $this->dicomSeries->series_uid . '/metadata?role=Supervisor&studyName=' . $this->studyName); + $response->assertStatus(403); + } + + public function testGetDicomSeriesPreview() + { + $userId = AuthorizationTools::actAsAdmin(false); + AuthorizationTools::addRoleToUser($userId, Constants::ROLE_SUPERVISOR, $this->studyName); + + $response = $this->get('api/dicom-series/' . $this->dicomSeries->series_uid . '/previews/0?role=Supervisor&studyName=' . $this->studyName); + $response->assertStatus(200); + $response->assertContent("plaintext"); + } +} diff --git a/GaelO2/tests/Unit/TestAdapters/FileCacheAdapterTest.php b/GaelO2/tests/Unit/TestAdapters/FileCacheAdapterTest.php new file mode 100644 index 000000000..9c6c585a3 --- /dev/null +++ b/GaelO2/tests/Unit/TestAdapters/FileCacheAdapterTest.php @@ -0,0 +1,39 @@ +fileCacheAdapter = new FileCacheAdapter(); + } + + public function testReadFile() + { + $this->fileCacheAdapter->store('keyFile2', 'payload'); + $content = $this->fileCacheAdapter->get('keyFile2'); + $this->assertEquals('payload', $content); + } + + public function testStoreFile() + { + $success = $this->fileCacheAdapter->store('keyFile', 'payload'); + $this->assertTrue($success); + } + + public function testDeleteFile() + { + $this->fileCacheAdapter->store('keyFile3', 'payload'); + $success = $this->fileCacheAdapter->delete('keyFile3'); + $this->assertTrue($success); + } +} diff --git a/README.md b/README.md index b9959d5c8..df8c52a89 100755 --- a/README.md +++ b/README.md @@ -26,9 +26,6 @@ sheetname limité a 31 caractère $sheetName = substr($role, 0, 3) . '_' . $vi # Regenerate views email using mjml template ``` node_modules/mjml/bin/mjml ./app/GaelO/views/mails/mjml/qc_report_buttons.mjml -o ./app/GaelO/views/mails/mail_qc_report_buttons.blade.php -node_modules/mjml/bin/mjml ./app/GaelO/views/mails/mjml/qc_report_series.mjml -o ./app/GaelO/views/mails/mail_qc_report_series.blade.php -node_modules/mjml/bin/mjml ./app/GaelO/views/mails/mjml/qc_report_study.mjml -o ./app/GaelO/views/mails/mail_qc_report_study.blade.php -node_modules/mjml/bin/mjml ./app/GaelO/views/mails/mjml/qc_report_investigator_form.mjml -o ./app/GaelO/views/mails/mail_qc_report_investigator_form.blade.php node_modules/mjml/bin/mjml ./app/GaelO/views/mails/mjml/radiomics_report.mjml -o ./app/GaelO/views/mails/mail_radiomics_report.blade.php ``` In blade generated files, edit file to keep only body content (remove header...) From 5976c5d60dda4582b9b7a7979caf54d71e2813f0 Mon Sep 17 00:00:00 2001 From: Salim Kanoun Date: Mon, 15 Jan 2024 00:42:53 +0100 Subject: [PATCH 12/72] fix cache store --- GaelO2/.env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GaelO2/.env.example b/GaelO2/.env.example index f61ba626c..382dd1c07 100644 --- a/GaelO2/.env.example +++ b/GaelO2/.env.example @@ -41,7 +41,7 @@ AZURE_BLOB_DSN='' AZURE_CONTAINER_NAME='' AZURE_BLOB_PREFIX='' AZURE_FILE_CACHE_CONTAINER_NAME='' -FILE_CACHE_DRIVER='local' +FILE_CACHE_DRIVER='file' BROADCAST_DRIVER=log CACHE_DRIVER=file From 333f3e50df09e2eaf36781152da880b4312b7292 Mon Sep 17 00:00:00 2001 From: Salim Kanoun Date: Mon, 15 Jan 2024 00:45:29 +0100 Subject: [PATCH 13/72] fix stan --- GaelO2/app/GaelO/Adapters/AzureCacheAdapter.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/GaelO2/app/GaelO/Adapters/AzureCacheAdapter.php b/GaelO2/app/GaelO/Adapters/AzureCacheAdapter.php index d2c23feec..5a022a238 100644 --- a/GaelO2/app/GaelO/Adapters/AzureCacheAdapter.php +++ b/GaelO2/app/GaelO/Adapters/AzureCacheAdapter.php @@ -25,7 +25,7 @@ public function __construct(Filesystem $fileSystem) private function getPath(string $key) { $hash = sha1($key); - return $this->rootDirectory . '/'. $hash; + return $this->rootDirectory . '/' . $hash; } public function get($key) @@ -77,12 +77,14 @@ public function decrement($key, $value = 1) public function forever($key, $value) { $this->put($key, $value, 0); + return true; } public function forget($key) { $path = $this->getPath($key); $this->fileSystem->delete($path); + return true; } public function flush() @@ -91,6 +93,7 @@ public function flush() foreach ($files as $file) { $this->fileSystem->delete($file->path()); } + return true; } public function getPrefix() From 08afeb182b114dfb62f60d22894a4a1eee12dfaa Mon Sep 17 00:00:00 2001 From: Salim Kanoun Date: Mon, 15 Jan 2024 20:27:04 +0100 Subject: [PATCH 14/72] remove mail autoQc --- GaelO2/app/Jobs/JobQcReport.php | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/GaelO2/app/Jobs/JobQcReport.php b/GaelO2/app/Jobs/JobQcReport.php index 5de45c59f..da2bd1ce5 100644 --- a/GaelO2/app/Jobs/JobQcReport.php +++ b/GaelO2/app/Jobs/JobQcReport.php @@ -3,9 +3,7 @@ namespace App\Jobs; use App\GaelO\Constants\Constants; -use App\GaelO\Interfaces\Adapters\FrameworkInterface; use App\GaelO\Interfaces\Repositories\DicomStudyRepositoryInterface; -use App\GaelO\Interfaces\Repositories\UserRepositoryInterface; use App\GaelO\Interfaces\Repositories\VisitRepositoryInterface; use App\GaelO\Services\FileCacheService; use App\GaelO\Services\GaelOProcessingService\GaelOProcessingService; @@ -50,12 +48,9 @@ public function __construct(int $visitId) * @return void */ public function handle( - FrameworkInterface $frameworkInterface, FileCacheService $fileCacheService, - UserRepositoryInterface $userRepositoryInterface, VisitRepositoryInterface $visitRepositoryInterface, DicomStudyRepositoryInterface $dicomStudyRepositoryInterface, - MailServices $mailServices, OrthancService $orthancService, GaelOProcessingService $gaelOProcessingService, ) { @@ -133,18 +128,6 @@ public function handle( $studyInstanceUID = $dicomStudyEntity[0]['study_uid']; $fileCacheService->storeDicomMetadata($studyInstanceUID, json_encode($studyInfo)); - $controllerUsers = $userRepositoryInterface->getUsersByRolesInStudy($studyName, Constants::ROLE_CONTROLLER); - - foreach ($controllerUsers as $user) { - $redirectLink = '/magic-link-tools/auto-qc'; - $queryParams = [ - 'visitId' => $visitId, - 'studyName' => $studyName - ]; - $magicLink = $frameworkInterface->createMagicLink($user['id'], $redirectLink, $queryParams); - $mailServices->sendQcReport($studyName, $visitType, $patientCode, $magicLink, $user['email']); - } - //Once job finished remove preview file to avoid dangling temporary files foreach ($seriesReports as $seriesReport) { $seriesReport->deletePreviewImages(); From 516aedec91b7f6220ef84c1c470e9ac7a76a774c Mon Sep 17 00:00:00 2001 From: Salim Kanoun Date: Mon, 15 Jan 2024 21:44:31 +0100 Subject: [PATCH 15/72] cotinue --- .../GetDicomSeriesMetadata/GetDicomSeriesMetadata.php | 4 +++- .../GetDicomSeriesMetadata/GetDicomSeriesMetadataRequest.php | 1 + .../UseCases/GetDicomSeriesPreview/GetDicomSeriesPreview.php | 2 +- .../GetDicomSeriesPreview/GetDicomSeriesPreviewRequest.php | 1 + .../UseCases/GetDicomStudyMetadata/GetDicomStudyMetadata.php | 2 +- .../GetDicomStudyMetadata/GetDicomStudyMetadataRequest.php | 1 + GaelO2/app/Http/Controllers/DicomController.php | 4 ++++ GaelO2/app/Jobs/JobQcReport.php | 1 - 8 files changed, 12 insertions(+), 4 deletions(-) diff --git a/GaelO2/app/GaelO/UseCases/GetDicomSeriesMetadata/GetDicomSeriesMetadata.php b/GaelO2/app/GaelO/UseCases/GetDicomSeriesMetadata/GetDicomSeriesMetadata.php index 1c6af849b..30cd15b1c 100644 --- a/GaelO2/app/GaelO/UseCases/GetDicomSeriesMetadata/GetDicomSeriesMetadata.php +++ b/GaelO2/app/GaelO/UseCases/GetDicomSeriesMetadata/GetDicomSeriesMetadata.php @@ -3,12 +3,14 @@ namespace App\GaelO\UseCases\GetDicomSeriesMetadata; use App\GaelO\Exceptions\AbstractGaelOException; +use App\GaelO\Exceptions\GaelOException; use App\GaelO\Exceptions\GaelOForbiddenException; use App\GaelO\Interfaces\Repositories\VisitRepositoryInterface; use App\GaelO\Repositories\DicomSeriesRepository; use App\GaelO\Services\AuthorizationService\AuthorizationVisitService; use App\GaelO\Services\FileCacheService; use Exception; +use Illuminate\Support\Facades\Log; class GetDicomSeriesMetadata { @@ -33,12 +35,12 @@ public function execute(GetDicomSeriesMetadataRequest $getDicomSeriesMetadataReq $curentUserId = $getDicomSeriesMetadataRequest->currentUserId; $role = $getDicomSeriesMetadataRequest->role; $seriesInstanceUID = $getDicomSeriesMetadataRequest->seriesInstanceUID; + $studyName = $getDicomSeriesMetadataRequest->studyName; $seriesData = $this->dicomSeriesRepository->getSeries($seriesInstanceUID, false); $visitId = $seriesData['dicom_study']['visit_id']; $visitContext = $this->visitRepositoryInterface->getVisitContext($visitId); - $studyName = $visitContext['patient']['study_name']; $this->checkAuthorization($curentUserId, $role, $visitId, $studyName, $visitContext); diff --git a/GaelO2/app/GaelO/UseCases/GetDicomSeriesMetadata/GetDicomSeriesMetadataRequest.php b/GaelO2/app/GaelO/UseCases/GetDicomSeriesMetadata/GetDicomSeriesMetadataRequest.php index 7bfc1ebb6..88b686dee 100644 --- a/GaelO2/app/GaelO/UseCases/GetDicomSeriesMetadata/GetDicomSeriesMetadataRequest.php +++ b/GaelO2/app/GaelO/UseCases/GetDicomSeriesMetadata/GetDicomSeriesMetadataRequest.php @@ -7,4 +7,5 @@ class GetDicomSeriesMetadataRequest public string $seriesInstanceUID; public int $currentUserId; public string $role; + public string $studyName; } diff --git a/GaelO2/app/GaelO/UseCases/GetDicomSeriesPreview/GetDicomSeriesPreview.php b/GaelO2/app/GaelO/UseCases/GetDicomSeriesPreview/GetDicomSeriesPreview.php index da5e791c8..2fcee99fa 100644 --- a/GaelO2/app/GaelO/UseCases/GetDicomSeriesPreview/GetDicomSeriesPreview.php +++ b/GaelO2/app/GaelO/UseCases/GetDicomSeriesPreview/GetDicomSeriesPreview.php @@ -34,12 +34,12 @@ public function execute(GetDicomSeriesPreviewRequest $getDicomSeriesPreviewReque $role = $getDicomSeriesPreviewRequest->role; $seriesInstanceUID = $getDicomSeriesPreviewRequest->seriesInstanceUID; $index = $getDicomSeriesPreviewRequest->index; + $studyName = $getDicomSeriesPreviewRequest->studyName; $seriesData = $this->dicomSeriesRepository->getSeries($seriesInstanceUID, false); $visitId = $seriesData['dicom_study']['visit_id']; $visitContext = $this->visitRepositoryInterface->getVisitContext($visitId); - $studyName = $visitContext['patient']['study_name']; $this->checkAuthorization($curentUserId, $role, $visitId, $studyName, $visitContext); diff --git a/GaelO2/app/GaelO/UseCases/GetDicomSeriesPreview/GetDicomSeriesPreviewRequest.php b/GaelO2/app/GaelO/UseCases/GetDicomSeriesPreview/GetDicomSeriesPreviewRequest.php index 93e94ee22..fff9cfaeb 100644 --- a/GaelO2/app/GaelO/UseCases/GetDicomSeriesPreview/GetDicomSeriesPreviewRequest.php +++ b/GaelO2/app/GaelO/UseCases/GetDicomSeriesPreview/GetDicomSeriesPreviewRequest.php @@ -8,4 +8,5 @@ class GetDicomSeriesPreviewRequest public int $index; public int $currentUserId; public string $role; + public string $studyName; } diff --git a/GaelO2/app/GaelO/UseCases/GetDicomStudyMetadata/GetDicomStudyMetadata.php b/GaelO2/app/GaelO/UseCases/GetDicomStudyMetadata/GetDicomStudyMetadata.php index 065a3d39e..2989fc3e4 100644 --- a/GaelO2/app/GaelO/UseCases/GetDicomStudyMetadata/GetDicomStudyMetadata.php +++ b/GaelO2/app/GaelO/UseCases/GetDicomStudyMetadata/GetDicomStudyMetadata.php @@ -33,12 +33,12 @@ public function execute(GetDicomStudyMetadataRequest $getDicomStudyMetadataReque $curentUserId = $getDicomStudyMetadataRequest->currentUserId; $role = $getDicomStudyMetadataRequest->role; $studyInstanceUID = $getDicomStudyMetadataRequest->studyInstanceUID; + $studyName = $getDicomStudyMetadataRequest->studyName; $studyData = $this->dicomStudyRepositoryInterface->getDicomStudy($studyInstanceUID, false); $visitId = $studyData['visit_id']; $visitContext = $this->visitRepositoryInterface->getVisitContext($visitId); - $studyName = $visitContext['patient']['study_name']; $this->checkAuthorization($curentUserId, $role, $visitId, $studyName, $visitContext); diff --git a/GaelO2/app/GaelO/UseCases/GetDicomStudyMetadata/GetDicomStudyMetadataRequest.php b/GaelO2/app/GaelO/UseCases/GetDicomStudyMetadata/GetDicomStudyMetadataRequest.php index c0a2be9e4..d30b93045 100644 --- a/GaelO2/app/GaelO/UseCases/GetDicomStudyMetadata/GetDicomStudyMetadataRequest.php +++ b/GaelO2/app/GaelO/UseCases/GetDicomStudyMetadata/GetDicomStudyMetadataRequest.php @@ -7,4 +7,5 @@ class GetDicomStudyMetadataRequest public string $studyInstanceUID; public int $currentUserId; public string $role; + public string $studyName; } diff --git a/GaelO2/app/Http/Controllers/DicomController.php b/GaelO2/app/Http/Controllers/DicomController.php index 5f7114e87..202037128 100644 --- a/GaelO2/app/Http/Controllers/DicomController.php +++ b/GaelO2/app/Http/Controllers/DicomController.php @@ -174,7 +174,9 @@ public function getStudyMetadata(Request $request, GetDicomStudyMetadata $getDic $getDicomStudyMetadataRequest->studyInstanceUID = $studyInstanceUID; $getDicomStudyMetadataRequest->role = $queryParam['role']; + $getDicomStudyMetadataRequest->studyName = $queryParam['studyName']; $getDicomStudyMetadataRequest->currentUserId = $currentUser['id']; + $getDicomStudyMetadata->execute($getDicomStudyMetadataRequest, $getDicomStudyMetadataResponse); @@ -188,6 +190,7 @@ public function getSeriesMetadata(Request $request, GetDicomSeriesMetadata $getD $getDicomSeriesMetadataRequest->seriesInstanceUID = $seriesInstanceUID; $getDicomSeriesMetadataRequest->role = $queryParam['role']; + $getDicomSeriesMetadataRequest->studyName = $queryParam['studyName']; $getDicomSeriesMetadataRequest->currentUserId = $currentUser['id']; $getDicomSeriesMetadata->execute($getDicomSeriesMetadataRequest, $getDicomSeriesMetadataResponse); @@ -201,6 +204,7 @@ public function getSeriesPreview(Request $request, GetDicomSeriesPreview $getDic $queryParam = $request->query(); $getDicomSeriesPreviewRequest->role = $queryParam['role']; + $getDicomSeriesPreviewRequest->studyName = $queryParam['studyName']; $getDicomSeriesPreviewRequest->seriesInstanceUID = $seriesInstanceUID; $getDicomSeriesPreviewRequest->index = $index; $getDicomSeriesPreviewRequest->currentUserId = $currentUser['id']; diff --git a/GaelO2/app/Jobs/JobQcReport.php b/GaelO2/app/Jobs/JobQcReport.php index da2bd1ce5..fd8095cb5 100644 --- a/GaelO2/app/Jobs/JobQcReport.php +++ b/GaelO2/app/Jobs/JobQcReport.php @@ -61,7 +61,6 @@ public function handle( $visitEntity = $visitRepositoryInterface->getVisitContext($this->visitId); $studyName = $visitEntity['patient']['study_name']; - $visitId = $visitEntity['id']; $visitType = $visitEntity['visit_type']['name']; $patientCode = $visitEntity['patient']['code']; $visitDate = $visitEntity['visit_date']; From 336e7a60e804f0e6dd869565571fb5325e7a666c Mon Sep 17 00:00:00 2001 From: Salim Kanoun Date: Tue, 16 Jan 2024 08:58:20 +0100 Subject: [PATCH 16/72] add time to live to caching file --- .../app/GaelO/Adapters/FileCacheAdapter.php | 4 +-- .../Interfaces/Adapters/CacheInterface.php | 2 +- .../app/GaelO/Services/FileCacheService.php | 27 ++++++++++++++----- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/GaelO2/app/GaelO/Adapters/FileCacheAdapter.php b/GaelO2/app/GaelO/Adapters/FileCacheAdapter.php index 1ae6d75f2..31c7f17fd 100644 --- a/GaelO2/app/GaelO/Adapters/FileCacheAdapter.php +++ b/GaelO2/app/GaelO/Adapters/FileCacheAdapter.php @@ -14,9 +14,9 @@ public function get(string $key) return Cache::store(Config::get('cache.file-cache'))->get($key); } - public function store(string $key, $value): bool + public function store(string $key, $value, ?int $ttl): bool { - Cache::store(Config::get('cache.file-cache'))->put($key, $value); + Cache::store(Config::get('cache.file-cache'))->put($key, $value, $ttl); return true; } diff --git a/GaelO2/app/GaelO/Interfaces/Adapters/CacheInterface.php b/GaelO2/app/GaelO/Interfaces/Adapters/CacheInterface.php index 3af5e7181..e413babfc 100644 --- a/GaelO2/app/GaelO/Interfaces/Adapters/CacheInterface.php +++ b/GaelO2/app/GaelO/Interfaces/Adapters/CacheInterface.php @@ -4,7 +4,7 @@ interface CacheInterface { - public function store(string $key, $value): bool; + public function store(string $key, $value, ?int $ttl): bool; public function get(string $key); public function delete(string $key): bool; } diff --git a/GaelO2/app/GaelO/Services/FileCacheService.php b/GaelO2/app/GaelO/Services/FileCacheService.php index 02f3081a7..bf47e7769 100644 --- a/GaelO2/app/GaelO/Services/FileCacheService.php +++ b/GaelO2/app/GaelO/Services/FileCacheService.php @@ -3,6 +3,7 @@ namespace App\GaelO\Services; use App\GaelO\Adapters\FileCacheAdapter; +use App\GaelO\Exceptions\GaelONotFoundException; class FileCacheService { @@ -15,12 +16,19 @@ public function __construct(FileCacheAdapter $fileCacheAdapter) public function getSeriesPreview(string $seriesInstanceUID, int $index) { - return $this->fileCacheAdapter->get('preview-' . $seriesInstanceUID . '-' . $index); + $data = $this->fileCacheAdapter->get('preview-' . $seriesInstanceUID . '-' . $index); + if ($data == null) { + throw new GaelONotFoundException(); + } + return $data; } - public function storeSeriesPreview(string $seriesInstanceUID, int $index, $value) + /** + * ttl in seconds may not be supported with all drivers (ok with redis not with Azure) + */ + public function storeSeriesPreview(string $seriesInstanceUID, int $index, $value, ?int $ttl = null) { - return $this->fileCacheAdapter->store('preview-' . $seriesInstanceUID . '-' . $index, $value); + return $this->fileCacheAdapter->store('preview-' . $seriesInstanceUID . '-' . $index, $value, $ttl); } public function deleteSeriesPreview(string $seriesInstanceUID, int $index) @@ -28,14 +36,21 @@ public function deleteSeriesPreview(string $seriesInstanceUID, int $index) return $this->fileCacheAdapter->delete('preview-' . $seriesInstanceUID . '-' . $index); } - public function storeDicomMetadata(string $uid, $value) + /** + * ttl in seconds may not be supported with all drivers (ok with redis not with Azure) + */ + public function storeDicomMetadata(string $uid, $value, ?int $ttl = null) { - return $this->fileCacheAdapter->store('metadata-' . $uid, $value); + return $this->fileCacheAdapter->store('metadata-' . $uid, $value, $ttl); } public function getDicomMetadata(string $uid) { - return $this->fileCacheAdapter->get('metadata-' . $uid); + $data = $this->fileCacheAdapter->get('metadata-' . $uid); + if ($data == null) { + throw new GaelONotFoundException(); + } + return $data; } public function deleteDicomMetadata(string $uid) From 479adf291ceaa3cd72da0d315043cf4e525cde0d Mon Sep 17 00:00:00 2001 From: Salim Kanoun Date: Tue, 16 Jan 2024 09:03:50 +0100 Subject: [PATCH 17/72] fix --- .../UseCases/GetDicomSeriesPreview/GetDicomSeriesPreview.php | 1 + 1 file changed, 1 insertion(+) diff --git a/GaelO2/app/GaelO/UseCases/GetDicomSeriesPreview/GetDicomSeriesPreview.php b/GaelO2/app/GaelO/UseCases/GetDicomSeriesPreview/GetDicomSeriesPreview.php index 2fcee99fa..85b4d03ff 100644 --- a/GaelO2/app/GaelO/UseCases/GetDicomSeriesPreview/GetDicomSeriesPreview.php +++ b/GaelO2/app/GaelO/UseCases/GetDicomSeriesPreview/GetDicomSeriesPreview.php @@ -56,6 +56,7 @@ public function execute(GetDicomSeriesPreviewRequest $getDicomSeriesPreviewReque $getDicomSeriesPreviewResponse->body = $e->getErrorBody(); $getDicomSeriesPreviewResponse->status = $e->statusCode; $getDicomSeriesPreviewResponse->statusText = $e->statusText; + $getDicomSeriesPreviewResponse->contentType = "application/json"; } catch (Exception $e) { throw $e; } From 7ee89c484b88d4ad07f622e0bd00988c2e7d4b37 Mon Sep 17 00:00:00 2001 From: Salim Kanoun Date: Tue, 16 Jan 2024 09:06:28 +0100 Subject: [PATCH 18/72] fix tests --- GaelO2/tests/Unit/TestAdapters/FileCacheAdapterTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/GaelO2/tests/Unit/TestAdapters/FileCacheAdapterTest.php b/GaelO2/tests/Unit/TestAdapters/FileCacheAdapterTest.php index 9c6c585a3..5497de3e8 100644 --- a/GaelO2/tests/Unit/TestAdapters/FileCacheAdapterTest.php +++ b/GaelO2/tests/Unit/TestAdapters/FileCacheAdapterTest.php @@ -19,20 +19,20 @@ protected function setUp(): void public function testReadFile() { - $this->fileCacheAdapter->store('keyFile2', 'payload'); + $this->fileCacheAdapter->store('keyFile2', 'payload', null); $content = $this->fileCacheAdapter->get('keyFile2'); $this->assertEquals('payload', $content); } public function testStoreFile() { - $success = $this->fileCacheAdapter->store('keyFile', 'payload'); + $success = $this->fileCacheAdapter->store('keyFile', 'payload', null); $this->assertTrue($success); } public function testDeleteFile() { - $this->fileCacheAdapter->store('keyFile3', 'payload'); + $this->fileCacheAdapter->store('keyFile3', 'payload', null); $success = $this->fileCacheAdapter->delete('keyFile3'); $this->assertTrue($success); } From d535b5238f28fcadc40c02bd2012bc0fb4d74702 Mon Sep 17 00:00:00 2001 From: Salim Kanoun Date: Tue, 16 Jan 2024 10:26:58 +0100 Subject: [PATCH 19/72] fix read from azure --- GaelO2/app/GaelO/Adapters/AzureCacheAdapter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GaelO2/app/GaelO/Adapters/AzureCacheAdapter.php b/GaelO2/app/GaelO/Adapters/AzureCacheAdapter.php index 5a022a238..976859096 100644 --- a/GaelO2/app/GaelO/Adapters/AzureCacheAdapter.php +++ b/GaelO2/app/GaelO/Adapters/AzureCacheAdapter.php @@ -36,7 +36,7 @@ public function get($key) throw new GaelONotFoundException("File doesn't exist in azure cache"); } - $this->fileSystem->read($path); + return $this->fileSystem->read($path); } public function many(array $keys) From b00f24a656f0ed296322c65b2b72360713192821 Mon Sep 17 00:00:00 2001 From: Salim Kanoun Date: Tue, 16 Jan 2024 10:39:45 +0100 Subject: [PATCH 20/72] try fix --- GaelO2/app/GaelO/Adapters/AzureCacheAdapter.php | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/GaelO2/app/GaelO/Adapters/AzureCacheAdapter.php b/GaelO2/app/GaelO/Adapters/AzureCacheAdapter.php index 976859096..11e64a50c 100644 --- a/GaelO2/app/GaelO/Adapters/AzureCacheAdapter.php +++ b/GaelO2/app/GaelO/Adapters/AzureCacheAdapter.php @@ -6,6 +6,7 @@ use App\GaelO\Exceptions\GaelONotFoundException; use Illuminate\Contracts\Cache\Store; use League\Flysystem\Filesystem; +use Throwable; /** * File Cache using Azure blob storage, this cache adapter does not implement auto delete of files which should be @@ -31,12 +32,11 @@ private function getPath(string $key) public function get($key) { $path = $this->getPath($key); - - if ($this->fileSystem->fileExists($path)) { + try { + return $this->fileSystem->read($path); + } catch (Throwable $e) { throw new GaelONotFoundException("File doesn't exist in azure cache"); } - - return $this->fileSystem->read($path); } public function many(array $keys) @@ -83,8 +83,12 @@ public function forever($key, $value) public function forget($key) { $path = $this->getPath($key); - $this->fileSystem->delete($path); - return true; + try { + $this->fileSystem->delete($path); + return true; + } catch (Throwable $t) { + throw new GaelONotFoundException("File cache delete file"); + } } public function flush() From c999888488151dc8ee3cf4921940bee238ce1586 Mon Sep 17 00:00:00 2001 From: salim kanoun Date: Wed, 17 Jan 2024 16:33:41 +0100 Subject: [PATCH 21/72] tmtv stored in cache file --- .../app/GaelO/Services/FileCacheService.php | 35 +++++++- .../TmtvProcessingService.php | 13 +++ GaelO2/app/GaelO/Services/MailServices.php | 8 +- .../GetDicomSeriesTmtvReport.php | 83 +++++++++++++++++++ .../GetDicomSeriesTmtvReportRequest.php | 13 +++ .../GetDicomSeriesTmtvReportResponse.php | 11 +++ .../mails/mail_radiomics_report.blade.php | 70 ++-------------- .../views/mails/mjml/radiomics_report.mjml | 37 +-------- .../app/Http/Controllers/DicomController.php | 22 ++++- GaelO2/routes/api.php | 1 + .../TestDicoms/DicomTmtvReportTest.php | 65 +++++++++++++++ 11 files changed, 254 insertions(+), 104 deletions(-) create mode 100644 GaelO2/app/GaelO/UseCases/GetDicomSeriesTmtvReport/GetDicomSeriesTmtvReport.php create mode 100644 GaelO2/app/GaelO/UseCases/GetDicomSeriesTmtvReport/GetDicomSeriesTmtvReportRequest.php create mode 100644 GaelO2/app/GaelO/UseCases/GetDicomSeriesTmtvReport/GetDicomSeriesTmtvReportResponse.php create mode 100644 GaelO2/tests/Feature/TestDicoms/DicomTmtvReportTest.php diff --git a/GaelO2/app/GaelO/Services/FileCacheService.php b/GaelO2/app/GaelO/Services/FileCacheService.php index bf47e7769..da8470980 100644 --- a/GaelO2/app/GaelO/Services/FileCacheService.php +++ b/GaelO2/app/GaelO/Services/FileCacheService.php @@ -31,9 +31,42 @@ public function storeSeriesPreview(string $seriesInstanceUID, int $index, $value return $this->fileCacheAdapter->store('preview-' . $seriesInstanceUID . '-' . $index, $value, $ttl); } + /** + * Methodology should be 41 / 25 / 4 + */ + public function storeTmtvPreview(string $seriesInstanceUID, string $methodology, $value, ?int $ttl = null) + { + return $this->fileCacheAdapter->store('tmtv-preview-' . $seriesInstanceUID . '-' . $methodology, $value, $ttl); + } + + public function storeTmtvResults(string $seriesInstanceUID, string $methodology, $value, ?int $ttl = null) + { + return $this->fileCacheAdapter->store('tmtv-results-' . $seriesInstanceUID . '-' . $methodology, $value, $ttl); + } + public function deleteSeriesPreview(string $seriesInstanceUID, int $index) { - return $this->fileCacheAdapter->delete('preview-' . $seriesInstanceUID . '-' . $index); + return $this->fileCacheAdapter->delete('tmtv-preview-' . $seriesInstanceUID . '-' . $index); + } + + public function deleteTmtvResults(string $seriesInstanceUID, int $index) + { + return $this->fileCacheAdapter->delete('tmtv-results-' . $seriesInstanceUID . '-' . $index); + } + + public function deleteTmtvPreview(string $seriesInstanceUID, string $methodology) + { + return $this->fileCacheAdapter->delete('tmtv-' . $seriesInstanceUID . '-' . $methodology); + } + + public function getTmtvResults(string $seriesInstanceUID, string $methodology) + { + return $this->fileCacheAdapter->get('tmtv-results-' . $seriesInstanceUID . '-' . $methodology); + } + + public function getTmtvPreview(string $seriesInstanceUID, string $methodology) + { + return $this->fileCacheAdapter->get('tmtv-preview-' . $seriesInstanceUID . '-' . $methodology); } /** diff --git a/GaelO2/app/GaelO/Services/GaelOProcessingService/TmtvProcessingService.php b/GaelO2/app/GaelO/Services/GaelOProcessingService/TmtvProcessingService.php index c63265676..06b978bbd 100644 --- a/GaelO2/app/GaelO/Services/GaelOProcessingService/TmtvProcessingService.php +++ b/GaelO2/app/GaelO/Services/GaelOProcessingService/TmtvProcessingService.php @@ -16,6 +16,8 @@ class TmtvProcessingService private GaelOProcessingService $gaelOProcessingService; private string $ptOrthancSeriesId; private string $ctOrthancSeriesId; + private string $ptSeriesUid; + private string $ctSeriesUid; private array $createdFiles = []; @@ -63,15 +65,19 @@ public function loadPetAndCtSeriesOrthancIdsFromVisit($visitId): void $dicomStudyEntity = $this->dicomStudyRepositoryInterface->getDicomsDataFromVisit($visitId, false, false); $idPT = null; + $ptSeriesUid = null; $idCT = null; + $ctSeriesUid = null; foreach ($dicomStudyEntity[0]['dicom_series'] as $series) { if ($series['modality'] == 'PT') { if ($idPT) throw new GaelOException('Multiple PET Series, unable to perform segmentation'); $idPT = $series['orthanc_id']; + $ptSeriesUid = $series['series_uid']; } if ($series['modality'] == 'CT') { if ($idCT) throw new GaelOException('Multiple CT Series, unable to perform segmentation'); $idCT = $series['orthanc_id']; + $ctSeriesUid = $series['series_uid']; } } @@ -82,6 +88,13 @@ public function loadPetAndCtSeriesOrthancIdsFromVisit($visitId): void $this->ctOrthancSeriesId = $idCT; $this->ptOrthancSeriesId = $idPT; + $this->ptSeriesUid = $ptSeriesUid; + $this->ctSeriesUid = $ctSeriesUid; + } + + public function getInferedPtSeriesUid(): string + { + return $this->ptSeriesUid; } public function addCreatedRessource(string $type, string $id) diff --git a/GaelO2/app/GaelO/Services/MailServices.php b/GaelO2/app/GaelO/Services/MailServices.php index f5d3a7823..e171cf13a 100644 --- a/GaelO2/app/GaelO/Services/MailServices.php +++ b/GaelO2/app/GaelO/Services/MailServices.php @@ -607,18 +607,16 @@ public function sendQcReport(string $studyName, string $visitType, string $patie $this->mailInterface->send(); } - public function sendRadiomicsReport(string $studyName, string $patientCode, string $visitType, string $visitDate, string $imagePath, array $stats, array $emailList) + public function sendRadiomicsReport(string $studyName, string $patientCode, string $visitType, string $magicLink, string $email) { $parameters = [ 'patientCode' => $patientCode, 'visitType' => $visitType, 'studyName' => $studyName, - 'visitDate' => $visitDate, - 'image_path' => [$imagePath], - 'stats' => $stats + 'magicLink' => $magicLink, ]; - $this->mailInterface->setTo($emailList); + $this->mailInterface->setTo([$email]); $this->mailInterface->setReplyTo($this->getStudyContactEmail($studyName)); $this->mailInterface->setParameters($parameters); $this->mailInterface->setBody(MailConstants::EMAIL_RADIOMICS_REPORT); diff --git a/GaelO2/app/GaelO/UseCases/GetDicomSeriesTmtvReport/GetDicomSeriesTmtvReport.php b/GaelO2/app/GaelO/UseCases/GetDicomSeriesTmtvReport/GetDicomSeriesTmtvReport.php new file mode 100644 index 000000000..4d2d76aab --- /dev/null +++ b/GaelO2/app/GaelO/UseCases/GetDicomSeriesTmtvReport/GetDicomSeriesTmtvReport.php @@ -0,0 +1,83 @@ +authorizationVisitService = $authorizationVisitService; + $this->fileCacheService = $fileCacheService; + $this->dicomSeriesRepository = $dicomSeriesRepository; + $this->visitRepositoryInterface = $visitRepositoryInterface; + } + + + public function execute(GetDicomSeriesTmtvReportRequest $getDicomSeriesTmtvReportRequest, GetDicomSeriesTmtvReportResponse $getDicomSeriesTmtvReportResponse) + { + + try { + $curentUserId = $getDicomSeriesTmtvReportRequest->currentUserId; + $role = $getDicomSeriesTmtvReportRequest->role; + $seriesInstanceUID = $getDicomSeriesTmtvReportRequest->seriesInstanceUID; + $studyName = $getDicomSeriesTmtvReportRequest->studyName; + $type = $getDicomSeriesTmtvReportRequest->type; + $methodology = $getDicomSeriesTmtvReportRequest->methodology; + + $seriesData = $this->dicomSeriesRepository->getSeries($seriesInstanceUID, false); + $visitId = $seriesData['dicom_study']['visit_id']; + + $visitContext = $this->visitRepositoryInterface->getVisitContext($visitId); + + $this->checkAuthorization($curentUserId, $role, $visitId, $studyName, $visitContext); + + if ($type === "stats") { + $previewData = $this->fileCacheService->getTmtvResults($seriesInstanceUID, $methodology); + } else { + $previewData = $this->fileCacheService->getTmtvPreview($seriesInstanceUID, $methodology); + } + + + $finfo = finfo_open(); + $mimeType = finfo_buffer($finfo, $previewData, FILEINFO_MIME_TYPE); + finfo_close($finfo); + + $getDicomSeriesTmtvReportResponse->body = $previewData; + $getDicomSeriesTmtvReportResponse->status = 200; + $getDicomSeriesTmtvReportResponse->statusText = 'OK'; + $getDicomSeriesTmtvReportResponse->contentType = $mimeType; + } catch (AbstractGaelOException $e) { + $getDicomSeriesTmtvReportResponse->body = $e->getErrorBody(); + $getDicomSeriesTmtvReportResponse->status = $e->statusCode; + $getDicomSeriesTmtvReportResponse->statusText = $e->statusText; + $getDicomSeriesTmtvReportResponse->contentType = "application/json"; + } catch (Exception $e) { + throw $e; + } + } + + private function checkAuthorization(int $userId, string $role, int $visitId, string $studyName, array $visitContext): void + { + $this->authorizationVisitService->setUserId($userId); + $this->authorizationVisitService->setVisitId($visitId); + $this->authorizationVisitService->setStudyName($studyName); + $this->authorizationVisitService->setVisitContext($visitContext); + + if (!$this->authorizationVisitService->isVisitAllowed($role)) { + throw new GaelOForbiddenException(); + } + } +} diff --git a/GaelO2/app/GaelO/UseCases/GetDicomSeriesTmtvReport/GetDicomSeriesTmtvReportRequest.php b/GaelO2/app/GaelO/UseCases/GetDicomSeriesTmtvReport/GetDicomSeriesTmtvReportRequest.php new file mode 100644 index 000000000..fe09f2545 --- /dev/null +++ b/GaelO2/app/GaelO/UseCases/GetDicomSeriesTmtvReport/GetDicomSeriesTmtvReportRequest.php @@ -0,0 +1,13 @@ +