diff --git a/tools/appscript/export-grid.mjs b/tools/appscript/export-grid.mjs index 5d8fb00..9926f78 100644 --- a/tools/appscript/export-grid.mjs +++ b/tools/appscript/export-grid.mjs @@ -79,9 +79,9 @@ export default async function () { console.warn(`- updating meeting info for #${ghSession.number}... done`); } - if ((ghSession.validation.note ?? '') !== (ssSession.note ?? '')) { + if ((ghSession.validation.note ?? '') !== (ssSession.validation.note ?? '')) { console.warn(`- updating note for #${ghSession.number}...`); - await saveSessionNote(ghSession, ssSession.note, githubProject); + await saveSessionNote(ghSession, ssSession.validation.note, githubProject); console.warn(`- updating note for #${ghSession.number}... done`); } } diff --git a/tools/appscript/generate-grid.mjs b/tools/appscript/generate-grid.mjs index 473d0b3..3a32bdd 100644 --- a/tools/appscript/generate-grid.mjs +++ b/tools/appscript/generate-grid.mjs @@ -13,31 +13,44 @@ export default function () { * Generate the grid in the provided spreadsheet */ function generateGrid(spreadsheet) { - // These are the sheets we expect to find - const project = getProject(spreadsheet); - if (!project.sheets.sessions.sheet) { - reportError('No sheet found that contains the list of sessions, please import data from GitHub first.'); + try { + console.log('Read data from spreadsheet...'); + const project = getProject(spreadsheet); + if (!project.sheets.sessions.sheet) { + reportError('No sheet found that contains the list of sessions, please import data from GitHub first.'); + return; + } + console.log('Read data from spreadsheet... done'); + + console.log('Generate grid sheet...'); + const sheet = project.sheets.grid.sheet; + sheet.clear(); + console.log('- sheet cleared'); + createHeaderRow(sheet, project.rooms); + console.log('- header row created'); + createDaySlotColumns(sheet, project.days, project.slots); + console.log('- days/slots headers created'); + addSessions(sheet, + project.sessions, + project.sessions, // TODO: real meetings for TPAC group meetings! + project.rooms, + project.days, + project.slots, + spreadsheet.getUrl() + '#gid=' + project.sheets.meetings.sheet.getSheetId() + ); + console.log('- sessions added to the grid'); + addBorders(sheet, + project.rooms, + project.days, + project.slots + ); + console.log('- borders added'); + console.log('Generate grid sheet... done'); + } + catch(err) { + reportError(err.toString()); return; } - - // Re-generate the grid view - const sheet = project.sheets.grid.sheet; - sheet.clear(); - createHeaderRow(sheet, project.rooms); - createDaySlotColumns(sheet, project.days, project.slots); - addSessions(sheet, - project.sessions, - project.sessions, // TODO: real meetings for TPAC group meetings! - project.rooms, - project.days, - project.slots, - spreadsheet.getUrl() + '#gid=' + project.sheets.meetings.sheet.getSheetId() - ); - addBorders(sheet, - project.rooms, - project.days, - project.slots - ); } diff --git a/tools/appscript/project.mjs b/tools/appscript/project.mjs index 37ee21f..379c862 100644 --- a/tools/appscript/project.mjs +++ b/tools/appscript/project.mjs @@ -213,7 +213,21 @@ export function getProject(spreadsheet) { labels: [], // TODO: complete with meetings sheet if it exists - sessions: sheets.sessions.values, + sessions: (sheets.sessions?.values ?? []).map(session => + Object.assign(session, { + author: { + databaseId: session['author id'], + login: session.author + }, + labels: (session.labels ?? '').split(', '), + validation: { + check: session.check, + warning: session.warning, + error: session.error, + note: session.note + } + }) + ), sheets: sheets }; @@ -278,6 +292,9 @@ function setValues(sheet, values) { if (value === 'vip room') { return 'vip'; } + if (value === 'author id') { + return 'authorid'; + } return value; }); console.log(' - sheet headers', headers); @@ -285,6 +302,12 @@ function setValues(sheet, values) { // Values is an array of indexed objects, while we need a two-dimensional // array of raw values. Let's convert the values. const rawValues = values.map((obj, vidx) => headers.map(header => { + if (header === 'author' && obj.author?.login) { + return obj.author.login; + } + if (header === 'authorid' && obj.author?.databaseId) { + return obj.author.databaseId; + } if (!Object.hasOwn(obj, header)) { return ''; } @@ -300,9 +323,6 @@ function setValues(sheet, values) { if (header === 'labels' && obj[header]) { return obj[header].join(', '); } - if (header === 'author' && obj[header].login) { - return obj[header].login; - } return obj[header]; })); console.log(' - raw values to set', rawValues); @@ -508,7 +528,7 @@ function createSessionsSheet(spreadsheet, sheets, project) { // Set the headers row const headers = [ - 'Number', 'Title', 'Author', 'Body', 'Labels', + 'Number', 'Title', 'Author', 'Author ID', 'Body', 'Labels', 'Room', 'Day', 'Slot', 'Meeting', 'Error', 'Warning', 'Check', 'Note', 'Registrants' diff --git a/tools/appscript/validate-grid.mjs b/tools/appscript/validate-grid.mjs index 4ab6c39..20ed534 100644 --- a/tools/appscript/validate-grid.mjs +++ b/tools/appscript/validate-grid.mjs @@ -1,3 +1,4 @@ +import reportError from './report-error.mjs'; import { getProject } from './project.mjs'; import { validateGrid } from '../common/validate.mjs'; @@ -5,15 +6,37 @@ import { validateGrid } from '../common/validate.mjs'; * Export the event data as JSON */ export default async function () { - const project = getProject(SpreadsheetApp.getActiveSpreadsheet()); + try { + console.log('Read data from spreadsheet...'); + const project = getProject(SpreadsheetApp.getActiveSpreadsheet()); + if ((project.metadata.type === 'tpac-breakouts') || + (project.metadata.type === 'breakouts-day')) { + // Only two types of events from an internal perspective + project.metadata.type = 'breakouts'; + } + console.log('Read data from spreadsheet... done'); - const res = await validateGrid(project, { what: 'everything' }); + console.log('Validate the grid...'); + const res = await validateGrid(project, { what: 'everything' }); + console.log('Validate the grid... done'); - const htmlOutput = HtmlService - .createHtmlOutput( - '
' + JSON.stringify(res, null, 2) + '' - ) - .setWidth(300) - .setHeight(400); - SpreadsheetApp.getUi().showModalDialog(htmlOutput, 'Validation result'); + console.log('Refresh grid view, with validation result...'); + console.log('- TODO: re-generate grid'); + console.log('- TODO: report validation result'); + console.log('Refresh grid view, with validation result... done'); + + console.log('Report validation result...'); + const htmlOutput = HtmlService + .createHtmlOutput( + '
' + JSON.stringify(res, null, 2) + '' + ) + .setWidth(300) + .setHeight(400); + SpreadsheetApp.getUi().showModalDialog(htmlOutput, 'Validation result'); + console.log('Report validation result... done'); + } + catch(err) { + reportError(err.toString()); + return; + } } \ No newline at end of file diff --git a/tools/common/session-sections.mjs b/tools/common/session-sections.mjs index 2c7a764..9347d05 100644 --- a/tools/common/session-sections.mjs +++ b/tools/common/session-sections.mjs @@ -7,20 +7,24 @@ export function getSessionSections(template) { // purpose of validation and serialization, we need to add them to the list // of sections (as custom "auto hide" sections that only get displayed when // they are not empty). - sessionSections.push({ - id: 'calendar', - attributes: { - label: 'Links to calendar', - autoHide: true - } - }); - sessionSections.push({ - id: 'materials', - attributes: { - label: 'Meeting materials', - autoHide: true - } - }); + if (!sessionSections.find(section => section.id === 'calendar')) { + sessionSections.push({ + id: 'calendar', + attributes: { + label: 'Links to calendar', + autoHide: true + } + }); + } + if (!sessionSections.find(section => section.id === 'materials')) { + sessionSections.push({ + id: 'materials', + attributes: { + label: 'Meeting materials', + autoHide: true + } + }); + } return sessionSections; } \ No newline at end of file diff --git a/tools/common/session.mjs b/tools/common/session.mjs index 01cea8e..7668085 100644 --- a/tools/common/session.mjs +++ b/tools/common/session.mjs @@ -1,6 +1,29 @@ import { sendGraphQLRequest } from './graphql.mjs'; import todoStrings from './todostrings.mjs'; +/** + * Returns true if the given URL is valid, false otherwise. + * + * Unfortunately, the Appscript runtime does not support the URL object, + * so we'll fallback to a simple regular expression in that case. It is + * possible that the validation on GitHub rejects a URL that validation in the + * spreadsheet accepts, but so be it. + */ +function isUrlValid(url) { + if (typeof URL === 'undefined') { + return /^https?:\/\/[^ "]+$/.test(url); + } + else { + try { + new URL(url); + return true; + } + catch (err) { + return false; + } + } +} + /** * The list of sections that may be found in a session body and, for each of @@ -160,18 +183,7 @@ export async function initSectionHandlers(project) { }; handler.validate = value => { const match = value.match(/^\[(.+)\]\((.*)\)$/i); - try { - if (match) { - new URL(match[2]); - } - else { - new URL(value); - } - return true; - } - catch (err) { - return false; - } + return isUrlValid(match ? match[2] : value); }; break; @@ -325,14 +337,10 @@ export async function initSectionHandlers(project) { if (!match) { return false; } - try { - new URL(match[2]); - return !!match[1].match(reCalendarInfo); - } - catch { + if (!isUrlValid(match[2])) { return false; } - + return !!match[1].match(reCalendarInfo); }); }; handler.serialize = value => value @@ -361,13 +369,7 @@ export async function initSectionHandlers(project) { return false; } if (!todoStrings.includes(match[2].toUpperCase())) { - try { - new URL(match[2]); - return true; - } - catch (err) { - return false; - } + return isUrlValid(match[2]); } return true; }); @@ -425,6 +427,8 @@ export function validateSessionBody(body) { return `Unexpected empty section "${section.title}"`; } if (section.value && !sectionHandler.validate(section.value)) { + console.warn(`Invalid content in section "${section.title}": + ${section.value}`); return `Invalid content in section "${section.title}"`; } return null; diff --git a/tools/common/validate.mjs b/tools/common/validate.mjs index 86979db..8cdc954 100644 --- a/tools/common/validate.mjs +++ b/tools/common/validate.mjs @@ -631,7 +631,8 @@ ${projectErrors.map(error => '- ' + error).join('\n')}`); const minutesNeeded = meetings .filter(meeting => meeting.room && meeting.day && meeting.slot) .find(meeting => { - const day = project.days.find(d => d.name === meeting.day); + const day = project.days.find(d => + d.name === meeting.day || d.date === meeting.day); const twoDaysInMs = 48 * 60 * 60 * 1000; return ( (new Date()).getTime() - diff --git a/tools/common/w3c.mjs b/tools/common/w3c.mjs index 9afcac1..a8ea723 100644 --- a/tools/common/w3c.mjs +++ b/tools/common/w3c.mjs @@ -1,4 +1,5 @@ import { getEnvKey } from './envkeys.mjs'; +import wrappedFetch from './wrappedfetch.mjs'; /** * Internal memory cache to avoid sending the same request more than once @@ -40,7 +41,7 @@ export async function fetchW3CAccount(databaseId) { return cache[databaseId]; } - const res = await fetch( + const res = await wrappedFetch( `https://api.w3.org/users/connected/github/${databaseId}` ); @@ -135,7 +136,7 @@ export async function fetchW3CGroups() { const groups = []; for (const groupType of ['bg', 'cg', 'ig', 'wg', 'other', 'tf']) { - const res = await fetch(`https://api.w3.org/groups/${groupType}?embed=1&items=200`); + const res = await wrappedFetch(`https://api.w3.org/groups/${groupType}?embed=1&items=200`); if (res.status !== 200) { throw new Error(`W3C API server returned an unexpected HTTP status ${res.status}`); } diff --git a/tools/common/wrappedfetch.mjs b/tools/common/wrappedfetch.mjs index dfa0506..07455f5 100644 --- a/tools/common/wrappedfetch.mjs +++ b/tools/common/wrappedfetch.mjs @@ -2,7 +2,9 @@ export default async function (url, options) { options = options ?? {}; if (typeof UrlFetchApp !== 'undefined') { // AppScript runtime, cannot use fetch directly - const params = {}; + const params = { + muteHttpExceptions: true + }; if (options.method) { params.method = options.method; }