diff --git a/src/main/java/edu/byu/cs/analytics/CommitAnalytics.java b/src/main/java/edu/byu/cs/analytics/CommitAnalytics.java index d8e704282..c3ec8a587 100755 --- a/src/main/java/edu/byu/cs/analytics/CommitAnalytics.java +++ b/src/main/java/edu/byu/cs/analytics/CommitAnalytics.java @@ -51,6 +51,14 @@ public static CommitsByDay countCommitsByDay( Git git, @NonNull CommitThreshold lowerBound, @NonNull CommitThreshold upperBound) throws GitAPIException, IOException { + // Verify arguments + if (git == null) { + throw new RuntimeException("The git parameter cannot be null"); + } + if (lowerBound == null || upperBound == null) { + throw new IllegalArgumentException("Both bounds must not be null"); + } + // Prepare data for repeated calculation DiffFormatter diffFormatter = prepareDiffFormatter(git); Iterable commits = getCommitsBetweenBounds(git, upperBound.commitHash(), lowerBound.commitHash()); diff --git a/src/main/java/edu/byu/cs/autograder/Grader.java b/src/main/java/edu/byu/cs/autograder/Grader.java index c42f11120..5509b68a2 100644 --- a/src/main/java/edu/byu/cs/autograder/Grader.java +++ b/src/main/java/edu/byu/cs/autograder/Grader.java @@ -94,6 +94,7 @@ public void run() { Rubric rubric = evaluateProject(RUN_COMPILATION ? rubricConfig : null); Submission submission = new Scorer(gradingContext).score(rubric, commitVerificationResult); + DaoService.getSubmissionDao().insertSubmission(submission); observer.notifyDone(submission); } catch (GradingException ge) { @@ -115,7 +116,6 @@ public void run() { } private Rubric evaluateProject(RubricConfig rubricConfig) throws GradingException, DataAccessException { - // NOTE: Ideally these would be treated with enum types. That will need to be improved with #300. EnumMap rubricItems = new EnumMap<>(Rubric.RubricType.class); if (rubricConfig == null) { return new Rubric(new EnumMap<>(Rubric.RubricType.class), false, "No Rubric Config"); diff --git a/src/main/java/edu/byu/cs/autograder/git/GitHelper.java b/src/main/java/edu/byu/cs/autograder/git/GitHelper.java index 65a97b1d4..21f21cfa6 100644 --- a/src/main/java/edu/byu/cs/autograder/git/GitHelper.java +++ b/src/main/java/edu/byu/cs/autograder/git/GitHelper.java @@ -11,7 +11,6 @@ import edu.byu.cs.dataAccess.SubmissionDao; import edu.byu.cs.model.Submission; import edu.byu.cs.util.PhaseUtils; -import org.eclipse.jetty.util.ajax.JSON; import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.api.CloneCommand; import org.eclipse.jgit.api.Git; @@ -143,9 +142,7 @@ CommitVerificationResult verifyCommitRequirements(File stageRepo) throws Grading CommitThreshold lowerThreshold = getMostRecentPassingSubmission(git, passingSubmissions); CommitThreshold upperThreshold = constructCurrentThreshold(git); - var results = verifyRegularCommits(git, lowerThreshold, upperThreshold); - notifyVerificationComplete(results); - return results; + return verifyRegularCommits(git, lowerThreshold, upperThreshold); } } catch (IOException | GitAPIException | DataAccessException e) { var observer = gradingContext.observer(); @@ -154,16 +151,6 @@ CommitVerificationResult verifyCommitRequirements(File stageRepo) throws Grading throw new GradingException("Failed to verify commits: " + e.getMessage()); } } - private void notifyVerificationComplete(CommitVerificationResult commitVerificationResult) { - LOGGER.debug("Commit verification result: " + JSON.toString(commitVerificationResult)); - - var observer = gradingContext.observer(); - if (commitVerificationResult.verified()) { - observer.update("Passed commit verification."); - } else { - observer.update("Failed commit verification. Continuing with grading anyways."); - } - } /** * Performs the explicit remembering of the authorization of previous phases. @@ -179,19 +166,32 @@ private CommitVerificationResult preserveOriginalVerification() throws DataAcces // We have a previous result to defer to: int originalPenaltyPct = firstPassingSubmission.getPenaltyPct(); - boolean verified = firstPassingSubmission.verifiedStatus().isApproved(); - String message = verified ? - "You passed the commit verification on your first passing submission! You're good to go!" : - "You have previously failed commit verification.\n"+ - "You still need to meet with a TA or a professor to gain credit for this phase."; + boolean verified = firstPassingSubmission.isApproved(); + String failureMessage = generateFailureMessage(verified, firstPassingSubmission); return new CommitVerificationResult( verified, true, - 0, 0, 0, originalPenaltyPct, message, + 0, 0, 0, originalPenaltyPct, failureMessage, null, null, firstPassingSubmission.headHash(), null ); } + private static String generateFailureMessage(boolean verified, Submission firstPassingSubmission) { + String message; + if (!verified) { + message = "You have previously failed commit verification.\n"+ + "You still need to meet with a TA or a professor to gain credit for this phase."; + } else { + var verification = firstPassingSubmission.verification(); + if (verification == null || verification.penaltyPct() <= 0) { + message = "You passed the commit verification on your first passing submission! You're good to go!"; + } else { + message = "Your commit verification was previously approved with a penalty. That penalty is being applied to this submission as well."; + } + } + return message; + } + /** * Analyzes the commits in the given directory within the bounds provided. * @@ -360,7 +360,7 @@ private static ArrayList evaluateConditions(CV[] assertedConditions, int if (!errorMessages.isEmpty()) { errorMessages.add("Since you did not meet the prerequisites for commit frequency, " + "you will need to talk to a TA to receive a score. "); - errorMessages.add(String.format("It will come with a %d%% penalty.", commitVerificationPenaltyPct)); + errorMessages.add(String.format("It may come with a %d%% penalty.", commitVerificationPenaltyPct)); } return errorMessages; } diff --git a/src/main/java/edu/byu/cs/autograder/score/Scorer.java b/src/main/java/edu/byu/cs/autograder/score/Scorer.java index c252fe42d..85e2af6b5 100644 --- a/src/main/java/edu/byu/cs/autograder/score/Scorer.java +++ b/src/main/java/edu/byu/cs/autograder/score/Scorer.java @@ -17,7 +17,9 @@ import org.slf4j.LoggerFactory; import java.time.ZonedDateTime; +import java.util.EnumMap; import java.util.HashMap; +import java.util.Map; import static edu.byu.cs.model.Submission.VerifiedStatus; @@ -35,78 +37,158 @@ public Scorer(GradingContext gradingContext) { this.gradingContext = gradingContext; } + /** + * Main entry point for the {@link Scorer} class. + * This method takes in a rubric and a commit verification result + * and scores them together. + *
+ * When appropriate, it will save the score the grade-book, + * but it always returns a {@link Submission} that can be + * @param rubric A freshly generated {@link Rubric} from the grading system. + * @param commitVerificationResult The associated {@link CommitVerificationResult} from the verification system. + * @return A {@link Submission} ready to save in the database. + * @throws GradingException When pre-conditions are not met. + * @throws DataAccessException When the database cannot be accessed. + */ public Submission score(Rubric rubric, CommitVerificationResult commitVerificationResult) throws GradingException, DataAccessException { gradingContext.observer().update("Grading..."); - rubric = CanvasUtils.decimalScoreToPoints(gradingContext.phase(), rubric); - rubric = annotateRubric(rubric); + rubric = transformRubric(rubric); // Exit early when the score isn't important if (gradingContext.admin() || !PhaseUtils.isPhaseGraded(gradingContext.phase())) { - return saveResults(rubric, commitVerificationResult, 0, getScore(rubric), ""); + return generateSubmissionObject(rubric, commitVerificationResult, 0, getScore(rubric), ""); } int daysLate = new LateDayCalculator().calculateLateDays(gradingContext.phase(), gradingContext.netId()); - float thisScore = calculateScoreWithLatePenalty(rubric, daysLate); + applyLatePenalty(rubric.items(), daysLate); + float thisScore = getScore(rubric); // Validate several conditions before submitting to the grade-book - Submission thisSubmission; if (!rubric.passed()) { - thisSubmission = saveResults(rubric, commitVerificationResult, daysLate, thisScore, ""); + return generateSubmissionObject(rubric, commitVerificationResult, daysLate, thisScore, ""); } else if (!commitVerificationResult.verified()) { - thisSubmission = saveResults(rubric, commitVerificationResult, daysLate, thisScore, commitVerificationResult.failureMessage()); + return generateSubmissionObject(rubric, commitVerificationResult, daysLate, thisScore, commitVerificationResult.failureMessage()); } else { - // The student receives a score! - thisSubmission = attemptSendToCanvas(rubric, commitVerificationResult, daysLate, thisScore); + // The student (may) receive a score in canvas! + return successfullyProcessSubmission(rubric, commitVerificationResult, daysLate, thisScore); } + } - return thisSubmission; + /** + * Transforms the values in the rubric from percentage grading results to point valued scores. + * The resulting {@link Rubric} is ready to save in the database and give to the grade-book system. + * + * @param rubric A freshly generated Rubric still containing decimal scores from the grading process. + * @return A new Rubric with values ready for the grade-book. + * @throws GradingException When pre-conditions are not met. + * @throws DataAccessException When the database cannot be accessed. + */ + private Rubric transformRubric(Rubric rubric) throws GradingException, DataAccessException { + rubric = CanvasUtils.decimalScoreToPoints(gradingContext.phase(), rubric); + return new Rubric(rubric.items(), passed(rubric), rubric.notes()); } + /** - * Saves the generated submission and carefully submits the score to Canvas when it helps the student's grade. + * Carefully submits the score to Canvas when it helps the student's grade. + * Returns the submission, now with all missing rubric items populated with their previous values from Canvas. *
- * Calling this method constitutes a successful, verified submission that will be saved and submitted. + * Calling this method constitutes a successful, verified submission that will be submitted to canvas. * - * @param rubric Required. - * @param commitVerificationResult Required. - * @param daysLate Required. - * @param thisScore Required. + * @param rubric Required. + * @param commitVerificationResult Required when originally creating a submission. + * Can be null when sending scores to Canvas; this will disable + * any automatic point deductions for verification, and also result in + * null being returned instead of a {@link Submission}. + * @param daysLate Required. Used to add a note to the resulting submission object. + * @param thisScore Required. Used to place a value in the {@link Submission} object. + * The Canvas grade is based entirely on the provided {@link Rubric}. * @return A construction Submission for continued processing * @throws DataAccessException When the database can't be reached. - * @throws GradingException When other conditions fail. + * @throws GradingException When other conditions fail. */ - private Submission attemptSendToCanvas( + private Submission successfullyProcessSubmission( Rubric rubric, CommitVerificationResult commitVerificationResult, int daysLate, float thisScore ) throws DataAccessException, GradingException { if (!ApplicationProperties.useCanvas()) { - return saveResults(rubric, commitVerificationResult, daysLate, thisScore, + return generateSubmissionObject(rubric, commitVerificationResult, daysLate, thisScore, "Would have attempted grade-book submission, but skipped due to application properties."); } + AssessmentSubmittalRemnants submittalRemnants = attemptSendToCanvas(rubric, commitVerificationResult, false); + return generateSubmissionObject(rubric, commitVerificationResult, daysLate, thisScore, submittalRemnants.notes); + } + + /** + * Uses several values stored in the {@link GradingContext} to send the provided Rubric to Canvas. + *
+ * Note that this operation will be performed carefully so that existing RubricItem's in Canvas + * will not be overwritten by the operation. + * + * @param rubric A {@link Rubric} containing values to set in Canvas. + * Any items not set will be populated with their value from Canvas. + * @param penaltyPct The approved GIT_COMMITS penalty percentage + * @param forceSendScore Forces the grade to be submitted, even if it would lower the student's score. + * @throws GradingException When preconditions are not met. + * @throws DataAccessException When the database cannot be reached. + */ + public void attemptSendToCanvas(Rubric rubric, int penaltyPct, String commitPenaltyMsg, boolean forceSendScore) throws GradingException, DataAccessException { + /** + * Set only the fields that will be used by + * {@link Scorer#setCommitVerificationPenalty(CanvasRubricAssessment, GradingContext, CommitVerificationResult)} + * to reduce the score based on the latest data from the grade-book. + */ + CommitVerificationResult verification = new CommitVerificationResult( + true, true, 0, 0, 0, + penaltyPct, commitPenaltyMsg, + null, null, null, null); + attemptSendToCanvas(rubric, verification, forceSendScore); + } + + /** + * Sends a rubric to Canvas, respecting configuration for lowering a student's score. + * + * @param rubric The Rubric to submit + * @param commitVerificationResult Will be used to apply the Git Commits penalty + * @param forceSendScore When true, this will overwrite the score even if the new score is lower than the old score. + * @return {@link AssessmentSubmittalRemnants} that contains some information about the results of the operation. + * @throws DataAccessException When the database cannot be accessed. + * @throws GradingException When pre-conditions are not met. + */ + private AssessmentSubmittalRemnants attemptSendToCanvas( + Rubric rubric, CommitVerificationResult commitVerificationResult, boolean forceSendScore + ) throws DataAccessException, GradingException { + int canvasUserId = getCanvasUserId(); int assignmentNum = PhaseUtils.getPhaseAssignmentNumber(gradingContext.phase()); CanvasRubricAssessment existingAssessment = getExistingAssessment(canvasUserId, assignmentNum); - CanvasRubricAssessment newAssessment = - addExistingPoints(constructCanvasRubricAssessment(rubric, daysLate), existingAssessment); + CanvasRubricAssessment newAssessment = addExistingPoints(constructCanvasRubricAssessment(rubric), existingAssessment); setCommitVerificationPenalty(newAssessment, gradingContext, commitVerificationResult); // prevent score from being saved to canvas if it will lower their score - Submission submission; - if (totalPoints(newAssessment) <= totalPoints(existingAssessment)) { - String notes = "Submission did not improve current score. Score not saved to Canvas.\n"; - submission = saveResults(rubric, commitVerificationResult, daysLate, thisScore, notes); + String notes = ""; + boolean didSend = false; + float newPoints = totalPoints(newAssessment); + if (!forceSendScore && newPoints <= totalPoints(existingAssessment)) { + notes = "Submission did not improve current score. Score not saved to Canvas.\n"; } else { - submission = saveResults(rubric, commitVerificationResult, daysLate, thisScore, ""); + didSend = true; sendToCanvas(canvasUserId, assignmentNum, newAssessment, rubric.notes()); } - return submission; + return new AssessmentSubmittalRemnants(didSend, newPoints, notes); } + private record AssessmentSubmittalRemnants( + boolean didSend, + float pointsSent, + String notes + ) { } + /** * Simplifying overload that modifies the {@link CanvasRubricAssessment} by setting the GIT_COMMITS penalty properly. * @@ -114,11 +196,12 @@ private Submission attemptSendToCanvas( * * @param assessment The assessment to modify. * @param gradingContext Represents the current grading. - * @param verification The evaluated CommitVerificationResult. + * @param verification The evaluated CommitVerificationResult. If null, then no effects will take place. * @throws GradingException When grading errors occur such as the phase not having a GIT_COMMITS rubric item. */ - public static void setCommitVerificationPenalty(CanvasRubricAssessment assessment, GradingContext gradingContext, + private static void setCommitVerificationPenalty(CanvasRubricAssessment assessment, GradingContext gradingContext, CommitVerificationResult verification) throws GradingException { + if (verification == null) return; setCommitVerificationPenalty(assessment, gradingContext.phase(), verification.penaltyPct(), verification.failureMessage()); } @@ -142,10 +225,10 @@ public static void setCommitVerificationPenalty(CanvasRubricAssessment assessmen // Prepare conditions and then calculate penalty CanvasRubricItem emptyRubricItem = new CanvasRubricItem("", 0); - assessment.addItem(commitRubricId, emptyRubricItem); + assessment.insertItem(commitRubricId, emptyRubricItem); float commitPenalty = getCommitPenaltyValue(penaltyPct, assessment); - assessment.addItem(commitRubricId, new CanvasRubricItem(commitComment, commitPenalty)); + assessment.insertItem(commitRubricId, new CanvasRubricItem(commitComment, commitPenalty)); } /** @@ -175,21 +258,10 @@ private int getCanvasUserId() throws DataAccessException { return user.canvasUserId(); } - private CanvasRubricAssessment constructCanvasRubricAssessment(Rubric rubric, int daysLate) + private CanvasRubricAssessment constructCanvasRubricAssessment(Rubric rubric) throws DataAccessException, GradingException { RubricConfig rubricConfig = DaoService.getRubricConfigDao().getRubricConfig(gradingContext.phase()); - float lateAdjustment = daysLate * PER_DAY_LATE_PENALTY; - return CanvasUtils.convertToAssessment(rubric, rubricConfig, lateAdjustment, gradingContext.phase()); - } - - /** - * Annotates the rubric with notes and passed status - * - * @param rubric the rubric to annotate - * @return the annotated rubric - */ - private Rubric annotateRubric(Rubric rubric) { - return new Rubric(rubric.items(), passed(rubric), rubric.notes()); + return CanvasUtils.convertToAssessment(rubric, rubricConfig, gradingContext.phase()); } private boolean passed(Rubric rubric) { @@ -203,6 +275,21 @@ private boolean passed(Rubric rubric) { return passoffTestItem.results().score() >= passoffTestItem.results().possiblePoints(); } + private void applyLatePenalty(EnumMap items, int daysLate) { + float lateAdjustment = daysLate * PER_DAY_LATE_PENALTY; + for(Map.Entry entry : items.entrySet()) { + Rubric.Results results = entry.getValue().results(); + results = new Rubric.Results(results.notes(), + results.score() * (1 - lateAdjustment), + results.possiblePoints(), + results.testResults(), + results.textResults()); + Rubric.RubricItem rubricItem = entry.getValue(); + rubricItem = new Rubric.RubricItem(rubricItem.category(), results, rubricItem.criteria()); + items.put(entry.getKey(), rubricItem); + } + } + /** * Returns a new CanvasRubricAssessment that represents the result of merging `assessment` into `existing`. *
@@ -241,17 +328,10 @@ private static float totalPoints(CanvasRubricAssessment assessment) { return points; } - private float calculateScoreWithLatePenalty(Rubric rubric, int numDaysLate) throws GradingException, DataAccessException { - float score = getScore(rubric); - score *= 1 - (numDaysLate * PER_DAY_LATE_PENALTY); - if (score < 0) score = 0; - return score; - } - /** * Gets the score for the phase * - * @return the score + * @return the score as a percentage value from [0-1]. */ private float getScore(Rubric rubric) throws GradingException, DataAccessException { int totalPossiblePoints = DaoService.getRubricConfigDao().getPhaseTotalPossiblePoints(gradingContext.phase()); @@ -260,22 +340,46 @@ private float getScore(Rubric rubric) throws GradingException, DataAccessExcepti throw new GradingException("Total possible points for phase " + gradingContext.phase() + " is 0"); } - return rubric.getTotalPoints() / totalPossiblePoints; + float score = 0; + for (Rubric.RubricType type : Rubric.RubricType.values()) { + var rubricItem = rubric.items().get(type); + if (rubricItem == null) continue; + score += rubricItem.results().score(); + } + + return score / totalPossiblePoints; } /** - * Saves the results of the grading to the database if the submission passed + * Prepares the necessary data pieces to construct a {@link Submission}. + * This can be saved in the database, and has information which is + * displayed to the user. + *
+ * Note that this object is not sent directly to any grade-book. + * Other objects are constructed independently for that purpose. * - * @param rubric the rubric for the phase + * @param rubric A fully transformed and populated Rubric. + * @param commitVerificationResult Results from the commit verification system. + * If this value is null, the function will return null. + * @param numDaysLate The number of days late this submission was handed-in. + * For note generating purposes only; this is not used to + * calculate any penalties. + * @param score The final approved score on the submission represented in points. + * @param notes Any notes that are associated with the submission. + * More comments may be added to this string while preparing the Submission. */ - private Submission saveResults(Rubric rubric, CommitVerificationResult commitVerificationResult, - int numDaysLate, float score, String notes) + private Submission generateSubmissionObject(Rubric rubric, CommitVerificationResult commitVerificationResult, + int numDaysLate, float score, String notes) throws GradingException, DataAccessException { + if (commitVerificationResult == null) { + return null; // This is allowed. + } + String headHash = commitVerificationResult.headHash(); String netId = gradingContext.netId(); if (numDaysLate > 0) - notes += numDaysLate + " days late. -" + (int)(numDaysLate * PER_DAY_LATE_PENALTY * 100) + "%"; + notes += " " + numDaysLate + " days late. -" + (int)(numDaysLate * PER_DAY_LATE_PENALTY * 100) + "%"; ZonedDateTime handInDate = ScorerHelper.getHandInDateZoned(netId); Submission.VerifiedStatus verifiedStatus; @@ -290,8 +394,7 @@ private Submission saveResults(Rubric rubric, CommitVerificationResult commitVer notes += "Commit history approved with a penalty of %d%%".formatted(commitVerificationResult.penaltyPct()); } - SubmissionDao submissionDao = DaoService.getSubmissionDao(); - Submission submission = new Submission( + return new Submission( netId, gradingContext.repoUrl(), headHash, @@ -305,17 +408,18 @@ private Submission saveResults(Rubric rubric, CommitVerificationResult commitVer verifiedStatus, null ); - - submissionDao.insertSubmission(submission); - return submission; } private void sendToCanvas(int userId, int assignmentNum, CanvasRubricAssessment assessment, String notes) throws GradingException { + sendToCanvas(userId, assignmentNum, assessment, notes, gradingContext.netId()); + } + + private static void sendToCanvas(int userId, int assignmentNum, CanvasRubricAssessment assessment, String notes, String netId) throws GradingException { try { CanvasService.getCanvasIntegration().submitGrade(userId, assignmentNum, assessment, notes); } catch (CanvasException e) { - LOGGER.error("Error submitting to canvas for user {}", gradingContext.netId(), e); + LOGGER.error("Error submitting to canvas for user {}", netId, e); throw new GradingException("Error contacting canvas to record scores"); } } diff --git a/src/main/java/edu/byu/cs/canvas/CanvasUtils.java b/src/main/java/edu/byu/cs/canvas/CanvasUtils.java index 167c1699b..40eed7573 100644 --- a/src/main/java/edu/byu/cs/canvas/CanvasUtils.java +++ b/src/main/java/edu/byu/cs/canvas/CanvasUtils.java @@ -42,6 +42,7 @@ public static Rubric decimalScoreToPoints(Phase phase, Rubric rubric) throws Gra } } + return new Rubric(rubricItems, rubric.passed(), rubric.notes()); } @@ -55,27 +56,26 @@ private static Rubric.Results convertPoints(Rubric.Results results, int points) ); } - public static CanvasRubricAssessment convertToAssessment(Rubric rubric, RubricConfig config, - float lateAdjustment, Phase phase) + public static CanvasRubricAssessment convertToAssessment(Rubric rubric, RubricConfig config, Phase phase) throws GradingException { Map items = new HashMap<>(); for(Rubric.RubricType type : Rubric.RubricType.values()) { - items.putAll(convertToCanvasFormat(rubric.items().get(type), lateAdjustment, phase, - config.items().get(type), type).items()); + items.putAll(convertToCanvasFormat(rubric.items().get(type), phase, config.items().get(type), type).items()); } return new CanvasRubricAssessment(items); } - private static CanvasRubricAssessment convertToCanvasFormat(Rubric.RubricItem rubricItem, - float lateAdjustment, Phase phase, RubricConfig.RubricConfigItem rubricConfigItem, - Rubric.RubricType rubricType) throws GradingException { + private static CanvasRubricAssessment convertToCanvasFormat( + Rubric.RubricItem rubricItem, Phase phase, RubricConfig.RubricConfigItem rubricConfigItem, + Rubric.RubricType rubricType + ) throws GradingException { Map items = new HashMap<>(); - if (rubricConfigItem != null && rubricConfigItem.points() > 0) { + if (rubricConfigItem != null && rubricItem != null) { Rubric.Results results = rubricItem.results(); items.put(PhaseUtils.getCanvasRubricId(rubricType, phase), - new CanvasRubricItem(results.notes(), results.score() * (1 - lateAdjustment))); + new CanvasRubricItem(results.notes(), results.score())); } return new CanvasRubricAssessment(items); } diff --git a/src/main/java/edu/byu/cs/canvas/model/CanvasRubricAssessment.java b/src/main/java/edu/byu/cs/canvas/model/CanvasRubricAssessment.java index 985fd0c75..66e084e65 100644 --- a/src/main/java/edu/byu/cs/canvas/model/CanvasRubricAssessment.java +++ b/src/main/java/edu/byu/cs/canvas/model/CanvasRubricAssessment.java @@ -4,7 +4,7 @@ import java.util.Map; public record CanvasRubricAssessment(Map items) { - public void addItem(String itemId, CanvasRubricItem rubricItem) { + public void insertItem(String itemId, CanvasRubricItem rubricItem) { items.put(itemId, rubricItem); } diff --git a/src/main/java/edu/byu/cs/controller/SubmissionController.java b/src/main/java/edu/byu/cs/controller/SubmissionController.java index 94b43dcce..40aa390e2 100644 --- a/src/main/java/edu/byu/cs/controller/SubmissionController.java +++ b/src/main/java/edu/byu/cs/controller/SubmissionController.java @@ -1,12 +1,12 @@ package edu.byu.cs.controller; -import edu.byu.cs.autograder.Grader; -import edu.byu.cs.autograder.GradingException; -import edu.byu.cs.autograder.GradingObserver; -import edu.byu.cs.autograder.TrafficController; +import edu.byu.cs.autograder.*; +import edu.byu.cs.autograder.git.CommitVerificationConfig; +import edu.byu.cs.autograder.score.Scorer; import edu.byu.cs.canvas.CanvasException; import edu.byu.cs.canvas.CanvasIntegration; import edu.byu.cs.canvas.CanvasService; +import edu.byu.cs.controller.netmodel.ApprovalRequest; import edu.byu.cs.controller.netmodel.GradeRequest; import edu.byu.cs.dataAccess.*; import edu.byu.cs.model.*; @@ -79,7 +79,7 @@ private static Boolean getSubmissionsEnabledConfig() { LOGGER.info("Admin {} submitted phase {} on repo {} for test grading", user.netId(), request.phase(), request.repoUrl()); - + DaoService.getSubmissionDao().removeSubmissionsByNetId(user.netId(), 3); startGrader(user.netId(), request.phase(), request.repoUrl(), true); @@ -305,23 +305,17 @@ public static Submission getMostRecentSubmission(String netId, Phase phase) thro }; public static final Route approveSubmissionPost = (req, res) -> { - // FIXME: These are only provided as reasonable guesses, and as a starting point. - // This may not be the way we want to actually go with this end-point, - // the only reflect the data that will need to be transferred. - String studentNetId = req.params(":studentNetId"); - Phase phase = Phase.valueOf(req.params(":phase")); - Float approvedScore = Float.valueOf(req.params(":approvedScore")); - Integer penaltyPct = Integer.valueOf(req.params(":penaltyPct")); - String approvingNetId = req.params(":approvingNetId"); - - // FIXME: Validate that all of the parameters were received as valid, non-empty types. - // Note that the `approvedScore` field can be optionally `null`. - - approveSubmission(studentNetId, phase, approvingNetId, penaltyPct); -// // TODO: Lookup a submission by phase, netId, and headHash to pass in as `targetSubmission` -// approveSubmission(studentNetId, phase, approvingNetId, penaltyPct, approvedScore, null); - - // FIXME: Consider returning more interesting or hepful data. + User adminUser = req.session().attribute("user"); + + ApprovalRequest request = Serializer.deserialize(req.body(), ApprovalRequest.class); + + int penalty = 0; + if (request.penalize()) { + //TODO: Put somewhere better/more configurable + penalty = 10; + } + + approveSubmission(request.netId(), request.phase(), adminUser.netId(), penalty); return "{}"; }; @@ -474,7 +468,7 @@ public static void reRunSubmissionsInQueue() throws IOException, DataAccessExcep */ public static void approveSubmission( @NonNull String studentNetId, @NonNull Phase phase, @NonNull String approverNetId, @NonNull Integer penaltyPct) - throws DataAccessException { + throws DataAccessException, GradingException { approveSubmission(studentNetId, phase, approverNetId, penaltyPct, null, null); } /** @@ -487,11 +481,15 @@ public static void approveSubmission( * @param approverNetId Identifies the TA or professor approving the score * @param penaltyPct The penalty applied for the reduction. * This should already be reflected in the `approvedScore` if present. - * @param approvedScore

The final score that should go in the grade-book.

- *

If `null`, we'll apply the penalty to the highest score for any submission in the phase.

+ *

This will be used to reduce all other submissions, + * but has no effect in the grade-book when approvedScore is provided.

+ * @param approvedScore

The final score that should go in the grade-book. + * Submissions in the database will not be affected by this value.

+ *

If `null`, we'll determine this value by applying the penalty to + * the highest score for any submission in the phase.

*

Provided so that a TA can approve an arbitrary (highest score) * submission with a penalty instead of any other fixed rule.

- * @param targetSubmission Required when `approvedScored` is passed in. + * @param submissionToUse Required when `approvedScored` is passed in. * Provides a submission which will be used to overwrite the existing score in the grade-book. * If a full {@link Submission} object is not available, the {@link Rubric} is only required field in it. */ @@ -501,8 +499,12 @@ public static void approveSubmission( @NonNull String approverNetId, @NonNull Integer penaltyPct, @Nullable Float approvedScore, - @Nullable Submission targetSubmission - ) throws DataAccessException { + @Nullable Submission submissionToUse + ) throws DataAccessException, GradingException { + if (approvedScore != null) { + throw new IllegalArgumentException("Passing in non-null approvedScore is not yet implemented."); + } + // Validate params if (studentNetId == null || phase == null || approverNetId == null || penaltyPct == null) { throw new IllegalArgumentException("All of studentNetId, approverNetId, and penaltyPct must not be null."); @@ -516,45 +518,109 @@ public static void approveSubmission( // Read in data SubmissionDao submissionDao = DaoService.getSubmissionDao(); + assertSubmissionUnapproved(submissionDao, studentNetId, phase); + Submission withheldSubmission = determineSubmissionForConsideration(submissionDao, submissionToUse, studentNetId, phase); + + // Modify values in our database first + int submissionsAffected = modifySubmissionEntriesInDatabase(submissionDao, withheldSubmission, approverNetId, penaltyPct); + + // Send score to Grade-book + if (approvedScore == null) { + approvedScore = SubmissionHelper.prepareModifiedScore(withheldSubmission.score(), penaltyPct); + } + String gitCommitsComment = "Submission initially blocked due to low commits. Submission approved by admin " + approverNetId; + sendScoreToCanvas(withheldSubmission, penaltyPct, gitCommitsComment); + + // Done + LOGGER.info("Approved submission for %s on phase %s with score %f. Approval by %s. Affected %d submissions." + .formatted(studentNetId, phase.name(), approvedScore, approverNetId, submissionsAffected)); + } + + private static void assertSubmissionUnapproved(SubmissionDao submissionDao, String studentNetId, Phase phase) throws DataAccessException { Submission withheldSubmission = submissionDao.getFirstPassingSubmission(studentNetId, phase); + if (withheldSubmission.isApproved()) { + throw new RuntimeException(studentNetId + " needs no approval for phase " + phase); + } + } + + private static Submission determineSubmissionForConsideration( + SubmissionDao submissionDao, Submission providedValue, String studentNetId, Phase phase) + throws DataAccessException, GradingException { + if (providedValue != null) return providedValue; + + Submission submission = submissionDao.getBestSubmissionForPhase(studentNetId, phase); + if (submission == null) { + throw new GradingException("No submission was provided nor found for phase " + phase + " with user " + studentNetId); + } + return submission; + } + + private static int modifySubmissionEntriesInDatabase( + SubmissionDao submissionDao, Submission withheldSubmission, String approvingNetId, int penaltyPct) + throws DataAccessException { - // Update Submissions Float originalScore = withheldSubmission.score(); Instant approvedTimestamp = Instant.now(); Submission.ScoreVerification scoreVerification = new Submission.ScoreVerification( originalScore, - approverNetId, + approvingNetId, approvedTimestamp, penaltyPct); - int submissionsAffected = SubmissionHelper.approveWithheldSubmissions(submissionDao, studentNetId, phase, scoreVerification); + int submissionsAffected = SubmissionHelper.approveWithheldSubmissions( + submissionDao, + withheldSubmission.netId(), + withheldSubmission.phase(), + scoreVerification); if (submissionsAffected < 1) { LOGGER.warn("Approving submissions did not affect any submissions. Something probably went wrong."); } + return submissionsAffected; + } - // Determine approvedScore - if (approvedScore == null) { - float bestScoreForPhase = submissionDao.getBestScoreForPhase(studentNetId, phase); - if (bestScoreForPhase < 0.0f) { - throw new RuntimeException("Cannot determine best score for phase without any submissions in the phase."); - } - approvedScore = SubmissionHelper.prepareModifiedScore(bestScoreForPhase, scoreVerification); - } - if (approvedScore <= 0.0f) { - throw new RuntimeException("Cannot set grade without a positive approvedScore!"); + /** + * Submits the approvedScore to Canvas by modifying the GIT_COMMITS rubric item. + *
+ * In this context, the withheldSubmission isn't strictly necessary, + * but it is used to transfer several fields and values that would need to be transferred individually + * without it. The most important of these is the field, which will be used to calculate the penalty. + * + * @param withheldSubmission The baseline submission to approve + * @param penaltyPct The percentage that should be reduced from the score for GIT_COMMITS. + * @param commitPenaltyComment The comment that should be associated with the GIT_COMMITS rubric, if any. + * @throws DataAccessException When the database cannot be accessed. + * @throws GradingException When pre-conditions are not met. + */ + private static void sendScoreToCanvas(Submission withheldSubmission, int penaltyPct, String commitPenaltyComment) throws DataAccessException, GradingException { + // Prepare and assert arguments + if (withheldSubmission == null) { + throw new IllegalArgumentException("Withheld submission cannot be null"); } + Scorer scorer = getScorer(withheldSubmission); + scorer.attemptSendToCanvas(withheldSubmission.rubric(), penaltyPct, commitPenaltyComment, true); + } - // Update grade-book - CanvasIntegration canvasIntegration = CanvasService.getCanvasIntegration(); - // FIXME: Store `approvedScore` in the Grade-book - // canvasIntegration.submitGrade(studentNetId, approvedScore, ); - if (true) { - throw new RuntimeException("ApproveSubmission not implemented!"); // TODO: Finish implementing method + /** + * Constructs an instance of {@link Scorer} using the limited data available in a {@link Submission}. + * + * @param submission A submission containing context to extract. + * @return A constructed Scorer instance. + */ + private static Scorer getScorer(Submission submission) { + String studentNetId = submission.netId(); + Phase phase = submission.phase(); + if (studentNetId == null) { + throw new IllegalArgumentException("Student net ID cannot be null"); + } + if (phase == null) { + throw new IllegalArgumentException("Phase cannot be null"); } - // Done - LOGGER.info("Approved submission for %s on phase %s with score %f. Approval by %s. Affected %d submissions." - .formatted(studentNetId, phase.name(), approvedScore, approverNetId, submissionsAffected)); + return new Scorer(new GradingContext( + studentNetId, phase, null, null, null, null, + new CommitVerificationConfig(0, 0, 0, 0, 0), + null, submission.admin() + )); } } diff --git a/src/main/java/edu/byu/cs/controller/netmodel/ApprovalRequest.java b/src/main/java/edu/byu/cs/controller/netmodel/ApprovalRequest.java new file mode 100644 index 000000000..7b24c308e --- /dev/null +++ b/src/main/java/edu/byu/cs/controller/netmodel/ApprovalRequest.java @@ -0,0 +1,6 @@ +package edu.byu.cs.controller.netmodel; + +import edu.byu.cs.model.Phase; + +public record ApprovalRequest(String netId, Phase phase, boolean penalize) { +} diff --git a/src/main/java/edu/byu/cs/dataAccess/SubmissionDao.java b/src/main/java/edu/byu/cs/dataAccess/SubmissionDao.java index 548b757fa..6f3a1b610 100644 --- a/src/main/java/edu/byu/cs/dataAccess/SubmissionDao.java +++ b/src/main/java/edu/byu/cs/dataAccess/SubmissionDao.java @@ -68,21 +68,14 @@ public interface SubmissionDao { Submission getFirstPassingSubmission(String netId, Phase phase) throws DataAccessException; /** - * Retrieves the highest score of a student's submissions for a phase. + * Retrieves the highest scoring submission of a student's submissions for a phase. *
- * A value of less than zero will be returned if there are no submissions. - * Here's a summary: - *
    - *
  • -1.0f means that no submissions exist for the student on the given phase
  • - *
  • 0.0f means that all submissions have a score of zero
  • - *
  • > 0.0f means that the student has received a score on this phase
  • - *
* * @param netId Representing a student. * @param phase Representing a phase to submit - * @return The score (points) as a float, or -1.0 if no submissions exist. + * @return The submission with the highest score for the phase, or null if no submissions exist. */ - float getBestScoreForPhase(String netId, Phase phase) throws DataAccessException; + Submission getBestSubmissionForPhase(String netId, Phase phase) throws DataAccessException; /** * Gets all submissions that `passed` the grading for any phase. diff --git a/src/main/java/edu/byu/cs/dataAccess/SubmissionHelper.java b/src/main/java/edu/byu/cs/dataAccess/SubmissionHelper.java index 0e7be76b7..0b061f4d3 100644 --- a/src/main/java/edu/byu/cs/dataAccess/SubmissionHelper.java +++ b/src/main/java/edu/byu/cs/dataAccess/SubmissionHelper.java @@ -3,6 +3,8 @@ import edu.byu.cs.model.Phase; import edu.byu.cs.model.Submission; +import java.util.HashSet; + public class SubmissionHelper { /** @@ -27,8 +29,8 @@ public static int approveWithheldSubmissions( throws DataAccessException { int affected = 0; - var phaseSubmissions = submissionDao.getSubmissionsForPhase(studentNetId, phase); - Float modifiedScore; + var phaseSubmissions = new HashSet<>(submissionDao.getSubmissionsForPhase(studentNetId, phase)); + float modifiedScore; Submission.ScoreVerification individualVerification; for (var submission : phaseSubmissions) { if (!submission.passed()) continue; diff --git a/src/main/java/edu/byu/cs/dataAccess/memory/SubmissionMemoryDao.java b/src/main/java/edu/byu/cs/dataAccess/memory/SubmissionMemoryDao.java index adf0227c2..9af0e3e3c 100644 --- a/src/main/java/edu/byu/cs/dataAccess/memory/SubmissionMemoryDao.java +++ b/src/main/java/edu/byu/cs/dataAccess/memory/SubmissionMemoryDao.java @@ -95,13 +95,14 @@ public Submission getFirstPassingSubmission(String netId, Phase phase) { } @Override - public float getBestScoreForPhase(String netId, Phase phase) { + public Submission getBestSubmissionForPhase(String netId, Phase phase) { Collection submissions = getSubmissionsForPhase(netId, phase); - float bestScore = -1.0f; // This implementation **can** differentiate between submissions and no submissions + Submission bestSubmission = null; for (Submission s : submissions) { - if (s.score() > bestScore) { bestScore = s.score(); } + if (bestSubmission == null) { bestSubmission = s; } + else if (s.score() > bestSubmission.score()) { bestSubmission = s; } } - return bestScore; + return bestSubmission; } @Override diff --git a/src/main/java/edu/byu/cs/dataAccess/sql/SubmissionSqlDao.java b/src/main/java/edu/byu/cs/dataAccess/sql/SubmissionSqlDao.java index e9433ace3..715ecee5f 100644 --- a/src/main/java/edu/byu/cs/dataAccess/sql/SubmissionSqlDao.java +++ b/src/main/java/edu/byu/cs/dataAccess/sql/SubmissionSqlDao.java @@ -8,6 +8,8 @@ import edu.byu.cs.model.Phase; import edu.byu.cs.model.Rubric; import edu.byu.cs.model.Submission; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import edu.byu.cs.util.Serializer; import java.sql.ResultSet; @@ -20,6 +22,8 @@ import java.util.List; public class SubmissionSqlDao implements SubmissionDao { + + private static final Logger LOGGER = LoggerFactory.getLogger(SubmissionSqlDao.class); private static final ColumnDefinition[] COLUMN_DEFINITIONS = { new ColumnDefinition("net_id", Submission::netId), new ColumnDefinition("repo_url", Submission::repoUrl), @@ -171,23 +175,19 @@ public Submission getFirstPassingSubmission(String netId, Phase phase) throws Da } @Override - public float getBestScoreForPhase(String netId, Phase phase) throws DataAccessException { - return sqlReader.executeQuery( + public Submission getBestSubmissionForPhase(String netId, Phase phase) throws DataAccessException { + var submissions = sqlReader.executeQuery( """ - SELECT max(score) as highestScore - FROM %s - WHERE net_id = ? AND phase = ? - """.formatted(sqlReader.getTableName()), + WHERE net_id = ? AND phase = ? AND passed = 1 + ORDER BY score DESC + LIMIT 1 + """, ps -> { ps.setString(1, netId); ps.setString(2, phase.toString()); - }, - rs -> { - rs.next(); - float highestScore = rs.getFloat("highestScore"); - return rs.wasNull() ? -1.0f : highestScore; } ); + return sqlReader.expectOneItem(submissions); } @Override @@ -224,14 +224,15 @@ public void manuallyApproveSubmission(Submission submission, Float newScore, Sub ps.setString(3, phase); } ); - if (matchingSubmissions.size() != 1) { - throw new ItemNotFoundException( - "Submission could not be identified. Found %s matches. Searched with the following information:\n " - .formatted(matchingSubmissions.size()) - + " net_id: %s\n phase: %s\n head_hash: %s\n " - .formatted(netId, headHash, phase) - + matchingSubmissions.toString() + if (matchingSubmissions.isEmpty()) { + throw new ItemNotFoundException("Submission could not be located in database. Cannot edit it." + + getSubmissionIdentDebugInfo(netId, headHash, phase, matchingSubmissions)); + } else if (matchingSubmissions.size() > 1) { + LOGGER.warn("Expected to edit 1 submission, but found %s submissions with the same identifying information.".formatted(matchingSubmissions.size() + + getSubmissionIdentDebugInfo(netId, headHash, phase, null)) ); + // CONSIDER: Including the `matchingSubmissions` value while debugging this method. + // It is not included in production code to keep the logs clean. } // Then update it @@ -254,4 +255,16 @@ public void manuallyApproveSubmission(Submission submission, Float newScore, Sub ); } + private String getSubmissionIdentDebugInfo(String netId, String headHash, String phase, + Collection matchingSubmissions) { + String debugInfo = "\n\nSearched with the following information:"; + debugInfo += "\n net_id: %s\n phase: %s\n head_hash: %s".formatted(netId, phase, headHash); + if (matchingSubmissions != null && !matchingSubmissions.isEmpty()) { + // CONSIDER: Printing a summary of each submission on its own numbered line + debugInfo += "\n The returned submissions are: " + matchingSubmissions.toString(); + } + + return debugInfo; + } + } diff --git a/src/main/java/edu/byu/cs/model/Phase.java b/src/main/java/edu/byu/cs/model/Phase.java index 180aab2a2..9749f0225 100644 --- a/src/main/java/edu/byu/cs/model/Phase.java +++ b/src/main/java/edu/byu/cs/model/Phase.java @@ -1,5 +1,8 @@ package edu.byu.cs.model; +/** + * Represent the phases that can be graded. + */ public enum Phase { Phase0, Phase1, @@ -7,5 +10,18 @@ public enum Phase { Phase4, Phase5, Phase6, - Quality + /** + * This special phase is never graded for credit, but + * allows students to receive the code-quality checking + * feedback from the system without evaluating the project. + */ + Quality, + /** + * This special phase is never to be graded, but exists to + * serve as the global {@link RubricConfig} for all phases. + *
+ * Would be named GitCommits, but the SQL table + * requires the phase to be no more than 9 chars. + */ + Commits } diff --git a/src/main/java/edu/byu/cs/model/Rubric.java b/src/main/java/edu/byu/cs/model/Rubric.java index 8e812412a..29efceccd 100644 --- a/src/main/java/edu/byu/cs/model/Rubric.java +++ b/src/main/java/edu/byu/cs/model/Rubric.java @@ -38,10 +38,10 @@ public float getTotalPoints() { * @param criteria The criteria of the rubric item */ public record RubricItem( - String category, Results results, String criteria - - ) { - } + String category, + Results results, + String criteria + ) { } /** * Represents the results of a rubric item. textResults or testResults may be null, but not both @@ -58,8 +58,7 @@ public record Results( Integer possiblePoints, TestAnalysis testResults, String textResults - ) { - } + ) { } public enum RubricType { PASSOFF_TESTS, diff --git a/src/main/java/edu/byu/cs/model/RubricConfig.java b/src/main/java/edu/byu/cs/model/RubricConfig.java index c80d84f47..c0d7f3201 100644 --- a/src/main/java/edu/byu/cs/model/RubricConfig.java +++ b/src/main/java/edu/byu/cs/model/RubricConfig.java @@ -2,15 +2,8 @@ import java.util.EnumMap; -public record RubricConfig( +public record RubricConfig(Phase phase, EnumMap items) { + + public record RubricConfigItem(String category, String criteria, int points) {} - Phase phase, - EnumMap items -) { - public record RubricConfigItem( - String category, - String criteria, - int points - ) { - } } diff --git a/src/main/java/edu/byu/cs/model/Submission.java b/src/main/java/edu/byu/cs/model/Submission.java index c0deefa9c..5babf9f49 100644 --- a/src/main/java/edu/byu/cs/model/Submission.java +++ b/src/main/java/edu/byu/cs/model/Submission.java @@ -17,13 +17,16 @@ * @param phase The phase being graded in this submission. * @param passed Signifies that the code passed all the grading tests. * Does NOT signify that the score was approved. - * @param score

The final score assigned to this submission (in points) that will + * @param score

The final score assigned to this submission (as a percentage [0-1]) that will * be sent to the grade-book, including penalties and extra credit.

*

This field will be updated if the score changes because of * additional penalties or manual corrections; however, this is * not the canonical source of truth.

- *

This serves as convenient reference while within the AutoGrader - * for many reasons, but the real source of truth is the grade-book.

+ *

These system deals with scores as values between 0 and 1. + * These scores are only converted into points as they are sent + * to an external source.

+ *

While the AutoGrader is storing scores and updating them, + * the real source of truth is the grade-book.

* @param notes Additional notes displayed to the user. * These usually represent the status of their score, or * provide remarks about why a passing score was not given. @@ -86,6 +89,13 @@ public boolean isApproved() { } } + public boolean isApproved() { + if (verifiedStatus == null) { + return true; // Old submissions without this field are assumed to be approved + } + return verifiedStatus.isApproved(); + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -128,4 +138,21 @@ public static String serializeVerifiedStatus(@NonNull Submission submission) { public static String serializeVerifiedStatus(@Nullable VerifiedStatus verifiedStatus) { return verifiedStatus == null ? null : verifiedStatus.name(); } + + public Submission replaceRubric(Rubric rubric) { + return new Submission( + this.netId, + this.repoUrl, + this.headHash, + this.timestamp, + this.phase, + this.passed, + this.score, + this.notes, + rubric, + this.admin, + this.verifiedStatus, + this.verification + ); + } } diff --git a/src/main/java/edu/byu/cs/server/Server.java b/src/main/java/edu/byu/cs/server/Server.java index 559a9bf2b..7bf3df684 100644 --- a/src/main/java/edu/byu/cs/server/Server.java +++ b/src/main/java/edu/byu/cs/server/Server.java @@ -78,13 +78,15 @@ public static int setupEndpoints(int port) { post("/submit", adminRepoSubmitPost); path("/submissions", () -> { + post("/approve", approveSubmissionPost); + get("/latest", latestSubmissionsGet); get("/latest/:count", latestSubmissionsGet); get("/active", submissionsActiveGet); - get("/student/:netID", studentSubmissionsGet); + get("/student/:netId", studentSubmissionsGet); post("/rerun", submissionsReRunPost); }); diff --git a/src/main/java/edu/byu/cs/util/PhaseUtils.java b/src/main/java/edu/byu/cs/util/PhaseUtils.java index 958261330..64b415f6c 100644 --- a/src/main/java/edu/byu/cs/util/PhaseUtils.java +++ b/src/main/java/edu/byu/cs/util/PhaseUtils.java @@ -27,7 +27,7 @@ public class PhaseUtils { */ public static Phase getPreviousPhase(Phase phase) { return switch (phase) { - case Phase0, Quality -> null; + case Phase0, Quality, Commits -> null; case Phase1 -> Phase.Phase0; case Phase3 -> Phase.Phase1; case Phase4 -> Phase.Phase3; @@ -51,6 +51,7 @@ public static String getPhaseAsString(Phase phase) { case Phase5 -> "5"; case Phase6 -> "6"; case Quality -> "Quality"; + case Commits -> "GitCommits"; }; } @@ -68,7 +69,7 @@ public static int getPhaseAssignmentNumber(Phase phase) { case Phase4 -> PHASE4_ASSIGNMENT_NUMBER; case Phase5 -> PHASE5_ASSIGNMENT_NUMBER; case Phase6 -> PHASE6_ASSIGNMENT_NUMBER; - case Quality -> 0; + case Quality, Commits -> 0; }; } @@ -77,13 +78,13 @@ public static Set passoffPackagesToTest(Phase phase) throws GradingExcep case Phase0 -> Set.of("passoff.chess", "passoff.chess.piece"); case Phase1 -> Set.of("passoff.chess.game", "passoff.chess.extracredit"); case Phase3, Phase4, Phase6 -> Set.of("passoff.server"); - case Phase5, Quality -> throw new GradingException("No passoff tests for this phase"); + case Phase5, Quality, Commits -> throw new GradingException("No passoff tests for this phase"); }; } public static Set unitTestPackagesToTest(Phase phase) throws GradingException { return switch (phase) { - case Phase0, Phase1, Phase6, Quality -> throw new GradingException("No unit tests for this phase"); + case Phase0, Phase1, Phase6, Quality, Commits -> throw new GradingException("No unit tests for this phase"); case Phase3 -> Set.of("service"); case Phase4 -> Set.of("dataaccess"); case Phase5 -> Set.of("client"); @@ -92,7 +93,7 @@ public static Set unitTestPackagesToTest(Phase phase) throws GradingExce public static Set unitTestPackagePaths(Phase phase) { return switch (phase) { - case Phase0, Phase6, Quality -> new HashSet<>(); + case Phase0, Phase6, Quality, Commits -> new HashSet<>(); case Phase1 -> Set.of("shared/src/test/java/passoff/chess/game"); case Phase3 -> Set.of("server/src/test/java/service", "server/src/test/java/passoff/server"); case Phase4 -> Set.of("server/src/test/java/dataaccess"); @@ -102,7 +103,7 @@ public static Set unitTestPackagePaths(Phase phase) { public static String unitTestCodeUnderTest(Phase phase) throws GradingException { return switch (phase) { - case Phase0, Phase1, Phase6, Quality -> throw new GradingException("No unit tests for this phase"); + case Phase0, Phase1, Phase6, Quality, Commits -> throw new GradingException("No unit tests for this phase"); case Phase3 -> "service"; case Phase4 -> "dao"; case Phase5 -> "server facade"; @@ -111,7 +112,7 @@ public static String unitTestCodeUnderTest(Phase phase) throws GradingException public static int minUnitTests(Phase phase) throws GradingException { return switch (phase) { - case Phase0, Phase1, Phase6, Quality -> throw new GradingException("No unit tests for this phase"); + case Phase0, Phase1, Phase6, Quality, Commits -> throw new GradingException("No unit tests for this phase"); case Phase3 -> 13; case Phase4 -> 18; case Phase5 -> 12; @@ -173,14 +174,14 @@ public static String getModuleUnderTest(Phase phase) { case Phase0, Phase1 -> "shared"; case Phase3, Phase4, Phase6 -> "server"; case Phase5 -> "client"; - case Quality -> null; + case Quality, Commits -> null; }; } public static boolean isPhaseGraded(Phase phase) { return switch (phase) { case Phase0, Phase1, Phase3, Phase4, Phase5, Phase6 -> true; - case Quality -> false; + case Quality, Commits -> false; }; } @@ -193,7 +194,7 @@ public static boolean isPhaseGraded(Phase phase) { public static boolean isPassoffRequired(Phase phase) { return switch (phase) { case Phase0, Phase1, Phase3, Phase4 -> true; - case Phase5, Phase6, Quality -> false; + case Phase5, Phase6, Quality, Commits -> false; }; } @@ -228,7 +229,7 @@ public static String getPassoffPackagePath(Phase phase) { case Phase3 -> "phases/phase3.passoff.server"; case Phase4 -> "phases/phase4.passoff.server"; case Phase6 -> "phases/phase6.passoff.server"; - case Phase5, Quality -> null; + case Phase5, Quality, Commits -> null; }; } diff --git a/src/main/resources/frontend/src/components/Panel.vue b/src/main/resources/frontend/src/components/Panel.vue new file mode 100644 index 000000000..8667a6a1b --- /dev/null +++ b/src/main/resources/frontend/src/components/Panel.vue @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/src/main/resources/frontend/src/services/adminService.ts b/src/main/resources/frontend/src/services/adminService.ts index 690cea42a..fee25c0c0 100755 --- a/src/main/resources/frontend/src/services/adminService.ts +++ b/src/main/resources/frontend/src/services/adminService.ts @@ -1,5 +1,5 @@ import {useAppConfigStore} from "@/stores/appConfig"; -import type {CanvasSection, Submission, User} from "@/types/types"; +import type {CanvasSection, Phase, Submission, User } from '@/types/types' import type {Option} from "@/views/AdminView/Analytics.vue"; export const usersGet = async (): Promise => { @@ -15,9 +15,9 @@ export const usersGet = async (): Promise => { } } -export const submissionsForUserGet = async (netID: string): Promise => { +export const submissionsForUserGet = async (netId: string): Promise => { try { - const response = await fetch(useAppConfigStore().backendUrl + '/api/admin/submissions/student/' + netID, { + const response = await fetch(useAppConfigStore().backendUrl + '/api/admin/submissions/student/' + netId, { method: 'GET', credentials: 'include' }); @@ -28,6 +28,26 @@ export const submissionsForUserGet = async (netID: string): Promise { + const response = await fetch(useAppConfigStore().backendUrl + '/api/admin/submissions/approve', { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + netId, + phase, + penalize, + }) + }); + + if (!response.ok) { + console.error(response); + throw new Error(await response.text()); + } +} + interface UserPatch { netId: string, firstName?: string, diff --git a/src/main/resources/frontend/src/views/AdminView/QueueStatus.vue b/src/main/resources/frontend/src/views/AdminView/QueueStatus.vue index 67c69889b..7dfacb643 100644 --- a/src/main/resources/frontend/src/views/AdminView/QueueStatus.vue +++ b/src/main/resources/frontend/src/views/AdminView/QueueStatus.vue @@ -2,6 +2,7 @@ import {onMounted, onUnmounted, reactive, ref} from "vue"; import {getQueueStatus} from "@/services/adminService"; import { reRunSubmissionsPost } from '@/services/submissionService' +import Panel from "@/components/Panel.vue"; const currentlyGrading = ref([]); const inQueue = ref([]); @@ -37,7 +38,7 @@ const reRunQueue = async () => { diff --git a/src/main/resources/frontend/src/views/AdminView/StudentsView.vue b/src/main/resources/frontend/src/views/AdminView/StudentsView.vue index 7240c16b2..e895632d7 100644 --- a/src/main/resources/frontend/src/views/AdminView/StudentsView.vue +++ b/src/main/resources/frontend/src/views/AdminView/StudentsView.vue @@ -9,6 +9,7 @@ import PopUp from "@/components/PopUp.vue"; import type {User} from "@/types/types"; import StudentInfo from "@/views/AdminView/StudentInfo.vue"; import {renderRepoLinkCell, standardColSettings} from "@/utils/tableUtils"; +import Panel from "@/components/Panel.vue"; const selectedStudent = ref(null); let studentData: User[] = []; @@ -34,8 +35,8 @@ onMounted(async () => { }) const columnDefs = reactive([ - { headerName: "Student Name", field: "name", flex: 2, onCellClicked: cellClickHandler }, - { headerName: "BYU netID", field: "netId", flex: 1, onCellClicked: cellClickHandler }, + { headerName: "Student Name", field: "name", flex: 2, minWidth: 150, onCellClicked: cellClickHandler }, + { headerName: "BYU netID", field: "netId", flex: 1, minWidth: 75, onCellClicked: cellClickHandler }, { headerName: "Github Repo URL", field: "repoUrl", flex: 5, sortable: false, cellRenderer: renderRepoLinkCell } ]) const rowData = reactive({ @@ -49,7 +50,7 @@ const activateTestStudentMode = async () => {