Last active
November 3, 2022 16:43
-
-
Save iamandrewluca/f723f2409443b30ddcc585c381736566 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env node | |
/** | |
* Makes quiz average from a bunch of Thinkific Quiz CSV files | |
* | |
* Prerequirements node and npm installed | |
* Drop this file in a folder only with Quiz CSVs exported from Thinkific | |
* Make this file executable by running `chmod +x quiz-totals` | |
* Install `csv` dependency by running `npm install csv` | |
* Execute it by giving output filename as argument | |
* ./quiz-totals fe1.csv | |
*/ | |
const process = require("process"); | |
const fs = require("fs"); | |
const csv = require("csv/sync"); | |
const Columns = { | |
email: "Student Email", | |
name: "Student Name", | |
score: "% Score", | |
quiz: "Quiz Name", | |
}; | |
const debugStudentEmail = "email of student"; | |
const [_node, _exec, fileName] = process.argv; | |
const files = fs | |
.readdirSync(process.cwd()) | |
.filter((f) => f.endsWith(".csv") && f !== fileName); | |
const filesContents = files.map((f) => fs.readFileSync(f, "utf8")); | |
const quizzesLength = filesContents.length; | |
const fullQuizzes = filesContents.flatMap((fc) => | |
csv.parse(fc, { columns: true }) | |
); | |
const shrinkedQuizzes = fullQuizzes.map(dropQuestionsColumns); | |
const studentQuizzes = removeQuizDuplicatesByTakingMax(shrinkedQuizzes).map( | |
(quiz) => dropOtherColumns(quiz) | |
); | |
// console.log( | |
// studentQuizzes | |
// .filter((q) => q[Columns.email] === debugStudentEmail) | |
// .sort((a, b) => a[Columns.quiz] > b[Columns.quiz]) | |
// ); | |
const quizzesAverageByStudent = makeQuizzesAverageByStudent( | |
studentQuizzes, | |
quizzesLength | |
).sort((a, b) => a[Columns.email].localeCompare(b[Columns.email])); | |
const output = csv.stringify(quizzesAverageByStudent, { header: true }); | |
console.table(quizzesAverageByStudent); | |
fs.writeFileSync(fileName, output); | |
function makeQuizzesAverageByStudent(quizzes, quizzesLength) { | |
let byStudentAverage = {}; | |
quizzes.forEach((quiz) => { | |
const studentEmail = quiz[Columns.email]; | |
if (!byStudentAverage[studentEmail]) { | |
byStudentAverage[studentEmail] = { | |
...quiz, | |
[Columns.score]: [quiz[Columns.score]], | |
}; | |
} else { | |
byStudentAverage[studentEmail][Columns.score].push(quiz[Columns.score]); | |
} | |
}); | |
// console.log(byStudentAverage[debugStudentEmail]); | |
return Object.values(byStudentAverage).map((quiz) => { | |
const scores = quiz[Columns.score]; | |
const average = sum(scores) / quizzesLength; | |
const score = Math.round(average); | |
// if (quiz["Student Email"] === debugStudentEmail) { | |
// console.log({ scores, average, score }); | |
// } | |
const data = { | |
...quiz, | |
["Score Percent"]: `${score}%`, | |
["Score Number"]: score, | |
["Score Normal"]: score / 100, | |
}; | |
delete data[Columns.score]; | |
return data; | |
}); | |
} | |
function removeQuizDuplicatesByTakingMax(quizzes) { | |
const groupedQuizzes = {}; | |
quizzes.forEach((quiz) => { | |
const studentEmail = quiz[Columns.email]; | |
const quizName = quiz["Quiz Name"]; | |
const quizScore = Number(quiz["% Score"]); | |
if (!groupedQuizzes[studentEmail]) groupedQuizzes[studentEmail] = {}; | |
if (!groupedQuizzes[studentEmail][quizName]) | |
groupedQuizzes[studentEmail][quizName] = quiz; | |
const existingQuizScore = | |
groupedQuizzes[studentEmail][quizName][Columns.score]; | |
if (quizScore > existingQuizScore) | |
groupedQuizzes[studentEmail][quizName] = quiz; | |
}); | |
return Object.values(groupedQuizzes).flatMap((quizzes) => | |
Object.values(quizzes) | |
); | |
} | |
function dropQuestionsColumns(quiz) { | |
return Object.fromEntries( | |
Object.entries(quiz).filter( | |
([column]) => !column.match(/(Correct\?)|Response|(Question #\d+)/) | |
) | |
); | |
} | |
function dropOtherColumns(quiz) { | |
return { | |
[Columns.name]: quiz[Columns.name].trim(), | |
[Columns.email]: quiz[Columns.email].trim(), | |
[Columns.score]: Number(quiz[Columns.score]), | |
}; | |
} | |
function groupBy(arr, key) { | |
return arr.reduce( | |
(acc, item) => ((acc[item[key]] = [...(acc[item[key]] || []), item]), acc), | |
{} | |
); | |
} | |
function sum(arr) { | |
return arr.reduce((a, b) => a + b, 0); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
javascript: void ((() => { | |
/* This bookmarklet should be run in browser, on the zoom recording page */ | |
let keyword = window.prompt("What is the searched keyword?").toUpperCase(); | |
if (!keyword) return; | |
/** @type {HTMLElement[]} */ | |
let allChatMessages = [...document.querySelectorAll(".chat-list-item")]; | |
let allWithKeyWord = allChatMessages | |
.filter((chatListItem) => { | |
let contentWrapper = chatListItem.querySelector(".content-wrapper"); | |
let text = contentWrapper.textContent.toUpperCase(); | |
if (!text.includes(keyword)) return false; | |
let onlyLettersText = text.replace(/[^A-Z]+/g, ""); | |
return onlyLettersText === keyword; | |
}) | |
.map(chatListItem => chatListItem.querySelector(".user-name").textContent); | |
allWithKeyWord = [...new Set(allWithKeyWord)]; | |
console.log(allWithKeyWord); | |
let textarea = document.createElement("textarea"); | |
textarea.cols = Math.max(...allWithKeyWord.map(n => n.length)) + 10; | |
textarea.rows = allWithKeyWord.length + 10; | |
textarea.readOnly = true; | |
textarea.textContent = allWithKeyWord.join("\n"); | |
textarea.style.display = "block"; | |
let close = document.createElement("button"); | |
close.textContent = "Close"; | |
close.addEventListener("click", () => dialog.remove()); | |
let copy = document.createElement("button"); | |
/** @type {Clipboard} */ | |
let clipboard = navigator.clipboard; | |
copy.textContent = "Copy"; | |
copy.addEventListener("click", () => clipboard.writeText(textarea.textContent)); | |
let dialog = document.createElement("dialog"); | |
dialog.append(textarea, close, copy); | |
document.body.append(dialog); | |
dialog.showModal(); | |
})()); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env node | |
/** | |
* Extracts a specific keyword from a Zoom recording conversation file | |
* | |
* Prerequirements node and npm installed | |
* Make this file executable by running `chmod +x zoomkey` | |
* Execute it by giving keyword and filename as arguments | |
* ./zoomkey php GMT20221017-134954_Recording.txt | |
*/ | |
const { readFileSync } = require('fs') | |
const process = require('process') | |
const [node, exec, k, file] = process.argv | |
const keyword = k.toUpperCase() | |
const lines = readFileSync(file, 'utf-8') | |
.split('\n') | |
.filter(Boolean) | |
.map(l => l.split('\t')) | |
.filter(l => l.length === 3) | |
.map(([date, user, text]) => [date, user, text.trim()]) | |
.filter(([,, text]) => text.toUpperCase().includes(keyword)) | |
.filter(([,, text]) => text.toUpperCase().replace(/[^A-Z]+/g, "") === keyword) | |
console.log(lines); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment