Skip to content

Commit

Permalink
Merge pull request #358 from softwareconstruction240/324-program-config
Browse files Browse the repository at this point in the history
Introduce Program Configuration (Set Live Phases / Banner Message)
  • Loading branch information
19mdavenport authored Jun 28, 2024
2 parents b6a5f66 + 939ac49 commit 6518ccb
Show file tree
Hide file tree
Showing 15 changed files with 409 additions and 18 deletions.
80 changes: 80 additions & 0 deletions src/main/java/edu/byu/cs/controller/ConfigController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package edu.byu.cs.controller;

import com.google.gson.Gson;
import com.google.gson.JsonObject;
import edu.byu.cs.dataAccess.ConfigurationDao;
import edu.byu.cs.dataAccess.DaoService;
import edu.byu.cs.dataAccess.DataAccessException;
import edu.byu.cs.model.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import spark.Route;

import java.util.ArrayList;

public class ConfigController {

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

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

public static final Route getConfigAdmin = (req, res) -> {
JsonObject response = getPublicConfig();

res.status(200);
return response.toString();
};

public static final Route getConfigStudent = (req, res) -> {
String response = getPublicConfig().toString();

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

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));
response.addProperty("phases", dao.getConfiguration(ConfigurationDao.Configuration.STUDENT_SUBMISSIONS_ENABLED, String.class));

return response;
}

public static final Route updateLivePhases = (req, res) -> {
ConfigurationDao dao = DaoService.getConfigurationDao();

JsonObject jsonObject = new Gson().fromJson(req.body(), JsonObject.class);
ArrayList phasesArray = new Gson().fromJson(jsonObject.get("phases"), ArrayList.class);

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

User user = req.session().attribute("user");
logConfigChange("set the following phases as live: %s".formatted(phasesArray), user.netId());

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

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

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);

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());
}

res.status(200);
return "";
};
}
26 changes: 19 additions & 7 deletions src/main/java/edu/byu/cs/controller/SubmissionController.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,8 @@ public class SubmissionController {

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

Boolean submissionsEnabled = getSubmissionsEnabledConfig();
if (submissionsEnabled == null) return null;

if (!submissionsEnabled) {
halt(400, "Student submission is disabled");
return null;
if (!phaseIsEnabled(request.phase())) {
halt(400, "Student submission is disabled for " + request.phase());
}

updateRepoFromCanvas(user, req);
Expand All @@ -56,11 +52,27 @@ public class SubmissionController {
return "";
};

private static boolean phaseIsEnabled(Phase phase) {
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);
halt(500);
return false;
}

return phaseEnabled;
}

private static Boolean getSubmissionsEnabledConfig() {
boolean submissionsEnabled;
try {
submissionsEnabled = DaoService.getConfigurationDao().getConfiguration(
ConfigurationDao.Configuration.STUDENT_SUBMISSION_ENABLED,
ConfigurationDao.Configuration.STUDENT_SUBMISSIONS_ENABLED,
Boolean.class);
} catch (Exception e) {
LOGGER.error("Error getting configuration", e);
Expand Down
8 changes: 8 additions & 0 deletions src/main/java/edu/byu/cs/dataAccess/ConfigREADME.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Autograder Config Info

The autograder has a bunch of settings that change from time to time, but need to persist across reboots. For instance, whether a phase is enabled for student submissions, or the Canvas Course ID. Thats where the Autograder Config system comes in.

The `Configuration` table in the database is a set of key value pairs. This gives us a lot of flexibility, but also the need to define how it will be used. That is what this file is for.

### Live Phases
The `STUDENT_SUBMISSION_ENABLED` Configuration enum has a value of an array of phases. Each phase in the array is enabled, the rest are not.
3 changes: 2 additions & 1 deletion src/main/java/edu/byu/cs/dataAccess/ConfigurationDao.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ public interface ConfigurationDao {
<T> T getConfiguration(Configuration key, Class<T> type) throws DataAccessException;

enum Configuration {
STUDENT_SUBMISSION_ENABLED
STUDENT_SUBMISSIONS_ENABLED,
BANNER_MESSAGE
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ public ConfigurationSqlDao() {
@Override
public <T> void setConfiguration(Configuration key, T value, Class<T> type) throws DataAccessException {
try (var connection = SqlDb.getConnection()) {
var statement = connection.prepareStatement("INSERT INTO configuration (config_key, value) VALUES (?, ?)");
var statement = connection.prepareStatement("""
INSERT INTO configuration (config_key, value) VALUES (?, ?)
ON DUPLICATE KEY UPDATE value = VALUES(value);
""");
statement.setString(1, key.toString());
statement.setString(2, value.toString());
statement.executeUpdate();
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/edu/byu/cs/server/Server.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import static edu.byu.cs.controller.AdminController.*;
import static edu.byu.cs.controller.AuthController.*;
import static edu.byu.cs.controller.CasController.*;
import static edu.byu.cs.controller.ConfigController.*;
import static edu.byu.cs.controller.SubmissionController.*;
import static spark.Spark.*;

Expand Down Expand Up @@ -65,6 +66,8 @@ public static int setupEndpoints(int port) {

get("/me", meGet);

get("/config", getConfigStudent);

path("/admin", () -> {
before("/*", (req, res) -> {
if (!req.requestMethod().equals("OPTIONS"))
Expand Down Expand Up @@ -100,6 +103,13 @@ public static int setupEndpoints(int port) {
get("/honorChecker/zip/:section", honorCheckerZipGet);

get("/sections", sectionsGet);

path("/config", () -> {
get("", getConfigAdmin);

post("/phases", updateLivePhases);
post("/banner", updateBannerMessage);
});
});
});

Expand Down
23 changes: 22 additions & 1 deletion src/main/resources/frontend/src/App.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
<script setup lang="ts">
import { computed } from 'vue'
import { computed, onMounted } from 'vue'
import {useAuthStore} from "@/stores/auth";
import { logoutPost } from '@/services/authService'
import router from '@/router'
import '@/assets/fontawesome/css/fontawesome.css'
import '@/assets/fontawesome/css/solid.css'
import { useAppConfigStore } from '@/stores/appConfig'
const greeting = computed(() => {
if (useAuthStore().isLoggedIn) {
Expand All @@ -22,6 +23,16 @@ const logOut = async () => {
}
router.push({name: "login"})
}
const bannerMessage = computed(() => {
if (useAuthStore().isLoggedIn) {
return useAppConfigStore().bannerMessage
}
});
onMounted( async () => {
await useAppConfigStore().updateConfig();
})
</script>

<template>
Expand All @@ -30,13 +41,23 @@ const logOut = async () => {
<h3>This is where you can submit your assignments and view your scores.</h3>
<p>{{ greeting }} <a v-if="useAuthStore().isLoggedIn" @click="logOut">Logout</a></p>
<p>{{ useAuthStore().user?.repoUrl }}</p>
<div v-if="bannerMessage" id="bannerMessage">
<span v-text="bannerMessage"/>
</div>
</header>
<main>
<router-view/>
</main>
</template>

<style scoped>
#bannerMessage {
width: 100%;
background-color: #4fa0ff;
border-radius: 3px;
padding: 7px;
margin-top: 15px;
}
header {
text-align: center;
Expand Down
5 changes: 5 additions & 0 deletions src/main/resources/frontend/src/assets/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@ button {
letter-spacing: 0.02857em;
}

button.small {
font-size: 0.9rem;
padding: 5px;
}

button:hover {
filter: brightness(1.1);
cursor: pointer;
Expand Down
61 changes: 61 additions & 0 deletions src/main/resources/frontend/src/services/configService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { type Config, useAppConfigStore } from '@/stores/appConfig'
import { Phase } from '@/types/types'
import { useAuthStore } from '@/stores/auth'

export const getConfig = async ():Promise<Config> => {
let path = "/api"
if (useAuthStore().user?.role == 'ADMIN') {
path += "/admin"
}
path += "/config"

try {
const response = await fetch(useAppConfigStore().backendUrl + path, {
method: 'GET',
credentials: 'include'
});

return await response.json();
} catch (e) {
console.error('Failed to get configuration: ', e);
throw "Failed to get configuration"
}
}

export const setBannerMessage = async (message: String): Promise<void> => {
const response = await fetch(useAppConfigStore().backendUrl + '/api/admin/config/banner', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
"bannerMessage": message,
})
});

if (!response.ok) {
console.error(response);
throw new Error(await response.text());
}
await useAppConfigStore().updateConfig();
}

export const setLivePhases = async (phases: Array<Phase>): Promise<void> => {
const response = await fetch(useAppConfigStore().backendUrl + '/api/admin/config/phases', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
"phases": phases,
})
});

if (!response.ok) {
console.error(response);
throw new Error(await response.text());
}
await useAppConfigStore().updateConfig();
}
24 changes: 22 additions & 2 deletions src/main/resources/frontend/src/stores/appConfig.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,34 @@
import { ref, computed } from 'vue'
import { ref, type Ref } from 'vue'
import { defineStore } from 'pinia'
import { listOfPhases, Phase } from '@/types/types'
import { getConfig } from '@/services/configService'

type ImportMeta = {
VITE_APP_BACKEND_URL: string
}

export type Config = {
bannerMessage: string
phases: Array<Phase>
}

// @ts-ignore
const env: ImportMeta = import.meta.env;
export const useAppConfigStore = defineStore('appConfig', () => {
const backendUrl = ref<string>(env.VITE_APP_BACKEND_URL);

return { backendUrl }
const updateConfig = async () => {
const latestConfig = await getConfig();

bannerMessage.value = latestConfig.bannerMessage
for (const phase of listOfPhases() as Phase[]) {
activePhaseList.value[phase] = latestConfig.phases.includes(phase);
}
}

const bannerMessage: Ref<string> = ref<string>("")
// using the enum, if phaseActivationList[phase] == true, then that phase is active
const activePhaseList: Ref<boolean[]> = ref<Array<boolean>>([])

return { updateConfig, backendUrl, bannerMessage, phaseActivationList: activePhaseList }
})
11 changes: 11 additions & 0 deletions src/main/resources/frontend/src/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@ export enum Phase {
GitHub
}

export const listOfPhases = ():Array<Phase> => {
let result = []
for (var phase in Phase) {
var isValueProperty = Number(phase) >= 0
if (isValueProperty) {
result.push(Phase[phase] as unknown as Phase);
}
}
return result
}

export type TestNode = {
testName: string,
passed: boolean,
Expand Down
Loading

0 comments on commit 6518ccb

Please sign in to comment.