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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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)
-
- {{ $key }}
-
- {{ var_export($value) }}
-
- @endforeach
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
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)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- @endforeach
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- @foreach ($series as $key => $value)
- @if ($value != null && $key !== 'image_path')
-
- {{ $key }}
-
- {{ $value }}
-
- @endif
- @endforeach
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
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)
-
- {{ $key }}
-
- {{ $value }}
-
- @endif
- @endforeach
-
-
-
-
-
-
-
-
-
-
-
-
-
- @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 @@