diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyController.java b/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyController.java new file mode 100644 index 0000000000000000000000000000000000000000..35767b0942d1937c513f5829b597f707d589ae3a --- /dev/null +++ b/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyController.java @@ -0,0 +1,137 @@ +package fr.inra.urgi.faidare.web.study; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import com.google.common.collect.Lists; +import fr.inra.urgi.faidare.api.NotFoundException; +import fr.inra.urgi.faidare.config.FaidareProperties; +import fr.inra.urgi.faidare.domain.criteria.GermplasmPOSTSearchCriteria; +import fr.inra.urgi.faidare.domain.data.TrialVO; +import fr.inra.urgi.faidare.domain.data.germplasm.GermplasmVO; +import fr.inra.urgi.faidare.domain.data.study.StudyDetailVO; +import fr.inra.urgi.faidare.domain.data.variable.ObservationVariableVO; +import fr.inra.urgi.faidare.domain.xref.XRefDocumentVO; +import fr.inra.urgi.faidare.repository.es.GermplasmRepository; +import fr.inra.urgi.faidare.repository.es.StudyRepository; +import fr.inra.urgi.faidare.repository.es.TrialRepository; +import fr.inra.urgi.faidare.repository.es.XRefDocumentRepository; +import fr.inra.urgi.faidare.repository.file.CropOntologyRepository; +import org.springframework.stereotype.Controller; +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.ModelAndView; + +/** + * Controller used to display a study card based on its ID. + * @author JB Nizet + */ +@Controller("webStudyController") +@RequestMapping("/studies") +public class StudyController { + + private final StudyRepository studyRepository; + private final FaidareProperties faidareProperties; + private final XRefDocumentRepository xRefDocumentRepository; + private final GermplasmRepository germplasmRepository; + private final CropOntologyRepository cropOntologyRepository; + private final TrialRepository trialRepository; + + public StudyController(StudyRepository studyRepository, + FaidareProperties faidareProperties, + XRefDocumentRepository xRefDocumentRepository, + GermplasmRepository germplasmRepository, + CropOntologyRepository cropOntologyRepository, + TrialRepository trialRepository) { + this.studyRepository = studyRepository; + this.faidareProperties = faidareProperties; + this.xRefDocumentRepository = xRefDocumentRepository; + this.germplasmRepository = germplasmRepository; + this.cropOntologyRepository = cropOntologyRepository; + this.trialRepository = trialRepository; + } + + @GetMapping("/{studyId}") + public ModelAndView site(@PathVariable("studyId") String studyId) { + StudyDetailVO study = studyRepository.getById(studyId); + + // List<XRefDocumentVO> crossReferences = xRefDocumentRepository.find( + // XRefDocumentSearchCriteria.forXRefId(site.getLocationDbId())); + List<XRefDocumentVO> crossReferences = Arrays.asList( + createXref("foobar"), + createXref("bazbing") + ); + + // LocationVO site = createSite(); + + if (study == null) { + throw new NotFoundException("Study with ID " + studyId + " not found"); + } + + List<GermplasmVO> germplasms = getGermplasms(study); + List<ObservationVariableVO>variables = getVariables(study); + List<TrialVO> trials = getTrials(study); + + return new ModelAndView("study", + "model", + new StudyModel( + study, + faidareProperties.getByUri(study.getSourceUri()), + germplasms, + variables, + trials, + crossReferences + ) + ); + } + + private List<GermplasmVO> getGermplasms(StudyDetailVO study) { + if (study.getGermplasmDbIds() == null || study.getGermplasmDbIds().isEmpty()) { + return Collections.emptyList(); + } else { + GermplasmPOSTSearchCriteria germplasmCriteria = new GermplasmPOSTSearchCriteria(); + germplasmCriteria.setGermplasmDbIds(Lists.newArrayList(study.getGermplasmDbIds())); + return germplasmRepository.find(germplasmCriteria) + .stream() + .sorted(Comparator.comparing(GermplasmVO::getGermplasmName)) + .collect(Collectors.toList()); + } + } + + private List<ObservationVariableVO> getVariables(StudyDetailVO study) { + Set<String> variableIds = studyRepository.getVariableIds(study.getStudyDbId()); + return cropOntologyRepository.getVariableByIds(variableIds) + .stream() + .sorted(Comparator.comparing(ObservationVariableVO::getObservationVariableDbId)) + .collect(Collectors.toList()); + } + + private List<TrialVO> getTrials(StudyDetailVO study) { + if (study.getTrialDbIds() == null || study.getTrialDbIds().isEmpty()) { + return Collections.emptyList(); + } + + return study.getTrialDbIds() + .stream() + .sorted(Comparator.naturalOrder()) + .map(trialRepository::getById) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + private XRefDocumentVO createXref(String name) { + XRefDocumentVO xref = new XRefDocumentVO(); + xref.setName(name); + xref.setDescription("A very large description for the xref " + name + " which has way more than 120 characters bla bla bla bla bla bla bla bla bla bla bla bla"); + xref.setDatabaseName("db_" + name); + xref.setUrl("https://google.com"); + xref.setEntryType("type " + name); + return xref; + } +} diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyModel.java b/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyModel.java new file mode 100644 index 0000000000000000000000000000000000000000..980c78d43d3dcae233c2371de2f80add8f11d421 --- /dev/null +++ b/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyModel.java @@ -0,0 +1,82 @@ +package fr.inra.urgi.faidare.web.study; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import fr.inra.urgi.faidare.domain.data.LocationVO; +import fr.inra.urgi.faidare.domain.data.TrialVO; +import fr.inra.urgi.faidare.domain.data.germplasm.GermplasmVO; +import fr.inra.urgi.faidare.domain.data.study.StudyDetailVO; +import fr.inra.urgi.faidare.domain.data.variable.ObservationVariableVO; +import fr.inra.urgi.faidare.domain.datadiscovery.data.DataSource; +import fr.inra.urgi.faidare.domain.xref.XRefDocumentVO; + +/** + * The model used by the study page + * @author JB Nizet + */ +public final class StudyModel { + private final StudyDetailVO study; + private final DataSource source; + private final List<GermplasmVO> germplasms; + private final List<ObservationVariableVO> variables; + private final List<TrialVO> trials; + private final List<XRefDocumentVO> crossReferences; + private final List<Map.Entry<String, Object>> additionalInfoProperties; + + public StudyModel(StudyDetailVO study, + DataSource source, + List<GermplasmVO> germplasms, + List<ObservationVariableVO> variables, + List<TrialVO> trials, + List<XRefDocumentVO> crossReferences) { + this.study = study; + this.source = source; + this.germplasms = germplasms; + this.variables = variables; + this.trials = trials; + this.crossReferences = crossReferences; + + Map<String, Object> additionalInfo = + study.getAdditionalInfo() == null ? Collections.emptyMap() : study.getAdditionalInfo().getProperties(); + this.additionalInfoProperties = + additionalInfo.entrySet() + .stream() + .filter(entry -> entry.getValue() != null && !entry.getValue().toString().isEmpty()) + .sorted(Map.Entry.comparingByKey()) + .collect(Collectors.toList()); + } + + public StudyDetailVO getStudy() { + return study; + } + + public DataSource getSource() { + return source; + } + + public List<XRefDocumentVO> getCrossReferences() { + return crossReferences; + } + + public List<GermplasmVO> getGermplasms() { + return germplasms; + } + + public List<ObservationVariableVO> getVariables() { + return variables; + } + + public List<TrialVO> getTrials() { + return trials; + } + + public List<Map.Entry<String, Object>> getAdditionalInfoProperties() { + return additionalInfoProperties; + } +} diff --git a/backend/src/main/resources/templates/study.html b/backend/src/main/resources/templates/study.html new file mode 100644 index 0000000000000000000000000000000000000000..a1cafc653a5706954e1a30249a63f68ce538abae --- /dev/null +++ b/backend/src/main/resources/templates/study.html @@ -0,0 +1,206 @@ +<!DOCTYPE html> + +<html + xmlns:th="http://www.thymeleaf.org" + th:replace="~{layout/main :: layout(title=~{::title}, content=~{::main})}" +> +<head> + <title>Study <th:block th:text="${model.study.studyType}" />: <th:block th:text="${model.study.studyName}" /></title> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> +</head> + +<body> +<main> + <h1>Study <th:block th:text="${model.study.studyType}" />: <th:block th:text="${model.study.studyName}" /></h1> + + <h2>Identification</h2> + + <div th:replace="fragments/row::text-row(label='Name', text=${model.study.studyName})"></div> + <div th:replace="fragments/row::text-row(label='Identifier', text=${model.study.studyDbId})"></div> + + <th:block th:if="${model.source != null}"> + <div th:replace="fragments/row::row(label='Source', content=~{::#source}, text='')"> + <a id="source" target="_blank" th:href="${model.source.url}"> + <img style="max-height: 60px;" th:src="${model.source.image}" th:alt="${model.source.name} + ' logo'" /> + </a> + </div> + </th:block> + + <th:block th:if="${model.study.url != null && model.source != null}"> + <div th:replace="fragments/row::row(label='Data link', content=~{::#url}, text='')"> + <a id="url" target="_blank" th:href="${model.study.url}"> + Link to this study on <th:block th:text="${model.source.name}" /> + </a> + </div> + </th:block> + + <div th:replace="fragments/row::text-row(label='Project name', text=${model.study.programName})"></div> + <div th:replace="fragments/row::text-row(label='Description', text=${model.study.studyDescription})"></div> + <th:block th:if="${model.study.active != null}"> + <div th:replace="fragments/row::text-row(label='Active', text=${model.study.active ? 'Yes' : 'No'})"></div> + </th:block> + + <th:block th:unless="${#lists.isEmpty(model.study.seasons)}"> + <div th:replace="fragments/row::text-row(label='Seasons', text=${#strings.listJoin(model.study.seasons, ',')})"></div> + </th:block> + <th:block th:if="${model.study.startDate != null && model.study.endDate != null}"> + <div th:replace="fragments/row::text-row(label='Date', text=${'From ' + #dates.format(model.study.startDate, 'yyyy-MM-dd') + ' to ' + #dates.format(model.study.endDate, 'yyyy-MM-dd') })"></div> + </th:block> + <th:block th:if="${model.study.startDate != null && model.study.endDate == null}"> + <div th:replace="fragments/row::text-row(label='Date', text=${'Started on ' + #dates.format(model.study.startDate, 'yyyy-MM-dd')})"></div> + </th:block> + + <th:block th:if="${model.study.locationDbId}"> + <div th:replace="fragments/row::row(label='Location name', content=~{::#location})"> + <a id="location" th:href="@{/sites/{siteId}(siteId=${model.study.locationDbId})}" th:text="${model.study.locationName}"></a> + </div> + </th:block> + + <th:block th:unless="${#lists.isEmpty(model.study.dataLinks)}"> + <div th:replace="fragments/row::row(label='Data files', content=~{::#data-files})"> + <ul id="data-files" class="list-unstyled"> + <li th:each="dataLink : ${model.study.dataLinks}"> + <a target="_blank" th:href="${dataLink.url}" th:text="${dataLink.name}"></a> + </li> + </ul> + </div> + </th:block> + + <th:block th:unles="${#lists.isEmpty(model.germplasms)}"> + <h2>Genotype</h2> + + <div class="table-responsive scroll-table table-card-body"> + <div class="card"> + <table class="table table-sm table-striped"> + <thead> + <tr> + <th scope="col">Accession number</th> + <th scope="col">Name</th> + <th scope="col">Taxon</th> + </tr> + </thead> + <tbody> + <tr th:each="row : ${model.germplasms}"> + <td> + <a th:href="@{/germplasms/{germplasmId}(germplasmId=${row.germplasmDbId})}" th:text="${row.accessionNumber}"></a> + </td> + <td th:text="${row.germplasmName}"></td> + <td th:text="${(row.genus == null ? '' : row.genus) + ' ' + (row.species == null ? '' : row.species)+ ' ' + (row.subtaxa == null ? '' : row.subtaxa) }"></td> + </tr> + </tbody> + </table> + </div> + </div> + </th:block> + + <th:block th:unless="${#lists.isEmpty(model.variables)}"> + <h2>Variables</h2> + <div class="table-responsive scroll-table table-card-body"> + <div class="card"> + <table class="table table-sm table-striped"> + <thead> + <tr> + <th scope="col">Variable ID</th> + <th scope="col">Variable short name</th> + <th scope="col">Variable long name</th> + <th scope="col">Ontology name</th> + <th scope="col">Trait description</th> + </tr> + </thead> + <tbody> + <tr th:each="row : ${model.variables}"> + <td> + <a th:unless="${#strings.isEmpty(row.documentationURL)}" th:href="${row.documentationURL}" th:text="${row.observationVariableDbId}" target="_blank" ></a> + <span th:if="${#strings.isEmpty(row.documentationURL)}" th:text="${row.observationVariableDbId}"></span> + </td> + <td th:text="${row.name}"></td> + <td th:text="${#lists.isEmpty(row.synonyms) ? '' : row.synonyms[0]}"></td> + <td th:text="${row.ontologyName}"></td> + <td th:text="${row.trait.description}"></td> + </tr> + </tbody> + </table> + </div> + </div> + </th:block> + + <th:block th:unless="${#lists.isEmpty(model.trials)}"> + <h2>Data Set</h2> + <div class="table-responsive scroll-big-table table-card-body"> + <div class="card"> + <table class="table table-sm table-striped"> + <thead> + <tr> + <th scope="col">Name</th> + <th scope="col">Type</th> + <th scope="col">Linked studies identifier</th> + </tr> + </thead> + <tbody> + <tr th:each="row : ${model.trials}"> + <td> + <a th:unless="${#strings.isEmpty(row.documentationURL)}" th:href="${row.documentationURL}" th:text="${row.trialName}" target="_blank" ></a> + <span th:if="${#strings.isEmpty(row.documentationURL)}" th:text="${row.trialName}"></span> + </td> + <td th:text="${row.trialType}"></td> + <td style="width: 60%"> + <th:block th:each="trialStudy, iterStat : ${row.studies}" + th:if="${trialStudy.studyDbId != model.study.studyDbId}"> + <a th:href="@{/studies/{studyId}(studyId=${trialStudy.studyDbId})}" + th:text="${trialStudy.studyName.trim()}"> + </a><th:block th:if="${iterStat.last}">; </th:block> + </th:block> + </td> + </tr> + </tbody> + </table> + </div> + </div> + </th:block> + + <th:block th:unless="${#lists.isEmpty(model.study.contacts)}"> + <h2>Contact</h2> + <div class="table-responsive scroll-table table-card-body"> + <div class="card"> + <table class="table table-sm table-striped"> + <thead> + <tr> + <th scope="col">Role</th> + <th scope="col">Name</th> + <th scope="col">Email</th> + <th scope="col">Institution</th> + </tr> + </thead> + <tbody> + <tr th:each="row : ${model.study.contacts}"> + <td th:text="${row.type}"></td> + <td th:text="${row.name}"></td> + <td th:text="${row.email}"></td> + <td th:text="${row.institutionName}"></td> + </tr> + </tbody> + </table> + </div> + </div> + </th:block> + + <th:block th:unless="${#lists.isEmpty(model.additionalInfoProperties)}"> + <h2>Additional information</h2> + <div class="table-responsive scroll-table table-card-body"> + <div class="card"> + <table class="table table-sm table-striped"> + <tbody> + <tr th:each="row : ${model.additionalInfoProperties}"> + <td style="width: 50%;" th:text="${row.key}"></td> + <td th:text="${row.value}"></td> + </tr> + </tbody> + </table> + </div> + </div> + </th:block> + + <div th:replace="fragments/xrefs::xrefs(crossReferences=${model.crossReferences})"></div> +</main> +</body> +</html>