diff --git a/docker-compose/database/db-changes/datasets/0036_dicom_condition_change.sql b/docker-compose/database/db-changes/datasets/0036_dicom_condition_change.sql new file mode 100644 index 0000000000..cd7f41964c --- /dev/null +++ b/docker-compose/database/db-changes/datasets/0036_dicom_condition_change.sql @@ -0,0 +1,3 @@ +update study_card_condition set cardinality = 1 where cardinality is NULL; +alter table study_card_condition modify scope varchar(47); +update study_card_condition set scope = 'StudyCardDICOMConditionOnDatasets' where scope like 'StudyCardDICOMCondition'; \ No newline at end of file diff --git a/docker-compose/database/db-changes/datasets/0035_update_datasets_acquisition_equipment.sql b/docker-compose/database/db-changes/datasets/0037_update_datasets_acquisition_equipment.sql similarity index 100% rename from docker-compose/database/db-changes/datasets/0035_update_datasets_acquisition_equipment.sql rename to docker-compose/database/db-changes/datasets/0037_update_datasets_acquisition_equipment.sql diff --git a/docker-compose/database/db-changes/datasets/0038_quality_control_or_conditions.sql b/docker-compose/database/db-changes/datasets/0038_quality_control_or_conditions.sql new file mode 100644 index 0000000000..8060bff09f --- /dev/null +++ b/docker-compose/database/db-changes/datasets/0038_quality_control_or_conditions.sql @@ -0,0 +1,4 @@ +ALTER TABLE `study_card_rule` +ADD COLUMN `or_conditions` bit(1) NOT NULL DEFAULT 0; +ALTER TABLE `quality_examination_rule` +ADD COLUMN `or_conditions` bit(1) NOT NULL DEFAULT 0; diff --git a/docker-compose/database/db-changes/users/0004_task_new_column.sql b/docker-compose/database/db-changes/users/0004_task_new_column.sql new file mode 100644 index 0000000000..3ac2451834 --- /dev/null +++ b/docker-compose/database/db-changes/users/0004_task_new_column.sql @@ -0,0 +1 @@ +alter table events add column report longtext; \ No newline at end of file diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/dataset/model/Dataset.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/dataset/model/Dataset.java index cbec47e808..68839c6a7f 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/dataset/model/Dataset.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/dataset/model/Dataset.java @@ -130,6 +130,10 @@ public abstract class Dataset extends AbstractEntity { @OneToOne(cascade = CascadeType.ALL) private DatasetMetadata updatedMetadata; + @JsonIgnore + @Transient + public String SOPInstanceUID; + /** * @return the creationDate */ @@ -392,4 +396,11 @@ public void setDownloadable(boolean downloadable) { this.downloadable = downloadable; } + public String getSOPInstanceUID() { + return SOPInstanceUID; + } + + public void setSOPInstanceUID(String sOPInstanceUID) { + SOPInstanceUID = sOPInstanceUID; + } } diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/dataset/security/DatasetSecurityService.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/dataset/security/DatasetSecurityService.java index 6b200a0759..822a8f6776 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/dataset/security/DatasetSecurityService.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/dataset/security/DatasetSecurityService.java @@ -174,11 +174,13 @@ public boolean hasRightOnSubjectName(String subjectName, String rightStr) { public boolean hasRightOnSubjectStudies(List subjectstudies, String rightStr) { if (KeycloakUtil.getTokenRoles().contains(ROLE_ADMIN)) { return true; - } - for (SubjectStudy subjectStudy : subjectstudies) { - boolean hasRight = commService.hasRightOnStudy(subjectStudy.getStudy().getId(), rightStr); - if (!hasRight) { - return false; + } + if (subjectstudies != null) { + for (SubjectStudy subjectStudy : subjectstudies) { + boolean hasRight = commService.hasRightOnStudy(subjectStudy.getStudy().getId(), rightStr); + if (!hasRight) { + return false; + } } } return true; diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/datasetacquisition/controler/DatasetAcquisitionApiController.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/datasetacquisition/controler/DatasetAcquisitionApiController.java index e559748a68..c1614477db 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/datasetacquisition/controler/DatasetAcquisitionApiController.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/datasetacquisition/controler/DatasetAcquisitionApiController.java @@ -14,11 +14,10 @@ package org.shanoir.ng.datasetacquisition.controler; -import com.fasterxml.jackson.core.JsonParseException; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.swagger.v3.oas.annotations.Parameter; -import jakarta.validation.Valid; +import java.io.IOException; +import java.util.Comparator; +import java.util.List; + import org.apache.solr.client.solrj.SolrServerException; import org.shanoir.ng.datasetacquisition.dto.DatasetAcquisitionDTO; import org.shanoir.ng.datasetacquisition.dto.DatasetAcquisitionDatasetsDTO; @@ -34,7 +33,11 @@ import org.shanoir.ng.importer.service.ImporterService; import org.shanoir.ng.shared.configuration.RabbitMQConfiguration; import org.shanoir.ng.shared.error.FieldErrorMap; -import org.shanoir.ng.shared.exception.*; +import org.shanoir.ng.shared.exception.EntityNotFoundException; +import org.shanoir.ng.shared.exception.ErrorDetails; +import org.shanoir.ng.shared.exception.ErrorModel; +import org.shanoir.ng.shared.exception.RestServiceException; +import org.shanoir.ng.shared.exception.ShanoirException; import org.shanoir.ng.utils.KeycloakUtil; import org.shanoir.ng.utils.SecurityContextUtil; import org.shanoir.ng.utils.usermock.WithMockKeycloakUser; @@ -55,9 +58,12 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; -import java.io.IOException; -import java.util.Comparator; -import java.util.List; +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.swagger.v3.oas.annotations.Parameter; +import jakarta.validation.Valid; @Controller public class DatasetAcquisitionApiController implements DatasetAcquisitionApi { @@ -116,7 +122,7 @@ public int createNewEegDatasetAcquisition(Message importJobAsString) throws IOEx @Transactional @WithMockKeycloakUser(authorities = { "ROLE_ADMIN" }) public void createNewDatasetAcquisition(Message importJobStr) throws JsonParseException, JsonMappingException, IOException, AmqpRejectAndDontRequeueException { - ImportJob importJob = objectMapper.readValue(importJobStr.getBody(), ImportJob.class); + ImportJob importJob = objectMapper.readValue(importJobStr.getBody(), ImportJob.class); try { createAllDatasetAcquisitions(importJob, importJob.getUserId()); } catch (Exception e) { diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/datasetacquisition/dto/mapper/DatasetAcquisitionDatasetsMapper.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/datasetacquisition/dto/mapper/DatasetAcquisitionDatasetsMapper.java index a7037f01f4..cf184f2b74 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/datasetacquisition/dto/mapper/DatasetAcquisitionDatasetsMapper.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/datasetacquisition/dto/mapper/DatasetAcquisitionDatasetsMapper.java @@ -19,7 +19,6 @@ import org.mapstruct.DecoratedWith; import org.mapstruct.Mapper; import org.mapstruct.MapperConfig; -import org.mapstruct.Mapping; import org.mapstruct.MappingInheritanceStrategy; import org.mapstruct.ObjectFactory; import org.shanoir.ng.dataset.dto.DatasetDTO; diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/dicom/DicomProcessing.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/dicom/DicomProcessing.java index 5aebc9af89..d9b24cc1be 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/dicom/DicomProcessing.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/dicom/DicomProcessing.java @@ -20,7 +20,13 @@ import org.dcm4che3.data.Attributes; import org.dcm4che3.data.Tag; import org.dcm4che3.io.DicomInputStream; +import org.shanoir.ng.download.AcquisitionAttributes; +import org.shanoir.ng.download.ExaminationAttributes; +import org.shanoir.ng.importer.dto.Dataset; import org.shanoir.ng.importer.dto.DatasetFile; +import org.shanoir.ng.importer.dto.Serie; +import org.shanoir.ng.importer.dto.Study; +import org.shanoir.ng.shared.exception.ShanoirException; import org.springframework.stereotype.Service; @Service @@ -41,4 +47,40 @@ public Attributes getDicomObjectAttributes(DatasetFile image, Boolean isEnhanced } } + public ExaminationAttributes getDicomExaminationAttributes(Study study, Boolean isEnhanced) throws ShanoirException { + ExaminationAttributes attributes = new ExaminationAttributes(); + if (study != null) { + for (Serie serie : study.getSeries()) { + attributes.addAcquisitionAttributes(serie.getSeriesInstanceUID(), getDicomAcquisitionAttributes(serie, isEnhanced)); + } + } + return attributes; + } + + public ExaminationAttributes getDicomExaminationAttributes(Study study) throws ShanoirException { + ExaminationAttributes attributes = new ExaminationAttributes(); + if (study != null) { + for (Serie serie : study.getSeries()) { + attributes.addAcquisitionAttributes(serie.getSeriesInstanceUID(), getDicomAcquisitionAttributes(serie)); + } + } + return attributes; + } + + public AcquisitionAttributes getDicomAcquisitionAttributes(Serie serie, Boolean isEnhanced) throws ShanoirException { + AcquisitionAttributes attributes = new AcquisitionAttributes(); + for (Dataset dataset : serie.getDatasets()) { + try { + attributes.addDatasetAttributes(dataset.getFirstImageSOPInstanceUID(), getDicomObjectAttributes(serie.getFirstDatasetFileForCurrentSerie(), isEnhanced)); + } catch (IOException e) { + throw new ShanoirException("Could not read dicom metadata from file for serie " + serie.getSopClassUID(), e); + } + } + return attributes; + } + + public AcquisitionAttributes getDicomAcquisitionAttributes(Serie serie) throws ShanoirException { + return getDicomAcquisitionAttributes(serie, serie.getIsEnhanced()); + } + } diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/download/AcquisitionAttributes.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/download/AcquisitionAttributes.java new file mode 100644 index 0000000000..031aade8d6 --- /dev/null +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/download/AcquisitionAttributes.java @@ -0,0 +1,119 @@ +/** + * Shanoir NG - Import, manage and share neuroimaging data + * Copyright (C) 2009-2019 Inria - https://www.inria.fr/ + * Contact us on https://project.inria.fr/shanoir/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/gpl-3.0.html + */ + +package org.shanoir.ng.download; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.dcm4che3.data.Attributes; + +/** + * The parametrized type is the type for the uid keys + */ +public class AcquisitionAttributes { + + private ConcurrentMap> datasetMap = new ConcurrentHashMap<>(); + + public Attributes getDatasetAttributes(T id) { + return datasetMap.get(id).orElse(null); + } + + public List getAllDatasetAttributes() { + List list = new ArrayList<>(); + for (Optional attributes : datasetMap.values()) { + if (attributes.isPresent()) { + list.add(attributes.get()); + } + } + return list; + } + + public void addDatasetAttributes(T id, Attributes attributes) { + if (id == null) throw new IllegalArgumentException("id cant be null here"); + this.datasetMap.put(id, Optional.ofNullable(attributes)); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + for (T dsId : datasetMap.keySet()) { + sb.append("dataset ").append(dsId).append("\n"); + if (datasetMap.get(dsId).isPresent()) { + for(String line : datasetMap.get(dsId).get().toString(1000, 1000).split("\n")) { + sb.append("\t").append(line).append("\n"); + } + } else { + sb.append("\tnull\n"); + } + } + return sb.toString(); + } + + public Set getDatasetIds() { + return datasetMap.keySet(); + } + + public void merge(AcquisitionAttributes dicomAcquisitionAttributes) { + for (T datasetId : dicomAcquisitionAttributes.getDatasetIds()) { + addDatasetAttributes(datasetId, dicomAcquisitionAttributes.getDatasetAttributes(datasetId)); + } + } + + public Attributes getFirstDatasetAttributes() { + if (datasetMap != null && datasetMap.size() > 0) { + return datasetMap.entrySet().iterator().next().getValue().orElse(null); + } else { + return null; + } + } + + @Override + public boolean equals(Object obj) { + if (obj == null || !(obj instanceof AcquisitionAttributes)) { + return false; + } + try { + @SuppressWarnings("unchecked") + AcquisitionAttributes other = (AcquisitionAttributes) obj; + for(T id : datasetMap.keySet()) { + Attributes attributes = datasetMap.get(id).orElse(null); + Attributes otherAttributes = other.getDatasetAttributes(id); + if (otherAttributes == null || !otherAttributes.equals(attributes)) { + return false; + } + } + return true; + } catch (ClassCastException e) { + return false; + } + + } + + public Class getParametrizedType() { + if (this.datasetMap != null && !this.datasetMap.keySet().isEmpty()) { + return this.datasetMap.keySet().iterator().next().getClass(); + } else { + return null; + } + } + + public boolean has(T acqId) { + return datasetMap.containsKey(acqId); + } +} \ No newline at end of file diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/download/DicomJsonUtils.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/download/DicomJsonUtils.java index d4b3197579..a2b900dc70 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/download/DicomJsonUtils.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/download/DicomJsonUtils.java @@ -53,6 +53,7 @@ public class DicomJsonUtils { public static final String STUDY_INSTANCE_UID = "0020000D"; public static final String SERIE_INSTANCE_UID = "0020000E"; public static final String OBJECT_INSTANCE_UID = "00080018"; + public static final String VALUE = "Value"; public static final String STUDIES = "studies"; diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/download/ExaminationAttributes.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/download/ExaminationAttributes.java new file mode 100644 index 0000000000..02b3909dad --- /dev/null +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/download/ExaminationAttributes.java @@ -0,0 +1,129 @@ +/** + * Shanoir NG - Import, manage and share neuroimaging data + * Copyright (C) 2009-2019 Inria - https://www.inria.fr/ + * Contact us on https://project.inria.fr/shanoir/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/gpl-3.0.html + */ + +package org.shanoir.ng.download; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.dcm4che3.data.Attributes; +import org.dcm4che3.data.Tag; +import org.shanoir.ng.dataset.model.Dataset; +import org.shanoir.ng.dataset.model.DatasetExpression; +import org.shanoir.ng.datasetacquisition.model.DatasetAcquisition; +import org.shanoir.ng.datasetfile.DatasetFile; +import org.shanoir.ng.examination.model.Examination; + +/** + * The parametrized type is the type for the uid keys + */ +public class ExaminationAttributes { + + private ConcurrentMap>> acquisitionMap = new ConcurrentHashMap<>(); + + public ExaminationAttributes() {} + + public AcquisitionAttributes getAcquisitionAttributes(T id) { + return acquisitionMap.get(id).orElse(null); + } + + public Attributes getDatasetAttributes(T acquisitionId, T datasetId) { + if (acquisitionMap.containsKey(acquisitionId)) { + if (acquisitionMap.get(acquisitionId).isPresent()) { + return acquisitionMap.get(acquisitionId).get().getDatasetAttributes(datasetId); + } else { + return null; + } + } else return null; + } + + public List getAllDatasetAttributes() { + List res = new ArrayList<>(); + for (Optional> acqAttributes : acquisitionMap.values()) { + if (acqAttributes.isPresent()) { + for (Attributes attr : acqAttributes.get().getAllDatasetAttributes()) { + res.add(attr); + } + } + } + return res; + } + + public void addDatasetAttributes(T acquisitionId, T datasetId, Attributes attributes) { + if (!acquisitionMap.containsKey(acquisitionId)) { + acquisitionMap.put(acquisitionId, Optional.of(new AcquisitionAttributes())); + } + acquisitionMap.get(acquisitionId).get().addDatasetAttributes(datasetId, attributes); + } + + public static void addDatasetAttributes(ExaminationAttributes examinationAttributes, Examination examination, Attributes singleImageAttributes) { + //String serieUID = singleImageAttributes.getString(Tag.SeriesInstanceUID); + String sopUID = singleImageAttributes.getString(Tag.SOPInstanceUID); + if (sopUID != null && examination != null && examination.getDatasetAcquisitions() != null && examinationAttributes != null) { + for (DatasetAcquisition acquisition : examination.getDatasetAcquisitions()) { + if (acquisition.getDatasets() != null) { + for (Dataset dataset : acquisition.getDatasets()) { + if (dataset.getDatasetExpressions() != null) { + for (DatasetExpression expression : dataset.getDatasetExpressions()) { + if (expression.getDatasetFiles() != null) { + for (DatasetFile file : expression.getDatasetFiles()) { + if (file.getPath() != null) { + String datasetSopUID = WADODownloaderService.extractInstanceUID(file.getPath()); + if (sopUID.equals(datasetSopUID)) { + examinationAttributes.addDatasetAttributes(acquisition.getId(), dataset.getId(), singleImageAttributes); + } + } + } + } + } + } + } + } + } + } + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + for (T acqId : acquisitionMap.keySet()) { + sb.append("acquisition ").append(acqId).append("\n"); + for(String line : acquisitionMap.get(acqId).toString().split("\n")) { + sb.append("\t").append(line).append("\n"); + } + } + return sb.toString(); + } + + public Set getAcquisitionIds() { + return acquisitionMap.keySet(); + } + + public void addAcquisitionAttributes(T acquisitionId, AcquisitionAttributes dicomAcquisitionAttributes) { + if (acquisitionMap.containsKey(acquisitionId) && acquisitionMap.get(acquisitionId).isPresent()) { + acquisitionMap.get(acquisitionId).get().merge(dicomAcquisitionAttributes); + } else { + acquisitionMap.put(acquisitionId, Optional.ofNullable(dicomAcquisitionAttributes)); + } + } + + public boolean has(T acqId) { + return acquisitionMap.containsKey(acqId); + } + +} \ No newline at end of file diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/download/WADODownloaderService.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/download/WADODownloaderService.java index 9ab1561954..5063167e8b 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/download/WADODownloaderService.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/download/WADODownloaderService.java @@ -14,8 +14,11 @@ package org.shanoir.ng.download; -import java.io.*; -import java.net.MalformedURLException; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.StringReader; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; @@ -25,25 +28,22 @@ import java.util.Date; import java.util.Iterator; import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import javax.json.Json; import javax.json.stream.JsonParser; +import org.apache.commons.lang3.StringUtils; import org.dcm4che3.data.Attributes; import org.dcm4che3.json.JSONReader; -import org.json.JSONException; import org.shanoir.ng.dataset.model.Dataset; import org.shanoir.ng.dataset.model.DatasetExpressionFormat; import org.shanoir.ng.dataset.service.DatasetUtils; import org.shanoir.ng.datasetacquisition.model.DatasetAcquisition; -import org.shanoir.ng.examination.model.Examination; +import org.shanoir.ng.datasetfile.DatasetFile; import org.shanoir.ng.shared.exception.PacsException; import org.shanoir.ng.shared.exception.RestServiceException; -import org.shanoir.ng.shared.model.Study; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -317,7 +317,7 @@ public String downloadDicomMetadataForURL(final URL url) throws IOException, Mes if (url != null) { String urlStr = url.toString(); if (urlStr.contains(WADO_REQUEST_STUDY_WADO_URI)) urlStr = wadoURItoWadoRS(urlStr); - urlStr = urlStr.split(CONTENT_TYPE)[0].concat("/metadata/"); + urlStr = urlStr.split(CONTENT_TYPE)[0].concat("/metadata"); return downloadMetadataFromPACS(urlStr); } else { return null; @@ -345,98 +345,67 @@ public Attributes getDicomAttributesForDataset(Dataset dataset) throws PacsExcep throw new PacsException("Can not get dicom attributes for dataset " + dataset.getId(), e); } return null; - } - - private String getExaminationFirstDatasetUrl(Examination examination) { - if (examination != null && examination.getDatasetAcquisitions() != null && !examination.getDatasetAcquisitions().isEmpty() - && examination.getDatasetAcquisitions().get(0) != null && examination.getDatasetAcquisitions().get(0).getDatasets() != null - && !examination.getDatasetAcquisitions().get(0).getDatasets().isEmpty() - && examination.getDatasetAcquisitions().get(0).getDatasets().get(0) != null) { - List urls = new ArrayList<>(); - try { - DatasetUtils.getDatasetFilePathURLs(examination.getDatasetAcquisitions().get(0).getDatasets().get(0), urls, DatasetExpressionFormat.DICOM); - if (!urls.isEmpty()) { - return urls.get(0).toString(); - } - } catch (MalformedURLException e) { - return null; - } - } - return null; } - public Attributes getDicomAttributesForStudy(Study study) throws PacsException { - long ts = new Date().getTime(); - Examination exam = getFirstIfExist(study.getExaminations()); - if (exam == null) return null; - Attributes result = getDicomAttributesForExamination(exam); - LOG.debug("get DICOM attributes for study " + study.getId() + " : " + (new Date().getTime() - ts) + " ms"); - return result; + static String getFirstDatasetUrl(Dataset dataset) { + boolean condition = dataset != null + && dataset.getDatasetExpressions() != null + && !dataset.getDatasetExpressions().isEmpty() + && dataset.getDatasetExpressions().get(0) != null + && DatasetExpressionFormat.DICOM.equals(dataset.getDatasetExpressions().get(0).getDatasetExpressionFormat()) + && dataset.getDatasetExpressions().get(0).getDatasetFiles() != null + && !dataset.getDatasetExpressions().get(0).getDatasetFiles().isEmpty() + && dataset.getDatasetExpressions().get(0).getDatasetFiles().get(0) != null; + if (condition) { + DatasetFile datasetFile = dataset.getDatasetExpressions().get(0).getDatasetFiles().get(0); + return StringUtils.replace(datasetFile.getPath(), "%20", " "); + } else { + return null; + } } - - public Attributes getDicomAttributesForExamination(Examination examination) throws PacsException { + public AcquisitionAttributes getDicomAttributesForAcquisition(DatasetAcquisition acquisition) throws PacsException { long ts = new Date().getTime(); - DatasetAcquisition acquisition = getFirstIfExist(examination.getDatasetAcquisitions()); - if (acquisition == null) return null; - Attributes result = getDicomAttributesForAcquisition(acquisition); + List datasets = new ArrayList<>(); + if (acquisition.getDatasets() != null) { + for (Dataset dataset : acquisition.getDatasets()) { + datasets.add(dataset); + } + } + AcquisitionAttributes dAcquisitionAttributes = new AcquisitionAttributes<>(); + datasets.parallelStream().forEach( + dataset -> { + try { + dAcquisitionAttributes.addDatasetAttributes(dataset.getId(), getDicomAttributesForDataset(dataset)); + } catch (PacsException e) { + throw new RuntimeException("could not get dicom attributes from pacs", e); + } + } + ); LOG.debug("get DICOM attributes for acquisition " + acquisition.getId() + " : " + (new Date().getTime() - ts) + " ms"); - return result; + return dAcquisitionAttributes; } - public Attributes getDicomAttributesForAcquisition(DatasetAcquisition acquisition) throws PacsException { - long ts = new Date().getTime(); - Dataset ds = getFirstIfExist(acquisition.getDatasets()); - if (ds == null) return null; - Attributes result = getDicomAttributesForDataset(ds); - LOG.debug("get DICOM attributes for dataset " + ds.getId() + " : " + (new Date().getTime() - ts) + " ms"); - return result; + static String extractInstanceUID(String url) { + boolean condition1 = url != null && url.contains("objectUID="); + boolean condition2 = url != null && url.contains("instances/"); + if (condition1) { + String[] split = StringUtils.splitByWholeSeparator(url, "objectUID=", 2); + return StringUtils.split(split[1], "&", 1)[0]; + } else if (condition2) { + String[] split = StringUtils.splitByWholeSeparator(url, "instances/", 2); + return StringUtils.split(split[1], "/", 1)[0]; + } else return null; } - public String getDicomJson(Examination examination) throws IOException { - String urlStr = getExaminationFirstDatasetUrl(examination); - if (urlStr != null) { - if (urlStr.contains(WADO_REQUEST_STUDY_WADO_URI)) urlStr = wadoURItoWadoRS(urlStr); - urlStr = urlStr.split(CONTENT_TYPE)[0]; - urlStr = urlStr.split("/series/")[0]; - urlStr = urlStr.concat("/metadata/"); - String json = downloadMetadataFromPACS(urlStr); - // transform from flat to tree - try { - return DicomJsonUtils.inflateDCM4CheeJSON(json); - //tree.put - } catch (JSONException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - } - return null; - } - private T getFirstIfExist(List list) { - if (list == null || list.size() == 0) return null; - else return list.get(0); - } - - /** - * The instanceUID (== objectUID) is inside the URL string - * and has to be extracted to be used. - * - * @param url - * @return - */ - private String extractInstanceUID(String url) { - Pattern p = null; - if (url.indexOf(CONTENT_TYPE) != -1) { - p = Pattern.compile("objectUID=(\\S+)&contentType"); - } else { - p = Pattern.compile("objectUID=(\\S+)"); - } - Matcher m = p.matcher(url); - if (m.find()) { - return m.group(1); - } else { + static String extractInstanceUID(Dataset dataset) { + String url = getFirstDatasetUrl(dataset); + if (url == null) { return null; + } else { + String ret = extractInstanceUID(url); + return ret; } } diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/examination/controler/ExaminationApi.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/examination/controler/ExaminationApi.java index b665d8b4f3..cb1ec8640a 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/examination/controler/ExaminationApi.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/examination/controler/ExaminationApi.java @@ -14,13 +14,9 @@ package org.shanoir.ng.examination.controler; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.servlet.http.HttpServletResponse; -import jakarta.validation.Valid; +import java.io.IOException; +import java.util.List; + import org.shanoir.ng.examination.dto.ExaminationDTO; import org.shanoir.ng.examination.dto.SubjectExaminationDTO; import org.shanoir.ng.shared.exception.RestServiceException; @@ -30,11 +26,22 @@ import org.springframework.security.access.prepost.PostAuthorize; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.BindingResult; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.multipart.MultipartFile; -import java.io.IOException; -import java.util.List; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; @Tag(name = "examination") @RequestMapping("/examinations") @@ -103,6 +110,18 @@ ResponseEntity> findExaminationsBySubjectIdStudyId( @Parameter(name = "id of the subject", required = true) @PathVariable("subjectId") Long subjectId, @Parameter(name = "id of the study", required = true) @PathVariable("studyId") Long studyId); + @Operation(summary = "", description = "Returns the list of examinations by study id") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "found examinations"), + @ApiResponse(responseCode = "204", description = "no examination found"), + @ApiResponse(responseCode = "401", description = "unauthorized"), + @ApiResponse(responseCode = "403", description = "forbidden"), + @ApiResponse(responseCode = "500", description = "unexpected error") }) + @GetMapping(value = "/study/{studyId}", produces = { "application/json" }) + @PreAuthorize("hasRole('ADMIN') or (hasAnyRole('EXPERT', 'USER') and @datasetSecurityService.hasRightOnStudy(#studyId, 'CAN_SEE_ALL'))") + ResponseEntity> findExaminationsByStudyId( + @Parameter(name = "id of the study", required = true) @PathVariable("studyId") Long studyId); + @Operation(summary = "", description = "Returns the list of examinations by subject id") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "found examinations"), diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/examination/controler/ExaminationApiController.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/examination/controler/ExaminationApiController.java index 7c8bc32a96..d21cbb5c58 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/examination/controler/ExaminationApiController.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/examination/controler/ExaminationApiController.java @@ -159,6 +159,16 @@ public ResponseEntity> findExaminationsBySubjectIdSt return new ResponseEntity<>(examinationMapper.examinationsToSubjectExaminationDTOs(examinations), HttpStatus.OK); } + @Override + public ResponseEntity> findExaminationsByStudyId( + @Parameter(name = "id of the study", required = true) @PathVariable("studyId") Long studyId) { + final List examinations = examinationService.findIdsByStudyId(studyId); + if (examinations.isEmpty()) { + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + return new ResponseEntity<>(examinations, HttpStatus.OK); + } + // Attention: this method is used by ShanoirUploader!!! @Override public ResponseEntity saveNewExamination( diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/examination/repository/ExaminationRepository.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/examination/repository/ExaminationRepository.java index 02690a5e03..02164827ed 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/examination/repository/ExaminationRepository.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/examination/repository/ExaminationRepository.java @@ -17,6 +17,7 @@ import org.shanoir.ng.examination.model.Examination; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.PagingAndSortingRepository; @@ -69,13 +70,19 @@ public interface ExaminationRepository extends PagingAndSortingRepository findByStudy_Id(Long studyId); + + /** + * Get a list of examinations for a study. + * + * @param studyId + * @return list of examinations. + */ + @Query("select e.id from Examination e where e.study.id = :studyId") + List findIdsByStudyId(Long studyId); /** * Get all examinations, clinical or preclinical. diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/examination/service/ExaminationService.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/examination/service/ExaminationService.java index d11fecddd8..e5b35a71e7 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/examination/service/ExaminationService.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/examination/service/ExaminationService.java @@ -103,6 +103,15 @@ public interface ExaminationService { @PostAuthorize("hasRole('ADMIN') or @datasetSecurityService.filterExaminationList(returnObject, 'CAN_SEE_ALL')") List findBySubjectId(Long subjectId); + /** + * Find examinations related to particular study + * @param subjectId + * @return + * @author yyao + */ + @PreAuthorize("hasAnyRole('ADMIN', 'EXPERT', 'USER')") + List findIdsByStudyId(Long studyId); + /** * Find examinations related to particular study * @param subjectId diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/examination/service/ExaminationServiceImpl.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/examination/service/ExaminationServiceImpl.java index aa931e1f26..d25bac8597 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/examination/service/ExaminationServiceImpl.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/examination/service/ExaminationServiceImpl.java @@ -178,6 +178,11 @@ public List findBySubjectId(final Long subjectId) { return examinationRepository.findBySubjectId(subjectId); } + @Override + public List findIdsByStudyId(Long studyId) { + return examinationRepository.findIdsByStudyId(studyId); + } + @Override public List findByStudyId(Long studyId) { return examinationRepository.findByStudy_Id(studyId); diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/dto/Dataset.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/dto/Dataset.java index 22445cf8ad..e5e37766c8 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/dto/Dataset.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/dto/Dataset.java @@ -19,6 +19,7 @@ import java.util.List; import java.util.Set; +import org.shanoir.ng.shared.dicom.EchoTime; import org.shanoir.ng.shared.model.DiffusionGradient; import com.fasterxml.jackson.annotation.JsonProperty; @@ -50,6 +51,9 @@ public class Dataset { @JsonProperty("bVectors") private List bVectors; + + @JsonProperty("firstImageSOPInstanceUID") + private String firstImageSOPInstanceUID; public String getName() { return name; @@ -138,4 +142,11 @@ public void setEchoTimes(Set echoTimes) { this.echoTimes = echoTimes; } + public String getFirstImageSOPInstanceUID() { + return this.firstImageSOPInstanceUID; + } + + public void setFirstImageSOPInstanceUID(String firstImageSOPInstanceUID) { + this.firstImageSOPInstanceUID = firstImageSOPInstanceUID; + } } diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/dto/EchoTime.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/dto/EchoTime.java deleted file mode 100644 index 337b11c7a5..0000000000 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/dto/EchoTime.java +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Shanoir NG - Import, manage and share neuroimaging data - * Copyright (C) 2009-2019 Inria - https://www.inria.fr/ - * Contact us on https://project.inria.fr/shanoir/ - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see https://www.gnu.org/licenses/gpl-3.0.html - */ - -package org.shanoir.ng.importer.dto; - -public class EchoTime { - - /** - * The echo number. Comes from dicom tag (0018,0086) VR=IS, VM=1-n Echo - * Number(s). - */ - - private Integer echoNumber; - - /** - * Comes from the dicom tag (0018,0081) VR=DS, VM=1 Echo Time. The unit of - * measure must be in millisec. - */ - - private Double echoTime; - - public Integer getEchoNumber() { - return echoNumber; - } - - public void setEchoNumber(Integer echoNumber) { - this.echoNumber = echoNumber; - } - - public Double getEchoTime() { - return echoTime; - } - - public void setEchoTime(Double echoTime) { - this.echoTime = echoTime; - } - - -} diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/dto/Image.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/dto/Image.java index a5a2dc2016..886802b732 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/dto/Image.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/dto/Image.java @@ -16,6 +16,8 @@ import java.util.List; +import org.shanoir.ng.shared.dicom.EchoTime; + import com.fasterxml.jackson.annotation.JsonProperty; public class Image { @@ -41,6 +43,8 @@ public class Image { @JsonProperty("imageOrientationPatient") public List imageOrientationPatient; + public String SOPInstanceUID; + public String getPath() { return path; } @@ -63,8 +67,8 @@ public List getImageOrientationPatient() { public void setImageOrientationPatient(List imageOrientationPatient) { this.imageOrientationPatient = imageOrientationPatient; + } - public List getEchoTimes() { return echoTimes; } @@ -96,7 +100,12 @@ public String getFlipAngle() { public void setFlipAngle(String flipAngle) { this.flipAngle = flipAngle; } - - + public String getSOPInstanceUID() { + return SOPInstanceUID; + } + + public void setSOPInstanceUID(String sOPInstanceUID) { + SOPInstanceUID = sOPInstanceUID; + } } \ No newline at end of file diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/dto/ImportJob.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/dto/ImportJob.java index 892758383a..da3a29b124 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/dto/ImportJob.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/dto/ImportJob.java @@ -235,4 +235,14 @@ public Serie getFirstSerie() { return getPatients().get(0).getStudies().get(0).getSeries().get(0); } } + + public Study getFirstStudy() { + if ( getPatients() == null + || getPatients().get(0) == null + || getPatients().get(0).getStudies() == null) { + return null; + } else { + return getPatients().get(0).getStudies().get(0); + } + } } diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/dto/Serie.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/dto/Serie.java index 8447d4c985..6fe9dcd44f 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/dto/Serie.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/dto/Serie.java @@ -269,6 +269,5 @@ public Boolean getIsSpectroscopy() { public void setIsSpectroscopy(Boolean isSpectroscopy) { this.isSpectroscopy = isSpectroscopy; - } - + } } diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/service/DatasetAcquisitionContext.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/service/DatasetAcquisitionContext.java index f260cc65a6..406b1aa242 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/service/DatasetAcquisitionContext.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/service/DatasetAcquisitionContext.java @@ -14,8 +14,8 @@ package org.shanoir.ng.importer.service; -import org.dcm4che3.data.Attributes; import org.shanoir.ng.datasetacquisition.model.DatasetAcquisition; +import org.shanoir.ng.download.AcquisitionAttributes; import org.shanoir.ng.importer.dto.ImportJob; import org.shanoir.ng.importer.dto.Serie; import org.shanoir.ng.importer.strategies.datasetacquisition.CtDatasetAcquisitionStrategy; @@ -52,7 +52,7 @@ public class DatasetAcquisitionContext implements DatasetAcquisitionStrategy { // add other strategies for other modalities here @Override - public DatasetAcquisition generateDatasetAcquisitionForSerie(Serie serie, int rank, ImportJob importJob, Attributes dicomAttributes) throws Exception { + public DatasetAcquisition generateDatasetAcquisitionForSerie(Serie serie, int rank, ImportJob importJob, AcquisitionAttributes dicomAttributes) throws Exception { DatasetAcquisitionStrategy datasetAcquisitionStrategy; String modality = serie.getModality(); if ("MR".equals(modality)) { diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/service/ImporterService.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/service/ImporterService.java index 2f38a45451..6e8d54492c 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/service/ImporterService.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/service/ImporterService.java @@ -14,24 +14,62 @@ package org.shanoir.ng.importer.service; -import org.dcm4che3.data.Attributes; -import org.shanoir.ng.dataset.modality.*; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.shanoir.ng.dataset.modality.CalibrationDataset; +import org.shanoir.ng.dataset.modality.CtDataset; +import org.shanoir.ng.dataset.modality.EegDataset; +import org.shanoir.ng.dataset.modality.MegDataset; +import org.shanoir.ng.dataset.modality.MeshDataset; +import org.shanoir.ng.dataset.modality.MrDataset; +import org.shanoir.ng.dataset.modality.ParameterQuantificationDataset; +import org.shanoir.ng.dataset.modality.PetDataset; +import org.shanoir.ng.dataset.modality.RegistrationDataset; +import org.shanoir.ng.dataset.modality.SegmentationDataset; +import org.shanoir.ng.dataset.modality.SpectDataset; +import org.shanoir.ng.dataset.modality.StatisticalDataset; +import org.shanoir.ng.dataset.modality.TemplateDataset; import org.shanoir.ng.dataset.model.Dataset; -import org.shanoir.ng.dataset.model.*; +import org.shanoir.ng.dataset.model.DatasetExpression; +import org.shanoir.ng.dataset.model.DatasetExpressionFormat; +import org.shanoir.ng.dataset.model.DatasetMetadata; +import org.shanoir.ng.dataset.model.DatasetModalityType; import org.shanoir.ng.dataset.service.DatasetService; import org.shanoir.ng.datasetacquisition.model.DatasetAcquisition; import org.shanoir.ng.datasetacquisition.service.DatasetAcquisitionService; import org.shanoir.ng.datasetfile.DatasetFile; import org.shanoir.ng.dicom.DicomProcessing; +import org.shanoir.ng.download.AcquisitionAttributes; +import org.shanoir.ng.download.ExaminationAttributes; +import org.shanoir.ng.download.WADODownloaderService; import org.shanoir.ng.examination.model.Examination; import org.shanoir.ng.examination.repository.ExaminationRepository; import org.shanoir.ng.examination.service.ExaminationService; -import org.shanoir.ng.importer.dto.*; +import org.shanoir.ng.importer.dto.ImportJob; +import org.shanoir.ng.importer.dto.Patient; +import org.shanoir.ng.importer.dto.ProcessedDatasetImportJob; +import org.shanoir.ng.importer.dto.Serie; +import org.shanoir.ng.importer.dto.Study; import org.shanoir.ng.processing.model.DatasetProcessing; import org.shanoir.ng.shared.event.ShanoirEvent; import org.shanoir.ng.shared.event.ShanoirEventService; import org.shanoir.ng.shared.event.ShanoirEventType; +import org.shanoir.ng.shared.exception.PacsException; import org.shanoir.ng.shared.exception.ShanoirException; +import org.shanoir.ng.shared.model.SubjectStudy; +import org.shanoir.ng.shared.quality.QualityTag; +import org.shanoir.ng.shared.service.SubjectStudyService; import org.shanoir.ng.studycard.dto.QualityCardResult; import org.shanoir.ng.studycard.model.QualityCard; import org.shanoir.ng.studycard.model.QualityException; @@ -50,15 +88,6 @@ import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardCopyOption; -import java.time.LocalDate; -import java.util.*; - @Service @Scope("prototype") public class ImporterService { @@ -100,9 +129,15 @@ public class ImporterService { @Autowired private DatasetAcquisitionService datasetAcquisitionService; + @Autowired + private SubjectStudyService subjectStudyService; + @Autowired private DicomProcessing dicomProcessing; + @Autowired + private WADODownloaderService downloader; + private static final String SUBJECT_PREFIX = "sub-"; private static final String PROCESSED_DATASET_PREFIX = "processed-dataset"; @@ -132,11 +167,21 @@ public void createAllDatasetAcquisition(ImportJob importJob, Long userId) throws generatedAcquisitions = generateAcquisitions(examination, importJob, event); examination.getDatasetAcquisitions().addAll(generatedAcquisitions); // change to set() ? // Quality check - QualityCardResult qualityResult = checkQuality(examination, importJob); + SubjectStudy subjectStudy = examination.getSubject().getSubjectStudyList().stream() + .filter(ss -> ss.getStudy().getId().equals(examination.getStudy().getId())) + .findFirst().orElse(null); + QualityTag tagSave = subjectStudy != null ? subjectStudy.getQualityTag() : null; + QualityCardResult qualityResult = checkQuality(examination, generatedAcquisitions, importJob); // Has quality check passed ? if (qualityResult.hasError()) { throw new QualityException(examination, qualityResult); } else { // Then do the import + if (qualityResult.hasWarning() || qualityResult.hasFailedValid()) { + event.setReport(qualityResult.toString()); + } + // add tag to subject-study + subjectStudyService.update(qualityResult.getUpdatedSubjectStudies()); + generatedAcquisitions = new HashSet(datasetAcquisitionService.createAll(generatedAcquisitions)); try { persistPatientInPacs(importJob.getPatients(), event); @@ -145,6 +190,9 @@ public void createAllDatasetAcquisition(ImportJob importJob, Long userId) throws for (DatasetAcquisition acquisition : generatedAcquisitions) { datasetAcquisitionService.deleteById(acquisition.getId()); } + // revert quality tag + subjectStudy.setQualityTag(tagSave); + subjectStudyService.update(qualityResult.getUpdatedSubjectStudies()); throw new ShanoirException("Error while saving data in pacs, the import is canceled and acquisitions were not saved"); } } @@ -189,7 +237,8 @@ public void createAllDatasetAcquisition(ImportJob importJob, Long userId) throws } catch (QualityException e) { String msg = e.buildErrorMessage(); event.setStatus(ShanoirEvent.ERROR); - event.setMessage(msg); + event.setMessage("Quality checks didn't pass at import, import aborted"); + event.setReport(e.getQualityResult().toString()); event.setProgress(-1f); eventService.publishEvent(event); LOG.warn(msg, e); @@ -217,11 +266,11 @@ private Set generateAcquisitions(Examination examination, Im float progress = 0.5f; for (Serie serie : study.getSelectedSeries() ) { // get dicomAttributes - Attributes dicomAttributes = null; + AcquisitionAttributes dicomAttributes = null; try { - dicomAttributes = dicomProcessing.getDicomObjectAttributes(serie.getFirstDatasetFileForCurrentSerie(), serie.getIsEnhanced()); - } catch (IOException e) { - throw new ShanoirException("Unable to retrieve dicom attributes in serie: " + serie.getSeriesDescription(), e); + dicomAttributes = dicomProcessing.getDicomAcquisitionAttributes(serie, serie.getIsEnhanced()); + } catch (PacsException e) { + throw new ShanoirException("Unable to retrieve dicom attributes in file " + serie.getFirstDatasetFileForCurrentSerie().getPath(), e); } // Generate acquisition object with all sub objects : datasets, protocols, expressions, ... @@ -249,26 +298,37 @@ private Set generateAcquisitions(Examination examination, Im } private QualityCardResult checkQuality(Examination examination, ImportJob importJob) throws ShanoirException { - Attributes dicomAttributes = null; - try { - Serie firstSerie = importJob.getFirstSerie(); - if (firstSerie == null) { - throw new ShanoirException("The given import job does not provide any serie. Examination : " + examination.getId()); - } - dicomAttributes = dicomProcessing.getDicomObjectAttributes(firstSerie.getFirstDatasetFileForCurrentSerie(), firstSerie.getIsEnhanced()); - } catch (IOException e) { - throw new ShanoirException("Unable to retrieve dicom attributes for examination " + examination.getId(), e); + ExaminationAttributes dicomAttributes = null; + Study firstStudy = importJob.getFirstStudy(); + if (firstStudy == null) { + throw new ShanoirException("The given import job does not provide any serie. Examination : " + examination.getId()); } + dicomAttributes = dicomProcessing.getDicomExaminationAttributes(firstStudy); List qualityCards = qualityCardService.findByStudy(examination.getStudyId()); QualityCardResult qualityResult = new QualityCardResult(); for (QualityCard qualityCard : qualityCards) { if (qualityCard.isToCheckAtImport()) { - qualityResult.merge(qualityCard.apply(examination, dicomAttributes)); + qualityResult.merge(qualityCard.apply(examination, dicomAttributes, downloader)); } } return qualityResult; } + private QualityCardResult checkQuality(Examination examination, Set limitToTheseAcquisitions, ImportJob importJob) throws ShanoirException { + // save the exam acquisitions + List saveList = new ArrayList<>(); + for (DatasetAcquisition acquisition : examination.getDatasetAcquisitions()) { + saveList.add(acquisition); + } + // replace ths exam acquisitions by the reduced set + examination.setDatasetAcquisitions(Utils.toList(limitToTheseAcquisitions)); + // check quality + QualityCardResult result = checkQuality(examination, importJob); + // set the data back + examination.setDatasetAcquisitions(saveList); + return result; + } + StudyCard getStudyCard(ImportJob importJob) { if (importJob.getStudyCardId() != null) { // makes sense: imports without studycard exist StudyCard studyCard = getStudyCard(importJob.getStudyCardId()); @@ -314,7 +374,7 @@ private void persistPatientInPacs(List patients, ShanoirEvent event) th } } - public DatasetAcquisition createDatasetAcquisitionForSerie(Serie serie, int rank, Examination examination, ImportJob importJob, Attributes dicomAttributes) throws Exception { + public DatasetAcquisition createDatasetAcquisitionForSerie(Serie serie, int rank, Examination examination, ImportJob importJob, AcquisitionAttributes dicomAttributes) throws Exception { if (checkSerieForDicomImages(serie)) { DatasetAcquisition datasetAcquisition = datasetAcquisitionContext.generateDatasetAcquisitionForSerie(serie, rank, importJob, dicomAttributes); datasetAcquisition.setExamination(examination); diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/strategies/dataset/CtDatasetStrategy.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/strategies/dataset/CtDatasetStrategy.java index e5aededf6b..5f2d8b20b9 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/strategies/dataset/CtDatasetStrategy.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/strategies/dataset/CtDatasetStrategy.java @@ -22,6 +22,7 @@ import org.shanoir.ng.dataset.model.DatasetMetadata; import org.shanoir.ng.dataset.model.DatasetModalityType; import org.shanoir.ng.dicom.DicomProcessing; +import org.shanoir.ng.download.AcquisitionAttributes; import org.shanoir.ng.importer.dto.Dataset; import org.shanoir.ng.importer.dto.DatasetsWrapper; import org.shanoir.ng.importer.dto.ExpressionFormat; @@ -41,7 +42,7 @@ public class CtDatasetStrategy implements DatasetStrategy { DatasetExpressionContext datasetExpressionContext; @Override - public DatasetsWrapper generateDatasetsForSerie(Attributes dicomAttributes, Serie serie, + public DatasetsWrapper generateDatasetsForSerie(AcquisitionAttributes dicomAttributes, Serie serie, ImportJob importJob) throws Exception { DatasetsWrapper datasetWrapper = new DatasetsWrapper<>(); @@ -59,7 +60,7 @@ public DatasetsWrapper generateDatasetsForSerie(Attributes dicomAttri for (Dataset anyDataset : serie.getDatasets()) { importJob.getProperties().put(ImportJob.INDEX_PROPERTY, String.valueOf(datasetIndex)); - CtDataset dataset = generateSingleDataset(dicomAttributes, serie, anyDataset, datasetIndex, importJob); + CtDataset dataset = generateSingleDataset(dicomAttributes.getDatasetAttributes(anyDataset.getFirstImageSOPInstanceUID()), serie, anyDataset, datasetIndex, importJob); datasetWrapper.getDatasets().add(dataset); datasetIndex++; } @@ -72,6 +73,7 @@ public DatasetsWrapper generateDatasetsForSerie(Attributes dicomAttri public CtDataset generateSingleDataset(Attributes dicomAttributes, Serie serie, Dataset dataset, int datasetIndex, ImportJob importJob) throws Exception { CtDataset ctDataset = new CtDataset(); + ctDataset.setSOPInstanceUID(dataset.getFirstImageSOPInstanceUID()); ctDataset.setCreationDate(serie.getSeriesDate()); final String serieDescription = serie.getSeriesDescription(); diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/strategies/dataset/DatasetStrategy.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/strategies/dataset/DatasetStrategy.java index df2b6920e1..07f54e7ab6 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/strategies/dataset/DatasetStrategy.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/strategies/dataset/DatasetStrategy.java @@ -15,6 +15,7 @@ package org.shanoir.ng.importer.strategies.dataset; import org.dcm4che3.data.Attributes; +import org.shanoir.ng.download.AcquisitionAttributes; import org.shanoir.ng.importer.dto.Dataset; import org.shanoir.ng.importer.dto.DatasetsWrapper; import org.shanoir.ng.importer.dto.ImportJob; @@ -36,7 +37,7 @@ public interface DatasetStrategy { - DatasetsWrapper generateDatasetsForSerie(Attributes dicomAttributes, Serie serie, ImportJob importJob) throws Exception; + DatasetsWrapper generateDatasetsForSerie(AcquisitionAttributes dicomAttributes, Serie serie, ImportJob importJob) throws Exception; T generateSingleDataset(Attributes dicomAttributes, Serie serie, Dataset dataset, int datasetIndex, ImportJob importJob) throws Exception; diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/strategies/dataset/GenericDatasetStrategy.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/strategies/dataset/GenericDatasetStrategy.java index d819a32767..63619ce3f6 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/strategies/dataset/GenericDatasetStrategy.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/strategies/dataset/GenericDatasetStrategy.java @@ -9,6 +9,7 @@ import org.shanoir.ng.dataset.model.DatasetMetadata; import org.shanoir.ng.dataset.model.DatasetModalityType; import org.shanoir.ng.dicom.DicomProcessing; +import org.shanoir.ng.download.AcquisitionAttributes; import org.shanoir.ng.importer.dto.Dataset; import org.shanoir.ng.importer.dto.DatasetsWrapper; import org.shanoir.ng.importer.dto.ExpressionFormat; @@ -28,7 +29,7 @@ public class GenericDatasetStrategy implements DatasetStrategy { DatasetExpressionContext datasetExpressionContext; @Override - public DatasetsWrapper generateDatasetsForSerie(Attributes dicomAttributes, Serie serie, + public DatasetsWrapper generateDatasetsForSerie(AcquisitionAttributes dicomAttributes, Serie serie, ImportJob importJob) throws Exception { DatasetsWrapper datasetWrapper = new DatasetsWrapper<>(); /** @@ -45,7 +46,7 @@ public DatasetsWrapper generateDatasetsForSerie(Attributes dicom for (Dataset anyDataset : serie.getDatasets()) { importJob.getProperties().put(ImportJob.INDEX_PROPERTY, String.valueOf(datasetIndex)); - GenericDataset dataset = generateSingleDataset(dicomAttributes, serie, anyDataset, datasetIndex, importJob); + GenericDataset dataset = generateSingleDataset(dicomAttributes.getDatasetAttributes(anyDataset.getFirstImageSOPInstanceUID()), serie, anyDataset, datasetIndex, importJob); datasetWrapper.getDatasets().add(dataset); datasetIndex++; } @@ -58,6 +59,7 @@ public DatasetsWrapper generateDatasetsForSerie(Attributes dicom public GenericDataset generateSingleDataset(Attributes dicomAttributes, Serie serie, Dataset dataset, int datasetIndex, ImportJob importJob) throws Exception { GenericDataset genericDataset = new GenericDataset(); + genericDataset.setSOPInstanceUID(dataset.getFirstImageSOPInstanceUID()); genericDataset.setCreationDate(serie.getSeriesDate()); final String serieDescription = serie.getSeriesDescription(); diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/strategies/dataset/MrDatasetStrategy.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/strategies/dataset/MrDatasetStrategy.java index 7646662b46..2cc2df99d4 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/strategies/dataset/MrDatasetStrategy.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/strategies/dataset/MrDatasetStrategy.java @@ -29,14 +29,14 @@ import org.shanoir.ng.dataset.model.DatasetMetadata; import org.shanoir.ng.dataset.model.DatasetModalityType; import org.shanoir.ng.dicom.DicomProcessing; +import org.shanoir.ng.download.AcquisitionAttributes; import org.shanoir.ng.importer.dto.Dataset; import org.shanoir.ng.importer.dto.DatasetsWrapper; -import org.shanoir.ng.importer.dto.EchoTime; import org.shanoir.ng.importer.dto.ExpressionFormat; import org.shanoir.ng.importer.dto.ImportJob; import org.shanoir.ng.importer.dto.Serie; -import org.shanoir.ng.importer.service.ImporterService; import org.shanoir.ng.importer.strategies.datasetexpression.DatasetExpressionContext; +import org.shanoir.ng.shared.dicom.EchoTime; import org.shanoir.ng.shared.mapper.EchoTimeMapper; import org.shanoir.ng.shared.mapper.FlipAngleMapper; import org.shanoir.ng.shared.mapper.InversionTimeMapper; @@ -70,7 +70,7 @@ public class MrDatasetStrategy implements DatasetStrategy { private FlipAngleMapper flipAngleMapper; @Override - public DatasetsWrapper generateDatasetsForSerie(Attributes dicomAttributes, Serie serie, + public DatasetsWrapper generateDatasetsForSerie(AcquisitionAttributes serieAttributes, Serie serie, ImportJob importJob) throws Exception { DatasetsWrapper datasetWrapper = new DatasetsWrapper<>(); @@ -90,7 +90,7 @@ public DatasetsWrapper generateDatasetsForSerie(Attributes dicomAttri importJob.getProperties().put(ImportJob.INDEX_PROPERTY, String.valueOf(datasetIndex)); MrDataset mrDataset = new MrDataset(); - mrDataset = generateSingleDataset(dicomAttributes, serie, dataset, datasetIndex, importJob); + mrDataset = generateSingleDataset(serieAttributes.getDatasetAttributes(dataset.getFirstImageSOPInstanceUID()), serie, dataset, datasetIndex, importJob); if (mrDataset.getFirstImageAcquisitionTime() != null) { if (datasetWrapper.getFirstImageAcquisitionTime() == null) { datasetWrapper.setFirstImageAcquisitionTime(mrDataset.getFirstImageAcquisitionTime()); @@ -124,6 +124,7 @@ public DatasetsWrapper generateDatasetsForSerie(Attributes dicomAttri public MrDataset generateSingleDataset(Attributes dicomAttributes, Serie serie, Dataset dataset, int datasetIndex, ImportJob importJob) throws Exception { MrDataset mrDataset = new MrDataset(); + mrDataset.setSOPInstanceUID(dataset.getFirstImageSOPInstanceUID()); mrDataset.setCreationDate(serie.getSeriesDate()); mrDataset.setDiffusionGradients(dataset.getDiffusionGradients()); final String serieDescription = serie.getSeriesDescription(); diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/strategies/dataset/PetDatasetStragegy.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/strategies/dataset/PetDatasetStragegy.java index d74715dc21..b62535e35c 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/strategies/dataset/PetDatasetStragegy.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/strategies/dataset/PetDatasetStragegy.java @@ -20,6 +20,7 @@ import org.shanoir.ng.dataset.model.DatasetExpression; import org.shanoir.ng.dataset.model.DatasetMetadata; import org.shanoir.ng.dataset.model.DatasetModalityType; +import org.shanoir.ng.download.AcquisitionAttributes; import org.shanoir.ng.importer.dto.Dataset; import org.shanoir.ng.importer.dto.DatasetsWrapper; import org.shanoir.ng.importer.dto.ExpressionFormat; @@ -40,7 +41,7 @@ public class PetDatasetStragegy implements DatasetStrategy{ DatasetExpressionContext datasetExpressionContext; @Override - public DatasetsWrapper generateDatasetsForSerie(Attributes dicomAttributes, Serie serie, + public DatasetsWrapper generateDatasetsForSerie(AcquisitionAttributes dicomAttributes, Serie serie, ImportJob importJob) throws Exception { DatasetsWrapper datasetWrapper = new DatasetsWrapper<>(); @@ -58,7 +59,7 @@ public DatasetsWrapper generateDatasetsForSerie(Attributes dicomAttr for (Dataset dataset : serie.getDatasets()) { importJob.getProperties().put(ImportJob.INDEX_PROPERTY, String.valueOf(datasetIndex)); - PetDataset petDataset = generateSingleDataset(dicomAttributes, serie, dataset, datasetIndex, importJob); + PetDataset petDataset = generateSingleDataset(dicomAttributes.getDatasetAttributes(dataset.getFirstImageSOPInstanceUID()), serie, dataset, datasetIndex, importJob); datasetWrapper.getDatasets().add(petDataset); datasetIndex++; } @@ -70,6 +71,7 @@ public DatasetsWrapper generateDatasetsForSerie(Attributes dicomAttr public PetDataset generateSingleDataset(Attributes dicomAttributes, Serie serie, Dataset dataset, int datasetIndex, ImportJob importJob) throws Exception { PetDataset petDataset = new PetDataset(); + petDataset.setSOPInstanceUID(dataset.getFirstImageSOPInstanceUID()); petDataset.setCreationDate(serie.getSeriesDate()); final String serieDescription = serie.getSeriesDescription(); diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/strategies/datasetacquisition/CtDatasetAcquisitionStrategy.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/strategies/datasetacquisition/CtDatasetAcquisitionStrategy.java index 5eb295cda9..3005828cf9 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/strategies/datasetacquisition/CtDatasetAcquisitionStrategy.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/strategies/datasetacquisition/CtDatasetAcquisitionStrategy.java @@ -25,6 +25,7 @@ import org.shanoir.ng.datasetacquisition.model.DatasetAcquisition; import org.shanoir.ng.datasetacquisition.model.ct.CtDatasetAcquisition; import org.shanoir.ng.datasetacquisition.model.ct.CtProtocol; +import org.shanoir.ng.download.AcquisitionAttributes; import org.shanoir.ng.importer.dto.DatasetsWrapper; import org.shanoir.ng.importer.dto.ImportJob; import org.shanoir.ng.importer.dto.Serie; @@ -58,7 +59,7 @@ public class CtDatasetAcquisitionStrategy implements DatasetAcquisitionStrategy private DatasetStrategy datasetStrategy; @Override - public DatasetAcquisition generateDatasetAcquisitionForSerie(Serie serie, int rank, ImportJob importJob, Attributes dicomAttributes) throws Exception { + public DatasetAcquisition generateDatasetAcquisitionForSerie(Serie serie, int rank, ImportJob importJob, AcquisitionAttributes dicomAttributes) throws Exception { CtDatasetAcquisition datasetAcquisition = new CtDatasetAcquisition(); LOG.info("Generating DatasetAcquisition for : {} - {} - Rank:{}",serie.getSequenceName(), serie.getProtocolName(), rank); @@ -66,7 +67,7 @@ public DatasetAcquisition generateDatasetAcquisitionForSerie(Serie serie, int ra datasetAcquisition.setRank(rank); importJob.getProperties().put(ImportJob.RANK_PROPERTY, String.valueOf(rank)); datasetAcquisition.setSortingIndex(serie.getSeriesNumber()); - datasetAcquisition.setSoftwareRelease(dicomAttributes.getString(Tag.SoftwareVersions)); + datasetAcquisition.setSoftwareRelease(dicomAttributes.getFirstDatasetAttributes().getString(Tag.SoftwareVersions)); CtProtocol protocol = protocolStrategy.generateProtocolForSerie(dicomAttributes, serie); datasetAcquisition.setCtProtocol(protocol); diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/strategies/datasetacquisition/DatasetAcquisitionStrategy.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/strategies/datasetacquisition/DatasetAcquisitionStrategy.java index 3769afbe4e..12faa69d2a 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/strategies/datasetacquisition/DatasetAcquisitionStrategy.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/strategies/datasetacquisition/DatasetAcquisitionStrategy.java @@ -14,8 +14,8 @@ package org.shanoir.ng.importer.strategies.datasetacquisition; -import org.dcm4che3.data.Attributes; import org.shanoir.ng.datasetacquisition.model.DatasetAcquisition; +import org.shanoir.ng.download.AcquisitionAttributes; import org.shanoir.ng.importer.dto.ImportJob; import org.shanoir.ng.importer.dto.Serie; @@ -35,6 +35,6 @@ public interface DatasetAcquisitionStrategy { // Create a new dataset acquisition - DatasetAcquisition generateDatasetAcquisitionForSerie(Serie serie, int rank, ImportJob importJob, Attributes dicomAttributes) throws Exception; + DatasetAcquisition generateDatasetAcquisitionForSerie(Serie serie, int rank, ImportJob importJob, AcquisitionAttributes dicomAttributes) throws Exception; } \ No newline at end of file diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/strategies/datasetacquisition/GenericDatasetAcquisitionStrategy.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/strategies/datasetacquisition/GenericDatasetAcquisitionStrategy.java index 20a61b7fc9..a23c467318 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/strategies/datasetacquisition/GenericDatasetAcquisitionStrategy.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/strategies/datasetacquisition/GenericDatasetAcquisitionStrategy.java @@ -9,6 +9,7 @@ import org.shanoir.ng.dataset.model.Dataset; import org.shanoir.ng.datasetacquisition.model.DatasetAcquisition; import org.shanoir.ng.datasetacquisition.model.GenericDatasetAcquisition; +import org.shanoir.ng.download.AcquisitionAttributes; import org.shanoir.ng.importer.dto.DatasetsWrapper; import org.shanoir.ng.importer.dto.ImportJob; import org.shanoir.ng.importer.dto.Serie; @@ -28,13 +29,13 @@ public class GenericDatasetAcquisitionStrategy implements DatasetAcquisitionStra @Override - public DatasetAcquisition generateDatasetAcquisitionForSerie(Serie serie, int rank, ImportJob importJob, Attributes dicomAttributes) throws Exception { + public DatasetAcquisition generateDatasetAcquisitionForSerie(Serie serie, int rank, ImportJob importJob, AcquisitionAttributes dicomAttributes) throws Exception { GenericDatasetAcquisition datasetAcquisition = new GenericDatasetAcquisition(); LOG.info("Generating DatasetAcquisition for : {} - {} - Rank:{}",serie.getSequenceName(), serie.getProtocolName(), rank); datasetAcquisition.setRank(rank); importJob.getProperties().put(ImportJob.RANK_PROPERTY, String.valueOf(rank)); datasetAcquisition.setSortingIndex(serie.getSeriesNumber()); - datasetAcquisition.setSoftwareRelease(dicomAttributes.getString(Tag.SoftwareVersions)); + datasetAcquisition.setSoftwareRelease(dicomAttributes.getFirstDatasetAttributes().getString(Tag.SoftwareVersions)); DatasetsWrapper datasetsWrapper = datasetStrategy.generateDatasetsForSerie(dicomAttributes, serie, importJob); List genericizedList = new ArrayList<>(); for (Dataset dataset : datasetsWrapper.getDatasets()) { diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/strategies/datasetacquisition/MrDatasetAcquisitionStrategy.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/strategies/datasetacquisition/MrDatasetAcquisitionStrategy.java index d4c2b68687..3130a8cbbd 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/strategies/datasetacquisition/MrDatasetAcquisitionStrategy.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/strategies/datasetacquisition/MrDatasetAcquisitionStrategy.java @@ -22,7 +22,6 @@ import java.util.List; import java.util.Map; -import org.dcm4che3.data.Attributes; import org.dcm4che3.data.Tag; import org.shanoir.ng.dataset.modality.BidsDataType; import org.shanoir.ng.dataset.modality.MrDataset; @@ -31,6 +30,7 @@ import org.shanoir.ng.datasetacquisition.model.mr.MrDatasetAcquisition; import org.shanoir.ng.datasetacquisition.model.mr.MrProtocol; import org.shanoir.ng.datasetacquisition.model.mr.MrProtocolSCMetadata; +import org.shanoir.ng.download.AcquisitionAttributes; import org.shanoir.ng.importer.dto.DatasetsWrapper; import org.shanoir.ng.importer.dto.ImportJob; import org.shanoir.ng.importer.dto.Serie; @@ -80,14 +80,14 @@ public class MrDatasetAcquisitionStrategy implements DatasetAcquisitionStrategy } @Override - public DatasetAcquisition generateDatasetAcquisitionForSerie(Serie serie, int rank, ImportJob importJob, Attributes dicomAttributes) throws Exception { + public DatasetAcquisition generateDatasetAcquisitionForSerie(Serie serie, int rank, ImportJob importJob, AcquisitionAttributes dicomAttributes) throws Exception { MrDatasetAcquisition mrDatasetAcquisition = new MrDatasetAcquisition(); LOG.info("Generating DatasetAcquisition for : {} - {} - Rank:{}", serie.getSequenceName(), serie.getProtocolName(), rank); mrDatasetAcquisition.setCreationDate(LocalDate.now()); mrDatasetAcquisition.setRank(rank); importJob.getProperties().put(ImportJob.RANK_PROPERTY, String.valueOf(rank)); mrDatasetAcquisition.setSortingIndex(serie.getSeriesNumber()); - mrDatasetAcquisition.setSoftwareRelease(dicomAttributes.getString(Tag.SoftwareVersions)); + mrDatasetAcquisition.setSoftwareRelease(dicomAttributes.getFirstDatasetAttributes().getString(Tag.SoftwareVersions)); MrProtocol mrProtocol = mrProtocolStrategy.generateProtocolForSerie(dicomAttributes, serie); mrDatasetAcquisition.setMrProtocol(mrProtocol); @@ -112,9 +112,8 @@ public DatasetAcquisition generateDatasetAcquisitionForSerie(Serie serie, int ra } // Can be overridden by study cards - String imageType = dicomAttributes.getString(Tag.ImageType, 2); + String imageType = dicomAttributes.getFirstDatasetAttributes().getString(Tag.ImageType, 2); if (imageType != null && dataTypeMapping.get(imageType) != null) { - MrProtocolSCMetadata metadata = mrDatasetAcquisition.getMrProtocol().getUpdatedMetadata(); if (mrDatasetAcquisition.getMrProtocol().getUpdatedMetadata() == null) { mrDatasetAcquisition.getMrProtocol().setUpdatedMetadata(new MrProtocolSCMetadata()); } diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/strategies/datasetacquisition/PetDatasetAcquisitionStrategy.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/strategies/datasetacquisition/PetDatasetAcquisitionStrategy.java index 72ca0aec57..736538a2a6 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/strategies/datasetacquisition/PetDatasetAcquisitionStrategy.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/strategies/datasetacquisition/PetDatasetAcquisitionStrategy.java @@ -24,6 +24,7 @@ import org.shanoir.ng.datasetacquisition.model.DatasetAcquisition; import org.shanoir.ng.datasetacquisition.model.pet.PetDatasetAcquisition; import org.shanoir.ng.datasetacquisition.model.pet.PetProtocol; +import org.shanoir.ng.download.AcquisitionAttributes; import org.shanoir.ng.importer.dto.DatasetsWrapper; import org.shanoir.ng.importer.dto.ImportJob; import org.shanoir.ng.importer.dto.Serie; @@ -53,7 +54,7 @@ public class PetDatasetAcquisitionStrategy implements DatasetAcquisitionStrategy @Override - public DatasetAcquisition generateDatasetAcquisitionForSerie(Serie serie, int rank, ImportJob importJob, Attributes dicomAttributes) + public DatasetAcquisition generateDatasetAcquisitionForSerie(Serie serie, int rank, ImportJob importJob, AcquisitionAttributes dicomAttributes) throws Exception { PetDatasetAcquisition datasetAcquisition = new PetDatasetAcquisition(); @@ -64,9 +65,9 @@ public DatasetAcquisition generateDatasetAcquisitionForSerie(Serie serie, int ra importJob.getProperties().put(ImportJob.RANK_PROPERTY, String.valueOf(rank)); datasetAcquisition.setSortingIndex(serie.getSeriesNumber()); - datasetAcquisition.setSoftwareRelease(dicomAttributes.getString(Tag.SoftwareVersions)); + datasetAcquisition.setSoftwareRelease(dicomAttributes.getFirstDatasetAttributes().getString(Tag.SoftwareVersions)); - PetProtocol protocol = protocolStrategy.generateProtocolForSerie(dicomAttributes); + PetProtocol protocol = protocolStrategy.generateProtocolForSerie(dicomAttributes.getFirstDatasetAttributes()); datasetAcquisition.setPetProtocol(protocol); // TODO ATO add Compatibility check between study card Equipment and dicomEquipment if not done at front level. diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/strategies/protocol/CtProtocolStrategy.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/strategies/protocol/CtProtocolStrategy.java index 7d41ae8c04..60c02df1e1 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/strategies/protocol/CtProtocolStrategy.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/strategies/protocol/CtProtocolStrategy.java @@ -14,15 +14,15 @@ package org.shanoir.ng.importer.strategies.protocol; -import org.dcm4che3.data.Attributes; import org.shanoir.ng.datasetacquisition.model.ct.CtProtocol; +import org.shanoir.ng.download.AcquisitionAttributes; import org.shanoir.ng.importer.dto.Serie; import org.springframework.stereotype.Component; @Component public class CtProtocolStrategy { - public CtProtocol generateProtocolForSerie(Attributes attributes, Serie serie) { + public CtProtocol generateProtocolForSerie(AcquisitionAttributes attributes, Serie serie) { CtProtocol protocol = new CtProtocol(); return protocol; } diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/strategies/protocol/MrProtocolStrategy.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/strategies/protocol/MrProtocolStrategy.java index 78568dd4c3..3ff430e586 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/strategies/protocol/MrProtocolStrategy.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/importer/strategies/protocol/MrProtocolStrategy.java @@ -34,6 +34,7 @@ import org.shanoir.ng.datasetacquisition.model.mr.MrSequenceKSpaceFill; import org.shanoir.ng.datasetacquisition.model.mr.ParallelAcquisitionTechnique; import org.shanoir.ng.datasetacquisition.model.mr.PatientPosition; +import org.shanoir.ng.download.AcquisitionAttributes; import org.shanoir.ng.importer.dto.CoilDTO; import org.shanoir.ng.importer.dto.CoilType; import org.shanoir.ng.importer.dto.Serie; @@ -47,7 +48,9 @@ public class MrProtocolStrategy { /** Logger. */ private static final Logger LOG = LoggerFactory.getLogger(MrProtocolStrategy.class); - public MrProtocol generateProtocolForSerie(Attributes attributes, Serie serie) { + public MrProtocol generateProtocolForSerie(AcquisitionAttributes acquisitionAttributes, Serie serie) { + + Attributes attributes = acquisitionAttributes.getFirstDatasetAttributes(); // dcm4che3 does not support MultiframeExtraction for MRS if (Boolean.TRUE.equals(serie.getIsEnhanced()) && !serie.getIsSpectroscopy()) { // MultiFrameExtractor is only used in case of EnhancedMR MRI. diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/shared/mapper/EchoTimeMapper.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/shared/mapper/EchoTimeMapper.java index 55dd019fe3..52af753a76 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/shared/mapper/EchoTimeMapper.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/shared/mapper/EchoTimeMapper.java @@ -33,12 +33,12 @@ public interface EchoTimeMapper { List EchoTimeDTOListToEchoTimeList( - List echoTimeDTOList); + List echoTimeDTOList); @Mapping(target = "echoTimeValue", source = "echoTime") EchoTime EchoTimeDTOToEchoTime( - org.shanoir.ng.importer.dto.EchoTime echoTimes); + org.shanoir.ng.shared.dicom.EchoTime echoTimes); } diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/shared/repository/SubjectStudyRepository.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/shared/repository/SubjectStudyRepository.java index 5665a92b49..b7ef3cf28a 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/shared/repository/SubjectStudyRepository.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/shared/repository/SubjectStudyRepository.java @@ -11,4 +11,6 @@ public interface SubjectStudyRepository extends CrudRepository findByStudy_Id(Long studyId); + public List findByStudy_IdAndSubjectId(Long studyId, Long subjectId); + } diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/shared/service/SubjectStudyService.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/shared/service/SubjectStudyService.java index 6ce6b28dcb..5c81f49df2 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/shared/service/SubjectStudyService.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/shared/service/SubjectStudyService.java @@ -36,4 +36,15 @@ public interface SubjectStudyService { */ @PreAuthorize("hasRole('ADMIN') or (hasAnyRole('EXPERT', 'USER') and (@datasetSecurityService.hasRightOnSubjectStudies(#subjectStudies, 'CAN_IMPORT') || @datasetSecurityService.hasRightOnSubjectStudies(#subjectStudies, 'CAN_ADMINISTRATE')))") List update(Iterable subjectStudies) throws EntityNotFoundException, MicroServiceCommunicationException; + + /** + * get subject-studies. + * + * @param subjectId + * @param studyId + * @return subject studies + * @throws EntityNotFoundException + */ + @PreAuthorize("hasRole('ADMIN') or (hasAnyRole('EXPERT', 'USER') and (@datasetSecurityService.hasRightOnSubjectStudies(#subjectStudies, 'CAN_SEE_ALL') || @datasetSecurityService.hasRightOnSubjectStudies(#subjectStudies, 'CAN_ADMINISTRATE')))") + List get(Long subjectId, Long studyId); } diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/shared/service/SubjectStudyServiceImpl.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/shared/service/SubjectStudyServiceImpl.java index 493b6b9c76..d4c7a83837 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/shared/service/SubjectStudyServiceImpl.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/shared/service/SubjectStudyServiceImpl.java @@ -47,7 +47,7 @@ public class SubjectStudyServiceImpl implements SubjectStudyService { @Override public List update(final Iterable subjectStudies) throws EntityNotFoundException, MicroServiceCommunicationException { - if (subjectStudies == null) return null; + if (subjectStudies == null) return null; Set ids = new HashSet<>(); for (SubjectStudy subjectStudy : subjectStudies) { ids.add(subjectStudy.getId()); @@ -94,4 +94,9 @@ private List getSubjectStudyTagDTOs(List get(Long subjectId, Long studyId) { + return subjectStudyRepository.findByStudy_IdAndSubjectId(studyId, subjectId); + } } diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/solr/service/SolrServiceImpl.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/solr/service/SolrServiceImpl.java index 70e18df95a..e76bec6786 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/solr/service/SolrServiceImpl.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/solr/service/SolrServiceImpl.java @@ -19,6 +19,15 @@ */ package org.shanoir.ng.solr.service; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + import org.apache.solr.client.solrj.SolrServerException; import org.shanoir.ng.shared.dateTime.DateTimeUtils; import org.shanoir.ng.shared.exception.RestServiceException; @@ -28,9 +37,7 @@ import org.shanoir.ng.shared.paging.PageImpl; import org.shanoir.ng.shared.repository.CenterRepository; import org.shanoir.ng.shared.repository.SubjectStudyRepository; -import org.shanoir.ng.shared.security.rights.StudyUserRight; import org.shanoir.ng.shared.subjectstudy.SubjectType; - import org.shanoir.ng.solr.model.ShanoirMetadata; import org.shanoir.ng.solr.model.ShanoirSolrDocument; import org.shanoir.ng.solr.model.ShanoirSolrQuery; @@ -53,10 +60,6 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; -import java.io.IOException; -import java.util.*; -import java.util.stream.Collectors; - /** * @author yyao * diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/controler/QualityCardApi.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/controler/QualityCardApi.java index fb369e0d70..0f5c58f2de 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/controler/QualityCardApi.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/controler/QualityCardApi.java @@ -136,5 +136,20 @@ ResponseEntity applyQualityCardOnStudy( @PreAuthorize("hasRole('ADMIN') or (hasRole('EXPERT') and @datasetSecurityService.hasRightOnQualityCard(#qualityCardId, 'CAN_ADMINISTRATE'))") ResponseEntity testQualityCardOnStudy( @Parameter(name = "id of the quality card", required = true) @PathVariable("qualityCardId") Long qualityCardId) throws RestServiceException, MicroServiceCommunicationException; - -} + + @Operation(summary = "", description = "Test a quality card on a study for quality control") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "applied a quality card on its study for quality control"), + @ApiResponse(responseCode = "401", description = "unauthorized"), + @ApiResponse(responseCode = "403", description = "forbidden"), + @ApiResponse(responseCode = "422", description = "bad parameters"), + @ApiResponse(responseCode = "500", description = "unexpected error") + }) + + @RequestMapping(value = "/test/{qualityCardId}/{start}/{stop}", method = RequestMethod.GET) + @PreAuthorize("hasRole('ADMIN') or (hasRole('EXPERT') and @datasetSecurityService.hasRightOnQualityCard(#qualityCardId, 'CAN_ADMINISTRATE'))") + ResponseEntity testQualityCardOnStudy( + @Parameter(name = "id of the quality card", required = true) @PathVariable("qualityCardId") Long qualityCardId, + @Parameter(name = "examination number start ", required = true) @PathVariable("start") int start, + @Parameter(name = "examination number stop", required = true) @PathVariable("stop") int stop) throws RestServiceException, MicroServiceCommunicationException; +} \ No newline at end of file diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/controler/QualityCardApiController.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/controler/QualityCardApiController.java index 12e29726c2..096913b881 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/controler/QualityCardApiController.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/controler/QualityCardApiController.java @@ -14,11 +14,17 @@ package org.shanoir.ng.studycard.controler; -import io.swagger.v3.oas.annotations.Parameter; +import java.util.List; + import org.shanoir.ng.shared.error.FieldErrorMap; -import org.shanoir.ng.shared.exception.*; +import org.shanoir.ng.shared.exception.EntityNotFoundException; +import org.shanoir.ng.shared.exception.ErrorDetails; +import org.shanoir.ng.shared.exception.ErrorModel; +import org.shanoir.ng.shared.exception.MicroServiceCommunicationException; +import org.shanoir.ng.shared.exception.RestServiceException; import org.shanoir.ng.studycard.dto.QualityCardResult; import org.shanoir.ng.studycard.model.QualityCard; +import org.shanoir.ng.studycard.model.rule.QualityExaminationRule; import org.shanoir.ng.studycard.service.CardsProcessingService; import org.shanoir.ng.studycard.service.QualityCardService; import org.shanoir.ng.studycard.service.QualityCardUniqueConstraintManager; @@ -32,7 +38,7 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; -import java.util.List; +import io.swagger.v3.oas.annotations.Parameter; @Controller public class QualityCardApiController implements QualityCardApi { @@ -114,6 +120,7 @@ public ResponseEntity updateQualityCard( @Parameter(name = "id of the quality card", required = true) @PathVariable("qualityCardId") Long qualityCardId, @Parameter(name = "quality card to update", required = true) @RequestBody QualityCard qualityCard, final BindingResult result) throws RestServiceException { + validate(qualityCard, result); try { qualityCardService.update(qualityCard); @@ -169,4 +176,18 @@ public ResponseEntity testQualityCardOnStudy( return new ResponseEntity<>(results, HttpStatus.OK); } + @Override + public ResponseEntity testQualityCardOnStudy( + @Parameter(name = "id of the quality card", required = true) @PathVariable("qualityCardId") Long qualityCardId, + @Parameter(name = "examination number start ", required = true) @PathVariable("start") int start, + @Parameter(name = "examination number stop", required = true) @PathVariable("stop") int stop) throws RestServiceException, MicroServiceCommunicationException { + + final QualityCard qualityCard = qualityCardService.findById(qualityCardId); + if (qualityCard == null) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + LOG.info("test quality card: name:" + qualityCard.getName() + ", studyId: " + qualityCard.getStudyId()); + QualityCardResult results = cardProcessingService.applyQualityCardOnStudy(qualityCard, start, stop); + return new ResponseEntity<>(results, HttpStatus.OK); + } } diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/dto/QualityCardResult.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/dto/QualityCardResult.java index 15f1c13ac8..b0315f9062 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/dto/QualityCardResult.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/dto/QualityCardResult.java @@ -21,6 +21,9 @@ import org.shanoir.ng.shared.model.SubjectStudy; import org.shanoir.ng.shared.quality.QualityTag; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + /** * This class contains the result of an application of a study card * on an entire study. For each examination, when the result is wrong @@ -92,5 +95,34 @@ public boolean hasError() { } return false; } + + public boolean hasWarning() { + for (QualityCardResultEntry entry : this) { + if (QualityTag.WARNING.equals(entry.getTagSet())) { + return true; + } + } + return false; + } + + public boolean hasFailedValid() { + for (QualityCardResultEntry entry : this) { + if (entry.isFailedValid()) { + return true; + } + } + return false; + } + + @Override + public String toString() { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.findAndRegisterModules(); + try { + return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(this); + } catch (JsonProcessingException e) { + return "json error"; + } + } } diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/dto/QualityCardResultEntry.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/dto/QualityCardResultEntry.java index 31ed9a48ec..6190dbf10e 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/dto/QualityCardResultEntry.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/dto/QualityCardResultEntry.java @@ -35,6 +35,8 @@ public class QualityCardResultEntry { private QualityTag tagSet; + private boolean failedValid = false; // if tag VALID was to put but conditions failed + public String getSubjectName() { return subjectName; } @@ -74,4 +76,12 @@ public QualityTag getTagSet() { public void setTagSet(QualityTag tagSet) { this.tagSet = tagSet; } + + public boolean isFailedValid() { + return failedValid; + } + + public void setFailedValid(boolean failedValid) { + this.failedValid = failedValid; + } } diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/Operation.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/Operation.java index 290bcbd619..b3f336fcbb 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/Operation.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/Operation.java @@ -22,7 +22,10 @@ public enum Operation { CONTAINS(4), SMALLER_THAN(5), BIGGER_THAN(6), - DOES_NOT_CONTAIN(7); + DOES_NOT_CONTAIN(7), + DOES_NOT_START_WITH(8), + NOT_EQUALS(9), + DOES_NOT_END_WITH(10); private int id; @@ -53,11 +56,11 @@ public int getId() { } public boolean isNumerical() { - return this.equals(BIGGER_THAN) || this.equals(EQUALS) || this.equals(SMALLER_THAN); + return this.equals(BIGGER_THAN) || this.equals(EQUALS) || this.equals(SMALLER_THAN) || this.equals(NOT_EQUALS); } public boolean isTextual() { - return this.equals(CONTAINS) || this.equals(DOES_NOT_CONTAIN) || this.equals(ENDS_WITH) || this.equals(EQUALS) || this.equals(STARTS_WITH); + return this.equals(CONTAINS) || this.equals(DOES_NOT_CONTAIN) || this.equals(ENDS_WITH) || this.equals(DOES_NOT_END_WITH) || this.equals(EQUALS) || this.equals(NOT_EQUALS) || this.equals(STARTS_WITH) || this.equals(DOES_NOT_START_WITH); } } diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/QualityCard.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/QualityCard.java index 78e87da779..71916b4e27 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/QualityCard.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/QualityCard.java @@ -14,12 +14,12 @@ package org.shanoir.ng.studycard.model; -import com.fasterxml.jackson.annotation.JsonPropertyOrder; -import jakarta.persistence.*; -import jakarta.validation.constraints.NotNull; -import org.dcm4che3.data.Attributes; +import java.util.List; + import org.hibernate.annotations.GenericGenerator; import org.hibernate.validator.constraints.NotBlank; +import org.shanoir.ng.download.ExaminationAttributes; +import org.shanoir.ng.download.WADODownloaderService; import org.shanoir.ng.examination.model.Examination; import org.shanoir.ng.shared.hateoas.HalEntity; import org.shanoir.ng.shared.hateoas.Links; @@ -27,7 +27,16 @@ import org.shanoir.ng.studycard.dto.QualityCardResult; import org.shanoir.ng.studycard.model.rule.QualityExaminationRule; -import java.util.List; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToMany; +import jakarta.persistence.PostLoad; +import jakarta.validation.constraints.NotNull; /** * Study card. @@ -95,11 +104,26 @@ public void setRules(List rules) { * @param studyCard * @param dicomAttributes */ - public QualityCardResult apply(Examination examination, Attributes dicomAttributes) { + public QualityCardResult apply(Examination examination, ExaminationAttributes dicomAttributes, WADODownloaderService downloader) { QualityCardResult result = new QualityCardResult(); if (this.getRules() != null) { for (QualityExaminationRule rule : this.getRules()) { - rule.apply(examination, dicomAttributes, result); + rule.apply(examination, dicomAttributes, result, downloader); + } + } + return result; + } + + /** + * Application during import, when dicoms are present in tmp directory. + * @param examination + * @param studyCard + */ + public QualityCardResult apply(Examination examination, WADODownloaderService downloader) { + QualityCardResult result = new QualityCardResult(); + if (this.getRules() != null) { + for (QualityExaminationRule rule : this.getRules()) { + rule.apply(examination, result, downloader); } } return result; @@ -112,4 +136,15 @@ public boolean isToCheckAtImport() { public void setToCheckAtImport(boolean toCheckAtImport) { this.toCheckAtImport = toCheckAtImport; } + + public boolean hasDicomConditions() { + if (getRules() != null) { + for (QualityExaminationRule rule : getRules()) { + if (rule.hasDicomConditions()) { + return true; + } + } + } + return false; + } } diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/QualityException.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/QualityException.java index 3730912692..43cb715bfc 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/QualityException.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/QualityException.java @@ -59,8 +59,6 @@ public void setQualityResult(QualityCardResult qualityResult) { public String buildErrorMessage() { StringBuilder sb = new StringBuilder(); - sb.append("Quality checks didn't pass at import."); - sb.append("\n"); sb.append("Study : ") .append(this.getExamination().getStudy().getName()) .append(" (").append(this.getExamination().getStudy().getId()).append(")"); diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/StudyCard.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/StudyCard.java index 19e81267a4..57178d7365 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/StudyCard.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/StudyCard.java @@ -21,6 +21,7 @@ import org.hibernate.validator.constraints.NotBlank; import org.shanoir.ng.dataset.model.Dataset; import org.shanoir.ng.datasetacquisition.model.DatasetAcquisition; +import org.shanoir.ng.download.AcquisitionAttributes; import org.shanoir.ng.shared.hateoas.HalEntity; import org.shanoir.ng.shared.hateoas.Links; import org.shanoir.ng.shared.validation.Unique; @@ -142,7 +143,7 @@ public void setLastEditTimestamp(Long lastEditTimestamp) { * @param dicomAttributes * @return true if the application had any effect on acquisitions */ - public boolean apply(DatasetAcquisition acquisition, Attributes dicomAttributes) { + public boolean apply(DatasetAcquisition acquisition, AcquisitionAttributes dicomAttributes) { boolean changeInAtLeastOneAcquisition = false; if (this.getRules() != null) { for (StudyCardRule rule : this.getRules()) { @@ -152,7 +153,16 @@ public boolean apply(DatasetAcquisition acquisition, Attributes dicomAttributes) } else if (rule instanceof DatasetRule && acquisition.getDatasets() != null) { for (Dataset dataset : acquisition.getDatasets()) { changeInAtLeastOneAcquisition = true; - ((DatasetRule) rule).apply(dataset, dicomAttributes); + Attributes attributes; + if (String.class.equals(dicomAttributes.getParametrizedType())) { + // @SuppressWarnings("unchecked") doesn't work ... + attributes = ((AcquisitionAttributes)dicomAttributes).getDatasetAttributes(dataset.getSOPInstanceUID()); + } else if (Long.class.equals(dicomAttributes.getParametrizedType())) { + attributes = ((AcquisitionAttributes)dicomAttributes).getDatasetAttributes(dataset.getId()); + } else { + throw new IllegalStateException("the parametrized type of AcquisitionAttributes is not implemented, use String or Long"); + } + ((DatasetRule) rule).apply(dataset, attributes); } } else { throw new IllegalStateException("unknown type of rule"); diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/condition/AcqMetadataCondOnDatasets.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/condition/AcqMetadataCondOnDatasets.java index 42cc06d9dd..58371af3da 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/condition/AcqMetadataCondOnDatasets.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/condition/AcqMetadataCondOnDatasets.java @@ -32,7 +32,7 @@ @Entity @DiscriminatorValue("AcqMetadataCondOnDatasets") @JsonTypeName("AcqMetadataCondOnDatasets") -public class AcqMetadataCondOnDatasets extends StudyCardMetadataConditionWithCardinality{ +public class AcqMetadataCondOnDatasets extends StudyCardMetadataCondition{ private static final Logger LOG = LoggerFactory.getLogger(AcqMetadataCondOnDatasets.class); diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/condition/ExamMetadataCondOnAcq.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/condition/ExamMetadataCondOnAcq.java index b37bcd38ea..9296d10023 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/condition/ExamMetadataCondOnAcq.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/condition/ExamMetadataCondOnAcq.java @@ -33,7 +33,7 @@ @Entity @DiscriminatorValue("ExamMetadataCondOnAcq") @JsonTypeName("ExamMetadataCondOnAcq") -public class ExamMetadataCondOnAcq extends StudyCardMetadataConditionWithCardinality{ +public class ExamMetadataCondOnAcq extends StudyCardMetadataCondition{ private static final Logger LOG = LoggerFactory.getLogger(ExamMetadataCondOnAcq.class); @@ -78,14 +78,14 @@ public boolean fulfilled(List acquisitions, StringBuffer err boolean complies = cardinalityComplies(nbOk, total); if (!complies) { if (getCardinality() == -1) { - errorMsg.append("condition [" + toString() + "] failed because only " + nbOk + " out of all (" + total + ") acquisitions complied"); + errorMsg.append("\ncondition [" + toString() + "] failed because only " + nbOk + " out of all (" + total + ") acquisitions complied"); } else if (getCardinality() == 0) { - errorMsg.append("condition [" + toString() + "] failed because " + nbOk + " acquisitions complied where 0 was required"); + errorMsg.append("\ncondition [" + toString() + "] failed because " + nbOk + " acquisitions complied where 0 was required"); } else { - errorMsg.append("condition [" + toString() + "] failed because only " + nbOk + " out of " + total + " acquisitions complied"); + errorMsg.append("\ncondition [" + toString() + "] failed because only " + nbOk + " out of " + total + " acquisitions complied"); } } else { - errorMsg.append("condition [" + toString() + "] succeed"); + errorMsg.append("\ncondition [" + toString() + "] succeed"); } return complies; } diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/condition/ExamMetadataCondOnDatasets.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/condition/ExamMetadataCondOnDatasets.java index e9dd8990b2..aa0b7de823 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/condition/ExamMetadataCondOnDatasets.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/condition/ExamMetadataCondOnDatasets.java @@ -34,7 +34,7 @@ @Entity @DiscriminatorValue("ExamMetadataCondOnDatasets") @JsonTypeName("ExamMetadataCondOnDatasets") -public class ExamMetadataCondOnDatasets extends StudyCardMetadataConditionWithCardinality{ +public class ExamMetadataCondOnDatasets extends StudyCardMetadataCondition{ private static final Logger LOG = LoggerFactory.getLogger(ExamMetadataCondOnDatasets.class); @@ -83,14 +83,14 @@ public boolean fulfilled(List acquisitions, StringBuffer err boolean complies = cardinalityComplies(nbOk, total); if (!complies) { if (getCardinality() == -1) { - errorMsg.append("condition [" + toString() + "] failed because only " + nbOk + " out of all (" + total + ") acquisitions complied"); + errorMsg.append("\ncondition [" + toString() + "] failed because only " + nbOk + " out of all (" + total + ") acquisitions complied"); } else if (getCardinality() == 0) { - errorMsg.append("condition [" + toString() + "] failed because " + nbOk + " acquisitions complied where 0 was required"); + errorMsg.append("\ncondition [" + toString() + "] failed because " + nbOk + " acquisitions complied where 0 was required"); } else { - errorMsg.append("condition [" + toString() + "] failed because only " + nbOk + " out of " + total + " acquisitions complied"); + errorMsg.append("\ncondition [" + toString() + "] failed because only " + nbOk + " out of " + total + " acquisitions complied"); } } else { - errorMsg.append("condition [" + toString() + "] succeed"); + errorMsg.append("\ncondition [" + toString() + "] succeed"); } return complies; } diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/condition/StudyCardCondition.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/condition/StudyCardCondition.java index c35d6e0414..76ff4b576a 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/condition/StudyCardCondition.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/condition/StudyCardCondition.java @@ -36,7 +36,7 @@ @DiscriminatorColumn(name="scope", discriminatorType = DiscriminatorType.STRING) @JsonTypeInfo(use = Id.NAME, include = As.PROPERTY, property = "scope") @JsonSubTypes({ - @JsonSubTypes.Type(value = StudyCardDICOMCondition.class, name = "StudyCardDICOMCondition"), + @JsonSubTypes.Type(value = StudyCardDICOMConditionOnDatasets.class, name = "StudyCardDICOMConditionOnDatasets"), @JsonSubTypes.Type(value = ExamMetadataCondOnDatasets.class, name = "ExamMetadataCondOnDatasets"), @JsonSubTypes.Type(value = ExamMetadataCondOnAcq.class, name = "ExamMetadataCondOnAcq"), @JsonSubTypes.Type(value = DatasetMetadataCondOnDataset.class, name = "DatasetMetadataCondOnDataset"), @@ -53,6 +53,29 @@ public abstract class StudyCardCondition extends AbstractEntity { @NotNull private int operation; + @NotNull + private int cardinality; + + public int getCardinality() { + return cardinality; + } + + public void setCardinality(int cardinality) { + this.cardinality = cardinality; + } + + protected boolean cardinalityComplies(int nbOk, int nbUnknown, int total) { + if (getCardinality() == -1) return total == nbOk || (nbOk > 0 && total == nbOk + nbUnknown); // all + if (getCardinality() == 0) return 0 == nbOk; // none + else return nbOk >= getCardinality(); // n + } + + protected boolean cardinalityComplies(int nbOk, int total) { + if (getCardinality() == -1) return total == nbOk; // all + if (getCardinality() == 0) return 0 == nbOk; // none + else return nbOk >= getCardinality(); // n + } + public Operation getOperation() { return Operation.getType(operation); } @@ -76,6 +99,8 @@ protected boolean numericalCompare(Operation operation, int comparison) { return comparison == 0; } else if (Operation.SMALLER_THAN.equals(operation)) { return comparison < 0; + } else if (Operation.NOT_EQUALS.equals(operation)) { + return comparison != 0; } throw new IllegalArgumentException("Cannot use this method for non-numerical operations (" + operation + ")"); } @@ -84,7 +109,9 @@ protected boolean textualCompare(Operation operation, String original, String st if (original != null) { if (Operation.EQUALS.equals(operation)) { return original.equals(studycardStr); - } else if (Operation.CONTAINS.equals(operation)) { + } else if (Operation.NOT_EQUALS.equals(operation)) { + return !original.equals(studycardStr); + } else if (Operation.CONTAINS.equals(operation)) { return original.contains(studycardStr); } else if (Operation.DOES_NOT_CONTAIN.equals(operation)) { return !original.contains(studycardStr); @@ -92,6 +119,10 @@ protected boolean textualCompare(Operation operation, String original, String st return original.startsWith(studycardStr); } else if (Operation.ENDS_WITH.equals(operation)) { return original.endsWith(studycardStr); + } else if (Operation.DOES_NOT_START_WITH.equals(operation)) { + return !original.startsWith(studycardStr); + } else if (Operation.DOES_NOT_END_WITH.equals(operation)) { + return !original.endsWith(studycardStr); } } else { LOG.error("Error in studycard processing: tag (from pacs) or field (from database) null."); diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/condition/StudyCardDICOMCondition.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/condition/StudyCardDICOMCondition.java deleted file mode 100644 index 74bf8d4ecc..0000000000 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/condition/StudyCardDICOMCondition.java +++ /dev/null @@ -1,164 +0,0 @@ -/** - * Shanoir NG - Import, manage and share neuroimaging data - * Copyright (C) 2009-2019 Inria - https://www.inria.fr/ - * Contact us on https://project.inria.fr/shanoir/ - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see https://www.gnu.org/licenses/gpl-3.0.html - */ - -package org.shanoir.ng.studycard.model.condition; - -import com.fasterxml.jackson.annotation.JsonTypeName; -import jakarta.persistence.DiscriminatorValue; -import jakarta.persistence.Entity; -import org.apache.commons.lang3.StringUtils; -import org.dcm4che3.data.Attributes; -import org.dcm4che3.data.Keyword; -import org.dcm4che3.data.StandardElementDictionary; -import org.dcm4che3.data.VR; -import org.shanoir.ng.studycard.model.DicomTagType; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.math.BigDecimal; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Date; - -@Entity -@DiscriminatorValue("StudyCardDICOMCondition") -@JsonTypeName("StudyCardDICOMCondition") -public class StudyCardDICOMCondition extends StudyCardCondition { - - private static final Logger LOG = LoggerFactory.getLogger(StudyCardDICOMCondition.class); - - private int dicomTag; - - public Integer getDicomTag() { - return dicomTag; - } - - public void setDicomTag(Integer dicomTag) { - this.dicomTag = dicomTag; - } - - public boolean fulfilled(Attributes dicomAttributes) { - return fulfilled(dicomAttributes, null); - } - - public boolean fulfilled(Attributes dicomAttributes, StringBuffer errorMsg) { - LOG.debug("conditionFulfilled: " + this.getId() + " processing one condition with all its values: "); - this.getValues().stream().forEach(s -> LOG.debug(s)); - if (dicomAttributes == null) { - if (errorMsg != null) errorMsg.append("condition [" + toString() - + "] was ignored because no dicom data was provided"); - return true; - } - if (!dicomAttributes.contains(getDicomTag())) { - if (errorMsg != null) errorMsg.append("condition [" + toString() - + "] failed because no value was found in the dicom for the tag " + getDicomTagCodeAndLabel(this.getDicomTag())); - return false; - } - VR tagVr = StandardElementDictionary.INSTANCE.vrOf(this.getDicomTag()); - DicomTagType tagType = DicomTagType.valueOf(tagVr); - // get all possible values, that can fulfill the condition - for (String value : this.getValues()) { - if (tagType.isNumerical()) { - if (!this.getOperation().isNumerical()) { - throw new IllegalArgumentException("Study card processing : operation " + this.getOperation() + " is not compatible with dicom tag " - + this.getDicomTag() + " of type " + tagType + "(condition id : " + this.getId() + ")"); - } - BigDecimal scValue = new BigDecimal(value); - Integer comparison = null; - if (DicomTagType.Float.equals(tagType)) { - Float floatValue = dicomAttributes.getFloat(this.getDicomTag(), Float.NaN); - if (floatValue.equals(Float.NaN)) { - if (errorMsg != null) errorMsg.append("condition [" + toString() - + "] failed because there was a problem when reading the tag " + getDicomTagCodeAndLabel(this.getDicomTag())); - return false; - } else comparison = BigDecimal.valueOf(floatValue).compareTo(scValue); - // There is no dicomAttributes.getLong() ! - } else if (DicomTagType.Double.equals(tagType) || DicomTagType.Long.equals(tagType)) { - Double doubleValue = dicomAttributes.getDouble(this.getDicomTag(), Double.NaN); - if (doubleValue.equals(Double.NaN)) { - if (errorMsg != null) errorMsg.append("condition [" + toString() - + "] failed because there was a problem when reading the tag " + getDicomTagCodeAndLabel(this.getDicomTag())); - return false; - } else comparison = BigDecimal.valueOf(doubleValue).compareTo(scValue); - } else if (DicomTagType.Integer.equals(tagType)) { - Integer integerValue = dicomAttributes.getInt(this.getDicomTag(), Integer.MIN_VALUE); - if (integerValue.equals(Integer.MIN_VALUE)) { - if (errorMsg != null) errorMsg.append("condition [" + toString() - + "] failed because there was a problem when reading the tag " + getDicomTagCodeAndLabel(this.getDicomTag())); - return false; - } else comparison = BigDecimal.valueOf(integerValue).compareTo(scValue); - } else if (DicomTagType.Date.equals(tagType)) { - Date dateValue = dicomAttributes.getDate(this.getDicomTag()); - if (dateValue.equals(null)) { - if (errorMsg != null) errorMsg.append("condition [" + toString() - + "] failed because there was a problem when reading the date tag " + getDicomTagCodeAndLabel(this.getDicomTag())); - return false; - } else { - try { - Date scDate = new SimpleDateFormat("yyyyMMdd").parse(value); - comparison = dateValue.compareTo(scDate); - } catch (ParseException e) { - if (errorMsg != null) errorMsg.append("condition [" + toString() - + "] failed because there was a problem parsing the value as a date"); - return false; - } - } - } - if (comparison != null && numericalCompare(this.getOperation(), comparison)) { - if (errorMsg != null) errorMsg.append("condition [" + toString() + "] succeed"); - return true; // as condition values are combined by OR: return if one is true - } - } else if (tagType.isTextual()) { - if (!this.getOperation().isTextual()) { - throw new IllegalArgumentException("Study card processing : operation " + this.getOperation() + " is not compatible with dicom tag " - + getDicomTagCodeAndLabel(this.getDicomTag()) + " of type " + tagType + "(condition id : " + this.getId() + ")"); - } - String stringValue = dicomAttributes.getString(this.getDicomTag()); - if (stringValue == null) { - LOG.warn("Could not find a value in the dicom for the tag " + getDicomTagCodeAndLabel(this.getDicomTag())); - if (errorMsg != null) errorMsg.append("condition [" + toString() - + "] failed because there was a problem when reading the tag " + getDicomTagCodeAndLabel(this.getDicomTag())); - return false; - } - if (textualCompare(this.getOperation(), stringValue, value)) { - if (errorMsg != null) errorMsg.append("condition [" + toString() + "] succeed"); - return true; // as condition values are combined by OR: return if one is true - } - } - } - if (errorMsg != null) errorMsg.append("condition [" + toString() + "] failed "); - return false; - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("DICOM field ").append(getDicomTagCodeAndLabel(getDicomTag())) - .append(" ").append(getOperation().name()) - .append(" to ") - .append(StringUtils.join(getValues(), " or ")); - return sb.toString(); - } - - private String getDicomTagHexString(int tag) { - String hexStr = Integer.toHexString(tag); - hexStr = StringUtils.leftPad(hexStr, 8, "0"); - hexStr = hexStr.substring(0, 5) + "," + hexStr.substring(5); - return hexStr; - } - - private String getDicomTagCodeAndLabel(int tag) { - return Keyword.valueOf(tag) + " (" + getDicomTagHexString(tag) + ")"; - } -} diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/condition/StudyCardDICOMConditionOnDatasets.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/condition/StudyCardDICOMConditionOnDatasets.java new file mode 100644 index 0000000000..bfda066beb --- /dev/null +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/condition/StudyCardDICOMConditionOnDatasets.java @@ -0,0 +1,311 @@ +/** + * Shanoir NG - Import, manage and share neuroimaging data + * Copyright (C) 2009-2019 Inria - https://www.inria.fr/ + * Contact us on https://project.inria.fr/shanoir/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/gpl-3.0.html + */ + +package org.shanoir.ng.studycard.model.condition; + +import java.math.BigDecimal; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.dcm4che3.data.Attributes; +import org.dcm4che3.data.Keyword; +import org.dcm4che3.data.StandardElementDictionary; +import org.dcm4che3.data.VR; +import org.shanoir.ng.dataset.model.Dataset; +import org.shanoir.ng.datasetacquisition.model.DatasetAcquisition; +import org.shanoir.ng.download.AcquisitionAttributes; +import org.shanoir.ng.download.ExaminationAttributes; +import org.shanoir.ng.download.WADODownloaderService; +import org.shanoir.ng.shared.exception.PacsException; +import org.shanoir.ng.studycard.model.DicomTagType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.annotation.JsonTypeName; + +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; + +@Entity +@DiscriminatorValue("StudyCardDICOMConditionOnDatasets") +@JsonTypeName("StudyCardDICOMConditionOnDatasets") +public class StudyCardDICOMConditionOnDatasets extends StudyCardCondition { + + private static final Logger LOG = LoggerFactory.getLogger(StudyCardDICOMConditionOnDatasets.class); + + private int dicomTag; + + public Integer getDicomTag() { + return dicomTag; + } + + public void setDicomTag(Integer dicomTag) { + this.dicomTag = dicomTag; + } + + public boolean fulfilled(ExaminationAttributes dicomAttributes) { + return fulfilled(dicomAttributes, new StringBuffer()); + } + + /** + * Check the conditions on a complete set of already known dicom Attributes + * @param the type of the used keys + * @param examinationAttributes complete set of already known dicom Attributes, not a cache + * @param errorMsg + * @return + */ + public boolean fulfilled(ExaminationAttributes examinationAttributes, StringBuffer errorMsg) { + if (examinationAttributes == null) throw new IllegalArgumentException("dicomAttributes can not be null"); + int nbOk = 0; int total = 0; int nbUnknown = 0; + for (T acqId : examinationAttributes.getAcquisitionIds()) { + AcquisitionAttributes acqAttributes = examinationAttributes.getAcquisitionAttributes(acqId); + for (T datasetId : acqAttributes.getDatasetIds()) { + total++; + boolean alreadyFulfilled = getCardinality() >= 1 && nbOk >= getCardinality(); + if (!alreadyFulfilled) { + Boolean fulfilled = fulfilled(acqAttributes.getDatasetAttributes(datasetId), errorMsg, datasetId ); + if (fulfilled == null) { + nbUnknown++; + } + else if (fulfilled) { + nbOk++; + } + } + } + } + boolean complies = cardinalityComplies(nbOk, nbUnknown, total); + writeConditionsReport(errorMsg, complies, nbOk, nbUnknown, total); + return complies; + } + + /** + * Check condition on acquisitions + * @param acquisitions data checked + * @param examinationAttributesCache to be used as a cache + * @param errorMsg + * @return + */ + public boolean fulfilled(List acquisitions, ExaminationAttributes examinationAttributesCache, WADODownloaderService downloader, StringBuffer errorMsg) { + if (acquisitions == null) throw new IllegalArgumentException("acquisitions can not be null"); + int nbOk = 0; int total = 0; int nbUnknown = 0; + for (DatasetAcquisition acquisition : acquisitions) { + if (!examinationAttributesCache.has(acquisition.getId())) { + examinationAttributesCache.addAcquisitionAttributes(acquisition.getId(), new AcquisitionAttributes()); + } + AcquisitionAttributes acqAttributes = examinationAttributesCache.getAcquisitionAttributes(acquisition.getId()); + for (Dataset dataset : acquisition.getDatasets()) { + total++; + boolean alreadyFulfilled = getCardinality() >= 1 && nbOk >= getCardinality(); + if (!alreadyFulfilled) { + if (!acqAttributes.has(dataset.getId())) { + acqAttributes.addDatasetAttributes(dataset.getId(), downloadAttributes(dataset, downloader, errorMsg)); + } + if (acqAttributes.getDatasetAttributes(dataset.getId()) == null) { // in case of pacs error + nbUnknown++; + } else { + Boolean fulfilled = fulfilled(acqAttributes.getDatasetAttributes(dataset.getId()), errorMsg, dataset.getId() ); + if (fulfilled == null) { + nbUnknown++; + } + else if (fulfilled) { + nbOk++; + } + } + } + } + } + boolean complies = cardinalityComplies(nbOk, nbUnknown, total); + writeConditionsReport(errorMsg, complies, nbOk, nbUnknown, total); + return complies; + } + + public boolean fulfilled(AcquisitionAttributes acqAttributes) { + return fulfilled(acqAttributes, new StringBuffer()); + } + + public boolean fulfilled(AcquisitionAttributes acqAttributes, StringBuffer errorMsg) { + if (acqAttributes == null) throw new IllegalArgumentException("dicomAttributes can not be null"); + int nbOk = 0; int total = 0; int nbUnknown = 0; + for (T datasetId : acqAttributes.getDatasetIds()) { + total++; + boolean alreadyFulfilled = getCardinality() >= 1 && nbOk >= getCardinality(); + if (!alreadyFulfilled) { + Boolean fulfilled = fulfilled(acqAttributes.getDatasetAttributes(datasetId), errorMsg,datasetId); + if (fulfilled == null) { + nbUnknown++; + } + else if (fulfilled) { + nbOk++; + } + } + } + boolean complies = cardinalityComplies(nbOk, nbUnknown, total); + writeConditionsReport(errorMsg, complies, nbOk, nbUnknown, total); + return complies; + } + + public Boolean fulfilled(Attributes dicomAttributes, Object datasetId) { + return fulfilled(dicomAttributes, new StringBuffer(), datasetId); + } + + private Boolean fulfilled(Attributes dicomAttributes, StringBuffer errorMsg, Object datasetId) { + LOG.debug("conditionFulfilled: " + this.getId() + " processing condition " + getId() + " with all its values: "); + this.getValues().stream().forEach(s -> LOG.debug(s)); + if (dicomAttributes == null) { + throw new IllegalArgumentException("dicomAttributes can't be null"); + } + if (!dicomAttributes.contains(getDicomTag())) { + if (errorMsg != null) errorMsg.append("\ncondition [" + toString() + + "] failed on dataset " + datasetId + " because no value was found in the dicom for the tag " + getDicomTagCodeAndLabel(this.getDicomTag())); + return false; + } + VR tagVr = StandardElementDictionary.INSTANCE.vrOf(dicomTag); + DicomTagType tagType = DicomTagType.valueOf(tagVr); + // get all possible values, that can fulfill the condition + for (String value : this.getValues()) { + if (tagType.isNumerical()) { + if (!this.getOperation().isNumerical()) { + throw new IllegalArgumentException("Study card processing : operation " + this.getOperation() + " is not compatible with dicom tag " + + this.getDicomTag() + " of type " + tagType + "(condition id : " + this.getId() + ")"); + } else { + BigDecimal scValue = new BigDecimal(value); + Integer comparison = null; + if (DicomTagType.Float.equals(tagType)) { + Float floatValue = dicomAttributes.getFloat(this.getDicomTag(), Float.NaN); + if (floatValue.equals(Float.NaN)) { + if (errorMsg != null) errorMsg.append("\ncondition [" + toString() + + "] failed on dataset " + datasetId + " because could not find/extract a value in the dicom for the tag " + getDicomTagCodeAndLabel(this.getDicomTag())); + return false; + } else comparison = BigDecimal.valueOf(floatValue).compareTo(scValue); + // There is no dicomAttributes.getLong() ! + } else if (DicomTagType.Double.equals(tagType) || DicomTagType.Long.equals(tagType)) { + Double doubleValue = dicomAttributes.getDouble(this.getDicomTag(), Double.NaN); + if (doubleValue.equals(Double.NaN)) { + if (errorMsg != null) errorMsg.append("\ncondition [" + toString() + + "] failed on dataset " + datasetId + " because could not find/extract a value in the dicom for the tag " + getDicomTagCodeAndLabel(this.getDicomTag())); + return false; + } else comparison = BigDecimal.valueOf(doubleValue).compareTo(scValue); + } else if (DicomTagType.Integer.equals(tagType)) { + Integer integerValue = dicomAttributes.getInt(this.getDicomTag(), Integer.MIN_VALUE); + if (integerValue.equals(Integer.MIN_VALUE)) { + if (errorMsg != null) errorMsg.append("\ncondition [" + toString() + + "] failed on dataset " + datasetId + " because could not find/extract a value in the dicom for the tag " + getDicomTagCodeAndLabel(this.getDicomTag())); + return false; + } else comparison = BigDecimal.valueOf(integerValue).compareTo(scValue); + } else if (DicomTagType.Date.equals(tagType)) { + Date dateValue = dicomAttributes.getDate(this.getDicomTag()); + if (dateValue.equals(null)) { + if (errorMsg != null) errorMsg.append("\ncondition [" + toString() + + "] failed on dataset " + datasetId + " because could not find/extract a value in the dicom for the date tag " + getDicomTagCodeAndLabel(this.getDicomTag())); + return false; + } else { + try { + Date scDate = new SimpleDateFormat("yyyyMMdd").parse(value); + comparison = dateValue.compareTo(scDate); + } catch (ParseException e) { + if (errorMsg != null) errorMsg.append("\ncondition [" + toString() + + "] could not be checked on dataset " + datasetId + " because there was a date format problem"); + return null; + } + } + } else { + throw new IllegalStateException("tagType for tag " + dicomTag + " is not implemented, tagType : " + tagType); + } + if (comparison != null && numericalCompare(this.getOperation(), comparison)) { + if (errorMsg != null) errorMsg.append("\ncondition [" + toString() + "] succeed on dataset " + datasetId + ", value found : " + dicomAttributes.getString(this.getDicomTag())); + return true; // as condition values are combined by OR: return if one is true + } // else continue to check other values + } + } else if (tagType.isTextual()) { + if (!this.getOperation().isTextual()) { + throw new IllegalArgumentException("Study card processing : operation " + this.getOperation() + " is not a textual operation and is not compatible with the textual dicom tag " + + getDicomTagCodeAndLabel(this.getDicomTag()) + " of type " + tagType + "(condition id : " + this.getId() + ")"); + } else { + String stringValue = dicomAttributes.getString(this.getDicomTag()); + if (stringValue == null) { + LOG.warn("Could not find a value in the dicom for the tag " + getDicomTagCodeAndLabel(this.getDicomTag())); + if (errorMsg != null) errorMsg.append("\ncondition [" + toString() + + "] failed because could not find/extract a value in the dicom for the tag " + getDicomTagCodeAndLabel(this.getDicomTag())); + return false; + } else if (textualCompare(this.getOperation(), stringValue, value)) { + if (errorMsg != null) errorMsg.append("\ncondition [" + toString() + "] succeed on acquisition "); + return true; // as condition values are combined by OR: return if one is true + } // else continue to check other values + } + } else { + throw new IllegalStateException("tagType for tag " + dicomTag + " is neither numerical or textual, tagType : " + tagType); + } + } + if (errorMsg != null) errorMsg.append("\ncondition [" + toString() + "] failed on dataset " + datasetId + ", the found dicom value : " + dicomAttributes.getString(this.getDicomTag()) + " matches none of the given values : [" + String.join(", ", getValues()) + "] - operator : " + getOperation() + ")"); + return false; + } + + private Attributes downloadAttributes(Dataset dataset, WADODownloaderService downloader, StringBuffer errorMsg) { + try { + Attributes attributes = downloader.getDicomAttributesForDataset(dataset); + return attributes; + } catch (PacsException e) { + if (errorMsg != null) errorMsg.append("\ncondition [" + toString() + + "] was ignored on dataset " + dataset.getId() + " because no dicom data could be found on pacs"); + LOG.warn("condition [" + toString() + + "] was ignored on dataset " + dataset.getId() + " because no dicom data could be found on pacs, reason : " + e.getMessage()); + return null; + } + } + + private void writeConditionsReport(StringBuffer errorMsg, boolean complies, int nbOk, int nbUnknown, int total) { + if (!complies) { + if (getCardinality() == -1) { + errorMsg.append("\ncondition [" + toString() + "] failed because only " + nbOk + " out of all (" + total + ") datasets complied" + (nbUnknown > 0 ? " (" + nbUnknown + " unknown)" : "")); + } else if (getCardinality() == 0) { + errorMsg.append("\ncondition [" + toString() + "] failed because " + nbOk + " datasets complied where 0 was required" + (nbUnknown > 0 ? " (" + nbUnknown + " unknown)" : "")); + } else { + errorMsg.append("\ncondition [" + toString() + "] failed because only " + nbOk + " out of " + total + " datasets complied" + (nbUnknown > 0 ? " (" + nbUnknown + " unknown)" : "")); + } + } else { + errorMsg.append("\ncondition [" + toString() + "] succeed because " + nbOk + " out of " + total + " datasets complied" + (nbUnknown > 0 ? " (" + nbUnknown + " unknown)" : "")); + } + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + if (getCardinality() == -1) { + sb.append("all datasets "); + } else if (getCardinality() == 0) { + sb.append("no dataset "); + } else { + sb.append(getCardinality()).append(" datasets "); + } + sb.append("DICOM field ").append(getDicomTagCodeAndLabel(getDicomTag())) + .append(" ").append(getOperation().name()) + .append(" to ") + .append(StringUtils.join(getValues(), " or ")); + return sb.toString(); + } + + private String getDicomTagHexString(int tag) { + String hexStr = Integer.toHexString(tag); + hexStr = StringUtils.leftPad(hexStr, 8, "0"); + hexStr = hexStr.substring(0, 5) + "," + hexStr.substring(5); + return hexStr; + } + + private String getDicomTagCodeAndLabel(int tag) { + return Keyword.valueOf(tag) + " (" + getDicomTagHexString(tag) + ")"; + } +} diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/condition/StudyCardMetadataConditionWithCardinality.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/condition/StudyCardMetadataConditionWithCardinality.java deleted file mode 100644 index bfcdec86b8..0000000000 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/condition/StudyCardMetadataConditionWithCardinality.java +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Shanoir NG - Import, manage and share neuroimaging data - * Copyright (C) 2009-2019 Inria - https://www.inria.fr/ - * Contact us on https://project.inria.fr/shanoir/ - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see https://www.gnu.org/licenses/gpl-3.0.html - */ - -package org.shanoir.ng.studycard.model.condition; - -import jakarta.persistence.Entity; - -@Entity -public abstract class StudyCardMetadataConditionWithCardinality extends StudyCardMetadataCondition { - -private int cardinality; - - public int getCardinality() { - return cardinality; - } - - public void setCardinality(int cardinality) { - this.cardinality = cardinality; - } - - protected boolean cardinalityComplies(int nbOk, int total) { - if (getCardinality() == -1) return total == nbOk; - if (getCardinality() == 0) return 0 == nbOk; - else return nbOk >= getCardinality(); - } - -} diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/rule/DatasetAcquisitionRule.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/rule/DatasetAcquisitionRule.java index e9629b3ef2..af0cadd1a0 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/rule/DatasetAcquisitionRule.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/rule/DatasetAcquisitionRule.java @@ -14,19 +14,21 @@ package org.shanoir.ng.studycard.model.rule; -import com.fasterxml.jackson.annotation.JsonTypeName; -import jakarta.persistence.DiscriminatorValue; -import jakarta.persistence.Entity; -import org.dcm4che3.data.Attributes; import org.shanoir.ng.dataset.model.Dataset; import org.shanoir.ng.datasetacquisition.model.DatasetAcquisition; +import org.shanoir.ng.download.AcquisitionAttributes; import org.shanoir.ng.studycard.model.assignment.DatasetAcquisitionAssignment; import org.shanoir.ng.studycard.model.assignment.DatasetAssignment; import org.shanoir.ng.studycard.model.assignment.StudyCardAssignment; import org.shanoir.ng.studycard.model.condition.AcqMetadataCondOnAcq; import org.shanoir.ng.studycard.model.condition.AcqMetadataCondOnDatasets; import org.shanoir.ng.studycard.model.condition.StudyCardCondition; -import org.shanoir.ng.studycard.model.condition.StudyCardDICOMCondition; +import org.shanoir.ng.studycard.model.condition.StudyCardDICOMConditionOnDatasets; + +import com.fasterxml.jackson.annotation.JsonTypeName; + +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; /** * A rule that applies to a {@link DatasetAcquisition} @@ -36,18 +38,17 @@ @JsonTypeName("DatasetAcquisition") public class DatasetAcquisitionRule extends StudyCardRule { - @Override - public void apply(DatasetAcquisition acquisition, Attributes dicomAttributes) { + public void apply(DatasetAcquisition acquisition, AcquisitionAttributes dicomAttributes) { if (this.getConditions() == null || this.getConditions().isEmpty() || conditionsfulfilled(dicomAttributes, acquisition)) { if (this.getAssignments() != null) applyAssignments(acquisition); } } - private boolean conditionsfulfilled(Attributes dicomAttributes, DatasetAcquisition acquisition) { + private boolean conditionsfulfilled(AcquisitionAttributes dicomAttributes, DatasetAcquisition acquisition) { boolean fulfilled = true; for (StudyCardCondition condition : getConditions()) { - if (condition instanceof StudyCardDICOMCondition) { - fulfilled &= ((StudyCardDICOMCondition) condition).fulfilled(dicomAttributes); + if (condition instanceof StudyCardDICOMConditionOnDatasets) { + fulfilled &= ((StudyCardDICOMConditionOnDatasets) condition).fulfilled(dicomAttributes); } else if (condition instanceof AcqMetadataCondOnAcq) { fulfilled &= ((AcqMetadataCondOnAcq) condition).fulfilled(acquisition); } else if (condition instanceof AcqMetadataCondOnDatasets) { diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/rule/DatasetRule.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/rule/DatasetRule.java index 9c757ffdf3..d0d592d65a 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/rule/DatasetRule.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/rule/DatasetRule.java @@ -23,7 +23,7 @@ import org.shanoir.ng.studycard.model.assignment.StudyCardAssignment; import org.shanoir.ng.studycard.model.condition.DatasetMetadataCondOnDataset; import org.shanoir.ng.studycard.model.condition.StudyCardCondition; -import org.shanoir.ng.studycard.model.condition.StudyCardDICOMCondition; +import org.shanoir.ng.studycard.model.condition.StudyCardDICOMConditionOnDatasets; /** * A rule that applies to a {@link Dataset} @@ -33,7 +33,6 @@ @JsonTypeName("Dataset") public class DatasetRule extends StudyCardRule { - @Override public void apply(Dataset dataset, Attributes dicomAttributes) { if (this.getConditions() == null || this.getConditions().isEmpty() || conditionsfulfilled(dicomAttributes, dataset)) { if (this.getAssignments() != null) applyAssignments(dataset); @@ -43,8 +42,8 @@ public void apply(Dataset dataset, Attributes dicomAttributes) { private boolean conditionsfulfilled(Attributes dicomAttributes, Dataset dataset) { boolean fulfilled = true; for (StudyCardCondition condition : getConditions()) { - if (condition instanceof StudyCardDICOMCondition) { - fulfilled &= ((StudyCardDICOMCondition) condition).fulfilled(dicomAttributes); + if (condition instanceof StudyCardDICOMConditionOnDatasets) { + fulfilled &= ((StudyCardDICOMConditionOnDatasets) condition).fulfilled(dicomAttributes, dataset.getId()); } else if (condition instanceof DatasetMetadataCondOnDataset) { fulfilled &= ((DatasetMetadataCondOnDataset) condition).fulfilled(dataset); } else { diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/rule/QualityExaminationRule.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/rule/QualityExaminationRule.java index 4a42f4cb1e..cd5aae8d94 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/rule/QualityExaminationRule.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/rule/QualityExaminationRule.java @@ -14,10 +14,15 @@ package org.shanoir.ng.studycard.model.rule; -import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + import org.apache.commons.lang3.StringUtils; -import org.dcm4che3.data.Attributes; import org.hibernate.annotations.GenericGenerator; +import org.shanoir.ng.download.ExaminationAttributes; +import org.shanoir.ng.download.WADODownloaderService; import org.shanoir.ng.examination.model.Examination; import org.shanoir.ng.shared.core.model.AbstractEntity; import org.shanoir.ng.shared.model.SubjectStudy; @@ -28,22 +33,28 @@ import org.shanoir.ng.studycard.model.condition.ExamMetadataCondOnAcq; import org.shanoir.ng.studycard.model.condition.ExamMetadataCondOnDatasets; import org.shanoir.ng.studycard.model.condition.StudyCardCondition; -import org.shanoir.ng.studycard.model.condition.StudyCardDICOMCondition; +import org.shanoir.ng.studycard.model.condition.StudyCardDICOMConditionOnDatasets; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; +import jakarta.persistence.OneToMany; +import jakarta.validation.constraints.NotNull; @Entity @GenericGenerator(name = "IdOrGenerate", strategy = "org.shanoir.ng.shared.model.UseIdOrGenerate") public class QualityExaminationRule extends AbstractEntity { private Integer tag; - - @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL) + + @NotNull + private boolean orConditions; + + @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL) // there is a join table because a rule_id fk would lead to an ambiguity and bugs // because it could refer to a study card or quality card rule @JoinTable(name="quality_card_condition_join", joinColumns = {@JoinColumn(name = "quality_card_rule_id")}, inverseJoinColumns = {@JoinColumn(name = "condition_id")}) @@ -64,23 +75,35 @@ public List getConditions() { public void setConditions(List conditions) { this.conditions = conditions; } + + public boolean isOrConditions() { + return orConditions; + } + + public void setOrConditions(boolean orConditions) { + this.orConditions = orConditions; + } + + public void apply(Examination examination, QualityCardResult result, WADODownloaderService downloader) { + apply(examination, null, result, downloader); + } - public void apply(Examination examination, Attributes examinationDicomAttributes, QualityCardResult result) { + public void apply(Examination examination, ExaminationAttributes examinationDicomAttributes, QualityCardResult result, WADODownloaderService downloader) { ExaminationData examData = convert(examination); if (examData.getSubjectStudy() == null) { Logger log = LoggerFactory.getLogger(QualityExaminationRule.class); log.warn("No subject study in exam " + examination.getId()); } else { - apply(examData, examinationDicomAttributes, result); + apply(examData, examinationDicomAttributes, result, downloader); } } - public void apply(ExaminationData examination, Attributes examinationDicomAttributes, QualityCardResult result) { + public void apply(ExaminationData examination, ExaminationAttributes examinationDicomAttributes, QualityCardResult result, WADODownloaderService downloader) { if (this.getConditions() == null || this.getConditions().isEmpty()) { result.addUpdatedSubjectStudy( setTagToSubjectStudy(examination.getSubjectStudy())); } else { - ConditionResult conditionResult = conditionsfulfilled(examinationDicomAttributes, examination, result); + ConditionResult conditionResult = conditionsfulfilled(examinationDicomAttributes, examination, result, downloader); if (conditionResult.isFulfilled()) { result.addUpdatedSubjectStudy( setTagToSubjectStudy(examination.getSubjectStudy())); @@ -91,6 +114,7 @@ public void apply(ExaminationData examination, Attributes examinationDicomAttrib if ((conditionResult.isFulfilled() && !getQualityTag().equals(QualityTag.VALID)) || (!conditionResult.isFulfilled() && getQualityTag().equals(QualityTag.VALID))) { QualityCardResultEntry resultEntry = initResult(examination); + resultEntry.setFailedValid(QualityTag.VALID.equals(getQualityTag()) && !conditionResult.isFulfilled()); resultEntry.setTagSet(getQualityTag()); if (conditionResult.isFulfilled()) { resultEntry.setMessage("Tag " + getQualityTag().name() + " was set because those conditions were fulfilled : " + StringUtils.join(conditionResult.getFulfilledConditionsMsgList(), ", ")); @@ -117,15 +141,35 @@ private SubjectStudy setTagToSubjectStudy(SubjectStudy subjectStudy) { return subjectStudyCopy; } - private ConditionResult conditionsfulfilled(Attributes dicomAttributes, ExaminationData examination, QualityCardResult result) { + /** + * + * @param dicomAttributes if null conditions will be checked on the examination data and dicom data will be fetched from pacs. + * Else conditions will be checked on the looping on the given dicom attributes + * @param examination + * @param result + * @return + */ + private ConditionResult conditionsfulfilled(ExaminationAttributes dicomAttributes, ExaminationData examination, QualityCardResult result, WADODownloaderService downloader) { boolean allFulfilled = true; ConditionResult condResult = new ConditionResult(); Collections.sort(conditions, new ConditionComparator()); // sort by level + boolean pilotedByDicomAttributes; + ExaminationAttributes examinationAttributesCache = new ExaminationAttributes(); + if (dicomAttributes != null) { + pilotedByDicomAttributes = true; + } else { + pilotedByDicomAttributes = false; + examinationAttributesCache = new ExaminationAttributes(); + } for (StudyCardCondition condition : getConditions()) { StringBuffer msg = new StringBuffer(); boolean fulfilled = true; - if (condition instanceof StudyCardDICOMCondition) { - fulfilled = ((StudyCardDICOMCondition) condition).fulfilled(dicomAttributes, msg); + if (condition instanceof StudyCardDICOMConditionOnDatasets) { + if (pilotedByDicomAttributes) { + fulfilled = ((StudyCardDICOMConditionOnDatasets) condition).fulfilled(dicomAttributes, msg); + } else { + fulfilled = ((StudyCardDICOMConditionOnDatasets) condition).fulfilled(examination.getDatasetAcquisitions(), examinationAttributesCache, downloader, msg); + } } else if (condition instanceof ExamMetadataCondOnAcq) { fulfilled = ((ExamMetadataCondOnAcq) condition).fulfilled(examination.getDatasetAcquisitions(), msg); } else if (condition instanceof ExamMetadataCondOnDatasets) { @@ -138,7 +182,13 @@ private ConditionResult conditionsfulfilled(Attributes dicomAttributes, Examinat } else { condResult.addUnfulfilledConditionsMsg(msg.toString()); } - allFulfilled &= fulfilled; + if (isOrConditions() && fulfilled) { + allFulfilled = true; + break; + } + else { + allFulfilled &= fulfilled; + } } condResult.setFulfilled(allFulfilled); return condResult; @@ -180,7 +230,7 @@ public int compare(StudyCardCondition cond1, StudyCardCondition cond2) { * the higher the priority, the higher is the returned number. */ private int priority(StudyCardCondition condition) { - if (condition instanceof StudyCardDICOMCondition) { + if (condition instanceof StudyCardDICOMConditionOnDatasets) { return 1; } else if (condition instanceof ExamMetadataCondOnAcq) { return 3; @@ -222,4 +272,15 @@ public void addUnfulfilledConditionsMsg(String msg) { this.unfulfilledConditionsMsgList.add(msg); } } + + public boolean hasDicomConditions() { + if (getConditions() != null) { + for (StudyCardCondition condition: getConditions()) { + if (condition instanceof StudyCardDICOMConditionOnDatasets) { + return true; + } + } + } + return false; + } } diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/rule/StudyCardRule.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/rule/StudyCardRule.java index 048a0d2131..047d7c9f50 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/rule/StudyCardRule.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/model/rule/StudyCardRule.java @@ -14,18 +14,29 @@ package org.shanoir.ng.studycard.model.rule; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import com.fasterxml.jackson.annotation.JsonTypeInfo.As; -import com.fasterxml.jackson.annotation.JsonTypeInfo.Id; -import jakarta.persistence.*; -import org.dcm4che3.data.Attributes; +import java.util.List; + import org.hibernate.annotations.GenericGenerator; import org.shanoir.ng.shared.core.model.AbstractEntity; import org.shanoir.ng.studycard.model.assignment.StudyCardAssignment; import org.shanoir.ng.studycard.model.condition.StudyCardCondition; -import java.util.List; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeInfo.As; +import com.fasterxml.jackson.annotation.JsonTypeInfo.Id; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.DiscriminatorColumn; +import jakarta.persistence.DiscriminatorType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; +import jakarta.persistence.OneToMany; +import jakarta.validation.constraints.NotNull; @Entity @Inheritance(strategy = InheritanceType.SINGLE_TABLE) @@ -47,6 +58,9 @@ public abstract class StudyCardRule extends AbstractEntity { @JoinTable(name="study_card_condition_join", joinColumns = {@JoinColumn(name = "study_card_rule_id")}, inverseJoinColumns = {@JoinColumn(name = "condition_id")}) private List conditions; + @NotNull + private boolean orConditions; + public List> getAssignments() { return assignments; } @@ -62,6 +76,13 @@ public List getConditions() { public void setConditions(List conditions) { this.conditions = conditions; } - - abstract void apply(T object, Attributes dicomAttributes); + + + public boolean isOrConditions() { + return orConditions; + } + + public void setOrConditions(boolean orConditions) { + this.orConditions = orConditions; + } } diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/service/CardsProcessingService.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/service/CardsProcessingService.java index 5abb0f1a2d..00109f260e 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/service/CardsProcessingService.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/service/CardsProcessingService.java @@ -14,34 +14,31 @@ package org.shanoir.ng.studycard.service; -import java.util.ArrayList; import java.util.Date; import java.util.List; -import java.util.stream.Collectors; import org.apache.commons.collections4.CollectionUtils; -import org.dcm4che3.data.Attributes; -import org.shanoir.ng.configuration.amqp.RabbitMQSendService; import org.shanoir.ng.datasetacquisition.model.DatasetAcquisition; -import org.shanoir.ng.datasetacquisition.model.mr.MrDatasetAcquisition; import org.shanoir.ng.datasetacquisition.service.DatasetAcquisitionService; +import org.shanoir.ng.download.AcquisitionAttributes; import org.shanoir.ng.download.WADODownloaderService; import org.shanoir.ng.examination.model.Examination; -import org.shanoir.ng.shared.configuration.RabbitMQConfiguration; +import org.shanoir.ng.examination.service.ExaminationService; +import org.shanoir.ng.shared.event.ShanoirEvent; +import org.shanoir.ng.shared.event.ShanoirEventService; +import org.shanoir.ng.shared.event.ShanoirEventType; import org.shanoir.ng.shared.exception.EntityNotFoundException; import org.shanoir.ng.shared.exception.MicroServiceCommunicationException; import org.shanoir.ng.shared.exception.PacsException; import org.shanoir.ng.shared.model.Study; import org.shanoir.ng.shared.model.SubjectStudy; -import org.shanoir.ng.shared.quality.QualityTag; -import org.shanoir.ng.shared.quality.SubjectStudyQualityTagDTO; import org.shanoir.ng.shared.service.StudyService; import org.shanoir.ng.shared.service.SubjectStudyService; import org.shanoir.ng.studycard.dto.QualityCardResult; -import org.shanoir.ng.studycard.dto.QualityCardResultEntry; import org.shanoir.ng.studycard.model.QualityCard; import org.shanoir.ng.studycard.model.StudyCard; import org.shanoir.ng.studycard.model.rule.QualityExaminationRule; +import org.shanoir.ng.utils.KeycloakUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -55,6 +52,9 @@ public class CardsProcessingService { @Autowired private StudyService studyService; + + @Autowired + private ExaminationService examinationService; @Autowired private DatasetAcquisitionService datasetAcquisitionService; @@ -65,6 +65,9 @@ public class CardsProcessingService { @Autowired private SubjectStudyService subjectStudyService; + @Autowired + private ShanoirEventService eventService; + /** * Apply study card on given acquisitions @@ -77,7 +80,7 @@ public void applyStudyCard(StudyCard studyCard, List acquisi boolean changeInAtLeastOneAcquisition = false; for (DatasetAcquisition acquisition : acquisitions) { if (CollectionUtils.isNotEmpty(acquisition.getDatasets()) && CollectionUtils.isNotEmpty(studyCard.getRules())) { - Attributes dicomAttributes = downloader.getDicomAttributesForAcquisition(acquisition); + AcquisitionAttributes dicomAttributes = downloader.getDicomAttributesForAcquisition(acquisition); changeInAtLeastOneAcquisition = studyCard.apply(acquisition, dicomAttributes); } } @@ -85,76 +88,141 @@ public void applyStudyCard(StudyCard studyCard, List acquisi datasetAcquisitionService.update(acquisitions); } } - - /** - * Study cards for quality control: apply on entire study. + + /** + * Study cards for quality control: apply on entire exam. * * @param studyCard * @throws MicroServiceCommunicationException */ - public QualityCardResult applyQualityCardOnStudy(QualityCard qualityCard, boolean updateTags) throws MicroServiceCommunicationException { - if (qualityCard == null) throw new IllegalArgumentException("qualityCard can't be null"); - Study study = studyService.findById(qualityCard.getStudyId()); - if (study == null ) throw new IllegalArgumentException("study can't be null"); - if (qualityCard.getStudyId() != study.getId()) throw new IllegalStateException("study and studycard ids don't match"); + public QualityCardResult applyQualityCardOnExamination(QualityCard qualityCard, Examination examination, boolean updateTags) throws MicroServiceCommunicationException { + long startTs = new Date().getTime(); + if (qualityCard == null) throw new IllegalArgumentException("qualityCard can't be null"); + if (examination == null ) throw new IllegalArgumentException("examination can't be null"); + LOG.debug("Quality check for examination " + examination.getId() + " started"); + if (qualityCard.getStudyId() != examination.getStudy().getId()) throw new IllegalStateException("study and studycard ids don't match"); if (CollectionUtils.isNotEmpty(qualityCard.getRules())) { QualityCardResult result = new QualityCardResult(); - resetSubjectStudies(study); - try { - subjectStudyService.update(study.getSubjectStudyList()); - } catch (EntityNotFoundException e) {} // too bad - for (Examination examination : study.getExaminations()) { - // For now, just take the first DICOM instance - // Later, use DICOM json to have a hierarchical structure of DICOM metata (study -> serie -> instance) + if (updateTags) { + List subjectsStudies = subjectStudyService.get(examination.getSubject().getId(), examination.getStudy().getId()); + resetSubjectStudies(subjectsStudies); try { - Attributes examinationDicomAttributes = downloader.getDicomAttributesForExamination(examination); - List acquisitions = examination.getDatasetAcquisitions(); - // today study cards are only used for MR modality - // acquisitions = acquisitions.stream().filter(a -> a instanceof MrDatasetAcquisition).collect(Collectors.toList()); - if (CollectionUtils.isNotEmpty(acquisitions)) { - LOG.info(acquisitions.size() + " acquisitions found for examination with id: " + examination.getId()); - LOG.info(qualityCard.getRules().size() + " rules found for study card with id: " + qualityCard.getId() + " and name: " + qualityCard.getName()); - for (QualityExaminationRule rule : qualityCard.getRules()) { - rule.apply(examination, examinationDicomAttributes, result); - } - } - } catch (PacsException e) { - long ts = new Date().getTime(); - LOG.warn("Examination" + examination.getId() + " metadata could not be retreived from the Shanoir pacs (ts:" + ts + ")"); - QualityCardResultEntry resultEntry = initResult(examination); - resultEntry.setTagSet(QualityTag.ERROR); - resultEntry.setMessage("Examination " + examination.getId() + " could not be checked because its metadata could not be retreived from the Shanoir pacs (ts:" + ts + ")"); - result.add(resultEntry); + subjectStudyService.update(subjectsStudies); + } catch (EntityNotFoundException e) {} // too bad + } + List acquisitions = examination.getDatasetAcquisitions(); + if (CollectionUtils.isNotEmpty(acquisitions)) { + LOG.debug(acquisitions.size() + " acquisitions found for examination with id: " + examination.getId()); + LOG.debug(qualityCard.getRules().size() + " rules found for study card with id: " + qualityCard.getId() + " and name: " + qualityCard.getName()); + long rulesStartTs = new Date().getTime(); + for (QualityExaminationRule rule : qualityCard.getRules()) { + rule.apply(examination, result, downloader); } - }; - //result.removeUnchanged(study); + LOG.debug("Quality check for examination " + examination.getId() + " : rules application took " + (new Date().getTime() - rulesStartTs) + "ms"); + } if (updateTags) { try { subjectStudyService.update(result.getUpdatedSubjectStudies()); } catch (EntityNotFoundException e) { - throw new IllegalStateException("Could not update subject-studies", e); + throw new IllegalStateException("Could not update subject-studies", e); } } - return result; + LOG.info("Quality check for examination " + examination.getId() + " finished in " + (new Date().getTime() - startTs) + " ms"); + return result; } else { - throw new RestClientException("Study card used with emtpy rules."); + throw new RestClientException("Quality card used with emtpy rules."); } } - - private void resetSubjectStudies(Study study) { - if (study != null && study.getSubjectStudyList() != null) { - for (SubjectStudy subjectStudy : study.getSubjectStudyList()) { - subjectStudy.setQualityTag(null); + + /** + * Study cards for quality control: apply on entire study. + * + * @param studyCard + * @throws MicroServiceCommunicationException + */ + public QualityCardResult applyQualityCardOnStudy(QualityCard qualityCard, boolean updateTags, Integer start, Integer stop) throws MicroServiceCommunicationException { + long startTs = new Date().getTime(); + if (qualityCard == null) throw new IllegalArgumentException("qualityCard can't be null"); + ShanoirEvent event = new ShanoirEvent(ShanoirEventType.CHECK_QUALITY_EVENT, null, KeycloakUtil.getTokenUserId(), "Quality check started on study " + qualityCard.getStudyId() , 4); + eventService.publishEvent(event); + Study study = studyService.findById(qualityCard.getStudyId()); + if (study == null ) throw new IllegalArgumentException("study can't be null"); + if (qualityCard.getStudyId() != study.getId()) throw new IllegalStateException("study and studycard ids don't match"); + if (CollectionUtils.isNotEmpty(qualityCard.getRules())) { + if (updateTags) { // first reset subject studies + event.setMessage("resetting quality subject tags"); + eventService.publishEvent(event); + resetSubjectStudies(study.getSubjectStudyList()); + try { + subjectStudyService.update(study.getSubjectStudyList()); + } catch (EntityNotFoundException e) {} // too bad + } + QualityCardResult result = new QualityCardResult(); + int i = 0; + List examinations; + if (start != null && stop != null) { + examinations = study.getExaminations().subList(start, stop < study.getExaminations().size() ? stop : study.getExaminations().size()); + } else { + examinations = study.getExaminations(); } + for (Examination examination : examinations) { + event.setStatus(2); + event.setProgress((float)i / examinations.size()); + event.setMessage("checking quality for examination " + examination.getComment()); + //event.setReport(result.toString()); // too heavy + eventService.publishEvent(event); + result.merge(applyQualityCardOnExamination(qualityCard, examination, false)); + i++; + }; + if (updateTags) { // update subject studies + try { + event.setMessage("setting quality subject tags"); + eventService.publishEvent(event); + subjectStudyService.update(result.getUpdatedSubjectStudies()); + } catch (EntityNotFoundException e) { + throw new IllegalStateException("Could not update subject-studies", e); + } + } + event.setProgress(1f); + event.setStatus(1); + event.setMessage("Quality card applied on study " + study.getName() + " in " + (new Date().getTime() - startTs) + " ms."); + event.setReport(result.toString()); + eventService.publishEvent(event); + return result; + } else { + event.setStatus(-1); + event.setMessage("Quality card used with emtpy rules."); + event.setProgress(1f); + eventService.publishEvent(event); + throw new RestClientException("Quality card used with emtpy rules."); } - } + } + + /** + * Study cards for quality control: apply on entire study. + * + * @param studyCard + * @throws MicroServiceCommunicationException + */ + public QualityCardResult applyQualityCardOnStudy(QualityCard qualityCard, boolean updateTags) throws MicroServiceCommunicationException { + return applyQualityCardOnStudy(qualityCard, updateTags, null, null); + } - private QualityCardResultEntry initResult(Examination examination) { - QualityCardResultEntry result = new QualityCardResultEntry(); - result.setSubjectName(examination.getSubject().getName()); - result.setExaminationDate(examination.getExaminationDate()); - result.setExaminationComment(examination.getComment()); - return result; + /** + * Study cards for quality control: apply on entire study. + * + * @param studyCard + * @throws MicroServiceCommunicationException + */ + public QualityCardResult applyQualityCardOnStudy(QualityCard qualityCard, Integer start, Integer stop) throws MicroServiceCommunicationException { + return applyQualityCardOnStudy(qualityCard, false, start, stop); + } + + private void resetSubjectStudies(List subjectStudies) { + if (subjectStudies != null) { + for (SubjectStudy subjectStudy : subjectStudies) { + subjectStudy.setQualityTag(null); + } + } } - } diff --git a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/service/QualityCardServiceImpl.java b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/service/QualityCardServiceImpl.java index 4db2e942b7..91c9a13935 100644 --- a/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/service/QualityCardServiceImpl.java +++ b/shanoir-ng-datasets/src/main/java/org/shanoir/ng/studycard/service/QualityCardServiceImpl.java @@ -17,6 +17,7 @@ import org.shanoir.ng.shared.exception.EntityNotFoundException; import org.shanoir.ng.shared.exception.MicroServiceCommunicationException; import org.shanoir.ng.studycard.model.QualityCard; +import org.shanoir.ng.studycard.model.condition.StudyCardCondition; import org.shanoir.ng.studycard.model.rule.QualityExaminationRule; import org.shanoir.ng.studycard.repository.QualityCardRepository; import org.shanoir.ng.utils.Utils; @@ -80,9 +81,9 @@ public List search(final List studyIdList) { @Override @PreAuthorize("hasRole('ADMIN') or (hasRole('EXPERT') and @datasetSecurityService.hasUpdateRightOnCard(#card, 'CAN_ADMINISTRATE'))") public QualityCard update(final QualityCard card) throws EntityNotFoundException, MicroServiceCommunicationException { - final QualityCard qualityCardDb = qualityCardRepository.findById(card.getId()).orElse(null); + QualityCard qualityCardDb = qualityCardRepository.findById(card.getId()).orElse(null); if (qualityCardDb == null) throw new EntityNotFoundException(QualityCard.class, card.getId()); - updateQualityCardValues(qualityCardDb, card); + qualityCardDb = updateQualityCardValues(qualityCardDb, card); qualityCardRepository.save(qualityCardDb); return qualityCardDb; } diff --git a/shanoir-ng-datasets/src/main/resources/scripts/import.sql b/shanoir-ng-datasets/src/main/resources/scripts/import.sql index 4362200045..46f9418f5a 100644 --- a/shanoir-ng-datasets/src/main/resources/scripts/import.sql +++ b/shanoir-ng-datasets/src/main/resources/scripts/import.sql @@ -48,16 +48,16 @@ VALUES (2,null,false,0,'QualityCard_UCAN',null,1); INSERT INTO study_card_rule - (id, study_card_id, scope) + (id, study_card_id, scope,or_conditions) VALUES - (1,2,'DatasetAcquisition'), - (2,2,'Dataset'); + (1,2,'DatasetAcquisition',false), + (2,2,'Dataset',false); INSERT INTO study_card_condition - (id, dicom_tag, operation, scope) + (id, dicom_tag, operation, scope, cardinality) VALUES - (1,2,4,'StudyCardDICOMCondition'), - (2,2,4,'StudyCardDICOMCondition'); + (1,2,4,'StudyCardDICOMCondition', 1), + (2,2,4,'StudyCardDICOMCondition', 1); INSERT INTO study_card_condition_join (study_card_rule_id, condition_id) diff --git a/shanoir-ng-datasets/src/main/resources/scripts/test-data-h2.sql b/shanoir-ng-datasets/src/main/resources/scripts/test-data-h2.sql index c42cdfb848..954df18bf3 100644 --- a/shanoir-ng-datasets/src/main/resources/scripts/test-data-h2.sql +++ b/shanoir-ng-datasets/src/main/resources/scripts/test-data-h2.sql @@ -29,8 +29,8 @@ VALUES (5,null,false,0,'QualityCard1',null,1); INSERT INTO study_card_rule - (id, scope, study_card_id) -VALUES (3,'DatasetAcquisition',1),(4,'DatasetAcquisition',1),(5,'DatasetAcquisition',1),(6,'DatasetAcquisition',1),(7,'Dataset',5),(8,'Dataset',5); + (id, scope, study_card_id, or_conditions) +VALUES (3,'DatasetAcquisition',1,false),(4,'DatasetAcquisition',1,false),(5,'DatasetAcquisition',1,false),(6,'DatasetAcquisition',1,false),(7,'Dataset',5,false),(8,'Dataset',5,false); INSERT INTO study_card_assignment (id, field, value, scope) @@ -43,13 +43,13 @@ VALUES (8,4,'4','Dataset'); INSERT INTO study_card_condition - (id, shanoir_field, operation, scope, dicom_tag) + (id, shanoir_field, operation, scope, dicom_tag, cardinality) VALUES - (1,2,4,'AcqMetadataCondOnAcq', null), - (2,2,4,'AcqMetadataCondOnAcq', null), - (3,1573009,5,'AcqMetadataCondOnAcq', null), - (4,1573009,6,'AcqMetadataCondOnAcq', null), - (5,1573013,6,'AcqMetadataCondOnAcq', null); + (1,2,4,'AcqMetadataCondOnAcq', null, 1), + (2,2,4,'AcqMetadataCondOnAcq', null, 1), + (3,1573009,5,'AcqMetadataCondOnAcq', null, 1), + (4,1573009,6,'AcqMetadataCondOnAcq', null, 1), + (5,1573013,6,'AcqMetadataCondOnAcq', null, 1); INSERT INTO study_card_condition_values (value, study_card_condition_id) diff --git a/shanoir-ng-datasets/src/test/java/org/shanoir/ng/datasetacquisition/service/ImporterServiceTest.java b/shanoir-ng-datasets/src/test/java/org/shanoir/ng/datasetacquisition/service/ImporterServiceTest.java index c94d093105..312deed0dc 100644 --- a/shanoir-ng-datasets/src/test/java/org/shanoir/ng/datasetacquisition/service/ImporterServiceTest.java +++ b/shanoir-ng-datasets/src/test/java/org/shanoir/ng/datasetacquisition/service/ImporterServiceTest.java @@ -14,9 +14,11 @@ import org.shanoir.ng.importer.dto.*; import org.shanoir.ng.importer.service.DatasetAcquisitionContext; import org.shanoir.ng.importer.service.DicomPersisterService; +import org.shanoir.ng.importer.service.ImporterMailService; import org.shanoir.ng.importer.service.ImporterService; import org.shanoir.ng.shared.event.ShanoirEvent; import org.shanoir.ng.shared.event.ShanoirEventService; +import org.shanoir.ng.shared.service.SubjectStudyService; import org.shanoir.ng.study.rights.StudyUserRightsRepository; import org.shanoir.ng.studycard.model.QualityCard; import org.shanoir.ng.studycard.service.QualityCardService; @@ -78,6 +80,12 @@ public class ImporterServiceTest { @Mock private DatasetAcquisitionRepository datasetAcquisitionRepository; + @Mock + private ImporterMailService importerServiceMail; + + @Mock + private SubjectStudyService subjectStudyService; + private Examination exam; @BeforeEach @@ -128,14 +136,21 @@ public void createAllDatasetAcquisition() throws Exception { importJob.setStudyId(1L); importJob.setStudyCardName("SCname"); importJob.setShanoirEvent(new ShanoirEvent()); - + + org.shanoir.ng.shared.model.Subject subject = new org.shanoir.ng.shared.model.Subject(); + subject.setSubjectStudyList(Collections.emptyList()); + Examination examination = new Examination(); examination.setId(2L); examination.setExaminationDate(LocalDate.now()); examination.setDatasetAcquisitions(new ArrayList<>()); + examination.setSubject(subject); when(examinationRepository.findById(importJob.getExaminationId())).thenReturn(Optional.of(examination)); DatasetAcquisition datasetAcq = new MrDatasetAcquisition(); - when(datasetAcquisitionContext.generateDatasetAcquisitionForSerie(serie, 0, importJob, new Attributes())).thenReturn(datasetAcq); + + //DatasetAcquisition datasetAcquisition = datasetAcquisitionContext.generateDatasetAcquisitionForSerie(serie, rank, importJob, dicomAttributes); + + when(datasetAcquisitionContext.generateDatasetAcquisitionForSerie(Mockito.eq(serie), Mockito.eq(0), Mockito.eq(importJob), Mockito.any())).thenReturn(datasetAcq); when(studyUserRightRepo.findByStudyId(importJob.getStudyId())).thenReturn(Collections.emptyList()); when(dicomProcessing.getDicomObjectAttributes(serie.getFirstDatasetFileForCurrentSerie(), serie.getIsEnhanced())).thenReturn(new Attributes()); when(qualityCardService.findByStudy(examination.getStudyId())).thenReturn(Utils.toList(new QualityCard())); // TODO perform quality card tests @@ -165,4 +180,4 @@ public void createAllDatasetAcquisition() throws Exception { List extradata = datasetAcq.getExamination().getExtraDataFilePathList(); assertNull(extradata); } -} +} \ No newline at end of file diff --git a/shanoir-ng-datasets/src/test/java/org/shanoir/ng/importer/strategies/protocol/MrProtocolStrategyTest.java b/shanoir-ng-datasets/src/test/java/org/shanoir/ng/importer/strategies/protocol/MrProtocolStrategyTest.java index 9169d30776..c428909ca9 100644 --- a/shanoir-ng-datasets/src/test/java/org/shanoir/ng/importer/strategies/protocol/MrProtocolStrategyTest.java +++ b/shanoir-ng-datasets/src/test/java/org/shanoir/ng/importer/strategies/protocol/MrProtocolStrategyTest.java @@ -9,6 +9,7 @@ import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.shanoir.ng.datasetacquisition.model.mr.MrProtocol; +import org.shanoir.ng.download.AcquisitionAttributes; import org.shanoir.ng.importer.dto.Serie; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,7 +45,9 @@ public void setup() { public void testGenerateMrProtocolForSerieNotEnhancedMR() throws IOException { Attributes attributes = getAttributesFromFile("/1.3.12.2.1107.5.2.43.166066.2018042412210060639615964"); Serie serie = generateSerie(attributes); - MrProtocol mrProtocol = mrProtocolStrategy.generateProtocolForSerie(attributes, serie); + AcquisitionAttributes acqAttributes = new AcquisitionAttributes<>(); + acqAttributes.addDatasetAttributes("UID12345", attributes); + MrProtocol mrProtocol = mrProtocolStrategy.generateProtocolForSerie(acqAttributes, serie); Assertions.assertTrue(mrProtocol.getNumberOfAverages().equals(1)); Assertions.assertTrue(mrProtocol.getFilters().equals("77")); } diff --git a/shanoir-ng-front/package-lock.json b/shanoir-ng-front/package-lock.json index 99aa6ab67a..a473c9294d 100644 --- a/shanoir-ng-front/package-lock.json +++ b/shanoir-ng-front/package-lock.json @@ -21,14 +21,16 @@ "@angular/router": "^15.1.5", "@stomp/ng2-stompjs": "^7.2.0", "@types/node": "^12.11.1", + "@types/wicg-file-system-access": "^2020.9.6", "file-saver-es": "^2.0.5", "git-describe": "4.0.4", "http-status-codes": "^2.1.4", "ng-autosize": "^1.1.0", "ng-event-source": "^1.0.14", + "ng2-pdf-viewer": "^10.0.0", "ngx-json-viewer": "2.4.0", "rxjs": "~6.6.3", - "rxjs-compat": "^6.5.3", + "rxjs-compat": "^6.6.3", "sha.js": "^2.4.11", "tslib": "^2.0.0", "zone.js": "~0.11.4" @@ -4170,6 +4172,11 @@ "@types/node": "*" } }, + "node_modules/@types/wicg-file-system-access": { + "version": "2020.9.8", + "resolved": "https://registry.npmjs.org/@types/wicg-file-system-access/-/wicg-file-system-access-2020.9.8.tgz", + "integrity": "sha512-ggMz8nOygG7d/stpH40WVaNvBwuyYLnrg5Mbyf6bmsj/8+gb6Ei4ZZ9/4PNpcPNTT8th9Q8sM8wYmWGjMWLX/A==" + }, "node_modules/@types/ws": { "version": "8.5.4", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz", @@ -6400,6 +6407,12 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/dommatrix": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dommatrix/-/dommatrix-1.0.3.tgz", + "integrity": "sha512-l32Xp/TLgWb8ReqbVJAFIvXmY7go4nTxxlWiAFyhoQw9RKEOHBZNnyGvJWqDVSPmq3Y9HlM4npqF/T6VMOXhww==", + "deprecated": "dommatrix is no longer maintained. Please use @thednp/dommatrix." + }, "node_modules/domutils": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", @@ -10189,6 +10202,18 @@ "resolved": "https://registry.npmjs.org/ng-event-source/-/ng-event-source-1.0.14.tgz", "integrity": "sha512-RtgmjtTDSQG1yDcsVzT1Yafv6E2PlF64/f2TouN+/DyQuWIFYRg+qOvRJMB08Nqy76CJB30Mvsw6InHrmrGTvg==" }, + "node_modules/ng2-pdf-viewer": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/ng2-pdf-viewer/-/ng2-pdf-viewer-10.0.0.tgz", + "integrity": "sha512-zEefcAsTpDoxFceQYs3ycPMaUAkt5UX4OcTstVQoNqRK6w+vOY+V8z8aFCuBwnt+7iN1EHaIpquOf4S9mWc04g==", + "dependencies": { + "pdfjs-dist": "~2.16.105", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "pdfjs-dist": "~2.16.105" + } + }, "node_modules/ngx-json-viewer": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/ngx-json-viewer/-/ngx-json-viewer-2.4.0.tgz", @@ -10973,6 +10998,23 @@ "node": ">=8" } }, + "node_modules/pdfjs-dist": { + "version": "2.16.105", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-2.16.105.tgz", + "integrity": "sha512-J4dn41spsAwUxCpEoVf6GVoz908IAA3mYiLmNxg8J9kfRXc2jxpbUepcP0ocp0alVNLFthTAM8DZ1RaHh8sU0A==", + "dependencies": { + "dommatrix": "^1.0.3", + "web-streams-polyfill": "^3.2.1" + }, + "peerDependencies": { + "worker-loader": "^3.0.8" + }, + "peerDependenciesMeta": { + "worker-loader": { + "optional": true + } + } + }, "node_modules/performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -12317,9 +12359,9 @@ } }, "node_modules/rxjs-compat": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/rxjs-compat/-/rxjs-compat-6.5.3.tgz", - "integrity": "sha512-BIJX2yovz3TBpjJoAZyls2QYuU6ZiCaZ+U96SmxQpuSP/qDUfiXPKOVLbThBB2WZijNHkdTTJXKRwvv5Y48H7g==" + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs-compat/-/rxjs-compat-6.6.7.tgz", + "integrity": "sha512-szN4fK+TqBPOFBcBcsR0g2cmTTUF/vaFEOZNuSdfU8/pGFnNmmn2u8SystYXG1QMrjOPBc6XTKHMVfENDf6hHw==" }, "node_modules/rxjs/node_modules/tslib": { "version": "1.14.1", @@ -14218,6 +14260,14 @@ "defaults": "^1.0.3" } }, + "node_modules/web-streams-polyfill": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", + "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", + "engines": { + "node": ">= 8" + } + }, "node_modules/webdriver-js-extender": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/webdriver-js-extender/-/webdriver-js-extender-2.1.0.tgz", diff --git a/shanoir-ng-front/src/app/app.component.ts b/shanoir-ng-front/src/app/app.component.ts index 607dc1c9fa..3d0345c25a 100644 --- a/shanoir-ng-front/src/app/app.component.ts +++ b/shanoir-ng-front/src/app/app.component.ts @@ -12,21 +12,19 @@ * along with this program. If not, see https://www.gnu.org/licenses/gpl-3.0.html */ -import { Component, ElementRef, ViewContainerRef, HostBinding, HostListener, ViewChild } from '@angular/core'; +import { Component, ElementRef, HostBinding, HostListener, ViewChild, ViewContainerRef } from '@angular/core'; +import { Router } from '@angular/router'; +import { parent, slideMarginLeft, slideRight } from './shared/animations/animations'; +import { ConfirmDialogService } from './shared/components/confirm-dialog/confirm-dialog.service'; +import { ConsoleComponent } from './shared/console/console.component'; import { KeycloakService } from './shared/keycloak/keycloak.service'; import { GlobalService } from './shared/services/global.service'; -import { ServiceLocator } from './utils/locator.service'; -import { slideRight, parent, slideMarginLeft } from './shared/animations/animations'; import { WindowService } from './shared/services/window.service'; import { KeycloakSessionService } from './shared/session/keycloak-session.service'; -import { ConfirmDialogService } from './shared/components/confirm-dialog/confirm-dialog.service'; -import { Router } from '@angular/router'; import { StudyService } from './studies/shared/study.service'; -import { ConsoleComponent } from './shared/console/console.component'; import { UserService } from './users/shared/user.service'; -import { UserComponent } from './users/user/user.component'; -import { DatasetListComponent } from './datasets/dataset-list/dataset-list.component'; +import { ServiceLocator } from './utils/locator.service'; @Component({ @@ -94,7 +92,7 @@ export class AppComponent { const text: string = 'You are a member of at least one study that needs you to accept its data user agreement. ' + 'Until you have agreed those terms you cannot access to any data from these studies. ' + 'Would you like to review those terms now?'; - const buttons = {ok: 'Yes, proceed to the signing page', cancel: 'Later'}; + const buttons = {yes: 'Yes, proceed to the signing page', cancel: 'Later'}; this.confirmService.confirm(title, text, buttons).then(response => { if (response == true) this.router.navigate(['/dua']); }); diff --git a/shanoir-ng-front/src/app/app.module.ts b/shanoir-ng-front/src/app/app.module.ts index d85b771d4b..8521427a9a 100644 --- a/shanoir-ng-front/src/app/app.module.ts +++ b/shanoir-ng-front/src/app/app.module.ts @@ -266,6 +266,7 @@ import { ClipboardModule } from '@angular/cdk/clipboard'; import { TaskStatusComponent } from './async-tasks/status/task-status.component'; import { DownloadSetupComponent } from './shared/mass-download/download-setup/download-setup.component'; import { DownloadSetupAltComponent } from './shared/mass-download/download-setup-alt/download-setup-alt.component'; +import { TestQualityCardOptionsComponent } from './study-cards/test-quality-card-options/test-quality-card-options.component'; @NgModule({ imports: [ @@ -452,7 +453,8 @@ import { DownloadSetupAltComponent } from './shared/mass-download/download-setup QualityCardRuleComponent, TaskStatusComponent, DownloadSetupComponent, - DownloadSetupAltComponent + DownloadSetupAltComponent, + TestQualityCardOptionsComponent ], providers: [ AcquisitionEquipmentService, diff --git a/shanoir-ng-front/src/app/async-tasks/async-tasks.component.html b/shanoir-ng-front/src/app/async-tasks/async-tasks.component.html index e6cda0c591..aeed135ddb 100644 --- a/shanoir-ng-front/src/app/async-tasks/async-tasks.component.html +++ b/shanoir-ng-front/src/app/async-tasks/async-tasks.component.html @@ -19,7 +19,7 @@

My jobs

[columnDefs]="columnDefs" [customActionDefs]="customActionDefs" [browserSearch]="false" - (rowClick)="selected = $event" + (rowClick)="select($event)" [selectedId]="selected?.id"> diff --git a/shanoir-ng-front/src/app/async-tasks/async-tasks.component.ts b/shanoir-ng-front/src/app/async-tasks/async-tasks.component.ts index 4a2406c467..b4bcad4eb4 100644 --- a/shanoir-ng-front/src/app/async-tasks/async-tasks.component.ts +++ b/shanoir-ng-front/src/app/async-tasks/async-tasks.component.ts @@ -73,9 +73,9 @@ export class AsyncTasksComponent extends EntityListComponent implements Af headerName: 'Progress', field: 'progress', width: '110px', type: 'progress', cellRenderer: params => { return {progress: params.data?.progress, status: params.data?.status}; } }, { - headerName: "Creation", field: "creationDate", width: '130px', type: 'date' + headerName: "Creation", field: "creationDate", width: '130px', type: 'dateTime', defaultSortCol: true, defaultAsc: false, }, { - headerName: "Last update", field: "lastUpdate", width: '130px', defaultSortCol: true, defaultAsc: false, type: 'date' + headerName: "Last update", field: "lastUpdate", width: '130px', type: 'dateTime' } ]; } @@ -84,4 +84,14 @@ export class AsyncTasksComponent extends EntityListComponent implements Af return []; } + select(lightTask: Task) { + this.selected = null; + if (!lightTask) return; + if (lightTask.report || !lightTask.hasReport) { + this.selected = lightTask; + } else { + this.taskService.get(lightTask.completeId).then(task => this.selected = task); + } + } + } diff --git a/shanoir-ng-front/src/app/async-tasks/status/task-status.component.html b/shanoir-ng-front/src/app/async-tasks/status/task-status.component.html index 543d74f7c5..128b626188 100644 --- a/shanoir-ng-front/src/app/async-tasks/status/task-status.component.html +++ b/shanoir-ng-front/src/app/async-tasks/status/task-status.component.html @@ -14,7 +14,7 @@
- Details {{task.status}} + Details
  1. @@ -59,8 +59,8 @@ {{task.lastUpdate | date: 'dd/MM/yyyy HH:mm:ss'}}
  2. -
  3. - +
  4. +
    {{task?.report}}
    @@ -68,5 +68,18 @@
+
    + Report +
  1. + +
  2. +
diff --git a/shanoir-ng-front/src/app/async-tasks/status/task-status.component.ts b/shanoir-ng-front/src/app/async-tasks/status/task-status.component.ts index 1aedbba88f..018a64fd83 100644 --- a/shanoir-ng-front/src/app/async-tasks/status/task-status.component.ts +++ b/shanoir-ng-front/src/app/async-tasks/status/task-status.component.ts @@ -13,7 +13,11 @@ */ import { Component, Input, OnChanges, OnDestroy, SimpleChanges } from '@angular/core'; import { Subscription } from 'rxjs'; +import { BrowserPaging } from 'src/app/shared/components/table/browser-paging.model'; +import { ColumnDefinition } from 'src/app/shared/components/table/column.definition.type'; +import { FilterablePageable, Page } from 'src/app/shared/components/table/pageable.model'; import { MassDownloadService } from 'src/app/shared/mass-download/mass-download.service'; +import { QualityCardComponent } from 'src/app/study-cards/quality-card/quality-card.component'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { Task } from '../task.model'; @@ -28,6 +32,16 @@ export class TaskStatusComponent implements OnDestroy, OnChanges { importTs: number; protected subscriptions: Subscription[] = []; @Input() task: Task; + private tableRefresh: () => void; + + reportColumns: ColumnDefinition[] = [ + {headerName: 'Subject Name', field: 'subjectName', width: '20%'}, + {headerName: 'Examination Comment', field: 'examinationComment', width: '25%'}, + {headerName: 'Examination Date', field: 'examinationDate', type: 'date', width: '100px'}, + {headerName: 'Details', field: 'message', wrap: true} + ]; + report: BrowserPaging; + reportActions: any = [{title: "Download as csv", awesome: "fa-solid fa-download", action: () => this.downloadReport()}]; // @ts-ignore browserCompatible: boolean = window.showDirectoryPicker; loading: boolean = false; @@ -40,6 +54,17 @@ export class TaskStatusComponent implements OnDestroy, OnChanges { ngOnChanges(changes: SimpleChanges): void { if (changes.task && this.task) { + this.report = null; + if (this.task) { + let reportArray: []; + try { + reportArray = JSON.parse(this.task.report); + } catch (e) {} + if (reportArray && Array.isArray(reportArray)) { + this.report = new BrowserPaging(reportArray, this.reportColumns); + if (this.tableRefresh) this.tableRefresh(); + } + } this.subscriptions.push( this.notificationsService.getNotifications().subscribe(tasks => { this.task = tasks.find(task => task.id == this.task.id); @@ -54,6 +79,18 @@ export class TaskStatusComponent implements OnDestroy, OnChanges { } } + getPage(pageable: FilterablePageable): Promise> { + return Promise.resolve(this.report?.getPage(pageable)); + } + + downloadReport() { + QualityCardComponent.downloadReport(this.report); + } + + registerTableRefresh(refresh: () => void) { + this.tableRefresh = refresh; + } + retry() { this.loading = true; this.downloadService.retry(this.task).finally(() => this.loading = false); diff --git a/shanoir-ng-front/src/app/async-tasks/task.model.ts b/shanoir-ng-front/src/app/async-tasks/task.model.ts index 096805b430..e3456129cc 100644 --- a/shanoir-ng-front/src/app/async-tasks/task.model.ts +++ b/shanoir-ng-front/src/app/async-tasks/task.model.ts @@ -13,7 +13,6 @@ */ import { Entity } from '../shared/components/entity/entity.abstract'; -import { Report } from '../shared/mass-download/mass-download.service'; import { camelToSpaces } from '../utils/app.utils'; export enum TaskStatus { @@ -38,16 +37,18 @@ export class Task extends Entity { debugTs: number = Date.now(); id: number; + completeId: BigInt; creationDate: Date; lastUpdate: Date; + report: string; private _status: TaskStatus; private _message: string; private _progress: number; - private _eventType: string; + _eventType: string; eventLabel: string; objectId: number; route: string; - report: string; + hasReport: boolean; private readonly FIELDS: string[] = ['id', 'creationDate', 'lastUpdate','_status','_message', '_progress', '_eventType', 'eventLabel', 'objectId', 'route', 'report']; set eventType(eventType: string) { diff --git a/shanoir-ng-front/src/app/async-tasks/task.service.ts b/shanoir-ng-front/src/app/async-tasks/task.service.ts index 5cf3b8be8e..c36ef87ff2 100644 --- a/shanoir-ng-front/src/app/async-tasks/task.service.ts +++ b/shanoir-ng-front/src/app/async-tasks/task.service.ts @@ -14,10 +14,10 @@ import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; import { EntityService } from '../shared/components/entity/entity.abstract.service'; import * as AppUtils from '../utils/app.utils'; import { Task } from './task.model'; -import { HttpClient } from '@angular/common/http'; @Injectable() export class TaskService extends EntityService { @@ -36,4 +36,18 @@ export class TaskService extends EntityService { .toPromise() .then(this.mapEntityList); } + + protected toRealObject(entity: any): Task { + let trueObject = Object.assign(new Task(), entity); + trueObject.completeId = entity.idAsString; + Object.keys(entity).forEach(key => { + if (key != 'idAsString') { + let value = entity[key]; + if (['creationDate', 'lastUpdate'].includes(key) && value) { + trueObject[key] = new Date(value); + } + } + }); + return trueObject; + } } \ No newline at end of file diff --git a/shanoir-ng-front/src/app/examinations/shared/examination.service.ts b/shanoir-ng-front/src/app/examinations/shared/examination.service.ts index 27f4b50329..024d2f9636 100644 --- a/shanoir-ng-front/src/app/examinations/shared/examination.service.ts +++ b/shanoir-ng-front/src/app/examinations/shared/examination.service.ts @@ -40,12 +40,18 @@ export class ExaminationService extends EntityService { findExaminationsBySubjectAndStudy(subjectId: number, studyId: number): Promise { let url = AppUtils.BACKEND_API_EXAMINATION_URL + '/subject/' + subjectId - + '/study/' + studyId - + + '/study/' + studyId; return this.http.get(url) .toPromise(); } + findExaminationIdsByStudy(studyId: number): Promise { + let url = AppUtils.BACKEND_API_EXAMINATION_URL + + '/study/' + studyId; + return this.http.get(url) + .toPromise(); + } + getPage(pageable: Pageable, preclinical: boolean = false): Promise> { return this.http.get>( (!preclinical) ? AppUtils.BACKEND_API_EXAMINATION_URL : (AppUtils.BACKEND_API_EXAMINATION_PRECLINICAL_URL+'/1'), diff --git a/shanoir-ng-front/src/app/shared/components/confirm-dialog/confirm-dialog.component.html b/shanoir-ng-front/src/app/shared/components/confirm-dialog/confirm-dialog.component.html index 437409a2bd..a02f83abd3 100644 --- a/shanoir-ng-front/src/app/shared/components/confirm-dialog/confirm-dialog.component.html +++ b/shanoir-ng-front/src/app/shared/components/confirm-dialog/confirm-dialog.component.html @@ -17,15 +17,19 @@
- {{ title }} + {{ title }} - {{mode}}

diff --git a/shanoir-ng-front/src/app/shared/components/confirm-dialog/confirm-dialog.component.ts b/shanoir-ng-front/src/app/shared/components/confirm-dialog/confirm-dialog.component.ts index c8c5d8912a..b13d3d8bea 100644 --- a/shanoir-ng-front/src/app/shared/components/confirm-dialog/confirm-dialog.component.ts +++ b/shanoir-ng-front/src/app/shared/components/confirm-dialog/confirm-dialog.component.ts @@ -13,6 +13,7 @@ */ import { Component } from '@angular/core'; +import { SuperPromise } from 'src/app/utils/super-promise'; @Component({ selector: 'confirm-dialog', @@ -22,10 +23,10 @@ import { Component } from '@angular/core'; export class ConfirmDialogComponent { title: string; + mode: 'confirm' | 'choose' | 'info' | 'error'; private _message: string; - buttons: {ok: string, cancel: string}; - mode: 'confirm' | 'info' | 'error'; - private closeResolve: (value?: boolean | PromiseLike) => void; + buttons: {yes: string, no?: string, cancel: string}; + private closePromise: SuperPromise = new SuperPromise(); public get message(): string { return this._message; @@ -34,36 +35,38 @@ export class ConfirmDialogComponent { this._message = value?.split(' ').map(w => w.startsWith('https://') ? '' + w + '' : w).join(' '); } - public openConfirm(title: string, message: string, buttons?: {ok: string, cancel: string}): Promise { + public openConfirm(title: string, message: string, buttons?: {yes: string, cancel: string}): Promise { this.title = title; this.message = message; this.buttons = buttons; this.mode = 'confirm'; - return new Promise((resolve, reject) => { - this.closeResolve = resolve; - }); + return this.closePromise; + } + + public openChoose(title: string, message: string, buttons?: {yes: string, no: string, cancel: string}): Promise<'yes' | 'no' | false> { + this.title = title; + this.message = message; + this.buttons = buttons; + this.mode = 'choose'; + return this.closePromise; } public openInfo(title: string, message: string): Promise { this.title = title; this.message = message; this.mode = 'info'; - return new Promise((resolve, reject) => { - this.closeResolve = resolve; - }); + return this.closePromise; } public openError(title: string, message: string): Promise { this.title = title; this.message = message; this.mode = 'error'; - return new Promise((resolve, reject) => { - this.closeResolve = resolve; - }); + return this.closePromise; } - public close(answer: boolean) { - this.closeResolve(answer == true); // forces boolean to be returned + public close(answer: any) { + this.closePromise.resolve(answer); // forces boolean to be returned } } \ No newline at end of file diff --git a/shanoir-ng-front/src/app/shared/components/confirm-dialog/confirm-dialog.service.ts b/shanoir-ng-front/src/app/shared/components/confirm-dialog/confirm-dialog.service.ts index 2315359c33..5dd90a21e6 100644 --- a/shanoir-ng-front/src/app/shared/components/confirm-dialog/confirm-dialog.service.ts +++ b/shanoir-ng-front/src/app/shared/components/confirm-dialog/confirm-dialog.service.ts @@ -22,7 +22,7 @@ export class ConfirmDialogService { constructor(private componentFactoryResolver: ComponentFactoryResolver) { } - public confirm(title: string, message: string, buttons?: {ok: string, cancel: string}): Promise { + public confirm(title: string, message: string, buttons?: {yes: string, cancel: string}): Promise { const componentFactory = this.componentFactoryResolver.resolveComponentFactory(ConfirmDialogComponent); const ref: ComponentRef = ServiceLocator.rootViewContainerRef.createComponent(componentFactory); let dialog: ConfirmDialogComponent = ref.instance; @@ -32,6 +32,16 @@ export class ConfirmDialogService { }); } + public choose(title: string, message: string, buttons?: {yes: string, no: string, cancel: string}): Promise<'yes' | 'no' | false> { + const componentFactory = this.componentFactoryResolver.resolveComponentFactory(ConfirmDialogComponent); + const ref: ComponentRef = ServiceLocator.rootViewContainerRef.createComponent(componentFactory); + let dialog: ConfirmDialogComponent = ref.instance; + return dialog.openChoose(title, message, buttons).then(answer => { + ref.destroy(); + return answer; + }); + } + public inform(title: string, message: string): Promise { const componentFactory = this.componentFactoryResolver.resolveComponentFactory(ConfirmDialogComponent); const ref: ComponentRef = ServiceLocator.rootViewContainerRef.createComponent(componentFactory); diff --git a/shanoir-ng-front/src/app/shared/components/entity/entity.abstract.service.ts b/shanoir-ng-front/src/app/shared/components/entity/entity.abstract.service.ts index 9c49195415..9f075fe073 100644 --- a/shanoir-ng-front/src/app/shared/components/entity/entity.abstract.service.ts +++ b/shanoir-ng-front/src/app/shared/components/entity/entity.abstract.service.ts @@ -107,7 +107,7 @@ export abstract class EntityService implements OnDestroy { }) } - get(id: number, mode: 'eager' | 'lazy' = 'eager'): Promise { + get(id: number | BigInt, mode: 'eager' | 'lazy' = 'eager'): Promise { return this.http.get(this.API_URL + '/' + id) .toPromise() .then(entity => this.mapEntity(entity, null, mode)); @@ -140,7 +140,7 @@ export abstract class EntityService implements OnDestroy { }); } - protected toRealObject(entity: T): T { + protected toRealObject(entity: any): T { let trueObject = Object.assign(this.getEntityInstance(entity), entity); Object.keys(entity).forEach(key => { let value = entity[key]; diff --git a/shanoir-ng-front/src/app/shared/components/table/column.definition.type.ts b/shanoir-ng-front/src/app/shared/components/table/column.definition.type.ts index eeb98f3b80..dfef9a18d6 100644 --- a/shanoir-ng-front/src/app/shared/components/table/column.definition.type.ts +++ b/shanoir-ng-front/src/app/shared/components/table/column.definition.type.ts @@ -22,7 +22,7 @@ export type ColumnDefinition = { defaultField?: string, /** default is string, progress should be included in [0, 1] */ - type?: 'string' | 'number' | 'boolean' | 'button' | 'link' | 'date' | 'progress', + type?: 'string' | 'number' | 'boolean' | 'button' | 'link' | 'date' | 'dateTime' | 'progress', /** tells if the table is sorted by this column by default */ defaultSortCol?: boolean, diff --git a/shanoir-ng-front/src/app/shared/components/table/table.component.css b/shanoir-ng-front/src/app/shared/components/table/table.component.css index 17109a104e..afc2ccf5a6 100644 --- a/shanoir-ng-front/src/app/shared/components/table/table.component.css +++ b/shanoir-ng-front/src/app/shared/components/table/table.component.css @@ -61,12 +61,13 @@ td { padding: 0 5px; border: none; white-space: nowrap; text-overflow: ellipsis; td span.button i { color: var(--color-b); } td.loading, td.empty { padding: 20px 20px; text-align: center; } - .cell-container:not(.select) { overflow: hidden; text-overflow: ellipsis; position: absolute; top: 0; right: 0; bottom: 0; left: 0; padding: 1px 5px; } + .cell-container:not(.select):not(.wrap) { overflow: hidden; text-overflow: ellipsis; position: absolute; top: 0; right: 0; bottom: 0; left: 0; padding: 1px 5px; } .cell-container a { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .cell-container.multiselect a, .cell-container.wrap a { white-space: initial; width: auto; height: 100%; } /*.cell-container:empty:after { content: '.'; visibility: hidden; }*/ - .cell-new-tab { line-height:21px; height: inherit; display: block; text-decoration: none; color: black; width: auto; } - .default-data-parent { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .cell-container:not(.wrap) .default-data-parent { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .cell-container.wrap .default-data-parent { white-space-collapse: break-spaces; } + .cell-new-tab { line-height:21px; height: inherit; display: block; text-decoration: none; color: black; width: auto; } .loader { float: right; border-left: none !important; } .loader img { margin-left: 5px; vertical-align: -1; height: 10px; } diff --git a/shanoir-ng-front/src/app/shared/components/table/table.component.ts b/shanoir-ng-front/src/app/shared/components/table/table.component.ts index 7ac02821ea..e8bf60da83 100644 --- a/shanoir-ng-front/src/app/shared/components/table/table.component.ts +++ b/shanoir-ng-front/src/app/shared/components/table/table.component.ts @@ -52,6 +52,7 @@ export class TableComponent implements OnInit, OnChanges, OnDestroy { @Input() disableCondition: (item: any) => boolean; @Input() maxResults: number = 20; @Input() subRowsKey: string; + @Output() registerRefresh: EventEmitter<(number?) => void> = new EventEmitter(); page: Page; isLoading: boolean = false; maxResultsField: number; @@ -111,6 +112,7 @@ export class TableComponent implements OnInit, OnChanges, OnDestroy { this.checkCompactMode(); })); this.checkCompactMode(); + this.registerRefresh.emit(this.refresh.bind(this)); } private computeItemVars() { @@ -247,14 +249,17 @@ export class TableComponent implements OnInit, OnChanges, OnDestroy { let result: any = this.getCellValue(item, col); if (result == null || this.isValueBoolean(result)) { return ""; - } else if (col.type == 'date') { + } else if (col.type == 'date' || col.type == 'dateTime') { let date: Date; if (result instanceof Date) { date = result; } else { date = this.stringToDate(result); } - return date?.toLocaleDateString(undefined, {year: "numeric", month: "2-digit", day: "2-digit"}) || result; + let dateFormat; + if (col.type == 'dateTime') dateFormat = {year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hour12: false }; + else dateFormat = {year: "numeric", month: "2-digit", day: "2-digit"}; + return date?.toLocaleDateString(undefined, dateFormat) || result; } else if (result.text) { return result; } else { @@ -262,8 +267,9 @@ export class TableComponent implements OnInit, OnChanges, OnDestroy { } } - private stringToDate(dateString: String): Date { - if (!dateString) return null; + private stringToDate(dateString: string): Date { + if (!dateString) return null; + dateString += ''; let split: string[] = dateString.split('-'); if (split.length != 3) return null; let splitNum: number[] = split.map(elt => parseInt(elt)); diff --git a/shanoir-ng-front/src/app/shared/components/tree/tree-node.component.html b/shanoir-ng-front/src/app/shared/components/tree/tree-node.component.html index edf72e7329..80f2aef443 100644 --- a/shanoir-ng-front/src/app/shared/components/tree/tree-node.component.html +++ b/shanoir-ng-front/src/app/shared/components/tree/tree-node.component.html @@ -41,7 +41,10 @@ - + + {{qualityTag}} + + {{tag.name}} {{tooltip}} diff --git a/shanoir-ng-front/src/app/shared/components/tree/tree-node.component.ts b/shanoir-ng-front/src/app/shared/components/tree/tree-node.component.ts index 4da42b2958..160e7756e5 100644 --- a/shanoir-ng-front/src/app/shared/components/tree/tree-node.component.ts +++ b/shanoir-ng-front/src/app/shared/components/tree/tree-node.component.ts @@ -27,6 +27,7 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { CheckboxComponent } from '../../checkbox/checkbox.component'; import { Tag } from '../../../tags/tag.model'; import { isDarkColor } from '../../../utils/app.utils'; +import { QualityTag } from 'src/app/study-cards/shared/quality-card.model'; const noop = () => { }; @@ -59,6 +60,7 @@ export class TreeNodeComponent implements ControlValueAccessor, OnChanges { @Input() dataLoading: boolean = false; @Input() title: string; @Input() tags: Tag[]; + @Input() qualityTag: QualityTag; public isOpen: boolean = false; @Input() opened: boolean = false; @Output() openedChange: EventEmitter = new EventEmitter(); diff --git a/shanoir-ng-front/src/app/shared/mass-download/download-setup-alt/download-setup-alt.component.html b/shanoir-ng-front/src/app/shared/mass-download/download-setup-alt/download-setup-alt.component.html index 259e53db1c..d3770f03c7 100644 --- a/shanoir-ng-front/src/app/shared/mass-download/download-setup-alt/download-setup-alt.component.html +++ b/shanoir-ng-front/src/app/shared/mass-download/download-setup-alt/download-setup-alt.component.html @@ -1,3 +1,17 @@ + +

Download Datasets

diff --git a/shanoir-ng-front/src/app/shared/select/select.component.html b/shanoir-ng-front/src/app/shared/select/select.component.html index 3fe0287c99..b1e3108263 100644 --- a/shanoir-ng-front/src/app/shared/select/select.component.html +++ b/shanoir-ng-front/src/app/shared/select/select.component.html @@ -15,7 +15,7 @@
- + {{inputText ? inputText : (placeholder ? placeholder : 'Select an option...')}} diff --git a/shanoir-ng-front/src/app/shared/side-menu/side-menu.component.html b/shanoir-ng-front/src/app/shared/side-menu/side-menu.component.html index 69f07fef26..d88e842645 100644 --- a/shanoir-ng-front/src/app/shared/side-menu/side-menu.component.html +++ b/shanoir-ng-front/src/app/shared/side-menu/side-menu.component.html @@ -81,6 +81,9 @@ + + + diff --git a/shanoir-ng-front/src/app/shared/switch/switch.component.css b/shanoir-ng-front/src/app/shared/switch/switch.component.css index b9fb2ccef5..a5a060a514 100644 --- a/shanoir-ng-front/src/app/shared/switch/switch.component.css +++ b/shanoir-ng-front/src/app/shared/switch/switch.component.css @@ -1,8 +1,7 @@ -:host { position: relative; display: inline-block; width: 40px; height: 19px; vertical-align: bottom; } +:host { position: relative; display: inline-block; width: 40px; height: 19px; vertical-align: bottom; text-align: left; } :host.disabled { opacity: 0.5; } :host:focus { outline: none; } - .line { border: 1px solid var(--grey); border-radius: 5px; display: block; position: absolute; left: 0; right: 0; top: 5px; bottom: 5px; background-color: var(--light-grey); } .over-line { position: absolute; left: 0; width: 0px; transition: width .4s; background-color: orange; } .over-line.on { width: 22px; } diff --git a/shanoir-ng-front/src/app/studies/shared/study.service.ts b/shanoir-ng-front/src/app/studies/shared/study.service.ts index b0497ee593..891ca1f3ec 100644 --- a/shanoir-ng-front/src/app/studies/shared/study.service.ts +++ b/shanoir-ng-front/src/app/studies/shared/study.service.ts @@ -128,7 +128,7 @@ export class StudyService extends EntityService implements OnDestroy { } else { return this.getAll().then(studies => { const myId: number = KeycloakService.auth.userId; - return studies.filter(study => { + return studies?.filter(study => { return study.studyUserList.filter(su => su.userId == myId && su.studyUserRights.includes(StudyUserRight.CAN_ADMINISTRATE)).length > 0; }); }); @@ -136,11 +136,11 @@ export class StudyService extends EntityService implements OnDestroy { } findStudyIdsIcanAdmin(): Promise { - return this.findStudiesIcanAdmin().then(studies => studies.map(study => study.id)); + return this.findStudiesIcanAdmin().then(studies => studies?.map(study => study.id)); } findStudyIdNamesIcanAdmin(): Promise { - return this.findStudiesIcanAdmin().then(studies => studies.map(study => new IdName(study.id, study.name))); + return this.findStudiesIcanAdmin().then(studies => studies?.map(study => new IdName(study.id, study.name))); } uploadFile(fileToUpload: File, studyId: number, fileType: 'protocol-file'|'dua'): Promise { diff --git a/shanoir-ng-front/src/app/studies/study-list/study-list.component.ts b/shanoir-ng-front/src/app/studies/study-list/study-list.component.ts index 9bc1765aa7..9663c5bdf5 100644 --- a/shanoir-ng-front/src/app/studies/study-list/study-list.component.ts +++ b/shanoir-ng-front/src/app/studies/study-list/study-list.component.ts @@ -13,21 +13,19 @@ */ import { Component, ViewChild } from '@angular/core'; +import { EntityService } from 'src/app/shared/components/entity/entity.abstract.service'; +import { DatasetExpressionFormat } from "../../enum/dataset-expression-format.enum"; +import { ConfirmDialogService } from "../../shared/components/confirm-dialog/confirm-dialog.service"; import { BrowserPaginEntityListComponent } from '../../shared/components/entity/entity-list.browser.component.abstract'; -import { TableComponent } from '../../shared/components/table/table.component'; import { ColumnDefinition } from '../../shared/components/table/column.definition.type'; +import { TableComponent } from '../../shared/components/table/table.component'; +import { AccessRequest } from "../../users/access-request/access-request.model"; +import { UserService } from '../../users/shared/user.service'; import { capitalsAndUnderscoresToDisplayable } from '../../utils/app.utils'; import { StudyUserRight } from '../shared/study-user-right.enum'; +import { StudyUser } from "../shared/study-user.model"; import { Study } from '../shared/study.model'; import { StudyService } from '../shared/study.service'; -import { EntityService } from 'src/app/shared/components/entity/entity.abstract.service'; -import { UserService } from '../../users/shared/user.service'; -import {ConfirmDialogService} from "../../shared/components/confirm-dialog/confirm-dialog.service"; -import {DatasetExpressionFormat} from "../../enum/dataset-expression-format.enum"; -import {Page} from "../../shared/components/table/pageable.model"; -import {StudyUser} from "../shared/study-user.model"; -import {AccessRequest} from "../../users/access-request/access-request.model"; -import {EntityRoutes} from "../../shared/components/entity/entity.abstract"; @Component({ @@ -261,7 +259,7 @@ export class StudyListComponent extends BrowserPaginEntityListComponent { const text: string = 'You are a member of at least one study that needs you to accept its data user agreement. ' + 'Until you have agreed those terms you cannot access to any data from these studies. ' + 'Would you like to review those terms now?'; - const buttons = {ok: 'Yes, proceed to the signing page', cancel: 'Later'}; + const buttons = {yes: 'Yes, proceed to the signing page', cancel: 'Later'}; this.confirmService.confirm(title, text, buttons).then(response => { if (response == true) this.router.navigate(['/dua']); }); diff --git a/shanoir-ng-front/src/app/study-cards/apply-study-card-on/apply-study-card-on.component.css b/shanoir-ng-front/src/app/study-cards/apply-study-card-on/apply-study-card-on.component.css index a672609391..6a15edbaad 100644 --- a/shanoir-ng-front/src/app/study-cards/apply-study-card-on/apply-study-card-on.component.css +++ b/shanoir-ng-front/src/app/study-cards/apply-study-card-on/apply-study-card-on.component.css @@ -20,4 +20,5 @@ div.warning { color: orangered; } .done, .error { font-size: 35px; font-weight: bold; } .done { color: darkgreen; } .error { color: var(--color-error); } +.error .details { font-size: 12px; } input[type="checkbox"] { padding: 0; margin: 0 0 0 5px; vertical-align: text-bottom; } \ No newline at end of file diff --git a/shanoir-ng-front/src/app/study-cards/apply-study-card-on/apply-study-card-on.component.html b/shanoir-ng-front/src/app/study-cards/apply-study-card-on/apply-study-card-on.component.html index 515f23e228..7aa412de21 100644 --- a/shanoir-ng-front/src/app/study-cards/apply-study-card-on/apply-study-card-on.component.html +++ b/shanoir-ng-front/src/app/study-cards/apply-study-card-on/apply-study-card-on.component.html @@ -83,6 +83,7 @@

Apply Study Card

Error +
{{errorMessage}}
; + errorMessage: string; constructor( private datasetAcquisitionService: DatasetAcquisitionService, @@ -167,6 +168,9 @@ export class ApplyStudyCardOnComponent implements OnInit { this.status = 'done'; }).catch(error => { this.status = 'error'; + if (error?.error?.message?.includes('could not get dicom attributes from pacs')) { + this.errorMessage = 'Could not get dicom attributes from pacs'; + } throw error; }); } diff --git a/shanoir-ng-front/src/app/study-cards/quality-card/quality-card.component.css b/shanoir-ng-front/src/app/study-cards/quality-card/quality-card.component.css index 176dbd66c6..5fb790b22f 100644 --- a/shanoir-ng-front/src/app/study-cards/quality-card/quality-card.component.css +++ b/shanoir-ng-front/src/app/study-cards/quality-card/quality-card.component.css @@ -11,4 +11,8 @@ .center:not(:first-child)::before { content: ","; } -tool-tip li { list-style: circle; margin-left: 17px; } \ No newline at end of file +tool-tip li { list-style: circle; margin-left: 17px; } +.progress { color: var(--color-a-light); } +.progress progress-bar { margin-right: 10px; vertical-align: -5px; } + +.info { margin: 5px 0 20px 0; color: var(--color-b); font-style: italic; } \ No newline at end of file diff --git a/shanoir-ng-front/src/app/study-cards/quality-card/quality-card.component.html b/shanoir-ng-front/src/app/study-cards/quality-card/quality-card.component.html index 5981132043..239c893433 100644 --- a/shanoir-ng-front/src/app/study-cards/quality-card/quality-card.component.html +++ b/shanoir-ng-front/src/app/study-cards/quality-card/quality-card.component.html @@ -30,7 +30,7 @@

Create quality card

  • - + {{qualityCard.name}} @@ -73,6 +73,9 @@

    Create quality card

    Rules +
    + This rule set will be applied to each given examination +
    Create quality card (selectedRulesChange)="selectedRules = $event"> +
    + + Computing quality checks... +
    Application Report (test) - { {headerName: 'Details', field: 'message', wrap: true} ]; forceStudyId: number; + nbExaminations: number; + progress: number; + readonly NB_EXAM_THRESHOLD: number = 50; constructor( private route: ActivatedRoute, private qualityCardService: QualityCardService, private studyService: StudyService, + private examinationService: ExaminationService, private studyRightsService: StudyRightsService, keycloakService: KeycloakService, coilService: CoilService, @@ -188,31 +198,74 @@ export class QualityCardComponent extends EntityComponent { test() { this.testing = true; this.report = null; - this.qualityCardService.testOnStudy(this.qualityCard.id).then(result => { + + this.examinationService.findExaminationIdsByStudy(this.qualityCard.study.id).then(examIds => { + this.nbExaminations = examIds.length; + if (examIds?.length > 0) { + if (examIds.length > this.NB_EXAM_THRESHOLD) { + this.openSetTestInterval(this.nbExaminations).then(response => { + if (!response) { + this.performTest(); + } else if (response instanceof Interval) { + this.performTest((response as Interval).from, (response as Interval).to); + } + }).finally(() => { + this.testing = false; + }); + } else { + this.performTest(); + } + } + }); + } + + openSetTestInterval(nbExaminations: number): Promise { + let modalRef: ComponentRef = ServiceLocator.rootViewContainerRef.createComponent(TestQualityCardOptionsComponent); + modalRef.instance.nbExaminations = nbExaminations; + return this.waitForEnd(modalRef); + } + + private waitForEnd(modalRef: ComponentRef): Promise { + let resPromise: SuperPromise = new SuperPromise(); + let result: Observable = Observable.race([ + modalRef.instance.test, + modalRef.instance.close.map(() => 'cancel') + ]); + result.pipe(take(1)).subscribe(ret => { + modalRef.destroy(); + resPromise.resolve(ret); + }, error => { + modalRef.destroy(); + resPromise.reject(error); + }); + return resPromise; + } + + performTest(start?: number, stop?: number) { + this.qualityCardService.testOnStudy(this.qualityCard.id, start, stop).then(result => { this.report = new BrowserPaging(result, this.reportColumns); this.reportIsTest = true; }).finally(() => this.testing = false); } - getPage(pageable: FilterablePageable): Promise> { return Promise.resolve(this.report.getPage(pageable)); } - downloadReport() { - if (!this.report) return; + static downloadReport(report: any, name?: string) { + if (!report) return; let csvStr: string = ''; - csvStr += this.report.columnDefs.map(col => col.headerName).join(','); - for (let entry of this.report.items) { - csvStr += '\n' + this.report.columnDefs.map(col => '"' + TableComponent.getCellValue(entry, col) + '"').join(','); + csvStr += report.columnDefs.map(col => col.headerName).join(','); + for (let entry of report.items) { + csvStr += '\n' + report.columnDefs.map(col => '"' + TableComponent.getCellValue(entry, col) + '"').join(','); } const csvBlob = new Blob([csvStr], { type: 'text/csv' }); - AppUtils.browserDownloadFile(csvBlob, this.getReportFileName()); + AppUtils.browserDownloadFile(csvBlob, 'qcReport_' + (name ? name + '_' : '') + Date.now().toLocaleString('fr-FR')); } - private getReportFileName(): string { - return 'qcReport_' + this.qualityCard.name + '_' + Date.now().toLocaleString('fr-FR'); + protected downloadReport() { + QualityCardComponent.downloadReport(this.report, this.qualityCard?.name); } } \ No newline at end of file diff --git a/shanoir-ng-front/src/app/study-cards/shared/quality-card.dto.model.ts b/shanoir-ng-front/src/app/study-cards/shared/quality-card.dto.model.ts index d850d3fc5b..94133f436d 100644 --- a/shanoir-ng-front/src/app/study-cards/shared/quality-card.dto.model.ts +++ b/shanoir-ng-front/src/app/study-cards/shared/quality-card.dto.model.ts @@ -32,6 +32,7 @@ export class QualityCardDTO { let ruleDTO: QualityCardRuleDTO = new QualityCardRuleDTO(); ruleDTO.conditions = rule.conditions.map(cond => new StudyCardConditionDTO(cond)); ruleDTO.qualityTag = rule.tag; + ruleDTO.orConditions = rule.orConditions; return ruleDTO; }); this.toCheckAtImport = qualityCard.toCheckAtImport; @@ -42,4 +43,5 @@ export class QualityCardDTO { export class QualityCardRuleDTO { qualityTag: QualityTag[]; conditions: StudyCardConditionDTO[]; + orConditions: boolean; } diff --git a/shanoir-ng-front/src/app/study-cards/shared/quality-card.dto.ts b/shanoir-ng-front/src/app/study-cards/shared/quality-card.dto.ts index 21e094ef4d..6a9b4d5824 100644 --- a/shanoir-ng-front/src/app/study-cards/shared/quality-card.dto.ts +++ b/shanoir-ng-front/src/app/study-cards/shared/quality-card.dto.ts @@ -99,6 +99,7 @@ export class QualityCardDTOService { for (let ruleDTO of dto.rules) { let rule: QualityCardRule = new QualityCardRule(); rule.tag = ruleDTO.qualityTag; + rule.orConditions = ruleDTO.orConditions; if (ruleDTO.conditions) { rule.conditions = []; for (let conditionDTO of ruleDTO.conditions) { diff --git a/shanoir-ng-front/src/app/study-cards/shared/quality-card.model.ts b/shanoir-ng-front/src/app/study-cards/shared/quality-card.model.ts index 80770381ba..7a82af8b03 100644 --- a/shanoir-ng-front/src/app/study-cards/shared/quality-card.model.ts +++ b/shanoir-ng-front/src/app/study-cards/shared/quality-card.model.ts @@ -32,6 +32,7 @@ export class QualityCardRule { tag: QualityTag[]; conditions: StudyCardCondition[]; + orConditions: boolean = false; static copy(rule: QualityCardRule): QualityCardRule { let copy: QualityCardRule = new QualityCardRule(); diff --git a/shanoir-ng-front/src/app/study-cards/shared/quality-card.service.ts b/shanoir-ng-front/src/app/study-cards/shared/quality-card.service.ts index 7f3daca5c3..fe4216cc5d 100644 --- a/shanoir-ng-front/src/app/study-cards/shared/quality-card.service.ts +++ b/shanoir-ng-front/src/app/study-cards/shared/quality-card.service.ts @@ -21,6 +21,10 @@ import { QualityCardDTOService } from './quality-card.dto'; import { QualityCardDTO } from './quality-card.dto.model'; import { QualityCard } from './quality-card.model'; +export class Interval { + constructor(public from: number, public to: number) {} +}; + @Injectable() export class QualityCardService extends EntityService { @@ -62,8 +66,15 @@ export class QualityCardService extends EntityService { .toPromise(); } - testOnStudy(qualityCardId: number): Promise { - return this.http.get(this.API_URL + '/test/' + qualityCardId) + testOnStudy(qualityCardId: number, start?: number, stop?: number): Promise { + return this.http.get(this.API_URL + '/test/' + qualityCardId + + (start != null && start != undefined && stop != null && stop != undefined + ? '/' + (start - 1) + '/' + stop : '') + ).toPromise(); + } + + testOnExamination(qualityCardId: number, examinationId: number): Promise { + return this.http.get(this.API_URL + '/test/' + qualityCardId + '/exam/' + examinationId) .toPromise(); } diff --git a/shanoir-ng-front/src/app/study-cards/shared/study-card.model.ts b/shanoir-ng-front/src/app/study-cards/shared/study-card.model.ts index e5eca07415..5992fb50d3 100644 --- a/shanoir-ng-front/src/app/study-cards/shared/study-card.model.ts +++ b/shanoir-ng-front/src/app/study-cards/shared/study-card.model.ts @@ -35,6 +35,7 @@ export class StudyCardRule { assignments: StudyCardAssignment[]; conditions: StudyCardCondition[]; + orConditions: boolean = false; static copy(rule: StudyCardRule): StudyCardRule { let copy: StudyCardRule = new StudyCardRule(rule.scope); @@ -101,9 +102,9 @@ export class DicomTag { } } -export type Operation = 'STARTS_WITH' | 'EQUALS' | 'ENDS_WITH' | 'CONTAINS' | 'DOES_NOT_CONTAIN' | 'SMALLER_THAN' | 'BIGGER_THAN'; +export type Operation = 'STARTS_WITH' | 'EQUALS' | 'ENDS_WITH' | 'CONTAINS' | 'DOES_NOT_CONTAIN' | 'SMALLER_THAN' | 'BIGGER_THAN' | 'DOES_NOT_START_WITH' | 'NOT_EQUALS' | 'DOES_NOT_END_WITH'; -export type ConditionScope = 'StudyCardDICOMCondition' | 'AcqMetadataCondOnAcq' | 'AcqMetadataCondOnDatasets' | +export type ConditionScope = 'StudyCardDICOMConditionOnDatasets' | 'AcqMetadataCondOnAcq' | 'AcqMetadataCondOnDatasets' | 'DatasetMetadataCondOnDataset' | 'ExamMetadataCondOnAcq' | 'ExamMetadataCondOnDatasets'; export type MetadataFieldScope = 'Dataset' | 'DatasetAcquisition'; \ No newline at end of file diff --git a/shanoir-ng-front/src/app/study-cards/study-card-rules/condition/condition.component.css b/shanoir-ng-front/src/app/study-cards/study-card-rules/condition/condition.component.css index 37eac63742..ea49838443 100644 --- a/shanoir-ng-front/src/app/study-cards/study-card-rules/condition/condition.component.css +++ b/shanoir-ng-front/src/app/study-cards/study-card-rules/condition/condition.component.css @@ -1,15 +1,13 @@ :host { white-space: nowrap; color: purple; } select-box, input { border: none !important; margin-right: 5px; } -input { box-sizing: border-box; width: auto; padding: 0 1px; } -auto-ajust-input { vertical-align: bottom; } +input { box-sizing: border-box; width: auto; padding: 0 1px; vertical-align: middle; } input[type=number]::-webkit-inner-spin-button, input[type=number]::-webkit-outer-spin-button { opacity: 1; } .cardinality-type { color: darkgreen; } .cardinality { color: chocolate; } -.type { } .var { color: blue; } .method { color: darkgreen; } .value { color: chocolate; text-overflow: ellipsis; } @@ -31,10 +29,14 @@ input[type=number]::-webkit-outer-spin-button { opacity: 1; } .error:not(:focus-within) { color: white !important; background-color: var(--color-error) !important; border-radius: 4px !important; } .error:not(:focus-within)::ng-deep ::placeholder { color: white !important; } +.multi-value.edit { margin-top: 3px; } .multi-value { display: inline-block; vertical-align: top; position: relative; } .multi-value .one-value { display: flex; } +auto-ajust-input { margin-top: -3px; } .add-cond-value { text-align: left; color: var(--color-b); cursor: pointer; display: inline-block; padding: 1px 8px; border: 1px dashed var(--grey); border-radius: 10px; margin: 1px 0 4px -7px; padding: 0px 7px; } .add-cond-value i { margin-right: 7px; color: purple; } -.one-value + .one-value::before { content: "or "; margin-right: 5px; color: purple; } \ No newline at end of file +.one-value + .one-value::before { content: "or "; margin-right: 5px; color: purple; } + +span { vertical-align: middle; } \ No newline at end of file diff --git a/shanoir-ng-front/src/app/study-cards/study-card-rules/condition/condition.component.html b/shanoir-ng-front/src/app/study-cards/study-card-rules/condition/condition.component.html index 848a0985cf..7dc02bfd14 100644 --- a/shanoir-ng-front/src/app/study-cards/study-card-rules/condition/condition.component.html +++ b/shanoir-ng-front/src/app/study-cards/study-card-rules/condition/condition.component.html @@ -12,15 +12,24 @@ along with this program. If not, see https://www.gnu.org/licenses/gpl-3.0.html --> -if  +if,  - all of - none of - at least + for every + for no + for at least {{condition.cardinality}} - {{condition.dicomTag?.label ? condition.dicomTag.label : (condition.dicomTag?.code | dicomTagLabel)}} - {{fieldLabel}} - {{condition.operation}} + datasets in the exam, + {{condition.dicomTag?.label ? condition.dicomTag.label : (condition.dicomTag?.code | dicomTagLabel)}} + {{fieldLabel}} + + + + + + + {{condition.operation}} + +
    or @@ -29,26 +38,23 @@ - - - - - + + + + datasets in the exam, - +
    - + []; shanoirFieldOptions: Option[]; - operations: Operation[] = ['STARTS_WITH', 'EQUALS', 'ENDS_WITH', 'CONTAINS', "DOES_NOT_CONTAIN", 'SMALLER_THAN', 'BIGGER_THAN']; + operations: Option[] = [ + new Option('EQUALS', ' ', null, null, 'fa-solid fa-equals'), + new Option('NOT_EQUALS', ' ', null, null, 'fa-solid fa-not-equal'), + new Option('SMALLER_THAN', ' ', null, null, 'fa-solid fa-less-than'), + new Option('BIGGER_THAN', ' ', null, null, 'fa-solid fa-greater-than'), + new Option('CONTAINS', 'contains', null, null), + new Option('DOES_NOT_CONTAIN', '! contains', null, null), + new Option('STARTS_WITH', 'starts with', null, null), + new Option('DOES_NOT_START_WITH', '! starts with', null, null), + new Option('ENDS_WITH', 'ends with', null, null), + new Option('DOES_NOT_END_WITH', '! ends with'), + ]; @Output() delete: EventEmitter = new EventEmitter(); init: boolean = false; conditionTypeOptions: Option[]; @@ -80,18 +91,18 @@ export class StudyCardConditionComponent implements OnInit, OnDestroy, OnChanges if (changes.ruleScope && this.ruleScope) { if (this.ruleScope == 'Dataset') { this.conditionTypeOptions = [ - new Option('StudyCardDICOMCondition', 'the DICOM field'), + new Option('StudyCardDICOMConditionOnDatasets', 'the DICOM field'), new Option('DatasetMetadataCondOnDataset', 'the dataset field'), ]; } else if (this.ruleScope == 'DatasetAcquisition') { this.conditionTypeOptions = [ - new Option('StudyCardDICOMCondition', 'the DICOM field'), + new Option('StudyCardDICOMConditionOnDatasets', 'the DICOM field'), new Option('AcqMetadataCondOnAcq', 'the acquisition field'), new Option('AcqMetadataCondOnDatasets', 'the dataset field'), ]; } else if (this.ruleScope == 'Examination') { this.conditionTypeOptions = [ - new Option('StudyCardDICOMCondition', 'the DICOM field'), + new Option('StudyCardDICOMConditionOnDatasets', 'the DICOM field'), new Option('ExamMetadataCondOnAcq', 'the acquisition field'), new Option('ExamMetadataCondOnDatasets', 'the dataset field'), ]; @@ -179,7 +190,7 @@ export class StudyCardConditionComponent implements OnInit, OnDestroy, OnChanges } onFieldChange(field: string) { - if (!(this.condition.scope == 'StudyCardDICOMCondition' && this.condition.dicomTag.code+"" == field)) { + if (!(this.condition.scope == 'StudyCardDICOMConditionOnDatasets' && this.condition.dicomTag.code+"" == field)) { this.computeConditionOptions(); if (this.shanoirFieldOptions?.length > 0) this.condition.operation = 'EQUALS'; else this.condition.operation = null; @@ -211,7 +222,7 @@ export class StudyCardConditionComponent implements OnInit, OnDestroy, OnChanges this.computeConditionOptionsSubscription.unsubscribe(); this.computeConditionOptionsSubscription = null; } - if (this.condition.scope != 'StudyCardDICOMCondition') { + if (this.condition.scope != 'StudyCardDICOMConditionOnDatasets') { let conditionField: ShanoirMetadataField = this.fields.find(metadataField => metadataField.field == this.condition.shanoirField); if (conditionField && conditionField.options) { this.computeConditionOptionsSubscription = conditionField.options.subscribe(opts => { diff --git a/shanoir-ng-front/src/app/study-cards/study-card-rules/quality-card-rule.component.html b/shanoir-ng-front/src/app/study-cards/study-card-rules/quality-card-rule.component.html index 63f80c1003..322978ae5a 100644 --- a/shanoir-ng-front/src/app/study-cards/study-card-rules/quality-card-rule.component.html +++ b/shanoir-ng-front/src/app/study-cards/study-card-rules/quality-card-rule.component.html @@ -12,7 +12,7 @@ along with this program. If not, see https://www.gnu.org/licenses/gpl-3.0.html --> -
    +
    Always
    -
    - Add a condition +
    + + And + + Or + + Add a condition
    diff --git a/shanoir-ng-front/src/app/study-cards/study-card-rules/quality-card-rule.component.ts b/shanoir-ng-front/src/app/study-cards/study-card-rules/quality-card-rule.component.ts index ba9dc32a11..fbc3c31c63 100644 --- a/shanoir-ng-front/src/app/study-cards/study-card-rules/quality-card-rule.component.ts +++ b/shanoir-ng-front/src/app/study-cards/study-card-rules/quality-card-rule.component.ts @@ -64,7 +64,7 @@ export class QualityCardRuleComponent implements OnChanges { } addNewCondition() { - let cond = new StudyCardCondition('StudyCardDICOMCondition'); + let cond = new StudyCardCondition('StudyCardDICOMConditionOnDatasets'); cond.values = [null]; this.rule.conditions.push(cond); this.change.emit(this.rule); diff --git a/shanoir-ng-front/src/app/study-cards/study-card-rules/study-card-rule.component.css b/shanoir-ng-front/src/app/study-cards/study-card-rules/study-card-rule.component.css index deef5696e0..4a73895cab 100644 --- a/shanoir-ng-front/src/app/study-cards/study-card-rules/study-card-rule.component.css +++ b/shanoir-ng-front/src/app/study-cards/study-card-rules/study-card-rule.component.css @@ -5,7 +5,8 @@ .conditions, .separator, .actions { vertical-align: middle; display: inline-block; position: relative; } -.conditions::ng-deep .condition:not(:first-child) condition::before { display: inline; content: "and " } +.conditions::ng-deep .condition:not(:first-child) condition::before { display: inline; vertical-align: middle; content: "and " } +.conditions.or::ng-deep .condition:not(:first-child) condition::before { display: inline; vertical-align: middle; content: "or " } condition, action, .add, .always, .quality-tag { border: 1px solid var(--grey); @@ -41,4 +42,7 @@ condition.edit:hover, action.edit:hover { border-color: var(--color-b); } .sep-symbol { padding: 20px; } .error { background-color: var(--color-error); color: white; } -.error i { color: white; } \ No newline at end of file +.error i { color: white; } + +.and-or { margin-right: 20px; color: var(--color-a); } +toggle-switch { vertical-align: middle; margin: 0 2px; } \ No newline at end of file diff --git a/shanoir-ng-front/src/app/study-cards/study-card-rules/study-card-rule.component.ts b/shanoir-ng-front/src/app/study-cards/study-card-rules/study-card-rule.component.ts index 34c7d023fa..df33606ad8 100644 --- a/shanoir-ng-front/src/app/study-cards/study-card-rules/study-card-rule.component.ts +++ b/shanoir-ng-front/src/app/study-cards/study-card-rules/study-card-rule.component.ts @@ -86,7 +86,7 @@ export class StudyCardRuleComponent implements OnChanges { } addNewCondition() { - let cond = new StudyCardCondition('StudyCardDICOMCondition'); + let cond = new StudyCardCondition('StudyCardDICOMConditionOnDatasets'); cond.values = [null]; this.rule.conditions.push(cond); this.change.emit(this.rule); diff --git a/shanoir-ng-front/src/app/study-cards/study-card-rules/study-card-rules.component.ts b/shanoir-ng-front/src/app/study-cards/study-card-rules/study-card-rules.component.ts index a1d54e51c5..74f56ef712 100644 --- a/shanoir-ng-front/src/app/study-cards/study-card-rules/study-card-rules.component.ts +++ b/shanoir-ng-front/src/app/study-cards/study-card-rules/study-card-rules.component.ts @@ -85,7 +85,14 @@ export class StudyCardRulesComponent implements OnChanges, ControlValueAccessor private element: ElementRef, private confirmDialogService: ConfirmDialogService, private breadcrumbService: BreadcrumbsService) { - + + if (this.breadcrumbService.currentStep.data.rulesToAnimate) + this.rulesToAnimate = this.breadcrumbService.currentStep.data.rulesToAnimate; + else + this.breadcrumbService.currentStep.data.rulesToAnimate = this.rulesToAnimate; + } + + initFields() { this.assignmentFields = [ new ShanoirMetadataField('Dataset modality type', 'MODALITY_TYPE', 'Dataset', DatasetModalityType.options), new ShanoirMetadataField('Protocol name', 'PROTOCOL_NAME', 'DatasetAcquisition'), @@ -96,21 +103,15 @@ export class StudyCardRulesComponent implements OnChanges, ControlValueAccessor new ShanoirMetadataField('Acquisition contrast', 'ACQUISITION_CONTRAST', 'DatasetAcquisition', AcquisitionContrast.options), new ShanoirMetadataField('MR sequence application', 'MR_SEQUENCE_APPLICATION', 'DatasetAcquisition', MrSequenceApplication.options), new ShanoirMetadataField('MR sequence physics', 'MR_SEQUENCE_PHYSICS', 'DatasetAcquisition', MrSequencePhysics.options), - new ShanoirMetadataField('New name for the dataset', 'NAME', 'Dataset'), + new ShanoirMetadataField(this.cardType == 'studycard' ? 'New name for the dataset' : 'Dataset name' , 'NAME', 'Dataset'), new ShanoirMetadataField('Dataset comment', 'COMMENT', 'Dataset'), new ShanoirMetadataField('MR sequence name', 'MR_SEQUENCE_NAME', 'DatasetAcquisition'), new ShanoirMetadataField('Contrast agent used', 'CONTRAST_AGENT_USED', 'DatasetAcquisition', ContrastAgent.options), new ShanoirMetadataField('Mr Dataset Nature', 'MR_DATASET_NATURE', 'Dataset', MrDatasetNature.options), new ShanoirMetadataField('BIDS data type', 'BIDS_DATA_TYPE', 'DatasetAcquisition', BidsDataType.options) ]; - // here we reference assignment fields but conditions could be different this.conditionFields = this.assignmentFields; - - if (this.breadcrumbService.currentStep.data.rulesToAnimate) - this.rulesToAnimate = this.breadcrumbService.currentStep.data.rulesToAnimate; - else - this.breadcrumbService.currentStep.data.rulesToAnimate = this.rulesToAnimate; } ngOnChanges(changes: SimpleChanges): void { @@ -127,8 +128,13 @@ export class StudyCardRulesComponent implements OnChanges, ControlValueAccessor this.coilOptionsSubject.next([]); } - } else if (changes.allCoils && this.allCoils) { + } + if (changes.allCoils && this.allCoils) { this.allCoilsPromise.resolve(this.allCoils); + } + if (changes.cardType && this.cardType && (!this.assignmentFields || !this.conditionFields)) { + console.log(this.cardType) + this.initFields(); } } @@ -245,10 +251,10 @@ export class StudyCardRulesComponent implements OnChanges, ControlValueAccessor if (rule.conditions?.find(cond => cond.scope == null)) { errors.noType = true; } - if (rule.conditions?.find(cond => cond.scope == 'StudyCardDICOMCondition' && !cond.dicomTag)) { + if (rule.conditions?.find(cond => cond.scope == 'StudyCardDICOMConditionOnDatasets' && !cond.dicomTag)) { errors.missingField = 'condition dicomTag'; } - if (rule.conditions?.find(cond => cond.scope != 'StudyCardDICOMCondition' && !cond.shanoirField)) { + if (rule.conditions?.find(cond => cond.scope != 'StudyCardDICOMConditionOnDatasets' && !cond.shanoirField)) { errors.missingField = 'condition shanoirField'; } if (rule.conditions?.find(cond => !cond.operation)) { diff --git a/shanoir-ng-front/src/app/study-cards/test-quality-card-options/test-quality-card-options.component.css b/shanoir-ng-front/src/app/study-cards/test-quality-card-options/test-quality-card-options.component.css new file mode 100644 index 0000000000..c3db907373 --- /dev/null +++ b/shanoir-ng-front/src/app/study-cards/test-quality-card-options/test-quality-card-options.component.css @@ -0,0 +1,10 @@ +:host { display: table; background-color: var(--color-overlay); position: fixed; top: 0; bottom: 0; left: 0; right: 0; height: 100%; width: 100%; z-index: 10000; + vertical-align: middle; text-align: center; } +.cell { display: table-cell; vertical-align: middle; text-align: center; } +.window { display: inline-block; background-color: var(--very-light-grey); border-radius: 2px; vertical-align: middle; padding: 15px 20px; } + +.body { width: 100%; text-align: left; margin-top: 20px; } +.footer { margin-top: 20px; } +input[type="number"] { width: 52px; } + +.msg { color: var(--color-a); } \ No newline at end of file diff --git a/shanoir-ng-front/src/app/study-cards/test-quality-card-options/test-quality-card-options.component.html b/shanoir-ng-front/src/app/study-cards/test-quality-card-options/test-quality-card-options.component.html new file mode 100644 index 0000000000..9688910590 --- /dev/null +++ b/shanoir-ng-front/src/app/study-cards/test-quality-card-options/test-quality-card-options.component.html @@ -0,0 +1,54 @@ + + +
    + +

    Large Volume

    +

    + This study contains {{nbExaminations}} examinations. The quality check could take several minutes. +
    Do you want to test the quality card on a sample to reduce the computing time ? +

    +
    +
      +
    1. + + + + +
    2. +
    3. + + + + +
    4. +
    5. + + + {{form.get('to').value - form.get('from').value + 1}} examinations + +
    6. +
    +
    + + +
    \ No newline at end of file diff --git a/shanoir-ng-front/src/app/study-cards/test-quality-card-options/test-quality-card-options.component.ts b/shanoir-ng-front/src/app/study-cards/test-quality-card-options/test-quality-card-options.component.ts new file mode 100644 index 0000000000..e4e570605e --- /dev/null +++ b/shanoir-ng-front/src/app/study-cards/test-quality-card-options/test-quality-card-options.component.ts @@ -0,0 +1,86 @@ +/** + * Shanoir NG - Import, manage and share neuroimaging data + * Copyright (C) 2009-2019 Inria - https://www.inria.fr/ + * Contact us on https://project.inria.fr/shanoir/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see https://www.gnu.org/licenses/gpl-3.0.html + */ +import { Component, ElementRef, EventEmitter, HostListener, Input, OnInit, Output, ViewChild } from '@angular/core'; +import { Interval } from '../shared/quality-card.service'; +import { AbstractControl, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { GlobalService } from 'src/app/shared/services/global.service'; + + +@Component({ + selector: 'test-quality-card-options', + templateUrl: 'test-quality-card-options.component.html', + styleUrls: ['test-quality-card-options.component.css'] +}) +export class TestQualityCardOptionsComponent implements OnInit { + + @Input() nbExaminations: number; + @Output() test: EventEmitter = new EventEmitter(); + @Output() close: EventEmitter = new EventEmitter(); + form: UntypedFormGroup + @ViewChild('window') window: ElementRef; + + constructor(private formBuilder: UntypedFormBuilder, globalService: GlobalService) { + globalService.onNavigate.subscribe(() => { + this.cancel(); + }); + } + + ngOnInit(): void { + this.form = this.buildForm(); + } + + private buildForm(): UntypedFormGroup { + let formGroup = this.formBuilder.group({ + 'from': [1], + 'to': [this.nbExaminations > 20 ? 20 : this.nbExaminations], + }); + formGroup.controls.from.setValidators([ + Validators.required, + Validators.min(1), + (control: AbstractControl) => Validators.max(formGroup.get('to').value)(control) + ]); + formGroup.controls.to.setValidators([ + Validators.required, + (control: AbstractControl) => Validators.min(formGroup.get('from').value)(control) + ]); + if (this.nbExaminations) { + formGroup.controls.to.addValidators([Validators.max(this.nbExaminations)]); + } + return formGroup; + }; + + testOnAll() { + this.test.emit(); + } + + testOnInterval() { + this.test.emit(new Interval(this.form.get('from').value, this.form.get('to').value)); + } + + cancel() { + this.close.emit(); + } + + @HostListener('click', ['$event']) + onClick(clickEvent) { + if (!this.window.nativeElement.contains(clickEvent.target)) { + this.cancel(); + } + } + + updateValidity() { + this.form.controls.from.updateValueAndValidity(); + this.form.controls.to.updateValueAndValidity(); + } +} \ No newline at end of file diff --git a/shanoir-ng-front/src/app/subjects/tree/subject-node.component.html b/shanoir-ng-front/src/app/subjects/tree/subject-node.component.html index 09edaaa844..22f83b4fc9 100644 --- a/shanoir-ng-front/src/app/subjects/tree/subject-node.component.html +++ b/shanoir-ng-front/src/app/subjects/tree/subject-node.component.html @@ -17,6 +17,7 @@ [class.selected]="this.menuOpened" [label]="node.label" [tags]="node.tags" + [qualityTag]="node.qualityTag" [awesome]="node.awesome" (firstOpen)="loadExaminations()" [(opened)]="node.open" diff --git a/shanoir-ng-front/src/app/subjects/tree/subject-node.component.ts b/shanoir-ng-front/src/app/subjects/tree/subject-node.component.ts index b8d0cacc18..c432f23343 100644 --- a/shanoir-ng-front/src/app/subjects/tree/subject-node.component.ts +++ b/shanoir-ng-front/src/app/subjects/tree/subject-node.component.ts @@ -11,27 +11,29 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see https://www.gnu.org/licenses/gpl-3.0.html */ -import {Component, EventEmitter, Input, OnChanges, Output, SimpleChanges} from '@angular/core'; -import {Router} from '@angular/router'; - -import {DatasetAcquisition} from '../../dataset-acquisitions/shared/dataset-acquisition.model'; -import {DatasetProcessing} from '../../datasets/shared/dataset-processing.model'; -import {Dataset} from '../../datasets/shared/dataset.model'; -import {DatasetProcessingType} from '../../enum/dataset-processing-type.enum'; -import {ExaminationPipe} from '../../examinations/shared/examination.pipe'; -import {ExaminationService} from '../../examinations/shared/examination.service'; -import {SubjectExamination} from '../../examinations/shared/subject-examination.model'; +import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; +import { Router } from '@angular/router'; + +import { MassDownloadService } from 'src/app/shared/mass-download/mass-download.service'; +import { DatasetAcquisition } from '../../dataset-acquisitions/shared/dataset-acquisition.model'; +import { DatasetProcessing } from '../../datasets/shared/dataset-processing.model'; +import { Dataset } from '../../datasets/shared/dataset.model'; +import { DatasetProcessingType } from '../../enum/dataset-processing-type.enum'; +import { ExaminationPipe } from '../../examinations/shared/examination.pipe'; +import { ExaminationService } from '../../examinations/shared/examination.service'; +import { SubjectExamination } from '../../examinations/shared/subject-examination.model'; import { + ClinicalSubjectNode, DatasetAcquisitionNode, DatasetNode, ExaminationNode, + PreclinicalSubjectNode, ProcessingNode, - ClinicalSubjectNode, - UNLOADED, SubjectNode, PreclinicalSubjectNode, + SubjectNode, + UNLOADED, } from '../../tree/tree.model'; -import {Subject} from '../shared/subject.model'; -import {SubjectService} from "../shared/subject.service"; -import { MassDownloadService } from 'src/app/shared/mass-download/mass-download.service'; +import { Subject } from '../shared/subject.model'; +import { SubjectService } from "../shared/subject.service"; @Component({ diff --git a/shanoir-ng-front/src/app/tree/tree.model.ts b/shanoir-ng-front/src/app/tree/tree.model.ts index c83906f693..00a95af5b5 100644 --- a/shanoir-ng-front/src/app/tree/tree.model.ts +++ b/shanoir-ng-front/src/app/tree/tree.model.ts @@ -85,6 +85,7 @@ export abstract class SubjectNode implements ShanoirNode { export class ClinicalSubjectNode extends SubjectNode { public title = "subject"; public awesome = "fas fa-user-injured"; + qualityTag: QualityTag; } export class PreclinicalSubjectNode extends SubjectNode { diff --git a/shanoir-ng-front/tsconfig.json b/shanoir-ng-front/tsconfig.json index b574017494..02fbf7a9d3 100644 --- a/shanoir-ng-front/tsconfig.json +++ b/shanoir-ng-front/tsconfig.json @@ -15,7 +15,7 @@ "node_modules/@types" ], "lib": [ - "es2018", + "es2022", "dom" ], "useDefineForClassFields": false diff --git a/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dcm2nii/DatasetsCreatorAndNIfTIConverterService.java b/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dcm2nii/DatasetsCreatorAndNIfTIConverterService.java index ad436371a6..194e8e6baf 100644 --- a/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dcm2nii/DatasetsCreatorAndNIfTIConverterService.java +++ b/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dcm2nii/DatasetsCreatorAndNIfTIConverterService.java @@ -43,7 +43,6 @@ import org.shanoir.ng.importer.model.Dataset; import org.shanoir.ng.importer.model.DatasetFile; import org.shanoir.ng.importer.model.DiffusionGradient; -import org.shanoir.ng.importer.model.EchoTime; import org.shanoir.ng.importer.model.ExpressionFormat; import org.shanoir.ng.importer.model.Image; import org.shanoir.ng.importer.model.ImportJob; @@ -51,6 +50,8 @@ import org.shanoir.ng.importer.model.Serie; import org.shanoir.ng.importer.model.Study; import org.shanoir.ng.shared.configuration.RabbitMQConfiguration; +import org.shanoir.ng.shared.dicom.EchoTime; +import org.shanoir.ng.shared.dicom.SerieToDatasetsSeparator; import org.shanoir.ng.shared.event.ShanoirEventService; import org.shanoir.ng.shared.exception.RestServiceException; import org.shanoir.ng.shared.exception.ShanoirException; @@ -707,9 +708,10 @@ private void constructDicom(final File serieIDFolderFile, final Serie serie, fin datasetMap.get(seriesToDatasetsSeparator).getRepetitionTimes().add(image.getRepetitionTime()); datasetMap.get(seriesToDatasetsSeparator).getInversionTimes().add(image.getInversionTime()); datasetMap.get(seriesToDatasetsSeparator).setEchoTimes(image.getEchoTimes()); - // new dataset has to be created, new expression format and add image/datasetfile + // new dataset has to be created, new expression format and add image/datasetfile } else { Dataset dataset = new Dataset(); + dataset.setFirstImageSOPInstanceUID(image.getSOPInstanceUID()); ExpressionFormat expressionFormat = new ExpressionFormat(); expressionFormat.setType("dcm"); dataset.getExpressionFormats().add(expressionFormat); diff --git a/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dicom/ImagesCreatorAndDicomFileAnalyzerService.java b/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dicom/ImagesCreatorAndDicomFileAnalyzerService.java index 0549c1be25..05e7565e19 100644 --- a/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dicom/ImagesCreatorAndDicomFileAnalyzerService.java +++ b/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dicom/ImagesCreatorAndDicomFileAnalyzerService.java @@ -29,7 +29,6 @@ import org.dcm4che3.data.UID; import org.dcm4che3.emf.MultiframeExtractor; import org.dcm4che3.io.DicomInputStream; -import org.shanoir.ng.importer.model.EchoTime; import org.shanoir.ng.importer.model.EquipmentDicom; import org.shanoir.ng.importer.model.Image; import org.shanoir.ng.importer.model.Instance; @@ -38,6 +37,7 @@ import org.shanoir.ng.importer.model.Serie; import org.shanoir.ng.importer.model.Study; import org.shanoir.ng.shared.dateTime.DateTimeUtils; +import org.shanoir.ng.shared.dicom.EchoTime; import org.shanoir.ng.shared.event.ShanoirEvent; import org.shanoir.ng.shared.event.ShanoirEventService; import org.slf4j.Logger; @@ -282,6 +282,7 @@ private void addImageSeparateDatasetsInfo(Image image, Attributes attributes) th MultiframeExtractor emf = new MultiframeExtractor(); attributes = emf.extract(attributes, 0); } + image.setSOPInstanceUID(attributes.getString(Tag.SOPInstanceUID)); // acquisition number image.setAcquisitionNumber(attributes.getInt(Tag.AcquisitionNumber, 0)); // image orientation patient diff --git a/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/model/Dataset.java b/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/model/Dataset.java index 85228b43a5..b4e7b71d0f 100644 --- a/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/model/Dataset.java +++ b/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/model/Dataset.java @@ -19,6 +19,8 @@ import java.util.List; import java.util.Set; +import org.shanoir.ng.shared.dicom.EchoTime; + import com.fasterxml.jackson.annotation.JsonProperty; public class Dataset { @@ -42,6 +44,9 @@ public class Dataset { @JsonProperty("flipAngles") public Set flipAngles; + + @JsonProperty("firstImageSOPInstanceUID") + private String firstImageSOPInstanceUID; public String getName() { return name; @@ -114,6 +119,11 @@ public void setEchoTimes(Set echoTimes) { this.echoTimes = echoTimes; } + public String getFirstImageSOPInstanceUID() { + return firstImageSOPInstanceUID; + } - + public void setFirstImageSOPInstanceUID(String firstImageSOPInstanceUID) { + this.firstImageSOPInstanceUID = firstImageSOPInstanceUID; + } } diff --git a/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/model/Image.java b/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/model/Image.java index ace1e0f39f..c64d26b747 100644 --- a/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/model/Image.java +++ b/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/model/Image.java @@ -17,6 +17,8 @@ import java.util.List; import java.util.Set; +import org.shanoir.ng.shared.dicom.EchoTime; + import com.fasterxml.jackson.annotation.JsonProperty; public class Image { @@ -42,6 +44,8 @@ public class Image { @JsonProperty("imageOrientationPatient") public List imageOrientationPatient; + public String SOPInstanceUID; + public String getPath() { return path; } @@ -98,4 +102,11 @@ public void setFlipAngle(String flipAngle) { this.flipAngle = flipAngle; } + public String getSOPInstanceUID() { + return SOPInstanceUID; + } + + public void setSOPInstanceUID(String sOPInstanceUID) { + SOPInstanceUID = sOPInstanceUID; + } } diff --git a/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/model/EchoTime.java b/shanoir-ng-ms-common/src/main/java/org/shanoir/ng/shared/dicom/EchoTime.java similarity index 97% rename from shanoir-ng-import/src/main/java/org/shanoir/ng/importer/model/EchoTime.java rename to shanoir-ng-ms-common/src/main/java/org/shanoir/ng/shared/dicom/EchoTime.java index 77ccda8b4c..07f891a94e 100644 --- a/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/model/EchoTime.java +++ b/shanoir-ng-ms-common/src/main/java/org/shanoir/ng/shared/dicom/EchoTime.java @@ -12,7 +12,7 @@ * along with this program. If not, see https://www.gnu.org/licenses/gpl-3.0.html */ -package org.shanoir.ng.importer.model; +package org.shanoir.ng.shared.dicom; public class EchoTime { @@ -20,12 +20,14 @@ public class EchoTime { * The echo number. Comes from dicom tag (0018,0086) VR=IS, VM=1-n Echo * Number(s). */ + private Integer echoNumber; /** * Comes from the dicom tag (0018,0081) VR=DS, VM=1 Echo Time. The unit of * measure must be in millisec. */ + private Double echoTime; public Integer getEchoNumber() { @@ -52,5 +54,4 @@ public int hashCode() { result = prime * result + echoTime.hashCode(); return result; } - } diff --git a/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dcm2nii/SerieToDatasetsSeparator.java b/shanoir-ng-ms-common/src/main/java/org/shanoir/ng/shared/dicom/SerieToDatasetsSeparator.java similarity index 95% rename from shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dcm2nii/SerieToDatasetsSeparator.java rename to shanoir-ng-ms-common/src/main/java/org/shanoir/ng/shared/dicom/SerieToDatasetsSeparator.java index 71d4e63063..04f705da6f 100644 --- a/shanoir-ng-import/src/main/java/org/shanoir/ng/importer/dcm2nii/SerieToDatasetsSeparator.java +++ b/shanoir-ng-ms-common/src/main/java/org/shanoir/ng/shared/dicom/SerieToDatasetsSeparator.java @@ -12,12 +12,12 @@ * along with this program. If not, see https://www.gnu.org/licenses/gpl-3.0.html */ -package org.shanoir.ng.importer.dcm2nii; +package org.shanoir.ng.shared.dicom; import java.util.Arrays; +import java.util.List; import java.util.Set; -import org.shanoir.ng.importer.model.EchoTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -60,6 +60,11 @@ public SerieToDatasetsSeparator(final int acquisitionNumber, final Set this.imageOrientationPatient = imageOrientationPatient; } + public SerieToDatasetsSeparator(String acquisitionNumber, List echoTimes, + List imageOrientationPatient) { + + } + /* * (non-Javadoc) * diff --git a/shanoir-ng-ms-common/src/main/java/org/shanoir/ng/shared/event/ShanoirEvent.java b/shanoir-ng-ms-common/src/main/java/org/shanoir/ng/shared/event/ShanoirEvent.java index 3dbb281ea1..f4d7211c70 100644 --- a/shanoir-ng-ms-common/src/main/java/org/shanoir/ng/shared/event/ShanoirEvent.java +++ b/shanoir-ng-ms-common/src/main/java/org/shanoir/ng/shared/event/ShanoirEvent.java @@ -18,6 +18,8 @@ public class ShanoirEvent { protected String message; + protected String report; + protected int status; protected Float progress; @@ -123,7 +125,16 @@ public String getMessage() { * @param message the message to set */ public void setMessage(String message) { - this.message = message; + this.message = message == null ? null : message.replaceAll("\uFFFD", "?"); + } + + public String getReport() { + return report; + } + + public void setReport(String report) { + //.replaceAll("[^a-zA-Z0-9]+", ""); + this.report = report == null ? null : report.replaceAll("\uFFFD", "?"); } /** @@ -176,4 +187,6 @@ public void setTimestamp(Long timestamp) { this.timestamp = timestamp; } + + } diff --git a/shanoir-ng-ms-common/src/main/java/org/shanoir/ng/shared/event/ShanoirEventService.java b/shanoir-ng-ms-common/src/main/java/org/shanoir/ng/shared/event/ShanoirEventService.java index 750d5121d9..e33d815c45 100644 --- a/shanoir-ng-ms-common/src/main/java/org/shanoir/ng/shared/event/ShanoirEventService.java +++ b/shanoir-ng-ms-common/src/main/java/org/shanoir/ng/shared/event/ShanoirEventService.java @@ -38,6 +38,7 @@ public void publishEvent(ShanoirEvent event) { .append("event_type=").append(event.getEventType()).append(";") .append("object_id=").append(event.getObjectId()).append(";") .append("message=").append(event.getMessage()).append(";") + .append("report=").append(event.getReport()).append(";") .append("status=").append(event.getStatus()).append(";") .append("progress=").append(event.getProgress()).append("]"); LOG.info(builder.toString()); diff --git a/shanoir-ng-ms-common/src/main/java/org/shanoir/ng/shared/event/ShanoirEventType.java b/shanoir-ng-ms-common/src/main/java/org/shanoir/ng/shared/event/ShanoirEventType.java index e20b34ada0..7e7dd9dc47 100644 --- a/shanoir-ng-ms-common/src/main/java/org/shanoir/ng/shared/event/ShanoirEventType.java +++ b/shanoir-ng-ms-common/src/main/java/org/shanoir/ng/shared/event/ShanoirEventType.java @@ -85,4 +85,6 @@ public class ShanoirEventType { /** User added to a study. */ public static final String USER_ADD_TO_STUDY_EVENT = "userAddToStudy.event"; + + public static final String CHECK_QUALITY_EVENT = "checkQuality.event"; } diff --git a/shanoir-ng-users/src/main/java/org/shanoir/ng/events/ShanoirEvent.java b/shanoir-ng-users/src/main/java/org/shanoir/ng/events/ShanoirEvent.java index 5c79ee1d44..a1ff62fb0b 100644 --- a/shanoir-ng-users/src/main/java/org/shanoir/ng/events/ShanoirEvent.java +++ b/shanoir-ng-users/src/main/java/org/shanoir/ng/events/ShanoirEvent.java @@ -1,207 +1,48 @@ package org.shanoir.ng.events; import java.sql.Types; -import java.util.Date; -import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.JdbcTypeCode; -import org.hibernate.annotations.UpdateTimestamp; -import jakarta.persistence.Column; import jakarta.persistence.Entity; -import jakarta.persistence.Id; import jakarta.persistence.Index; import jakarta.persistence.Table; -/** - * A shanoir event is an event allowing to keep some history during CRUD events of shanoir elements - * @author JComeD - * - */ @Entity @Table(name = "events", indexes = { - @Index(name = "i_user_type", columnList = "userId,eventType"), + @Index(name = "i_user_type", columnList = "userId, eventType"), } ) -public class ShanoirEvent { +public class ShanoirEvent extends ShanoirEventLight { - /** ID of the event, normally generated BEFORE arriving here **/ - @Id - protected Long id; - - /** See EventType **/ - protected String eventType; - - /** ID of the concerned object **/ - protected String objectId; - - /** user ID creating the event **/ - protected Long userId; - - /** Message of the event, can be informative, or display an error **/ @JdbcTypeCode(Types.LONGVARCHAR) - protected String message; - - /** Creation date, automatically generated **/ - @CreationTimestamp - @Column(updatable=false) - protected Date creationDate; - - /** Last update date, automatically generated **/ - @UpdateTimestamp - protected Date lastUpdate; - - /** Status, can be either 0 (created), 1 (success) or -1 (in error) **/ - protected int status; - - /** The progress of the event. */ - protected Float progress; + protected String report; - /** The study ID of the event */ - protected Long studyId; public ShanoirEvent() { // Default empty constructor for json deserializer. } /** - * @return the id - */ - public Long getId() { - return id; - } - - /** - * @param id the id to set - */ - public void setId(Long id) { - this.id = id; - } - - /** - * @return the eventType - */ - public String getEventType() { - return eventType; - } - - /** - * @param eventType the eventType to set - */ - public void setEventType(String eventType) { - this.eventType = eventType; - } - - /** - * @return the objectId - */ - public String getObjectId() { - return objectId; - } - - /** - * @param objectId the objectId to set - */ - public void setObjectId(String objectId) { - this.objectId = objectId; - } - - /** - * @return the userId + * @return the report */ - public Long getUserId() { - return userId; + public String getReport() { + return report; } /** - * @param userId the userId to set + * @param message the report to set */ - public void setUserId(Long userId) { - this.userId = userId; + public void setReport(String report) { + this.report = report; } - /** - * @return the message - */ - public String getMessage() { - return message; - } - - /** - * @param message the message to set - */ - public void setMessage(String message) { - this.message = message; - } - - /** - * @return the creationDate - */ - public Date getCreationDate() { - return creationDate; - } - - /** - * @param creationDate the creationDate to set - */ - public void setCreationDate(Date creationDate) { - this.creationDate = creationDate; - } - - /** - * @return the lastUpdate - */ - public Date getLastUpdate() { - return lastUpdate; - } - - /** - * @param lastUpdate the lastUpdate to set - */ - public void setLastUpdate(Date lastUpdate) { - this.lastUpdate = lastUpdate; - } - - /** - * @return the status - */ - public int getStatus() { - return status; - } - - /** - * @param status the status to set - */ - public void setStatus(int status) { - this.status = status; - } - - /** - * @return the progress - */ - public Float getProgress() { - return progress; - } - - /** - * @param progress the progress to set - */ - public void setProgress(Float progress) { - this.progress = progress; - } - - /** - * @return the studyId - */ - public Long getStudyId() { - return studyId; - } - - /** - * @param studyId the studyId to set - */ - public void setStudyId(Long studyId) { - this.studyId = studyId; + /** also modifies the current object by setting report to null */ + public ShanoirEventLight toLightEvent() { + ShanoirEventLight light = (ShanoirEventLight) this; + light.setHasReport(getReport() != null && !getReport().isEmpty()); + setReport(null); + return light; } } diff --git a/shanoir-ng-users/src/main/java/org/shanoir/ng/events/ShanoirEventLight.java b/shanoir-ng-users/src/main/java/org/shanoir/ng/events/ShanoirEventLight.java new file mode 100644 index 0000000000..ba5d04f5a3 --- /dev/null +++ b/shanoir-ng-users/src/main/java/org/shanoir/ng/events/ShanoirEventLight.java @@ -0,0 +1,216 @@ +package org.shanoir.ng.events; + +import java.sql.Types; +import java.util.Date; + +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.annotations.UpdateTimestamp; + +import jakarta.persistence.Column; +import jakarta.persistence.Id; +import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.Transient; + +/** + * A shanoir event is an event allowing to keep some history during CRUD events of shanoir elements + * @author JComeD + * + */ +@MappedSuperclass +public class ShanoirEventLight { + + /** ID of the event, normally generated BEFORE arriving here **/ + @Id + protected Long id; + + /** See EventType **/ + protected String eventType; + + /** ID of the concerned object **/ + protected String objectId; + + /** user ID creating the event **/ + protected Long userId; + + /** Message of the event, can be informative, or display an error **/ + @JdbcTypeCode(Types.LONGVARCHAR) + protected String message; + + /** Creation date, automatically generated **/ + @CreationTimestamp + @Column(updatable=false) + protected Date creationDate; + + /** Last update date, automatically generated **/ + @UpdateTimestamp + protected Date lastUpdate; + + /** Status, can be either 0 (created), 1 (success) or -1 (in error) **/ + protected int status; + + /** The progress of the event. */ + protected Float progress; + + /** The study ID of the event */ + protected Long studyId; + + @Transient + private Boolean hasReport; + + public ShanoirEventLight() { + // Default empty constructor for json deserializer. + } + + /** + * @return the id + */ + public Long getId() { + return id; + } + + /** + * @param id the id to set + */ + public void setId(Long id) { + this.id = id; + } + + /** + * @return the eventType + */ + public String getEventType() { + return eventType; + } + + /** + * @param eventType the eventType to set + */ + public void setEventType(String eventType) { + this.eventType = eventType; + } + + /** + * @return the objectId + */ + public String getObjectId() { + return objectId; + } + + /** + * @param objectId the objectId to set + */ + public void setObjectId(String objectId) { + this.objectId = objectId; + } + + /** + * @return the userId + */ + public Long getUserId() { + return userId; + } + + /** + * @param userId the userId to set + */ + public void setUserId(Long userId) { + this.userId = userId; + } + + /** + * @return the message + */ + public String getMessage() { + return message; + } + + /** + * @param message the message to set + */ + public void setMessage(String message) { + this.message = message; + } + + /** + * @return the creationDate + */ + public Date getCreationDate() { + return creationDate; + } + + /** + * @param creationDate the creationDate to set + */ + public void setCreationDate(Date creationDate) { + this.creationDate = creationDate; + } + + /** + * @return the lastUpdate + */ + public Date getLastUpdate() { + return lastUpdate; + } + + /** + * @param lastUpdate the lastUpdate to set + */ + public void setLastUpdate(Date lastUpdate) { + this.lastUpdate = lastUpdate; + } + + /** + * @return the status + */ + public int getStatus() { + return status; + } + + /** + * @param status the status to set + */ + public void setStatus(int status) { + this.status = status; + } + + /** + * @return the progress + */ + public Float getProgress() { + return progress; + } + + /** + * @param progress the progress to set + */ + public void setProgress(Float progress) { + this.progress = progress; + } + + /** + * @return the studyId + */ + public Long getStudyId() { + return studyId; + } + + /** + * @param studyId the studyId to set + */ + public void setStudyId(Long studyId) { + this.studyId = studyId; + } + + public Boolean getHasReport() { + return hasReport; + } + + public void setHasReport(Boolean hasReport) { + this.hasReport = hasReport; + } + + public String getIdAsString() { + return id == null ? null : id.toString(); + } +} diff --git a/shanoir-ng-users/src/main/java/org/shanoir/ng/events/ShanoirEventRepository.java b/shanoir-ng-users/src/main/java/org/shanoir/ng/events/ShanoirEventRepository.java index 839fd76b79..47d0bc498f 100644 --- a/shanoir-ng-users/src/main/java/org/shanoir/ng/events/ShanoirEventRepository.java +++ b/shanoir-ng-users/src/main/java/org/shanoir/ng/events/ShanoirEventRepository.java @@ -16,11 +16,13 @@ public interface ShanoirEventRepository extends CrudRepository findByUserIdAndEventType(Long userId, String eventType); + List findByUserIdAndEventTypeIn(Long userId, List eventType); /** * Deletes all events older than a date. * @param expiryDate the expiration date. */ public void deleteByLastUpdateBefore(Date expiryDate); + + ShanoirEvent findByIdAndUserId(Long taskId, long userId); } diff --git a/shanoir-ng-users/src/main/java/org/shanoir/ng/events/ShanoirEventsService.java b/shanoir-ng-users/src/main/java/org/shanoir/ng/events/ShanoirEventsService.java index 8d43f9857c..411ff5f293 100644 --- a/shanoir-ng-users/src/main/java/org/shanoir/ng/events/ShanoirEventsService.java +++ b/shanoir-ng-users/src/main/java/org/shanoir/ng/events/ShanoirEventsService.java @@ -8,6 +8,7 @@ import org.apache.commons.lang3.time.DateUtils; import org.shanoir.ng.shared.event.ShanoirEventType; import org.shanoir.ng.tasks.AsyncTaskApiController; +import org.shanoir.ng.utils.KeycloakUtil; import org.shanoir.ng.utils.Utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -36,13 +37,23 @@ public void addEvent(ShanoirEvent event) { repository.save(event); // Push notification to UI - if (ShanoirEventType.IMPORT_DATASET_EVENT.equals(event.getEventType())) { + if (ShanoirEventType.IMPORT_DATASET_EVENT.equals(event.getEventType()) + || ShanoirEventType.CHECK_QUALITY_EVENT.equals(event.getEventType())) { + sendSseEventsToUI(event); } } - public List getEventsByUserAndType(Long userId, String eventType) { - return Utils.toList(repository.findByUserIdAndEventType(userId, eventType)); + public List getEventsByUserAndType(Long userId, String... eventType) { + List list = new ArrayList(); + for (String type : eventType) { + list.add(type); + } + List events = new ArrayList<>(); + for (ShanoirEvent event : Utils.toList(repository.findByUserIdAndEventTypeIn(userId, list))) { + events.add(event.toLightEvent()); + } + return events; } /** @@ -92,4 +103,9 @@ private void keepConnectionAlive( ) { }); AsyncTaskApiController.emitters.removeAll(sseEmitterListToRemove); } + + public ShanoirEvent findById(Long taskId) { + Long userId = KeycloakUtil.getTokenUserId(); + return repository.findByIdAndUserId(taskId, userId); + } } diff --git a/shanoir-ng-users/src/main/java/org/shanoir/ng/tasks/AsyncTaskApi.java b/shanoir-ng-users/src/main/java/org/shanoir/ng/tasks/AsyncTaskApi.java index 761553c3c6..62846a631f 100644 --- a/shanoir-ng-users/src/main/java/org/shanoir/ng/tasks/AsyncTaskApi.java +++ b/shanoir-ng-users/src/main/java/org/shanoir/ng/tasks/AsyncTaskApi.java @@ -4,15 +4,18 @@ import java.util.List; import org.shanoir.ng.events.ShanoirEvent; +import org.shanoir.ng.events.ShanoirEventLight; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; @@ -34,7 +37,17 @@ public interface AsyncTaskApi { @ApiResponse(responseCode = "500", description = "unexpected error") }) @GetMapping(value = "", produces = { "application/json" }) @PreAuthorize("hasAnyRole('ADMIN', 'EXPERT', 'USER')") - ResponseEntity> findTasks(); + ResponseEntity> findTasks(); + + @Operation(summary = "", description = "If exists, returns the requested task") + @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "found tasks"), + @ApiResponse(responseCode = "401", description = "unauthorized"), + @ApiResponse(responseCode = "403", description = "forbidden"), + @ApiResponse(responseCode = "404", description = "no task found"), + @ApiResponse(responseCode = "500", description = "unexpected error") }) + @GetMapping(value = "/{taskId}", produces = { "application/json" }) + @PreAuthorize("hasAnyRole('ADMIN', 'EXPERT', 'USER')") + ResponseEntity getTaskDetails(@Parameter(name = "id of the task", required = true) @PathVariable("taskId") Long taskId); @Operation(summary = "", description = "Pushes a new event emitter to front") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "found tasks"), diff --git a/shanoir-ng-users/src/main/java/org/shanoir/ng/tasks/AsyncTaskApiController.java b/shanoir-ng-users/src/main/java/org/shanoir/ng/tasks/AsyncTaskApiController.java index 2f1a1337c0..cf06f05c16 100644 --- a/shanoir-ng-users/src/main/java/org/shanoir/ng/tasks/AsyncTaskApiController.java +++ b/shanoir-ng-users/src/main/java/org/shanoir/ng/tasks/AsyncTaskApiController.java @@ -10,6 +10,7 @@ import org.apache.commons.lang3.time.DateUtils; import org.shanoir.ng.events.ShanoirEvent; +import org.shanoir.ng.events.ShanoirEventLight; import org.shanoir.ng.events.ShanoirEventsService; import org.shanoir.ng.shared.event.ShanoirEventType; import org.shanoir.ng.utils.KeycloakUtil; @@ -17,8 +18,11 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; +import io.swagger.v3.oas.annotations.Parameter; + /** * API to manage asynchronous tasks: * - Retrieve a list of tasks for a user @@ -34,19 +38,19 @@ public class AsyncTaskApiController implements AsyncTaskApi { public static final List emitters = Collections.synchronizedList(new ArrayList<>()); @Override - public ResponseEntity> findTasks() { + public ResponseEntity> findTasks() { Long userId = KeycloakUtil.getTokenUserId(); - List taskList = taskService.getEventsByUserAndType(userId, ShanoirEventType.IMPORT_DATASET_EVENT); - + List taskList = taskService.getEventsByUserAndType(userId, ShanoirEventType.IMPORT_DATASET_EVENT, ShanoirEventType.CHECK_QUALITY_EVENT); + // Get only event with last updates < 7 days Date now = new Date(); Long nowMinusSevenDays = now.getTime() - 7 * DateUtils.MILLIS_PER_DAY; taskList = taskList.stream().filter(event -> event.getLastUpdate().getTime() > nowMinusSevenDays).collect(Collectors.toList()); // Order by last update date - Comparator comparator = new Comparator() { + Comparator comparator = new Comparator() { @Override - public int compare(ShanoirEvent event1, ShanoirEvent event2) { + public int compare(ShanoirEventLight event1, ShanoirEventLight event2) { return event1.getLastUpdate().before(event2.getLastUpdate()) ? 1 : -1; } }; @@ -55,6 +59,17 @@ public int compare(ShanoirEvent event1, ShanoirEvent event2) { return new ResponseEntity<>(taskList, HttpStatus.OK); } + @Override + public ResponseEntity getTaskDetails( + @Parameter(name = "id of the task", required = true) @PathVariable("taskId") Long taskId) { + ShanoirEvent event = taskService.findById(taskId); + if (event == null) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } else { + return new ResponseEntity<>(event, HttpStatus.OK); + } + } + @Override public ResponseEntity updateTasks() throws IOException { SseEmitter emitter = new SseEmitter(-1L);