Skip to content

Commit

Permalink
Adjust grid validation logic to run in spreadsheet
Browse files Browse the repository at this point in the history
The event data can now be validated from within the spreadsheet.

One caveat: the appscript runtime has no support for the URL primitive, so URL
validation is minimal.

Still todo:
- Validation results are reported as a JSON dump for now. Next step is to
report them in the grid sheet itself.
- The W3C_ID variable isn't stored anywhere for now so validation of chairs
who haven't connected their GitHub and W3C accounts currently fails.
  • Loading branch information
tidoust committed Feb 18, 2025
1 parent aa2b462 commit 47b95d4
Show file tree
Hide file tree
Showing 9 changed files with 150 additions and 82 deletions.
4 changes: 2 additions & 2 deletions tools/appscript/export-grid.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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`);
}
}
Expand Down
59 changes: 36 additions & 23 deletions tools/appscript/generate-grid.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
}


Expand Down
30 changes: 25 additions & 5 deletions tools/appscript/project.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
Expand Down Expand Up @@ -278,13 +292,22 @@ function setValues(sheet, values) {
if (value === 'vip room') {
return 'vip';
}
if (value === 'author id') {
return 'authorid';
}
return value;
});
console.log(' - sheet headers', headers);

// 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 '';
}
Expand All @@ -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);
Expand Down Expand Up @@ -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'
Expand Down
41 changes: 32 additions & 9 deletions tools/appscript/validate-grid.mjs
Original file line number Diff line number Diff line change
@@ -1,19 +1,42 @@
import reportError from './report-error.mjs';
import { getProject } from './project.mjs';
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(
'<pre>' + JSON.stringify(res, null, 2) + '</pre>'
)
.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(
'<pre>' + JSON.stringify(res, null, 2) + '</pre>'
)
.setWidth(300)
.setHeight(400);
SpreadsheetApp.getUi().showModalDialog(htmlOutput, 'Validation result');
console.log('Report validation result... done');
}
catch(err) {
reportError(err.toString());
return;
}
}
32 changes: 18 additions & 14 deletions tools/common/session-sections.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
54 changes: 29 additions & 25 deletions tools/common/session.mjs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
});
Expand Down Expand Up @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion tools/common/validate.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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() -
Expand Down
5 changes: 3 additions & 2 deletions tools/common/w3c.mjs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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}`
);

Expand Down Expand Up @@ -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}`);
}
Expand Down
4 changes: 3 additions & 1 deletion tools/common/wrappedfetch.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down

0 comments on commit 47b95d4

Please sign in to comment.