Skip to content

Commit

Permalink
Merge pull request #478 from softwareconstruction240/466-grader-sched…
Browse files Browse the repository at this point in the history
…ule-shutdown
  • Loading branch information
webecke authored Nov 17, 2024
2 parents 51ea85f + 275db19 commit c6ca8f4
Show file tree
Hide file tree
Showing 16 changed files with 350 additions and 50 deletions.
41 changes: 31 additions & 10 deletions src/main/java/edu/byu/cs/controller/ConfigController.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import edu.byu.cs.dataAccess.DataAccessException;
import edu.byu.cs.model.*;
import edu.byu.cs.service.ConfigService;
import edu.byu.cs.util.Serializer;
import spark.Route;

import java.util.ArrayList;
Expand Down Expand Up @@ -33,9 +34,8 @@ public class ConfigController {
};

public static final Route updateLivePhases = (req, res) -> {
Gson gson = new Gson();
JsonObject jsonObject = gson.fromJson(req.body(), JsonObject.class);
ArrayList phasesArray = gson.fromJson(jsonObject.get("phases"), ArrayList.class);
JsonObject jsonObject = Serializer.deserialize(req.body(), JsonObject.class);
ArrayList phasesArray = Serializer.deserialize(jsonObject.get("phases"), ArrayList.class);
User user = req.session().attribute("user");

ConfigService.updateLivePhases(phasesArray, user);
Expand All @@ -44,16 +44,37 @@ public class ConfigController {
return "";
};

public static final Route scheduleShutdown = (req, res) -> {
User user = req.session().attribute("user");

JsonObject jsonObject = Serializer.deserialize(req.body(), JsonObject.class);
String shutdownTimestampString = Serializer.deserialize(jsonObject.get("shutdownTimestamp"), String.class);
Integer shutdownWarningMilliseconds = Serializer.deserialize(jsonObject.get("shutdownWarningMilliseconds"), Integer.class);

try {
ConfigService.scheduleShutdown(user, shutdownTimestampString);
ConfigService.setShutdownWarningDuration(user, shutdownWarningMilliseconds);
} catch (DataAccessException e) {
halt(500, e.getMessage());
return null;
} catch (IllegalArgumentException e) {
halt(400, e.getMessage());
return null;
}

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

public static final Route updateBannerMessage = (req, res) -> {
User user = req.session().attribute("user");

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

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);
String message = Serializer.deserialize(jsonObject.get("bannerMessage"), String.class);
String link = Serializer.deserialize(jsonObject.get("bannerLink"), String.class);
String color = Serializer.deserialize(jsonObject.get("bannerColor"), String.class);

try {
ConfigService.updateBannerMessage(user, expirationString, message, link, color);
Expand All @@ -67,7 +88,7 @@ public class ConfigController {
};

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

User user = req.session().attribute("user");

Expand Down
2 changes: 2 additions & 0 deletions src/main/java/edu/byu/cs/dataAccess/ConfigurationDao.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ public interface ConfigurationDao {

enum Configuration {
STUDENT_SUBMISSIONS_ENABLED,
GRADER_SHUTDOWN_DATE,
GRADER_SHUTDOWN_WARNING_MILLISECONDS,
BANNER_MESSAGE,
BANNER_LINK,
BANNER_COLOR,
Expand Down
1 change: 1 addition & 0 deletions src/main/java/edu/byu/cs/server/Server.java
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ public static int setupEndpoints(int port) {
get("", getConfigAdmin);

post("/phases", updateLivePhases);
post("/phases/shutdown", scheduleShutdown);
post("/banner", updateBannerMessage);

post("/courseIds", updateCourseIdsPost);
Expand Down
117 changes: 103 additions & 14 deletions src/main/java/edu/byu/cs/service/ConfigService.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,21 @@
import java.util.EnumMap;
import java.util.Map;

import static edu.byu.cs.util.PhaseUtils.isPhaseEnabled;
import static edu.byu.cs.util.PhaseUtils.isPhaseGraded;

public class ConfigService {

private static final Logger LOGGER = LoggerFactory.getLogger(ConfigService.class);
private static final ConfigurationDao dao = DaoService.getConfigurationDao();


private static void logConfigChange(String changeMessage, String adminNetId) {
LOGGER.info("[CONFIG] Admin {} has {}}", adminNetId, changeMessage);
LOGGER.info("[CONFIG] Admin {} has {}", adminNetId, changeMessage);
}

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

/**
Expand All @@ -42,22 +47,24 @@ private static void logAutomaticConfigChange(String changeMessage) {
* @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();
}

if (bannerExpiration.equals(Instant.MAX)) { //shutdown is not set
response.addProperty("bannerExpiration", "never");
} else {
response.addProperty("bannerExpiration", bannerExpiration.toString());
}

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);
Expand All @@ -67,13 +74,26 @@ private static void clearBannerConfig() throws DataAccessException {
}

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

JsonObject response = new JsonObject();

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

Instant shutdownTimestamp = dao.getConfiguration(ConfigurationDao.Configuration.GRADER_SHUTDOWN_DATE, Instant.class);
if (shutdownTimestamp.isBefore(Instant.now())) { //shutdown time has passed
clearShutdownSchedule();
}

if (shutdownTimestamp.equals(Instant.MAX)) { //shutdown is not set
response.addProperty("shutdownSchedule", "never");
} else {
response.addProperty("shutdownSchedule", shutdownTimestamp.toString());
}

response.addProperty("shutdownWarningMilliseconds", dao.getConfiguration(ConfigurationDao.Configuration.GRADER_SHUTDOWN_WARNING_MILLISECONDS, Integer.class));

return response;
}

Expand All @@ -82,7 +102,7 @@ public static JsonObject getPrivateConfig() throws DataAccessException {
Map<Phase, Map<Rubric.RubricType, CanvasAssignment.CanvasRubric>> rubricInfo = new EnumMap<>(Phase.class);

for (Phase phase : Phase.values()) {
if (!PhaseUtils.isPhaseGraded(phase)) continue;
if (!isPhaseGraded(phase)) continue;
Integer assignmentId = PhaseUtils.getPhaseAssignmentNumber(phase);
assignmentIds.put(phase, assignmentId);
if (rubricInfo.get(phase) == null) {
Expand All @@ -100,7 +120,7 @@ public static JsonObject getPrivateConfig() throws DataAccessException {
}

JsonObject response = getPublicConfig();
int courseNumber = DaoService.getConfigurationDao().getConfiguration(
int courseNumber = dao.getConfiguration(
ConfigurationDao.Configuration.COURSE_NUMBER,
Integer.class
);
Expand All @@ -111,16 +131,85 @@ public static JsonObject getPrivateConfig() throws DataAccessException {
}

public static void updateLivePhases(ArrayList phasesArray, User user) throws DataAccessException {
ConfigurationDao dao = DaoService.getConfigurationDao();

dao.setConfiguration(ConfigurationDao.Configuration.STUDENT_SUBMISSIONS_ENABLED, phasesArray, ArrayList.class);

logConfigChange("set the following phases as live: %s".formatted(phasesArray), user.netId());
}

public static void updateBannerMessage(User user, String expirationString, String message, String link, String color) throws DataAccessException {
ConfigurationDao dao = DaoService.getConfigurationDao();
public static void setShutdownWarningDuration(User user, Integer warningMilliseconds) throws DataAccessException {
if (warningMilliseconds < 0) {
throw new IllegalArgumentException("warningMilliseconds must be non-negative");
}

dao.setConfiguration(ConfigurationDao.Configuration.GRADER_SHUTDOWN_WARNING_MILLISECONDS, warningMilliseconds, Integer.class);

logConfigChange("set the shutdown warning duration to %s milliseconds".formatted(warningMilliseconds), user.netId());
}

public static void scheduleShutdown(User user, String shutdownTimestampString) throws DataAccessException {
if (shutdownTimestampString.isEmpty()) {
clearShutdownSchedule(user);
return;
}

Instant shutdownTimestamp;
try {
shutdownTimestamp = getInstantFromUnzonedTime(shutdownTimestampString);
} catch (Exception e) {
throw new IllegalArgumentException("Incomplete timestamp. Send a full timestamp {YYYY-MM-DDTHH:MM:SS} or send none at all", e);
}

if (shutdownTimestamp.isBefore(Instant.now())) {
throw new IllegalArgumentException("You tried to schedule the shutdown in the past");
}

dao.setConfiguration(ConfigurationDao.Configuration.GRADER_SHUTDOWN_DATE, shutdownTimestamp, Instant.class);
logConfigChange("scheduled a grader shutdown for %s".formatted(shutdownTimestampString), user.netId());
}

public static void triggerShutdown() {
try {
ArrayList<Phase> phases = new ArrayList<>();
for (Phase phase: Phase.values()) {
if (!isPhaseGraded(phase) && isPhaseEnabled(phase)) {
phases.add(phase);
}
}
dao.setConfiguration(ConfigurationDao.Configuration.STUDENT_SUBMISSIONS_ENABLED, phases, ArrayList.class);
logAutomaticConfigChange("Student submissions have shutdown. These phases remain active: " + phases);
} catch (DataAccessException e) {
LOGGER.error("Something went wrong while shutting down graded phases: " + e.getMessage());
}
}

/**
* Checks that the date for a scheduled shutdown has passed. If it has, it triggers the shutdown method
*/
public static void checkForShutdown() {
try {
Instant shutdownInstant = dao.getConfiguration(ConfigurationDao.Configuration.GRADER_SHUTDOWN_DATE, Instant.class);
if (shutdownInstant.isBefore(Instant.now())) {
triggerShutdown();
}
} catch (DataAccessException e) {
LOGGER.error("Something went wrong while checking for shutdown: " + e.getMessage());
}
}

public static void clearShutdownSchedule() throws DataAccessException {
clearShutdownSchedule(null);
}

public static void clearShutdownSchedule(User user) throws DataAccessException {
dao.setConfiguration(ConfigurationDao.Configuration.GRADER_SHUTDOWN_DATE, Instant.MAX, Instant.class);
if (user == null) {
logAutomaticConfigChange("Grader Shutdown Schedule was cleared");
} else {
logConfigChange("cleared the Grader Shutdown Schedule", user.netId() );
}
}

public static void updateBannerMessage(User user, String expirationString, String message, String link, String color) throws DataAccessException {
Instant expirationTimestamp = Instant.MAX;
if (!expirationString.isEmpty()) {
try {
Expand Down Expand Up @@ -166,7 +255,7 @@ private static Instant getInstantFromUnzonedTime(String timestampString) throws
public static void updateCourseIds(User user, SetCourseIdsRequest setCourseIdsRequest) throws DataAccessException {

// Course Number
DaoService.getConfigurationDao().setConfiguration(
dao.setConfiguration(
ConfigurationDao.Configuration.COURSE_NUMBER,
setCourseIdsRequest.courseNumber(),
Integer.class
Expand Down
21 changes: 5 additions & 16 deletions src/main/java/edu/byu/cs/service/SubmissionService.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,16 @@
import java.time.Instant;
import java.util.*;

import static edu.byu.cs.util.PhaseUtils.isPhaseEnabled;

public class SubmissionService {

private static final Logger LOGGER = LoggerFactory.getLogger(SubmissionService.class);

public static void submit(User user, GradeRequest request) throws BadRequestException, DataAccessException, InternalServerException {
if (!phaseIsEnabled(request.phase())) {
ConfigService.checkForShutdown();

if (!isPhaseEnabled(request.phase())) {
throw new BadRequestException("Student submission is disabled for " + request.phase());
}

Expand All @@ -39,21 +43,6 @@ public static void submit(User user, GradeRequest request) throws BadRequestExce

}

private static boolean phaseIsEnabled(Phase phase) throws DataAccessException {
boolean phaseEnabled;

try {
phaseEnabled = DaoService.getConfigurationDao()
.getConfiguration(ConfigurationDao.Configuration.STUDENT_SUBMISSIONS_ENABLED, String.class)
.contains(phase.toString());
} catch (DataAccessException e) {
LOGGER.error("Error getting configuration for live phase", e);
throw e;
}

return phaseEnabled;
}

public static void adminRepoSubmit(String netId, GradeRequest request) throws DataAccessException, InternalServerException, BadRequestException {
LOGGER.info("Admin {} submitted phase {} on repo {} for test grading", netId, request.phase(),
request.repoUrl());
Expand Down
19 changes: 19 additions & 0 deletions src/main/java/edu/byu/cs/util/PhaseUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import edu.byu.cs.model.Phase;
import edu.byu.cs.model.Rubric;
import edu.byu.cs.model.RubricConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Collection;
import java.util.Collections;
Expand All @@ -16,6 +18,23 @@

public class PhaseUtils {

private static final Logger LOGGER = LoggerFactory.getLogger(PhaseUtils.class);

public static boolean isPhaseEnabled(Phase phase) throws DataAccessException {
boolean phaseEnabled;

try {
phaseEnabled = DaoService.getConfigurationDao()
.getConfiguration(ConfigurationDao.Configuration.STUDENT_SUBMISSIONS_ENABLED, String.class)
.contains(phase.toString());
} catch (DataAccessException e) {
LOGGER.error("Error getting configuration for live phase", e);
throw e;
}

return phaseEnabled;
}

/**
* Given a phase, returns the phase before it, or null.
*
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/edu/byu/cs/util/Serializer.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ public static String serialize(Object obj) {
}
}

public static <T> T deserialize(JsonElement jsonElement, Class<T> classOfT) {
return deserialize(jsonElement.toString(), classOfT);
}
public static <T> T deserialize(String jsonStr, Class<T> classOfT) {
try {
return GSON.fromJson(jsonStr, classOfT);
Expand Down
Loading

0 comments on commit c6ca8f4

Please sign in to comment.