diff --git a/.github/workflows/clean-packages.yml b/.github/workflows/clean-packages.yml index fe16d64b1..e90eb324d 100644 --- a/.github/workflows/clean-packages.yml +++ b/.github/workflows/clean-packages.yml @@ -11,10 +11,10 @@ jobs: runs-on: ubuntu-latest steps: - name: 'Build checkout' - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Login to github registery - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: registry: ghcr.io username: 'salimkanoun' diff --git a/.github/workflows/php-stan.yml b/.github/workflows/php-stan.yml index baa0c4db5..aeccee0c1 100644 --- a/.github/workflows/php-stan.yml +++ b/.github/workflows/php-stan.yml @@ -14,12 +14,12 @@ jobs: runs-on: ubuntu-latest steps: - name: 'Build checkout' - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: 'Setup PHP' uses: shivammathur/setup-php@v2 with: - php-version: '8.2' + php-version: '8.3' - name: 'Copy .env' working-directory: ./GaelO2 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 6a2125460..fa6401ba9 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -14,11 +14,11 @@ jobs: runs-on: ubuntu-latest steps: - name: 'Build checkout' - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Docker meta id: meta - uses: docker/metadata-action@v4 + uses: docker/metadata-action@v5 with: images: ghcr.io/pixilib/gaelo tags: | @@ -27,17 +27,17 @@ jobs: v2-latest - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Login to github registery - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: registry: ghcr.io username: 'salimkanoun' password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v5 with: context: . push: true diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0a8712d2f..3852e4d36 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,12 +15,12 @@ jobs: runs-on: ubuntu-latest steps: - name: 'Build checkout' - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: 'Setup PHP' uses: shivammathur/setup-php@v2 with: - php-version: '8.2' + php-version: '8.3' - name: 'Copy .env' working-directory: ./GaelO2 diff --git a/Dockerfile b/Dockerfile index bfe32e1b4..2ffee4adf 100755 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,11 @@ -FROM php:8.2.13-apache-bullseye +FROM php:8.3.2-fpm-bullseye ENV PHP_OPCACHE_VALIDATE_TIMESTAMPS="0" ENV TZ="UTC" RUN apt-get update -qy && \ apt-get install -y --no-install-recommends apt-utils\ + nginx \ git \ cron \ nano \ @@ -16,6 +17,7 @@ RUN apt-get update -qy && \ libbz2-dev \ libmcrypt-dev \ libxml2-dev \ + libcurl4-gnutls-dev \ openssl \ sqlite3 \ supervisor \ @@ -27,46 +29,44 @@ RUN apt-get update -qy && \ apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* RUN pecl install pcov redis memcached -RUN docker-php-ext-install gd zip pdo pdo_mysql pdo_pgsql mbstring bcmath ctype fileinfo xml bz2 pcntl +RUN docker-php-ext-install gd zip pdo pdo_mysql pdo_pgsql mbstring bcmath ctype fileinfo xml bz2 pcntl curl ftp RUN docker-php-ext-configure opcache --enable-opcache \ && docker-php-ext-install opcache RUN docker-php-ext-enable redis memcached pcov -COPY php.ini "$PHP_INI_DIR/php.ini" - RUN curl -s https://getcomposer.org/installer | php -- --install-dir=/usr/bin/ --filename=composer -COPY vhost.conf /etc/apache2/sites-available/000-default.conf -COPY apache.conf /etc/apache2/conf-available/zgaelo.conf - -RUN a2enmod rewrite -RUN a2enmod headers -RUN a2enmod remoteip -RUN a2enmod deflate -RUN a2enmod http2 - -RUN a2enconf zgaelo +# Copy configuration files. +COPY php.ini /usr/local/etc/php/php.ini +COPY php-fpm.conf /usr/local/etc/php-fpm.d/www.conf +COPY nginx.conf /etc/nginx/nginx.conf -ENV APP_HOME /var/www/html +ENV APP_HOME /var/www ENV COMPOSER_ALLOW_SUPERUSER=1 WORKDIR $APP_HOME COPY docker_start.sh /usr/local/bin/start -COPY --chown=www-data:www-data GaelO2 . +COPY GaelO2 . COPY laravel-worker.conf /etc/supervisor/conf.d RUN mv .env.example .env RUN composer install --optimize-autoloader --no-interaction -# docker_start.sh COPY docker_start.sh /usr/local/bin/start RUN chmod u+x /usr/local/bin/start -EXPOSE 80 +# Adjust user permission & group +RUN usermod --uid 1000 www-data +RUN groupmod --gid 1001 www-data + +# Set correct permission. +RUN chown -R www-data:www-data $APP_HOME +RUN chmod -R 755 /var/www/storage +RUN chmod -R 755 /var/www/bootstrap -RUN service apache2 restart +EXPOSE 80 ENTRYPOINT ["/usr/local/bin/start"] diff --git a/GaelO2/.env.example b/GaelO2/.env.example index caddf52d1..382dd1c07 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='file' BROADCAST_DRIVER=log CACHE_DRIVER=file diff --git a/GaelO2/.env.testing b/GaelO2/.env.testing new file mode 100644 index 000000000..48f4f5c12 --- /dev/null +++ b/GaelO2/.env.testing @@ -0,0 +1,73 @@ +APP_NAME=GaelO +APP_CORPORATION=Pixilib +APP_ENV="testing" +APP_KEY=base64:Eqa/LcdPhfxg6PQ9uytXcu5WoMN8p5Ms2rlBywsOq08= +APP_DEBUG=false +TZ=UTC +DB_CONNECTION="sqlite_testing" +APP_URL='http://localhost:80' +LOG_CHANNEL=stderr + +TUS_URL='http://localhost:1080' + +ORTHANC_TEMPORARY_URL='http://localhost:8042' +ORTHANC_TEMPORARY_LOGIN='login' +ORTHANC_TEMPORARY_PASSWORD='password' + +ORTHANC_STORAGE_URL='http://localhost:8043' +ORTHANC_STORAGE_LOGIN='login' +ORTHANC_STORAGE_PASSWORD='password' + +GAELO_PROCESSING_URL='http://gaeloprocessing:8000' +GAELO_PROCESSING_LOGIN='login' +GAELO_PROCESSING_PASSWORD='password' + +AZURE_CLIENT_ID='' +AZURE_DIRECTORY_ID='' +AZURE_CLIENT_SECRET='' +AZURE_SUBSCRIPTION_ID='' +AZURE_CONTAINER_GROUP='' +AZURE_RESOURCE_GROUP='' + +FILESYSTEM_DISK='local' +AZURE_BLOB_DSN='' +AZURE_CONTAINER_NAME='' +AZURE_BLOB_PREFIX='' +AZURE_FILE_CACHE_CONTAINER_NAME='' +FILE_CACHE_DRIVER='file' + +BROADCAST_DRIVER=log +CACHE_DRIVER=file +QUEUE_CONNECTION=sync +SESSION_DRIVER=array +SESSION_LIFETIME=120 +SESSION_SECURE_COOKIE=true + +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +MAIL_MAILER=array +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_ENCRYPTION=null +MAIL_FROM_ADDRESS="notifications@gaelo.fr" +MAIL_FROM_NAME="${APP_NAME}" +MAIL_REPLY_TO_DEFAULT="support@gaelo.fr" + +SENTRY_DSN=null +SENTRY_ENVIRONMENT=null +SENTRY_TRACES_SAMPLE_RATE=0.0 + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 +AWS_BUCKET= + +PUSHER_APP_ID= +PUSHER_APP_KEY= +PUSHER_APP_SECRET= +PUSHER_APP_CLUSTER=mt1 + +MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}" +MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" \ No newline at end of file diff --git a/GaelO2/app/Console/Commands/DeleteStudy.php b/GaelO2/app/Console/Commands/DeleteStudy.php index 60ab75ea2..db20bdd11 100644 --- a/GaelO2/app/Console/Commands/DeleteStudy.php +++ b/GaelO2/app/Console/Commands/DeleteStudy.php @@ -4,49 +4,33 @@ 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\Log; -use Illuminate\Support\Facades\Storage; 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; + private GaelODeleteRessourcesRepository $gaelODeleteRessourcesRepository; /** * The name and signature of the console command. * * @var string */ - protected $signature = 'gaelo:delete-study {studyName : the study name to delete} {--deleteDicom : delete dicom in Orthanc} { --deleteAssociatedFile : delete associated files}'; + protected $signature = 'gaelo:delete-study {studyName : the study name to delete}'; /** * The console command description. * * @var string */ - protected $description = 'Delete a Study from GaelO (hard delete)'; + protected $description = 'Delete a Study from GaelO (hard delete), including DICOM and associated files'; /** * Execute the console command. @@ -55,30 +39,16 @@ 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, + GaelODeleteRessourcesRepository $gaelODeleteRessourcesRepository + ) { $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->gaelODeleteRessourcesRepository = $gaelODeleteRessourcesRepository; $this->orthancService->setOrthancServer(true); $studyName = $this->argument('studyName'); @@ -106,9 +76,9 @@ 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); + $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; @@ -118,10 +88,9 @@ public function handle( return $visit['id']; }, $visits->toArray()); - - $this->deleteReviews($visitIds, $studyName); - $this->deleteReviewStatus($visitIds, $studyName); - + $this->gaelODeleteRessourcesRepository->deleteReviews($visitIds, $studyName); + $this->gaelODeleteRessourcesRepository->deleteReviewStatus($visitIds, $studyName); + if ($studyEntity['ancillary_of'] === null) { $dicomSeries = $this->getDicomSeriesOfVisits($visitIds); @@ -129,38 +98,32 @@ public function handle( $orthancIdArray = array_map(function ($seriesId) { return $seriesId['orthanc_id']; }, $dicomSeries); - + $this->line(implode(" ", $orthancIdArray)); - + $this->table( ['orthanc_id'], $dicomSeries ); - - $this->deleteDicomsSeries($visitIds); - $this->deleteDicomsStudies($visitIds); - $this->deleteVisits($visitIds); - $this->deleteVisitGroupAndVisitType($studyName); - $this->deletePatient($studyName); + $this->gaelODeleteRessourcesRepository->deleteDicomsSeries($visitIds); + $this->gaelODeleteRessourcesRepository->deleteDicomsStudies($visitIds); + $this->gaelODeleteRessourcesRepository->deleteVisits($visitIds); + $this->gaelODeleteRessourcesRepository->deleteVisitGroupAndVisitType($studyName); + $this->gaelODeleteRessourcesRepository->deleteAllPatientsOfStudy($studyName); } - $studyEntity->forceDelete(); + $this->gaelODeleteRessourcesRepository->deleteStudy($studyName); - 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){ - Log::error($e->getMessage()); - } + 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 ?')){ - Storage::deleteDirectory($studyName); - } - $this->info('The command was successful !'); } @@ -182,61 +145,4 @@ private function getVisitsOfStudy(string $studyName) $query->where('study_name', $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/DeleteVisitsOlderThan.php b/GaelO2/app/Console/Commands/DeleteVisitsOlderThan.php new file mode 100644 index 000000000..f22ebe7ef --- /dev/null +++ b/GaelO2/app/Console/Commands/DeleteVisitsOlderThan.php @@ -0,0 +1,144 @@ +study = $study; + $this->visit = $visit; + $this->dicomSeries = $dicomSeries; + $this->orthancService = $orthancService; + $this->gaelODeleteRessourcesRepository = $gaelODeleteRessourcesRepository; + $this->orthancService->setOrthancServer(true); + + $studyName = $this->argument('studyName'); + $numberOfDays = $this->argument('numberOfDays'); + $force = $this->option('force'); + + if (!$force) { + $studyNameConfirmation = $this->ask('Warning : Please confirm study Name'); + + if ($studyName !== $studyNameConfirmation) { + $this->error('Wrong study name, terminating'); + return 0; + } + + if (!$this->confirm('Warning : This CANNOT be undone, do you wish to continue?')) { + 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; + } + + //Get visits created more than 5 day + $visits = $this->getOlderVisitsOfStudy($studyName, date('Y.m.d', strtotime("-" . $numberOfDays . " days"))); + + $visitIds = array_map(function ($visit) { + return $visit['id']; + }, $visits->toArray()); + + $patientIds = array_map(function ($visit) { + return $visit['patient']['id']; + }, $visits->toArray()); + + $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->info(implode(",", $visitIds)); + + $this->gaelODeleteRessourcesRepository->deleteDicomsSeries($visitIds); + $this->gaelODeleteRessourcesRepository->deleteDicomsStudies($visitIds); + $this->gaelODeleteRessourcesRepository->deleteVisits($visitIds); + //Remove patients with no visits + $this->gaelODeleteRessourcesRepository->deletePatientsWithNoVisits($patientIds); + + foreach ($orthancIdArray as $seriesOrthancId) { + try { + $this->info('Deleting ' . $seriesOrthancId); + $this->orthancService->deleteFromOrthanc('series', $seriesOrthancId); + } catch (Exception $e) { + Log::error($e->getMessage()); + } + } + + + $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 getOlderVisitsOfStudy(string $studyName, string $datelimit) + { + 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 new file mode 100644 index 000000000..9223e8cd1 --- /dev/null +++ b/GaelO2/app/Console/Commands/GaelODeleteRessourcesRepository.php @@ -0,0 +1,152 @@ +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->frameworkInterface = $frameworkInterface; + } + + public function deleteDocumentation(string $studyName) + { + $documentations = $this->documentation->where('study_name', $studyName)->withTrashed(); + foreach($documentations as $documentation){ + $this->frameworkInterface->deleteFile($documentation['path']); + $documentation->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 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(); + } + + public function deleteDicomsSeries(array $visitId) + { + return $this->dicomSeries->whereHas('dicomStudy', function ($query) use ($visitId) { + $query->whereIn('visit_id', $visitId)->withTrashed(); + })->withTrashed()->forceDelete(); + } + + public function deleteAllPatientsOfStudy(string $studyName) + { + $this->patient->where('study_name', $studyName)->delete(); + } + + public function deletePatientsWithNoVisits(array $patientIds) + { + $this->patient->whereIn('id', $patientIds)->doesntHave('visits')->delete(); + } + + public function deleteReviews(array $visitIds, string $studyName) + { + $reviews = $this->review->where('study_name', $studyName)->whereIn('visit_id', $visitIds)->withTrashed()->get(); + foreach ($reviews as $review) { + $files = $review['sent_files']; + foreach ($files as $key => $path) { + $this->frameworkInterface->deleteFile($path); + } + $review->forceDelete(); + } + } + + 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) + { + $visits = $this->visit->whereIn('id', $visitIds)->withTrashed()->get(); + foreach ($visits as $visit) { + $files = $visit['sent_files']; + foreach ($files as $key => $path) { + $this->frameworkInterface->deleteFile($path); + } + $visit->forceDelete(); + } + } + + public function deleteVisitGroupAndVisitType(string $studyName) + { + $visitGroups = $this->visitGroup->where('study_name', $studyName)->get(); + foreach ($visitGroups as $visitGroup) { + $visitGroup->visitTypes()->delete(); + $visitGroup->delete(); + } + } +} diff --git a/GaelO2/app/Console/Kernel.php b/GaelO2/app/Console/Kernel.php index 28a137e7d..21641d1d3 100644 --- a/GaelO2/app/Console/Kernel.php +++ b/GaelO2/app/Console/Kernel.php @@ -5,7 +5,6 @@ use App\GaelO\CronJobs\GaelOScheduler; use Illuminate\Console\Scheduling\Schedule; use Illuminate\Foundation\Console\Kernel as ConsoleKernel; -use Illuminate\Support\Facades\Log; use DateTimeZone; class Kernel extends ConsoleKernel @@ -31,12 +30,6 @@ protected function schedule(Schedule $schedule) $schedule->command('cache:prune-stale-tags')->hourly(); //Register custom jobs GaelOScheduler::registerScheduledJobs($schedule); - //Scheduler Probe for monitoring - $schedule->call(function () { - Log::info("Scheduler Probe"); - }) - ->hourly() - ->sentryMonitor('gaelo-scheduler'); } /** diff --git a/GaelO2/app/GaelO/Adapters/AzureCacheAdapter.php b/GaelO2/app/GaelO/Adapters/AzureCacheAdapter.php new file mode 100644 index 000000000..11e64a50c --- /dev/null +++ b/GaelO2/app/GaelO/Adapters/AzureCacheAdapter.php @@ -0,0 +1,107 @@ +fileSystem = $fileSystem; + } + + private function getPath(string $key) + { + $hash = sha1($key); + return $this->rootDirectory . '/' . $hash; + } + + public function get($key) + { + $path = $this->getPath($key); + try { + return $this->fileSystem->read($path); + } catch (Throwable $e) { + throw new GaelONotFoundException("File doesn't exist in azure cache"); + } + } + + 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); + return true; + } + + public function forget($key) + { + $path = $this->getPath($key); + try { + $this->fileSystem->delete($path); + return true; + } catch (Throwable $t) { + throw new GaelONotFoundException("File cache delete file"); + } + } + + public function flush() + { + $files = $this->fileSystem->listContents($this->rootDirectory); + foreach ($files as $file) { + $this->fileSystem->delete($file->path()); + } + return true; + } + + 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..31c7f17fd --- /dev/null +++ b/GaelO2/app/GaelO/Adapters/FileCacheAdapter.php @@ -0,0 +1,28 @@ +get($key); + } + + public function store(string $key, $value, ?int $ttl): bool + { + Cache::store(Config::get('cache.file-cache'))->put($key, $value, $ttl); + 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/Adapters/FrameworkAdapter.php b/GaelO2/app/GaelO/Adapters/FrameworkAdapter.php index 03d8b6c92..72737fd25 100644 --- a/GaelO2/app/GaelO/Adapters/FrameworkAdapter.php +++ b/GaelO2/app/GaelO/Adapters/FrameworkAdapter.php @@ -46,10 +46,15 @@ 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): mixed { - $file = Storage::get($path); - if($file === null){ + if ($asStream) { + $file = Storage::readStream($path); + } else { + $file = Storage::get($path); + } + + if ($file === null) { throw new GaelOException("File not found in storage"); } return $file; diff --git a/GaelO2/app/GaelO/CronJobs/GaelOScheduler.php b/GaelO2/app/GaelO/CronJobs/GaelOScheduler.php index a50c1d579..873b5c4b6 100644 --- a/GaelO2/app/GaelO/CronJobs/GaelOScheduler.php +++ b/GaelO2/app/GaelO/CronJobs/GaelOScheduler.php @@ -13,11 +13,8 @@ class GaelOScheduler public static function registerScheduledJobs(Schedule $schedule) { - - /* $schedule->call(function () { - Log::info("Scheduled custom job"); - })->everyMinute(); - */ + Log::info("Scheduler Probe"); + })->hourly()->sentryMonitor('gaelo-scheduler'); } } diff --git a/GaelO2/app/GaelO/Interfaces/Adapters/CacheInterface.php b/GaelO2/app/GaelO/Interfaces/Adapters/CacheInterface.php new file mode 100644 index 000000000..e413babfc --- /dev/null +++ b/GaelO2/app/GaelO/Interfaces/Adapters/CacheInterface.php @@ -0,0 +1,10 @@ +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/Services/FileCacheService.php b/GaelO2/app/GaelO/Services/FileCacheService.php new file mode 100644 index 000000000..da8470980 --- /dev/null +++ b/GaelO2/app/GaelO/Services/FileCacheService.php @@ -0,0 +1,93 @@ +fileCacheAdapter = $fileCacheAdapter; + } + + public function getSeriesPreview(string $seriesInstanceUID, int $index) + { + $data = $this->fileCacheAdapter->get('preview-' . $seriesInstanceUID . '-' . $index); + if ($data == null) { + throw new GaelONotFoundException(); + } + return $data; + } + + /** + * 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, $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('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); + } + + /** + * 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, $ttl); + } + + public function getDicomMetadata(string $uid) + { + $data = $this->fileCacheAdapter->get('metadata-' . $uid); + if ($data == null) { + throw new GaelONotFoundException(); + } + return $data; + } + + 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 00393f282..518180a55 100644 --- a/GaelO2/app/GaelO/Services/GaelOProcessingService/GaelOProcessingService.php +++ b/GaelO2/app/GaelO/Services/GaelOProcessingService/GaelOProcessingService.php @@ -49,11 +49,24 @@ public function executeInference(string $modelName, array $payload) return $request->getJsonBody(); } - public function createMIPForSeries(string $seriesId, array $payload = []): string + /** + * Return gif + */ + 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; + } + + /** + * 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; } @@ -61,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; } @@ -69,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/GaelOProcessingService/TmtvProcessingService.php b/GaelO2/app/GaelO/Services/GaelOProcessingService/TmtvProcessingService.php index a94cb6d33..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 = []; @@ -33,8 +35,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,30 +60,24 @@ 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); $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']; } } @@ -90,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 d01915dcd..949ccde80 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]); @@ -617,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, int $userId) { $parameters = [ 'patientCode' => $patientCode, 'visitType' => $visitType, 'studyName' => $studyName, - 'visitDate' => $visitDate, - 'image_path' => [$imagePath], - 'stats' => $stats + 'magicLink' => $magicLink, ]; - $this->mailInterface->setTo($emailList); + $this->mailInterface->setTo([$this->getUserEmail($userId)]); $this->mailInterface->setReplyTo($this->getStudyContactEmail($studyName)); $this->mailInterface->setParameters($parameters); $this->mailInterface->setBody(MailConstants::EMAIL_RADIOMICS_REPORT); 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/GaelO/Services/PdfServices.php b/GaelO2/app/GaelO/Services/PdfServices.php index 4b0384fa6..0cb9c127a 100644 --- a/GaelO2/app/GaelO/Services/PdfServices.php +++ b/GaelO2/app/GaelO/Services/PdfServices.php @@ -14,13 +14,13 @@ public function __construct(PdfInterface $pdfInterface) $this->pdfInterface = $pdfInterface; } - public function saveRadiomicsPdf(string $studyName, string $patientCode, string $visitType, string $visitDate, array $stats): string + public function saveRadiomicsPdf(string $studyName, string $patientCode, string $visitType, string $magicLink, array $stats): string { $parameters = [ 'patientCode' => $patientCode, 'visitType' => $visitType, 'studyName' => $studyName, - 'visitDate' => $visitDate, + 'magicLink' => $magicLink, 'stats' => $stats ]; 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/ExportDatabase/ExportDatabase.php b/GaelO2/app/GaelO/UseCases/ExportDatabase/ExportDatabase.php index c28de6fef..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); @@ -38,8 +39,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/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(); diff --git a/GaelO2/app/GaelO/UseCases/GetDicomSeriesMetadata/GetDicomSeriesMetadata.php b/GaelO2/app/GaelO/UseCases/GetDicomSeriesMetadata/GetDicomSeriesMetadata.php new file mode 100644 index 000000000..30cd15b1c --- /dev/null +++ b/GaelO2/app/GaelO/UseCases/GetDicomSeriesMetadata/GetDicomSeriesMetadata.php @@ -0,0 +1,72 @@ +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; + $studyName = $getDicomSeriesMetadataRequest->studyName; + + $seriesData = $this->dicomSeriesRepository->getSeries($seriesInstanceUID, false); + $visitId = $seriesData['dicom_study']['visit_id']; + + $visitContext = $this->visitRepositoryInterface->getVisitContext($visitId); + + $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..88b686dee --- /dev/null +++ b/GaelO2/app/GaelO/UseCases/GetDicomSeriesMetadata/GetDicomSeriesMetadataRequest.php @@ -0,0 +1,11 @@ +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; + $studyName = $getDicomSeriesPreviewRequest->studyName; + + $seriesData = $this->dicomSeriesRepository->getSeries($seriesInstanceUID, false); + $visitId = $seriesData['dicom_study']['visit_id']; + + $visitContext = $this->visitRepositoryInterface->getVisitContext($visitId); + + $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; + $getDicomSeriesPreviewResponse->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/GetDicomSeriesPreview/GetDicomSeriesPreviewRequest.php b/GaelO2/app/GaelO/UseCases/GetDicomSeriesPreview/GetDicomSeriesPreviewRequest.php new file mode 100644 index 000000000..fff9cfaeb --- /dev/null +++ b/GaelO2/app/GaelO/UseCases/GetDicomSeriesPreview/GetDicomSeriesPreviewRequest.php @@ -0,0 +1,12 @@ +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 @@ +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; + $studyName = $getDicomStudyMetadataRequest->studyName; + + $studyData = $this->dicomStudyRepositoryInterface->getDicomStudy($studyInstanceUID, false); + $visitId = $studyData['visit_id']; + + $visitContext = $this->visitRepositoryInterface->getVisitContext($visitId); + + $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..d30b93045 --- /dev/null +++ b/GaelO2/app/GaelO/UseCases/GetDicomStudyMetadata/GetDicomStudyMetadataRequest.php @@ -0,0 +1,11 @@ +httpClientInterface->rawRequest('GET', $calledUrl, null, $headers); $responseHeaders = $response->getHeaders(); diff --git a/GaelO2/app/GaelO/Util.php b/GaelO2/app/GaelO/Util.php index f33d287ae..fad808ffc 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); @@ -45,15 +45,24 @@ 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 - $fileContent = FrameworkAdapter::getFile($file); - $zip->addFromString($file, $fileContent); + // 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, 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); } } @@ -131,7 +140,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); } diff --git a/GaelO2/app/GaelO/views/mails/mail_qc_report.blade.php b/GaelO2/app/GaelO/views/mails/mail_qc_report.blade.php index 7e173480e..48fc185a0 100644 --- a/GaelO2/app/GaelO/views/mails/mail_qc_report.blade.php +++ b/GaelO2/app/GaelO/views/mails/mail_qc_report.blade.php @@ -1,116 +1,11 @@ @extends('mails.mail_template') @section('content') - - - - - 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/mail_radiomics_report.blade.php b/GaelO2/app/GaelO/views/mails/mail_radiomics_report.blade.php index 592bf0a42..519a10847 100644 --- a/GaelO2/app/GaelO/views/mails/mail_radiomics_report.blade.php +++ b/GaelO2/app/GaelO/views/mails/mail_radiomics_report.blade.php @@ -81,16 +81,6 @@