From a110bd192117f9debf41058694b93bbd5c80f28d Mon Sep 17 00:00:00 2001 From: David Robinson <14000840+khelwood@users.noreply.github.com> Date: Wed, 11 Sep 2024 11:45:35 +0100 Subject: [PATCH] x1237: Add reagent lot to cell segmentation op --- .../sccp/stan/config/FieldValidation.java | 5 ++ .../stan/request/SegmentationRequest.java | 14 ++++- .../stan/service/SegmentationServiceImp.java | 34 ++++++++++- src/main/resources/schema.graphqls | 2 + .../TestSegmentationMutation.java | 12 ++++ .../stan/service/TestSegmentationService.java | 56 +++++++++++++++++-- .../resources/graphql/segmentation.graphql | 1 + 7 files changed, 114 insertions(+), 10 deletions(-) diff --git a/src/main/java/uk/ac/sanger/sccp/stan/config/FieldValidation.java b/src/main/java/uk/ac/sanger/sccp/stan/config/FieldValidation.java index 54d59a12..9d8174a8 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/config/FieldValidation.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/config/FieldValidation.java @@ -295,6 +295,11 @@ public Validator samplePrepReagentLotValidator() { return new StringValidator("Sample prep reagent lot", 6, 6, CharacterType.DIGIT); } + @Bean + public Validator reagentLotValidator() { + return new StringValidator("Reagent lot", 6, 6, CharacterType.DIGIT); + } + @Bean public Validator cytAssistBarcodeValidator() { Set charTypes = EnumSet.of(CharacterType.DIGIT, CharacterType.ALPHA, CharacterType.HYPHEN); diff --git a/src/main/java/uk/ac/sanger/sccp/stan/request/SegmentationRequest.java b/src/main/java/uk/ac/sanger/sccp/stan/request/SegmentationRequest.java index 23c59732..e246e005 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/request/SegmentationRequest.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/request/SegmentationRequest.java @@ -78,6 +78,7 @@ public static class SegmentationLabware { private List commentIds = List.of(); private SlideCosting costing; private LocalDateTime performed; + private String reagentLot; /** * The barcode of the labware. @@ -134,6 +135,14 @@ public void setPerformed(LocalDateTime performed) { this.performed = performed; } + public String getReagentLot() { + return this.reagentLot; + } + + public void setReagentLot(String reagentLot) { + this.reagentLot = reagentLot; + } + @Override public String toString() { return BasicUtils.describe(this) @@ -142,6 +151,7 @@ public String toString() { .add("commentIds", commentIds) .add("costing", costing) .add("performed", performed==null ? null : performed.toString()) + .add("reagentLot", reagentLot) .reprStringValues() .toString(); } @@ -155,7 +165,9 @@ public boolean equals(Object o) { && Objects.equals(this.workNumber, that.workNumber) && Objects.equals(this.commentIds, that.commentIds) && this.costing == that.costing - && Objects.equals(this.performed, that.performed)); + && Objects.equals(this.performed, that.performed) + && Objects.equals(this.reagentLot, that.reagentLot) + ); } @Override diff --git a/src/main/java/uk/ac/sanger/sccp/stan/service/SegmentationServiceImp.java b/src/main/java/uk/ac/sanger/sccp/stan/service/SegmentationServiceImp.java index 11c949dd..86422a3b 100644 --- a/src/main/java/uk/ac/sanger/sccp/stan/service/SegmentationServiceImp.java +++ b/src/main/java/uk/ac/sanger/sccp/stan/service/SegmentationServiceImp.java @@ -1,5 +1,6 @@ package uk.ac.sanger.sccp.stan.service; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; import uk.ac.sanger.sccp.stan.model.*; import uk.ac.sanger.sccp.stan.repo.*; @@ -14,11 +15,11 @@ import java.time.*; import java.util.*; +import java.util.function.Consumer; import java.util.stream.Stream; import static java.util.stream.Collectors.toSet; -import static uk.ac.sanger.sccp.utils.BasicUtils.inMap; -import static uk.ac.sanger.sccp.utils.BasicUtils.nullOrEmpty; +import static uk.ac.sanger.sccp.utils.BasicUtils.*; /** * @author dr6 @@ -39,10 +40,13 @@ public class SegmentationServiceImp implements SegmentationService { private final OperationCommentRepo opComRepo; private final LabwareNoteRepo noteRepo; + private final Validator reagentLotValidator; + public SegmentationServiceImp(Clock clock, ValidationHelperFactory valHelperFactory, OperationService opService, WorkService workService, OperationRepo opRepo, OperationTypeRepo opTypeRepo, - OperationCommentRepo opComRepo, LabwareNoteRepo noteRepo) { + OperationCommentRepo opComRepo, LabwareNoteRepo noteRepo, + @Qualifier("reagentLotValidator") Validator reagentLotValidator) { this.clock = clock; this.valHelperFactory = valHelperFactory; this.opService = opService; @@ -51,6 +55,7 @@ public SegmentationServiceImp(Clock clock, ValidationHelperFactory valHelperFact this.opTypeRepo = opTypeRepo; this.opComRepo = opComRepo; this.noteRepo = noteRepo; + this.reagentLotValidator = reagentLotValidator; } @Override @@ -92,6 +97,7 @@ SegmentationData validate(User user, SegmentationRequest request) { checkCostings(problems, data.opType, request.getLabware()); UCMap priorOpTimes = checkPriorOps(problems, data.opType, data.labware.values()); checkTimestamps(val, clock, request.getLabware(), data.labware, priorOpTimes); + checkReagentLots(problems, request.getLabware()); return data; } @@ -221,6 +227,25 @@ void checkTimestamps(ValidationHelper val, Clock clock, List problems, final Collection lwReqs) { + final Consumer problemAdd = problems::add; + for (SegmentationLabware lwReq : lwReqs) { + String lot = lwReq.getReagentLot(); + if (lot != null) { + lot = emptyToNull(lot.trim()); + lwReq.setReagentLot(lot); + } + if (lot != null) { + reagentLotValidator.validate(lot, problemAdd); + } + } + } + /** * Records the operations and all associated information for the request * @param lwReqs details of the request @@ -281,6 +306,9 @@ Operation recordOp(User user, SegmentationData data, SegmentationLabware lwReq, newNotes.add(new LabwareNote(null, lw.getId(), op.getId(), "costing", lwReq.getCosting().name())); } + if (lwReq.getReagentLot()!=null) { + newNotes.add(new LabwareNote(null, lw.getId(), op.getId(), "reagent lot", lwReq.getReagentLot())); + } if (!lwReq.getCommentIds().isEmpty()) { lwReq.getCommentIds().stream() .map(data.comments::get) diff --git a/src/main/resources/schema.graphqls b/src/main/resources/schema.graphqls index f5e974d6..500aa143 100644 --- a/src/main/resources/schema.graphqls +++ b/src/main/resources/schema.graphqls @@ -1908,6 +1908,8 @@ input SegmentationLabware { costing: SlideCosting """The time with which the operation should be recorded.""" performed: Timestamp + """The reagent lot number.""" + reagentLot: String } """A request to record segmentation on one or more labware.""" diff --git a/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestSegmentationMutation.java b/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestSegmentationMutation.java index 787a1aea..c3bc4a1d 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestSegmentationMutation.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/integrationtest/TestSegmentationMutation.java @@ -9,9 +9,12 @@ import uk.ac.sanger.sccp.stan.EntityCreator; import uk.ac.sanger.sccp.stan.GraphQLTester; import uk.ac.sanger.sccp.stan.model.*; +import uk.ac.sanger.sccp.stan.repo.LabwareNoteRepo; +import uk.ac.sanger.sccp.utils.UCMap; import javax.persistence.EntityManager; import javax.transaction.Transactional; +import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -32,6 +35,8 @@ public class TestSegmentationMutation { private EntityCreator entityCreator; @Autowired private EntityManager entityManager; + @Autowired + private LabwareNoteRepo lwNoteRepo; @Test @Transactional @@ -68,6 +73,13 @@ public void testSegmentation() throws Exception { new Work.SampleSlotId(sam2.getId(), slot2.getId()) ); + List notes = lwNoteRepo.findAllByOperationIdIn(List.of(opId)); + assertThat(notes).hasSize(2); + notes.forEach(note -> assertEquals(lw.getId(), note.getLabwareId())); + UCMap noteValues = notes.stream().collect(UCMap.toUCMap(LabwareNote::getName, LabwareNote::getValue)); + assertEquals("Faculty", noteValues.get("costing")); + assertEquals("123456", noteValues.get("reagent lot")); + testSegmentationQC(work, lw); } diff --git a/src/test/java/uk/ac/sanger/sccp/stan/service/TestSegmentationService.java b/src/test/java/uk/ac/sanger/sccp/stan/service/TestSegmentationService.java index 2a93fb29..92736288 100644 --- a/src/test/java/uk/ac/sanger/sccp/stan/service/TestSegmentationService.java +++ b/src/test/java/uk/ac/sanger/sccp/stan/service/TestSegmentationService.java @@ -17,9 +17,11 @@ import uk.ac.sanger.sccp.stan.service.work.WorkService; import uk.ac.sanger.sccp.stan.service.work.WorkService.WorkOp; import uk.ac.sanger.sccp.utils.UCMap; +import uk.ac.sanger.sccp.utils.Zip; import java.time.*; import java.util.*; +import java.util.function.Consumer; import java.util.function.Predicate; import java.util.stream.IntStream; import java.util.stream.Stream; @@ -52,6 +54,8 @@ class TestSegmentationService { private OperationCommentRepo mockOpComRepo; @Mock private LabwareNoteRepo mockNoteRepo; + @Mock + private Validator mockReagentLotValidator; @InjectMocks private SegmentationServiceImp service; @@ -362,6 +366,35 @@ void testCheckPriorOps(boolean anyMissing) { } } + @Test + public void testCheckReagentLots() { + final List problems = new ArrayList<>(2); + when(mockReagentLotValidator.validate(any(), any())).then(invocation -> { + String lot = invocation.getArgument(0); + if (lot != null && lot.indexOf('!') >= 0) { + Consumer addProblem = invocation.getArgument(1); + addProblem.accept("Bad lot: "+lot); + return false; + } + return true; + }); + String[] inputLots = {null, " ", "", "123456", " 23456 ", "Alpha!", "Beta! "}; + String[] expectedLots = {null, null, null, "123456", "23456", "Alpha!", "Beta!"}; + List sls = Arrays.stream(inputLots) + .map(lot -> { + SegmentationLabware sl = new SegmentationLabware(); + sl.setReagentLot(lot); + return sl; + }) + .toList(); + service.checkReagentLots(problems, sls); + verify(mockReagentLotValidator, times(4)).validate(any(), any()); + Arrays.stream(expectedLots).filter(Objects::nonNull) + .forEach(lot -> verify(mockReagentLotValidator).validate(eq(lot), any())); + Zip.forEach(sls.stream(), Arrays.stream(expectedLots), (sl, lot) -> assertEquals(lot, sl.getReagentLot())); + assertThat(problems).containsExactlyInAnyOrder("Bad lot: Alpha!", "Bad lot: Beta!"); + } + @Test void testGreater() { for (Object[] args : new Object[][]{ @@ -480,12 +513,13 @@ void testRecord() { @ParameterizedTest @CsvSource({ - "false,false,false,false", - "false,true,false,true", - "false,false,true,true", - "true,true,false,false", + "false,false,false,false,false", + "false,true,false,true,false", + "false,false,true,true,false", + "true,true,false,false,false", + "true,true,false,false,true", }) - void testRecordOp(boolean hasTime, boolean hasCosting, boolean hasCommentIds, boolean hasWork) { + void testRecordOp(boolean hasTime, boolean hasCosting, boolean hasCommentIds, boolean hasWork, boolean hasLot) { SegmentationLabware lwReq = new SegmentationLabware(); SegmentationData data = new SegmentationData(List.of()); data.opType = EntityFactory.makeOperationType("opname", null); @@ -519,6 +553,9 @@ void testRecordOp(boolean hasTime, boolean hasCosting, boolean hasCommentIds, bo } else { work = null; } + if (hasLot) { + lwReq.setReagentLot("123456"); + } when(mockOpService.createOperationInPlace(data.opType, user, lw, null, null)).thenReturn(op); @@ -531,7 +568,14 @@ void testRecordOp(boolean hasTime, boolean hasCosting, boolean hasCommentIds, bo verify(mockOpService).createOperationInPlace(data.opType, user, lw, null, null); assertMayContain(opsToUpdate, hasTime ? op : null); - assertMayContain(newNotes, hasCosting ? new LabwareNote(null, lw.getId(), op.getId(), "costing", "Faculty") : null); + final List expectedNotes = new ArrayList<>(2); + if (hasCosting) { + expectedNotes.add(new LabwareNote(null, lw.getId(), op.getId(), "costing", "Faculty")); + } + if (hasLot) { + expectedNotes.add(new LabwareNote(null, lw.getId(), op.getId(), "reagent lot", "123456")); + } + assertThat(newNotes).containsExactlyInAnyOrderElementsOf(expectedNotes); assertMayContain(newWorkOps, hasWork ? new WorkOp(work, op) : null); if (hasCommentIds) { final int slotId = lw.getFirstSlot().getId(); diff --git a/src/test/resources/graphql/segmentation.graphql b/src/test/resources/graphql/segmentation.graphql index 3b4eb4f7..af5a5ea2 100644 --- a/src/test/resources/graphql/segmentation.graphql +++ b/src/test/resources/graphql/segmentation.graphql @@ -6,6 +6,7 @@ mutation { workNumber: "[WORK]" commentIds: [1] costing: Faculty + reagentLot: "123456" }] }) { labware {