From c0ac03e69638941e9b141fb11ce4f6767c7da463 Mon Sep 17 00:00:00 2001 From: Kevin Date: Fri, 22 Jul 2022 17:49:57 +0300 Subject: [PATCH 01/13] Updated dateToSeconds function --- app/shared/dateToSeconds.ts | 14 ++++++++++++++ app/shared/specs/dateToSeconds.spec.ts | 23 +++++++++++++++++++++++ package.json | 1 + yarn.lock | 5 +++++ 4 files changed, 43 insertions(+) create mode 100644 app/shared/dateToSeconds.ts create mode 100644 app/shared/specs/dateToSeconds.spec.ts diff --git a/app/shared/dateToSeconds.ts b/app/shared/dateToSeconds.ts new file mode 100644 index 0000000000..4f5293a31b --- /dev/null +++ b/app/shared/dateToSeconds.ts @@ -0,0 +1,14 @@ +import parser from 'any-date-parser'; + +const dateToSeconds = (value: string) => { + // Remove accents + const parsedValue = value.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); + let getDate = parser.fromString(parsedValue); + if (getDate.invalid) { + getDate = Date.parse(`${parsedValue} GMT`); + } + const formattedDate = getDate / 1000; + return formattedDate; +}; + +export { dateToSeconds }; diff --git a/app/shared/specs/dateToSeconds.spec.ts b/app/shared/specs/dateToSeconds.spec.ts new file mode 100644 index 0000000000..9461cccb70 --- /dev/null +++ b/app/shared/specs/dateToSeconds.spec.ts @@ -0,0 +1,23 @@ +import { dateToSeconds } from '../dateToSeconds'; + +describe('dateToSeconds', () => { + it.each` + stringDate | expectedMilliseconds + ${'2020-01-01'} | ${1577836800} + ${'17-05-2018'} | ${1526515200} + ${'11-21-1982'} | ${406684800} + ${'September 30, 1999'} | ${938649600} + ${'13 October 2012'} | ${1350086400} + ${'13th October 2012'} | ${1350086400} + ${'October 13th, 2012'} | ${1350086400} + ${'22 decembre 2012'} | ${1356134400} + ${'mercredi 22 decembre 2010'} | ${1292976000} + ${'mercredi 22 décembre 2010'} | ${1292976000} + ${'Marzo 14, 2006'} | ${1142294400} + `( + 'should convert $stringDate to $expectedMilliseconds', + ({ stringDate, expectedMilliseconds }) => { + expect(dateToSeconds(stringDate)).toBe(expectedMilliseconds); + } + ); +}); diff --git a/package.json b/package.json index 5ed3101464..34d30e085d 100644 --- a/package.json +++ b/package.json @@ -98,6 +98,7 @@ "ajv": "^8.11.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0", + "any-date-parser": "^1.5.3", "bcryptjs": "^2.4.3", "big.js": "^6.1.1", "body-parser": "^1.20.0", diff --git a/yarn.lock b/yarn.lock index 5a770f47d0..a13873392d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3415,6 +3415,11 @@ ansi-styles@^5.0.0: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== +any-date-parser@^1.5.3: + version "1.5.3" + resolved "https://registry.yarnpkg.com/any-date-parser/-/any-date-parser-1.5.3.tgz#2a7a16e5fc863b8b7c4d0e4572ed50ff73f0b54b" + integrity sha512-X4sMMSzNoGmZTrFzsNKygRF2J1rBQvHNEW6rArOEH+pWHsyM6SDiqVz8yAbuYE2qiVN7btluIjijMwVDMkDj+g== + any-promise@^1.0.0: version "1.3.0" resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" From 2284a722078b06e0cdb23ccc098498b6e3718365 Mon Sep 17 00:00:00 2001 From: Kevin Date: Mon, 25 Jul 2022 12:18:09 +0300 Subject: [PATCH 02/13] Removed dateToSeconds to dataUtils --- app/shared/dataUtils.ts | 13 ++++++------- app/shared/dateToSeconds.ts | 14 -------------- app/shared/specs/dataUtils.spec.ts | 19 ++++++++++++------- app/shared/specs/dateToSeconds.spec.ts | 23 ----------------------- 4 files changed, 18 insertions(+), 51 deletions(-) delete mode 100644 app/shared/dateToSeconds.ts delete mode 100644 app/shared/specs/dateToSeconds.spec.ts diff --git a/app/shared/dataUtils.ts b/app/shared/dataUtils.ts index e7a1ef3ec0..94699e294d 100644 --- a/app/shared/dataUtils.ts +++ b/app/shared/dataUtils.ts @@ -1,4 +1,4 @@ -import moment from 'moment'; +import parser from 'any-date-parser'; const arrayDeepEquals = (a: Array, b: Array): boolean => { if (a.length !== b.length) return false; @@ -37,12 +37,11 @@ const deepEquals = (a: any, b: any): boolean => { }; const dateToSeconds = (value: string) => { - let getDate = Date.parse(`${value} GMT`); - if (Number.isNaN(getDate)) { - const momentDate = moment - .utc(value, ['DD-MM-YYYY', 'MM-DD-YYYY', 'YYYY-MM-DD', 'YYYY'], false) - .format('x'); - getDate = parseInt(momentDate, 10); + // Remove accents + const parsedValue = value.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); + let getDate = parser.fromString(parsedValue); + if (getDate.invalid) { + getDate = Date.parse(`${parsedValue} GMT`); } const formattedDate = getDate / 1000; return formattedDate; diff --git a/app/shared/dateToSeconds.ts b/app/shared/dateToSeconds.ts deleted file mode 100644 index 4f5293a31b..0000000000 --- a/app/shared/dateToSeconds.ts +++ /dev/null @@ -1,14 +0,0 @@ -import parser from 'any-date-parser'; - -const dateToSeconds = (value: string) => { - // Remove accents - const parsedValue = value.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); - let getDate = parser.fromString(parsedValue); - if (getDate.invalid) { - getDate = Date.parse(`${parsedValue} GMT`); - } - const formattedDate = getDate / 1000; - return formattedDate; -}; - -export { dateToSeconds }; diff --git a/app/shared/specs/dataUtils.spec.ts b/app/shared/specs/dataUtils.spec.ts index 5ff03338dd..0b7f1886b5 100644 --- a/app/shared/specs/dataUtils.spec.ts +++ b/app/shared/specs/dataUtils.spec.ts @@ -293,13 +293,18 @@ describe('deepEquals', () => { describe('dateToSeconds', () => { it.each` - stringDate | expectedMilliseconds - ${'2020-01-01'} | ${1577836800} - ${'17-05-2018'} | ${1526515200} - ${'11-21-1982'} | ${406684800} - ${'September 30, 1999'} | ${938649600} - ${'13 October 2012'} | ${1350086400} - ${'Marzo 14, 2006'} | ${1142294400} + stringDate | expectedMilliseconds + ${'2020-01-01'} | ${1577836800} + ${'17-05-2018'} | ${1526515200} + ${'11-21-1982'} | ${406684800} + ${'September 30, 1999'} | ${938649600} + ${'13 October 2012'} | ${1350086400} + ${'13th October 2012'} | ${1350086400} + ${'October 13th, 2012'} | ${1350086400} + ${'22 decembre 2012'} | ${1356134400} + ${'mercredi 22 decembre 2010'} | ${1292976000} + ${'mercredi 22 décembre 2010'} | ${1292976000} + ${'Marzo 14, 2006'} | ${1142294400} `( 'should convert $stringDate to $expectedMilliseconds', ({ stringDate, expectedMilliseconds }) => { diff --git a/app/shared/specs/dateToSeconds.spec.ts b/app/shared/specs/dateToSeconds.spec.ts deleted file mode 100644 index 9461cccb70..0000000000 --- a/app/shared/specs/dateToSeconds.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { dateToSeconds } from '../dateToSeconds'; - -describe('dateToSeconds', () => { - it.each` - stringDate | expectedMilliseconds - ${'2020-01-01'} | ${1577836800} - ${'17-05-2018'} | ${1526515200} - ${'11-21-1982'} | ${406684800} - ${'September 30, 1999'} | ${938649600} - ${'13 October 2012'} | ${1350086400} - ${'13th October 2012'} | ${1350086400} - ${'October 13th, 2012'} | ${1350086400} - ${'22 decembre 2012'} | ${1356134400} - ${'mercredi 22 decembre 2010'} | ${1292976000} - ${'mercredi 22 décembre 2010'} | ${1292976000} - ${'Marzo 14, 2006'} | ${1142294400} - `( - 'should convert $stringDate to $expectedMilliseconds', - ({ stringDate, expectedMilliseconds }) => { - expect(dateToSeconds(stringDate)).toBe(expectedMilliseconds); - } - ); -}); From 3ea39fc9287d0841ef39723bc2a5d2bd4495b006 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 26 Jul 2022 11:50:15 +0300 Subject: [PATCH 03/13] Moved dateToSecond function to the server --- app/api/utils/date.js | 26 -------------------- app/api/utils/date.ts | 39 ++++++++++++++++++++++++++++++ app/api/utils/specs/date.spec.js | 27 +++++++++++++++++++++ app/shared/dataUtils.ts | 15 +----------- app/shared/specs/dataUtils.spec.ts | 24 +----------------- 5 files changed, 68 insertions(+), 63 deletions(-) delete mode 100644 app/api/utils/date.js create mode 100644 app/api/utils/date.ts diff --git a/app/api/utils/date.js b/app/api/utils/date.js deleted file mode 100644 index 812ebf6ac3..0000000000 --- a/app/api/utils/date.js +++ /dev/null @@ -1,26 +0,0 @@ -import moment from 'moment'; - -export default { - currentUTC() { - return moment.utc().toDate().getTime(); - }, - - descriptionToTimestamp(date) { - if (date === 'last-day-last-month') { - return moment.utc().subtract(1, 'months').endOf('month').unix(); - } - - if (date === 'first-day-last-month') { - return moment.utc().subtract(1, 'months').startOf('month').unix(); - } - - return date; - }, - - addYearsToCurrentDate(yearsToAdd) { - const newDate = new Date(); - newDate.setHours(0, 0, 0, 0); - newDate.setFullYear(newDate.getFullYear() + yearsToAdd); - return newDate; - }, -}; diff --git a/app/api/utils/date.ts b/app/api/utils/date.ts new file mode 100644 index 0000000000..581d19904e --- /dev/null +++ b/app/api/utils/date.ts @@ -0,0 +1,39 @@ +import moment from 'moment'; +// @ts-ignore +import parser from 'any-date-parser'; + +export default { + currentUTC() { + return moment.utc().toDate().getTime(); + }, + + descriptionToTimestamp(date: string) { + if (date === 'last-day-last-month') { + return moment.utc().subtract(1, 'months').endOf('month').unix(); + } + + if (date === 'first-day-last-month') { + return moment.utc().subtract(1, 'months').startOf('month').unix(); + } + + return date; + }, + + addYearsToCurrentDate(yearsToAdd: number) { + const newDate = new Date(); + newDate.setHours(0, 0, 0, 0); + newDate.setFullYear(newDate.getFullYear() + yearsToAdd); + return newDate; + }, + + dateToSeconds(value: string, locale: string | undefined) { + // console.log('Date value: ', value, ' locale: ', locale); + const parsedValue = value.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); + let getDate = parser.fromString(parsedValue, locale); + if (getDate.invalid) { + getDate = Date.parse(`${parsedValue} GMT`); + } + const formattedDate = getDate / 1000; + return formattedDate; + }, +}; diff --git a/app/api/utils/specs/date.spec.js b/app/api/utils/specs/date.spec.js index b2bf93814d..0619f26094 100644 --- a/app/api/utils/specs/date.spec.js +++ b/app/api/utils/specs/date.spec.js @@ -32,4 +32,31 @@ describe('date helper', () => { const newDate = date.addYearsToCurrentDate(1); expect(newDate).toEqual(nextYear); }); + describe('dateToSeconds', () => { + it.each` + stringDate | language | expectedMilliseconds + ${'5-2-2020'} | ${'en-GB'} | ${1580860800} + ${'5-2-2020'} | ${'en-US'} | ${1588377600} + ${'2020-01-01'} | ${undefined} | ${1577836800} + ${'17-05-2018'} | ${undefined} | ${1526515200} + ${'17/05/2018'} | ${undefined} | ${1526515200} + ${'11-21-1982'} | ${undefined} | ${406684800} + ${'05-17-2018'} | ${undefined} | ${1526515200} + ${'September 30, 1999'} | ${undefined} | ${938649600} + ${'13 October 2012'} | ${undefined} | ${1350086400} + ${'13th October 2012'} | ${undefined} | ${1350086400} + ${'October 13th, 2012'} | ${undefined} | ${1350086400} + ${'22 decembre 2012'} | ${undefined} | ${1356134400} + ${'mercredi 22 décembre 2010'} | ${undefined} | ${1292976000} + ${'Marzo 14, 2006'} | ${undefined} | ${1142294400} + ${'28 августа 2017'} | ${'ru'} | ${1503878400} + ${'августа 28, 2017'} | ${'ru'} | ${1503878400} + ${'shahr sibtambar 2017'} | ${'ar'} | ${1000} + `( + 'should convert $stringDate to $expectedMilliseconds', + ({ stringDate, language, expectedMilliseconds }) => { + expect(date.dateToSeconds(stringDate, language)).toBe(expectedMilliseconds); + } + ); + }); }); diff --git a/app/shared/dataUtils.ts b/app/shared/dataUtils.ts index 94699e294d..3e807b05e2 100644 --- a/app/shared/dataUtils.ts +++ b/app/shared/dataUtils.ts @@ -1,5 +1,3 @@ -import parser from 'any-date-parser'; - const arrayDeepEquals = (a: Array, b: Array): boolean => { if (a.length !== b.length) return false; for (let i = 0; i < a.length; i += 1) { @@ -36,15 +34,4 @@ const deepEquals = (a: any, b: any): boolean => { return a === b; }; -const dateToSeconds = (value: string) => { - // Remove accents - const parsedValue = value.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); - let getDate = parser.fromString(parsedValue); - if (getDate.invalid) { - getDate = Date.parse(`${parsedValue} GMT`); - } - const formattedDate = getDate / 1000; - return formattedDate; -}; - -export { deepEquals, dateToSeconds }; +export { deepEquals }; diff --git a/app/shared/specs/dataUtils.spec.ts b/app/shared/specs/dataUtils.spec.ts index 0b7f1886b5..2d55cc4d7a 100644 --- a/app/shared/specs/dataUtils.spec.ts +++ b/app/shared/specs/dataUtils.spec.ts @@ -1,4 +1,4 @@ -import { dateToSeconds, deepEquals } from '../dataUtils'; +import { deepEquals } from '../dataUtils'; describe('deepEquals', () => { it('should handle null or undefined', () => { @@ -290,25 +290,3 @@ describe('deepEquals', () => { }); }); }); - -describe('dateToSeconds', () => { - it.each` - stringDate | expectedMilliseconds - ${'2020-01-01'} | ${1577836800} - ${'17-05-2018'} | ${1526515200} - ${'11-21-1982'} | ${406684800} - ${'September 30, 1999'} | ${938649600} - ${'13 October 2012'} | ${1350086400} - ${'13th October 2012'} | ${1350086400} - ${'October 13th, 2012'} | ${1350086400} - ${'22 decembre 2012'} | ${1356134400} - ${'mercredi 22 decembre 2010'} | ${1292976000} - ${'mercredi 22 décembre 2010'} | ${1292976000} - ${'Marzo 14, 2006'} | ${1142294400} - `( - 'should convert $stringDate to $expectedMilliseconds', - ({ stringDate, expectedMilliseconds }) => { - expect(dateToSeconds(stringDate)).toBe(expectedMilliseconds); - } - ); -}); From 1869251073939de48a1c35e326c1db8172ab5053 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 26 Jul 2022 12:29:31 +0300 Subject: [PATCH 04/13] Renamed date.ts back to date.js --- app/api/utils/{date.ts => date.js} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename app/api/utils/{date.ts => date.js} (86%) diff --git a/app/api/utils/date.ts b/app/api/utils/date.js similarity index 86% rename from app/api/utils/date.ts rename to app/api/utils/date.js index 581d19904e..f1e09752c4 100644 --- a/app/api/utils/date.ts +++ b/app/api/utils/date.js @@ -7,7 +7,7 @@ export default { return moment.utc().toDate().getTime(); }, - descriptionToTimestamp(date: string) { + descriptionToTimestamp(date) { if (date === 'last-day-last-month') { return moment.utc().subtract(1, 'months').endOf('month').unix(); } @@ -19,14 +19,14 @@ export default { return date; }, - addYearsToCurrentDate(yearsToAdd: number) { + addYearsToCurrentDate(yearsToAdd) { const newDate = new Date(); newDate.setHours(0, 0, 0, 0); newDate.setFullYear(newDate.getFullYear() + yearsToAdd); return newDate; }, - dateToSeconds(value: string, locale: string | undefined) { + dateToSeconds(value, locale) { // console.log('Date value: ', value, ' locale: ', locale); const parsedValue = value.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); let getDate = parser.fromString(parsedValue, locale); From 09acbb6bf00b749543f1dd3c6fce903bce36f710 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 26 Jul 2022 13:56:57 +0300 Subject: [PATCH 05/13] Added endpoint --- app/api/api.js | 1 + app/api/dates/routes.ts | 26 ++++++++++++++++++++++++++ app/react/utils/dateAPI.ts | 10 ++++++++++ 3 files changed, 37 insertions(+) create mode 100644 app/api/dates/routes.ts create mode 100644 app/react/utils/dateAPI.ts diff --git a/app/api/api.js b/app/api/api.js index b0f5ce8907..8560411a86 100644 --- a/app/api/api.js +++ b/app/api/api.js @@ -40,4 +40,5 @@ export default (app, server) => { require('./permissions/routes').permissionRoutes(app); require('./suggestions/routes').suggestionsRoutes(app); require('./preserve/routes').PreserveRoutes(app); + require('./dates/routes').datesConversionRoutes(app); }; diff --git a/app/api/dates/routes.ts b/app/api/dates/routes.ts new file mode 100644 index 0000000000..b370bd0d84 --- /dev/null +++ b/app/api/dates/routes.ts @@ -0,0 +1,26 @@ +import { Application, Request, Response, NextFunction } from 'express'; +import { validation } from 'api/utils'; +import date from '../utils/date'; + +export const datesConversionRoutes = (app: Application) => { + app.post( + '/api/date-to-seconds', + validation.validateRequest({ + type: 'object', + properties: { + body: { + type: 'object', + properties: { + dateString: { type: 'string' }, + locale: { type: 'string' }, + }, + }, + }, + }), + + async (req: Request, res: Response, _next: NextFunction) => { + const dateSeconds = date.dateToSeconds(req.body.dateString, req.body.locale); + res.status(200).json({ success: true, date: dateSeconds }); + } + ); +}; diff --git a/app/react/utils/dateAPI.ts b/app/react/utils/dateAPI.ts new file mode 100644 index 0000000000..385abbbd03 --- /dev/null +++ b/app/react/utils/dateAPI.ts @@ -0,0 +1,10 @@ +import api from 'app/utils/api'; +import { RequestParams } from './RequestParams'; + +async function dateToSeconds(dateString: string, locale?: string) { + const requestParams = new RequestParams({ dateString, locale }); + const res = await api.post('date-to-seconds', requestParams); + return res.json; +} + +export { dateToSeconds }; From 630fff9a4ef1881ea2f8d7e44a3dd9816c8a02b4 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 26 Jul 2022 13:57:14 +0300 Subject: [PATCH 06/13] Updated frontend to use date to seconds endpoint --- .../InformationExtraction.ts | 16 +++++++++--- .../actions/metadataExtractionActions.ts | 25 +++++++++++-------- .../Metadata/components/MetadataExtractor.tsx | 7 +++--- .../Metadata/components/MetadataFormFields.js | 3 ++- .../specs/MetadataExtractor.spec.tsx | 6 ++++- 5 files changed, 37 insertions(+), 20 deletions(-) diff --git a/app/api/services/informationextraction/InformationExtraction.ts b/app/api/services/informationextraction/InformationExtraction.ts index 2cc4bdc6dc..7712560003 100644 --- a/app/api/services/informationextraction/InformationExtraction.ts +++ b/app/api/services/informationextraction/InformationExtraction.ts @@ -22,7 +22,7 @@ import { EntitySchema } from 'shared/types/entityType'; import { ObjectIdSchema, PropertySchema } from 'shared/types/commonTypes'; import { IXSuggestionType } from 'shared/types/suggestionType'; import { FileType } from 'shared/types/fileType'; -import { dateToSeconds } from 'shared/dataUtils'; +import date from 'api/utils/date'; import { FileWithAggregation, getFilesForTraining, @@ -173,13 +173,17 @@ class InformationExtraction { return this._getEntityFromFile(file); }; - coerceSuggestionValue = (suggestion: RawSuggestion, property?: PropertySchema) => { + coerceSuggestionValue = ( + suggestion: RawSuggestion, + property?: PropertySchema, + language?: string + ) => { const suggestedValue = suggestion.text.trim(); switch (property?.type) { case 'numeric': return parseFloat(suggestedValue) || null; case 'date': - return dateToSeconds(suggestedValue); + return date.dateToSeconds(suggestedValue, language); default: return suggestedValue; } @@ -219,7 +223,11 @@ class InformationExtraction { ); const property = allProps.find(p => p.name === rawSuggestion.property_name); - const suggestedValue = this.coerceSuggestionValue(rawSuggestion, property); + const suggestedValue = this.coerceSuggestionValue( + rawSuggestion, + property, + currentSuggestion.language + ); if (suggestedValue === null) { status = 'failed'; diff --git a/app/react/Metadata/actions/metadataExtractionActions.ts b/app/react/Metadata/actions/metadataExtractionActions.ts index 74f88bde86..23b5a90cfa 100644 --- a/app/react/Metadata/actions/metadataExtractionActions.ts +++ b/app/react/Metadata/actions/metadataExtractionActions.ts @@ -1,6 +1,7 @@ import { actions as formActions } from 'react-redux-form'; -import { dateToSeconds } from 'shared/dataUtils'; +import { dateToSeconds } from 'app/utils/dateAPI'; import { actions } from 'app/BasicReducer'; +import { Dispatch } from 'redux'; const updateSelection = ( selection: { [key: string]: string }, @@ -18,17 +19,19 @@ const updateSelection = ( return actions.updateIn('documentViewer.metadataExtraction', ['selections'], data, 'propertyID'); }; -const updateFormField = (value: string, model: string, fieldType?: string) => { - if (fieldType === 'date') { - const dateForPicker = dateToSeconds(value); - return formActions.change(model, dateForPicker); - } +const updateFormField = + (value: string, model: string, fieldType?: string, locale?: string) => + async (dispatch: Dispatch<{}>) => { + if (fieldType === 'date') { + const { date } = await dateToSeconds(value, locale); + return dispatch(formActions.change(model, date)); + } - if (fieldType === 'numeric' && Number.isNaN(Number.parseInt(value, 10))) { - return formActions.change(model, '0'); - } + if (fieldType === 'numeric' && Number.isNaN(Number.parseInt(value, 10))) { + return dispatch(formActions.change(model, '0')); + } - return formActions.change(model, value); -}; + return dispatch(formActions.change(model, value)); + }; export { updateSelection, updateFormField }; diff --git a/app/react/Metadata/components/MetadataExtractor.tsx b/app/react/Metadata/components/MetadataExtractor.tsx index d42d215f0e..7e9dcbfc9c 100644 --- a/app/react/Metadata/components/MetadataExtractor.tsx +++ b/app/react/Metadata/components/MetadataExtractor.tsx @@ -11,6 +11,7 @@ export type OwnPropTypes = { model: string; fieldId?: string; fieldType?: string; + locale?: string; }; type selection = { @@ -25,10 +26,10 @@ const mapStateToProps = (state: IStore) => ({ }); const mapDispatchToProps = (dispatch: Dispatch<{}>, ownProps: OwnPropTypes) => { - const { fieldName, fieldId, model, fieldType = undefined } = ownProps; + const { fieldName, fieldId, model, fieldType, locale } = ownProps; return bindActionCreators( { - updateField: value => updateFormField(value, model, fieldType), + updateField: value => updateFormField(value, model, fieldType, locale), setSelection: selection => updateSelection(selection, fieldName, fieldId), }, dispatch @@ -40,7 +41,7 @@ const connector = connect(mapStateToProps, mapDispatchToProps); type mappedProps = ConnectedProps; const MetadataExtractorComponent = ({ selection, setSelection, updateField }: mappedProps) => { - const onClick = () => { + const onClick = async () => { setSelection(selection); updateField(selection.text); }; diff --git a/app/react/Metadata/components/MetadataFormFields.js b/app/react/Metadata/components/MetadataFormFields.js index b3d3b26a8f..94f41d52f8 100644 --- a/app/react/Metadata/components/MetadataFormFields.js +++ b/app/react/Metadata/components/MetadataFormFields.js @@ -287,7 +287,7 @@ class MetadataFormFields extends Component { } render() { - const { thesauris, template, model, showSubset, storeKey } = this.props; + const { thesauris, template, model, showSubset, storeKey, locale } = this.props; const mlThesauri = thesauris .filter(thes => !!thes.get('enable_classification')) @@ -333,6 +333,7 @@ class MetadataFormFields extends Component { fieldId={property._id} fieldType={property.type} model={`${model}.metadata.${property.name}`} + locale={locale} /> )} {this.getField(property, `.metadata.${property.name}`, thesauris, model)} diff --git a/app/react/Metadata/components/specs/MetadataExtractor.spec.tsx b/app/react/Metadata/components/specs/MetadataExtractor.spec.tsx index fd7a039b60..a3c50120b3 100644 --- a/app/react/Metadata/components/specs/MetadataExtractor.spec.tsx +++ b/app/react/Metadata/components/specs/MetadataExtractor.spec.tsx @@ -22,7 +22,11 @@ describe('MetadataExtractor', () => { const render = () => { component = shallow( - + ) .dive() From 583d5894f6dcde54e36547ea19f25119abd6d6ab Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 26 Jul 2022 16:26:31 +0300 Subject: [PATCH 07/13] Added an action to get seconds from date string --- .../actions/metadataExtractionActions.ts | 28 ++++++++++--------- .../Metadata/components/MetadataExtractor.tsx | 5 +++- .../specs/MetadataExtractor.spec.tsx | 7 +++-- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/app/react/Metadata/actions/metadataExtractionActions.ts b/app/react/Metadata/actions/metadataExtractionActions.ts index 23b5a90cfa..f730903526 100644 --- a/app/react/Metadata/actions/metadataExtractionActions.ts +++ b/app/react/Metadata/actions/metadataExtractionActions.ts @@ -1,7 +1,6 @@ import { actions as formActions } from 'react-redux-form'; import { dateToSeconds } from 'app/utils/dateAPI'; import { actions } from 'app/BasicReducer'; -import { Dispatch } from 'redux'; const updateSelection = ( selection: { [key: string]: string }, @@ -19,19 +18,22 @@ const updateSelection = ( return actions.updateIn('documentViewer.metadataExtraction', ['selections'], data, 'propertyID'); }; -const updateFormField = - (value: string, model: string, fieldType?: string, locale?: string) => - async (dispatch: Dispatch<{}>) => { - if (fieldType === 'date') { - const { date } = await dateToSeconds(value, locale); - return dispatch(formActions.change(model, date)); - } +const updateFormField = async ( + value: string, + model: string, + fieldType?: string, + locale?: string +) => { + if (fieldType === 'date') { + const { date } = await dateToSeconds(value, locale); + return formActions.change(model, date); + } - if (fieldType === 'numeric' && Number.isNaN(Number.parseInt(value, 10))) { - return dispatch(formActions.change(model, '0')); - } + if (fieldType === 'numeric' && Number.isNaN(Number.parseInt(value, 10))) { + return formActions.change(model, '0'); + } - return dispatch(formActions.change(model, value)); - }; + return formActions.change(model, value); +}; export { updateSelection, updateFormField }; diff --git a/app/react/Metadata/components/MetadataExtractor.tsx b/app/react/Metadata/components/MetadataExtractor.tsx index 7e9dcbfc9c..65b0a8775c 100644 --- a/app/react/Metadata/components/MetadataExtractor.tsx +++ b/app/react/Metadata/components/MetadataExtractor.tsx @@ -29,7 +29,10 @@ const mapDispatchToProps = (dispatch: Dispatch<{}>, ownProps: OwnPropTypes) => { const { fieldName, fieldId, model, fieldType, locale } = ownProps; return bindActionCreators( { - updateField: value => updateFormField(value, model, fieldType, locale), + updateField: value => async (dis: Dispatch<{}>) => { + const action = await updateFormField(value, model, fieldType, locale); + dis(action); + }, setSelection: selection => updateSelection(selection, fieldName, fieldId), }, dispatch diff --git a/app/react/Metadata/components/specs/MetadataExtractor.spec.tsx b/app/react/Metadata/components/specs/MetadataExtractor.spec.tsx index a3c50120b3..5ca36cf938 100644 --- a/app/react/Metadata/components/specs/MetadataExtractor.spec.tsx +++ b/app/react/Metadata/components/specs/MetadataExtractor.spec.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import thunk from 'redux-thunk'; import { shallow, ShallowWrapper } from 'enzyme'; import configureStore, { MockStore, MockStoreCreator } from 'redux-mock-store'; import { Provider } from 'react-redux'; @@ -9,7 +10,7 @@ import { MetadataExtractor } from '../MetadataExtractor'; describe('MetadataExtractor', () => { let component: ShallowWrapper; let store: MockStore; - const mockStoreCreator: MockStoreCreator = configureStore(); + const mockStoreCreator: MockStoreCreator = configureStore([thunk]); beforeEach(() => { store = mockStoreCreator({ @@ -46,10 +47,10 @@ describe('MetadataExtractor', () => { render(); expect(component.find('.extraction-button')).toHaveLength(0); }); - it('should call update store function and the react redux form change function on click', () => { + it('should call update store function and the react redux form change function on click', async () => { render(); component.find('.extraction-button').simulate('click'); - expect(store.getActions()).toEqual( + expect(await store.getActions()).toEqual( expect.arrayContaining([ expect.objectContaining({ type: 'documentViewer.metadataExtraction/UPDATE_IN', From 38bd36601eed9c98232fb9c7fcdf53980baa8bda Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 27 Jul 2022 11:38:57 +0300 Subject: [PATCH 08/13] Fixed tests --- .../specs/metadataExtractionActions.spec.ts | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/app/react/Metadata/actions/specs/metadataExtractionActions.spec.ts b/app/react/Metadata/actions/specs/metadataExtractionActions.spec.ts index 21330bd2f8..44e2c78779 100644 --- a/app/react/Metadata/actions/specs/metadataExtractionActions.spec.ts +++ b/app/react/Metadata/actions/specs/metadataExtractionActions.spec.ts @@ -1,5 +1,6 @@ import { actions } from 'app/BasicReducer'; import { actions as formActions } from 'react-redux-form'; +import * as api from 'app/utils/dateAPI'; import { updateSelection, updateFormField } from '../metadataExtractionActions'; describe('metadataExtractionActions', () => { @@ -8,33 +9,40 @@ describe('metadataExtractionActions', () => { spyOn(formActions, 'change'); }); - it('should update the form fields with the selected value', () => { - updateFormField('value to put in form', 'fieldModel'); + it('should update the form fields with the selected value', async () => { + await updateFormField('value to put in form', 'fieldModel'); expect(formActions.change).toHaveBeenCalledWith('fieldModel', 'value to put in form'); }); it.each(['01/30/1999', '30/01/1999', '01-30-1999', '01 30 1999', '30 01 1999'])( 'should format valid date inputs for Datepicker.js component', - dateStrings => { + async dateStrings => { const dateForDatepickerInUTC = 917654400; - updateFormField(dateStrings, 'fieldModel', 'date'); + spyOn(api, 'dateToSeconds').and.returnValue( + Promise.resolve({ date: dateForDatepickerInUTC }) + ); + await updateFormField(dateStrings, 'fieldModel', 'date'); expect(formActions.change).toHaveBeenCalledWith('fieldModel', dateForDatepickerInUTC); } ); - it('should parse dates that are only years, and set it to 01/01/YEAR', () => { + it('should parse dates that are only years, and set it to 01/01/YEAR', async () => { const dateForDatepickerInUTC = 1609459200; - updateFormField('2021', 'fieldModel', 'date'); + spyOn(api, 'dateToSeconds').and.returnValue( + Promise.resolve({ date: dateForDatepickerInUTC }) + ); + + await updateFormField('2021', 'fieldModel', 'date'); expect(formActions.change).toHaveBeenCalledWith('fieldModel', dateForDatepickerInUTC); }); describe('numeric fields', () => { - it('should check that selections for numeric fields are actual numbers', () => { - updateFormField('12345', 'fieldModel', 'numeric'); + it('should check that selections for numeric fields are actual numbers', async () => { + await updateFormField('12345', 'fieldModel', 'numeric'); expect(formActions.change).toHaveBeenCalledWith('fieldModel', '12345'); }); - it('should set the numeric field to 0 if the value is not a number', () => { - updateFormField('une two three', 'fieldModel', 'numeric'); + it('should set the numeric field to 0 if the value is not a number', async () => { + await updateFormField('une two three', 'fieldModel', 'numeric'); expect(formActions.change).toHaveBeenCalledWith('fieldModel', '0'); }); }); From 4abedbddc9e6dc962f216eff8ea4c8fc1f37aa6a Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 27 Jul 2022 11:41:17 +0300 Subject: [PATCH 09/13] Updated server dateToSeconds tests --- app/api/utils/specs/date.spec.js | 1 - 1 file changed, 1 deletion(-) diff --git a/app/api/utils/specs/date.spec.js b/app/api/utils/specs/date.spec.js index 0619f26094..9ea5c63b84 100644 --- a/app/api/utils/specs/date.spec.js +++ b/app/api/utils/specs/date.spec.js @@ -51,7 +51,6 @@ describe('date helper', () => { ${'Marzo 14, 2006'} | ${undefined} | ${1142294400} ${'28 августа 2017'} | ${'ru'} | ${1503878400} ${'августа 28, 2017'} | ${'ru'} | ${1503878400} - ${'shahr sibtambar 2017'} | ${'ar'} | ${1000} `( 'should convert $stringDate to $expectedMilliseconds', ({ stringDate, language, expectedMilliseconds }) => { From d97c0d6021805451aa5894584f9bb00d4625c55a Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 27 Jul 2022 12:00:35 +0300 Subject: [PATCH 10/13] Put entity suggestion if suggestion language is not found --- app/api/services/informationextraction/InformationExtraction.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/services/informationextraction/InformationExtraction.ts b/app/api/services/informationextraction/InformationExtraction.ts index 7712560003..f4bd04ff16 100644 --- a/app/api/services/informationextraction/InformationExtraction.ts +++ b/app/api/services/informationextraction/InformationExtraction.ts @@ -226,7 +226,7 @@ class InformationExtraction { const suggestedValue = this.coerceSuggestionValue( rawSuggestion, property, - currentSuggestion.language + currentSuggestion?.language || entity.language ); if (suggestedValue === null) { From efbe2170189f61b253d2995bae9d430d93b86617 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 27 Jul 2022 14:02:45 +0300 Subject: [PATCH 11/13] Moved dateToSeconds call to coerceValue call on front end --- app/react/Entities/EntitiesAPI.js | 5 +++++ .../actions/metadataExtractionActions.ts | 20 ++++++++++++++++--- .../specs/metadataExtractionActions.spec.ts | 10 +++++----- .../Metadata/components/MetadataExtractor.tsx | 3 ++- app/react/utils/dateAPI.ts | 10 ---------- 5 files changed, 29 insertions(+), 19 deletions(-) delete mode 100644 app/react/utils/dateAPI.ts diff --git a/app/react/Entities/EntitiesAPI.js b/app/react/Entities/EntitiesAPI.js index ba0a585f75..3f5096060c 100644 --- a/app/react/Entities/EntitiesAPI.js +++ b/app/react/Entities/EntitiesAPI.js @@ -2,6 +2,11 @@ import api from 'app/utils/api'; import { RequestParams } from 'app/utils/RequestParams'; export default { + coerceValue(requestParams) { + const url = 'entities/coerce_value'; + return api.post(url, requestParams).then(response => response.json); + }, + get(requestParams = new RequestParams(), language) { const params = requestParams.add({ include: diff --git a/app/react/Metadata/actions/metadataExtractionActions.ts b/app/react/Metadata/actions/metadataExtractionActions.ts index f730903526..c61e7ab81b 100644 --- a/app/react/Metadata/actions/metadataExtractionActions.ts +++ b/app/react/Metadata/actions/metadataExtractionActions.ts @@ -1,6 +1,9 @@ import { actions as formActions } from 'react-redux-form'; -import { dateToSeconds } from 'app/utils/dateAPI'; +import entitiesAPI from 'app/Entities/EntitiesAPI'; import { actions } from 'app/BasicReducer'; +import { RequestParams } from 'app/utils/RequestParams'; +import { notificationActions } from 'app/Notifications'; +import { t } from 'app/I18N'; const updateSelection = ( selection: { [key: string]: string }, @@ -25,8 +28,19 @@ const updateFormField = async ( locale?: string ) => { if (fieldType === 'date') { - const { date } = await dateToSeconds(value, locale); - return formActions.change(model, date); + const requestParams = new RequestParams({ + locale, + value, + type: 'date', + }); + const { value: coercedValue, success } = await entitiesAPI.coerceValue(requestParams); + if (!success) { + return notificationActions.notify( + t('System', 'Value cannot be transformed to date', null, false), + 'danger' + ); + } + return formActions.change(model, coercedValue); } if (fieldType === 'numeric' && Number.isNaN(Number.parseInt(value, 10))) { diff --git a/app/react/Metadata/actions/specs/metadataExtractionActions.spec.ts b/app/react/Metadata/actions/specs/metadataExtractionActions.spec.ts index 44e2c78779..76d97d79b9 100644 --- a/app/react/Metadata/actions/specs/metadataExtractionActions.spec.ts +++ b/app/react/Metadata/actions/specs/metadataExtractionActions.spec.ts @@ -1,6 +1,6 @@ import { actions } from 'app/BasicReducer'; import { actions as formActions } from 'react-redux-form'; -import * as api from 'app/utils/dateAPI'; +import api from 'app/Entities/EntitiesAPI'; import { updateSelection, updateFormField } from '../metadataExtractionActions'; describe('metadataExtractionActions', () => { @@ -18,8 +18,8 @@ describe('metadataExtractionActions', () => { 'should format valid date inputs for Datepicker.js component', async dateStrings => { const dateForDatepickerInUTC = 917654400; - spyOn(api, 'dateToSeconds').and.returnValue( - Promise.resolve({ date: dateForDatepickerInUTC }) + spyOn(api, 'coerceValue').and.returnValue( + Promise.resolve({ value: dateForDatepickerInUTC, success: true }) ); await updateFormField(dateStrings, 'fieldModel', 'date'); expect(formActions.change).toHaveBeenCalledWith('fieldModel', dateForDatepickerInUTC); @@ -28,8 +28,8 @@ describe('metadataExtractionActions', () => { it('should parse dates that are only years, and set it to 01/01/YEAR', async () => { const dateForDatepickerInUTC = 1609459200; - spyOn(api, 'dateToSeconds').and.returnValue( - Promise.resolve({ date: dateForDatepickerInUTC }) + spyOn(api, 'coerceValue').and.returnValue( + Promise.resolve({ value: dateForDatepickerInUTC, success: true }) ); await updateFormField('2021', 'fieldModel', 'date'); diff --git a/app/react/Metadata/components/MetadataExtractor.tsx b/app/react/Metadata/components/MetadataExtractor.tsx index 65b0a8775c..25b0f4f3b9 100644 --- a/app/react/Metadata/components/MetadataExtractor.tsx +++ b/app/react/Metadata/components/MetadataExtractor.tsx @@ -5,6 +5,7 @@ import { Icon } from 'app/UI'; import { IStore } from 'app/istore'; import { Translate } from 'app/I18N'; import { updateSelection, updateFormField } from '../actions/metadataExtractionActions'; +import { ModelAction } from 'react-redux-form'; export type OwnPropTypes = { fieldName: string; @@ -31,7 +32,7 @@ const mapDispatchToProps = (dispatch: Dispatch<{}>, ownProps: OwnPropTypes) => { { updateField: value => async (dis: Dispatch<{}>) => { const action = await updateFormField(value, model, fieldType, locale); - dis(action); + dis(action as ModelAction); }, setSelection: selection => updateSelection(selection, fieldName, fieldId), }, diff --git a/app/react/utils/dateAPI.ts b/app/react/utils/dateAPI.ts deleted file mode 100644 index 385abbbd03..0000000000 --- a/app/react/utils/dateAPI.ts +++ /dev/null @@ -1,10 +0,0 @@ -import api from 'app/utils/api'; -import { RequestParams } from './RequestParams'; - -async function dateToSeconds(dateString: string, locale?: string) { - const requestParams = new RequestParams({ dateString, locale }); - const res = await api.post('date-to-seconds', requestParams); - return res.json; -} - -export { dateToSeconds }; From 14ade176d7bbcd9971ed04c263e2f3f280761ca4 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 27 Jul 2022 14:03:23 +0300 Subject: [PATCH 12/13] Added coerce-values endoint instead of date-to-seconds in the server --- app/api/api.js | 1 - app/api/dates/routes.ts | 26 ------------------------ app/api/entities/routes.js | 41 ++++++++++++++++++++++++++++++++++++++ app/api/utils/date.js | 1 - 4 files changed, 41 insertions(+), 28 deletions(-) delete mode 100644 app/api/dates/routes.ts diff --git a/app/api/api.js b/app/api/api.js index 8560411a86..b0f5ce8907 100644 --- a/app/api/api.js +++ b/app/api/api.js @@ -40,5 +40,4 @@ export default (app, server) => { require('./permissions/routes').permissionRoutes(app); require('./suggestions/routes').suggestionsRoutes(app); require('./preserve/routes').PreserveRoutes(app); - require('./dates/routes').datesConversionRoutes(app); }; diff --git a/app/api/dates/routes.ts b/app/api/dates/routes.ts deleted file mode 100644 index b370bd0d84..0000000000 --- a/app/api/dates/routes.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Application, Request, Response, NextFunction } from 'express'; -import { validation } from 'api/utils'; -import date from '../utils/date'; - -export const datesConversionRoutes = (app: Application) => { - app.post( - '/api/date-to-seconds', - validation.validateRequest({ - type: 'object', - properties: { - body: { - type: 'object', - properties: { - dateString: { type: 'string' }, - locale: { type: 'string' }, - }, - }, - }, - }), - - async (req: Request, res: Response, _next: NextFunction) => { - const dateSeconds = date.dateToSeconds(req.body.dateString, req.body.locale); - res.status(200).json({ success: true, date: dateSeconds }); - } - ); -}; diff --git a/app/api/entities/routes.js b/app/api/entities/routes.js index 5c0b24d5a2..10426a6251 100644 --- a/app/api/entities/routes.js +++ b/app/api/entities/routes.js @@ -6,6 +6,7 @@ import { saveEntity } from 'api/entities/entitySavingManager'; import entities from './entities'; import templates from '../templates/templates'; import thesauri from '../thesauri/thesauri'; +import date from '../utils/date'; import needsAuthorization from '../auth/authMiddleware'; import { parseQuery, validation } from '../utils'; @@ -22,7 +23,47 @@ async function updateThesauriWithEntity(entity, req) { req.sockets.emitToCurrentTenant('thesauriChange', templateTransformed); } +function coerceValues(value, type, locale) { + let dateSeconds = ''; + switch (type) { + case 'date': + dateSeconds = date.dateToSeconds(value, locale); + if (Number.isNaN(dateSeconds)) { + return { success: false }; + } + return { success: true, value: dateSeconds }; + default: + throw Error('Unsupported type'); + } +} + export default app => { + app.post( + '/api/entities/coerce_value', + needsAuthorization(['admin']), + validation.validateRequest({ + type: 'object', + properties: { + body: { + type: 'object', + properties: { + value: { type: 'string' }, + type: { type: 'string' }, + locale: { type: 'string' }, + }, + }, + }, + }), + async (req, res, next) => { + const { value, type, locale } = req.body; + try { + const coerced = coerceValues(value, type, locale); + return res.json(coerced); + } catch (e) { + return next(e); + } + } + ); app.post( '/api/entities', needsAuthorization(['admin', 'editor', 'collaborator']), diff --git a/app/api/utils/date.js b/app/api/utils/date.js index f1e09752c4..0a0fd66618 100644 --- a/app/api/utils/date.js +++ b/app/api/utils/date.js @@ -27,7 +27,6 @@ export default { }, dateToSeconds(value, locale) { - // console.log('Date value: ', value, ' locale: ', locale); const parsedValue = value.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); let getDate = parser.fromString(parsedValue, locale); if (getDate.invalid) { From da59a2b9678d4c531d7cbe42bcbd8a1691f7aa9b Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 27 Jul 2022 14:26:41 +0300 Subject: [PATCH 13/13] Added a migration --- .../90-add_system_key_translations/index.js | 57 ++++++++++++ .../90-add_system_key_translations.spec.js | 64 +++++++++++++ .../specs/fixtures.js | 90 +++++++++++++++++++ 3 files changed, 211 insertions(+) create mode 100644 app/api/migrations/migrations/90-add_system_key_translations/index.js create mode 100644 app/api/migrations/migrations/90-add_system_key_translations/specs/90-add_system_key_translations.spec.js create mode 100644 app/api/migrations/migrations/90-add_system_key_translations/specs/fixtures.js diff --git a/app/api/migrations/migrations/90-add_system_key_translations/index.js b/app/api/migrations/migrations/90-add_system_key_translations/index.js new file mode 100644 index 0000000000..8000102183 --- /dev/null +++ b/app/api/migrations/migrations/90-add_system_key_translations/index.js @@ -0,0 +1,57 @@ +/* +This migration is meant to be repeatable. +After copy pasting: + - change the contents of system_keys.csv to the new keyset + - change the file location in the readCsvToSystemKeys call + - change the tests, if necessary +*/ + +async function insertSystemKeys(db, newKeys) { + const translations = await db.collection('translations').find().toArray(); + const locales = translations.map(tr => tr.locale); + + const locToSystemContext = {}; + translations.forEach(tr => { + locToSystemContext[tr.locale] = tr.contexts.find(c => c.id === 'System'); + }); + const locToKeys = {}; + Object.entries(locToSystemContext).forEach(([loc, context]) => { + locToKeys[loc] = new Set(context.values.map(v => v.key)); + }); + + newKeys.forEach(row => { + const { key, value: optionalValue } = row; + + locales.forEach(loc => { + if (!locToKeys[loc].has(key)) { + const newValue = optionalValue || key; + locToSystemContext[loc].values.push({ key, value: newValue }); + locToKeys[loc].add(key); + } + }); + }); + + await Promise.all( + translations.map(tr => db.collection('translations').replaceOne({ _id: tr._id }, tr)) + ); +} + +export default { + delta: 90, + + reindex: false, + + name: 'add_system_key_translations', + + description: 'Adding missing translations for system keys.', + + async up(db) { + process.stdout.write(`${this.name}...\r\n`); + const systemKeys = [ + { + key: 'Value cannot be transformed to date', + }, + ]; + await insertSystemKeys(db, systemKeys); + }, +}; diff --git a/app/api/migrations/migrations/90-add_system_key_translations/specs/90-add_system_key_translations.spec.js b/app/api/migrations/migrations/90-add_system_key_translations/specs/90-add_system_key_translations.spec.js new file mode 100644 index 0000000000..77b3a34049 --- /dev/null +++ b/app/api/migrations/migrations/90-add_system_key_translations/specs/90-add_system_key_translations.spec.js @@ -0,0 +1,64 @@ +import { testingDB } from 'api/utils/testing_db'; +import migration from '../index.js'; +import { fixtures, templateId, defaultTemplateName, defaultTemplateTitle } from './fixtures.js'; + +const locales = ['en', 'es', 'hu']; +const newKeyValues = [ + { + key: 'Value cannot be transformed to date', + value: 'Value cannot be transformed to date', + }, +]; +const alreadyInAllContexts = { + key: 'Duplicated label', + en: 'Duplicated label', + es: 'Nombre duplicado', + hu: 'Ismétlődő címke', +}; + +describe('migration add_system_key_translations', () => { + beforeEach(async () => { + spyOn(process.stdout, 'write'); + await testingDB.setupFixturesAndContext(fixtures); + }); + + afterAll(async () => { + await testingDB.disconnect(); + }); + + it('should have a delta number', () => { + expect(migration.delta).toBe(90); + }); + + it('should append new keys, leave existing keys intact.', async () => { + await migration.up(testingDB.mongodb); + + const allTranslations = await testingDB.mongodb.collection('translations').find().toArray(); + function testKeyValue(key, value, locale, contextId) { + expect( + allTranslations + .find(tr => tr.locale === locale) + .contexts.find(c => c.id === contextId) + .values.find(v => v.key === key).value + ).toBe(value); + } + + newKeyValues.forEach(({ key, value }) => { + locales.forEach(loc => { + testKeyValue(key, value, loc, 'System'); + }); + }); + locales.forEach(loc => { + testKeyValue(alreadyInAllContexts.key, alreadyInAllContexts[loc], loc, 'System'); + }); + locales.forEach(loc => { + expect( + allTranslations + .find(tr => tr.locale === loc) + .contexts.find(c => c.id === templateId.toString()).values + ).toHaveLength(2); + testKeyValue(defaultTemplateName, defaultTemplateName, loc, templateId.toString()); + testKeyValue(defaultTemplateTitle, defaultTemplateTitle, loc, templateId.toString()); + }); + }); +}); diff --git a/app/api/migrations/migrations/90-add_system_key_translations/specs/fixtures.js b/app/api/migrations/migrations/90-add_system_key_translations/specs/fixtures.js new file mode 100644 index 0000000000..fd0204ac02 --- /dev/null +++ b/app/api/migrations/migrations/90-add_system_key_translations/specs/fixtures.js @@ -0,0 +1,90 @@ +import db from 'api/utils/testing_db'; + +const templateId = db.id(); +const defaultTemplateName = 'default template'; +const defaultTemplateTitle = 'Title'; + +//contexts +const commonContext = { + id: 'System', + label: 'User Interface', + type: 'Uwazi UI', + values: [ + { + key: 'existing-key-in-system', + value: 'existing-key-in-system', + }, + ], +}; +const templateContext = { + id: templateId.toString(), + label: defaultTemplateName, + type: 'Entity', + values: [ + { + key: defaultTemplateName, + value: defaultTemplateName, + }, + { + key: defaultTemplateTitle, + value: defaultTemplateTitle, + }, + ], +}; + +const fixtures = { + templates: [ + //default template name - correct + { + _id: templateId, + name: defaultTemplateName, + commonProperties: [{ name: 'title', label: defaultTemplateTitle, type: 'text' }], + properties: [], + }, + ], + translations: [ + { + _id: db.id(), + locale: 'es', + contexts: [ + { + ...commonContext, + values: commonContext.values.concat([ + { key: 'Drag properties here', value: 'Arrastra propiedades aquí' }, + { key: 'Duplicated label', value: 'Nombre duplicado' }, + ]), + }, + templateContext, + ], + }, + { + _id: db.id(), + locale: 'en', + contexts: [ + { + ...commonContext, + values: commonContext.values.concat([ + { key: 'Priority sorting', value: 'Priority sort' }, + { key: 'Duplicated label', value: 'Duplicated label' }, + ]), + }, + templateContext, + ], + }, + { + _id: db.id(), + locale: 'hu', + contexts: [ + { + ...commonContext, + values: commonContext.values.concat([ + { key: 'Duplicated label', value: 'Ismétlődő címke' }, + ]), + }, + templateContext, + ], + }, + ], +}; + +export { fixtures, templateId, defaultTemplateName, defaultTemplateTitle };