mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-15 04:13:47 +00:00
refactor: migrate custom fields to transactions
refactor: extract functions to api domain refactor: strict custom field parsing refactor: remove rundown cache utilities refactor: directory restructure
This commit is contained in:
@@ -0,0 +1,582 @@
|
||||
import { CustomFields, OntimeEvent, SupportedEntry, TimerType } from 'ontime-types';
|
||||
import { defaultImportMap, ImportMap, MILLIS_PER_MINUTE } from 'ontime-utils';
|
||||
|
||||
import { getCustomFieldData, parseExcel } from '../excel.parser.js';
|
||||
|
||||
import { dataFromExcelTemplate } from './mockData.js';
|
||||
|
||||
describe('parseExcel()', () => {
|
||||
it('parses the example file', () => {
|
||||
// partial import map with only custom fields
|
||||
const importMap = {
|
||||
custom: {
|
||||
user0: 't0',
|
||||
user1: 'Test1',
|
||||
user2: 'test2',
|
||||
user3: 'test3',
|
||||
},
|
||||
};
|
||||
|
||||
const existingCustomFields: CustomFields = {
|
||||
user0: { type: 'string', colour: 'red', label: 'user0' },
|
||||
user1: { type: 'string', colour: 'green', label: 'user1' },
|
||||
user2: { type: 'string', colour: 'blue', label: 'user2' },
|
||||
};
|
||||
|
||||
const parsedData = parseExcel(dataFromExcelTemplate, existingCustomFields, 'testSheet', importMap);
|
||||
expect(parsedData.customFields).toStrictEqual({
|
||||
user0: {
|
||||
type: 'string',
|
||||
colour: 'red',
|
||||
label: 'user0',
|
||||
},
|
||||
user1: {
|
||||
type: 'string',
|
||||
colour: 'green',
|
||||
label: 'user1',
|
||||
},
|
||||
user2: {
|
||||
type: 'string',
|
||||
colour: 'blue',
|
||||
label: 'user2',
|
||||
},
|
||||
user3: {
|
||||
type: 'string',
|
||||
colour: '',
|
||||
label: 'user3',
|
||||
},
|
||||
});
|
||||
expect(parsedData.rundown.order.length).toBe(2);
|
||||
// TODO: why dont we parse the date in UTC?
|
||||
expect(parsedData.rundown.entries).toMatchObject({
|
||||
'event-a': {
|
||||
id: 'event-a',
|
||||
//timeStart: 28800000,
|
||||
//timeEnd: 32410000,
|
||||
title: 'Guest Welcome',
|
||||
timerType: 'count-down',
|
||||
endAction: 'none',
|
||||
isPublic: true,
|
||||
skip: false,
|
||||
note: 'Ballyhoo',
|
||||
custom: {
|
||||
user0: 'a0',
|
||||
user1: 'a1',
|
||||
user2: 'a2',
|
||||
user3: 'a3',
|
||||
},
|
||||
colour: 'red',
|
||||
type: 'event',
|
||||
cue: '101',
|
||||
},
|
||||
'event-b': {
|
||||
id: 'event-b',
|
||||
//timeStart: 32400000,
|
||||
//timeEnd: 34200000,
|
||||
title: 'A song from the hearth',
|
||||
timerType: 'clock',
|
||||
endAction: 'load-next',
|
||||
isPublic: false,
|
||||
skip: true,
|
||||
note: 'Rainbow chase',
|
||||
custom: {},
|
||||
colour: '#F00',
|
||||
type: 'event',
|
||||
cue: '102',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('parses a file without custom fields', () => {
|
||||
// partial import map with only custom fields
|
||||
const importMap = {
|
||||
custom: {
|
||||
niu1: 'niu1',
|
||||
niu2: 'niu2',
|
||||
},
|
||||
};
|
||||
|
||||
const parsedData = parseExcel(dataFromExcelTemplate, {}, 'testSheet', importMap);
|
||||
expect(parsedData.customFields).toStrictEqual({
|
||||
niu1: {
|
||||
type: 'string',
|
||||
colour: '',
|
||||
label: 'niu1',
|
||||
},
|
||||
niu2: {
|
||||
type: 'string',
|
||||
colour: '',
|
||||
label: 'niu2',
|
||||
},
|
||||
});
|
||||
expect(parsedData.rundown.order).toMatchObject(['event-a', 'event-b']);
|
||||
expect(parsedData.rundown.entries['event-a']).toMatchObject({
|
||||
//timeStart: 28800000,
|
||||
//timeEnd: 32410000,
|
||||
id: 'event-a',
|
||||
title: 'Guest Welcome',
|
||||
timerType: 'count-down',
|
||||
endAction: 'none',
|
||||
isPublic: true,
|
||||
skip: false,
|
||||
note: 'Ballyhoo',
|
||||
custom: {},
|
||||
colour: 'red',
|
||||
type: 'event',
|
||||
cue: '101',
|
||||
});
|
||||
expect(parsedData.rundown.entries['event-b']).toMatchObject({
|
||||
//timeStart: 32400000,
|
||||
//timeEnd: 34200000,
|
||||
id: 'event-b',
|
||||
title: 'A song from the hearth',
|
||||
timerType: 'clock',
|
||||
endAction: 'load-next',
|
||||
isPublic: false,
|
||||
skip: true,
|
||||
note: 'Rainbow chase',
|
||||
custom: {},
|
||||
colour: '#F00',
|
||||
type: 'event',
|
||||
cue: '102',
|
||||
});
|
||||
});
|
||||
|
||||
it('ignores unknown event types', () => {
|
||||
const testdata = [
|
||||
['Title', 'Timer type'],
|
||||
['Guest Welcome', 'x'],
|
||||
['A song from the hearth', 'clock'],
|
||||
];
|
||||
|
||||
const importMap = {
|
||||
title: 'title',
|
||||
timerType: 'timer type',
|
||||
};
|
||||
const result = parseExcel(testdata, {}, 'testSheet', importMap);
|
||||
const firstEvent = result.rundown.entries[result.rundown.order[0]];
|
||||
|
||||
expect(result.rundown.order.length).toBe(1);
|
||||
expect((firstEvent as OntimeEvent).title).toBe('A song from the hearth');
|
||||
});
|
||||
|
||||
it('imports blocks', () => {
|
||||
const testdata = [
|
||||
['Title', 'Timer type'],
|
||||
['a block', 'block'],
|
||||
['an event', 'clock'],
|
||||
];
|
||||
|
||||
const importMap = {
|
||||
title: 'title',
|
||||
timerType: 'timer type',
|
||||
};
|
||||
const result = parseExcel(testdata, {}, 'testSheet', importMap);
|
||||
const firstEvent = result.rundown.entries[result.rundown.order[0]];
|
||||
|
||||
expect(result.rundown.order.length).toBe(2);
|
||||
expect((firstEvent as OntimeEvent).type).toBe(SupportedEntry.Block);
|
||||
});
|
||||
|
||||
it('imports as events if there is no timer type column', () => {
|
||||
const testdata = [['Title'], ['no timer type'], ['also no timer type']];
|
||||
|
||||
const importMap = {
|
||||
title: 'title',
|
||||
};
|
||||
|
||||
const result = parseExcel(testdata, {}, 'testSheet', importMap);
|
||||
const firstEvent = result.rundown.entries[result.rundown.order[0]];
|
||||
const secondEvent = result.rundown.entries[result.rundown.order[1]];
|
||||
|
||||
expect(result.rundown.order.length).toBe(2);
|
||||
expect(firstEvent).toMatchObject({
|
||||
type: SupportedEntry.Event,
|
||||
timerType: TimerType.CountDown,
|
||||
});
|
||||
|
||||
expect(secondEvent).toMatchObject({
|
||||
type: SupportedEntry.Event,
|
||||
timerType: TimerType.CountDown,
|
||||
});
|
||||
});
|
||||
|
||||
it('imports as events if timer type is empty or has whitespace', () => {
|
||||
const testdata = [
|
||||
['Title', 'Timer type'],
|
||||
['first', ' '],
|
||||
['second', undefined],
|
||||
['third', ' count-up '],
|
||||
];
|
||||
|
||||
const importMap = {
|
||||
title: 'title',
|
||||
timerType: 'timer type',
|
||||
};
|
||||
const result = parseExcel(testdata, {}, 'testSheet', importMap);
|
||||
const firstEvent = result.rundown.entries[result.rundown.order[0]];
|
||||
const secondEvent = result.rundown.entries[result.rundown.order[1]];
|
||||
const thirdEvent = result.rundown.entries[result.rundown.order[2]];
|
||||
expect(result.rundown.order.length).toBe(3);
|
||||
expect(firstEvent).toMatchObject({ title: 'first', type: SupportedEntry.Event, timerType: TimerType.CountDown });
|
||||
expect(secondEvent).toMatchObject({ title: 'second', type: SupportedEntry.Event, timerType: TimerType.CountDown });
|
||||
expect(thirdEvent).toMatchObject({ title: 'third', type: SupportedEntry.Event, timerType: TimerType.CountUp });
|
||||
});
|
||||
|
||||
it('am/pm conversion to 24h', () => {
|
||||
const testData = [
|
||||
['Time Start', 'Time End', 'ID'],
|
||||
['4:30:00', '4:36:00', 'event-1'],
|
||||
['9:45:00', '10:56:00', 'event-2'],
|
||||
['16:30:00', '16:36:00', 'event-3'],
|
||||
['21:45:00', '22:56:00', 'event-4'],
|
||||
['4:30:00AM', '4:36:00AM', 'event-5'],
|
||||
['9:45:00AM', '10:56:00AM', 'event-6'],
|
||||
['4:30:00PM', '4:36:00PM', 'event-7'],
|
||||
['9:45:00PM', '10:56:00PM', 'event-8'],
|
||||
];
|
||||
|
||||
const importMap = {
|
||||
timeStart: 'time start',
|
||||
timeEnd: 'time end',
|
||||
id: 'id',
|
||||
};
|
||||
const result = parseExcel(testData, {}, 'testSheet', importMap);
|
||||
expect(result.rundown.order.length).toBe(8);
|
||||
expect(result.rundown.entries['event-1']).toMatchObject({
|
||||
timeStart: 16200000,
|
||||
timeEnd: 16560000,
|
||||
});
|
||||
expect(result.rundown.entries['event-2']).toMatchObject({
|
||||
timeStart: 35100000,
|
||||
timeEnd: 39360000,
|
||||
});
|
||||
expect(result.rundown.entries['event-3']).toMatchObject({
|
||||
timeStart: 59400000,
|
||||
timeEnd: 59760000,
|
||||
});
|
||||
expect(result.rundown.entries['event-4']).toMatchObject({
|
||||
timeStart: 78300000,
|
||||
timeEnd: 82560000,
|
||||
});
|
||||
expect(result.rundown.entries['event-5']).toMatchObject({
|
||||
timeStart: 16200000,
|
||||
timeEnd: 16560000,
|
||||
});
|
||||
expect(result.rundown.entries['event-6']).toMatchObject({
|
||||
timeStart: 35100000,
|
||||
timeEnd: 39360000,
|
||||
});
|
||||
expect(result.rundown.entries['event-7']).toMatchObject({
|
||||
timeStart: 59400000,
|
||||
timeEnd: 59760000,
|
||||
});
|
||||
expect(result.rundown.entries['event-8']).toMatchObject({
|
||||
timeStart: 78300000,
|
||||
timeEnd: 82560000,
|
||||
});
|
||||
});
|
||||
|
||||
it('handle leading and trailing whitespace', () => {
|
||||
const testData = [
|
||||
[' ID', ' title ', 'Colour '], // <--- leading and trailing white space
|
||||
['event-a', 'title', '#F00'],
|
||||
];
|
||||
|
||||
const importMap = {
|
||||
id: 'id',
|
||||
title: ' title', // <--- leading white space
|
||||
colour: 'colour ', // <--- trailing white space
|
||||
};
|
||||
|
||||
const result = parseExcel(testData, {}, 'testSheet', importMap);
|
||||
expect(result.rundown.order.length).toBe(1);
|
||||
expect(result.rundown.entries['event-a']).toMatchObject({
|
||||
colour: '#F00',
|
||||
id: 'event-a',
|
||||
title: 'title',
|
||||
});
|
||||
});
|
||||
|
||||
it('parses link start', () => {
|
||||
const testData = [
|
||||
['Time Start', 'Time End', 'ID', 'Link Start', 'Timer type'],
|
||||
['4:30:00', '9:45:00', 'A', '', 'count-down'],
|
||||
['9:45:00', '10:56:00', 'B', 'x', 'count-down'],
|
||||
['10:00:00', '16:36:00', 'C', 'x', 'count-down'],
|
||||
['21:45:00', '22:56:00', 'D', '', 'count-down'],
|
||||
['', '', 'BLOCK', 'x', 'block'], // <-- block with link
|
||||
['00:0:00', '23:56:00', 'E', 'x', 'count-down'], // <-- link past blocks
|
||||
];
|
||||
|
||||
const importMap = {
|
||||
timeStart: 'time start',
|
||||
timeEnd: 'time end',
|
||||
linkStart: 'link start',
|
||||
id: 'id',
|
||||
timerType: 'timer type',
|
||||
};
|
||||
|
||||
const result = parseExcel(testData, {}, 'testSheet', importMap);
|
||||
expect(result.rundown.order.length).toBe(6);
|
||||
expect(result.rundown.order).toMatchObject(['A', 'B', 'C', 'D', 'BLOCK', 'E']);
|
||||
|
||||
expect(result.rundown.entries).toMatchObject({
|
||||
A: {
|
||||
linkStart: false,
|
||||
},
|
||||
B: {
|
||||
linkStart: true,
|
||||
},
|
||||
C: {
|
||||
linkStart: true,
|
||||
},
|
||||
D: {
|
||||
linkStart: false,
|
||||
},
|
||||
BLOCK: {
|
||||
type: SupportedEntry.Block,
|
||||
},
|
||||
E: {
|
||||
linkStart: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('#971 BUG: parses time fields and booleans', () => {
|
||||
const testData = [
|
||||
[
|
||||
'ID',
|
||||
'Time Start',
|
||||
'Time End',
|
||||
'Duration',
|
||||
'Link Start',
|
||||
'Timer Type',
|
||||
'End Action',
|
||||
'Warning time',
|
||||
'Danger time',
|
||||
],
|
||||
[
|
||||
'SETUP',
|
||||
'1899-12-30T07:15:00.000Z',
|
||||
'1899-12-30T08:30:00.000Z',
|
||||
'',
|
||||
'false',
|
||||
'count-down',
|
||||
'none',
|
||||
'15',
|
||||
'00:05:00',
|
||||
],
|
||||
[
|
||||
'MEET1',
|
||||
'1899-12-30T08:30:00.000Z',
|
||||
'1899-12-30T10:00:00.000Z',
|
||||
'',
|
||||
'false',
|
||||
'count-down',
|
||||
'none',
|
||||
15,
|
||||
'00:05:00',
|
||||
],
|
||||
['MEET2', '1899-12-30T10:00:00.000Z', '', '60', 'false', 'count-down', 'none', '13', '5'],
|
||||
['lunch', '', '1899-12-30T11:30:00.000Z', '', 'true', 'count-down', 'none', 13, 5],
|
||||
['MEET3', '1899-12-30T11:30:00.000Z', '', 90, false, 'count-up', 'none', '11', 5],
|
||||
['MEET4', '', '', 30, true, 'count-up', 'none', 11, '00:05:00'],
|
||||
];
|
||||
|
||||
const parsedData = parseExcel(testData, {}, 'bug-report');
|
||||
|
||||
// '15' as a string is parsed by smart time entry as minutes
|
||||
expect(parsedData.rundown.entries['SETUP']).toMatchObject({
|
||||
timeWarning: 15 * MILLIS_PER_MINUTE,
|
||||
});
|
||||
|
||||
// elements in bug report
|
||||
// 15 is a number, in which case we parse it as a minutes value
|
||||
expect(parsedData.rundown.entries['MEET1']).toMatchObject({
|
||||
timeWarning: 15 * MILLIS_PER_MINUTE,
|
||||
});
|
||||
|
||||
// in the case where a string is passed, we need to check whether it is an ISO 8601 date
|
||||
expect(parsedData.rundown.entries['MEET2']).toMatchObject({
|
||||
duration: 60 * MILLIS_PER_MINUTE,
|
||||
timeDanger: 5 * MILLIS_PER_MINUTE,
|
||||
});
|
||||
|
||||
expect(parsedData.rundown.entries['lunch']).toMatchObject({
|
||||
timeWarning: 13 * MILLIS_PER_MINUTE,
|
||||
timeDanger: 5 * MILLIS_PER_MINUTE,
|
||||
});
|
||||
|
||||
expect(parsedData.rundown.entries['MEET3']).toMatchObject({
|
||||
duration: 90 * MILLIS_PER_MINUTE,
|
||||
linkStart: false,
|
||||
timeWarning: 11 * MILLIS_PER_MINUTE,
|
||||
timeDanger: 5 * MILLIS_PER_MINUTE,
|
||||
});
|
||||
|
||||
expect(parsedData.rundown.entries['MEET4']).toMatchObject({
|
||||
duration: 30 * MILLIS_PER_MINUTE,
|
||||
timeWarning: 11 * MILLIS_PER_MINUTE,
|
||||
linkStart: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCustomFieldData()', () => {
|
||||
it('generates a list of keys from the given import map', () => {
|
||||
const importMap = {
|
||||
worksheet: 'event schedule',
|
||||
timeStart: 'time start',
|
||||
linkStart: 'link start',
|
||||
timeEnd: 'time end',
|
||||
duration: 'duration',
|
||||
cue: 'cue',
|
||||
title: 'title',
|
||||
countToEnd: 'count to end',
|
||||
isPublic: 'public',
|
||||
skip: 'skip',
|
||||
note: 'notes',
|
||||
colour: 'colour',
|
||||
endAction: 'end action',
|
||||
timerType: 'timer type',
|
||||
timeWarning: 'warning time',
|
||||
timeDanger: 'danger time',
|
||||
custom: {
|
||||
lighting: 'lx',
|
||||
sound: 'sound',
|
||||
video: 'av',
|
||||
},
|
||||
entryId: 'id',
|
||||
} as ImportMap;
|
||||
|
||||
const result = getCustomFieldData(importMap, {});
|
||||
expect(result.mergedCustomFields).toStrictEqual({
|
||||
lighting: {
|
||||
type: 'string',
|
||||
colour: '',
|
||||
label: 'lighting',
|
||||
},
|
||||
sound: {
|
||||
type: 'string',
|
||||
colour: '',
|
||||
label: 'sound',
|
||||
},
|
||||
video: {
|
||||
type: 'string',
|
||||
colour: '',
|
||||
label: 'video',
|
||||
},
|
||||
});
|
||||
|
||||
// it is an inverted record of <importKey, ontimeKey>
|
||||
expect(result.customFieldImportKeys).toStrictEqual({
|
||||
lx: 'lighting',
|
||||
sound: 'sound',
|
||||
av: 'video',
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps colour information from existing fields', () => {
|
||||
const importMap = {
|
||||
worksheet: 'event schedule',
|
||||
timeStart: 'time start',
|
||||
linkStart: 'link start',
|
||||
timeEnd: 'time end',
|
||||
duration: 'duration',
|
||||
cue: 'cue',
|
||||
title: 'title',
|
||||
countToEnd: 'count to end',
|
||||
isPublic: 'public',
|
||||
skip: 'skip',
|
||||
note: 'notes',
|
||||
colour: 'colour',
|
||||
endAction: 'end action',
|
||||
timerType: 'timer type',
|
||||
timeWarning: 'warning time',
|
||||
timeDanger: 'danger time',
|
||||
custom: {
|
||||
lighting: 'lx',
|
||||
sound: 'sound',
|
||||
video: 'av',
|
||||
'ontime key': 'excel label',
|
||||
},
|
||||
entryId: 'id',
|
||||
} as ImportMap;
|
||||
|
||||
const existingCustomFields: CustomFields = {
|
||||
lighting: { label: 'lighting', type: 'string', colour: 'red' },
|
||||
sound: { label: 'sound', type: 'string', colour: 'green' },
|
||||
ontime_key: { label: 'ontime key', type: 'string', colour: 'blue' },
|
||||
};
|
||||
|
||||
const result = getCustomFieldData(importMap, existingCustomFields);
|
||||
expect(result.mergedCustomFields).toStrictEqual({
|
||||
lighting: {
|
||||
type: 'string',
|
||||
colour: 'red',
|
||||
label: 'lighting',
|
||||
},
|
||||
sound: {
|
||||
type: 'string',
|
||||
colour: 'green',
|
||||
label: 'sound',
|
||||
},
|
||||
video: {
|
||||
type: 'string',
|
||||
colour: '',
|
||||
label: 'video',
|
||||
},
|
||||
ontime_key: {
|
||||
type: 'string',
|
||||
colour: 'blue',
|
||||
label: 'ontime key',
|
||||
},
|
||||
});
|
||||
|
||||
// it is an inverted record of <importKey, ontimeKey>
|
||||
expect(result.customFieldImportKeys).toStrictEqual({
|
||||
lx: 'lighting',
|
||||
sound: 'sound',
|
||||
av: 'video',
|
||||
'excel label': 'ontime_key',
|
||||
});
|
||||
});
|
||||
|
||||
it('lowercases the keys in the import map', () => {
|
||||
const importMap: ImportMap = {
|
||||
...defaultImportMap,
|
||||
custom: {
|
||||
Lighting: 'Lx',
|
||||
Sound: 'sound',
|
||||
video: 'av',
|
||||
},
|
||||
};
|
||||
|
||||
const result = getCustomFieldData(importMap, {});
|
||||
expect(result.mergedCustomFields).toStrictEqual({
|
||||
Lighting: {
|
||||
type: 'string',
|
||||
colour: '',
|
||||
label: 'Lighting',
|
||||
},
|
||||
Sound: {
|
||||
type: 'string',
|
||||
colour: '',
|
||||
label: 'Sound',
|
||||
},
|
||||
video: {
|
||||
type: 'string',
|
||||
colour: '',
|
||||
label: 'video',
|
||||
},
|
||||
});
|
||||
|
||||
// notice that the keys excel keys are lowercased
|
||||
expect(result.customFieldImportKeys).toStrictEqual({
|
||||
lx: 'Lighting',
|
||||
sound: 'Sound',
|
||||
av: 'video',
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user