Skip to content

Commit

Permalink
Add "Try me out" field mechanism to try schedule updates (#120)
Browse files Browse the repository at this point in the history
This adds a new `try-changes` command that tries schedule updates defined in
a "Try me out" field, as discussed in #115.

The optional `--apply` flag tells the command to apply the adjusted schedule.
Applying the adjusted schedule resets the "Try me out" fields.

The command is restricted to group meetings for now. Running it on projects that
do not define a "Meeting" field will not work.
  • Loading branch information
tidoust authored May 7, 2024
1 parent 18f676c commit 2747c70
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 3 deletions.
12 changes: 12 additions & 0 deletions test/stubs.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ async function getTestData(testDataId) {
field: { name: 'Meeting' }
});
}
if (session.trymeout) {
fields.push({
text: session.trymeout,
field: { name: 'Try me out' }
});
}

return {
id: `id_${uid++}`,
Expand Down Expand Up @@ -125,6 +131,9 @@ async function getTestData(testDataId) {
if (custom.allowMultipleMeetings) {
testData.allowMultipleMeetings = custom.allowMultipleMeetings;
}
if (custom.allowTryMeOut) {
testData.allowTryMeOut = custom.allowTryMeOut;
}

testDataCache[testDataId] = testData;
return JSON.parse(JSON.stringify(testData));
Expand Down Expand Up @@ -156,6 +165,9 @@ export async function sendGraphQLRequest(query, acceptHeader = '') {
if ((name === 'Meeting') && !testData.allowMultipleMeetings) {
field = null;
}
else if ((name === 'Try me out') && !testData.allowTryMeOut) {
field = null;
}
else {
field = {
id: `id_field_${name}`,
Expand Down
25 changes: 25 additions & 0 deletions tools/cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import schedule from './commands/schedule.mjs';
import synchronizeCalendar from './commands/sync-calendar.mjs';
import validate from './commands/validate.mjs';
import viewEvent from './commands/view-event.mjs';
import tryChanges from './commands/try-changes.mjs';


/**
Expand Down Expand Up @@ -188,6 +189,30 @@ Usage notes for the options:
`);


/******************************************************************************
* The "try-changes" command
*****************************************************************************/
program
.command('try-changes')
.summary('Try schedule changes in "Try me out" field.')
.description('Update the schedule with the meeting changes proposed in the "Try me out" field and report the adjusted grid and validation issues.')
.option('-a, --apply', 'apply the adjusted schedule, updating events information on GitHub')
.action(getCommandRunner(tryChanges))
.addHelpText('after', `
Output:
The command returns the generated schedule grid as HTML content (same structure as the one returned by the \`view\` command). You may want to redirect the output to a file. For example:
$ npx tpac-breakouts try-changes > grid.html
The command also emits warnings to the console to report on progress.
Usage notes for the options:
-a, --apply
When the option is not set, the command merely reports the adjusted schedule.
When the option is set, the command applies the adjusted schedule, meaning it updates the scheduling information in the GitHub project associated with the event repository. It resets the "Try me out" field accordingly.
`);


/******************************************************************************
* The "sync-calendar" command
*****************************************************************************/
Expand Down
60 changes: 60 additions & 0 deletions tools/commands/try-changes.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { validateGrid } from '../lib/validate.mjs';
import { saveSessionMeetings } from '../lib/project.mjs';
import { convertProjectToHTML } from '../lib/project2html.mjs';

export default async function (project, options) {
if (!project.allowTryMeOut) {
throw new Error(`No "Try me out" custom field in project`);
}

if (!project.sessions.find(session => session.trymeout)) {
throw new Error(`No schedule adjustments to try!`);
}

console.warn();
console.warn(`Adjust schedule with "Try me out" field...`);
for (const session of project.sessions) {
if (session.trymeout) {
// TODO: also support "try me out" for breakout sessions,
// updating the room, day and slot fields instead of meeting which
// does not exist for breakout sessions.
session.meeting = session.trymeout;
session.trymeout = null;
session.updated = true;
console.warn(`- session ${session.number}: from "${session.meeting}" to "${session.trymeout}"`);
}
}
console.warn(`Adjust schedule with "Try me out" field... done`);

console.warn();
console.warn(`Validate created grid...`);
const { errors: newErrors } = await validateGrid(project, { what: 'scheduling' })
if (newErrors.length) {
for (const error of newErrors) {
console.warn(`- [${error.severity}: ${error.type}] #${error.session}: ${error.messages.join(', ')}`);
}
}
else {
console.warn(`- looks good!`);
}
console.warn(`Validate created grid... done`);

console.warn();
console.warn(`Report project as HTML...`);
const html = await convertProjectToHTML(project);
console.warn();
console.log(html);
console.warn(`Report project as HTML... done`);

if (options.apply) {
console.warn();
console.warn(`Apply created grid...`);
const sessionsToUpdate = project.sessions.filter(s => s.updated);
for (const session of sessionsToUpdate) {
console.warn(`- updating #${session.number}...`);
await saveSessionMeetings(session, project);
console.warn(`- updating #${session.number}... done`);
}
console.warn(`Apply created grid... done`);
}
}
29 changes: 27 additions & 2 deletions tools/lib/project.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,21 @@ export async function fetchProject(login, id) {
}`);
const meeting = meetingResponse.data[type].projectV2.field;

// Project may also have a "Try me out" custom field to adjust the schedule
const tryMeetingResponse = await sendGraphQLRequest(`query {
${type}(login: "${login}"){
projectV2(number: ${id}) {
field(name: "Try me out") {
... on ProjectV2FieldCommon {
id
name
}
}
}
}
}`);
const tryMeeting = tryMeetingResponse.data[type].projectV2.field;

// Another request to retrieve the list of sessions associated with the project.
const sessionsResponse = await sendGraphQLRequest(`query {
${type}(login: "${login}") {
Expand Down Expand Up @@ -802,6 +817,11 @@ export async function fetchProject(login, id) {
meetingsFieldId: meeting?.id,
allowMultipleMeetings: !!meeting?.id,

// ID of the "Try me out" custom field, if it exists
// (it signals the ability to try schedule adjustments from GitHub)
trymeoutsFieldId: tryMeeting?.id,
allowTryMeOut: !!tryMeeting?.id,

// Sections defined in the issue template
sessionSections,

Expand Down Expand Up @@ -832,6 +852,8 @@ export async function fetchProject(login, id) {
.find(value => value.field?.name === 'Slot')?.name,
meeting: session.fieldValues.nodes
.find(value => value.field?.name === 'Meeting')?.text,
trymeout: session.fieldValues.nodes
.find(value => value.field?.name === 'Try me out')?.text,
validation: {
check: session.fieldValues.nodes.find(value => value.field?.name === 'Check')?.text,
warning: session.fieldValues.nodes.find(value => value.field?.name === 'Warning')?.text,
Expand Down Expand Up @@ -870,12 +892,12 @@ function parseProjectDescription(desc) {
* Record the meetings assignments for the provided session
*/
export async function saveSessionMeetings(session, project) {
for (const field of ['room', 'day', 'slot', 'meeting']) {
for (const field of ['room', 'day', 'slot', 'meeting', 'trymeout']) {
// Project may not allow multiple meetings
if (!project[field + 'sFieldId']) {
continue;
}
const prop = (field === 'meeting') ? 'text': 'singleSelectOptionId';
const prop = (field === 'meeting' || field === 'trymeout') ? 'text': 'singleSelectOptionId';
let value = null;
if (prop === 'text') {
// Text field
Expand Down Expand Up @@ -998,6 +1020,9 @@ export function convertProjectToJSON(project) {
if (project.allowMultipleMeetings) {
data.allowMultipleMeetings = true;
}
if (project.allowTryMeOut) {
data.allowTryMeOut = true;
}
for (const list of ['days', 'rooms', 'slots', 'labels']) {
data[list] = toNameList(project[list]);
}
Expand Down
3 changes: 2 additions & 1 deletion tools/lib/validate.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ const schedulingErrors = [
'warning: capacity',
'warning: conflict',
'warning: duration',
'warning: track'
'warning: track',
'warning: times'
];

/**
Expand Down

0 comments on commit 2747c70

Please sign in to comment.