diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmController.java b/backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmController.java new file mode 100644 index 0000000000000000000000000000000000000000..cf3430490eb3b9cba9803c41ab775b7a2558ce14 --- /dev/null +++ b/backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmController.java @@ -0,0 +1,392 @@ +package fr.inra.urgi.faidare.web.germplasm; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +import fr.inra.urgi.faidare.api.NotFoundException; +import fr.inra.urgi.faidare.config.FaidareProperties; +import fr.inra.urgi.faidare.domain.brapi.v1.data.BrapiGermplasmAttributeValue; +import fr.inra.urgi.faidare.domain.brapi.v1.data.BrapiSibling; +import fr.inra.urgi.faidare.domain.criteria.GermplasmAttributeCriteria; +import fr.inra.urgi.faidare.domain.criteria.GermplasmGETSearchCriteria; +import fr.inra.urgi.faidare.domain.data.germplasm.CollPopVO; +import fr.inra.urgi.faidare.domain.data.germplasm.DonorVO; +import fr.inra.urgi.faidare.domain.data.germplasm.GenealogyVO; +import fr.inra.urgi.faidare.domain.data.germplasm.GermplasmAttributeValueVO; +import fr.inra.urgi.faidare.domain.data.germplasm.GermplasmInstituteVO; +import fr.inra.urgi.faidare.domain.data.germplasm.GermplasmVO; +import fr.inra.urgi.faidare.domain.data.germplasm.InstituteVO; +import fr.inra.urgi.faidare.domain.data.germplasm.PedigreeVO; +import fr.inra.urgi.faidare.domain.data.germplasm.PhotoVO; +import fr.inra.urgi.faidare.domain.data.germplasm.PuiNameValueVO; +import fr.inra.urgi.faidare.domain.data.germplasm.SiblingVO; +import fr.inra.urgi.faidare.domain.data.germplasm.SimpleVO; +import fr.inra.urgi.faidare.domain.data.germplasm.SiteVO; +import fr.inra.urgi.faidare.domain.data.germplasm.TaxonSourceVO; +import fr.inra.urgi.faidare.domain.xref.XRefDocumentVO; +import fr.inra.urgi.faidare.repository.es.GermplasmAttributeRepository; +import fr.inra.urgi.faidare.repository.es.GermplasmRepository; +import fr.inra.urgi.faidare.repository.es.XRefDocumentRepository; +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.bind.annotation.RequestParam; +import org.springframework.web.servlet.ModelAndView; + +/** + * Controller used to display a germplasm card based on its ID. + * @author JB Nizet + */ +@Controller("webGermplasmController") +@RequestMapping("/germplasms") +public class GermplasmController { + + private final GermplasmRepository germplasmRepository; + private final FaidareProperties faidareProperties; + private final XRefDocumentRepository xRefDocumentRepository; + private GermplasmAttributeRepository germplasmAttributeRepository; + + public GermplasmController(GermplasmRepository germplasmRepository, + FaidareProperties faidareProperties, + XRefDocumentRepository xRefDocumentRepository, + GermplasmAttributeRepository germplasmAttributeRepository) { + this.germplasmRepository = germplasmRepository; + this.faidareProperties = faidareProperties; + this.xRefDocumentRepository = xRefDocumentRepository; + this.germplasmAttributeRepository = germplasmAttributeRepository; + } + + @GetMapping("/{germplasmId}") + public ModelAndView get(@PathVariable("germplasmId") String germplasmId) { + // GermplasmVO germplasm = germplasmRepository.getById(germplasmId); + + // TODO replace this block by the above commented one + GermplasmVO germplasm = createGermplasm(); + + if (germplasm == null) { + throw new NotFoundException("Germplasm with ID " + germplasmId + " not found"); + } + + return toModelAndView(germplasm); + } + + @GetMapping(params = "pui") + public ModelAndView getByPui(@RequestParam("pui") String pui) { + GermplasmGETSearchCriteria criteria = new GermplasmGETSearchCriteria(); + criteria.setGermplasmPUI(Collections.singletonList(pui)); + List<GermplasmVO> germplasms = germplasmRepository.find(criteria); + if (germplasms.size() != 1) { + throw new NotFoundException("Germplasm with PUI " + pui + " not found"); + } + + return toModelAndView(germplasms.get(0)); + } + + private ModelAndView toModelAndView(GermplasmVO germplasm) { + // List<BrapiGermplasmAttributeValue> attributes = getAttributes(germplasm); + // List<XRefDocumentVO> crossReferences = xRefDocumentRepository.find( + // XRefDocumentSearchCriteria.forXRefId(site.getLocationDbId())); + // PedigreeVO pedigree = getPedigree(germplasm); + // List<XRefDocumentVO> crossReferences = xRefDocumentRepository.find( + // XRefDocumentSearchCriteria.forXRefId(germplasm.getGermplasmDbId()) + // ); + + // TODO replace this block by the above commented one + List<BrapiGermplasmAttributeValue> attributes = Arrays.asList( + createAttribute() + ); + PedigreeVO pedigree = createPedigree(); + List<XRefDocumentVO> crossReferences = Arrays.asList( + createXref("foobar"), + createXref("bazbing") + ); + + + sortDonors(germplasm); + sortPopulations(germplasm); + sortCollections(germplasm); + sortPanels(germplasm); + return new ModelAndView("germplasm", + "model", + new GermplasmModel( + germplasm, + faidareProperties.getByUri(germplasm.getSourceUri()), + attributes, + pedigree, + crossReferences + ) + ); + } + + private void sortPopulations(GermplasmVO germplasm) { + if (germplasm.getPopulation() != null) { + germplasm.setPopulation(germplasm.getPopulation() + .stream() + .sorted(Comparator.comparing( + CollPopVO::getName)) + .collect(Collectors.toList())); + } + } + + private void sortCollections(GermplasmVO germplasm) { + if (germplasm.getCollection() != null) { + germplasm.setCollection(germplasm.getCollection() + .stream() + .sorted(Comparator.comparing(CollPopVO::getName)) + .collect(Collectors.toList())); + } + } + + private void sortPanels(GermplasmVO germplasm) { + if (germplasm.getPanel() != null) { + germplasm.setPanel(germplasm.getPanel() + .stream() + .sorted(Comparator.comparing(CollPopVO::getName)) + .collect(Collectors.toList())); + } + } + + private void sortDonors(GermplasmVO germplasm) { + if (germplasm.getDonors() != null) { + germplasm.setDonors(germplasm.getDonors() + .stream() + .sorted(Comparator.comparing(donor -> donor.getDonorInstitute() + .getInstituteName())) + .collect(Collectors.toList())); + } + } + + private List<BrapiGermplasmAttributeValue> getAttributes(GermplasmVO germplasm) { + GermplasmAttributeCriteria criteria = new GermplasmAttributeCriteria(); + criteria.setGermplasmDbId(germplasm.getGermplasmDbId()); + return germplasmAttributeRepository.find(criteria) + .stream() + .flatMap(vo -> vo.getData().stream()) + .sorted(Comparator.comparing(BrapiGermplasmAttributeValue::getAttributeName)) + .collect(Collectors.toList()); + } + + private PedigreeVO getPedigree(GermplasmVO germplasm) { + return germplasmRepository.findPedigree(germplasm.getGermplasmDbId()); + } + + private BrapiGermplasmAttributeValue createAttribute() { + GermplasmAttributeValueVO result = new GermplasmAttributeValueVO(); + result.setAttributeName("A1"); + result.setValue("V1"); + return result; + } + + private GermplasmVO createGermplasm() { + GermplasmVO result = new GermplasmVO(); + + result.setGermplasmName("BLE BARBU DU ROUSSILLON"); + result.setAccessionNumber("1408"); + result.setSynonyms(Arrays.asList("BLE DU ROUSSILLON", "FRA051:1699", "ROUSSILLON")); + PhotoVO photo = new PhotoVO(); + photo.setPhotoName("Blé du roussillon"); + photo.setCopyright("INRA, Emmanuelle BOULAT/Lionel BARDY 2012"); + photo.setThumbnailFile("https://urgi.versailles.inrae.fr/files/siregal/images/accession/CEREALS/thumbnails/thumb_1408_R09_S.jpg"); + photo.setFile("https://urgi.versailles.inrae.fr/files/siregal/images/accession/CEREALS/1408_R09_S.jpg"); + result.setPhoto(photo); + + InstituteVO holdingGenBank = new InstituteVO(); + holdingGenBank.setLogo("https://urgi.versailles.inra.fr/files/siregal/images/grc/inra_brc_en.png"); + holdingGenBank.setInstituteName("INRA BRC"); + holdingGenBank.setWebSite("http://google.fr"); + result.setHoldingGenbank(holdingGenBank); + + result.setBiologicalStatusOfAccessionCode("Traditional cultivar/landrace "); + result.setPedigree("LV"); + SiteVO originSite = new SiteVO(); + originSite.setSiteId("1234"); + originSite.setSiteName("Le Moulon"); + result.setOriginSite(originSite); + + result.setGenus("Genus 1"); + result.setSpecies("Species 1"); + result.setSpeciesAuthority("Species Auth"); + result.setSourceUri("https://urgi.versailles.inrae.fr/gnpis"); + result.setSubtaxa("Subtaxa 1"); + result.setGenusSpeciesSubtaxa("Triticum aestivum subsp. aestivum"); + result.setSubtaxaAuthority("INRAE"); + result.setTaxonIds(Arrays.asList(createTaxonId(), createTaxonId())); + result.setTaxonComment("C'est bon le blé"); + result.setTaxonCommonNames(Arrays.asList("Blé tendre", "Bread wheat", "Soft wheat")); + result.setTaxonSynonyms(Arrays.asList("Blé tendre1", "Bread wheat1", "Soft wheat1")); + + InstituteVO holdingInstitute = new InstituteVO(); + holdingInstitute.setInstituteName("GDEC - UMR Génétique, Diversité et Ecophysiologie des Céréales"); + holdingInstitute.setLogo("https://urgi.versailles.inra.fr/files/siregal/images/grc/inra_brc_en.png"); + holdingInstitute.setWebSite("https://google.fr/q=qsdqsdqsdslqlsdnqlsdqlsdlqskdlqdqlsdqsdqsdqd"); + holdingInstitute.setInstituteCode("GDEC"); + holdingInstitute.setInstituteType("Type1"); + holdingInstitute.setAcronym("G.D.E.C"); + holdingInstitute.setAddress("Lyon"); + holdingInstitute.setOrganisation("SAS"); + result.setHoldingInstitute(holdingInstitute); + + result.setPresenceStatus("Maintained"); + + GermplasmInstituteVO collector = new GermplasmInstituteVO(); + collector.setMaterialType("Fork"); + collector.setCollectors("Joe, Jack, William, Averell"); + InstituteVO collectingInstitute = new InstituteVO(); + collectingInstitute.setInstituteName("Ninja Squad"); + collector.setInstitute(collectingInstitute); + collector.setAccessionNumber("567"); + result.setCollector(collector); + + result.setCollectingSite(originSite); + result.setAcquisitionDate("In the summer"); + + GermplasmInstituteVO breeder = new GermplasmInstituteVO(); + InstituteVO breedingInstitute = new InstituteVO(); + breedingInstitute.setInstituteName("Microsoft"); + breeder.setInstitute(breedingInstitute); + breeder.setAccessionCreationDate(2015); + breeder.setAccessionNumber("678"); + breeder.setRegistrationYear(2016); + breeder.setDeregistrationYear(2019); + result.setBreeder(breeder); + + result.setDonors(Arrays.asList( + createDonor() + )); + + result.setDistributors(Arrays.asList( + createDistributor() + )); + + result.setChildren(Arrays.asList(createChild(), createChild())); + + result.setGermplasmPUI("germplasmPUI"); + result.setPopulation(Arrays.asList(createPopulation1(), createPopulation2(), createPopulation3())); + + result.setCollection(Arrays.asList(createCollection())); + + result.setPanel(Arrays.asList(createPanel())); + + return result; + } + + private DonorVO createDonor() { + DonorVO result = new DonorVO(); + result.setDonorGermplasmPUI("PUI1"); + result.setDonationDate(2017); + result.setDonorAccessionNumber("3456"); + result.setDonorInstituteCode("GD46U"); + InstituteVO institute = new InstituteVO(); + institute.setInstituteName("Hello"); + result.setDonorInstitute(institute); + return result; + } + + private GermplasmInstituteVO createDistributor() { + GermplasmInstituteVO result = new GermplasmInstituteVO(); + InstituteVO institute = new InstituteVO(); + institute.setInstituteName("Microsoft"); + result.setInstitute(institute); + result.setAccessionNumber("678"); + result.setDistributionStatus("OK"); + return result; + } + + private PedigreeVO createPedigree() { + PedigreeVO result = new PedigreeVO(); + result.setPedigree("Pedigree 1"); + result.setParent1DbId("12345"); + result.setParent1Name("Parent 1"); + result.setParent1Type("P1"); + result.setParent2DbId("12346"); + result.setParent2Name("Parent 2"); + result.setParent2Type("P2"); + result.setCrossingPlan("crossing plan 1"); + result.setCrossingYear("2012"); + result.setSiblings(Arrays.asList(createBrapiSibling())); + return result; + } + + private BrapiSibling createBrapiSibling() { + SiblingVO sibling = new SiblingVO(); + sibling.setGermplasmDbId("5678"); + sibling.setDefaultDisplayName("Sibling 5678"); + return sibling; + } + + private GenealogyVO createChild() { + GenealogyVO result = new GenealogyVO(); + result.setFirstParentName("CP1"); + result.setSecondParentName("CP2"); + result.setSibblings(Arrays.asList(createPuiNameValueVO(), createPuiNameValueVO())); + return result; + } + + private PuiNameValueVO createPuiNameValueVO() { + PuiNameValueVO result = new PuiNameValueVO(); + result.setName("Child 1"); + result.setPui("pui1"); + return result; + } + + private CollPopVO createPopulation1() { + CollPopVO result = new CollPopVO(); + result.setName("Population 1"); + result.setType("Pop Type 1"); + result.setGermplasmCount(3); + result.setGermplasmRef(createPuiNameValueVO()); + return result; + } + + private CollPopVO createPopulation2() { + CollPopVO result = new CollPopVO(); + result.setName("Population 2"); + result.setGermplasmCount(3); + PuiNameValueVO puiNameValueVO = createPuiNameValueVO(); + puiNameValueVO.setPui("germplasmPUI"); + result.setGermplasmRef(puiNameValueVO); + return result; + } + + private CollPopVO createPopulation3() { + CollPopVO result = new CollPopVO(); + result.setName("Population 3"); + result.setGermplasmCount(5); + return result; + } + + private CollPopVO createCollection() { + CollPopVO result = new CollPopVO(); + result.setName("Collection 1"); + result.setGermplasmCount(7); + return result; + } + + private CollPopVO createPanel() { + CollPopVO result = new CollPopVO(); + result.setName("The_panel_1"); + result.setGermplasmCount(2); + return result; + } + + private TaxonSourceVO createTaxonId() { + TaxonSourceVO result = new TaxonSourceVO(); + result.setTaxonId("taxon1"); + result.setSourceName("ThePlantList"); + return result; + } + + 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/germplasm/GermplasmModel.java b/backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmModel.java new file mode 100644 index 0000000000000000000000000000000000000000..8acdf78fc5f36d5427dba77b1abf57463f388f74 --- /dev/null +++ b/backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmModel.java @@ -0,0 +1,139 @@ +package fr.inra.urgi.faidare.web.germplasm; + +import java.util.List; + +import fr.inra.urgi.faidare.domain.brapi.v1.data.BrapiGermplasmAttributeValue; +import fr.inra.urgi.faidare.domain.data.germplasm.GermplasmInstituteVO; +import fr.inra.urgi.faidare.domain.data.germplasm.GermplasmVO; +import fr.inra.urgi.faidare.domain.data.germplasm.PedigreeVO; +import fr.inra.urgi.faidare.domain.datadiscovery.data.DataSource; +import fr.inra.urgi.faidare.domain.xref.XRefDocumentVO; +import org.apache.logging.log4j.util.Strings; + +/** + * The model used by the germplasm page + * @author JB Nizet + */ +public final class GermplasmModel { + private final GermplasmVO germplasm; + private final DataSource source; + private final List<BrapiGermplasmAttributeValue> attributes; + private final PedigreeVO pedigree; + private final List<XRefDocumentVO> crossReferences; + + public GermplasmModel(GermplasmVO germplasm, + DataSource source, + List<BrapiGermplasmAttributeValue> attributes, + PedigreeVO pedigree, + List<XRefDocumentVO> crossReferences) { + this.germplasm = germplasm; + this.source = source; + this.attributes = attributes; + this.pedigree = pedigree; + this.crossReferences = crossReferences; + } + + public GermplasmVO getGermplasm() { + return germplasm; + } + + public DataSource getSource() { + return source; + } + + public List<BrapiGermplasmAttributeValue> getAttributes() { + return attributes; + } + + public PedigreeVO getPedigree() { + return pedigree; + } + + public List<XRefDocumentVO> getCrossReferences() { + return crossReferences; + } + + public String getTaxon() { + if (Strings.isNotBlank(this.germplasm.getGenusSpeciesSubtaxa())) { + return this.germplasm.getGenusSpeciesSubtaxa(); + } else if (Strings.isNotBlank(this.germplasm.getGenusSpecies())) { + return this.germplasm.getGenusSpecies(); + } else if (Strings.isNotBlank(this.germplasm.getSubtaxa())) { + return this.germplasm.getGenus() + " " + this.germplasm.getSpecies() + " " + this.germplasm.getSubtaxa(); + } else if (Strings.isNotBlank(this.germplasm.getSpecies())) { + return this.germplasm.getGenus() + " " + this.germplasm.getSpecies(); + } else { + return this.germplasm.getGenus(); + } + } + + public String getTaxonAuthor() { + if (Strings.isNotBlank(this.germplasm.getGenusSpeciesSubtaxa())) { + return this.germplasm.getSubtaxaAuthority(); + } else if (Strings.isNotBlank(this.germplasm.getGenusSpecies())) { + return this.germplasm.getSpeciesAuthority(); + } else if (Strings.isNotBlank(this.germplasm.getSubtaxa())) { + return this.germplasm.getSubtaxaAuthority(); + } else if (Strings.isNotBlank(this.germplasm.getSpecies())) { + return this.germplasm.getSpeciesAuthority(); + } else { + return null; + } + } + + public boolean isCollecting() { + return this.isCollectingSitePresent() + || this.isCollectorInstitutePresent() + || this.isCollectorIntituteFieldPresent(); + } + + private boolean isCollectingSitePresent() { + return this.germplasm.getCollectingSite() != null && Strings.isNotBlank(this.germplasm.getCollectingSite().getSiteName()); + } + + private boolean isCollectorInstitutePresent() { + return this.germplasm.getCollector() != null && + this.germplasm.getCollector().getInstitute() != null && + Strings.isNotBlank(this.germplasm.getCollector().getInstitute().getInstituteName()); + } + + private boolean isCollectorIntituteFieldPresent() { + GermplasmInstituteVO collector = this.germplasm.getCollector(); + return (collector != null) && + (Strings.isNotBlank(collector.getAccessionNumber()) + || collector.getAccessionCreationDate() != null + || Strings.isNotBlank(collector.getMaterialType()) + || Strings.isNotBlank(collector.getCollectors()) + || collector.getRegistrationYear() != null + || collector.getDeregistrationYear() != null + || Strings.isNotBlank(collector.getDistributionStatus()) + ); + } + + public boolean isBreeding() { + GermplasmInstituteVO breeder = this.germplasm.getBreeder(); + return breeder != null && + ((breeder.getInstitute() != null && Strings.isNotBlank(breeder.getInstitute().getInstituteName())) || + breeder.getAccessionCreationDate() != null || + Strings.isNotBlank(breeder.getAccessionNumber()) || + breeder.getRegistrationYear() != null || + breeder.getDeregistrationYear() != null); + } + + public boolean isGenealogyPresent() { + return isPedigreePresent() || isProgenyPresent(); + } + + private boolean isProgenyPresent() { + return germplasm.getChildren() != null && !germplasm.getChildren().isEmpty(); + } + + private boolean isPedigreePresent() { + return this.pedigree != null && + (Strings.isNotBlank(this.pedigree.getParent1Name()) + || Strings.isNotBlank(this.pedigree.getParent2Name()) + || Strings.isNotBlank(this.pedigree.getCrossingPlan()) + || Strings.isNotBlank(this.pedigree.getCrossingYear()) + || Strings.isNotBlank(this.pedigree.getFamilyCode())); + } +} diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteController.java b/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteController.java index 1ed29438af4084d9ee9bd90bcdd3ac49d1826678..151da527d4393d1f388de8d6098b52d4219d98b2 100644 --- a/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteController.java +++ b/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteController.java @@ -37,7 +37,7 @@ public class SiteController { } @GetMapping("/{siteId}") - public ModelAndView site(@PathVariable("siteId") String siteId) { + public ModelAndView get(@PathVariable("siteId") String siteId) { LocationVO site = locationRepository.getById(siteId); // List<XRefDocumentVO> crossReferences = xRefDocumentRepository.find( 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 index 35767b0942d1937c513f5829b597f707d589ae3a..ff6aee3bfb5e322b4385cbd491ad9ee9f336b5cd 100644 --- 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 @@ -58,7 +58,7 @@ public class StudyController { } @GetMapping("/{studyId}") - public ModelAndView site(@PathVariable("studyId") String studyId) { + public ModelAndView get(@PathVariable("studyId") String studyId) { StudyDetailVO study = studyRepository.getById(studyId); // List<XRefDocumentVO> crossReferences = xRefDocumentRepository.find( diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareDialect.java b/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareDialect.java new file mode 100644 index 0000000000000000000000000000000000000000..2a02a842a28ba411f8f6d16d6c5254e41a53524b --- /dev/null +++ b/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareDialect.java @@ -0,0 +1,25 @@ +package fr.inra.urgi.faidare.web.thymeleaf; + +import org.springframework.stereotype.Component; +import org.thymeleaf.dialect.AbstractDialect; +import org.thymeleaf.dialect.IExpressionObjectDialect; +import org.thymeleaf.expression.IExpressionObjectFactory; + +/** + * A thymeleaf dialect allowing to perform various tasks in the template related to Faidare + * @author JB Nizet + */ +@Component +public class FaidareDialect extends AbstractDialect implements IExpressionObjectDialect { + + private final IExpressionObjectFactory FAIDARE_EXPRESSION_OBJECTS_FACTORY = new FaidareExpressionFactory(); + + protected FaidareDialect() { + super("faidare"); + } + + @Override + public IExpressionObjectFactory getExpressionObjectFactory() { + return FAIDARE_EXPRESSION_OBJECTS_FACTORY; + } +} diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareExpressionFactory.java b/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareExpressionFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..e873375c1a7681d2837c0bf6591174d1aa557e3d --- /dev/null +++ b/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareExpressionFactory.java @@ -0,0 +1,33 @@ +package fr.inra.urgi.faidare.web.thymeleaf; + +import java.util.Collections; +import java.util.Set; + +import org.thymeleaf.context.IExpressionContext; +import org.thymeleaf.expression.IExpressionObjectFactory; + +/** + * The object factory for the {@link FaidareDialect} + * @author JB Nizet + */ +public class FaidareExpressionFactory implements IExpressionObjectFactory { + private static final String FAIDARE_EVALUATION_VARIABLE_NAME = "faidare"; + + private static final Set<String> ALL_EXPRESSION_OBJECT_NAMES = + Collections.singleton(FAIDARE_EVALUATION_VARIABLE_NAME); + + @Override + public Set<String> getAllExpressionObjectNames() { + return ALL_EXPRESSION_OBJECT_NAMES; + } + + @Override + public Object buildObject(IExpressionContext context, String expressionObjectName) { + return new FaidareExpressions(context.getLocale()); + } + + @Override + public boolean isCacheable(String expressionObjectName) { + return true; + } +} diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareExpressions.java b/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareExpressions.java new file mode 100644 index 0000000000000000000000000000000000000000..a9f699de3dc80e7bc1b3bc4a7af0bc7bbb5d5da3 --- /dev/null +++ b/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareExpressions.java @@ -0,0 +1,65 @@ +package fr.inra.urgi.faidare.web.thymeleaf; + +import java.nio.charset.StandardCharsets; +import java.text.DecimalFormat; +import java.util.Base64; +import java.util.Collections; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.function.Function; + +import fr.inra.urgi.faidare.domain.data.germplasm.CollPopVO; +import fr.inra.urgi.faidare.domain.data.germplasm.TaxonSourceVO; +import org.apache.logging.log4j.util.Strings; + +/** + * The actual object offering Faidare helper methods to thymeleaf + * @author JB Nizet + */ +public class FaidareExpressions { + + private static final Map<String, Function<String, String>> TAXON_ID_URL_FACTORIES_BY_SOURCE_NAME = + createTaxonIdUrlFactories(); + + private static Map<String, Function<String, String>> createTaxonIdUrlFactories() { + Map<String, Function<String, String>> result = new HashMap<>(); + result.put("NCBI", s -> "https://www.ncbi.nlm.nih.gov/Taxonomy/Browser/wwwtax.cgi?mode=Info&id=" + s); + result.put("ThePlantList", s -> "http://www.theplantlist.org/tpl1.1/record/" + s); + result.put("TAXREF", s -> "https://inpn.mnhn.fr/espece/cd_nom/" + s); + result.put("CatalogueOfLife", s -> "http://www.catalogueoflife.org/col/details/species/id/" + s); + return Collections.unmodifiableMap(result); + } + + private final Locale locale; + + public FaidareExpressions(Locale locale) { + this.locale = locale; + } + + public String toSiteParam(String siteId) { + return Base64.getUrlEncoder().encodeToString(("urn:URGI/location/" + siteId).getBytes(StandardCharsets.US_ASCII)); + } + + public String collPopTitle(CollPopVO collPopVO) { + return collPopTitle(collPopVO, Function.identity()); + } + + public String collPopTitleWithoutUnderscores(CollPopVO collPopVO) { + return collPopTitle(collPopVO, s -> s.replace('_', ' ')); + } + + public String taxonIdUrl(TaxonSourceVO taxonSource) { + Function<String, String> urlFactory = + TAXON_ID_URL_FACTORIES_BY_SOURCE_NAME.get(taxonSource.getSourceName()); + return urlFactory != null ? urlFactory.apply(taxonSource.getTaxonId()) : null; + } + + private String collPopTitle(CollPopVO collPopVO, Function<String, String> nameTransformer) { + if (Strings.isBlank(collPopVO.getType())) { + return nameTransformer.apply(collPopVO.getName()); + } else { + return nameTransformer.apply(collPopVO.getName()) + " (" + collPopVO.getType() + ")"; + } + } +} diff --git a/backend/src/main/resources/static/assets/style.css b/backend/src/main/resources/static/assets/style.css index 59bcd1175312e3afb552a30ba2e0c39f09374d54..340b22eae82d8f58d76250fdb37ded071695b6c5 100644 --- a/backend/src/main/resources/static/assets/style.css +++ b/backend/src/main/resources/static/assets/style.css @@ -1,3 +1,7 @@ .label { font-weight: 500; } + +.popover { + max-width: min(80vw, 600px); +} diff --git a/backend/src/main/resources/templates/fragments/institute.html b/backend/src/main/resources/templates/fragments/institute.html new file mode 100644 index 0000000000000000000000000000000000000000..7e60309571e11a94c9d725e7934817651e918da2 --- /dev/null +++ b/backend/src/main/resources/templates/fragments/institute.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> + +<html xmlns:th="http://www.thymeleaf.org"> + +<body> + +<!-- +Reusable fragment displaying the content of an institute popover. +Its unique argument (institute) is an InstituteVO +--> + +<th:block th:fragment="institute(institute)"> + <div class="text-center py-2" th:if="${institute.logo}"> + <img th:src="${institute.logo}" th:alt="${institute.instituteName}"/> + </div> + <div th:replace="fragments/row::text-row(label='Code', text=${institute.instituteCode})"></div> + <div th:replace="fragments/row::text-row(label='Acronym', text=${institute.acronym})"></div> + <div th:replace="fragments/row::text-row(label='Organization', text=${institute.organisation})"></div> + <div th:replace="fragments/row::text-row(label='Type', text=${institute.instituteType})"></div> + <div th:replace="fragments/row::text-row(label='Address', text=${institute.address})"></div> + + <th:block th:if="${institute.webSite}"> + <div th:replace="fragments/row::row(label='Website', content=~{::.institute-website})"> + <a class="institute-website" + target="_blank" + th:href="${institute.webSite}" + th:text="${#strings.abbreviate(institute.webSite, 25)}"></a> + </div> + </th:block> +</th:block> + +</body> + +</html> diff --git a/backend/src/main/resources/templates/fragments/link.html b/backend/src/main/resources/templates/fragments/link.html new file mode 100644 index 0000000000000000000000000000000000000000..d7c43bbdb422c31c5bce647d302c803270672669 --- /dev/null +++ b/backend/src/main/resources/templates/fragments/link.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> + +<html xmlns:th="http://www.thymeleaf.org"> + +<body> +<!-- +Reusable fragment displaying a link with a label if the provided url is not +empty, or a span with the label if the provided url is empty. +Both arguments are strings. +--> +<th:block th:fragment="link(label, url)"> + <a th:unless="${#strings.isEmpty(url)}" + th:href="${url}" + th:text="${label}"></a> + <span th:if="${#strings.isEmpty(url)}" th:text="${label}"></span> +</th:block> + +</body> + +</html> diff --git a/backend/src/main/resources/templates/fragments/row.html b/backend/src/main/resources/templates/fragments/row.html index 5b523ceff59a8601e0e9569a97ed51b6381b594a..e5eb9e7f78172597de3e88235ae796c77c9a08e4 100644 --- a/backend/src/main/resources/templates/fragments/row.html +++ b/backend/src/main/resources/templates/fragments/row.html @@ -4,6 +4,21 @@ <body> +<!-- +Reusable fragment displaying a responsive row containing a label and a content. +The label argument is a string. +The content argument is a fragment which is displayed at the right of the label. + +Note that `th:if` is not evaluated when th:replace is used. So if this row must +be displayed only if some condition is true, the fragment should be enclosed +into a block with the condition: + <th:block th:if="${someCondition}"> + <div th:replace="fragments/row::row(label='Some label', content=~{::#some-content-id})"> + <span id="some-content-id">the content here</span> + </div> + </th:block> +--> + <div th:fragment="row(label, content)" class="row py-2"> <div class="col-md-4 label pb-1 pb-md-0" th:text="${label}"></div> <div class="col"> @@ -11,7 +26,21 @@ </div> </div> -<div th:fragment="text-row(label, text)" th:if="${!#strings.isEmpty(text)}" class="row py-2"> +<!-- +Reusable fragment displaying a responsive row containing a label and a textual content. +The label argument is a string. +The text argument is a string which is displayed at the right of the label. +The whole row is omitted if the textual content is empty, so the caller does not +need to test that condition. + +Note that `th:if` is not evaluated when th:replace is used. So if this row must +be displayed only if some other condition is true, the fragment should be enclosed +into a block with the condition: + <th:block th:if="${someCondition}"> + <div th:replace="fragments/row::text-row(label='Some label', text=${someTextExpression})"></div> + </th:block> +--> +<div th:fragment="text-row(label, text)" th:unless="${#strings.isEmpty(text)}" class="row py-2"> <div class="col-md-4 label pb-1 pb-md-0" th:text="${label}"></div> <div class="col" th:text="${text}"></div> </div> diff --git a/backend/src/main/resources/templates/fragments/source.html b/backend/src/main/resources/templates/fragments/source.html new file mode 100644 index 0000000000000000000000000000000000000000..795717ca8c2df1445dbf30999e6fbc8dc5b65a1a --- /dev/null +++ b/backend/src/main/resources/templates/fragments/source.html @@ -0,0 +1,35 @@ +<!DOCTYPE html> + +<html xmlns:th="http://www.thymeleaf.org"> + +<body> + +<!-- +Reusable fragment displaying the source and the data links of an entity (site, study or germplasm). +The source argument is a DataSource. +The url argument is a string, which is the URL of the entity. +The entityType argument is a string, which is used in the message +"Link to this <entityType>". +--> + +<th:block th:fragment="source(source, url, entityType)"> + <th:block th:if="${source != null}"> + <div th:replace="fragments/row::row(label='Source', content=~{::.source})"> + <a class="source" target="_blank" th:href="${source.url}"> + <img style="max-height: 60px;" th:src="${source.image}" th:alt="${source.name} + ' logo'" /> + </a> + </div> + </th:block> + + <th:block th:if="${url != null && source != null}"> + <div th:replace="fragments/row::row(label='Data link', content=~{::.source-url})"> + <a class="source-url" target="_blank" th:href="${url}"> + Link to this <span th:text="${entityType}"></span> on <th:block th:text="${source.name}" /> + </a> + </div> + </th:block> +</th:block> + +</body> + +</html> diff --git a/backend/src/main/resources/templates/fragments/xrefs.html b/backend/src/main/resources/templates/fragments/xrefs.html index d51f1186e1c2c5bcc8bfef48b557d637d3749820..508b8923a8944dbdf0760d4b58e49a80156bca84 100644 --- a/backend/src/main/resources/templates/fragments/xrefs.html +++ b/backend/src/main/resources/templates/fragments/xrefs.html @@ -4,6 +4,11 @@ <body> +<!-- +Reusable fragment displaying a cross references section, with its title. +The unique argument (crossReferences) is a List<XRefDocumentVO> +--> + <div th:fragment="xrefs(crossReferences)" th:if="${!#lists.isEmpty(crossReferences)}"> <h2>Cross references</h2> diff --git a/backend/src/main/resources/templates/germplasm.html b/backend/src/main/resources/templates/germplasm.html new file mode 100644 index 0000000000000000000000000000000000000000..1c51d43887a55f7e125dc46a85ae961de9d5b7bd --- /dev/null +++ b/backend/src/main/resources/templates/germplasm.html @@ -0,0 +1,418 @@ +<!DOCTYPE html> + +<html + xmlns:th="http://www.thymeleaf.org" + th:replace="~{layout/main :: layout(title=~{::title}, content=~{::main})}" +> +<head> + <title>Germplasm: <th:block th:text="${model.germplasm.germplasmName}" /></title> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> +</head> + +<body> +<main> + <div class="d-flex"> + <h1 class="flex-grow-1">Germplasm: <th:block th:text="${model.germplasm.germplasmName}" /></h1> + <div th:if="${model.germplasm.holdingGenbank != null && model.germplasm.holdingGenbank.logo != null}"> + <img th:src="${model.germplasm.holdingGenbank.logo}" th:alt="${model.germplasm.holdingGenbank.instituteName}" /> + </div> + </div> + + <div class="row align-items-center justify-content-center"> + <div class="col-auto field" th:if="${model.germplasm.photo != null && model.germplasm.photo.thumbnailFile != null}"> + <template id="photo-popover"> + <div class="card"> + <img th:src="${model.germplasm.photo.file}" class="card-img-top" alt=""> + <div class="card-body"> + <div th:replace="fragments/row::text-row(label='Accession name', text=${model.germplasm.germplasmName})"></div> + <div th:replace="fragments/row::text-row(label='Photo name', text=${model.germplasm.photo.photoName})"></div> + <div th:replace="fragments/row::text-row(label='Description', text=${model.germplasm.photo.description})"></div> + <div th:replace="fragments/row::text-row(label='Copyright', text=${model.germplasm.photo.copyright})"></div> + </div> + </div> + </template> + + <button class="btn btn-link p-0" + data-bs-toggle="popover" + th:data-bs-title="${model.germplasm.photo.photoName}" + data-bs-element="#photo-popover" + data-bs-container="body" + data-bs-trigger="focus"> + <img th:src="${model.germplasm.photo.thumbnailFile}" class="img-fluid" /> + + <figcaption class="figure-caption"> + © <span th:text="${model.germplasm.photo.copyright}"></span> + </figcaption> + </button> + </div> + + + <div class="col-12 col-lg"> + <h2>Identification</h2> + + <div th:replace="fragments/row::text-row(label='Germplasm name', text=${model.germplasm.germplasmName})"></div> + <div th:replace="fragments/row::text-row(label='Accession number', text=${model.germplasm.accessionNumber})"></div> + + <div th:replace="fragments/source::source(source=${model.source}, url=${model.germplasm.url}, entityType='germplasm')"></div> + + <th:block th:unless="${#lists.isEmpty(model.germplasm.synonyms)}"> + <div th:replace="fragments/row::row(label='Accession synonyms', content=~{::#accession-synonyms})"> + <div id="accession-synonyms" class="content-overflow" th:text="${#strings.listJoin(model.germplasm.synonyms, ', ')}"></div> + </div> + </th:block> + + <th:block th:unless="${#strings.isEmpty(model.taxon)}"> + <div th:replace="fragments/row::row(label='Taxon', content=~{::#taxon})"> + <div id="taxon"> + <template id="taxon-popover"> + <th:block th:unless="${#strings.isEmpty(model.germplasm.genus)}"> + <div th:replace="fragments/row::row(label='Genus', content=~{::#taxon-genus})"> + <em id="taxon-genus" th:text="${model.germplasm.genus}"></em> + </div> + </th:block> + <th:block th:unless="${#strings.isEmpty(model.germplasm.species)}"> + <div th:replace="fragments/row::row(label='Species', content=~{::#taxon-species})"> + <span id="taxon-species"> + <em th:text="${model.germplasm.species}"></em> + <span th:unless="${#strings.isEmpty(model.germplasm.speciesAuthority)}" + th:text="${'(' + model.germplasm.speciesAuthority + ')'}"></span> + </span> + </div> + </th:block> + <th:block th:unless="${#strings.isEmpty(model.germplasm.subtaxa)}"> + <div th:replace="fragments/row::row(label='Subtaxa', content=~{::#taxon-subtaxa})"> + <span id="taxon-subtaxa"> + <em th:text="${model.germplasm.subtaxa}"></em> + <span th:unless="${#strings.isEmpty(model.germplasm.subtaxaAuthority)}" + th:text="${'(' + model.germplasm.subtaxaAuthority + ')'}"></span> + </span> + </div> + </th:block> + + <div th:replace="fragments/row::text-row(label='Authority', text=${model.taxonAuthor})"></div> + + <th:block th:unless="${#lists.isEmpty(model.germplasm.taxonIds)}"> + <div th:replace="fragments/row::row(label='Taxon IDs', content=~{::#taxon-ids})"> + <div id="taxon-ids"> + <div th:each="taxonId : ${model.germplasm.taxonIds}" class="row"> + <div class="col-6 text-nowrap" th:text="${taxonId.sourceName}"></div> + <div class="col-6"> + <span class="taxon-id" + th:replace="fragments/link::link(label=${taxonId.taxonId}, url=${#faidare.taxonIdUrl(taxonId)})"></span> + </div> + </div> + </div> + </div> + </th:block> + + <div th:replace="fragments/row::text-row(label='Comment', text=${model.germplasm.taxonComment})"></div> + <th:block th:unless="${#lists.isEmpty(model.germplasm.taxonCommonNames)}"> + <div th:replace="fragments/row::row(label='Taxon common names', content=~{::#taxon-common-names})"> + <div id="taxon-common-names" class="content-overflow" th:text="${#strings.listJoin(model.germplasm.taxonCommonNames, ', ')}"></div> + </div> + </th:block> + <th:block th:unless="${#lists.isEmpty(model.germplasm.taxonSynonyms)}"> + <div th:replace="fragments/row::row(label='Taxon synonyms', content=~{::#taxon-synonyms})"> + <div id="taxon-synonyms" class="content-overflow" th:text="${#strings.listJoin(model.germplasm.taxonSynonyms, ', ')}"></div> + </div> + </th:block> + </template> + <button class="btn btn-link p-0" + data-bs-toggle="popover" + th:data-bs-title="${model.taxon}" + data-bs-element="#taxon-popover" + data-bs-container="body" + data-bs-trigger="focus"> + <em th:text="${model.taxon}"></em> + <th:block th:unless="${#strings.isEmpty(model.taxonAuthor)}">(<span th:text="${model.taxonAuthor}"></span>)</th:block> + </button> + </div> + </div> + </th:block> + + <div th:replace="fragments/row::text-row(label='Biological status', text=${model.germplasm.biologicalStatusOfAccessionCode})"></div> + <div th:replace="fragments/row::text-row(label='Genetic nature', text=${model.germplasm.geneticNature})"></div> + <div th:replace="fragments/row::text-row(label='Seed source', text=${model.germplasm.seedSource})"></div> + <div th:replace="fragments/row::text-row(label='Pedigree', text=${model.germplasm.pedigree})"></div> + <div th:replace="fragments/row::text-row(label='Comments', text=${model.germplasm.comment})"></div> + + <th:block th:if="${model.germplasm.originSite != null && !#strings.isEmpty(model.germplasm.originSite.siteName)}"> + <div th:replace="fragments/row::row(label='Origin site', content=~{::#origin-site})"> + <a id="origin-site" th:href="@{/sites/{siteId}(siteId=${#faidare.toSiteParam(model.germplasm.originSite.siteId)})}" th:text="${model.germplasm.originSite.siteName}"></a> + </div> + </th:block> + </div> + </div> + + <th:block th:if="${model.germplasm.holdingInstitute}"> + <h2>Depositary</h2> + <template id="holding-institute-popover"> + <div th:replace="fragments/institute::institute(institute=${model.germplasm.holdingInstitute})"></div> + </template> + <div th:replace="fragments/row::row(label='Institution', content=~{::#institution})"> + <button id="institution" + data-bs-toggle="popover" + th:data-bs-title="${model.germplasm.holdingInstitute.instituteName}" + data-bs-element="#holding-institute-popover" + data-bs-container="body" + data-bs-trigger="focus" + class="btn btn-link p-0" + th:text="${model.germplasm.holdingInstitute.instituteName}"></button> + </div> + + <th:block th:if="${model.germplasm.holdingGenbank != null && !#strings.isEmpty(model.germplasm.holdingGenbank.instituteName) && !#strings.isEmpty(model.germplasm.holdingGenbank.webSite)}"> + <div th:replace="fragments/row::row(label='Stock center name', content=~{::#stock-center-name})"> + <a id="stock-center-name" + target="_blank" + th:href="${model.germplasm.holdingGenbank.webSite}" + th:text="${model.germplasm.holdingGenbank.instituteName}"></a> + </div> + </th:block> + + <div th:replace="fragments/row::text-row(label='Presence status', text=${model.germplasm.presenceStatus})"></div> + </th:block> + + <th:block th:if="${model.collecting}"> + <h2>Collector</h2> + <th:block th:if="${model.germplasm.collectingSite != null && !#strings.isEmpty(model.germplasm.collectingSite.siteName)}"> + <div th:replace="fragments/row::row(label='Collecting site', content=~{::#collecting-site})"> + <a id="collecting-site" + th:href="@{/sites/{siteId}(siteId=${#faidare.toSiteParam(model.germplasm.collectingSite.siteId)})}" + th:text="${model.germplasm.collectingSite.siteName}" + ></a> + </div> + </th:block> + + <div th:replace="fragments/row::text-row(label='Material type', text=${model.germplasm.collector.materialType})"></div> + <div th:replace="fragments/row::text-row(label='Collectors', text=${model.germplasm.collector.collectors})"></div> + + <th:block th:if="${!#strings.isEmpty(model.germplasm.acquisitionDate) && model.germplasm.collector.accessionCreationDate == null}"> + <div th:replace="fragments/row::text-row(label='Acquisition / Creation date', text=${model.germplasm.acquisitionDate})"></div> + </th:block> + + <th:block th:if="${model.germplasm.collector.institute != null && !#strings.isEmpty(model.germplasm.collector.institute.instituteName)}"> + <template id="collector-institute-popover"> + <div th:replace="fragments/institute::institute(institute=${model.germplasm.collector.institute})"></div> + </template> + <div th:replace="fragments/row::row(label='Institution', content=~{::#collecting-institution})"> + <button id="collecting-institution" + data-bs-toggle="popover" + th:data-bs-title="${model.germplasm.collector.institute.instituteName}" + data-bs-element="#collector-institute-popover" + data-bs-container="body" + data-bs-trigger="focus" + class="btn btn-link p-0" + th:text="${model.germplasm.collector.institute.instituteName}"></button> + </div> + </th:block> + + <div th:replace="fragments/row::text-row(label='Accession number', text=${model.germplasm.collector.accessionNumber})"></div> + </th:block> + + <th:block th:if="${model.breeding}"> + <h2>Breeder</h2> + <th:block th:if="${model.germplasm.breeder.institute != null && !#strings.isEmpty(model.germplasm.breeder.institute.instituteName)}"> + <template id="breeder-institute-popover"> + <div th:replace="fragments/institute::institute(institute=${model.germplasm.breeder.institute})"></div> + </template> + <div th:replace="fragments/row::row(label='Institute', content=~{::#breeding-institution})"> + <button id="breeding-institution" + data-bs-toggle="popover" + th:data-bs-title="${model.germplasm.breeder.institute.instituteName}" + data-bs-element="#breeder-institute-popover" + data-bs-container="body" + data-bs-trigger="focus" + class="btn btn-link p-0" + th:text="${model.germplasm.breeder.institute.instituteName}"></button> + </div> + </th:block> + + <div th:replace="fragments/row::text-row(label='Accession creation year', text=${model.germplasm.breeder.accessionCreationDate})"></div> + <div th:replace="fragments/row::text-row(label='Accession number', text=${model.germplasm.breeder.accessionNumber})"></div> + <div th:replace="fragments/row::text-row(label='Catalog registration year', text=${model.germplasm.breeder.registrationYear})"></div> + <div th:replace="fragments/row::text-row(label='Catalog deregistration year', text=${model.germplasm.breeder.deregistrationYear})"></div> + </th:block> + + <th:block th:unless="${#lists.isEmpty(model.germplasm.donors)}"> + <h2>Donors</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">Institute name</th> + <th scope="col">Institute code</th> + <th scope="col">Donation date</th> + <th scope="col">Accession number</th> + <th scope="col">Accession PUI</th> + </tr> + </thead> + <tbody> + <tr th:each="row, donorIterStat : ${model.germplasm.donors}"> + <td> + <template th:id="${'donor-institute-popover-' + donorIterStat.index}"> + <div th:replace="fragments/institute::institute(institute=${row.donorInstitute})"></div> + </template> + <button data-bs-toggle="popover" + th:data-bs-title="${row.donorInstitute.instituteName}" + th:data-bs-element="${'#donor-institute-popover-' + donorIterStat.index}" + data-bs-container="body" + data-bs-trigger="focus" + class="btn btn-link p-0" + th:text="${row.donorInstitute.instituteName}"></button> + </td> + <td th:text="${row.donorInstituteCode}"></td> + <td th:text="${row.donationDate}"></td> + <td th:text="${row.donorAccessionNumber}"></td> + <td th:text="${row.donorGermplasmPUI}"></td> + </tr> + </tbody> + </table> + </div> + </div> + </th:block> + + <th:block th:unless="${#lists.isEmpty(model.germplasm.distributors)}"> + <h2>Donors</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">Institute</th> + <th scope="col">Accession number</th> + <th scope="col">Distribution status</th> + </tr> + </thead> + <tbody> + <tr th:each="row, distributorIterStat : ${model.germplasm.distributors}"> + <td> + <template th:id="${'distributor-institute-popover-' + distributorIterStat.index}"> + <div th:replace="fragments/institute::institute(institute=${row.institute})"></div> + </template> + <button data-bs-toggle="popover" + th:data-bs-title="${row.institute.instituteName}" + th:data-bs-element="${'#distributor-institute-popover-' + distributorIterStat.index}" + data-bs-container="body" + data-bs-trigger="focus" + class="btn btn-link p-0" + th:text="${row.institute.instituteName}"></button> + </td> + <td th:text="${row.accessionNumber}"></td> + <td th:text="${row.distributionStatus}"></td> + </tr> + </tbody> + </table> + </div> + </div> + </th:block> + + <th:block th:unless="${#lists.isEmpty(model.attributes)}"> + <h2>Evaluation Data</h2> + <th:block th:each="descriptor : ${model.attributes}"> + <div th:replace="fragments/row::text-row(label=${descriptor.attributeName}, text=${descriptor.value})"></div> + </th:block> + </th:block> + + <th:block th:if="${model.genealogyPresent}"> + <h2>Genealogy</h2> + + <th:block th:if="${model.pedigree != null}"> + <div th:replace="fragments/row::text-row(label='Crossing plan', text=${model.pedigree.crossingPlan})"></div> + <div th:replace="fragments/row::text-row(label='Crossing year', text=${model.pedigree.crossingYear})"></div> + <div th:replace="fragments/row::text-row(label='Family code', text=${model.pedigree.familyCode})"></div> + <th:block th:unless="${#strings.isEmpty(model.pedigree.parent1Name)}"> + <div th:replace="fragments/row::row(label='Parent accessions', content=~{::#parent-accessions})"> + <div id="parent-accessions"> + <th:block th:if="${model.pedigree.parent1DbId}"> + <div th:replace="fragments/row::row(label=${model.pedigree.parent1Type}, content=~{::#parent1-link})"> + <a id="parent1-link" th:href="@{/germplasms/{germplasmId}(germplasmId=${model.pedigree.parent1DbId})}" th:text="${model.pedigree.parent1Name}"></a> + </div> + </th:block> + + <th:block th:if="${model.pedigree.parent2DbId}"> + <div th:replace="fragments/row::row(label=${model.pedigree.parent2Type}, content=~{::#parent2-link})"> + <a id="parent2-link" th:href="@{/germplasms/{germplasmId}(germplasmId=${model.pedigree.parent2DbId})}" th:text="${model.pedigree.parent2Name}"></a> + </div> + </th:block> + </div> + </div> + </th:block> + + <th:block th:unless="${#lists.isEmpty(model.pedigree.siblings)}"> + <div th:replace="fragments/row::row(label='Sibling accessions', content=~{::#sibling-accessions})"> + <div id="sibling-accessions" class="content-overflow"> + <a th:each="sibling : ${model.pedigree.siblings}" + th:href="@{/germplasms/{germplasmId}(germplasmId=${sibling.germplasmDbId})}" + th:text="${sibling.defaultDisplayName}"></a> + </div> + </div> + </th:block> + </th:block> + + <th:block th:unless="${#lists.isEmpty(model.germplasm.children)}"> + <div th:replace="fragments/row::row(label='Descendants', content=~{::#descendants})"> + <div id="descendants" class="content-overflow-big"> + <th:block th:each="child : ${model.germplasm.children}"> + <div th:replace="fragments/row::row(label=${#strings.isEmpty(child.secondParentName) ? ('children of ' + child.firstParentName) : ('children of ' + child.firstParentName + ' x ' + child.secondParentName) }, content=~{::.descendant-child})"> + <div class="descendant-child"> + <th:block th:each="sibling, siblingIterStat : ${child.sibblings}"> + <a th:href="@{/germplasms(pui=${sibling.pui})}" + th:text="${sibling.name}"></a><th:block th:unless="${siblingIterStat.last}">, </th:block> + </th:block> + </div> + </div> + </th:block> + </div> + </div> + </th:block> + + </th:block> + + <th:block th:unless="${#lists.isEmpty(model.germplasm.population)}"> + <h2>Population</h2> + <th:block th:each="population : ${model.germplasm.population}"> + + <th:block th:if="${population.germplasmRef != null}"> + <th:block th:unless="${#strings.isEmpty(population.germplasmRef.pui)}"> + <div th:replace="fragments/row::row(label=${#faidare.collPopTitle(population)}, content=~{::.population-1})"> + <div class="population-1"> + <a th:if="${population.germplasmRef.pui != model.germplasm.germplasmPUI}" + th:href="@{/germplasms(pui=${population.germplasmRef.pui})}" + th:text="${population.germplasmRef.name}"></a> + <span th:if="${population.germplasmRef.pui == model.germplasm.germplasmPUI}" + th:text="${population.germplasmRef.name}"></span> + is composed by <span th:text="${population.germplasmCount}"></span> accession(s) + <!-- TODO there was a link pointing at a search here --> + </div> + </div> + </th:block> + </th:block> + + <th:block th:if="${population.germplasmRef == null}"> + <div th:replace="fragments/row::text-row(label=${#faidare.collPopTitle(population)}, text=${population.germplasmCount + ' accession(s)'})"></div> + <!-- TODO there was a link pointing at a search here --> + </th:block> + </th:block> + </th:block> + + <th:block th:unless="${#lists.isEmpty(model.germplasm.collection)}"> + <h2>Collection</h2> + <th:block th:each="collection : ${model.germplasm.collection}"> + <div th:replace="fragments/row::text-row(label=${#faidare.collPopTitle(collection)}, text=${collection.germplasmCount + ' accession(s)'})"></div> + <!-- TODO there was a link pointing at a search here --> + </th:block> + </th:block> + + <th:block th:unless="${#lists.isEmpty(model.germplasm.panel)}"> + <h2>Panel</h2> + <th:block th:each="panel : ${model.germplasm.panel}"> + <div th:replace="fragments/row::text-row(label=${#faidare.collPopTitleWithoutUnderscores(panel)}, text=${panel.germplasmCount + ' accession(s)'})"></div> + <!-- TODO there was a link pointing at a search here --> + </th:block> + </th:block> + + <div th:replace="fragments/xrefs::xrefs(crossReferences=${model.crossReferences})"></div> +</main> +</body> +</html> diff --git a/backend/src/main/resources/templates/layout/main.html b/backend/src/main/resources/templates/layout/main.html index 22bcdfd5c2ba01624fe98a49e3f9d6ef15692837..4cd33f7022d4752767011dba6f59cf6391bd8c4c 100644 --- a/backend/src/main/resources/templates/layout/main.html +++ b/backend/src/main/resources/templates/layout/main.html @@ -26,5 +26,27 @@ common footer </footer> </div> + <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-U1DAWAznBHeqEIlVSCgzq+c9gqGAJn5c/t99JyeKa9xxaYpSvHU5awsuZVVFIhvj" crossorigin="anonymous"></script> + <script type="text/javascript"> + const popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')) + popoverTriggerList.forEach(popoverTriggerEl => { + const options = {}; + const contentSelector = popoverTriggerEl.dataset.bsElement; + if (contentSelector) { + const content = document.querySelector(contentSelector); + if (content) { + options.content = () => { + const element = document.createElement('div'); + element.innerHTML = content.innerHTML; + return element; + }; + options.html = true; + } else { + throw new Error('element with selector ' + contentSelector + ' not found'); + } + } + return new bootstrap.Popover(popoverTriggerEl, options); + }); + </script> </body> </html> diff --git a/backend/src/main/resources/templates/site.html b/backend/src/main/resources/templates/site.html index ad6affc4cad15723ea182c897910d6918f2190b2..d5d65e5c598f80106ef4bc910b7184114ec40d5c 100644 --- a/backend/src/main/resources/templates/site.html +++ b/backend/src/main/resources/templates/site.html @@ -17,22 +17,7 @@ <div th:replace="fragments/row::text-row(label='Permanent unique identifier', text=${model.site.uri})"></div> </th:block> - <th:block th:if="${model.source != null}"> - <div th:replace="fragments/row::row(label='Source', content=~{::#source})"> - <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.site.url != null && model.source != null}"> - <div - th:replace="fragments/row::row(label='Data link', content=~{::#url})"> - <a id="url" target="_blank" th:href="${model.site.url}"> - Link to this site on <th:block th:text="${model.source.name}" /> - </a> - </div> - </th:block> + <div th:replace="fragments/source::source(source=${model.source}, url=${model.site.url}, entityType='site')"></div> <div th:replace="fragments/row::text-row(label='Abbreviation', text=${model.site.abbreviation})"></div> <div th:replace="fragments/row::text-row(label='Type', text=${model.site.locationType})"></div> diff --git a/backend/src/main/resources/templates/study.html b/backend/src/main/resources/templates/study.html index a1cafc653a5706954e1a30249a63f68ce538abae..7bd5bbbd64fb18a2bff615b3f76e23e5153b3031 100644 --- a/backend/src/main/resources/templates/study.html +++ b/backend/src/main/resources/templates/study.html @@ -18,21 +18,7 @@ <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/source::source(source=${model.source}, url=${model.study.url}, entityType='study')"></div> <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>