Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into 430-fullstack-enhance…
Browse files Browse the repository at this point in the history
…-late-penalty-applied-presentation-experience
  • Loading branch information
19mdavenport committed Oct 21, 2024
2 parents a6935de + 22ac932 commit c0a1266
Show file tree
Hide file tree
Showing 30 changed files with 724 additions and 205 deletions.
42 changes: 7 additions & 35 deletions src/main/java/edu/byu/cs/autograder/Grader.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
import edu.byu.cs.properties.ApplicationProperties;
import edu.byu.cs.util.FileUtils;
import edu.byu.cs.util.PhaseUtils;
import edu.byu.cs.util.RepoUrlValidator;
import org.eclipse.jgit.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -59,15 +61,15 @@ public class Grader implements Runnable {
public Grader(String repoUrl, String netId, GradingObserver observer, Phase phase, boolean admin) throws IOException, GradingException {
// Init files
if (!admin) {
repoUrl = cleanRepoUrl(repoUrl);
repoUrl = RepoUrlValidator.clean(repoUrl);
}
String phasesPath = new File("./phases").getCanonicalPath();
long salt = Instant.now().getEpochSecond();
String stagePath = new File("./tmp-" + repoUrl.hashCode() + "-" + salt).getCanonicalPath();
File stageRepo = new File(stagePath, "repo");

// Init Grading Context
CommitVerificationConfig cvConfig = PhaseUtils.requiresTAPassoffForCommits(phase) ?
CommitVerificationConfig cvConfig = PhaseUtils.shouldVerifyCommits(phase) ?
PhaseUtils.verificationConfig(phase) : null;
this.observer = observer;
this.gradingContext = new GradingContext(
Expand All @@ -94,7 +96,7 @@ public void run() {
}

RubricConfig rubricConfig = DaoService.getRubricConfigDao().getRubricConfig(gradingContext.phase());
Rubric rubric = evaluateProject(RUN_COMPILATION ? rubricConfig : null);
Rubric rubric = evaluateProject(RUN_COMPILATION ? rubricConfig : null, commitVerificationResult);

Submission submission = new Scorer(gradingContext).score(rubric, commitVerificationResult);
DaoService.getSubmissionDao().insertSubmission(submission);
Expand All @@ -111,7 +113,7 @@ public void run() {
}
}

private Rubric evaluateProject(RubricConfig rubricConfig) throws GradingException, DataAccessException {
private Rubric evaluateProject(RubricConfig rubricConfig, CommitVerificationResult commitVerificationResult) throws GradingException, DataAccessException {
EnumMap<Rubric.RubricType, Rubric.RubricItem> rubricItems = new EnumMap<>(Rubric.RubricType.class);
if (rubricConfig == null) {
return new Rubric(new EnumMap<>(Rubric.RubricType.class), false, "No Rubric Config");
Expand All @@ -124,7 +126,7 @@ private Rubric evaluateProject(RubricConfig rubricConfig) throws GradingExceptio
case PASSOFF_TESTS -> new PassoffTestGrader(gradingContext).runTests();
case UNIT_TESTS -> new UnitTestGrader(gradingContext).runTests();
case QUALITY -> new QualityGrader(gradingContext).runQualityChecks();
case GITHUB_REPO -> new GitHubAssignmentGrader().grade();
case GITHUB_REPO -> new GitHubAssignmentGrader().grade(commitVerificationResult);
case GIT_COMMITS, GRADING_ISSUE -> null;
};
if (results != null) {
Expand All @@ -136,36 +138,6 @@ private Rubric evaluateProject(RubricConfig rubricConfig) throws GradingExceptio
return new Rubric(rubricItems, false, "");
}

/**
* Cleans the student's by removing trailing characters after the repo name,
* unless it ends in `.git`.
*
* @param repoUrl The student's repository URL.
* @return Cleaned URL with everything after the repo name stripped off.
* @throws GradingException Throws IOException if repoUrl does not follow expected format
*/
public static String cleanRepoUrl(String repoUrl) throws GradingException {
String[] regexPatterns = {
"https?://github\\.com/([^/?]+)/([^/?]+)", // https
"[email protected]:([^/]+)/([^/]+).git" // ssh
};
Pattern pattern;
Matcher matcher;
String githubUsername;
String repositoryName;
for (String regexPattern: regexPatterns) {
pattern = Pattern.compile(regexPattern);
matcher = pattern.matcher(repoUrl);
if (matcher.find()) {
githubUsername = matcher.group(1);
repositoryName = matcher.group(2);
return String.format("https://github.com/%s/%s", githubUsername, repositoryName);
}
}
throw new GradingException("Could not find github username and repository name given '" + repoUrl + "'.");
}


private void handleException(GradingException ge, CommitVerificationResult cvr) {
if(cvr == null) {
observer.notifyError(ge.getMessage());
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/edu/byu/cs/autograder/GradingException.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import java.util.EnumMap;
import java.util.Map;

public class GradingException extends Exception{
public class GradingException extends Exception {
private static final String CATEGORY = "Grading Issue";
private static final String CRITERIA = "An issue arose while grading this submission";

Expand Down
2 changes: 1 addition & 1 deletion src/main/java/edu/byu/cs/autograder/git/GitHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ public CommitVerificationResult verifyCommitHistory() {
* @return True if the commits should be verified; otherwise, false.
*/
private boolean shouldVerifyCommits() {
return !gradingContext.admin() && PhaseUtils.requiresTAPassoffForCommits(gradingContext.phase());
return !gradingContext.admin() && PhaseUtils.shouldVerifyCommits(gradingContext.phase());
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,20 @@
* then this grader will give them full points.
*/
public class GitHubAssignmentGrader {
private static final String SUCCESS_MESSAGE = "Successfully fetched your repository and verified commits.";
private static final String RESUBMIT_PROMPT = " Push another commit to your repository and re-request grading to receive points for this assignment.";
private static final String FAILURE_MESSAGE = "Successfully fetched your repository, but the number of commits is insufficient." + RESUBMIT_PROMPT;

public Rubric.Results grade() throws DataAccessException {
public Rubric.Results grade(CommitVerificationResult commitVerificationResult) throws DataAccessException {
RubricConfig rubricConfig = DaoService.getRubricConfigDao().getRubricConfig(Phase.GitHub);
RubricConfig.RubricConfigItem configItem = rubricConfig.items().get(Rubric.RubricType.GITHUB_REPO);
return new Rubric.Results("Successfully fetched your repository", 1f, configItem.points(), null, null);
}

if(commitVerificationResult.verified()) {
return new Rubric.Results(SUCCESS_MESSAGE, 1f, configItem.points(), null, null);
}
else {
return new Rubric.Results(FAILURE_MESSAGE, 0f, configItem.points(), null,
commitVerificationResult.failureMessage() + RESUBMIT_PROMPT);
}
}
}
16 changes: 9 additions & 7 deletions src/main/java/edu/byu/cs/autograder/score/Scorer.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.slf4j.LoggerFactory;

import java.time.ZonedDateTime;
import java.util.Collection;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.Map;
Expand Down Expand Up @@ -274,14 +275,15 @@ private static CanvasRubricAssessment constructCanvasRubricAssessment(Rubric rub
}

private boolean passed(Rubric rubric) {
if(!PhaseUtils.isPassoffRequired(gradingContext.phase())) {
return true;
Collection<Rubric.RubricType> requiredTypes = PhaseUtils.requiredRubricTypes(gradingContext.phase());
for(Rubric.RubricType type : requiredTypes) {
Rubric.RubricItem typedRubricItem = rubric.items().get(type);
if(typedRubricItem != null && typedRubricItem.results() != null &&
typedRubricItem.results().score() < typedRubricItem.results().possiblePoints()) {
return false;
}
}
Rubric.RubricItem passoffTestItem = rubric.items().get(Rubric.RubricType.PASSOFF_TESTS);
if (passoffTestItem == null || passoffTestItem.results() == null) {
return true;
}
return passoffTestItem.results().score() >= passoffTestItem.results().possiblePoints();
return true;
}

private Rubric applyLatePenalty(Rubric rubric, int daysLate) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import java.util.HashSet;
import java.util.Set;

public class PreviousPhasePassoffTestGrader extends TestGrader{
public class PreviousPhasePassoffTestGrader extends TestGrader {
private static final String ERROR_MESSAGE = "Failed previous phases' tests. Cannot pass off until previous tests pass.";

public PreviousPhasePassoffTestGrader(GradingContext gradingContext) {
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/edu/byu/cs/canvas/CanvasException.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package edu.byu.cs.canvas;

public class CanvasException extends Exception{
public class CanvasException extends Exception {

public CanvasException() {
super();
Expand Down
89 changes: 85 additions & 4 deletions src/main/java/edu/byu/cs/controller/ConfigController.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,18 @@
import org.slf4j.LoggerFactory;
import spark.Route;

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.Map;

import static spark.Spark.halt;

public class ConfigController {

private static final Logger LOGGER = LoggerFactory.getLogger(SubmissionController.class);
Expand All @@ -26,6 +34,10 @@ private static void logConfigChange(String changeMessage, String adminNetId) {
LOGGER.info("[CONFIG] Admin %s has %s".formatted(adminNetId, changeMessage));
}

private static void logAutomaticConfigChange(String changeMessage) {
LOGGER.info("[CONFIG] Automatic change: %s".formatted(changeMessage));
}

public static final Route getConfigAdmin = (req, res) -> {
try {
JsonObject response = getPrivateConfig();
Expand All @@ -45,12 +57,43 @@ private static void logConfigChange(String changeMessage, String adminNetId) {
return response;
};

/**
* Edits in place the passed in response object
*
* @param response the Json Object to add banner info to
* @throws DataAccessException if it screws up while getting into the database
*/
private static void addBannerConfig(JsonObject response) throws DataAccessException {
ConfigurationDao dao = DaoService.getConfigurationDao();
Instant bannerExpiration = dao.getConfiguration(ConfigurationDao.Configuration.BANNER_EXPIRATION, Instant.class);

if (bannerExpiration.isBefore(Instant.now())) { //Banner has expired
clearBannerConfig();
}

response.addProperty("bannerMessage", dao.getConfiguration(ConfigurationDao.Configuration.BANNER_MESSAGE, String.class));
response.addProperty("bannerLink", dao.getConfiguration(ConfigurationDao.Configuration.BANNER_LINK, String.class));
response.addProperty("bannerColor", dao.getConfiguration(ConfigurationDao.Configuration.BANNER_COLOR, String.class));
response.addProperty("bannerExpiration", bannerExpiration.toString());
}

private static void clearBannerConfig() throws DataAccessException {
ConfigurationDao dao = DaoService.getConfigurationDao();

dao.setConfiguration(ConfigurationDao.Configuration.BANNER_MESSAGE, "", String.class);
dao.setConfiguration(ConfigurationDao.Configuration.BANNER_LINK, "", String.class);
dao.setConfiguration(ConfigurationDao.Configuration.BANNER_COLOR, "", String.class);
dao.setConfiguration(ConfigurationDao.Configuration.BANNER_EXPIRATION, Instant.MAX, Instant.class);

logAutomaticConfigChange("Banner message has expired");
}

private static JsonObject getPublicConfig() throws DataAccessException {
ConfigurationDao dao = DaoService.getConfigurationDao();

JsonObject response = new JsonObject();

response.addProperty("bannerMessage", dao.getConfiguration(ConfigurationDao.Configuration.BANNER_MESSAGE, String.class));
addBannerConfig(response);
response.addProperty("phases", dao.getConfiguration(ConfigurationDao.Configuration.STUDENT_SUBMISSIONS_ENABLED, String.class));

return response;
Expand Down Expand Up @@ -106,22 +149,60 @@ private static JsonObject getPrivateConfig() throws DataAccessException {

public static final Route updateBannerMessage = (req, res) -> {
ConfigurationDao dao = DaoService.getConfigurationDao();
Gson gson = new Gson();

JsonObject jsonObject = gson.fromJson(req.body(), JsonObject.class);
String expirationString = gson.fromJson(jsonObject.get("bannerExpiration"), String.class);

Instant expirationTimestamp = Instant.MAX;
if (!expirationString.isEmpty()) {
try {
expirationTimestamp = getInstantFromUnzonedTime(expirationString);
} catch (Exception e) {
halt(400,"Incomplete timestamp. Send a full timestamp {YYYY-MM-DDTHH:MM:SS} or send none at all");
}
}

if (expirationTimestamp.isBefore(Instant.now())) {
halt(400, "You tried to set the banner to expire in the past");
}

String message = gson.fromJson(jsonObject.get("bannerMessage"), String.class);
String link = gson.fromJson(jsonObject.get("bannerLink"), String.class);
String color = gson.fromJson(jsonObject.get("bannerColor"), String.class);

// If they give us a color, and it's not long enough or is missing #
if (!color.isEmpty() && ((color.length() != 7) || !color.startsWith("#"))) {
halt(400, "Invalid hex color code. Must provide a hex code starting with a # symbol, followed by 6 hex digits");
}

JsonObject jsonObject = new Gson().fromJson(req.body(), JsonObject.class);
String message = new Gson().fromJson(jsonObject.get("bannerMessage"), String.class);
dao.setConfiguration(ConfigurationDao.Configuration.BANNER_MESSAGE, message, String.class);
dao.setConfiguration(ConfigurationDao.Configuration.BANNER_LINK, link, String.class);
dao.setConfiguration(ConfigurationDao.Configuration.BANNER_COLOR, color, String.class);
dao.setConfiguration(ConfigurationDao.Configuration.BANNER_EXPIRATION, expirationTimestamp, Instant.class);

User user = req.session().attribute("user");
if (message.isEmpty()) {
logConfigChange("cleared the banner message", user.netId());
} else {
logConfigChange("set the banner message to: '%s'".formatted(message), user.netId());
expirationString = !expirationString.isEmpty() ? expirationString : "never";
logConfigChange("set the banner message to: '%s' with link: {%s} to expire at %s".formatted(message, link, expirationString), user.netId());
}

res.status(200);
return "";
};

private static Instant getInstantFromUnzonedTime(String timestampString) throws DateTimeParseException {
ZoneId utahZone = ZoneId.of("America/Denver");

DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
LocalDateTime localDateTime = LocalDateTime.parse(timestampString, formatter);
ZonedDateTime utahTime = localDateTime.atZone(utahZone);

return utahTime.toInstant();
}

public static final Route updateCourseIdsPost = (req, res) -> {
SetCourseIdsRequest setCourseIdsRequest = new Gson().fromJson(req.body(), SetCourseIdsRequest.class);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
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.controller.netmodel.ApprovalRequest;
import edu.byu.cs.controller.netmodel.GradeRequest;
import edu.byu.cs.dataAccess.*;
Expand Down Expand Up @@ -113,7 +112,7 @@ private static boolean verifyHasNewCommits(User user, Phase phase) throws DataAc
String headHash;
try {
headHash = SubmissionUtils.getRemoteHeadHash(user.repoUrl());
} catch (Exception e) {
} catch (DataAccessException e) {
LOGGER.error("Error getting remote head hash", e);
halt(400, "Invalid repo url");
return false;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package edu.byu.cs.autograder;
package edu.byu.cs.controller;

import edu.byu.cs.controller.WebSocketController;
import edu.byu.cs.autograder.Grader;
import edu.byu.cs.dataAccess.DaoService;
import edu.byu.cs.dataAccess.DataAccessException;
import edu.byu.cs.model.QueueItem;
Expand Down
Loading

0 comments on commit c0a1266

Please sign in to comment.