diff --git a/data-serving/data-service/api/openapi.yaml b/data-serving/data-service/api/openapi.yaml index 6bd8d5df7..5cb28cf97 100644 --- a/data-serving/data-service/api/openapi.yaml +++ b/data-serving/data-service/api/openapi.yaml @@ -656,6 +656,9 @@ paths: $ref: '#/components/responses/500' components: schemas: + YesNo: + type: string + enum: ['Y', 'N', ''] BatchUpdateResponse: description: Response to batch update cases API requests properties: @@ -766,8 +769,7 @@ components: occupation: type: string healthcareWorker: - type: string - enum: ['Y', 'N', 'NA'] + $ref: '#/components/schemas/YesNo' location: $ref: '#/components/schemas/Location' events: @@ -788,8 +790,7 @@ components: dateOfFirstConsult: $ref: '#/components/schemas/Date' hospitalized: - type: string - enum: ['Y', 'N', 'NA'] + $ref: '#/components/schemas/YesNo' reasonForHospitalization: type: string enum: [monitoring, treatment, unknown] @@ -798,18 +799,15 @@ components: dateDischargeHospital: $ref: '#/components/schemas/Date' intensiveCare: - type: string - enum: ['Y', 'N', 'NA'] + $ref: '#/components/schemas/YesNo' dateAdmissionICU: $ref: '#/components/schemas/Date' dateDischargeICU: $ref: '#/components/schemas/Date' homeMonitoring: - type: string - enum: ['Y', 'N', 'NA'] + $ref: '#/components/schemas/YesNo' isolated: - type: string - enum: ['Y', 'N', 'NA'] + $ref: '#/components/schemas/YesNo' dateIsolation: $ref: '#/components/schemas/Date' outcome: @@ -825,21 +823,18 @@ components: type: object properties: previousInfection: - type: string - enum: ['Y', 'N', 'NA'] + $ref: '#/components/schemas/YesNo' coInfection: type: string preexistingCondition: type: string pregnancyStatus: - type: string - enum: ['Y', 'N', 'NA'] + $ref: '#/components/schemas/YesNo' transmission: type: object properties: contactWithCase: - type: string - enum: ['Y', 'N', 'NA'] + $ref: '#/components/schemas/YesNo' contactId: type: string contactSetting: @@ -854,8 +849,7 @@ components: type: object properties: travelHistory: - type: string - enum: ['Y', 'N', 'NA'] + $ref: '#/components/schemas/YesNo' travelHistoryEntry: $ref: '#/components/schemas/Date' travelHistoryStart: @@ -875,8 +869,7 @@ components: type: object properties: vaccination: - type: string - enum: ['Y', 'N', 'NA'] + $ref: '#/components/schemas/YesNo' vaccineName: type: string vaccineDate: diff --git a/data-serving/data-service/src/controllers/case.ts b/data-serving/data-service/src/controllers/case.ts index 2b16ac2ec..1e40064ff 100644 --- a/data-serving/data-service/src/controllers/case.ts +++ b/data-serving/data-service/src/controllers/case.ts @@ -355,6 +355,14 @@ export class CasesController { header: false, columns: this.csvHeaders, delimiter: delimiter, + cast: { + date: (value: Date) => { + if (value) { + return new Date(value).toISOString().split('T')[0]; + } + return value; + }, + } }); res.write(stringifiedCase); doc = await cursor.next(); @@ -798,6 +806,13 @@ export class CasesController { res.status(201).json(result); } catch (e) { const err = e as Error; + if (err.name === 'MongoServerError') { + logger.error((e as any).errInfo); + res.status(422).json({ + message: (err as any).errInfo, + }); + return; + } if (err instanceof GeocodeNotFoundError) { res.status(404).json({ message: err.message, diff --git a/data-serving/data-service/src/types/enums.ts b/data-serving/data-service/src/types/enums.ts index 56d5ef12c..e6d5ed844 100644 --- a/data-serving/data-service/src/types/enums.ts +++ b/data-serving/data-service/src/types/enums.ts @@ -8,7 +8,7 @@ export enum CaseStatus { export enum YesNo { Y = 'Y', N = 'N', - NA = 'NA', + None = '', } export enum Role { diff --git a/data-serving/data-service/src/util/case.ts b/data-serving/data-service/src/util/case.ts index db8faba83..3989908f0 100644 --- a/data-serving/data-service/src/util/case.ts +++ b/data-serving/data-service/src/util/case.ts @@ -103,6 +103,11 @@ export const removeBlankHeader = (headers: string[]): string[] => { return headers; }; +export const formatDateWithoutTime = (date: Date | undefined): string => { + if (!date) return ''; + return date.toISOString().split('T')[0]; +}; + export const denormalizeFields = async ( doc: CaseDocument, ): Promise<Partial<CaseDocument>> => { @@ -178,9 +183,8 @@ function denormalizeCaseReferenceFields( additionalSources.push(source.sourceUrl); } } - denormalizedData[ - 'caseReference.additionalSources' - ] = additionalSources.join(','); + denormalizedData['caseReference.additionalSources'] = + additionalSources.join(','); denormalizedData['caseReference.sourceEntryId'] = doc.sourceEntryId || ''; denormalizedData['caseReference.sourceId'] = doc.sourceId || ''; denormalizedData['caseReference.sourceUrl'] = doc.sourceUrl || ''; @@ -211,38 +215,46 @@ export const denormalizeEventsFields = ( const denormalizedData: Record<string, string> = {}; denormalizedData['events.dateEntry'] = doc.dateEntry - ? doc.dateEntry.toDateString() + ? formatDateWithoutTime(doc.dateEntry) : undefined || ''; denormalizedData['events.dateReported'] = doc.dateReported - ? doc.dateReported.toDateString() + ? formatDateWithoutTime(doc.dateReported) : undefined || ''; - denormalizedData['events.dateOnset'] = doc.dateOnset?.toDateString() || ''; - denormalizedData['events.dateConfirmation'] = - doc.dateConfirmation?.toDateString() || ''; + denormalizedData['events.dateOnset'] = formatDateWithoutTime(doc.dateOnset); + denormalizedData['events.dateConfirmation'] = formatDateWithoutTime( + doc.dateConfirmation, + ); denormalizedData['events.confirmationMethod'] = doc.confirmationMethod || ''; - denormalizedData['events.dateOfFirstConsult'] = - doc.dateOfFirstConsult?.toDateString() || ''; + denormalizedData['events.dateOfFirstConsult'] = formatDateWithoutTime( + doc.dateOfFirstConsult, + ); denormalizedData['events.hospitalized'] = doc.hospitalized || ''; denormalizedData['events.reasonForHospitalization'] = doc.reasonForHospitalization || ''; - denormalizedData['events.dateHospitalization'] = - doc.dateHospitalization?.toDateString() || ''; - denormalizedData['events.dateDischargeHospital'] = - doc.dateDischargeHospital?.toDateString() || ''; + denormalizedData['events.dateHospitalization'] = formatDateWithoutTime( + doc.dateHospitalization, + ); + denormalizedData['events.dateDischargeHospital'] = formatDateWithoutTime( + doc.dateDischargeHospital, + ); denormalizedData['events.intensiveCare'] = doc.intensiveCare || ''; - denormalizedData['events.dateAdmissionICU'] = - doc.dateAdmissionICU?.toDateString() || ''; - denormalizedData['events.dateDischargeICU'] = - doc.dateDischargeICU?.toDateString() || ''; + denormalizedData['events.dateAdmissionICU'] = formatDateWithoutTime( + doc.dateAdmissionICU, + ); + denormalizedData['events.dateDischargeICU'] = formatDateWithoutTime( + doc.dateDischargeICU, + ); denormalizedData['events.homeMonitoring'] = doc.homeMonitoring || ''; denormalizedData['events.isolated'] = doc.isolated || ''; - denormalizedData['events.dateIsolation'] = - doc.dateIsolation?.toDateString() || ''; + denormalizedData['events.dateIsolation'] = formatDateWithoutTime( + doc.dateIsolation, + ); denormalizedData['events.outcome'] = doc.outcome || ''; - denormalizedData['events.dateDeath'] = doc.dateDeath?.toDateString() || ''; - denormalizedData['events.dateRecovered'] = - doc.dateRecovered?.toDateString() || ''; + denormalizedData['events.dateDeath'] = formatDateWithoutTime(doc.dateDeath); + denormalizedData['events.dateRecovered'] = formatDateWithoutTime( + doc.dateRecovered, + ); return denormalizedData; }; @@ -334,7 +346,7 @@ function denormalizeTravelHistoryFields( denormalizedData['travelHistory.travelHistory'] = doc?.travelHistory || ''; denormalizedData['travelHistory.travelHistoryEntry'] = - doc?.travelHistoryEntry?.toDateString() || ''; + formatDateWithoutTime(doc?.travelHistoryEntry); denormalizedData['travelHistory.travelHistoryStart'] = doc?.travelHistoryStart || ''; denormalizedData['travelHistory.travelHistoryLocation'] = @@ -352,8 +364,9 @@ function denormalizeVaccineFields( denormalizedData['vaccination.vaccination'] = doc?.vaccination || ''; denormalizedData['vaccination.vaccineName'] = doc?.vaccineName || ''; - denormalizedData['vaccination.vaccineDate'] = - doc?.vaccineDate?.toDateString() || ''; + denormalizedData['vaccination.vaccineDate'] = formatDateWithoutTime( + doc?.vaccineDate, + ); denormalizedData['vaccination.vaccineSideEffects'] = doc?.vaccineSideEffects || ''; diff --git a/data-serving/data-service/test/util/case.test.ts b/data-serving/data-service/test/util/case.test.ts index 205bcb7d7..fcc1108a4 100644 --- a/data-serving/data-service/test/util/case.test.ts +++ b/data-serving/data-service/test/util/case.test.ts @@ -13,11 +13,16 @@ import { RevisionMetadataDocument } from '../model/revision-metadata'; import { TransmissionDocument } from '../model/transmission'; import { TravelHistoryDocument } from '../model/travel-history'; import { VaccineDocument } from '../model/vaccine'; -import { removeBlankHeader, denormalizeFields } from '../../src/util/case'; +import { + removeBlankHeader, + denormalizeFields, + formatDateWithoutTime, +} from '../../src/util/case'; import mongoose from 'mongoose'; import { MongoMemoryServer } from 'mongodb-memory-server'; import { GenomeSequenceDocument } from '../../src/model/genome-sequence'; import { EventsDocument } from '../../src/model/events'; +import { YesNo } from '../../src/types/enums'; let mongoServer: MongoMemoryServer; @@ -364,7 +369,7 @@ describe('Case', () => { ageBuckets: [anAgeBucket._id], gender: 'male', occupation: 'Anesthesiologist', - healthcareWorker: 'Y', + healthcareWorker: YesNo.Y, } as DemographicsDocument; const caseDoc = { @@ -391,7 +396,7 @@ describe('Case', () => { expect(denormalizedCase['demographics.occupation']).toEqual( 'Anesthesiologist', ); - expect(denormalizedCase['demographics.healthcareWorker']).toEqual('Y'); + expect(denormalizedCase['demographics.healthcareWorker']).toEqual(YesNo.Y); }); it('denormalizes events fields', async () => { const eventsDoc = { @@ -421,16 +426,16 @@ describe('Case', () => { const denormalizedCase = await denormalizeFields(caseDoc); expect(denormalizedCase['events.dateEntry']).toEqual( - eventsDoc.dateEntry.toDateString(), + formatDateWithoutTime(eventsDoc.dateEntry), ); expect(denormalizedCase['events.dateReported']).toEqual( - eventsDoc.dateReported.toDateString(), + formatDateWithoutTime(eventsDoc.dateReported), ); expect(denormalizedCase['events.dateOnset']).toEqual( - eventsDoc.dateOnset?.toDateString(), + formatDateWithoutTime(eventsDoc.dateOnset), ); expect(denormalizedCase['events.dateConfirmation']).toEqual( - eventsDoc.dateConfirmation?.toDateString(), + formatDateWithoutTime(eventsDoc.dateConfirmation), ); expect(denormalizedCase['events.confirmationMethod']).toEqual(''); expect(denormalizedCase['events.dateOfFirstConsult']).toEqual(''); @@ -503,10 +508,10 @@ describe('Case', () => { }); it('denormalizes preexisting conditions fields', async () => { const conditionsDoc = { - previousInfection: 'Y', + previousInfection: YesNo.Y, coInfection: 'Flu', preexistingCondition: '', - pregnancyStatus: 'NA', + pregnancyStatus: YesNo.None, } as PreexistingConditionsDocument; const caseDoc = { @@ -529,7 +534,7 @@ describe('Case', () => { const denormalizedCase = await denormalizeFields(caseDoc); expect( denormalizedCase['preexistingConditions.previousInfection'], - ).toEqual('Y'); + ).toEqual(YesNo.Y); expect(denormalizedCase['preexistingConditions.coInfection']).toEqual( 'Flu', ); @@ -538,11 +543,11 @@ describe('Case', () => { ).toEqual(''); expect( denormalizedCase['preexistingConditions.pregnancyStatus'], - ).toEqual('NA'); + ).toEqual(YesNo.None); }); it('denormalizes transmission fields', async () => { const transmissionDoc = { - contactWithCase: 'Y', + contactWithCase: YesNo.Y, contactId: 'abc123', contactSetting: 'setting', contactAnimal: 'animal', @@ -568,7 +573,7 @@ describe('Case', () => { const denormalizedCase = await denormalizeFields(caseDoc); - expect(denormalizedCase['transmission.contactWithCase']).toEqual('Y'); + expect(denormalizedCase['transmission.contactWithCase']).toEqual(YesNo.Y); expect(denormalizedCase['transmission.contactId']).toEqual('abc123'); expect(denormalizedCase['transmission.contactSetting']).toEqual( 'setting', @@ -585,7 +590,7 @@ describe('Case', () => { }); it('denormalizes travel history fields', async () => { const travelHistoryDoc = { - travelHistory: 'Y', + travelHistory: YesNo.Y, travelHistoryEntry: new Date('2020-11-01'), travelHistoryStart: 'start', travelHistoryLocation: 'London', @@ -610,9 +615,9 @@ describe('Case', () => { const denormalizedCase = await denormalizeFields(caseDoc); - expect(denormalizedCase['travelHistory.travelHistory']).toEqual('Y'); + expect(denormalizedCase['travelHistory.travelHistory']).toEqual(YesNo.Y); expect(denormalizedCase['travelHistory.travelHistoryEntry']).toEqual( - travelHistoryDoc.travelHistoryEntry.toDateString(), + formatDateWithoutTime(travelHistoryDoc.travelHistoryEntry), ); expect(denormalizedCase['travelHistory.travelHistoryStart']).toEqual( 'start', @@ -626,7 +631,7 @@ describe('Case', () => { }); it('denormalizes vaccine fields', async () => { const vaccinationDoc = { - vaccination: 'Y', + vaccination: YesNo.Y, vaccineName: 'Pfizer', vaccineDate: new Date('2020-11-01'), vaccineSideEffects: 'cough', @@ -649,10 +654,10 @@ describe('Case', () => { } as CaseDocument; const denormalizedCase = await denormalizeFields(caseDoc); - expect(denormalizedCase['vaccination.vaccination']).toEqual('Y'); + expect(denormalizedCase['vaccination.vaccination']).toEqual(YesNo.Y); expect(denormalizedCase['vaccination.vaccineName']).toEqual('Pfizer'); expect(denormalizedCase['vaccination.vaccineDate']).toEqual( - vaccinationDoc.vaccineDate.toDateString(), + formatDateWithoutTime(vaccinationDoc.vaccineDate), ); expect(denormalizedCase['vaccination.vaccineSideEffects']).toEqual( 'cough', @@ -689,4 +694,13 @@ describe('Case', () => { '1234', ); }); + + it('formatsDateWithoutTimeCorrectly', async () => { + const correctDateString = '2024-01-01'; + const correctDate = new Date(`${correctDateString}T03:24:00`); + const missingDate = undefined; + + expect(formatDateWithoutTime(correctDate)).toEqual(correctDateString); + expect(formatDateWithoutTime(missingDate)).toEqual(''); + }); }); diff --git a/data-serving/scripts/setup-db/manual-migration-helpers/age-buckets-transition/docker-compose-test.yml b/data-serving/scripts/setup-db/manual-migration-helpers/age-buckets-transition/docker-compose-test.yml index 413591d83..7fa0db21a 100644 --- a/data-serving/scripts/setup-db/manual-migration-helpers/age-buckets-transition/docker-compose-test.yml +++ b/data-serving/scripts/setup-db/manual-migration-helpers/age-buckets-transition/docker-compose-test.yml @@ -6,6 +6,6 @@ services: context: . dockerfile: Dockerfile-test mongo: - image: mongo:5.0 + image: mongo:6.0.17 ports: - "27017:27017" diff --git a/data-serving/scripts/setup-db/migrations/20210902121948-initial.js b/data-serving/scripts/setup-db/migrations/20210902121948-initial.js index 27063fa83..3b3bc08e5 100644 --- a/data-serving/scripts/setup-db/migrations/20210902121948-initial.js +++ b/data-serving/scripts/setup-db/migrations/20210902121948-initial.js @@ -1,57 +1,60 @@ const fs = require('fs'); module.exports = { - async up(db, client) { - await createCollectionValidationAndIndexes( - db, - 'cases', - 'schemas/cases.schema.json', - 'schemas/cases.indexes.json' - ); - - await createCollectionValidationAndIndexes( - db, - 'sources', - 'schemas/sources.schema.json', - 'schemas/sources.indexes.json' - ); - }, - - async down(db, client) { - // initial migration has no rollback - } + async up(db, client) { + await createCollectionValidationAndIndexes( + db, + 'day0cases', + 'schemas/day0cases.schema.json', + 'schemas/day0cases.indexes.json', + ); + + await createCollectionValidationAndIndexes( + db, + 'sources', + 'schemas/sources.schema.json', + 'schemas/sources.indexes.json', + ); + }, + + async down(db, client) { + // initial migration has no rollback + }, }; -async function createCollectionValidationAndIndexes(db, collectionName, schemaPath, indexPath) { - const schemaFile = await fs.promises.readFile(schemaPath); - const schema = JSON.parse(schemaFile); +async function createCollectionValidationAndIndexes( + db, + collectionName, + schemaPath, + indexPath, +) { + const schemaFile = await fs.promises.readFile(schemaPath); + const schema = JSON.parse(schemaFile); + + const indexFile = await fs.promises.readFile(indexPath); + const indexes = JSON.parse(indexFile); + /* + * because this migration might run against a DB from before we had the migrations infra, + * check whether the collection already exists. If it does, then modify its validation schema. + * If it doesn't, then create it. + */ + try { + await db.collection(collectionName); + await db.command({ + collMod: collectionName, + validator: schema, + }); + } catch { + await db.createCollection(collectionName, { + validator: schema, + }); + } + + const collection = db.collection(collectionName); + await collection.dropIndexes(); - const indexFile = await fs.promises.readFile(indexPath); - const indexes = JSON.parse(indexFile); - /* - * because this migration might run against a DB from before we had the migrations infra, - * check whether the collection already exists. If it does, then modify its validation schema. - * If it doesn't, then create it. - */ - try { - await db.collection(collectionName); await db.command({ - collMod: collectionName, - validator: schema, - }); - } - catch { - await db.createCollection(collectionName, { - validator: schema, + createIndexes: collectionName, + indexes: indexes, }); - } - - const collection = db.collection(collectionName); - await collection.dropIndexes(); - - await db.command({ - createIndexes: collectionName, - indexes: indexes, - }); } - diff --git a/data-serving/scripts/setup-db/migrations/20210922082905-additional-case-indexes.js b/data-serving/scripts/setup-db/migrations/20210922082905-additional-case-indexes.js index 4fe9bbbee..b47fbf266 100644 --- a/data-serving/scripts/setup-db/migrations/20210922082905-additional-case-indexes.js +++ b/data-serving/scripts/setup-db/migrations/20210922082905-additional-case-indexes.js @@ -1,29 +1,29 @@ const indexes = [ - { - name: 'byGenderAndCountry', - key: { - 'demographics.gender': -1, - 'location.countryISO3': -1 + { + name: 'byGenderAndCountry', + key: { + 'demographics.gender': -1, + 'location.countryISO3': -1, + }, + collation: { + locale: 'en_US', + strength: 2, + }, }, - collation: { - locale: 'en_US', - strength: 2, - }, - } ]; module.exports = { - async up(db, client) { - await db.command({ - createIndexes: 'cases', - indexes: indexes, - }); - }, + async up(db, client) { + await db.command({ + createIndexes: 'day0cases', + indexes: indexes, + }); + }, - async down(db, client) { - await db.command({ - dropIndexes: 'cases', - index: ['byGenderAndCountry'] - }); - } + async down(db, client) { + await db.command({ + dropIndexes: 'day0cases', + index: ['byGenderAndCountry'], + }); + }, }; diff --git a/data-serving/scripts/setup-db/package.json b/data-serving/scripts/setup-db/package.json index 1eec4d3a5..fcdd3b211 100644 --- a/data-serving/scripts/setup-db/package.json +++ b/data-serving/scripts/setup-db/package.json @@ -8,6 +8,7 @@ "lint": "tsc --noEmit && eslint '*/**/*.{js,ts,tsx}' --quiet --fix", "import-sample-data": "python3 ./import-sample-data.py", "migrate": "npm ci && migrate-mongo up", + "migrate-down": "npm ci && migrate-mongo down", "delete-all-cases": "mongosh $CONN --eval 'db.cases.deleteMany({})'" }, "repository": { diff --git a/data-serving/scripts/setup-db/schemas/cases.indexes.json b/data-serving/scripts/setup-db/schemas/day0cases.indexes.json similarity index 78% rename from data-serving/scripts/setup-db/schemas/cases.indexes.json rename to data-serving/scripts/setup-db/schemas/day0cases.indexes.json index 210417dd7..b55dd708a 100644 --- a/data-serving/scripts/setup-db/schemas/cases.indexes.json +++ b/data-serving/scripts/setup-db/schemas/day0cases.indexes.json @@ -4,9 +4,12 @@ "key": { "demographics.occupation": "text", "location.country": "text", - "location.city": "text", + "location.admin1": "text", + "location.admin2": "text", + "location.admin3": "text", "caseReference.sourceUrl": "text", - "caseStatus": "text" + "caseStatus": "text", + "comment": "text" } }, { @@ -74,9 +77,29 @@ } }, { - "name": "locationCityIdx", + "name": "locationAdmin1Idx", "key": { - "location.city": -1 + "location.admin1": -1 + }, + "collation": { + "locale": "en_US", + "strength": 2 + } + }, + { + "name": "locationAdmin2Idx", + "key": { + "location.admin2": -1 + }, + "collation": { + "locale": "en_US", + "strength": 2 + } + }, + { + "name": "locationAdmin3Idx", + "key": { + "location.admin3": -1 }, "collation": { "locale": "en_US", @@ -123,6 +146,16 @@ "strength": 2 } }, + { + "name": "commentIdx", + "key": { + "comment": -1 + }, + "collation": { + "locale": "en_US", + "strength": 2 + } + }, { "name": "countryAndDate", "key": { diff --git a/data-serving/scripts/setup-db/schemas/cases.schema.json b/data-serving/scripts/setup-db/schemas/day0cases.schema.json similarity index 60% rename from data-serving/scripts/setup-db/schemas/cases.schema.json rename to data-serving/scripts/setup-db/schemas/day0cases.schema.json index addf0e2c8..4175de3de 100644 --- a/data-serving/scripts/setup-db/schemas/cases.schema.json +++ b/data-serving/scripts/setup-db/schemas/day0cases.schema.json @@ -2,6 +2,7 @@ "$jsonSchema": { "bsonType": "object", "additionalProperties": false, + "required": ["caseStatus"], "properties": { "_id": { "bsonType": "number", @@ -13,6 +14,102 @@ "list": { "bsonType": "bool" }, + "caseStatus": { + "bsonType": "string" + }, + "comment": { + "bsonType": "string" + }, + "curators": { + "bsonType": "object", + "additionalProperties": false, + "properties": { + "verifiedBy": { + "bsonType": "object", + "additionalProperties": false, + "properties": { + "_id": { + "bsonType": "objectId" + }, + "name": { + "bsonType": "string" + }, + "email": { + "bsonType": "string" + }, + "roles": { + "bsonType": "array", + "uniqueItems": true, + "items": { + "bsonType": "string" + } + } + } + }, + "createdBy": { + "bsonType": "object", + "additionalProperties": false, + "properties": { + "_id": { + "bsonType": "objectId" + }, + "name": { + "bsonType": "string" + }, + "email": { + "bsonType": "string" + }, + "roles": { + "bsonType": "array", + "uniqueItems": true, + "items": { + "bsonType": "string" + } + } + } + } + } + }, + "revisionMetadata": { + "bsonType": "object", + "additionalProperties": false, + "properties": { + "revisionNumber": { + "bsonType": "number", + "minimum": 0 + }, + "creationMetadata": { + "bsonType": "object", + "additionalProperties": false, + "properties": { + "curator": { + "bsonType": "string" + }, + "date": { + "bsonType": "date" + }, + "notes": { + "bsonType": "string" + } + } + }, + "updateMetadata": { + "bsonType": "object", + "additionalProperties": false, + "properties": { + "curator": { + "bsonType": "string" + }, + "date": { + "bsonType": "date" + }, + "notes": { + "bsonType": "string" + } + } + } + } + }, "caseReference": { "bsonType": "object", "additionalProperties": false, @@ -26,6 +123,9 @@ "sourceUrl": { "bsonType": "string" }, + "isGovernmentSource": { + "bsonType": "bool" + }, "uploadIds": { "bsonType": "array", "uniqueItems": true, @@ -42,6 +142,9 @@ "properties": { "sourceUrl": { "bsonType": "string" + }, + "isGovernmentSource": { + "bsonType": "bool" } } } @@ -68,6 +171,13 @@ } } }, + "ageBuckets": { + "bsonType": "array", + "uniqueItems": true, + "items": { + "bsonType": "objectId" + } + }, "gender": { "bsonType": "string" }, @@ -111,16 +221,56 @@ "bsonType": "object", "additionalProperties": false, "properties": { + "admin1": { + "bsonType": "string" + }, + "admin1WikiId": { + "bsonType": "string" + }, + "admin2": { + "bsonType": "string" + }, + "admin2WikiId": { + "bsonType": "string" + }, + "admin3": { + "bsonType": "string" + }, + "admin3WikiId": { + "bsonType": "string" + }, + "comment": { + "bsonType": "string" + }, "country": { "bsonType": "string" }, "countryISO3": { "bsonType": "string" }, + "geometry": { + "bsonType": "object", + "additionalProperties": false, + "properties": { + "latitude": { + "bsonType": "number", + "minimum": -90, + "maximum": 90 + }, + "longitude": { + "bsonType": "number", + "minimum": -180, + "maximum": 180 + } + } + }, + "geoResolution": { + "bsonType": "string" + }, "location": { "bsonType": "string" }, - "place": { + "name": { "bsonType": "string" }, "query": { @@ -135,7 +285,7 @@ "bsonType": ["date"] }, "dateLastModified": { - "bsonType": ["date"] + "bsonType": ["date", "null"] }, "dateOnset": { "bsonType": ["date"] diff --git a/dev/README.md b/dev/README.md index b05064b04..619e09eb6 100644 --- a/dev/README.md +++ b/dev/README.md @@ -52,6 +52,11 @@ For local development, it's fine to use your own values for these secrets if you have them. For instance, if you have a developer Mapbox API token, or if you'd like to use a different GMail account for mailing notifications, or different OAuth client values. +#### Database Schema + +Before inputting any case data to the local portal instance a database schema migration needs to be run. +Navigate to the `data-serving/scripts/setup-db/` and follow instructions in the [README](../data-serving/scripts/setup-db/README.md) to set up the database schema. + #### Permissions Give your user all the permissions to access the portal and make CRUD updates. diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml index ee28d8b52..d5129743b 100644 --- a/dev/docker-compose.yml +++ b/dev/docker-compose.yml @@ -18,7 +18,7 @@ services: retries: 3 start_period: 30s mongo: - image: mongo:5.0.6 + image: mongo:6.0.17 restart: always init: true ports: diff --git a/dev/make_superuser.sh b/dev/make_superuser.sh index 2e53d722a..5ff05f6f5 100755 --- a/dev/make_superuser.sh +++ b/dev/make_superuser.sh @@ -10,6 +10,6 @@ cd `dirname "$0"` # Pass database name as the first parameter DB="${GDH_DATABASE:-$1}" # Pass email of user to grant an admin role as second parameter -docker-compose -f docker-compose.yml -f docker-compose.dev.yml exec mongo mongo "${DB}" --eval "var email='$2'; var roles=['admin', 'curator'];" /verification/scripts/roles.js +docker compose -f docker-compose.yml -f docker-compose.dev.yml exec mongo mongosh "${DB}" --eval "var email='$2'; var roles=['admin', 'curator'];" /verification/scripts/roles.js # Restore directory. popd \ No newline at end of file diff --git a/geocoding/location-service/Dockerfile b/geocoding/location-service/Dockerfile index 7b156c704..03b99b645 100644 --- a/geocoding/location-service/Dockerfile +++ b/geocoding/location-service/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.10-slim +FROM python:3.10-bullseye RUN apt-get update -y RUN apt-get install -y python3-pip @@ -14,7 +14,7 @@ WORKDIR /usr/src/app/geocoding/location-service USER flask # install dependencies -RUN pip install poetry +RUN pip install poetry==1.8.5 COPY geocoding/location-service/pyproject.toml . COPY geocoding/location-service/poetry.lock . RUN /home/flask/.local/bin/poetry install diff --git a/geocoding/location-service/Dockerfile-test b/geocoding/location-service/Dockerfile-test index 7c7a80aa2..0a346eea5 100644 --- a/geocoding/location-service/Dockerfile-test +++ b/geocoding/location-service/Dockerfile-test @@ -1,5 +1,5 @@ # `python-base` sets up all our shared environment variables -FROM python:3.10-slim as python-base +FROM python:3.10-bullseye as python-base ENV PYTHONUNBUFFERED=1 \ # prevents python creating .pyc files diff --git a/geocoding/location-service/docker-compose-test.yml b/geocoding/location-service/docker-compose-test.yml index 413591d83..7fa0db21a 100644 --- a/geocoding/location-service/docker-compose-test.yml +++ b/geocoding/location-service/docker-compose-test.yml @@ -6,6 +6,6 @@ services: context: . dockerfile: Dockerfile-test mongo: - image: mongo:5.0 + image: mongo:6.0.17 ports: - "27017:27017" diff --git a/verification/curator-service/api/openapi/openapi.yaml b/verification/curator-service/api/openapi/openapi.yaml index 062408b0f..ac7241369 100644 --- a/verification/curator-service/api/openapi/openapi.yaml +++ b/verification/curator-service/api/openapi/openapi.yaml @@ -1283,6 +1283,9 @@ paths: components: schemas: + YesNo: + type: string + enum: [ 'Y', 'N', '' ] Parser: description: A parser for automated ingestion type: object @@ -1593,8 +1596,7 @@ components: occupation: type: string healthcareWorker: - type: string - enum: ['Y', 'N', 'NA', undefined] + $ref: '#/components/schemas/YesNo' location: $ref: '#/components/schemas/Location' events: @@ -1615,8 +1617,7 @@ components: dateOfFirstConsult: $ref: '#/components/schemas/Date' hospitalized: - type: string - enum: ['Y', 'N', 'NA', undefined] + $ref: '#/components/schemas/YesNo' reasonForHospitalization: type: string enum: [monitoring, treatment, unknown] @@ -1625,18 +1626,15 @@ components: dateDischargeHospital: $ref: '#/components/schemas/Date' intensiveCare: - type: string - enum: ['Y', 'N', 'NA', undefined] + $ref: '#/components/schemas/YesNo' dateAdmissionICU: $ref: '#/components/schemas/Date' dateDischargeICU: $ref: '#/components/schemas/Date' homeMonitoring: - type: string - enum: ['Y', 'N', 'NA', undefined] + $ref: '#/components/schemas/YesNo' isolated: - type: string - enum: ['Y', 'N', 'NA', undefined] + $ref: '#/components/schemas/YesNo' dateIsolation: $ref: '#/components/schemas/Date' outcome: @@ -1653,22 +1651,19 @@ components: type: object properties: previousInfection: - type: string - enum: ['Y', 'N', 'NA', undefined] + $ref: '#/components/schemas/YesNo' coInfection: type: string preexistingCondition: type: string pregnancyStatus: - type: string - enum: ['Y', 'N', 'NA', undefined] + $ref: '#/components/schemas/YesNo' transmission: description: How this case got infected and by who if known type: object properties: contactWithCase: - type: string - enum: ['Y', 'N', 'NA', undefined] + $ref: '#/components/schemas/YesNo' contactId: type: string contactSetting: @@ -1684,8 +1679,7 @@ components: description: Travel history of the patient if known properties: travelHistory: - type: string - enum: ['Y', 'N', 'NA', undefined] + $ref: '#/components/schemas/YesNo' travelHistoryEntry: $ref: '#/components/schemas/Date' travelHistoryStart: @@ -1709,8 +1703,7 @@ components: type: object properties: vaccination: - type: string - enum: ['Y', 'N', 'NA', undefined] + $ref: '#/components/schemas/YesNo' vaccineName: type: string vaccineDate: diff --git a/verification/curator-service/api/package-lock.json b/verification/curator-service/api/package-lock.json index 5b1f08919..2ad47fdae 100644 --- a/verification/curator-service/api/package-lock.json +++ b/verification/curator-service/api/package-lock.json @@ -9,6 +9,10 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "@types/passport": "^1.0.4", + "@types/passport-google-oauth20": "^2.0.4", + "@types/passport-http-bearer": "^1.0.36", + "@types/passport-local": "^1.0.33", "@types/swagger-ui-express": "^4.1.6", "aws-sdk": "^2.1631.0", "axios": "^1.7.2", @@ -32,7 +36,7 @@ "mongodb": "^6.7.0", "mongodb-memory-server": "^9.2.0", "nodemailer": "^6.9.13", - "passport": "^0.7.0", + "passport": "^0.5.3", "passport-google-oauth20": "^2.0.0", "passport-http-bearer": "^1.0.1", "passport-local": "^1.0.0", @@ -55,10 +59,6 @@ "@types/lodash": "^4.17.4", "@types/node": "^20.12.13", "@types/nodemailer": "^6.4.15", - "@types/passport": "^1.0.16", - "@types/passport-google-oauth20": "^2.0.16", - "@types/passport-http-bearer": "^1.0.41", - "@types/passport-local": "^1.0.38", "@types/pino": "^7.0.5", "@types/supertest": "^6.0.2", "@types/yamljs": "^0.2.34", @@ -1410,7 +1410,6 @@ "version": "1.3.7", "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.7.tgz", "integrity": "sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==", - "dev": true, "dependencies": { "@types/node": "*" } @@ -1485,8 +1484,7 @@ "node_modules/@types/content-disposition": { "version": "0.5.8", "resolved": "https://registry.npmjs.org/@types/content-disposition/-/content-disposition-0.5.8.tgz", - "integrity": "sha512-QVSSvno3dE0MgO76pJhmv4Qyi/j0Yk9pBp0Y7TJ2Tlj+KCgJWY6qX7nnxCOLkZ3VYRSIk1WTxCvwUSdx6CCLdg==", - "dev": true + "integrity": "sha512-QVSSvno3dE0MgO76pJhmv4Qyi/j0Yk9pBp0Y7TJ2Tlj+KCgJWY6qX7nnxCOLkZ3VYRSIk1WTxCvwUSdx6CCLdg==" }, "node_modules/@types/cookie-parser": { "version": "1.4.7", @@ -1507,7 +1505,6 @@ "version": "0.9.0", "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.9.0.tgz", "integrity": "sha512-40Zk8qR147RABiQ7NQnBzWzDcjKzNrntB5BAmeGCb2p/MIyOE+4BVvc17wumsUqUw00bJYqoXFHYygQnEFh4/Q==", - "dev": true, "dependencies": { "@types/connect": "*", "@types/express": "*", @@ -1593,14 +1590,12 @@ "node_modules/@types/http-assert": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.5.tgz", - "integrity": "sha512-4+tE/lwdAahgZT1g30Jkdm9PzFRde0xwxBNUyRsCitRvCQB90iuA2uJYdUnhnANRcqGXaWOGY4FEoxeElNAK2g==", - "dev": true + "integrity": "sha512-4+tE/lwdAahgZT1g30Jkdm9PzFRde0xwxBNUyRsCitRvCQB90iuA2uJYdUnhnANRcqGXaWOGY4FEoxeElNAK2g==" }, "node_modules/@types/http-errors": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", - "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", - "dev": true + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.4", @@ -1644,14 +1639,12 @@ "node_modules/@types/keygrip": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.6.tgz", - "integrity": "sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==", - "dev": true + "integrity": "sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==" }, "node_modules/@types/koa": { "version": "2.15.0", "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.15.0.tgz", "integrity": "sha512-7QFsywoE5URbuVnG3loe03QXuGajrnotr3gQkXcEBShORai23MePfFYdhz90FEtBBpkyIYQbVD+evKtloCgX3g==", - "dev": true, "dependencies": { "@types/accepts": "*", "@types/content-disposition": "*", @@ -1667,7 +1660,6 @@ "version": "3.2.8", "resolved": "https://registry.npmjs.org/@types/koa-compose/-/koa-compose-3.2.8.tgz", "integrity": "sha512-4Olc63RY+MKvxMwVknCUDhRQX1pFQoBZ/lXcRLP69PQkEpze/0cr8LNqJQe5NFb/b19DWi2a5bTi2VAlQzhJuA==", - "dev": true, "dependencies": { "@types/koa": "*" } @@ -1718,25 +1710,22 @@ "version": "0.9.5", "resolved": "https://registry.npmjs.org/@types/oauth/-/oauth-0.9.5.tgz", "integrity": "sha512-+oQ3C2Zx6ambINOcdIARF5Z3Tu3x//HipE889/fqo3sgpQZbe9c6ExdQFtN6qlhpR7p83lTZfPJt0tCAW29dog==", - "dev": true, "dependencies": { "@types/node": "*" } }, "node_modules/@types/passport": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.16.tgz", - "integrity": "sha512-FD0qD5hbPWQzaM0wHUnJ/T0BBCJBxCeemtnCwc/ThhTg3x9jfrAcRUmj5Dopza+MfFS9acTe3wk7rcVnRIp/0A==", - "dev": true, + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.4.tgz", + "integrity": "sha512-h5OfAbfBBYSzjeU0GTuuqYEk9McTgWeGQql9g3gUw2/NNCfD7VgExVRYJVVeU13Twn202Mvk9BT0bUrl30sEgA==", "dependencies": { "@types/express": "*" } }, "node_modules/@types/passport-google-oauth20": { - "version": "2.0.16", - "resolved": "https://registry.npmjs.org/@types/passport-google-oauth20/-/passport-google-oauth20-2.0.16.tgz", - "integrity": "sha512-ayXK2CJ7uVieqhYOc6k/pIr5pcQxOLB6kBev+QUGS7oEZeTgIs1odDobXRqgfBPvXzl0wXCQHftV5220czZCPA==", - "dev": true, + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/passport-google-oauth20/-/passport-google-oauth20-2.0.4.tgz", + "integrity": "sha512-lYLsLzbYKlCopSO1b/FdxsBOgpIIZfvhVdVj5Wgx97udiPDGqv3g8Sq5OsWk20idtPL606SnfPRFgWfx698Nkw==", "dependencies": { "@types/express": "*", "@types/passport": "*", @@ -1744,10 +1733,9 @@ } }, "node_modules/@types/passport-http-bearer": { - "version": "1.0.41", - "resolved": "https://registry.npmjs.org/@types/passport-http-bearer/-/passport-http-bearer-1.0.41.tgz", - "integrity": "sha512-ecW+9e8C+0id5iz3YZ+uIarsk/vaRPkKSajt1i1Am66t0mC9gDfQDKXZz9fnPOW2xKUufbmCSou4005VM94Feg==", - "dev": true, + "version": "1.0.36", + "resolved": "https://registry.npmjs.org/@types/passport-http-bearer/-/passport-http-bearer-1.0.36.tgz", + "integrity": "sha512-D6yFiojv/JSxuQY2FcT/dzFHw+ypVOkKN4QzTdt6xZyrmMQBI7p1wr5F3+clCNUgxRgoNaBVRuzlwu5NSV530w==", "dependencies": { "@types/express": "*", "@types/koa": "*", @@ -1755,10 +1743,9 @@ } }, "node_modules/@types/passport-local": { - "version": "1.0.38", - "resolved": "https://registry.npmjs.org/@types/passport-local/-/passport-local-1.0.38.tgz", - "integrity": "sha512-nsrW4A963lYE7lNTv9cr5WmiUD1ibYJvWrpE13oxApFsRt77b0RdtZvKbCdNIY4v/QZ6TRQWaDDEwV1kCTmcXg==", - "dev": true, + "version": "1.0.33", + "resolved": "https://registry.npmjs.org/@types/passport-local/-/passport-local-1.0.33.tgz", + "integrity": "sha512-+rn6ZIxje0jZ2+DAiWFI8vGG7ZFKB0hXx2cUdMmudSWsigSq6ES7Emso46r4HJk0qCgrZVfI8sJiM7HIYf4SbA==", "dependencies": { "@types/express": "*", "@types/passport": "*", @@ -1769,7 +1756,6 @@ "version": "1.4.17", "resolved": "https://registry.npmjs.org/@types/passport-oauth2/-/passport-oauth2-1.4.17.tgz", "integrity": "sha512-ODiAHvso6JcWJ6ZkHHroVp05EHGhqQN533PtFNBkg8Fy5mERDqsr030AX81M0D69ZcaMvhF92SRckEk2B0HYYg==", - "dev": true, "dependencies": { "@types/express": "*", "@types/oauth": "*", @@ -1780,7 +1766,6 @@ "version": "0.2.38", "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz", "integrity": "sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==", - "dev": true, "dependencies": { "@types/express": "*", "@types/passport": "*" @@ -8074,13 +8059,12 @@ } }, "node_modules/passport": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", - "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.5.3.tgz", + "integrity": "sha512-gGc+70h4gGdBWNsR3FuV3byLDY6KBTJAIExGFXTpQaYfbbcHCBlRRKx7RBQSpqEqc5Hh2qVzRs7ssvSfOpkUEA==", "dependencies": { "passport-strategy": "1.x.x", - "pause": "0.0.1", - "utils-merge": "^1.0.1" + "pause": "0.0.1" }, "engines": { "node": ">= 0.4.0" @@ -11678,7 +11662,6 @@ "version": "1.3.7", "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.7.tgz", "integrity": "sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==", - "dev": true, "requires": { "@types/node": "*" } @@ -11753,8 +11736,7 @@ "@types/content-disposition": { "version": "0.5.8", "resolved": "https://registry.npmjs.org/@types/content-disposition/-/content-disposition-0.5.8.tgz", - "integrity": "sha512-QVSSvno3dE0MgO76pJhmv4Qyi/j0Yk9pBp0Y7TJ2Tlj+KCgJWY6qX7nnxCOLkZ3VYRSIk1WTxCvwUSdx6CCLdg==", - "dev": true + "integrity": "sha512-QVSSvno3dE0MgO76pJhmv4Qyi/j0Yk9pBp0Y7TJ2Tlj+KCgJWY6qX7nnxCOLkZ3VYRSIk1WTxCvwUSdx6CCLdg==" }, "@types/cookie-parser": { "version": "1.4.7", @@ -11775,7 +11757,6 @@ "version": "0.9.0", "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.9.0.tgz", "integrity": "sha512-40Zk8qR147RABiQ7NQnBzWzDcjKzNrntB5BAmeGCb2p/MIyOE+4BVvc17wumsUqUw00bJYqoXFHYygQnEFh4/Q==", - "dev": true, "requires": { "@types/connect": "*", "@types/express": "*", @@ -11860,14 +11841,12 @@ "@types/http-assert": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.5.tgz", - "integrity": "sha512-4+tE/lwdAahgZT1g30Jkdm9PzFRde0xwxBNUyRsCitRvCQB90iuA2uJYdUnhnANRcqGXaWOGY4FEoxeElNAK2g==", - "dev": true + "integrity": "sha512-4+tE/lwdAahgZT1g30Jkdm9PzFRde0xwxBNUyRsCitRvCQB90iuA2uJYdUnhnANRcqGXaWOGY4FEoxeElNAK2g==" }, "@types/http-errors": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", - "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", - "dev": true + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" }, "@types/istanbul-lib-coverage": { "version": "2.0.4", @@ -11911,14 +11890,12 @@ "@types/keygrip": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.6.tgz", - "integrity": "sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==", - "dev": true + "integrity": "sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==" }, "@types/koa": { "version": "2.15.0", "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.15.0.tgz", "integrity": "sha512-7QFsywoE5URbuVnG3loe03QXuGajrnotr3gQkXcEBShORai23MePfFYdhz90FEtBBpkyIYQbVD+evKtloCgX3g==", - "dev": true, "requires": { "@types/accepts": "*", "@types/content-disposition": "*", @@ -11934,7 +11911,6 @@ "version": "3.2.8", "resolved": "https://registry.npmjs.org/@types/koa-compose/-/koa-compose-3.2.8.tgz", "integrity": "sha512-4Olc63RY+MKvxMwVknCUDhRQX1pFQoBZ/lXcRLP69PQkEpze/0cr8LNqJQe5NFb/b19DWi2a5bTi2VAlQzhJuA==", - "dev": true, "requires": { "@types/koa": "*" } @@ -11985,25 +11961,22 @@ "version": "0.9.5", "resolved": "https://registry.npmjs.org/@types/oauth/-/oauth-0.9.5.tgz", "integrity": "sha512-+oQ3C2Zx6ambINOcdIARF5Z3Tu3x//HipE889/fqo3sgpQZbe9c6ExdQFtN6qlhpR7p83lTZfPJt0tCAW29dog==", - "dev": true, "requires": { "@types/node": "*" } }, "@types/passport": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.16.tgz", - "integrity": "sha512-FD0qD5hbPWQzaM0wHUnJ/T0BBCJBxCeemtnCwc/ThhTg3x9jfrAcRUmj5Dopza+MfFS9acTe3wk7rcVnRIp/0A==", - "dev": true, + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.4.tgz", + "integrity": "sha512-h5OfAbfBBYSzjeU0GTuuqYEk9McTgWeGQql9g3gUw2/NNCfD7VgExVRYJVVeU13Twn202Mvk9BT0bUrl30sEgA==", "requires": { "@types/express": "*" } }, "@types/passport-google-oauth20": { - "version": "2.0.16", - "resolved": "https://registry.npmjs.org/@types/passport-google-oauth20/-/passport-google-oauth20-2.0.16.tgz", - "integrity": "sha512-ayXK2CJ7uVieqhYOc6k/pIr5pcQxOLB6kBev+QUGS7oEZeTgIs1odDobXRqgfBPvXzl0wXCQHftV5220czZCPA==", - "dev": true, + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/passport-google-oauth20/-/passport-google-oauth20-2.0.4.tgz", + "integrity": "sha512-lYLsLzbYKlCopSO1b/FdxsBOgpIIZfvhVdVj5Wgx97udiPDGqv3g8Sq5OsWk20idtPL606SnfPRFgWfx698Nkw==", "requires": { "@types/express": "*", "@types/passport": "*", @@ -12011,10 +11984,9 @@ } }, "@types/passport-http-bearer": { - "version": "1.0.41", - "resolved": "https://registry.npmjs.org/@types/passport-http-bearer/-/passport-http-bearer-1.0.41.tgz", - "integrity": "sha512-ecW+9e8C+0id5iz3YZ+uIarsk/vaRPkKSajt1i1Am66t0mC9gDfQDKXZz9fnPOW2xKUufbmCSou4005VM94Feg==", - "dev": true, + "version": "1.0.36", + "resolved": "https://registry.npmjs.org/@types/passport-http-bearer/-/passport-http-bearer-1.0.36.tgz", + "integrity": "sha512-D6yFiojv/JSxuQY2FcT/dzFHw+ypVOkKN4QzTdt6xZyrmMQBI7p1wr5F3+clCNUgxRgoNaBVRuzlwu5NSV530w==", "requires": { "@types/express": "*", "@types/koa": "*", @@ -12022,10 +11994,9 @@ } }, "@types/passport-local": { - "version": "1.0.38", - "resolved": "https://registry.npmjs.org/@types/passport-local/-/passport-local-1.0.38.tgz", - "integrity": "sha512-nsrW4A963lYE7lNTv9cr5WmiUD1ibYJvWrpE13oxApFsRt77b0RdtZvKbCdNIY4v/QZ6TRQWaDDEwV1kCTmcXg==", - "dev": true, + "version": "1.0.33", + "resolved": "https://registry.npmjs.org/@types/passport-local/-/passport-local-1.0.33.tgz", + "integrity": "sha512-+rn6ZIxje0jZ2+DAiWFI8vGG7ZFKB0hXx2cUdMmudSWsigSq6ES7Emso46r4HJk0qCgrZVfI8sJiM7HIYf4SbA==", "requires": { "@types/express": "*", "@types/passport": "*", @@ -12036,7 +12007,6 @@ "version": "1.4.17", "resolved": "https://registry.npmjs.org/@types/passport-oauth2/-/passport-oauth2-1.4.17.tgz", "integrity": "sha512-ODiAHvso6JcWJ6ZkHHroVp05EHGhqQN533PtFNBkg8Fy5mERDqsr030AX81M0D69ZcaMvhF92SRckEk2B0HYYg==", - "dev": true, "requires": { "@types/express": "*", "@types/oauth": "*", @@ -12047,7 +12017,6 @@ "version": "0.2.38", "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz", "integrity": "sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==", - "dev": true, "requires": { "@types/express": "*", "@types/passport": "*" @@ -16655,13 +16624,12 @@ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, "passport": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", - "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.5.3.tgz", + "integrity": "sha512-gGc+70h4gGdBWNsR3FuV3byLDY6KBTJAIExGFXTpQaYfbbcHCBlRRKx7RBQSpqEqc5Hh2qVzRs7ssvSfOpkUEA==", "requires": { "passport-strategy": "1.x.x", - "pause": "0.0.1", - "utils-merge": "^1.0.1" + "pause": "0.0.1" } }, "passport-google-oauth20": { diff --git a/verification/curator-service/api/package.json b/verification/curator-service/api/package.json index fdab145a6..2fc81dbe5 100644 --- a/verification/curator-service/api/package.json +++ b/verification/curator-service/api/package.json @@ -38,10 +38,6 @@ "@types/lodash": "^4.17.4", "@types/node": "^20.12.13", "@types/nodemailer": "^6.4.15", - "@types/passport": "^1.0.16", - "@types/passport-google-oauth20": "^2.0.16", - "@types/passport-http-bearer": "^1.0.41", - "@types/passport-local": "^1.0.38", "@types/pino": "^7.0.5", "@types/supertest": "^6.0.2", "@types/yamljs": "^0.2.34", @@ -63,6 +59,10 @@ "typescript": "^5.4.5" }, "dependencies": { + "@types/passport": "^1.0.4", + "@types/passport-google-oauth20": "^2.0.4", + "@types/passport-http-bearer": "^1.0.36", + "@types/passport-local": "^1.0.33", "@types/swagger-ui-express": "^4.1.6", "aws-sdk": "^2.1631.0", "axios": "^1.7.2", @@ -86,7 +86,7 @@ "mongodb": "^6.7.0", "mongodb-memory-server": "^9.2.0", "nodemailer": "^6.9.13", - "passport": "^0.7.0", + "passport": "^0.5.3", "passport-google-oauth20": "^2.0.0", "passport-http-bearer": "^1.0.1", "passport-local": "^1.0.0", diff --git a/verification/curator-service/api/src/controllers/auth.ts b/verification/curator-service/api/src/controllers/auth.ts index b881d96f1..47546eeae 100644 --- a/verification/curator-service/api/src/controllers/auth.ts +++ b/verification/curator-service/api/src/controllers/auth.ts @@ -293,11 +293,9 @@ export class AuthController { }, ); - this.router.get('/logout', (req: Request, res: Response, next: NextFunction): void => { - req.logout(function(err) { - if (err) { return next(err); } - res.redirect('/'); - }); + this.router.get('/logout', function(req, res, next) { + req.logout(); + res.status(200).json({ message: 'Logged out' }); }); // Starts the authentication flow with Google OAuth. diff --git a/verification/curator-service/api/test/auth.test.ts b/verification/curator-service/api/test/auth.test.ts index 58cf267b1..a16587e60 100644 --- a/verification/curator-service/api/test/auth.test.ts +++ b/verification/curator-service/api/test/auth.test.ts @@ -57,8 +57,7 @@ describe('auth', () => { // Redirects to / request(app) .get('/auth/logout') - .expect(302) - .expect('Location', '/') + .expect(200) .end(done); }); it('handles redirect from google', (done) => { @@ -93,7 +92,7 @@ describe('bearer token auth', () => { roles: ['curator'], }) .expect(200, /test-user/); - await request.get('/auth/logout').expect(302); + await request.get('/auth/logout').expect(200); mockedAxios.get.mockResolvedValueOnce({ data: { email: 'foo@bar.com' }, }); @@ -111,7 +110,7 @@ describe('bearer token auth', () => { roles: ['curator'], }) .expect(200, /test-user/); - await request.get('/auth/logout').expect(302); + await request.get('/auth/logout').expect(200); mockedAxios.get.mockResolvedValueOnce({ data: { name: 'my name' }, }); @@ -129,7 +128,7 @@ describe('bearer token auth', () => { roles: ['curator'], }) .expect(200, /test-user/); - await request.get('/auth/logout').expect(302); + await request.get('/auth/logout').expect(200); mockedAxios.get.mockRejectedValueOnce('Oops!'); await request .get('/api/sources?access_token=mF_9.B5f-4.1JqM') @@ -229,7 +228,7 @@ describe('mustHaveAnyRole', () => { roles: ['curator', 'admin'], }) .expect(200, /test-curator/); - await request.get('/auth/logout').expect(302); + await request.get('/auth/logout').expect(200); mockedAxios.get.mockResolvedValueOnce({ data: { email: 'foo@bar.com' }, }); @@ -288,7 +287,7 @@ describe('api keys', () => { await request.post('/auth/profile/apiKey').expect(201); const apiKey = await request.get('/auth/profile/apiKey'); request.set('X-API-key', apiKey.body); - await request.get('/auth/logout').expect(302); + await request.get('/auth/logout').expect(200); await request.get('/auth/profile').expect(200, /test-curator/); }); @@ -303,7 +302,7 @@ describe('api keys', () => { .expect(200, /test-curator/); await request.post('/auth/profile/apiKey').expect(201); const apiKey = await request.get('/auth/profile/apiKey'); - await request.get('/auth/logout').expect(302); + await request.get('/auth/logout').expect(200); await request .post('/auth/register') .send({ @@ -316,7 +315,7 @@ describe('api keys', () => { .post(`/auth/deleteApiKey/${userRes.body._id}`) .expect(204); // now try to use the API key - await request.get('/auth/logout').expect(302); + await request.get('/auth/logout').expect(200); request.set('X-API-key', apiKey.body); await request.get('/auth/profile').expect(403); }); @@ -332,7 +331,7 @@ describe('api keys', () => { .expect(200, /test-curator/); await request.post('/auth/profile/apiKey').expect(201); const apiKey = await request.get('/auth/profile/apiKey'); - await request.get('/auth/logout').expect(302); + await request.get('/auth/logout').expect(200); await request .post('/auth/register') .send({ diff --git a/verification/curator-service/ui/cypress/e2e/components/Curator.spec.ts b/verification/curator-service/ui/cypress/e2e/components/Curator.spec.ts index ab24ff822..1007db534 100644 --- a/verification/curator-service/ui/cypress/e2e/components/Curator.spec.ts +++ b/verification/curator-service/ui/cypress/e2e/components/Curator.spec.ts @@ -1,4 +1,6 @@ /* eslint-disable no-undef */ +import { YesNo } from '../../support/commands'; + describe('Curator', function () { beforeEach(() => { cy.task('clearCasesDB', {}); @@ -56,27 +58,35 @@ describe('Curator', function () { // GENERAL cy.get('div[data-testid="caseStatus"]').click(); cy.get('li[data-value="confirmed"').click(); - cy.get('div[data-testid="comment"]').type('This case should be consulted with Supervisor.'); + cy.get('div[data-testid="comment"]').type( + 'This case should be consulted with Supervisor.', + ); // DATA SOURCE cy.get('div[data-testid="caseReference"]').type('www.example.com'); cy.contains('www.example.com').click(); - cy.get('input[name="caseReference.isGovernmentSource"]').click() + cy.get('input[name="caseReference.isGovernmentSource"]').click(); cy.get('button[data-testid="add-additional-source"]').click(); - cy.get('div[data-testid="additional-source-0"]').type('www.example2.com'); + cy.get('div[data-testid="additional-source-0"]').type( + 'www.example2.com', + ); cy.get('button[data-testid="add-additional-source"]').click(); - cy.get('div[data-testid="additional-source-1"]').type('www.example3.com'); + cy.get('div[data-testid="additional-source-1"]').type( + 'www.example3.com', + ); cy.get('span[data-testid="government-source-1"]').click(); // LOCATION - cy.get('div[data-testid="location.geocodeLocation"]').type('France', { delay: 0}); + cy.get('div[data-testid="location.geocodeLocation"]').type('France', { + delay: 0, + }); cy.wait('@geolocationFranceSuggest'); cy.contains('France').click(); /* Change France to something else to check we can edit geocode results. * We need to change it to a valid country so that we can find the ISO code! */ - cy.get('div[data-testid="location.country"]').click() - cy.get('input[name="location.country"]').clear().type('Germany') + cy.get('div[data-testid="location.country"]').click(); + cy.get('input[name="location.country"]').clear().type('Germany'); cy.get('p').contains('Germany').click(); cy.get('input[name="location.admin1"]').type('Berlin'); cy.get('p').contains('Berlin').click(); @@ -84,7 +94,9 @@ describe('Curator', function () { cy.get('p').contains('SK Berlin').click(); cy.get('input[name="location.admin3"]').type('Berlin'); cy.get('p').contains('Berlin').click(); - cy.get('div[data-testid="location.comment"]').type('Martin Luther Hospital'); + cy.get('div[data-testid="location.comment"]').type( + 'Martin Luther Hospital', + ); // EVENTS cy.get('input[name="events.dateEntry"]').type('2020-01-01'); @@ -94,16 +106,16 @@ describe('Curator', function () { cy.get('input[name="events.dateOnset"]').type('2020-01-03'); cy.get('input[name="events.dateOfFirstConsult"]').type('2020-01-04'); cy.get('div[data-testid="events.homeMonitoring"').click(); - cy.get('li[data-value="Y"]').click(); + cy.get(`li[data-value="${YesNo.Y}"]`).click(); cy.get('div[data-testid="events.isolated"]').click(); - cy.get('li[data-value="Y"]').click(); + cy.get(`li[data-value="${YesNo.Y}"]`).click(); cy.get('input[name="events.dateIsolation"]').type('2020-01-05'); cy.get('div[data-testid="events.hospitalized"]').click(); - cy.get('li[data-value="Y"]').click(); + cy.get(`li[data-value="${YesNo.Y}"]`).click(); cy.get('input[name="events.dateHospitalization"]').type('2020-01-06'); cy.get('input[name="events.dateDischargeHospital"]').type('2020-01-07'); cy.get('div[data-testid="events.intensiveCare"]').click(); - cy.get('li[data-value="Y"]').click(); + cy.get(`li[data-value="${YesNo.Y}"]`).click(); cy.get('input[name="events.dateAdmissionICU"]').type('2020-01-08'); cy.get('input[name="events.dateDischargeICU"]').type('2020-01-09'); cy.get('div[data-testid="events.outcome"]').click(); @@ -117,7 +129,7 @@ describe('Curator', function () { cy.get('div[data-testid="occupation"]').click(); cy.contains('li', 'Accountant').click(); cy.get('div[data-testid="demographics.healthcareWorker"]').click(); - cy.get('li[data-value="N"]').click(); + cy.get(`li[data-value="${YesNo.N}"]`).click(); // SYMPTOMS cy.get('div[data-testid="symptoms"]').type('dry cough'); @@ -129,7 +141,7 @@ describe('Curator', function () { cy.get( 'div[data-testid="preexistingConditions.previousInfection"]', ).click(); - cy.get('li[data-value="Y"').click(); + cy.get(`li[data-value="${YesNo.Y}"]`).click(); cy.get('input[name="preexistingConditions.coInfection"]').type('Flu'); cy.get('div[data-testid="preexistingConditionsHelper"]').type( 'ABCD syndrome', @@ -142,11 +154,11 @@ describe('Curator', function () { cy.get( 'div[data-testid="preexistingConditions.pregnancyStatus"]', ).click(); - cy.get('li[data-value="N"]').click(); + cy.get(`li[data-value="${YesNo.N}"]`).click(); // TRANSMISSION cy.get('div[data-testid="transmission.contactWithCase"]').click(); - cy.get('li[data-value="Y"]').click(); + cy.get(`li[data-value="${YesNo.Y}"]`).click(); cy.get('input[name="transmission.contactId"]').type('ABC123'); cy.get('input[name="transmission.contactSetting"]').type( 'test setting', @@ -160,7 +172,7 @@ describe('Curator', function () { // TRAVEL HISTORY cy.get('div[data-testid="travelHistory.travelHistory"]').click(); - cy.get('li[data-value="Y"]').click(); + cy.get(`li[data-value="${YesNo.Y}"]`).click(); cy.get('input[name="travelHistory.travelHistoryEntry"]').type( '2020-02-01', ); @@ -184,7 +196,7 @@ describe('Curator', function () { // VACCINES cy.get('div[data-testid="vaccination.vaccination"]').click(); - cy.get('li[data-value="Y"]').click(); + cy.get(`li[data-value="${YesNo.Y}"]`).click(); cy.get('input[name="vaccination.vaccineName"]').type('Moderna'); cy.get('input[name="vaccination.vaccineDate"]').type('2020-03-01'); cy.get('div[data-testid="vaccineSideEffects"]').click(); @@ -268,7 +280,7 @@ describe('Curator', function () { 'have.value', 'Berlin', ); - cy.get('input[value="Martin Luther Hospital"]').should("exist"); + cy.get('input[value="Martin Luther Hospital"]').should('exist'); // Events. cy.get('input[name="events.dateEntry"]').should( @@ -297,16 +309,19 @@ describe('Curator', function () { ); cy.get('input[name="events.homeMonitoring"]').should( 'have.value', - 'Y', + YesNo.Y, + ); + cy.get('input[name="events.isolated"]').should( + 'have.value', + YesNo.Y, ); - cy.get('input[name="events.isolated"]').should('have.value', 'Y'); cy.get('input[name="events.dateIsolation"]').should( 'have.value', '2020/01/05', ); cy.get('input[name="events.hospitalized"]').should( 'have.value', - 'Y', + YesNo.Y, ); cy.get('input[name="events.dateHospitalization"]').should( 'have.value', @@ -318,7 +333,7 @@ describe('Curator', function () { ); cy.get('input[name="events.intensiveCare"]').should( 'have.value', - 'Y', + YesNo.Y, ); cy.get('input[name="events.dateAdmissionICU"]').should( 'have.value', @@ -344,7 +359,7 @@ describe('Curator', function () { // Preconditions. cy.get( 'input[name="preexistingConditions.previousInfection"]', - ).should('have.value', 'Y'); + ).should('have.value', YesNo.Y); cy.get('input[name="preexistingConditions.coInfection"]').should( 'have.value', 'Flu', @@ -353,12 +368,12 @@ describe('Curator', function () { cy.contains('ADULT syndrome'); cy.get( 'input[name="preexistingConditions.pregnancyStatus"]', - ).should('have.value', 'N'); + ).should('have.value', YesNo.N); // Transmission cy.get('input[name="transmission.contactWithCase"]').should( 'have.value', - 'Y', + YesNo.Y, ); cy.get('input[name="transmission.contactId"]').should( 'have.value', @@ -386,7 +401,7 @@ describe('Curator', function () { // Travel history. cy.get('input[name="travelHistory.travelHistory"]').should( 'have.value', - 'Y', + YesNo.Y, ); cy.get('input[name="travelHistory.travelHistoryEntry"]').should( 'have.value', @@ -408,7 +423,7 @@ describe('Curator', function () { // Vaccination cy.get('input[name="vaccination.vaccination"]').should( 'have.value', - 'Y', + YesNo.Y, ); cy.get('input[name="vaccination.vaccineName"]').should( 'have.value', @@ -428,7 +443,7 @@ describe('Curator', function () { cy.get('input[type="text"]').clear().type('Test occupation'); }); cy.contains('li', 'Test occupation').click(); - cy.get('div[data-testid="location.comment"]').clear() + cy.get('div[data-testid="location.comment"]').clear(); cy.contains('li', 'Martin Luther Hospital').click(); // Submit the changes. cy.get('button[data-testid="submit"]').click(); diff --git a/verification/curator-service/ui/cypress/e2e/components/LinelistTableTest.spec.ts b/verification/curator-service/ui/cypress/e2e/components/LinelistTableTest.spec.ts index e11307ab7..be520b375 100644 --- a/verification/curator-service/ui/cypress/e2e/components/LinelistTableTest.spec.ts +++ b/verification/curator-service/ui/cypress/e2e/components/LinelistTableTest.spec.ts @@ -491,4 +491,90 @@ describe('Linelist table', function () { cy.get('tr').eq(1).contains('France'); }); + + it('Can search the data', () => { + cy.addCase({ + country: 'France', + countryISO3: 'FRA', + dateEntry: '2020-05-01', + dateReported: '2020-05-01', + sourceUrl: 'www.example.com', + caseStatus: CaseStatus.Confirmed, + occupation: 'Actor', + comment: 'note', + }); + cy.addCase({ + country: 'Germany', + countryISO3: 'DEU', + dateEntry: '2020-05-01', + dateReported: '2020-05-01', + sourceUrl: 'www.example.com', + caseStatus: CaseStatus.Confirmed, + occupation: 'Doctor', + }); + cy.addCase({ + country: 'Argentina', + countryISO3: 'ARG', + dateEntry: '2020-05-01', + dateReported: '2020-05-01', + sourceUrl: 'www.example.com', + caseStatus: CaseStatus.Confirmed, + occupation: 'Engineer', + }); + + cy.intercept('GET', getDefaultQuery({ limit: 50 })).as('getCases'); + cy.intercept('GET', getDefaultQuery({ limit: 50, query: 'Argentina' })).as('getCasesWithSearch1'); + cy.intercept('GET', getDefaultQuery({ limit: 50, query: 'Doctor' })).as('getCasesWithSearch2'); + cy.intercept('GET', getDefaultQuery({ limit: 50, query: 'note' })).as('getCasesWithSearch3'); + + cy.visit('/cases'); + cy.wait('@getCases'); + + cy.contains('Argentina').should('exist'); + cy.contains('France').should('exist'); + cy.contains('Germany').should('exist'); + + cy.get('#search-field').type('Argentina'); + cy.wait('@getCasesWithSearch1'); + cy.contains('Argentina').should('exist'); + cy.contains('France').should('not.exist'); + cy.contains('Germany').should('not.exist'); + + cy.get('#clear-search').click(); + cy.wait('@getCases'); + cy.contains('Argentina').should('exist'); + cy.contains('France').should('exist'); + cy.contains('Germany').should('exist'); + + cy.get('#search-field').type('Doctor'); + cy.wait('@getCasesWithSearch2'); + cy.contains('Argentina').should('not.exist'); + cy.contains('France').should('not.exist'); + cy.contains('Germany').should('exist'); + + cy.get('#clear-search').click(); + cy.wait('@getCases'); + cy.contains('Argentina').should('exist'); + cy.contains('France').should('exist'); + cy.contains('Germany').should('exist'); + + cy.get('#search-field').type('note'); + cy.wait('@getCasesWithSearch3'); + cy.contains('Argentina').should('not.exist'); + cy.contains('France').should('exist'); + cy.contains('Germany').should('not.exist'); + }); + + it('Informs user when uneven number of quotes is present in free-text search', () => { + cy.intercept('GET', getDefaultQuery({ limit: 50, query: '"Bus driver"' })).as('getCasesWithSearch'); + cy.visit('/cases'); + cy.contains('Please make sure you have an even number of quotes.').should('not.exist'); + + cy.get('#search-field').type('"Bus driver'); + cy.contains('Please make sure you have an even number of quotes.').should('exist'); + + cy.get('#search-field').type('"'); + cy.wait('@getCasesWithSearch'); + cy.contains('Please make sure you have an even number of quotes.').should('not.exist'); + }); }); diff --git a/verification/curator-service/ui/cypress/support/commands.ts b/verification/curator-service/ui/cypress/support/commands.ts index 4ddc86446..ce7c0dbf4 100644 --- a/verification/curator-service/ui/cypress/support/commands.ts +++ b/verification/curator-service/ui/cypress/support/commands.ts @@ -12,7 +12,7 @@ export enum Outcome { export enum YesNo { Y = 'Y', N = 'N', - NA = 'NA', + None = '', } export enum Gender { @@ -43,6 +43,7 @@ interface AddCaseProps { gender?: Gender; outcome?: Outcome; uploadIds?: string[]; + comment?: string; } declare global { @@ -111,6 +112,7 @@ export function addCase(opts: AddCaseProps): void { travelHistory: {}, genomeSequences: {}, vaccination: {}, + comment: opts.comment || '', }, }); } diff --git a/verification/curator-service/ui/src/api/models/Case.ts b/verification/curator-service/ui/src/api/models/Case.ts index 65a113dea..dbcf13d9f 100644 --- a/verification/curator-service/ui/src/api/models/Case.ts +++ b/verification/curator-service/ui/src/api/models/Case.ts @@ -1,4 +1,6 @@ // Case definitions as defined by the /api/cases endpoint. +import {YesNo} from "./Day0Case"; + export enum VerificationStatus { Unverified = 'UNVERIFIED', Verified = 'VERIFIED', @@ -132,7 +134,7 @@ export interface Vaccine { batch: string; date: Date; sideEffects: Symptoms; - previousInfection: 'yes' | 'no' | 'NA'; + previousInfection: YesNo; previousInfectionDetectionMethod: string; } diff --git a/verification/curator-service/ui/src/api/models/Day0Case.ts b/verification/curator-service/ui/src/api/models/Day0Case.ts index 434a47f30..c56030ca1 100644 --- a/verification/curator-service/ui/src/api/models/Day0Case.ts +++ b/verification/curator-service/ui/src/api/models/Day0Case.ts @@ -14,7 +14,7 @@ export enum Gender { export enum YesNo { Y = 'Y', N = 'N', - NA = 'NA', + None = '', } export enum HospitalizationReason { diff --git a/verification/curator-service/ui/src/components/App/index.tsx b/verification/curator-service/ui/src/components/App/index.tsx index eb8a9e22d..44c0e58c4 100644 --- a/verification/curator-service/ui/src/components/App/index.tsx +++ b/verification/curator-service/ui/src/components/App/index.tsx @@ -146,6 +146,7 @@ const useStyles = makeStyles()((theme: Theme) => ({ function ProfileMenu(props: { user: User; version: string }): JSX.Element { const dispatch = useAppDispatch(); + const user = useAppSelector(selectUser); const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null); @@ -157,6 +158,10 @@ function ProfileMenu(props: { user: User; version: string }): JSX.Element { setAnchorEl(null); }; + useCallback((): void => { + dispatch(getUserProfile()); + }, [user]); + const { classes } = menuStyles(); const releaseNotesUrl = getReleaseNotesUrl(props.version); @@ -322,13 +327,14 @@ export default function App(): JSX.Element { }; const onModalClose = (): void => { + const searchQueryObject = new URLSearchParams(searchQuery); navigate( { pathname: location.state && location.state.lastLocation ? location.state.lastLocation : '/cases', - search: searchQuery, + search: searchQueryObject.toString(), }, { state: { lastLocation: '/case/view' } }, ); @@ -366,7 +372,8 @@ export default function App(): JSX.Element { ) return; - dispatch(setSearchQuery(location.search)); + const searchParams = new URLSearchParams(location.search); + dispatch(setSearchQuery(searchParams.toString())); // Save searchQuery to local storage not to lost it when user goes through auth process localStorage.setItem('searchQuery', location.search); diff --git a/verification/curator-service/ui/src/components/DataGuideDialog.tsx b/verification/curator-service/ui/src/components/DataGuideDialog.tsx index 95c406d5b..8a3c92801 100644 --- a/verification/curator-service/ui/src/components/DataGuideDialog.tsx +++ b/verification/curator-service/ui/src/components/DataGuideDialog.tsx @@ -1,7 +1,7 @@ import React, { useEffect } from 'react'; import { Box, Portal, Theme, Typography } from '@mui/material'; import { withStyles } from 'tss-react/mui'; -import CloseIcon from '@mui/icons-material/Close'; +import { Close as CloseIcon } from '@mui/icons-material'; import Draggable, { ControlPosition } from 'react-draggable'; // As per this issue from react-draggable library: https://github.com/react-grid-layout/react-draggable/pull/648 @@ -97,7 +97,48 @@ const SearchGuideDialog = ({ </Typography> <Typography className={classes?.textSection}> <strong>For full-text search</strong>, enter any - combination of search terms. + combination of search terms. Rules for full-text + search: + <br /> + <ul> + <li> + Full-text search covers: occupation, admin0, + admin1, admin2, admin3, sourceUrl, comment + and caseStatus. + </li> + <li> + Search terms must be exact (example:{' '} + <b> + <i>German</i> + </b>{' '} + will not match{' '} + <b> + <i>Germany</i> + </b> + ). + </li> + <li> + Full-text search matches cases that contain + any of the search terms, not a combination. + </li> + <li> + To search for a combination of terms, wrap + the combination in quotation marks (example:{' '} + <b> + <i>"Bus driver"</i> + </b> + ). + </li> + <li> + No special characters apart from dot are + allowed. Search terms with dot must be + contained within quotation marks (example:{' '} + <b> + <i>"global.health"</i> + </b> + ). + </li> + </ul> </Typography> <Typography> You can use the icons on the right to navigate diff --git a/verification/curator-service/ui/src/components/FiltersDialog/index.tsx b/verification/curator-service/ui/src/components/FiltersDialog/index.tsx index 810a28c8d..a18d3c69e 100644 --- a/verification/curator-service/ui/src/components/FiltersDialog/index.tsx +++ b/verification/curator-service/ui/src/components/FiltersDialog/index.tsx @@ -17,7 +17,7 @@ import { useMediaQuery, } from '@mui/material'; import { Close as CloseIcon } from '@mui/icons-material'; -import { filtersToURL, URLToFilters } from '../util/searchQuery'; +import { URLToFilters } from '../util/searchQuery'; import { hasAnyRole } from '../util/helperFunctions'; import { useAppSelector, useAppDispatch } from '../../hooks/redux'; import { fetchCountries } from '../../redux/filters/thunk'; @@ -153,13 +153,19 @@ export default function FiltersDialog({ handleSetModalAlert(); dispatch(setModalOpen(false)); - const searchQuery = filtersToURL(values); + const searchParams = new URLSearchParams(); + for (const [key, value] of Object.entries(values)) { + if (value) searchParams.set(key, value); + } + const q = (new URLSearchParams(location.search)).get('q') + if (q) searchParams.set('q', q); + const searchParamsString = searchParams.toString(); - sendCustomGtmEvent('filters_applied', { query: searchQuery }); + sendCustomGtmEvent('filters_applied', { query: searchParamsString }); navigate({ pathname: '/cases', - search: searchQuery, + search: searchParamsString, }); }, }); diff --git a/verification/curator-service/ui/src/components/SearchBar.tsx b/verification/curator-service/ui/src/components/SearchBar.tsx index 9b9d79d53..f45287740 100644 --- a/verification/curator-service/ui/src/components/SearchBar.tsx +++ b/verification/curator-service/ui/src/components/SearchBar.tsx @@ -15,7 +15,6 @@ import clsx from 'clsx'; import DataGuideDialog from './DataGuideDialog'; import { useDebounce } from '../hooks/useDebounce'; import FiltersDialog from './FiltersDialog'; -import { searchQueryToURL, URLToSearchQuery } from './util/searchQuery'; import { useLocation, useNavigate } from 'react-router-dom'; import { KeyboardEvent, ChangeEvent } from 'react'; import { useAppSelector, useAppDispatch } from '../hooks/redux'; @@ -85,9 +84,7 @@ export default function SearchBar({ const [isUserTyping, setIsUserTyping] = useState<boolean>(false); const [isDataGuideOpen, setIsDataGuideOpen] = useState<boolean>(false); const [searchInput, setSearchInput] = useState<string>( - location.search.includes('?q=') - ? URLToSearchQuery(location.search) - : '', + new URLSearchParams(location.search).get('q') || '', ); const [modalAlert, setModalAlert] = useState<boolean>(false); const guideButtonRef = React.useRef<HTMLButtonElement>(null); @@ -103,28 +100,30 @@ export default function SearchBar({ } }, [filtersBreadcrumb]); + useEffect(() => { + const q = new URLSearchParams(location.search).get('q') || ''; + if (q !== searchInput) setSearchInput(q); + }, [location.search]); + // Set search query debounce to 1000ms const debouncedSearch = useDebounce(searchInput, 2000); - // Update search input based on search query - useEffect(() => { - if (!location.search.includes('?q=')) { - setSearchInput(''); - return; - } + const handleNavigating = (q: string) => { + const searchParams = new URLSearchParams(location.search); + q !== '' ? searchParams.set('q', q) : searchParams.delete('q'); - setSearchInput(URLToSearchQuery(location.search)); - }, [location.search]); + navigate({ + pathname: '/cases', + search: searchParams.toString(), + }); + }; // Apply filter parameters after delay useEffect(() => { if (!isUserTyping) return; - setIsUserTyping(false); - navigate({ - pathname: '/cases', - search: searchQueryToURL(debouncedSearch), - }); + + handleNavigating(debouncedSearch); //eslint-disable-next-line }, [debouncedSearch]); @@ -136,10 +135,8 @@ export default function SearchBar({ if (ev.key === 'Enter') { ev.preventDefault(); setIsUserTyping(false); - navigate({ - pathname: '/cases', - search: searchQueryToURL(searchInput), - }); + + handleNavigating(searchInput); } }; @@ -174,16 +171,24 @@ export default function SearchBar({ return searchStringStrippedOutColon; } + const renderSearchErrorMessage = () => { + if (searchError) { + return 'Incorrect entry. ":" characters have been removed. Please use filters instead.'; + } else { + const quoteCount = searchInput.split('"').length - 1; + if (quoteCount % 2 !== 0) { + return 'Incorrect entry. Please make sure you have an even number of quotes.'; + } + } + }; + return ( <> <div className={classes.searchRoot}> <StyledSearchTextField size="small" error={searchError} - helperText={ - searchError && - 'Incorrect entry. ":" characters have been removed. Please use filters instead.' - } + helperText={renderSearchErrorMessage()} id="search-field" data-testid="searchbar" name="searchbar" @@ -254,6 +259,7 @@ export default function SearchBar({ <IconButton color="primary" aria-label="clear search" + id="clear-search" onClick={(): void => { setSearchInput(''); navigate({ diff --git a/verification/curator-service/ui/src/components/ViewCase.tsx b/verification/curator-service/ui/src/components/ViewCase.tsx index edc5eab8d..15b669087 100644 --- a/verification/curator-service/ui/src/components/ViewCase.tsx +++ b/verification/curator-service/ui/src/components/ViewCase.tsx @@ -1,6 +1,19 @@ +import axios from 'axios'; import React, { useEffect, useState } from 'react'; +import Highlighter from 'react-highlight-words'; +import { useSelector } from 'react-redux'; +import { Link, useParams } from 'react-router-dom'; +import Scroll from 'react-scroll'; +import { makeStyles } from 'tss-react/mui'; import { + CheckCircleOutline as CheckIcon, + Close as CloseIcon, + EditOutlined as EditIcon, +} from '@mui/icons-material'; +import { + Alert, Button, + Chip, Dialog, DialogContent, DialogTitle, @@ -9,30 +22,20 @@ import { LinearProgress, Paper, Typography, + useMediaQuery, } from '@mui/material'; +import { useTheme } from '@mui/material/styles'; + import { Day0Case, Outcome, YesNo } from '../api/models/Day0Case'; +import { Role } from '../api/models/User'; import AppModal from './AppModal'; -import EditIcon from '@mui/icons-material/EditOutlined'; -import CheckIcon from '@mui/icons-material/CheckCircleOutline'; -import { Link, useParams } from 'react-router-dom'; -import MuiAlert from '@mui/material/Alert'; -import Scroll from 'react-scroll'; -import axios from 'axios'; -import createHref from './util/links'; -import { makeStyles } from 'tss-react/mui'; import renderDate from './util/date'; -import useMediaQuery from '@mui/material/useMediaQuery'; -import { useTheme } from '@mui/material/styles'; -import Highlighter from 'react-highlight-words'; -import { useSelector } from 'react-redux'; +import createHref from './util/links'; import { selectFilterBreadcrumbs } from '../redux/app/selectors'; +import { selectUser } from '../redux/auth/selectors'; import { selectSearchQuery } from '../redux/linelistTable/selectors'; -import Chip from '@mui/material/Chip'; import { nameCountry } from './util/countryNames'; import { parseAgeRange } from './util/helperFunctions'; -import CloseIcon from '@mui/icons-material/Close'; -import { selectUser } from '../redux/auth/selectors'; -import { Role } from '../api/models/User'; const styles = makeStyles()(() => ({ errorMessage: { @@ -98,14 +101,14 @@ export default function ViewCase(props: Props): JSX.Element { <AppModal title="Case details" onModalClose={props.onModalClose}> {loading && <LinearProgress />} {errorMessage && ( - <MuiAlert + <Alert className={classes.errorMessage} elevation={6} variant="filled" severity="error" > {errorMessage} - </MuiAlert> + </Alert> )} {c && ( <CaseDetails @@ -966,16 +969,36 @@ function RowContent(props: { linkComment?: string; }): JSX.Element { const searchQuery = useSelector(selectSearchQuery); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const searchQueryArray: any[] = []; + const searchQueryArray: string[] = []; function words(s: string) { - const regex = /"([^"]+)"|(\w{3,})/g; - let match; - while ((match = regex.exec(s))) { - searchQueryArray.push(match[match[1] ? 1 : 2]); + const q = new URLSearchParams(s).get('q'); + if (!q) return; + + const quoted: string[] = []; + const notQuoted: string[] = []; + if (q.includes('"') && q.replace(/[^"]/g, '').length % 2 !== 1) { + q.split('"').map((subs: string, i: number) => { + subs != '' && i % 2 ? quoted.push(subs) : notQuoted.push(subs); + }); + } else notQuoted.push(q); + + const regex = /"([^"]+)"|(\S{1,})/g; + // Make sure that terms in quotes will be highlighted as one search term + for (const quotedEntry of quoted) { + let match; + let accumulator: string[] = []; + while ((match = regex.exec(quotedEntry))) { + accumulator.push(match[match[1] ? 1 : 2]); + } + searchQueryArray.push(accumulator.join(' ')); + } + for (const notQuotedEntry of notQuoted) { + let match; + while ((match = regex.exec(notQuotedEntry))) { + searchQueryArray.push(match[match[1] ? 1 : 2]); + } } - return searchQueryArray; } words(searchQuery); diff --git a/verification/curator-service/ui/src/components/common-form-fields/FormikFields.tsx b/verification/curator-service/ui/src/components/common-form-fields/FormikFields.tsx index 783ff5bcf..15b61bb6d 100644 --- a/verification/curator-service/ui/src/components/common-form-fields/FormikFields.tsx +++ b/verification/curator-service/ui/src/components/common-form-fields/FormikFields.tsx @@ -173,7 +173,7 @@ export function SelectField(props: SelectFieldProps): JSX.Element { > {props.values.map((value) => ( <MenuItem key={value} value={value}> - {value} + {value === '' ? <i>None</i> : value} </MenuItem> ))} </FastField> diff --git a/verification/curator-service/ui/src/components/fixtures/fullCase.json b/verification/curator-service/ui/src/components/fixtures/fullCase.json index 8d3f915d9..cc5730249 100644 --- a/verification/curator-service/ui/src/components/fixtures/fullCase.json +++ b/verification/curator-service/ui/src/components/fixtures/fullCase.json @@ -37,7 +37,7 @@ "symptoms": "Severe pneumonia, Dyspnea, Weakness", "preexistingConditions": { "previousInfection": "N", - "pregnancyStatus": "NA" + "pregnancyStatus": "" }, "transmission": { "contactWithCase": "N" diff --git a/verification/curator-service/ui/src/components/new-case-form-fields/CaseFormValues.tsx b/verification/curator-service/ui/src/components/new-case-form-fields/CaseFormValues.tsx index e40bf2e3f..ab9a71f1c 100644 --- a/verification/curator-service/ui/src/components/new-case-form-fields/CaseFormValues.tsx +++ b/verification/curator-service/ui/src/components/new-case-form-fields/CaseFormValues.tsx @@ -1,5 +1,6 @@ import { CaseReferenceForm } from '../common-form-fields/Source'; import { Location as Loc } from '../../api/models/Case'; +import { YesNo } from '../../api/models/Day0Case'; /** * CaseFormValues defines all the values contained in the manual case entry form. @@ -88,7 +89,7 @@ export interface Vaccine { batch?: string; date?: Date; sideEffects: Symptom; - previousInfection: 'yes' | 'no' | 'NA'; + previousInfection: YesNo; previousInfectionDetectionMethod?: string; } diff --git a/verification/curator-service/ui/src/components/new-case-form-fields/Demographics.tsx b/verification/curator-service/ui/src/components/new-case-form-fields/Demographics.tsx index 66baef092..3fcd37454 100644 --- a/verification/curator-service/ui/src/components/new-case-form-fields/Demographics.tsx +++ b/verification/curator-service/ui/src/components/new-case-form-fields/Demographics.tsx @@ -12,7 +12,7 @@ import { TextField } from 'formik-mui'; import { StyledTooltip } from './StyledTooltip'; import axios from 'axios'; import { makeStyles } from 'tss-react/mui'; -import { Gender, Day0CaseFormValues } from '../../api/models/Day0Case'; +import { Gender, Day0CaseFormValues, YesNo } from '../../api/models/Day0Case'; import { useStyles } from './styled'; const styles = makeStyles()(() => ({ @@ -169,7 +169,7 @@ export default function Demographics(): JSX.Element { <SelectField name="demographics.healthcareWorker" label="Healthcare worker" - values={['Y', 'N', 'NA']} + values={Object.values(YesNo)} /> </Scroll.Element> ); diff --git a/verification/curator-service/ui/src/components/new-case-form-fields/Events.tsx b/verification/curator-service/ui/src/components/new-case-form-fields/Events.tsx index c4da29e8e..73032ace3 100644 --- a/verification/curator-service/ui/src/components/new-case-form-fields/Events.tsx +++ b/verification/curator-service/ui/src/components/new-case-form-fields/Events.tsx @@ -1,15 +1,13 @@ import clsx from 'clsx'; -import { FastField, useFormikContext } from 'formik'; -import { TextField } from 'formik-mui'; +import {FastField, useFormikContext} from 'formik'; +import {TextField} from 'formik-mui'; import Scroll from 'react-scroll'; -import { Outcome, Day0CaseFormValues } from '../../api/models/Day0Case'; -import { DateField, SelectField } from '../common-form-fields/FormikFields'; +import {Day0CaseFormValues, Outcome, YesNo} from '../../api/models/Day0Case'; +import {DateField, SelectField} from '../common-form-fields/FormikFields'; import FieldTitle from '../common-form-fields/FieldTitle'; -import { useStyles } from './styled'; -import { StyledTooltip } from './StyledTooltip'; - -const yesNoUndefined = ['Y', 'N', 'NA']; +import {useStyles} from './styled'; +import {StyledTooltip} from './StyledTooltip'; const outcomes = ['recovered', 'death']; @@ -184,14 +182,14 @@ export default function Events(): JSX.Element { <SelectField name="events.homeMonitoring" label="Home monitoring" - values={yesNoUndefined} + values={Object.values(YesNo)} /> <SelectField name="events.isolated" label="Isolated" - values={yesNoUndefined} + values={Object.values(YesNo)} /> - {values.events.isolated === 'Y' && ( + {values.events.isolated === YesNo.Y && ( <DateField name="events.dateIsolation" label="Date of isolation" @@ -206,9 +204,9 @@ export default function Events(): JSX.Element { <SelectField name="events.hospitalized" label="Hospital admission" - values={yesNoUndefined} + values={Object.values(YesNo)} /> - {values.events.hospitalized === 'Y' && ( + {values.events.hospitalized === YesNo.Y && ( <> <DateField name="events.dateHospitalization" @@ -241,9 +239,9 @@ export default function Events(): JSX.Element { <SelectField name="events.intensiveCare" label="Intensive care" - values={yesNoUndefined} + values={Object.values(YesNo)} /> - {values.events.intensiveCare === 'Y' && ( + {values.events.intensiveCare === YesNo.Y && ( <> <DateField name="events.dateAdmissionICU" diff --git a/verification/curator-service/ui/src/components/new-case-form-fields/Location.tsx b/verification/curator-service/ui/src/components/new-case-form-fields/Location.tsx index 607260287..906b812af 100644 --- a/verification/curator-service/ui/src/components/new-case-form-fields/Location.tsx +++ b/verification/curator-service/ui/src/components/new-case-form-fields/Location.tsx @@ -120,7 +120,6 @@ export default function Location(): JSX.Element { setSelectedAdmin1(foundAdmin1Entry); } } - // eslint-disable-next-line react-hooks/exhaustive-deps }, [values.location.countryISO3, admin1Entries]); useEffect(() => { @@ -177,8 +176,6 @@ export default function Location(): JSX.Element { // Update mapbox indicator for admin1 setAdmin1AvailableOnMap(!!values.location.admin1WikiId); - - // eslint-disable-next-line react-hooks/exhaustive-deps }, [values.location.admin1WikiId, values.location.admin1, admin2Entries]); useEffect(() => { @@ -235,8 +232,6 @@ export default function Location(): JSX.Element { // Update mapbox indicator for admin2 setAdmin2AvailableOnMap(!!values.location.admin2WikiId); - - // eslint-disable-next-line react-hooks/exhaustive-deps }, [values.location.admin2WikiId, values.location.admin2, admin3Entries]); useEffect(() => { @@ -258,8 +253,6 @@ export default function Location(): JSX.Element { // Update mapbox indicator for admin3 setAdmin3AvailableOnMap(!!values.location.admin3WikiId); - - // eslint-disable-next-line react-hooks/exhaustive-deps }, [values.location.admin3WikiId, values.location.admin3]); useEffect(() => { @@ -326,15 +319,14 @@ export default function Location(): JSX.Element { 'location.geometry.latitude', values.location.geocodeLocation.geometry?.latitude || values.location.geometry?.latitude || - '', + undefined, ); setFieldValue( 'location.geometry.longitude', values.location.geocodeLocation.geometry?.longitude || values.location.geometry?.longitude || - '', + undefined, ); - // eslint-disable-next-line react-hooks/exhaustive-deps }, [values.location.geocodeLocation]); return ( @@ -486,7 +478,10 @@ export default function Location(): JSX.Element { _: unknown, newValue: adminEntry | null, ): void => { - setFieldValue('location.admin1', newValue?.name || ''); + setFieldValue( + 'location.admin1', + newValue?.name || '', + ); setFieldValue( 'location.admin1WikiId', newValue?.wiki || '', @@ -588,7 +583,10 @@ export default function Location(): JSX.Element { _: unknown, newValue: adminEntry | null, ): void => { - setFieldValue('location.admin2', newValue?.name || ''); + setFieldValue( + 'location.admin2', + newValue?.name || '', + ); setFieldValue( 'location.admin2WikiId', newValue?.wiki || '', @@ -690,7 +688,10 @@ export default function Location(): JSX.Element { _: unknown, newValue: adminEntry | null, ): void => { - setFieldValue('location.admin3', newValue?.name || ''); + setFieldValue( + 'location.admin3', + newValue?.name || '', + ); setFieldValue( 'location.admin3WikiId', newValue?.wiki || '', diff --git a/verification/curator-service/ui/src/components/new-case-form-fields/PreexistingConditions.tsx b/verification/curator-service/ui/src/components/new-case-form-fields/PreexistingConditions.tsx index 8fac45d58..1e3015ea0 100644 --- a/verification/curator-service/ui/src/components/new-case-form-fields/PreexistingConditions.tsx +++ b/verification/curator-service/ui/src/components/new-case-form-fields/PreexistingConditions.tsx @@ -3,7 +3,7 @@ import { FastField, useFormikContext } from 'formik'; import { TextField } from 'formik-mui'; import Scroll from 'react-scroll'; -import { Day0CaseFormValues } from '../../api/models/Day0Case'; +import { Day0CaseFormValues, YesNo } from '../../api/models/Day0Case'; import { FormikAutocomplete, SelectField, @@ -12,8 +12,6 @@ import FieldTitle from '../common-form-fields/FieldTitle'; import { useStyles } from './styled'; import { StyledTooltip } from './StyledTooltip'; -const hasPreexistingConditionsValues = ['Y', 'N', 'NA']; - const TooltipText = () => ( <StyledTooltip> <ul> @@ -60,7 +58,7 @@ export default function PreexistingConditions(): JSX.Element { <SelectField name="preexistingConditions.previousInfection" label="Previous infection" - values={hasPreexistingConditionsValues} + values={Object.values(YesNo)} /> <div className={clsx([classes.fieldRow, classes.halfWidth])}> @@ -87,7 +85,7 @@ export default function PreexistingConditions(): JSX.Element { <SelectField name="preexistingConditions.pregnancyStatus" label="Pregnancy status" - values={hasPreexistingConditionsValues} + values={Object.values(YesNo)} /> </Scroll.Element> ); diff --git a/verification/curator-service/ui/src/components/new-case-form-fields/Transmission.tsx b/verification/curator-service/ui/src/components/new-case-form-fields/Transmission.tsx index 300e00c82..77d5585ce 100644 --- a/verification/curator-service/ui/src/components/new-case-form-fields/Transmission.tsx +++ b/verification/curator-service/ui/src/components/new-case-form-fields/Transmission.tsx @@ -3,7 +3,7 @@ import { FastField, useFormikContext } from 'formik'; import { TextField } from 'formik-mui'; import Scroll from 'react-scroll'; -import { Day0CaseFormValues } from '../../api/models/Day0Case'; +import { Day0CaseFormValues, YesNo } from '../../api/models/Day0Case'; import FieldTitle from '../common-form-fields/FieldTitle'; import { FormikAutocomplete, @@ -54,7 +54,7 @@ export default function Transmission(): JSX.Element { <SelectField name="transmission.contactWithCase" label="Contact with case" - values={['Y', 'N', 'NA']} + values={Object.values(YesNo)} /> <div className={clsx([classes.fieldRow, classes.halfWidth])}> <FastField diff --git a/verification/curator-service/ui/src/components/new-case-form-fields/TravelHistory.tsx b/verification/curator-service/ui/src/components/new-case-form-fields/TravelHistory.tsx index 225d00e00..26e1e72cf 100644 --- a/verification/curator-service/ui/src/components/new-case-form-fields/TravelHistory.tsx +++ b/verification/curator-service/ui/src/components/new-case-form-fields/TravelHistory.tsx @@ -8,8 +8,7 @@ import { DateField, SelectField } from '../common-form-fields/FormikFields'; import FieldTitle from '../common-form-fields/FieldTitle'; import { useStyles } from './styled'; import { StyledTooltip } from './StyledTooltip'; - -const hasTravelledValues = ['Y', 'N', 'NA']; +import { YesNo } from '../../api/models/Day0Case'; const TooltipText = () => ( <StyledTooltip> @@ -59,9 +58,9 @@ export default function TravelHistory(): JSX.Element { <SelectField name="travelHistory.travelHistory" label="Travel history" - values={hasTravelledValues} + values={Object.values(YesNo)} /> - {values.travelHistory.travelHistory === 'Y' && ( + {values.travelHistory.travelHistory === YesNo.Y && ( <> <DateField name="travelHistory.travelHistoryEntry" diff --git a/verification/curator-service/ui/src/components/new-case-form-fields/Vaccines.tsx b/verification/curator-service/ui/src/components/new-case-form-fields/Vaccines.tsx index d04115690..88be2afe3 100644 --- a/verification/curator-service/ui/src/components/new-case-form-fields/Vaccines.tsx +++ b/verification/curator-service/ui/src/components/new-case-form-fields/Vaccines.tsx @@ -1,14 +1,14 @@ import clsx from 'clsx'; -import { FastField, useFormikContext } from 'formik'; -import { TextField } from 'formik-mui'; +import {FastField, useFormikContext} from 'formik'; +import {TextField} from 'formik-mui'; import Scroll from 'react-scroll'; -import { Day0CaseFormValues } from '../../api/models/Day0Case'; -import { DateField, SelectField } from '../common-form-fields/FormikFields'; +import {Day0CaseFormValues, YesNo} from '../../api/models/Day0Case'; +import {DateField, SelectField} from '../common-form-fields/FormikFields'; import FieldTitle from '../common-form-fields/FieldTitle'; -import { useStyles } from './styled'; -import { StyledTooltip } from './StyledTooltip'; -import { VaccineSideEffects } from './Symptoms'; +import {useStyles} from './styled'; +import {StyledTooltip} from './StyledTooltip'; +import {VaccineSideEffects} from './Symptoms'; const TooltipText = () => ( <StyledTooltip> @@ -45,9 +45,9 @@ export default function Vaccines(): JSX.Element { <SelectField name="vaccination.vaccination" label="Vaccination" - values={['Y', 'N', 'NA']} + values={Object.values(YesNo)} /> - {values.vaccination.vaccination === 'Y' && ( + {values.vaccination.vaccination === YesNo.Y && ( <> <div className={clsx([classes.fieldRow, classes.halfWidth])} diff --git a/verification/curator-service/ui/src/redux/auth/slice.ts b/verification/curator-service/ui/src/redux/auth/slice.ts index 3a56a5915..b4e38fee5 100644 --- a/verification/curator-service/ui/src/redux/auth/slice.ts +++ b/verification/curator-service/ui/src/redux/auth/slice.ts @@ -157,6 +157,7 @@ const authSlice = createSlice({ // LOGOUT builder.addCase(logout.fulfilled, (state) => { state.user = undefined; + localStorage.removeItem('user'); }); // CHANGE PASSWORD