From ede65829e443235800defea1f2f59864132f3f48 Mon Sep 17 00:00:00 2001 From: Sol Dubock <94075844+sjd210@users.noreply.github.com> Date: Thu, 16 Oct 2025 15:05:53 +0100 Subject: [PATCH 01/38] Add alternate state logic for LLM question parts Since an LLM-marked question may have multiple marks per question rather than a binary correct/incorrect --- .../dtg/isaac/api/managers/GameManager.java | 80 ++++++++++++++----- 1 file changed, 62 insertions(+), 18 deletions(-) diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/GameManager.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/GameManager.java index 40945ba2a8..d7b078f984 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/GameManager.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/GameManager.java @@ -29,9 +29,11 @@ import uk.ac.cam.cl.dtg.isaac.dos.AudienceContext; import uk.ac.cam.cl.dtg.isaac.dos.GameboardContentDescriptor; import uk.ac.cam.cl.dtg.isaac.dos.GameboardCreationMethod; +import uk.ac.cam.cl.dtg.isaac.dos.IsaacLLMFreeTextQuestion; import uk.ac.cam.cl.dtg.isaac.dos.IsaacQuestionPage; import uk.ac.cam.cl.dtg.isaac.dos.IsaacQuickQuestion; import uk.ac.cam.cl.dtg.isaac.dos.IsaacWildcard; +import uk.ac.cam.cl.dtg.isaac.dos.LLMFreeTextQuestionValidationResponse; import uk.ac.cam.cl.dtg.isaac.dos.LightweightQuestionValidationResponse; import uk.ac.cam.cl.dtg.isaac.dos.QuestionValidationResponse; import uk.ac.cam.cl.dtg.isaac.dos.content.Content; @@ -1148,33 +1150,75 @@ private void augmentGameItemWithAttemptInformation( for (Content questionPart : listOfQuestionParts) { List questionPartAttempts = questionAttempts.get(questionPart.getId()); - if (questionPartAttempts != null) { - // Go through the attempts in reverse chronological order for this question part to determine if - // there is a correct answer somewhere. - boolean foundCorrectForThisQuestion = false; - for (int i = questionPartAttempts.size() - 1; i >= 0; i--) { - if (questionPartAttempts.get(i).isCorrect() != null - && questionPartAttempts.get(i).isCorrect()) { - foundCorrectForThisQuestion = true; - break; + log.warn("queue {}", questionPart); + if (Objects.equals(questionPart.getType(), "isaacLLMFreeTextQuestion")) { + int maxMarks = ((IsaacLLMFreeTextQuestion) questionPart).getMaxMarks(); + if (questionPartAttempts != null) { + // Go through the attempts for this question part to determine the attempt with the + // greatest number of marks awarded. + int greatestMarksForThisQuestion = 0; + for (LightweightQuestionValidationResponse attempt: questionPartAttempts) { + log.warn(attempt.toString()); + if (attempt instanceof LLMFreeTextQuestionValidationResponse) { + LLMFreeTextQuestionValidationResponse llmFreeTextAttempt = (LLMFreeTextQuestionValidationResponse) attempt; + if (llmFreeTextAttempt.getMarksAwarded() > greatestMarksForThisQuestion) { + greatestMarksForThisQuestion = llmFreeTextAttempt.getMarksAwarded(); + } + } + } + for (int markCorrect = 0; markCorrect < greatestMarksForThisQuestion; markCorrect++) { + questionPartStates.add(QuestionPartState.CORRECT); + questionPartsCorrect++; + } + for (int markIncorrect = 0; markIncorrect < (maxMarks - greatestMarksForThisQuestion); markIncorrect++) { + questionPartStates.add(QuestionPartState.INCORRECT); + questionPartsIncorrect++; + } + } else { + for (int mark = 0; mark < maxMarks; mark++) { + questionPartStates.add(QuestionPartState.NOT_ATTEMPTED); + questionPartsNotAttempted++; } } - if (foundCorrectForThisQuestion) { - questionPartStates.add(QuestionPartState.CORRECT); - questionPartsCorrect++; + } + else { + if (questionPartAttempts != null) { + // Go through the attempts in reverse chronological order for this question part to determine if + // there is a correct answer somewhere. + boolean foundCorrectForThisQuestion = false; + for (int i = questionPartAttempts.size() - 1; i >= 0; i--) { + if (questionPartAttempts.get(i).isCorrect() != null + && questionPartAttempts.get(i).isCorrect()) { + foundCorrectForThisQuestion = true; + break; + } + } + if (foundCorrectForThisQuestion) { + questionPartStates.add(QuestionPartState.CORRECT); + questionPartsCorrect++; + } else { + questionPartStates.add(QuestionPartState.INCORRECT); + questionPartsIncorrect++; + } } else { - questionPartStates.add(QuestionPartState.INCORRECT); - questionPartsIncorrect++; + questionPartStates.add(QuestionPartState.NOT_ATTEMPTED); + questionPartsNotAttempted++; + } + } + } + } else { + for (Content questionPart : listOfQuestionParts) { + if (questionPart instanceof IsaacLLMFreeTextQuestion) { + int maxMarks = ((IsaacLLMFreeTextQuestion) questionPart).getMaxMarks(); + for (int mark = 0; mark < maxMarks; mark++) { + questionPartStates.add(QuestionPartState.NOT_ATTEMPTED); + questionPartsNotAttempted++; } } else { questionPartStates.add(QuestionPartState.NOT_ATTEMPTED); questionPartsNotAttempted++; } } - } else { - questionPartsNotAttempted = listOfQuestionParts.size(); - questionPartStates = listOfQuestionParts.stream() - .map(_q -> QuestionPartState.NOT_ATTEMPTED).collect(Collectors.toList()); } // Get the pass mark for the question page From 1bfd46ece0c52254e24b00282e83401ab5c20ed0 Mon Sep 17 00:00:00 2001 From: Sol Dubock <94075844+sjd210@users.noreply.github.com> Date: Tue, 21 Oct 2025 11:47:46 +0100 Subject: [PATCH 02/38] Add function to extract attempts by question ID We previously allowed extracting all attempts for a page, but not for an individual question part. This adds that functionality. --- .../isaac/quiz/IQuestionAttemptManager.java | 12 +++++++++++ .../cl/dtg/isaac/quiz/PgQuestionAttempts.java | 20 +++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/IQuestionAttemptManager.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/IQuestionAttemptManager.java index 71b9dd99cd..1a756570da 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/IQuestionAttemptManager.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/IQuestionAttemptManager.java @@ -70,6 +70,18 @@ Map>> getLightwe Map>> getQuestionAttempts(Long userId, String questionPageId) throws SegueDatabaseException; + /** + * Get a users question attempts on a specific question part. + * + * @param userId - the id of the user to search for. + * @param questionId - the id of the question part. + * @return the questionAttempts map or an empty map if the user has not yet registered any attempts. + * @throws SegueDatabaseException + * - If there is a database error. + */ + Map>> getQuestionAttemptsByQuestionId(final Long userId, String questionId) + throws SegueDatabaseException; + /** * A method that makes a single database request for a group of users and questions to get all of their attempt * information back. diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java index f48ad5ef73..d8cf405641 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java @@ -253,6 +253,26 @@ public Map>> getQuestionAtt } } + @Override + public Map>> getQuestionAttemptsByQuestionId(final Long userId, final String questionId) + throws SegueDatabaseException { + String query = "SELECT * FROM question_attempts WHERE user_id = ? AND question_id = ? ORDER BY \"timestamp\" ASC"; + try (Connection conn = database.getDatabaseConnection(); + PreparedStatement pst = conn.prepareStatement(query); + ) { + pst.setLong(1, userId); + pst.setString(2, questionId); + + try (ResultSet results = pst.executeQuery()) { + return resultsToMapValidationResponseByPagePart(results); + } + } catch (SQLException e) { + throw new SegueDatabaseException("Postgres exception", e); + } catch (IOException e) { + throw new SegueDatabaseException("Exception while parsing json", e); + } + } + public Map>>> getLightweightQuestionAttemptsByUsers(final List userIds) throws SegueDatabaseException { From 2a4b1bf2c2b177d4db3c9ceb0b447ef086b837e7 Mon Sep 17 00:00:00 2001 From: Sol Dubock <94075844+sjd210@users.noreply.github.com> Date: Tue, 21 Oct 2025 15:29:14 +0100 Subject: [PATCH 03/38] Read marksAwarded value for LLMFreeText responses In the context of the markbook/assignment progress, LLMFreeTextQuestionValidationResponses have a marksAwarded field that we need to extract the full question attempt to read, but we want to keep these attempts lightweight for all other question parts to minimise processing unnecessary data. This change checks each question part and extracts the full response for only LLMFreeText ones. --- .../dtg/isaac/api/managers/GameManager.java | 9 ++-- .../isaac/quiz/IQuestionAttemptManager.java | 3 +- .../cl/dtg/isaac/quiz/PgQuestionAttempts.java | 15 +++++- .../segue/api/managers/QuestionManager.java | 53 ++++++++++++++++++- .../api/AbstractIsaacIntegrationTest.java | 10 ++-- 5 files changed, 76 insertions(+), 14 deletions(-) diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/GameManager.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/GameManager.java index d7b078f984..a6113eb52d 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/GameManager.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/GameManager.java @@ -53,6 +53,7 @@ import uk.ac.cam.cl.dtg.isaac.dto.content.QuestionDTO; import uk.ac.cam.cl.dtg.isaac.dto.users.AbstractSegueUserDTO; import uk.ac.cam.cl.dtg.isaac.dto.users.RegisteredUserDTO; +import uk.ac.cam.cl.dtg.isaac.quiz.IQuestionAttemptManager; import uk.ac.cam.cl.dtg.segue.api.managers.QuestionManager; import uk.ac.cam.cl.dtg.segue.dao.ResourceNotFoundException; import uk.ac.cam.cl.dtg.segue.dao.SegueDatabaseException; @@ -652,7 +653,7 @@ public List>> gatherGamePro Map>>> questionAttemptsForAllUsersOfInterest = - questionManager.getMatchingLightweightQuestionAttempts(users, questionPageIds); + questionManager.getMatchingDecoratedLightweightQuestionAttempts(users, questionPageIds); for (RegisteredUserDTO user : users) { List userGameItems = Lists.newArrayList(); @@ -1150,15 +1151,11 @@ private void augmentGameItemWithAttemptInformation( for (Content questionPart : listOfQuestionParts) { List questionPartAttempts = questionAttempts.get(questionPart.getId()); - log.warn("queue {}", questionPart); if (Objects.equals(questionPart.getType(), "isaacLLMFreeTextQuestion")) { int maxMarks = ((IsaacLLMFreeTextQuestion) questionPart).getMaxMarks(); - if (questionPartAttempts != null) { - // Go through the attempts for this question part to determine the attempt with the - // greatest number of marks awarded. + if (questionPartAttempts != null && !questionPartAttempts.isEmpty()) { int greatestMarksForThisQuestion = 0; for (LightweightQuestionValidationResponse attempt: questionPartAttempts) { - log.warn(attempt.toString()); if (attempt instanceof LLMFreeTextQuestionValidationResponse) { LLMFreeTextQuestionValidationResponse llmFreeTextAttempt = (LLMFreeTextQuestionValidationResponse) attempt; if (llmFreeTextAttempt.getMarksAwarded() > greatestMarksForThisQuestion) { diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/IQuestionAttemptManager.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/IQuestionAttemptManager.java index 1a756570da..a1a48bae0a 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/IQuestionAttemptManager.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/IQuestionAttemptManager.java @@ -1,5 +1,6 @@ package uk.ac.cam.cl.dtg.isaac.quiz; +import uk.ac.cam.cl.dtg.isaac.dos.LLMFreeTextQuestionValidationResponse; import uk.ac.cam.cl.dtg.isaac.dos.LightweightQuestionValidationResponse; import uk.ac.cam.cl.dtg.isaac.dos.QuestionValidationResponse; import uk.ac.cam.cl.dtg.isaac.dos.users.Role; @@ -79,7 +80,7 @@ Map>> getQuestionAttempts(L * @throws SegueDatabaseException * - If there is a database error. */ - Map>> getQuestionAttemptsByQuestionId(final Long userId, String questionId) + List getQuestionAttemptsByQuestionId(final Long userId, String questionId) throws SegueDatabaseException; /** diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java index d8cf405641..da0df7ec04 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java @@ -22,6 +22,7 @@ import com.google.inject.Inject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import uk.ac.cam.cl.dtg.isaac.dos.LLMFreeTextQuestionValidationResponse; import uk.ac.cam.cl.dtg.isaac.dos.LightweightQuestionValidationResponse; import uk.ac.cam.cl.dtg.isaac.dos.QuestionValidationResponse; import uk.ac.cam.cl.dtg.isaac.dos.users.Role; @@ -43,6 +44,7 @@ import java.util.Collections; import java.util.Date; import java.util.HashMap; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; @@ -254,7 +256,7 @@ public Map>> getQuestionAtt } @Override - public Map>> getQuestionAttemptsByQuestionId(final Long userId, final String questionId) + public List getQuestionAttemptsByQuestionId(final Long userId, final String questionId) throws SegueDatabaseException { String query = "SELECT * FROM question_attempts WHERE user_id = ? AND question_id = ? ORDER BY \"timestamp\" ASC"; try (Connection conn = database.getDatabaseConnection(); @@ -262,9 +264,18 @@ public Map>> getQuestionAtt ) { pst.setLong(1, userId); pst.setString(2, questionId); + log.warn("ick {} {}", userId, questionId); try (ResultSet results = pst.executeQuery()) { - return resultsToMapValidationResponseByPagePart(results); + List c = new LinkedList<>(); + + while (results.next()) { + LLMFreeTextQuestionValidationResponse questionAttempt = objectMapper.readValue( + results.getString("question_attempt"), LLMFreeTextQuestionValidationResponse.class); + + c.add(questionAttempt); + } + return c; } } catch (SQLException e) { throw new SegueDatabaseException("Postgres exception", e); diff --git a/src/main/java/uk/ac/cam/cl/dtg/segue/api/managers/QuestionManager.java b/src/main/java/uk/ac/cam/cl/dtg/segue/api/managers/QuestionManager.java index b6ebf70409..47bd745e68 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/segue/api/managers/QuestionManager.java +++ b/src/main/java/uk/ac/cam/cl/dtg/segue/api/managers/QuestionManager.java @@ -26,6 +26,8 @@ import org.joda.time.LocalDate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import uk.ac.cam.cl.dtg.isaac.dos.IsaacQuestionPage; +import uk.ac.cam.cl.dtg.isaac.dos.LLMFreeTextQuestionValidationResponse; import uk.ac.cam.cl.dtg.isaac.dos.LightweightQuestionValidationResponse; import uk.ac.cam.cl.dtg.isaac.dos.QuestionValidationResponse; import uk.ac.cam.cl.dtg.isaac.dos.TestCase; @@ -61,13 +63,16 @@ import uk.ac.cam.cl.dtg.segue.api.ErrorResponseWrapper; import uk.ac.cam.cl.dtg.segue.configuration.SegueGuiceConfigurationModule; import uk.ac.cam.cl.dtg.segue.dao.SegueDatabaseException; +import uk.ac.cam.cl.dtg.segue.dao.content.ContentManagerException; import uk.ac.cam.cl.dtg.segue.dao.content.ContentMapper; +import uk.ac.cam.cl.dtg.segue.dao.content.GitContentManager; import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.core.Response; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.List; @@ -75,6 +80,7 @@ import java.util.Objects; import java.util.Random; +import static uk.ac.cam.cl.dtg.isaac.api.managers.GameManager.getAllMarkableDOQuestionPartsDFSOrder; import static uk.ac.cam.cl.dtg.segue.api.monitors.SegueMetrics.VALIDATOR_LATENCY_HISTOGRAM; /** @@ -89,6 +95,8 @@ public class QuestionManager { private final ContentMapper mapper; private final IQuestionAttemptManager questionAttemptPersistenceManager; + private final GitContentManager contentManager; + /** * Create a default Question manager object. * @@ -97,9 +105,13 @@ public class QuestionManager { * @param questionPersistenceManager - for question attempt persistence. */ @Inject - public QuestionManager(final ContentMapper mapper, final IQuestionAttemptManager questionPersistenceManager) { + public QuestionManager( + final ContentMapper mapper, final IQuestionAttemptManager questionPersistenceManager, + final GitContentManager contentManager + ) { this.mapper = mapper; this.questionAttemptPersistenceManager = questionPersistenceManager; + this.contentManager = contentManager; } /** @@ -441,6 +453,45 @@ public Map>>> getMatchingDecoratedLightweightQuestionAttempts( + final List users, final List questionPageIds) throws SegueDatabaseException, ContentManagerException { + Map>>> a = getMatchingLightweightQuestionAttempts(users, questionPageIds); + + // Loop over all users, question pages, and question part, and if the question part is an LLMFreeTextQuestion, log.warn("LLM") + for (Map.Entry>>> userEntry : a.entrySet()) { + Long userId = userEntry.getKey(); + Map>> questionPages = userEntry.getValue(); + + for (Map.Entry>> pageEntry : questionPages.entrySet()) { + String questionPageId = pageEntry.getKey(); + Map> questionPartResponseMap = pageEntry.getValue(); + + IsaacQuestionPage questionPage = (IsaacQuestionPage) this.contentManager.getContentDOById(questionPageId); + // get all question parts in the question page: depends on each question + // having an id that starts with the question page id. + Collection listOfQuestionParts = getAllMarkableDOQuestionPartsDFSOrder(questionPage); + + for (Question questionPart : listOfQuestionParts) { + if (Objects.equals(questionPart.getType(), "isaacLLMFreeTextQuestion")) { + String questionPartId = questionPart.getId(); + List decoratedQuestionValidationResponses = this.questionAttemptPersistenceManager.getQuestionAttemptsByQuestionId(userId, questionPartId); + List lightweightList = new ArrayList<>(decoratedQuestionValidationResponses); + questionPartResponseMap.put(questionPartId, lightweightList); + // questionPartResponseMap.put(questionPartId, decoratedQuestionValidationResponses); + } + } + } + } + + return a; + } + /** * Helper method for attempts from a single user. * diff --git a/src/test/java/uk/ac/cam/cl/dtg/isaac/api/AbstractIsaacIntegrationTest.java b/src/test/java/uk/ac/cam/cl/dtg/isaac/api/AbstractIsaacIntegrationTest.java index 96652e6da3..5f43e6ae9b 100644 --- a/src/test/java/uk/ac/cam/cl/dtg/isaac/api/AbstractIsaacIntegrationTest.java +++ b/src/test/java/uk/ac/cam/cl/dtg/isaac/api/AbstractIsaacIntegrationTest.java @@ -243,8 +243,13 @@ public static void setUpClass() throws Exception { passwordDataManager = new PgPasswordDataManager(postgresSqlDb); ContentMapper contentMapper = new ContentMapper(new Reflections("uk.ac.cam.cl.dtg")); + + Git git = createNiceMock(Git.class); + GitDb gitDb = new GitDb(git); + contentManager = new GitContentManager(gitDb, elasticSearchProvider, contentMapper, properties); + PgQuestionAttempts pgQuestionAttempts = new PgQuestionAttempts(postgresSqlDb, contentMapper); - questionManager = new QuestionManager(contentMapper, pgQuestionAttempts); + questionManager = new QuestionManager(contentMapper, pgQuestionAttempts, contentManager); mapperFacade = contentMapper.getAutoMapper(); @@ -264,9 +269,6 @@ public static void setUpClass() throws Exception { EmailCommunicator communicator = new EmailCommunicator("localhost", "default@localhost", "Howdy!"); userPreferenceManager = new PgUserPreferenceManager(postgresSqlDb); - Git git = createNiceMock(Git.class); - GitDb gitDb = new GitDb(git); - contentManager = new GitContentManager(gitDb, elasticSearchProvider, contentMapper, properties); logManager = createNiceMock(ILogManager.class); IDeletionTokenPersistenceManager deletionTokenPersistenceManager = new PgDeletionTokenPersistenceManager(postgresSqlDb); From 983de47e07b8246f98a22818cbce3a9dc61fdba7 Mon Sep 17 00:00:00 2001 From: Sol Dubock <94075844+sjd210@users.noreply.github.com> Date: Tue, 21 Oct 2025 16:35:06 +0100 Subject: [PATCH 04/38] Change question correctness threshold to maxMarks --- .../uk/ac/cam/cl/dtg/isaac/quiz/IsaacLLMFreeTextValidator.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/IsaacLLMFreeTextValidator.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/IsaacLLMFreeTextValidator.java index a811eec313..333053c05c 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/IsaacLLMFreeTextValidator.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/IsaacLLMFreeTextValidator.java @@ -276,12 +276,13 @@ private int evaluateMarkTotal(final IsaacLLMFreeTextQuestion question, final Map * @param question the question being marked so that we can return the mark scheme. * @param answer the user's attempt at the question. * @param awardedMarks the marks awarded for each field in the mark scheme according to the LLM response. + * @param markTotal the calculated mark value based on which individual marks were awarded * @return a response to the user's attempt at the question. */ private LLMFreeTextQuestionValidationResponse generateQuestionValidationResponse( final IsaacLLMFreeTextQuestion question, final Choice answer, final Map awardedMarks, final int markTotal) { - boolean isConsideredCorrect = markTotal > 0; + boolean isConsideredCorrect = markTotal == question.getMaxMarks(); // We create a fresh copy of the mark scheme with the full description and the awarded mark values. List markBreakdown = question.getMarkScheme().stream().map(mark -> { From 501149df8e353837eafc169245483766248aa839 Mon Sep 17 00:00:00 2001 From: Sol Dubock <94075844+sjd210@users.noreply.github.com> Date: Tue, 21 Oct 2025 16:35:47 +0100 Subject: [PATCH 05/38] Clean up experimental code --- .../isaac/quiz/IQuestionAttemptManager.java | 2 +- .../cl/dtg/isaac/quiz/PgQuestionAttempts.java | 11 ++++----- .../segue/api/managers/QuestionManager.java | 23 ++++++++----------- 3 files changed, 16 insertions(+), 20 deletions(-) diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/IQuestionAttemptManager.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/IQuestionAttemptManager.java index a1a48bae0a..48c4c744a6 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/IQuestionAttemptManager.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/IQuestionAttemptManager.java @@ -80,7 +80,7 @@ Map>> getQuestionAttempts(L * @throws SegueDatabaseException * - If there is a database error. */ - List getQuestionAttemptsByQuestionId(final Long userId, String questionId) + List getQuestionAttemptsByQuestionId(final Long userId, String questionId) throws SegueDatabaseException; /** diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java index da0df7ec04..9e2183b964 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java @@ -256,7 +256,7 @@ public Map>> getQuestionAtt } @Override - public List getQuestionAttemptsByQuestionId(final Long userId, final String questionId) + public List getQuestionAttemptsByQuestionId(final Long userId, final String questionId) throws SegueDatabaseException { String query = "SELECT * FROM question_attempts WHERE user_id = ? AND question_id = ? ORDER BY \"timestamp\" ASC"; try (Connection conn = database.getDatabaseConnection(); @@ -264,19 +264,18 @@ public List getQuestionAttemptsByQuestion ) { pst.setLong(1, userId); pst.setString(2, questionId); - log.warn("ick {} {}", userId, questionId); - try (ResultSet results = pst.executeQuery()) { - List c = new LinkedList<>(); + List questionAttemptList = new LinkedList<>(); + try (ResultSet results = pst.executeQuery()) { while (results.next()) { LLMFreeTextQuestionValidationResponse questionAttempt = objectMapper.readValue( results.getString("question_attempt"), LLMFreeTextQuestionValidationResponse.class); - c.add(questionAttempt); + questionAttemptList.add(questionAttempt); } - return c; } + return questionAttemptList; } catch (SQLException e) { throw new SegueDatabaseException("Postgres exception", e); } catch (IOException e) { diff --git a/src/main/java/uk/ac/cam/cl/dtg/segue/api/managers/QuestionManager.java b/src/main/java/uk/ac/cam/cl/dtg/segue/api/managers/QuestionManager.java index 47bd745e68..746cc2a2b2 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/segue/api/managers/QuestionManager.java +++ b/src/main/java/uk/ac/cam/cl/dtg/segue/api/managers/QuestionManager.java @@ -461,35 +461,32 @@ public Map>>> getMatchingDecoratedLightweightQuestionAttempts( final List users, final List questionPageIds) throws SegueDatabaseException, ContentManagerException { - Map>>> a = getMatchingLightweightQuestionAttempts(users, questionPageIds); + Map>>> decoratedLightweightQuestionAttempts = + getMatchingLightweightQuestionAttempts(users, questionPageIds); - // Loop over all users, question pages, and question part, and if the question part is an LLMFreeTextQuestion, log.warn("LLM") - for (Map.Entry>>> userEntry : a.entrySet()) { + // Iterate over all question parts in the mapping, replacing those on an isaacLLMFreeTextQuestion with a full QuestionValidationResponse + for (Map.Entry>>> userEntry : decoratedLightweightQuestionAttempts.entrySet()) { Long userId = userEntry.getKey(); Map>> questionPages = userEntry.getValue(); for (Map.Entry>> pageEntry : questionPages.entrySet()) { String questionPageId = pageEntry.getKey(); - Map> questionPartResponseMap = pageEntry.getValue(); - + Map> questionParts = pageEntry.getValue(); IsaacQuestionPage questionPage = (IsaacQuestionPage) this.contentManager.getContentDOById(questionPageId); - // get all question parts in the question page: depends on each question - // having an id that starts with the question page id. - Collection listOfQuestionParts = getAllMarkableDOQuestionPartsDFSOrder(questionPage); + Collection contentQuestionParts = getAllMarkableDOQuestionPartsDFSOrder(questionPage); - for (Question questionPart : listOfQuestionParts) { + for (Question questionPart : contentQuestionParts) { if (Objects.equals(questionPart.getType(), "isaacLLMFreeTextQuestion")) { String questionPartId = questionPart.getId(); - List decoratedQuestionValidationResponses = this.questionAttemptPersistenceManager.getQuestionAttemptsByQuestionId(userId, questionPartId); + List decoratedQuestionValidationResponses = this.questionAttemptPersistenceManager.getQuestionAttemptsByQuestionId(userId, questionPartId); List lightweightList = new ArrayList<>(decoratedQuestionValidationResponses); - questionPartResponseMap.put(questionPartId, lightweightList); - // questionPartResponseMap.put(questionPartId, decoratedQuestionValidationResponses); + questionParts.put(questionPartId, lightweightList); } } } } - return a; + return decoratedLightweightQuestionAttempts; } /** From c76f5f0cb5f1ff2a9699e4e88417134c502334d5 Mon Sep 17 00:00:00 2001 From: Sol Dubock <94075844+sjd210@users.noreply.github.com> Date: Tue, 21 Oct 2025 16:58:14 +0100 Subject: [PATCH 06/38] Adapt retrieval function response type as argument --- .../cam/cl/dtg/isaac/quiz/IQuestionAttemptManager.java | 3 ++- .../ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java | 10 ++++++---- .../cam/cl/dtg/segue/api/managers/QuestionManager.java | 3 ++- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/IQuestionAttemptManager.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/IQuestionAttemptManager.java index 48c4c744a6..10f117be82 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/IQuestionAttemptManager.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/IQuestionAttemptManager.java @@ -80,7 +80,8 @@ Map>> getQuestionAttempts(L * @throws SegueDatabaseException * - If there is a database error. */ - List getQuestionAttemptsByQuestionId(final Long userId, String questionId) + List getQuestionAttemptsByQuestionId(final Long userId, + String questionId, Class responseType) throws SegueDatabaseException; /** diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java index 9e2183b964..19810f1bd3 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java @@ -26,6 +26,7 @@ import uk.ac.cam.cl.dtg.isaac.dos.LightweightQuestionValidationResponse; import uk.ac.cam.cl.dtg.isaac.dos.QuestionValidationResponse; import uk.ac.cam.cl.dtg.isaac.dos.users.Role; +import uk.ac.cam.cl.dtg.segue.api.managers.QuestionManager; import uk.ac.cam.cl.dtg.segue.dao.SegueDatabaseException; import uk.ac.cam.cl.dtg.segue.dao.SegueDatabaseLockTimoutException; import uk.ac.cam.cl.dtg.segue.dao.content.ContentMapper; @@ -256,8 +257,9 @@ public Map>> getQuestionAtt } @Override - public List getQuestionAttemptsByQuestionId(final Long userId, final String questionId) - throws SegueDatabaseException { + public List getQuestionAttemptsByQuestionId( + final Long userId, final String questionId, + Class responseType) throws SegueDatabaseException { String query = "SELECT * FROM question_attempts WHERE user_id = ? AND question_id = ? ORDER BY \"timestamp\" ASC"; try (Connection conn = database.getDatabaseConnection(); PreparedStatement pst = conn.prepareStatement(query); @@ -269,8 +271,8 @@ public List getQuestionAttemptsByQuestionId(final Lo try (ResultSet results = pst.executeQuery()) { while (results.next()) { - LLMFreeTextQuestionValidationResponse questionAttempt = objectMapper.readValue( - results.getString("question_attempt"), LLMFreeTextQuestionValidationResponse.class); + QuestionValidationResponse questionAttempt = objectMapper.readValue( + results.getString("question_attempt"), responseType); questionAttemptList.add(questionAttempt); } diff --git a/src/main/java/uk/ac/cam/cl/dtg/segue/api/managers/QuestionManager.java b/src/main/java/uk/ac/cam/cl/dtg/segue/api/managers/QuestionManager.java index 746cc2a2b2..5bec9cc93c 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/segue/api/managers/QuestionManager.java +++ b/src/main/java/uk/ac/cam/cl/dtg/segue/api/managers/QuestionManager.java @@ -478,7 +478,8 @@ public Map decoratedQuestionValidationResponses = this.questionAttemptPersistenceManager.getQuestionAttemptsByQuestionId(userId, questionPartId); + List decoratedQuestionValidationResponses = + this.questionAttemptPersistenceManager.getQuestionAttemptsByQuestionId(userId, questionPartId, LLMFreeTextQuestionValidationResponse.class); List lightweightList = new ArrayList<>(decoratedQuestionValidationResponses); questionParts.put(questionPartId, lightweightList); } From 7a6e2e300489cdfa5d6c482fe1e9f470b81f1964 Mon Sep 17 00:00:00 2001 From: Sol Dubock <94075844+sjd210@users.noreply.github.com> Date: Wed, 22 Oct 2025 11:09:33 +0100 Subject: [PATCH 07/38] Migrate database to add mark to question_attempts --- .../ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java | 5 ++--- .../2025-10-questions-attempts-add-mark-field.sql | 11 +++++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 src/main/resources/db_scripts/migrations/2025-10-questions-attempts-add-mark-field.sql diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java index 19810f1bd3..e17989bb7f 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java @@ -258,8 +258,7 @@ public Map>> getQuestionAtt @Override public List getQuestionAttemptsByQuestionId( - final Long userId, final String questionId, - Class responseType) throws SegueDatabaseException { + final Long userId, final String questionId, Class responseType) throws SegueDatabaseException { String query = "SELECT * FROM question_attempts WHERE user_id = ? AND question_id = ? ORDER BY \"timestamp\" ASC"; try (Connection conn = database.getDatabaseConnection(); PreparedStatement pst = conn.prepareStatement(query); @@ -470,7 +469,7 @@ public Map getQuestionAttemptCountForUserByDateRange(final Date from private LightweightQuestionValidationResponse resultsToLightweightValidationResponse(final ResultSet results) throws SQLException { LightweightQuestionValidationResponse partialQuestionAttempt = new QuestionValidationResponse(); - partialQuestionAttempt.setCorrect(results.getBoolean("correct")); + partialQuestionAttempt.setCorrect(results.getInt("marks") > 0); partialQuestionAttempt.setQuestionId(results.getString("question_id")); partialQuestionAttempt.setDateAttempted(results.getTimestamp("timestamp")); diff --git a/src/main/resources/db_scripts/migrations/2025-10-questions-attempts-add-mark-field.sql b/src/main/resources/db_scripts/migrations/2025-10-questions-attempts-add-mark-field.sql new file mode 100644 index 0000000000..f3cb5ee78d --- /dev/null +++ b/src/main/resources/db_scripts/migrations/2025-10-questions-attempts-add-mark-field.sql @@ -0,0 +1,11 @@ +ALTER TABLE question_attempts +ADD marks integer GENERATED ALWAYS AS ( + CASE + WHEN + (question_attempt -> 'answer' ->> 'type') = 'llmFreeTextChoice' + THEN + (question_attempt -> 'marksAwarded')::int + ELSE + correct::int + END + ) STORED; \ No newline at end of file From 92203a6eb18d9019439cc2387b5376cd27aaa234 Mon Sep 17 00:00:00 2001 From: Sol Dubock <94075844+sjd210@users.noreply.github.com> Date: Wed, 22 Oct 2025 12:12:23 +0100 Subject: [PATCH 08/38] Convert database migration query to split by dates --- ...5-10-questions-attempts-add-mark-field.sql | 43 ++++++++++++++----- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/src/main/resources/db_scripts/migrations/2025-10-questions-attempts-add-mark-field.sql b/src/main/resources/db_scripts/migrations/2025-10-questions-attempts-add-mark-field.sql index f3cb5ee78d..5128d23b96 100644 --- a/src/main/resources/db_scripts/migrations/2025-10-questions-attempts-add-mark-field.sql +++ b/src/main/resources/db_scripts/migrations/2025-10-questions-attempts-add-mark-field.sql @@ -1,11 +1,34 @@ ALTER TABLE question_attempts -ADD marks integer GENERATED ALWAYS AS ( - CASE - WHEN - (question_attempt -> 'answer' ->> 'type') = 'llmFreeTextChoice' - THEN - (question_attempt -> 'marksAwarded')::int - ELSE - correct::int - END - ) STORED; \ No newline at end of file +ADD marks integer; + +CREATE FUNCTION update_marks_for_period(start_date date, end_date date) + RETURNS void AS $$ +BEGIN + WITH case_statement AS ( + SELECT + id, + CASE + WHEN + (question_attempt -> 'answer' ->> 'type') = 'llmFreeTextChoice' + THEN + (question_attempt ->> 'marksAwarded')::int + ELSE + correct::int + END AS marks + FROM question_attempts + WHERE timestamp > start_date + AND timestamp <= end_date + ) + + UPDATE question_attempts + SET marks = case_statement.marks + FROM case_statement + WHERE question_attempts.id = case_statement.id; +END; +$$ LANGUAGE plpgsql; + +SELECT update_marks_for_period('2014-01-01', '2015-01-02'); +SELECT update_marks_for_period('2015-01-01', '2016-01-02'); +SELECT update_marks_for_period('2016-01-02', '2025-12-01'); + +DROP FUNCTION update_marks_for_period; \ No newline at end of file From 9230a5526900df78bccd972ac57961f30d00eb20 Mon Sep 17 00:00:00 2001 From: Sol Dubock <94075844+sjd210@users.noreply.github.com> Date: Wed, 22 Oct 2025 12:23:53 +0100 Subject: [PATCH 09/38] Update tests to new LLMFreeText correct definition --- .../cam/cl/dtg/isaac/quiz/IsaacLLMFreeTextValidator.java | 2 +- .../cl/dtg/isaac/quiz/IsaacLLMFreeTextValidatorTest.java | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/IsaacLLMFreeTextValidator.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/IsaacLLMFreeTextValidator.java index 333053c05c..7384c12805 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/IsaacLLMFreeTextValidator.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/IsaacLLMFreeTextValidator.java @@ -282,7 +282,7 @@ private int evaluateMarkTotal(final IsaacLLMFreeTextQuestion question, final Map private LLMFreeTextQuestionValidationResponse generateQuestionValidationResponse( final IsaacLLMFreeTextQuestion question, final Choice answer, final Map awardedMarks, final int markTotal) { - boolean isConsideredCorrect = markTotal == question.getMaxMarks(); + boolean isConsideredCorrect = markTotal >= question.getMaxMarks(); // We create a fresh copy of the mark scheme with the full description and the awarded mark values. List markBreakdown = question.getMarkScheme().stream().map(mark -> { diff --git a/src/test/java/uk/ac/cam/cl/dtg/isaac/quiz/IsaacLLMFreeTextValidatorTest.java b/src/test/java/uk/ac/cam/cl/dtg/isaac/quiz/IsaacLLMFreeTextValidatorTest.java index 40756488ae..2ebac5124b 100644 --- a/src/test/java/uk/ac/cam/cl/dtg/isaac/quiz/IsaacLLMFreeTextValidatorTest.java +++ b/src/test/java/uk/ac/cam/cl/dtg/isaac/quiz/IsaacLLMFreeTextValidatorTest.java @@ -106,7 +106,7 @@ private static Object[][] genericTwoMarkCases() { {"A two-mark answer for a default marking formula two-mark question gets recognised as correct", question, "{\"reasonFoo\": 1, \"reasonBar\": 1, \"reasonFizz\": 0}", CORRECT, TWO_MARKS}, {"A one-mark answer for a default marking formula two-mark question receives exactly one mark", - question, "{\"reasonFoo\": 1, \"reasonBar\": 0, \"reasonFizz\": 0}", CORRECT, ONE_MARK}}; + question, "{\"reasonFoo\": 1, \"reasonBar\": 0, \"reasonFizz\": 0}", INCORRECT, ONE_MARK}}; } /* @@ -127,9 +127,9 @@ private static Object[][] advantageCases() { {"An answer containing an advantage and a disadvantage mark receives two marks", question, "{\"adv1\": 1, \"adv2\": 0, \"dis1\": 1, \"dis2\": 0}", CORRECT, TWO_MARKS}, {"An answer containing only a disadvantage mark receives one mark", - question, "{\"adv1\": 0, \"adv2\": 0, \"dis1\": 1, \"dis2\": 0}", CORRECT, ONE_MARK}, + question, "{\"adv1\": 0, \"adv2\": 0, \"dis1\": 1, \"dis2\": 0}", INCORRECT, ONE_MARK}, {"An answer containing two advantage marks receives one mark", - question, "{\"adv1\": 1, \"adv2\": 1, \"dis1\": 0, \"dis2\": 0}", CORRECT, ONE_MARK}}; + question, "{\"adv1\": 1, \"adv2\": 1, \"dis1\": 0, \"dis2\": 0}", INCORRECT, ONE_MARK}}; } @@ -156,7 +156,7 @@ private static Object[][] pointExplanationCases() { {"An answer containing an explanation without a matching point receives zero marks", question, "{\"pnt1\": 0, \"pnt2\": 0, \"expl1\": 1, \"expl2\": 0}", INCORRECT, NO_MARKS}, {"An answer containing a point and a mismatched explanation receives one mark", - question, "{\"pnt1\": 1, \"pnt2\": 0, \"expl1\": 0, \"expl2\": 0}", CORRECT, ONE_MARK}}; + question, "{\"pnt1\": 1, \"pnt2\": 0, \"expl1\": 0, \"expl2\": 0}", INCORRECT, ONE_MARK}}; } } From a02f051ad0df64dc338d6f3e7ec85f3ac2db565f Mon Sep 17 00:00:00 2001 From: Sol Dubock <94075844+sjd210@users.noreply.github.com> Date: Wed, 22 Oct 2025 14:17:52 +0100 Subject: [PATCH 10/38] Update postgres functions to process marks field --- .../cl/dtg/isaac/quiz/PgQuestionAttempts.java | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java index e17989bb7f..b4bb1a9ebe 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java @@ -189,8 +189,8 @@ public Map>> getAnonymousQu public void registerQuestionAttempt(final Long userId, final String questionPageId, final String fullQuestionId, final QuestionValidationResponse questionAttempt) throws SegueDatabaseException { - String query = "INSERT INTO question_attempts(user_id, page_id, question_id, question_attempt, correct, \"timestamp\")" - + " VALUES (?, ?, ?, ?::text::jsonb, ?, ?);"; + String query = "INSERT INTO question_attempts(user_id, page_id, question_id, question_attempt, correct, \"timestamp\", marks)" + + " VALUES (?, ?, ?, ?::text::jsonb, ?, ?, ?);"; try (Connection conn = database.getDatabaseConnection(); PreparedStatement pst = conn.prepareStatement(query, Statement.RETURN_GENERATED_KEYS); ) { @@ -204,8 +204,23 @@ public void registerQuestionAttempt(final Long userId, final String questionPage } else { pst.setNull(5, java.sql.Types.NULL); } + pst.setTimestamp(6, new java.sql.Timestamp(questionAttempt.getDateAttempted().getTime())); + if (questionAttempt.isCorrect() != null) { + if (questionAttempt instanceof LLMFreeTextQuestionValidationResponse) { + pst.setInt(7, ((LLMFreeTextQuestionValidationResponse) questionAttempt).getMarksAwarded()); + } else { + if (questionAttempt.isCorrect()) { + pst.setInt(7, 1); + } else { + pst.setInt(7, 0); + } + } + } else { + pst.setInt(7, java.sql.Types.NULL); + } + if (pst.executeUpdate() == 0) { throw new SegueDatabaseException("Unable to save question attempt."); } @@ -291,7 +306,7 @@ public Map>>> mapToReturn @@ -338,7 +353,7 @@ public Map>> get = userIds.stream().collect(Collectors.toMap(Function.identity(), k -> Maps.newHashMap()));; try (Connection conn = database.getDatabaseConnection()) { - String query = "SELECT id, user_id, question_id, correct, timestamp FROM question_attempts" + String query = "SELECT id, user_id, question_id, correct, timestamp, marks FROM question_attempts" + " WHERE user_id = ANY(?) AND page_id = ANY(?)" + " ORDER BY \"timestamp\" ASC"; From f2c4fd4e5b7210ae890c318c8457de0b1b8ab483 Mon Sep 17 00:00:00 2001 From: Sol Dubock <94075844+sjd210@users.noreply.github.com> Date: Wed, 22 Oct 2025 14:27:35 +0100 Subject: [PATCH 11/38] Add marks to test question_attempts table --- .../test-postgres-rutherford-data-dump.sql | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/test/resources/test-postgres-rutherford-data-dump.sql b/src/test/resources/test-postgres-rutherford-data-dump.sql index c28660a359..28b87b37f1 100644 --- a/src/test/resources/test-postgres-rutherford-data-dump.sql +++ b/src/test/resources/test-postgres-rutherford-data-dump.sql @@ -160,14 +160,14 @@ COPY public.logged_events (id, user_id, anonymous_user, event_type, event_detail -- Data for Name: question_attempts; Type: TABLE DATA; Schema: public; Owner: rutherford -- -COPY public.question_attempts (id, user_id, page_id, question_id, question_attempt, correct, "timestamp") FROM stdin; -2 7 _regression_test_ _regression_test_|acc_multi_q|_regression_test_multi_ {"answer": {"type": "choice", "value": "$42$", "correct": false, "children": [], "encoding": "markdown"}, "correct": true, "questionId": "_regression_test_|acc_multi_q|_regression_test_multi_", "explanation": {"tags": [], "type": "content", "children": [{"tags": [], "type": "content", "value": "This is a correct choice.", "children": [], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}, "dateAttempted": 1713449470730} t 2024-04-18 15:11:10.73 -3 7 _regression_test_ _regression_test_|acc_numeric_q|_regresssion_test_numeric_ {"answer": {"type": "quantity", "units": "m\\\\,s^{-1}", "value": "2.01", "correct": false, "children": []}, "correct": true, "questionId": "_regression_test_|acc_numeric_q|_regresssion_test_numeric_", "explanation": {"tags": [], "type": "content", "children": [{"tags": [], "type": "content", "value": "This is a correct choice.", "children": [], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}, "correctUnits": true, "correctValue": true, "dateAttempted": 1713449479419} t 2024-04-18 15:11:19.419 -4 7 _regression_test_ _regression_test_|acc_symbolic_q|_regression_test_symbolic_ {"answer": {"type": "formula", "value": "{\\"result\\":{\\"tex\\":\\"x\\",\\"mhchem\\":\\"\\",\\"python\\":\\"x\\",\\"mathml\\":\\"x\\",\\"uniqueSymbols\\":\\"x\\"},\\"symbols\\":[{\\"type\\":\\"Symbol\\",\\"position\\":{\\"x\\":239.5,\\"y\\":322.3333333333333},\\"expression\\":{\\"latex\\":\\"x\\",\\"python\\":\\"x\\"},\\"properties\\":{\\"letter\\":\\"x\\",\\"modifier\\":\\"\\"}}],\\"textEntry\\":true,\\"userInput\\":\\"x\\"}", "correct": false, "children": [], "pythonExpression": "x", "requiresExactMatch": false}, "correct": true, "questionId": "_regression_test_|acc_symbolic_q|_regression_test_symbolic_", "explanation": {"tags": [], "type": "content", "children": [{"tags": [], "type": "content", "value": "This is a correct choice. It requires an exact match!", "children": [], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}, "dateAttempted": 1713449484110} t 2024-04-18 15:11:24.11 -5 7 _regression_test_ _regression_test_|acc_stringmatch_q|_regression_test_stringmatch_ {"answer": {"type": "stringChoice", "value": "hello", "correct": false, "children": [], "caseInsensitive": false}, "correct": true, "questionId": "_regression_test_|acc_stringmatch_q|_regression_test_stringmatch_", "explanation": {"tags": [], "type": "content", "children": [{"tags": [], "type": "content", "value": "This needs a lower case \\"h\\".", "children": [], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}, "dateAttempted": 1713449505160} t 2024-04-18 15:11:45.16 -6 7 _regression_test_ _regression_test_|acc_chemistry_q|_regression_test_chemistry_ {"answer": {"type": "chemicalFormula", "value": "{\\"result\\":{\\"tex\\":\\"\\\\\\\\text{H} + \\\\\\\\text{Cl}\\",\\"mhchem\\":\\"H + Cl\\",\\"python\\":\\"\\\\\\\\text{H}\\",\\"mathml\\":\\"H+Cl\\",\\"uniqueSymbols\\":\\"H, Cl\\"},\\"symbols\\":[{\\"type\\":\\"ChemicalElement\\",\\"position\\":{\\"x\\":392.575,\\"y\\":531},\\"expression\\":{\\"latex\\":\\"\\\\\\\\text{H} + \\\\\\\\text{Cl}\\",\\"python\\":\\"\\\\\\\\text{H}\\"},\\"children\\":{\\"right\\":{\\"type\\":\\"BinaryOperation\\",\\"children\\":{\\"right\\":{\\"type\\":\\"ChemicalElement\\",\\"properties\\":{\\"element\\":\\"Cl\\"}}},\\"properties\\":{\\"operation\\":\\"+\\"}}},\\"properties\\":{\\"element\\":\\"H\\"}}],\\"textEntry\\":false,\\"userInput\\":\\"\\"}", "correct": false, "children": [], "mhchemExpression": "H + Cl"}, "correct": true, "questionId": "_regression_test_|acc_chemistry_q|_regression_test_chemistry_", "explanation": {"tags": [], "type": "content", "children": [{"tags": [], "type": "content", "value": "This is a correct choice.", "children": [], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}, "dateAttempted": 1713449507230} t 2024-04-18 15:11:47.23 -7 7 _regression_test_ _regression_test_|acc_freetext_q|_regression_test_freetext_ {"answer": {"type": "stringChoice", "value": "it didn't", "correct": false, "children": [], "caseInsensitive": false}, "correct": false, "questionId": "_regression_test_|acc_freetext_q|_regression_test_freetext_", "explanation": {"tags": [], "type": "content", "children": [{"tags": [], "type": "content", "value": "Spoil sport!", "children": [], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}, "dateAttempted": 1713449514726} f 2024-04-18 15:11:54.726 -8 7 _regression_test_ _regression_test_|_regression_test_logic_ {"answer": {"type": "logicFormula", "value": "{\\"result\\":{\\"tex\\":\\"A \\\\\\\\land B\\",\\"mhchem\\":\\"\\",\\"python\\":\\"A & B\\",\\"mathml\\":\\"\\",\\"uniqueSymbols\\":\\"A, B\\"},\\"symbols\\":[{\\"type\\":\\"Symbol\\",\\"position\\":{\\"x\\":284.625,\\"y\\":482},\\"expression\\":{\\"latex\\":\\"A \\\\\\\\land B\\",\\"python\\":\\"A & B\\"},\\"children\\":{\\"right\\":{\\"type\\":\\"LogicBinaryOperation\\",\\"children\\":{\\"right\\":{\\"type\\":\\"Symbol\\",\\"properties\\":{\\"letter\\":\\"B\\",\\"modifier\\":\\"\\"}}},\\"properties\\":{\\"operation\\":\\"and\\"}}},\\"properties\\":{\\"letter\\":\\"A\\",\\"modifier\\":\\"\\"}}],\\"textEntry\\":false,\\"userInput\\":\\"\\"}", "correct": false, "children": [], "pythonExpression": "A & B", "requiresExactMatch": false}, "correct": true, "questionId": "_regression_test_|_regression_test_logic_", "explanation": {"tags": [], "type": "content", "children": [{"tags": [], "type": "content", "value": "This simplifies to $\\\\and{A}{B}$!", "children": [], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}, "dateAttempted": 1713449529969} t 2024-04-18 15:12:09.969 +COPY public.question_attempts (id, user_id, page_id, question_id, question_attempt, correct, "timestamp", marks) FROM stdin; +2 7 _regression_test_ _regression_test_|acc_multi_q|_regression_test_multi_ {"answer": {"type": "choice", "value": "$42$", "correct": false, "children": [], "encoding": "markdown"}, "correct": true, "questionId": "_regression_test_|acc_multi_q|_regression_test_multi_", "explanation": {"tags": [], "type": "content", "children": [{"tags": [], "type": "content", "value": "This is a correct choice.", "children": [], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}, "dateAttempted": 1713449470730} t 2024-04-18 15:11:10.73 1 +3 7 _regression_test_ _regression_test_|acc_numeric_q|_regresssion_test_numeric_ {"answer": {"type": "quantity", "units": "m\\\\,s^{-1}", "value": "2.01", "correct": false, "children": []}, "correct": true, "questionId": "_regression_test_|acc_numeric_q|_regresssion_test_numeric_", "explanation": {"tags": [], "type": "content", "children": [{"tags": [], "type": "content", "value": "This is a correct choice.", "children": [], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}, "correctUnits": true, "correctValue": true, "dateAttempted": 1713449479419} t 2024-04-18 15:11:19.419 1 +4 7 _regression_test_ _regression_test_|acc_symbolic_q|_regression_test_symbolic_ {"answer": {"type": "formula", "value": "{\\"result\\":{\\"tex\\":\\"x\\",\\"mhchem\\":\\"\\",\\"python\\":\\"x\\",\\"mathml\\":\\"x\\",\\"uniqueSymbols\\":\\"x\\"},\\"symbols\\":[{\\"type\\":\\"Symbol\\",\\"position\\":{\\"x\\":239.5,\\"y\\":322.3333333333333},\\"expression\\":{\\"latex\\":\\"x\\",\\"python\\":\\"x\\"},\\"properties\\":{\\"letter\\":\\"x\\",\\"modifier\\":\\"\\"}}],\\"textEntry\\":true,\\"userInput\\":\\"x\\"}", "correct": false, "children": [], "pythonExpression": "x", "requiresExactMatch": false}, "correct": true, "questionId": "_regression_test_|acc_symbolic_q|_regression_test_symbolic_", "explanation": {"tags": [], "type": "content", "children": [{"tags": [], "type": "content", "value": "This is a correct choice. It requires an exact match!", "children": [], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}, "dateAttempted": 1713449484110} t 2024-04-18 15:11:24.11 1 +5 7 _regression_test_ _regression_test_|acc_stringmatch_q|_regression_test_stringmatch_ {"answer": {"type": "stringChoice", "value": "hello", "correct": false, "children": [], "caseInsensitive": false}, "correct": true, "questionId": "_regression_test_|acc_stringmatch_q|_regression_test_stringmatch_", "explanation": {"tags": [], "type": "content", "children": [{"tags": [], "type": "content", "value": "This needs a lower case \\"h\\".", "children": [], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}, "dateAttempted": 1713449505160} t 2024-04-18 15:11:45.16 1 +6 7 _regression_test_ _regression_test_|acc_chemistry_q|_regression_test_chemistry_ {"answer": {"type": "chemicalFormula", "value": "{\\"result\\":{\\"tex\\":\\"\\\\\\\\text{H} + \\\\\\\\text{Cl}\\",\\"mhchem\\":\\"H + Cl\\",\\"python\\":\\"\\\\\\\\text{H}\\",\\"mathml\\":\\"H+Cl\\",\\"uniqueSymbols\\":\\"H, Cl\\"},\\"symbols\\":[{\\"type\\":\\"ChemicalElement\\",\\"position\\":{\\"x\\":392.575,\\"y\\":531},\\"expression\\":{\\"latex\\":\\"\\\\\\\\text{H} + \\\\\\\\text{Cl}\\",\\"python\\":\\"\\\\\\\\text{H}\\"},\\"children\\":{\\"right\\":{\\"type\\":\\"BinaryOperation\\",\\"children\\":{\\"right\\":{\\"type\\":\\"ChemicalElement\\",\\"properties\\":{\\"element\\":\\"Cl\\"}}},\\"properties\\":{\\"operation\\":\\"+\\"}}},\\"properties\\":{\\"element\\":\\"H\\"}}],\\"textEntry\\":false,\\"userInput\\":\\"\\"}", "correct": false, "children": [], "mhchemExpression": "H + Cl"}, "correct": true, "questionId": "_regression_test_|acc_chemistry_q|_regression_test_chemistry_", "explanation": {"tags": [], "type": "content", "children": [{"tags": [], "type": "content", "value": "This is a correct choice.", "children": [], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}, "dateAttempted": 1713449507230} t 2024-04-18 15:11:47.23 1 +7 7 _regression_test_ _regression_test_|acc_freetext_q|_regression_test_freetext_ {"answer": {"type": "stringChoice", "value": "it didn't", "correct": false, "children": [], "caseInsensitive": false}, "correct": false, "questionId": "_regression_test_|acc_freetext_q|_regression_test_freetext_", "explanation": {"tags": [], "type": "content", "children": [{"tags": [], "type": "content", "value": "Spoil sport!", "children": [], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}, "dateAttempted": 1713449514726} f 2024-04-18 15:11:54.726 0 +8 7 _regression_test_ _regression_test_|_regression_test_logic_ {"answer": {"type": "logicFormula", "value": "{\\"result\\":{\\"tex\\":\\"A \\\\\\\\land B\\",\\"mhchem\\":\\"\\",\\"python\\":\\"A & B\\",\\"mathml\\":\\"\\",\\"uniqueSymbols\\":\\"A, B\\"},\\"symbols\\":[{\\"type\\":\\"Symbol\\",\\"position\\":{\\"x\\":284.625,\\"y\\":482},\\"expression\\":{\\"latex\\":\\"A \\\\\\\\land B\\",\\"python\\":\\"A & B\\"},\\"children\\":{\\"right\\":{\\"type\\":\\"LogicBinaryOperation\\",\\"children\\":{\\"right\\":{\\"type\\":\\"Symbol\\",\\"properties\\":{\\"letter\\":\\"B\\",\\"modifier\\":\\"\\"}}},\\"properties\\":{\\"operation\\":\\"and\\"}}},\\"properties\\":{\\"letter\\":\\"A\\",\\"modifier\\":\\"\\"}}],\\"textEntry\\":false,\\"userInput\\":\\"\\"}", "correct": false, "children": [], "pythonExpression": "A & B", "requiresExactMatch": false}, "correct": true, "questionId": "_regression_test_|_regression_test_logic_", "explanation": {"tags": [], "type": "content", "children": [{"tags": [], "type": "content", "value": "This simplifies to $\\\\and{A}{B}$!", "children": [], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}, "dateAttempted": 1713449529969} t 2024-04-18 15:12:09.969 1 \. From 2700c481ac423f38ccd35d3425cab1df601d47ab Mon Sep 17 00:00:00 2001 From: Sol Dubock <94075844+sjd210@users.noreply.github.com> Date: Wed, 22 Oct 2025 14:57:10 +0100 Subject: [PATCH 12/38] Add marks to lightweight response --- .../dtg/isaac/api/managers/GameManager.java | 3 ++- ...LightweightQuestionValidationResponse.java | 25 ++++++++++++++++++- .../isaac/quiz/IQuestionAttemptManager.java | 5 ++-- .../cl/dtg/isaac/quiz/PgQuestionAttempts.java | 1 + 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/GameManager.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/GameManager.java index a6113eb52d..84dc85c4ed 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/GameManager.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/GameManager.java @@ -1157,7 +1157,8 @@ private void augmentGameItemWithAttemptInformation( int greatestMarksForThisQuestion = 0; for (LightweightQuestionValidationResponse attempt: questionPartAttempts) { if (attempt instanceof LLMFreeTextQuestionValidationResponse) { - LLMFreeTextQuestionValidationResponse llmFreeTextAttempt = (LLMFreeTextQuestionValidationResponse) attempt; + LLMFreeTextQuestionValidationResponse llmFreeTextAttempt = + (LLMFreeTextQuestionValidationResponse) attempt; if (llmFreeTextAttempt.getMarksAwarded() > greatestMarksForThisQuestion) { greatestMarksForThisQuestion = llmFreeTextAttempt.getMarksAwarded(); } diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/LightweightQuestionValidationResponse.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/LightweightQuestionValidationResponse.java index fab7a7ef32..2ad9f48661 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/LightweightQuestionValidationResponse.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/LightweightQuestionValidationResponse.java @@ -10,6 +10,7 @@ public class LightweightQuestionValidationResponse { private String questionId; private Boolean correct; private Date dateAttempted; + private Integer marks; /** * Default Constructor for mappers. @@ -27,12 +28,15 @@ public LightweightQuestionValidationResponse() { * - * @param dateAttempted * - + * @param marks + * - */ public LightweightQuestionValidationResponse(final String questionId, final Boolean correct, - final Date dateAttempted) { + final Date dateAttempted, final Integer marks) { this.questionId = questionId; this.correct = correct; this.dateAttempted = dateAttempted; + this.marks = marks; } /** @@ -92,6 +96,25 @@ public void setDateAttempted(final Date dateAttempted) { this.dateAttempted = dateAttempted; } + /** + * Gets the marks. + * + * @return the marks + */ + public Integer getMarks() { + return marks; + } + + /** + * Sets the marks. + * + * @param marks + * the marks to set + */ + public void setMarks(final Integer marks) { + this.marks = marks; + } + @Override public String toString() { return "QuestionValidationResponse [questionId=" + questionId + ", correct=" + correct + diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/IQuestionAttemptManager.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/IQuestionAttemptManager.java index 10f117be82..e4b650aed7 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/IQuestionAttemptManager.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/IQuestionAttemptManager.java @@ -80,9 +80,8 @@ Map>> getQuestionAttempts(L * @throws SegueDatabaseException * - If there is a database error. */ - List getQuestionAttemptsByQuestionId(final Long userId, - String questionId, Class responseType) - throws SegueDatabaseException; + List getQuestionAttemptsByQuestionId(Long userId, + String questionId, Class responseType) throws SegueDatabaseException; /** * A method that makes a single database request for a group of users and questions to get all of their attempt diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java index b4bb1a9ebe..22805f557e 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java @@ -487,6 +487,7 @@ private LightweightQuestionValidationResponse resultsToLightweightValidationResp partialQuestionAttempt.setCorrect(results.getInt("marks") > 0); partialQuestionAttempt.setQuestionId(results.getString("question_id")); partialQuestionAttempt.setDateAttempted(results.getTimestamp("timestamp")); + partialQuestionAttempt.setMarks(results.getInt("marks")); return partialQuestionAttempt; } From 0b5809d250f896c5fd8bbf3d0c608775993b684f Mon Sep 17 00:00:00 2001 From: Sol Dubock <94075844+sjd210@users.noreply.github.com> Date: Wed, 22 Oct 2025 14:59:46 +0100 Subject: [PATCH 13/38] Add marks to lightweight responses in tests --- .../api/managers/UserAttemptManagerTest.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/test/java/uk/ac/cam/cl/dtg/isaac/api/managers/UserAttemptManagerTest.java b/src/test/java/uk/ac/cam/cl/dtg/isaac/api/managers/UserAttemptManagerTest.java index bcaf2a1e81..8ec693a108 100644 --- a/src/test/java/uk/ac/cam/cl/dtg/isaac/api/managers/UserAttemptManagerTest.java +++ b/src/test/java/uk/ac/cam/cl/dtg/isaac/api/managers/UserAttemptManagerTest.java @@ -29,14 +29,14 @@ public class UserAttemptManagerTest { private ContentSummaryDTO fakeQuestionSummary; private ContentSummaryDTO fakeConceptSummary; - private final LightweightQuestionValidationResponse p1CorrectAttempt = new LightweightQuestionValidationResponse(QUESTION_PART_1_ID, true, null); - private final LightweightQuestionValidationResponse p1IncorrectAttempt = new LightweightQuestionValidationResponse(QUESTION_PART_1_ID, false, null); - private final LightweightQuestionValidationResponse p2CorrectAttempt = new LightweightQuestionValidationResponse(QUESTION_PART_2_ID, true, null); - private final LightweightQuestionValidationResponse p2IncorrectAttempt = new LightweightQuestionValidationResponse(QUESTION_PART_2_ID, false, null); - private final LightweightQuestionValidationResponse p3CorrectAttempt = new LightweightQuestionValidationResponse(QUESTION_PART_3_ID, true, null); - private final LightweightQuestionValidationResponse p3IncorrectAttempt = new LightweightQuestionValidationResponse(QUESTION_PART_3_ID, false, null); - - private final LightweightQuestionValidationResponse someOtherCorrectAttempt = new LightweightQuestionValidationResponse("some-other-part-id", true, null); + private final LightweightQuestionValidationResponse p1CorrectAttempt = new LightweightQuestionValidationResponse(QUESTION_PART_1_ID, true, null, 1); + private final LightweightQuestionValidationResponse p1IncorrectAttempt = new LightweightQuestionValidationResponse(QUESTION_PART_1_ID, false, null, 0); + private final LightweightQuestionValidationResponse p2CorrectAttempt = new LightweightQuestionValidationResponse(QUESTION_PART_2_ID, true, null, 1); + private final LightweightQuestionValidationResponse p2IncorrectAttempt = new LightweightQuestionValidationResponse(QUESTION_PART_2_ID, false, null, 0); + private final LightweightQuestionValidationResponse p3CorrectAttempt = new LightweightQuestionValidationResponse(QUESTION_PART_3_ID, true, null, 1); + private final LightweightQuestionValidationResponse p3IncorrectAttempt = new LightweightQuestionValidationResponse(QUESTION_PART_3_ID, false, null, 0); + + private final LightweightQuestionValidationResponse someOtherCorrectAttempt = new LightweightQuestionValidationResponse("some-other-part-id", true, null, 1); @Before public void setUp() throws Exception { From 248a000b4b530b1dd9c33ccdf6d07a27303a2b90 Mon Sep 17 00:00:00 2001 From: Sol Dubock <94075844+sjd210@users.noreply.github.com> Date: Wed, 22 Oct 2025 17:22:24 +0100 Subject: [PATCH 14/38] Allow QuestionValidationResponse to derive marks --- .../isaac/dos/QuestionValidationResponse.java | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/QuestionValidationResponse.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/QuestionValidationResponse.java index 916ec005c5..c93fc487e4 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/QuestionValidationResponse.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/QuestionValidationResponse.java @@ -51,10 +51,33 @@ public QuestionValidationResponse() { * - * @param dateAttempted * - + * @param marks + * - + */ + public QuestionValidationResponse(final String questionId, final Choice answer, final Boolean correct, + final Content explanation, final Date dateAttempted, final Integer marks) { + super(questionId, correct, dateAttempted, marks); + this.answer = answer; + this.explanation = explanation; + } + + /** + * Constructor without specifying marks (derived from correct) + * + * @param questionId + * - + * @param answer + * - + * @param correct + * - + * @param explanation + * - + * @param dateAttempted + * - */ public QuestionValidationResponse(final String questionId, final Choice answer, final Boolean correct, final Content explanation, final Date dateAttempted) { - super(questionId, correct, dateAttempted); + super(questionId, correct, dateAttempted, (correct != null && correct) ? 1 : 0); this.answer = answer; this.explanation = explanation; } From 696ecdf1dc6ee0ac822705f071fc8ea083e16c45 Mon Sep 17 00:00:00 2001 From: Sol Dubock <94075844+sjd210@users.noreply.github.com> Date: Thu, 23 Oct 2025 09:41:42 +0100 Subject: [PATCH 15/38] Also add marks field to quiz_question_attempts --- ...QuizQuestionAttemptPersistenceManager.java | 2 +- .../db_scripts/create_anonymous_database.sql | 6 ++-- .../migrations/2021-02-create_quiz_tables.sql | 3 +- ...5-10-questions-attempts-add-mark-field.sql | 36 ++++++++++++++++++- .../postgres-rutherford-create-script.sql | 6 ++-- .../test-postgres-rutherford-data-dump.sql | 2 +- 6 files changed, 47 insertions(+), 8 deletions(-) diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/dao/PgQuizQuestionAttemptPersistenceManager.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/dao/PgQuizQuestionAttemptPersistenceManager.java index af593668d2..aafa1a70f4 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/dao/PgQuizQuestionAttemptPersistenceManager.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/dao/PgQuizQuestionAttemptPersistenceManager.java @@ -59,7 +59,7 @@ public PgQuizQuestionAttemptPersistenceManager(final PostgresSqlDb database, fin @Override public void registerQuestionAttempt(Long quizAttemptId, QuestionValidationResponse questionResponse) throws SegueDatabaseException { - String query = "INSERT INTO quiz_question_attempts(quiz_attempt_id, question_id, question_attempt, correct, \"timestamp\")" + + String query = "INSERT INTO quiz_question_attempts(quiz_attempt_id, question_id, question_attempt, correct, \"timestamp\", marks)" + " VALUES (?, ?, ?::text::jsonb, ?, ?);"; try (Connection conn = database.getDatabaseConnection(); PreparedStatement pst = conn.prepareStatement(query, Statement.RETURN_GENERATED_KEYS); diff --git a/src/main/resources/db_scripts/create_anonymous_database.sql b/src/main/resources/db_scripts/create_anonymous_database.sql index b95b101599..eaa4c0d7af 100644 --- a/src/main/resources/db_scripts/create_anonymous_database.sql +++ b/src/main/resources/db_scripts/create_anonymous_database.sql @@ -151,7 +151,8 @@ CREATE TABLE anonymous.question_attempts AS question_id, question_attempt, correct, - timestamp + timestamp, + marks FROM public.question_attempts; CREATE TABLE anonymous.user_streak_freezes AS @@ -200,7 +201,8 @@ CREATE TABLE anonymous.quiz_question_attempts AS question_id, question_attempt, correct, - timestamp + timestamp, + marks FROM public.quiz_question_attempts; -- Logged events: diff --git a/src/main/resources/db_scripts/migrations/2021-02-create_quiz_tables.sql b/src/main/resources/db_scripts/migrations/2021-02-create_quiz_tables.sql index e87a0fb8ec..484f5d297b 100644 --- a/src/main/resources/db_scripts/migrations/2021-02-create_quiz_tables.sql +++ b/src/main/resources/db_scripts/migrations/2021-02-create_quiz_tables.sql @@ -77,7 +77,8 @@ CREATE TABLE public.quiz_question_attempts ( question_id text NOT NULL, question_attempt jsonb, correct boolean, - "timestamp" timestamp without time zone + "timestamp" timestamp without time zone, + marks integer ); CREATE SEQUENCE public.quiz_question_attempts_id_seq diff --git a/src/main/resources/db_scripts/migrations/2025-10-questions-attempts-add-mark-field.sql b/src/main/resources/db_scripts/migrations/2025-10-questions-attempts-add-mark-field.sql index 5128d23b96..7000fa2b8c 100644 --- a/src/main/resources/db_scripts/migrations/2025-10-questions-attempts-add-mark-field.sql +++ b/src/main/resources/db_scripts/migrations/2025-10-questions-attempts-add-mark-field.sql @@ -1,6 +1,9 @@ ALTER TABLE question_attempts ADD marks integer; +ALTER TABLE quiz_question_attempts +ADD marks integer; + CREATE FUNCTION update_marks_for_period(start_date date, end_date date) RETURNS void AS $$ BEGIN @@ -27,8 +30,39 @@ BEGIN END; $$ LANGUAGE plpgsql; +CREATE FUNCTION quiz_update_marks_for_period(start_date date, end_date date) + RETURNS void AS $$ +BEGIN + WITH case_statement AS ( + SELECT + id, + CASE + WHEN + (question_attempt -> 'answer' ->> 'type') = 'llmFreeTextChoice' + THEN + (question_attempt ->> 'marksAwarded')::int + ELSE + correct::int + END AS marks + FROM quiz_question_attempts + WHERE timestamp > start_date + AND timestamp <= end_date + ) + + UPDATE quiz_question_attempts + SET marks = case_statement.marks + FROM case_statement + WHERE quiz_question_attempts.id = case_statement.id; +END; +$$ LANGUAGE plpgsql; + SELECT update_marks_for_period('2014-01-01', '2015-01-02'); SELECT update_marks_for_period('2015-01-01', '2016-01-02'); SELECT update_marks_for_period('2016-01-02', '2025-12-01'); -DROP FUNCTION update_marks_for_period; \ No newline at end of file +SELECT quiz_update_marks_for_period('2014-01-01', '2015-01-02'); +SELECT quiz_update_marks_for_period('2015-01-01', '2016-01-02'); +SELECT quiz_update_marks_for_period('2016-01-02', '2025-12-01'); + +DROP FUNCTION update_marks_for_period; +DROP FUNCTION quiz_update_marks_for_period; \ No newline at end of file diff --git a/src/main/resources/db_scripts/postgres-rutherford-create-script.sql b/src/main/resources/db_scripts/postgres-rutherford-create-script.sql index 0647661efb..3f976ce9ad 100644 --- a/src/main/resources/db_scripts/postgres-rutherford-create-script.sql +++ b/src/main/resources/db_scripts/postgres-rutherford-create-script.sql @@ -346,7 +346,8 @@ CREATE TABLE public.question_attempts ( question_id text NOT NULL, question_attempt jsonb, correct boolean, - "timestamp" timestamp without time zone + "timestamp" timestamp without time zone, + marks integer ); @@ -460,7 +461,8 @@ CREATE TABLE public.quiz_question_attempts ( question_id text NOT NULL, question_attempt jsonb, correct boolean, - "timestamp" timestamp without time zone + "timestamp" timestamp without time zone, + marks integer ); diff --git a/src/test/resources/test-postgres-rutherford-data-dump.sql b/src/test/resources/test-postgres-rutherford-data-dump.sql index 28b87b37f1..b5dab6ea91 100644 --- a/src/test/resources/test-postgres-rutherford-data-dump.sql +++ b/src/test/resources/test-postgres-rutherford-data-dump.sql @@ -191,7 +191,7 @@ COPY public.quiz_attempts (id, user_id, quiz_id, quiz_assignment_id, start_date, -- Data for Name: quiz_question_attempts; Type: TABLE DATA; Schema: public; Owner: rutherford -- -COPY public.quiz_question_attempts (id, quiz_attempt_id, question_id, question_attempt, correct, "timestamp") FROM stdin; +COPY public.quiz_question_attempts (id, quiz_attempt_id, question_id, question_attempt, correct, "timestamp", marks) FROM stdin; \. From c9825795c45da1f3a55383b84308e83ecce2101b Mon Sep 17 00:00:00 2001 From: Sol Dubock <94075844+sjd210@users.noreply.github.com> Date: Thu, 23 Oct 2025 09:58:05 +0100 Subject: [PATCH 16/38] Remove question ID extraction code --- .../dtg/isaac/api/managers/GameManager.java | 3 +- .../isaac/quiz/IQuestionAttemptManager.java | 12 ------ .../cl/dtg/isaac/quiz/PgQuestionAttempts.java | 29 -------------- .../segue/api/managers/QuestionManager.java | 39 +------------------ 4 files changed, 2 insertions(+), 81 deletions(-) diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/GameManager.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/GameManager.java index 84dc85c4ed..e11c022590 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/GameManager.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/GameManager.java @@ -53,7 +53,6 @@ import uk.ac.cam.cl.dtg.isaac.dto.content.QuestionDTO; import uk.ac.cam.cl.dtg.isaac.dto.users.AbstractSegueUserDTO; import uk.ac.cam.cl.dtg.isaac.dto.users.RegisteredUserDTO; -import uk.ac.cam.cl.dtg.isaac.quiz.IQuestionAttemptManager; import uk.ac.cam.cl.dtg.segue.api.managers.QuestionManager; import uk.ac.cam.cl.dtg.segue.dao.ResourceNotFoundException; import uk.ac.cam.cl.dtg.segue.dao.SegueDatabaseException; @@ -653,7 +652,7 @@ public List>> gatherGamePro Map>>> questionAttemptsForAllUsersOfInterest = - questionManager.getMatchingDecoratedLightweightQuestionAttempts(users, questionPageIds); + questionManager.getMatchingLightweightQuestionAttempts(users, questionPageIds); for (RegisteredUserDTO user : users) { List userGameItems = Lists.newArrayList(); diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/IQuestionAttemptManager.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/IQuestionAttemptManager.java index e4b650aed7..1e1dfde0ae 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/IQuestionAttemptManager.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/IQuestionAttemptManager.java @@ -71,18 +71,6 @@ Map>> getLightwe Map>> getQuestionAttempts(Long userId, String questionPageId) throws SegueDatabaseException; - /** - * Get a users question attempts on a specific question part. - * - * @param userId - the id of the user to search for. - * @param questionId - the id of the question part. - * @return the questionAttempts map or an empty map if the user has not yet registered any attempts. - * @throws SegueDatabaseException - * - If there is a database error. - */ - List getQuestionAttemptsByQuestionId(Long userId, - String questionId, Class responseType) throws SegueDatabaseException; - /** * A method that makes a single database request for a group of users and questions to get all of their attempt * information back. diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java index 22805f557e..159ad78af8 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java @@ -45,7 +45,6 @@ import java.util.Collections; import java.util.Date; import java.util.HashMap; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; @@ -271,34 +270,6 @@ public Map>> getQuestionAtt } } - @Override - public List getQuestionAttemptsByQuestionId( - final Long userId, final String questionId, Class responseType) throws SegueDatabaseException { - String query = "SELECT * FROM question_attempts WHERE user_id = ? AND question_id = ? ORDER BY \"timestamp\" ASC"; - try (Connection conn = database.getDatabaseConnection(); - PreparedStatement pst = conn.prepareStatement(query); - ) { - pst.setLong(1, userId); - pst.setString(2, questionId); - - List questionAttemptList = new LinkedList<>(); - - try (ResultSet results = pst.executeQuery()) { - while (results.next()) { - QuestionValidationResponse questionAttempt = objectMapper.readValue( - results.getString("question_attempt"), responseType); - - questionAttemptList.add(questionAttempt); - } - } - return questionAttemptList; - } catch (SQLException e) { - throw new SegueDatabaseException("Postgres exception", e); - } catch (IOException e) { - throw new SegueDatabaseException("Exception while parsing json", e); - } - } - public Map>>> getLightweightQuestionAttemptsByUsers(final List userIds) throws SegueDatabaseException { diff --git a/src/main/java/uk/ac/cam/cl/dtg/segue/api/managers/QuestionManager.java b/src/main/java/uk/ac/cam/cl/dtg/segue/api/managers/QuestionManager.java index 5bec9cc93c..5c5565a6cd 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/segue/api/managers/QuestionManager.java +++ b/src/main/java/uk/ac/cam/cl/dtg/segue/api/managers/QuestionManager.java @@ -452,44 +452,7 @@ public Map>>> getMatchingDecoratedLightweightQuestionAttempts( - final List users, final List questionPageIds) throws SegueDatabaseException, ContentManagerException { - Map>>> decoratedLightweightQuestionAttempts = - getMatchingLightweightQuestionAttempts(users, questionPageIds); - - // Iterate over all question parts in the mapping, replacing those on an isaacLLMFreeTextQuestion with a full QuestionValidationResponse - for (Map.Entry>>> userEntry : decoratedLightweightQuestionAttempts.entrySet()) { - Long userId = userEntry.getKey(); - Map>> questionPages = userEntry.getValue(); - - for (Map.Entry>> pageEntry : questionPages.entrySet()) { - String questionPageId = pageEntry.getKey(); - Map> questionParts = pageEntry.getValue(); - IsaacQuestionPage questionPage = (IsaacQuestionPage) this.contentManager.getContentDOById(questionPageId); - Collection contentQuestionParts = getAllMarkableDOQuestionPartsDFSOrder(questionPage); - - for (Question questionPart : contentQuestionParts) { - if (Objects.equals(questionPart.getType(), "isaacLLMFreeTextQuestion")) { - String questionPartId = questionPart.getId(); - List decoratedQuestionValidationResponses = - this.questionAttemptPersistenceManager.getQuestionAttemptsByQuestionId(userId, questionPartId, LLMFreeTextQuestionValidationResponse.class); - List lightweightList = new ArrayList<>(decoratedQuestionValidationResponses); - questionParts.put(questionPartId, lightweightList); - } - } - } - } - - return decoratedLightweightQuestionAttempts; - } - + /** * Helper method for attempts from a single user. * From d4e0cd9a163cd6af8f73b99ff894cd2b86b571d6 Mon Sep 17 00:00:00 2001 From: Sol Dubock <94075844+sjd210@users.noreply.github.com> Date: Thu, 23 Oct 2025 10:15:18 +0100 Subject: [PATCH 17/38] Augment game items with new marks field --- .../cam/cl/dtg/isaac/api/managers/GameManager.java | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/GameManager.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/GameManager.java index e11c022590..6e5310d5c7 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/GameManager.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/GameManager.java @@ -151,7 +151,7 @@ public GameManager(final GitContentManager contentManager, * list of question categories (i.e. problem_solving, book) to include in filtered results * @param boardOwner * The user that should be marked as the creator of the gameBoard. - * @return a gameboard if possible that satisifies the conditions provided by the parameters. Will return null if no + * @return a gameboard if possible that satisfies the conditions provided by the parameters. Will return null if no * questions can be provided. * @throws SegueDatabaseException * - if there is an error contacting the database. @@ -1150,17 +1150,13 @@ private void augmentGameItemWithAttemptInformation( for (Content questionPart : listOfQuestionParts) { List questionPartAttempts = questionAttempts.get(questionPart.getId()); - if (Objects.equals(questionPart.getType(), "isaacLLMFreeTextQuestion")) { + if (questionPart instanceof IsaacLLMFreeTextQuestion) { int maxMarks = ((IsaacLLMFreeTextQuestion) questionPart).getMaxMarks(); if (questionPartAttempts != null && !questionPartAttempts.isEmpty()) { int greatestMarksForThisQuestion = 0; for (LightweightQuestionValidationResponse attempt: questionPartAttempts) { - if (attempt instanceof LLMFreeTextQuestionValidationResponse) { - LLMFreeTextQuestionValidationResponse llmFreeTextAttempt = - (LLMFreeTextQuestionValidationResponse) attempt; - if (llmFreeTextAttempt.getMarksAwarded() > greatestMarksForThisQuestion) { - greatestMarksForThisQuestion = llmFreeTextAttempt.getMarksAwarded(); - } + if (attempt.getMarks() > greatestMarksForThisQuestion) { + greatestMarksForThisQuestion = attempt.getMarks(); } } for (int markCorrect = 0; markCorrect < greatestMarksForThisQuestion; markCorrect++) { From 36a60e7660246ada7e2ad6e400a4c857bc9873d9 Mon Sep 17 00:00:00 2001 From: Sol Dubock <94075844+sjd210@users.noreply.github.com> Date: Thu, 23 Oct 2025 10:37:11 +0100 Subject: [PATCH 18/38] Update migration script dates split ranges --- .../cl/dtg/isaac/quiz/PgQuestionAttempts.java | 2 +- .../segue/api/managers/QuestionManager.java | 2 +- ...5-10-questions-attempts-add-mark-field.sql | 64 ++++++++++++++++++- 3 files changed, 64 insertions(+), 4 deletions(-) diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java index 159ad78af8..3f94ba7d55 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java @@ -206,7 +206,7 @@ public void registerQuestionAttempt(final Long userId, final String questionPage pst.setTimestamp(6, new java.sql.Timestamp(questionAttempt.getDateAttempted().getTime())); - if (questionAttempt.isCorrect() != null) { + if (questionAttempt.getMarks() != null) { if (questionAttempt instanceof LLMFreeTextQuestionValidationResponse) { pst.setInt(7, ((LLMFreeTextQuestionValidationResponse) questionAttempt).getMarksAwarded()); } else { diff --git a/src/main/java/uk/ac/cam/cl/dtg/segue/api/managers/QuestionManager.java b/src/main/java/uk/ac/cam/cl/dtg/segue/api/managers/QuestionManager.java index 5c5565a6cd..da7f6f953b 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/segue/api/managers/QuestionManager.java +++ b/src/main/java/uk/ac/cam/cl/dtg/segue/api/managers/QuestionManager.java @@ -452,7 +452,7 @@ public Map Date: Thu, 23 Oct 2025 13:28:26 +0100 Subject: [PATCH 19/38] Restructure update_marks function as a scalar --- ...5-10-questions-attempts-add-mark-field.sql | 189 +++++++----------- 1 file changed, 74 insertions(+), 115 deletions(-) diff --git a/src/main/resources/db_scripts/migrations/2025-10-questions-attempts-add-mark-field.sql b/src/main/resources/db_scripts/migrations/2025-10-questions-attempts-add-mark-field.sql index b725097df5..6b67d1707a 100644 --- a/src/main/resources/db_scripts/migrations/2025-10-questions-attempts-add-mark-field.sql +++ b/src/main/resources/db_scripts/migrations/2025-10-questions-attempts-add-mark-field.sql @@ -4,125 +4,84 @@ ADD marks integer; ALTER TABLE quiz_question_attempts ADD marks integer; -CREATE FUNCTION update_marks_for_period(start_date date, end_date date) - RETURNS void AS $$ +CREATE OR REPLACE FUNCTION update_marks_for_period(qa question_attempts) + RETURNS int AS $$ BEGIN - WITH case_statement AS ( - SELECT - id, - CASE - WHEN - (question_attempt -> 'answer' ->> 'type') = 'llmFreeTextChoice' - THEN - (question_attempt ->> 'marksAwarded')::int - ELSE - correct::int - END AS marks - FROM question_attempts - WHERE timestamp > start_date - AND timestamp <= end_date - ) - - UPDATE question_attempts - SET marks = case_statement.marks - FROM case_statement - WHERE question_attempts.id = case_statement.id; + RETURN CASE + WHEN (qa.question_attempt -> 'answer' ->> 'type') = 'llmFreeTextChoice' + THEN (qa.question_attempt ->> 'marksAwarded')::int + ELSE qa.correct::int + END; END; $$ LANGUAGE plpgsql; -CREATE FUNCTION quiz_update_marks_for_period(start_date date, end_date date) - RETURNS void AS $$ -BEGIN - WITH case_statement AS ( - SELECT - id, - CASE - WHEN - (question_attempt -> 'answer' ->> 'type') = 'llmFreeTextChoice' - THEN - (question_attempt ->> 'marksAwarded')::int - ELSE - correct::int - END AS marks - FROM quiz_question_attempts - WHERE timestamp > start_date - AND timestamp <= end_date - ) - - UPDATE quiz_question_attempts - SET marks = case_statement.marks - FROM case_statement - WHERE quiz_question_attempts.id = case_statement.id; -END; -$$ LANGUAGE plpgsql; +UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2014-01-01' AND timestamp <= '2015-01-02'; +UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2015-01-01' AND timestamp <= '2016-01-02'; +UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2016-01-01' AND timestamp <= '2016-06-02'; +UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2016-06-01' AND timestamp <= '2017-01-02'; +UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2017-01-01' AND timestamp <= '2017-06-02'; +UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2017-06-01' AND timestamp <= '2018-01-02'; +UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2018-01-01' AND timestamp <= '2018-06-02'; +UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2018-06-01' AND timestamp <= '2018-10-02'; +UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2018-10-01' AND timestamp <= '2019-01-02'; +UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2019-01-01' AND timestamp <= '2019-06-02'; +UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2019-06-01' AND timestamp <= '2019-10-02'; +UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2019-10-01' AND timestamp <= '2020-01-02'; +UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2020-01-01' AND timestamp <= '2020-04-02'; +UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2020-04-01' AND timestamp <= '2020-09-02'; +UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2020-09-01' AND timestamp <= '2020-10-02'; +UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2020-10-01' AND timestamp <= '2021-01-02'; +UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2021-01-01' AND timestamp <= '2021-04-02'; +UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2021-04-01' AND timestamp <= '2021-09-02'; +UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2021-09-01' AND timestamp <= '2021-10-02'; +UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2021-10-01' AND timestamp <= '2022-01-02'; +UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2022-01-01' AND timestamp <= '2022-04-02'; +UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2022-04-01' AND timestamp <= '2022-09-02'; +UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2022-09-01' AND timestamp <= '2022-10-02'; +UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2022-10-01' AND timestamp <= '2023-01-02'; +UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2023-01-01' AND timestamp <= '2023-03-02'; +UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2023-03-01' AND timestamp <= '2023-06-02'; +UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2023-06-01' AND timestamp <= '2023-09-02'; +UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2023-09-01' AND timestamp <= '2023-10-02'; +UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2023-10-01' AND timestamp <= '2023-11-02'; +UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2023-11-01' AND timestamp <= '2024-01-02'; +UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2024-01-01' AND timestamp <= '2024-02-02'; +UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2024-02-01' AND timestamp <= '2024-03-02'; +UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2024-03-01' AND timestamp <= '2026-01-02'; -SELECT update_marks_for_period('2014-01-01', '2015-01-02'); -SELECT update_marks_for_period('2015-01-01', '2016-01-02'); -SELECT update_marks_for_period('2016-01-01', '2016-06-02'); -SELECT update_marks_for_period('2016-06-01', '2017-01-02'); -SELECT update_marks_for_period('2017-01-01', '2017-06-02'); -SELECT update_marks_for_period('2017-06-01', '2018-01-02'); -SELECT update_marks_for_period('2018-01-01', '2018-06-02'); -SELECT update_marks_for_period('2018-06-01', '2018-10-02'); -SELECT update_marks_for_period('2018-10-01', '2019-01-02'); -SELECT update_marks_for_period('2019-01-01', '2019-06-02'); -SELECT update_marks_for_period('2019-06-01', '2019-10-02'); -SELECT update_marks_for_period('2019-10-01', '2020-01-02'); -SELECT update_marks_for_period('2020-01-01', '2020-04-02'); -SELECT update_marks_for_period('2020-04-01', '2020-09-02'); -SELECT update_marks_for_period('2020-09-01', '2020-10-02'); -SELECT update_marks_for_period('2020-10-01', '2021-01-02'); -SELECT update_marks_for_period('2021-01-01', '2021-04-02'); -SELECT update_marks_for_period('2021-04-01', '2021-09-02'); -SELECT update_marks_for_period('2021-09-01', '2021-10-02'); -SELECT update_marks_for_period('2021-10-01', '2022-01-02'); -SELECT update_marks_for_period('2022-01-01', '2022-04-02'); -SELECT update_marks_for_period('2022-04-01', '2022-09-02'); -SELECT update_marks_for_period('2022-09-01', '2022-10-02'); -SELECT update_marks_for_period('2022-10-01', '2023-01-02'); -SELECT update_marks_for_period('2023-01-01', '2023-03-02'); -SELECT update_marks_for_period('2023-03-01', '2023-06-02'); -SELECT update_marks_for_period('2023-06-01', '2023-09-02'); -SELECT update_marks_for_period('2023-09-01', '2023-10-02'); -SELECT update_marks_for_period('2023-10-01', '2023-11-02'); -SELECT update_marks_for_period('2023-11-01', '2024-01-02'); -SELECT update_marks_for_period('2024-01-01', '2024-02-02'); -SELECT update_marks_for_period('2024-02-01', '2024-03-02'); -SELECT update_marks_for_period('2024-03-01', '2026-01-02'); -SELECT quiz_update_marks_for_period('2014-01-01', '2015-01-02'); -SELECT quiz_update_marks_for_period('2015-01-01', '2016-01-02'); -SELECT quiz_update_marks_for_period('2016-01-01', '2016-06-02'); -SELECT quiz_update_marks_for_period('2016-06-01', '2017-01-02'); -SELECT quiz_update_marks_for_period('2017-01-01', '2017-06-02'); -SELECT quiz_update_marks_for_period('2017-06-01', '2018-01-02'); -SELECT quiz_update_marks_for_period('2018-01-01', '2018-06-02'); -SELECT quiz_update_marks_for_period('2018-06-01', '2018-10-02'); -SELECT quiz_update_marks_for_period('2018-10-01', '2019-01-02'); -SELECT quiz_update_marks_for_period('2019-01-01', '2019-06-02'); -SELECT quiz_update_marks_for_period('2019-06-01', '2019-10-02'); -SELECT quiz_update_marks_for_period('2019-10-01', '2020-01-02'); -SELECT quiz_update_marks_for_period('2020-01-01', '2020-04-02'); -SELECT quiz_update_marks_for_period('2020-04-01', '2020-09-02'); -SELECT quiz_update_marks_for_period('2020-09-01', '2020-10-02'); -SELECT quiz_update_marks_for_period('2020-10-01', '2021-01-02'); -SELECT quiz_update_marks_for_period('2021-01-01', '2021-04-02'); -SELECT quiz_update_marks_for_period('2021-04-01', '2021-09-02'); -SELECT quiz_update_marks_for_period('2021-09-01', '2021-10-02'); -SELECT quiz_update_marks_for_period('2021-10-01', '2022-01-02'); -SELECT quiz_update_marks_for_period('2022-01-01', '2022-04-02'); -SELECT quiz_update_marks_for_period('2022-04-01', '2022-09-02'); -SELECT quiz_update_marks_for_period('2022-09-01', '2022-10-02'); -SELECT quiz_update_marks_for_period('2022-10-01', '2023-01-02'); -SELECT quiz_update_marks_for_period('2023-01-01', '2023-03-02'); -SELECT quiz_update_marks_for_period('2023-03-01', '2023-06-02'); -SELECT quiz_update_marks_for_period('2023-06-01', '2023-09-02'); -SELECT quiz_update_marks_for_period('2023-09-01', '2023-10-02'); -SELECT quiz_update_marks_for_period('2023-10-01', '2023-11-02'); -SELECT quiz_update_marks_for_period('2023-11-01', '2024-01-02'); -SELECT quiz_update_marks_for_period('2024-01-01', '2024-02-02'); -SELECT quiz_update_marks_for_period('2024-02-01', '2024-03-02'); -SELECT quiz_update_marks_for_period('2024-03-01', '2026-01-02'); +UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2014-01-01' AND timestamp <= '2015-01-02'; +UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2015-01-01' AND timestamp <= '2016-01-02'; +UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2016-01-01' AND timestamp <= '2016-06-02'; +UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2016-06-01' AND timestamp <= '2017-01-02'; +UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2017-01-01' AND timestamp <= '2017-06-02'; +UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2017-06-01' AND timestamp <= '2018-01-02'; +UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2018-01-01' AND timestamp <= '2018-06-02'; +UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2018-06-01' AND timestamp <= '2018-10-02'; +UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2018-10-01' AND timestamp <= '2019-01-02'; +UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2019-01-01' AND timestamp <= '2019-06-02'; +UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2019-06-01' AND timestamp <= '2019-10-02'; +UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2019-10-01' AND timestamp <= '2020-01-02'; +UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2020-01-01' AND timestamp <= '2020-04-02'; +UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2020-04-01' AND timestamp <= '2020-09-02'; +UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2020-09-01' AND timestamp <= '2020-10-02'; +UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2020-10-01' AND timestamp <= '2021-01-02'; +UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2021-01-01' AND timestamp <= '2021-04-02'; +UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2021-04-01' AND timestamp <= '2021-09-02'; +UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2021-09-01' AND timestamp <= '2021-10-02'; +UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2021-10-01' AND timestamp <= '2022-01-02'; +UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2022-01-01' AND timestamp <= '2022-04-02'; +UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2022-04-01' AND timestamp <= '2022-09-02'; +UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2022-09-01' AND timestamp <= '2022-10-02'; +UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2022-10-01' AND timestamp <= '2023-01-02'; +UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2023-01-01' AND timestamp <= '2023-03-02'; +UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2023-03-01' AND timestamp <= '2023-06-02'; +UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2023-06-01' AND timestamp <= '2023-09-02'; +UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2023-09-01' AND timestamp <= '2023-10-02'; +UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2023-10-01' AND timestamp <= '2023-11-02'; +UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2023-11-01' AND timestamp <= '2024-01-02'; +UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2024-01-01' AND timestamp <= '2024-02-02'; +UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2024-02-01' AND timestamp <= '2024-03-02'; +UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2024-03-01' AND timestamp <= '2026-01-02'; -DROP FUNCTION update_marks_for_period; -DROP FUNCTION quiz_update_marks_for_period; \ No newline at end of file +DROP FUNCTION update_marks_for_period; \ No newline at end of file From a62e73fa76311b9beabb8d7f59d60f907db389db Mon Sep 17 00:00:00 2001 From: Sol Dubock <94075844+sjd210@users.noreply.github.com> Date: Thu, 23 Oct 2025 14:06:31 +0100 Subject: [PATCH 20/38] Move game item augmentation to another branch --- .../dtg/isaac/api/managers/GameManager.java | 73 +++++------------ ...5-10-questions-attempts-add-mark-field.sql | 80 +++++++++++-------- 2 files changed, 64 insertions(+), 89 deletions(-) diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/GameManager.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/GameManager.java index 6e5310d5c7..a479ebd0b6 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/GameManager.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/api/managers/GameManager.java @@ -29,11 +29,9 @@ import uk.ac.cam.cl.dtg.isaac.dos.AudienceContext; import uk.ac.cam.cl.dtg.isaac.dos.GameboardContentDescriptor; import uk.ac.cam.cl.dtg.isaac.dos.GameboardCreationMethod; -import uk.ac.cam.cl.dtg.isaac.dos.IsaacLLMFreeTextQuestion; import uk.ac.cam.cl.dtg.isaac.dos.IsaacQuestionPage; import uk.ac.cam.cl.dtg.isaac.dos.IsaacQuickQuestion; import uk.ac.cam.cl.dtg.isaac.dos.IsaacWildcard; -import uk.ac.cam.cl.dtg.isaac.dos.LLMFreeTextQuestionValidationResponse; import uk.ac.cam.cl.dtg.isaac.dos.LightweightQuestionValidationResponse; import uk.ac.cam.cl.dtg.isaac.dos.QuestionValidationResponse; import uk.ac.cam.cl.dtg.isaac.dos.content.Content; @@ -1150,68 +1148,33 @@ private void augmentGameItemWithAttemptInformation( for (Content questionPart : listOfQuestionParts) { List questionPartAttempts = questionAttempts.get(questionPart.getId()); - if (questionPart instanceof IsaacLLMFreeTextQuestion) { - int maxMarks = ((IsaacLLMFreeTextQuestion) questionPart).getMaxMarks(); - if (questionPartAttempts != null && !questionPartAttempts.isEmpty()) { - int greatestMarksForThisQuestion = 0; - for (LightweightQuestionValidationResponse attempt: questionPartAttempts) { - if (attempt.getMarks() > greatestMarksForThisQuestion) { - greatestMarksForThisQuestion = attempt.getMarks(); - } - } - for (int markCorrect = 0; markCorrect < greatestMarksForThisQuestion; markCorrect++) { - questionPartStates.add(QuestionPartState.CORRECT); - questionPartsCorrect++; - } - for (int markIncorrect = 0; markIncorrect < (maxMarks - greatestMarksForThisQuestion); markIncorrect++) { - questionPartStates.add(QuestionPartState.INCORRECT); - questionPartsIncorrect++; - } - } else { - for (int mark = 0; mark < maxMarks; mark++) { - questionPartStates.add(QuestionPartState.NOT_ATTEMPTED); - questionPartsNotAttempted++; + if (questionPartAttempts != null) { + // Go through the attempts in reverse chronological order for this question part to determine if + // there is a correct answer somewhere. + boolean foundCorrectForThisQuestion = false; + for (int i = questionPartAttempts.size() - 1; i >= 0; i--) { + if (questionPartAttempts.get(i).isCorrect() != null + && questionPartAttempts.get(i).isCorrect()) { + foundCorrectForThisQuestion = true; + break; } } - } - else { - if (questionPartAttempts != null) { - // Go through the attempts in reverse chronological order for this question part to determine if - // there is a correct answer somewhere. - boolean foundCorrectForThisQuestion = false; - for (int i = questionPartAttempts.size() - 1; i >= 0; i--) { - if (questionPartAttempts.get(i).isCorrect() != null - && questionPartAttempts.get(i).isCorrect()) { - foundCorrectForThisQuestion = true; - break; - } - } - if (foundCorrectForThisQuestion) { - questionPartStates.add(QuestionPartState.CORRECT); - questionPartsCorrect++; - } else { - questionPartStates.add(QuestionPartState.INCORRECT); - questionPartsIncorrect++; - } + if (foundCorrectForThisQuestion) { + questionPartStates.add(QuestionPartState.CORRECT); + questionPartsCorrect++; } else { - questionPartStates.add(QuestionPartState.NOT_ATTEMPTED); - questionPartsNotAttempted++; - } - } - } - } else { - for (Content questionPart : listOfQuestionParts) { - if (questionPart instanceof IsaacLLMFreeTextQuestion) { - int maxMarks = ((IsaacLLMFreeTextQuestion) questionPart).getMaxMarks(); - for (int mark = 0; mark < maxMarks; mark++) { - questionPartStates.add(QuestionPartState.NOT_ATTEMPTED); - questionPartsNotAttempted++; + questionPartStates.add(QuestionPartState.INCORRECT); + questionPartsIncorrect++; } } else { questionPartStates.add(QuestionPartState.NOT_ATTEMPTED); questionPartsNotAttempted++; } } + } else { + questionPartsNotAttempted = listOfQuestionParts.size(); + questionPartStates = listOfQuestionParts.stream() + .map(_q -> QuestionPartState.NOT_ATTEMPTED).collect(Collectors.toList()); } // Get the pass mark for the question page diff --git a/src/main/resources/db_scripts/migrations/2025-10-questions-attempts-add-mark-field.sql b/src/main/resources/db_scripts/migrations/2025-10-questions-attempts-add-mark-field.sql index 6b67d1707a..d7886066d6 100644 --- a/src/main/resources/db_scripts/migrations/2025-10-questions-attempts-add-mark-field.sql +++ b/src/main/resources/db_scripts/migrations/2025-10-questions-attempts-add-mark-field.sql @@ -15,6 +15,17 @@ BEGIN END; $$ LANGUAGE plpgsql; +CREATE OR REPLACE FUNCTION quiz_update_marks_for_period(qa quiz_question_attempts) + RETURNS int AS $$ +BEGIN + RETURN CASE + WHEN (qa.question_attempt -> 'answer' ->> 'type') = 'llmFreeTextChoice' + THEN (qa.question_attempt ->> 'marksAwarded')::int + ELSE qa.correct::int + END; +END; +$$ LANGUAGE plpgsql; + UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2014-01-01' AND timestamp <= '2015-01-02'; UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2015-01-01' AND timestamp <= '2016-01-02'; UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2016-01-01' AND timestamp <= '2016-06-02'; @@ -50,38 +61,39 @@ UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2024-03-01' AND timestamp <= '2026-01-02'; -UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2014-01-01' AND timestamp <= '2015-01-02'; -UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2015-01-01' AND timestamp <= '2016-01-02'; -UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2016-01-01' AND timestamp <= '2016-06-02'; -UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2016-06-01' AND timestamp <= '2017-01-02'; -UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2017-01-01' AND timestamp <= '2017-06-02'; -UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2017-06-01' AND timestamp <= '2018-01-02'; -UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2018-01-01' AND timestamp <= '2018-06-02'; -UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2018-06-01' AND timestamp <= '2018-10-02'; -UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2018-10-01' AND timestamp <= '2019-01-02'; -UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2019-01-01' AND timestamp <= '2019-06-02'; -UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2019-06-01' AND timestamp <= '2019-10-02'; -UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2019-10-01' AND timestamp <= '2020-01-02'; -UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2020-01-01' AND timestamp <= '2020-04-02'; -UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2020-04-01' AND timestamp <= '2020-09-02'; -UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2020-09-01' AND timestamp <= '2020-10-02'; -UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2020-10-01' AND timestamp <= '2021-01-02'; -UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2021-01-01' AND timestamp <= '2021-04-02'; -UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2021-04-01' AND timestamp <= '2021-09-02'; -UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2021-09-01' AND timestamp <= '2021-10-02'; -UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2021-10-01' AND timestamp <= '2022-01-02'; -UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2022-01-01' AND timestamp <= '2022-04-02'; -UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2022-04-01' AND timestamp <= '2022-09-02'; -UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2022-09-01' AND timestamp <= '2022-10-02'; -UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2022-10-01' AND timestamp <= '2023-01-02'; -UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2023-01-01' AND timestamp <= '2023-03-02'; -UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2023-03-01' AND timestamp <= '2023-06-02'; -UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2023-06-01' AND timestamp <= '2023-09-02'; -UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2023-09-01' AND timestamp <= '2023-10-02'; -UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2023-10-01' AND timestamp <= '2023-11-02'; -UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2023-11-01' AND timestamp <= '2024-01-02'; -UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2024-01-01' AND timestamp <= '2024-02-02'; -UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2024-02-01' AND timestamp <= '2024-03-02'; -UPDATE quiz_question_attempts SET marks = update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2024-03-01' AND timestamp <= '2026-01-02'; +UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2014-01-01' AND timestamp <= '2015-01-02'; +UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2015-01-01' AND timestamp <= '2016-01-02'; +UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2016-01-01' AND timestamp <= '2016-06-02'; +UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2016-06-01' AND timestamp <= '2017-01-02'; +UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2017-01-01' AND timestamp <= '2017-06-02'; +UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2017-06-01' AND timestamp <= '2018-01-02'; +UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2018-01-01' AND timestamp <= '2018-06-02'; +UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2018-06-01' AND timestamp <= '2018-10-02'; +UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2018-10-01' AND timestamp <= '2019-01-02'; +UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2019-01-01' AND timestamp <= '2019-06-02'; +UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2019-06-01' AND timestamp <= '2019-10-02'; +UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2019-10-01' AND timestamp <= '2020-01-02'; +UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2020-01-01' AND timestamp <= '2020-04-02'; +UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2020-04-01' AND timestamp <= '2020-09-02'; +UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2020-09-01' AND timestamp <= '2020-10-02'; +UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2020-10-01' AND timestamp <= '2021-01-02'; +UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2021-01-01' AND timestamp <= '2021-04-02'; +UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2021-04-01' AND timestamp <= '2021-09-02'; +UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2021-09-01' AND timestamp <= '2021-10-02'; +UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2021-10-01' AND timestamp <= '2022-01-02'; +UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2022-01-01' AND timestamp <= '2022-04-02'; +UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2022-04-01' AND timestamp <= '2022-09-02'; +UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2022-09-01' AND timestamp <= '2022-10-02'; +UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2022-10-01' AND timestamp <= '2023-01-02'; +UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2023-01-01' AND timestamp <= '2023-03-02'; +UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2023-03-01' AND timestamp <= '2023-06-02'; +UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2023-06-01' AND timestamp <= '2023-09-02'; +UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2023-09-01' AND timestamp <= '2023-10-02'; +UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2023-10-01' AND timestamp <= '2023-11-02'; +UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2023-11-01' AND timestamp <= '2024-01-02'; +UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2024-01-01' AND timestamp <= '2024-02-02'; +UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2024-02-01' AND timestamp <= '2024-03-02'; +UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2024-03-01' AND timestamp <= '2026-01-02'; -DROP FUNCTION update_marks_for_period; \ No newline at end of file +DROP FUNCTION update_marks_for_period; +DROP FUNCTION quiz_update_marks_for_period; \ No newline at end of file From 187471661efa32b5f1832ab46e2a759492aa4d72 Mon Sep 17 00:00:00 2001 From: Sol Dubock <94075844+sjd210@users.noreply.github.com> Date: Thu, 23 Oct 2025 14:42:09 +0100 Subject: [PATCH 21/38] Add marks to QuestionValidationResponseDTO --- ...LightweightQuestionValidationResponse.java | 2 +- .../isaac/dos/QuestionValidationResponse.java | 6 ++--- .../dto/FormulaValidationResponseDTO.java | 8 +++--- .../isaac/dto/ItemValidationResponseDTO.java | 5 ++-- .../dto/QuantityValidationResponseDTO.java | 6 +++-- .../dto/QuestionValidationResponseDTO.java | 27 +++++++++++++++++-- .../cl/dtg/isaac/quiz/PgQuestionAttempts.java | 4 ++- .../api/managers/QuizQuestionManagerTest.java | 4 +-- 8 files changed, 46 insertions(+), 16 deletions(-) diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/LightweightQuestionValidationResponse.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/LightweightQuestionValidationResponse.java index 2ad9f48661..223b93df85 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/LightweightQuestionValidationResponse.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/LightweightQuestionValidationResponse.java @@ -118,6 +118,6 @@ public void setMarks(final Integer marks) { @Override public String toString() { return "QuestionValidationResponse [questionId=" + questionId + ", correct=" + correct + - ", dateAttempted=" + dateAttempted + "]"; + ", dateAttempted=" + dateAttempted + ", marks=" + marks + "]"; } } diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/QuestionValidationResponse.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/QuestionValidationResponse.java index c93fc487e4..f635c63c7e 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/QuestionValidationResponse.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/QuestionValidationResponse.java @@ -62,7 +62,7 @@ public QuestionValidationResponse(final String questionId, final Choice answer, } /** - * Constructor without specifying marks (derived from correct) + * Constructor without specifying marks (instead derived from 'correct') * * @param questionId * - @@ -77,7 +77,7 @@ public QuestionValidationResponse(final String questionId, final Choice answer, */ public QuestionValidationResponse(final String questionId, final Choice answer, final Boolean correct, final Content explanation, final Date dateAttempted) { - super(questionId, correct, dateAttempted, (correct != null && correct) ? 1 : 0); + super(questionId, correct, dateAttempted, Boolean.TRUE.equals(correct) ? 1 : 0); this.answer = answer; this.explanation = explanation; } @@ -124,7 +124,7 @@ public final void setExplanation(final Content explanation) { public String toString() { return "QuestionValidationResponse [questionId=" + super.getQuestionId() + ", answer=" + answer + ", correct=" + super.isCorrect() + ", explanation=" + explanation + - ", dateAttempted=" + super.getDateAttempted() + "]"; + ", dateAttempted=" + super.getDateAttempted() + ", marks=" + super.getMarks() + "]"; } } diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/FormulaValidationResponseDTO.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/FormulaValidationResponseDTO.java index 5bd5b4c2ec..eff458da4e 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/FormulaValidationResponseDTO.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/FormulaValidationResponseDTO.java @@ -52,12 +52,14 @@ public FormulaValidationResponseDTO() { * - * @param dateAttempted * - + * @param marks + * - */ public FormulaValidationResponseDTO(final String questionId, final ChoiceDTO answer, final ContentDTO explanation, final Boolean correctExact, final Boolean correctSymbolic, final Boolean correctNumeric, - final Date dateAttempted) { - super(questionId, answer, correctSymbolic || correctNumeric, explanation, dateAttempted); + final Date dateAttempted, final Integer marks) { + super(questionId, answer, correctSymbolic || correctNumeric, explanation, dateAttempted, marks); this.correctExact = correctExact; this.correctSymbolic = correctSymbolic; this.correctNumeric = correctNumeric; @@ -128,6 +130,6 @@ public final void setCorrectNumeric(final Boolean correctNumeric) { @Override public String toString() { - return "FormulaValidationResponseDTO [correctExact=" + correctExact + "correctSymbolic=" + correctSymbolic + ", correctNumeric=" + correctNumeric + "]"; + return "FormulaValidationResponseDTO [correctExact=" + correctExact + "correctSymbolic=" + correctSymbolic + ", correctNumeric=" + correctNumeric + ", marks=" + marks"]"; } } diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/ItemValidationResponseDTO.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/ItemValidationResponseDTO.java index f4bfb3bab9..d39f35a8ab 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/ItemValidationResponseDTO.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/ItemValidationResponseDTO.java @@ -46,11 +46,12 @@ public ItemValidationResponseDTO() { * @param itemsCorrect - ordered list of correctness status of each submitted item. * @param explanation - explanation. * @param dateAttempted - dateAttempted. + * @param marks - marks */ public ItemValidationResponseDTO(final String questionId, final ChoiceDTO answer, final Boolean correct, final List itemsCorrect, - final ContentDTO explanation, final Date dateAttempted) { - super(questionId, answer, correct, explanation, dateAttempted); + final ContentDTO explanation, final Date dateAttempted, final Integer marks) { + super(questionId, answer, correct, explanation, dateAttempted, marks); this.itemsCorrect = itemsCorrect; } diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/QuantityValidationResponseDTO.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/QuantityValidationResponseDTO.java index 0b9b4af85e..8e49389a8f 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/QuantityValidationResponseDTO.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/QuantityValidationResponseDTO.java @@ -53,11 +53,13 @@ public QuantityValidationResponseDTO() { * - * @param dateAttempted * - + * @param marks + * - */ public QuantityValidationResponseDTO(final String questionId, final ChoiceDTO answer, final Boolean correct, final ContentDTO explanation, final Boolean correctValue, - final Boolean correctUnits, final Date dateAttempted) { - super(questionId, answer, correct, explanation, dateAttempted); + final Boolean correctUnits, final Date dateAttempted, final Integer marks) { + super(questionId, answer, correct, explanation, dateAttempted, marks); this.correctValue = correctValue; this.correctUnits = correctUnits; } diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/QuestionValidationResponseDTO.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/QuestionValidationResponseDTO.java index 4b06bfdaa6..3c92f8cb4a 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/QuestionValidationResponseDTO.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/QuestionValidationResponseDTO.java @@ -30,6 +30,7 @@ public class QuestionValidationResponseDTO { private Boolean correct; private ContentDTO explanation; private Date dateAttempted; + private Integer marks; /** * Default Constructor for mappers. @@ -51,14 +52,17 @@ public QuestionValidationResponseDTO() { * - * @param dateAttempted * - + * @param marks + * - */ public QuestionValidationResponseDTO(final String questionId, final ChoiceDTO answer, final Boolean correct, - final ContentDTO explanation, final Date dateAttempted) { + final ContentDTO explanation, final Date dateAttempted, final Integer marks) { this.questionId = questionId; this.answer = answer; this.correct = correct; this.explanation = explanation; this.dateAttempted = dateAttempted; + this.marks = marks; } /** @@ -156,9 +160,28 @@ public void setDateAttempted(final Date dateAttempted) { this.dateAttempted = dateAttempted; } + /** + * Gets the marks. + * + * @return the marks + */ + public Integer getMarks() { + return marks; + } + + /** + * Sets the marks. + * + * @param marks + * the marks to set + */ + public void setMarks(final Integer marks) { + this.marks = marks; + } + @Override public String toString() { return "QuestionValidationResponseDTO [questionId=" + questionId + ", answer=" + answer + ", correct=" - + correct + ", explanation=" + explanation + ", dateAttempted=" + dateAttempted + "]"; + + correct + ", explanation=" + explanation + ", dateAttempted=" + dateAttempted + ", marks=" + marks + "]"; } } diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java index 3f94ba7d55..19a2d83198 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java @@ -26,7 +26,6 @@ import uk.ac.cam.cl.dtg.isaac.dos.LightweightQuestionValidationResponse; import uk.ac.cam.cl.dtg.isaac.dos.QuestionValidationResponse; import uk.ac.cam.cl.dtg.isaac.dos.users.Role; -import uk.ac.cam.cl.dtg.segue.api.managers.QuestionManager; import uk.ac.cam.cl.dtg.segue.dao.SegueDatabaseException; import uk.ac.cam.cl.dtg.segue.dao.SegueDatabaseLockTimoutException; import uk.ac.cam.cl.dtg.segue.dao.content.ContentMapper; @@ -209,11 +208,14 @@ public void registerQuestionAttempt(final Long userId, final String questionPage if (questionAttempt.getMarks() != null) { if (questionAttempt instanceof LLMFreeTextQuestionValidationResponse) { pst.setInt(7, ((LLMFreeTextQuestionValidationResponse) questionAttempt).getMarksAwarded()); + log.warn("LLM response"); } else { if (questionAttempt.isCorrect()) { pst.setInt(7, 1); + log.warn("1 mark response"); } else { pst.setInt(7, 0); + log.warn("0 mark response"); } } } else { diff --git a/src/test/java/uk/ac/cam/cl/dtg/isaac/api/managers/QuizQuestionManagerTest.java b/src/test/java/uk/ac/cam/cl/dtg/isaac/api/managers/QuizQuestionManagerTest.java index c524a7c421..6409a1e424 100644 --- a/src/test/java/uk/ac/cam/cl/dtg/isaac/api/managers/QuizQuestionManagerTest.java +++ b/src/test/java/uk/ac/cam/cl/dtg/isaac/api/managers/QuizQuestionManagerTest.java @@ -111,8 +111,8 @@ public void initializeAdditionalObjects() { correctResponse = new QuestionValidationResponse(question.getId(), correctAnswer, true, correctExplanation, somePastDate); wrongResponse = new QuestionValidationResponse(question.getId(), wrongAnswer, false, wrongExplanation, somePastDate); - correctResponseDTO = new QuestionValidationResponseDTO(question.getId(), correctAnswerDTO, true, correctExplanationDTO, somePastDate); - wrongResponseDTO = new QuestionValidationResponseDTO(question.getId(), wrongAnswerDTO, false, wrongExplanationDTO, somePastDate); + correctResponseDTO = new QuestionValidationResponseDTO(question.getId(), correctAnswerDTO, true, correctExplanationDTO, somePastDate, 1); + wrongResponseDTO = new QuestionValidationResponseDTO(question.getId(), wrongAnswerDTO, false, wrongExplanationDTO, somePastDate, 0); quantityResponse = new QuantityValidationResponse(question.getId(), correctAnswer, true, correctExplanation, true, true, somePastDate); From 733d7774d44205a1225d9f277b11f69ca76e9c2a Mon Sep 17 00:00:00 2001 From: Sol Dubock <94075844+sjd210@users.noreply.github.com> Date: Thu, 23 Oct 2025 14:47:39 +0100 Subject: [PATCH 22/38] Correct syntax error --- .../ac/cam/cl/dtg/isaac/dto/FormulaValidationResponseDTO.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/FormulaValidationResponseDTO.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/FormulaValidationResponseDTO.java index eff458da4e..c685e95385 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/FormulaValidationResponseDTO.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/FormulaValidationResponseDTO.java @@ -130,6 +130,6 @@ public final void setCorrectNumeric(final Boolean correctNumeric) { @Override public String toString() { - return "FormulaValidationResponseDTO [correctExact=" + correctExact + "correctSymbolic=" + correctSymbolic + ", correctNumeric=" + correctNumeric + ", marks=" + marks"]"; + return "FormulaValidationResponseDTO [correctExact=" + correctExact + "correctSymbolic=" + correctSymbolic + ", correctNumeric=" + correctNumeric + "]"; } } From 4cd2ea1a8d2234f3bac5716113c74760db712be3 Mon Sep 17 00:00:00 2001 From: Sol Dubock <94075844+sjd210@users.noreply.github.com> Date: Thu, 23 Oct 2025 14:49:12 +0100 Subject: [PATCH 23/38] Simplify conditional --- .../uk/ac/cam/cl/dtg/isaac/dos/QuestionValidationResponse.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/QuestionValidationResponse.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/QuestionValidationResponse.java index f635c63c7e..8325a6ea2b 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/QuestionValidationResponse.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/QuestionValidationResponse.java @@ -77,7 +77,7 @@ public QuestionValidationResponse(final String questionId, final Choice answer, */ public QuestionValidationResponse(final String questionId, final Choice answer, final Boolean correct, final Content explanation, final Date dateAttempted) { - super(questionId, correct, dateAttempted, Boolean.TRUE.equals(correct) ? 1 : 0); + super(questionId, correct, dateAttempted, (correct != null && correct) ? 1 : 0); this.answer = answer; this.explanation = explanation; } From 351d5146dab77c8174d4c512a4b12d8e61117c77 Mon Sep 17 00:00:00 2001 From: Sol Dubock <94075844+sjd210@users.noreply.github.com> Date: Thu, 23 Oct 2025 15:10:20 +0100 Subject: [PATCH 24/38] Replace marksAwarded with marks in LLM response --- .../dtg/isaac/dos/LLMFreeTextQuestionValidationResponse.java | 5 ++--- .../isaac/dto/LLMFreeTextQuestionValidationResponseDTO.java | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/LLMFreeTextQuestionValidationResponse.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/LLMFreeTextQuestionValidationResponse.java index f408ef2a76..87caa526b6 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/LLMFreeTextQuestionValidationResponse.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/LLMFreeTextQuestionValidationResponse.java @@ -11,7 +11,6 @@ @DTOMapping(LLMFreeTextQuestionValidationResponseDTO.class) public class LLMFreeTextQuestionValidationResponse extends QuestionValidationResponse { - private Integer marksAwarded; private List markBreakdown; public LLMFreeTextQuestionValidationResponse() { @@ -24,10 +23,10 @@ public LLMFreeTextQuestionValidationResponse(final String questionId, final Choi } public Integer getMarksAwarded() { - return marksAwarded; + return super.getMarks(); } public void setMarksAwarded(Integer marksAwarded) { - this.marksAwarded = marksAwarded; + super.setMarks(marksAwarded); } public List getMarkBreakdown() { diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/LLMFreeTextQuestionValidationResponseDTO.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/LLMFreeTextQuestionValidationResponseDTO.java index 8baa85aaa1..f23652cd7f 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/LLMFreeTextQuestionValidationResponseDTO.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/LLMFreeTextQuestionValidationResponseDTO.java @@ -5,17 +5,16 @@ import java.util.List; public class LLMFreeTextQuestionValidationResponseDTO extends QuestionValidationResponseDTO { - private Integer marksAwarded; private List markBreakdown; public LLMFreeTextQuestionValidationResponseDTO() { } public Integer getMarksAwarded() { - return marksAwarded; + return super.getMarks(); } public void setMarksAwarded(Integer marksAwarded) { - this.marksAwarded = marksAwarded; + super.setMarks(marksAwarded); } public List getMarkBreakdown() { From b750792a9a6991bb3fad53d01e99635f7185eec3 Mon Sep 17 00:00:00 2001 From: Sol Dubock <94075844+sjd210@users.noreply.github.com> Date: Thu, 23 Oct 2025 15:38:56 +0100 Subject: [PATCH 25/38] Allow quiz question attempt to register with marks --- ...PgQuizQuestionAttemptPersistenceManager.java | 17 ++++++++++++++++- .../cl/dtg/isaac/quiz/PgQuestionAttempts.java | 3 --- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/dao/PgQuizQuestionAttemptPersistenceManager.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/dao/PgQuizQuestionAttemptPersistenceManager.java index aafa1a70f4..864fe006c8 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/dao/PgQuizQuestionAttemptPersistenceManager.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/dao/PgQuizQuestionAttemptPersistenceManager.java @@ -22,6 +22,7 @@ import com.google.inject.Inject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import uk.ac.cam.cl.dtg.isaac.dos.LLMFreeTextQuestionValidationResponse; import uk.ac.cam.cl.dtg.segue.dao.SegueDatabaseException; import uk.ac.cam.cl.dtg.segue.dao.content.ContentMapper; import uk.ac.cam.cl.dtg.segue.database.PostgresSqlDb; @@ -60,7 +61,7 @@ public PgQuizQuestionAttemptPersistenceManager(final PostgresSqlDb database, fin public void registerQuestionAttempt(Long quizAttemptId, QuestionValidationResponse questionResponse) throws SegueDatabaseException { String query = "INSERT INTO quiz_question_attempts(quiz_attempt_id, question_id, question_attempt, correct, \"timestamp\", marks)" + - " VALUES (?, ?, ?::text::jsonb, ?, ?);"; + " VALUES (?, ?, ?::text::jsonb, ?, ?, ?);"; try (Connection conn = database.getDatabaseConnection(); PreparedStatement pst = conn.prepareStatement(query, Statement.RETURN_GENERATED_KEYS); ) { @@ -75,6 +76,20 @@ public void registerQuestionAttempt(Long quizAttemptId, QuestionValidationRespon } pst.setTimestamp(5, new java.sql.Timestamp(questionResponse.getDateAttempted().getTime())); + if (questionResponse.getMarks() != null) { + if (questionResponse instanceof LLMFreeTextQuestionValidationResponse) { + pst.setInt(6, ((LLMFreeTextQuestionValidationResponse) questionResponse).getMarksAwarded()); + } else { + if (questionResponse.isCorrect()) { + pst.setInt(6, 1); + } else { + pst.setInt(6, 0); + } + } + } else { + pst.setInt(6, java.sql.Types.NULL); + } + if (pst.executeUpdate() == 0) { throw new SegueDatabaseException("Unable to save quiz question attempt."); } diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java index 19a2d83198..ebf26c36f4 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java @@ -208,14 +208,11 @@ public void registerQuestionAttempt(final Long userId, final String questionPage if (questionAttempt.getMarks() != null) { if (questionAttempt instanceof LLMFreeTextQuestionValidationResponse) { pst.setInt(7, ((LLMFreeTextQuestionValidationResponse) questionAttempt).getMarksAwarded()); - log.warn("LLM response"); } else { if (questionAttempt.isCorrect()) { pst.setInt(7, 1); - log.warn("1 mark response"); } else { pst.setInt(7, 0); - log.warn("0 mark response"); } } } else { From 05b8e3c3ca0ea982df35416966b62a785c101e41 Mon Sep 17 00:00:00 2001 From: Sol Dubock <94075844+sjd210@users.noreply.github.com> Date: Thu, 23 Oct 2025 16:39:31 +0100 Subject: [PATCH 26/38] Cleanup unnecessary changes --- .../cl/dtg/isaac/quiz/IQuestionAttemptManager.java | 1 - .../cl/dtg/segue/api/managers/QuestionManager.java | 14 +------------- .../migrations/2021-02-create_quiz_tables.sql | 3 +-- .../isaac/api/AbstractIsaacIntegrationTest.java | 10 ++++------ 4 files changed, 6 insertions(+), 22 deletions(-) diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/IQuestionAttemptManager.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/IQuestionAttemptManager.java index 1e1dfde0ae..71b9dd99cd 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/IQuestionAttemptManager.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/IQuestionAttemptManager.java @@ -1,6 +1,5 @@ package uk.ac.cam.cl.dtg.isaac.quiz; -import uk.ac.cam.cl.dtg.isaac.dos.LLMFreeTextQuestionValidationResponse; import uk.ac.cam.cl.dtg.isaac.dos.LightweightQuestionValidationResponse; import uk.ac.cam.cl.dtg.isaac.dos.QuestionValidationResponse; import uk.ac.cam.cl.dtg.isaac.dos.users.Role; diff --git a/src/main/java/uk/ac/cam/cl/dtg/segue/api/managers/QuestionManager.java b/src/main/java/uk/ac/cam/cl/dtg/segue/api/managers/QuestionManager.java index da7f6f953b..b6ebf70409 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/segue/api/managers/QuestionManager.java +++ b/src/main/java/uk/ac/cam/cl/dtg/segue/api/managers/QuestionManager.java @@ -26,8 +26,6 @@ import org.joda.time.LocalDate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import uk.ac.cam.cl.dtg.isaac.dos.IsaacQuestionPage; -import uk.ac.cam.cl.dtg.isaac.dos.LLMFreeTextQuestionValidationResponse; import uk.ac.cam.cl.dtg.isaac.dos.LightweightQuestionValidationResponse; import uk.ac.cam.cl.dtg.isaac.dos.QuestionValidationResponse; import uk.ac.cam.cl.dtg.isaac.dos.TestCase; @@ -63,16 +61,13 @@ import uk.ac.cam.cl.dtg.segue.api.ErrorResponseWrapper; import uk.ac.cam.cl.dtg.segue.configuration.SegueGuiceConfigurationModule; import uk.ac.cam.cl.dtg.segue.dao.SegueDatabaseException; -import uk.ac.cam.cl.dtg.segue.dao.content.ContentManagerException; import uk.ac.cam.cl.dtg.segue.dao.content.ContentMapper; -import uk.ac.cam.cl.dtg.segue.dao.content.GitContentManager; import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.core.Response; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.List; @@ -80,7 +75,6 @@ import java.util.Objects; import java.util.Random; -import static uk.ac.cam.cl.dtg.isaac.api.managers.GameManager.getAllMarkableDOQuestionPartsDFSOrder; import static uk.ac.cam.cl.dtg.segue.api.monitors.SegueMetrics.VALIDATOR_LATENCY_HISTOGRAM; /** @@ -95,8 +89,6 @@ public class QuestionManager { private final ContentMapper mapper; private final IQuestionAttemptManager questionAttemptPersistenceManager; - private final GitContentManager contentManager; - /** * Create a default Question manager object. * @@ -105,13 +97,9 @@ public class QuestionManager { * @param questionPersistenceManager - for question attempt persistence. */ @Inject - public QuestionManager( - final ContentMapper mapper, final IQuestionAttemptManager questionPersistenceManager, - final GitContentManager contentManager - ) { + public QuestionManager(final ContentMapper mapper, final IQuestionAttemptManager questionPersistenceManager) { this.mapper = mapper; this.questionAttemptPersistenceManager = questionPersistenceManager; - this.contentManager = contentManager; } /** diff --git a/src/main/resources/db_scripts/migrations/2021-02-create_quiz_tables.sql b/src/main/resources/db_scripts/migrations/2021-02-create_quiz_tables.sql index 484f5d297b..e87a0fb8ec 100644 --- a/src/main/resources/db_scripts/migrations/2021-02-create_quiz_tables.sql +++ b/src/main/resources/db_scripts/migrations/2021-02-create_quiz_tables.sql @@ -77,8 +77,7 @@ CREATE TABLE public.quiz_question_attempts ( question_id text NOT NULL, question_attempt jsonb, correct boolean, - "timestamp" timestamp without time zone, - marks integer + "timestamp" timestamp without time zone ); CREATE SEQUENCE public.quiz_question_attempts_id_seq diff --git a/src/test/java/uk/ac/cam/cl/dtg/isaac/api/AbstractIsaacIntegrationTest.java b/src/test/java/uk/ac/cam/cl/dtg/isaac/api/AbstractIsaacIntegrationTest.java index 5f43e6ae9b..96652e6da3 100644 --- a/src/test/java/uk/ac/cam/cl/dtg/isaac/api/AbstractIsaacIntegrationTest.java +++ b/src/test/java/uk/ac/cam/cl/dtg/isaac/api/AbstractIsaacIntegrationTest.java @@ -243,13 +243,8 @@ public static void setUpClass() throws Exception { passwordDataManager = new PgPasswordDataManager(postgresSqlDb); ContentMapper contentMapper = new ContentMapper(new Reflections("uk.ac.cam.cl.dtg")); - - Git git = createNiceMock(Git.class); - GitDb gitDb = new GitDb(git); - contentManager = new GitContentManager(gitDb, elasticSearchProvider, contentMapper, properties); - PgQuestionAttempts pgQuestionAttempts = new PgQuestionAttempts(postgresSqlDb, contentMapper); - questionManager = new QuestionManager(contentMapper, pgQuestionAttempts, contentManager); + questionManager = new QuestionManager(contentMapper, pgQuestionAttempts); mapperFacade = contentMapper.getAutoMapper(); @@ -269,6 +264,9 @@ public static void setUpClass() throws Exception { EmailCommunicator communicator = new EmailCommunicator("localhost", "default@localhost", "Howdy!"); userPreferenceManager = new PgUserPreferenceManager(postgresSqlDb); + Git git = createNiceMock(Git.class); + GitDb gitDb = new GitDb(git); + contentManager = new GitContentManager(gitDb, elasticSearchProvider, contentMapper, properties); logManager = createNiceMock(ILogManager.class); IDeletionTokenPersistenceManager deletionTokenPersistenceManager = new PgDeletionTokenPersistenceManager(postgresSqlDb); From ecd6cf4a82fffed328087b9e7dec3a611175999c Mon Sep 17 00:00:00 2001 From: Sol Dubock <94075844+sjd210@users.noreply.github.com> Date: Mon, 27 Oct 2025 10:18:41 +0000 Subject: [PATCH 27/38] Replace placeholder database access code --- .../dao/PgQuizQuestionAttemptPersistenceManager.java | 10 +--------- .../ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java | 10 +--------- 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/dao/PgQuizQuestionAttemptPersistenceManager.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/dao/PgQuizQuestionAttemptPersistenceManager.java index 864fe006c8..52fda1259e 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/dao/PgQuizQuestionAttemptPersistenceManager.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/dao/PgQuizQuestionAttemptPersistenceManager.java @@ -77,15 +77,7 @@ public void registerQuestionAttempt(Long quizAttemptId, QuestionValidationRespon pst.setTimestamp(5, new java.sql.Timestamp(questionResponse.getDateAttempted().getTime())); if (questionResponse.getMarks() != null) { - if (questionResponse instanceof LLMFreeTextQuestionValidationResponse) { - pst.setInt(6, ((LLMFreeTextQuestionValidationResponse) questionResponse).getMarksAwarded()); - } else { - if (questionResponse.isCorrect()) { - pst.setInt(6, 1); - } else { - pst.setInt(6, 0); - } - } + pst.setInt(6, questionResponse.getMarks()); } else { pst.setInt(6, java.sql.Types.NULL); } diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java index ebf26c36f4..84526c56a5 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java @@ -206,15 +206,7 @@ public void registerQuestionAttempt(final Long userId, final String questionPage pst.setTimestamp(6, new java.sql.Timestamp(questionAttempt.getDateAttempted().getTime())); if (questionAttempt.getMarks() != null) { - if (questionAttempt instanceof LLMFreeTextQuestionValidationResponse) { - pst.setInt(7, ((LLMFreeTextQuestionValidationResponse) questionAttempt).getMarksAwarded()); - } else { - if (questionAttempt.isCorrect()) { - pst.setInt(7, 1); - } else { - pst.setInt(7, 0); - } - } + pst.setInt(7, questionAttempt.getMarks()); } else { pst.setInt(7, java.sql.Types.NULL); } From 00db082eccf717a8bd2b1ff5d145e37822956053 Mon Sep 17 00:00:00 2001 From: Sol Dubock <94075844+sjd210@users.noreply.github.com> Date: Mon, 27 Oct 2025 10:21:28 +0000 Subject: [PATCH 28/38] Remove unused imports --- .../dtg/isaac/dao/PgQuizQuestionAttemptPersistenceManager.java | 1 - .../java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java | 1 - 2 files changed, 2 deletions(-) diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/dao/PgQuizQuestionAttemptPersistenceManager.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/dao/PgQuizQuestionAttemptPersistenceManager.java index 52fda1259e..1728197de8 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/dao/PgQuizQuestionAttemptPersistenceManager.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/dao/PgQuizQuestionAttemptPersistenceManager.java @@ -22,7 +22,6 @@ import com.google.inject.Inject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import uk.ac.cam.cl.dtg.isaac.dos.LLMFreeTextQuestionValidationResponse; import uk.ac.cam.cl.dtg.segue.dao.SegueDatabaseException; import uk.ac.cam.cl.dtg.segue.dao.content.ContentMapper; import uk.ac.cam.cl.dtg.segue.database.PostgresSqlDb; diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java index 84526c56a5..958107ae53 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java @@ -22,7 +22,6 @@ import com.google.inject.Inject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import uk.ac.cam.cl.dtg.isaac.dos.LLMFreeTextQuestionValidationResponse; import uk.ac.cam.cl.dtg.isaac.dos.LightweightQuestionValidationResponse; import uk.ac.cam.cl.dtg.isaac.dos.QuestionValidationResponse; import uk.ac.cam.cl.dtg.isaac.dos.users.Role; From 9f6d09a5bf4c363a85ab08152cf9274e7d9116df Mon Sep 17 00:00:00 2001 From: Sol Dubock <94075844+sjd210@users.noreply.github.com> Date: Wed, 5 Nov 2025 16:41:35 +0000 Subject: [PATCH 29/38] Reorder marks column to be after correct This will not happen for the database itself in the first instance since columns can only be added to the end, however whenever it is flushed and rebuilt the create script will enforce the desired ordering --- ...QuizQuestionAttemptPersistenceManager.java | 9 ++- ...LightweightQuestionValidationResponse.java | 62 +++++++++------ .../isaac/dos/QuestionValidationResponse.java | 12 +-- .../dto/FormulaValidationResponseDTO.java | 6 +- .../isaac/dto/ItemValidationResponseDTO.java | 5 +- .../dto/QuantityValidationResponseDTO.java | 6 +- .../dto/QuestionValidationResponseDTO.java | 75 +++++++++++++------ .../cl/dtg/isaac/quiz/PgQuestionAttempts.java | 16 ++-- .../db_scripts/create_anonymous_database.sql | 8 +- .../postgres-rutherford-create-script.sql | 8 +- .../api/managers/QuizQuestionManagerTest.java | 4 +- .../api/managers/UserAttemptManagerTest.java | 16 ++-- .../test-postgres-rutherford-data-dump.sql | 16 ++-- 13 files changed, 142 insertions(+), 101 deletions(-) diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/dao/PgQuizQuestionAttemptPersistenceManager.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/dao/PgQuizQuestionAttemptPersistenceManager.java index 1728197de8..f82b829bcc 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/dao/PgQuizQuestionAttemptPersistenceManager.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/dao/PgQuizQuestionAttemptPersistenceManager.java @@ -59,7 +59,7 @@ public PgQuizQuestionAttemptPersistenceManager(final PostgresSqlDb database, fin @Override public void registerQuestionAttempt(Long quizAttemptId, QuestionValidationResponse questionResponse) throws SegueDatabaseException { - String query = "INSERT INTO quiz_question_attempts(quiz_attempt_id, question_id, question_attempt, correct, \"timestamp\", marks)" + + String query = "INSERT INTO quiz_question_attempts(quiz_attempt_id, question_id, question_attempt, correct, marks, \"timestamp\")" + " VALUES (?, ?, ?::text::jsonb, ?, ?, ?);"; try (Connection conn = database.getDatabaseConnection(); PreparedStatement pst = conn.prepareStatement(query, Statement.RETURN_GENERATED_KEYS); @@ -73,14 +73,15 @@ public void registerQuestionAttempt(Long quizAttemptId, QuestionValidationRespon } else { pst.setNull(4, Types.BOOLEAN); } - pst.setTimestamp(5, new java.sql.Timestamp(questionResponse.getDateAttempted().getTime())); if (questionResponse.getMarks() != null) { - pst.setInt(6, questionResponse.getMarks()); + pst.setInt(5, questionResponse.getMarks()); } else { - pst.setInt(6, java.sql.Types.NULL); + pst.setInt(5, java.sql.Types.NULL); } + pst.setTimestamp(6, new java.sql.Timestamp(questionResponse.getDateAttempted().getTime())); + if (pst.executeUpdate() == 0) { throw new SegueDatabaseException("Unable to save quiz question attempt."); } diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/LightweightQuestionValidationResponse.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/LightweightQuestionValidationResponse.java index 223b93df85..4c108b9afe 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/LightweightQuestionValidationResponse.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/LightweightQuestionValidationResponse.java @@ -26,17 +26,35 @@ public LightweightQuestionValidationResponse() { * - * @param correct * - + * @param marks + * - * @param dateAttempted * - - * @param marks + */ + public LightweightQuestionValidationResponse(final String questionId, final Boolean correct, + final Integer marks, final Date dateAttempted) { + this.questionId = questionId; + this.correct = correct; + this.marks = marks; + this.dateAttempted = dateAttempted; + } + + /** + * Constructor without specifying marks (instead derived from 'correct') + * + * @param questionId + * - + * @param correct + * - + * @param dateAttempted * - */ public LightweightQuestionValidationResponse(final String questionId, final Boolean correct, - final Date dateAttempted, final Integer marks) { + final Date dateAttempted) { this.questionId = questionId; this.correct = correct; + this.marks = (correct != null && correct) ? 1 : 0; this.dateAttempted = dateAttempted; - this.marks = marks; } /** @@ -78,46 +96,46 @@ public final void setCorrect(final Boolean correct) { } /** - * Gets the dateAttempted. + * Gets the marks. * - * @return the dateAttempted + * @return the marks */ - public Date getDateAttempted() { - return dateAttempted; + public Integer getMarks() { + return marks; } /** - * Sets the dateAttempted. + * Sets the marks. * - * @param dateAttempted - * the dateAttempted to set + * @param marks + * the marks to set */ - public void setDateAttempted(final Date dateAttempted) { - this.dateAttempted = dateAttempted; + public void setMarks(final Integer marks) { + this.marks = marks; } /** - * Gets the marks. + * Gets the dateAttempted. * - * @return the marks + * @return the dateAttempted */ - public Integer getMarks() { - return marks; + public Date getDateAttempted() { + return dateAttempted; } /** - * Sets the marks. + * Sets the dateAttempted. * - * @param marks - * the marks to set + * @param dateAttempted + * the dateAttempted to set */ - public void setMarks(final Integer marks) { - this.marks = marks; + public void setDateAttempted(final Date dateAttempted) { + this.dateAttempted = dateAttempted; } @Override public String toString() { return "QuestionValidationResponse [questionId=" + questionId + ", correct=" + correct + - ", dateAttempted=" + dateAttempted + ", marks=" + marks + "]"; + ", marks=" + marks + ", dateAttempted=" + dateAttempted + "]"; } } diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/QuestionValidationResponse.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/QuestionValidationResponse.java index 8325a6ea2b..1487e9f29b 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/QuestionValidationResponse.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/QuestionValidationResponse.java @@ -47,16 +47,16 @@ public QuestionValidationResponse() { * - * @param correct * - + * @param marks + * - * @param explanation * - * @param dateAttempted * - - * @param marks - * - */ public QuestionValidationResponse(final String questionId, final Choice answer, final Boolean correct, - final Content explanation, final Date dateAttempted, final Integer marks) { - super(questionId, correct, dateAttempted, marks); + final Integer marks, final Content explanation, final Date dateAttempted) { + super(questionId, correct, marks, dateAttempted); this.answer = answer; this.explanation = explanation; } @@ -77,7 +77,7 @@ public QuestionValidationResponse(final String questionId, final Choice answer, */ public QuestionValidationResponse(final String questionId, final Choice answer, final Boolean correct, final Content explanation, final Date dateAttempted) { - super(questionId, correct, dateAttempted, (correct != null && correct) ? 1 : 0); + super(questionId, correct, dateAttempted); this.answer = answer; this.explanation = explanation; } @@ -124,7 +124,7 @@ public final void setExplanation(final Content explanation) { public String toString() { return "QuestionValidationResponse [questionId=" + super.getQuestionId() + ", answer=" + answer + ", correct=" + super.isCorrect() + ", explanation=" + explanation + - ", dateAttempted=" + super.getDateAttempted() + ", marks=" + super.getMarks() + "]"; + ", marks=" + super.getMarks() + ", dateAttempted=" + super.getDateAttempted() + "]"; } } diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/FormulaValidationResponseDTO.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/FormulaValidationResponseDTO.java index c685e95385..5bd5b4c2ec 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/FormulaValidationResponseDTO.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/FormulaValidationResponseDTO.java @@ -52,14 +52,12 @@ public FormulaValidationResponseDTO() { * - * @param dateAttempted * - - * @param marks - * - */ public FormulaValidationResponseDTO(final String questionId, final ChoiceDTO answer, final ContentDTO explanation, final Boolean correctExact, final Boolean correctSymbolic, final Boolean correctNumeric, - final Date dateAttempted, final Integer marks) { - super(questionId, answer, correctSymbolic || correctNumeric, explanation, dateAttempted, marks); + final Date dateAttempted) { + super(questionId, answer, correctSymbolic || correctNumeric, explanation, dateAttempted); this.correctExact = correctExact; this.correctSymbolic = correctSymbolic; this.correctNumeric = correctNumeric; diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/ItemValidationResponseDTO.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/ItemValidationResponseDTO.java index d39f35a8ab..f4bfb3bab9 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/ItemValidationResponseDTO.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/ItemValidationResponseDTO.java @@ -46,12 +46,11 @@ public ItemValidationResponseDTO() { * @param itemsCorrect - ordered list of correctness status of each submitted item. * @param explanation - explanation. * @param dateAttempted - dateAttempted. - * @param marks - marks */ public ItemValidationResponseDTO(final String questionId, final ChoiceDTO answer, final Boolean correct, final List itemsCorrect, - final ContentDTO explanation, final Date dateAttempted, final Integer marks) { - super(questionId, answer, correct, explanation, dateAttempted, marks); + final ContentDTO explanation, final Date dateAttempted) { + super(questionId, answer, correct, explanation, dateAttempted); this.itemsCorrect = itemsCorrect; } diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/QuantityValidationResponseDTO.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/QuantityValidationResponseDTO.java index 8e49389a8f..0b9b4af85e 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/QuantityValidationResponseDTO.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/QuantityValidationResponseDTO.java @@ -53,13 +53,11 @@ public QuantityValidationResponseDTO() { * - * @param dateAttempted * - - * @param marks - * - */ public QuantityValidationResponseDTO(final String questionId, final ChoiceDTO answer, final Boolean correct, final ContentDTO explanation, final Boolean correctValue, - final Boolean correctUnits, final Date dateAttempted, final Integer marks) { - super(questionId, answer, correct, explanation, dateAttempted, marks); + final Boolean correctUnits, final Date dateAttempted) { + super(questionId, answer, correct, explanation, dateAttempted); this.correctValue = correctValue; this.correctUnits = correctUnits; } diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/QuestionValidationResponseDTO.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/QuestionValidationResponseDTO.java index 3c92f8cb4a..75d368b55c 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/QuestionValidationResponseDTO.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/QuestionValidationResponseDTO.java @@ -17,6 +17,8 @@ import java.util.Date; +import uk.ac.cam.cl.dtg.isaac.dos.content.Choice; +import uk.ac.cam.cl.dtg.isaac.dos.content.Content; import uk.ac.cam.cl.dtg.isaac.dto.content.ChoiceDTO; import uk.ac.cam.cl.dtg.isaac.dto.content.ContentDTO; @@ -28,9 +30,9 @@ public class QuestionValidationResponseDTO { private String questionId; private ChoiceDTO answer; private Boolean correct; + private Integer marks; private ContentDTO explanation; private Date dateAttempted; - private Integer marks; /** * Default Constructor for mappers. @@ -48,23 +50,48 @@ public QuestionValidationResponseDTO() { * - * @param correct * - + * @param marks + * - * @param explanation * - * @param dateAttempted * - - * @param marks + */ + public QuestionValidationResponseDTO(final String questionId, final ChoiceDTO answer, final Boolean correct, + final Integer marks, final ContentDTO explanation, final Date dateAttempted) { + this.questionId = questionId; + this.answer = answer; + this.correct = correct; + this.marks = marks; + this.explanation = explanation; + this.dateAttempted = dateAttempted; + } + + /** + * Constructor without specifying marks (instead derived from 'correct') + * + * @param questionId + * - + * @param answer + * - + * @param correct + * - + * @param explanation + * - + * @param dateAttempted * - */ public QuestionValidationResponseDTO(final String questionId, final ChoiceDTO answer, final Boolean correct, - final ContentDTO explanation, final Date dateAttempted, final Integer marks) { + final ContentDTO explanation, final Date dateAttempted) { this.questionId = questionId; this.answer = answer; this.correct = correct; + this.marks = (correct != null && correct) ? 1 : 0; this.explanation = explanation; this.dateAttempted = dateAttempted; - this.marks = marks; } + /** * Gets the questionId. * @@ -122,6 +149,25 @@ public final void setCorrect(final Boolean correct) { this.correct = correct; } + /** + * Gets the marks. + * + * @return the marks + */ + public Integer getMarks() { + return marks; + } + + /** + * Sets the marks. + * + * @param marks + * the marks to set + */ + public void setMarks(final Integer marks) { + this.marks = marks; + } + /** * Gets the explanation. * @@ -160,28 +206,9 @@ public void setDateAttempted(final Date dateAttempted) { this.dateAttempted = dateAttempted; } - /** - * Gets the marks. - * - * @return the marks - */ - public Integer getMarks() { - return marks; - } - - /** - * Sets the marks. - * - * @param marks - * the marks to set - */ - public void setMarks(final Integer marks) { - this.marks = marks; - } - @Override public String toString() { return "QuestionValidationResponseDTO [questionId=" + questionId + ", answer=" + answer + ", correct=" - + correct + ", explanation=" + explanation + ", dateAttempted=" + dateAttempted + ", marks=" + marks + "]"; + + correct + ", marks=" + marks + ", explanation=" + explanation + ", dateAttempted=" + dateAttempted + "]"; } } diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java index 958107ae53..50e09dab5c 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java @@ -186,7 +186,7 @@ public Map>> getAnonymousQu public void registerQuestionAttempt(final Long userId, final String questionPageId, final String fullQuestionId, final QuestionValidationResponse questionAttempt) throws SegueDatabaseException { - String query = "INSERT INTO question_attempts(user_id, page_id, question_id, question_attempt, correct, \"timestamp\", marks)" + String query = "INSERT INTO question_attempts(user_id, page_id, question_id, question_attempt, correct, marks, \"timestamp\")" + " VALUES (?, ?, ?, ?::text::jsonb, ?, ?, ?);"; try (Connection conn = database.getDatabaseConnection(); PreparedStatement pst = conn.prepareStatement(query, Statement.RETURN_GENERATED_KEYS); @@ -202,14 +202,14 @@ public void registerQuestionAttempt(final Long userId, final String questionPage pst.setNull(5, java.sql.Types.NULL); } - pst.setTimestamp(6, new java.sql.Timestamp(questionAttempt.getDateAttempted().getTime())); - if (questionAttempt.getMarks() != null) { - pst.setInt(7, questionAttempt.getMarks()); + pst.setInt(6, questionAttempt.getMarks()); } else { - pst.setInt(7, java.sql.Types.NULL); + pst.setInt(6, java.sql.Types.NULL); } + pst.setTimestamp(7, new java.sql.Timestamp(questionAttempt.getDateAttempted().getTime())); + if (pst.executeUpdate() == 0) { throw new SegueDatabaseException("Unable to save question attempt."); } @@ -267,7 +267,7 @@ public Map>>> mapToReturn @@ -314,7 +314,7 @@ public Map>> get = userIds.stream().collect(Collectors.toMap(Function.identity(), k -> Maps.newHashMap()));; try (Connection conn = database.getDatabaseConnection()) { - String query = "SELECT id, user_id, question_id, correct, timestamp, marks FROM question_attempts" + String query = "SELECT id, user_id, question_id, correct, marks, timestamp FROM question_attempts" + " WHERE user_id = ANY(?) AND page_id = ANY(?)" + " ORDER BY \"timestamp\" ASC"; @@ -445,7 +445,7 @@ public Map getQuestionAttemptCountForUserByDateRange(final Date from private LightweightQuestionValidationResponse resultsToLightweightValidationResponse(final ResultSet results) throws SQLException { LightweightQuestionValidationResponse partialQuestionAttempt = new QuestionValidationResponse(); - partialQuestionAttempt.setCorrect(results.getInt("marks") > 0); + partialQuestionAttempt.setCorrect(results.getBoolean("correct")); partialQuestionAttempt.setQuestionId(results.getString("question_id")); partialQuestionAttempt.setDateAttempted(results.getTimestamp("timestamp")); partialQuestionAttempt.setMarks(results.getInt("marks")); diff --git a/src/main/resources/db_scripts/create_anonymous_database.sql b/src/main/resources/db_scripts/create_anonymous_database.sql index eaa4c0d7af..d8d84bf3ee 100644 --- a/src/main/resources/db_scripts/create_anonymous_database.sql +++ b/src/main/resources/db_scripts/create_anonymous_database.sql @@ -151,8 +151,8 @@ CREATE TABLE anonymous.question_attempts AS question_id, question_attempt, correct, - timestamp, - marks + marks, + timestamp FROM public.question_attempts; CREATE TABLE anonymous.user_streak_freezes AS @@ -201,8 +201,8 @@ CREATE TABLE anonymous.quiz_question_attempts AS question_id, question_attempt, correct, - timestamp, - marks + marks, + timestamp FROM public.quiz_question_attempts; -- Logged events: diff --git a/src/main/resources/db_scripts/postgres-rutherford-create-script.sql b/src/main/resources/db_scripts/postgres-rutherford-create-script.sql index 3f976ce9ad..046854ed5d 100644 --- a/src/main/resources/db_scripts/postgres-rutherford-create-script.sql +++ b/src/main/resources/db_scripts/postgres-rutherford-create-script.sql @@ -346,8 +346,8 @@ CREATE TABLE public.question_attempts ( question_id text NOT NULL, question_attempt jsonb, correct boolean, - "timestamp" timestamp without time zone, - marks integer + marks integer, + "timestamp" timestamp without time zone ); @@ -461,8 +461,8 @@ CREATE TABLE public.quiz_question_attempts ( question_id text NOT NULL, question_attempt jsonb, correct boolean, - "timestamp" timestamp without time zone, - marks integer + marks integer, + "timestamp" timestamp without time zone ); diff --git a/src/test/java/uk/ac/cam/cl/dtg/isaac/api/managers/QuizQuestionManagerTest.java b/src/test/java/uk/ac/cam/cl/dtg/isaac/api/managers/QuizQuestionManagerTest.java index 6409a1e424..c524a7c421 100644 --- a/src/test/java/uk/ac/cam/cl/dtg/isaac/api/managers/QuizQuestionManagerTest.java +++ b/src/test/java/uk/ac/cam/cl/dtg/isaac/api/managers/QuizQuestionManagerTest.java @@ -111,8 +111,8 @@ public void initializeAdditionalObjects() { correctResponse = new QuestionValidationResponse(question.getId(), correctAnswer, true, correctExplanation, somePastDate); wrongResponse = new QuestionValidationResponse(question.getId(), wrongAnswer, false, wrongExplanation, somePastDate); - correctResponseDTO = new QuestionValidationResponseDTO(question.getId(), correctAnswerDTO, true, correctExplanationDTO, somePastDate, 1); - wrongResponseDTO = new QuestionValidationResponseDTO(question.getId(), wrongAnswerDTO, false, wrongExplanationDTO, somePastDate, 0); + correctResponseDTO = new QuestionValidationResponseDTO(question.getId(), correctAnswerDTO, true, correctExplanationDTO, somePastDate); + wrongResponseDTO = new QuestionValidationResponseDTO(question.getId(), wrongAnswerDTO, false, wrongExplanationDTO, somePastDate); quantityResponse = new QuantityValidationResponse(question.getId(), correctAnswer, true, correctExplanation, true, true, somePastDate); diff --git a/src/test/java/uk/ac/cam/cl/dtg/isaac/api/managers/UserAttemptManagerTest.java b/src/test/java/uk/ac/cam/cl/dtg/isaac/api/managers/UserAttemptManagerTest.java index 8ec693a108..bcaf2a1e81 100644 --- a/src/test/java/uk/ac/cam/cl/dtg/isaac/api/managers/UserAttemptManagerTest.java +++ b/src/test/java/uk/ac/cam/cl/dtg/isaac/api/managers/UserAttemptManagerTest.java @@ -29,14 +29,14 @@ public class UserAttemptManagerTest { private ContentSummaryDTO fakeQuestionSummary; private ContentSummaryDTO fakeConceptSummary; - private final LightweightQuestionValidationResponse p1CorrectAttempt = new LightweightQuestionValidationResponse(QUESTION_PART_1_ID, true, null, 1); - private final LightweightQuestionValidationResponse p1IncorrectAttempt = new LightweightQuestionValidationResponse(QUESTION_PART_1_ID, false, null, 0); - private final LightweightQuestionValidationResponse p2CorrectAttempt = new LightweightQuestionValidationResponse(QUESTION_PART_2_ID, true, null, 1); - private final LightweightQuestionValidationResponse p2IncorrectAttempt = new LightweightQuestionValidationResponse(QUESTION_PART_2_ID, false, null, 0); - private final LightweightQuestionValidationResponse p3CorrectAttempt = new LightweightQuestionValidationResponse(QUESTION_PART_3_ID, true, null, 1); - private final LightweightQuestionValidationResponse p3IncorrectAttempt = new LightweightQuestionValidationResponse(QUESTION_PART_3_ID, false, null, 0); - - private final LightweightQuestionValidationResponse someOtherCorrectAttempt = new LightweightQuestionValidationResponse("some-other-part-id", true, null, 1); + private final LightweightQuestionValidationResponse p1CorrectAttempt = new LightweightQuestionValidationResponse(QUESTION_PART_1_ID, true, null); + private final LightweightQuestionValidationResponse p1IncorrectAttempt = new LightweightQuestionValidationResponse(QUESTION_PART_1_ID, false, null); + private final LightweightQuestionValidationResponse p2CorrectAttempt = new LightweightQuestionValidationResponse(QUESTION_PART_2_ID, true, null); + private final LightweightQuestionValidationResponse p2IncorrectAttempt = new LightweightQuestionValidationResponse(QUESTION_PART_2_ID, false, null); + private final LightweightQuestionValidationResponse p3CorrectAttempt = new LightweightQuestionValidationResponse(QUESTION_PART_3_ID, true, null); + private final LightweightQuestionValidationResponse p3IncorrectAttempt = new LightweightQuestionValidationResponse(QUESTION_PART_3_ID, false, null); + + private final LightweightQuestionValidationResponse someOtherCorrectAttempt = new LightweightQuestionValidationResponse("some-other-part-id", true, null); @Before public void setUp() throws Exception { diff --git a/src/test/resources/test-postgres-rutherford-data-dump.sql b/src/test/resources/test-postgres-rutherford-data-dump.sql index b5dab6ea91..4bab640494 100644 --- a/src/test/resources/test-postgres-rutherford-data-dump.sql +++ b/src/test/resources/test-postgres-rutherford-data-dump.sql @@ -160,14 +160,14 @@ COPY public.logged_events (id, user_id, anonymous_user, event_type, event_detail -- Data for Name: question_attempts; Type: TABLE DATA; Schema: public; Owner: rutherford -- -COPY public.question_attempts (id, user_id, page_id, question_id, question_attempt, correct, "timestamp", marks) FROM stdin; +COPY public.question_attempts (id, user_id, page_id, question_id, question_attempt, correct, marks, "timestamp") FROM stdin; 2 7 _regression_test_ _regression_test_|acc_multi_q|_regression_test_multi_ {"answer": {"type": "choice", "value": "$42$", "correct": false, "children": [], "encoding": "markdown"}, "correct": true, "questionId": "_regression_test_|acc_multi_q|_regression_test_multi_", "explanation": {"tags": [], "type": "content", "children": [{"tags": [], "type": "content", "value": "This is a correct choice.", "children": [], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}, "dateAttempted": 1713449470730} t 2024-04-18 15:11:10.73 1 -3 7 _regression_test_ _regression_test_|acc_numeric_q|_regresssion_test_numeric_ {"answer": {"type": "quantity", "units": "m\\\\,s^{-1}", "value": "2.01", "correct": false, "children": []}, "correct": true, "questionId": "_regression_test_|acc_numeric_q|_regresssion_test_numeric_", "explanation": {"tags": [], "type": "content", "children": [{"tags": [], "type": "content", "value": "This is a correct choice.", "children": [], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}, "correctUnits": true, "correctValue": true, "dateAttempted": 1713449479419} t 2024-04-18 15:11:19.419 1 -4 7 _regression_test_ _regression_test_|acc_symbolic_q|_regression_test_symbolic_ {"answer": {"type": "formula", "value": "{\\"result\\":{\\"tex\\":\\"x\\",\\"mhchem\\":\\"\\",\\"python\\":\\"x\\",\\"mathml\\":\\"x\\",\\"uniqueSymbols\\":\\"x\\"},\\"symbols\\":[{\\"type\\":\\"Symbol\\",\\"position\\":{\\"x\\":239.5,\\"y\\":322.3333333333333},\\"expression\\":{\\"latex\\":\\"x\\",\\"python\\":\\"x\\"},\\"properties\\":{\\"letter\\":\\"x\\",\\"modifier\\":\\"\\"}}],\\"textEntry\\":true,\\"userInput\\":\\"x\\"}", "correct": false, "children": [], "pythonExpression": "x", "requiresExactMatch": false}, "correct": true, "questionId": "_regression_test_|acc_symbolic_q|_regression_test_symbolic_", "explanation": {"tags": [], "type": "content", "children": [{"tags": [], "type": "content", "value": "This is a correct choice. It requires an exact match!", "children": [], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}, "dateAttempted": 1713449484110} t 2024-04-18 15:11:24.11 1 -5 7 _regression_test_ _regression_test_|acc_stringmatch_q|_regression_test_stringmatch_ {"answer": {"type": "stringChoice", "value": "hello", "correct": false, "children": [], "caseInsensitive": false}, "correct": true, "questionId": "_regression_test_|acc_stringmatch_q|_regression_test_stringmatch_", "explanation": {"tags": [], "type": "content", "children": [{"tags": [], "type": "content", "value": "This needs a lower case \\"h\\".", "children": [], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}, "dateAttempted": 1713449505160} t 2024-04-18 15:11:45.16 1 -6 7 _regression_test_ _regression_test_|acc_chemistry_q|_regression_test_chemistry_ {"answer": {"type": "chemicalFormula", "value": "{\\"result\\":{\\"tex\\":\\"\\\\\\\\text{H} + \\\\\\\\text{Cl}\\",\\"mhchem\\":\\"H + Cl\\",\\"python\\":\\"\\\\\\\\text{H}\\",\\"mathml\\":\\"H+Cl\\",\\"uniqueSymbols\\":\\"H, Cl\\"},\\"symbols\\":[{\\"type\\":\\"ChemicalElement\\",\\"position\\":{\\"x\\":392.575,\\"y\\":531},\\"expression\\":{\\"latex\\":\\"\\\\\\\\text{H} + \\\\\\\\text{Cl}\\",\\"python\\":\\"\\\\\\\\text{H}\\"},\\"children\\":{\\"right\\":{\\"type\\":\\"BinaryOperation\\",\\"children\\":{\\"right\\":{\\"type\\":\\"ChemicalElement\\",\\"properties\\":{\\"element\\":\\"Cl\\"}}},\\"properties\\":{\\"operation\\":\\"+\\"}}},\\"properties\\":{\\"element\\":\\"H\\"}}],\\"textEntry\\":false,\\"userInput\\":\\"\\"}", "correct": false, "children": [], "mhchemExpression": "H + Cl"}, "correct": true, "questionId": "_regression_test_|acc_chemistry_q|_regression_test_chemistry_", "explanation": {"tags": [], "type": "content", "children": [{"tags": [], "type": "content", "value": "This is a correct choice.", "children": [], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}, "dateAttempted": 1713449507230} t 2024-04-18 15:11:47.23 1 -7 7 _regression_test_ _regression_test_|acc_freetext_q|_regression_test_freetext_ {"answer": {"type": "stringChoice", "value": "it didn't", "correct": false, "children": [], "caseInsensitive": false}, "correct": false, "questionId": "_regression_test_|acc_freetext_q|_regression_test_freetext_", "explanation": {"tags": [], "type": "content", "children": [{"tags": [], "type": "content", "value": "Spoil sport!", "children": [], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}, "dateAttempted": 1713449514726} f 2024-04-18 15:11:54.726 0 -8 7 _regression_test_ _regression_test_|_regression_test_logic_ {"answer": {"type": "logicFormula", "value": "{\\"result\\":{\\"tex\\":\\"A \\\\\\\\land B\\",\\"mhchem\\":\\"\\",\\"python\\":\\"A & B\\",\\"mathml\\":\\"\\",\\"uniqueSymbols\\":\\"A, B\\"},\\"symbols\\":[{\\"type\\":\\"Symbol\\",\\"position\\":{\\"x\\":284.625,\\"y\\":482},\\"expression\\":{\\"latex\\":\\"A \\\\\\\\land B\\",\\"python\\":\\"A & B\\"},\\"children\\":{\\"right\\":{\\"type\\":\\"LogicBinaryOperation\\",\\"children\\":{\\"right\\":{\\"type\\":\\"Symbol\\",\\"properties\\":{\\"letter\\":\\"B\\",\\"modifier\\":\\"\\"}}},\\"properties\\":{\\"operation\\":\\"and\\"}}},\\"properties\\":{\\"letter\\":\\"A\\",\\"modifier\\":\\"\\"}}],\\"textEntry\\":false,\\"userInput\\":\\"\\"}", "correct": false, "children": [], "pythonExpression": "A & B", "requiresExactMatch": false}, "correct": true, "questionId": "_regression_test_|_regression_test_logic_", "explanation": {"tags": [], "type": "content", "children": [{"tags": [], "type": "content", "value": "This simplifies to $\\\\and{A}{B}$!", "children": [], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}, "dateAttempted": 1713449529969} t 2024-04-18 15:12:09.969 1 +3 7 _regression_test_ _regression_test_|acc_numeric_q|_regresssion_test_numeric_ {"answer": {"type": "quantity", "units": "m\\\\,s^{-1}", "value": "2.01", "correct": false, "children": []}, "correct": true, "questionId": "_regression_test_|acc_numeric_q|_regresssion_test_numeric_", "explanation": {"tags": [], "type": "content", "children": [{"tags": [], "type": "content", "value": "This is a correct choice.", "children": [], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}, "correctUnits": true, "correctValue": true, "dateAttempted": 1713449479419} t 1 2024-04-18 15:11:19.419 +4 7 _regression_test_ _regression_test_|acc_symbolic_q|_regression_test_symbolic_ {"answer": {"type": "formula", "value": "{\\"result\\":{\\"tex\\":\\"x\\",\\"mhchem\\":\\"\\",\\"python\\":\\"x\\",\\"mathml\\":\\"x\\",\\"uniqueSymbols\\":\\"x\\"},\\"symbols\\":[{\\"type\\":\\"Symbol\\",\\"position\\":{\\"x\\":239.5,\\"y\\":322.3333333333333},\\"expression\\":{\\"latex\\":\\"x\\",\\"python\\":\\"x\\"},\\"properties\\":{\\"letter\\":\\"x\\",\\"modifier\\":\\"\\"}}],\\"textEntry\\":true,\\"userInput\\":\\"x\\"}", "correct": false, "children": [], "pythonExpression": "x", "requiresExactMatch": false}, "correct": true, "questionId": "_regression_test_|acc_symbolic_q|_regression_test_symbolic_", "explanation": {"tags": [], "type": "content", "children": [{"tags": [], "type": "content", "value": "This is a correct choice. It requires an exact match!", "children": [], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}, "dateAttempted": 1713449484110} t 1 2024-04-18 15:11:24.11 +5 7 _regression_test_ _regression_test_|acc_stringmatch_q|_regression_test_stringmatch_ {"answer": {"type": "stringChoice", "value": "hello", "correct": false, "children": [], "caseInsensitive": false}, "correct": true, "questionId": "_regression_test_|acc_stringmatch_q|_regression_test_stringmatch_", "explanation": {"tags": [], "type": "content", "children": [{"tags": [], "type": "content", "value": "This needs a lower case \\"h\\".", "children": [], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}, "dateAttempted": 1713449505160} t 1 2024-04-18 15:11:45.16 +6 7 _regression_test_ _regression_test_|acc_chemistry_q|_regression_test_chemistry_ {"answer": {"type": "chemicalFormula", "value": "{\\"result\\":{\\"tex\\":\\"\\\\\\\\text{H} + \\\\\\\\text{Cl}\\",\\"mhchem\\":\\"H + Cl\\",\\"python\\":\\"\\\\\\\\text{H}\\",\\"mathml\\":\\"H+Cl\\",\\"uniqueSymbols\\":\\"H, Cl\\"},\\"symbols\\":[{\\"type\\":\\"ChemicalElement\\",\\"position\\":{\\"x\\":392.575,\\"y\\":531},\\"expression\\":{\\"latex\\":\\"\\\\\\\\text{H} + \\\\\\\\text{Cl}\\",\\"python\\":\\"\\\\\\\\text{H}\\"},\\"children\\":{\\"right\\":{\\"type\\":\\"BinaryOperation\\",\\"children\\":{\\"right\\":{\\"type\\":\\"ChemicalElement\\",\\"properties\\":{\\"element\\":\\"Cl\\"}}},\\"properties\\":{\\"operation\\":\\"+\\"}}},\\"properties\\":{\\"element\\":\\"H\\"}}],\\"textEntry\\":false,\\"userInput\\":\\"\\"}", "correct": false, "children": [], "mhchemExpression": "H + Cl"}, "correct": true, "questionId": "_regression_test_|acc_chemistry_q|_regression_test_chemistry_", "explanation": {"tags": [], "type": "content", "children": [{"tags": [], "type": "content", "value": "This is a correct choice.", "children": [], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}, "dateAttempted": 1713449507230} t 1 2024-04-18 15:11:47.23 +7 7 _regression_test_ _regression_test_|acc_freetext_q|_regression_test_freetext_ {"answer": {"type": "stringChoice", "value": "it didn't", "correct": false, "children": [], "caseInsensitive": false}, "correct": false, "questionId": "_regression_test_|acc_freetext_q|_regression_test_freetext_", "explanation": {"tags": [], "type": "content", "children": [{"tags": [], "type": "content", "value": "Spoil sport!", "children": [], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}, "dateAttempted": 1713449514726} f 0 2024-04-18 15:11:54.726 +8 7 _regression_test_ _regression_test_|_regression_test_logic_ {"answer": {"type": "logicFormula", "value": "{\\"result\\":{\\"tex\\":\\"A \\\\\\\\land B\\",\\"mhchem\\":\\"\\",\\"python\\":\\"A & B\\",\\"mathml\\":\\"\\",\\"uniqueSymbols\\":\\"A, B\\"},\\"symbols\\":[{\\"type\\":\\"Symbol\\",\\"position\\":{\\"x\\":284.625,\\"y\\":482},\\"expression\\":{\\"latex\\":\\"A \\\\\\\\land B\\",\\"python\\":\\"A & B\\"},\\"children\\":{\\"right\\":{\\"type\\":\\"LogicBinaryOperation\\",\\"children\\":{\\"right\\":{\\"type\\":\\"Symbol\\",\\"properties\\":{\\"letter\\":\\"B\\",\\"modifier\\":\\"\\"}}},\\"properties\\":{\\"operation\\":\\"and\\"}}},\\"properties\\":{\\"letter\\":\\"A\\",\\"modifier\\":\\"\\"}}],\\"textEntry\\":false,\\"userInput\\":\\"\\"}", "correct": false, "children": [], "pythonExpression": "A & B", "requiresExactMatch": false}, "correct": true, "questionId": "_regression_test_|_regression_test_logic_", "explanation": {"tags": [], "type": "content", "children": [{"tags": [], "type": "content", "value": "This simplifies to $\\\\and{A}{B}$!", "children": [], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}, "dateAttempted": 1713449529969} t 1 2024-04-18 15:12:09.969 \. @@ -191,7 +191,7 @@ COPY public.quiz_attempts (id, user_id, quiz_id, quiz_assignment_id, start_date, -- Data for Name: quiz_question_attempts; Type: TABLE DATA; Schema: public; Owner: rutherford -- -COPY public.quiz_question_attempts (id, quiz_attempt_id, question_id, question_attempt, correct, "timestamp", marks) FROM stdin; +COPY public.quiz_question_attempts (id, quiz_attempt_id, question_id, question_attempt, correct, marks, "timestamp") FROM stdin; \. From 7b16212e1b662a3562ef1ea231085d9fb89ed2e5 Mon Sep 17 00:00:00 2001 From: Sol Dubock <94075844+sjd210@users.noreply.github.com> Date: Wed, 5 Nov 2025 16:42:20 +0000 Subject: [PATCH 30/38] Fully replace marksAwarded field in LLM response --- .../isaac/dos/LLMFreeTextQuestionValidationResponse.java | 7 ------- .../dto/LLMFreeTextQuestionValidationResponseDTO.java | 7 ------- .../cam/cl/dtg/isaac/quiz/IsaacLLMFreeTextValidator.java | 2 +- .../cl/dtg/isaac/quiz/IsaacLLMFreeTextValidatorTest.java | 2 +- 4 files changed, 2 insertions(+), 16 deletions(-) diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/LLMFreeTextQuestionValidationResponse.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/LLMFreeTextQuestionValidationResponse.java index 87caa526b6..e47a100ae4 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/LLMFreeTextQuestionValidationResponse.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/LLMFreeTextQuestionValidationResponse.java @@ -22,13 +22,6 @@ public LLMFreeTextQuestionValidationResponse(final String questionId, final Choi super(questionId, answer, correct, explanation, dateAttempted); } - public Integer getMarksAwarded() { - return super.getMarks(); - } - public void setMarksAwarded(Integer marksAwarded) { - super.setMarks(marksAwarded); - } - public List getMarkBreakdown() { return markBreakdown; } diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/LLMFreeTextQuestionValidationResponseDTO.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/LLMFreeTextQuestionValidationResponseDTO.java index f23652cd7f..8dbab4a999 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/LLMFreeTextQuestionValidationResponseDTO.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/LLMFreeTextQuestionValidationResponseDTO.java @@ -10,13 +10,6 @@ public class LLMFreeTextQuestionValidationResponseDTO extends QuestionValidation public LLMFreeTextQuestionValidationResponseDTO() { } - public Integer getMarksAwarded() { - return super.getMarks(); - } - public void setMarksAwarded(Integer marksAwarded) { - super.setMarks(marksAwarded); - } - public List getMarkBreakdown() { return markBreakdown; } diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/IsaacLLMFreeTextValidator.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/IsaacLLMFreeTextValidator.java index 7384c12805..712ca0f2fb 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/IsaacLLMFreeTextValidator.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/IsaacLLMFreeTextValidator.java @@ -295,7 +295,7 @@ private LLMFreeTextQuestionValidationResponse generateQuestionValidationResponse LLMFreeTextQuestionValidationResponse validationResponse = new LLMFreeTextQuestionValidationResponse( question.getId(), answer, isConsideredCorrect, null, new Date()); - validationResponse.setMarksAwarded(markTotal); + validationResponse.setMarks(markTotal); validationResponse.setMarkBreakdown(markBreakdown); return validationResponse; } diff --git a/src/test/java/uk/ac/cam/cl/dtg/isaac/quiz/IsaacLLMFreeTextValidatorTest.java b/src/test/java/uk/ac/cam/cl/dtg/isaac/quiz/IsaacLLMFreeTextValidatorTest.java index 2ebac5124b..e936048208 100644 --- a/src/test/java/uk/ac/cam/cl/dtg/isaac/quiz/IsaacLLMFreeTextValidatorTest.java +++ b/src/test/java/uk/ac/cam/cl/dtg/isaac/quiz/IsaacLLMFreeTextValidatorTest.java @@ -257,7 +257,7 @@ public static void expectMark(LLMFreeTextQuestionValidationResponse response, int marksAwarded, List expectedMarks) { assertEquals(isCorrect, response.isCorrect()); - assertEquals(marksAwarded, (long) response.getMarksAwarded()); + assertEquals(marksAwarded, (long) response.getMarks()); assertTrue(expectedMarks.containsAll(response.getMarkBreakdown())); assertTrue(response.getMarkBreakdown().containsAll(expectedMarks)); } From 08263ed395c72852ecd96d67423a397a92c0bc2f Mon Sep 17 00:00:00 2001 From: Sol Dubock <94075844+sjd210@users.noreply.github.com> Date: Wed, 5 Nov 2025 17:11:49 +0000 Subject: [PATCH 31/38] Reset correctness criteria on LLM-marked questions We will be moving away from using the field anyway, so it's probably best to keep it consistent now and update the calculation more universally --- .../uk/ac/cam/cl/dtg/isaac/quiz/IsaacLLMFreeTextValidator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/IsaacLLMFreeTextValidator.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/IsaacLLMFreeTextValidator.java index 712ca0f2fb..0ac5c11477 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/IsaacLLMFreeTextValidator.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/IsaacLLMFreeTextValidator.java @@ -282,7 +282,7 @@ private int evaluateMarkTotal(final IsaacLLMFreeTextQuestion question, final Map private LLMFreeTextQuestionValidationResponse generateQuestionValidationResponse( final IsaacLLMFreeTextQuestion question, final Choice answer, final Map awardedMarks, final int markTotal) { - boolean isConsideredCorrect = markTotal >= question.getMaxMarks(); + boolean isConsideredCorrect = markTotal > 0; // We create a fresh copy of the mark scheme with the full description and the awarded mark values. List markBreakdown = question.getMarkScheme().stream().map(mark -> { From bedca790aa8b6c4b17972f7b25701767dd228ac4 Mon Sep 17 00:00:00 2001 From: Sol Dubock <94075844+sjd210@users.noreply.github.com> Date: Wed, 5 Nov 2025 17:15:14 +0000 Subject: [PATCH 32/38] Correct SQL table representation --- src/test/resources/test-postgres-rutherford-data-dump.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/resources/test-postgres-rutherford-data-dump.sql b/src/test/resources/test-postgres-rutherford-data-dump.sql index 4bab640494..e6b6ccba95 100644 --- a/src/test/resources/test-postgres-rutherford-data-dump.sql +++ b/src/test/resources/test-postgres-rutherford-data-dump.sql @@ -161,7 +161,7 @@ COPY public.logged_events (id, user_id, anonymous_user, event_type, event_detail -- COPY public.question_attempts (id, user_id, page_id, question_id, question_attempt, correct, marks, "timestamp") FROM stdin; -2 7 _regression_test_ _regression_test_|acc_multi_q|_regression_test_multi_ {"answer": {"type": "choice", "value": "$42$", "correct": false, "children": [], "encoding": "markdown"}, "correct": true, "questionId": "_regression_test_|acc_multi_q|_regression_test_multi_", "explanation": {"tags": [], "type": "content", "children": [{"tags": [], "type": "content", "value": "This is a correct choice.", "children": [], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}, "dateAttempted": 1713449470730} t 2024-04-18 15:11:10.73 1 +2 7 _regression_test_ _regression_test_|acc_multi_q|_regression_test_multi_ {"answer": {"type": "choice", "value": "$42$", "correct": false, "children": [], "encoding": "markdown"}, "correct": true, "questionId": "_regression_test_|acc_multi_q|_regression_test_multi_", "explanation": {"tags": [], "type": "content", "children": [{"tags": [], "type": "content", "value": "This is a correct choice.", "children": [], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}, "dateAttempted": 1713449470730} t 1 2024-04-18 15:11:10.73 3 7 _regression_test_ _regression_test_|acc_numeric_q|_regresssion_test_numeric_ {"answer": {"type": "quantity", "units": "m\\\\,s^{-1}", "value": "2.01", "correct": false, "children": []}, "correct": true, "questionId": "_regression_test_|acc_numeric_q|_regresssion_test_numeric_", "explanation": {"tags": [], "type": "content", "children": [{"tags": [], "type": "content", "value": "This is a correct choice.", "children": [], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}, "correctUnits": true, "correctValue": true, "dateAttempted": 1713449479419} t 1 2024-04-18 15:11:19.419 4 7 _regression_test_ _regression_test_|acc_symbolic_q|_regression_test_symbolic_ {"answer": {"type": "formula", "value": "{\\"result\\":{\\"tex\\":\\"x\\",\\"mhchem\\":\\"\\",\\"python\\":\\"x\\",\\"mathml\\":\\"x\\",\\"uniqueSymbols\\":\\"x\\"},\\"symbols\\":[{\\"type\\":\\"Symbol\\",\\"position\\":{\\"x\\":239.5,\\"y\\":322.3333333333333},\\"expression\\":{\\"latex\\":\\"x\\",\\"python\\":\\"x\\"},\\"properties\\":{\\"letter\\":\\"x\\",\\"modifier\\":\\"\\"}}],\\"textEntry\\":true,\\"userInput\\":\\"x\\"}", "correct": false, "children": [], "pythonExpression": "x", "requiresExactMatch": false}, "correct": true, "questionId": "_regression_test_|acc_symbolic_q|_regression_test_symbolic_", "explanation": {"tags": [], "type": "content", "children": [{"tags": [], "type": "content", "value": "This is a correct choice. It requires an exact match!", "children": [], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}, "dateAttempted": 1713449484110} t 1 2024-04-18 15:11:24.11 5 7 _regression_test_ _regression_test_|acc_stringmatch_q|_regression_test_stringmatch_ {"answer": {"type": "stringChoice", "value": "hello", "correct": false, "children": [], "caseInsensitive": false}, "correct": true, "questionId": "_regression_test_|acc_stringmatch_q|_regression_test_stringmatch_", "explanation": {"tags": [], "type": "content", "children": [{"tags": [], "type": "content", "value": "This needs a lower case \\"h\\".", "children": [], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}, "dateAttempted": 1713449505160} t 1 2024-04-18 15:11:45.16 From 43a51be7a06b5c28cbecc8221b9a54ec0cd91b6c Mon Sep 17 00:00:00 2001 From: Sol Dubock <94075844+sjd210@users.noreply.github.com> Date: Thu, 6 Nov 2025 09:36:28 +0000 Subject: [PATCH 33/38] Reset LLM question test correctness criteria --- .../cl/dtg/isaac/quiz/IsaacLLMFreeTextValidatorTest.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/test/java/uk/ac/cam/cl/dtg/isaac/quiz/IsaacLLMFreeTextValidatorTest.java b/src/test/java/uk/ac/cam/cl/dtg/isaac/quiz/IsaacLLMFreeTextValidatorTest.java index e936048208..028da55007 100644 --- a/src/test/java/uk/ac/cam/cl/dtg/isaac/quiz/IsaacLLMFreeTextValidatorTest.java +++ b/src/test/java/uk/ac/cam/cl/dtg/isaac/quiz/IsaacLLMFreeTextValidatorTest.java @@ -106,7 +106,7 @@ private static Object[][] genericTwoMarkCases() { {"A two-mark answer for a default marking formula two-mark question gets recognised as correct", question, "{\"reasonFoo\": 1, \"reasonBar\": 1, \"reasonFizz\": 0}", CORRECT, TWO_MARKS}, {"A one-mark answer for a default marking formula two-mark question receives exactly one mark", - question, "{\"reasonFoo\": 1, \"reasonBar\": 0, \"reasonFizz\": 0}", INCORRECT, ONE_MARK}}; + question, "{\"reasonFoo\": 1, \"reasonBar\": 0, \"reasonFizz\": 0}", CORRECT, ONE_MARK}}; } /* @@ -127,9 +127,9 @@ private static Object[][] advantageCases() { {"An answer containing an advantage and a disadvantage mark receives two marks", question, "{\"adv1\": 1, \"adv2\": 0, \"dis1\": 1, \"dis2\": 0}", CORRECT, TWO_MARKS}, {"An answer containing only a disadvantage mark receives one mark", - question, "{\"adv1\": 0, \"adv2\": 0, \"dis1\": 1, \"dis2\": 0}", INCORRECT, ONE_MARK}, + question, "{\"adv1\": 0, \"adv2\": 0, \"dis1\": 1, \"dis2\": 0}", CORRECT, ONE_MARK}, {"An answer containing two advantage marks receives one mark", - question, "{\"adv1\": 1, \"adv2\": 1, \"dis1\": 0, \"dis2\": 0}", INCORRECT, ONE_MARK}}; + question, "{\"adv1\": 1, \"adv2\": 1, \"dis1\": 0, \"dis2\": 0}", CORRECT, ONE_MARK}}; } @@ -156,7 +156,7 @@ private static Object[][] pointExplanationCases() { {"An answer containing an explanation without a matching point receives zero marks", question, "{\"pnt1\": 0, \"pnt2\": 0, \"expl1\": 1, \"expl2\": 0}", INCORRECT, NO_MARKS}, {"An answer containing a point and a mismatched explanation receives one mark", - question, "{\"pnt1\": 1, \"pnt2\": 0, \"expl1\": 0, \"expl2\": 0}", INCORRECT, ONE_MARK}}; + question, "{\"pnt1\": 1, \"pnt2\": 0, \"expl1\": 0, \"expl2\": 0}", CORRECT, ONE_MARK}}; } } From 3ae21ff445be6354ee56636950a6862fe94b8e6e Mon Sep 17 00:00:00 2001 From: Sol Dubock <94075844+sjd210@users.noreply.github.com> Date: Thu, 6 Nov 2025 10:24:18 +0000 Subject: [PATCH 34/38] Replace space with tab --- src/test/resources/test-postgres-rutherford-data-dump.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/resources/test-postgres-rutherford-data-dump.sql b/src/test/resources/test-postgres-rutherford-data-dump.sql index e6b6ccba95..416680e746 100644 --- a/src/test/resources/test-postgres-rutherford-data-dump.sql +++ b/src/test/resources/test-postgres-rutherford-data-dump.sql @@ -162,7 +162,7 @@ COPY public.logged_events (id, user_id, anonymous_user, event_type, event_detail COPY public.question_attempts (id, user_id, page_id, question_id, question_attempt, correct, marks, "timestamp") FROM stdin; 2 7 _regression_test_ _regression_test_|acc_multi_q|_regression_test_multi_ {"answer": {"type": "choice", "value": "$42$", "correct": false, "children": [], "encoding": "markdown"}, "correct": true, "questionId": "_regression_test_|acc_multi_q|_regression_test_multi_", "explanation": {"tags": [], "type": "content", "children": [{"tags": [], "type": "content", "value": "This is a correct choice.", "children": [], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}, "dateAttempted": 1713449470730} t 1 2024-04-18 15:11:10.73 -3 7 _regression_test_ _regression_test_|acc_numeric_q|_regresssion_test_numeric_ {"answer": {"type": "quantity", "units": "m\\\\,s^{-1}", "value": "2.01", "correct": false, "children": []}, "correct": true, "questionId": "_regression_test_|acc_numeric_q|_regresssion_test_numeric_", "explanation": {"tags": [], "type": "content", "children": [{"tags": [], "type": "content", "value": "This is a correct choice.", "children": [], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}, "correctUnits": true, "correctValue": true, "dateAttempted": 1713449479419} t 1 2024-04-18 15:11:19.419 +3 7 _regression_test_ _regression_test_|acc_numeric_q|_regresssion_test_numeric_ {"answer": {"type": "quantity", "units": "m\\\\,s^{-1}", "value": "2.01", "correct": false, "children": []}, "correct": true, "questionId": "_regression_test_|acc_numeric_q|_regresssion_test_numeric_", "explanation": {"tags": [], "type": "content", "children": [{"tags": [], "type": "content", "value": "This is a correct choice.", "children": [], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}, "correctUnits": true, "correctValue": true, "dateAttempted": 1713449479419} t 1 2024-04-18 15:11:19.419 4 7 _regression_test_ _regression_test_|acc_symbolic_q|_regression_test_symbolic_ {"answer": {"type": "formula", "value": "{\\"result\\":{\\"tex\\":\\"x\\",\\"mhchem\\":\\"\\",\\"python\\":\\"x\\",\\"mathml\\":\\"x\\",\\"uniqueSymbols\\":\\"x\\"},\\"symbols\\":[{\\"type\\":\\"Symbol\\",\\"position\\":{\\"x\\":239.5,\\"y\\":322.3333333333333},\\"expression\\":{\\"latex\\":\\"x\\",\\"python\\":\\"x\\"},\\"properties\\":{\\"letter\\":\\"x\\",\\"modifier\\":\\"\\"}}],\\"textEntry\\":true,\\"userInput\\":\\"x\\"}", "correct": false, "children": [], "pythonExpression": "x", "requiresExactMatch": false}, "correct": true, "questionId": "_regression_test_|acc_symbolic_q|_regression_test_symbolic_", "explanation": {"tags": [], "type": "content", "children": [{"tags": [], "type": "content", "value": "This is a correct choice. It requires an exact match!", "children": [], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}, "dateAttempted": 1713449484110} t 1 2024-04-18 15:11:24.11 5 7 _regression_test_ _regression_test_|acc_stringmatch_q|_regression_test_stringmatch_ {"answer": {"type": "stringChoice", "value": "hello", "correct": false, "children": [], "caseInsensitive": false}, "correct": true, "questionId": "_regression_test_|acc_stringmatch_q|_regression_test_stringmatch_", "explanation": {"tags": [], "type": "content", "children": [{"tags": [], "type": "content", "value": "This needs a lower case \\"h\\".", "children": [], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}, "dateAttempted": 1713449505160} t 1 2024-04-18 15:11:45.16 6 7 _regression_test_ _regression_test_|acc_chemistry_q|_regression_test_chemistry_ {"answer": {"type": "chemicalFormula", "value": "{\\"result\\":{\\"tex\\":\\"\\\\\\\\text{H} + \\\\\\\\text{Cl}\\",\\"mhchem\\":\\"H + Cl\\",\\"python\\":\\"\\\\\\\\text{H}\\",\\"mathml\\":\\"H+Cl\\",\\"uniqueSymbols\\":\\"H, Cl\\"},\\"symbols\\":[{\\"type\\":\\"ChemicalElement\\",\\"position\\":{\\"x\\":392.575,\\"y\\":531},\\"expression\\":{\\"latex\\":\\"\\\\\\\\text{H} + \\\\\\\\text{Cl}\\",\\"python\\":\\"\\\\\\\\text{H}\\"},\\"children\\":{\\"right\\":{\\"type\\":\\"BinaryOperation\\",\\"children\\":{\\"right\\":{\\"type\\":\\"ChemicalElement\\",\\"properties\\":{\\"element\\":\\"Cl\\"}}},\\"properties\\":{\\"operation\\":\\"+\\"}}},\\"properties\\":{\\"element\\":\\"H\\"}}],\\"textEntry\\":false,\\"userInput\\":\\"\\"}", "correct": false, "children": [], "mhchemExpression": "H + Cl"}, "correct": true, "questionId": "_regression_test_|acc_chemistry_q|_regression_test_chemistry_", "explanation": {"tags": [], "type": "content", "children": [{"tags": [], "type": "content", "value": "This is a correct choice.", "children": [], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}, "dateAttempted": 1713449507230} t 1 2024-04-18 15:11:47.23 From 7541eba1a368442966aa0ef6777f50c31e9a5b4d Mon Sep 17 00:00:00 2001 From: Sol Dubock <94075844+sjd210@users.noreply.github.com> Date: Thu, 6 Nov 2025 10:30:37 +0000 Subject: [PATCH 35/38] Explicitly cast null marks to 0 --- .../uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java index 50e09dab5c..289dea85b8 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/quiz/PgQuestionAttempts.java @@ -448,7 +448,12 @@ private LightweightQuestionValidationResponse resultsToLightweightValidationResp partialQuestionAttempt.setCorrect(results.getBoolean("correct")); partialQuestionAttempt.setQuestionId(results.getString("question_id")); partialQuestionAttempt.setDateAttempted(results.getTimestamp("timestamp")); - partialQuestionAttempt.setMarks(results.getInt("marks")); + int marks = results.getInt("marks"); + if (results.wasNull()) { + partialQuestionAttempt.setMarks(0); + } else { + partialQuestionAttempt.setMarks(marks); + } return partialQuestionAttempt; } From 07c9d400e66b09fadffb300e47159e6703c7095d Mon Sep 17 00:00:00 2001 From: Sol Dubock <94075844+sjd210@users.noreply.github.com> Date: Thu, 6 Nov 2025 11:44:25 +0000 Subject: [PATCH 36/38] Replace another space with tab to fix test --- src/test/resources/test-postgres-rutherford-data-dump.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/resources/test-postgres-rutherford-data-dump.sql b/src/test/resources/test-postgres-rutherford-data-dump.sql index 416680e746..71b579c217 100644 --- a/src/test/resources/test-postgres-rutherford-data-dump.sql +++ b/src/test/resources/test-postgres-rutherford-data-dump.sql @@ -162,7 +162,7 @@ COPY public.logged_events (id, user_id, anonymous_user, event_type, event_detail COPY public.question_attempts (id, user_id, page_id, question_id, question_attempt, correct, marks, "timestamp") FROM stdin; 2 7 _regression_test_ _regression_test_|acc_multi_q|_regression_test_multi_ {"answer": {"type": "choice", "value": "$42$", "correct": false, "children": [], "encoding": "markdown"}, "correct": true, "questionId": "_regression_test_|acc_multi_q|_regression_test_multi_", "explanation": {"tags": [], "type": "content", "children": [{"tags": [], "type": "content", "value": "This is a correct choice.", "children": [], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}, "dateAttempted": 1713449470730} t 1 2024-04-18 15:11:10.73 -3 7 _regression_test_ _regression_test_|acc_numeric_q|_regresssion_test_numeric_ {"answer": {"type": "quantity", "units": "m\\\\,s^{-1}", "value": "2.01", "correct": false, "children": []}, "correct": true, "questionId": "_regression_test_|acc_numeric_q|_regresssion_test_numeric_", "explanation": {"tags": [], "type": "content", "children": [{"tags": [], "type": "content", "value": "This is a correct choice.", "children": [], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}, "correctUnits": true, "correctValue": true, "dateAttempted": 1713449479419} t 1 2024-04-18 15:11:19.419 +3 7 _regression_test_ _regression_test_|acc_numeric_q|_regresssion_test_numeric_ {"answer": {"type": "quantity", "units": "m\\\\,s^{-1}", "value": "2.01", "correct": false, "children": []}, "correct": true, "questionId": "_regression_test_|acc_numeric_q|_regresssion_test_numeric_", "explanation": {"tags": [], "type": "content", "children": [{"tags": [], "type": "content", "value": "This is a correct choice.", "children": [], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}, "correctUnits": true, "correctValue": true, "dateAttempted": 1713449479419} t 1 2024-04-18 15:11:19.419 4 7 _regression_test_ _regression_test_|acc_symbolic_q|_regression_test_symbolic_ {"answer": {"type": "formula", "value": "{\\"result\\":{\\"tex\\":\\"x\\",\\"mhchem\\":\\"\\",\\"python\\":\\"x\\",\\"mathml\\":\\"x\\",\\"uniqueSymbols\\":\\"x\\"},\\"symbols\\":[{\\"type\\":\\"Symbol\\",\\"position\\":{\\"x\\":239.5,\\"y\\":322.3333333333333},\\"expression\\":{\\"latex\\":\\"x\\",\\"python\\":\\"x\\"},\\"properties\\":{\\"letter\\":\\"x\\",\\"modifier\\":\\"\\"}}],\\"textEntry\\":true,\\"userInput\\":\\"x\\"}", "correct": false, "children": [], "pythonExpression": "x", "requiresExactMatch": false}, "correct": true, "questionId": "_regression_test_|acc_symbolic_q|_regression_test_symbolic_", "explanation": {"tags": [], "type": "content", "children": [{"tags": [], "type": "content", "value": "This is a correct choice. It requires an exact match!", "children": [], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}, "dateAttempted": 1713449484110} t 1 2024-04-18 15:11:24.11 5 7 _regression_test_ _regression_test_|acc_stringmatch_q|_regression_test_stringmatch_ {"answer": {"type": "stringChoice", "value": "hello", "correct": false, "children": [], "caseInsensitive": false}, "correct": true, "questionId": "_regression_test_|acc_stringmatch_q|_regression_test_stringmatch_", "explanation": {"tags": [], "type": "content", "children": [{"tags": [], "type": "content", "value": "This needs a lower case \\"h\\".", "children": [], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}, "dateAttempted": 1713449505160} t 1 2024-04-18 15:11:45.16 6 7 _regression_test_ _regression_test_|acc_chemistry_q|_regression_test_chemistry_ {"answer": {"type": "chemicalFormula", "value": "{\\"result\\":{\\"tex\\":\\"\\\\\\\\text{H} + \\\\\\\\text{Cl}\\",\\"mhchem\\":\\"H + Cl\\",\\"python\\":\\"\\\\\\\\text{H}\\",\\"mathml\\":\\"H+Cl\\",\\"uniqueSymbols\\":\\"H, Cl\\"},\\"symbols\\":[{\\"type\\":\\"ChemicalElement\\",\\"position\\":{\\"x\\":392.575,\\"y\\":531},\\"expression\\":{\\"latex\\":\\"\\\\\\\\text{H} + \\\\\\\\text{Cl}\\",\\"python\\":\\"\\\\\\\\text{H}\\"},\\"children\\":{\\"right\\":{\\"type\\":\\"BinaryOperation\\",\\"children\\":{\\"right\\":{\\"type\\":\\"ChemicalElement\\",\\"properties\\":{\\"element\\":\\"Cl\\"}}},\\"properties\\":{\\"operation\\":\\"+\\"}}},\\"properties\\":{\\"element\\":\\"H\\"}}],\\"textEntry\\":false,\\"userInput\\":\\"\\"}", "correct": false, "children": [], "mhchemExpression": "H + Cl"}, "correct": true, "questionId": "_regression_test_|acc_chemistry_q|_regression_test_chemistry_", "explanation": {"tags": [], "type": "content", "children": [{"tags": [], "type": "content", "value": "This is a correct choice.", "children": [], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}], "encoding": "markdown", "canonicalSourceFile": "content/_regression_test_.json"}, "dateAttempted": 1713449507230} t 1 2024-04-18 15:11:47.23 From 108d55ae815718eb522a4322f4af85988b214f62 Mon Sep 17 00:00:00 2001 From: Sol Dubock <94075844+sjd210@users.noreply.github.com> Date: Fri, 7 Nov 2025 14:41:03 +0000 Subject: [PATCH 37/38] Explicitly ignore "marksAwarded" field in Jackson --- .../cl/dtg/isaac/dos/LLMFreeTextQuestionValidationResponse.java | 2 ++ .../dtg/isaac/dto/LLMFreeTextQuestionValidationResponseDTO.java | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/LLMFreeTextQuestionValidationResponse.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/LLMFreeTextQuestionValidationResponse.java index e47a100ae4..4679bb57ce 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/LLMFreeTextQuestionValidationResponse.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/dos/LLMFreeTextQuestionValidationResponse.java @@ -1,5 +1,6 @@ package uk.ac.cam.cl.dtg.isaac.dos; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import uk.ac.cam.cl.dtg.isaac.dos.content.Choice; import uk.ac.cam.cl.dtg.isaac.dos.content.Content; import uk.ac.cam.cl.dtg.isaac.dos.content.DTOMapping; @@ -9,6 +10,7 @@ import java.util.Date; import java.util.List; +@JsonIgnoreProperties({ "marksAwarded" }) @DTOMapping(LLMFreeTextQuestionValidationResponseDTO.class) public class LLMFreeTextQuestionValidationResponse extends QuestionValidationResponse { private List markBreakdown; diff --git a/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/LLMFreeTextQuestionValidationResponseDTO.java b/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/LLMFreeTextQuestionValidationResponseDTO.java index 8dbab4a999..20b85bef0a 100644 --- a/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/LLMFreeTextQuestionValidationResponseDTO.java +++ b/src/main/java/uk/ac/cam/cl/dtg/isaac/dto/LLMFreeTextQuestionValidationResponseDTO.java @@ -1,9 +1,11 @@ package uk.ac.cam.cl.dtg.isaac.dto; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import uk.ac.cam.cl.dtg.isaac.dto.content.LLMFreeTextMarkSchemeEntryDTO; import java.util.List; +@JsonIgnoreProperties({ "marksAwarded" }) public class LLMFreeTextQuestionValidationResponseDTO extends QuestionValidationResponseDTO { private List markBreakdown; From accaa393016d6f6575f3f5566ad566355f66ef28 Mon Sep 17 00:00:00 2001 From: Sol Dubock <94075844+sjd210@users.noreply.github.com> Date: Fri, 7 Nov 2025 15:58:35 +0000 Subject: [PATCH 38/38] Update script to add but not update columns first In the subsequent week's release, we will use another database migration script to update the columns --- ...5-10-questions-attempts-add-mark-field.sql | 99 ------------------- ...5-11-questions-attempts-add-mark-field.sql | 5 + 2 files changed, 5 insertions(+), 99 deletions(-) delete mode 100644 src/main/resources/db_scripts/migrations/2025-10-questions-attempts-add-mark-field.sql create mode 100644 src/main/resources/db_scripts/migrations/2025-11-questions-attempts-add-mark-field.sql diff --git a/src/main/resources/db_scripts/migrations/2025-10-questions-attempts-add-mark-field.sql b/src/main/resources/db_scripts/migrations/2025-10-questions-attempts-add-mark-field.sql deleted file mode 100644 index d7886066d6..0000000000 --- a/src/main/resources/db_scripts/migrations/2025-10-questions-attempts-add-mark-field.sql +++ /dev/null @@ -1,99 +0,0 @@ -ALTER TABLE question_attempts -ADD marks integer; - -ALTER TABLE quiz_question_attempts -ADD marks integer; - -CREATE OR REPLACE FUNCTION update_marks_for_period(qa question_attempts) - RETURNS int AS $$ -BEGIN - RETURN CASE - WHEN (qa.question_attempt -> 'answer' ->> 'type') = 'llmFreeTextChoice' - THEN (qa.question_attempt ->> 'marksAwarded')::int - ELSE qa.correct::int - END; -END; -$$ LANGUAGE plpgsql; - -CREATE OR REPLACE FUNCTION quiz_update_marks_for_period(qa quiz_question_attempts) - RETURNS int AS $$ -BEGIN - RETURN CASE - WHEN (qa.question_attempt -> 'answer' ->> 'type') = 'llmFreeTextChoice' - THEN (qa.question_attempt ->> 'marksAwarded')::int - ELSE qa.correct::int - END; -END; -$$ LANGUAGE plpgsql; - -UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2014-01-01' AND timestamp <= '2015-01-02'; -UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2015-01-01' AND timestamp <= '2016-01-02'; -UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2016-01-01' AND timestamp <= '2016-06-02'; -UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2016-06-01' AND timestamp <= '2017-01-02'; -UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2017-01-01' AND timestamp <= '2017-06-02'; -UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2017-06-01' AND timestamp <= '2018-01-02'; -UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2018-01-01' AND timestamp <= '2018-06-02'; -UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2018-06-01' AND timestamp <= '2018-10-02'; -UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2018-10-01' AND timestamp <= '2019-01-02'; -UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2019-01-01' AND timestamp <= '2019-06-02'; -UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2019-06-01' AND timestamp <= '2019-10-02'; -UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2019-10-01' AND timestamp <= '2020-01-02'; -UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2020-01-01' AND timestamp <= '2020-04-02'; -UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2020-04-01' AND timestamp <= '2020-09-02'; -UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2020-09-01' AND timestamp <= '2020-10-02'; -UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2020-10-01' AND timestamp <= '2021-01-02'; -UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2021-01-01' AND timestamp <= '2021-04-02'; -UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2021-04-01' AND timestamp <= '2021-09-02'; -UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2021-09-01' AND timestamp <= '2021-10-02'; -UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2021-10-01' AND timestamp <= '2022-01-02'; -UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2022-01-01' AND timestamp <= '2022-04-02'; -UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2022-04-01' AND timestamp <= '2022-09-02'; -UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2022-09-01' AND timestamp <= '2022-10-02'; -UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2022-10-01' AND timestamp <= '2023-01-02'; -UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2023-01-01' AND timestamp <= '2023-03-02'; -UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2023-03-01' AND timestamp <= '2023-06-02'; -UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2023-06-01' AND timestamp <= '2023-09-02'; -UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2023-09-01' AND timestamp <= '2023-10-02'; -UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2023-10-01' AND timestamp <= '2023-11-02'; -UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2023-11-01' AND timestamp <= '2024-01-02'; -UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2024-01-01' AND timestamp <= '2024-02-02'; -UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2024-02-01' AND timestamp <= '2024-03-02'; -UPDATE question_attempts SET marks = update_marks_for_period(question_attempts) WHERE timestamp > '2024-03-01' AND timestamp <= '2026-01-02'; - - -UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2014-01-01' AND timestamp <= '2015-01-02'; -UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2015-01-01' AND timestamp <= '2016-01-02'; -UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2016-01-01' AND timestamp <= '2016-06-02'; -UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2016-06-01' AND timestamp <= '2017-01-02'; -UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2017-01-01' AND timestamp <= '2017-06-02'; -UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2017-06-01' AND timestamp <= '2018-01-02'; -UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2018-01-01' AND timestamp <= '2018-06-02'; -UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2018-06-01' AND timestamp <= '2018-10-02'; -UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2018-10-01' AND timestamp <= '2019-01-02'; -UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2019-01-01' AND timestamp <= '2019-06-02'; -UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2019-06-01' AND timestamp <= '2019-10-02'; -UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2019-10-01' AND timestamp <= '2020-01-02'; -UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2020-01-01' AND timestamp <= '2020-04-02'; -UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2020-04-01' AND timestamp <= '2020-09-02'; -UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2020-09-01' AND timestamp <= '2020-10-02'; -UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2020-10-01' AND timestamp <= '2021-01-02'; -UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2021-01-01' AND timestamp <= '2021-04-02'; -UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2021-04-01' AND timestamp <= '2021-09-02'; -UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2021-09-01' AND timestamp <= '2021-10-02'; -UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2021-10-01' AND timestamp <= '2022-01-02'; -UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2022-01-01' AND timestamp <= '2022-04-02'; -UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2022-04-01' AND timestamp <= '2022-09-02'; -UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2022-09-01' AND timestamp <= '2022-10-02'; -UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2022-10-01' AND timestamp <= '2023-01-02'; -UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2023-01-01' AND timestamp <= '2023-03-02'; -UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2023-03-01' AND timestamp <= '2023-06-02'; -UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2023-06-01' AND timestamp <= '2023-09-02'; -UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2023-09-01' AND timestamp <= '2023-10-02'; -UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2023-10-01' AND timestamp <= '2023-11-02'; -UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2023-11-01' AND timestamp <= '2024-01-02'; -UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2024-01-01' AND timestamp <= '2024-02-02'; -UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2024-02-01' AND timestamp <= '2024-03-02'; -UPDATE quiz_question_attempts SET marks = quiz_update_marks_for_period(quiz_question_attempts) WHERE timestamp > '2024-03-01' AND timestamp <= '2026-01-02'; - -DROP FUNCTION update_marks_for_period; -DROP FUNCTION quiz_update_marks_for_period; \ No newline at end of file diff --git a/src/main/resources/db_scripts/migrations/2025-11-questions-attempts-add-mark-field.sql b/src/main/resources/db_scripts/migrations/2025-11-questions-attempts-add-mark-field.sql new file mode 100644 index 0000000000..776bcf136c --- /dev/null +++ b/src/main/resources/db_scripts/migrations/2025-11-questions-attempts-add-mark-field.sql @@ -0,0 +1,5 @@ +ALTER TABLE question_attempts +ADD marks integer; + +ALTER TABLE quiz_question_attempts +ADD marks integer;