diff --git a/LICENSE b/LICENSE index 3df23cf0..2a71b63b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2017 HackGT +Copyright (c) 2018 HackGT Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index db6e7462..ecd817d6 100644 --- a/README.md +++ b/README.md @@ -140,4 +140,4 @@ If you have some time and want to help us out with development, thank you! You c ## License -Copyright © 2017 HackGT. Released under the MIT license. See [LICENSE](LICENSE) for more information. +Copyright © 2018 HackGT. Released under the MIT license. See [LICENSE](LICENSE) for more information. diff --git a/client/admin.html b/client/admin.html index 4d8a82f0..4b814941 100644 --- a/client/admin.html +++ b/client/admin.html @@ -220,6 +220,10 @@

Applicants

Settings

+
+ +
+
@@ -304,6 +308,9 @@

Edit email content

{{/each}} + + +
Rendered HTML and text:
@@ -361,7 +368,7 @@

config.json options

- +
diff --git a/client/js/admin.ts b/client/js/admin.ts index 1ab64560..ee80b8f7 100644 --- a/client/js/admin.ts +++ b/client/js/admin.ts @@ -570,6 +570,7 @@ sendAcceptancesButton.addEventListener("click", async e => { declare let SimpleMDE: any; const emailTypeSelect = document.getElementById("email-type") as HTMLSelectElement; +const emailSubject = document.getElementById("email-subject") as HTMLInputElement; let emailRenderedArea: HTMLElement | ShadowRoot = document.getElementById("email-rendered") as HTMLElement; if (document.head.attachShadow) { // Browser supports Shadow DOM @@ -616,8 +617,9 @@ async function emailTypeChange(): Promise { // Load editor content via AJAX try { - let content = (await fetch(`/api/settings/email_content/${emailTypeSelect.value}`, { credentials: "same-origin" }).then(checkStatus).then(parseJSON)).content as string; - markdownEditor.value(content); + let emailSettings: { subject: string; content: string } = await fetch(`/api/settings/email_content/${emailTypeSelect.value}`, { credentials: "same-origin" }).then(checkStatus).then(parseJSON); + emailSubject.value = emailSettings.subject; + markdownEditor.value(emailSettings.content); } catch { markdownEditor.value("Couldn't retrieve email content"); @@ -645,14 +647,23 @@ function parseDateTime(dateTime: string) { let digits = dateTime.split(/\D+/).map(num => parseInt(num, 10)); return new Date(digits[0], digits[1] - 1, digits[2], digits[3], digits[4], digits[5] || 0, digits[6] || 0); } -let settingsUpdateButton = document.querySelector("#settings input[type=submit]") as HTMLInputElement; +let settingsUpdateButtons = document.querySelectorAll("#settings input[type=submit]") as NodeListOf; let settingsForm = document.querySelector("#settings form") as HTMLFormElement; -settingsUpdateButton.addEventListener("click", e => { +for (let i = 0; i < settingsUpdateButtons.length; i++) { + settingsUpdateButtons[i].addEventListener("click", settingsUpdate); +} +function settingsUpdateButtonDisabled(disabled: boolean) { + for (let i = 0; i < settingsUpdateButtons.length; i++) { + settingsUpdateButtons[i].disabled = disabled; + } +} + +function settingsUpdate(e: MouseEvent) { if (!settingsForm.checkValidity() || !settingsForm.dataset.action) { return; } e.preventDefault(); - settingsUpdateButton.disabled = true; + settingsUpdateButtonDisabled(true); let teamsEnabledData = new FormData(); teamsEnabledData.append("enabled", (document.getElementById("teams-enabled") as HTMLInputElement).checked ? "true" : "false"); @@ -700,6 +711,7 @@ settingsUpdateButton.addEventListener("click", e => { } let emailContentData = new FormData(); + emailContentData.append("subject", emailSubject.value); emailContentData.append("content", markdownEditor.value()); const defaultOptions: RequestInit = { @@ -739,9 +751,9 @@ settingsUpdateButton.addEventListener("click", e => { window.location.reload(); }).catch(async (err: Error) => { await sweetAlert("Oh no!", err.message, "error"); - settingsUpdateButton.disabled = false; + settingsUpdateButtonDisabled(false); }); -}); +} // // Graphs diff --git a/package-lock.json b/package-lock.json index 59ec5049..62a56fa6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "registration", - "version": "1.12.4", + "version": "1.12.5", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -208,7 +208,7 @@ "integrity": "sha512-HG4pUK/fTrGY3FerMlINxK74MxdAxkCRYrp5AM+oJ2jLcK0jWUi64ZV15JKwDR4TYLIxrT3y9SVnEWcLPbC/YA==", "dev": true, "requires": { - "moment": "2.18.1" + "moment": "2.21.0" } }, "@types/mongodb": { @@ -2198,16 +2198,16 @@ } }, "moment": { - "version": "2.18.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.18.1.tgz", - "integrity": "sha1-w2GT3Tzhwu7SrbfIAtu8d6gbHA8=" + "version": "2.21.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.21.0.tgz", + "integrity": "sha512-TCZ36BjURTeFTM/CwRcViQlfkMvL1/vFISuNLO5GkcVm1+QHfbSiNqZuWeMFjj1/3+uAjXswgRk30j1kkLYJBQ==" }, "moment-timezone": { "version": "0.5.13", "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.13.tgz", "integrity": "sha1-mc5cfYJyYusPH3AgRBd/YHRde5A=", "requires": { - "moment": "2.18.1" + "moment": "2.21.0" } }, "mongodb": { diff --git a/package.json b/package.json index f8b03340..fcb3704d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "registration", - "version": "1.12.4", + "version": "1.12.5", "description": "TBD", "main": "server/app.js", "scripts": { @@ -51,7 +51,7 @@ "json-schema-to-typescript": "^4.4.0", "json2csv": "^3.7.3", "marked": "^0.3.9", - "moment": "^2.18.1", + "moment": "^2.21.0", "moment-timezone": "^0.5.13", "mongoose": "^4.10.3", "morgan": "^1.8.2", diff --git a/server/common.ts b/server/common.ts index 5ff57a5d..c1070d45 100644 --- a/server/common.ts +++ b/server/common.ts @@ -376,6 +376,11 @@ import * as marked from "marked"; const striptags = require("striptags"); import { IUser, Team, IFormItem } from "./schema"; +export const defaultEmailSubjects = { + apply: `[${config.eventName}] - Thank you for applying!`, + accept: `[${config.eventName}] - You've been accepted!`, + attend: `[${config.eventName}] - Thank you for RSVPing!` +}; interface IMailObject { to: string; from: string; diff --git a/server/routes/api/settings.ts b/server/routes/api/settings.ts index f0d2a111..66c239f4 100644 --- a/server/routes/api/settings.ts +++ b/server/routes/api/settings.ts @@ -1,7 +1,7 @@ import * as express from "express"; import { - getSetting, updateSetting, setDefaultSettings, renderEmailHTML, renderEmailText + getSetting, updateSetting, setDefaultSettings, renderEmailHTML, renderEmailText, defaultEmailSubjects } from "../../common"; import { isAdmin, uploadHandler @@ -180,19 +180,41 @@ settingsRoutes.route("/branch_roles") settingsRoutes.route("/email_content/:type") .get(isAdmin, async (request, response) => { let content: string; + let subject: string; try { content = await getSetting(`${request.params.type}-email`, false); } - catch (err) { + catch { // Content not set yet content = ""; } + try { + subject = await getSetting(`${request.params.type}-email-subject`, false); + } + catch { + // Subject not set yet + let type: string = request.params.type; + if (type.match(/-apply$/)) { + subject = defaultEmailSubjects.apply; + } + else if (type.match(/-accept$/)) { + subject = defaultEmailSubjects.accept; + } + else if (type.match(/-attend$/)) { + subject = defaultEmailSubjects.attend; + } + else { + subject = ""; + } + } - response.json({ content }); + response.json({ subject, content }); }) .put(isAdmin, uploadHandler.any(), async (request, response) => { - let content = request.body.content as string; + let subject: string = request.body.subject; + let content: string = request.body.content; try { + await updateSetting(`${request.params.type}-email-subject`, subject); await updateSetting(`${request.params.type}-email`, content); response.json({ "success": true diff --git a/server/routes/api/user.ts b/server/routes/api/user.ts index 85aeed95..9811c849 100644 --- a/server/routes/api/user.ts +++ b/server/routes/api/user.ts @@ -7,7 +7,7 @@ import * as moment from "moment-timezone"; import { STORAGE_ENGINE, formatSize, - config, getSetting, renderEmailHTML, renderEmailText, sendMailAsync + config, getSetting, renderEmailHTML, renderEmailText, sendMailAsync, defaultEmailSubjects } from "../../common"; import { MAX_FILE_SIZE, postParser, uploadHandler, @@ -177,12 +177,19 @@ async function postApplicationBranchHandler(request: express.Request, response: return item; }); // Email the applicant to confirm + let type = requestType === ApplicationType.Application ? "apply" : "attend"; + let emailSubject: string | null; + try { + emailSubject = await getSetting(`${questionBranch.name}-${type}-email-subject`, false); + } + catch { + emailSubject = null; + } let emailMarkdown: string; try { - let type = requestType === ApplicationType.Application ? "apply" : "attend"; emailMarkdown = await getSetting(`${questionBranch.name}-${type}-email`, false); } - catch (err) { + catch { // Content not set yet emailMarkdown = ""; } @@ -195,7 +202,7 @@ async function postApplicationBranchHandler(request: express.Request, response: await sendMailAsync({ from: config.email.from, to: user.email, - subject: `[${config.eventName}] - Thank you for applying!`, + subject: emailSubject || defaultEmailSubjects.apply, html: emailHTML, text: emailText }); @@ -222,7 +229,7 @@ async function postApplicationBranchHandler(request: express.Request, response: await sendMailAsync({ from: config.email.from, to: user.email, - subject: `[${config.eventName}] - Thank you for RSVPing!`, + subject: emailSubject || defaultEmailSubjects.attend, html: emailHTML, text: emailText }); @@ -337,11 +344,18 @@ userRoutes.route("/send_acceptances").post(isAdmin, async (request, response): P let users = await User.find({ "accepted": true, "acceptedEmailSent": { $ne: true } }); for (let user of users) { // Email the applicant about their acceptance + let emailSubject: string | null; + try { + emailSubject = await getSetting(`${user.applicationBranch}-accept-email-subject`, false); + } + catch { + emailSubject = null; + } let emailMarkdown: string; try { emailMarkdown = await getSetting(`${user.applicationBranch}-accept-email`, false); } - catch (err) { + catch { // Content not set yet emailMarkdown = ""; } @@ -352,7 +366,7 @@ userRoutes.route("/send_acceptances").post(isAdmin, async (request, response): P await sendMailAsync({ from: config.email.from, to: user.email, - subject: `[${config.eventName}] - You've been accepted!`, + subject: emailSubject || defaultEmailSubjects.accept, html: emailHTML, text: emailText });