From 929ea2a477345444e6162b7274cc5d5dc8305248 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Stanis=C5=82aw=20Zakrzewski?=
<41862803+stanislaw-zakrzewski@users.noreply.github.com>
Date: Thu, 17 Oct 2024 14:05:11 +0200
Subject: [PATCH 1/3] [131] update mongodb validation for the case schema to
allow search (#136)
* Update libraries batch-2 first part
* Move some testing to TODO
* Update Yup when
* update set field values
* Update breaking changes
* Update comment
* Remove unused
* update datepicker
* Update libraries batch-2 second part
* Update return values of the functions for countries
* update typescript
* Update mismatched versions
* WIP update router and eslint + vite instead of cra
* Update tests for new vitest library
* Update command for test and coverage
* Skip tests that were blocking flow
* Update Location.test.tsx
* Update varaible name
* Fix merging issues
* Update index.tsx
* Update index.tsx
* WIP tests for location and landing page
* Fixed all landing page tests
* Unskip and fix verification status indicator tests
* Fix automated source form tests
* WIP unmocking EditCase tests
* Update EditCase.test.tsx
* Fix viewcase tests
* Resolve warnings regarding act
* Update Dockerfile
* Update Location.test.tsx
* Fix tests failing at the first day of the month
* Cleanup sourcetable
* Update Users Uploads and Sources tables
* Add backfill back
It was removed after splitting one component into multiple
* Rename collection from cases to day0cases
* Update day0cases.schema.json
* Update day0cases.schema.json
* Initial schema work
* initial schema work
* Fix location lat/long update issue
* Add missing schema element - isGovernmentSource for Additional Sources
* Update tests for searchbar and data guide
* Update README.md
---
.../data-service/src/controllers/case.ts | 7 +
.../migrations/20210902121948-initial.js | 99 +++++------
.../20210922082905-additional-case-indexes.js | 44 ++---
data-serving/scripts/setup-db/package.json | 1 +
...es.indexes.json => day0cases.indexes.json} | 28 +++-
...ases.schema.json => day0cases.schema.json} | 154 +++++++++++++++++-
dev/README.md | 5 +
.../e2e/components/LinelistTableTest.spec.ts | 59 +++++++
.../ui/src/components/DataGuideDialog.tsx | 12 +-
.../ui/src/components/SearchBar.tsx | 3 +-
.../new-case-form-fields/Location.tsx | 27 +--
11 files changed, 348 insertions(+), 91 deletions(-)
rename data-serving/scripts/setup-db/schemas/{cases.indexes.json => day0cases.indexes.json} (83%)
rename data-serving/scripts/setup-db/schemas/{cases.schema.json => day0cases.schema.json} (60%)
diff --git a/data-serving/data-service/src/controllers/case.ts b/data-serving/data-service/src/controllers/case.ts
index f90663ee2..02ce0eaef 100644
--- a/data-serving/data-service/src/controllers/case.ts
+++ b/data-serving/data-service/src/controllers/case.ts
@@ -795,6 +795,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/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 83%
rename from data-serving/scripts/setup-db/schemas/cases.indexes.json
rename to data-serving/scripts/setup-db/schemas/day0cases.indexes.json
index 210417dd7..a7007c6b6 100644
--- a/data-serving/scripts/setup-db/schemas/cases.indexes.json
+++ b/data-serving/scripts/setup-db/schemas/day0cases.indexes.json
@@ -4,7 +4,9 @@
"key": {
"demographics.occupation": "text",
"location.country": "text",
- "location.city": "text",
+ "location.admin1": "text",
+ "location.admin2": "text",
+ "location.admin3": "text",
"caseReference.sourceUrl": "text",
"caseStatus": "text"
}
@@ -74,9 +76,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",
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/verification/curator-service/ui/cypress/e2e/components/LinelistTableTest.spec.ts b/verification/curator-service/ui/cypress/e2e/components/LinelistTableTest.spec.ts
index e11307ab7..7cc942796 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,63 @@ 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',
+ });
+ 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.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');
+ });
});
diff --git a/verification/curator-service/ui/src/components/DataGuideDialog.tsx b/verification/curator-service/ui/src/components/DataGuideDialog.tsx
index 95c406d5b..d24c7a03c 100644
--- a/verification/curator-service/ui/src/components/DataGuideDialog.tsx
+++ b/verification/curator-service/ui/src/components/DataGuideDialog.tsx
@@ -96,8 +96,16 @@ const SearchGuideDialog = ({
the left and choose ascending or descending.
- For full-text search, enter any
- combination of search terms.
+ For full-text search, enter any combination of search terms.
+
+ Full-text search covers: occupation, admin0, admin1, admin2, admin3, sourceUrl and caseStatus.
+
+ Search terms must be exact (example: "German" will not match "Germany").
+
+ Full-text search matches cases that contain any ot the search terms, not a combination.
+
+ No special characters apart from "." are allowed and if the "." is used in a search term
+ given search term must be contained within quotation marks.
You can use the icons on the right to navigate
diff --git a/verification/curator-service/ui/src/components/SearchBar.tsx b/verification/curator-service/ui/src/components/SearchBar.tsx
index 9b9d79d53..a4ebf82e2 100644
--- a/verification/curator-service/ui/src/components/SearchBar.tsx
+++ b/verification/curator-service/ui/src/components/SearchBar.tsx
@@ -203,7 +203,7 @@ export default function SearchBar({
}
}}
placeholder="Fulltext search"
- value={searchInput}
+ value={decodeURI(searchInput)}
variant="outlined"
fullWidth
InputProps={{
@@ -254,6 +254,7 @@ export default function SearchBar({
{
setSearchInput('');
navigate({
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 || '',
From 881e39ef33e2dd1be1738c8ede4d683f226a3fac Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Stanis=C5=82aw=20Zakrzewski?=
<41862803+stanislaw-zakrzewski@users.noreply.github.com>
Date: Mon, 28 Oct 2024 15:56:30 +0100
Subject: [PATCH 2/3] [98] add query for curator comments (#154)
* Allow search for curators comment
* Fix highlighting of the text in view case form
* Update DataGuideDialog.tsx
* Cleanup
* Inform user if there is any not closed quote
* Cleanup code + remove console.log
* Add cypress tests for comments and uneven quote warning
* WIP fixed issues with filters and special characters in free text search
* Update index.tsx
* Update SearchBar.tsx
* Update index.tsx
* Fix setting filters
* Fix issues with text-search not visible
* Cleanup
* Cleanup SearchBar
* Remove console.log
---
.../setup-db/schemas/day0cases.indexes.json | 13 +++-
.../e2e/components/LinelistTableTest.spec.ts | 27 +++++++
.../ui/cypress/support/commands.ts | 2 +
.../ui/src/components/App/index.tsx | 6 +-
.../ui/src/components/DataGuideDialog.tsx | 55 +++++++++++---
.../ui/src/components/FiltersDialog/index.tsx | 14 +++-
.../ui/src/components/SearchBar.tsx | 57 ++++++++-------
.../ui/src/components/ViewCase.tsx | 73 ++++++++++++-------
8 files changed, 178 insertions(+), 69 deletions(-)
diff --git a/data-serving/scripts/setup-db/schemas/day0cases.indexes.json b/data-serving/scripts/setup-db/schemas/day0cases.indexes.json
index a7007c6b6..b55dd708a 100644
--- a/data-serving/scripts/setup-db/schemas/day0cases.indexes.json
+++ b/data-serving/scripts/setup-db/schemas/day0cases.indexes.json
@@ -8,7 +8,8 @@
"location.admin2": "text",
"location.admin3": "text",
"caseReference.sourceUrl": "text",
- "caseStatus": "text"
+ "caseStatus": "text",
+ "comment": "text"
}
},
{
@@ -145,6 +146,16 @@
"strength": 2
}
},
+ {
+ "name": "commentIdx",
+ "key": {
+ "comment": -1
+ },
+ "collation": {
+ "locale": "en_US",
+ "strength": 2
+ }
+ },
{
"name": "countryAndDate",
"key": {
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 7cc942796..be520b375 100644
--- a/verification/curator-service/ui/cypress/e2e/components/LinelistTableTest.spec.ts
+++ b/verification/curator-service/ui/cypress/e2e/components/LinelistTableTest.spec.ts
@@ -501,6 +501,7 @@ describe('Linelist table', function () {
sourceUrl: 'www.example.com',
caseStatus: CaseStatus.Confirmed,
occupation: 'Actor',
+ comment: 'note',
});
cy.addCase({
country: 'Germany',
@@ -524,6 +525,7 @@ describe('Linelist table', function () {
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');
@@ -549,5 +551,30 @@ describe('Linelist table', function () {
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..c951c0be8 100644
--- a/verification/curator-service/ui/cypress/support/commands.ts
+++ b/verification/curator-service/ui/cypress/support/commands.ts
@@ -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/components/App/index.tsx b/verification/curator-service/ui/src/components/App/index.tsx
index 0cc3fe81d..44c0e58c4 100644
--- a/verification/curator-service/ui/src/components/App/index.tsx
+++ b/verification/curator-service/ui/src/components/App/index.tsx
@@ -327,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' } },
);
@@ -371,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 d24c7a03c..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
@@ -96,16 +96,49 @@ const SearchGuideDialog = ({
the left and choose ascending or descending.
- For full-text search, enter any combination of search terms.
-
- Full-text search covers: occupation, admin0, admin1, admin2, admin3, sourceUrl and caseStatus.
-
- Search terms must be exact (example: "German" will not match "Germany").
-
- Full-text search matches cases that contain any ot the search terms, not a combination.
-
- No special characters apart from "." are allowed and if the "." is used in a search term
- given search term must be contained within quotation marks.
+ For full-text search, enter any
+ combination of search terms. Rules for full-text
+ search:
+
+
+ -
+ Full-text search covers: occupation, admin0,
+ admin1, admin2, admin3, sourceUrl, comment
+ and caseStatus.
+
+ -
+ Search terms must be exact (example:{' '}
+
+ German
+ {' '}
+ will not match{' '}
+
+ Germany
+
+ ).
+
+ -
+ Full-text search matches cases that contain
+ any of the search terms, not a combination.
+
+ -
+ To search for a combination of terms, wrap
+ the combination in quotation marks (example:{' '}
+
+ "Bus driver"
+
+ ).
+
+ -
+ No special characters apart from dot are
+ allowed. Search terms with dot must be
+ contained within quotation marks (example:{' '}
+
+ "global.health"
+
+ ).
+
+
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 a4ebf82e2..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(false);
const [isDataGuideOpen, setIsDataGuideOpen] = useState(false);
const [searchInput, setSearchInput] = useState(
- location.search.includes('?q=')
- ? URLToSearchQuery(location.search)
- : '',
+ new URLSearchParams(location.search).get('q') || '',
);
const [modalAlert, setModalAlert] = useState(false);
const guideButtonRef = React.useRef(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 (
<>
({
errorMessage: {
@@ -98,14 +101,14 @@ export default function ViewCase(props: Props): JSX.Element {
{loading && }
{errorMessage && (
-
{errorMessage}
-
+
)}
{c && (
{
+ 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);
From 8db28d3d61d82e516d37453fc849cf4e0d03c81e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Stanis=C5=82aw=20Zakrzewski?=
<41862803+stanislaw-zakrzewski@users.noreply.github.com>
Date: Wed, 20 Nov 2024 08:11:42 +0100
Subject: [PATCH 3/3] [104] change date format (#155)
* Format date properly for csv, tsv and json download
* Update tests
* Use dedicated function for date formatting
* Remove only from tests
---
.../data-service/src/controllers/case.ts | 8 +++
data-serving/data-service/src/util/case.ts | 65 +++++++++++--------
.../data-service/test/util/case.test.ts | 27 ++++++--
3 files changed, 67 insertions(+), 33 deletions(-)
diff --git a/data-serving/data-service/src/controllers/case.ts b/data-serving/data-service/src/controllers/case.ts
index 02ce0eaef..11e59454b 100644
--- a/data-serving/data-service/src/controllers/case.ts
+++ b/data-serving/data-service/src/controllers/case.ts
@@ -352,6 +352,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();
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> => {
@@ -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 = {};
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..b43715dc5 100644
--- a/data-serving/data-service/test/util/case.test.ts
+++ b/data-serving/data-service/test/util/case.test.ts
@@ -13,7 +13,11 @@ 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';
@@ -421,16 +425,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('');
@@ -612,7 +616,7 @@ describe('Case', () => {
expect(denormalizedCase['travelHistory.travelHistory']).toEqual('Y');
expect(denormalizedCase['travelHistory.travelHistoryEntry']).toEqual(
- travelHistoryDoc.travelHistoryEntry.toDateString(),
+ formatDateWithoutTime(travelHistoryDoc.travelHistoryEntry),
);
expect(denormalizedCase['travelHistory.travelHistoryStart']).toEqual(
'start',
@@ -652,7 +656,7 @@ describe('Case', () => {
expect(denormalizedCase['vaccination.vaccination']).toEqual('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 +693,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('');
+ });
});