datatracker/playwright/helpers/meeting.js
Nicolas Giard d1ec7378da
fix: use groupAcronym to determine hackathon icon in agenda (#8540)
* fix: use groupAcronym to determine hackathon icon in agenda

* test: fix agenda tests related to hackathon
2025-02-14 17:14:03 -06:00

675 lines
21 KiB
JavaScript

const { DateTime } = require('luxon')
const { faker } = require('@faker-js/faker')
const seedrandom = require('seedrandom')
const _ = require('lodash')
const slugify = require('slugify')
const ms = require('ms')
const floorsMeta = require('../data/meeting-floors')
const urlRe = /http[s]?:\/\/(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*(),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+/
const conferenceDomains = ['webex.com', 'zoom.us', 'jitsi.org', 'meetecho.com', 'gather.town']
const xslugify = (str) => slugify(str.replace('/', '-'), { lower: true, strict: true })
const TEST_SEED = 123
const sessionsWithNotes = [3, 6, 20, 48, 49, 60]
const sessionsCancelled = [29, 93]
const sessionsRescheduled = [76]
const sessionsMissingAgenda = [5, 10]
const sessionsWithWebex = [3, 4]
// Use constant randomness seed
seedrandom(TEST_SEED.toString(), { global: true })
faker.seed(TEST_SEED)
const { random, sample, sampleSize } = _.runInContext()
/**
* Generate area response from label + children
*/
function createArea ({ label, children = [] }) {
return {
label,
keyword: xslugify(label),
toggled_by: [],
is_bof: false,
children: children.map(gr => {
gr.toggled_by.push(xslugify(label))
return gr
})
}
}
/**
* Generate group response from label
*/
const uniqueGroupNames = []
function createGroup ({ label, mayBeBof = false, toggledBy = [] }) {
// make sure group name is unique
while (!label) {
const nameAttempt = faker.word.verb()
if (!uniqueGroupNames.includes(nameAttempt)) {
label = nameAttempt
uniqueGroupNames.push(nameAttempt)
}
}
// Set toggledBy
if (!toggledBy) {
toggledBy = []
}
// 10% chance of BoF, if enabled
const isBof = mayBeBof && random(0, 100) < 10
if (isBof) {
toggledBy.push('bof')
}
return {
label,
keyword: xslugify(label),
toggled_by: toggledBy,
is_bof: isBof
}
}
/**
* Find area and group based on group slug
*/
function findAreaGroup (slug, areas) {
for (const area of areas) {
for (const group of area.children) {
if (group.keyword === slug) {
return { area, group }
}
}
}
throw new Error('Requested group does not exist!')
}
/**
* Reverse areas and groups mapping
*/
function reverseAreaGroupsMapping (areas) {
const groups = []
for (const area of areas) {
for (const group of area.children) {
groups.push({
...group,
area
})
}
}
return groups
}
function getEventStatus (idx) {
if (sessionsCancelled.includes(idx)) {
return 'canceled'
} else if (sessionsRescheduled.includes(idx)) {
return 'resched'
} else {
return 'sched'
}
}
/**
* Generate event
*/
let lastEventId = 100000
let lastSessionId = 25000
let lastRecordingId = 150000
function createEvent ({
name = '',
slotName = '',
startDateTime,
duration = '1h',
area,
group,
type = 'other',
status = 'sched',
hasLocation = true,
hasNote = false,
hasAgenda = false,
showAgenda = false,
hasRecordings = false,
hasVideoStream = true,
hasWebex = false,
isBoF = false
}, floors) {
const floor = sample(floors)
const room = hasLocation ? sample(floor.rooms) : { name: 'Somewhere' }
const eventName = name ?? faker.lorem.sentence(random(2, 5))
return {
id: ++lastEventId,
sessionId: ++lastSessionId,
room: room.name,
location: hasLocation
? {
short: floor.short,
name: floor.name
}
: {},
acronym: group.keyword,
duration: typeof duration === 'string' ? ms(duration) / 1000 : duration,
name: eventName,
slotName: slotName,
startDateTime: startDateTime.toISO({ includeOffset: false, suppressMilliseconds: true }),
status,
type,
isBoF,
filterKeywords: [
'coding',
'hackathon',
'hackathon-sessc'
],
groupAcronym: group.keyword,
groupName: faker.lorem.sentence(random(2, 5)),
groupParent: {
acronym: area.keyword
},
note: (hasNote || status === 'resched') ? faker.lorem.sentence(4) : '',
remoteInstructions: '',
flags: {
agenda: hasAgenda,
showAgenda
},
agenda: {
url: hasAgenda ? 'https://datatracker.ietf.org/meeting/123/materials/agenda-123-ietf-sessa-00' : null
},
orderInMeeting: 1,
short: eventName,
sessionToken: 'sessa',
links: {
chat: `https://zulip.ietf.org/#narrow/stream/${group.keyword}`,
chatArchive: `https://zulip.ietf.org/#narrow/stream/${group.keyword}`,
recordings: hasRecordings
? [
{
id: ++lastRecordingId,
name: `recording-123-${group.keyword}-1`,
title: `Video recording for ${group.keyword} on ${startDateTime.toFormat('yyyy-LL-dd \'at\' HH:mm:ss')}`,
url: 'https://www.youtube.com/watch?v=1eq_5xvacl0'
}
]
: [],
videoStream: showAgenda && hasVideoStream ? 'https://meetings.conf.meetecho.com/ietf{meeting.number}/?group={group.acronym}&short={short}&item={order_number}' : null,
audioStream: hasAgenda ? 'https://mp3.conf.meetecho.com/ietf123/{group.acronym}/{order_number}.m3u' : null,
webex: hasWebex ? 'https://webex.com/123' : null,
onsiteTool: hasAgenda ? 'https://meetings.conf.meetecho.com/onsite{meeting.number}/?group={group.acronym}&short={short}&item={order_number}' : null,
calendar: `/meeting/123/session/${lastSessionId}.ics`
}
}
}
module.exports = {
/**
* Generate a standard agenda data reponse
*/
generateAgendaResponse ({ dateMode = 'past', skipSchedule = false } = {}) {
// Get random date but always start on a saturday
let startDate = null
switch (dateMode) {
case 'current': {
startDate = DateTime.fromISO('2022-02-01T13:45:15', { zone: 'Asia/Tokyo' }).startOf('week').minus({ days: 2 })
break
}
case 'future': {
startDate = DateTime.fromISO(faker.date.future({ years: 1 }).toISOString(), { zone: 'Asia/Tokyo' }).startOf('week').minus({ days: 2 })
break
}
default: {
startDate = DateTime.fromISO(faker.date.past({ years: 5, refDate: DateTime.utc().minus({ months: 3 }) }).toISOString(), { zone: 'Asia/Tokyo' }).startOf('week').minus({ days: 2 })
break
}
}
const endDate = startDate.plus({ days: 7 })
// Generate floors
const floors = _.times(6, (idx) => {
const floorIdx = idx + 1
const floor = floorsMeta[idx]
return {
id: floorIdx,
image: `/media/floor/${floor.path}`,
name: `Level ${_.startCase(faker.color.human())} ${floorIdx}`,
short: `L${floorIdx}`,
width: floor.width,
height: floor.height,
rooms: _.times(random(5, 10), (ridx) => {
const roomName = `${faker.science.chemicalElement().name} ${floorIdx}-${ridx + 1}`
// Keep 10% margin on each side
const roomXUnit = Math.round(floor.width / 10)
const roomYUnit = Math.round(floor.height / 10)
const roomX = random(roomXUnit, roomXUnit * 8)
const roomY = random(roomYUnit, roomYUnit * 8)
return {
id: floorIdx * 100 + ridx,
name: roomName,
functionalName: _.startCase(faker.lorem.words(2)),
slug: xslugify(roomName),
left: roomX,
right: roomX + roomXUnit,
top: roomY,
bottom: roomY + roomYUnit
}
})
}
})
// Generate categories (groups/areas)
const categories = []
if (!skipSchedule) {
// Generate first group of areas
// -----------------------------
const firstAreas = []
const firstAreasNames = ['ABC', 'DEF', 'GHI', 'JKL', 'MNO', 'PQR', 'STU']
for (const area of firstAreasNames) {
firstAreas.push(createArea({
label: area,
children: _.times(random(2, 25), (idx) => {
return createGroup({ mayBeBof: true })
})
}))
}
categories.push(firstAreas)
// Generate second group of areas
// ------------------------------
const secondAreas = []
for (const area of ['UVW', 'XYZ0']) {
secondAreas.push(createArea({
label: area,
children: _.times(random(2, 25), (idx) => {
return createGroup({ mayBeBof: true })
})
}))
}
categories.push(secondAreas)
// Generate last group of areas
// ----------------------------
categories.push(
[
createArea({
label: 'Administrative',
children: [
createGroup({ label: 'IETF Registration' })
]
}),
createArea({
label: 'Coding',
children: [
createGroup({ label: 'Hackathon', toggledBy: ['hackathon'] }),
createGroup({ label: 'Code Sprint', toggledBy: ['tools'] })
]
}),
createArea({
label: 'Office hours',
children: firstAreasNames.map(n => createGroup({ label: `${n} Office Hours` }))
}),
createArea({
label: 'Open meeting',
children: [
createGroup({ label: 'WG Chairs Forum' }),
createGroup({ label: 'Newcomers\' Feedback Session' })
]
}),
createArea({
label: 'Plenary',
children: [
createGroup({ label: 'IETF Plenary', toggledBy: ['ietf'] })
]
}),
createArea({
label: 'Presentation',
children: [
createGroup({ label: 'Hackathon Kickoff', toggledBy: ['hackathon'] }),
createGroup({ label: 'Hackathon Project Results Presentations', toggledBy: ['hackathon'] }),
createGroup({ label: 'Host Speaker Series', toggledBy: ['ietf'] })
]
}),
createArea({
label: 'Social',
children: [
createGroup({ label: 'Newcomers\' Quick Connections' }),
createGroup({ label: 'Welcome Reception', toggledBy: ['ietf'] }),
createGroup({ label: 'Break', toggledBy: ['secretariat'] }),
createGroup({ label: 'Beverage and Snack Break', toggledBy: ['secretariat'] }),
createGroup({ label: 'Hackdemo Happy Hour', toggledBy: ['hackathon'] })
]
}),
createArea({
label: 'Tutorial',
children: [
createGroup({ label: 'Tutorial: Newcomers\' Overview' })
]
}),
createArea({
label: '',
children: [
createGroup({ label: 'BoF' }),
createGroup({ label: 'qwerty', toggledBy: ['abc'] }),
createGroup({ label: 'azerty', toggledBy: ['def'] }),
createGroup({ label: 'Tools' })
]
})
]
)
}
// Generate schedule
const schedule = []
if (!skipSchedule) {
let sessionIdx = 0
const daySessions = []
const regGroups = reverseAreaGroupsMapping([...categories[0], ...categories[1]])
// DAY 1 - No regular sessions
// ---------------------------
const day1 = startDate
schedule.push(createEvent({
name: 'Hackathon',
startDateTime: day1.set({ hour: 9, minute: 30 }),
duration: '11.5h',
...findAreaGroup('hackathon', categories[2]),
showAgenda: true,
hasAgenda: true,
hasRecordings: true,
hasVideoStream: false
}, floors))
schedule.push(createEvent({
name: 'Code Sprint',
startDateTime: day1.set({ hour: 10 }),
duration: '12h',
...findAreaGroup('code-sprint', categories[2])
}, floors))
schedule.push(createEvent({
name: 'Hackathon Kickoff',
startDateTime: day1.set({ hour: 10, minute: 30 }),
duration: '30m',
...findAreaGroup('hackathon', categories[2]),
showAgenda: true,
hasAgenda: true,
hasRecordings: true,
hasVideoStream: false
}, floors))
// DAY 2 - No regular sessions
// ---------------------------
const day2 = startDate.plus({ days: 1 })
schedule.push(createEvent({
name: 'Hackathon',
startDateTime: day2.set({ hour: 9, minute: 30 }),
duration: '6.5h',
...findAreaGroup('hackathon', categories[2]),
showAgenda: true,
hasAgenda: true,
hasVideoStream: false
}, floors))
schedule.push(createEvent({
name: 'IETF Registration',
startDateTime: day2.set({ hour: 10 }),
duration: '8h',
...findAreaGroup('ietf-registration', categories[2])
}, floors))
schedule.push(createEvent({
name: 'Tutorial: Newcomers',
startDateTime: day2.set({ hour: 12, minute: 30 }),
duration: '1h',
...findAreaGroup('tutorial-newcomers-overview', categories[2]),
showAgenda: true,
hasRecordings: true,
hasVideoStream: true
}, floors))
schedule.push(createEvent({
name: 'Hackathon Results Presentations',
startDateTime: day2.set({ hour: 14 }),
duration: '2h',
...findAreaGroup('hackathon-project-results-presentations', categories[2])
}, floors))
schedule.push(createEvent({
name: 'Newcomers\' Quick Connections (Note that pre-registration is required)',
startDateTime: day2.set({ hour: 16 }),
duration: '1h',
...findAreaGroup('newcomers-quick-connections', categories[2]),
hasLocation: false
}, floors))
schedule.push(createEvent({
name: 'ABC AD Office Hours',
startDateTime: day2.set({ hour: 16 }),
duration: '1h',
...findAreaGroup('abc-office-hours', categories[2])
}, floors))
schedule.push(createEvent({
name: 'DEF AD Office Hours',
startDateTime: day2.set({ hour: 16, minute: 15 }),
duration: '45m',
...findAreaGroup('def-office-hours', categories[2])
}, floors))
schedule.push(createEvent({
name: 'Welcome Reception',
startDateTime: day2.set({ hour: 17 }),
duration: '2h',
...findAreaGroup('welcome-reception', categories[2])
}, floors))
// DAY 3-7 - Regular Sessions
// --------------------------
for (let dayIdx = 2; dayIdx < 7; dayIdx++) {
const curDay = startDate.plus({ days: dayIdx })
daySessions.push(...sampleSize(regGroups, 24))
schedule.push(createEvent({
name: 'Continental Breakfast',
startDateTime: curDay.set({ hour: 8, minute: 30 }),
duration: '1.5h',
type: 'break',
...findAreaGroup('beverage-and-snack-break', categories[2])
}, floors))
schedule.push(createEvent({
name: 'ABC AD Office Hours',
startDateTime: curDay.set({ hour: 8, minute: 30 }),
duration: '8.5h',
...findAreaGroup('abc-office-hours', categories[2])
}, floors))
schedule.push(createEvent({
name: 'IETF Registration',
startDateTime: curDay.set({ hour: 8, minute: 30 }),
duration: '8h',
...findAreaGroup('ietf-registration', categories[2])
}, floors))
schedule.push(createEvent({
name: 'DEF AD Office Hours',
startDateTime: curDay.set({ hour: 9 }),
duration: '8.5h',
...findAreaGroup('def-office-hours', categories[2])
}, floors))
schedule.push(createEvent({
name: 'GHI AD Office Hours',
startDateTime: curDay.set({ hour: 9 }),
duration: '30m',
...findAreaGroup('ghi-office-hours', categories[2]),
hasLocation: false
}, floors))
// -> Session I
_.times(8, () => { // 8 lanes per session time
const { area, ...group } = daySessions.pop()
schedule.push(createEvent({
slotName: 'Session I',
startDateTime: curDay.set({ hour: 10 }),
duration: '2h',
type: 'regular',
group,
area,
status: getEventStatus(sessionIdx),
hasNote: sessionsWithNotes.includes(sessionIdx),
isBoF: group.is_bof,
showAgenda: true,
hasAgenda: !sessionsMissingAgenda.includes(sessionIdx),
hasRecordings: !sessionsMissingAgenda.includes(sessionIdx),
hasWebex: sessionsWithWebex.includes(sessionIdx)
}, floors))
sessionIdx++
})
schedule.push(createEvent({
name: 'Break',
startDateTime: curDay.set({ hour: 12 }),
duration: '1.5h',
type: 'break',
...findAreaGroup('beverage-and-snack-break', categories[2])
}, floors))
// -> Session II
_.times(8, () => { // 8 lanes per session time
const { area, ...group } = daySessions.pop()
schedule.push(createEvent({
slotName: 'Session II',
startDateTime: curDay.set({ hour: 13, minute: 30 }),
duration: '1h',
type: 'regular',
group,
area,
status: getEventStatus(sessionIdx),
hasNote: sessionsWithNotes.includes(sessionIdx),
isBoF: group.is_bof,
showAgenda: true,
hasAgenda: !sessionsMissingAgenda.includes(sessionIdx),
hasRecordings: !sessionsMissingAgenda.includes(sessionIdx),
hasWebex: sessionsWithWebex.includes(sessionIdx)
}, floors))
sessionIdx++
})
// -> No 3rd session on last day
if (dayIdx < 6) {
schedule.push(createEvent({
name: 'Beverage and Snack Break',
startDateTime: curDay.set({ hour: 14, minute: 30 }),
duration: '30m',
type: 'break',
...findAreaGroup('beverage-and-snack-break', categories[2])
}, floors))
// -> Session III
_.times(8, () => { // 8 lanes per session time
const { area, ...group } = daySessions.pop()
schedule.push(createEvent({
slotName: 'Session III',
startDateTime: curDay.set({ hour: 15 }),
duration: '2h',
type: 'regular',
group,
area,
status: getEventStatus(sessionIdx),
hasNote: sessionsWithNotes.includes(sessionIdx),
isBoF: group.is_bof,
showAgenda: true,
hasAgenda: !sessionsMissingAgenda.includes(sessionIdx),
hasRecordings: !sessionsMissingAgenda.includes(sessionIdx),
hasWebex: sessionsWithWebex.includes(sessionIdx)
}, floors))
sessionIdx++
})
}
// -> Plenary
if (dayIdx === 4) {
schedule.push(createEvent({
name: 'Beverage and Snack Break',
startDateTime: curDay.set({ hour: 17 }),
duration: '30m',
type: 'break',
...findAreaGroup('beverage-and-snack-break', categories[2])
}, floors))
schedule.push(createEvent({
name: 'IETF Plenary',
startDateTime: curDay.set({ hour: 17, minute: 30 }),
duration: '2h',
type: 'plenary',
...findAreaGroup('ietf-plenary', categories[2])
}, floors))
}
}
}
// Return response object
return {
meeting: {
number: '123',
city: faker.location.city(),
startDate: startDate.toISODate(),
endDate: endDate.toISODate(),
updated: faker.date.between({ from: startDate.toISO(), to: endDate.toISO() }).toISOString(),
timezone: 'Asia/Tokyo',
infoNote: faker.lorem.paragraph(4),
warningNote: ''
},
categories,
isCurrentMeeting: dateMode !== 'past',
usesNotes: true,
schedule,
floors
}
},
/**
* Format URL by replacing inline variables
*
* @param {String} url Raw URL
* @param {Object} session Session Object
* @param {String} meetingNumber Meeting Number
* @returns Formatted URL
*/
formatLinkUrl: (url, session, meetingNumber) => {
return url
? url.replace('{meeting.number}', meetingNumber)
.replace('{group.acronym}', session.groupAcronym)
.replace('{short}', session.short)
.replace('{order_number}', session.orderInMeeting)
: url
},
/**
* Find the first URL in text matching a conference domain
*
* @param {String} txt Raw Text
* @returns First URL found
*/
findFirstConferenceUrl: (txt) => {
try {
const fUrl = txt.match(urlRe)
if (fUrl && fUrl[0].length > 0) {
const pUrl = new URL(fUrl[0])
if (conferenceDomains.some(d => pUrl.hostname.endsWith(d))) {
return fUrl[0]
}
}
} catch (err) { }
return null
}
}