diff --git a/common-theme/layouts/partials/register-attendance.html b/common-theme/layouts/partials/register-attendance.html index 8fdf8da0a..91bc86d0a 100644 --- a/common-theme/layouts/partials/register-attendance.html +++ b/common-theme/layouts/partials/register-attendance.html @@ -13,7 +13,15 @@ {{ $module := .module }} {{ $day := .day | urlize}} +{{/* Note: These keys are hard-coded in submission-created.js - if you change them, you need to change them there as well. */}} {{ $orgData := index site.Data.courses $course }} +{{ if eq $orgData nil }} + {{ $keys := slice }} + {{ range $key, $value := site.Data.courses }} + {{ $keys = append $key $keys }} + {{ end }} + {{ errorf "Couldn't find course data for course %s - knew about %s" $course $keys }} +{{ end }} {{ $locations := $orgData.locations }} {{/* if any of these params are null, error */}} diff --git a/org-cyf-itd/hugo.toml b/org-cyf-itd/hugo.toml index 6208a1e87..8c3b20bea 100644 --- a/org-cyf-itd/hugo.toml +++ b/org-cyf-itd/hugo.toml @@ -1,4 +1,4 @@ -title = "Intro to Digital" +title = "ITD" description="Meet the world of tech in 30 days" [module] @@ -33,4 +33,4 @@ description="Meet the world of tech in 30 days" # because of this 'unexpected behaviour' https://gohugo.io/methods/page/nextinsection/ [page] nextPrevInSectionSortOrder = 'asc' - nextPrevSortOrder = 'asc' \ No newline at end of file + nextPrevSortOrder = 'asc' diff --git a/org-cyf-itp/tooling/netlify/functions/submission-created.js b/org-cyf-itp/tooling/netlify/functions/submission-created.js deleted file mode 100644 index 32eddd505..000000000 --- a/org-cyf-itp/tooling/netlify/functions/submission-created.js +++ /dev/null @@ -1,108 +0,0 @@ -// This function handles forwards netlify form submissions to our register spreadsheets. -// Note that errors are reported in the netlify console, but are _not_ exposed to users who submit the form - they will see successful submission even if this function fails. -// -// This function is automatically called for all form submissions, because it is named exactly submission-create. -// It cannot be manually called by users, and is only callable by Netlify itself. -// If we end up adding multiple forms to the site, we'll need to demux them here in some way and filter out other forms. -// -// The underlying form data is in the body (which is a JSON object) under .payload.data. - -import {Auth, google} from "googleapis"; - -// Each sheet listed here was manually crated, and its spreadsheet ID is taken from its URL. -// Each spreadsheet is expected to contain one sheet per module, named for the module. -// Each module sheet is expected to contain the following columns, in order: -// Given Name | Family Name | Email | Timestamp | Course | Module | Day | Build Time -// Each spreadsheet must also give write access to the email listed below in CREDENTIALS. -const COURSE_TO_SPREADSHEET_ID = { - "cyf-itp": "1YHKPCCN55PJD-o1jg4wbVKI3kbhB-ULiwB5hhG17DcA", - "cyf-piscine": "1XabWuYqvOUiY7HpUra0Vdic4pSxmXmNRZHMR72I1bjk", -}; - -const CREDENTIALS = { - // This was generated by: - // 1. Visit https://console.cloud.google.com/apis/credentials?project=cyf-syllabus - // 2. Generate or find the service account. - // 3. Download its credentials as a JSON file. - // 4. Fetch the keys listed below - the non-env-var ones are not really secret. - // 5. For private_key, this is read from an env var on netlify, and env vars cannot contain newlines, so we put the string "\n" where we need newlines, accordingly, we undo this below. - // The env var was constructed by running: `jq '.private_key' { - console.log("Got request with body", event.body); - let body; - try { - // TODO: Check for structure - body = JSON.parse(event.body); - } catch (error) { - console.error(`Failed to parse request as valid JSON: ${event.body}: ${error}`); - return {statusCode: 400, body: JSON.stringify("Failed to parse body as JSON")}; - } - if (!("payload" in body) || !("data" in body.payload)) { - console.error(`Failed to parse request - missing .payload.data: ${event.body}`); - return {statusCode: 400, body: JSON.stringify("Failed to parse body as JSON")}; - } - const request = body.payload.data; - - for (const requiredField of ["course", "module", "name", "email", "day", "location", "buildTime"]) { - if (!(requiredField in request)) { - const error = `Request missing field ${requiredField}`; - console.error(error, request); - return {statusCode: 400, body: JSON.stringify(error)}; - } - const type = typeof request[requiredField]; - if (type !== "string") { - const error = `Field ${requiredField} must be of type string but was ${type}`; - console.error(error, request); - return {statusCode: 400, body: JSON.stringify(error)}; - } - } - - if (!(request.course in COURSE_TO_SPREADSHEET_ID)) { - console.error(`Didn't recognise course ${request.course}`); - return {statusCode: 404, body: JSON.stringify(`Unknown course: ${request.course}`)}; - } - - const auth = new Auth.GoogleAuth({ - credentials: CREDENTIALS, - scopes: "https://www.googleapis.com/auth/spreadsheets", - }); - - const sheets = google.sheets({version: "v4", auth}); - - try { - const response = await sheets.spreadsheets.values.append({ - spreadsheetId: COURSE_TO_SPREADSHEET_ID[request.course], - // Trust that the user-supplied module exists. - // If it's wrong, we'll get a 400 from the API and pass it on to the user without much useful detail. - range: request.module, - valueInputOption: "RAW", - insertDataOption: "INSERT_ROWS", - resource: { - values: [ - [request.name, request.email, new Date().toISOString(), request.course, request.module, request.day, request.location, request.buildTime], - ], - }, - }); - } catch (error) { - console.error("Error from Google API", error); - let message = "An error occurred signing the register"; - if (error.errors) { - if ("message" in error.errors[0]) { - message += ": " + error.errors[0].message; - } - } - return {statusCode: 400, body: JSON.stringify(message)}; - } - - return { - statusCode: 200, - body: JSON.stringify("You have successfully signed the register"), - }; -}; - -export { handler }; diff --git a/org-cyf-itp/tooling/netlify/functions/submission-created.js b/org-cyf-itp/tooling/netlify/functions/submission-created.js new file mode 120000 index 000000000..bd794c774 --- /dev/null +++ b/org-cyf-itp/tooling/netlify/functions/submission-created.js @@ -0,0 +1 @@ +../../../../tooling/netlify/functions/submission-created.js \ No newline at end of file diff --git a/org-cyf-launch/hugo.toml b/org-cyf-launch/hugo.toml index 44b506e83..1a9bdcf13 100644 --- a/org-cyf-launch/hugo.toml +++ b/org-cyf-launch/hugo.toml @@ -1,4 +1,4 @@ -title = "The Launch" +title = "Launch" baseURL = "https://launch.codeyourfuture.io/" [module] @@ -35,4 +35,4 @@ baseURL = "https://launch.codeyourfuture.io/" # because of this 'unexpected behaviour' https://gohugo.io/methods/page/nextinsection/ [page] nextPrevInSectionSortOrder = 'asc' - nextPrevSortOrder = 'asc' \ No newline at end of file + nextPrevSortOrder = 'asc' diff --git a/org-cyf-piscine/hugo.toml b/org-cyf-piscine/hugo.toml index fbee95829..9beee90e4 100644 --- a/org-cyf-piscine/hugo.toml +++ b/org-cyf-piscine/hugo.toml @@ -1,4 +1,4 @@ -title = "CYF Piscine" +title = "Piscine" [module] [[module.imports]] diff --git a/org-cyf-piscine/tooling/netlify/functions/submission-created.js b/org-cyf-piscine/tooling/netlify/functions/submission-created.js new file mode 120000 index 000000000..bd794c774 --- /dev/null +++ b/org-cyf-piscine/tooling/netlify/functions/submission-created.js @@ -0,0 +1 @@ +../../../../tooling/netlify/functions/submission-created.js \ No newline at end of file diff --git a/org-cyf-sdc/hugo.toml b/org-cyf-sdc/hugo.toml index 44c3b5a66..157fb43b7 100644 --- a/org-cyf-sdc/hugo.toml +++ b/org-cyf-sdc/hugo.toml @@ -1,4 +1,4 @@ -title = "CYF SDC Curriculum" +title = "SDC" baseURL = "https://sdc.codeyourfuture.io/" [module] diff --git a/org-cyf-theme/data/courses/cyf-piscine.toml b/org-cyf-theme/data/courses/piscine.toml similarity index 100% rename from org-cyf-theme/data/courses/cyf-piscine.toml rename to org-cyf-theme/data/courses/piscine.toml diff --git a/tooling/netlify/functions/submission-created.js b/tooling/netlify/functions/submission-created.js new file mode 100644 index 000000000..837a21b66 --- /dev/null +++ b/tooling/netlify/functions/submission-created.js @@ -0,0 +1,108 @@ +// This function handles forwards netlify form submissions to our register spreadsheets. +// Note that errors are reported in the netlify console, but are _not_ exposed to users who submit the form - they will see successful submission even if this function fails. +// +// This function is automatically called for all form submissions, because it is named exactly submission-create. +// It cannot be manually called by users, and is only callable by Netlify itself. +// If we end up adding multiple forms to the site, we'll need to demux them here in some way and filter out other forms. +// +// The underlying form data is in the body (which is a JSON object) under .payload.data. + +import {Auth, google} from "googleapis"; + +// Each sheet listed here was manually crated, and its spreadsheet ID is taken from its URL. +// Each spreadsheet is expected to contain one sheet per module, named for the module. +// Each module sheet is expected to contain the following columns, in order: +// Given Name | Family Name | Email | Timestamp | Course | Module | Day | Build Time +// Each spreadsheet must also give write access to the email listed below in CREDENTIALS. +const COURSE_TO_SPREADSHEET_ID = { + "itp": "1YHKPCCN55PJD-o1jg4wbVKI3kbhB-ULiwB5hhG17DcA", + "piscine": "1XabWuYqvOUiY7HpUra0Vdic4pSxmXmNRZHMR72I1bjk", +}; + +const CREDENTIALS = { + // This was generated by: + // 1. Visit https://console.cloud.google.com/apis/credentials?project=cyf-syllabus + // 2. Generate or find the service account. + // 3. Download its credentials as a JSON file. + // 4. Fetch the keys listed below - the non-env-var ones are not really secret. + // 5. For private_key, this is read from an env var on netlify, and env vars cannot contain newlines, so we put the string "\n" where we need newlines, accordingly, we undo this below. + // The env var was constructed by running: `jq '.private_key' { + console.log("Got request with body", event.body); + let body; + try { + // TODO: Check for structure + body = JSON.parse(event.body); + } catch (error) { + console.error(`Failed to parse request as valid JSON: ${event.body}: ${error}`); + return {statusCode: 400, body: JSON.stringify("Failed to parse body as JSON")}; + } + if (!("payload" in body) || !("data" in body.payload)) { + console.error(`Failed to parse request - missing .payload.data: ${event.body}`); + return {statusCode: 400, body: JSON.stringify("Failed to parse body as JSON")}; + } + const request = body.payload.data; + + for (const requiredField of ["course", "module", "name", "email", "day", "location", "buildTime"]) { + if (!(requiredField in request)) { + const error = `Request missing field ${requiredField}`; + console.error(error, request); + return {statusCode: 400, body: JSON.stringify(error)}; + } + const type = typeof request[requiredField]; + if (type !== "string") { + const error = `Field ${requiredField} must be of type string but was ${type}`; + console.error(error, request); + return {statusCode: 400, body: JSON.stringify(error)}; + } + } + + if (!(request.course in COURSE_TO_SPREADSHEET_ID)) { + console.error(`Didn't recognise course ${request.course}`); + return {statusCode: 404, body: JSON.stringify(`Unknown course: ${request.course}`)}; + } + + const auth = new Auth.GoogleAuth({ + credentials: CREDENTIALS, + scopes: "https://www.googleapis.com/auth/spreadsheets", + }); + + const sheets = google.sheets({version: "v4", auth}); + + try { + const response = await sheets.spreadsheets.values.append({ + spreadsheetId: COURSE_TO_SPREADSHEET_ID[request.course], + // Trust that the user-supplied module exists. + // If it's wrong, we'll get a 400 from the API and pass it on to the user without much useful detail. + range: request.module, + valueInputOption: "RAW", + insertDataOption: "INSERT_ROWS", + resource: { + values: [ + [request.name, request.email, new Date().toISOString(), request.course, request.module, request.day, request.location, request.buildTime], + ], + }, + }); + } catch (error) { + console.error("Error from Google API", error); + let message = "An error occurred signing the register"; + if (error.errors) { + if ("message" in error.errors[0]) { + message += ": " + error.errors[0].message; + } + } + return {statusCode: 400, body: JSON.stringify(message)}; + } + + return { + statusCode: 200, + body: JSON.stringify("You have successfully signed the register"), + }; +}; + +export { handler };