From b640a09a0d984dd89005b433bcbc19f29d2c64e5 Mon Sep 17 00:00:00 2001 From: Abraham Williams <4braham@gmail.com> Date: Mon, 5 Oct 2020 08:30:07 -0500 Subject: [PATCH] Add sessionize import script --- package-lock.json | 6 + package.json | 2 + .../experimental-sessionize-import/index.ts | 19 +++ .../schedule.ts | 90 ++++++++++++++ .../sessions.ts | 111 ++++++++++++++++++ .../speakers.ts | 94 +++++++++++++++ .../experimental-sessionize-import/types.ts | 28 +++++ .../experimental-sessionize-import/utils.ts | 41 +++++++ 8 files changed, 391 insertions(+) create mode 100644 scripts/experimental-sessionize-import/index.ts create mode 100644 scripts/experimental-sessionize-import/schedule.ts create mode 100644 scripts/experimental-sessionize-import/sessions.ts create mode 100644 scripts/experimental-sessionize-import/speakers.ts create mode 100644 scripts/experimental-sessionize-import/types.ts create mode 100644 scripts/experimental-sessionize-import/utils.ts diff --git a/package-lock.json b/package-lock.json index ee6a9fcd84..9263348f5f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20548,6 +20548,12 @@ "minimist": "^1.2.5" } }, + "moment": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.0.tgz", + "integrity": "sha512-z6IJ5HXYiuxvFTI6eiQ9dm77uE0gyy1yXNApVHqTcnIKfY9tIwEjlzsZ6u1LQXvVgKeTnv9Xm7NDvJ7lso3MtA==", + "dev": true + }, "morgan": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", diff --git a/package.json b/package.json index 294f861e2f..fc26e43886 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "build": "npm run clean && tsc && NODE_ENV=production rollup --config rollup.config.js", "clean": "rimraf dist out-tsc", "deploy": "npm run build && NODE_ENV=production firebase deploy", + "experimental:sessionize:import": "ts-node-script ./scripts/experimental-sessionize-import", "firestore:copy": "ts-node-script ./scripts/firestore-copy", "firestore:init": "firebase functions:config:set schedule.enabled=true && firebase deploy --except hosting && ts-node-script ./scripts/firestore-init", "fix": "concurrently npm:fix:*", @@ -99,6 +100,7 @@ "jest-runner-prettier": "^0.3.6", "jest-runner-stylelint": "^2.3.7", "jest-runner-tsc": "^1.6.0", + "moment": "^2.29.0", "nunjucks": "^3.2.2", "prettier": "2.1.2", "prettier-plugin-package": "1.1.0", diff --git a/scripts/experimental-sessionize-import/index.ts b/scripts/experimental-sessionize-import/index.ts new file mode 100644 index 0000000000..b63e928471 --- /dev/null +++ b/scripts/experimental-sessionize-import/index.ts @@ -0,0 +1,19 @@ +import { importSchedule } from './schedule'; +import { importSessions } from './sessions'; +import { importSpeakers } from './speakers'; + +// TODO: accept sessionize file as a param +// TODO: support not having a schedule +// TODO: support multiple tracks on multiple days + +importSpeakers() + .then(() => importSessions()) + .then(() => importSchedule()) + .then(() => { + console.log('Finished'); + process.exit(); + }) + .catch((err: Error) => { + console.log(err); + process.exit(); + }); diff --git a/scripts/experimental-sessionize-import/schedule.ts b/scripts/experimental-sessionize-import/schedule.ts new file mode 100644 index 0000000000..c95babd1c0 --- /dev/null +++ b/scripts/experimental-sessionize-import/schedule.ts @@ -0,0 +1,90 @@ +import moment from 'moment'; +import data from '../../sessionize.json'; +import { Schedule } from '../../src/models/schedule'; +import { Timeslot } from '../../src/models/timeslot'; +import { Track } from '../../src/models/track'; +import { firestore } from '../firebase-config'; +import { SessionizeSession } from './types'; + +export const importSchedule = async () => { + const schedule: Schedule = await convertSchedule(); + const { length } = await save(schedule); + console.log(`Imported data for ${length} day(s)`); +}; + +const convertTracks = (): Track[] => { + return data.rooms.map(({ name, id }) => ({ title: name, sessionizeId: id })); +}; + +const getSessionids = async (): Promise<{ [id: string]: string }> => { + const { docs } = await firestore.collection('sessions').get(); + const ids: { [id: string]: string } = {}; + + docs.forEach((doc) => { + const { sessionizeId } = doc.data() as SessionizeSession; + ids[sessionizeId] = doc.id; + }); + + return ids; +}; + +const convertSchedule = async (): Promise => { + const sessionIds = await getSessionids(); + const schedule: Schedule = {}; + + for (const sessionData of data.sessions) { + console.log(`Importing session ${sessionData.title} at ${sessionData.startsAt}`); + const date = moment(sessionData.startsAt).format('YYYY-MM-DD'); + if (!schedule[date]) { + schedule[date] = { + date, + dateReadable: moment(sessionData.startsAt).format('MMMM D'), + timeslots: [], + tracks: convertTracks(), + }; + } + const startTime = time(sessionData.startsAt); + const existingTimeslot = schedule[date].timeslots.find((timeslot) => { + return timeslot.startTime === startTime; + }); + + if (existingTimeslot) { + existingTimeslot.sessions.push({ + items: [sessionIds[sessionData.id]], + }); + } else { + const timeslot: Timeslot = { + endTime: time(sessionData.endsAt), + startTime, + sessions: [ + { + items: [sessionIds[sessionData.id]], + }, + ], + }; + schedule[date].timeslots.push(timeslot); + } + } + + return schedule; +}; + +const time = (date: string): string => { + return moment(date).format('HH:mm'); +}; + +const save = (schedule: Schedule) => { + const { length } = Object.keys(schedule); + if (length === 0) { + throw new Error('No schedule days found!'); + } + console.log(`Importing ${length} day(s)...`); + + const batch = firestore.batch(); + + Object.keys(schedule).forEach(async (date) => { + batch.set(firestore.collection('schedule').doc(date), schedule[date]); + }); + + return batch.commit(); +}; diff --git a/scripts/experimental-sessionize-import/sessions.ts b/scripts/experimental-sessionize-import/sessions.ts new file mode 100644 index 0000000000..eb32b9c894 --- /dev/null +++ b/scripts/experimental-sessionize-import/sessions.ts @@ -0,0 +1,111 @@ +import data from '../../sessionize.json'; +import { firestore } from '../firebase-config'; +import { Answer, SessionizeSession } from './types'; +import { categoryItem, nameToId, questionAnswer } from './utils'; + +export const importSessions = async () => { + const sessions: SessionizeSession[] = convertSessions(); + const { length } = await save(sessions); + console.log(`Imported data for ${length} sessions`); +}; + +const cleanTags = (text: string): string[] => { + return text + .split(',') + .map((dirtyTag) => dirtyTag.trim()) + .filter(Boolean); +}; + +const cleanComplexity = (text: string): string => { + return text === 'Introductory and overview' ? 'Beginner' : text; +}; + +const convertSpeakerIds = (ids: string[]): string[] => { + return ids.map((sessionizeId) => { + const speaker = data.speakers.find(({ id }) => id === sessionizeId); + if (speaker) { + return nameToId(speaker.fullName); + } else { + throw new Error(`Unable to find speaker with id ${sessionizeId}`); + } + }); +}; + +const matchIcon = ({ + title, + isServiceSession, +}: { + title: string; + isServiceSession: boolean; +}): string => { + if (isServiceSession) { + switch (true) { + case title.toLowerCase().includes('break'): + return 'coffee-break'; + case title.toLowerCase().includes('closing'): + case title.toLowerCase().includes('welcome'): + return 'opening'; + case title.toLowerCase().includes('lunch'): + return 'lunch'; + case title.toLowerCase().includes('party'): + return 'party'; + default: + return ''; + } + } + return ''; +}; + +const getTags = (answers: Answer[], items: number[]): string[] => { + const isKeynote = categoryItem('Session format', items).toLowerCase() === 'keynote'; + const isLive = categoryItem('Delivery', items).toLowerCase() === 'live'; + const isPrerecorded = categoryItem('Delivery', items).toLowerCase() === 'pre-recorded'; + const tags = [ + questionAnswer('Tags', answers), + isKeynote ? 'keynote' : '', + isLive ? 'live' : '', + isPrerecorded ? 'prerecorded' : '', + ].join(','); + return cleanTags(tags); +}; + +const convertSessions = (): SessionizeSession[] => { + const sessions: SessionizeSession[] = []; + for (const sessionData of data.sessions) { + console.log(`Importing session ${sessionData.title}`); + sessions.push({ + sessionizeId: sessionData.id, + complexity: cleanComplexity(categoryItem('Level', sessionData.categoryItems)), + description: sessionData.description || '', + icon: matchIcon(sessionData), + image: '', + language: 'en', + speakers: convertSpeakerIds(sessionData.speakers), + tags: getTags(sessionData.questionAnswers, sessionData.categoryItems), + title: sessionData.title, + }); + } + return sessions; +}; + +const save = async (sessions: SessionizeSession[]) => { + if (sessions.length === 0) { + throw new Error('No sessions found!'); + } + console.log(`Importing ${sessions.length} sessions...`); + + const collectionRef = firestore.collection('sessions'); + const { docs } = await collectionRef.get(); + const batch = firestore.batch(); + + sessions.forEach(async (session) => { + const existingDoc = docs.find((doc) => doc.data().sessionizeId === session.sessionizeId); + if (existingDoc) { + batch.set(existingDoc.ref, session); + } else { + batch.set(collectionRef.doc(), session); + } + }); + + return batch.commit(); +}; diff --git a/scripts/experimental-sessionize-import/speakers.ts b/scripts/experimental-sessionize-import/speakers.ts new file mode 100644 index 0000000000..3a1e53532f --- /dev/null +++ b/scripts/experimental-sessionize-import/speakers.ts @@ -0,0 +1,94 @@ +import data from '../../sessionize.json'; +import { Badge } from '../../src/models/badge'; +import { Social } from '../../src/models/social'; +import { firestore } from '../firebase-config'; +import { Link, SessionizeSpeaker } from './types'; +import { nameToId, questionAnswer } from './utils'; + +export const importSpeakers = async () => { + const speakers: SessionizeSpeaker[] = convertSpeakers(); + await save(speakers); +}; + +const selectBadges = (company: string): Badge[] => { + return [ + { + description: 'Google', + link: 'https://www.google.com', + name: 'google', + }, + ].filter(({ name }) => name.toLowerCase() === company.toLowerCase()); +}; + +const selectSocials = (links: Link[]): Social[] => { + const approvedSources = [ + 'facebook', + 'github', + 'instagram', + 'linkedin', + 'twitter', + 'website', + 'youtube', + ]; + const approvedLinks = links.filter(({ linkType }) => + approvedSources.includes(linkType.toLowerCase()) + ); + + return approvedLinks.map((link) => { + return { + icon: link.linkType.toLowerCase(), + link: link.url, + name: link.title, + }; + }); +}; + +const convertSpeakers = (): SessionizeSpeaker[] => { + const speakers: SessionizeSpeaker[] = []; + for (const speakerData of data.speakers) { + console.log(`Importing speaker ${speakerData.fullName}`); + speakers.push({ + sessionizeId: speakerData.id, + badges: selectBadges(questionAnswer('Company', speakerData.questionAnswers)), + bio: speakerData.bio, + company: questionAnswer('Company', speakerData.questionAnswers), + companyLogo: '', + companyLogoUrl: '', + country: questionAnswer('Location', speakerData.questionAnswers), + featured: speakerData.isTopSpeaker, + name: speakerData.fullName, + order: 0, + photo: '', + photoUrl: speakerData.profilePicture, + pronouns: questionAnswer('Pronouns', speakerData.questionAnswers), + shortBio: '', + socials: selectSocials(speakerData.links), + title: speakerData.tagLine, + }); + } + return speakers; +}; + +const save = async (speakers: SessionizeSpeaker[]) => { + if (speakers.length === 0) { + throw new Error('No speakers found!'); + } + console.log(`Importing ${speakers.length} speakers...`); + + const collectionRef = firestore.collection('speakers'); + const { docs } = await collectionRef.get(); + const batch = firestore.batch(); + + speakers.forEach(async (speaker) => { + const existingDoc = docs.find((doc) => doc.data().sessionizeId === speaker.sessionizeId); + if (existingDoc) { + batch.set(existingDoc.ref, speaker); + } else { + const id = nameToId(speaker.name); + batch.set(collectionRef.doc(id), speaker); + } + }); + + const { length } = await batch.commit(); + console.log(`Imported data for ${length} speakers`); +}; diff --git a/scripts/experimental-sessionize-import/types.ts b/scripts/experimental-sessionize-import/types.ts new file mode 100644 index 0000000000..02afd0d60f --- /dev/null +++ b/scripts/experimental-sessionize-import/types.ts @@ -0,0 +1,28 @@ +import { Session } from '../../src/models/session'; +import { Speaker } from '../../src/models/speaker'; + +export interface Question { + id: number; + question: string; + questionType: string; + sort: number; +} + +export interface Answer { + questionId: number; + answerValue: string; +} + +export interface Link { + title: string; + url: string; + linkType: string; +} + +export type SessionizeSpeaker = Speaker & { + sessionizeId: string; +}; + +export type SessionizeSession = Session & { + sessionizeId: string; +}; diff --git a/scripts/experimental-sessionize-import/utils.ts b/scripts/experimental-sessionize-import/utils.ts new file mode 100644 index 0000000000..56321f2b8c --- /dev/null +++ b/scripts/experimental-sessionize-import/utils.ts @@ -0,0 +1,41 @@ +import data from '../../sessionize.json'; +import { Answer, Question } from './types'; + +export const nameToId = (name: string): string => { + return name.toLowerCase().replace(/ /g, '_'); +}; + +const findQuestion = (text: string): Question => { + const question = data.questions.find( + ({ question }) => question.trim().toLowerCase() === text.trim().toLowerCase() + ); + + if (question) { + return question; + } else { + throw new Error(`Could not find Question with text ${text}`); + } +}; + +export const questionAnswer = (question: string, answers: Answer[]): string => { + const { id } = findQuestion(question); + return answers.find(({ questionId }) => questionId === id)?.answerValue || ''; +}; + +const findCategory = (text: string) => { + const category = data.categories.find(({ title }) => { + return title.trim().toLowerCase() === text.trim().toLowerCase(); + }); + + if (category) { + return category; + } else { + throw new Error(`Could not find Category with text ${text}`); + } +}; + +export const categoryItem = (text: string, itemIds: number[]): string => { + const { items } = findCategory(text); + const item = items.find(({ id }) => itemIds.includes(id)); + return item?.name || ''; +};