diff --git a/db/data/gene-samples/README.md b/db/data/gene-samples/README.md new file mode 100644 index 0000000..4fd54d6 --- /dev/null +++ b/db/data/gene-samples/README.md @@ -0,0 +1,9 @@ +# Gene samples + +This directory contains a script to convert the gene sample PDF names to a hash of the person id. This is done so that the players can't guess the gene sample file names. + +1. Download the gene sample PDFs from Google Drive and place them in this directory +2. Make sure that files.csv is up to date, as it maps the file names to person id's +3. Make sure you are in this directory and run `./convert.sh` +4. The script will create a new directory called `processed` that contains the PDFs where the file name is now the first 8 characters of the sha1 hash of the person id +5. The contents of `processed` can be uploaded to the server to be served under `/gene-samples/` diff --git a/db/data/gene-samples/convert.sh b/db/data/gene-samples/convert.sh new file mode 100755 index 0000000..b0a888d --- /dev/null +++ b/db/data/gene-samples/convert.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +set -euo pipefail + +input="files.csv" + +# Function to generate a short hash from an ID +generate_hash() { + echo -n "$1" | sha1sum | cut -c1-8 +} + +mkdir -p ./processed +rm -f ./processed/*.pdf + +while IFS=',' read -r id file; do + # Skip the header line + if [ "$id" != "id" ]; then + # Remove leading ./ from the filename if present + filename="${file#./}" + hash=$(generate_hash "$id") + # hash="$id" + cp "$filename" "./processed/${hash}.pdf" + fi +done < "$input" + diff --git a/db/data/gene-samples/files.csv b/db/data/gene-samples/files.csv new file mode 100644 index 0000000..14da778 --- /dev/null +++ b/db/data/gene-samples/files.csv @@ -0,0 +1,105 @@ +id,file +20077,./Gene test - Amir Bolton image.pdf +20107,./Gene test - Jonah Malone image.pdf +20011,./Gene test - Terran Wells image.pdf +20022,./Gene test - Jardan image.pdf +20066,./Gene test - Lane Hayakawa image.pdf +20087,./Gene test - Beva Drugov image.pdf +20040,./Gene test - Deron Richard image.pdf +20026,./Gene test - Naethan image.pdf +20073,./Gene test - Torrey Watson image.pdf +20386,./Gene test - The Guardian image.pdf +20130,./Gene test - Anix image.pdf +20021,./Gene test - Alia Swanson image.pdf +20096,./Gene test - Dayle Rogers image.pdf +20085,./Gene test - Abe Arima image.pdf +20004,./Gene test - Dallan Jordan image.pdf +20025,./Gene test - Jaeco image.pdf +20052,./Gene test - Glen Hollow image.pdf +20012,./Gene test - Blake Ishimoto image.pdf +20090,./Gene test - Harper Ellis image.pdf +20005,./Gene test - Yera Romero image.pdf +20060,./Gene test - Nicol Wells image.pdf +20108,./Gene test - Malak Fukui image.pdf +20094,./Gene test - Hayden Carson image.pdf +20089,./Gene test - Fenix Ellis image.pdf +20083,./Gene test - Tan Ellis image.pdf +20124,./Gene test - Jovian Aurelios Cauruleos image.pdf +20015,./Gene test - Gallan Reid image.pdf +20086,./Gene test - Aeran Lester image.pdf +20001,./Gene test - Vane Hodge image.pdf +20039,./Gene test - Noe Walker image.pdf +20121,./Gene test - Pax Houghton image.pdf +20059,./Gene test - Ballard Case image.pdf +20051,./Gene test - Roan Rowen image.pdf +20006,./Gene test - Kai Rogers image.pdf +20132,./Gene test - Nayel image.pdf +20110,./Gene test - Jose Cain image.pdf +20044,./Gene test - Jill Montoya image.pdf +20007,./Gene test - Cal Allen image.pdf +20131,./Gene test - Keana image.pdf +20120,./Gene test - Remi Sharp image.pdf +20023,./Gene test - Zaera image.pdf +20104,./Gene test - Espen Nakahara image.pdf +20043,./Gene test - Gail Wells image.pdf +20058,./Gene test - Hali Okuma image.pdf +20084,./Gene test - Arlyn Booth image.pdf +20055,./Gene test - Julia Aurelios Cauruleos image.pdf +20134,./Gene test - Tarai image.pdf +20013,./Gene test - Devyn Pearson image.pdf +20003,./Gene test - Jin Komatsu image.pdf +20119,./Gene test - Yuri Mills image.pdf +20126,./Gene test - Gene Hawkins image.pdf +20123,./Gene test - Lee Savage image.pdf +20014,./Gene test - Evin Reid image.pdf +20082,./Gene test - Eva Ellis image.pdf +20098,./Gene test - Briana Chambers image.pdf +20103,./Gene test - Valerian Fukui image.pdf +20101,./Gene test - Osha Green image.pdf +20095,./Gene test - Han Barnes image.pdf +20074,./Gene test - Nikita Watson image.pdf +20106,./Gene test - Nico Lawrence image.pdf +20088,./Gene test - Fran Abrankowich image.pdf +20099,./Gene test - Heath Steele image.pdf +20020,./Gene test - Xavier Blake image.pdf +20133,./Gene test - Saria image.pdf +20063,./Gene test - Eli Booth image.pdf +20045,./Gene test - Isha Hayakawa image.pdf +20097,./Gene test - Mel McBride image.pdf +20019,./Gene test - Malak Kovalenko image.pdf +20117,./Gene test - Leigh Kent image.pdf +20017,./Gene test - Skye Duran image.pdf +20116,./Gene test - Nolan Hunter image.pdf +20061,./Gene test - Flann Hollow image.pdf +20037,./Gene test - Hale Green image.pdf +20018,./Gene test - Zyra Lee image.pdf +20028,./Gene test - Briya image.pdf +20100,./Gene test - Caden Andrews image.pdf +20102,./Gene test - Lynn Ryan image.pdf +20041,./Gene test - Nickie Ramirez image.pdf +20125,./Gene test - Avery Higashi image.pdf +20128,./Gene test - Aedan image.pdf +20070,./Gene test - Jodey Agaki image.pdf +20008,./Gene test - Idris McBride image.pdf +20042,./Gene test - Lowan Romero image.pdf +20024,./Gene test - Mael image.pdf +20002,./Gene test - Lex Peters image.pdf +20057,./Gene test - Eugenie Russell image.pdf +20112,./Gene test - Zeya Cook image.pdf +20127,./Gene test - Harley Carroll image.pdf +20092,./Gene test - Hedly Walker image.pdf +20062,./Gene test - Tristan Fukui image.pdf +20016,./Gene test - Tyler Carrillo image.pdf +20118,./Gene test - Lonnie Gordon image.pdf +20081,./Gene test - Oriel Cook image.pdf +20113,./Gene test - Karin Alexandrov image.pdf +20111,./Gene test - Ziva Callahan image.pdf +20114,./Gene test - Leone Mills image.pdf +20064,./Gene test - Ismy Arima image.pdf +20091,./Gene test - Gale Chapman image.pdf +20038,./Gene test - Ashlin Hall image.pdf +20129,./Gene test - Taelor image.pdf +20010,./Gene test - Remy Hall image.pdf +20093,./Gene test - Taren Yates image.pdf +20115,./Gene test - Kerrie Ray image.pdf +20050,./Gene test - Gaylen Russell image.pdf diff --git a/src/routes/operation.ts b/src/routes/operation.ts index 14e4b45..a250212 100644 --- a/src/routes/operation.ts +++ b/src/routes/operation.ts @@ -7,9 +7,9 @@ import { logger } from '@/logger'; import { NotFound } from 'http-errors'; import { get } from 'lodash'; import Bookshelf from 'bookshelf'; -import { getBloodTestResultText } from '@/utils/blood-test-results'; +import { getBloodTestResultText, getGeneSampleResultText } from '@/utils/medical-test-results'; import { getScienceAnalysisTime, addOperationResultsToArtifactEntry } from '@/utils/science'; -import { getPath } from "@/store/store"; +import { getPath } from '@/store/store'; import moment from 'moment'; import { Stores } from '@/store/types'; import { saveBlob } from '@/rules/helpers'; @@ -57,6 +57,7 @@ async function processXrayOperation(operationResult: Bookshelf.Model) { const addOperationResultToMedicalEntry = async (operationResult: Bookshelf.Model) => { const isBloodSample = operationResult.get('additional_type') === 'BLOOD_SAMPLE'; + const isGeneSample = operationResult.get('additional_type') === 'GENE_SAMPLE'; const isAnalysed = operationResult.get('is_analysed'); const isComplete = operationResult.get('is_complete'); if (isBloodSample && isAnalysed && !isComplete) { @@ -68,6 +69,15 @@ const addOperationResultToMedicalEntry = async (operationResult: Bookshelf.Model `Added blood test results to ${person.get('full_name')} (${person.get('id')}), marking the operation as complete` ); await operationResult.save({ is_complete: true }, { method: 'update', patch: true }); + } else if (isGeneSample && isAnalysed && !isComplete) { + const person = await new Person().where({ bio_id: operationResult.get('bio_id') }).fetch(); + const geneTestResult = getGeneSampleResultText(person.get('id')); + const entry = new Entry(); + await entry.save({ added_by: EVA_ID, entry: geneTestResult, person_id: person.get('id'), type: 'MEDICAL' }); + logger.success( + `Added gene test results to ${person.get('full_name')} (${person.get('id')}), marking the operation as complete` + ); + await operationResult.save({ is_complete: true }, { method: 'update', patch: true }); } }; @@ -100,7 +110,7 @@ async function scheduleAddOperationResultToArtifactEntry(operationResult: Booksh operation_result_id: operationResult.get('id'), started_at: Date.now(), }, - ] + ], }); } @@ -112,12 +122,15 @@ async function scheduleAddOperationResultToArtifactEntry(operationResult: Booksh * @param {boolean} include_complete.query - True if completed results should be included, defaults to false * @returns {Array.} 200 - List of all OperationResult models */ -router.get('/', handleAsyncErrors(async (req: Request, res: Response) => { - const shouldContainRelations = get(req, 'query.relations') === 'true'; - const include_complete = get(req, 'query.include_complete') === 'true'; - const where = include_complete ? {} : { is_complete: false }; - res.json(await OperationResult.forge().where(where)[shouldContainRelations ? 'fetchAllWithRelated' : 'fetchAll']()); -})); +router.get( + '/', + handleAsyncErrors(async (req: Request, res: Response) => { + const shouldContainRelations = get(req, 'query.relations') === 'true'; + const include_complete = get(req, 'query.include_complete') === 'true'; + const where = include_complete ? {} : { is_complete: false }; + res.json(await OperationResult.forge().where(where)[shouldContainRelations ? 'fetchAllWithRelated' : 'fetchAll']()); + }) +); /** * Get a single operation by operation id @@ -128,13 +141,17 @@ router.get('/', handleAsyncErrors(async (req: Request, res: Response) => { * @returns {Error} 404 - OperationResult not found * @returns {OperationResult.model} 200 - OperationResult model */ -router.get('/:id', handleAsyncErrors(async (req: Request, res: Response) => { - const shouldContainRelations = get(req, 'query.relations') === 'true'; - const operationResult = await OperationResult - .forge({ id: req.params.id })[shouldContainRelations ? 'fetchWithRelated' : 'fetch'](); - if (!operationResult) throw new NotFound('OperationResult not found'); - res.json(operationResult); -})); +router.get( + '/:id', + handleAsyncErrors(async (req: Request, res: Response) => { + const shouldContainRelations = get(req, 'query.relations') === 'true'; + const operationResult = await OperationResult.forge({ id: req.params.id })[ + shouldContainRelations ? 'fetchWithRelated' : 'fetch' + ](); + if (!operationResult) throw new NotFound('OperationResult not found'); + res.json(operationResult); + }) +); /** * Insert a new operation result @@ -144,27 +161,33 @@ router.get('/:id', handleAsyncErrors(async (req: Request, res: Response) => { * @param {OperationResult.model} operationresult.body.required - OperationResult model * @returns {OperationResult.model} 200 - Inserted OperationResult model */ -router.post('/', handleAsyncErrors(async (req: Request, res: Response) => { - const operationResult = await OperationResult.forge().save({ - ...req.body, - is_analysed: true, // All operations are now analysed by default and results get posted automatically - }, { method: 'insert' }); +router.post( + '/', + handleAsyncErrors(async (req: Request, res: Response) => { + const operationResult = await OperationResult.forge().save( + { + ...req.body, + is_analysed: true, // All operations are now analysed by default and results get posted automatically + }, + { method: 'insert' } + ); - const operationResultType = operationResult.get('type'); + const operationResultType = operationResult.get('type'); - if (operationResultType === 'MEDIC') { - // Automatically post blood test results, in case this is a blood sample - await addOperationResultToMedicalEntry(operationResult); - } + if (operationResultType === 'MEDIC') { + // Automatically post blood test results, in case this is a blood sample + await addOperationResultToMedicalEntry(operationResult); + } - if (operationResultType === 'SCIENCE') { - // Automatically post artifact test results if available - await scheduleAddOperationResultToArtifactEntry(operationResult); - } + if (operationResultType === 'SCIENCE') { + // Automatically post artifact test results if available + await scheduleAddOperationResultToArtifactEntry(operationResult); + } - await processXrayOperation(operationResult); - res.json(operationResult); -})); + await processXrayOperation(operationResult); + res.json(operationResult); + }) +); /** * Update an operation result by id @@ -175,13 +198,16 @@ router.post('/', handleAsyncErrors(async (req: Request, res: Response) => { * @param {OperationResult.model} operationresult.body.required - OperationResult model new values * @returns {OperationResult.model} 200 - Updated OperationResult model */ -router.put('/:id', handleAsyncErrors(async (req: Request, res: Response) => { - const operationResult = await OperationResult.forge({ id: req.params.id }).fetch(); - if (!operationResult) throw new NotFound('OperationResult not found'); - await operationResult.save(req.body, { method: 'update', patch: true }); - await addOperationResultToMedicalEntry(operationResult); - await processXrayOperation(operationResult); - res.json(operationResult); -})); +router.put( + '/:id', + handleAsyncErrors(async (req: Request, res: Response) => { + const operationResult = await OperationResult.forge({ id: req.params.id }).fetch(); + if (!operationResult) throw new NotFound('OperationResult not found'); + await operationResult.save(req.body, { method: 'update', patch: true }); + await addOperationResultToMedicalEntry(operationResult); + await processXrayOperation(operationResult); + res.json(operationResult); + }) +); export default router; diff --git a/src/utils/blood-test-results.ts b/src/utils/medical-test-results.ts similarity index 79% rename from src/utils/blood-test-results.ts rename to src/utils/medical-test-results.ts index 209246f..219d0b8 100644 --- a/src/utils/blood-test-results.ts +++ b/src/utils/medical-test-results.ts @@ -1,3 +1,13 @@ +import crypto from 'crypto'; + +const GENE_SAMPLE_BASE_PATH = '/gene-samples'; + +export function getGeneSampleFilename(personId: string) { + const hash = crypto.createHash('sha1').update(personId.toString()).digest('hex'); + const shortHash = hash.substring(0, 8); + return `${GENE_SAMPLE_BASE_PATH}/${shortHash}.pdf`; +} + function getHemoglobinStatus(hemoglobinStr: string) { const hemoglobin = parseFloat(hemoglobinStr); if (isNaN(hemoglobin)) return null; @@ -28,7 +38,8 @@ function getKaliumStatus(kaliumStr: string) { const kalium = parseFloat(kaliumStr); if (isNaN(kalium)) return null; - if (kalium < 3.5) return 'Hypokalemia (too little kalium in the system), feeling tired, leg cramps, weakness, and constipation. Risk of an abnormal heart rhythm. Can cause cardiac arrest.'; + if (kalium < 3.5) + return 'Hypokalemia (too little kalium in the system), feeling tired, leg cramps, weakness, and constipation. Risk of an abnormal heart rhythm. Can cause cardiac arrest.'; if (kalium > 5.1) return 'Dehydrated'; return null; } @@ -88,3 +99,8 @@ export function getBloodTestResultText(resultsModel: any) { Details: ${resultsModel.get('details') || 'None'}`; } + +export function getGeneSampleResultText(personId: string) { + const filename = getGeneSampleFilename(personId); + return `**Gene sample results:** Click here to view the results`; +}