From 3117f77b8dd342c0c30ee2a97edb1310f98a8bc7 Mon Sep 17 00:00:00 2001 From: Francois Daoust Date: Tue, 10 Sep 2024 15:09:36 +0200 Subject: [PATCH] Add ability to read room metadata from the room description Implements part of https://github.com/w3c/tpac-breakouts/issues/134#issuecomment-2155995592, making it possible to define the room's metadata within the description field next to the definition of the room in the GitHub project. Note that this does not make any practical change to available semantics. The code does not yet support any more metadata key (such as "availability"). To be done later on. --- test/check-room-metadata.mjs | 98 ++++++++++++++++++++++++++++++++++++ test/data/room-metadata.mjs | 47 +++++++++++++++++ test/stubs.mjs | 16 +++++- tools/lib/project.mjs | 35 ++++++++----- 4 files changed, 183 insertions(+), 13 deletions(-) create mode 100644 test/check-room-metadata.mjs create mode 100644 test/data/room-metadata.mjs diff --git a/test/check-room-metadata.mjs b/test/check-room-metadata.mjs new file mode 100644 index 0000000..9d2249d --- /dev/null +++ b/test/check-room-metadata.mjs @@ -0,0 +1,98 @@ +import * as assert from 'node:assert'; +import { initTestEnv } from './init-test-env.mjs'; +import { getEnvKey, setEnvKey } from '../tools/lib/envkeys.mjs'; +import { fetchProject } from '../tools/lib/project.mjs'; + +async function fetchTestProject() { + const project = await fetchProject( + await getEnvKey('PROJECT_OWNER'), + await getEnvKey('PROJECT_NUMBER')); + return project; +} + +function assertRoom(project, name, metadata) { + const room = project.rooms.find(r => r.name === name); + assert.ok(room, `Room "${name}" not found in project`); + const roomCopy = Object.assign({}, room); + if (roomCopy.id) { + delete roomCopy.id; + } + const metadataCopy = Object.assign({}, metadata); + metadataCopy.name = metadata.name ?? name; + assert.deepStrictEqual(roomCopy, metadataCopy); +} + +describe('The room definition', function () { + before(function () { + initTestEnv(); + setEnvKey('PROJECT_NUMBER', 'room-metadata'); + setEnvKey('ISSUE_TEMPLATE', 'test/data/template-breakout.yml'); + }); + + it('may be just a name', async function () { + const project = await fetchTestProject(); + const name = 'Just a room'; + assertRoom(project, name, { + label: name, + location: '', + capacity: 30, + vip: false + }); + }); + + it('may inline room information in the name', async function () { + const project = await fetchTestProject(); + const name = 'Inline (75 - basement) (VIP)'; + assertRoom(project, name, { + label: 'Inline', + capacity: 75, + location: 'basement', + vip: true + }); + }); + + it('may contain VIP info in the description', async function () { + const project = await fetchTestProject(); + const name = 'VIP room'; + assertRoom(project, name, { + label: name, + location: '', + capacity: 25, + vip: true + }); + }); + + it('may contain additional metadata in the description', async function () { + const project = await fetchTestProject(); + const name = 'In the back'; + assertRoom(project, name, { + label: name, + location: '2nd floor', + capacity: 40, + vip: false, + type: 'backroom' + }); + }); + + it('may contain invalid metadata in the description', async function () { + const project = await fetchTestProject(); + const name = 'Weird'; + assertRoom(project, name, { + label: name, + location: 'somewhere', + capacity: 30, + vip: false + }); + }); + + it('may define metadata inline and in the description', async function () { + const project = await fetchTestProject(); + const name = 'Hybrid (42)'; + assertRoom(project, name, { + label: 'Hybrid', + location: 'on ze web', + capacity: 42, + vip: false + }); + }); +}); \ No newline at end of file diff --git a/test/data/room-metadata.mjs b/test/data/room-metadata.mjs new file mode 100644 index 0000000..170315f --- /dev/null +++ b/test/data/room-metadata.mjs @@ -0,0 +1,47 @@ +export default { + description: 'meeting: Validation of room metadata, timezone: Etc/UTC', + + days: [ + '2042-04-05' + ], + + slots: [ + '9:00 - 10:00', + '10:00 - 11:00' + ], + + rooms: [ + 'Just a room', + 'Inline (75 - basement) (VIP)', + { + name: 'VIP room', + description: ` +- capacity: 25 +- vip: true +` + }, + { + name: 'In the back', + description: ` +* location: 2nd floor +* capacity: 40 +* vip: false +* type: backroom` + }, + { + name: 'Weird', + description: ` +- +- yes +- location: somewhere` + }, + { + name: 'Hybrid (42)', + description: `capacity: 35 +location: on ze web` + } + ], + + sessions: [ + ] +}; \ No newline at end of file diff --git a/test/stubs.mjs b/test/stubs.mjs index 6ba42f0..f643186 100644 --- a/test/stubs.mjs +++ b/test/stubs.mjs @@ -39,7 +39,21 @@ async function getTestData(testDataId) { } function toGraphQLNameList(arr) { - return arr.map(name => Object.assign({ id: `id_${uid++}`, name })); + return arr.map(item => { + if (typeof item === 'string') { + return { + id: `id_${uid++}`, + name: item + }; + } + else { + return { + id: `id_${uid++}`, + name: item.name, + description: item.description + }; + } + }); } function toGraphQLAuthor(login) { diff --git a/tools/lib/project.mjs b/tools/lib/project.mjs index 2d7c0fa..46c34a5 100644 --- a/tools/lib/project.mjs +++ b/tools/lib/project.mjs @@ -557,7 +557,6 @@ export async function fetchProject(login, id) { ... on ProjectV2SingleSelectFieldOption { id name - description } } } @@ -579,7 +578,6 @@ export async function fetchProject(login, id) { ... on ProjectV2SingleSelectFieldOption { id name - description } } } @@ -762,10 +760,10 @@ export async function fetchProject(login, id) { metadata: parseProjectDescription(project.shortDescription), // List of rooms. For each of them, we return the exact name of the option - // for the "Room" custom field in the project (which includes all info), - // the actual room label, the room's capacity in number of seats, the - // location of the room, and the possible "vip" flag. - // The room's full name should follow the pattern: + // for the "Room" custom field in the project. If the exact name can be + // split into a room label, capacity in number of seats, location, and the + // possible "vip" flag, then that information is used to initialize the + // room's metadata. The room's full name should follow the pattern: // "label (xx - location) (vip)" // Examples: // Catalina (25) @@ -774,18 +772,31 @@ export async function fetchProject(login, id) { // Business (vip) // Small (15) // Plenary (150 - 18th floor) (vip) + // The exact same information can be provided using actual metadata in the + // description of the room, given as a list of key/value pairs such as: + // - capacity: 40 + // - location: 2nd floor + // Possible metadata keys are expected to evolve over time. If the + // information is duplicated in the room name and in metadata, the + // information in the room name will be used roomsFieldId: rooms.id, rooms: rooms.options.map(room => { + const metadata = {}; + (room.description ?? '') + .split(/\n/) + .map(line => line.trim().replace(/^[*\-] /, '').split(/:\s*/)) + .filter(data => data[0] && data[1]) + .filter(data => data[0].toLowerCase() !== 'capacity' || data[1]?.match(/^\d+$/)) + .forEach(data => metadata[data[0].toLowerCase()] = data[1]); const match = room.name.match(/^(.*?)(?:\s*\((\d+)\s*(?:\-\s*([^\)]+))?\))?(?:\s*\((vip)\))?$/i); - return { + return Object.assign(metadata, { id: room.id, name: match[0], label: match[1], - location: match[3] ?? '', - capacity: parseInt(match[2] ?? '30', 10), - vip: !!match[4], - description: room.description - }; + location: match[3] ?? metadata.location ?? '', + capacity: parseInt(match[2] ?? metadata.capacity ?? '30', 10), + vip: !!match[4] || (metadata.vip === 'true') + }); }), // IDs of custom fields used to store validation problems