diff --git a/src/main/java/uk/ac/sanger/sccp/stan/request/register/SectionRegisterContent.java b/src/main/java/uk/ac/sanger/sccp/stan/request/register/SectionRegisterContent.java index 14e8a692..77df638c 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/request/register/SectionRegisterContent.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/request/register/SectionRegisterContent.java @@ -4,6 +4,7 @@ import uk.ac.sanger.sccp.stan.model.LifeStage; import uk.ac.sanger.sccp.utils.BasicUtils; +import java.time.LocalDate; import java.util.Objects; /** @@ -25,6 +26,7 @@ public class SectionRegisterContent { private Integer sectionNumber; private Integer sectionThickness; private String region; + private LocalDate dateSectioned; public SectionRegisterContent() {} @@ -147,6 +149,15 @@ public void setRegion(String region) { this.region = region; } + /** The date the sample was sectioned */ + public LocalDate getDateSectioned() { + return this.dateSectioned; + } + + public void setDateSectioned(LocalDate dateSectioned) { + this.dateSectioned = dateSectioned; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -166,6 +177,7 @@ public boolean equals(Object o) { && Objects.equals(this.sectionNumber, that.sectionNumber) && Objects.equals(this.sectionThickness, that.sectionThickness) && Objects.equals(this.region, that.region) + && Objects.equals(this.dateSectioned, that.dateSectioned) ); } @@ -191,6 +203,7 @@ public String toString() { .add("sectionNumber", sectionNumber) .add("sectionThickness", sectionThickness) .add("region", region) + .add("dateSectioned", dateSectioned) .reprStringValues() .omitNullValues() .toString(); diff --git a/src/main/java/uk/ac/sanger/sccp/stan/service/register/SectionRegisterServiceImp.java b/src/main/java/uk/ac/sanger/sccp/stan/service/register/SectionRegisterServiceImp.java index 93966481..f16b1779 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/service/register/SectionRegisterServiceImp.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/service/register/SectionRegisterServiceImp.java @@ -248,14 +248,23 @@ public Operation createOp(User user, OperationType opType, Labware lw) { * @return the measurements created */ public Iterable createMeasurements(SectionRegisterLabware srl, Labware lw, Operation op, - UCMap sampleMap) { - List measurements = srl.getContents().stream() - .filter(content -> content.getSectionThickness() != null) - .map(content -> new Measurement( - null, "Thickness", content.getSectionThickness().toString(), - sampleMap.get(content.getExternalIdentifier()).getId(), op.getId(), - lw.getSlot(content.getAddress()).getId())) - .collect(toList()); + UCMap sampleMap) { + List measurements = new ArrayList<>(); + for (var src : srl.getContents()) { + if (src.getDateSectioned() == null && src.getSectionThickness() == null) { + continue; + } + Sample sample = sampleMap.get(src.getExternalIdentifier()); + Slot slot = lw.getSlot(src.getAddress()); + if (src.getSectionThickness() != null) { + measurements.add(new Measurement(null, "Thickness", src.getSectionThickness().toString(), + sample.getId(), op.getId(), slot.getId())); + } + if (src.getDateSectioned() != null) { + measurements.add(new Measurement(null, "Date sectioned", src.getDateSectioned().toString(), + sample.getId(), op.getId(), slot.getId())); + } + } return (measurements.isEmpty() ? measurements : measurementRepo.saveAll(measurements)); } diff --git a/src/main/java/uk/ac/sanger/sccp/stan/service/register/filereader/SectionRegisterFileReader.java b/src/main/java/uk/ac/sanger/sccp/stan/service/register/filereader/SectionRegisterFileReader.java index d46c5b8b..d1944b4f 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/service/register/filereader/SectionRegisterFileReader.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/service/register/filereader/SectionRegisterFileReader.java @@ -6,6 +6,7 @@ import uk.ac.sanger.sccp.stan.service.ValidationException; import java.io.IOException; +import java.time.LocalDate; import java.util.List; import java.util.regex.Pattern; @@ -34,6 +35,7 @@ enum Column implements IColumn { Section_external_ID, Section_number(Integer.class), Section_thickness(Integer.class), + Date_sectioned(LocalDate.class, Pattern.compile("date.*sectioned|section.*date", Pattern.CASE_INSENSITIVE), false), Section_position(Pattern.compile("(if.+)?(section\\s+)?position", Pattern.CASE_INSENSITIVE)), ; @@ -64,7 +66,7 @@ public String toString() { return this.name().replace('_',' '); } - /** The data type (String or Integer) expected in the column. */ + /** The data type (e.g. String or Integer) expected in the column. */ @Override public Class getDataType() { return this.dataType; diff --git a/src/main/java/uk/ac/sanger/sccp/stan/service/register/filereader/SectionRegisterFileReaderImp.java b/src/main/java/uk/ac/sanger/sccp/stan/service/register/filereader/SectionRegisterFileReaderImp.java index 5942754f..167f4541 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/service/register/filereader/SectionRegisterFileReaderImp.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/service/register/filereader/SectionRegisterFileReaderImp.java @@ -9,6 +9,7 @@ import java.io.IOException; import java.nio.file.*; +import java.time.LocalDate; import java.util.*; import static java.util.stream.Collectors.toList; @@ -39,7 +40,7 @@ public SectionRegisterRequest createRequest(Collection problems, List (String) row.get(Column.Work_number)), () -> problems.add("Multiple work numbers specified.")); - if(nullOrEmpty(workNumber)) { + if (nullOrEmpty(workNumber)) { problems.add("Missing work number."); } @@ -114,6 +115,7 @@ public SectionRegisterContent createRequestContent(Collection problems, content.setTissueType((String) row.get(Column.Tissue_type)); content.setSpatialLocation((Integer) row.get(Column.Spatial_location)); content.setRegion((String) row.get(Column.Section_position)); + content.setDateSectioned((LocalDate) row.get(Column.Date_sectioned)); return content; } diff --git a/src/main/java/uk/ac/sanger/sccp/stan/service/releasefile/ReleaseFileService.java b/src/main/java/uk/ac/sanger/sccp/stan/service/releasefile/ReleaseFileService.java index ac75a321..c981c006 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/service/releasefile/ReleaseFileService.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/service/releasefile/ReleaseFileService.java @@ -18,6 +18,8 @@ import uk.ac.sanger.sccp.utils.tsv.TsvColumn; import javax.persistence.EntityNotFoundException; +import java.time.LocalDate; +import java.time.format.DateTimeParseException; import java.util.*; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -514,6 +516,65 @@ public void loadSectionDate(Collection entries, Ancestry ancestry) } }); } + + Map slotSampleSectionDates = findSlotSampleDates( + measurementRepo.findAllBySlotIdInAndName(slotIds, "Date sectioned") + ); + if (!slotSampleSectionDates.isEmpty()) { + for (ReleaseEntry entry : entries) { + if (entry.getSlot() != null && entry.getSample() != null && entry.getSectionDate() == null) { + entry.setSectionDate(findEntrySectionDate(entry, slotSampleSectionDates, ancestry)); + } + } + } + } + + /** + * Loads the section date for the given entry from the given map using the given ancestry + * @param entry the entry to load the date for + * @param slotSampleSectionDates map of slot/sample ids to section date + * @param ancestry the ancestry for the slot/samples + * @return the section date for the given entry + */ + public LocalDate findEntrySectionDate(ReleaseEntry entry, Map slotSampleSectionDates, + Ancestry ancestry) { + SlotSample entrySs = new SlotSample(entry.getSlot(), entry.getSample()); + for (SlotSample ss : ancestry.ancestors(entrySs)) { + LocalDate date = slotSampleSectionDates.get(new SlotIdSampleId(ss.slotId(), ss.sampleId())); + if (date != null) { + return date; + } + } + return null; + } + + /** + * Builds a map from slot/sample ids to the localdate in the given measurements + * @param measurements the measurements to look through + * @return a map from slot/sample ids to the relevant date in the measurements + */ + public Map findSlotSampleDates(Collection measurements) { + if (measurements.isEmpty()) { + return Map.of(); + } + Map map = new HashMap<>(measurements.size()); + for (Measurement measurement : measurements) { + if (measurement.getSampleId()==null || measurement.getSlotId()==null || measurement.getValue()==null) { + continue; + } + LocalDate value; + try { + value = LocalDate.parse(measurement.getValue()); + } catch (DateTimeParseException e) { + continue; + } + SlotIdSampleId key = new SlotIdSampleId(measurement.getSlotId(), measurement.getSampleId()); + LocalDate oldValue = map.get(key); + if (oldValue==null || oldValue.isBefore(value)) { + map.put(key, value); + } + } + return map; } /** diff --git a/src/main/resources/schema.graphqls b/src/main/resources/schema.graphqls index 2ccd4280..77d6be1d 100644 --- a/src/main/resources/schema.graphqls +++ b/src/main/resources/schema.graphqls @@ -336,6 +336,8 @@ input SectionRegisterContent { sectionThickness: Int """The region of this sample in this slot, if any.""" region: String + """The date the sample was sectioned.""" + dateSectioned: Date } """A request to register one or more sections into one piece of labware.""" diff --git a/src/test/java/uk/ac/sanger/sccp/stan/service/register/TestSectionRegisterService.java b/src/test/java/uk/ac/sanger/sccp/stan/service/register/TestSectionRegisterService.java index 2ae3dabe..e0611c6c 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/service/register/TestSectionRegisterService.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/service/register/TestSectionRegisterService.java @@ -1,8 +1,6 @@ package uk.ac.sanger.sccp.stan.service.register; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.*; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mock; @@ -10,20 +8,13 @@ import uk.ac.sanger.sccp.stan.EntityFactory; import uk.ac.sanger.sccp.stan.model.*; import uk.ac.sanger.sccp.stan.repo.*; -import uk.ac.sanger.sccp.stan.request.register.RegisterResult; -import uk.ac.sanger.sccp.stan.request.register.SectionRegisterContent; -import uk.ac.sanger.sccp.stan.request.register.SectionRegisterLabware; -import uk.ac.sanger.sccp.stan.request.register.SectionRegisterRequest; -import uk.ac.sanger.sccp.stan.service.LabwareService; -import uk.ac.sanger.sccp.stan.service.OperationService; -import uk.ac.sanger.sccp.stan.service.ValidationException; +import uk.ac.sanger.sccp.stan.request.register.*; +import uk.ac.sanger.sccp.stan.service.*; import uk.ac.sanger.sccp.stan.service.work.WorkService; import uk.ac.sanger.sccp.utils.UCMap; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; +import java.time.LocalDate; +import java.util.*; import java.util.stream.IntStream; import java.util.stream.Stream; @@ -429,8 +420,10 @@ public void testCreateMeasurements() { content(A1, xns[0], 14), content(A1, xns[1], 15), content(B1, xns[2]), - content(B2, xns[3], 16) + content(B2, xns[3]) ); + contents.get(1).setDateSectioned(LocalDate.of(2024,1,13)); + contents.get(3).setDateSectioned(LocalDate.of(2024,2,14)); UCMap sampleMap = UCMap.from((Sample sam) -> sam.getTissue().getExternalName(), samples); SectionRegisterLabware srl = new SectionRegisterLabware(lw.getExternalBarcode(), lt.getName(), contents); @@ -447,7 +440,8 @@ public void testCreateMeasurements() { verify(mockMeasurementRepo).saveAll(List.of( new Measurement(null, "Thickness", "14", samples[0].getId(), opId, slotA1.getId()), new Measurement(null, "Thickness", "15", samples[1].getId(), opId, slotA1.getId()), - new Measurement(null, "Thickness", "16", samples[3].getId(), opId, slotB2.getId()) + new Measurement(null, "Date sectioned", "2024-01-13", samples[1].getId(), opId, slotA1.getId()), + new Measurement(null, "Date sectioned", "2024-02-14", samples[3].getId(), opId, slotB2.getId()) )); } diff --git a/src/test/java/uk/ac/sanger/sccp/stan/service/register/filereader/TestSectionRegisterFileReader.java b/src/test/java/uk/ac/sanger/sccp/stan/service/register/filereader/TestSectionRegisterFileReader.java index 60de3336..52aa593c 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/service/register/filereader/TestSectionRegisterFileReader.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/service/register/filereader/TestSectionRegisterFileReader.java @@ -129,7 +129,7 @@ void testIndexColumns() { Row row = mockRow("work number", "slide type", "external slide id", "xenium barcode", "section address", "fixative", "embedding medium", "donor id", "life stage", "species", "humfre", "tissue type", "spatial location", "replicate number", - "section external id", "section number", "section thickness", "if bla bla bla position", null, ""); + "section external id", "section number", "section thickness", "date sectioned", "if bla bla bla position", null, ""); List problems = new ArrayList<>(); var result = reader.indexColumns(problems, row); assertThat(problems).isEmpty(); diff --git a/src/test/java/uk/ac/sanger/sccp/stan/service/releasefile/TestReleaseFileService.java b/src/test/java/uk/ac/sanger/sccp/stan/service/releasefile/TestReleaseFileService.java index 4315d6a9..180a4fdf 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/service/releasefile/TestReleaseFileService.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/service/releasefile/TestReleaseFileService.java @@ -17,6 +17,7 @@ import uk.ac.sanger.sccp.stan.service.releasefile.Ancestoriser.SlotSample; import javax.persistence.EntityNotFoundException; +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.*; import java.util.stream.*; @@ -576,12 +577,14 @@ public void testLoadSectionDate() { Labware lw1 = EntityFactory.makeLabware(lt, sample); Labware lw2 = EntityFactory.makeLabware(lt, sample); Labware lw3 = EntityFactory.makeLabware(lt, sample); + Labware lw4 = EntityFactory.makeLabware(lt, sample); Ancestry ancestry = makeAncestry(lw2, sample, lw1, sample, lw1, sample, lw1, sample, lw3, sample, lw3, sample); OperationType opType = EntityFactory.makeOperationType("Section", null); when(mockOpTypeRepo.getByName(any())).thenReturn(opType); List entries = List.of(new ReleaseEntry(lw2, lw2.getFirstSlot(), sample), - new ReleaseEntry(lw3, lw3.getFirstSlot(), sample)); + new ReleaseEntry(lw3, lw3.getFirstSlot(), sample), + new ReleaseEntry(lw4, lw4.getFirstSlot(), sample)); Operation op = new Operation(); op.setPerformed(LocalDateTime.of(2022,1,2, 12, 0)); @@ -592,16 +595,78 @@ public void testLoadSectionDate() { doReturn(lwSectionOpMap).when(service).labwareIdToOp(any()); doReturn(Map.of(entries.getFirst(), op)).when(service).findEntryOps(any(), any(), any()); + List measurements = List.of(new Measurement(10, "Date sectioned", "2024-01-01", sample.getId(), 100, lw4.getFirstSlot().getId())); + when(mockMeasurementRepo.findAllBySlotIdInAndName(any(), any())).thenReturn(measurements); + + Map slotSampleDates = Map.of( + new SlotIdSampleId(lw4.getFirstSlot(), sample), LocalDate.of(2024,1,2) + ); + doReturn(slotSampleDates).when(service).findSlotSampleDates(any()); + LocalDate lw4Date = LocalDate.of(2024,1,13); + doReturn(null).when(service).findEntrySectionDate(any(), any(), any()); + doReturn(lw4Date).when(service).findEntrySectionDate(entries.get(2), slotSampleDates, ancestry); + service.loadSectionDate(entries, ancestry); assertEquals(op.getPerformed().toLocalDate(), entries.get(0).getSectionDate()); assertNull(entries.get(1).getSectionDate()); + assertEquals(lw4Date, entries.get(2).getSectionDate()); verify(mockOpTypeRepo).getByName("Section"); Set slotIds = Stream.of(lw1, lw2, lw3).map(lw -> lw.getFirstSlot().getId()).collect(toSet()); verify(mockOpRepo).findAllByOperationTypeAndDestinationSlotIdIn(opType, slotIds); verify(service).labwareIdToOp(ops); verify(service).findEntryOps(entries, lwSectionOpMap, ancestry); + verify(mockMeasurementRepo).findAllBySlotIdInAndName(slotIds, "Date sectioned"); + verify(service).findSlotSampleDates(measurements); + verify(service).findEntrySectionDate(entries.get(2), slotSampleDates, ancestry); + } + + @Test + public void testFindSlotSampleDates() { + List measurements = List.of( + new Measurement(1, "Date sectioned", "2024-01-13", 10, 1, 11), + new Measurement(2, "Date sectioned", "2024-01-14", 10, 1, 12), + new Measurement(3, "Date sectioned", "2024-01-15", 11, 1, 11), + new Measurement(4, "Date sectioned", "2024-01-16", 11, 1, 11) + ); + Map map = service.findSlotSampleDates(measurements); + assertThat(map).hasSize(3); + assertEquals(LocalDate.of(2024,1,13), map.get(new SlotIdSampleId(11, 10))); + assertEquals(LocalDate.of(2024,1,14), map.get(new SlotIdSampleId(12, 10))); + assertEquals(LocalDate.of(2024,1,16), map.get(new SlotIdSampleId(11, 11))); + } + + @ParameterizedTest + @MethodSource("findEntrySectionDateArgs") + public void testFindEntrySectionDate(ReleaseEntry entry, Map slotSampleDates, + Ancestry ancestry, LocalDate expectedDate) { + assertEquals(expectedDate, service.findEntrySectionDate(entry, slotSampleDates, ancestry)); + } + + static Stream findEntrySectionDateArgs() { + Sample[] samples = EntityFactory.makeSamples(4); + LabwareType lt = EntityFactory.getTubeType(); + Labware[] labware = Arrays.stream(samples) + .map(sam -> EntityFactory.makeLabware(lt, sam)) + .toArray(Labware[]::new); + Slot[] slots = Arrays.stream(labware) + .map(Labware::getFirstSlot) + .toArray(Slot[]::new); + Ancestry ancestry = makeAncestry(labware[0], samples[0], labware[1], samples[1]); + LocalDate[] dates = IntStream.of(13,14) + .mapToObj(d -> LocalDate.of(2024,1,d)) + .toArray(LocalDate[]::new); + Map dateMap = Map.of( + new SlotIdSampleId(slots[1], samples[1]), dates[0], + new SlotIdSampleId(slots[2], samples[2]), dates[1] + ); + ReleaseEntry[] entries = IntStream.range(0, labware.length) + .mapToObj(i -> new ReleaseEntry(labware[i], slots[i], samples[i])) + .toArray(ReleaseEntry[]::new); + LocalDate[] expected = { dates[0], dates[0], dates[1], null }; + return IntStream.range(0, expected.length) + .mapToObj(i -> Arguments.of(entries[i], dateMap, ancestry, expected[i])); } @Test @@ -1428,7 +1493,7 @@ private Operation makeOp(int id, OperationType opType, Labware lw, LocalDateTime return new Operation(id, opType, time, List.of(action), null); } - private Ancestry makeAncestry(Object... args) { + private static Ancestry makeAncestry(Object... args) { Ancestry ancestry = new Ancestry(); for (int i = 0; i < args.length; i += 4) { ancestry.put(slotSample(args[i], args[i+1]), Set.of(slotSample(args[i+2], args[i+3]))); @@ -1436,11 +1501,11 @@ private Ancestry makeAncestry(Object... args) { return ancestry; } - private SlotSample slotSample(Object arg1, Object arg2) { + private static SlotSample slotSample(Object arg1, Object arg2) { return new SlotSample(slot(arg1), (Sample) arg2); } - private Slot slot(Object arg) { + private static Slot slot(Object arg) { if (arg instanceof Labware) { return ((Labware) arg).getFirstSlot(); }