Skip to content

Commit

Permalink
Merge pull request #431 from softwareconstruction240/426-frontend-imp…
Browse files Browse the repository at this point in the history
…rove-banner-message

add customization options to banner message
  • Loading branch information
webecke authored Oct 2, 2024
2 parents 13e8fd1 + d5f11b2 commit 03f1762
Show file tree
Hide file tree
Showing 12 changed files with 390 additions and 48 deletions.
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
3 changes: 3 additions & 0 deletions src/main/java/edu/byu/cs/dataAccess/ConfigurationDao.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ public interface ConfigurationDao {
enum Configuration {
STUDENT_SUBMISSIONS_ENABLED,
BANNER_MESSAGE,
BANNER_LINK,
BANNER_COLOR,
BANNER_EXPIRATION,
GITHUB_ASSIGNMENT_NUMBER,
PHASE0_ASSIGNMENT_NUMBER,
PHASE1_ASSIGNMENT_NUMBER,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.Instant;

public class ConfigurationSqlDao implements ConfigurationDao {

private static final Logger LOGGER = LoggerFactory.getLogger(ConfigurationSqlDao.class);
Expand Down Expand Up @@ -77,6 +79,8 @@ private <T> T getValue(String value, Class<T> type) {
return type.cast(Integer.parseInt(value));
} else if (type == Boolean.class) {
return type.cast(Boolean.parseBoolean(value));
} else if (type == Instant.class) {
return type.cast(Instant.parse(value));
} else {
throw new IllegalArgumentException("Unsupported configuration type: " + type);
}
Expand Down
34 changes: 5 additions & 29 deletions src/main/resources/frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import router from '@/router'
import '@/assets/fontawesome/css/fontawesome.css'
import '@/assets/fontawesome/css/solid.css'
import { useAppConfigStore } from '@/stores/appConfig'
import BannerMessage from '@/components/BannerMessage.vue'
import PopUp from '@/components/PopUp.vue'
import RepoEditor from '@/components/RepoEditor.vue'
import AboutPage from '@/components/AboutPage.vue'
const greeting = computed(() => {
if (useAuthStore().isLoggedIn) {
Expand All @@ -26,12 +28,6 @@ const logOut = async () => {
router.push({name: "login"})
}
const bannerMessage = computed(() => {
if (useAuthStore().isLoggedIn) {
return useAppConfigStore().bannerMessage
}
});
onMounted( async () => {
await useAppConfigStore().updateConfig();
})
Expand All @@ -49,18 +45,8 @@ const repoEditDone = () => {
<h1>CS 240 Autograder</h1>
<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
v-if="useAuthStore().user?.repoUrl"
@click="openRepoEditor.value = true"
id="repoLink"
>
{{ useAuthStore().user?.repoUrl }}
<i class="fa-solid fa-pen-to-square"/>
</p>

<div v-if="bannerMessage" id="bannerMessage">
<span v-text="bannerMessage"/>
</div>
<p>{{ useAuthStore().user?.repoUrl }}</p>
<BannerMessage/>
</header>
<main>
<PopUp
Expand All @@ -72,17 +58,11 @@ const repoEditDone = () => {
</PopUp>

<router-view/>
<AboutPage/>
</main>
</template>

<style scoped>
#bannerMessage {
width: 100%;
background-color: #4fa0ff;
border-radius: 3px;
padding: 7px;
margin-top: 15px;
}
header {
text-align: center;
Expand All @@ -100,10 +80,6 @@ h1 {
font-weight: bold;
}
#repoLink {
cursor: pointer;
}
main {
display: flex;
flex-direction: column;
Expand Down
Binary file added src/main/resources/frontend/src/assets/saints.mp3
Binary file not shown.
Loading

0 comments on commit 03f1762

Please sign in to comment.